Compare commits

...

6 Commits

Author SHA1 Message Date
Evan Rusackas
0a9e3a751d feat(docs): tier 2 — dashboard view and save-modal tutorial screenshots (#39466)
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Superset Dev <dev@superset.apache.org>
2026-04-21 17:29:05 -07:00
Claude
ab658ac4b5 fix(docs): address Copilot review feedback on screenshot generator
- Use stable data-test="run-query-action" selector for SQL Lab Run button
- Update docstring port 9000 -> 8088 to match Superset default
- Include 'community' in screenshot-manifest type values comment

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 08:59:44 -07:00
Claude
b34b799d2c fix(docs): add ASF license header to screenshot manifest
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 08:53:53 -07:00
Claude
406358ca3b fix(docs): screenshot quality improvements and chart type picker dataset selection
- Add settle() helper (1s wait) before every screenshot to let ECharts
  animations and lazy-loaded images finish before capture
- Datasets list: switch from table-element screenshot to viewport screenshot
  so the SubMenu (Datasets nav, Bulk Select, + Dataset buttons) is visible
- Chart type picker: select birth_names dataset and Pivot Table chart type
  before screenshotting, showing both wizard steps in a useful state
- Fix strict-mode violations in chart type picker test (step title vs
  placeholder both match "Choose a dataset"; dataset select uses hidden ARIA
  listbox with ID-based option names rather than display labels)
- Regenerate all screenshots with updated tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:26:23 -07:00
Claude
12403b98c0 fix(docs): update screenshot generator tests and regenerate screenshots
- Remove deprecated schema/table selector steps in SQL Lab test (UI changed to tree view)
- Fix datasets table row check to skip hidden ant-table-measure-row
- Fix strict-mode violation in Run button selector (scoped to button role)
- Add baseURL explicitly to generator playwright config project use block
- Regenerate all 6 doc screenshots with latest UI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 09:37:52 -07:00
Claude
df61792f59 feat(docs): add screenshot manifest and expand docs:screenshots generator
Introduces screenshot-manifest.yaml as a living inventory of all images
in the Superset docs, tracking output path, selector, and app_path for
each entry. Entries are removed from the manifest as Playwright tests
are written for them.

Adds two new tutorial screenshot tests to docs-screenshots.spec.ts:
- Datasets list (tutorial_08_sources_tables.png)
- Chart type picker (create_pivot.png)

Also updates DOCS_STATIC/TUTORIAL_DIR path constants and run instructions
to reflect the PLAYWRIGHT_BASE_URL env var override for local Docker use.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 09:12:14 -07:00
14 changed files with 1838 additions and 67 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -25,22 +25,108 @@
*
* Run locally:
* cd superset-frontend
* npm run docs:screenshots
* PLAYWRIGHT_BASE_URL=http://localhost:8088 PLAYWRIGHT_ADMIN_PASSWORD=admin npm run docs:screenshots
*
* Or directly:
* npx playwright test --config=playwright/generators/playwright.config.ts docs/
*
* Screenshots are saved to docs/static/img/screenshots/.
* Screenshots are saved under docs/static/img/.
* As new screenshots are scripted, entries are removed from screenshot-manifest.yaml
* and the output path moves from that manifest into the test below.
*/
import path from 'path';
import { Page } from '@playwright/test';
import { test, expect } from '@playwright/test';
import { URL } from '../../utils/urls';
import { apiDelete, apiGet } from '../../helpers/api/requests';
const SCREENSHOTS_DIR = path.resolve(
__dirname,
'../../../../docs/static/img/screenshots',
);
const DOCS_STATIC = path.resolve(__dirname, '../../../../docs/static/img');
const SCREENSHOTS_DIR = path.join(DOCS_STATIC, 'screenshots');
const TUTORIAL_DIR = path.join(DOCS_STATIC, 'tutorial');
/**
* Waits for animations and async renders to settle before taking a screenshot.
* ECharts entry animations, image lazy-loading, and other async UI updates
* require a short pause that can't be expressed as a deterministic wait condition.
*/
async function settle(page: Page, ms = 1000): Promise<void> {
await page.waitForTimeout(ms);
}
/**
* Navigates to the Sales Dashboard (from example data) and waits for charts
* to finish rendering. Used by several tutorial screenshots that show the
* dashboard in view or edit mode.
*/
async function openSalesDashboard(page: Page): Promise<void> {
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');
const dashboardLink = page.getByRole('link', { name: /sales dashboard/i });
await expect(dashboardLink).toBeVisible({ timeout: 10000 });
await dashboardLink.click();
const dashboardWrapper = page.locator(
'[data-test="dashboard-content-wrapper"]',
);
await expect(dashboardWrapper).toBeVisible({ timeout: 30000 });
await expect(
page.locator('.dashboard-component-chart-holder').first(),
).toBeVisible({ timeout: 15000 });
await expect(
dashboardWrapper.locator('[data-test="loading-indicator"]'),
).toHaveCount(0, { timeout: 30000 });
await expect(
page.locator('.dashboard-component-chart-holder canvas').first(),
).toBeVisible({ timeout: 15000 });
}
/**
* Delete all dashboards matching the given exact title, along with the
* charts attached to them. Used by the save-flow test to clean up after
* itself and to recover from prior failed runs (idempotent pre-cleanup).
*
* Only safe because the title is unique to the test ("Superset Duper
* Sales Dashboard"); don't reuse this against titles that could match
* example-data dashboards.
*/
async function deleteDashboardByTitle(
page: Page,
title: string,
): Promise<void> {
const filter = `(filters:!((col:dashboard_title,opr:eq,value:'${title}')))`;
const resp = await apiGet(page, 'api/v1/dashboard/', {
params: { q: filter },
failOnStatusCode: false,
});
if (!resp.ok()) return;
const body = await resp.json();
const dashboards: { id: number }[] = body.result || [];
for (const dash of dashboards) {
const chartsResp = await apiGet(
page,
`api/v1/dashboard/${dash.id}/charts`,
{ failOnStatusCode: false },
);
const chartIds: number[] = chartsResp.ok()
? ((await chartsResp.json()).result || [])
.map((c: { id?: number }) => c.id)
.filter((id: unknown): id is number => typeof id === 'number')
: [];
await apiDelete(page, `api/v1/dashboard/${dash.id}`, {
failOnStatusCode: false,
});
for (const id of chartIds) {
await apiDelete(page, `api/v1/chart/${id}`, { failOnStatusCode: false });
}
}
}
test('chart gallery screenshot', async ({ page }) => {
await page.goto(URL.CHART_ADD);
@@ -58,6 +144,7 @@ test('chart gallery screenshot', async ({ page }) => {
vizGallery.locator('[data-test="viztype-selector-container"]').first(),
).toBeVisible();
await settle(page);
await vizGallery.screenshot({
path: path.join(SCREENSHOTS_DIR, 'gallery.jpg'),
type: 'jpeg',
@@ -65,36 +152,7 @@ test('chart gallery screenshot', async ({ page }) => {
});
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 });
await openSalesDashboard(page);
// Open the filter bar (collapsed by default)
const expandButton = page.locator('[data-test="filter-bar__expand-button"]');
@@ -109,6 +167,8 @@ test('dashboard screenshot', async ({ page }) => {
).toBeVisible({ timeout: 5000 });
}
// Allow ECharts entry animations to finish before capturing
await settle(page);
await page.screenshot({
path: path.join(SCREENSHOTS_DIR, 'dashboard.jpg'),
type: 'jpeg',
@@ -143,6 +203,7 @@ test('chart editor screenshot', async ({ page }) => {
timeout: 15000,
});
await settle(page);
await page.screenshot({
path: path.join(SCREENSHOTS_DIR, 'explore.jpg'),
type: 'jpeg',
@@ -151,7 +212,7 @@ test('chart editor screenshot', async ({ page }) => {
});
test('SQL Lab screenshot', async ({ page }) => {
// SQL Lab has many interactive steps (schema, table, query, results) — allow extra time
// SQL Lab has many interactive steps — allow extra time
test.setTimeout(90000);
await page.goto(URL.SQLLAB);
@@ -168,34 +229,7 @@ test('SQL Lab screenshot', async ({ page }) => {
}
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();
// Click the active query tab to ensure focus is on the editor pane
await page.getByText('Untitled Query').first().click();
// Write a multi-line SELECT with explicit columns to fill the editor
@@ -205,8 +239,8 @@ test('SQL Lab screenshot', async ({ page }) => {
'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 });
// Run the query — use the stable data-test attribute on the action button
const runButton = page.locator('[data-test="run-query-action"]');
await expect(runButton).toBeVisible();
await runButton.click();
@@ -222,9 +256,352 @@ test('SQL Lab screenshot', async ({ page }) => {
await page.mouse.move(0, 0);
await expect(page.getByRole('tooltip')).toHaveCount(0, { timeout: 2000 });
await settle(page);
await page.screenshot({
path: path.join(SCREENSHOTS_DIR, 'sql_lab.jpg'),
type: 'jpeg',
fullPage: true,
});
});
// ---------------------------------------------------------------------------
// Tutorial screenshots
// ---------------------------------------------------------------------------
test('datasets list screenshot', async ({ page }) => {
await page.goto(URL.DATASET_LIST);
const table = page.locator('[data-test="listview-table"]');
await expect(table).toBeVisible({ timeout: 15000 });
// Wait for at least one visible data row (skip ant-table-measure-row which is always hidden)
await expect(
table.locator('tbody tr:not(.ant-table-measure-row)').first(),
).toBeVisible({ timeout: 10000 });
// Viewport screenshot (not fullPage) captures the SubMenu — showing the
// "Datasets" nav item, Bulk Select button, and + Dataset button — plus the
// top of the table. This is more informative than screenshotting the table alone.
await settle(page);
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_08_sources_tables.png'),
type: 'png',
});
});
test('chart type picker screenshot', async ({ page }) => {
await page.goto(URL.CHART_ADD);
// Wait for the dataset step to appear (step title is first match; placeholder is second)
await expect(page.getByText('Choose a dataset').first()).toBeVisible({
timeout: 15000,
});
// Open the dataset selector and choose birth_names
await page.getByTestId('Dataset').click();
await page.keyboard.type('birth_names');
// The dataset select uses a hidden ARIA listbox — the visible popup is a portal.
// Wait for the first option to appear in the DOM, then select it via keyboard.
await expect(
page.locator('[role="listbox"] [role="option"]').first(),
).toBeAttached({ timeout: 10000 });
await page.keyboard.press('ArrowDown');
await page.keyboard.press('Enter');
// Open the chart gallery and wait for thumbnails to render
await expect(page.getByText('Choose chart type')).toBeVisible({
timeout: 10000,
});
await page.getByRole('tab', { name: 'All charts' }).click();
const vizGallery = page.locator('.viz-gallery');
await expect(vizGallery).toBeVisible();
await expect(
vizGallery.locator('[data-test="viztype-selector-container"]').first(),
).toBeVisible();
// Select the Pivot Table chart type
await vizGallery
.locator('[data-test="viztype-selector-container"]')
.filter({ hasText: 'Pivot Table' })
.first()
.click();
// Allow thumbnails to finish loading and selection state to render
await settle(page);
// Viewport screenshot shows the dataset step (birth_names selected) and
// the chart type gallery (Pivot Table highlighted)
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'create_pivot.png'),
type: 'png',
});
});
test('publish button dashboard screenshot', async ({ page }) => {
// Toggle Sales Dashboard to Draft, hover the label so the tooltip renders,
// then capture the header area plus enough room below for the tooltip.
// Always restores the dashboard to Published at the end.
await openSalesDashboard(page);
const publishedLabel = page.getByText('Published', { exact: true }).first();
await expect(publishedLabel).toBeVisible({ timeout: 10000 });
await publishedLabel.click();
const draftLabel = page.getByText('Draft', { exact: true }).first();
await expect(draftLabel).toBeVisible({ timeout: 10000 });
try {
await draftLabel.hover();
await expect(page.getByRole('tooltip')).toBeVisible({ timeout: 5000 });
await settle(page, 500);
const headerBox = await page
.locator('[data-test="dashboard-header-container"]')
.boundingBox();
if (!headerBox) {
throw new Error('Could not locate dashboard header container');
}
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'publish_button_dashboard.png'),
type: 'png',
clip: {
x: headerBox.x,
y: headerBox.y,
width: headerBox.width,
height: headerBox.height + 140,
},
});
} finally {
// Restore: click Draft to re-publish so other runs start from a clean state
await page.mouse.move(0, 0);
await draftLabel.click();
await expect(
page.getByText('Published', { exact: true }).first(),
).toBeVisible({ timeout: 10000 });
}
});
test('edit button screenshot', async ({ page }) => {
// Capture the right-side action buttons (Edit dashboard + "..." menu)
// rather than the edit button in isolation.
await openSalesDashboard(page);
await settle(page);
const rightPanel = page.locator('.right-button-panel');
await expect(rightPanel).toBeVisible({ timeout: 5000 });
await rightPanel.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_edit_button.png'),
type: 'png',
});
});
test('chart resize screenshot', async ({ page }) => {
// Enter edit mode, start a resize drag on the right-edge handle, then
// screenshot the chart mid-drag. While `DashboardGrid` is in the resizing
// state it renders vertical `grid-column-guide` overlays across the grid
// and the chart gets a blue `--resizing` outline — that's the state the
// original tutorial screenshot was capturing.
await openSalesDashboard(page);
const editButton = page.locator('[data-test="edit-dashboard-button"]');
await expect(editButton).toBeVisible();
await editButton.click();
await expect(
page.locator('[data-test="dashboard-builder-sidepane"]'),
).toBeVisible({ timeout: 10000 });
const chart = page.locator('.dashboard-component-chart-holder').first();
await expect(chart).toBeVisible();
const chartBox = await chart.boundingBox();
if (!chartBox) {
throw new Error('Could not locate chart bounding box');
}
// Hover over the chart so the on-hover action buttons (drag/trash/settings)
// and resize handles become visible.
await page.mouse.move(
chartBox.x + chartBox.width / 2,
chartBox.y + chartBox.height / 2,
);
await settle(page, 200);
// The right-edge handle is a `<span>` added by re-resizable with our
// custom class. Locating it by class is more reliable than computing
// coordinates from the chart-holder (which isn't the full resizable box).
const rightHandle = page
.locator('.resizable-container-handle--right')
.first();
await expect(rightHandle).toBeVisible();
const handleBox = await rightHandle.boundingBox();
if (!handleBox) {
throw new Error('Could not locate right-edge resize handle');
}
const handleX = handleBox.x + handleBox.width / 2;
const handleY = handleBox.y + handleBox.height / 2;
await page.mouse.move(handleX, handleY);
await page.mouse.down();
// Move far enough to snap at least one grid column, which puts
// DashboardGrid into isResizing=true so the column guides render.
await page.mouse.move(handleX + 80, handleY, { steps: 10 });
await settle(page, 500);
// Clip to the chart area plus a left gutter for the hover action rail
// and right padding that reaches past the dragged handle position.
const leftGutter = 32;
const rightPadding = 100;
const topPadding = 16;
const bottomPadding = 24;
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_chart_resize.png'),
type: 'png',
clip: {
x: Math.max(0, chartBox.x - leftGutter),
y: Math.max(0, chartBox.y - topPadding),
width: chartBox.width + leftGutter + rightPadding,
height: chartBox.height + topPadding + bottomPadding,
},
});
// Release back at the start to avoid persisting a size change. Edit-mode
// changes aren't saved (we never click the dashboard Save button).
await page.mouse.move(handleX, handleY, { steps: 6 });
await page.mouse.up();
});
test('save flow and first dashboard screenshots', async ({ page }) => {
// Captures two linked tutorial screenshots in a single flow so the second
// faithfully shows the dashboard the user just created:
// 1. tutorial_save_slice.png — Save modal with the "Add to dashboard"
// dropdown surfacing a creatable option for a new dashboard.
// 2. tutorial_first_dashboard.png — the freshly-created dashboard with
// the single saved chart (matches the tutorial narrative).
//
// Creates and then deletes a "Superset Duper Sales Dashboard" dashboard
// plus the duplicate chart it owns. Pre-cleans in case a prior run failed.
const NEW_DASHBOARD_NAME = 'Superset Duper Sales Dashboard';
await deleteDashboardByTitle(page, NEW_DASHBOARD_NAME);
// 1100px is wide enough to show the full "Superset Duper Sales Dashboard"
// title alongside the header actions without truncation.
await page.setViewportSize({ width: 1100, height: 800 });
await page.goto(URL.CHART_LIST);
const searchInput = page.getByPlaceholder('Type a value');
await expect(searchInput).toBeVisible({ timeout: 15000 });
await searchInput.fill('Scatter Plot');
await searchInput.press('Enter');
const chartLink = page.getByRole('link', { name: /scatter plot/i });
await expect(chartLink).toBeVisible({ timeout: 10000 });
await chartLink.click();
await page.waitForURL('**/explore/**', { timeout: 15000 });
const sliceContainer = page.locator('[data-test="slice-container"]');
await expect(sliceContainer).toBeVisible({ timeout: 15000 });
await expect(
sliceContainer.locator('[data-test="loading-indicator"]'),
).toHaveCount(0, { timeout: 15000 });
const saveButton = page.locator('[data-test="query-save-button"]');
await expect(saveButton).toBeVisible({ timeout: 10000 });
await saveButton.click();
const modal = page.locator('.ant-modal-content').filter({
has: page.locator('[data-test="save-modal-body"]'),
});
await expect(modal).toBeVisible({ timeout: 10000 });
// Open the "Add to dashboard" select and type a new dashboard name so
// the dropdown surfaces the creatable option.
const dashboardSelect = page.getByRole('combobox', {
name: /select a dashboard/i,
});
await dashboardSelect.click();
await page.keyboard.type(NEW_DASHBOARD_NAME);
// Ant Design portals the visible dropdown with the class
// `.ant-select-item-option` on each option (distinct from the hidden
// ARIA listbox options rendered inside the combobox itself).
const createOption = page
.locator('.ant-select-item-option')
.filter({ hasText: NEW_DASHBOARD_NAME });
await expect(createOption).toBeVisible({ timeout: 10000 });
await settle(page);
try {
// Screenshot 1: save modal + portaled dropdown.
const modalBox = await modal.boundingBox();
const optionBox = await createOption.boundingBox();
if (!modalBox || !optionBox) {
throw new Error('Could not locate save modal or create-option');
}
const padding = 16;
const top = Math.max(0, modalBox.y - padding);
const bottom = optionBox.y + optionBox.height + padding;
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_save_slice.png'),
type: 'png',
clip: {
x: Math.max(0, modalBox.x - padding),
y: top,
width: modalBox.width + padding * 2,
height: bottom - top,
},
});
// Pick the creatable option, then click "Save & go to dashboard" so the
// backend creates the dashboard + slice and redirects us to the new one.
await createOption.click();
const saveAndGotoBtn = page.locator('#btn_modal_save_goto_dash');
await expect(saveAndGotoBtn).toBeEnabled({ timeout: 5000 });
await saveAndGotoBtn.click();
await page.waitForURL(/\/dashboard\/[^/]+\/?/, { timeout: 30000 });
await expect(
page.locator('[data-test="dashboard-content-wrapper"]'),
).toBeVisible({ timeout: 30000 });
await expect(
page.locator('.dashboard-component-chart-holder').first(),
).toBeVisible({ timeout: 30000 });
await expect(
page.locator('.dashboard-component-chart-holder canvas').first(),
).toBeVisible({ timeout: 15000 });
// Dismiss the "Chart [X] has been saved" toast so it doesn't appear in
// the screenshot. The close button is inside the toast container.
const toast = page.locator('[data-test="toast-container"]').first();
if (await toast.isVisible().catch(() => false)) {
await toast.locator('.toast__close').click();
await expect(toast).toBeHidden({ timeout: 5000 });
}
await settle(page);
// Screenshot 2: the newly-created single-chart dashboard (title + chart).
const headerBox = await page
.locator('[data-test="dashboard-header-wrapper"]')
.boundingBox();
const chartBox = await page
.locator('.dashboard-component-chart-holder')
.first()
.boundingBox();
if (!headerBox || !chartBox) {
throw new Error('Could not locate dashboard header or chart');
}
// Trim right edge to just past the chart so the screenshot isn't padded
// with empty grid space.
const rightPadding = 16;
await page.screenshot({
path: path.join(TUTORIAL_DIR, 'tutorial_first_dashboard.png'),
type: 'png',
clip: {
x: 0,
y: headerBox.y,
width: Math.min(1100, chartBox.x + chartBox.width + rightPadding),
height: chartBox.y + chartBox.height - headerBox.y + 16,
},
});
} finally {
await deleteDashboardByTitle(page, NEW_DASHBOARD_NAME);
}
});

File diff suppressed because it is too large Load Diff

View File

@@ -64,6 +64,7 @@ export default defineConfig({
name: 'docs-generators',
use: {
browserName: 'chromium',
baseURL, // explicit here so globalSetup can read it from config.projects[0].use.baseURL
testIdAttribute: 'data-test',
storageState: path.resolve(__dirname, '../.auth/user.json'),
},