This is an automated email from the ASF dual-hosted git repository. hainenber pushed a commit to branch feat/generate-playwright-runnable-tests-from-storybook-stories in repository https://gitbox.apache.org/repos/asf/superset.git
commit cfbff8888e5f0afc2c5445c0a89aaf2ed43008c0 Author: hainenber <[email protected]> AuthorDate: Sun Oct 4 16:03:42 2026 +0700 feat(build): generate Playwright-runnable tests from Storybook stories Signed-off-by: hainenber <[email protected]> --- .../.storybook/playwright.storybook-ci.config.ts | 36 ++++++ superset-frontend/.storybook/setup-stories.ts | 143 +++++++++++++++++++++ superset-frontend/.storybook/stories.spec.ts | 26 ++++ 3 files changed, 205 insertions(+) diff --git a/superset-frontend/.storybook/playwright.storybook-ci.config.ts b/superset-frontend/.storybook/playwright.storybook-ci.config.ts new file mode 100644 index 00000000000..e8c0b25fbb4 --- /dev/null +++ b/superset-frontend/.storybook/playwright.storybook-ci.config.ts @@ -0,0 +1,36 @@ +/** + * 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. + */ + +// Source: https://gist.github.com/AriPerkkio/99b9eedc7d8f71ff6e6770f9425a4be4 +import { defineConfig } from '@playwright/test'; + +const storybookUrl = process.env.STORYBOOK_URL ?? 'http://localhost:6006'; +export default defineConfig({ + testDir: '.', + globalSetup: 'setup-stories.ts', + use: { baseURL: storybookUrl }, + fullyParallel: true, + webServer: process.env.STORYBOOK_URL + ? undefined + : { + command: 'npm run storybook -- --ci', + url: `${storybookUrl}/index.json`, + reuseExistingServer: true, + }, +}); diff --git a/superset-frontend/.storybook/setup-stories.ts b/superset-frontend/.storybook/setup-stories.ts new file mode 100644 index 00000000000..909c0675e40 --- /dev/null +++ b/superset-frontend/.storybook/setup-stories.ts @@ -0,0 +1,143 @@ +/** + * 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. + */ + +// Source: https://gist.github.com/AriPerkkio/99b9eedc7d8f71ff6e6770f9425a4be4 +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { type test, type Page } from '@playwright/test'; + +declare global { + var __storyResult: { status: string; errors: string[] }; +} + +interface Entry { + id: string; + title: string; + name: string; + type: 'story' | 'docs'; + tags?: string[]; +} + +interface Report { + type: string; + status: string; + result?: { violations?: { id: string; help: string }[] }; +} + +const storybookURL = process.env.STORYBOOK_URL ?? 'http://localhost:6006'; +const indexFile = resolve( + '../node_modules/.cache/storybook-playwright/index.json', +); + +export default async function globalSetup() { + const response = await fetch(`${storybookURL}/index.json`); + + if (!response.ok) { + throw new Error( + `Failed to fetch ${storybookURL}/index.json: ${response.status}`, + ); + } + + mkdirSync(dirname(indexFile), { recursive: true }); + writeFileSync(indexFile, await response.text()); +} + +function captureStoryResult() { + const errors: string[] = []; + const serialize = (error: Error) => + error.stack ?? `${error.name}: ${error.message}`; + + let channel: any; + Object.defineProperty(globalThis, '__STORYBOOK_ADDONS_CHANNEL__', { + configurable: true, + get: () => channel, + set: value => { + channel = value; + channel.on( + 'storyErrored', + ({ title, description }: Record<string, string>) => + errors.push(`${title}\n${description}`), + ); + channel.on('storyThrewException', (error: Error) => + errors.push(serialize(error)), + ); + channel.on('playFunctionThrewException', (error: Error) => + errors.push(serialize(error)), + ); + channel.on('unhandledErrorsWhilePlaying', (unhandled: Error[]) => + errors.push(...unhandled.map(serialize)), + ); + channel.on('storyMissing', (id: string) => { + globalThis.__storyResult = { + status: 'missing', + errors: [`Story "${id}" not found`], + }; + }); + channel.on( + 'storyFinished', + ({ status, reporters }: { status: string; reporters: Report[] }) => { + for (const report of reporters.filter( + report => report.status === 'failed', + )) { + const violations = report.result?.violations ?? []; + errors.push( + [ + `${report.type} report failed`, + ...violations.map(v => `- ${v.id}: ${v.help}`), + ].join('\n'), + ); + } + globalThis.__storyResult = { status, errors }; + }, + ); + }, + }); +} + +async function visitStory(page: Page, entry: Entry) { + await page.addInitScript(captureStoryResult); + await page.goto(`/iframe.html?id=${entry.id}&viewMode=story`); + + const handle = await page.waitForFunction(() => globalThis.__storyResult); + const result = await handle.jsonValue(); + + if (result.status !== 'success' || result.errors.length > 0) { + throw new Error( + [ + `Story "${entry.title} › ${entry.name}" ${result.status === 'missing' ? 'is missing' : 'failed'}.`, + `Open it in Storybook: ${storybookURL}/?path=/story/${entry.id}`, + '', + ...result.errors, + ].join('\n'), + ); + } +} + +export function loadStories(): [string, Parameters<typeof test>[2]][] { + const index = JSON.parse(readFileSync(indexFile, 'utf8')) as { + entries: Record<string, Entry>; + }; + + return Object.values(index.entries) + .filter(entry => entry.type === 'story' && entry.tags?.includes('test')) + .map(entry => [ + `${entry.title} › ${entry.name}`, + async ({ page }) => visitStory(page, entry), + ]); +} diff --git a/superset-frontend/.storybook/stories.spec.ts b/superset-frontend/.storybook/stories.spec.ts new file mode 100644 index 00000000000..16631a3a77c --- /dev/null +++ b/superset-frontend/.storybook/stories.spec.ts @@ -0,0 +1,26 @@ +/** + * 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. + */ + +// Source: https://gist.github.com/AriPerkkio/99b9eedc7d8f71ff6e6770f9425a4be4 +import { test } from '@playwright/test'; +import { loadStories } from './setup-stories'; + +for (const [title, body] of loadStories()) { + test(title, body); +}
