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

@@ -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' }