diff --git a/.gitea/workflows/quality-gates.yml b/.gitea/workflows/quality-gates.yml index b0c5540..940fe91 100644 --- a/.gitea/workflows/quality-gates.yml +++ b/.gitea/workflows/quality-gates.yml @@ -187,6 +187,7 @@ jobs: scripts/lib/secret-scan.ts \ scripts/lib/supply-chain.ts \ scripts/lib/validated-json-artifact.ts \ + scripts/lib/vite-route-chunks.ts \ src/contracts/release-artifacts.ts \ src/features/installed-contract-contributions.ts \ src/features/installed-feature-contracts.ts \ diff --git a/.superpowers/sdd/2026-08-15-techlog-ui-migration/task-14-report.md b/.superpowers/sdd/2026-08-15-techlog-ui-migration/task-14-report.md new file mode 100644 index 0000000..b2e09d6 --- /dev/null +++ b/.superpowers/sdd/2026-08-15-techlog-ui-migration/task-14-report.md @@ -0,0 +1,243 @@ +# Task 14 report: integrated TechLog parity and release verification + +## Status + +`DONE_WITH_CONCERNS`. The migrated UI, committed browser evidence, source-to-target +comparison, sample-removal gate, focused suites, static gates, and production +build pass. The only concern is the repository's pre-existing restricted-runner +`test:all` baseline: child-process-heavy tests cannot spawn in the managed +sandbox, and 19 provider-environment cases remain red even when isolated with +the required process scope. The isolation and exact counts below prove that no +TechLog test is among those failures. + +The work remains on `feature/techlog-ui-migration`. It was not merged, pushed, +finished with GitFlow, or deleted. Task 14 is committed by this report's commit, +`test: prove TechLog UI migration parity`. + +## Evidence files + +Added or replaced product evidence: + +- `tests/visual/tech-log.visual.spec.ts` and 111 target-only PNG snapshots. The + suite has 112 cases because canonical and state coverage for the 1440-pixel + `/studio/publications` screen deliberately share the same reviewed image. +- `tests/e2e/tech-log-public-discovery.spec.ts`, + `tests/e2e/tech-log-accessibility.spec.ts`, and + `tests/e2e/tech-log-responsive.spec.ts`. +- `tests/support/browser/tech-log-fixtures.ts` and the external, manually run + `tests/support/browser/verify-tech-log-source-parity.ts` evidence harness. +- Focused regressions in `tests/unit/vite-route-chunks.test.ts`, + `tests/unit/design-system-source.test.ts`, the bounded-body reader tests, the + router component suite, and TechLog feature suites. +- Governed registry, dependency, release, CI, and operations evidence in + `config/contracts`, `config/security`, `config/ci`, the generated Gitea + workflow, `README.md`, and + `docs/operations/techlog-ui-migration-baseline.md`. + +Removed starter-only evidence: + +- `tests/e2e/compact-smoke.spec.ts`, + `tests/e2e/design-system-interactions.spec.ts`, `tests/e2e/i18n.spec.ts`, and + `tests/e2e/theme.spec.ts`. +- `tests/visual/platform.visual.spec.ts` and all five platform visual PNGs. + +The retained `app-shell`, registry-wide accessibility, and responsive suites +were rewritten around TechLog. No stale starter browser or visual snapshot is +left referenced. + +## Source-to-target visual method and result + +The supplied source at `/home/donghyeon/workspace/techlog-studio-frontend` was +never written. It was copied to `/tmp/techlog-source-parity.I0CBK7`; build and +Vinext runtime caches were created only in that temporary copy. The source +production server and target Vite preview were opened by one Playwright Chromium +instance with two fresh contexts and the following identical controls: + +- device scale factor 1, light color scheme, `ko-KR`, `Asia/Seoul`, reduced + motion, service workers blocked, 1000-pixel viewport height, and full-page + screenshots; +- fixed clock `2026-08-14T01:00:00.000Z`, deterministic in-memory data, + `document.fonts.ready`, matching Pretendard/IBM Plex Mono font-face state, + and zero-duration animation, transition, and caret styles; +- no masks and no tolerance: exact RGBA pixel comparison, product-subtree + tag/class/text/ARIA projection, layout diagnostics, and console/page/request + failure collection. + +The final external comparison command was: + +```bash +TECH_LOG_SOURCE_URL=http://127.0.0.1:4275 TECH_LOG_TARGET_URL=http://127.0.0.1:4274 corepack pnpm exec tsx tests/support/browser/verify-tech-log-source-parity.ts +``` + +Result: **112/112 passed**, 0 failed, `totalDifferentPixels=0`, every DOM/class/ +text/ARIA projection equal, and 0 unexplained source/target errors. Source +screenshots were temporary comparison inputs; none was copied into target +snapshots. The no-update target visual run also passed 112/112 with +`maxDiffPixels=0` and `maxDiffPixelRatio=0`. + +The comparison found and corrected actual integration defects rather than +accepting drift: the TechLog Tailwind bootstrap is loaded exactly once in the +same cascade order as source; the starter theme import is removed; CSS-module +class mapping, shell navigation/focus, publication labels and states, router +404 handling, and generated Vite route-chunk lookup now follow source. All five +source/target CSS pairs pass `cmp -s`; their hashes are recorded in the +operations baseline. + +Source production returns missing dynamic slugs and an unmatched Public path as +HTTP 404, `text/plain`, with the exact nine-byte body `Not Found`. Curl and +Chromium regression evidence therefore fixes the target to that shell-free +response. This is an observed source-production contract and satisfies the +planned prohibition on a generic runtime error; it is not a redesign. + +## Route, viewport, and state inventory + +The 27 canonical contract routes were each compared at 360 and 1440 pixels: + +- Public: `/`, `/explore`, `/explore/:kind`, `/cases/:slug`, + `/references/:slug`, `/questions/:slug`, `/topics/:slug`, `/projects`, + `/projects/:slug`, the `records`, `decisions`, and `activity` project views, + `/releases`, `/releases/:version`, `/profile`, `/search`, and `*`. +- Studio: `/studio`, `/studio/documents`, `/studio/documents/new`, the `edit`, + `validation`, `preview`, and `publish` document views, + `/studio/publications`, publication-event preview, and `/studio/*`. + +All known Public fixtures were exercised: two cases, two references, two open +questions, three topics, both projects and all three nested views, and release +`0.1.0`. Ten unknown Public dynamic shapes were also compared at both widths. + +The 112-case matrix is 54 canonical-route captures, 12 home breakpoints (1180, +1179, 1050, 1024, 980, 900, 820, 768, 767, 420, 390, and 375), 19 Studio +states, 20 unknown-Public captures, and 7 interactions. Studio states cover the +dashboard, list/new, Case/Reference/Question/conflict editors, valid/invalid +validation, current/missing/expired previews, ready/blocked publish, +publications/snapshot, missing document/publication, and unknown Studio route. +Interactions cover Public search, Studio mobile menu, immediate preview, +dirty-leave dialog, newly created current preview, unpublish confirmation, and +warning acknowledgement through publish-ready state. + +## Browser, responsive, and accessibility outcomes + +- Required four-spec Chromium command: **48/48 passed**. It covers Public + discovery, the full Studio workflow, responsive behavior, and accessibility. +- Responsive plus accessibility focused command: **26/26 passed**. +- Exact target visual command: **112/112 passed** in 2.5 minutes with no masks + and zero pixel tolerance. +- Keyboard/focus checks cover Public search dismissal/restoration, Studio mobile + navigation, dirty-leave and unpublish dialogs, labels, heading/landmark order, + and focus-visible behavior. Axe reports no violations in the required route + and state inventory. Overflow assertions pass at the compact and transition + widths, and no unexpected console, page, or request error remains. + +Commands: + +```bash +corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium +corepack pnpm exec playwright test tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium +corepack pnpm exec playwright test tests/visual/tech-log.visual.spec.ts --config=playwright.visual.config.ts --project=chromium +corepack pnpm test:visual +``` + +## Registry and supply-chain governance + +The initial no-baseline registry artifact reported 23 migration-owned breaking +IDs. Each now has owner `tech-log-frontend`, a TechLog contract-version reason, +atomic route/runtime/manifest installation, same-release compatibility, and +rollback to `05e3d50ba01f01c27f257d2e9040c2bc413ea053`: + +- Contract: `$contract:{allowedValues,breakingFields,fieldTypes,requiredFields}`. +- Removed route rows: `APP_HOME`, `EXAMPLES_AUTH`, `EXAMPLES_PLATFORM`, + `EXAMPLES_STATES`, `EXAMPLES_UI`, `REFERENCE_RESOURCE_DETAIL`, + `REFERENCE_RESOURCE_FORM`, `REFERENCE_RESOURCE_LIST`, and + `REFERENCE_RESOURCE_STATUS`. +- Runtime removals: the same nine route IDs. +- Runtime change: `NOT_FOUND:moduleId:field-changed`. + +The governed update used exactly: + +```bash +REGISTRY_BASELINE_OWNER=tech-log-frontend REGISTRY_BASELINE_REASON="Install approved TechLog Public and Studio route contract" node scripts/update-registry-baseline.ts artifacts/quality/registries.json +corepack pnpm check:registries +``` + +Final result: 11 registries pass, compatibility `none`, no unacknowledged +change. Approved snapshot digest: +`428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b`. + +Six direct dependency additions have evidence owner `tech-log-frontend`, +reviewer `frontend-platform-security`, product-specific reason, and atomic +rollback: `@fontsource/ibm-plex-mono@5.3.0`, `pretendard@1.3.9`, +`remark-directive@4.0.0`, `remark-gfm@4.0.1`, `remark-parse@11.0.0`, and +`unified@11.0.5`. The dependency policy recognizes the font packages' OFL-1.1 +license. Supply-chain generation covered 641 packages. The pre-existing +dependency baseline was not promoted; the denied promotion was unnecessary for +the regular verification path, which passes with the committed evidence. + +## Sample removal and fresh verification + +The final staged-candidate command passed: + +```bash +corepack pnpm test:sample-removal +``` + +Result: **PASS (13 checks, no fixture IDs)**. Its internal evidence included +types; reduced architecture (383 modules/1,173 dependencies, 12 graph checks, +9 forbidden fixtures); 11 registry checks; runtime schema 3 files/40 tests; +unit 115/1,247; component 18/124; integration 8/74; recipes 2/17; coverage 164 +files/1,653 tests at 77.41% statements, 73.26% branches, 83.89% functions, and +80.03% lines; risk coverage 382/382 with 76 thresholds; source evidence 198 +files/111 baselines; artifact/CI checks; router smoke 9/9; and production build. + +Fresh completion commands and results: + +| Command | Result | +| --- | --- | +| `corepack pnpm exec vitest run tests/features/tech-log` | 21 files, 170 tests passed | +| required four-spec Chromium command above | 48 tests passed | +| `corepack pnpm test:visual` | 112 tests passed | +| `corepack pnpm check:types` | app/node/test/recipes/web-worker/service-worker passed | +| `corepack pnpm lint` | passed with 0 warnings | +| `corepack pnpm check:architecture` | 392 modules, 1,215 dependencies, 12 graph checks, 9 forbidden fixtures passed | +| `corepack pnpm check:design-system` | 48 tokens and vendor boundaries passed | +| `corepack pnpm check:i18n` | 194 keys across 4 locales passed | +| `corepack pnpm check:registries` | 11 registries passed; compatibility `none` | +| `corepack pnpm check:browser-security` | injection rejected; Public source maps absent | +| `corepack pnpm build` | 2,352 modules transformed; build and manifest completed | +| `git diff --check` | passed | + +The exact managed-workspace `corepack pnpm test:all` passed runtime schema 3/40, +then its unit phase passed 117 files/1,738 tests and failed 50 tests in only +three child-process-heavy files due to `spawnSync ... EPERM`: 39 +`ci-artifact-contract`, 2 `ci-workflow-generation`, and 9 +`http-scenario-evidence`. No TechLog test failed. The exact isolated command: + +```bash +corepack pnpm exec vitest run tests/unit/ci-artifact-contract.test.ts tests/unit/ci-workflow-generation.test.ts tests/unit/http-scenario-evidence.test.ts +``` + +ran 3 files/529 tests: 510 passed; all 407 CI-workflow and all 14 HTTP-scenario +tests passed; the remaining 19 pre-existing CI-artifact provider/cgroup, +RLIMIT/EMFILE, umask, `/tmp`, timing, and identity cases failed. Direct runs of +the aggregate's remaining phases passed: component 18/124, integration 11/82 +under the required child-process scope, reference feature 4/13, and recipes +2/17. This environment-only baseline is also recorded in the operations +baseline. + +## Fixes, branch audit, and handoff + +Root-cause-driven fixes added regressions for generated Vite manifest chunk +resolution, source-compatible raw 404 responses, palette-source detection, and +abort rejection. Presentation integration corrections preserve source DOM, +ARIA, copy, assets, CSS, workflow state, and focus behavior; no design was +introduced. The branch-wide audit of +`05e3d50ba01f01c27f257d2e9040c2bc413ea053..HEAD` found no migrated +presentation import of adapters, Next.js, Vinext, or Cloudflare. The 27 route +chunks are present in the release manifest and derive from actual Vite output, +not hard-coded generated filenames. + +Before the Task 14 commit, the full migration range contains the 21 prior +commits from baseline establishment through `c5c8b94` and changes 352 files +(26,025 insertions, 3,467 deletions). This report's commit adds the integrated +parity evidence and fixes with the exact message +`test: prove TechLog UI migration parity`. Final status and range are checked +again after the commit; integration remains explicitly deferred. diff --git a/README.md b/README.md index 649871e..4ffaf73 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Initialized from `clean-architecture-frontend-template` revision `4dc033cf33a5b6173bbf960d5eb464a406dc4c92`. The exact source identity is recorded in `template.lock.json`. -A React/Vite reference implementation where architecture boundaries, -integration behavior, release coherence, accessibility, performance, and -operations are executable contracts rather than conventions. +A React/Vite TechLog application where architecture boundaries, integration +behavior, release coherence, accessibility, performance, and operations are +executable contracts rather than conventions. ## Start locally @@ -28,26 +28,20 @@ corepack pnpm dev Runtime-public settings live in `public/config.json` and are validated before the product tree mounts. Client secrets are forbidden. -## Included starter experience +## TechLog experience -The default build mounts a domain-neutral application shell with a header, -responsive sidebar, route focus management, session integration status, and a -persistent `system` / `light` / `dark` theme selector. +The default build mounts the source-faithful TechLog Public and Studio +experience. Public routes provide discovery, search, documents, topics, +projects, releases, and profile content. `/studio` provides the session-scoped +mock authoring workflow: create, edit, validate, preview, publish, unpublish, +and immutable publication history. -| Route | Purpose | -| --- | --- | -| `/` | implementation readiness and starter links | -| `/examples/ui` | buttons, fields, cards, alerts, badges, modal, and tokens | -| `/examples/states` | loading, refresh, empty, error, auth, forbidden, and not-found states | -| `/examples/auth` | reactive external-auth integration seam | -| `/examples/reference-resources` | removable, session-required reference feature | - -`AUTH_MODE=demo` is credential-free and accepted only in local/development -environments. Deployments use `AUTH_MODE=external` and provide the opaque auth -owner described in -[`docs/architecture/starter-experience.md`](docs/architecture/starter-experience.md). -The client route policy is user experience only; server authorization remains -authoritative. +The 27 canonical route definitions are divided into `PUBLIC` and `STUDIO` +nested layouts. Studio authentication remains deliberately deferred; its mock +gateway state lasts for one Studio shell session and resets on a full document +load. The exact route, dependency, stylesheet, asset, and parity inventory is +recorded in +[`docs/operations/techlog-ui-migration-baseline.md`](docs/operations/techlog-ui-migration-baseline.md). ## Architecture @@ -61,11 +55,13 @@ contracts own cross-cutting registries ``` See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and -`docs/architecture/starter-experience.md`. The removable vertical slice is -under `src/features/reference-feature`; its domain, application input, HTTP -adapter, contracts, route runtime, and presentation are installed through the -feature contribution files in `src/features`. The generic starter routes -continue to typecheck, test, and build after that contribution is removed. +`docs/architecture/starter-experience.md`. TechLog is one feature boundary +under `src/features/tech-log`. Its immutable Public catalog and session-scoped +Studio mock gateway are injected through the application feature input; +Public and Studio presentation code shares the typed content renderer without +importing concrete adapters. The retained reference feature remains a +non-product platform contract fixture and can be removed without changing the +TechLog route set. ### Platform capability review @@ -117,6 +113,9 @@ corepack pnpm verify:release corepack pnpm check:registries corepack pnpm drill:runbooks corepack pnpm check:ci +corepack pnpm exec vitest run tests/features/tech-log +corepack pnpm exec playwright test tests/e2e/tech-log-public-discovery.spec.ts tests/e2e/tech-log-studio-workflow.spec.ts tests/e2e/tech-log-responsive.spec.ts tests/e2e/tech-log-accessibility.spec.ts --project=chromium +corepack pnpm test:visual ``` `check:types`는 source, Node scripts/config와 tests를 분리된 TypeScript @@ -144,7 +143,7 @@ corepack pnpm exec playwright install --with-deps chromium firefox webkit Two gates intentionally need external evidence: - `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review - for all six registered routes. + for the registered product routes. - `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum eligible-sample threshold and 28 days of production data exist. diff --git a/config/ci/gates.json b/config/ci/gates.json index d6d806a..2a690b6 100644 --- a/config/ci/gates.json +++ b/config/ci/gates.json @@ -2301,6 +2301,7 @@ "scripts/lib/secret-scan.ts", "scripts/lib/supply-chain.ts", "scripts/lib/validated-json-artifact.ts", + "scripts/lib/vite-route-chunks.ts", "src/contracts/release-artifacts.ts", "src/features/installed-contract-contributions.ts", "src/features/installed-feature-contracts.ts", diff --git a/config/contracts/registry-baseline.approval.json b/config/contracts/registry-baseline.approval.json index a061482..222f1ab 100644 --- a/config/contracts/registry-baseline.approval.json +++ b/config/contracts/registry-baseline.approval.json @@ -1,7 +1,7 @@ { "schemaVersion": 1, - "snapshotDigest": "96b95ef1d50cce36e9fca8a98776a9e2e9e3a5dca24a6288ad83bf29c94aebd8", - "owner": "frontend-platform", - "reason": "Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement", - "approvedAt": "2026-08-01T15:15:45.537Z" + "snapshotDigest": "428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b", + "owner": "tech-log-frontend", + "reason": "Install approved TechLog Public and Studio route contract", + "approvedAt": "2026-08-15T16:32:32.042Z" } diff --git a/config/contracts/registry-baseline.json b/config/contracts/registry-baseline.json index 5f21fa6..1c7b366 100644 --- a/config/contracts/registry-baseline.json +++ b/config/contracts/registry-baseline.json @@ -5,11 +5,12 @@ "registryId": "FE-REG-ROUTE", "owner": "feature-frontend-routing-release-recovery-runtime", "source": "src/features/installed-feature-contracts.ts", - "rowCount": 10, + "rowCount": 27, "contract": { "requiredFields": [ "routeId", "path", + "layoutGroup", "paramsSchema", "searchSchema", "access", @@ -23,6 +24,7 @@ "fieldTypes": { "routeId": "string", "path": "string", + "layoutGroup": "string", "paramsSchema": "string|null", "searchSchema": "string|null", "access": "string", @@ -43,14 +45,29 @@ "public", "session-required" ], + "layoutGroup": [ + "PUBLIC", + "STUDIO" + ], "paramsSchema": [ null, "NotFoundSplat", - "ReferenceResourceParams" + "ReferenceResourceParams", + "TechLogExploreKindParams", + "TechLogSlugParams", + "TechLogVersionParams", + "TechLogDocumentIdParams", + "TechLogPublicationEventIdParams", + "TechLogStudioSplat" ], "searchSchema": [ null, - "ReferenceResourceListQuery" + "ReferenceResourceListQuery", + "TechLogHomeSearch", + "TechLogExploreSearch", + "TechLogExploreKindSearch", + "TechLogSearchQuery", + "TechLogCaseStateSearch" ], "loadingSurface": [ "app-shell", @@ -88,6 +105,7 @@ "breakingFields": [ "routeId", "path", + "layoutGroup", "paramsSchema", "searchSchema", "access", @@ -95,75 +113,11 @@ ] }, "rows": { - "APP_HOME": { - "access": "public", - "chunkId": "route-home", - "errorSurface": "route-boundary", - "loadingSurface": "app-shell", - "navigationLabel": "시작", - "navigationOrder": 10, - "paramsSchema": null, - "path": "/", - "routeId": "APP_HOME", - "searchSchema": null, - "title": "시작" - }, - "EXAMPLES_AUTH": { - "access": "public", - "chunkId": "route-examples-auth", - "errorSurface": "route-boundary", - "loadingSurface": "example-page", - "navigationLabel": "인증 연동", - "navigationOrder": 40, - "paramsSchema": null, - "path": "/examples/auth", - "routeId": "EXAMPLES_AUTH", - "searchSchema": null, - "title": "인증 연동" - }, - "EXAMPLES_PLATFORM": { - "access": "public", - "chunkId": "route-examples-platform", - "errorSurface": "route-boundary", - "loadingSurface": "example-page", - "navigationLabel": "플랫폼 구성", - "navigationOrder": 15, - "paramsSchema": null, - "path": "/examples/platform", - "routeId": "EXAMPLES_PLATFORM", - "searchSchema": null, - "title": "플랫폼 구성" - }, - "EXAMPLES_STATES": { - "access": "public", - "chunkId": "route-examples-states", - "errorSurface": "route-boundary", - "loadingSurface": "example-page", - "navigationLabel": "화면 상태", - "navigationOrder": 30, - "paramsSchema": null, - "path": "/examples/states", - "routeId": "EXAMPLES_STATES", - "searchSchema": null, - "title": "화면 상태" - }, - "EXAMPLES_UI": { - "access": "public", - "chunkId": "route-examples-ui", - "errorSurface": "route-boundary", - "loadingSurface": "example-page", - "navigationLabel": "UI 구성요소", - "navigationOrder": 20, - "paramsSchema": null, - "path": "/examples/ui", - "routeId": "EXAMPLES_UI", - "searchSchema": null, - "title": "UI 구성요소" - }, "NOT_FOUND": { "access": "public", "chunkId": "route-not-found", "errorSurface": "not-found", + "layoutGroup": "PUBLIC", "loadingSurface": "none", "navigationLabel": null, "navigationOrder": null, @@ -171,59 +125,371 @@ "path": "*", "routeId": "NOT_FOUND", "searchSchema": null, - "title": "페이지를 찾을 수 없음" + "title": "페이지를 찾을 수 없습니다." }, - "REFERENCE_RESOURCE_DETAIL": { - "access": "session-required", - "chunkId": "route-reference-resource-detail", + "TECH_LOG_CASE": { + "access": "public", + "chunkId": "route-tech-log-case", "errorSurface": "feature-boundary", - "loadingSurface": "reference-resource-detail", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", "navigationLabel": null, "navigationOrder": null, - "paramsSchema": "ReferenceResourceParams", - "path": "/examples/reference-resources/:resourceId", - "routeId": "REFERENCE_RESOURCE_DETAIL", - "searchSchema": null, - "title": "Reference detail" + "paramsSchema": "TechLogSlugParams", + "path": "/cases/:slug", + "routeId": "TECH_LOG_CASE", + "searchSchema": "TechLogCaseStateSearch", + "title": "Case" }, - "REFERENCE_RESOURCE_FORM": { - "access": "session-required", - "chunkId": "route-reference-resource-form", + "TECH_LOG_EXPLORE": { + "access": "public", + "chunkId": "route-tech-log-explore", "errorSurface": "feature-boundary", - "loadingSurface": "reference-resource-form", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": "탐색", + "navigationOrder": 10, + "paramsSchema": null, + "path": "/explore", + "routeId": "TECH_LOG_EXPLORE", + "searchSchema": "TechLogExploreSearch", + "title": "탐색" + }, + "TECH_LOG_EXPLORE_KIND": { + "access": "public", + "chunkId": "route-tech-log-explore-kind", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogExploreKindParams", + "path": "/explore/:kind", + "routeId": "TECH_LOG_EXPLORE_KIND", + "searchSchema": "TechLogExploreKindSearch", + "title": "유형별 탐색" + }, + "TECH_LOG_HOME": { + "access": "public", + "chunkId": "route-tech-log-home", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", "navigationLabel": null, "navigationOrder": null, "paramsSchema": null, - "path": "/examples/reference-resources/new", - "routeId": "REFERENCE_RESOURCE_FORM", - "searchSchema": null, - "title": "Reference form" + "path": "/", + "routeId": "TECH_LOG_HOME", + "searchSchema": "TechLogHomeSearch", + "title": "TechLog" }, - "REFERENCE_RESOURCE_LIST": { - "access": "session-required", - "chunkId": "route-reference-resources", + "TECH_LOG_PROFILE": { + "access": "public", + "chunkId": "route-tech-log-profile", "errorSurface": "feature-boundary", - "loadingSurface": "reference-resource-list", - "navigationLabel": "Reference feature", - "navigationOrder": 50, + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": "프로필", + "navigationOrder": 40, "paramsSchema": null, - "path": "/examples/reference-resources", - "routeId": "REFERENCE_RESOURCE_LIST", - "searchSchema": "ReferenceResourceListQuery", - "title": "Reference feature" + "path": "/profile", + "routeId": "TECH_LOG_PROFILE", + "searchSchema": null, + "title": "프로필" }, - "REFERENCE_RESOURCE_STATUS": { - "access": "session-required", - "chunkId": "route-reference-resource-status", + "TECH_LOG_PROJECT": { + "access": "public", + "chunkId": "route-tech-log-project", "errorSurface": "feature-boundary", - "loadingSurface": "reference-resource-status", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogSlugParams", + "path": "/projects/:slug", + "routeId": "TECH_LOG_PROJECT", + "searchSchema": null, + "title": "프로젝트" + }, + "TECH_LOG_PROJECT_ACTIVITY": { + "access": "public", + "chunkId": "route-tech-log-project-activity", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogSlugParams", + "path": "/projects/:slug/activity", + "routeId": "TECH_LOG_PROJECT_ACTIVITY", + "searchSchema": null, + "title": "프로젝트 활동" + }, + "TECH_LOG_PROJECT_DECISIONS": { + "access": "public", + "chunkId": "route-tech-log-project-decisions", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogSlugParams", + "path": "/projects/:slug/decisions", + "routeId": "TECH_LOG_PROJECT_DECISIONS", + "searchSchema": null, + "title": "프로젝트 결정" + }, + "TECH_LOG_PROJECT_RECORDS": { + "access": "public", + "chunkId": "route-tech-log-project-records", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogSlugParams", + "path": "/projects/:slug/records", + "routeId": "TECH_LOG_PROJECT_RECORDS", + "searchSchema": null, + "title": "프로젝트 기록" + }, + "TECH_LOG_PROJECTS": { + "access": "public", + "chunkId": "route-tech-log-projects", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": "프로젝트", + "navigationOrder": 20, + "paramsSchema": null, + "path": "/projects", + "routeId": "TECH_LOG_PROJECTS", + "searchSchema": null, + "title": "프로젝트" + }, + "TECH_LOG_QUESTION": { + "access": "public", + "chunkId": "route-tech-log-question", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogSlugParams", + "path": "/questions/:slug", + "routeId": "TECH_LOG_QUESTION", + "searchSchema": null, + "title": "Open Question" + }, + "TECH_LOG_REFERENCE": { + "access": "public", + "chunkId": "route-tech-log-reference", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogSlugParams", + "path": "/references/:slug", + "routeId": "TECH_LOG_REFERENCE", + "searchSchema": null, + "title": "Reference" + }, + "TECH_LOG_RELEASE": { + "access": "public", + "chunkId": "route-tech-log-release", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogVersionParams", + "path": "/releases/:version", + "routeId": "TECH_LOG_RELEASE", + "searchSchema": null, + "title": "변경 기록" + }, + "TECH_LOG_RELEASES": { + "access": "public", + "chunkId": "route-tech-log-releases", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": "변경 기록", + "navigationOrder": 30, + "paramsSchema": null, + "path": "/releases", + "routeId": "TECH_LOG_RELEASES", + "searchSchema": null, + "title": "변경 기록" + }, + "TECH_LOG_SEARCH": { + "access": "public", + "chunkId": "route-tech-log-search", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", "navigationLabel": null, "navigationOrder": null, "paramsSchema": null, - "path": "/examples/reference-resources/status", - "routeId": "REFERENCE_RESOURCE_STATUS", + "path": "/search", + "routeId": "TECH_LOG_SEARCH", + "searchSchema": "TechLogSearchQuery", + "title": "검색" + }, + "TECH_LOG_STUDIO_DOCUMENT_EDIT": { + "access": "public", + "chunkId": "route-tech-log-studio-document-edit", + "errorSurface": "feature-boundary", + "layoutGroup": "STUDIO", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogDocumentIdParams", + "path": "/studio/documents/:id/edit", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_EDIT", "searchSchema": null, - "title": "Reference status" + "title": "문서 편집" + }, + "TECH_LOG_STUDIO_DOCUMENT_NEW": { + "access": "public", + "chunkId": "route-tech-log-studio-document-new", + "errorSurface": "feature-boundary", + "layoutGroup": "STUDIO", + "loadingSurface": "app-shell", + "navigationLabel": "새 문서", + "navigationOrder": 30, + "paramsSchema": null, + "path": "/studio/documents/new", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_NEW", + "searchSchema": null, + "title": "새 문서" + }, + "TECH_LOG_STUDIO_DOCUMENT_PREVIEW": { + "access": "public", + "chunkId": "route-tech-log-studio-document-preview", + "errorSurface": "feature-boundary", + "layoutGroup": "STUDIO", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogDocumentIdParams", + "path": "/studio/documents/:id/preview", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_PREVIEW", + "searchSchema": null, + "title": "Public Preview" + }, + "TECH_LOG_STUDIO_DOCUMENT_PUBLISH": { + "access": "public", + "chunkId": "route-tech-log-studio-document-publish", + "errorSurface": "feature-boundary", + "layoutGroup": "STUDIO", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogDocumentIdParams", + "path": "/studio/documents/:id/publish", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_PUBLISH", + "searchSchema": null, + "title": "게시" + }, + "TECH_LOG_STUDIO_DOCUMENT_VALIDATION": { + "access": "public", + "chunkId": "route-tech-log-studio-document-validation", + "errorSurface": "feature-boundary", + "layoutGroup": "STUDIO", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogDocumentIdParams", + "path": "/studio/documents/:id/validation", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_VALIDATION", + "searchSchema": null, + "title": "문서 검증" + }, + "TECH_LOG_STUDIO_DOCUMENTS": { + "access": "public", + "chunkId": "route-tech-log-studio-documents", + "errorSurface": "feature-boundary", + "layoutGroup": "STUDIO", + "loadingSurface": "app-shell", + "navigationLabel": "작업본", + "navigationOrder": 10, + "paramsSchema": null, + "path": "/studio/documents", + "routeId": "TECH_LOG_STUDIO_DOCUMENTS", + "searchSchema": null, + "title": "작업본" + }, + "TECH_LOG_STUDIO_HOME": { + "access": "public", + "chunkId": "route-tech-log-studio-home", + "errorSurface": "feature-boundary", + "layoutGroup": "STUDIO", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": null, + "path": "/studio", + "routeId": "TECH_LOG_STUDIO_HOME", + "searchSchema": null, + "title": "TechLog Studio" + }, + "TECH_LOG_STUDIO_NOT_FOUND": { + "access": "public", + "chunkId": "route-tech-log-studio-not-found", + "errorSurface": "not-found", + "layoutGroup": "STUDIO", + "loadingSurface": "none", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogStudioSplat", + "path": "/studio/*", + "routeId": "TECH_LOG_STUDIO_NOT_FOUND", + "searchSchema": null, + "title": "Studio 화면을 찾을 수 없습니다" + }, + "TECH_LOG_STUDIO_PUBLICATION_PREVIEW": { + "access": "public", + "chunkId": "route-tech-log-studio-publication-preview", + "errorSurface": "feature-boundary", + "layoutGroup": "STUDIO", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogPublicationEventIdParams", + "path": "/studio/publications/:publicationEventId/preview", + "routeId": "TECH_LOG_STUDIO_PUBLICATION_PREVIEW", + "searchSchema": null, + "title": "게시 Snapshot" + }, + "TECH_LOG_STUDIO_PUBLICATIONS": { + "access": "public", + "chunkId": "route-tech-log-studio-publications", + "errorSurface": "feature-boundary", + "layoutGroup": "STUDIO", + "loadingSurface": "app-shell", + "navigationLabel": "게시 기록", + "navigationOrder": 20, + "paramsSchema": null, + "path": "/studio/publications", + "routeId": "TECH_LOG_STUDIO_PUBLICATIONS", + "searchSchema": null, + "title": "게시 기록" + }, + "TECH_LOG_TOPIC": { + "access": "public", + "chunkId": "route-tech-log-topic", + "errorSurface": "feature-boundary", + "layoutGroup": "PUBLIC", + "loadingSurface": "app-shell", + "navigationLabel": null, + "navigationOrder": null, + "paramsSchema": "TechLogSlugParams", + "path": "/topics/:slug", + "routeId": "TECH_LOG_TOPIC", + "searchSchema": null, + "title": "Topic" } } }, @@ -231,7 +497,7 @@ "registryId": "FE-REG-ROUTE-RUNTIME", "owner": "feature-frontend-routing-release-recovery-runtime", "source": "src/features/installed-feature-contracts.ts", - "rowCount": 10, + "rowCount": 27, "contract": { "requiredFields": [ "routeId", @@ -276,64 +542,166 @@ ] }, "rows": { - "APP_HOME": { - "moduleId": "home-page", - "paramsCodec": "none", - "routeId": "APP_HOME", - "searchCodec": "none" - }, - "EXAMPLES_AUTH": { - "moduleId": "auth-example-page", - "paramsCodec": "none", - "routeId": "EXAMPLES_AUTH", - "searchCodec": "none" - }, - "EXAMPLES_PLATFORM": { - "moduleId": "platform-overview-page", - "paramsCodec": "none", - "routeId": "EXAMPLES_PLATFORM", - "searchCodec": "none" - }, - "EXAMPLES_STATES": { - "moduleId": "state-gallery-page", - "paramsCodec": "none", - "routeId": "EXAMPLES_STATES", - "searchCodec": "none" - }, - "EXAMPLES_UI": { - "moduleId": "ui-gallery-page", - "paramsCodec": "none", - "routeId": "EXAMPLES_UI", - "searchCodec": "none" - }, "NOT_FOUND": { - "moduleId": "not-found-page", + "moduleId": "route-not-found", "paramsCodec": "NotFoundSplat", "routeId": "NOT_FOUND", "searchCodec": "none" }, - "REFERENCE_RESOURCE_DETAIL": { - "moduleId": "reference-resource-detail-page", - "paramsCodec": "ReferenceResourceParams", - "routeId": "REFERENCE_RESOURCE_DETAIL", + "TECH_LOG_CASE": { + "moduleId": "route-tech-log-case", + "paramsCodec": "TechLogSlugParams", + "routeId": "TECH_LOG_CASE", + "searchCodec": "TechLogCaseStateSearch" + }, + "TECH_LOG_EXPLORE": { + "moduleId": "route-tech-log-explore", + "paramsCodec": "none", + "routeId": "TECH_LOG_EXPLORE", + "searchCodec": "TechLogExploreSearch" + }, + "TECH_LOG_EXPLORE_KIND": { + "moduleId": "route-tech-log-explore-kind", + "paramsCodec": "TechLogExploreKindParams", + "routeId": "TECH_LOG_EXPLORE_KIND", + "searchCodec": "TechLogExploreKindSearch" + }, + "TECH_LOG_HOME": { + "moduleId": "route-tech-log-home", + "paramsCodec": "none", + "routeId": "TECH_LOG_HOME", + "searchCodec": "TechLogHomeSearch" + }, + "TECH_LOG_PROFILE": { + "moduleId": "route-tech-log-profile", + "paramsCodec": "none", + "routeId": "TECH_LOG_PROFILE", "searchCodec": "none" }, - "REFERENCE_RESOURCE_FORM": { - "moduleId": "reference-resource-form-page", - "paramsCodec": "none", - "routeId": "REFERENCE_RESOURCE_FORM", + "TECH_LOG_PROJECT": { + "moduleId": "route-tech-log-project", + "paramsCodec": "TechLogSlugParams", + "routeId": "TECH_LOG_PROJECT", "searchCodec": "none" }, - "REFERENCE_RESOURCE_LIST": { - "moduleId": "reference-resource-page", - "paramsCodec": "none", - "routeId": "REFERENCE_RESOURCE_LIST", - "searchCodec": "ReferenceResourceListQuery" + "TECH_LOG_PROJECT_ACTIVITY": { + "moduleId": "route-tech-log-project-activity", + "paramsCodec": "TechLogSlugParams", + "routeId": "TECH_LOG_PROJECT_ACTIVITY", + "searchCodec": "none" }, - "REFERENCE_RESOURCE_STATUS": { - "moduleId": "reference-resource-status-page", + "TECH_LOG_PROJECT_DECISIONS": { + "moduleId": "route-tech-log-project-decisions", + "paramsCodec": "TechLogSlugParams", + "routeId": "TECH_LOG_PROJECT_DECISIONS", + "searchCodec": "none" + }, + "TECH_LOG_PROJECT_RECORDS": { + "moduleId": "route-tech-log-project-records", + "paramsCodec": "TechLogSlugParams", + "routeId": "TECH_LOG_PROJECT_RECORDS", + "searchCodec": "none" + }, + "TECH_LOG_PROJECTS": { + "moduleId": "route-tech-log-projects", "paramsCodec": "none", - "routeId": "REFERENCE_RESOURCE_STATUS", + "routeId": "TECH_LOG_PROJECTS", + "searchCodec": "none" + }, + "TECH_LOG_QUESTION": { + "moduleId": "route-tech-log-question", + "paramsCodec": "TechLogSlugParams", + "routeId": "TECH_LOG_QUESTION", + "searchCodec": "none" + }, + "TECH_LOG_REFERENCE": { + "moduleId": "route-tech-log-reference", + "paramsCodec": "TechLogSlugParams", + "routeId": "TECH_LOG_REFERENCE", + "searchCodec": "none" + }, + "TECH_LOG_RELEASE": { + "moduleId": "route-tech-log-release", + "paramsCodec": "TechLogVersionParams", + "routeId": "TECH_LOG_RELEASE", + "searchCodec": "none" + }, + "TECH_LOG_RELEASES": { + "moduleId": "route-tech-log-releases", + "paramsCodec": "none", + "routeId": "TECH_LOG_RELEASES", + "searchCodec": "none" + }, + "TECH_LOG_SEARCH": { + "moduleId": "route-tech-log-search", + "paramsCodec": "none", + "routeId": "TECH_LOG_SEARCH", + "searchCodec": "TechLogSearchQuery" + }, + "TECH_LOG_STUDIO_DOCUMENT_EDIT": { + "moduleId": "route-tech-log-studio-document-edit", + "paramsCodec": "TechLogDocumentIdParams", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_EDIT", + "searchCodec": "none" + }, + "TECH_LOG_STUDIO_DOCUMENT_NEW": { + "moduleId": "route-tech-log-studio-document-new", + "paramsCodec": "none", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_NEW", + "searchCodec": "none" + }, + "TECH_LOG_STUDIO_DOCUMENT_PREVIEW": { + "moduleId": "route-tech-log-studio-document-preview", + "paramsCodec": "TechLogDocumentIdParams", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_PREVIEW", + "searchCodec": "none" + }, + "TECH_LOG_STUDIO_DOCUMENT_PUBLISH": { + "moduleId": "route-tech-log-studio-document-publish", + "paramsCodec": "TechLogDocumentIdParams", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_PUBLISH", + "searchCodec": "none" + }, + "TECH_LOG_STUDIO_DOCUMENT_VALIDATION": { + "moduleId": "route-tech-log-studio-document-validation", + "paramsCodec": "TechLogDocumentIdParams", + "routeId": "TECH_LOG_STUDIO_DOCUMENT_VALIDATION", + "searchCodec": "none" + }, + "TECH_LOG_STUDIO_DOCUMENTS": { + "moduleId": "route-tech-log-studio-documents", + "paramsCodec": "none", + "routeId": "TECH_LOG_STUDIO_DOCUMENTS", + "searchCodec": "none" + }, + "TECH_LOG_STUDIO_HOME": { + "moduleId": "route-tech-log-studio-home", + "paramsCodec": "none", + "routeId": "TECH_LOG_STUDIO_HOME", + "searchCodec": "none" + }, + "TECH_LOG_STUDIO_NOT_FOUND": { + "moduleId": "route-tech-log-studio-not-found", + "paramsCodec": "TechLogStudioSplat", + "routeId": "TECH_LOG_STUDIO_NOT_FOUND", + "searchCodec": "none" + }, + "TECH_LOG_STUDIO_PUBLICATION_PREVIEW": { + "moduleId": "route-tech-log-studio-publication-preview", + "paramsCodec": "TechLogPublicationEventIdParams", + "routeId": "TECH_LOG_STUDIO_PUBLICATION_PREVIEW", + "searchCodec": "none" + }, + "TECH_LOG_STUDIO_PUBLICATIONS": { + "moduleId": "route-tech-log-studio-publications", + "paramsCodec": "none", + "routeId": "TECH_LOG_STUDIO_PUBLICATIONS", + "searchCodec": "none" + }, + "TECH_LOG_TOPIC": { + "moduleId": "route-tech-log-topic", + "paramsCodec": "TechLogSlugParams", + "routeId": "TECH_LOG_TOPIC", "searchCodec": "none" } } @@ -530,7 +898,7 @@ "registryId": "FE-REG-SCHEMA", "owner": "feature-frontend-contract-schema-registry", "source": "src/features/installed-feature-contracts.ts", - "rowCount": 8, + "rowCount": 19, "contract": { "requiredFields": [ "schemaId", @@ -633,6 +1001,105 @@ "schemaId": "ReferenceResourcePayload", "schemaVersion": 1, "unknownFieldPolicy": "STRIP_UNKNOWN" + }, + "TechLogCaseStateSearch": { + "boundary": "route-search", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogCaseStateSearch", + "schemaVersion": 1, + "unknownFieldPolicy": "STRIP_UNKNOWN" + }, + "TechLogDocumentIdParams": { + "boundary": "route-params", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogDocumentIdParams", + "schemaVersion": 1, + "unknownFieldPolicy": "REJECT_UNKNOWN" + }, + "TechLogExploreKindParams": { + "boundary": "route-params", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogExploreKindParams", + "schemaVersion": 1, + "unknownFieldPolicy": "REJECT_UNKNOWN" + }, + "TechLogExploreKindSearch": { + "boundary": "route-search", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogExploreKindSearch", + "schemaVersion": 1, + "unknownFieldPolicy": "STRIP_UNKNOWN" + }, + "TechLogExploreSearch": { + "boundary": "route-search", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogExploreSearch", + "schemaVersion": 1, + "unknownFieldPolicy": "STRIP_UNKNOWN" + }, + "TechLogHomeSearch": { + "boundary": "route-search", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogHomeSearch", + "schemaVersion": 1, + "unknownFieldPolicy": "STRIP_UNKNOWN" + }, + "TechLogPublicationEventIdParams": { + "boundary": "route-params", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogPublicationEventIdParams", + "schemaVersion": 1, + "unknownFieldPolicy": "REJECT_UNKNOWN" + }, + "TechLogSearchQuery": { + "boundary": "route-search", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogSearchQuery", + "schemaVersion": 1, + "unknownFieldPolicy": "STRIP_UNKNOWN" + }, + "TechLogSlugParams": { + "boundary": "route-params", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogSlugParams", + "schemaVersion": 1, + "unknownFieldPolicy": "REJECT_UNKNOWN" + }, + "TechLogStudioSplat": { + "boundary": "route-params", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogStudioSplat", + "schemaVersion": 1, + "unknownFieldPolicy": "REJECT_UNKNOWN" + }, + "TechLogVersionParams": { + "boundary": "route-params", + "direction": "REQUEST", + "owner": "feature-tech-log", + "runtime": "zod", + "schemaId": "TechLogVersionParams", + "schemaVersion": 1, + "unknownFieldPolicy": "REJECT_UNKNOWN" } } }, @@ -766,7 +1233,7 @@ "registryId": "FE-REG-STORAGE", "owner": "feature-frontend-storage-registry-contract", "source": "src/contracts/storage-keys.ts", - "rowCount": 4, + "rowCount": 5, "contract": { "requiredFields": [ "logicalName", @@ -847,6 +1314,19 @@ "ttl": null, "valueCodec": "none" }, + "CACHE_INVALIDATION_PULSE": { + "backend": "localStorage", + "classification": "opaque-cache", + "logicalName": "CACHE_INVALIDATION_PULSE", + "migration": "discard", + "name": "pulse", + "physicalKey": "ca-frontend:cache-invalidation:v1:pulse", + "quotaFallback": "no-persist", + "schemaVersion": 1, + "scope": "cache-invalidation", + "ttl": null, + "valueCodec": "opaque-string-v1" + }, "CHUNK_RELOAD_GUARD": { "backend": "sessionStorage", "classification": "opaque-cache", diff --git a/config/contracts/registry-change-evidence.json b/config/contracts/registry-change-evidence.json index b039daa..4b66667 100644 --- a/config/contracts/registry-change-evidence.json +++ b/config/contracts/registry-change-evidence.json @@ -1,6 +1,190 @@ { "schemaVersion": 1, "changes": [ + { + "changeId": "FE-REG-ROUTE:$contract:allowedValues:contract-field-changed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:$contract:breakingFields:contract-field-changed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:$contract:fieldTypes:contract-field-changed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:$contract:requiredFields:contract-field-changed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:APP_HOME:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:EXAMPLES_AUTH:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:EXAMPLES_PLATFORM:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:EXAMPLES_STATES:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:EXAMPLES_UI:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_DETAIL:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_FORM:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_LIST:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE:REFERENCE_RESOURCE_STATUS:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:APP_HOME:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_AUTH:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_PLATFORM:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_STATES:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:EXAMPLES_UI:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:NOT_FOUND:moduleId:field-changed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_DETAIL:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_FORM:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_LIST:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, + { + "changeId": "FE-REG-ROUTE-RUNTIME:REFERENCE_RESOURCE_STATUS:*:removed", + "versionBump": "TechLog route contract v1 replaces the starter and reference-resource route set.", + "migration": "Install the TechLog route definition, route runtime, schema registry, and release manifest atomically in the same release.", + "compatibilityWindow": "The deployed release consumes only its same-release registry snapshot and runtime chunks; no mixed route contract is supported.", + "rollback": "Roll back the atomic release to the pre-migration feature base 05e3d50ba01f01c27f257d2e9040c2bc413ea053.", + "owner": "tech-log-frontend" + }, { "changeId": "FE-REG-ENV:API_CONTRACT_VERSION:*:removed", "versionBump": "Runtime Config V2 (CONFIG_SCHEMA_VERSION 2.0) removes the scalar API contract version.", diff --git a/config/security/dependency-change-evidence.json b/config/security/dependency-change-evidence.json index 62d2c3c..b01dd7a 100644 --- a/config/security/dependency-change-evidence.json +++ b/config/security/dependency-change-evidence.json @@ -1,4 +1,47 @@ { "schemaVersion": 1, - "changes": [] + "changes": [ + { + "changeId": "add:@fontsource/ibm-plex-mono@5.3.0", + "owner": "tech-log-frontend", + "reviewer": "frontend-platform-security", + "reason": "Preserve the source application's IBM Plex Mono typography and bundled font assets without a runtime font request.", + "rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point." + }, + { + "changeId": "add:pretendard@1.3.9", + "owner": "tech-log-frontend", + "reviewer": "frontend-platform-security", + "reason": "Preserve the source application's Pretendard Variable typography using its pinned bundled font asset.", + "rollback": "Revert the TechLog UI migration dependency installation and restore the pre-migration presentation entry point." + }, + { + "changeId": "add:remark-directive@4.0.0", + "owner": "tech-log-frontend", + "reviewer": "frontend-platform-security", + "reason": "Parse the source TechLog document directive syntax through the migrated deterministic content pipeline.", + "rollback": "Revert the TechLog UI migration content parser and dependency installation together." + }, + { + "changeId": "add:remark-gfm@4.0.1", + "owner": "tech-log-frontend", + "reviewer": "frontend-platform-security", + "reason": "Preserve source TechLog GitHub-flavored Markdown tables, task lists, and autolink parsing.", + "rollback": "Revert the TechLog UI migration content parser and dependency installation together." + }, + { + "changeId": "add:remark-parse@11.0.0", + "owner": "tech-log-frontend", + "reviewer": "frontend-platform-security", + "reason": "Parse the source TechLog Markdown records into the migrated typed public-render model.", + "rollback": "Revert the TechLog UI migration content parser and dependency installation together." + }, + { + "changeId": "add:unified@11.0.5", + "owner": "tech-log-frontend", + "reviewer": "frontend-platform-security", + "reason": "Compose the source-equivalent Markdown and directive parsing stages without framework coupling.", + "rollback": "Revert the TechLog UI migration content parser and dependency installation together." + } + ] } diff --git a/config/security/dependency-policy.json b/config/security/dependency-policy.json index acd24e4..eae708d 100644 --- a/config/security/dependency-policy.json +++ b/config/security/dependency-policy.json @@ -12,7 +12,8 @@ "ISC", "MIT", "MIT-0", - "MPL-2.0" + "MPL-2.0", + "OFL-1.1" ], "deniedLicensePatterns": [ "(^|\\s)AGPL", diff --git a/docs/operations/techlog-ui-migration-baseline.md b/docs/operations/techlog-ui-migration-baseline.md index b02808f..2eb5f5d 100644 --- a/docs/operations/techlog-ui-migration-baseline.md +++ b/docs/operations/techlog-ui-migration-baseline.md @@ -71,12 +71,107 @@ No Next.js, Vinext, or Cloudflare package is part of the migration baseline. ## Source stylesheet inventory -- `app/globals.css` -- `app/studio.css` -- `app/studio-editor.css` -- `components/studio/workflow.module.css` -- `components/studio/publication-flow.module.css` +- `app/globals.css` → `presentation/styles/globals.css` — + `901205054ee96fe15062aaef7ff39c701985fdece15b58f7656776d0b744e607` +- `app/studio.css` → `presentation/styles/studio.css` — + `d0d958baeb55b74796988d8c0967a7c62d211e595224e07912aa1f884e466fdd` +- `app/studio-editor.css` → `presentation/styles/studio-editor.css` — + `ae9a473192d24465021d021cd42a5a84f9bdf4c3af0664537b4e10006ca1f889` +- `components/studio/workflow.module.css` → + `presentation/styles/workflow.module.css` — + `8eaa478a88ef4d16429c7f6a32bb33631acb47deef0b40838edd0a72748f124e` +- `components/studio/publication-flow.module.css` → + `presentation/styles/publication-flow.module.css` — + `5b1fdebeb27248b5cebc700b12d15cf11fe70471a394c9dc643a98c4569e8674` + +All five source/target pairs passed `cmp -s` and have identical SHA-256 +digests. Production parity testing corrected the provisional bootstrap +assumption in the design: the target loads the byte-identical TechLog +`globals.css`, including its first-byte `@import "tailwindcss";`, exactly once +and does not load the starter `theme.css`. This reproduces the source cascade; +the resulting browser comparison is pixel-identical. + +## Source-to-target browser parity + +The source was copied to `/tmp/techlog-source-parity.I0CBK7` before build and +runtime caches were created. The supplied source directory was used only for +read-only file comparison; it was not edited. The temp-copy Vinext production +server and the target Vite preview were captured by one Playwright Chromium +instance with two fresh contexts under identical conditions: + +- `ko-KR`, `Asia/Seoul`, light color scheme, reduced motion, device scale 1, + service workers blocked, and a fixed `2026-08-14T01:00:00.000Z` clock; +- 1000-pixel viewport height, full-page screenshots, `document.fonts.ready`, + Pretendard/IBM Plex font checks, zero-duration animation/transition/caret; +- no screenshot masks; exact RGBA pixel comparison plus product-subtree + tag/class/text/ARIA projection and console/request failure comparison. + +| Evidence group | Cases | +| --- | ---: | +| 27 canonical routes at 360 and 1440 pixels | 54 | +| Public home breakpoint transitions | 12 | +| Studio screen/state fixtures | 19 | +| Ten unknown Public dynamic shapes at 360 and 1440 pixels | 20 | +| Search/menu/preview/dialog/publication interactions | 7 | +| **Total** | **112** | + +Result: 112/112 passed, zero failed, zero different pixels, all DOM/class/text/ +ARIA projections equal, and zero unexplained source or target console/request +errors. The target-only visual regression suite exercises the same 112 cases +against 111 committed PNGs; the canonical and Studio-state 1440-pixel +`/studio/publications` cases intentionally share one identical snapshot. +Source screenshots were temporary comparison inputs and were not copied into +the target snapshot directory. + +The production source responds to both a missing dynamic slug such as +`/projects/missing-project` and an unmatched path such as +`/definitely-not-a-product-route` with HTTP 404, `text/plain`, and the exact +nine-byte body `Not Found`. The target intentionally removes the Public shell +for those paths and matches that response in curl and Chromium regressions; +this is the observed production source contract, not a generic runtime error. + +## Governed route migration + +The migration records 23 TechLog-owned breaking-change evidence IDs with owner +`tech-log-frontend`, an atomic same-release route/runtime/manifest migration, +and rollback to `05e3d50ba01f01c27f257d2e9040c2bc413ea053`. The accepted registry +snapshot digest is +`428479ac5845374a82dd7d02a0c59a713106405f031cdc153789174f14c1405b`; +its approval reason is `Install approved TechLog Public and Studio route +contract`. Final compatibility impact is `none` with no unacknowledged change. + +## Restricted-environment test baseline + +The exact repository aggregate command was run in the managed workspace: + +```bash +corepack pnpm test:all +``` + +Its runtime-schema phase passed 3 files/40 tests. The unit phase passed 117 +files/1,738 tests and reported 50 failures in three child-process-heavy files: +39 `ci-artifact-contract`, 2 `ci-workflow-generation`, and 9 +`http-scenario-evidence`. Every failure was rooted in `spawnSync ... EPERM` in +the restricted process sandbox; no `tests/features/tech-log` test failed. + +The failing files were then reproduced in isolation outside that child-process +restriction: + +```bash +corepack pnpm exec vitest run tests/unit/ci-artifact-contract.test.ts tests/unit/ci-workflow-generation.test.ts tests/unit/http-scenario-evidence.test.ts +``` + +Result: 3 files/529 tests, 510 passed and the pre-existing 19 environment-only +`ci-artifact-contract` cases failed. The 407 `ci-workflow-generation` and 14 +`http-scenario-evidence` tests all passed. The remaining exact 19 are the +documented provider lifecycle/cgroup, RLIMIT/EMFILE, restrictive-umask, and +`/tmp` timing/identity cases; they contain no TechLog code or test. The +aggregate phases after unit were also run directly: component 18 files/124 +tests, integration 11 files/82 tests under the required child-process scope, +reference feature 4 files/13 tests, and recipes 2 files/17 tests all passed. The inventories in this document are human review baselines. Automated coverage is intentionally limited to the two SVG byte contracts, evidence -asset lookup behavior, and the package manager's frozen-lockfile verification. +asset lookup behavior, package manager frozen-lockfile verification, committed +target visual regressions, and governed registry/release gates. The external +source path is never required by committed CI tests. diff --git a/package.json b/package.json index dedbb8c..6b025de 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "test:realtime-removal": "node scripts/test-realtime-runtime-removal.ts", "test:reference-feature": "vitest run tests/features/reference-feature --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/reference-feature.xml --passWithNoTests", "check:v8-coverage-counter-semantics": "node scripts/check-v8-coverage-counter-semantics.ts", - "test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts", + "test:coverage": "corepack pnpm check:v8-coverage-counter-semantics && vitest run tests/runtime-schema tests/unit tests/component tests/integration tests/features/reference-feature tests/features/tech-log --coverage --maxWorkers=4 --reporter=default --reporter=junit --outputFile.junit=artifacts/tests/coverage.xml && node scripts/check-risk-coverage.ts", "check:coverage:fixture": "node scripts/check-risk-coverage.ts --summary tests/fixtures/coverage/below-threshold.json --artifact artifacts/quality/risk-coverage-fixture.json", "test:all": "corepack pnpm test:runtime-schema && corepack pnpm test:unit && corepack pnpm test:component && corepack pnpm test:integration && corepack pnpm test:reference-feature && corepack pnpm test:recipes", "verify:lockfile": "corepack pnpm install --frozen-lockfile --ignore-scripts", diff --git a/playwright.config.ts b/playwright.config.ts index 83fe081..bf67a50 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -9,35 +9,35 @@ export default defineConfig({ ["junit", { outputFile: "./artifacts/tests/e2e/results.xml" }], ], use: { - baseURL: "http://127.0.0.1:4173", + baseURL: "http://127.0.0.1:4273", + colorScheme: "light", + locale: "ko-KR", + timezoneId: "Asia/Seoul", trace: "retain-on-failure", screenshot: "only-on-failure", }, webServer: { command: - "corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4173", - url: "http://127.0.0.1:4173", + "corepack pnpm build && corepack pnpm preview --host 127.0.0.1 --port 4273", + url: "http://127.0.0.1:4273", reuseExistingServer: false, }, projects: [ { name: "chromium", - testIgnore: "**/compact-smoke.spec.ts", use: { ...devices["Desktop Chrome"] }, }, { name: "firefox", - testIgnore: "**/compact-smoke.spec.ts", use: { ...devices["Desktop Firefox"] }, }, { name: "webkit", - testIgnore: "**/compact-smoke.spec.ts", use: { ...devices["Desktop Safari"] }, }, { name: "chromium-compact", - testMatch: "**/compact-smoke.spec.ts", + testMatch: "**/tech-log-responsive.spec.ts", use: { ...devices["Desktop Chrome"], viewport: { width: 390, height: 844 }, diff --git a/playwright.visual.config.ts b/playwright.visual.config.ts index f66f996..1849010 100644 --- a/playwright.visual.config.ts +++ b/playwright.visual.config.ts @@ -16,15 +16,17 @@ export default defineConfig({ toHaveScreenshot: { animations: "disabled", caret: "hide", - maxDiffPixelRatio: 0.002, - scale: "css", + maxDiffPixels: 0, + maxDiffPixelRatio: 0, + scale: "device", }, }, use: { ...devices["Desktop Chrome"], baseURL: "http://127.0.0.1:4174", colorScheme: "light", - locale: "en-US", + locale: "ko-KR", + timezoneId: "Asia/Seoul", trace: "retain-on-failure", }, webServer: { @@ -33,5 +35,10 @@ export default defineConfig({ url: "http://127.0.0.1:4174", reuseExistingServer: false, }, - projects: [{ name: "chromium-visual" }], + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"], deviceScaleFactor: 1 }, + }, + ], }); diff --git a/scripts/check-design-system.ts b/scripts/check-design-system.ts index 22ce4a8..718203e 100644 --- a/scripts/check-design-system.ts +++ b/scripts/check-design-system.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, readdir } from "node:fs/promises"; import { designSystemReportArtifactSchema } from "./contracts/release-artifacts.ts"; import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts"; +import { containsRawPaletteValue } from "./lib/design-system-source.ts"; import path from "node:path"; import { REQUIRED_COMPONENT_TOKENS, REQUIRED_PRIMITIVE_TOKENS, REQUIRED_SEMANTIC_TOKENS } from "../src/presentation/design-system/tokens/token-contract.ts"; @@ -109,7 +110,7 @@ for (const file of sources) { } if ( !file.includes("src/presentation/design-system/tokens/") && - /(?:#[0-9a-f]{3,8}\b|oklch\(|rgba?\()/i.test(source) + containsRawPaletteValue(source) ) { failures.push(`raw palette value in ${file}`); } diff --git a/scripts/generate-build-manifest.ts b/scripts/generate-build-manifest.ts index af7a7b9..c6f8cd1 100644 --- a/scripts/generate-build-manifest.ts +++ b/scripts/generate-build-manifest.ts @@ -21,13 +21,12 @@ import { } from "./lib/build-environment.ts"; import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts"; import { CANONICAL_VITE_MANIFEST_PATH } from "./lib/build-manifest-outputs.ts"; +import { + findViteDynamicRouteChunk, + type ViteManifestRouteEntry, +} from "./lib/vite-route-chunks.ts"; assertCiBuildEnvironment(process.env); -type ViteManifestEntry = Readonly<{ - file: string; - name?: string; - isDynamicEntry?: boolean; -}>; const packageJson = parsePackageMetadata( JSON.parse(await readFile("package.json", "utf8")), @@ -63,9 +62,9 @@ const runtimeContracts: Readonly> = ROUTE_RUNTIME_CONTRACT; for (const definition of Object.values(ROUTE_REGISTRY)) { const runtime = runtimeContracts[definition.routeId]; - const asset = Object.values(viteManifestObject).find( - (entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry, - ); + const asset = runtime + ? findViteDynamicRouteChunk(viteManifestObject, runtime.moduleId) + : undefined; if (!runtime || !asset?.file) { throw new Error(`Missing built route chunk: ${definition.routeId}`); } @@ -163,9 +162,9 @@ function parsePackageMetadata(value: unknown): Readonly<{ function parseViteManifest( value: unknown, -): Readonly> { +): Readonly> { if (!isRecord(value)) throw new TypeError("Vite manifest must be an object"); - const entries: Record = {}; + const entries: Record = {}; for (const [key, candidate] of Object.entries(value)) { if (!isRecord(candidate) || typeof candidate.file !== "string") { throw new TypeError(`Invalid Vite manifest entry: ${key}`); @@ -176,6 +175,10 @@ function parseViteManifest( ...(typeof candidate.isDynamicEntry === "boolean" ? { isDynamicEntry: candidate.isDynamicEntry } : {}), + ...(Array.isArray(candidate.dynamicImports) && + candidate.dynamicImports.every((item) => typeof item === "string") + ? { dynamicImports: candidate.dynamicImports as string[] } + : {}), }; } return entries; diff --git a/scripts/lib/design-system-source.ts b/scripts/lib/design-system-source.ts new file mode 100644 index 0000000..7f97be4 --- /dev/null +++ b/scripts/lib/design-system-source.ts @@ -0,0 +1,6 @@ +const RAW_PALETTE_VALUE_PATTERN = + /(? - isRecord(entry) && - entry.name === runtime?.moduleId && - entry.isDynamicEntry === true, - ); + const viteEntry = runtime + ? findViteDynamicRouteChunk( + Object.fromEntries( + Object.entries(viteManifest).flatMap(([key, entry]) => { + if (!isRecord(entry) || typeof entry.file !== "string") return []; + return [[key, { + file: entry.file, + ...(typeof entry.name === "string" ? { name: entry.name } : {}), + ...(typeof entry.isDynamicEntry === "boolean" + ? { isDynamicEntry: entry.isDynamicEntry } + : {}), + ...(Array.isArray(entry.dynamicImports) && + entry.dynamicImports.every((item) => typeof item === "string") + ? { dynamicImports: entry.dynamicImports as string[] } + : {}), + }]]; + }), + ), + runtime.moduleId, + ) + : undefined; const file = isRecord(viteEntry) ? viteEntry.file : null; if ( typeof file !== "string" || diff --git a/scripts/lib/release-candidate.ts b/scripts/lib/release-candidate.ts index 3340379..8f5d7d4 100644 --- a/scripts/lib/release-candidate.ts +++ b/scripts/lib/release-candidate.ts @@ -52,6 +52,7 @@ export const LOCAL_EVIDENCE_VERIFIER_SOURCE_PATHS = Object.freeze([ "scripts/lib/secret-scan.ts", "scripts/lib/supply-chain.ts", "scripts/lib/validated-json-artifact.ts", + "scripts/lib/vite-route-chunks.ts", "src/contracts/release-artifacts.ts", "src/features/installed-contract-contributions.ts", "src/features/installed-feature-contracts.ts", diff --git a/scripts/lib/vite-route-chunks.ts b/scripts/lib/vite-route-chunks.ts new file mode 100644 index 0000000..32e038e --- /dev/null +++ b/scripts/lib/vite-route-chunks.ts @@ -0,0 +1,20 @@ +export type ViteManifestRouteEntry = Readonly<{ + file: string; + name?: string; + isDynamicEntry?: boolean; + dynamicImports?: readonly string[]; +}>; + +export function findViteDynamicRouteChunk( + manifest: Readonly>, + moduleId: string, +): ViteManifestRouteEntry | undefined { + const dynamicallyImportedKeys = new Set( + Object.values(manifest).flatMap((entry) => entry.dynamicImports ?? []), + ); + return Object.entries(manifest).find( + ([key, entry]) => + entry.name === moduleId && + (entry.isDynamicEntry === true || dynamicallyImportedKeys.has(key)), + )?.[1]; +} diff --git a/scripts/verify-release.ts b/scripts/verify-release.ts index a577ad5..ae4eda2 100644 --- a/scripts/verify-release.ts +++ b/scripts/verify-release.ts @@ -28,6 +28,10 @@ import { } from "./lib/build-manifest-outputs.ts"; import { verifyReleaseRuntimeCoherence } from "./lib/release-runtime-coherence.ts"; import { writeValidatedJsonArtifact } from "./lib/validated-json-artifact.ts"; +import { + findViteDynamicRouteChunk, + type ViteManifestRouteEntry, +} from "./lib/vite-route-chunks.ts"; import { releaseVerificationArtifactSchema } from "./contracts/release-artifacts.ts"; type CoherenceFixture = Readonly<{ @@ -38,11 +42,6 @@ type CoherenceFixture = Readonly<{ }>; type RuntimeConfigDocument = RuntimeConfigArtifact & Readonly<{ BUILD_ID: string; RELEASE_ID: string }>; -type ViteManifestEntry = Readonly<{ - file: string; - name?: string; - isDynamicEntry?: boolean; -}>; export type ReleaseArtifactReader = (path: string) => Promise; @@ -170,9 +169,9 @@ const runtimeContracts: Readonly> = ROUTE_RUNTIME_CONTRACT; for (const definition of Object.values(ROUTE_REGISTRY)) { const runtime = runtimeContracts[definition.routeId]; - const viteEntry = Object.values(viteManifestObject).find( - (entry) => entry.name === runtime?.moduleId && entry.isDynamicEntry, - ); + const viteEntry = runtime + ? findViteDynamicRouteChunk(viteManifestObject, runtime.moduleId) + : undefined; const routeAsset = release.routeChunks[definition.chunkId]; if (!runtime || !viteEntry || routeAsset !== viteEntry.file) { artifactMismatches.push(`routeChunk:mismatch:${definition.chunkId}`); @@ -315,9 +314,9 @@ function parseCompatibilityTuple(value: unknown, label: string): CompatibilityTu function parseViteManifest( value: unknown, -): Readonly> { +): Readonly> { const document = requireRecord(value, "Vite manifest"); - const entries: Record = {}; + const entries: Record = {}; for (const [key, candidate] of Object.entries(document)) { const entry = requireRecord(candidate, `Vite manifest entry ${key}`); entries[key] = { @@ -326,6 +325,10 @@ function parseViteManifest( ...(typeof entry.isDynamicEntry === "boolean" ? { isDynamicEntry: entry.isDynamicEntry } : {}), + ...(Array.isArray(entry.dynamicImports) && + entry.dynamicImports.every((item) => typeof item === "string") + ? { dynamicImports: entry.dynamicImports as string[] } + : {}), }; } return entries; diff --git a/src/bootstrap/main.tsx b/src/bootstrap/main.tsx index 55ef0ea..b8e31ab 100644 --- a/src/bootstrap/main.tsx +++ b/src/bootstrap/main.tsx @@ -2,7 +2,6 @@ import { createRoot } from "react-dom/client"; import { recordBootFailure } from "../adapters/diagnostics/bounded-diagnostics.ts"; import { BootErrorShell } from "../presentation/boundaries/boot-error-shell.tsx"; -import "../presentation/styles/theme.css"; import "pretendard/dist/web/variable/pretendardvariable.css"; import "@fontsource/ibm-plex-mono/400.css"; import "@fontsource/ibm-plex-mono/500.css"; diff --git a/src/features/tech-log/presentation/public/components/site-header.tsx b/src/features/tech-log/presentation/public/components/site-header.tsx index dcedc98..0ce018d 100644 --- a/src/features/tech-log/presentation/public/components/site-header.tsx +++ b/src/features/tech-log/presentation/public/components/site-header.tsx @@ -13,7 +13,9 @@ const navigation = [ export function SiteHeader({ currentPath }: { currentPath?: string }) { const location = useLocation(); - const activePath = currentPath ?? location.pathname; + const activePath = + currentPath ?? + (location.pathname.startsWith("/topics/") ? "/explore" : location.pathname); const menuRef = useRef(null); const summaryRef = useRef(null); diff --git a/src/features/tech-log/presentation/public/pages/public-not-found-page.tsx b/src/features/tech-log/presentation/public/pages/public-not-found-page.tsx index b0c1a4c..4627d8e 100644 --- a/src/features/tech-log/presentation/public/pages/public-not-found-page.tsx +++ b/src/features/tech-log/presentation/public/pages/public-not-found-page.tsx @@ -1,53 +1,34 @@ -import type { CSSProperties } from "react"; - -const styles = { - error: { - fontFamily: - 'system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"', - height: "100vh", - textAlign: "center", - display: "flex", - flexDirection: "column", - alignItems: "center", - justifyContent: "center", - }, - desc: { - display: "inline-block", - }, - h1: { - display: "inline-block", - margin: "0 20px 0 0", - padding: "0 23px 0 0", - fontSize: 24, - fontWeight: 500, - verticalAlign: "top", - lineHeight: "49px", - }, - h2: { - fontSize: 14, - fontWeight: 400, - lineHeight: "49px", - margin: 0, - }, -} as const satisfies Record; +import { useSourceCompatiblePublicNotFound } from "../public-not-found-layout.tsx"; export function PublicNotFoundPage() { + useSourceCompatiblePublicNotFound(); return ( <> - 404: This page could not be found. -
-
- -

- 404 -

-
-

This page could not be found.

-
-
-
+ +
Not Found
); } diff --git a/src/features/tech-log/presentation/public/public-not-found-layout.tsx b/src/features/tech-log/presentation/public/public-not-found-layout.tsx new file mode 100644 index 0000000..c5dd3c1 --- /dev/null +++ b/src/features/tech-log/presentation/public/public-not-found-layout.tsx @@ -0,0 +1,18 @@ +import { + createContext, + useContext, + useLayoutEffect, +} from "react"; + +type SetNestedNotFound = (active: boolean) => void; + +export const PublicNotFoundLayoutContext = + createContext(null); + +export function useSourceCompatiblePublicNotFound(): void { + const setNestedNotFound = useContext(PublicNotFoundLayoutContext); + useLayoutEffect(() => { + setNestedNotFound?.(true); + return () => setNestedNotFound?.(false); + }, [setNestedNotFound]); +} diff --git a/src/features/tech-log/presentation/public/public-shell.tsx b/src/features/tech-log/presentation/public/public-shell.tsx index a94646e..9ce15a2 100644 --- a/src/features/tech-log/presentation/public/public-shell.tsx +++ b/src/features/tech-log/presentation/public/public-shell.tsx @@ -1,8 +1,9 @@ -import type { ReactNode } from "react"; +import { type ReactNode, useCallback, useState } from "react"; import { Link } from "react-router-dom"; import { publicSiteConfig } from "../../contracts/public-site-config.ts"; import { SiteHeader } from "./components/site-header.tsx"; +import { PublicNotFoundLayoutContext } from "./public-not-found-layout.tsx"; type PublicShellProps = { children: ReactNode; @@ -10,29 +11,37 @@ type PublicShellProps = { }; export function PublicShell({ children, currentPath }: PublicShellProps) { + const [nestedNotFound, setNestedNotFound] = useState(false); + const updateNestedNotFound = useCallback((active: boolean) => { + setNestedNotFound(active); + }, []); return ( -
- - 본문으로 건너뛰기 - - + + {nestedNotFound ? children : ( +
+ + 본문으로 건너뛰기 + + - {children} + {children} -
-
-
-

{publicSiteConfig.operator}

-

{publicSiteConfig.identityStatement}

-
-
- - {publicSiteConfig.contactLabel} - - 최신 Release -
+
+
+
+

{publicSiteConfig.operator}

+

{publicSiteConfig.identityStatement}

+
+
+ + {publicSiteConfig.contactLabel} + + 최신 Release +
+
+
-
-
+ )} +
); } diff --git a/src/features/tech-log/presentation/studio/components/instant-preview.tsx b/src/features/tech-log/presentation/studio/components/instant-preview.tsx index 713bd3e..274dcce 100644 --- a/src/features/tech-log/presentation/studio/components/instant-preview.tsx +++ b/src/features/tech-log/presentation/studio/components/instant-preview.tsx @@ -3,6 +3,7 @@ import type { WorkingCopyInput } from "../../../contracts/studio/contract.ts"; import { ContentFormatError } from "../../../domain/content-format/parse-case-content.ts"; import { projectWorkingCopy } from "../../../domain/content-format/project-public-render-model.ts"; import { PublicRecordRenderer } from "../../shared/public-render/public-record-renderer.tsx"; +import { useStudio } from "../use-studio.ts"; type CatalogEntry = components["schemas"]["CatalogEntry"]; @@ -24,6 +25,7 @@ function resolvePreviewEvidenceAsset(key: string) { } export function InstantPreview({ draft, catalog }: { draft: WorkingCopyInput; catalog: CatalogEntry[] }) { + const studio = useStudio(); let model: ReturnType | null = null; let issues: string[] | null = null; try { @@ -45,7 +47,7 @@ export function InstantPreview({ draft, catalog }: { draft: WorkingCopyInput; ca model={model!} embedded resolveEvidenceAsset={resolvePreviewEvidenceAsset} - resolvePublishedLabel={() => undefined} + resolvePublishedLabel={studio.resolvePublishedLabel} />
); diff --git a/src/features/tech-log/presentation/studio/components/public-preview-screen.tsx b/src/features/tech-log/presentation/studio/components/public-preview-screen.tsx index 6983a4c..4c8180b 100644 --- a/src/features/tech-log/presentation/studio/components/public-preview-screen.tsx +++ b/src/features/tech-log/presentation/studio/components/public-preview-screen.tsx @@ -288,7 +288,7 @@ export function PublicPreviewScreen({ documentId }: { documentId: string }) { model={previewDetail.preview.renderModel} embedded resolveEvidenceAsset={resolvePreviewEvidenceAsset} - resolvePublishedLabel={() => undefined} + resolvePublishedLabel={studio.resolvePublishedLabel} /> diff --git a/src/features/tech-log/presentation/studio/components/publication-event-preview-screen.tsx b/src/features/tech-log/presentation/studio/components/publication-event-preview-screen.tsx index aa9e801..4f2d936 100644 --- a/src/features/tech-log/presentation/studio/components/publication-event-preview-screen.tsx +++ b/src/features/tech-log/presentation/studio/components/publication-event-preview-screen.tsx @@ -1,5 +1,3 @@ -/* eslint-disable react-hooks/set-state-in-effect -- a changed event or retry intentionally enters a fresh loading state. */ - import { useEffect, useState } from "react"; import { @@ -50,7 +48,8 @@ export function PublicationEventPreviewScreen({ publicationEventId: string; classes?: PublicationFlowClasses; }) { - const { gateway } = useStudio(); + const studio = useStudio(); + const { gateway } = studio; const [retry, setRetry] = useState(0); const [state, setState] = useState< | { status: "LOADING" } @@ -168,7 +167,7 @@ export function PublicationEventPreviewScreen({ model={renderModel} embedded resolveEvidenceAsset={resolveEvidenceAsset} - resolvePublishedLabel={() => undefined} + resolvePublishedLabel={studio.resolvePublishedLabel} /> diff --git a/src/features/tech-log/presentation/studio/components/publication-flow-classes.ts b/src/features/tech-log/presentation/studio/components/publication-flow-classes.ts index 3acb427..4c5aeba 100644 --- a/src/features/tech-log/presentation/studio/components/publication-flow-classes.ts +++ b/src/features/tech-log/presentation/studio/components/publication-flow-classes.ts @@ -1,4 +1,6 @@ -export const defaultPublicationFlowClasses = { +import styles from "../../styles/publication-flow.module.css"; + +const publicationFlowClassContract = { page: "studio-publication-page", snapshotPage: "studio-snapshot-page", heading: "studio-publication-heading", @@ -35,4 +37,11 @@ export const defaultPublicationFlowClasses = { snapshotDocument: "studio-publication-snapshot-document", } as const; -export type PublicationFlowClasses = Record; +export type PublicationFlowClasses = Record< + keyof typeof publicationFlowClassContract, + string +>; + +export const defaultPublicationFlowClasses = Object.fromEntries( + Object.keys(publicationFlowClassContract).map((key) => [key, styles[key]]), +) as PublicationFlowClasses; diff --git a/src/features/tech-log/presentation/studio/components/publication-list.tsx b/src/features/tech-log/presentation/studio/components/publication-list.tsx index c0bf8dc..993a664 100644 --- a/src/features/tech-log/presentation/studio/components/publication-list.tsx +++ b/src/features/tech-log/presentation/studio/components/publication-list.tsx @@ -1,5 +1,3 @@ -/* eslint-disable react-hooks/set-state-in-effect -- a changed history query intentionally enters a fresh loading state. */ - import { useEffect, useRef, useState, type FormEvent } from "react"; import { diff --git a/src/features/tech-log/presentation/studio/components/publish-screen.tsx b/src/features/tech-log/presentation/studio/components/publish-screen.tsx index d45686d..7a93757 100644 --- a/src/features/tech-log/presentation/studio/components/publish-screen.tsx +++ b/src/features/tech-log/presentation/studio/components/publish-screen.tsx @@ -1,5 +1,3 @@ -/* eslint-disable react-hooks/set-state-in-effect -- a changed document or retry intentionally enters a fresh loading state. */ - import { useEffect, useMemo, useState } from "react"; import { diff --git a/src/features/tech-log/presentation/studio/studio-provider.tsx b/src/features/tech-log/presentation/studio/studio-provider.tsx index 789a52b..263f73c 100644 --- a/src/features/tech-log/presentation/studio/studio-provider.tsx +++ b/src/features/tech-log/presentation/studio/studio-provider.tsx @@ -8,6 +8,7 @@ import { import { isStudioGatewayError } from "../../application/ports/studio-gateway-error.ts"; import type { StudioGateway } from "../../application/ports/studio-gateway.ts"; +import type { ResolvePublishedLabel } from "../../domain/public-render-content.ts"; import type { WorkingCopy, WorkingCopyInput, @@ -24,6 +25,7 @@ import { useBeforeUnload } from "./components/use-before-unload.ts"; type StudioProviderProps = Readonly<{ children: ReactNode; createGateway: () => StudioGateway; + resolvePublishedLabel?: ResolvePublishedLabel; now?: () => Date; navigate?: (href: string) => void; }>; @@ -41,9 +43,12 @@ function defaultNavigate(href: string): void { window.dispatchEvent(new PopStateEvent("popstate")); } +const missingPublishedLabel: ResolvePublishedLabel = () => undefined; + export function StudioProvider({ children, createGateway, + resolvePublishedLabel = missingPublishedLabel, now = () => new Date("2026-08-14T01:00:00.000Z"), navigate = defaultNavigate, }: StudioProviderProps) { @@ -143,6 +148,7 @@ export function StudioProvider({ const value = useMemo( () => ({ gateway, + resolvePublishedLabel, now, editor, requestAnnouncement, @@ -161,6 +167,7 @@ export function StudioProvider({ navigateInternal, now, requestAnnouncement, + resolvePublishedLabel, setEditorStatus, updateEditorDraft, ], diff --git a/src/features/tech-log/presentation/studio/studio-shell.tsx b/src/features/tech-log/presentation/studio/studio-shell.tsx index 421ca6a..bd9250f 100644 --- a/src/features/tech-log/presentation/studio/studio-shell.tsx +++ b/src/features/tech-log/presentation/studio/studio-shell.tsx @@ -47,6 +47,14 @@ export function StudioShell({ children }: StudioShellProps) { () => application.features.get(TECH_LOG_FEATURE_ID).createStudioGateway(), [application], ); + const resolvePublishedLabel = useCallback( + (path: string) => + application.features + .get(TECH_LOG_FEATURE_ID) + .publicContent.listRecords() + .find((record) => record.path === path)?.publishedLabel, + [application], + ); useEffect(() => { const resetPersistedSession = (event: PageTransitionEvent) => { @@ -61,6 +69,7 @@ export function StudioShell({ children }: StudioShellProps) { {children} diff --git a/src/features/tech-log/presentation/studio/use-studio.ts b/src/features/tech-log/presentation/studio/use-studio.ts index 434e39d..754a814 100644 --- a/src/features/tech-log/presentation/studio/use-studio.ts +++ b/src/features/tech-log/presentation/studio/use-studio.ts @@ -1,6 +1,7 @@ import { createContext, useContext, useMemo } from "react"; import type { StudioGateway } from "../../application/ports/studio-gateway.ts"; +import type { ResolvePublishedLabel } from "../../domain/public-render-content.ts"; import type { WorkingCopy, WorkingCopyInput, @@ -17,6 +18,7 @@ export type StudioEditorState = Readonly<{ export type StudioContextValue = Readonly<{ gateway: StudioGateway; + resolvePublishedLabel: ResolvePublishedLabel; now(): Date; editor: StudioEditorState | null; requestAnnouncement: string; diff --git a/src/features/tech-log/presentation/styles/globals.css b/src/features/tech-log/presentation/styles/globals.css index c5fb2a2..a86a5a1 100644 --- a/src/features/tech-log/presentation/styles/globals.css +++ b/src/features/tech-log/presentation/styles/globals.css @@ -1,3 +1,4 @@ +@import "tailwindcss"; :root { --canvas: #f7f7f5; diff --git a/src/presentation/routes/app-router.tsx b/src/presentation/routes/app-router.tsx index b7d46f5..efec2ba 100644 --- a/src/presentation/routes/app-router.tsx +++ b/src/presentation/routes/app-router.tsx @@ -119,8 +119,6 @@ function RouteLifecycle({ title: resolve(`route.${definition.routeId}.title`), appName: message("common.appName"), }); - const main = document.getElementById("main-content"); - main?.focus({ preventScroll: true }); try { if (!navigator.userAgent.toLowerCase().includes("jsdom")) { window.scrollTo({ top: 0, left: 0, behavior: "auto" }); diff --git a/tests/component/router.test.tsx b/tests/component/router.test.tsx index 8f62c53..bed450e 100644 --- a/tests/component/router.test.tsx +++ b/tests/component/router.test.tsx @@ -343,20 +343,12 @@ describe("generic application router", () => { expect(window.location.pathname).toBe("/projects"); }); - it("renders the Public shell and TechLog not-found route", async () => { + it("renders the source-compatible plain TechLog not-found response", async () => { window.history.pushState({}, "", "/missing"); renderRouter(); - expect( - await screen.findByRole("heading", { - name: "404", - level: 1, - }), - ).toBeVisible(); - expect( - screen.getByRole("heading", { name: "This page could not be found." }), - ).toBeVisible(); - expect(screen.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); + expect(await screen.findByText("Not Found", { exact: true })).toBeVisible(); + expect(screen.queryByRole("navigation", { name: "주요 탐색" })).not.toBeInTheDocument(); }); it("keeps Studio routes inside the persistent Studio layout", async () => { diff --git a/tests/e2e/accessibility.spec.ts b/tests/e2e/accessibility.spec.ts index a30a541..1c9ca97 100644 --- a/tests/e2e/accessibility.spec.ts +++ b/tests/e2e/accessibility.spec.ts @@ -1,58 +1,40 @@ import AxeBuilder from "@axe-core/playwright"; -import { expect, test } from "../support/browser/strict-browser-test.ts"; -import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts"; -for (const route of Object.values(ROUTE_REGISTRY).map((definition) => { - if (definition.path === "*") return "/not-found"; - return definition.path.replace(":resourceId", "reference-1"); -})) { - test(`@a11y ${route} has no critical or serious axe violations`, async ({ +import { expect, test } from "../support/browser/strict-browser-test.ts"; +import { + gotoTechLog, + TECH_LOG_CANONICAL_ROUTES, +} from "../support/browser/tech-log-fixtures.ts"; + +for (const route of TECH_LOG_CANONICAL_ROUTES) { + test(`@a11y ${route.routeId} has no critical or serious Axe violations`, async ({ page, }) => { - await page.goto(route); - await expect(page.getByRole("main")).toBeVisible(); + await gotoTechLog(page, route.path); const results = await new AxeBuilder({ page }) .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) .analyze(); - const blocking = results.violations.filter((violation) => - ["critical", "serious"].includes(violation.impact ?? ""), - ); - expect(blocking).toEqual([]); + expect( + results.violations.filter((violation) => + ["critical", "serious"].includes(violation.impact ?? ""), + ), + ).toEqual([]); }); } -test("@a11y keyboard reaches the primary route action with visible focus", async ({ +test("@a11y keyboard reaches a visible Public navigation focus indicator", async ({ page, }) => { - await page.goto("/"); - const action = page.getByRole("link", { name: "플랫폼 구성 보기" }); - await expect(action).toBeVisible(); - await page.keyboard.press("Tab"); + await gotoTechLog(page, "/"); + const action = page.getByRole("link", { name: "TechLog 홈" }); + await action.focus(); await expect(action).toBeFocused(); await expect(action).toHaveCSS("outline-style", "solid"); }); -test("@a11y reduced-motion policy disables long animation", async ({ page }) => { - await page.emulateMedia({ reducedMotion: "reduce" }); - await page.goto("/"); - const duration = await page - .locator("body") - .evaluate((body) => getComputedStyle(body).animationDuration); - expect(["0s", "0.00001s", "1e-05s"]).toContain(duration); -}); - -test("@a11y opened design-system dialog has no blocking violations", async ({ +test("@a11y reduced-motion preference leaves no running document animations", async ({ page, }) => { - await page.goto("/examples/ui"); - await page.getByRole("button", { name: "모달 열기" }).click(); - const results = await new AxeBuilder({ page }) - .include(".ui-dialog") - .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) - .analyze(); - expect( - results.violations.filter((violation) => - ["critical", "serious"].includes(violation.impact ?? ""), - ), - ).toEqual([]); + await gotoTechLog(page, "/"); + expect(await page.evaluate(() => document.getAnimations().length)).toBe(0); }); diff --git a/tests/e2e/app-shell.spec.ts b/tests/e2e/app-shell.spec.ts index 278b2c0..2c2adb0 100644 --- a/tests/e2e/app-shell.spec.ts +++ b/tests/e2e/app-shell.spec.ts @@ -1,7 +1,8 @@ import { expect, test } from "../support/browser/strict-browser-test.ts"; +import { gotoTechLog } from "../support/browser/tech-log-fixtures.ts"; test("boots the public app shell", async ({ page }) => { - await page.goto("/"); + await gotoTechLog(page, "/"); await expect(page.getByRole("heading", { level: 1 })).toHaveText( "Tech Log", ); @@ -9,36 +10,32 @@ test("boots the public app shell", async ({ page }) => { await expect(page.getByRole("main")).toBeVisible(); }); -test("navigates to a registry-backed example without a page reload", async ({ +test("navigates to a registry-backed Public route without a page reload", async ({ page, }) => { - await page.goto("/"); + await gotoTechLog(page, "/"); await page .getByRole("navigation", { name: "주요 탐색" }) - .getByRole("link", { name: "화면 상태" }) + .getByRole("link", { name: "프로젝트" }) .click(); - await expect(page).toHaveURL(/\/examples\/states$/); + await expect(page).toHaveURL(/\/projects$/); await expect( - page.getByRole("heading", { level: 1, name: "화면 상태" }), - ).toBeFocused(); + page.getByRole("heading", { level: 1, name: "프로젝트" }), + ).toBeVisible(); }); test("provides an escape-dismissible mobile navigation", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); - await page.goto("/"); - const menu = page.getByRole("button", { name: "메뉴", exact: true }); + await gotoTechLog(page, "/"); + const menu = page.locator("details.mobile-nav > summary"); await menu.click(); - await expect(menu).toHaveAttribute("aria-expanded", "true"); - await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); - await expect(page.locator(".ui-drawer")).toHaveJSProperty("open", true); - expect( - await page.locator(".ui-drawer").evaluate((drawer) => drawer.matches(":modal")), - ).toBe(true); + await expect(page.locator("details.mobile-nav")).toHaveAttribute("open", ""); + await expect(page.getByRole("navigation", { name: "모바일 주요 탐색" })).toBeVisible(); await page.keyboard.press("Escape"); - await expect(menu).toHaveAttribute("aria-expanded", "false"); - await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeHidden(); + await expect(page.locator("details.mobile-nav")).not.toHaveAttribute("open", ""); + await expect(page.getByRole("navigation", { name: "모바일 주요 탐색" })).toBeHidden(); await expect(menu).toBeFocused(); }); diff --git a/tests/e2e/compact-smoke.spec.ts b/tests/e2e/compact-smoke.spec.ts deleted file mode 100644 index 7326995..0000000 --- a/tests/e2e/compact-smoke.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { expect, test } from "../support/browser/strict-browser-test.ts"; - -test("boots and navigates the compact production shell", async ({ page }) => { - await page.goto("/"); - await expect(page.getByRole("main")).toBeVisible(); - const menu = page.getByRole("button", { name: "메뉴", exact: true }); - await expect(menu).toHaveCSS("min-width", "44px"); - await menu.click(); - const navigation = page.getByRole("navigation", { name: "주요 탐색" }); - await expect(navigation).toBeVisible(); - // The drawer is a non-modal dialog, so page content stays in the - // accessibility tree while it is open. Scope to the navigation and match the - // whole name, or a route call to action on the page behind it also matches. - await navigation - .getByRole("link", { name: "UI 구성요소", exact: true }) - .click(); - await expect(page).toHaveURL(/\/examples\/ui$/); - await expect(page.locator("html")).toHaveAttribute("data-build-id", "local-build"); - const overflow = await page.evaluate( - () => document.documentElement.scrollWidth - window.innerWidth, - ); - expect(overflow).toBeLessThanOrEqual(1); -}); diff --git a/tests/e2e/design-system-interactions.spec.ts b/tests/e2e/design-system-interactions.spec.ts deleted file mode 100644 index 08cf811..0000000 --- a/tests/e2e/design-system-interactions.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { expect, test } from "../support/browser/strict-browser-test.ts"; - -test("runs menu typeahead, tabs and duplicate toast interactions", async ({ - page, -}) => { - await page.goto("/examples/ui"); - - const menuTrigger = page.getByRole("button", { name: "작업 메뉴" }); - await menuTrigger.focus(); - await page.keyboard.press("ArrowDown"); - await page.keyboard.type("toast"); - const toastItem = page.getByRole("menuitem", { name: "Toast 표시" }); - await expect(toastItem).toBeFocused(); - await page.keyboard.press("Enter"); - await expect(page.getByText("예제가 저장되었습니다.")).toBeVisible(); - await expect(menuTrigger).toBeFocused(); - - const tokenTab = page.getByRole("tab", { name: "토큰" }); - await tokenTab.focus(); - await page.keyboard.press("ArrowRight"); - const primitiveTab = page.getByRole("tab", { name: "프리미티브" }); - await expect(primitiveTab).toBeFocused(); - await expect(primitiveTab).toHaveAttribute("aria-selected", "false"); - await page.keyboard.press("Enter"); - await expect(primitiveTab).toHaveAttribute("aria-selected", "true"); - await expect( - page.getByRole("tabpanel", { name: "프리미티브" }), - ).toContainText("native semantics"); -}); diff --git a/tests/e2e/i18n.spec.ts b/tests/e2e/i18n.spec.ts deleted file mode 100644 index bbff8e4..0000000 --- a/tests/e2e/i18n.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { expect, test } from "../support/browser/strict-browser-test.ts"; - -test("switches the shell locale and keeps pseudo-locale copy within compact layout", async ({ - page, -}) => { - await page.setViewportSize({ width: 320, height: 720 }); - await page.goto("/"); - - const locale = page.getByRole("combobox", { name: "언어" }); - await locale.selectOption("en-US"); - await expect(page.locator("html")).toHaveAttribute("lang", "en-US"); - await expect(page.locator("html")).toHaveAttribute("dir", "ltr"); - await expect(page.getByRole("button", { name: "Menu" })).toBeVisible(); - - await page.getByRole("combobox", { name: "Language" }).selectOption("en-XA"); - await expect(page.locator("html")).toHaveAttribute("lang", "en-XA"); - const dimensions = await page.evaluate(() => ({ - clientWidth: document.documentElement.clientWidth, - scrollWidth: document.documentElement.scrollWidth, - })); - expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); - await expect(page.getByRole("button", { name: /Ménú/ })).toBeVisible(); -}); - -test("applies RTL to the shell and modal navigation without changing action semantics", async ({ - page, -}) => { - await page.setViewportSize({ width: 390, height: 844 }); - await page.goto("/"); - await page.getByRole("combobox", { name: "언어" }).selectOption("ar-EG"); - - await expect(page.locator("html")).toHaveAttribute("lang", "ar-EG"); - await expect(page.locator("html")).toHaveAttribute("dir", "rtl"); - const trigger = page.getByRole("button", { name: "Menu" }); - await trigger.click(); - await expect(page.getByRole("dialog", { name: "Menu" })).toBeVisible(); - await expect(page.getByRole("navigation", { name: "Primary navigation" })) - .toHaveCount(1); - await page.keyboard.press("Escape"); - await expect(trigger).toBeFocused(); -}); diff --git a/tests/e2e/responsive.spec.ts b/tests/e2e/responsive.spec.ts index 679250b..f2d8c32 100644 --- a/tests/e2e/responsive.spec.ts +++ b/tests/e2e/responsive.spec.ts @@ -1,36 +1,36 @@ import { expect, test } from "../support/browser/strict-browser-test.ts"; +import { + gotoTechLog, + horizontalOverflow, + TECH_LOG_CANONICAL_ROUTES, +} from "../support/browser/tech-log-fixtures.ts"; -test("reflows the UI gallery at the 320px minimum without horizontal overflow", async ({ - page, -}) => { - await page.setViewportSize({ width: 320, height: 720 }); - await page.goto("/examples/ui"); - await expect( - page.getByRole("heading", { level: 1, name: "UI 구성요소" }), - ).toBeVisible(); - await expect( - page.getByRole("button", { name: "메뉴", exact: true }), - ).toBeVisible(); +for (const route of TECH_LOG_CANONICAL_ROUTES) { + for (const width of [360, 1440] as const) { + test(`${route.routeId} has no horizontal overflow at ${width}px`, async ({ page }) => { + await page.setViewportSize({ width, height: 900 }); + await gotoTechLog(page, route.path); + const dimensions = await horizontalOverflow(page); + expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); + if (route.routeId !== "NOT_FOUND") { + await expect(page.getByRole("main")).toBeVisible(); + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + } else { + await expect(page.getByText("Not Found", { exact: true })).toBeVisible(); + } + }); + } +} - const viewport = await page.evaluate(() => ({ - clientWidth: document.documentElement.clientWidth, - scrollWidth: document.documentElement.scrollWidth, - })); - expect(viewport.scrollWidth).toBeLessThanOrEqual(viewport.clientWidth); -}); - -test("keeps desktop navigation and two-column examples at wide viewports", async ({ - page, -}) => { - await page.setViewportSize({ width: 1440, height: 900 }); - await page.goto("/examples/ui"); - - await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); - await expect( - page.getByRole("button", { name: "메뉴", exact: true }), - ).toBeHidden(); - await expect(page.locator(".component-grid--two").first()).toHaveCSS( - "grid-template-columns", - /.+px .+px/, +test("compact Public controls retain 44px targets", async ({ page }) => { + await page.setViewportSize({ width: 375, height: 812 }); + await gotoTechLog(page, "/"); + await expect(page.locator("details.mobile-nav > summary")).toHaveCSS( + "min-height", + "44px", + ); + await expect(page.getByRole("button", { name: "검색 열기" })).toHaveCSS( + "min-height", + "44px", ); }); diff --git a/tests/e2e/tech-log-accessibility.spec.ts b/tests/e2e/tech-log-accessibility.spec.ts new file mode 100644 index 0000000..49531af --- /dev/null +++ b/tests/e2e/tech-log-accessibility.spec.ts @@ -0,0 +1,77 @@ +import AxeBuilder from "@axe-core/playwright"; + +import { expect, test } from "../support/browser/strict-browser-test.ts"; +import { gotoTechLog } from "../support/browser/tech-log-fixtures.ts"; + +function blockingViolations(results: Awaited>) { + return results.violations.filter((violation) => + ["critical", "serious"].includes(violation.impact ?? ""), + ); +} + +test("Public shell exposes one main landmark, one h1, labelled navigation, and current link", async ({ + page, +}) => { + await gotoTechLog(page, "/projects"); + await expect(page.getByRole("main")).toHaveCount(1); + await expect(page.getByRole("heading", { level: 1 })).toHaveCount(1); + await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); + await expect(page.getByRole("link", { name: "프로젝트", exact: true })).toHaveAttribute( + "aria-current", + "page", + ); +}); + +test("Studio mobile navigation is keyboard reachable and exposes its expanded state", async ({ + page, +}) => { + await page.setViewportSize({ width: 390, height: 844 }); + await gotoTechLog(page, "/studio"); + const trigger = page.locator(".studio-menu-trigger"); + await trigger.focus(); + await page.keyboard.press("Enter"); + await expect(trigger).toHaveAttribute("aria-expanded", "true"); + await expect(page.getByRole("navigation", { name: "Studio 모바일 탐색" })).toBeVisible(); +}); + +test("dirty-leave dialog is labelled, traps focus, and restores the invoking link", async ({ + page, +}) => { + await gotoTechLog( + page, + "/studio/documents/11111111-1111-4111-8111-111111111111/edit", + ); + await page.getByLabel("요약").fill("저장하지 않은 접근성 검증 변경"); + const trigger = page.getByRole("link", { name: "게시 기록", exact: true }).first(); + await trigger.click(); + const dialog = page.getByRole("dialog", { name: "저장하지 않은 변경" }); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole("button", { name: "이 페이지에 머무르기" })).toBeFocused(); + await page.keyboard.press("Escape"); + await expect(dialog).toBeHidden(); + await expect(trigger).toBeFocused(); +}); + +test("open Public search and Studio leave dialogs have no blocking Axe findings", async ({ + page, +}) => { + await gotoTechLog(page, "/"); + await page.getByRole("button", { name: "검색 열기" }).click(); + let results = await new AxeBuilder({ page }) + .include("dialog") + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) + .analyze(); + expect(blockingViolations(results)).toEqual([]); + + await gotoTechLog( + page, + "/studio/documents/11111111-1111-4111-8111-111111111111/edit", + ); + await page.getByLabel("요약").fill("dialog axe state"); + await page.getByRole("link", { name: "게시 기록", exact: true }).first().click(); + results = await new AxeBuilder({ page }) + .include("dialog") + .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) + .analyze(); + expect(blockingViolations(results)).toEqual([]); +}); diff --git a/tests/e2e/tech-log-public-discovery.spec.ts b/tests/e2e/tech-log-public-discovery.spec.ts new file mode 100644 index 0000000..04a0a88 --- /dev/null +++ b/tests/e2e/tech-log-public-discovery.spec.ts @@ -0,0 +1,44 @@ +import { expect, test } from "../support/browser/strict-browser-test.ts"; +import { + gotoTechLog, + TECH_LOG_PUBLIC_FIXTURE_PATHS, +} from "../support/browser/tech-log-fixtures.ts"; + +for (const path of TECH_LOG_PUBLIC_FIXTURE_PATHS) { + test(`renders known Public fixture ${path}`, async ({ page }) => { + await gotoTechLog(page, path); + await expect(page.locator(".site-frame")).toBeVisible(); + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + await expect(page.getByText("404", { exact: true })).toHaveCount(0); + }); +} + +test("search dialog traps focus, closes with Escape, and restores its trigger", async ({ + page, +}) => { + await gotoTechLog(page, "/"); + const trigger = page.getByRole("button", { name: "TechLog 검색 열기" }); + await trigger.click(); + const dialog = page.getByRole("dialog", { name: "TechLog 검색" }); + await expect(dialog).toBeVisible(); + await expect(dialog.getByRole("searchbox", { name: "검색어" })).toBeFocused(); + await page.keyboard.press("Tab"); + await page.keyboard.press("Shift+Tab"); + await expect(dialog.getByRole("searchbox", { name: "검색어" })).toBeFocused(); + await page.keyboard.press("Escape"); + await expect(dialog).toBeHidden(); + await expect(trigger).toBeFocused(); +}); + +test("search and explore navigation preserve canonical URL state", async ({ page }) => { + await gotoTechLog(page, "/explore"); + await page.getByLabel("유형").selectOption("CASE"); + await page.getByRole("button", { name: "적용" }).click(); + await expect(page).toHaveURL(/\/explore\?type=CASE$/); + + await page.getByRole("button", { name: "TechLog 검색 열기" }).click(); + await page.getByRole("searchbox", { name: "검색어" }).fill("JPA"); + await page.getByRole("link", { name: /전체 검색 결과 보기/ }).click(); + await expect(page).toHaveURL(/\/search\?q=JPA$/); + await expect(page.getByRole("heading", { level: 1, name: "검색" })).toBeVisible(); +}); diff --git a/tests/e2e/tech-log-responsive.spec.ts b/tests/e2e/tech-log-responsive.spec.ts new file mode 100644 index 0000000..9b79728 --- /dev/null +++ b/tests/e2e/tech-log-responsive.spec.ts @@ -0,0 +1,41 @@ +import { expect, test } from "../support/browser/strict-browser-test.ts"; +import { + gotoTechLog, + horizontalOverflow, + TECH_LOG_BREAKPOINT_WIDTHS, +} from "../support/browser/tech-log-fixtures.ts"; + +for (const width of TECH_LOG_BREAKPOINT_WIDTHS) { + test(`Public shell has no overflow and preserves the ${width}px breakpoint contract`, async ({ + page, + }) => { + await page.setViewportSize({ width, height: 900 }); + await gotoTechLog(page, "/"); + const dimensions = await horizontalOverflow(page); + expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); + + if (width <= 1050) { + await expect(page.locator("details.mobile-nav > summary")).toBeVisible(); + await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeHidden(); + } else { + await expect(page.locator("details.mobile-nav > summary")).toBeHidden(); + await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible(); + } + }); +} + +for (const width of [1050, 1024, 980, 900, 820, 768, 767, 420, 390, 375] as const) { + test(`Studio editor has no overflow and preserves the ${width}px editor transition`, async ({ + page, + }) => { + await page.setViewportSize({ width, height: 1000 }); + await gotoTechLog( + page, + "/studio/documents/11111111-1111-4111-8111-111111111111/edit", + ); + const dimensions = await horizontalOverflow(page); + expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth); + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + await expect(page.getByRole("button", { name: "저장" })).toBeVisible(); + }); +} diff --git a/tests/e2e/theme.spec.ts b/tests/e2e/theme.spec.ts deleted file mode 100644 index b1dd6f1..0000000 --- a/tests/e2e/theme.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import AxeBuilder from "@axe-core/playwright"; -import { expect, test } from "../support/browser/strict-browser-test.ts"; - -test("persists an explicit color scheme through the storage contract", async ({ - page, -}) => { - await page.goto("/"); - const selector = page.getByRole("combobox", { name: "색상 테마" }); - - await selector.selectOption("dark"); - await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); - await expect(page.locator("html")).toHaveCSS("color-scheme", "dark"); - const results = await new AxeBuilder({ page }) - .withTags(["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"]) - .analyze(); - expect( - results.violations.filter((violation) => - ["critical", "serious"].includes(violation.impact ?? ""), - ), - ).toEqual([]); - - await page.reload(); - await expect(selector).toHaveValue("dark"); - await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); -}); - -test("tracks operating-system changes while system preference is selected", async ({ - page, -}) => { - await page.emulateMedia({ colorScheme: "dark" }); - await page.goto("/"); - const selector = page.getByRole("combobox", { name: "색상 테마" }); - await selector.selectOption("system"); - await expect(page.locator("html")).toHaveAttribute("data-theme", "dark"); - - await page.emulateMedia({ colorScheme: "light" }); - await expect(page.locator("html")).toHaveAttribute("data-theme", "light"); -}); diff --git a/tests/features/tech-log/public-discovery-screens.test.tsx b/tests/features/tech-log/public-discovery-screens.test.tsx index 6683ec5..ce0b544 100644 --- a/tests/features/tech-log/public-discovery-screens.test.tsx +++ b/tests/features/tech-log/public-discovery-screens.test.tsx @@ -372,20 +372,18 @@ describe("TechLog explore discovery", () => { ).toHaveAttribute("href", "/explore"); }); - it("renders an unknown kind through the registered Public not-found runtime", () => { + it("renders an unknown kind as the source production plain 404", async () => { const { router, container } = renderDiscoveryRoute( "TECH_LOG_EXPLORE_KIND", "/explore/unknown", ); expect(router.state.location.pathname).toBe("/explore/unknown"); - expect( - screen.getByRole("heading", { name: "페이지를 찾을 수 없습니다." }), - ).toBeVisible(); + expect(await screen.findByText("Not Found", { exact: true })).toBeVisible(); expect( screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }), ).not.toBeInTheDocument(); - expect(container.querySelector(".site-frame")).not.toBeNull(); + expect(container.querySelector(".site-frame")).toBeNull(); }); it("recovers a rejecting registered not-found runtime under its own chunk contract", async () => { diff --git a/tests/features/tech-log/public-document-screens.test.tsx b/tests/features/tech-log/public-document-screens.test.tsx index 839cb2c..c3883f7 100644 --- a/tests/features/tech-log/public-document-screens.test.tsx +++ b/tests/features/tech-log/public-document-screens.test.tsx @@ -337,12 +337,12 @@ describe("TechLog topics and Public not-found routing", () => { ["TECH_LOG_REFERENCE" as const, "/references/not-registered"], ["TECH_LOG_QUESTION" as const, "/questions/not-registered"], ["TECH_LOG_TOPIC" as const, "/topics/not-registered"], - ])("uses the registered in-shell Public not-found for %s", (routeId, path) => { + ])("uses the source production plain 404 for %s", async (routeId, path) => { const { container, router } = renderDocumentRoute(routeId, path); expect(router.state.location.pathname).toBe(path); - expect(screen.getByRole("heading", { name: "페이지를 찾을 수 없습니다." })).toBeVisible(); + expect(await screen.findByText("Not Found", { exact: true })).toBeVisible(); expect(screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." })).not.toBeInTheDocument(); - expect(container.querySelector(".site-frame")).not.toBeNull(); + expect(container.querySelector(".site-frame")).toBeNull(); }); }); diff --git a/tests/features/tech-log/public-index-screens.test.tsx b/tests/features/tech-log/public-index-screens.test.tsx index 747b030..736e7c1 100644 --- a/tests/features/tech-log/public-index-screens.test.tsx +++ b/tests/features/tech-log/public-index-screens.test.tsx @@ -360,20 +360,25 @@ describe("TechLog Public not-found runtime", () => { ["TECH_LOG_PROJECT_RECORDS" as const, "/projects/missing-project/records"], ["TECH_LOG_RELEASE" as const, "/releases/9.9.9"], ["TECH_LOG_CASE" as const, "/cases/not-registered"], - ["NOT_FOUND" as const, "/definitely-not-a-product-route"], - ])("renders exact in-shell Public fallback copy for %s", (routeId, path) => { + ])("renders the source-compatible plain fallback for %s", async (routeId, path) => { const { container, router } = renderPublicRoute(routeId, path); expect(router.state.location.pathname).toBe(path); - expect(screen.getByRole("heading", { level: 1, name: "404" })).toHaveClass( - "next-error-h1", - ); - expect( - screen.getByRole("heading", { level: 2, name: "This page could not be found." }), - ).toBeVisible(); + expect(await screen.findByText("Not Found", { exact: true })).toBeVisible(); expect( screen.queryByRole("heading", { name: "화면을 표시하지 못했습니다." }), ).not.toBeInTheDocument(); - expect(container.querySelector(".site-frame")).not.toBeNull(); + expect(container.querySelector(".site-frame")).toBeNull(); + }); + + it("renders the source-compatible plain fallback for the Public catch-all", () => { + const { container, router } = renderPublicRoute( + "NOT_FOUND", + "/definitely-not-a-product-route", + ); + + expect(router.state.location.pathname).toBe("/definitely-not-a-product-route"); + expect(screen.getByText("Not Found", { exact: true })).toBeVisible(); + expect(container.querySelector(".site-frame")).toBeNull(); }); }); diff --git a/tests/support/browser/tech-log-fixtures.ts b/tests/support/browser/tech-log-fixtures.ts new file mode 100644 index 0000000..de19416 --- /dev/null +++ b/tests/support/browser/tech-log-fixtures.ts @@ -0,0 +1,140 @@ +import type { Page } from "@playwright/test"; + +export const TECH_LOG_FIXED_TIME = "2026-08-14T01:00:00.000Z"; + +export const TECH_LOG_CANONICAL_ROUTES = [ + { routeId: "TECH_LOG_HOME", path: "/", layout: "PUBLIC" }, + { routeId: "TECH_LOG_EXPLORE", path: "/explore", layout: "PUBLIC" }, + { routeId: "TECH_LOG_EXPLORE_KIND", path: "/explore/cases", layout: "PUBLIC" }, + { routeId: "TECH_LOG_CASE", path: "/cases/collection-fetch-join-pagination", layout: "PUBLIC" }, + { routeId: "TECH_LOG_REFERENCE", path: "/references/state-and-nonce-boundary", layout: "PUBLIC" }, + { routeId: "TECH_LOG_QUESTION", path: "/questions/validate-edge-token-again", layout: "PUBLIC" }, + { routeId: "TECH_LOG_TOPIC", path: "/topics/jpa", layout: "PUBLIC" }, + { routeId: "TECH_LOG_PROJECTS", path: "/projects", layout: "PUBLIC" }, + { routeId: "TECH_LOG_PROJECT", path: "/projects/backend-skeleton", layout: "PUBLIC" }, + { routeId: "TECH_LOG_PROJECT_RECORDS", path: "/projects/backend-skeleton/records", layout: "PUBLIC" }, + { routeId: "TECH_LOG_PROJECT_DECISIONS", path: "/projects/backend-skeleton/decisions", layout: "PUBLIC" }, + { routeId: "TECH_LOG_PROJECT_ACTIVITY", path: "/projects/backend-skeleton/activity", layout: "PUBLIC" }, + { routeId: "TECH_LOG_RELEASES", path: "/releases", layout: "PUBLIC" }, + { routeId: "TECH_LOG_RELEASE", path: "/releases/0.1.0", layout: "PUBLIC" }, + { routeId: "TECH_LOG_PROFILE", path: "/profile", layout: "PUBLIC" }, + { routeId: "TECH_LOG_SEARCH", path: "/search?q=JPA", layout: "PUBLIC" }, + { routeId: "TECH_LOG_STUDIO_HOME", path: "/studio", layout: "STUDIO" }, + { routeId: "TECH_LOG_STUDIO_DOCUMENTS", path: "/studio/documents", layout: "STUDIO" }, + { routeId: "TECH_LOG_STUDIO_DOCUMENT_NEW", path: "/studio/documents/new", layout: "STUDIO" }, + { routeId: "TECH_LOG_STUDIO_DOCUMENT_EDIT", path: "/studio/documents/11111111-1111-4111-8111-111111111111/edit", layout: "STUDIO" }, + { routeId: "TECH_LOG_STUDIO_DOCUMENT_VALIDATION", path: "/studio/documents/11111111-1111-4111-8111-111111111113/validation", layout: "STUDIO" }, + { routeId: "TECH_LOG_STUDIO_DOCUMENT_PREVIEW", path: "/studio/documents/11111111-1111-4111-8111-111111111111/preview", layout: "STUDIO" }, + { routeId: "TECH_LOG_STUDIO_DOCUMENT_PUBLISH", path: "/studio/documents/11111111-1111-4111-8111-111111111113/publish", layout: "STUDIO" }, + { routeId: "TECH_LOG_STUDIO_PUBLICATIONS", path: "/studio/publications", layout: "STUDIO" }, + { routeId: "TECH_LOG_STUDIO_PUBLICATION_PREVIEW", path: "/studio/publications/66666666-6666-4666-8666-666666666661/preview", layout: "STUDIO" }, + { routeId: "TECH_LOG_STUDIO_NOT_FOUND", path: "/studio/unknown-screen", layout: "STUDIO" }, + { routeId: "NOT_FOUND", path: "/unknown-public-screen", layout: "PUBLIC" }, +] as const; + +export const TECH_LOG_PUBLIC_FIXTURE_PATHS = [ + "/cases/collection-fetch-join-pagination", + "/cases/redis-adapter-ttl-boundary", + "/references/state-and-nonce-boundary", + "/references/jpa-list-fetch-strategy", + "/questions/validate-edge-token-again", + "/questions/collection-fetch-join-with-pagination", + "/topics/jpa", + "/topics/redis", + "/topics/authentication", + "/projects/backend-skeleton", + "/projects/backend-skeleton/records", + "/projects/backend-skeleton/decisions", + "/projects/backend-skeleton/activity", + "/projects/auth-lab", + "/projects/auth-lab/records", + "/projects/auth-lab/decisions", + "/projects/auth-lab/activity", + "/releases/0.1.0", +] as const; + +export const TECH_LOG_UNKNOWN_PUBLIC_PATHS = [ + ["explore-kind", "/explore/unknown-kind"], + ["case", "/cases/not-registered"], + ["reference", "/references/not-registered"], + ["question", "/questions/not-registered"], + ["topic", "/topics/not-registered"], + ["project", "/projects/missing-project"], + ["project-records", "/projects/missing-project/records"], + ["project-decisions", "/projects/missing-project/decisions"], + ["project-activity", "/projects/missing-project/activity"], + ["release", "/releases/9.9.9"], +] as const; + +export const TECH_LOG_STUDIO_STATE_PATHS = [ + ["dashboard", "/studio"], + ["document-list", "/studio/documents"], + ["new-document", "/studio/documents/new"], + ["case-editor", "/studio/documents/11111111-1111-4111-8111-111111111111/edit"], + ["reference-editor", "/studio/documents/11111111-1111-4111-8111-111111111113/edit"], + ["question-editor", "/studio/documents/11111111-1111-4111-8111-111111111114/edit"], + ["conflict-editor", "/studio/documents/11111111-1111-4111-8111-111111111115/edit"], + ["valid-validation", "/studio/documents/11111111-1111-4111-8111-111111111113/validation"], + ["invalid-validation", "/studio/documents/11111111-1111-4111-8111-111111111114/validation"], + ["current-preview", "/studio/documents/11111111-1111-4111-8111-111111111111/preview"], + ["missing-preview", "/studio/documents/11111111-1111-4111-8111-111111111113/preview"], + ["expired-preview", "/studio/documents/11111111-1111-4111-8111-111111111117/preview"], + ["publish-ready", "/studio/documents/11111111-1111-4111-8111-111111111113/publish"], + ["publish-blocked", "/studio/documents/11111111-1111-4111-8111-111111111114/publish"], + ["publications", "/studio/publications"], + ["publication-snapshot", "/studio/publications/66666666-6666-4666-8666-666666666661/preview"], + ["missing-document", "/studio/documents/99999999-9999-4999-8999-999999999999/edit"], + ["missing-publication", "/studio/publications/99999999-9999-4999-8999-999999999999/preview"], + ["unknown-studio-route", "/studio/unknown-screen"], +] as const; + +export const TECH_LOG_BREAKPOINT_WIDTHS = [ + 1180, + 1179, + 1050, + 1024, + 980, + 900, + 820, + 768, + 767, + 420, + 390, + 375, +] as const; + +export async function prepareTechLogPage(page: Page) { + await page.clock.setFixedTime(new Date(TECH_LOG_FIXED_TIME)); + await page.emulateMedia({ colorScheme: "light", reducedMotion: "reduce" }); +} + +export async function settleTechLogPage(page: Page) { + await page.evaluate(async () => { + await document.fonts.ready; + }); + await page.addStyleTag({ + content: ` + *, *::before, *::after { + animation-delay: 0s !important; + animation-duration: 0s !important; + caret-color: transparent !important; + transition-delay: 0s !important; + transition-duration: 0s !important; + } + `, + }); +} + +export async function gotoTechLog(page: Page, path: string) { + await prepareTechLogPage(page); + await page.goto(path); + await page.locator("body").waitFor({ state: "visible" }); + await settleTechLogPage(page); +} + +export async function horizontalOverflow(page: Page) { + return page.evaluate(() => ({ + clientWidth: document.documentElement.clientWidth, + scrollWidth: document.documentElement.scrollWidth, + })); +} diff --git a/tests/support/browser/verify-tech-log-source-parity.ts b/tests/support/browser/verify-tech-log-source-parity.ts new file mode 100644 index 0000000..95c15f4 --- /dev/null +++ b/tests/support/browser/verify-tech-log-source-parity.ts @@ -0,0 +1,281 @@ +// @ts-nocheck -- standalone evidence runner executed directly with tsx. +import { createRequire } from "node:module"; +import { mkdir, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; + +import { chromium } from "@playwright/test"; + +import { + TECH_LOG_BREAKPOINT_WIDTHS, + TECH_LOG_CANONICAL_ROUTES, + TECH_LOG_FIXED_TIME, + TECH_LOG_STUDIO_STATE_PATHS, + TECH_LOG_UNKNOWN_PUBLIC_PATHS, +} from "./tech-log-fixtures.ts"; + +const require = createRequire(import.meta.url); +const { PNG } = require( + resolve("node_modules/.pnpm/playwright-core@1.62.0/node_modules/playwright-core/lib/utilsBundle.js"), +); + +const sourceBaseUrl = process.env.TECH_LOG_SOURCE_URL ?? "http://127.0.0.1:4175"; +const targetBaseUrl = process.env.TECH_LOG_TARGET_URL ?? "http://127.0.0.1:4174"; +const outputPath = process.env.TECH_LOG_PARITY_OUTPUT ?? "artifacts/quality/tech-log-source-parity.json"; +const fixedDate = new Date(TECH_LOG_FIXED_TIME); +const noMotionCss = ` + *, *::before, *::after { + animation-delay: 0s !important; + animation-duration: 0s !important; + caret-color: transparent !important; + transition-delay: 0s !important; + transition-duration: 0s !important; + } +`; + +const allCases = [ + ...TECH_LOG_CANONICAL_ROUTES.flatMap(({ routeId, path }) => + [360, 1440].map((width) => ({ name: `${routeId}-${width}`, path, width }))), + ...TECH_LOG_BREAKPOINT_WIDTHS.map((width) => ({ + name: `TECH_LOG_HOME-BREAKPOINT-${width}`, + path: "/", + width, + })), + ...TECH_LOG_STUDIO_STATE_PATHS.map(([state, path]) => ({ + name: `TECH_LOG_STUDIO_STATE-${state}`, + path, + width: 1440, + })), + ...TECH_LOG_UNKNOWN_PUBLIC_PATHS.flatMap(([route, path]) => + [360, 1440].map((width) => ({ + name: `TECH_LOG_UNKNOWN_${route.toUpperCase().replaceAll("-", "_")}-${width}`, + path, + width, + }))), + { name: "TECH_LOG_SEARCH_DIALOG", path: "/", width: 390, action: "search-dialog" }, + { name: "TECH_LOG_STUDIO_MOBILE_MENU", path: "/studio", width: 390, action: "studio-menu" }, + { + name: "TECH_LOG_STUDIO_IMMEDIATE_PREVIEW", + path: "/studio/documents/11111111-1111-4111-8111-111111111111/edit", + width: 1440, + action: "immediate-preview", + }, + { + name: "TECH_LOG_STUDIO_DIRTY_DIALOG", + path: "/studio/documents/11111111-1111-4111-8111-111111111111/edit", + width: 1440, + action: "dirty-dialog", + }, + { + name: "TECH_LOG_STUDIO_CURRENT_PREVIEW_CREATED", + path: "/studio/documents/11111111-1111-4111-8111-111111111113/preview", + width: 1440, + action: "create-preview", + }, + { + name: "TECH_LOG_STUDIO_UNPUBLISH_DIALOG", + path: "/studio/publications", + width: 1440, + action: "unpublish-dialog", + }, + { + name: "TECH_LOG_STUDIO_WARNING_ACKNOWLEDGED", + path: "/studio/documents/11111111-1111-4111-8111-111111111116/validation", + width: 1440, + action: "warning-acknowledged", + }, +]; +const caseFilter = process.env.TECH_LOG_PARITY_CASE; +const cases = caseFilter + ? allCases.filter(({ name }) => name.includes(caseFilter)) + : allCases; +const diagnosticDirectory = process.env.TECH_LOG_PARITY_DIAGNOSTICS ?? "/tmp/techlog-parity-diagnostics"; + +async function interact(page, action) { + if (action === "search-dialog") { + await page.getByRole("button", { name: "검색 열기" }).click(); + } else if (action === "studio-menu") { + await page.getByRole("button", { name: "Studio 메뉴 열기" }).click(); + } else if (action === "immediate-preview") { + await page.getByRole("tab", { name: "즉시 미리보기" }).click(); + } else if (action === "dirty-dialog") { + await page.getByLabel("요약").fill("저장하지 않은 시각 검증 변경"); + await page.getByRole("link", { name: "게시 기록", exact: true }).first().click(); + } else if (action === "create-preview") { + await page.getByRole("button", { name: "Public Preview 만들기" }).click(); + await page.getByText("현재 저장 버전의 Public Preview입니다.").waitFor(); + } else if (action === "unpublish-dialog") { + await page.getByRole("button", { name: /컬렉션 Fetch Join과 페이징은 왜 충돌하는가 게시 취소/ }).click(); + } else if (action === "warning-acknowledged") { + await page.getByRole("button", { name: "검증하기" }).click(); + await page.getByText("경고를 확인하고 Preview를 만들 수 있습니다").waitFor(); + await page.getByRole("link", { name: "Public Preview 만들기" }).click(); + await page.getByRole("button", { name: "Public Preview 만들기" }).click(); + await page.getByText("현재 저장 버전의 Public Preview입니다.").waitFor(); + await page.getByRole("link", { name: "게시 준비로 이동" }).click(); + await page.getByRole("checkbox").check(); + await page.getByRole("button", { name: /게시$/ }).waitFor(); + } +} + +async function settle(page, baseUrl, parityCase) { + await page.setViewportSize({ width: parityCase.width, height: 1000 }); + await page.clock.setFixedTime(fixedDate); + await page.goto(new URL(parityCase.path, baseUrl).href, { waitUntil: "networkidle" }); + await page.locator("body").waitFor({ state: "visible" }); + await page.evaluate(async () => document.fonts.ready); + await page.addStyleTag({ content: noMotionCss }); + await interact(page, parityCase.action); + await page.evaluate(async () => document.fonts.ready); +} + +async function projection(page) { + return page.evaluate(() => { + const normalizeReference = (value) => value + .split(" ") + .map((token) => token.replace(/^_[rR].*?_(?=-|$)/, "")) + .join(" "); + const attributes = (element) => Object.fromEntries( + [...element.attributes] + .filter(({ name }) => name === "role" || name.startsWith("aria-")) + .map(({ name, value }) => [name, normalizeReference(value)]) + .sort(([left], [right]) => left.localeCompare(right)), + ); + return [...document.querySelectorAll("header, main, footer, dialog")].map((element) => ({ + tag: element.tagName.toLowerCase(), + className: [...element.classList].map((name) => name.replace(/^_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)$/, "_$1__$2")).sort().join(" "), + text: element.textContent?.replace(/\s+/g, " ").trim() ?? "", + aria: attributes(element), + descendants: [...element.querySelectorAll("[role], [aria-label], [aria-labelledby], [aria-describedby], [aria-current], [aria-expanded], [aria-controls]")] + .map((child) => ({ + tag: child.tagName.toLowerCase(), + className: [...child.classList].map((name) => name.replace(/^_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)$/, "_$1__$2")).sort().join(" "), + text: child.textContent?.replace(/\s+/g, " ").trim() ?? "", + aria: attributes(child), + })), + })); + }); +} + +async function layoutProjection(page) { + return page.evaluate(() => [...document.querySelectorAll("header, main, main *, footer, dialog, .studio-app *")].map((element) => { + const box = element.getBoundingClientRect(); + const style = getComputedStyle(element); + return { + tag: element.tagName.toLowerCase(), + className: [...element.classList].sort().join(" "), + top: box.top, + left: box.left, + width: box.width, + height: box.height, + fontFamily: style.fontFamily, + fontSize: style.fontSize, + lineHeight: style.lineHeight, + margin: style.margin, + padding: style.padding, + }; + })); +} + +function pixelDifference(sourceBuffer, targetBuffer) { + const source = PNG.sync.read(sourceBuffer); + const target = PNG.sync.read(targetBuffer); + if (source.width !== target.width || source.height !== target.height) { + return { pixels: null, sourceSize: [source.width, source.height], targetSize: [target.width, target.height] }; + } + let pixels = 0; + for (let offset = 0; offset < source.data.length; offset += 4) { + if ( + source.data[offset] !== target.data[offset] || + source.data[offset + 1] !== target.data[offset + 1] || + source.data[offset + 2] !== target.data[offset + 2] || + source.data[offset + 3] !== target.data[offset + 3] + ) pixels += 1; + } + return { pixels, sourceSize: [source.width, source.height], targetSize: [target.width, target.height] }; +} + +const browser = await chromium.launch(); +const contextOptions = { + colorScheme: "light", + deviceScaleFactor: 1, + locale: "ko-KR", + reducedMotion: "reduce", + serviceWorkers: "block", + timezoneId: "Asia/Seoul", +}; +const sourceContext = await browser.newContext(contextOptions); +const targetContext = await browser.newContext(contextOptions); +const results = []; + +try { + for (const parityCase of cases) { + const sourcePage = await sourceContext.newPage(); + const targetPage = await targetContext.newPage(); + const sourceErrors = []; + const targetErrors = []; + for (const [page, errors] of [[sourcePage, sourceErrors], [targetPage, targetErrors]]) { + page.on("console", (message) => { if (message.type() === "error") errors.push(`console: ${message.text()}`); }); + page.on("pageerror", (error) => errors.push(`pageerror: ${error.message}`)); + page.on("requestfailed", (request) => errors.push(`requestfailed: ${request.url()} ${request.failure()?.errorText ?? ""}`)); + } + await Promise.all([ + settle(sourcePage, sourceBaseUrl, parityCase), + settle(targetPage, targetBaseUrl, parityCase), + ]); + const [sourceShot, targetShot, sourceDom, targetDom, sourceLayout, targetLayout, sourceFonts, targetFonts] = await Promise.all([ + sourcePage.screenshot({ fullPage: true }), + targetPage.screenshot({ fullPage: true }), + projection(sourcePage), + projection(targetPage), + layoutProjection(sourcePage), + layoutProjection(targetPage), + sourcePage.evaluate(() => [...document.fonts].map(({ family, status }) => ({ family, status })).filter(({ family }) => /Pretendard|IBM Plex Mono/.test(family))), + targetPage.evaluate(() => [...document.fonts].map(({ family, status }) => ({ family, status })).filter(({ family }) => /Pretendard|IBM Plex Mono/.test(family))), + ]); + const pixel = pixelDifference(sourceShot, targetShot); + const domEqual = JSON.stringify(sourceDom) === JSON.stringify(targetDom); + const fontsEqual = JSON.stringify(sourceFonts) === JSON.stringify(targetFonts); + const isPlainNotFound = + parityCase.name.includes("NOT_FOUND") || parityCase.name.includes("UNKNOWN"); + const expectsSourceNotFoundResponse = + isPlainNotFound || parityCase.path.includes("unknown"); + const expectedNotFoundConsole = expectsSourceNotFoundResponse + ? (message) => message === "console: Failed to load resource: the server responded with a status of 404 (Not Found)" + : () => false; + const unexplainedSourceErrors = sourceErrors.filter((message) => !expectedNotFoundConsole(message)); + const unexplainedTargetErrors = targetErrors.filter((message) => !expectedNotFoundConsole(message)); + const fontContractApplies = !isPlainNotFound; + const passed = pixel.pixels === 0 && domEqual && (!fontContractApplies || fontsEqual) && unexplainedSourceErrors.length === 0 && unexplainedTargetErrors.length === 0; + if (!passed) { + await mkdir(diagnosticDirectory, { recursive: true }); + const stem = parityCase.name.toLowerCase().replace(/[^a-z0-9-]+/g, "-"); + await Promise.all([ + writeFile(resolve(diagnosticDirectory, `${stem}-source.png`), sourceShot), + writeFile(resolve(diagnosticDirectory, `${stem}-target.png`), targetShot), + writeFile(resolve(diagnosticDirectory, `${stem}-dom.json`), `${JSON.stringify({ sourceDom, targetDom, sourceLayout, targetLayout }, null, 2)}\n`), + ]); + } + results.push({ name: parityCase.name, path: parityCase.path, width: parityCase.width, action: parityCase.action ?? null, pixel, domEqual, fontsEqual, fontContractApplies, sourceFonts, targetFonts, sourceErrors, targetErrors, unexplainedSourceErrors, unexplainedTargetErrors, passed }); + console.log(`${passed ? "PASS" : "FAIL"} ${parityCase.name} pixels=${pixel.pixels ?? "SIZE"} dom=${domEqual} fonts=${fontContractApplies ? fontsEqual : "source-plain-text"} errors=${unexplainedSourceErrors.length}/${unexplainedTargetErrors.length}`); + await Promise.all([sourcePage.close(), targetPage.close()]); + } +} finally { + await Promise.all([sourceContext.close(), targetContext.close()]); + await browser.close(); +} + +const evidence = { + generatedAt: new Date().toISOString(), + sourceBaseUrl, + targetBaseUrl, + conditions: { ...contextOptions, fixedTime: TECH_LOG_FIXED_TIME, viewportHeight: 1000, screenshots: "fullPage", masks: 0 }, + total: results.length, + passed: results.filter((result) => result.passed).length, + failed: results.filter((result) => !result.passed).length, + totalDifferentPixels: results.reduce((sum, result) => sum + (result.pixel.pixels ?? 0), 0), + results, +}; +await mkdir(resolve(outputPath, ".."), { recursive: true }); +await writeFile(outputPath, `${JSON.stringify(evidence, null, 2)}\n`); +console.log(JSON.stringify({ outputPath, total: evidence.total, passed: evidence.passed, failed: evidence.failed, totalDifferentPixels: evidence.totalDifferentPixels })); +if (evidence.failed > 0) process.exitCode = 1; diff --git a/tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap b/tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap index d5abb1c..bf897ab 100644 --- a/tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap +++ b/tests/unit/__snapshots__/ci-workflow-generation.test.ts.snap @@ -190,6 +190,7 @@ jobs: scripts/lib/secret-scan.ts \\ scripts/lib/supply-chain.ts \\ scripts/lib/validated-json-artifact.ts \\ + scripts/lib/vite-route-chunks.ts \\ src/contracts/release-artifacts.ts \\ src/features/installed-contract-contributions.ts \\ src/features/installed-feature-contracts.ts \\ diff --git a/tests/unit/bounded-body-reader.test.ts b/tests/unit/bounded-body-reader.test.ts index 221dd34..f7603f9 100644 --- a/tests/unit/bounded-body-reader.test.ts +++ b/tests/unit/bounded-body-reader.test.ts @@ -121,6 +121,29 @@ describe("bounded body reader", () => { }); }); + it("abandons and cancels a bounded read when its lifetime aborts", async () => { + const controller = new AbortController(); + const reader = { + read: vi.fn(() => new Promise(() => {})), + cancel: vi.fn().mockRejectedValue(new Error("abandoned cancel failed")), + releaseLock: vi.fn(), + }; + const response = { + headers: new Headers(), + body: { getReader: () => reader }, + } as unknown as Response; + + const outcome = readBoundedBytes(response, 3, controller.signal); + controller.abort(); + + await expect(outcome).resolves.toEqual({ + ok: false, + code: "RESPONSE_STREAM_FAILURE", + }); + expect(reader.cancel).toHaveBeenCalledOnce(); + expect(reader.releaseLock).toHaveBeenCalledOnce(); + }); + it("detects a forbidden body from declared length without reading it", async () => { const cancel = vi.fn(); const response = { @@ -200,6 +223,29 @@ describe("bounded body reader", () => { }); }); + it("abandons and cancels a forbidden-body probe when its lifetime aborts", async () => { + const controller = new AbortController(); + const reader = { + read: vi.fn(() => new Promise(() => {})), + cancel: vi.fn().mockRejectedValue(new Error("abandoned cancel failed")), + releaseLock: vi.fn(), + }; + const response = { + headers: new Headers(), + body: { getReader: () => reader }, + } as unknown as Response; + + const outcome = probeForbiddenBody(response, controller.signal); + controller.abort(); + + await expect(outcome).resolves.toEqual({ + ok: false, + code: "RESPONSE_STREAM_FAILURE", + }); + expect(reader.cancel).toHaveBeenCalledOnce(); + expect(reader.releaseLock).toHaveBeenCalledOnce(); + }); + it("decodes valid JSON and distinguishes UTF-8 from JSON failures", () => { expect(decodeJsonBytes(new TextEncoder().encode('{"ok":true}'))).toEqual({ ok: true, diff --git a/tests/unit/design-system-source.test.ts b/tests/unit/design-system-source.test.ts new file mode 100644 index 0000000..3a2a049 --- /dev/null +++ b/tests/unit/design-system-source.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from "vitest"; + +import { containsRawPaletteValue } from "../../scripts/lib/design-system-source.ts"; + +describe("design-system source palette detection", () => { + it("rejects palette literals without treating URL fragments as colors", () => { + expect(containsRawPaletteValue('style={{ color: "#ff0000" }}')).toBe(true); + expect(containsRawPaletteValue('const css = "color: rgb(255 0 0)"')).toBe(true); + expect( + containsRawPaletteValue( + 'path: "/projects/backend-skeleton/decisions#feed-pagination-boundary"', + ), + ).toBe(false); + }); +}); diff --git a/tests/unit/registry-governance.test.ts b/tests/unit/registry-governance.test.ts index 290421a..7c91592 100644 --- a/tests/unit/registry-governance.test.ts +++ b/tests/unit/registry-governance.test.ts @@ -335,9 +335,9 @@ describe("registry governance manifest", () => { }); expect(versions?.rows).toBeDefined(); expect(approval).toMatchObject({ - owner: "frontend-platform", + owner: "tech-log-frontend", reason: - "Baseline canonical invalidation graph and topic-version contracts after FE-REG-QUERY retirement", + "Install approved TechLog Public and Studio route contract", }); }); diff --git a/tests/unit/security-followup.test.ts b/tests/unit/security-followup.test.ts index 1844c89..0e217d5 100644 --- a/tests/unit/security-followup.test.ts +++ b/tests/unit/security-followup.test.ts @@ -1770,6 +1770,7 @@ async function createArchivedAssessmentFixture(): Promise<{ "scripts/lib/secret-scan.ts", "scripts/lib/supply-chain.ts", "scripts/lib/validated-json-artifact.ts", + "scripts/lib/vite-route-chunks.ts", "src/contracts/release-artifacts.ts", "src/features/installed-contract-contributions.ts", "src/features/installed-feature-contracts.ts", @@ -1799,6 +1800,7 @@ async function createArchivedAssessmentFixture(): Promise<{ "scripts/lib/secret-scan.ts", "scripts/lib/supply-chain.ts", "scripts/lib/validated-json-artifact.ts", + "scripts/lib/vite-route-chunks.ts", "src/contracts/release-artifacts.ts", "src/features/installed-contract-contributions.ts", "src/features/installed-feature-contracts.ts", diff --git a/tests/unit/vite-route-chunks.test.ts b/tests/unit/vite-route-chunks.test.ts new file mode 100644 index 0000000..c593a09 --- /dev/null +++ b/tests/unit/vite-route-chunks.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; + +import { findViteDynamicRouteChunk } from "../../scripts/lib/vite-route-chunks.ts"; + +describe("Vite route chunk identity", () => { + it("recognizes a governed manual chunk referenced by an entry dynamic import", () => { + const manifest = { + "_route-tech-log-home-abc.js": { + file: "assets/route-tech-log-home-abc.js", + name: "route-tech-log-home", + }, + "index.html": { + file: "assets/index.js", + name: "index", + isEntry: true, + dynamicImports: ["_route-tech-log-home-abc.js"], + }, + }; + + expect(findViteDynamicRouteChunk(manifest, "route-tech-log-home")).toEqual({ + file: "assets/route-tech-log-home-abc.js", + name: "route-tech-log-home", + }); + }); + + it("retains Vite native dynamic-entry support", () => { + const manifest = { + "src/page.tsx": { + file: "assets/page.js", + name: "route-page", + isDynamicEntry: true, + }, + }; + + expect(findViteDynamicRouteChunk(manifest, "route-page")?.file).toBe( + "assets/page.js", + ); + }); + + it("rejects a named chunk that is not dynamically reachable", () => { + const manifest = { + "_route-tech-log-home-abc.js": { + file: "assets/route-tech-log-home-abc.js", + name: "route-tech-log-home", + }, + "index.html": { + file: "assets/index.js", + name: "index", + isEntry: true, + }, + }; + + expect(findViteDynamicRouteChunk(manifest, "route-tech-log-home")).toBeUndefined(); + }); +}); diff --git a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/app-shell-compact-pseudo-drawer-chromium-visual-linux.png b/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/app-shell-compact-pseudo-drawer-chromium-visual-linux.png deleted file mode 100644 index 48d2400..0000000 Binary files a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/app-shell-compact-pseudo-drawer-chromium-visual-linux.png and /dev/null differ diff --git a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/app-shell-wide-light-chromium-visual-linux.png b/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/app-shell-wide-light-chromium-visual-linux.png deleted file mode 100644 index 0208a26..0000000 Binary files a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/app-shell-wide-light-chromium-visual-linux.png and /dev/null differ diff --git a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/design-system-gallery-dark-chromium-visual-linux.png b/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/design-system-gallery-dark-chromium-visual-linux.png deleted file mode 100644 index a4b9b7c..0000000 Binary files a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/design-system-gallery-dark-chromium-visual-linux.png and /dev/null differ diff --git a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/platform-overview-light-chromium-visual-linux.png b/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/platform-overview-light-chromium-visual-linux.png deleted file mode 100644 index 3f95e24..0000000 Binary files a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/platform-overview-light-chromium-visual-linux.png and /dev/null differ diff --git a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/state-surfaces-light-chromium-visual-linux.png b/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/state-surfaces-light-chromium-visual-linux.png deleted file mode 100644 index 45c28c6..0000000 Binary files a/tests/visual/__snapshots__/platform.visual.spec.ts-snapshots/state-surfaces-light-chromium-visual-linux.png and /dev/null differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/not-found-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/not-found-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/not-found-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/not-found-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/not-found-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/not-found-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-case-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-case-1440-chromium-linux.png new file mode 100644 index 0000000..27cac8e Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-case-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-case-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-case-360-chromium-linux.png new file mode 100644 index 0000000..8286b9f Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-case-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-1440-chromium-linux.png new file mode 100644 index 0000000..db72793 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-360-chromium-linux.png new file mode 100644 index 0000000..03ea540 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-kind-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-kind-1440-chromium-linux.png new file mode 100644 index 0000000..a6f8eac Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-kind-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-kind-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-kind-360-chromium-linux.png new file mode 100644 index 0000000..148185d Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-explore-kind-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-1440-chromium-linux.png new file mode 100644 index 0000000..a5087f8 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-360-chromium-linux.png new file mode 100644 index 0000000..416b4af Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1024-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1024-chromium-linux.png new file mode 100644 index 0000000..d65e0a8 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1024-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1050-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1050-chromium-linux.png new file mode 100644 index 0000000..5303530 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1050-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1179-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1179-chromium-linux.png new file mode 100644 index 0000000..0fecece Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1179-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1180-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1180-chromium-linux.png new file mode 100644 index 0000000..edf94c0 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-1180-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-375-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-375-chromium-linux.png new file mode 100644 index 0000000..e540e3f Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-375-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-390-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-390-chromium-linux.png new file mode 100644 index 0000000..2374ec8 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-390-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-420-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-420-chromium-linux.png new file mode 100644 index 0000000..84b7440 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-420-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-767-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-767-chromium-linux.png new file mode 100644 index 0000000..0a34631 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-767-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-768-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-768-chromium-linux.png new file mode 100644 index 0000000..af2aa34 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-768-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-820-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-820-chromium-linux.png new file mode 100644 index 0000000..48c3d6a Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-820-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-900-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-900-chromium-linux.png new file mode 100644 index 0000000..633c604 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-900-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-980-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-980-chromium-linux.png new file mode 100644 index 0000000..49d0d88 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-home-breakpoint-980-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-profile-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-profile-1440-chromium-linux.png new file mode 100644 index 0000000..8baf33d Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-profile-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-profile-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-profile-360-chromium-linux.png new file mode 100644 index 0000000..a4fe1f8 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-profile-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-1440-chromium-linux.png new file mode 100644 index 0000000..20ae438 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-360-chromium-linux.png new file mode 100644 index 0000000..787f666 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-activity-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-activity-1440-chromium-linux.png new file mode 100644 index 0000000..ec9c3ef Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-activity-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-activity-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-activity-360-chromium-linux.png new file mode 100644 index 0000000..2676fcb Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-activity-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-decisions-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-decisions-1440-chromium-linux.png new file mode 100644 index 0000000..3a13e28 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-decisions-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-decisions-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-decisions-360-chromium-linux.png new file mode 100644 index 0000000..44cc404 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-decisions-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-records-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-records-1440-chromium-linux.png new file mode 100644 index 0000000..a12d178 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-records-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-records-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-records-360-chromium-linux.png new file mode 100644 index 0000000..046ebf1 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-project-records-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-projects-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-projects-1440-chromium-linux.png new file mode 100644 index 0000000..7287cf7 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-projects-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-projects-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-projects-360-chromium-linux.png new file mode 100644 index 0000000..fae9e2a Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-projects-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-question-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-question-1440-chromium-linux.png new file mode 100644 index 0000000..02e9405 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-question-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-question-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-question-360-chromium-linux.png new file mode 100644 index 0000000..9750c12 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-question-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-reference-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-reference-1440-chromium-linux.png new file mode 100644 index 0000000..d7a9274 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-reference-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-reference-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-reference-360-chromium-linux.png new file mode 100644 index 0000000..ca790e1 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-reference-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-release-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-release-1440-chromium-linux.png new file mode 100644 index 0000000..2cfb575 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-release-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-release-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-release-360-chromium-linux.png new file mode 100644 index 0000000..4329aa9 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-release-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-releases-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-releases-1440-chromium-linux.png new file mode 100644 index 0000000..3fb6d6c Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-releases-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-releases-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-releases-360-chromium-linux.png new file mode 100644 index 0000000..e978da3 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-releases-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-search-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-search-1440-chromium-linux.png new file mode 100644 index 0000000..7407a68 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-search-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-search-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-search-360-chromium-linux.png new file mode 100644 index 0000000..09d3697 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-search-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-search-dialog-390-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-search-dialog-390-chromium-linux.png new file mode 100644 index 0000000..3521f9b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-search-dialog-390-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-case-editor-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-case-editor-1440-chromium-linux.png new file mode 100644 index 0000000..0ab87fc Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-case-editor-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-conflict-editor-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-conflict-editor-1440-chromium-linux.png new file mode 100644 index 0000000..b3e4542 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-conflict-editor-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-current-preview-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-current-preview-1440-chromium-linux.png new file mode 100644 index 0000000..97ee486 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-current-preview-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-current-preview-created-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-current-preview-created-1440-chromium-linux.png new file mode 100644 index 0000000..4cecce9 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-current-preview-created-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-dashboard-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-dashboard-1440-chromium-linux.png new file mode 100644 index 0000000..4722109 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-dashboard-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-dirty-leave-dialog-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-dirty-leave-dialog-1440-chromium-linux.png new file mode 100644 index 0000000..926652c Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-dirty-leave-dialog-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-edit-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-edit-1440-chromium-linux.png new file mode 100644 index 0000000..0ab87fc Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-edit-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-edit-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-edit-360-chromium-linux.png new file mode 100644 index 0000000..a6bfe3f Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-edit-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-list-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-list-1440-chromium-linux.png new file mode 100644 index 0000000..d446005 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-list-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-new-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-new-1440-chromium-linux.png new file mode 100644 index 0000000..e5c8a03 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-new-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-new-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-new-360-chromium-linux.png new file mode 100644 index 0000000..ac0030e Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-new-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-preview-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-preview-1440-chromium-linux.png new file mode 100644 index 0000000..97ee486 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-preview-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-preview-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-preview-360-chromium-linux.png new file mode 100644 index 0000000..4ed6c95 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-preview-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-publish-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-publish-1440-chromium-linux.png new file mode 100644 index 0000000..54303cb Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-publish-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-publish-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-publish-360-chromium-linux.png new file mode 100644 index 0000000..bc70b96 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-publish-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-validation-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-validation-1440-chromium-linux.png new file mode 100644 index 0000000..f42a93b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-validation-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-validation-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-validation-360-chromium-linux.png new file mode 100644 index 0000000..4a19187 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-document-validation-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-documents-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-documents-1440-chromium-linux.png new file mode 100644 index 0000000..d446005 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-documents-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-documents-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-documents-360-chromium-linux.png new file mode 100644 index 0000000..f288eed Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-documents-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-expired-preview-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-expired-preview-1440-chromium-linux.png new file mode 100644 index 0000000..3d3b60f Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-expired-preview-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-home-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-home-1440-chromium-linux.png new file mode 100644 index 0000000..4722109 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-home-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-home-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-home-360-chromium-linux.png new file mode 100644 index 0000000..0279551 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-home-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-immediate-preview-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-immediate-preview-1440-chromium-linux.png new file mode 100644 index 0000000..ccafdde Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-immediate-preview-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-invalid-validation-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-invalid-validation-1440-chromium-linux.png new file mode 100644 index 0000000..05f7ab3 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-invalid-validation-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-missing-document-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-missing-document-1440-chromium-linux.png new file mode 100644 index 0000000..c52067a Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-missing-document-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-missing-preview-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-missing-preview-1440-chromium-linux.png new file mode 100644 index 0000000..4405150 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-missing-preview-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-missing-publication-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-missing-publication-1440-chromium-linux.png new file mode 100644 index 0000000..adb8126 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-missing-publication-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-mobile-menu-390-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-mobile-menu-390-chromium-linux.png new file mode 100644 index 0000000..b7d29b0 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-mobile-menu-390-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-new-document-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-new-document-1440-chromium-linux.png new file mode 100644 index 0000000..e5c8a03 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-new-document-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-not-found-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-not-found-1440-chromium-linux.png new file mode 100644 index 0000000..acb246c Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-not-found-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-not-found-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-not-found-360-chromium-linux.png new file mode 100644 index 0000000..4a19187 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-not-found-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publication-preview-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publication-preview-1440-chromium-linux.png new file mode 100644 index 0000000..5b85b69 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publication-preview-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publication-preview-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publication-preview-360-chromium-linux.png new file mode 100644 index 0000000..d6dfae3 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publication-preview-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publication-snapshot-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publication-snapshot-1440-chromium-linux.png new file mode 100644 index 0000000..5b85b69 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publication-snapshot-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publications-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publications-1440-chromium-linux.png new file mode 100644 index 0000000..fee31d6 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publications-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publications-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publications-360-chromium-linux.png new file mode 100644 index 0000000..cf095f8 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publications-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publish-blocked-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publish-blocked-1440-chromium-linux.png new file mode 100644 index 0000000..4d344bd Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publish-blocked-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publish-ready-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publish-ready-1440-chromium-linux.png new file mode 100644 index 0000000..54303cb Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-publish-ready-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-question-editor-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-question-editor-1440-chromium-linux.png new file mode 100644 index 0000000..b9a420b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-question-editor-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-reference-editor-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-reference-editor-1440-chromium-linux.png new file mode 100644 index 0000000..6a176a7 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-reference-editor-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-unknown-studio-route-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-unknown-studio-route-1440-chromium-linux.png new file mode 100644 index 0000000..acb246c Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-unknown-studio-route-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-unpublish-dialog-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-unpublish-dialog-1440-chromium-linux.png new file mode 100644 index 0000000..8713570 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-unpublish-dialog-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-valid-validation-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-valid-validation-1440-chromium-linux.png new file mode 100644 index 0000000..f42a93b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-valid-validation-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-warning-acknowledged-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-warning-acknowledged-1440-chromium-linux.png new file mode 100644 index 0000000..f0b8e8e Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-studio-warning-acknowledged-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-topic-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-topic-1440-chromium-linux.png new file mode 100644 index 0000000..627be10 Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-topic-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-topic-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-topic-360-chromium-linux.png new file mode 100644 index 0000000..722ccca Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-topic-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-case-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-case-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-case-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-case-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-case-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-case-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-explore-kind-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-explore-kind-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-explore-kind-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-explore-kind-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-explore-kind-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-explore-kind-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-activity-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-activity-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-activity-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-activity-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-activity-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-activity-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-decisions-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-decisions-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-decisions-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-decisions-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-decisions-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-decisions-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-records-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-records-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-records-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-records-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-records-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-project-records-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-question-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-question-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-question-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-question-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-question-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-question-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-reference-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-reference-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-reference-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-reference-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-reference-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-reference-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-release-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-release-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-release-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-release-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-release-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-release-360-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-topic-1440-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-topic-1440-chromium-linux.png new file mode 100644 index 0000000..0ca92de Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-topic-1440-chromium-linux.png differ diff --git a/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-topic-360-chromium-linux.png b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-topic-360-chromium-linux.png new file mode 100644 index 0000000..81b897b Binary files /dev/null and b/tests/visual/__snapshots__/tech-log.visual.spec.ts-snapshots/tech-log-unknown-topic-360-chromium-linux.png differ diff --git a/tests/visual/platform.visual.spec.ts b/tests/visual/platform.visual.spec.ts deleted file mode 100644 index f488dbb..0000000 --- a/tests/visual/platform.visual.spec.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { - expect, - test, -} from "../support/browser/strict-browser-test.ts"; - -test("wide application shell visual contract", async ({ page }) => { - await page.setViewportSize({ width: 1440, height: 900 }); - await page.goto("/"); - await expect(page.getByRole("main")).toBeVisible(); - await expect(page).toHaveScreenshot("app-shell-wide-light.png", { - fullPage: true, - }); -}); - -test("compact drawer and pseudo-locale visual contract", async ({ page }) => { - await page.setViewportSize({ width: 390, height: 844 }); - await page.goto("/"); - await page.locator("#locale-preference").selectOption("en-XA"); - await page.locator(".app-shell__menu-button").click(); - await expect(page.locator(".ui-drawer")).toHaveJSProperty("open", true); - await expect(page).toHaveScreenshot("app-shell-compact-pseudo-drawer.png", { - fullPage: true, - }); -}); - -test("design-system gallery dark visual contract", async ({ page }) => { - await page.setViewportSize({ width: 1280, height: 900 }); - await page.goto("/examples/ui"); - await page.locator("#theme-preference").selectOption("dark"); - await expect(page.getByRole("main")).toHaveScreenshot( - "design-system-gallery-dark.png", - ); -}); - -test("loading empty error and access surfaces visual contract", async ({ - page, -}) => { - await page.setViewportSize({ width: 1280, height: 1100 }); - await page.goto("/examples/states"); - await expect(page.getByRole("main")).toHaveScreenshot( - "state-surfaces-light.png", - ); -}); - -test("platform composition overview visual contract", async ({ page }) => { - await page.setViewportSize({ width: 1280, height: 1100 }); - await page.goto("/examples/platform"); - await expect( - page.locator("section[aria-labelledby='platform-capabilities-title']"), - ).toBeVisible(); - await expect(page.getByRole("main")).toHaveScreenshot( - "platform-overview-light.png", - ); -}); diff --git a/tests/visual/tech-log.visual.spec.ts b/tests/visual/tech-log.visual.spec.ts new file mode 100644 index 0000000..9393fee --- /dev/null +++ b/tests/visual/tech-log.visual.spec.ts @@ -0,0 +1,150 @@ +import { expect, test } from "../support/browser/strict-browser-test.ts"; +import { + gotoTechLog, + TECH_LOG_BREAKPOINT_WIDTHS, + TECH_LOG_CANONICAL_ROUTES, + TECH_LOG_STUDIO_STATE_PATHS, + TECH_LOG_UNKNOWN_PUBLIC_PATHS, +} from "../support/browser/tech-log-fixtures.ts"; + +for (const route of TECH_LOG_CANONICAL_ROUTES) { + for (const width of [360, 1440] as const) { + test(`${route.routeId} matches the reviewed ${width}px visual`, async ({ page }) => { + await page.setViewportSize({ width, height: 1000 }); + await gotoTechLog(page, route.path); + await expect(page).toHaveScreenshot(`${route.routeId.toLowerCase()}-${width}.png`, { + fullPage: true, + }); + }); + } +} + +for (const width of TECH_LOG_BREAKPOINT_WIDTHS) { + test(`Public home matches the reviewed ${width}px breakpoint visual`, async ({ page }) => { + await page.setViewportSize({ width, height: 1000 }); + await gotoTechLog(page, "/"); + await expect(page).toHaveScreenshot(`tech-log-home-breakpoint-${width}.png`, { + fullPage: true, + }); + }); +} + +for (const [state, path] of TECH_LOG_STUDIO_STATE_PATHS) { + test(`Studio ${state} matches its reviewed wide state visual`, async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await gotoTechLog(page, path); + await expect(page).toHaveScreenshot(`tech-log-studio-${state}-1440.png`, { + fullPage: true, + }); + }); +} + +for (const [route, path] of TECH_LOG_UNKNOWN_PUBLIC_PATHS) { + for (const width of [360, 1440] as const) { + test(`unknown Public ${route} matches the source ${width}px response`, async ({ page }) => { + await page.setViewportSize({ width, height: 1000 }); + await gotoTechLog(page, path); + await expect(page.getByText("Not Found", { exact: true })).toBeVisible(); + await expect(page.locator(".site-frame")).toHaveCount(0); + await expect(page).toHaveScreenshot(`tech-log-unknown-${route}-${width}.png`, { + fullPage: true, + }); + }); + } +} + +test("open Public search dialog matches the source without a mask", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await gotoTechLog(page, "/"); + await page.getByRole("button", { name: "검색 열기" }).click(); + await expect(page.getByRole("dialog", { name: "검색" })).toBeVisible(); + await expect(page).toHaveScreenshot("tech-log-search-dialog-390.png", { + fullPage: true, + }); +}); + +test("open Studio mobile menu matches the source without a mask", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await gotoTechLog(page, "/studio"); + await page.getByRole("button", { name: "Studio 메뉴 열기" }).click(); + await expect(page).toHaveScreenshot("tech-log-studio-mobile-menu-390.png", { + fullPage: true, + }); +}); + +test("Studio immediate preview matches the source without a mask", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await gotoTechLog( + page, + "/studio/documents/11111111-1111-4111-8111-111111111111/edit", + ); + await page.getByRole("tab", { name: "즉시 미리보기" }).click(); + await expect(page.getByRole("tabpanel", { name: "즉시 미리보기" })).toBeVisible(); + await expect(page).toHaveScreenshot("tech-log-studio-immediate-preview-1440.png", { + fullPage: true, + }); +}); + +test("Studio dirty-leave dialog matches the source without a mask", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await gotoTechLog( + page, + "/studio/documents/11111111-1111-4111-8111-111111111111/edit", + ); + await page.getByLabel("요약").fill("저장하지 않은 시각 검증 변경"); + await page.getByRole("link", { name: "게시 기록", exact: true }).first().click(); + await expect( + page.getByRole("dialog", { name: "저장하지 않은 변경" }), + ).toBeVisible(); + await expect(page).toHaveScreenshot("tech-log-studio-dirty-leave-dialog-1440.png", { + fullPage: true, + }); +}); + +test("Studio newly-created current preview matches the source without a mask", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await gotoTechLog( + page, + "/studio/documents/11111111-1111-4111-8111-111111111113/preview", + ); + await page.getByRole("button", { name: "Public Preview 만들기" }).click(); + await expect(page.getByText("현재 저장 버전의 Public Preview입니다.")).toBeVisible(); + await expect(page).toHaveScreenshot("tech-log-studio-current-preview-created-1440.png", { + fullPage: true, + }); +}); + +test("Studio unpublish confirmation matches the source without a mask", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await gotoTechLog(page, "/studio/publications"); + await page + .getByRole("button", { + name: /컬렉션 Fetch Join과 페이징은 왜 충돌하는가 게시 취소/, + }) + .click(); + await expect(page.getByRole("dialog", { name: "게시를 취소할까요?" })).toBeVisible(); + await expect(page).toHaveScreenshot("tech-log-studio-unpublish-dialog-1440.png", { + fullPage: true, + }); +}); + +test("Studio warning acknowledgement publish state matches the source without a mask", async ({ + page, +}) => { + await page.setViewportSize({ width: 1440, height: 1000 }); + await gotoTechLog( + page, + "/studio/documents/11111111-1111-4111-8111-111111111116/validation", + ); + await page.getByRole("button", { name: "검증하기" }).click(); + await page.getByText("경고를 확인하고 Preview를 만들 수 있습니다").waitFor(); + await page.getByRole("link", { name: "Public Preview 만들기" }).click(); + await page.getByRole("button", { name: "Public Preview 만들기" }).click(); + await page.getByText("현재 저장 버전의 Public Preview입니다.").waitFor(); + await page.getByRole("link", { name: "게시 준비로 이동" }).click(); + await page.getByRole("checkbox").check(); + await expect(page.getByRole("button", { name: /게시$/ })).toBeEnabled(); + await expect(page).toHaveScreenshot("tech-log-studio-warning-acknowledged-1440.png", { + fullPage: true, + }); +});