chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
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 ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto(route);
|
||||
await expect(page.getByRole("main")).toBeVisible();
|
||||
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([]);
|
||||
});
|
||||
}
|
||||
|
||||
test("@a11y keyboard reaches the primary route action with visible focus", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
const action = page.getByRole("link", { name: "플랫폼 구성 보기" });
|
||||
await expect(action).toBeVisible();
|
||||
await page.keyboard.press("Tab");
|
||||
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 ({
|
||||
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([]);
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
|
||||
test("boots the public app shell", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { level: 1 })).toHaveText(
|
||||
"Clean Architecture Frontend",
|
||||
);
|
||||
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
|
||||
await expect(page.getByRole("main")).toBeVisible();
|
||||
});
|
||||
|
||||
test("navigates to a registry-backed example without a page reload", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page
|
||||
.getByRole("navigation", { name: "주요 탐색" })
|
||||
.getByRole("link", { name: "화면 상태" })
|
||||
.click();
|
||||
|
||||
await expect(page).toHaveURL(/\/examples\/states$/);
|
||||
await expect(
|
||||
page.getByRole("heading", { level: 1, name: "화면 상태" }),
|
||||
).toBeFocused();
|
||||
});
|
||||
|
||||
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 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 page.keyboard.press("Escape");
|
||||
await expect(menu).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeHidden();
|
||||
await expect(menu).toBeFocused();
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
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);
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
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");
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
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();
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import { SERVER_STATE_PROFILES } from "../../src/contracts/server-state.ts";
|
||||
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
|
||||
|
||||
/**
|
||||
* The overview is a projection of the installed registries. These assertions
|
||||
* read the same registries the shipped bundle was built from, so they keep
|
||||
* meaning after a feature is added or removed.
|
||||
*/
|
||||
|
||||
test("projects the installed route registry into the shipped bundle", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/examples/platform");
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "플랫폼 구성", level: 1 }),
|
||||
).toBeVisible();
|
||||
|
||||
const table = page.getByRole("table", { name: "설치된 라우트 목록" });
|
||||
await expect(table.locator("tbody tr")).toHaveCount(
|
||||
Object.keys(ROUTE_REGISTRY).length,
|
||||
);
|
||||
for (const routeId of Object.keys(ROUTE_REGISTRY)) {
|
||||
await expect(table.getByText(routeId, { exact: true })).toBeVisible();
|
||||
}
|
||||
});
|
||||
|
||||
test("shows every fixed server-state profile", async ({ page }) => {
|
||||
await page.goto("/examples/platform");
|
||||
|
||||
const table = page.getByRole("table", { name: "서버 상태 프로파일" });
|
||||
await expect(table.locator("tbody tr")).toHaveCount(
|
||||
Object.keys(SERVER_STATE_PROFILES).length,
|
||||
);
|
||||
});
|
||||
|
||||
test("states the release contract identity verified at boot", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/examples/platform");
|
||||
|
||||
const release = page.locator(
|
||||
"section[aria-labelledby='platform-release-title']",
|
||||
);
|
||||
await expect(release.getByText(/^sha256:/)).toBeVisible();
|
||||
});
|
||||
|
||||
test("reports an unselected capability without claiming it was disabled", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/examples/platform");
|
||||
|
||||
const capabilities = page.locator(
|
||||
"section[aria-labelledby='platform-capabilities-title']",
|
||||
);
|
||||
await expect(capabilities.getByText("미선택")).toHaveCount(4);
|
||||
await expect(capabilities.getByText("운영자가 비활성화함")).toHaveCount(0);
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
|
||||
async function openReferenceForm(page: Page) {
|
||||
await page.goto("/examples/reference-resources/new");
|
||||
await page.getByRole("button", { name: "로그인 시작" }).click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Reference resource 만들기" }),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
test("validates a reference form and focuses the first invalid field", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openReferenceForm(page);
|
||||
await page.getByRole("button", { name: "저장" }).click();
|
||||
|
||||
const firstField = page.getByRole("textbox", { name: /새 항목 이름/ });
|
||||
await expect(firstField).toBeFocused();
|
||||
await expect(firstField).toHaveAttribute("aria-invalid", "true");
|
||||
await expect(page.getByRole("alert")).toContainText("입력 내용을 확인해 주세요.");
|
||||
});
|
||||
|
||||
test("guards dirty cancellation and restores focus when writing continues", async ({
|
||||
page,
|
||||
}) => {
|
||||
await openReferenceForm(page);
|
||||
await page
|
||||
.getByRole("textbox", { name: /새 항목 이름/ })
|
||||
.fill("Unsaved reference");
|
||||
const cancel = page.getByRole("button", { name: "취소" });
|
||||
await cancel.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("dialog", { name: "저장하지 않은 변경이 있습니다." }),
|
||||
).toBeVisible();
|
||||
await page.getByRole("button", { name: "계속 작성" }).click();
|
||||
await expect(cancel).toBeFocused();
|
||||
await expect(page).toHaveURL(/\/examples\/reference-resources\/new$/);
|
||||
});
|
||||
|
||||
test("keeps the form template within a 320px viewport", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 320, height: 720 });
|
||||
await openReferenceForm(page);
|
||||
const viewport = await page.evaluate(() => ({
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
}));
|
||||
expect(viewport.scrollWidth).toBeLessThanOrEqual(viewport.clientWidth);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
import { ROUTE_REGISTRY } from "../../src/features/installed-feature-contracts.ts";
|
||||
|
||||
test("opens the protected integration route through the local demo seam", async ({
|
||||
page,
|
||||
}) => {
|
||||
const protectedRoute = ROUTE_REGISTRY.REFERENCE_RESOURCE_LIST;
|
||||
await page.route(
|
||||
"http://localhost:8080/api/reference-resources?*",
|
||||
(route) =>
|
||||
route.fulfill({
|
||||
json: [
|
||||
{
|
||||
id: "browser-reference",
|
||||
name: "Browser reference",
|
||||
createdAt: "2026-07-26T00:00:00.000Z",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
await page.goto(protectedRoute.path);
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "세션이 필요합니다." }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByRole("button", { name: "로그인 시작" }).click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: protectedRoute.title }),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("인증됨")).toBeVisible();
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.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();
|
||||
|
||||
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/,
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
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");
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { expect, test } from "../support/browser/strict-browser-test.ts";
|
||||
|
||||
test("validates and reports the common text-field flow", async ({ page }) => {
|
||||
await page.goto("/examples/ui");
|
||||
await page.getByRole("button", { name: "입력 확인" }).click();
|
||||
|
||||
const field = page.getByRole("textbox", { name: "프로젝트 이름" });
|
||||
await expect(field).toHaveAttribute("aria-invalid", "true");
|
||||
await expect(field).toHaveAccessibleDescription(
|
||||
/프로젝트 이름을 입력해 주세요/,
|
||||
);
|
||||
|
||||
await field.fill("Starter");
|
||||
await page.getByRole("button", { name: "입력 확인" }).click();
|
||||
await expect(page.getByText("“Starter” 입력을 확인했습니다.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("traps modal interaction and restores focus to the trigger", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/examples/ui");
|
||||
const trigger = page.getByRole("button", { name: "모달 열기" });
|
||||
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();
|
||||
});
|
||||
Reference in New Issue
Block a user