test(sw): align storybook inventory checks with approved changes

This commit is contained in:
shkim4 committed 2026-10-20 23:04:14 +09:00
1 parent 522ecbd193
commit 20ed2f2afb
3 files changed
+64 -25

No files matched your search

+6 -11
View File
@@ -5,26 +5,22 @@ import { execFileSync } from 'node:child_process'
import path from 'node:path'
import http from 'node:http'
import { parse } from '@babel/parser'
import { swStoryEntries, retainedInventory, inventorySignature, retiredDropdownIds, integratedChangedFiles, integratedStoryIds, retiredPropertyPanelIds, retiredUnusedDemoIds, movedHomeMenuIds, retiredHomeMenuCompareIds, movedToPopupIds } from './storybook-inventory.mjs'
import { swStoryEntries, retainedInventory, inventorySignature, retiredDropdownIds, integratedChangedFiles, integratedStoryIds, retiredPropertyPanelIds, retiredUnusedDemoIds, movedHomeMenuIds, retiredHomeMenuCompareIds, movedToPopupIds, approvedMainCommit, homeMenuSidebarIds, withApprovedEntryChanges, approvedHeaderDeclarations, movedHeaderDeclarations } from './storybook-inventory.mjs'
import { loadPlaywright } from './playwright-runtime.mjs'
const out = path.resolve(process.argv[2] ?? 'docs/sw/storybook-overview-qa')
await mkdir(out, { recursive: true })
// 2026-10-20 (test alignment): 50bf5bd and its before-index.json stay the baseline. The checks below accept only the
// approved changes after it (storybook-inventory.mjs and the ledgers here) and the approved main merge 553e4df.
const report = { baseCommit: '50bf5bda8e956a4973b923d64131f2f2285bd38c', approvedMainCommit: '553e4df4a865aaf6514b1c2b86cb2e3e9a658e5f', checks: [], errors: [], sizes: [] }
// approved changes after it and the approved main merge, both recorded in storybook-inventory.mjs.
const report = { baseCommit: '50bf5bda8e956a4973b923d64131f2f2285bd38c', approvedMainCommit, checks: [], errors: [], sizes: [] }
const check = (name, ok, data) => { report.checks.push({ name, status: ok ? 'PASS' : 'FAIL', data }); console.log(ok ? 'PASS' : 'FAIL', name) }
const before = JSON.parse(await readFile(path.join(out, 'before-index.json'), 'utf8'))
const index = JSON.parse(await readFile('storybook-static/index.json', 'utf8'))
const after = swStoryEntries(index)
await writeFile(path.join(out, 'after-index.json'), JSON.stringify(after, null, 2) + '\n')
// 2b0e1a2 sidebar simplification: these two keep their ID and direct URL with the verification tag.
const verificationTaggedIds = ['sw-header--window-move-motion', 'sw-header--k-guide']
const removedHeaderIds = [...retiredPropertyPanelIds, ...retiredUnusedDemoIds, ...movedToPopupIds]
// 170a5c2 home menu merge: the sidebar Story sw-header--home-menu plus the moved sw-header--home-menu-* Stories.
const addedHeaderIds = [...integratedStoryIds.filter(id => id.startsWith('sw-header--')), 'sw-header--home-menu', ...Object.values(movedHomeMenuIds).filter(id => !retiredHomeMenuCompareIds.includes(id))]
const expectedRetained = retainedInventory(before).filter(entry => !removedHeaderIds.includes(entry.id))
.map(entry => verificationTaggedIds.includes(entry.id) ? { ...entry, tags: [...entry.tags, 'verification'] } : entry)
const addedHeaderIds = [...integratedStoryIds.filter(id => id.startsWith('sw-header--')), ...homeMenuSidebarIds, ...Object.values(movedHomeMenuIds).filter(id => !retiredHomeMenuCompareIds.includes(id))]
const expectedRetained = retainedInventory(before).filter(entry => !removedHeaderIds.includes(entry.id)).map(withApprovedEntryChanges)
const inventory = { expected: expectedRetained.length + addedHeaderIds.length, actual: after.length, missingAdded: addedHeaderIds.filter(id => !after.some(e => e.id === id)), movedToPopup: movedToPopupIds.map(id => index.entries[id]?.title ?? null) }
check(`${inventory.expected} SW/Header Story IDs: the 24 retained at 50bf5bd (names, tags) minus approved retirements and the Popup move, plus approved additions`,
after.length === inventory.expected && inventory.missingAdded.length === 0 && inventory.movedToPopup.every(title => title === 'SW/Popup')
@@ -39,8 +35,7 @@ const retiredExports = ['LayerDropdown','ColorDropdown','LineStyleDropdown','Tex
// retirement, or the Popup move compared in its new file. All others stay byte-identical to 50bf5bd.
const gitStories = (commit, file) => stories(execFileSync('git', ['show', `${commit}:${file}`], { encoding: 'utf8' }))
const storyId = name => `sw-header--${name.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()}`
const approvedDeclarations = { ResponsiveMore: '170a5c23aed10f8fc65e5b20e9b94bd3c1ff1227', WindowMoveMotion: '2b0e1a2262002aedb01c29c5d18ffae1219bf3ef', KGuide: '2b0e1a2262002aedb01c29c5d18ffae1219bf3ef' } // measured More section; verification tag
const movedDeclarations = { PropertyPanelCompare: ['src/sw/sw-property-panel.stories.tsx', '2b0e1a2262002aedb01c29c5d18ffae1219bf3ef'] }
const approvedDeclarations = approvedHeaderDeclarations, movedDeclarations = movedHeaderDeclarations
const movedNow = Object.fromEntries(await Promise.all(Object.entries(movedDeclarations).map(async ([name, [file]]) => [name, stories(await readFile(file, 'utf8'))[name]])))
const declarationIssues = Object.keys(oldStories).filter(k => k !== 'Overview' && !retiredExports.includes(k)).filter(k => {
if (movedDeclarations[k]) return !!newStories[k] || movedNow[k] !== gitStories(movedDeclarations[k][1], movedDeclarations[k][0])[k]
+29
View File
@@ -38,3 +38,32 @@ export const integratedChangedFiles = [
'packages/sw/src/components/sw-header/SWHeader.tsx', 'packages/sw/src/components/sw-header/index.ts', 'packages/sw/src/components/sw-header/types.ts',
...['SWHeaderDocumentTitle', 'SWHeaderDropdown', 'SWHeaderFields', 'SWHeaderLayerMenu', 'SWHeaderLineStyleMenu', 'SWHeaderMore', 'SWHeaderPropertyFields', 'SWHeaderPropertyPanel'].map(name => `packages/sw/src/components/sw-header/components/${name}.tsx`),
]
// 2026-10-20 (test alignment): approved SW Story changes after the unused-demo removal that the lists above do not name.
// Each comes from a user-requested commit merged into main as 553e4df. Regressions accept exactly these changes.
export const approvedMainCommit = '553e4df4a865aaf6514b1c2b86cb2e3e9a658e5f'
// 170a5c2: the SW/Header sidebar page of the home menu (the moved verification Stories are movedHomeMenuIds).
export const homeMenuSidebarIds = ['sw-header--home-menu']
// 6cf8854 SW Footer (current IDs after retiredFooterIds), 7f42923 viewer mode, 41e963a theme inheritance.
export const footerStoryIds = ['full-footer', 'tool-groups', 'figma-variants', 'custom-content', 'responsive', 'parts'].map(id => `sw-footer--${id}`)
export const viewerStoryIds = ['viewer-header', 'menu-groups', 'states', 'dropdowns', 'settings-panel', 'content-variants', 'variable-size', 'responsive', 'compare-with-sw-header', 'logo'].map(id => `sw-viewer--${id}`)
export const themeStoryIds = ['sw-theme--theme-inheritance']
// 2b0e1a2 sidebar simplification: ID and direct URL kept, hidden from the sidebar by the verification tag.
export const verificationTaggedIds = ['sw-header--window-move-motion', 'sw-header--k-guide']
// Every approved SW Story ID added after the unused-demo removal, as the current index lists them.
export const approvedAddedStoryIds = [...integratedStoryIds.filter(id => !retiredToolIds.includes(id)), ...homeMenuSidebarIds,
...Object.values(movedHomeMenuIds).filter(id => !retiredHomeMenuCompareIds.includes(id)), ...footerStoryIds, ...viewerStoryIds, ...themeStoryIds]
// An earlier index entry with the approved tag / Popup move applied (2b0e1a2; the moved Story has no component path).
export const withApprovedEntryChanges = entry => {
if (verificationTaggedIds.includes(entry.id)) return { ...entry, tags: [...entry.tags, 'verification'] }
if (!movedToPopupIds.includes(entry.id)) return entry
const { componentPath, ...rest } = entry
return { ...rest, title: 'SW/Popup', importPath: './src/sw/sw-property-panel.stories.tsx' }
}
// src/sw/sw-header.stories.tsx declarations changed or added by approved commits (compared with that commit's version)
// and the Popup move (compared in its new file).
export const approvedHeaderDeclarations = {
Overview: '170a5c23aed10f8fc65e5b20e9b94bd3c1ff1227', ResponsiveMore: '170a5c23aed10f8fc65e5b20e9b94bd3c1ff1227', // home menu, measured More
WindowMoveMotion: '2b0e1a2262002aedb01c29c5d18ffae1219bf3ef', KGuide: '2b0e1a2262002aedb01c29c5d18ffae1219bf3ef', // verification tag
PropertyPanelComparePage: '2b0e1a2262002aedb01c29c5d18ffae1219bf3ef', // exported for the moved Popup Story
}
export const movedHeaderDeclarations = { PropertyPanelCompare: ['src/sw/sw-property-panel.stories.tsx', '2b0e1a2262002aedb01c29c5d18ffae1219bf3ef'] }
@@ -7,7 +7,7 @@ import { execFileSync } from 'node:child_process'
import path from 'node:path'
import http from 'node:http'
import { parse } from '@babel/parser'
import { swStoryEntries, retiredUnusedDemoIds, integratedStoryIds, integratedChangedFiles, movedToPopupIds } from './storybook-inventory.mjs'
import { swStoryEntries, retiredUnusedDemoIds, integratedChangedFiles, movedToPopupIds, approvedMainCommit, approvedAddedStoryIds, withApprovedEntryChanges, approvedHeaderDeclarations, movedHeaderDeclarations } from './storybook-inventory.mjs'
import { loadPlaywright } from './playwright-runtime.mjs'
const out = path.resolve(process.argv[2] ?? 'docs/sw/storybook-unused-story-removal-qa/verify')
@@ -26,14 +26,21 @@ const beforeSW = swStoryEntries(beforeIndex), afterSW = swStoryEntries(afterInde
const expected = beforeSW.filter(e => !retiredUnusedDemoIds.includes(e.id))
report.data.inventory = { before: [stories(beforeIndex).length, beforeSW.length], after: [stories(afterIndex).length, afterSW.length], sw: afterSW.map(e => [e.id, e.name]) }
await writeFile(path.join(out, 'story-inventory-after.json'), JSON.stringify({ total: stories(afterIndex).length, sw: afterSW.map(e => ({ id: e.id, name: e.name, title: e.title, tags: e.tags })) }, null, 2) + '\n')
// 2026-10-19 integrated improvement: 329 + its 5 Stories (2 under SW/Header, SW/Tool, SW/Popup); the 10 retained SW/Header Stories stay.
// 2026-10-20 (test alignment): full-index-before.json (c8e5fbe) stays the baseline. After the nine removals (338 -> 329,
// 19 -> 10 SW Stories) the index may differ only by the approved changes of storybook-inventory.mjs: the added SW Stories
// (integrated improvement, home menu, Footer, Viewer, Theme), the verification tags, the Popup move and the SW sidebar order.
const added = stories(afterIndex).filter(e => !beforeIndex.entries[e.id])
const retainedSW = afterSW.filter(e => !integratedStoryIds.includes(e.id))
check('inventory: 19 -> 10 retained SW/Header Stories, 338 -> 329 total plus only the integrated-improvement Stories', beforeSW.length === 19 && retainedSW.length === 10 && stories(beforeIndex).length === 338 && stories(afterIndex).length === 329 + integratedStoryIds.length && JSON.stringify(added.map(e => e.id).sort()) === JSON.stringify([...integratedStoryIds].sort()), { ...report.data.inventory, added: added.map(e => e.id) })
const retainedSW = Object.values(afterIndex.entries).filter(e => expected.some(entry => entry.id === e.id))
check('inventory: 19 -> 10 retained SW Stories, 338 -> 329 total plus only the approved added Stories', beforeSW.length === 19 && expected.length === 10 && retainedSW.length === 10 && stories(beforeIndex).length === 338 && stories(afterIndex).length === 329 + approvedAddedStoryIds.length && JSON.stringify(added.map(e => e.id).sort()) === JSON.stringify([...approvedAddedStoryIds].sort()), { ...report.data.inventory, added: added.map(e => e.id), unapproved: added.map(e => e.id).filter(id => !approvedAddedStoryIds.includes(id)), missing: approvedAddedStoryIds.filter(id => !afterIndex.entries[id]) })
check('inventory: the nine requested IDs existed before and are absent now', retiredUnusedDemoIds.length === 9 && retiredUnusedDemoIds.every(id => beforeIndex.entries[id] && !afterIndex.entries[id]))
check('inventory: retained SW IDs, names, titles, tags and order unchanged', sig(expected) === sig(retainedSW), retainedSW.map(e => e.id))
const others = index => Object.values(index.entries).filter(e => !retiredUnusedDemoIds.includes(e.id) && !integratedStoryIds.includes(e.id) && !/^sw-(tool|popup)--docs$/.test(e.id))
check('inventory: every non-removed index entry identical (docs and other packages included)', JSON.stringify(others(beforeIndex)) === JSON.stringify(others(afterIndex)))
check('inventory: retained SW IDs, names, titles, tags and order unchanged except the approved tags and Popup move', sig(expected.map(withApprovedEntryChanges)) === sig(retainedSW), retainedSW.map(e => [e.id, e.title, e.tags]))
// Entries outside SW keep their values and order; retained SW entries keep their values (order checked above), and the SW
// block sits right after Center Graphic (2b0e1a2 storySort: SW > Header, Footer, Popup, Viewer, Theme).
const others = index => Object.values(index.entries).filter(e => !retiredUnusedDemoIds.includes(e.id) && !approvedAddedStoryIds.includes(e.id) && !/^sw-(tool|popup)--docs$/.test(e.id))
const outsideSW = list => JSON.stringify(list.filter(e => !e.id.startsWith('sw-')))
const keys = Object.keys(afterIndex.entries), firstSW = keys.findIndex(id => id.startsWith('sw-'))
const swOrder = { previous: keys[firstSW - 1], afterCenterGraphic: afterIndex.entries[keys[firstSW - 1]]?.title.startsWith('Center Graphic/') && !keys.slice(firstSW + 1).some(id => afterIndex.entries[id].title.startsWith('Center Graphic/')), contiguous: keys.filter(id => id.startsWith('sw-')).every((id, i) => keys[firstSW + i] === id) }
check('inventory: every non-removed index entry identical (docs and other packages included) except the approved SW changes', outsideSW(others(beforeIndex)) === outsideSW(others(afterIndex)) && JSON.stringify(others(beforeIndex).filter(e => e.id.startsWith('sw-')).map(withApprovedEntryChanges)) === JSON.stringify(others(beforeIndex).filter(e => e.id.startsWith('sw-')).map(e => afterIndex.entries[e.id])) && swOrder.afterCenterGraphic && swOrder.contiguous, swOrder)
check('inventory: Dropdown keeps sw-header--composed-controls / Dropdown / SW/Header', afterIndex.entries['sw-header--composed-controls']?.name === 'Dropdown' && afterIndex.entries['sw-header--composed-controls']?.title === 'SW/Header')
check('inventory: comparison keeps its ID and name', afterIndex.entries['sw-header--property-panel-compare']?.name === 'Property Panel · 4개 비교')
@@ -45,14 +52,22 @@ const oldExports = exports(execFileSync('git', ['show', `${baseCommit}:${sourceP
const newExports = exports((await readFile(sourcePath, 'utf8')).replaceAll('\r\n', '\n'))
const removedExports = ['PropertyFields', 'PropertyShapeSlot', 'Composition', 'Colorize', 'DynamicControls', 'PropertyBar', 'AdditionalHover', 'PropertyPanel', 'LayerDetailInput']
check('source: the nine Story exports are removed', removedExports.every(name => oldExports[name] && !newExports[name]))
const changed = Object.keys(oldExports).filter(name => !removedExports.includes(name) && oldExports[name] !== newExports[name])
check('source: retained exports (Stories and ComposedDemo) byte-identical', changed.length === 0 && Object.keys(newExports).every(name => oldExports[name]), changed)
// The SW package may change only in the files of the 2026-10-19 integrated improvement (verified by its own suites);
// other packages, Overview, Dropdown CSF, assets and Storybook config stay untouched.
// 2026-10-20 (test alignment): an export changed or added after c8e5fbe must equal its version at the approving commit;
// the moved Popup Story is compared in its new file. All other retained exports stay byte-identical to c8e5fbe.
const exportsAt = (commit, file) => exports(execFileSync('git', ['show', `${commit}:${file}`], { encoding: 'utf8' }).replaceAll('\r\n', '\n'))
const movedNow = Object.fromEntries(await Promise.all(Object.entries(movedHeaderDeclarations).map(async ([name, [file]]) => [name, exports((await readFile(file, 'utf8')).replaceAll('\r\n', '\n'))[name]])))
const approvedExport = name => approvedHeaderDeclarations[name] && newExports[name] === exportsAt(approvedHeaderDeclarations[name], sourcePath)[name]
const movedExport = name => movedHeaderDeclarations[name] && !newExports[name] && movedNow[name] === exportsAt(movedHeaderDeclarations[name][1], movedHeaderDeclarations[name][0])[name]
const changed = Object.keys(oldExports).filter(name => !removedExports.includes(name) && oldExports[name] !== newExports[name] && !approvedExport(name) && !movedExport(name))
const unapprovedNew = Object.keys(newExports).filter(name => !oldExports[name] && !approvedExport(name))
check('source: retained exports (Stories and ComposedDemo) byte-identical to c8e5fbe or their approving commit; only approved new or moved exports', changed.length === 0 && unapprovedNew.length === 0, { changed, unapprovedNew })
// Other packages, Overview, Dropdown CSF, assets and Storybook config, and the SW package outside the integrated-improvement
// files (verified by their own suites), may differ from the approved main merge in nothing. sinceBaseline lists the approved
// changes after c8e5fbe; update approvedMainCommit (storybook-inventory.mjs) on the next approved merge.
const untouched = ['packages/sw/src', 'packages/sw/ai', 'packages/sw/package.json', 'packages/sw/README.md', 'packages/ui', 'packages/org-chart', 'packages/org-chart-react', 'packages/center-graphic', 'src/sw/sw-header-overview.tsx', 'src/sw/sw-header-dropdown.stories.tsx', 'src/sw/sw-composed-controls.css', 'src/sw/assets', '.storybook']
const diff = execFileSync('git', ['diff', '--name-only', baseCommit, '--', ...untouched], { encoding: 'utf8' }).trim().split(/\r?\n/).filter(Boolean)
const unexpected = diff.filter(file => !integratedChangedFiles.includes(file))
check('source: other packages, Overview, Dropdown CSF, assets and Storybook config untouched; SW package only within the integrated-improvement files', unexpected.length === 0, { unexpected, integrated: diff.length })
const changedSince = ref => execFileSync('git', ['diff', '--name-only', ref, '--', ...untouched], { encoding: 'utf8' }).trim().split(/\r?\n/).filter(Boolean)
const unexpected = changedSince(approvedMainCommit).filter(file => !integratedChangedFiles.includes(file))
check('source: other packages, Overview, Dropdown CSF, assets and Storybook config unchanged since the approved main merge; SW package only within the integrated-improvement files', unexpected.length === 0, { unexpected, sinceBaseline: changedSince(baseCommit).length })
// ---- Browser
const root = path.resolve('storybook-static'), mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.gif': 'image/gif', '.ttf': 'font/ttf', '.woff2': 'font/woff2' }