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
70
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
Claude Opus 4.6以後の受託開発エンジニアの変化(Claude Code開発ノウハウ大公開スペシャルbyクラスメソッド)
iidatakuma
1
700
AIエージェントで 変わるAndroid開発環境
takahirom
2
630
えっ!!コードを読まずに開発を!?
hananouchi
0
200
AWS CDK を「作」ってみた 〜フルスクラッチで見えた CDK の裏側〜 / aws-cdk-from-scratch
gotok365
3
300
AIを活用したE2Eテスト実装効率化のあゆみ / ebisu-mobile-14-kotetu
kotetuco
0
170
The Past, Present, and Future of Enterprise Java
ivargrimstad
0
190
AI時代、エンジニアはどう育つのか -未経験エンジニアの成長を間近で見て考えたこと-
thasu0123
0
110
音楽のための関数型プログラミング言語mimiumにおける多段階計算の活用
tomoyanonymous
1
320
エンジニア向け会社紹介/Findy Company Profile
findyinc
6
360k
5分で問診!Composer セキュリティ健康診断
codmoninc
0
140
PHP Application における Kubernetes 内 gRPC 通信
ganchiku
0
380
技術記事、 専門家としてのプログラマ、 言語化
mizchi
14
7.5k
Featured
See All Featured
Bash Introduction
62gerente
615
220k
XXLCSS - How to scale CSS and keep your sanity
sugarenia
249
1.3M
How to build a perfect <img>
jonoalderson
1
5.8k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
25
2k
Kristin Tynski - Automating Marketing Tasks With AI
techseoconnect
PRO
0
290
Marketing Yourself as an Engineer | Alaka | Gurzu
gurzu
0
260
Templates, Plugins, & Blocks: Oh My! Creating the theme that thinks of everything
marktimemedia
31
2.8k
DevOps and Value Stream Thinking: Enabling flow, efficiency and business value
helenjbeal
1
260
Facilitating Awesome Meetings
lara
57
7k
End of SEO as We Know It (SMX Advanced Version)
ipullrank
3
4.3k
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
410
Navigating Algorithm Shifts & AI Overviews - #SMXNext
aleyda
1
1.4k
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