모바일 메뉴와 주요 화면 UX 개선 및 시각 검수

This commit is contained in:
Yun Chan 2026-09-12 17:57:59 +09:00
parent 89b5093ec7
commit e8b770e4d9
50 changed files with 3189 additions and 457 deletions

View file

@ -47,6 +47,111 @@ async function expectNoLocalStageDemoControl(page: Page) {
await expect(page.locator(".sx-track__advance")).toHaveCount(0);
}
async function expectPrestartSummaryReadableOnMobile(page: Page) {
if (!(await page.evaluate(() => window.matchMedia("(max-width: 720px)").matches))) return;
await page.evaluate(() => {
const section = document.querySelector(".sx-page--prestart");
let owner = document.scrollingElement;
for (let node = section?.parentElement; node; node = node.parentElement) {
const style = window.getComputedStyle(node);
if (/(auto|scroll)/.test(style.overflowY) && node.scrollHeight > node.clientHeight + 1) {
owner = node;
break;
}
}
if (!owner) throw new Error("Expected prestart scroll owner");
owner.scrollTop = Math.round((owner.scrollHeight - owner.clientHeight) / 2);
});
await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(resolve)));
const metrics = await page.evaluate(() => {
const facts = document.querySelector<HTMLElement>(".sx-page--prestart .sx-prestart__facts");
const complaint = facts?.querySelector<HTMLElement>(":scope > div:first-child");
const complaintText = complaint?.querySelector<HTMLElement>("dd");
const actions = document.querySelector<HTMLElement>(".sx-page--prestart .sx-prestart__actions");
if (!facts || !complaint || !complaintText || !actions) return null;
const complaintRect = complaint.getBoundingClientRect();
const factsRect = facts.getBoundingClientRect();
const complaintStyle = window.getComputedStyle(complaintText);
const actionsStyle = window.getComputedStyle(actions);
const actionsRect = actions.getBoundingClientRect();
const navigation = document.querySelector<HTMLElement>(".vg-nav");
const navigationRect = navigation?.getBoundingClientRect();
const startButton = actions.querySelector<HTMLElement>(".vg-btn");
const actionDescription = actions.querySelector<HTMLElement>("span");
const alphaMatch = actionsStyle.backgroundColor.match(/^rgba\([^,]+,\s*[^,]+,\s*[^,]+,\s*([0-9.]+)\)$/);
const actionsAlpha = actionsStyle.backgroundColor === "transparent"
? 0
: alphaMatch
? Number.parseFloat(alphaMatch[1])
: 1;
const hitPoints = [startButton, actionDescription, actions]
.filter((element): element is HTMLElement => Boolean(element))
.map((element) => {
const rect = element.getBoundingClientRect();
const x = Math.round(rect.left + rect.width / 2);
const y = Math.round(rect.top + rect.height / 2);
const hit = document.elementFromPoint(x, y);
return {
x,
y,
hitClass: hit instanceof HTMLElement ? hit.className : hit?.nodeName ?? null,
belongsToAction: Boolean(hit && actions.contains(hit)),
};
});
return {
complaintWidth: Math.round(complaintRect.width),
factsWidth: Math.round(factsRect.width),
fontSize: Number.parseFloat(complaintStyle.fontSize),
lineHeight: Number.parseFloat(complaintStyle.lineHeight),
actionsBackground: actionsStyle.backgroundColor,
actionsAlpha,
actionsPosition: actionsStyle.position,
actionsBottom: Math.round(actionsRect.bottom),
navigationTop: navigationRect ? Math.round(navigationRect.top) : null,
hitPoints,
};
});
expect(metrics, "Expected mobile prestart facts and start action").not.toBeNull();
expect(metrics!.complaintWidth, `Complaint should span the facts row: ${JSON.stringify(metrics)}`).toBeGreaterThanOrEqual(
metrics!.factsWidth - 2,
);
expect(metrics!.fontSize, `Complaint text is too small: ${JSON.stringify(metrics)}`).toBeGreaterThanOrEqual(14);
expect(metrics!.lineHeight, `Complaint line height is too tight: ${JSON.stringify(metrics)}`).toBeGreaterThanOrEqual(21);
expect(metrics!.actionsAlpha, `Start action must be opaque: ${JSON.stringify(metrics)}`).toBe(1);
expect(metrics!.actionsPosition, `Start action must stay at the mobile bottom: ${JSON.stringify(metrics)}`).toBe("fixed");
expect(metrics!.navigationTop, `Expected mobile navigation: ${JSON.stringify(metrics)}`).not.toBeNull();
expect(metrics!.actionsBottom, `Start action must stay above mobile navigation: ${JSON.stringify(metrics)}`).toBeLessThanOrEqual(
metrics!.navigationTop! + 1,
);
expect(metrics!.hitPoints, `Expected three start action hit points: ${JSON.stringify(metrics)}`).toHaveLength(3);
expect(metrics!.hitPoints.every((point) => point.belongsToAction), `Start action is covered at its controls: ${JSON.stringify(metrics)}`).toBe(true);
const goals = page.locator(".sx-page--prestart .sx-goals__grid button");
for (const goal of await goals.all()) {
await goal.scrollIntoViewIfNeeded();
const geometry = await goal.evaluate((button) => {
const action = document.querySelector<HTMLElement>(".sx-page--prestart .sx-prestart__actions");
const buttonRect = button.getBoundingClientRect();
const actionRect = action?.getBoundingClientRect();
return {
top: Math.round(buttonRect.top),
bottom: Math.round(buttonRect.bottom),
actionTop: actionRect ? Math.round(actionRect.top) : null,
};
});
expect(geometry.top, `Goal is above the viewport: ${JSON.stringify(geometry)}`).toBeGreaterThanOrEqual(0);
expect(geometry.actionTop, `Expected fixed start action: ${JSON.stringify(geometry)}`).not.toBeNull();
expect(geometry.bottom, `Goal is hidden behind the start action: ${JSON.stringify(geometry)}`).toBeLessThanOrEqual(
geometry.actionTop! - 1,
);
}
}
async function expectMobileContextIfNarrow(page: Page) {
const isNarrow = await page.evaluate(() =>
window.matchMedia("(max-width: 1180px)").matches,
@ -473,6 +578,7 @@ test.describe("learner session full-screen layout", () => {
await expect(page.getByRole("button", { name: "회기 시작" })).toBeVisible();
await expectNoHorizontalOverflow(page);
await expectPrestartSummaryReadableOnMobile(page);
await page.getByRole("button", { name: "회기 시작" }).click();
await completeAlliancePreCheckpoint(page);