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
業務時間外もAIに働いてもらう話
colorful12
3
9.5k
LLMは4年分のCompose移行を再現できるのか?実プロダクト279件のXMLで探る自動化の境界線
makun
0
240
AI Readyの正体はデータマネジメントだ メダリオン2.0の最前線
freee
PRO
0
490
進化を続けるGo toolsの現在地 / The Current State of Ever-Evolving Go Tools
hond0413
0
280
Claude Code全社展開のためにやったことn選~プラグイン302個・コミッター271人を支えるために~
kenchan
5
1.8k
Can LLMs Replicate 4 Years of Compose Migration? Exploring the boundaries of automation with 279 XML files from a real product
makun
0
110
tsc.rip を支える技術 / Kyoto.なんか #8
susisu
0
4.1k
Go 1.27 における memory allocation の高速化
andpad
0
360
DynamoDBの基礎を振り返りながらベクトル検索機能を理解する
musan
3
250
How I Won Prize Money at a Hackathon Using Codex and Symphony Alpha
yasei_no_otoko
0
130
30年振りにコンパイラの定数整数除算を改善した
herumi
9
4.4k
Hello, Hiroshima Geospatial Data! — Exploring DoboX with Python
ra0kley
0
150
Featured
See All Featured
The MySQL Ecosystem @ GitHub 2015
samlambert
251
13k
For a Future-Friendly Web
brad_frost
183
10k
How Fast Is Fast Enough? [PerfNow 2025]
tammyeverts
3
850
Prompt Engineering for Job Search
mfonobong
0
430
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
28
3.6k
How to audit for AI Accessibility on your Front & Back End
davetheseo
0
520
Fashionably flexible responsive web design (full day workshop)
malarkey
408
67k
The Illustrated Children's Guide to Kubernetes
chrisshort
51
53k
Designing Powerful Visuals for Engaging Learning
tmiket
1
520
Agile that works and the tools we love
rasmusluckow
331
22k
How to Ace a Technical Interview
jacobian
281
24k
How to Talk to Developers About Accessibility
jct
2
530
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