feat(playwright): add documentation screenshot generator (#37494)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
@@ -89,7 +89,7 @@ Superset provides:
|
||||
|
||||
**Craft Beautiful, Dynamic Dashboards**
|
||||
|
||||
<kbd><img title="View Dashboards" src="https://superset.apache.org/img/screenshots/slack_dash.jpg"/></kbd><br/>
|
||||
<kbd><img title="View Dashboards" src="https://superset.apache.org/img/screenshots/dashboard.jpg"/></kbd><br/>
|
||||
|
||||
**No-Code Chart Builder**
|
||||
|
||||
|
||||
BIN
docs/static/img/screenshots/dashboard.jpg
vendored
Normal file
|
After Width: | Height: | Size: 134 KiB |
BIN
docs/static/img/screenshots/explore.jpg
vendored
|
Before Width: | Height: | Size: 636 KiB After Width: | Height: | Size: 104 KiB |
BIN
docs/static/img/screenshots/gallery.jpg
vendored
|
Before Width: | Height: | Size: 943 KiB After Width: | Height: | Size: 118 KiB |
BIN
docs/static/img/screenshots/sql_lab.jpg
vendored
|
Before Width: | Height: | Size: 444 KiB After Width: | Height: | Size: 99 KiB |
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
81
superset-frontend/playwright/generators/playwright.config.ts
Normal file
@@ -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
|
||||
*/
|
||||
|
||||
/// <reference types="node" />
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
@@ -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;
|
||||
|
||||