// @ts-nocheck -- standalone evidence runner executed directly by Node 24. import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; import { createRequire } from "node:module"; import { lstat, mkdir, readFile, readdir, readlink, writeFile } from "node:fs/promises"; import { relative, resolve } from "node:path"; import { chromium } from "@playwright/test"; import { TECH_LOG_BREAKPOINT_WIDTHS, TECH_LOG_CANONICAL_ROUTES, TECH_LOG_FIXED_TIME, TECH_LOG_PUBLIC_FIXTURE_PATHS, 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:4375"; const targetBaseUrl = process.env.TECH_LOG_TARGET_URL ?? "http://127.0.0.1:4174"; const sourceRoot = process.env.TECH_LOG_SOURCE_ROOT ?? "/home/donghyeon/workspace/techlog-studio-frontend"; const outputPath = process.env.TECH_LOG_PARITY_OUTPUT ?? "docs/operations/evidence/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_PUBLIC_FIXTURE_PATHS.map((path) => ({ name: `TECH_LOG_PUBLIC_FIXTURE-${path.slice(1).replaceAll("/", "-").toUpperCase()}`, path, width: 1440, })), ...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("region", { name: "즉시 미리보기" }).waitFor(); } 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, waitForTargetBootstrap) { page.setDefaultNavigationTimeout(15_000); page.setDefaultTimeout(15_000); await page.setViewportSize({ width: parityCase.width, height: 1000 }); await page.clock.setFixedTime(fixedDate); const navigationResponse = await page.goto( new URL(parityCase.path, baseUrl).href, { waitUntil: "networkidle" }, ); if (!navigationResponse) throw new Error(`Missing navigation response: ${parityCase.name}`); const response = await responseMetadata(navigationResponse); if (response.mimeType === "text/html" && waitForTargetBootstrap) { // The target sets these only after config + release-manifest bodies have // been fully read, validated, and composed. This is the product's boot // completion signal, not a delay or retry. await page.locator("html[data-build-id][data-release-id]").waitFor(); } 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); return response; } async function productSubtree(page) { return page.evaluate(() => { const normalize = (value) => value .replace(/_[rR][^\s"']*?(?=-|\s|$)/g, "") .replace(/_([A-Za-z0-9]+)_[A-Za-z0-9]+_(\d+)/g, "_$1__$2"); const frameworkGeneratedAttributes = new Set([ "data-discover", // React Router link discovery metadata. "data-nimg", // Next/Image metadata derived from source props. "decoding", // Next/Image output derived from priority=false. "srcset", // Next/Image widths derived from one source SVG. "selected", // Next SSR output for a controlled