chore(e2e): remove Cypress infrastructure (#44829)

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Joe Li
2026-10-01 11:32:51 -07:00
committed by GitHub
parent 74d885233d
commit 8ab7a85a3d
52 changed files with 91 additions and 16877 deletions

View File

@@ -20,8 +20,8 @@ Apache Superset is a data visualization platform with Flask/Python backend and R
### Testing Strategy Migration
- **Prefer unit tests** over integration tests
- **Prefer integration tests** over Cypress end-to-end tests
- **Cypress is last resort** - Actively moving away from Cypress
- **Prefer integration tests** over end-to-end tests
- **Use Playwright for E2E tests** - Cypress has been removed
- **Use Jest + React Testing Library** for component testing
### Backend Type Safety

View File

@@ -39,7 +39,6 @@
tests/
docs/
install/
superset-frontend/cypress-base/
superset-frontend/coverage/
superset-frontend/.temp_cache/
superset/static/assets/

View File

@@ -32,7 +32,6 @@ _p=80; while ! _is_free $_p; do ((_p++)); done; export NGINX_PORT=$_p
_p=8088; while ! _is_free $_p; do ((_p++)); done; export SUPERSET_PORT=$_p
_p=9000; while ! _is_free $_p; do ((_p++)); done; export NODE_PORT=$_p
_p=8080; while ! _is_free $_p || [ $_p -eq $NGINX_PORT ]; do ((_p++)); done; export WEBSOCKET_PORT=$_p
_p=8081; while ! _is_free $_p || [ $_p -eq $WEBSOCKET_PORT ]; do ((_p++)); done; export CYPRESS_PORT=$_p
_p=5432; while ! _is_free $_p; do ((_p++)); done; export DATABASE_PORT=$_p
_p=6379; while ! _is_free $_p; do ((_p++)); done; export REDIS_PORT=$_p

2
.github/labeler.yml vendored
View File

@@ -54,8 +54,6 @@
- 'superset-embedded-sdk/package-lock.json'
- 'superset-websocket/package.json'
- 'superset-websocket/package-lock.json'
- 'superset-frontend/cypress-base/package.json'
- 'superset-frontend/cypress-base/package-lock.json'
- 'superset-frontend/packages/**/package.json'
- 'superset-frontend/plugins/**/package.json'

View File

@@ -161,108 +161,6 @@ celery-worker() {
say "::endgroup::"
}
cypress-install() {
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
cache-restore cypress
say "::group::Install Cypress"
npm ci
say "::endgroup::"
cache-save cypress
}
cypress-run-all() {
local USE_DASHBOARD=$1
local APP_ROOT=$2
cd "$GITHUB_WORKSPACE/superset-frontend/cypress-base"
# Start the Superset backend via gunicorn (not `flask run`). The Flask
# development server is single-threaded and has no crash-recovery, so
# heavy tests (dashboard import/export, SQL Lab) can knock it offline
# for the rest of the run — surfacing as `ECONNREFUSED` / `socket hang up`
# / `Missing CSRF token` cascades. Gunicorn gives us multiple workers,
# a request timeout, and worker-recycling under load.
local serverlog="${HOME}/superset-cypress.log"
local port=8081
CYPRESS_BASE_URL="http://localhost:${port}"
if [ -n "$APP_ROOT" ]; then
export SUPERSET_APP_ROOT=$APP_ROOT
CYPRESS_BASE_URL=${CYPRESS_BASE_URL}${APP_ROOT}
fi
export CYPRESS_BASE_URL
# Mirrors the args in docker/entrypoints/run-server.sh (1 worker × 20
# gthread threads) to keep parity with production. Multi-worker
# configurations expose timing-sensitive races in the SQL Lab → Explore
# navigation flow under E2E. We diverge from the entrypoint on:
# --timeout 120: heavy dashboard import/export specs exceed the 60s
# default
# superset.app:create_app(): explicit factory so we don't depend on
# FLASK_APP being exported
#
# No --max-requests, matching the entrypoint's default of 0 (recycling
# off). With a single worker a recycle takes the whole backend offline for
# the graceful-timeout drain — browser keep-alive connections hold it open
# for the full 30s — plus ~5s of app boot. A run issues ~3800 requests in
# ~8 minutes, so recycling every 500 produced seven ~35s outages per run
# and flaked whichever specs happened to navigate into one. Lowering
# --graceful-timeout is not enough: a dashboard load plus chart render
# needs 6-10s, which still lands inside the window.
nohup gunicorn \
--bind "127.0.0.1:$port" \
--workers 1 \
--worker-class gthread \
--threads 20 \
--timeout 120 \
--access-logfile - \
--error-logfile - \
"superset.app:create_app()" \
>"$serverlog" 2>&1 </dev/null &
local serverPid=$!
# Ensure the backend is cleaned up and its log is emitted even when the
# test runner fails under `set -e`.
trap '
echo "::group::gunicorn log for Cypress run"
cat "'"$serverlog"'" || true
echo "::endgroup::"
kill '"$serverPid"' 2>/dev/null || true
' EXIT
# Wait for the backend to be ready before launching Cypress; otherwise
# the first spec can race the server bind and see connection errors.
local timeout=60
say "Waiting for gunicorn server to start on port $port..."
while [ $timeout -gt 0 ]; do
if curl -f "http://localhost:${port}${APP_ROOT}/health" >/dev/null 2>&1; then
say "gunicorn server is ready"
break
fi
sleep 1
timeout=$((timeout - 1))
done
if [ $timeout -eq 0 ]; then
echo "::error::gunicorn server failed to start within 60 seconds"
echo "::group::Server startup log"
cat "$serverlog"
echo "::endgroup::"
return 1
fi
USE_DASHBOARD_FLAG=''
if [ "$USE_DASHBOARD" = "true" ]; then
USE_DASHBOARD_FLAG='--use-dashboard'
fi
# UNCOMMENT the next few commands to monitor memory usage
# monitor_memory & # Start memory monitoring in the background
# memoryMonitorPid=$!
python ../../scripts/cypress_run.py --retries 5 $USE_DASHBOARD_FLAG
# kill $memoryMonitorPid
}
playwright-install() {
cd "$GITHUB_WORKSPACE/superset-frontend"
@@ -278,9 +176,12 @@ playwright-run() {
local APP_ROOT=$1
local TEST_PATH=$2
# Start the Superset backend via gunicorn from the project root.
# See cypress-run-all() above for the rationale — the Flask dev server
# cannot survive the dashboard import/export tests under load.
# Start the Superset backend via gunicorn (not `flask run`). The Flask
# development server is single-threaded and has no crash-recovery, so
# heavy tests (dashboard import/export, SQL Lab) can knock it offline
# for the rest of the run — surfacing as `ECONNREFUSED` / `socket hang up`
# / `Missing CSRF token` cascades. Gunicorn gives us a request timeout
# and a multi-threaded worker.
cd "$GITHUB_WORKSPACE"
local serverlog="${HOME}/superset-playwright.log"
local port=8081
@@ -296,9 +197,22 @@ playwright-run() {
fi
export PLAYWRIGHT_BASE_URL
# See cypress-run-all() above for the args rationale (1 worker × 20
# gthread threads matching docker/entrypoints/run-server.sh, a 120s
# timeout for heavy E2E load, and why worker recycling is off).
# Mirrors the args in docker/entrypoints/run-server.sh (1 worker × 20
# gthread threads) to keep parity with production. Multi-worker
# configurations expose timing-sensitive races in the SQL Lab → Explore
# navigation flow under E2E. We diverge from the entrypoint on:
# --timeout 120: heavy dashboard import/export specs exceed the 60s
# default
# superset.app:create_app(): explicit factory so we don't depend on
# FLASK_APP being exported
#
# No --max-requests, matching the entrypoint's default of 0 (recycling
# off). With a single worker a recycle takes the whole backend offline for
# the graceful-timeout drain — browser keep-alive connections hold it open
# for the full 30s — plus ~5s of app boot, which flakes whichever specs
# happen to navigate into the outage. Lowering --graceful-timeout is not
# enough: a dashboard load plus chart render needs 6-10s, which still
# lands inside the window.
nohup gunicorn \
--bind "127.0.0.1:$port" \
--workers 1 \

View File

@@ -49,10 +49,4 @@ module.exports = {
// one is built with `npm run build`,
// another is built with `npm run build-instrumented`
'instrumented-assets': assetsConfig,
cypress: {
path: [`${homeDirectory}/.cache/Cypress`],
hashFiles: [
`${workspaceDirectory}/superset-frontend/cypress-base/package-lock.json`,
],
},
};

View File

@@ -9,10 +9,6 @@ on:
types: [synchronize, opened, reopened, ready_for_review]
workflow_dispatch:
inputs:
use_dashboard:
description: "Use Cypress Dashboard (true/false) [paid service - trigger manually when needed]. You MUST provide a branch and/or PR number below for this to work."
required: false
default: "false"
ref:
description: "The branch or tag to checkout"
required: false
@@ -50,156 +46,6 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
cypress-matrix:
needs: changes
if: (needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true') && github.event.pull_request.draft == false
runs-on: ubuntu-26.04
timeout-minutes: 30
permissions:
contents: read
pull-requests: read
strategy:
# when one test fails, DO NOT cancel the other
# app_root variant, because this will kill Cypress processes
# leaving the Dashboard hanging ...
# https://github.com/cypress-io/github-action/issues/48
fail-fast: false
matrix:
browser: ["chrome"]
app_root: ${{ github.event_name == 'push' && fromJSON('["", "/app/prefix"]') || fromJSON('[""]') }}
env:
SUPERSET_ENV: development
SUPERSET_CONFIG: tests.integration_tests.superset_test_config
SUPERSET__SQLALCHEMY_DATABASE_URI: postgresql+psycopg2://superset:superset@127.0.0.1:15432/superset
PYTHONPATH: ${{ github.workspace }}
REDIS_PORT: 16379
GITHUB_TOKEN: ${{ github.token }}
# Only use dashboard when explicitly requested via workflow_dispatch
USE_DASHBOARD: ${{ github.event.inputs.use_dashboard == 'true' || 'false' }}
services:
postgres:
image: ghcr.io/apache/superset/ci/postgres:17-alpine
env:
POSTGRES_USER: superset
POSTGRES_PASSWORD: superset
ports:
- 15432:5432
redis:
image: ghcr.io/apache/superset/ci/redis:7-alpine
ports:
- 16379:6379
steps:
# -------------------------------------------------------
# Conditional checkout based on context
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
submodules: recursive
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- name: Checkout using ref (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.ref != ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: ${{ github.event.inputs.ref }}
submodules: recursive
- name: Checkout using PR ID (workflow_dispatch)
if: github.event_name == 'workflow_dispatch' && github.event.inputs.pr_id != ''
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
ref: refs/pull/${{ github.event.inputs.pr_id }}/merge
submodules: recursive
# -------------------------------------------------------
- name: Setup Python
uses: $/.github/actions/setup-backend/
- name: Setup postgres
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: setup-postgres
- name: Import test data
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: testdata
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: "./superset-frontend/.nvmrc"
cache: "npm"
cache-dependency-path: "superset-frontend/package-lock.json"
- name: Install npm dependencies
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: npm-install
- name: Build javascript packages
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: build-instrumented-assets
- name: Install cypress
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
with:
run: cypress-install
- name: Run Cypress
# cached-dependencies is a submodule (not a plain directory), and
# the $/ self-repository syntax resolves action files directly from
# the repository without performing a real (submodule-aware)
# checkout, so it can't see into a submodule's link. Keep this one
# on the workspace-relative ./ form, consistent with every other
# workflow in the repo that references this action.
uses: ./.github/actions/cached-dependencies # zizmor: ignore[self-repository] - $/ cannot resolve an action that lives in a submodule; ./ is required here
env:
CYPRESS_BROWSER: ${{ matrix.browser }}
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
NODE_OPTIONS: "--max-old-space-size=4096"
with:
run: cypress-run-all ${{ env.USE_DASHBOARD }} ${{ matrix.app_root }}
- name: Set safe app root
if: failure()
id: set-safe-app-root
env:
APP_ROOT: ${{ matrix.app_root }}
run: |
SAFE_APP_ROOT=${APP_ROOT//\//_}
echo "safe_app_root=$SAFE_APP_ROOT" >> $GITHUB_OUTPUT
- name: Upload Artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
if: failure()
with:
path: ${{ github.workspace }}/superset-frontend/cypress-base/cypress/screenshots
name: cypress-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
playwright-tests:
needs: changes
if: needs.changes.outputs.python == 'true' || needs.changes.outputs.frontend == 'true'
@@ -215,8 +61,7 @@ jobs:
matrix:
browser: ["chromium"]
# Subdirectory deployment (APPLICATION_ROOT) is a required-to-pass
# dimension, not an optional one, so it runs on every event —
# unlike cypress-matrix above, which only widens on push.
# dimension, so it runs on every event.
app_root: ["", "/app/prefix"]
env:
SUPERSET_ENV: development
@@ -239,7 +84,7 @@ jobs:
- 16379:6379
steps:
# -------------------------------------------------------
# Conditional checkout based on context (same as Cypress workflow)
# Conditional checkout based on context
- name: Checkout for push or pull_request event
if: github.event_name == 'push' || github.event_name == 'pull_request'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -414,44 +259,22 @@ jobs:
${{ github.workspace }}/superset-frontend/test-results/
name: playwright-artifact-${{ github.run_id }}-${{ github.job }}-${{ matrix.browser }}--${{ steps.set-safe-app-root.outputs.safe_app_root }}
# Stable required-status-check anchors. cypress-matrix and playwright-tests
# are matrix jobs gated on change detection (python || frontend). On a PR
# that touches neither — e.g. a docs-only PR — they are skipped at the job
# level, which happens before matrix expansion, so the per-combination
# contexts (`cypress-matrix (0, chrome)`, `playwright-tests (chromium)`) are
# never produced and branch protection waits on them forever. These
# always-running jobs report a single stable context that passes when the
# underlying matrix job succeeded or was skipped, and fails only on a real
# failure. Require these in .asf.yaml instead of the matrix-expanded names.
# Stable required-status-check anchor. playwright-tests is a matrix job
# gated on change detection (python || frontend). On a PR that touches
# neither — e.g. a docs-only PR — it is skipped at the job level, which
# happens before matrix expansion, so the per-combination context
# (`playwright-tests (chromium)`) is never produced and branch protection
# waits on it forever. This always-running job reports a single stable
# context that passes when the underlying matrix job succeeded or was
# skipped, and fails only on a real failure. Require it in .asf.yaml instead
# of the matrix-expanded name.
#
# A matrix job reads as "skipped" in two distinct cases, and only the first
# is a legitimate pass: (a) change detection succeeded and gated the job off
# (docs-only PR); (b) the `changes` job itself failed or was cancelled, in
# which case GHA skips its dependents too. Accepting (b) would let a broken
# change-detector report a false green, so each anchor first requires
# change-detector report a false green, so the anchor first requires
# `changes` to have succeeded before honouring a skip.
cypress-matrix-required:
needs: [changes, cypress-matrix]
if: always()
runs-on: ubuntu-26.04
timeout-minutes: 5
permissions: {}
steps:
- name: Check cypress-matrix result
env:
CHANGES: ${{ needs.changes.result }}
RESULT: ${{ needs.cypress-matrix.result }}
run: |
if [ "$CHANGES" != "success" ]; then
echo "change detection did not succeed (result: $CHANGES); refusing to pass on a skipped matrix"
exit 1
fi
if [ "$RESULT" != "success" ] && [ "$RESULT" != "skipped" ]; then
echo "cypress-matrix did not pass (result: $RESULT)"
exit 1
fi
echo "cypress-matrix result: $RESULT (changes: $CHANGES)"
playwright-tests-required:
needs: [changes, playwright-tests]
if: always()
@@ -475,7 +298,7 @@ jobs:
echo "playwright-tests result: $RESULT (changes: $CHANGES)"
actions-timeline:
needs: [cypress-matrix, playwright-tests, cypress-matrix-required, playwright-tests-required]
needs: [playwright-tests, playwright-tests-required]
if: always()
runs-on: ubuntu-slim
permissions:

View File

@@ -105,10 +105,9 @@ repos:
files: ^docs/.*\.(js|jsx|ts|tsx)$
- id: type-checking-frontend
name: Type-Checking (Frontend)
entry: ./scripts/check-type.js package=superset-frontend excludeDeclarationDir=cypress-base
entry: ./scripts/check-type.js package=superset-frontend
language: system
files: ^superset-frontend\/.*\.(js|jsx|ts|tsx)$
exclude: ^superset-frontend/cypress-base\/
require_serial: true
# blacklist unsafe functions like make_url (see #19526)
- repo: https://github.com/skorokithakis/blacklist-pre-commit-hook

View File

@@ -44,8 +44,7 @@ Common pre-commit failures:
### Testing Strategy Migration
- **Prefer unit tests** over integration tests
- **Prefer integration tests** over end-to-end tests
- **Use Playwright for E2E tests** - Migrating from Cypress
- **Cypress is deprecated** - Will be removed once migration is completed
- **Use Playwright for E2E tests** - Cypress has been removed
- **Use Jest + React Testing Library** for component testing
- **Use `test()` instead of `describe()`** - Follow [avoid nesting when testing](https://kentcdodds.com/blog/avoid-nesting-when-youre-testing) principles
@@ -202,18 +201,13 @@ The Developer Portal auto-generates MDX documentation from Storybook stories. **
npm run test # All tests
npm run test -- filename.test.tsx # Single file
# E2E Tests (Playwright - NEW)
# E2E Tests (Playwright)
npm run playwright:test # All Playwright tests
npm run playwright:ui # Interactive UI mode
npm run playwright:headed # See browser during tests
npx playwright test tests/auth/login.spec.ts # Single file
npm run playwright:debug tests/auth/login.spec.ts # Debug specific file
# E2E Tests (Cypress - DEPRECATED)
cd superset-frontend/cypress-base
npm run cypress-run-chrome # All Cypress tests (headless)
npm run cypress-debug # Interactive Cypress UI
# Backend
pytest # All tests
pytest tests/unit_tests/specific_test.py # Single file

View File

@@ -103,14 +103,6 @@ flask-app:
node-app:
cd superset-frontend; npm run dev-server
build-cypress:
cd superset-frontend; npm run build-instrumented
cd superset-frontend/cypress-base; npm ci
open-cypress:
if ! [ $(port) ]; then cd superset-frontend/cypress-base; CYPRESS_BASE_URL=http://localhost:9000 npm run cypress open; fi
cd superset-frontend/cypress-base; CYPRESS_BASE_URL=http://localhost:$(port) npm run cypress open
report-celery-worker:
celery --app=superset.tasks.celery_app:app worker

View File

@@ -125,8 +125,6 @@ services:
restart: unless-stopped
ports:
- ${SUPERSET_PORT:-8088}:8088
# When in cypress-mode ->
- ${CYPRESS_PORT:-8081}:8081
extra_hosts:
- "host.docker.internal:host-gateway"
user: *superset-user

View File

@@ -26,7 +26,6 @@ DEV_MODE=true
# SUPERSET_PORT=8088
# NODE_PORT=9000
# WEBSOCKET_PORT=8080
# CYPRESS_PORT=8081
# DATABASE_PORT=5432
# REDIS_PORT=6379
@@ -79,7 +78,6 @@ SUPERSET_LOAD_EXAMPLES=yes
# examples anyway, e.g. after changing the example datasets or after a partial
# load.
#SUPERSET_FORCE_LOAD_EXAMPLES=no
CYPRESS_CONFIG=false
SUPERSET_PORT=8088
MAPBOX_API_KEY=''

View File

@@ -31,7 +31,6 @@ NGINX_PORT=81
SUPERSET_PORT=8089
NODE_PORT=9001
WEBSOCKET_PORT=8082
CYPRESS_PORT=8083
DATABASE_PORT=5433
REDIS_PORT=6380

View File

@@ -34,13 +34,6 @@ if [ "$DEV_MODE" == "true" ]; then
fi
REQUIREMENTS_LOCAL="/app/docker/requirements-local.txt"
PORT=${PORT:-8088}
# If Cypress run – overwrite the password for admin and export env variables
if [ "$CYPRESS_CONFIG" == "true" ]; then
export SUPERSET_TESTENV=true
export POSTGRES_DB=superset_cypress
export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset_cypress
PORT=8081
fi
# Skip postgres requirements installation for workers to avoid conflicts
if [[ "$DATABASE_DIALECT" == postgres* ]] && [ "$(whoami)" = "root" ] && [ "$1" != "worker" ] && [ "$1" != "beat" ]; then
# older images may not have the postgres dev requirements installed

View File

@@ -1,28 +0,0 @@
#!/usr/bin/env bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ------------------------------------------------------------------------
# Creates the examples database and respective user. This database location
# and access credentials are defined on the environment variables
# ------------------------------------------------------------------------
set -e
psql -v ON_ERROR_STOP=1 --username "${POSTGRES_USER}" <<-EOSQL
CREATE DATABASE superset_cypress;
EOSQL

View File

@@ -36,13 +36,6 @@ Init Step ${1}/${STEP_CNT} [${2}] -- ${3}
EOF
}
ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin}"
# If Cypress run – overwrite the password for admin and export env variables
if [ "$CYPRESS_CONFIG" == "true" ]; then
ADMIN_PASSWORD="general"
export SUPERSET_TESTENV=true
export POSTGRES_DB=superset_cypress
export SUPERSET__SQLALCHEMY_DATABASE_URI=postgresql+psycopg2://superset:superset@db:5432/superset_cypress
fi
# Initialize the database
echo_step "1" "Starting" "Applying DB migrations"
superset db upgrade
@@ -50,16 +43,12 @@ echo_step "1" "Complete" "Applying DB migrations"
# Create an admin user
echo_step "2" "Starting" "Setting up admin user ( admin / $ADMIN_PASSWORD )"
if [ "$CYPRESS_CONFIG" == "true" ]; then
superset load_test_users
else
superset fab create-admin \
--username admin \
--email admin@superset.com \
--password "$ADMIN_PASSWORD" \
--firstname Superset \
--lastname Admin
fi
superset fab create-admin \
--username admin \
--email admin@superset.com \
--password "$ADMIN_PASSWORD" \
--firstname Superset \
--lastname Admin
echo_step "2" "Complete" "Setting up admin user"
# Create default roles and permissions
echo_step "3" "Starting" "Setting up roles and perms"
@@ -97,12 +86,8 @@ PY
if [ "$SUPERSET_LOAD_EXAMPLES" = "yes" ]; then
echo_step "4" "Starting" "Loading examples"
# Cypress runs always load, since they need a distinct set of test data
# (`--load-test-data`) in a separate database. Set
# SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload the examples regardless.
if [ "$CYPRESS_CONFIG" == "true" ]; then
superset load_examples --load-test-data
elif [ "$SUPERSET_FORCE_LOAD_EXAMPLES" != "yes" ] && examples_already_loaded; then
# Set SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload the examples regardless.
if [ "$SUPERSET_FORCE_LOAD_EXAMPLES" != "yes" ] && examples_already_loaded; then
echo "Examples already loaded, skipping (set SUPERSET_FORCE_LOAD_EXAMPLES=yes to reload them)"
else
superset load_examples

View File

@@ -22,7 +22,6 @@
#
import logging
import os
import sys
from celery.schedules import crontab
from flask_caching.backends.filesystemcache import FileSystemCache
@@ -139,18 +138,6 @@ SQLLAB_CTAS_NO_LIMIT = True
log_level_text = os.getenv("SUPERSET_LOG_LEVEL", "INFO")
LOG_LEVEL = getattr(logging, log_level_text.upper(), logging.INFO)
if os.getenv("CYPRESS_CONFIG") == "true":
# When running the service as a cypress backend, we need to import the config
# located @ tests/integration_tests/superset_test_config.py
base_dir = os.path.dirname(__file__)
module_folder = os.path.abspath(
os.path.join(base_dir, "../../tests/integration_tests/")
)
sys.path.insert(0, module_folder)
from superset_test_config import * # noqa
sys.path.pop(0)
#
# Optionally import superset_config_docker.py (which will have been included on
# the PYTHONPATH) in order to allow for local settings to be overridden

View File

@@ -579,7 +579,7 @@ There are three types of assets you can build:
1. `npm run build`: the production assets, CSS/JSS minified and optimized
2. `npm run dev-server`: local development assets, with sourcemaps and hot refresh support
3. `npm run build-instrumented`: instrumented application code for collecting code coverage from Cypress tests
3. `npm run build-instrumented`: instrumented application code for collecting code coverage from end-to-end tests
If while using the above commands you encounter an error related to the limit of file watchers:
@@ -722,7 +722,7 @@ can find all of the workflows and other assets under the `.github/` folder. This
- running the backend unit test suites (`tests/`)
- running the frontend test suites (`superset-frontend/src/**.*.test.*`)
- running our Playwright end-to-end tests (`superset-frontend/playwright/`) and legacy Cypress tests (`superset-frontend/cypress-base/`)
- running our Playwright end-to-end tests (`superset-frontend/playwright/`)
- linting the codebase, including all Python, Typescript and Javascript, yaml and beyond
- checking for all sorts of other rules conventions
@@ -797,7 +797,7 @@ pytest tests/unit_tests/*
#### Integration Tests
For more complex pytest-defined integration tests (not to be confused with our end-to-end Cypress tests), many tests will require having a working test environment. Some tests require a database, Celery, and potentially other services or libraries installed.
For more complex pytest-defined integration tests (not to be confused with our end-to-end Playwright tests), many tests will require having a working test environment. Some tests require a database, Celery, and potentially other services or libraries installed.
### Running Tests with `act`
@@ -908,8 +908,6 @@ superset:
user: "root"
depends_on: *superset-depends-on
volumes: *superset-volumes
environment:
CYPRESS_CONFIG: "${CYPRESS_CONFIG}"
```
Start Superset as usual

View File

@@ -166,11 +166,9 @@ npm run test -- MyComponent.test.tsx
### E2E Integration Testing
We support both Playwright (recommended) and Cypress for end-to-end testing.
We use Playwright for end-to-end testing.
#### Playwright (Recommended - NEW)
Playwright is our new E2E testing framework, gradually replacing Cypress.
#### Playwright
```bash
# Navigate to frontend directory
@@ -199,30 +197,6 @@ npm run playwright:debug tests/auth/login.spec.ts
npm run playwright:report
```
#### Cypress (DEPRECATED - will be removed)
Cypress is being phased out in favor of Playwright but is still available:
```bash
# Set base URL for Cypress
export CYPRESS_BASE_URL='http://localhost:8088'
export CYPRESS_DATABASE=test
export CYPRESS_USERNAME=admin
export CYPRESS_PASSWORD=admin
# Navigate to Cypress directory
cd superset-frontend/cypress-base
# Run interactively
npm run cypress-debug
# Run headless (like CI)
npm run cypress-run-chrome
# Run specific file
npm run cypress-run-chrome -- --spec "cypress/e2e/explore/chart.test.js"
```
### Debugging Server App
For debugging the Flask backend:

View File

@@ -36,7 +36,7 @@ This is a list of statements that describe how we do frontend development in Sup
- We use [@emotion](https://emotion.sh/docs/introduction) to provide styling for our components, co-locating styling within component files.
- See: [SIP-37](https://github.com/apache/superset/issues/9145)
- See: [Emotion Styling Guidelines and Best Practices](./frontend/emotion-styling-guidelines.md)
- We use Jest for unit tests, React Testing Library for component tests, and Cypress for end-to-end tests.
- We use Jest for unit tests, React Testing Library for component tests, and Playwright for end-to-end tests.
- See: [SIP-56](https://github.com/apache/superset/issues/11830)
- See: [Testing Guidelines and Best Practices](../testing/testing-guidelines.md)
- We add tests for every new component or file added to the frontend.

View File

@@ -24,12 +24,10 @@ under the License.
# End-to-End Testing
Apache Superset uses Playwright for end-to-end testing, migrating from the legacy Cypress tests.
Apache Superset uses Playwright for end-to-end testing.
## Running Tests
### Playwright (Recommended)
```bash
cd superset-frontend
@@ -53,16 +51,6 @@ npm run playwright:debug tests/auth/login.spec.ts
# or: npx playwright test --debug tests/auth/login.spec.ts
```
### Cypress (Deprecated)
Cypress tests are being migrated to Playwright. For legacy tests:
```bash
cd superset-frontend/cypress-base
npm run cypress-run-chrome # Headless
npm run cypress-debug # Interactive UI
```
## Project Architecture
```
@@ -179,10 +167,10 @@ All debugging artifacts are available in the HTML report for easy analysis.
## Configuration
- **Config**: `playwright.config.ts` - matches Cypress settings
- **Config**: `playwright.config.ts`
- **Base URL**: `http://localhost:8088` (assumes Superset running)
- **Browsers**: Chrome only for Phase 1 (YAGNI)
- **Retries**: 2 in CI, 0 locally (matches Cypress)
- **Retries**: 2 in CI, 0 locally
## Contributing Guidelines
@@ -207,16 +195,6 @@ All debugging artifacts are available in the HTML report for easy analysis.
3. **Actions and queries only** - no assertions
4. **Use existing components** for DOM interactions
## Migration from Cypress
When porting Cypress tests:
1. **Port the logic**, not the implementation
2. **Use page objects** instead of inline selectors
3. **Replace `cy.intercept/cy.wait`** with `page.waitForRequest()`
4. **Use shared constants** from `utils/urls.ts`
5. **Follow the established patterns** shown in `tests/auth/login.spec.ts`
## Best Practices
- **Centralize selectors** in page objects

View File

@@ -169,7 +169,7 @@ npm run test:coverage
- **Jest** - Unit and integration testing framework
- **React Testing Library** - Component testing utilities
- **Playwright** - End-to-end testing (replacing Cypress)
- **Playwright** - End-to-end testing
- **Storybook** - Component development and testing
---

View File

@@ -26,7 +26,7 @@ under the License.
We feel that tests are an important part of a feature and not an additional or optional effort. That's why we colocate test files with functionality and sometimes write tests upfront to help validate requirements and shape the API of our components. Every new component or file added should have an associated test file with the `.test` extension.
We use Jest, React Testing Library (RTL), and Cypress to write our unit, integration, and end-to-end tests. For each type, we have a set of best practices/tips described below:
We use Jest, React Testing Library (RTL), and Playwright to write our unit, integration, and end-to-end tests. For each type, we have a set of best practices/tips described below:
## Jest
@@ -80,9 +80,9 @@ Prefer the [user-event](https://github.com/testing-library/user-event) library,
You can find an example of a test [here](https://github.com/apache/superset/blob/master/superset-frontend/src/dashboard/components/PublishedStatus/PublishedStatus.test.tsx).
## Cypress
## Playwright
### Prefer to use Cypress for e2e testing and RTL for integration testing
### Prefer to use Playwright for e2e testing and RTL for integration testing
Here it's important to make the distinction between e2e and integration testing. This [article](https://www.onpathtesting.com/blog/end-to-end-vs-integration-testing) gives an excellent definition:
@@ -91,30 +91,11 @@ Here it's important to make the distinction between e2e and integration testing.
> A typical software project consists of multiple software units, usually coded by different developers. Integration testing combines those software units logically and tests them as a group.
> Essentially, integration testing verifies whether or not the individual modules or services that make up your application work well together. The purpose of this level of testing is to expose defects in the interaction between these software modules when they are integrated.
Do not use Cypress when RTL can do it better and faster. Many of the Cypress tests that we have right now, fall into the integration testing category and can be ported to RTL which is much faster and gives more immediate feedback. Cypress should be used mainly for end-to-end testing, replicating the user experience, with positive and negative flows.
Do not use Playwright when RTL can do it better and faster. Tests that fall into the integration testing category belong in RTL, which is much faster and gives more immediate feedback. Playwright should be used mainly for end-to-end testing, replicating the user experience, with positive and negative flows. See [End-to-End Testing](./e2e-testing.md) for the Playwright setup and conventions.
### Isolated and standalone tests
Tests should never rely on other tests to pass. This might be hard when a single user is used for testing as data will be stored in the database. At every new test, we should reset the database.
### Cleaning state
Cleaning the state of the application, such as resetting the DB, or in general, any state that might affect consequent tests should always be done in the `beforeEach` hook and never in the `afterEach` one as the `beforeEach` is guaranteed to run, while the test might never reach the point to run the `afterEach` hook. One example would be if you refresh Cypress in the middle of the test. At this point, you will have built up a partial state in the database, and your clean-up function will never get called. You can read more about it [here](https://docs.cypress.io/guides/references/best-practices#Using-after-or-afterEach-hooks).
### Unnecessary use of `cy.wait`
- Unnecessary when using `cy.request()` as it will resolve when a response is received from the server
- Unnecessary when using `cy.visit()` as it resolves only when the page fires the load event
- Unnecessary when using `cy.get()`. When the selector should wait for a request to happen, aliases would come in handy:
```js
cy.intercept('GET', '/users', [{ name: 'Maggy' }, { name: 'Joan' }]).as(
'getUsers',
);
cy.get('#fetch').click();
cy.wait('@getUsers'); // <--- wait explicitly for this route to finish
cy.get('table tr').should('have.length', 2);
```
Tests should never rely on other tests to pass. Create the data a test needs through the API helpers and the `testAssets` fixture, which cleans up what it tracked, instead of depending on state left behind by another test.
### Accessibility and Resilience
@@ -127,5 +108,5 @@ The same accessibility principles in the RTL section apply here. Use accessible
- [Fix the "not wrapped in act(...)" warning](https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning)
- [Common mistakes with React Testing Library](https://kentcdodds.com/blog/common-mistakes-with-react-testing-library)
- [ARIA Roles](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles)
- [Cypress - Best Practices](https://docs.cypress.io/guides/references/best-practices)
- [Playwright - Best Practices](https://playwright.dev/docs/best-practices)
- [End-to-end vs integration tests: what's the difference?](https://www.onpathtesting.com/blog/end-to-end-vs-integration-testing)

View File

@@ -1,167 +0,0 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
import argparse
import os
import subprocess
from datetime import datetime
XVFB_PRE_CMD = "xvfb-run --auto-servernum --server-args='-screen 0, 1280x1024x24' "
REPO = os.getenv("GITHUB_REPOSITORY") or "apache/superset"
GITHUB_EVENT_NAME = os.getenv("GITHUB_EVENT_NAME") or "push"
CYPRESS_RECORD_KEY = os.getenv("CYPRESS_RECORD_KEY") or ""
def generate_build_id() -> str:
"""Generates a build ID based on the current timestamp."""
now = datetime.now()
rounded_minute = now.minute - (now.minute % 20)
rounded_time = now.replace(minute=rounded_minute, second=0, microsecond=0)
return (os.getenv("GITHUB_SHA") or "DUMMY")[:8] + rounded_time.strftime(
"%Y%m%d%H%M"
)
def run_cypress_for_test_file(
test_file: str, retries: int, use_dashboard: bool, group: str, dry_run: bool, i: int
) -> int:
"""Runs Cypress for a single test file and retries upon failure."""
cypress_cmd = "./node_modules/.bin/cypress run"
os.environ["TERM"] = "xterm"
os.environ["ELECTRON_DISABLE_GPU"] = "true"
build_id = generate_build_id()
browser = os.getenv("CYPRESS_BROWSER", "chrome")
chrome_flags = "--disable-dev-shm-usage"
for attempt in range(retries):
# Create Cypress command for a single test file
cmd: str = ""
if use_dashboard:
# If/when we want to use cypress' dashboard feature to record the run
group_id = f"matrix{group}-file{i}-{attempt}"
cmd = (
f"{XVFB_PRE_CMD} "
f'{cypress_cmd} --spec "{test_file}" '
f"--config numTestsKeptInMemory=0 "
f"--browser {browser} "
f"--record --group {group_id} --tag {REPO},{GITHUB_EVENT_NAME} "
f"--ci-build-id {build_id} "
f"-- {chrome_flags}"
)
else:
os.environ.pop("CYPRESS_RECORD_KEY", None)
cmd = (
f"{XVFB_PRE_CMD} "
f"{cypress_cmd} "
f"--browser {browser} "
f"--config numTestsKeptInMemory=0 "
f'--spec "{test_file}" '
f"-- {chrome_flags}"
)
print(f"RUN: {cmd} (Attempt {attempt + 1}/{retries})")
if dry_run:
# Print the command instead of executing it
print(f"DRY RUN: {cmd}")
return 0
process = subprocess.Popen( # noqa: S602
cmd,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
universal_newlines=True,
)
# Stream stdout in real-time
if process.stdout:
for stdout_line in iter(process.stdout.readline, ""):
print(stdout_line, end="")
process.wait()
if process.returncode == 0:
print(f"Test {test_file} succeeded on attempt {attempt + 1}")
return 0
else:
print(f"Test {test_file} failed on attempt {attempt + 1}")
print(f"Test {test_file} failed after {retries} retries.")
return process.returncode
def main() -> None:
parser = argparse.ArgumentParser(
description="Run Cypress tests with retries per test file"
)
parser.add_argument(
"--use-dashboard",
action="store_true",
help="Use Cypress Dashboard for parallelization",
)
parser.add_argument(
"--filter", type=str, required=False, default=None, help="Filter to test"
)
parser.add_argument("--group", type=str, default="Default", help="Group name")
parser.add_argument(
"--retries", type=int, default=3, help="Number of retries per test file"
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Print the command instead of executing it",
)
args = parser.parse_args()
script_dir = os.path.dirname(os.path.abspath(__file__))
cypress_base_path = "superset-frontend/cypress-base/"
cypress_base_full_path = os.path.join(script_dir, "../", cypress_base_path)
cypress_tests_path = os.path.join(cypress_base_full_path, "cypress/e2e")
test_files = []
file_count = 0
skipped_count = 0
for root, _, files in os.walk(cypress_tests_path):
for file in files:
if file.endswith("test.ts") or file.endswith("test.js"):
# Skip files prefixed with _skip. (excluded by excludeSpecPattern)
if file.startswith("_skip."):
skipped_count += 1
continue
file_count += 1
test_files.append(
os.path.join(root, file).replace(cypress_base_full_path, "")
)
print(f"Found {file_count} test files ({skipped_count} skipped).")
# Sort test files for deterministic, readable run order.
spec_list = sorted(test_files)
# Run each test file independently with retry logic or dry-run
processed_file_count: int = 0
for i, test_file in enumerate(spec_list):
result = run_cypress_for_test_file(
test_file, args.retries, args.use_dashboard, args.group, args.dry_run, i
)
if result != 0:
print(f"Exiting due to failure in {test_file}")
exit(result)
processed_file_count += 1
print(f"Ran {processed_file_count} test files successfully.")
if __name__ == "__main__":
main()

View File

@@ -98,7 +98,6 @@ BASE_NGINX=80
BASE_SUPERSET=8088
BASE_NODE=9000
BASE_WEBSOCKET=8080
BASE_CYPRESS=8081
BASE_DATABASE=5432
BASE_REDIS=6379
@@ -108,7 +107,6 @@ find_and_claim_port $BASE_NGINX NGINX_PORT
find_and_claim_port $BASE_SUPERSET SUPERSET_PORT
find_and_claim_port $BASE_NODE NODE_PORT
find_and_claim_port $BASE_WEBSOCKET WEBSOCKET_PORT
find_and_claim_port $BASE_CYPRESS CYPRESS_PORT
find_and_claim_port $BASE_DATABASE DATABASE_PORT
find_and_claim_port $BASE_REDIS REDIS_PORT
@@ -144,7 +142,6 @@ export NGINX_PORT
export SUPERSET_PORT
export NODE_PORT
export WEBSOCKET_PORT
export CYPRESS_PORT
export DATABASE_PORT
export REDIS_PORT
@@ -188,7 +185,6 @@ case "${1:-}" in
echo "export SUPERSET_PORT=$SUPERSET_PORT"
echo "export NODE_PORT=$NODE_PORT"
echo "export WEBSOCKET_PORT=$WEBSOCKET_PORT"
echo "export CYPRESS_PORT=$CYPRESS_PORT"
echo "export DATABASE_PORT=$DATABASE_PORT"
echo "export REDIS_PORT=$REDIS_PORT"
exit 0

View File

@@ -1,6 +1,4 @@
coverage/*
cypress/screenshots
cypress/videos
playwright/.auth
playwright-report/
test-results/

View File

@@ -90,7 +90,7 @@ export default {
'@babel/plugin-transform-export-namespace-from',
],
},
// build instrumented code for testing code coverage with Cypress
// build instrumented code for testing code coverage with E2E tests
instrumented: {
plugins: [
[

View File

@@ -1,41 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
{
"parser": "@typescript-eslint/parser",
"plugins": ["cypress"],
"extends": ["plugin:cypress/recommended"],
"rules": {
"import/no-unresolved": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/camelcase": 0,
"cypress/no-unnecessary-waiting": 0 // see issue https://github.com/cypress-io/eslint-plugin-cypress/issues/69
},
"settings": {
"import/resolver": {
"node": {
"extensions": [".js", ".jsx", ".ts", ".tsx"]
}
}
},
"env": {
"cypress/globals": true
}
}

View File

@@ -1,4 +0,0 @@
screenshots
.nyc_output
coverage
coverage.json

View File

@@ -1,84 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import { defineConfig } from 'cypress';
const { verifyDownloadTasks } = require('cy-verify-downloads');
export default defineConfig({
chromeWebSecurity: false,
defaultCommandTimeout: 8000,
numTestsKeptInMemory: 3,
// Disabled after realizing this MESSES UP rison encoding in intricate ways
experimentalFetchPolyfill: false,
experimentalMemoryManagement: true,
requestTimeout: 10000,
video: false,
viewportWidth: 1280,
viewportHeight: 1024,
projectId: 'ud5x2f',
retries: {
runMode: 2,
openMode: 0,
},
e2e: {
// We've imported your old cypress plugins here.
// You may want to clean this up later by importing these.
setupNodeEvents(on, config) {
// ECONNRESET on Chrome/Chromium 117.0.5851.0 when using Cypress <12.15.0
// Check https://github.com/cypress-io/cypress/issues/27804 for context
// TODO: This workaround should be removed when upgrading Cypress
on('before:browser:launch', (browser, launchOptions) => {
if (browser.name === 'chrome' && browser.isHeadless) {
// eslint-disable-next-line no-param-reassign
launchOptions.args = launchOptions.args.map(arg => {
if (arg === '--headless') {
return '--headless=new';
}
return arg;
});
launchOptions.args.push(
'--disable-dev-shm-usage',
'--disable-gpu',
'--no-sandbox',
'--disable-software-rasterizer',
'--memory-pressure-off',
'--js-flags=--max-old-space-size=4096',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
);
}
return launchOptions;
});
// eslint-disable-next-line global-require
require('@cypress/code-coverage/task')(on, config);
on('task', verifyDownloadTasks);
// eslint-disable-next-line global-require,import/extensions
return config;
},
baseUrl: 'http://localhost:8088',
excludeSpecPattern: ['**/_skip.*'],
experimentalRunAllSpecs: true,
specPattern: ['cypress/e2e/**/*.{js,jsx,ts,tsx}'],
},
});

View File

@@ -1,38 +0,0 @@
[
{
"slice_name": "1 - Sample chart",
"description": "chart description",
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
"datasource_type": "table",
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
},
{
"slice_name": "2 - Sample chart",
"description": "chart description",
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
"datasource_type": "table",
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
},
{
"slice_name": "3 - Sample chart",
"description": "chart description",
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
"datasource_type": "table",
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
},
{
"slice_name": "4 - Sample chart",
"description": "chart description",
"viz_type": "echarts_timeseries_line",
"cache_timeout": 1000,
"datasource_id": 2,
"datasource_type": "table",
"params": "{\"viz_type\":\"echarts_timeseries_line\",\"x_axis\":\"year\",\"metrics\":[\"count\"]}"
}
]

View File

@@ -1,46 +0,0 @@
[
{
"dashboard_title": "1 - Sample dashboard",
"slug": "1-sample-dashboard"
},
{
"dashboard_title": "2 - Sample dashboard",
"slug": "2-sample-dashboard"
},
{
"dashboard_title": "3 - Sample dashboard",
"slug": "3-sample-dashboard"
},
{
"dashboard_title": "4 - Sample dashboard",
"slug": "4-sample-dashboard"
},
{
"dashboard_title": "5 - Sample dashboard",
"slug": "5-sample-dashboard"
},
{
"dashboard_title": "6 - Sample dashboard",
"slug": "6-sample-dashboard"
},
{
"dashboard_title": "7 - Sample dashboard",
"slug": "7-sample-dashboard"
},
{
"dashboard_title": "8 - Sample dashboard",
"slug": "8-sample-dashboard"
},
{
"dashboard_title": "9 - Sample dashboard",
"slug": "9-sample-dashboard"
},
{
"dashboard_title": "10 - Sample dashboard",
"slug": "10-sample-dashboard"
},
{
"dashboard_title": "11 - Sample dashboard",
"slug": "11-sample-dashboard"
}
]

View File

@@ -1,455 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import '@cypress/code-coverage/support';
import { expect } from 'chai';
import rison from 'rison';
/* eslint-disable @typescript-eslint/no-explicit-any */
require('cy-verify-downloads').addCustomCommand();
// fail on console error, allow config to override individual tests
// these exceptions are a little pile of tech debt
//
// DISABLING FOR NOW
/*
const { getConfig, setConfig } = failOnConsoleError({
consoleMessages: [
/\[webpack-dev-server\]/,
'The pseudo class ":first-child" is potentially unsafe when doing server-side rendering. Try changing it to ":first-of-type".',
'The pseudo class ":nth-child" is potentially unsafe when doing server-side rendering. Try changing it to ":nth-of-type".',
'Error: Unknown Error',
/Unable to infer path to ace from script src/,
],
includeConsoleTypes: ['error'],
});
*/
// Set individual tests to allow certain console errors to NOT fail, e.g
// cy.allowConsoleErrors(['foo', /^some bar-regex.*/]);
// This will be reset between tests.
Cypress.Commands.addAll({
getConsoleMessages: () => cy.wrap(getConfig()?.consoleMessages),
allowConsoleErrors: (consoleMessages: (string | RegExp)[]) =>
setConfig({ ...getConfig(), consoleMessages }),
});
const BASE_EXPLORE_URL = '/explore/?form_data=';
let DASHBOARD_FIXTURES: Record<string, any>[] = [];
let CHART_FIXTURES: Record<string, any>[] = [];
Cypress.Commands.add('loadChartFixtures', () =>
cy.fixture('charts.json').then(charts => {
CHART_FIXTURES = charts;
}),
);
Cypress.Commands.add('loadDashboardFixtures', () =>
cy.fixture('dashboards.json').then(dashboards => {
DASHBOARD_FIXTURES = dashboards;
}),
);
const PATHS_TO_SKIP_LOGIN = ['login', 'register'];
const skipLogin = () => {
for (const path of PATHS_TO_SKIP_LOGIN) {
if (Cypress.currentTest.title.toLowerCase().includes(path)) {
return true;
}
}
return false;
};
before(() => {
if (skipLogin()) {
return;
}
cy.login();
Cypress.Cookies.defaults({ preserve: 'session' });
cy.loadChartFixtures();
cy.loadDashboardFixtures();
});
beforeEach(() => {
if (skipLogin()) {
return;
}
cy.cleanDashboards();
cy.cleanCharts();
});
Cypress.Commands.add('cleanDashboards', () => {
cy.getDashboards().then((sampleDashboards?: Record<string, any>[]) => {
const deletableDashboards = [];
for (let i = 0; i < DASHBOARD_FIXTURES.length; i += 1) {
const fixture = DASHBOARD_FIXTURES[i];
const isInDb = sampleDashboards?.find(
d => d.dashboard_title === fixture.dashboard_title,
);
if (isInDb) {
deletableDashboards.push(isInDb.id);
}
}
if (deletableDashboards.length) {
cy.request({
failOnStatusCode: false,
method: 'DELETE',
url: `api/v1/dashboard/?q=!(${deletableDashboards.join(',')})`,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
}).then(resp => resp);
}
});
});
Cypress.Commands.add('cleanCharts', () => {
cy.getCharts().then((sampleCharts?: Record<string, any>[]) => {
const deletableCharts = [];
for (let i = 0; i < CHART_FIXTURES.length; i += 1) {
const fixture = CHART_FIXTURES[i];
const isInDb = sampleCharts?.find(
c => c.slice_name === fixture.slice_name,
);
if (isInDb) {
deletableCharts.push(isInDb.id);
}
}
if (deletableCharts.length) {
cy.request({
failOnStatusCode: false,
method: 'DELETE',
url: `api/v1/chart/?q=!(${deletableCharts.join(',')})`,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
}).then(resp => resp);
}
});
});
Cypress.Commands.add('getBySel', (selector, ...args) =>
cy.get(`[data-test=${selector}]`, ...args),
);
Cypress.Commands.add('getBySelLike', (selector, ...args) =>
cy.get(`[data-test*=${selector}]`, ...args),
);
/* eslint-disable consistent-return */
Cypress.on('uncaught:exception', err => {
// ignore ResizeObserver client errors, as they are unrelated to operation
// and causing flaky test failures in CI
if (err.message && /ResizeObserver loop limit exceeded/.test(err.message)) {
// returning false here prevents Cypress from failing the test
return false;
}
return false; // TODO:@geido remove
});
/* eslint-enable consistent-return */
Cypress.Commands.add('login', () => {
cy.request({
method: 'POST',
url: '/login/',
body: { username: 'admin', password: 'general' },
}).then(response => {
if (response.status === 302) {
// If there's a redirect, follow it manually
const redirectUrl = response.headers.location;
cy.request({
method: 'GET',
url: redirectUrl,
}).then(finalResponse => {
expect(finalResponse.status).to.eq(200);
});
} else {
expect(response.status).to.eq(200);
}
});
});
Cypress.Commands.add('visitChartByName', name => {
const query = rison.encode({
columns: ['id'],
filters: [{ col: 'slice_name', opr: 'eq', value: name }],
});
cy.request(`/api/v1/chart?q=${query}`).then(response => {
cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${response.body.result[0].id}}`);
});
});
Cypress.Commands.add('visitChartById', chartId =>
cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`),
);
Cypress.Commands.add(
'visitChartByParams',
(formData: {
datasource?: string;
datasource_id?: number;
datasource_type?: string;
[key: string]: unknown;
}) => {
let datasource_id;
let datasource_type;
if (formData.datasource_id && formData.datasource_type) {
({ datasource_id, datasource_type } = formData);
} else {
[datasource_id, datasource_type] = formData.datasource?.split('__') || [];
}
const accessToken = window.localStorage.getItem('access_token');
cy.request({
method: 'POST',
url: 'api/v1/explore/form_data',
body: {
datasource_id,
datasource_type,
form_data: JSON.stringify(formData),
},
headers: {
...(accessToken && {
Cookie: `csrf_access_token=${accessToken}`,
'X-CSRFToken': accessToken,
}),
'Content-Type': 'application/json',
Referer: `${Cypress.config().baseUrl}/`,
},
}).then(response => {
const formDataKey = response.body.key;
const url = `/explore/?form_data_key=${formDataKey}`;
cy.visit(url);
});
},
);
Cypress.Commands.add('verifySliceContainer', chartSelector => {
// After a wait response check for valid slice container
cy.get('.slice_container')
.should('be.visible')
.within(() => {
if (chartSelector) {
cy.get(chartSelector)
.should('be.visible')
.then(chart => {
expect(chart[0].clientWidth).greaterThan(0);
expect(chart[0].clientHeight).greaterThan(0);
});
}
});
return cy;
});
Cypress.Commands.add(
'verifySliceSuccess',
({
waitAlias,
querySubstring,
chartSelector,
}: {
waitAlias: string;
chartSelector: JQuery.Selector;
querySubstring?: string | RegExp;
}) => {
cy.wait(waitAlias).then(({ response }) => {
cy.verifySliceContainer(chartSelector);
const responseBody = response?.body;
if (querySubstring) {
const query: string =
responseBody.query || responseBody.result[0].query || '';
if (querySubstring instanceof RegExp) {
expect(query).to.match(querySubstring);
} else {
expect(query).to.contain(querySubstring);
}
}
});
return cy;
},
);
Cypress.Commands.add('createSampleDashboards', (indexes?: number[]) =>
cy.cleanDashboards().then(() => {
for (let i = 0; i < DASHBOARD_FIXTURES.length; i += 1) {
if (indexes?.includes(i) || !indexes) {
cy.request({
method: 'POST',
url: `/api/v1/dashboard/`,
body: DASHBOARD_FIXTURES[i],
failOnStatusCode: false,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
});
}
}
}),
);
Cypress.Commands.add('createSampleCharts', (indexes?: number[]) =>
cy.cleanCharts().then(() => {
for (let i = 0; i < CHART_FIXTURES.length; i += 1) {
if (indexes?.includes(i) || !indexes) {
cy.request({
method: 'POST',
url: `/api/v1/chart/`,
body: CHART_FIXTURES[i],
failOnStatusCode: false,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
});
}
}
}),
);
Cypress.Commands.add(
'deleteDashboardByName',
(dashboardName: string, failOnStatusCode = false) =>
cy.getDashboards().then((sampleDashboards?: Record<string, any>[]) => {
const dashboard = sampleDashboards?.find(
d => d.dashboard_title === dashboardName,
);
if (dashboard) {
cy.deleteDashboard(dashboard.id, failOnStatusCode);
}
}),
);
Cypress.Commands.add(
'deleteDashboard',
(id: number, failOnStatusCode = false) =>
cy
.request({
failOnStatusCode,
method: 'DELETE',
url: `api/v1/dashboard/${id}`,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
})
.then(resp => resp),
);
Cypress.Commands.add('getDashboards', () => {
cy.request({
method: 'GET',
url: `api/v1/dashboard/`,
headers: {
'Content-Type': 'application/json',
},
}).then(resp => resp.body.result);
});
Cypress.Commands.add('getDashboard', (dashboardId: string | number) =>
cy
.request({
method: 'GET',
url: `api/v1/dashboard/${dashboardId}`,
headers: {
'Content-Type': 'application/json',
},
})
.then(resp => resp.body.result),
);
Cypress.Commands.add(
'updateDashboard',
(dashboardId: number, body: Record<string, any>) =>
cy
.request({
method: 'PUT',
url: `api/v1/dashboard/${dashboardId}`,
body,
headers: {
'Content-Type': 'application/json',
},
})
.then(resp => resp.body.result),
);
Cypress.Commands.add('deleteChart', (id: number, failOnStatusCode = false) =>
cy
.request({
failOnStatusCode,
method: 'DELETE',
url: `api/v1/chart/${id}`,
headers: {
Cookie: `csrf_access_token=${window.localStorage.getItem(
'access_token',
)}`,
'Content-Type': 'application/json',
'X-CSRFToken': `${window.localStorage.getItem('access_token')}`,
Referer: `${Cypress.config().baseUrl}/`,
},
})
.then(resp => resp),
);
Cypress.Commands.add('getCharts', () =>
cy
.request({
method: 'GET',
url: `api/v1/chart/`,
headers: {
'Content-Type': 'application/json',
},
})
.then(resp => resp.body.result),
);
Cypress.Commands.add(
'deleteChartByName',
(sliceName: string, failOnStatusCode = false) =>
cy.getCharts().then((sampleCharts?: Record<string, any>[]) => {
const chart = sampleCharts?.find(c => c.slice_name === sliceName);
if (chart) {
cy.deleteChart(chart.id, failOnStatusCode);
}
}),
);

View File

@@ -1,92 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// eslint-disable-next-line spaced-comment
/// <reference types="cypress" />
type JSONPrimitive = string | number | boolean | null;
type JSONValue = JSONPrimitive | JSONObject | JSONArray;
type JSONObject = { [member: string]: JSONValue };
type JSONArray = JSONValue[];
declare namespace Cypress {
interface Chainable {
/**
* Login test user.
*/
login(): void;
/**
*
* Utils
*/
getBySel(selector: string): cy;
getBySelLike(selector: string): cy;
cleanCharts(): cy;
cleanDashboards(): cy;
loadChartFixtures(): cy;
loadDashboardFixtures(): cy;
allowConsoleErrors(consoleMessages: (string | RegExp)[]): cy;
visitChartByParams(params: string | Record<string, unknown>): cy;
visitChartByName(name: string): cy;
visitChartById(id: number): cy;
/**
* Verify slice container renders.
*/
verifySliceContainer(chartSelector: JQuery.Selector): cy;
/**
* Verify slice successfully loaded.
*/
verifySliceSuccess(options: {
waitAlias: string;
querySubstring?: string | RegExp;
chartSelector?: JQuery.Selector;
}): cy;
/**
* Get
*/
getDashboards(): cy;
getDashboard(dashboardId: string | number): Record<string, any>;
getCharts(): cy;
/**
* Create
*/
createSampleDashboards(indexes?: number[]): void;
createSampleCharts(indexes?: number[]): void;
/**
* Delete
*/
deleteDashboard(id: number, failOnStatusCode: boolean): cy;
deleteDashboardByName(dashboardName: string, failOnStatusCode: boolean): cy;
deleteChartByName(name: string, failOnStatusCode: boolean): cy;
deleteChart(id: number, failOnStatusCode: boolean): cy;
/**
* Update
*/
updateDashboard(dashboardId: number, body: Record<string, any>): cy;
}
}
declare module '@cypress/code-coverage/task';

View File

@@ -1,121 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getChartAlias, Slice } from 'cypress/utils/vizPlugins';
export * from './vizPlugins';
export { default as parsePostForm } from './parsePostForm';
export interface ChartSpec {
name: string;
viz: string;
}
const toSlicelike = ($chart: JQuery<HTMLElement>): Slice => {
const chartId = $chart.attr('data-test-chart-id');
const vizType = $chart.attr('data-test-viz-type');
return {
slice_id: chartId ? parseInt(chartId, 10) : null,
form_data: {
viz_type: vizType || null,
},
};
};
export function getChartGridComponent({ name, viz }: ChartSpec) {
return cy
.get(`[data-test-chart-name="${name}"]`)
.should('have.attr', 'data-test-viz-type', viz);
}
export function getChartAliasBySpec(chart: ChartSpec) {
return getChartGridComponent(chart).then($chart =>
cy.wrap(getChartAlias(toSlicelike($chart))),
);
}
export function getChartAliasesBySpec(charts: readonly ChartSpec[]) {
const aliases: string[] = [];
charts.forEach(chart =>
getChartAliasBySpec(chart).then(alias => {
aliases.push(alias);
}),
);
// Wrapping the aliases is key.
// That way callers can chain off this function
// and actually get the list of aliases.
return cy.wrap(aliases);
}
export function waitForChartLoad(chart: ChartSpec) {
return getChartGridComponent(chart).then(gridComponent => {
const chartId = gridComponent.attr('data-test-chart-id');
// the chart should load in under half a minute
return (
cy
// this id only becomes visible when the chart is loaded
.get(`#chart-id-${chartId}`, {
timeout: 30000,
})
.should('be.visible')
// return the chart grid component
.then(() => gridComponent)
);
});
}
export function resize(selector: string) {
return {
to(cordX: number, cordY: number) {
cy.get(selector).trigger('mousedown', { which: 1, force: true });
cy.get(selector).trigger('mousemove', {
which: 1,
cordX,
cordY,
force: true,
});
cy.get(selector).trigger('mouseup', { which: 1, force: true });
},
};
}
export const setSelectSearchInput = (
$input: any,
value: string,
async = false,
) => {
// Ant Design 5 Select crashes Chromium with type/click events when showSearch is true.
// This copies the value directly to the input element as a workaround.
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value',
)?.set;
nativeInputValueSetter?.call($input[0], value);
// Trigger the input and change events
if (async) {
$input[0].dispatchEvent(new Event('mousedown', { bubbles: true }));
}
$input[0].dispatchEvent(new Event('input', { bubbles: true }));
$input[0].dispatchEvent(new Event('change', { bubbles: true }));
cy.get('.ant-select-item-option-content').should('exist').first().click({
force: true,
});
};

View File

@@ -1,60 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Parse multipart form data sent via POST requests.
*/
export default function parsePostForm(requestBody: ArrayBuffer) {
type ParsedFields = Record<string, string[] | string>;
if (requestBody.constructor.name !== 'ArrayBuffer') {
return requestBody;
}
const lines = new TextDecoder('utf-8').decode(requestBody).split('\n');
const fields: ParsedFields = {};
let key = '';
let value: string[] = [];
function addField(key: string, value: string) {
if (key in fields) {
if (Array.isArray(fields[key])) {
(fields[key] as string[]).push(value);
} else {
fields[key] = [fields[key] as string, value];
}
} else {
fields[key] = value;
}
}
lines.forEach(line => {
const nameMatch = line.match(/Content-Disposition: form-data; name="(.*)"/);
if (nameMatch) {
if (key) {
addField(key, value.join('\n'));
}
key = nameMatch[1];
value = [];
} else if (!/----.*FormBoundary/.test(line)) {
value.push(line);
}
});
if (key && value) {
addField(key, value.join('\n'));
}
return fields;
}

View File

@@ -1,24 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const DASHBOARD_LIST = '/dashboard/list/';
export const CHART_LIST = '/chart/list/';
export const WORLD_HEALTH_DASHBOARD = '/dashboard/world_health/';
export const TABBED_DASHBOARD = '/dashboard/tabbed_dash/';
export const DATABASE_LIST = '/databaseview/list';

View File

@@ -1,104 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
export type JsonArray = JsonValue[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type JsonObject = { [member: string]: any };
export interface Slice {
slice_id: number;
form_data: {
viz_type: string;
};
}
export interface Dashboard {
slices: Slice[];
}
const V1_PLUGINS = [
'box_plot',
'echarts_timeseries',
'word_cloud',
'pie',
'table',
'big_number',
'big_number_total',
];
export const DASHBOARD_CHART_ALIAS_PREFIX = 'getChartData_';
export function isLegacyChart(vizType: string): boolean {
return !V1_PLUGINS.includes(vizType);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isLegacyResponse(response: any): boolean {
return !response.result;
}
export function getSliceIdFromRequestUrl(url: string) {
const address = new URL(url);
const query = address.searchParams.get('form_data');
return query?.match(/\d+/)?.[0];
}
export function getChartDataRouteForSlice(slice: Slice) {
const vizType = slice.form_data.viz_type;
const isLegacy = isLegacyChart(vizType);
const formData = encodeURIComponent(`{"slice_id":${slice.slice_id}}`);
if (isLegacy) {
return `**/superset/explore_json/?*${formData}*`;
}
return `**/api/v1/chart/data?*${formData}*`;
}
export function getChartAlias(slice: Slice): string {
const alias = `${DASHBOARD_CHART_ALIAS_PREFIX}${slice.slice_id}_${slice.form_data.viz_type}`;
const route = getChartDataRouteForSlice(slice);
cy.intercept('POST', route).as(alias);
return `@${alias}`;
}
export function getChartAliases(slices: Slice[]): string[] {
return Array.from(slices).map(getChartAlias);
}
export function interceptChart({
sliceId,
legacy = false,
method = 'POST',
}: {
sliceId?: number;
legacy?: boolean;
method?: 'POST' | 'GET';
}) {
const urlBase = legacy ? '**/superset/explore_json/' : '**/api/v1/chart/data';
let url;
if (sliceId) {
const encodedFormData = encodeURIComponent(
JSON.stringify({ slice_id: sliceId }),
);
url = `${urlBase}?form_data=${encodedFormData}*`;
} else {
url = `${urlBase}**`;
}
return cy.intercept(method, url);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +0,0 @@
{
"name": "superset-cypress",
"version": "1.0.0",
"description": "run cypress against superset",
"license": "Apache-2.0",
"author": "Apache",
"scripts": {
"cypress": "cypress",
"cypress-debug": "cypress open --config watchForFileChanges=true",
"cypress-run-chrome": "cypress run --browser chrome --headless"
},
"dependencies": {
"@cypress/code-coverage": "^3.10.4",
"@superset-ui/core": "^2.1.0",
"brace": "^0.11.1",
"chai": "^4.5.0",
"cy-verify-downloads": "^0.2.5",
"cypress-fail-on-console-error": "^4.0.3",
"nanoid": "^6.0.0",
"querystringify": "^2.2.0",
"react-dom": "^16.13.0",
"rison": "^0.1.1"
},
"devDependencies": {
"@types/querystringify": "^2.0.0",
"cypress": "^11.2.0",
"eslint-plugin-cypress": "^3.5.0",
"tscw-config": "^1.1.2"
},
"overrides": {
"@babel/core": "^7.29.6",
"@cypress/code-coverage": {
"js-yaml": "^4.3.2"
},
"@cypress/request": "^3.0.0",
"cypress": {
"form-data": "^2.3.4"
},
"d3-interpolate": {
"d3-color": "3.1.0"
},
"minimatch@<10": {
"brace-expansion": "1.1.18"
},
"minimatch@>=10": {
"brace-expansion": ">=5.0.9"
},
"qs": "^6.14.2",
"uuid": "^11.1.1"
},
"nyc": {
"reporter": [
"html",
"json"
]
}
}

View File

@@ -1,16 +0,0 @@
{
"compilerOptions": {
"baseUrl": ".",
"allowSyntheticDefaultImports": true,
"moduleResolution": "Node",
"strict": true,
"target": "es2019",
"lib": ["es2019", "DOM"],
"types": ["cypress", "node"],
"allowJs": true,
"noEmit": true
},
"files": ["cypress/support/index.d.ts"],
"include": ["cypress/**/*.ts", "./cypress.config.ts"],
"exclude": ["node_modules"]
}

View File

@@ -1,46 +0,0 @@
#!/bin/bash
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
set -e
cd "$(dirname "$0")"
#run all the python steps in a background process
time superset db upgrade
time superset load_test_users
time superset load_examples --load-test-data
time superset init
echo "[completed python build steps]"
PORT='8081'
flask run -p $PORT --with-threads --reload --debugger &
#block on the longer running javascript process
time npm ci
time npm run build-instrumented
echo "[completed js build steps]"
#setup cypress
cd cypress-base
time npm ci
export CYPRESS_BASE_URL="http://localhost:${PORT}"
if [ -n "$1" ]; then
CYPRESS_PATH='cypress/e2e/'${1}'/*'
time npm run cypress-run-chrome -- --spec "$CYPRESS_PATH" --record false --config video=false || true
else
time npm run cypress-run-chrome -- --record false --config video=false || true
fi
kill %1

View File

@@ -120,7 +120,6 @@ export default [
'**/spec/**',
'**/__tests__/**',
'**/__mocks__/**',
'cypress-base/**',
],
rules: {
'theme-colors/no-literal-colors': 'off',

View File

@@ -314,7 +314,6 @@
],
"ignorePatterns": [
"packages/generator-superset/**/*",
"cypress-base/**",
"**/node_modules/**",
"build/**",
"**/dist/**",

View File

@@ -38,7 +38,7 @@
],
"type": "module",
"scripts": {
"_format": "oxfmt './{src,spec,cypress-base,plugins,packages,.storybook}/**/*{.js,.jsx,.ts,.tsx,.css,.scss,.sass,.json}'",
"_format": "oxfmt './{src,spec,plugins,packages,.storybook}/**/*{.js,.jsx,.ts,.tsx,.css,.scss,.sass,.json}'",
"build": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=production BABEL_ENV=\"${BABEL_ENV:=production}\" webpack --color --mode production",
"build-dev": "cross-env NODE_OPTIONS=--max_old_space_size=8192 NODE_ENV=development webpack --mode=development --color",
"build-instrumented": "cross-env NODE_ENV=production BABEL_ENV=instrumented webpack --mode=production --color",

View File

@@ -88,14 +88,29 @@ export class SaveChartModal extends Modal {
}
/**
* Adds the chart to a dashboard. When `dashboardTitle` matches an existing
* dashboard it is selected; otherwise the select's `allowNewOptions`
* affordance creates a new dashboard with that title.
* Adds the chart to a brand-new dashboard: the select's `allowNewOptions`
* affordance creates one with `dashboardTitle` when the chart is saved.
*/
async selectDashboard(dashboardTitle: string): Promise<void> {
await this.dashboardSelect.selectOption(dashboardTitle);
}
/**
* Adds the chart to an existing dashboard. Waits for the select's initial
* options to load before typing: the "create new" option is only offered
* when no loaded option has the typed title, so typing earlier leaves it as
* the sole match and picking it silently saves the chart to a duplicate
* dashboard. Relies on the workspace holding fewer dashboards than the
* select's page size, so the initial load contains the target and typing
* filters client-side without a server search.
*/
async selectExistingDashboard(dashboardTitle: string): Promise<void> {
await this.dashboardSelect.open();
await this.dashboardSelect.getVisibleOptionTexts();
await this.dashboardSelect.type(dashboardTitle);
await this.dashboardSelect.clickOption(dashboardTitle);
}
/**
* Clicks the primary "Save" footer button (`btn-modal-save`).
*/

View File

@@ -27,6 +27,7 @@
import type { Page } from '@playwright/test';
import { testWithAssets, expect } from '../../helpers/fixtures';
import { apiPutChart } from '../../helpers/api/chart';
import { getDashboardsByName } from '../../helpers/api/dashboard';
import { createTestDashboard } from '../dashboard/dashboard-test-helpers';
import { waitForPut } from '../../helpers/api/intercepts';
import { ExplorePage } from '../../pages/ExplorePage';
@@ -44,12 +45,15 @@ async function overwriteToDashboard(
): Promise<void> {
const saveModal = await explorePage.openSaveModal();
await saveModal.selectSaveAction('overwrite');
await saveModal.selectDashboard(dashboardName);
await saveModal.selectExistingDashboard(dashboardName);
const updated = waitForPut(page, `api/v1/chart/${chartId}`, {
pathMatch: true,
});
await saveModal.clickSave();
expect((await updated).ok()).toBe(true);
// A duplicate here means the save created a new dashboard instead of
// picking the existing one; it would also escape testAssets cleanup.
expect((await getDashboardsByName(page, dashboardName)).count).toBe(1);
await explorePage.waitForPageLoad();
}

View File

@@ -111,7 +111,7 @@ testWithAssets(
// Overwrite, selecting the now-existing dashboard by the same title.
const saveModal2 = await explorePage.openSaveModal();
await saveModal2.selectSaveAction('overwrite');
await saveModal2.selectDashboard(dashboardTitle);
await saveModal2.selectExistingDashboard(dashboardTitle);
const updated = waitForPut(page, `api/v1/chart/${newChartId}`, {
pathMatch: true,
});

View File

@@ -685,8 +685,6 @@ function main() {
/\/demo\//,
/\/examples\//,
/\/color\/colorSchemes\//,
/\/cypress\//,
/\/cypress-base\//,
/\/esm\//,
/\/lib\//,
/\/dist\//,
@@ -732,8 +730,6 @@ function main() {
'**/demo/**',
'**/examples/**',
'**/color/colorSchemes/**', // Color scheme definitions legitimately contain colors
'**/cypress/**',
'**/cypress-base/**',
'**/esm/**', // Build artifacts
'**/lib/**', // Build artifacts
'**/dist/**', // Build artifacts

View File

@@ -27,7 +27,6 @@ const ALWAYS_SKIP_SEGMENTS = new Set([
'build',
'coverage',
'__mocks__',
'cypress-base',
'playwright',
]);

View File

@@ -317,12 +317,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
}, [dashboardInfo?.id, dispatch]);
useEffect(() => {
// 'beforeunload' event interferes with Cypress data cleanup process.
// This code prevents 'beforeunload' from triggering in Cypress tests,
// as it is not required for end-to-end testing scenarios.
if (!(window as any).Cypress) {
window.addEventListener('beforeunload', onBeforeUnload);
}
window.addEventListener('beforeunload', onBeforeUnload);
return () => {
window.removeEventListener('beforeunload', onBeforeUnload);

View File

@@ -649,7 +649,6 @@ const config = {
'**/coverage',
'**/*.test.*',
'**/*.stories.*',
'**/cypress-base',
'**/*.geojson',
],
// Poll-based watching is needed in Docker/VM where native fs events