From ca63dada0f0c526aff2c189ab65f2d4ca7ca0d35 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Tue, 17 Feb 2026 12:27:40 -0600 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20Fix=20storybook=20story=20UR?= =?UTF-8?q?L=20capture=20and=20add=20preview=20upload?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/sdk-e2e.yml | 6 +++ clients/storybook/src/screenshot.js | 16 +++++++- clients/storybook/tests/screenshot.test.js | 44 ++++++++++++++++++++-- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sdk-e2e.yml b/.github/workflows/sdk-e2e.yml index c042019..1f223d3 100644 --- a/.github/workflows/sdk-e2e.yml +++ b/.github/workflows/sdk-e2e.yml @@ -187,6 +187,12 @@ jobs: VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }} VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }} + - name: Upload preview + working-directory: ./clients/storybook + run: ../../bin/vizzly.js preview example-storybook/dist + env: + VIZZLY_TOKEN: ${{ secrets.VIZZLY_STORYBOOK_CLIENT_TOKEN }} + # Static-Site SDK static-site: name: Static-Site SDK diff --git a/clients/storybook/src/screenshot.js b/clients/storybook/src/screenshot.js index a2880e9..5a482c5 100644 --- a/clients/storybook/src/screenshot.js +++ b/clients/storybook/src/screenshot.js @@ -60,6 +60,18 @@ export async function captureScreenshot(page, options = {}) { return screenshot; } +/** + * Convert an iframe.html URL to a Storybook story URL + * iframe.html URLs don't render in iframes on the dashboard — the story path format does + * @param {string} pageUrl - Current page URL (likely iframe.html?id=...&viewMode=story) + * @param {string} storyId - Story ID + * @returns {string} Storybook story URL (?path=/story/...) + */ +export function toStoryUrl(pageUrl, storyId) { + let url = new URL(pageUrl); + return `${url.origin}/?path=/story/${storyId}`; +} + /** * Capture and send screenshot to Vizzly * @param {Object} page - Playwright page instance @@ -81,8 +93,10 @@ export async function captureAndSendScreenshot( let screenshot = await captureScreenshot(page, screenshotOptions); let captureTime = Date.now() - t0; + let storyUrl = toStoryUrl(page.url(), story.id); + let t1 = Date.now(); - await vizzlyScreenshot(name, screenshot, { properties: { url: page.url() } }); + await vizzlyScreenshot(name, screenshot, { properties: { url: storyUrl } }); let sendTime = Date.now() - t1; if (verbose) { diff --git a/clients/storybook/tests/screenshot.test.js b/clients/storybook/tests/screenshot.test.js index f89e77e..e0bfd39 100644 --- a/clients/storybook/tests/screenshot.test.js +++ b/clients/storybook/tests/screenshot.test.js @@ -8,6 +8,7 @@ import { captureAndSendScreenshot, captureScreenshot, generateScreenshotName, + toStoryUrl, } from '../src/screenshot.js'; describe('generateScreenshotName', () => { @@ -85,12 +86,44 @@ describe('captureScreenshot', () => { }); }); +describe('toStoryUrl', () => { + it('should convert iframe.html URL to story path URL', () => { + let url = toStoryUrl( + 'http://localhost:6006/iframe.html?id=button--primary&viewMode=story', + 'button--primary' + ); + assert.equal(url, 'http://localhost:6006/?path=/story/button--primary'); + }); + + it('should handle encoded story IDs in the iframe URL', () => { + let url = toStoryUrl( + 'http://localhost:6006/iframe.html?id=components%2Fbutton--primary&viewMode=story', + 'components/button--primary' + ); + assert.equal( + url, + 'http://localhost:6006/?path=/story/components/button--primary' + ); + }); + + it('should preserve non-default ports', () => { + let url = toStoryUrl( + 'http://localhost:9009/iframe.html?id=card--default&viewMode=story', + 'card--default' + ); + assert.equal(url, 'http://localhost:9009/?path=/story/card--default'); + }); +}); + describe('captureAndSendScreenshot', () => { it('should capture and send screenshot to vizzly', async () => { let mockBuffer = Buffer.from('fake-screenshot'); let mockScreenshot = mock.fn(() => mockBuffer); - let mockPage = { screenshot: mockScreenshot, url: () => 'http://localhost:6006/?path=/story/button--primary' }; - let story = { title: 'Button', name: 'Primary' }; + let mockPage = { + screenshot: mockScreenshot, + url: () => 'http://localhost:6006/iframe.html?id=button--primary&viewMode=story', + }; + let story = { id: 'button--primary', title: 'Button', name: 'Primary' }; let viewport = { name: 'desktop' }; // This will use the mock vizzlyScreenshot from the module @@ -102,8 +135,11 @@ describe('captureAndSendScreenshot', () => { it('should pass screenshot options through', async () => { let mockBuffer = Buffer.from('fake-screenshot'); let mockScreenshot = mock.fn(() => mockBuffer); - let mockPage = { screenshot: mockScreenshot, url: () => 'http://localhost:6006/?path=/story/card--default' }; - let story = { title: 'Card', name: 'Default' }; + let mockPage = { + screenshot: mockScreenshot, + url: () => 'http://localhost:6006/iframe.html?id=card--default&viewMode=story', + }; + let story = { id: 'card--default', title: 'Card', name: 'Default' }; let viewport = { name: 'mobile' }; await captureAndSendScreenshot(mockPage, story, viewport, { From 59865e090ef8c9b7db0de8c8637d273be10f2d5e Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Tue, 17 Feb 2026 12:36:12 -0600 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=94=A7=20Address=20review:=20encode?= =?UTF-8?q?=20storyId,=20add=20URL=20assertion,=20guard=20CI=20preview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/sdk-e2e.yml | 1 + clients/storybook/src/screenshot.js | 17 +++++++++--- clients/storybook/tests/screenshot.test.js | 30 +++++++++++++++++----- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/workflows/sdk-e2e.yml b/.github/workflows/sdk-e2e.yml index 1f223d3..2cb3f6f 100644 --- a/.github/workflows/sdk-e2e.yml +++ b/.github/workflows/sdk-e2e.yml @@ -188,6 +188,7 @@ jobs: VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }} - name: Upload preview + if: success() working-directory: ./clients/storybook run: ../../bin/vizzly.js preview example-storybook/dist env: diff --git a/clients/storybook/src/screenshot.js b/clients/storybook/src/screenshot.js index 5a482c5..240da4d 100644 --- a/clients/storybook/src/screenshot.js +++ b/clients/storybook/src/screenshot.js @@ -15,6 +15,11 @@ try { vizzlyScreenshot = async () => {}; } +/** @internal Replace vizzlyScreenshot for testing */ +export function _setVizzlyScreenshot(fn) { + vizzlyScreenshot = fn; +} + /** * Generate screenshot name from story and viewport * Format: "ComponentName-StoryName@viewportName" @@ -63,13 +68,17 @@ export async function captureScreenshot(page, options = {}) { /** * Convert an iframe.html URL to a Storybook story URL * iframe.html URLs don't render in iframes on the dashboard — the story path format does - * @param {string} pageUrl - Current page URL (likely iframe.html?id=...&viewMode=story) + * @param {string} pageUrl - Current page URL (iframe.html?id=...&viewMode=story) * @param {string} storyId - Story ID - * @returns {string} Storybook story URL (?path=/story/...) + * @returns {string} Storybook story URL (?path=/story/...), or the raw pageUrl as fallback */ export function toStoryUrl(pageUrl, storyId) { - let url = new URL(pageUrl); - return `${url.origin}/?path=/story/${storyId}`; + try { + let url = new URL(pageUrl); + return `${url.origin}/?path=/story/${encodeURIComponent(storyId)}`; + } catch { + return pageUrl; + } } /** diff --git a/clients/storybook/tests/screenshot.test.js b/clients/storybook/tests/screenshot.test.js index e0bfd39..7946f9f 100644 --- a/clients/storybook/tests/screenshot.test.js +++ b/clients/storybook/tests/screenshot.test.js @@ -5,6 +5,7 @@ import assert from 'node:assert/strict'; import { describe, it, mock } from 'node:test'; import { + _setVizzlyScreenshot, captureAndSendScreenshot, captureScreenshot, generateScreenshotName, @@ -95,14 +96,14 @@ describe('toStoryUrl', () => { assert.equal(url, 'http://localhost:6006/?path=/story/button--primary'); }); - it('should handle encoded story IDs in the iframe URL', () => { + it('should encode special characters in story ID', () => { let url = toStoryUrl( 'http://localhost:6006/iframe.html?id=components%2Fbutton--primary&viewMode=story', 'components/button--primary' ); assert.equal( url, - 'http://localhost:6006/?path=/story/components/button--primary' + 'http://localhost:6006/?path=/story/components%2Fbutton--primary' ); }); @@ -113,26 +114,41 @@ describe('toStoryUrl', () => { ); assert.equal(url, 'http://localhost:9009/?path=/story/card--default'); }); + + it('should fall back to raw URL on invalid input', () => { + let url = toStoryUrl('not-a-url', 'button--primary'); + assert.equal(url, 'not-a-url'); + }); }); describe('captureAndSendScreenshot', () => { - it('should capture and send screenshot to vizzly', async () => { + it('should send the converted story URL to vizzly', async () => { + let mockVizzly = mock.fn(async () => {}); + _setVizzlyScreenshot(mockVizzly); + let mockBuffer = Buffer.from('fake-screenshot'); - let mockScreenshot = mock.fn(() => mockBuffer); let mockPage = { - screenshot: mockScreenshot, + screenshot: mock.fn(() => mockBuffer), url: () => 'http://localhost:6006/iframe.html?id=button--primary&viewMode=story', }; let story = { id: 'button--primary', title: 'Button', name: 'Primary' }; let viewport = { name: 'desktop' }; - // This will use the mock vizzlyScreenshot from the module await captureAndSendScreenshot(mockPage, story, viewport); - assert.equal(mockScreenshot.mock.calls.length, 1); + assert.equal(mockVizzly.mock.calls.length, 1); + let [name, , options] = mockVizzly.mock.calls[0].arguments; + assert.equal(name, 'Button-Primary@desktop'); + assert.equal( + options.properties.url, + 'http://localhost:6006/?path=/story/button--primary' + ); }); it('should pass screenshot options through', async () => { + let mockVizzly = mock.fn(async () => {}); + _setVizzlyScreenshot(mockVizzly); + let mockBuffer = Buffer.from('fake-screenshot'); let mockScreenshot = mock.fn(() => mockBuffer); let mockPage = { From cf30f37d8cade79871405e080ccd085828fd400c Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Tue, 17 Feb 2026 12:43:22 -0600 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=90=9B=20Use=20iframe.html=20URL=20fo?= =?UTF-8?q?r=20isolated=20story=20preview?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ?path=/story/... format loads the full Storybook UI with sidebar and controls. The iframe.html URL renders just the isolated component, matching what the screenshot actually captures. --- clients/storybook/src/screenshot.js | 20 +-------- clients/storybook/tests/screenshot.test.js | 49 ++++------------------ 2 files changed, 8 insertions(+), 61 deletions(-) diff --git a/clients/storybook/src/screenshot.js b/clients/storybook/src/screenshot.js index 240da4d..e13a412 100644 --- a/clients/storybook/src/screenshot.js +++ b/clients/storybook/src/screenshot.js @@ -65,22 +65,6 @@ export async function captureScreenshot(page, options = {}) { return screenshot; } -/** - * Convert an iframe.html URL to a Storybook story URL - * iframe.html URLs don't render in iframes on the dashboard — the story path format does - * @param {string} pageUrl - Current page URL (iframe.html?id=...&viewMode=story) - * @param {string} storyId - Story ID - * @returns {string} Storybook story URL (?path=/story/...), or the raw pageUrl as fallback - */ -export function toStoryUrl(pageUrl, storyId) { - try { - let url = new URL(pageUrl); - return `${url.origin}/?path=/story/${encodeURIComponent(storyId)}`; - } catch { - return pageUrl; - } -} - /** * Capture and send screenshot to Vizzly * @param {Object} page - Playwright page instance @@ -102,10 +86,8 @@ export async function captureAndSendScreenshot( let screenshot = await captureScreenshot(page, screenshotOptions); let captureTime = Date.now() - t0; - let storyUrl = toStoryUrl(page.url(), story.id); - let t1 = Date.now(); - await vizzlyScreenshot(name, screenshot, { properties: { url: storyUrl } }); + await vizzlyScreenshot(name, screenshot, { properties: { url: page.url() } }); let sendTime = Date.now() - t1; if (verbose) { diff --git a/clients/storybook/tests/screenshot.test.js b/clients/storybook/tests/screenshot.test.js index 7946f9f..5bde4f5 100644 --- a/clients/storybook/tests/screenshot.test.js +++ b/clients/storybook/tests/screenshot.test.js @@ -9,7 +9,6 @@ import { captureAndSendScreenshot, captureScreenshot, generateScreenshotName, - toStoryUrl, } from '../src/screenshot.js'; describe('generateScreenshotName', () => { @@ -87,49 +86,17 @@ describe('captureScreenshot', () => { }); }); -describe('toStoryUrl', () => { - it('should convert iframe.html URL to story path URL', () => { - let url = toStoryUrl( - 'http://localhost:6006/iframe.html?id=button--primary&viewMode=story', - 'button--primary' - ); - assert.equal(url, 'http://localhost:6006/?path=/story/button--primary'); - }); - - it('should encode special characters in story ID', () => { - let url = toStoryUrl( - 'http://localhost:6006/iframe.html?id=components%2Fbutton--primary&viewMode=story', - 'components/button--primary' - ); - assert.equal( - url, - 'http://localhost:6006/?path=/story/components%2Fbutton--primary' - ); - }); - - it('should preserve non-default ports', () => { - let url = toStoryUrl( - 'http://localhost:9009/iframe.html?id=card--default&viewMode=story', - 'card--default' - ); - assert.equal(url, 'http://localhost:9009/?path=/story/card--default'); - }); - - it('should fall back to raw URL on invalid input', () => { - let url = toStoryUrl('not-a-url', 'button--primary'); - assert.equal(url, 'not-a-url'); - }); -}); - describe('captureAndSendScreenshot', () => { - it('should send the converted story URL to vizzly', async () => { + it('should send the iframe URL for isolated story preview', async () => { let mockVizzly = mock.fn(async () => {}); _setVizzlyScreenshot(mockVizzly); let mockBuffer = Buffer.from('fake-screenshot'); + let iframeUrl = + 'http://localhost:6006/iframe.html?id=button--primary&viewMode=story'; let mockPage = { screenshot: mock.fn(() => mockBuffer), - url: () => 'http://localhost:6006/iframe.html?id=button--primary&viewMode=story', + url: () => iframeUrl, }; let story = { id: 'button--primary', title: 'Button', name: 'Primary' }; let viewport = { name: 'desktop' }; @@ -139,10 +106,7 @@ describe('captureAndSendScreenshot', () => { assert.equal(mockVizzly.mock.calls.length, 1); let [name, , options] = mockVizzly.mock.calls[0].arguments; assert.equal(name, 'Button-Primary@desktop'); - assert.equal( - options.properties.url, - 'http://localhost:6006/?path=/story/button--primary' - ); + assert.equal(options.properties.url, iframeUrl); }); it('should pass screenshot options through', async () => { @@ -153,7 +117,8 @@ describe('captureAndSendScreenshot', () => { let mockScreenshot = mock.fn(() => mockBuffer); let mockPage = { screenshot: mockScreenshot, - url: () => 'http://localhost:6006/iframe.html?id=card--default&viewMode=story', + url: () => + 'http://localhost:6006/iframe.html?id=card--default&viewMode=story', }; let story = { id: 'card--default', title: 'Card', name: 'Default' }; let viewport = { name: 'mobile' };