test: prove TechLog UI migration parity

This commit is contained in:
DongHyeonka
2026-08-16 02:48:26 +09:00
parent c5c8b9423c
commit 6c2780b7a7
176 changed files with 2393 additions and 616 deletions
+21 -39
View File
@@ -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);
});
+14 -17
View File
@@ -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();
});
-23
View File
@@ -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);
});
@@ -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");
});
-41
View File
@@ -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();
});
+31 -31
View File
@@ -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",
);
});
+77
View File
@@ -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<ReturnType<AxeBuilder["analyze"]>>) {
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([]);
});
@@ -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();
});
+41
View File
@@ -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();
});
}
-38
View File
@@ -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");
});