From 20ed2f2afb42e84d37263c73820c16179a9d4c7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EC=88=98=ED=98=84?= Date: Tue, 20 Oct 2026 23:04:14 +0900 Subject: [PATCH] test(sw): align storybook inventory checks with approved changes --- packages/sw/tests/overview.browser.mjs | 17 +++----- packages/sw/tests/storybook-inventory.mjs | 29 +++++++++++++ .../storybook-unused-removal.browser.mjs | 43 +++++++++++++------ 3 files changed, 64 insertions(+), 25 deletions(-) diff --git a/packages/sw/tests/overview.browser.mjs b/packages/sw/tests/overview.browser.mjs index e78cecc..045e289 100644 --- a/packages/sw/tests/overview.browser.mjs +++ b/packages/sw/tests/overview.browser.mjs @@ -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] diff --git a/packages/sw/tests/storybook-inventory.mjs b/packages/sw/tests/storybook-inventory.mjs index ce4a81a..2c2155c 100644 --- a/packages/sw/tests/storybook-inventory.mjs +++ b/packages/sw/tests/storybook-inventory.mjs @@ -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'] } diff --git a/packages/sw/tests/storybook-unused-removal.browser.mjs b/packages/sw/tests/storybook-unused-removal.browser.mjs index fe87eb1..347cfe2 100644 --- a/packages/sw/tests/storybook-unused-removal.browser.mjs +++ b/packages/sw/tests/storybook-unused-removal.browser.mjs @@ -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' }