chore(e2e): remove Cypress infrastructure (#44829)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
tests/
|
||||
docs/
|
||||
install/
|
||||
superset-frontend/cypress-base/
|
||||
superset-frontend/coverage/
|
||||
superset-frontend/.temp_cache/
|
||||
superset/static/assets/
|
||||
|
||||
@@ -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
2
.github/labeler.yml
vendored
@@ -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'
|
||||
|
||||
|
||||
130
.github/workflows/bashlib.sh
vendored
130
.github/workflows/bashlib.sh
vendored
@@ -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 \
|
||||
|
||||
6
.github/workflows/caches.js
vendored
6
.github/workflows/caches.js
vendored
@@ -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`,
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
203
.github/workflows/superset-e2e.yml
vendored
203
.github/workflows/superset-e2e.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
10
AGENTS.md
10
AGENTS.md
@@ -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
|
||||
|
||||
8
Makefile
8
Makefile
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=''
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
|
||||
2
superset-frontend/.gitignore
vendored
2
superset-frontend/.gitignore
vendored
@@ -1,6 +1,4 @@
|
||||
coverage/*
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
playwright/.auth
|
||||
playwright-report/
|
||||
test-results/
|
||||
|
||||
@@ -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: [
|
||||
[
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
4
superset-frontend/cypress-base/.gitignore
vendored
4
superset-frontend/cypress-base/.gitignore
vendored
@@ -1,4 +0,0 @@
|
||||
screenshots
|
||||
.nyc_output
|
||||
coverage
|
||||
coverage.json
|
||||
@@ -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}'],
|
||||
},
|
||||
});
|
||||
@@ -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\"]}"
|
||||
}
|
||||
]
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
@@ -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);
|
||||
}
|
||||
}),
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
}
|
||||
15006
superset-frontend/cypress-base/package-lock.json
generated
15006
superset-frontend/cypress-base/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
|
||||
@@ -120,7 +120,6 @@ export default [
|
||||
'**/spec/**',
|
||||
'**/__tests__/**',
|
||||
'**/__mocks__/**',
|
||||
'cypress-base/**',
|
||||
],
|
||||
rules: {
|
||||
'theme-colors/no-literal-colors': 'off',
|
||||
|
||||
@@ -314,7 +314,6 @@
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"packages/generator-superset/**/*",
|
||||
"cypress-base/**",
|
||||
"**/node_modules/**",
|
||||
"build/**",
|
||||
"**/dist/**",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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`).
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,7 +27,6 @@ const ALWAYS_SKIP_SEGMENTS = new Set([
|
||||
'build',
|
||||
'coverage',
|
||||
'__mocks__',
|
||||
'cypress-base',
|
||||
'playwright',
|
||||
]);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -649,7 +649,6 @@ const config = {
|
||||
'**/coverage',
|
||||
'**/*.test.*',
|
||||
'**/*.stories.*',
|
||||
'**/cypress-base',
|
||||
'**/*.geojson',
|
||||
],
|
||||
// Poll-based watching is needed in Docker/VM where native fs events
|
||||
|
||||
Reference in New Issue
Block a user