diff --git a/.github/workflows/superset-e2e.yml b/.github/workflows/superset-e2e.yml index c8fe616e7ec..e78927f28b2 100644 --- a/.github/workflows/superset-e2e.yml +++ b/.github/workflows/superset-e2e.yml @@ -169,7 +169,6 @@ jobs: PYTHONPATH: ${{ github.workspace }} REDIS_PORT: 16379 GITHUB_TOKEN: ${{ github.token }} - SUPERSET_FEATURE_EMBEDDED_SUPERSET: "true" services: postgres: image: postgres:17-alpine diff --git a/.github/workflows/superset-playwright.yml b/.github/workflows/superset-playwright.yml index 0d0f9007275..03d1c525e2c 100644 --- a/.github/workflows/superset-playwright.yml +++ b/.github/workflows/superset-playwright.yml @@ -44,6 +44,7 @@ jobs: REDIS_PORT: 16379 GITHUB_TOKEN: ${{ github.token }} SUPERSET_FEATURE_EMBEDDED_SUPERSET: "true" + INCLUDE_EMBEDDED: "true" services: postgres: image: postgres:17-alpine @@ -131,6 +132,13 @@ jobs: NODE_OPTIONS: "--max-old-space-size=4096" with: run: playwright-run "${{ matrix.app_root }}" experimental/ + - name: Run Playwright (Embedded Tests) + if: steps.check.outputs.python || steps.check.outputs.frontend + uses: ./.github/actions/cached-dependencies + env: + NODE_OPTIONS: "--max-old-space-size=4096" + with: + run: playwright-run "${{ matrix.app_root }}" embedded - name: Set safe app root if: failure() id: set-safe-app-root diff --git a/docker/pythonpath_dev/superset_config_docker_light.py b/docker/pythonpath_dev/superset_config_docker_light.py index 18086e60b2c..f5a10d4bd6a 100644 --- a/docker/pythonpath_dev/superset_config_docker_light.py +++ b/docker/pythonpath_dev/superset_config_docker_light.py @@ -53,16 +53,17 @@ FEATURE_FLAGS = { }, } -# Disable Talisman so /embedded/ doesn't return X-Frame-Options:SAMEORIGIN. -# Without this, browsers refuse to render Superset inside an iframe from a -# different origin (i.e. the embedded SDK use case). Production/CI configures -# Talisman with explicit `frame-ancestors`; for the lightweight local stack we -# just turn it off. -TALISMAN_ENABLED = False +if os.environ.get("SUPERSET_FEATURE_EMBEDDED_SUPERSET", "").strip().lower() == "true": + # Disable Talisman so /embedded/ doesn't return X-Frame-Options:SAMEORIGIN. + # Without this, browsers refuse to render Superset inside an iframe from a + # different origin (i.e. the embedded SDK use case). Production/CI configures + # Talisman with explicit `frame-ancestors`; for the lightweight local stack we + # just turn it off. + TALISMAN_ENABLED = False -# Guest tokens (used by the embedded SDK) inherit the "Public" role's perms. -# Out of the box Public has zero perms, so embedded dashboards immediately fail -# their first call (`/api/v1/me/roles/`) with 403. Mirror Public to Gamma — -# the standard read-only viewer role — so the embedded flow can authenticate -# and load dashboard data in local dev. -PUBLIC_ROLE_LIKE = "Gamma" + # Guest tokens (used by the embedded SDK) inherit the "Public" role's perms. + # Out of the box Public has zero perms, so embedded dashboards immediately fail + # their first call (`/api/v1/me/roles/`) with 403. Mirror Public to Gamma — + # the standard read-only viewer role — so the embedded flow can authenticate + # and load dashboard data in local dev. + PUBLIC_ROLE_LIKE = "Gamma" diff --git a/superset-frontend/playwright.config.ts b/superset-frontend/playwright.config.ts index 17d79e40283..c9f4d9bd9d7 100644 --- a/superset-frontend/playwright.config.ts +++ b/superset-frontend/playwright.config.ts @@ -133,22 +133,26 @@ export default defineConfig({ // No storageState = clean browser with no cached cookies }, }, - { - // Embedded dashboard tests - validates the full embedding flow: - // external app -> SDK -> iframe -> guest token -> dashboard render. - // Each spec file mutates per-dashboard embedding state (UUID, - // allowed_domains) on a single shared Superset, so files must not - // run in parallel even if more are added later. - name: 'chromium-embedded', - testMatch: '**/tests/embedded/**/*.spec.ts', - fullyParallel: false, - use: { - browserName: 'chromium', - testIdAttribute: 'data-test', - // Uses admin auth for API calls to configure embedding and get guest tokens - storageState: 'playwright/.auth/user.json', - }, - }, + ...(process.env.INCLUDE_EMBEDDED + ? [ + { + // Embedded dashboard tests - validates the full embedding flow: + // external app -> SDK -> iframe -> guest token -> dashboard render. + // Each spec file mutates per-dashboard embedding state (UUID, + // allowed_domains) on a single shared Superset, so files must not + // run in parallel even if more are added later. + name: 'chromium-embedded', + testMatch: '**/tests/embedded/**/*.spec.ts', + fullyParallel: false, + use: { + browserName: 'chromium' as const, + testIdAttribute: 'data-test', + // Uses admin auth for API calls to configure embedding and get guest tokens + storageState: 'playwright/.auth/user.json', + }, + }, + ] + : []), ], // Web server setup - disabled in CI (Flask started separately in workflow) diff --git a/superset-frontend/playwright/embedded-app/index.html b/superset-frontend/playwright/embedded-app/index.html index ef58a82ded0..2cbec243709 100644 --- a/superset-frontend/playwright/embedded-app/index.html +++ b/superset-frontend/playwright/embedded-app/index.html @@ -52,18 +52,14 @@ under the License. const statusEl = document.getElementById('status'); // fetchGuestToken is injected by Playwright via page.exposeFunction() - // Falls back to window.__guestToken for simple/static token injection async function fetchGuestToken() { - if (typeof window.__fetchGuestToken === 'function') { - statusEl.textContent = 'Fetching guest token...'; - const token = await window.__fetchGuestToken(); - statusEl.textContent = 'Guest token received, loading dashboard...'; - return token; + if (typeof window.__fetchGuestToken !== 'function') { + throw new Error('No guest token source available'); } - if (window.__guestToken) { - return window.__guestToken; - } - throw new Error('No guest token source available'); + statusEl.textContent = 'Fetching guest token...'; + const token = await window.__fetchGuestToken(); + statusEl.textContent = 'Guest token received, loading dashboard...'; + return token; } try { diff --git a/superset-frontend/playwright/helpers/api/embedded.ts b/superset-frontend/playwright/helpers/api/embedded.ts index 48905e14ede..cf389402a0a 100644 --- a/superset-frontend/playwright/helpers/api/embedded.ts +++ b/superset-frontend/playwright/helpers/api/embedded.ts @@ -116,6 +116,7 @@ export async function getGuestToken( // session, JWT, or both. const { token: csrfToken } = await getCsrfToken(page); const guestResponse = await page.request.post(ENDPOINTS.GUEST_TOKEN, { + failOnStatusCode: true, data: { user: { username: 'embedded_test_user', diff --git a/superset-frontend/playwright/tests/embedded/embedded-dashboard.spec.ts b/superset-frontend/playwright/tests/embedded/embedded-dashboard.spec.ts index 568c1d63cc2..9a717c50108 100644 --- a/superset-frontend/playwright/tests/embedded/embedded-dashboard.spec.ts +++ b/superset-frontend/playwright/tests/embedded/embedded-dashboard.spec.ts @@ -215,8 +215,9 @@ test.describe('Embedded Dashboard E2E', () => { try { const setupPage = await context.newPage(); await apiEnableEmbedding(setupPage, dashboardId, []); - } catch { - // Best-effort cleanup — never fail teardown. + } catch (err) { + // eslint-disable-next-line no-console + console.error('[embedded teardown] restore failed:', err); } finally { await context.close(); } @@ -320,7 +321,12 @@ test.describe('Embedded Dashboard E2E', () => { expect(response.status()).toBe(403); } finally { // Restore the open embedding config for other tests in this file. - await apiEnableEmbedding(setupPage, dashboardId, []); + try { + await apiEnableEmbedding(setupPage, dashboardId, []); + } catch (err) { + // eslint-disable-next-line no-console + console.error('[embedded teardown] restore failed:', err); + } await context.close(); } }); diff --git a/tests/integration_tests/superset_test_config.py b/tests/integration_tests/superset_test_config.py index bacc36d9fcb..56ab8ddd194 100644 --- a/tests/integration_tests/superset_test_config.py +++ b/tests/integration_tests/superset_test_config.py @@ -78,15 +78,6 @@ FEATURE_FLAGS = { WEBDRIVER_BASEURL = "http://0.0.0.0:8081/" -# Enable CORS for embedded dashboard E2E tests (test app on port 9000) -ENABLE_CORS = True -CORS_OPTIONS: dict = { - "origins": [ - "http://localhost:9000", - ], - "supports_credentials": True, -} - def GET_FEATURE_FLAGS_FUNC(ff): # noqa: N802 ff_copy = copy(ff)