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;