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);
+}

Reply via email to