Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Storybookを書くだけでリグレッションテストが 実行される世界へようこそ
Search
kubotak
October 20, 2023
Programming
12k
31
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Storybookを書くだけでリグレッションテストが 実行される世界へようこそ
Shizuoka.js #7
kubotak
October 20, 2023
More Decks by kubotak
See All by kubotak
ハーネスエンジニアリング白書
kubotak
0
76
Laravelにはdeleted_atがありますけど?
kubotak
2
110
PHPでWebSocketサーバーを実装しよう2025
kubotak
0
2.2k
情報漏洩させないための設計
kubotak
6
3.1k
Svelteコンポーネントの依存関係に秩序を〜
kubotak
0
240
DMARCレポート可視化ツールを SvelteKitで作った話
kubotak
2
680
Superforms本番投入で分かった良さとハマりどころ
kubotak
0
1.1k
(うまくいった||いかなかった) 技術選定は何を考えていたか
kubotak
1
1.5k
ウォーターフォールに思えたプロジェクトにあったアジャイルの要素
kubotak
2
1k
Other Decks in Programming
See All in Programming
言語を使う側から、作る側へ。 自作 Lisp で得た新たな気づき。
andpad
0
130
Claude Opus 4.6以後の受託開発エンジニアの変化(Claude Code開発ノウハウ大公開スペシャルbyクラスメソッド)
iidatakuma
1
860
壊れたパーサから始める関数型設計と構成的なパーサ #fp_matsuri
raiga0310
2
400
jsmini JavaScript Engine を作ってみた話
yosuke_furukawa
PRO
0
240
5分で問診!Composer セキュリティ健康診断
codmoninc
0
620
What's New in Android 2026
veronikapj
0
210
地域 SRE コミュニティ最前線 - ホンマでっかSRE勉強会
tk3fftk
0
280
Embedded SREと共に達成した会員管理システムのAWS移行 - SRE NEXT 2026 ランチスポンサーセッション
niftycorp
PRO
1
3.1k
PHP初心者セッション2026 〜生成AIでは見えない裏側を知る:今だからLAMPを通して仕組みを学ぶ〜
kashioka
0
670
AI駆動開発を妨げる技術的負債の解消アプローチ / ai-refactoring-approach
minodriven
17
9.3k
改善しないと、タスクが回らない。 “てんこ盛りポジション” を引き継いだ情シスの、入社3ヶ月の業務改善録
krm963
0
200
これからAgentCoreを触る方へトレンドはGatewayです
har1101
6
510
Featured
See All Featured
Noah Learner - AI + Me: how we built a GSC Bulk Export data pipeline
techseoconnect
PRO
0
340
Designing for Timeless Needs
cassininazir
1
400
Public Speaking Without Barfing On Your Shoes - THAT 2023
reverentgeek
1
460
The Psychology of Web Performance [Beyond Tellerrand 2023]
tammyeverts
49
3.5k
The Hidden Cost of Media on the Web [PixelPalooza 2025]
tammyeverts
2
380
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
Leading Effective Engineering Teams in the AI Era
addyosmani
9
2.2k
Code Review Best Practice
trishagee
74
20k
Abbi's Birthday
coloredviolet
3
8.8k
The Invisible Side of Design
smashingmag
301
52k
The Cost Of JavaScript in 2023
addyosmani
55
10k
Chasing Engaging Ingredients in Design
codingconduct
0
240
Transcript
Copyright© M&A Storybook 行 Shizuoka.js # 7 Kenjiro Kubota /
@kubotak_public
Copyright© M&A Profile 田 二 kubotak-is kubotak_public kenjiro.kubota M&A TypeScript
PHP https://kubotak.page Web Laravel ( ) 犬 豆
Copyright© M&A 水 身 玉 Web 田石 石
Copyright© M&A NFC
Copyright© M&A
Copyright© M&A
Copyright© M&A ・・・ import { describe, expect, it } from
'vitest' import { render } from '@testing-library/svelte' import Component from './Component.svelte' describe('ίϯϙʔωϯτͷදࣔςετ', () => { it('Props௨Γʹίϯϙʔωϯτ͕දࣔ͞ΕΔ͜ͱ', () => { const { getAllByText } = render(Component, { props: { text: '͜ΕςετͰ͢' } }) expect(getAllByText(/͜ΕςετͰ͢/)); }) })
Copyright© M&A ・・・ import { describe, expect, it } from
'vitest' import { render } from '@testing-library/svelte' import Component from './Component.svelte' describe('ίϯϙʔωϯτͷදࣔςετ', () => { it('Props௨Γʹίϯϙʔωϯτ͕දࣔ͞ΕΔ͜ͱ', () => { const { getAllByText } = render(Component, { props: { text: '͜ΕςετͰ͢' } }) expect(getAllByText(/͜ΕςετͰ͢/)); }) }) ・・・ 文
Copyright© M&A ・・・ import { describe, expect, it } from
'vitest' import { render } from '@testing-library/svelte' import Component from './Component.svelte' describe('ίϯϙʔωϯτͷදࣔςετ', () => { it('Props௨Γʹίϯϙʔωϯτ͕දࣔ͞ΕΔ͜ͱ', () => { const { getAllByText } = render(Component, { props: { text: '͜ΕςετͰ͢' } }) expect(getAllByText(/͜ΕςετͰ͢/)); }) }) ・・・ 文 文 示 ⾒ 文
Copyright© M&A ・・・ import { describe, expect, it } from
'vitest' import { render } from '@testing-library/svelte' import Component from './Component.svelte' describe('ίϯϙʔωϯτͷදࣔςετ', () => { it('Props௨Γʹίϯϙʔωϯτ͕දࣔ͞ΕΔ͜ͱ', () => { const { getAllByText } = render(Component, { props: { text: '͜ΕςετͰ͢' } }) expect(getAllByText(/͜ΕςετͰ͢/)); }) }) ・・・ 文 文 示 ⾒ 文 CSS
Copyright© M&A ・・・ import { describe, expect, it } from
'vitest' import { render } from '@testing-library/svelte' import Component from './Component.svelte' describe('ίϯϙʔωϯτͷදࣔςετ', () => { it('Props௨Γʹίϯϙʔωϯτ͕දࣔ͞ΕΔ͜ͱ', () => { const { getAllByText } = render(Component, { props: { text: '͜ΕςετͰ͢' } }) expect(getAllByText(/͜ΕςετͰ͢/)); }) }) ・・・ 文 文 示 ⾒ 文 CSS DOM ・・・
Copyright© M&A ・・・ import { describe, expect, it } from
'vitest' import { render } from '@testing-library/svelte' import Component from './Component.svelte' describe('ίϯϙʔωϯτͷදࣔςετ', () => { it('Props௨Γʹίϯϙʔωϯτ͕දࣔ͞ΕΔ͜ͱ', () => { const { getAllByText } = render(Component, { props: { text: '͜ΕςετͰ͢' } }) expect(getAllByText(/͜ΕςετͰ͢/)); }) }) ・・・ 文 文 示 ⾒ 文 CSS DOM ・・・ 手
Copyright© M&A
Copyright© M&A 入力 力 CI 用 言 用 Vitest JSDOM
行
Copyright© M&A 見 目 見 目 示
Copyright© M&A reg-suit + Storycap Playwright
Copyright© M&A reg-suit + Storycap reg-suit 比 CLI Storycap Storybook
力 Storybook Addon E 2 E Puppeteer Chromium
Copyright© M&A
Copyright© M&A Playwright Puppeteer E 2 E 行 HTML 力
Experimental
Copyright© M&A
Copyright© M&A Storybook SaaS chromatic https://www.chromatic.com Storybook UI Storybook UI
行 Chrome/Firefox/Safari/Edge Lost Pixel https://lost-pixel.com Storybook, Ladle, Next.js CI GitHub Actions GitHub Action Platform OSS
Copyright© M&A Chromatic
Copyright© M&A Chromatic Storybook
Copyright© M&A Chromatic
Copyright© M&A Chromatic 高 高 SaaS ・・・
Copyright© M&A 自
Copyright© M&A Storybook 行
Copyright© M&A Storybook Playwright Storybook storybook-static/stories.json 力 { "v": 3,
"stories": { "parent-a--defailt": { "id": "parent-a--defailt", "name": "Default", "title": "Parent/A", "importPath": "./src/lib/components/Parent/A.stories.ts", "tags": [], "kind": "Parent/A", "storiy": "Default", "parameters": { "__id": "parent-a--defailt", "fileName": "src/lib/components/Parent/A.stories.ts" } }, } }
Copyright© M&A Storybook Playwright Storybook storybook-static/stories.json 力 { "v": 3,
"stories": { "parent-a--defailt": { "id": "parent-a--defailt", "name": "Default", "title": "Parent/A", "importPath": "./src/lib/components/Parent/A.stories.ts", "tags": [], "kind": "Parent/A", "storiy": "Default", "parameters": { "__id": "parent-a--defailt", "fileName": "src/lib/components/Parent/A.stories.ts" } }, } } iframe.html?id={id} Story HTML 示
Copyright© M&A Storybook Playwright storybook-static/stories.json id 用 Playwright test(`snapshot test
${story.title}: ${story.name}`, async ({ page }) => { await page.goto(`http://localhost:8080/iframe.html?id=${story.id}`, { waitUntil: 'networkidle', timeout: 1000 * 10 }) await expect(page).toHaveScreenshot([story.title, `${story.id}.png`], { animations: 'disabled', timeout: 1000 * 10, threshold: 0.2 }) })
Copyright© M&A Storybook Playwright storybook-static/stories.json id 用 Playwright test(`snapshot test
${story.title}: ${story.name}`, async ({ page }) => { await page.goto(`http://localhost:8080/iframe.html?id=${story.id}`, { waitUntil: 'networkidle', timeout: 1000 * 10 }) await expect(page).toHaveScreenshot([story.title, `${story.id}.png`], { animations: 'disabled', timeout: 1000 * 10, threshold: 0.2 }) }) iframe.html?id={id}
Copyright© M&A Storybook Playwright storybook-static/stories.json id 用 Playwright test(`snapshot test
${story.title}: ${story.name}`, async ({ page }) => { await page.goto(`http://localhost:8080/iframe.html?id=${story.id}`, { waitUntil: 'networkidle', timeout: 1000 * 10 }) await expect(page).toHaveScreenshot([story.title, `${story.id}.png`], { animations: 'disabled', timeout: 1000 * 10, threshold: 0.2 }) }) 比
Copyright© M&A Storybook 行 土
Copyright© M&A ・・・ 比 大 ⾒ Git 行 git push
火 CI
Copyright© M&A S 3 main 行 Amazon S 3 CI
行 比 main S 3 行
Copyright© M&A S 3 main 行 Amazon S 3 CI
行 比 main S 3 行 行 --update-snapshots 行
Copyright© M&A Git vite-plugin-turbosnap Vite 入 Storybook storybook-static/preview-stats.json 力 Chromatic
行 TurboSnap 用 入
Copyright© M&A Git { "modules": [ ... { "id": "./src/lib/components/Child/A.svelte",
"name": "./src/lib/components/Child/A.svelte", "reasons": [ { "moduleName": "./src/lib/components/Child/A.stories.ts" }, { "moduleName": "./src/lib/components/Parent/A.svelte" } ] }, ... ] }
Copyright© M&A Git { "modules": [ ... { "id": "./src/lib/components/Child/A.svelte",
"name": "./src/lib/components/Child/A.svelte", "reasons": [ { "moduleName": "./src/lib/components/Child/A.stories.ts" }, { "moduleName": "./src/lib/components/Parent/A.svelte" } ] }, ... ] }
Copyright© M&A Git storybook-static/preview-stats.json Git diff ⾒ 行 TypeScript Git
diff simple-git 用
Copyright© M&A Git Grandchild D ⾒ Grandchild D Child B
Child C Parent A Parent B
Copyright© M&A Git const diffSummary = await simpleGit().diffSummary([targetBranch, 'origin/main']) const
storybookDir = resolve(__dirname, '..', 'storybook-static') const statsJson: StatsIndex = JSON.parse( readFileSync(resolve(storybookDir, 'preview-stats.json')).toString() ) const storiesJson: StoryIndex = JSON.parse( readFileSync(resolve(storybookDir, 'stories.json')).toString() )
Copyright© M&A Git const diffSummary = await simpleGit().diffSummary([targetBranch, 'origin/main']) const
storybookDir = resolve(__dirname, '..', 'storybook-static') const statsJson: StatsIndex = JSON.parse( readFileSync(resolve(storybookDir, 'preview-stats.json')).toString() ) const storiesJson: StoryIndex = JSON.parse( readFileSync(resolve(storybookDir, 'stories.json')).toString() ) CI 行 git branch
Copyright© M&A Git const diffSummary = await simpleGit().diffSummary([targetBranch, 'origin/main']) const
storybookDir = resolve(__dirname, '..', 'storybook-static') const statsJson: StatsIndex = JSON.parse( readFileSync(resolve(storybookDir, 'preview-stats.json')).toString() ) const storiesJson: StoryIndex = JSON.parse( readFileSync(resolve(storybookDir, 'stories.json')).toString() ) main
Copyright© M&A Git const getUpdatedStories = (): StoryIndex => {
const isUpdate = process.env.UPDATE_SNAPSHOT === 'all' // NOTE: εφοϓγϣοτΛߋ৽͢Δ߹ɺશͯͷεΫϦʔϯγϣοτΛߋ৽͢Δ if (isUpdate) { return storiesJson } // NOTE: ͕ࠩଘࡏ͠ͳ͍߹ɺεΫϦʔϯγϣοτςετΛ࣮ߦ͠ͳ͍ if (diffSummary.files.length === 0) return { stories: {} } // NOTE: ରͷϑΝΠϧ͕ࠩͱͯ͠ଘࡏ͢Δ߹ɺશͯͷεΫϦʔϯγϣοτΛߋ৽͢Δ const surveillanceList = [ 'package.json', 'package-lock.json', 'playwright.config.ts', '.storybook/main.ts', '.storybook/preview.ts', '.storybook/preview-head.html' ] const updatedStories = diffSummary.files.filter((file) => { return surveillanceList.some((surveillance) => { return file.file.includes(surveillance) }) }) // NOTE: શ݅νΣοΫ͢Δඞཁ͕͋Δ͔ if (updatedStories.length !== 0) return storiesJson ... }
Copyright© M&A Git const getUpdatedStories = (): StoryIndex => {
const isUpdate = process.env.UPDATE_SNAPSHOT === 'all' // NOTE: εφοϓγϣοτΛߋ৽͢Δ߹ɺશͯͷεΫϦʔϯγϣοτΛߋ৽͢Δ if (isUpdate) { return storiesJson } // NOTE: ͕ࠩଘࡏ͠ͳ͍߹ɺεΫϦʔϯγϣοτςετΛ࣮ߦ͠ͳ͍ if (diffSummary.files.length === 0) return { stories: {} } // NOTE: ରͷϑΝΠϧ͕ࠩͱͯ͠ଘࡏ͢Δ߹ɺશͯͷεΫϦʔϯγϣοτΛߋ৽͢Δ const surveillanceList = [ 'package.json', 'package-lock.json', 'playwright.config.ts', '.storybook/main.ts', '.storybook/preview.ts', '.storybook/preview-head.html' ] const updatedStories = diffSummary.files.filter((file) => { return surveillanceList.some((surveillance) => { return file.file.includes(surveillance) }) }) // NOTE: શ݅νΣοΫ͢Δඞཁ͕͋Δ͔ if (updatedStories.length !== 0) return storiesJson ... } stories.json
Copyright© M&A Git const getUpdatedStories = (): StoryIndex => {
const isUpdate = process.env.UPDATE_SNAPSHOT === 'all' // NOTE: εφοϓγϣοτΛߋ৽͢Δ߹ɺશͯͷεΫϦʔϯγϣοτΛߋ৽͢Δ if (isUpdate) { return storiesJson } // NOTE: ͕ࠩଘࡏ͠ͳ͍߹ɺεΫϦʔϯγϣοτςετΛ࣮ߦ͠ͳ͍ if (diffSummary.files.length === 0) return { stories: {} } // NOTE: ରͷϑΝΠϧ͕ࠩͱͯ͠ଘࡏ͢Δ߹ɺશͯͷεΫϦʔϯγϣοτΛߋ৽͢Δ const surveillanceList = [ 'package.json', 'package-lock.json', 'playwright.config.ts', '.storybook/main.ts', '.storybook/preview.ts', '.storybook/preview-head.html' ] const updatedStories = diffSummary.files.filter((file) => { return surveillanceList.some((surveillance) => { return file.file.includes(surveillance) }) }) // NOTE: શ݅νΣοΫ͢Δඞཁ͕͋Δ͔ if (updatedStories.length !== 0) return storiesJson ... } ⾒ 行
Copyright© M&A Git const getUpdatedStories = (): StoryIndex => {
const isUpdate = process.env.UPDATE_SNAPSHOT === 'all' // NOTE: εφοϓγϣοτΛߋ৽͢Δ߹ɺશͯͷεΫϦʔϯγϣοτΛߋ৽͢Δ if (isUpdate) { return storiesJson } // NOTE: ͕ࠩଘࡏ͠ͳ͍߹ɺεΫϦʔϯγϣοτςετΛ࣮ߦ͠ͳ͍ if (diffSummary.files.length === 0) return { stories: {} } // NOTE: ରͷϑΝΠϧ͕ࠩͱͯ͠ଘࡏ͢Δ߹ɺશͯͷεΫϦʔϯγϣοτΛߋ৽͢Δ const surveillanceList = [ 'package.json', 'package-lock.json', 'playwright.config.ts', '.storybook/main.ts', '.storybook/preview.ts', '.storybook/preview-head.html' ] const updatedStories = diffSummary.files.filter((file) => { return surveillanceList.some((surveillance) => { return file.file.includes(surveillance) }) }) // NOTE: શ݅νΣοΫ͢Δඞཁ͕͋Δ͔ if (updatedStories.length !== 0) return storiesJson ... } ⾒ stories.json
Copyright© M&A Git export const extractDependenciesRecursively = (path: string, statsJson:
StatsIndex): string[] => { const relatedComponent = new Set<string>() const explore = (name: string) => { // ͢Ͱʹ୳ࡧࡁΈͷ߹ऴྃ if (relatedComponent.has(name)) return // .svelteϑΝΠϧͷΈΛநग़ if (!name.endsWith('.svelte')) return // ґଘ͍ͯ͠ΔϑΝΠϧ͕ଘࡏ͢Δ͔ const target = Object.values(statsJson.modules).find((s) => s.id === name) if (target) { relatedComponent.add(name) target.reasons.forEach((r) => explore(r.moduleName)) } } explore(path) return Array.from<string>(relatedComponent) } export const getStories = ( pathList: string[], statsJson: StatsIndex, storiesJson: StoryIndex ): StoryIndex => { const updatedStoriesJson: StoryIndex = { stories: {} } pathList.forEach((path) => { // ରͷstatsΛऔಘ const stats = statsJson.modules.find((component) => component.id === path) if (!stats) return // .stories.tsΛऔಘ const storyPath = stats.reasons.find((r) => r.moduleName.includes('.stories.ts')) if (!storyPath) return // ରͷstoriesΛऔಘ const storyObj = Object.values(storiesJson.stories).find( (s) => s.importPath === storyPath.moduleName ) if (storyObj) updatedStoriesJson.stories[storyObj.id] = storyObj }) return updatedStoriesJson }
Copyright© M&A Git export const extractDependenciesRecursively = (path: string, statsJson:
StatsIndex): string[] => { const relatedComponent = new Set<string>() const explore = (name: string) => { // ͢Ͱʹ୳ࡧࡁΈͷ߹ऴྃ if (relatedComponent.has(name)) return // .svelteϑΝΠϧͷΈΛநग़ if (!name.endsWith('.svelte')) return // ґଘ͍ͯ͠ΔϑΝΠϧ͕ଘࡏ͢Δ͔ const target = Object.values(statsJson.modules).find((s) => s.id === name) if (target) { relatedComponent.add(name) target.reasons.forEach((r) => explore(r.moduleName)) } } explore(path) return Array.from<string>(relatedComponent) } export const getStories = ( pathList: string[], statsJson: StatsIndex, storiesJson: StoryIndex ): StoryIndex => { const updatedStoriesJson: StoryIndex = { stories: {} } pathList.forEach((path) => { // ରͷstatsΛऔಘ const stats = statsJson.modules.find((component) => component.id === path) if (!stats) return // .stories.tsΛऔಘ const storyPath = stats.reasons.find((r) => r.moduleName.includes('.stories.ts')) if (!storyPath) return // ରͷstoriesΛऔಘ const storyObj = Object.values(storiesJson.stories).find( (s) => s.importPath === storyPath.moduleName ) if (storyObj) updatedStoriesJson.stories[storyObj.id] = storyObj }) return updatedStoriesJson } storybook-static/preview-stats.json 用
Copyright© M&A Git export const extractDependenciesRecursively = (path: string, statsJson:
StatsIndex): string[] => { const relatedComponent = new Set<string>() const explore = (name: string) => { // ͢Ͱʹ୳ࡧࡁΈͷ߹ऴྃ if (relatedComponent.has(name)) return // .svelteϑΝΠϧͷΈΛநग़ if (!name.endsWith('.svelte')) return // ґଘ͍ͯ͠ΔϑΝΠϧ͕ଘࡏ͢Δ͔ const target = Object.values(statsJson.modules).find((s) => s.id === name) if (target) { relatedComponent.add(name) target.reasons.forEach((r) => explore(r.moduleName)) } } explore(path) return Array.from<string>(relatedComponent) } export const getStories = ( pathList: string[], statsJson: StatsIndex, storiesJson: StoryIndex ): StoryIndex => { const updatedStoriesJson: StoryIndex = { stories: {} } pathList.forEach((path) => { // ରͷstatsΛऔಘ const stats = statsJson.modules.find((component) => component.id === path) if (!stats) return // .stories.tsΛऔಘ const storyPath = stats.reasons.find((r) => r.moduleName.includes('.stories.ts')) if (!storyPath) return // ରͷstoriesΛऔಘ const storyObj = Object.values(storiesJson.stories).find( (s) => s.importPath === storyPath.moduleName ) if (storyObj) updatedStoriesJson.stories[storyObj.id] = storyObj }) return updatedStoriesJson } .stories.ts Storybook
Copyright© M&A CI 行
Copyright© M&A Storybook main 見
Copyright© M&A projects: [ { name: 'chromium', use: devices['Desktop Chrome']
}, { name: 'firefox', use: devices['Desktop Firefox'] }, { name: 'webkit', use: devices['Desktop Safari'] }, { name: 'Mobile Safari', use: devices['iPhone 14 Pro'] } ] playwright.config.ts 行 行
Copyright© M&A 一 用
Copyright© M&A 大 示 Storybook 手 高 Chromatic 高 ・・・
Playwright TurboSnap 用 行
Copyright© M&A main S 3 PR Playwright 行 っ 面
DOM 手
Copyright© M&A