diff --git a/README.md b/README.md index 617852c1908..e6dd708623d 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ Superset provides: **Craft Beautiful, Dynamic Dashboards** -
+
**No-Code Chart Builder** diff --git a/docs/static/img/screenshots/dashboard.jpg b/docs/static/img/screenshots/dashboard.jpg new file mode 100644 index 00000000000..9062d7a479d Binary files /dev/null and b/docs/static/img/screenshots/dashboard.jpg differ diff --git a/docs/static/img/screenshots/explore.jpg b/docs/static/img/screenshots/explore.jpg index cf2b242c35b..8ac41b65796 100644 Binary files a/docs/static/img/screenshots/explore.jpg and b/docs/static/img/screenshots/explore.jpg differ diff --git a/docs/static/img/screenshots/gallery.jpg b/docs/static/img/screenshots/gallery.jpg index 345302de689..2312bd37982 100644 Binary files a/docs/static/img/screenshots/gallery.jpg and b/docs/static/img/screenshots/gallery.jpg differ diff --git a/docs/static/img/screenshots/sql_lab.jpg b/docs/static/img/screenshots/sql_lab.jpg index b66bcc52b7f..d8f5a964adb 100644 Binary files a/docs/static/img/screenshots/sql_lab.jpg and b/docs/static/img/screenshots/sql_lab.jpg differ diff --git a/superset-frontend/package.json b/superset-frontend/package.json index c7e2a5f04a3..a20f29f344a 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -74,6 +74,7 @@ "playwright:headed": "playwright test --headed", "playwright:debug": "playwright test --debug", "playwright:report": "playwright show-report", + "docs:screenshots": "playwright test --config=playwright/generators/playwright.config.ts docs/", "prettier": "npm run _prettier -- --write", "prettier-check": "npm run _prettier -- --check", "prod": "npm run build", diff --git a/superset-frontend/playwright/generators/docs/docs-screenshots.spec.ts b/superset-frontend/playwright/generators/docs/docs-screenshots.spec.ts new file mode 100644 index 00000000000..5e3814b849e --- /dev/null +++ b/superset-frontend/playwright/generators/docs/docs-screenshots.spec.ts @@ -0,0 +1,230 @@ +/** + * 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. + */ + +/** + * Documentation Screenshot Generator + * + * Captures screenshots for the Superset documentation site and README. + * Depends on example data loaded via `superset load_examples`. + * + * Run locally: + * cd superset-frontend + * npm run docs:screenshots + * + * Or directly: + * npx playwright test --config=playwright/generators/playwright.config.ts docs/ + * + * Screenshots are saved to docs/static/img/screenshots/. + */ + +import path from 'path'; +import { test, expect } from '@playwright/test'; +import { URL } from '../../utils/urls'; + +const SCREENSHOTS_DIR = path.resolve( + __dirname, + '../../../../docs/static/img/screenshots', +); + +test('chart gallery screenshot', async ({ page }) => { + await page.goto(URL.CHART_ADD); + + // Wait for chart creation page to load + await expect(page.getByText('Choose chart type')).toBeVisible({ + timeout: 15000, + }); + await page.getByRole('tab', { name: 'All charts' }).click(); + + // Wait for viz gallery to render chart type thumbnails + const vizGallery = page.locator('.viz-gallery'); + await expect(vizGallery).toBeVisible(); + await expect( + vizGallery.locator('[data-test="viztype-selector-container"]').first(), + ).toBeVisible(); + + await vizGallery.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'gallery.jpg'), + type: 'jpeg', + }); +}); + +test('dashboard screenshot', async ({ page }) => { + // Navigate to Sales Dashboard via the dashboard list (slug is null) + await page.goto(URL.DASHBOARD_LIST); + const searchInput = page.getByPlaceholder('Type a value'); + await expect(searchInput).toBeVisible({ timeout: 15000 }); + await searchInput.fill('Sales Dashboard'); + await searchInput.press('Enter'); + + // Click the Sales Dashboard link + const dashboardLink = page.getByRole('link', { name: /sales dashboard/i }); + await expect(dashboardLink).toBeVisible({ timeout: 10000 }); + await dashboardLink.click(); + + // Wait for dashboard to fully render + const dashboardWrapper = page.locator( + '[data-test="dashboard-content-wrapper"]', + ); + await expect(dashboardWrapper).toBeVisible({ timeout: 30000 }); + + // Wait for chart holders to appear, then wait for all loading spinners to clear + await expect( + page.locator('.dashboard-component-chart-holder').first(), + ).toBeVisible({ timeout: 15000 }); + await expect( + dashboardWrapper.locator('[data-test="loading-indicator"]'), + ).toHaveCount(0, { timeout: 30000 }); + + // Wait for at least one chart to finish rendering (ECharts renders to canvas) + await expect( + page.locator('.dashboard-component-chart-holder canvas').first(), + ).toBeVisible({ timeout: 15000 }); + + // Open the filter bar (collapsed by default) + const expandButton = page.locator('[data-test="filter-bar__expand-button"]'); + if (await expandButton.isVisible()) { + await expandButton.click(); + // Wait for filter bar content to expand and render filter controls + await expect( + page.locator('[data-test="filter-bar__collapsable"]'), + ).toBeVisible({ timeout: 5000 }); + await expect( + page.locator('[data-test="filterbar-action-buttons"]'), + ).toBeVisible({ timeout: 5000 }); + } + + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'dashboard.jpg'), + type: 'jpeg', + fullPage: true, + }); +}); + +test('chart editor screenshot', async ({ page }) => { + await page.goto(URL.CHART_LIST); + + // Search for the Scatter Plot chart by name + const searchInput = page.getByPlaceholder('Type a value'); + await expect(searchInput).toBeVisible({ timeout: 15000 }); + await searchInput.fill('Scatter Plot'); + await searchInput.press('Enter'); + + // Click the Scatter Plot link to open explore + const chartLink = page.getByRole('link', { name: /scatter plot/i }); + await expect(chartLink).toBeVisible({ timeout: 10000 }); + await chartLink.click(); + + // Wait for explore page to fully load + await page.waitForURL('**/explore/**', { timeout: 15000 }); + const sliceContainer = page.locator('[data-test="slice-container"]'); + await expect(sliceContainer).toBeVisible({ timeout: 15000 }); + + // Wait for the chart to finish rendering (loading spinners clear, chart content appears) + await expect( + sliceContainer.locator('[data-test="loading-indicator"]'), + ).toHaveCount(0, { timeout: 15000 }); + await expect(sliceContainer.locator('canvas, svg').first()).toBeVisible({ + timeout: 15000, + }); + + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'explore.jpg'), + type: 'jpeg', + fullPage: true, + }); +}); + +test('SQL Lab screenshot', async ({ page }) => { + // SQL Lab has many interactive steps (schema, table, query, results) — allow extra time + test.setTimeout(90000); + await page.goto(URL.SQLLAB); + + // SQL Lab may open with no active query tab — create one if needed + const addTabButton = page.getByRole('tab', { name: /add a new tab/i }); + const aceEditor = page.locator('.ace_content'); + + // Wait for either the editor or the "add tab" prompt + await expect(addTabButton.or(aceEditor)).toBeVisible({ timeout: 15000 }); + + // If no editor is visible, click "Add a new tab" to create a query tab + if (await addTabButton.isVisible()) { + await addTabButton.click(); + } + await expect(aceEditor).toBeVisible({ timeout: 15000 }); + + // Select the "public" schema so we can pick a table from the left panel + const schemaSelect = page.locator('#select-schema'); + await expect(schemaSelect).toBeEnabled({ timeout: 10000 }); + await schemaSelect.click({ force: true }); + await schemaSelect.fill('public'); + await page.getByRole('option', { name: 'public' }).click(); + + // Wait for table list to load after schema change, then select birth_names + const tableSelectWrapper = page + .locator('.ant-select') + .filter({ has: page.locator('#select-table') }); + await expect(tableSelectWrapper).toBeVisible({ timeout: 10000 }); + await tableSelectWrapper.click(); + await page.keyboard.type('birth_names'); + // Wait for the filtered option to appear in the DOM, then select it + const tableOption = page + .locator('.ant-select-dropdown [role="option"]') + .filter({ hasText: 'birth_names' }); + await expect(tableOption).toBeAttached({ timeout: 10000 }); + await page.keyboard.press('Enter'); + + // Wait for table schema to load and show columns in the left panel + await expect(page.locator('[data-test="col-name"]').first()).toBeVisible({ + timeout: 15000, + }); + + // Close the table dropdown by clicking elsewhere, then switch to the query tab + await page.locator('[data-test="sql-editor-tabs"]').first().click(); + await page.getByText('Untitled Query').first().click(); + + // Write a multi-line SELECT with explicit columns to fill the editor + await aceEditor.click(); + const editor = page.getByRole('textbox', { name: /cursor/i }); + await editor.fill( + 'SELECT\n ds,\n name,\n gender,\n state,\n num\nFROM birth_names\nLIMIT 100', + ); + + // Run the query + const runButton = page.getByText('Run', { exact: true }); + await expect(runButton).toBeVisible(); + await runButton.click(); + + // Wait for results to appear (look for the "N rows" badge in the results panel) + await expect(page.getByText(/\d+ rows/)).toBeVisible({ + timeout: 30000, + }); + + // Switch to the Results tab to show the query output + await page.getByRole('tab', { name: 'Results' }).click(); + + // Move mouse away from buttons to dismiss any tooltips, then wait for them to disappear + await page.mouse.move(0, 0); + await expect(page.getByRole('tooltip')).toHaveCount(0, { timeout: 2000 }); + + await page.screenshot({ + path: path.join(SCREENSHOTS_DIR, 'sql_lab.jpg'), + type: 'jpeg', + fullPage: true, + }); +}); diff --git a/superset-frontend/playwright/generators/playwright.config.ts b/superset-frontend/playwright/generators/playwright.config.ts new file mode 100644 index 00000000000..54b8b4be702 --- /dev/null +++ b/superset-frontend/playwright/generators/playwright.config.ts @@ -0,0 +1,81 @@ +/** + * 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. + */ + +/** + * Playwright config for documentation generators (screenshots, etc.) + * + * Separate from the main test config so generators are never picked up + * by CI test sweeps. Run via: + * npm run docs:screenshots + */ + +/// + +import path from 'path'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { defineConfig } from '@playwright/test'; + +const serverURL = process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8088'; +const baseURL = serverURL.endsWith('/') ? serverURL : `${serverURL}/`; + +export default defineConfig({ + testDir: '.', + + globalSetup: '../global-setup.ts', + + timeout: 90000, + expect: { timeout: 30000 }, + + fullyParallel: false, + workers: 1, + retries: 0, + + reporter: [['list']], + + use: { + baseURL, + + headless: true, + viewport: { width: 1280, height: 1024 }, + + screenshot: 'off', + video: 'off', + trace: 'off', + }, + + projects: [ + { + name: 'docs-generators', + use: { + browserName: 'chromium', + testIdAttribute: 'data-test', + storageState: path.resolve(__dirname, '../.auth/user.json'), + }, + }, + ], + + webServer: process.env.CI + ? undefined + : { + command: `curl -f ${serverURL}/health`, + url: `${serverURL}/health`, + reuseExistingServer: true, + timeout: 5000, + }, +}); diff --git a/superset-frontend/playwright/utils/urls.ts b/superset-frontend/playwright/utils/urls.ts index d83e33f755f..fa5be71c8f1 100644 --- a/superset-frontend/playwright/utils/urls.ts +++ b/superset-frontend/playwright/utils/urls.ts @@ -28,8 +28,11 @@ * = 'http://localhost:8088/app/prefix/tablemodelview/list' */ export const URL = { - DATASET_LIST: 'tablemodelview/list', + CHART_ADD: 'chart/add', + CHART_LIST: 'chart/list/', DASHBOARD_LIST: 'dashboard/list/', + DATASET_LIST: 'tablemodelview/list', LOGIN: 'login/', + SQLLAB: 'sqllab', WELCOME: 'superset/welcome/', } as const;