test(sw): align regression tests with current components
This commit is contained in:
1 parent
3cbe879910
commit
b01ccaca01
5 files changed
+282
-31
No files matched your search
+198
@@ -0,0 +1,198 @@
|
|||||||
|
// Test-only host for property-fields.browser.mjs (2026-11-10). Not a Storybook Story and not shipped (package "files" is
|
||||||
|
// dist + ai). It restores the host data and host controls of the two demos removed in 5faa1f1 that the field checks
|
||||||
|
// need: SW/Header › Property Fields (data-fields live / default / pinned hover) and the Header-hosted Property Panel
|
||||||
|
// (원 1개 selection). Records and controls are copied from src/sw/sw-header.stories.tsx at 5faa1f1^ without changes.
|
||||||
|
// Uses only the public @hm-design/sw API, like a consuming project. The pinned Hover column uses the Storybook pseudo-state
|
||||||
|
// mirror (.storybook/pseudo-states.ts) exactly as the retired Story did.
|
||||||
|
import { useEffect, useRef, useState, type ReactNode } from "react"
|
||||||
|
import { createRoot } from "react-dom/client"
|
||||||
|
import {
|
||||||
|
SWHeader, SWHeaderPropertyPanel, SWTheme, swHeaderLinePatterns,
|
||||||
|
type SWHeaderColorMode, type SWHeaderColorOption, type SWHeaderLayer, type SWHeaderLineType, type SWHeaderLineWeight,
|
||||||
|
type SWHeaderProperty, type SWHeaderPropertyGroup, type SWHeaderPropertyMenus, type SWHeaderPropertySelection,
|
||||||
|
} from "@hm-design/sw"
|
||||||
|
import "@hm-design/sw/styles.css"
|
||||||
|
import { ensurePseudoStates } from "../../../../.storybook/pseudo-states"
|
||||||
|
|
||||||
|
const FIGMA_COLORS: SWHeaderColorOption[] = [
|
||||||
|
{ id: "white", color: "#ffffff", label: "흰색" },
|
||||||
|
{ id: "red", color: "#f10404", label: "빨강" },
|
||||||
|
{ id: "green", color: "#48ff08", label: "초록" },
|
||||||
|
{ id: "yellow", color: "#ffff00", label: "노랑" },
|
||||||
|
{ id: "blue", color: "#0012ff", label: "파랑" },
|
||||||
|
{ id: "magenta", color: "#ff00f6", label: "자홍" },
|
||||||
|
{ id: "cyan", color: "#00f6ff", label: "청록" },
|
||||||
|
]
|
||||||
|
const FIGMA_LINE_TYPES: SWHeaderLineType[] = [
|
||||||
|
{ id: "byblock", name: "ByBlock" },
|
||||||
|
{ id: "bylayer", name: "ByLayer" },
|
||||||
|
{ id: "continuous", name: "Continuous" },
|
||||||
|
{ id: "center", name: "CENTER", pattern: swHeaderLinePatterns.center },
|
||||||
|
{ id: "hidden", name: "HIDDEN", pattern: swHeaderLinePatterns.hidden },
|
||||||
|
]
|
||||||
|
const FIGMA_LINE_WEIGHTS: SWHeaderLineWeight[] = [
|
||||||
|
{ id: "w-bylayer", name: "ByLayer" },
|
||||||
|
{ id: "w-byblock", name: "ByBlock" },
|
||||||
|
{ id: "w-default", name: "ByDefault", value: "0.25" },
|
||||||
|
{ id: "w-user", name: "User", value: "0.25", editable: true },
|
||||||
|
]
|
||||||
|
const PROPERTY_GROUPS: SWHeaderPropertyGroup[] = [
|
||||||
|
{ id: "common", collapsedCount: 6 },
|
||||||
|
{ id: "shape", name: "형상", layout: "circle" },
|
||||||
|
{ id: "position", name: "위치", layout: "position", positionLabel: "중간점" },
|
||||||
|
]
|
||||||
|
const PROPERTY_FIXTURE: SWHeaderProperty[] = [
|
||||||
|
{ id: "layer", name: "레이어", value: "0_Layer_Cludmark", type: "text", groupId: "common", picker: "layer", validate: draft => draft.length <= 40 },
|
||||||
|
{ id: "color", name: "색상", value: "255,229,0", type: "color", groupId: "common" },
|
||||||
|
{ id: "line", name: "선스타일", value: "Continuous", type: "select", groupId: "common", unit: "(ByL)", linePreview: true, options: [{ id: "Continuous", name: "Continuous" }, { id: "CENTER", name: "CENTER" }, { id: "HIDDEN", name: "HIDDEN" }] },
|
||||||
|
{ id: "weight", name: "선가중치", value: "0.25", type: "number", groupId: "common", min: 0 },
|
||||||
|
{ id: "scale", name: "선축적", value: "1.0000", type: "number", groupId: "common", min: .0001, decimals: 4, unit: "(LTscale:1)" },
|
||||||
|
{ id: "opacity", name: "투명도", value: "0.25", type: "slider", groupId: "common", min: 0, max: 1, step: .01, annotation: "(ByL)", unit: "%" },
|
||||||
|
{ id: "link", name: "하이퍼링크", value: "--", type: "text", groupId: "common" },
|
||||||
|
{ id: "thickness", name: "두께", value: "0.25", type: "number", groupId: "common", unit: "mm", min: 0 },
|
||||||
|
{ id: "material", name: "재료", value: "-", type: "computed", groupId: "common", unit: "(ByL)" },
|
||||||
|
{ id: "circumference", name: "원주", value: "0.25", type: "computed", groupId: "shape", graphicSlot: "circumference" },
|
||||||
|
{ id: "diameter", name: "지름", value: "8.1502", type: "number", groupId: "shape", graphicSlot: "diameter", min: 0 },
|
||||||
|
{ id: "radius", name: "반지름", value: "4.0526", type: "number", groupId: "shape", graphicSlot: "radius", min: 0 },
|
||||||
|
{ id: "area", name: "면적", value: "51.5962", type: "computed", groupId: "shape", graphicSlot: "area" },
|
||||||
|
{ id: "x", name: "X", value: "48.0808", type: "number", groupId: "position" },
|
||||||
|
{ id: "y", name: "Y", value: "14.9160", type: "number", groupId: "position" },
|
||||||
|
{ id: "z", name: "Z", value: "0.0000", type: "number", groupId: "position" },
|
||||||
|
]
|
||||||
|
const PROPERTY_COUNTS = [
|
||||||
|
{ id: "shape", name: "도형", count: 10, row: 1, column: 1 }, { id: "line", name: "직선", count: 10, row: 1, column: 2 },
|
||||||
|
{ id: "circle", name: "원", count: 1, row: 1, column: 3 }, { id: "ray", name: "광선", count: 1, row: 1, column: 4 },
|
||||||
|
{ id: "curve", name: "곡선", count: 10, row: 2, column: 3 }, { id: "arc", name: "호", count: 1, row: 2, column: 4 },
|
||||||
|
{ id: "polyline", name: "폴리선", count: 10, row: 3, column: 3 }, { id: "polyline2", name: "폴리선", count: 2, row: 3, column: 4 },
|
||||||
|
{ id: "text1", name: "문자", count: 1, row: 4, column: 1 }, { id: "text2", name: "문자", count: 1, row: 4, column: 2 },
|
||||||
|
{ id: "text3", name: "문자", count: 1, row: 4, column: 3 },
|
||||||
|
]
|
||||||
|
type PropertyExample = "none" | "all" | "circle" | "expanded"
|
||||||
|
function propertySelection(example: PropertyExample): SWHeaderPropertySelection {
|
||||||
|
return example === "none" ? { id: "none", title: "선택된 객체 - 없음", kind: "none" }
|
||||||
|
: example === "all" ? { id: "all", title: "선택된 객체 - 전체 11", kind: "all", counts: PROPERTY_COUNTS }
|
||||||
|
: { id: "circle", title: "선택된 객체 - 원1", kind: "single", overviewTitle: "선택된 객체 - 전체 11" }
|
||||||
|
}
|
||||||
|
function propertyFixture(example: PropertyExample) {
|
||||||
|
return PROPERTY_FIXTURE.filter(item => (example === "none" || example === "all") ? item.groupId === "common" : true).map(item => ({
|
||||||
|
...item, ...(example === "none" && item.id === "layer" ? { value: "0" } : {}),
|
||||||
|
...(example === "none" && item.id === "color" ? { value: "138,138,138" } : {}),
|
||||||
|
...(example === "all" && item.id === "layer" ? { mixed: true } : {}),
|
||||||
|
...((example === "none" || example === "all") && item.id === "opacity" ? { value: "0" } : {}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
const PROPERTY_LAYERS: SWHeaderLayer[] = [
|
||||||
|
{ id: "0", name: "0" },
|
||||||
|
{ id: "0_Layer_Cludmark", name: "0_Layer_Cludmark", color: "#ff0000" },
|
||||||
|
{ id: "CC-D GNL", name: "CC-D GNL" },
|
||||||
|
{ id: "Sample-Wall", name: "Sample-Wall", color: "#ffe500", visible: false },
|
||||||
|
{ id: "Sample-Door", name: "Sample-Door", color: "#00ff00", frozen: true },
|
||||||
|
{ id: "Sample-Text", name: "Sample-Text", color: "#00ffff", locked: true },
|
||||||
|
{ id: "Sample-Defpoints", name: "Sample-Defpoints", color: "#808080", disabled: true },
|
||||||
|
]
|
||||||
|
const PROPERTY_LINE_TYPES: SWHeaderLineType[] = FIGMA_LINE_TYPES.map(item => ({ ...item, id: item.name }))
|
||||||
|
const linePatternOf = (value: string) => PROPERTY_LINE_TYPES.find(item => item.id === value)?.pattern
|
||||||
|
function usePropertyMenus(record: (line: string) => void, setProperties: (update: (items: SWHeaderProperty[]) => SWHeaderProperty[]) => void): SWHeaderPropertyMenus {
|
||||||
|
const [layers, setLayers] = useState(PROPERTY_LAYERS)
|
||||||
|
const [weights, setWeights] = useState(FIGMA_LINE_WEIGHTS)
|
||||||
|
const [weightId, setWeightId] = useState("w-default")
|
||||||
|
const [colorMode, setColorMode] = useState<SWHeaderColorMode | null>(null)
|
||||||
|
const setValue = (id: string, value: string) => setProperties(items => items.map(item => item.id === id ? { ...item, value, mixed: false } : item))
|
||||||
|
const scale = "1.0000"
|
||||||
|
const toggle = (key: "visible" | "frozen" | "locked") => (layer: SWHeaderLayer, next: boolean) => {
|
||||||
|
record(`layer.${key}(${layer.id}, ${next})`)
|
||||||
|
setLayers(items => items.map(item => item.id === layer.id ? { ...item, [key]: next } : item))
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
layers, onLayerVisibilityChange: toggle("visible"), onLayerFreezeChange: toggle("frozen"), onLayerLockChange: toggle("locked"),
|
||||||
|
onLayerDetail: () => record("layerDetail"),
|
||||||
|
lineTypes: PROPERTY_LINE_TYPES, lineWeights: weights, activeLineWeightId: weightId, lineScale: scale,
|
||||||
|
onLineWeightSelect: weight => { record(`lineWeight(${weight.id})`); setWeightId(weight.id); if (weight.value) setValue("weight", weight.value) },
|
||||||
|
onLineWeightValueChange: (weight, value) => { record(`lineWeightValue(${weight.id}, ${value})`); setWeights(items => items.map(item => item.id === weight.id ? { ...item, value } : item)) },
|
||||||
|
onLineScaleChange: value => { record(`lineScale(${value})`); setValue("scale", Number(value).toFixed(4)) },
|
||||||
|
onLineStyleDetail: () => record("lineStyleDetail"),
|
||||||
|
colors: FIGMA_COLORS, colorMode,
|
||||||
|
onColorModeChange: mode => { record(`colorMode(${mode})`); setColorMode(mode) },
|
||||||
|
onColorPick: property => record(`colorPick(${property.id})`),
|
||||||
|
onColorDetail: () => record("colorDetail"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function PinHover({ selectors, children }: { selectors: string[]; children: ReactNode }) {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
ensurePseudoStates()
|
||||||
|
ref.current?.querySelectorAll(selectors.join(",")).forEach(element => element.setAttribute("data-sb-hover", ""))
|
||||||
|
}, [selectors])
|
||||||
|
return <div ref={ref}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SW/Header › Property Fields (retired in 5faa1f1): the common rows of PROPERTY_FIXTURE plus a mixed layer row. Figma
|
||||||
|
sample text 255,259,0 is out of range: shown as host text only, never accepted as input. */
|
||||||
|
const FIELD_FIXTURE: SWHeaderProperty[] = PROPERTY_FIXTURE.filter(item => item.groupId === "common").flatMap(item =>
|
||||||
|
item.id === "layer" ? [{ ...item, id: "layer-mixed", mixed: true }, item]
|
||||||
|
: item.id === "color" ? [{ ...item, value: "255,259,0", swatch: "#ffe500" }]
|
||||||
|
: [item])
|
||||||
|
const FIELD_HOVER = [".hm-sw-property__value[data-field]", ".hm-sw-property__compact"]
|
||||||
|
function FieldPanel({ title, properties, onChange, menus }: { title: string; properties: SWHeaderProperty[]; onChange?: (property: SWHeaderProperty, value: string) => void; menus?: SWHeaderPropertyMenus }) {
|
||||||
|
return <SWTheme><SWHeaderPropertyPanel selection={{ id: title, title, kind: "none" }} properties={properties} groups={[{ id: "common" }]} onPropertyChange={onChange} menus={menus} /></SWTheme>
|
||||||
|
}
|
||||||
|
function PropertyFieldsHost() {
|
||||||
|
const [properties, setProperties] = useState(FIELD_FIXTURE)
|
||||||
|
const [log, setLog] = useState<string[]>([])
|
||||||
|
const record = (line: string) => setLog(lines => [...lines.slice(-11), line])
|
||||||
|
const menus = usePropertyMenus(record, setProperties as (update: (items: SWHeaderProperty[]) => SWHeaderProperty[]) => void)
|
||||||
|
const [noMenus, setNoMenus] = useState(false)
|
||||||
|
const patch = (id: string, change: Partial<SWHeaderProperty>) => setProperties(items => items.map(item => item.id === id ? { ...item, ...change } : item))
|
||||||
|
const change = (property: SWHeaderProperty, value: string) => {
|
||||||
|
record(`property(${property.id}, ${value})`)
|
||||||
|
patch(property.id, { value, mixed: false, ...(property.id === "line" ? { linePattern: linePatternOf(value) } : {}), ...(property.id === "color" ? { swatch: undefined } : {}) })
|
||||||
|
}
|
||||||
|
return <div data-fields-demo="">
|
||||||
|
<div><strong>외부 데이터</strong>
|
||||||
|
<button data-action="layer-mixed" onClick={() => patch("layer-mixed", { mixed: !properties.find(item => item.id === "layer-mixed")?.mixed })}>레이어 다양함 ↔ 단일</button>
|
||||||
|
<button data-action="layer-external" onClick={() => patch("layer", { value: "CC-D GNL" })}>레이어 외부 값 변경</button>
|
||||||
|
<button data-action="scale-external" onClick={() => patch("scale", { value: "2.5000" })}>선축적 외부 값 변경</button>
|
||||||
|
<button data-action="thickness-unit" onClick={() => patch("thickness", { unit: properties.find(item => item.id === "thickness")?.unit === "mm" ? "cm" : "mm" })}>두께 단위 변경</button>
|
||||||
|
<button data-action="line-center" onClick={() => patch("line", { value: "CENTER", linePattern: linePatternOf("CENTER") })}>선스타일 외부 CENTER</button>
|
||||||
|
<button data-action="readonly" onClick={() => setProperties(items => items.map(item => item.id === "thickness" || item.id === "layer" ? { ...item, readOnly: !item.readOnly } : item))}>두께·레이어 ReadOnly</button>
|
||||||
|
<button data-action="disabled" onClick={() => setProperties(items => items.map(item => item.id === "scale" || item.id === "color" ? { ...item, disabled: !item.disabled } : item))}>선축적·색상 Disabled</button>
|
||||||
|
<button data-action="add-row" onClick={() => setProperties(items => [...items, { id: `extra-${items.length}`, name: "추가 속성", value: "호스트 값", type: "text", groupId: "common" }])}>속성 추가</button>
|
||||||
|
<button data-action="remove-row" onClick={() => setProperties(items => items.filter(item => item.id !== "link"))}>하이퍼링크 삭제</button>
|
||||||
|
<button data-action="no-menus" onClick={() => setNoMenus(value => !value)}>menus 없음(options 사용): {String(noMenus)}</button>
|
||||||
|
</div>
|
||||||
|
<p data-property-log="" aria-live="polite">{log.join(" → ") || "Callback 없음"}</p>
|
||||||
|
<div style={{ display: "flex", gap: 24, alignItems: "flex-start" }}>
|
||||||
|
<figure style={{ margin: 0 }} data-fields="live"><FieldPanel title="실제 조작" properties={properties} onChange={change} menus={noMenus ? undefined : menus} /></figure>
|
||||||
|
<figure style={{ margin: 0 }} data-fields="default"><FieldPanel title="Default" properties={FIELD_FIXTURE} /></figure>
|
||||||
|
<figure style={{ margin: 0 }} data-fields="hover"><PinHover selectors={FIELD_HOVER}><FieldPanel title="Hover" properties={FIELD_FIXTURE} /></PinHover></figure>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
/* SW/Header › Property Panel (retired in 5faa1f1): SWHeader with the propertyPanel prop and the selection buttons. */
|
||||||
|
function HeaderPanelHost() {
|
||||||
|
const [selection, setSelection] = useState(propertySelection("none"))
|
||||||
|
const [properties, setProperties] = useState(propertyFixture("none"))
|
||||||
|
const [expanded, setExpanded] = useState<string[]>([])
|
||||||
|
const menus = usePropertyMenus(() => {}, setProperties)
|
||||||
|
const changeExample = (next: PropertyExample) => {
|
||||||
|
setSelection(propertySelection(next)); setProperties(propertyFixture(next))
|
||||||
|
setExpanded(next === "expanded" ? ["common"] : [])
|
||||||
|
}
|
||||||
|
const panel = {
|
||||||
|
selection, properties, groups: PROPERTY_GROUPS, expandedGroupIds: expanded, menus,
|
||||||
|
onExpandedGroupsChange: setExpanded,
|
||||||
|
onPropertyChange: (property: SWHeaderProperty, value: string) => setProperties(items => items.map(item => item.id === property.id ? { ...item, value, mixed: false } : item)),
|
||||||
|
onOverviewClick: () => changeExample("all"),
|
||||||
|
}
|
||||||
|
return <div data-property-demo="">
|
||||||
|
<div>{(["none", "all", "circle", "expanded"] as const).map((name, index) => <button key={name} data-example-button={name} onClick={() => changeExample(name)}>{["선택 없음", "전체 선택", "원 1개", "모두 펼침"][index]}</button>)}</div>
|
||||||
|
<div style={{ width: 1920, minHeight: 820 }}>
|
||||||
|
<SWHeader documentTitle="C0020101-006-평면및종단면도(06)" drawingModeLabel="그리기모드" layer={{ name: "0_Layer_Cludmark" }} color={{ value: "255,229,0", swatch: "#ffe500" }}
|
||||||
|
lineStyle={{ name: "Continuous", weight: "0.25", scale: "1.00" }} textStyle={{ font: "Arial", name: "Standard" }} dimensionStyle={{ name: "Standard" }} propertyPanel={panel} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = new URLSearchParams(location.search).get("view")
|
||||||
|
createRoot(document.getElementById("root")!).render(view === "header" ? <HeaderPanelHost /> : <PropertyFieldsHost />)
|
||||||
@@ -1,17 +1,34 @@
|
|||||||
// Historical since 2026-10-19: opens SW Story IDs retired by the unused demo removal (property-fields). Kept as evidence
|
|
||||||
// for its recorded run; current regression: storybook-unused-removal.browser.mjs.
|
|
||||||
// Property fields (Figma 26:12388 ~ 26:12649): Default / real pointer Hover / Mouse Leave / On (editing, dropdown),
|
// Property fields (Figma 26:12388 ~ 26:12649): Default / real pointer Hover / Mouse Leave / On (editing, dropdown),
|
||||||
// selection callbacks, keyboard, Escape, external data, Disabled / ReadOnly, and the four panel states.
|
// selection callbacks, keyboard, Escape, external data, Disabled / ReadOnly, and the four panel states.
|
||||||
|
// 2026-11-10 (test alignment): the SW/Header › Property Fields and Property Panel demos this suite opened were removed in
|
||||||
|
// 5faa1f1 (approved). Sections 1–9 and the Header-hosted panel run on the test-only fixture fixtures/property-fields.tsx,
|
||||||
|
// which restores those demos' host data and host controls with the public API (as header-composition does); the four
|
||||||
|
// panel states run on the current Story sw-header--property-panel-compare (SW/Popup › 속성창). Every check is unchanged.
|
||||||
// Usage: node packages/sw/tests/property-fields.browser.mjs <evidence-dir> <label> [storybook-url]
|
// Usage: node packages/sw/tests/property-fields.browser.mjs <evidence-dir> <label> [storybook-url]
|
||||||
// Storybook serves packages/sw/dist: build @hm-design/sw first.
|
// The fixture and Storybook use packages/sw/dist: build @hm-design/sw first.
|
||||||
import { mkdir, writeFile } from 'node:fs/promises'
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
||||||
|
import { createRequire } from 'node:module'
|
||||||
|
import http from 'node:http'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
import { loadPlaywright } from './playwright-runtime.mjs'
|
import { loadPlaywright } from './playwright-runtime.mjs'
|
||||||
const out = path.resolve(process.argv[2] ?? 'docs/sw/property-fields-qa')
|
const out = path.resolve(process.argv[2] ?? 'docs/sw/property-fields-qa')
|
||||||
const label = process.argv[3] ?? 'run'
|
const label = process.argv[3] ?? 'run'
|
||||||
const base = process.argv[4] ?? 'http://127.0.0.1:6024'
|
const base = process.argv[4] ?? 'http://127.0.0.1:6024'
|
||||||
const { chromium } = loadPlaywright()
|
const { chromium } = loadPlaywright()
|
||||||
await mkdir(out, { recursive: true })
|
await mkdir(out, { recursive: true })
|
||||||
|
// Test-only fixture page (built into the evidence directory, served only for this run).
|
||||||
|
const site = path.join(out, 'fixture')
|
||||||
|
await mkdir(site, { recursive: true })
|
||||||
|
await createRequire(import.meta.url)('esbuild').build({
|
||||||
|
entryPoints: [fileURLToPath(new URL('./fixtures/property-fields.tsx', import.meta.url))], bundle: true, outdir: site, format: 'esm', jsx: 'automatic', minify: true,
|
||||||
|
loader: { '.ttf': 'file', '.woff': 'file', '.woff2': 'file', '.svg': 'file', '.png': 'file' }, define: { 'process.env.NODE_ENV': '"production"' }, logLevel: 'warning',
|
||||||
|
})
|
||||||
|
await writeFile(path.join(site, 'index.html'), '<!doctype html><html lang="ko"><head><meta charset="utf-8"><link rel="icon" href="data:,"><link rel="stylesheet" href="property-fields.css"></head><body style="margin:0"><div id="root"></div><script type="module" src="property-fields.js"></script></body></html>')
|
||||||
|
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.ttf': 'font/ttf', '.woff2': 'font/woff2' }
|
||||||
|
const server = http.createServer(async (req, res) => { try { const p = new URL(req.url, 'http://localhost').pathname, file = path.resolve(site, '.' + decodeURIComponent(p === '/' ? '/index.html' : p)); if (path.relative(site, file).startsWith('..')) return res.writeHead(403).end(); res.setHeader('Content-Type', mime[path.extname(file)] ?? 'application/octet-stream'); res.end(await readFile(file)) } catch { res.writeHead(404).end() } })
|
||||||
|
await new Promise(resolve => server.listen(0, '127.0.0.1', resolve))
|
||||||
|
const fixture = `http://127.0.0.1:${server.address().port}/`
|
||||||
const browser = await chromium.launch({ executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
|
const browser = await chromium.launch({ executablePath: process.env.SW_CHROME_PATH ?? 'C:/Program Files/Google/Chrome/Application/chrome.exe' })
|
||||||
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 }, deviceScaleFactor: 2 })
|
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 }, deviceScaleFactor: 2 })
|
||||||
const report = { label, checks: [], errors: [] }
|
const report = { label, checks: [], errors: [] }
|
||||||
@@ -19,7 +36,7 @@ page.on('pageerror', e => report.errors.push(e.message)); page.on('console', m =
|
|||||||
const check = (name, ok, data) => { report.checks.push({ name, status: ok ? 'PASS' : 'FAIL', data }); console.log(ok ? 'PASS' : 'FAIL', name, JSON.stringify(data)) }
|
const check = (name, ok, data) => { report.checks.push({ name, status: ok ? 'PASS' : 'FAIL', data }); console.log(ok ? 'PASS' : 'FAIL', name, JSON.stringify(data)) }
|
||||||
const shot = async (name, locator) => writeFile(path.join(out, `${label}-${name}.png`), await (locator ?? page).screenshot())
|
const shot = async (name, locator) => writeFile(path.join(out, `${label}-${name}.png`), await (locator ?? page).screenshot())
|
||||||
|
|
||||||
await page.goto(`${base}/iframe.html?id=sw-header--property-fields&viewMode=story`)
|
await page.goto(fixture)
|
||||||
await page.locator('[data-fields="live"] .hm-sw-property').waitFor(); await page.evaluate(() => document.fonts.ready)
|
await page.locator('[data-fields="live"] .hm-sw-property').waitFor(); await page.evaluate(() => document.fonts.ready)
|
||||||
const live = page.locator('[data-fields="live"]')
|
const live = page.locator('[data-fields="live"]')
|
||||||
const row = id => live.locator(`.hm-sw-property__row[data-property-id="${id}"]`)
|
const row = id => live.locator(`.hm-sw-property__row[data-property-id="${id}"]`)
|
||||||
@@ -35,9 +52,13 @@ for (const id of ['layer-mixed', 'layer', 'color', 'line', 'scale', 'link', 'thi
|
|||||||
const s = await style(field(id), 'borderTopColor', 'backgroundColor', 'borderTopLeftRadius')
|
const s = await style(field(id), 'borderTopColor', 'backgroundColor', 'borderTopLeftRadius')
|
||||||
check(`Default ${id}: 200 x 28, radius 2, no border / surface`, s.w === 200 && s.h === 28 && s.borderTopColor === CLEAR && s.backgroundColor === CLEAR && s.borderTopLeftRadius === '2px', s)
|
check(`Default ${id}: 200 x 28, radius 2, no border / surface`, s.w === 200 && s.h === 28 && s.borderTopColor === CLEAR && s.backgroundColor === CLEAR && s.borderTopLeftRadius === '2px', s)
|
||||||
}
|
}
|
||||||
|
// 2026-11-10: since the user-approved 2026-10-21 design review (2fc1b3e) 선가중치 is no longer the 41px hug field but the
|
||||||
|
// 200 x 28 box like 선축적, and the 투명도 value is its own 28px box before the gauge (SWHeaderPropertyPanel.tsx). The
|
||||||
|
// same properties are checked on the current fields: size, padding (7px incl. no border), radius, no border / surface.
|
||||||
|
const compactField = id => id === 'weight' ? field('weight') : row(id).locator('.hm-sw-property__slider-value')
|
||||||
for (const id of ['weight', 'opacity']) {
|
for (const id of ['weight', 'opacity']) {
|
||||||
const s = await style(row(id).locator('.hm-sw-property__compact'), 'borderTopColor', 'backgroundColor', 'paddingLeft', 'paddingRight')
|
const s = await style(compactField(id), 'borderTopColor', 'backgroundColor', 'paddingLeft', 'paddingRight', 'borderTopLeftRadius')
|
||||||
check(`Default ${id}: compact 28px hug field (pl 6 / pr 4 incl. border), no border`, s.h === 28 && s.w < 60 && s.borderTopColor === CLEAR && s.paddingLeft === '5px' && s.paddingRight === '3px', s)
|
check(`Default ${id}: ${id === 'weight' ? '200 x 28 box' : '28px slider value box'} (pl / pr 7), radius 2, no border / surface`, s.h === 28 && (id === 'weight' ? s.w === 200 : s.w < 200) && s.borderTopColor === CLEAR && s.backgroundColor === CLEAR && s.paddingLeft === '7px' && s.paddingRight === '7px' && s.borderTopLeftRadius === '2px', s)
|
||||||
}
|
}
|
||||||
const typo = await row('scale').evaluate(r => { const v = getComputedStyle(r.querySelector('.hm-sw-header__input')), u = getComputedStyle(r.querySelector('.hm-sw-property__unit')); return { value: [v.color, v.fontSize], unit: [u.color, u.fontSize] } })
|
const typo = await row('scale').evaluate(r => { const v = getComputedStyle(r.querySelector('.hm-sw-header__input')), u = getComputedStyle(r.querySelector('.hm-sw-property__unit')); return { value: [v.color, v.fontSize], unit: [u.color, u.fontSize] } })
|
||||||
check('Typography: value #E60403 14px, unit #000 12px', typo.value.join() === 'rgb(230, 4, 3),14px' && typo.unit.join() === 'rgb(0, 0, 0),12px', typo)
|
check('Typography: value #E60403 14px, unit #000 12px', typo.value.join() === 'rgb(230, 4, 3),14px' && typo.unit.join() === 'rgb(0, 0, 0),12px', typo)
|
||||||
@@ -56,9 +77,9 @@ for (const id of ['layer-mixed', 'layer', 'color', 'line', 'scale', 'link', 'thi
|
|||||||
check(`Mouse Leave ${id}: back to Default`, left.borderTopColor === CLEAR, left)
|
check(`Mouse Leave ${id}: back to Default`, left.borderTopColor === CLEAR, left)
|
||||||
}
|
}
|
||||||
for (const id of ['weight', 'opacity']) {
|
for (const id of ['weight', 'opacity']) {
|
||||||
const c = row(id).locator('.hm-sw-property__compact')
|
const c = compactField(id)
|
||||||
await c.hover(); const s = await style(c, 'borderTopColor'); await page.mouse.move(1, 1); const l = await style(c, 'borderTopColor')
|
await c.hover(); const s = await style(c, 'borderTopColor'); await page.mouse.move(1, 1); const l = await style(c, 'borderTopColor')
|
||||||
check(`Hover / Leave ${id} compact`, s.borderTopColor === HOVER && l.borderTopColor === CLEAR, { hover: s.borderTopColor, leave: l.borderTopColor })
|
check(`Hover / Leave ${id} ${id === 'weight' ? 'box' : 'slider value box'}`, s.borderTopColor === HOVER && l.borderTopColor === CLEAR, { hover: s.borderTopColor, leave: l.borderTopColor })
|
||||||
}
|
}
|
||||||
await field('scale').hover(); await shot('hover-scale', field('scale')); await page.mouse.move(1, 1)
|
await field('scale').hover(); await shot('hover-scale', field('scale')); await page.mouse.move(1, 1)
|
||||||
|
|
||||||
@@ -78,8 +99,9 @@ check('Enter: 선축적 2 → validated, 4 decimals committed', (await log()).in
|
|||||||
await scale.fill('7'); await page.keyboard.press('Escape')
|
await scale.fill('7'); await page.keyboard.press('Escape')
|
||||||
check('Escape: draft cancelled (2.0000 restored, no callback)', await scale.inputValue() === '2.0000' && !(await log()).includes('7'), await scale.inputValue())
|
check('Escape: draft cancelled (2.0000 restored, no callback)', await scale.inputValue() === '2.0000' && !(await log()).includes('7'), await scale.inputValue())
|
||||||
const weight = row('weight').locator('input')
|
const weight = row('weight').locator('input')
|
||||||
await weight.click(); const w = await style(weight, 'backgroundColor')
|
// 2fc1b3e (2026-10-21): the editing face is on the 200px box (as 선축적), no longer on the hug field's input.
|
||||||
check('On(편집) 선가중치 compact: #0F0D0A', w.backgroundColor === 'rgb(15, 13, 10)', w)
|
await weight.click(); const w = await style(field('weight'), 'backgroundColor')
|
||||||
|
check('On(편집) 선가중치 box: #0F0D0A', w.backgroundColor === 'rgb(15, 13, 10)', w)
|
||||||
await shot('on-weight', row('weight'))
|
await shot('on-weight', row('weight'))
|
||||||
await weight.fill('-1'); await page.keyboard.press('Enter')
|
await weight.fill('-1'); await page.keyboard.press('Enter')
|
||||||
check('Invalid 선가중치 -1 (min 0): restored, no callback', await weight.inputValue() === '0.25' && !(await log()).includes('weight, -1'), await weight.inputValue())
|
check('Invalid 선가중치 -1 (min 0): restored, no callback', await weight.inputValue() === '0.25' && !(await log()).includes('weight, -1'), await weight.inputValue())
|
||||||
@@ -87,10 +109,11 @@ await weight.fill('0.5'); await page.keyboard.press('Enter')
|
|||||||
check('선가중치 0.5 committed', (await log()).includes('property(weight, 0.5)'), await log())
|
check('선가중치 0.5 committed', (await log()).includes('property(weight, 0.5)'), await log())
|
||||||
await page.mouse.click(1, 1)
|
await page.mouse.click(1, 1)
|
||||||
|
|
||||||
// 4. No invented On: 하이퍼링크 (text) focus keeps the Hover look only; 재료 (computed) is read-only.
|
// 4. 하이퍼링크 (editable text): since 6d46289 (2026-10-22, approved) its editing face is the numbers' On (#0F0D0A,
|
||||||
|
// Hover border); 재료 (computed) stays read-only without On.
|
||||||
await row('link').locator('input').click()
|
await row('link').locator('input').click()
|
||||||
const link = await style(field('link'), 'backgroundColor', 'borderTopColor')
|
const link = await style(field('link'), 'backgroundColor', 'borderTopColor')
|
||||||
check('하이퍼링크 focus: no On surface (Figma has no On), editable', link.backgroundColor === CLEAR && link.borderTopColor === HOVER, link)
|
check('하이퍼링크 focus: editing face #0F0D0A like numbers (6d46289), Hover border, editable', link.backgroundColor === 'rgb(15, 13, 10)' && link.borderTopColor === HOVER, link)
|
||||||
await row('link').locator('input').fill('http://a'); await page.keyboard.press('Enter')
|
await row('link').locator('input').fill('http://a'); await page.keyboard.press('Enter')
|
||||||
check('하이퍼링크 commit via host callback', (await log()).includes('property(link, http://a)'), await log())
|
check('하이퍼링크 commit via host callback', (await log()).includes('property(link, http://a)'), await log())
|
||||||
await row('material').locator('input').click()
|
await row('material').locator('input').click()
|
||||||
@@ -132,16 +155,20 @@ check('Keyboard: ArrowDown opens + focuses list, Escape closes to trigger', focu
|
|||||||
await layerTrigger.click(); await page.mouse.click(1300, 900)
|
await layerTrigger.click(); await page.mouse.click(1300, 900)
|
||||||
check('Outside click closes', await drop.count() === 0, null)
|
check('Outside click closes', await drop.count() === 0, null)
|
||||||
|
|
||||||
// 6. Line style (26:12388 / 26:12404): 466 = 182 + 142 + 142, right-aligned, preview follows the value.
|
// 6. Line style (26:12388 / 26:12404): right-aligned, preview follows the value. Since 2fc1b3e (2026-10-21 design
|
||||||
|
// review, DESIGN_RULES 속성창 입력) the property window list is the line type column only (preview + name; type column
|
||||||
|
// 182px, growing to 240px only for wider font names); weights / scale stay in the SWHeader line style field.
|
||||||
await field('line').locator('.hm-sw-property__trigger').click()
|
await field('line').locator('.hm-sw-property__trigger').click()
|
||||||
f = await style(field('line'), 'backgroundColor'); d = await style(drop.locator('.hm-sw-header__line-drop'))
|
f = await style(field('line'), 'backgroundColor'); d = await style(drop.locator('.hm-sw-header__line-drop'))
|
||||||
const cols = await drop.locator('.hm-sw-header__line-column').evaluateAll(es => es.map(e => +e.getBoundingClientRect().width.toFixed(2)))
|
const cols = await drop.locator('.hm-sw-header__line-column').evaluateAll(es => es.map(e => +e.getBoundingClientRect().width.toFixed(2)))
|
||||||
const unitOn = await style(field('line').locator('.hm-sw-property__unit'), 'color')
|
const unitOn = await style(field('line').locator('.hm-sw-property__unit'), 'color')
|
||||||
check('Line style On: #211D13, (ByL) #D4CBBD, 3 columns, right edges meet (or shifted to the 4px screen edge when no room)', f.backgroundColor === 'rgb(33, 29, 19)' && unitOn.color === 'rgb(212, 203, 189)' && cols.length === 3 && (Math.abs(d.x + d.w - (f.x + f.w)) < 0.5 || d.x === 4) && Math.abs(d.y - (f.y + f.h)) < 0.5, { f, d, cols })
|
const typeOnly = await drop.locator('.hm-sw-header__line-column').evaluateAll(es => es.length === 1 && es[0].classList.contains('hm-sw-header__line-column--type'))
|
||||||
check('Line style drop 466 x 190 (type column grows only for wider font names)', d.w >= 466 && d.h === 190, { w: d.w, h: d.h, cols })
|
check('Line style On: #211D13, (ByL) #D4CBBD, 1 column (line types), right edges meet (or shifted to the 4px screen edge when no room)', f.backgroundColor === 'rgb(33, 29, 19)' && unitOn.color === 'rgb(212, 203, 189)' && cols.length === 1 && typeOnly && (Math.abs(d.x + d.w - (f.x + f.w)) < 0.5 || d.x === 4) && Math.abs(d.y - (f.y + f.h)) < 0.5, { f, d, cols, typeOnly })
|
||||||
|
check('Line style drop 182–240 x 190 (type column grows only for wider font names)', d.w >= 182 && d.w <= 240 && Math.abs(d.w - cols[0]) < 0.5 && d.h === 190, { w: d.w, h: d.h, cols })
|
||||||
await shot('on-line', page)
|
await shot('on-line', page)
|
||||||
await drop.getByRole('button', { name: 'ByBlock', exact: true }).nth(1).click()
|
// No weight column: the only ByBlock entry is the line type, and no weight / scale callback was called.
|
||||||
check('Weight column: callback only, stays open', (await log()).includes('lineWeight(w-byblock)') && await drop.count() === 1, await log())
|
const byBlock = await drop.getByRole('button', { name: 'ByBlock', exact: true }).count()
|
||||||
|
check('Weight column: absent (ByBlock only as a line type), no lineWeight / lineScale callback, list stays open', byBlock === 1 && !/lineWeight|lineScale/.test(await log()) && await drop.count() === 1, { byBlock, log: await log() })
|
||||||
await drop.getByRole('button', { name: 'CENTER' }).click()
|
await drop.getByRole('button', { name: 'CENTER' }).click()
|
||||||
const segs = await field('line').locator('.hm-sw-header__line-preview > i').count()
|
const segs = await field('line').locator('.hm-sw-header__line-preview > i').count()
|
||||||
check('Line type select: property(line, CENTER), closes, preview = CENTER pattern (4 segments)', (await log()).includes('property(line, CENTER)') && await drop.count() === 0 && segs === 4, { segs })
|
check('Line type select: property(line, CENTER), closes, preview = CENTER pattern (4 segments)', (await log()).includes('property(line, CENTER)') && await drop.count() === 0 && segs === 4, { segs })
|
||||||
@@ -212,7 +239,7 @@ const want = { none: [314, 256], all: [314, 361], circle: [314, 658], expanded:
|
|||||||
check('4 panel states: 314 x 256 / 361 / 658 / 754', sizes.every(([k, w, h]) => w === want[k][0] && h === want[k][1]), sizes)
|
check('4 panel states: 314 x 256 / 361 / 658 / 754', sizes.every(([k, w, h]) => w === want[k][0] && h === want[k][1]), sizes)
|
||||||
await shot('compare', page.locator('.sw-story-compare'))
|
await shot('compare', page.locator('.sw-story-compare'))
|
||||||
// The header-hosted panel: layer field opens its list over the header page as well.
|
// The header-hosted panel: layer field opens its list over the header page as well.
|
||||||
await page.goto(`${base}/iframe.html?id=sw-header--property-panel&viewMode=story`)
|
await page.goto(`${fixture}?view=header`)
|
||||||
await page.locator('.hm-sw-header').waitFor()
|
await page.locator('.hm-sw-header').waitFor()
|
||||||
await page.getByRole('button', { name: '원 1개', exact: true }).click()
|
await page.getByRole('button', { name: '원 1개', exact: true }).click()
|
||||||
await page.locator('.hm-sw-header [data-property-panel-trigger]').click()
|
await page.locator('.hm-sw-header [data-property-panel-trigger]').click()
|
||||||
@@ -223,7 +250,7 @@ const stillOpen = await page.locator('.hm-sw-property').count()
|
|||||||
check('Header-hosted panel: layer list attached; Escape closes list only', hosted && Math.abs(hosted.right) < 0.5 && Math.abs(hosted.top) < 0.5 && stillOpen === 1, { hosted, stillOpen })
|
check('Header-hosted panel: layer list attached; Escape closes list only', hosted && Math.abs(hosted.right) < 0.5 && Math.abs(hosted.top) < 0.5 && stillOpen === 1, { hosted, stillOpen })
|
||||||
check('no console / page errors', report.errors.length === 0, report.errors)
|
check('no console / page errors', report.errors.length === 0, report.errors)
|
||||||
await writeFile(path.join(out, `${label}-result.json`), JSON.stringify(report, null, 2))
|
await writeFile(path.join(out, `${label}-result.json`), JSON.stringify(report, null, 2))
|
||||||
await browser.close()
|
await browser.close(); server.close()
|
||||||
const failed = report.checks.filter(c => c.status === 'FAIL').length
|
const failed = report.checks.filter(c => c.status === 'FAIL').length
|
||||||
console.log(`${report.checks.length - failed}/${report.checks.length} PASS`)
|
console.log(`${report.checks.length - failed}/${report.checks.length} PASS`)
|
||||||
process.exit(failed ? 1 : 0)
|
process.exit(failed ? 1 : 0)
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"package": "@hm-design/sw",
|
"package": "@hm-design/sw",
|
||||||
"approvedCommit": "8cb98b373f9e5321c8d6c032a6d2031e942dc508",
|
"approvedCommit": "3cbe87991010f08b131e8b80c9f478c3fa721004",
|
||||||
"manifest": {
|
"manifest": {
|
||||||
"name": "@hm-design/sw",
|
"name": "@hm-design/sw",
|
||||||
"exports": {
|
"exports": {
|
||||||
@@ -137,6 +137,18 @@
|
|||||||
"SWPopupRowProps": "type",
|
"SWPopupRowProps": "type",
|
||||||
"SWPopupSection": "value",
|
"SWPopupSection": "value",
|
||||||
"SWPopupSectionProps": "type",
|
"SWPopupSectionProps": "type",
|
||||||
|
"SWProcessAppearance": "type",
|
||||||
|
"SWProcessBar": "value",
|
||||||
|
"SWProcessBarProps": "type",
|
||||||
|
"SWProcessGroup": "type",
|
||||||
|
"SWProcessHeader": "value",
|
||||||
|
"SWProcessHeaderHideableControlId": "type",
|
||||||
|
"SWProcessHeaderLayer": "type",
|
||||||
|
"SWProcessHeaderProps": "type",
|
||||||
|
"SWProcessSplitButton": "value",
|
||||||
|
"SWProcessSplitButtonProps": "type",
|
||||||
|
"SWProcessSplitType": "type",
|
||||||
|
"SWProcessStep": "type",
|
||||||
"SWRecentFiles": "value",
|
"SWRecentFiles": "value",
|
||||||
"SWRecentFilesProps": "type",
|
"SWRecentFilesProps": "type",
|
||||||
"SWSelect": "value",
|
"SWSelect": "value",
|
||||||
@@ -207,7 +219,7 @@
|
|||||||
"components/sw-header/components/SWHeaderDropdown.d.ts": "06ce5aa914826b6dc85df923a24baded80d16f43aebce39845f5b18d9d996504",
|
"components/sw-header/components/SWHeaderDropdown.d.ts": "06ce5aa914826b6dc85df923a24baded80d16f43aebce39845f5b18d9d996504",
|
||||||
"components/sw-header/components/SWHeaderField.d.ts": "741fdd1b0e7da4cf8f7adeaec021eb4eb103d7312f34e80135d00c3510159901",
|
"components/sw-header/components/SWHeaderField.d.ts": "741fdd1b0e7da4cf8f7adeaec021eb4eb103d7312f34e80135d00c3510159901",
|
||||||
"components/sw-header/components/SWHeaderFields.d.ts": "43950332583b4c74231a793e18651b45494165626cc45e3dfcf50169ec189edc",
|
"components/sw-header/components/SWHeaderFields.d.ts": "43950332583b4c74231a793e18651b45494165626cc45e3dfcf50169ec189edc",
|
||||||
"components/sw-header/components/SWHeaderHome.d.ts": "aa1e56611e92f309d588fcb12c089bfb0cd336de050d9aca2f043118ba01ae54",
|
"components/sw-header/components/SWHeaderHome.d.ts": "656f93b57e670cb422946abdf711ebe779ddff41786159cdb6f4091bf366f7ee",
|
||||||
"components/sw-header/components/SWHeaderInput.d.ts": "6b379ee70b9bcbf6e9a0808ca891b86838a9bfa355bfa12f036088b26241dd9c",
|
"components/sw-header/components/SWHeaderInput.d.ts": "6b379ee70b9bcbf6e9a0808ca891b86838a9bfa355bfa12f036088b26241dd9c",
|
||||||
"components/sw-header/components/SWHeaderLayerIcons.d.ts": "bb4b955a7850c445a3ff14c0e25034124dca49208aed0fafd922878ff7099ac5",
|
"components/sw-header/components/SWHeaderLayerIcons.d.ts": "bb4b955a7850c445a3ff14c0e25034124dca49208aed0fafd922878ff7099ac5",
|
||||||
"components/sw-header/components/SWHeaderLayerMenu.d.ts": "0fb65b14f52cd2abf54e74ea09ed4b783fe1665416dfd910bd6cd7d3e1995711",
|
"components/sw-header/components/SWHeaderLayerMenu.d.ts": "0fb65b14f52cd2abf54e74ea09ed4b783fe1665416dfd910bd6cd7d3e1995711",
|
||||||
@@ -219,23 +231,28 @@
|
|||||||
"components/sw-header/components/SWHeaderWindowControls.d.ts": "db8bfbd14340e03c28e407f58fc08cef55b9eef49f406ba53fc50e994f5f0834",
|
"components/sw-header/components/SWHeaderWindowControls.d.ts": "db8bfbd14340e03c28e407f58fc08cef55b9eef49f406ba53fc50e994f5f0834",
|
||||||
"components/sw-header/index.d.ts": "958a64efd05f7b8ab683ecb342fdcdef488db0be97cf9a2bf5ad3e3ac4632ab7",
|
"components/sw-header/index.d.ts": "958a64efd05f7b8ab683ecb342fdcdef488db0be97cf9a2bf5ad3e3ac4632ab7",
|
||||||
"components/sw-header/SWHeader.d.ts": "3adac1db372451d9f86a6f6df1df0e9696cd40c3ed7945531ee378da122136d8",
|
"components/sw-header/SWHeader.d.ts": "3adac1db372451d9f86a6f6df1df0e9696cd40c3ed7945531ee378da122136d8",
|
||||||
"components/sw-header/types.d.ts": "fa7602374753bf7b6b48c0fa9f1c30a932ed12ccb4d16488c836e964b76f9122",
|
"components/sw-header/types.d.ts": "fdc3fafb111331f2edd754029bdc78231b18e563df662b849d8a6a8eaf30ebcd",
|
||||||
"components/sw-home-menu/index.d.ts": "b1f67a4e16c7b3a64b93f348bfb17fcd2ada999527cb93ccb77ca6308564091c",
|
"components/sw-home-menu/index.d.ts": "b1f67a4e16c7b3a64b93f348bfb17fcd2ada999527cb93ccb77ca6308564091c",
|
||||||
"components/sw-home-menu/SWHomeMenu.d.ts": "2ce530d253374a8b20a20d8b6ddd5c733abfc8d518162d5a7490d432986a662c",
|
"components/sw-home-menu/SWHomeMenu.d.ts": "2ce530d253374a8b20a20d8b6ddd5c733abfc8d518162d5a7490d432986a662c",
|
||||||
"components/sw-home-menu/SWRecentFiles.d.ts": "0c3c8ececc537ab6a03ce14a91dd1c878e9539b4022ce486ebe161262fb2c710",
|
"components/sw-home-menu/SWRecentFiles.d.ts": "0c3c8ececc537ab6a03ce14a91dd1c878e9539b4022ce486ebe161262fb2c710",
|
||||||
"components/sw-home-menu/types.d.ts": "81499bc8edf2f2acfb2f556d7f397019ba99046a808e0607cb96a29a67b6f315",
|
"components/sw-home-menu/types.d.ts": "81499bc8edf2f2acfb2f556d7f397019ba99046a808e0607cb96a29a67b6f315",
|
||||||
"components/sw-popup/SWPopup.d.ts": "9af7b5f34bed9c863e343018ed64dc6960d09f5664fdfc1c26fcc7204acdd041",
|
"components/sw-popup/SWPopup.d.ts": "9af7b5f34bed9c863e343018ed64dc6960d09f5664fdfc1c26fcc7204acdd041",
|
||||||
|
"components/sw-process-header/index.d.ts": "1fe66c0854bac5f2fa0eeb2bd0bb16056ab1af08750b9911841d34881ec7abb5",
|
||||||
|
"components/sw-process-header/SWProcessBar.d.ts": "51e69c5dfeeaecee0db96b3b1511ee32d69667ea85aa67e01210bc9fe70854fb",
|
||||||
|
"components/sw-process-header/SWProcessHeader.d.ts": "e7740f8a1131963a3e43b271ff1664fdd9f06a5fca60217501d7598f557e6b65",
|
||||||
|
"components/sw-process-header/SWProcessSplitButton.d.ts": "b865f46609301677f9686a6d6d00e8caacfc94e5fdc776eb87fb8f93184f96ff",
|
||||||
|
"components/sw-process-header/types.d.ts": "02778af5e73c4b3be04bc8fe939a105fe3588526ce10614cf43aebe1fce68849",
|
||||||
"components/sw-select/SWSelect.d.ts": "9cf523e8a8329a2be7bdfba56c1a2c647b2676480efa3f6dfa26a971ea0a9c3b",
|
"components/sw-select/SWSelect.d.ts": "9cf523e8a8329a2be7bdfba56c1a2c647b2676480efa3f6dfa26a971ea0a9c3b",
|
||||||
"components/sw-slider/SWSlider.d.ts": "7d9c48299cb5644d859753e1bc56ae43dc7ab282e64432d57d3e29b2960aaad5",
|
"components/sw-slider/SWSlider.d.ts": "7d9c48299cb5644d859753e1bc56ae43dc7ab282e64432d57d3e29b2960aaad5",
|
||||||
"components/sw-theme/SWTheme.d.ts": "cdcc2e7c4b80241afa4d0b7c3da671b7086c16f9b9c231c1927c931757e4244c",
|
"components/sw-theme/SWTheme.d.ts": "cdcc2e7c4b80241afa4d0b7c3da671b7086c16f9b9c231c1927c931757e4244c",
|
||||||
"components/sw-tool/SWTool.d.ts": "23b1eb156578cd3f1dee325afbd81988d88c4a3f421f7350a0e8c47c652e804c",
|
"components/sw-tool/SWTool.d.ts": "23b1eb156578cd3f1dee325afbd81988d88c4a3f421f7350a0e8c47c652e804c",
|
||||||
"components/sw-viewer/index.d.ts": "e57a9ec0b91ae576b4b09b4851cd8e19b6fb4ea4bbdf62a578b4b94d7da6ffe2",
|
"components/sw-viewer/index.d.ts": "e57a9ec0b91ae576b4b09b4851cd8e19b6fb4ea4bbdf62a578b4b94d7da6ffe2",
|
||||||
"components/sw-viewer/SWViewerControls.d.ts": "ca32e8a2a7de2cfefec36273ffb473271ad438cfb663f44a25871ee6aed9222d",
|
"components/sw-viewer/SWViewerControls.d.ts": "a75bc7d54db126d1bd9999cf12dc3b7edfb24cb16f75ba44b2e9dda422623aa2",
|
||||||
"components/sw-viewer/SWViewerHeader.d.ts": "586d07e74f5c2673fbe1d4849ed72cdcf80e7f8a391393168f101dc613490d24",
|
"components/sw-viewer/SWViewerHeader.d.ts": "0a46e07d7f6b33dc389cf4739e563be045cad12d84585384b1cc4d861c4acdbe",
|
||||||
"components/sw-viewer/SWViewerIcon.d.ts": "3c81582860e481b037b395b3584ff93af73500e4040d8423c9da15c2b4dd108e",
|
"components/sw-viewer/SWViewerIcon.d.ts": "3c81582860e481b037b395b3584ff93af73500e4040d8423c9da15c2b4dd108e",
|
||||||
"components/sw-viewer/SWViewerMenu.d.ts": "15fa615c3472d84037780f3c93387acbc6b4d05f5fb56731b0a99262b3f7f109",
|
"components/sw-viewer/SWViewerMenu.d.ts": "15fa615c3472d84037780f3c93387acbc6b4d05f5fb56731b0a99262b3f7f109",
|
||||||
"components/sw-viewer/SWViewerPanel.d.ts": "49b040d3f3d00de30e3fc0493fd2ce78e309616ec97a7ce226dfef300b640f92",
|
"components/sw-viewer/SWViewerPanel.d.ts": "49b040d3f3d00de30e3fc0493fd2ce78e309616ec97a7ce226dfef300b640f92",
|
||||||
"components/sw-viewer/types.d.ts": "e46b4605a3fcab78325bff526a374a891fdcf0256dc7824a9414d2336b2f85d7",
|
"components/sw-viewer/types.d.ts": "13b928b6871b9865e30929b37a17e6c820353f18477d96c1cc09a7d69e40cd18",
|
||||||
"index.d.ts": "34f0607f63df53d5a1186a4ad763a8bf7b433e20645d868f6f7368ed0853828c"
|
"index.d.ts": "1e156f688534c117f880c7bd32f06110240caff3d02157db0265edb8e3eb41f5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -16,7 +16,15 @@ import { existsSync } from "node:fs"
|
|||||||
import { readFile, writeFile } from "node:fs/promises"
|
import { readFile, writeFile } from "node:fs/promises"
|
||||||
import path from "node:path"
|
import path from "node:path"
|
||||||
import { fileURLToPath, pathToFileURL } from "node:url"
|
import { fileURLToPath, pathToFileURL } from "node:url"
|
||||||
import { approvedMainCommit } from "./storybook-inventory.mjs"
|
// Reviewed main merge the approved surface must belong to (2026-11-10: own baseline, split from approvedMainCommit in
|
||||||
|
// storybook-inventory.mjs, which also anchors the Story inventory / source regressions of overview, storybook-unused-
|
||||||
|
// removal, dropdown-consolidation and storybook-cleanup-stage1; moving that one would accept their unrecorded Story and
|
||||||
|
// source changes). Update it only in a reviewed test-only change that re-snapshots public-api.approved.json from a clean
|
||||||
|
// build of that merge after its .d.ts changes were compared with the previous approved build.
|
||||||
|
// History: approvedMainCommit fc65b2e (snapshot 8cb98b3) -> 3cbe879 (2026-11-10, Merge PR #30; snapshot from the clean
|
||||||
|
// 3cbe879 build: SWProcess* exports of the user-approved Step Header adbb034 / e700825, optional shortLabel? of the SWViewerHeaderItem switch item in
|
||||||
|
// cabd2f6, the internal SWHeaderLogo declaration of 205864b (not exported from the entry) and JSDoc-only changes).
|
||||||
|
const approvedPublicApiMainCommit = "3cbe87991010f08b131e8b80c9f478c3fa721004"
|
||||||
|
|
||||||
const packageRoot = fileURLToPath(new URL("../", import.meta.url))
|
const packageRoot = fileURLToPath(new URL("../", import.meta.url))
|
||||||
const snapshotFile = fileURLToPath(new URL("./public-api.approved.json", import.meta.url))
|
const snapshotFile = fileURLToPath(new URL("./public-api.approved.json", import.meta.url))
|
||||||
@@ -82,7 +90,7 @@ if (process.argv[2] === "--snapshot") {
|
|||||||
const results = []
|
const results = []
|
||||||
const check = (name, ok, data) => { results.push(ok); console.log(ok ? "PASS" : "FAIL", name, data === undefined ? "" : JSON.stringify(data)) }
|
const check = (name, ok, data) => { results.push(ok); console.log(ok ? "PASS" : "FAIL", name, data === undefined ? "" : JSON.stringify(data)) }
|
||||||
const ancestor = (a, b) => { try { execFileSync("git", ["merge-base", "--is-ancestor", a, b], { cwd: packageRoot, stdio: "ignore" }); return true } catch { return false } }
|
const ancestor = (a, b) => { try { execFileSync("git", ["merge-base", "--is-ancestor", a, b], { cwd: packageRoot, stdio: "ignore" }); return true } catch { return false } }
|
||||||
check("approved surface comes from a reviewed commit (ancestor of approvedMainCommit and HEAD)", ancestor(approved.approvedCommit, approvedMainCommit) && ancestor(approved.approvedCommit, "HEAD"), { approvedCommit: approved.approvedCommit, approvedMainCommit })
|
check("approved surface comes from a reviewed commit (ancestor of approvedPublicApiMainCommit and HEAD)", ancestor(approved.approvedCommit, approvedPublicApiMainCommit) && ancestor(approved.approvedCommit, "HEAD"), { approvedCommit: approved.approvedCommit, approvedPublicApiMainCommit })
|
||||||
|
|
||||||
const manifest = await manifestOf(manifestFile)
|
const manifest = await manifestOf(manifestFile)
|
||||||
const changedFields = ["name", ...manifestFields].filter(field => JSON.stringify(manifest[field]) !== JSON.stringify(approved.manifest[field]))
|
const changedFields = ["name", ...manifestFields].filter(field => JSON.stringify(manifest[field]) !== JSON.stringify(approved.manifest[field]))
|
||||||
|
|||||||
@@ -60,10 +60,11 @@ const only = (map, expected) => Object.keys(map).length > 0 && Object.keys(map).
|
|||||||
const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${family}`).join(' | ') || 'no text'
|
const fmt = map => Object.entries(map).map(([family, count]) => `${count}× ${family}`).join(' | ') || 'no text'
|
||||||
|
|
||||||
// More (2026-10-21): the header shows More only when its measured room runs out (full → icons → More). The Story's
|
// More (2026-10-21): the header shows More only when its measured room runs out (full → icons → More). The Story's
|
||||||
// 1000px header fits as icons, so the header is narrowed for this check (as text-weight does), then restored.
|
// 1000px header fits as icons, so the header is narrowed for this check, then restored. 2026-11-10: 480px, since
|
||||||
|
// cabd2f6 (2026-10-26) the viewer header keeps every menu down to about 520px and shows More only below that.
|
||||||
async function openMore(page, area) {
|
async function openMore(page, area) {
|
||||||
const header = page.locator(`${area} .hm-sw-viewer-header`)
|
const header = page.locator(`${area} .hm-sw-viewer-header`)
|
||||||
await header.evaluate(element => { element.style.width = "700px" })
|
await header.evaluate(element => { element.style.width = "480px" })
|
||||||
await page.locator(`${area} .hm-sw-viewer-header button[aria-label="추가 도구"]`).click()
|
await page.locator(`${area} .hm-sw-viewer-header button[aria-label="추가 도구"]`).click()
|
||||||
}
|
}
|
||||||
const restoreWidth = (page, area) => page.locator(`${area} .hm-sw-viewer-header`).evaluate(element => { element.style.width = "" })
|
const restoreWidth = (page, area) => page.locator(`${area} .hm-sw-viewer-header`).evaluate(element => { element.style.width = "" })
|
||||||
|
|||||||
Reference in new issue
Block a user