Slide 1

Slide 1 text

Copyright© M&A Storybook 行 Shizuoka.js # 7 Kenjiro Kubota / @kubotak_public

Slide 2

Slide 2 text

Copyright© M&A Profile 田 二 kubotak-is kubotak_public kenjiro.kubota M&A TypeScript PHP https://kubotak.page Web Laravel ( ) 犬 豆

Slide 3

Slide 3 text

Copyright© M&A 水 身 玉 Web 田石 石

Slide 4

Slide 4 text

Copyright© M&A NFC

Slide 5

Slide 5 text

Copyright© M&A

Slide 6

Slide 6 text

Copyright© M&A

Slide 7

Slide 7 text

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(/͜Ε͸ςετͰ͢/)); }) })

Slide 8

Slide 8 text

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(/͜Ε͸ςετͰ͢/)); }) }) ・・・ 文

Slide 9

Slide 9 text

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(/͜Ε͸ςετͰ͢/)); }) }) ・・・ 文 文 示 ⾒ 文

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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 ・・・

Slide 12

Slide 12 text

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 ・・・ 手

Slide 13

Slide 13 text

Copyright© M&A

Slide 14

Slide 14 text

Copyright© M&A 入力 力 CI 用 言 用 Vitest JSDOM 行

Slide 15

Slide 15 text

Copyright© M&A 見 目 見 目 示

Slide 16

Slide 16 text

Copyright© M&A reg-suit + Storycap Playwright

Slide 17

Slide 17 text

Copyright© M&A reg-suit + Storycap reg-suit 比 CLI Storycap Storybook 力 Storybook Addon E 2 E Puppeteer Chromium

Slide 18

Slide 18 text

Copyright© M&A

Slide 19

Slide 19 text

Copyright© M&A Playwright Puppeteer E 2 E 行 HTML 力 Experimental

Slide 20

Slide 20 text

Copyright© M&A

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

Copyright© M&A Chromatic

Slide 23

Slide 23 text

Copyright© M&A Chromatic Storybook

Slide 24

Slide 24 text

Copyright© M&A Chromatic

Slide 25

Slide 25 text

Copyright© M&A Chromatic 高 高 SaaS ・・・

Slide 26

Slide 26 text

Copyright© M&A 自

Slide 27

Slide 27 text

Copyright© M&A Storybook 行

Slide 28

Slide 28 text

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" } }, } }

Slide 29

Slide 29 text

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 示

Slide 30

Slide 30 text

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 }) })

Slide 31

Slide 31 text

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}

Slide 32

Slide 32 text

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 }) }) 比

Slide 33

Slide 33 text

Copyright© M&A Storybook 行 土

Slide 34

Slide 34 text

Copyright© M&A ・・・ 比 大 ⾒ Git 行 git push 火 CI

Slide 35

Slide 35 text

Copyright© M&A S 3 main 行 Amazon S 3 CI 行 比 main S 3 行

Slide 36

Slide 36 text

Copyright© M&A S 3 main 行 Amazon S 3 CI 行 比 main S 3 行 行 --update-snapshots 行

Slide 37

Slide 37 text

Copyright© M&A Git vite-plugin-turbosnap Vite 入 Storybook storybook-static/preview-stats.json 力 Chromatic 行 TurboSnap 用 入

Slide 38

Slide 38 text

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" } ] }, ... ] }

Slide 39

Slide 39 text

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" } ] }, ... ] }

Slide 40

Slide 40 text

Copyright© M&A Git storybook-static/preview-stats.json Git diff ⾒ 行 TypeScript Git diff simple-git 用

Slide 41

Slide 41 text

Copyright© M&A Git Grandchild D ⾒ Grandchild D Child B Child C Parent A Parent B

Slide 42

Slide 42 text

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() )

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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 ... }

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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 ... } ⾒ 行

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

Copyright© M&A Git export const extractDependenciesRecursively = (path: string, statsJson: StatsIndex): string[] => { const relatedComponent = new Set() 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(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 }

Slide 50

Slide 50 text

Copyright© M&A Git export const extractDependenciesRecursively = (path: string, statsJson: StatsIndex): string[] => { const relatedComponent = new Set() 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(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 用

Slide 51

Slide 51 text

Copyright© M&A Git export const extractDependenciesRecursively = (path: string, statsJson: StatsIndex): string[] => { const relatedComponent = new Set() 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(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

Slide 52

Slide 52 text

Copyright© M&A CI 行

Slide 53

Slide 53 text

Copyright© M&A Storybook main 見

Slide 54

Slide 54 text

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 行 行

Slide 55

Slide 55 text

Copyright© M&A 一 用

Slide 56

Slide 56 text

Copyright© M&A 大 示 Storybook 手 高 Chromatic 高 ・・・ Playwright TurboSnap 用 行

Slide 57

Slide 57 text

Copyright© M&A main S 3 PR Playwright 行 っ 面 DOM 手

Slide 58

Slide 58 text

Copyright© M&A