feat(playwright): add documentation screenshot generator (#37494)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Evan Rusackas
2026-01-29 10:14:53 -08:00
committed by GitHub
parent 5a99588f57
commit 3ef33dcb76
9 changed files with 317 additions and 2 deletions

View File

@@ -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**

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 636 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 943 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 KiB

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -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",

View File

@@ -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,
});
});

View 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,
},
});

View File

@@ -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;