Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
Storybookを書くだけでリグレッションテストが 実行される世界へようこそ
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
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
SaaS is Dead — 自作アプリで見えたSaaSの死
kubotak
0
17
僕も、PHPフレームワーク作ってみた
kubotak
0
310
ハーネスエンジニアリング白書
kubotak
0
180
Laravelにはdeleted_atがありますけど?
kubotak
2
130
PHPでWebSocketサーバーを実装しよう2025
kubotak
0
2.7k
情報漏洩させないための設計
kubotak
6
3.3k
Svelteコンポーネントの依存関係に秩序を〜
kubotak
0
260
DMARCレポート可視化ツールを SvelteKitで作った話
kubotak
2
740
Superforms本番投入で分かった良さとハマりどころ
kubotak
0
1.1k
Other Decks in Programming
See All in Programming
AIが実装を担う時代の「技術設計」と「チームマネジメント」
kamina_zzz
2
230
地方こそサーバーレス — 10年続けた、顔が見えるチームで確かな基盤を / Serverless Matters Most in Regional Areas — 10 Years of Building Reliable Foundations with Close-Knit Teams
seike460
PRO
1
290
flutter_test のその先へ — Flutter アプリの LLM 機能に「評価」という品質保証を
kosukesaigusa
0
200
Goコンパイラを作ってGoをもっと理解する / Build a Go compiler to better understand Go
ikura_hamu
0
130
Coding with AI: How to trust the storyteller (AWS Community Day Adria, Ljubljana 2026)
slobodan
0
140
Zero-Tap Sign-In対応、結局何をすればいいの?
mitohato14
0
1.8k
クラウドゲーミングのパフォーマンス最適化に挑む / Cloud Gaming Performance Tuning
vivion
0
330
クラウドエージェントは、 何がそんなに便利なのか / what makes cloud agents so useful
rkaga
5
800
Spec-Driven Development - Why Vaadin Is the Perfect Fit
simas
PRO
0
240
S3 TablesのIcebergデータに、AIエージェントからアクセスしてみた / 20261016developersio2026joetsu-accessing-iceberg-in-s3tables-via-agent
kasacchiful
PRO
0
290
LLM 101
dalinaum
0
470
AIがコードを書いてくれる時代、我々は何を確かめるのか
yosuke_furukawa
PRO
13
5.8k
Featured
See All Featured
Producing Creativity
orderedlist
PRO
348
41k
Keith and Marios Guide to Fast Websites
keithpitt
413
23k
How to train your dragon (web standard)
notwaldorf
96
6.9k
The agentic SEO stack - context over prompts
schlessera
0
1k
From π to Pie charts
rasagy
1
440
Art, The Web, and Tiny UX
lynnandtonic
304
22k
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
254
23k
[RailsConf 2023] Rails as a piece of cake
palkan
59
7.1k
Navigating Team Friction
lara
192
16k
Building a Scalable Design System with Sketch
lauravandoore
464
34k
A Modern Web Designer's Workflow
chriscoyier
699
190k
Sam Torres - BigQuery for SEOs
techseoconnect
PRO
0
610
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