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
僕も、PHPフレームワーク作ってみた
kubotak
0
260
ハーネスエンジニアリング白書
kubotak
0
100
Laravelにはdeleted_atがありますけど?
kubotak
2
120
PHPでWebSocketサーバーを実装しよう2025
kubotak
0
2.4k
情報漏洩させないための設計
kubotak
6
3.2k
Svelteコンポーネントの依存関係に秩序を〜
kubotak
0
240
DMARCレポート可視化ツールを SvelteKitで作った話
kubotak
2
700
Superforms本番投入で分かった良さとハマりどころ
kubotak
0
1.1k
(うまくいった||いかなかった) 技術選定は何を考えていたか
kubotak
1
1.6k
Other Decks in Programming
See All in Programming
Swift愛好会と私(ウホーイ) / Swift Fan Club and Uhooi
uhooi
0
100
バグを直したら useEffect が消えた
colorful12
3
780
PyConJP2026_wat_Python × Signal Processing: How to Draw Pictures with Sound Using Spectrogram Art
wat
0
640
VibeCodingからAgenticWorkflowへ
starfish719
0
1k
コンパウンドプロダクト開発のためのローカルプロセスマネージャー再発明 #layerxgo
izumin5210
0
600
Webエンジニアなのにブラウザの仕組みがわからないので、Pythonで自作してみた
tatsuki12
4
1.6k
「人を評価する AI」の設計と実装
ryoyanara
0
280
プロポーザルを書いてもらう
pvcresin
0
590
仕様駆動開発の消費期限
watany
20
9.2k
ソフトウェアエンジニアにとっての生成AI - 特性を知って使い倒す / generative ai for software enginner
kishida
7
2.3k
Can LLMs Replicate 4 Years of Compose Migration? Exploring the boundaries of automation with 279 XML files from a real product
makun
0
110
KotlinConf Extended South Korea 2026 Keynote
l2hyunwoo
0
130
Featured
See All Featured
Building the Perfect Custom Keyboard
takai
2
860
Building AI with AI
inesmontani
PRO
1
1.2k
Neural Spatial Audio Processing for Sound Field Analysis and Control
skoyamalab
0
430
Lightning Talk: Beautiful Slides for Beginners
inesmontani
PRO
2
660
XXLCSS - How to scale CSS and keep your sanity
sugarenia
249
1.3M
Conquering PDFs: document understanding beyond plain text
inesmontani
PRO
4
3k
Stop Working from a Prison Cell
hatefulcrawdad
274
21k
Everyday Curiosity
cassininazir
0
300
Skip the Path - Find Your Career Trail
mkilby
1
210
Why Mistakes Are the Best Teachers: Turning Failure into a Pathway for Growth
auna
0
230
A designer walks into a library…
pauljervisheath
211
24k
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
1
2.6k
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