rebuild(stage-13): redesign mobile information architecture
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const { expect, test } = require("@playwright/test");
|
||||
|
||||
const evidence = path.resolve(__dirname, "../../docs/evidence/stage-13");
|
||||
test.beforeAll(() => fs.mkdirSync(evidence, { recursive: true }));
|
||||
|
||||
async function authenticate(page) {
|
||||
await page.goto("/");
|
||||
await page.getByLabel("账号名").fill("stage13admin");
|
||||
await page.getByLabel("密码").fill("Stage13-pass-123!");
|
||||
await page.getByRole("button", { name: "登录", exact: true }).click();
|
||||
await expect(page.locator(".sidebar, .field-error")).toBeVisible();
|
||||
if (!(await page.locator(".sidebar").isVisible())) {
|
||||
await page.getByRole("tab", { name: "注册" }).click();
|
||||
await page.getByRole("button", { name: "注册并登录" }).click();
|
||||
}
|
||||
}
|
||||
|
||||
function summary() {
|
||||
return {
|
||||
context: { requested_date: "2026-07-30", actual_date: "2026-07-30", previous_date: "2026-07-29", observed_at: "2026-07-30T15:00:00+08:00", state: "final", carried_forward: false, message: "" },
|
||||
values: { temperature: 42, limit_up: 68, limit_down: 4, broken: 24, seal_rate: 73.9, amount: 1628000000000 },
|
||||
};
|
||||
}
|
||||
|
||||
function pool() {
|
||||
const items = Array.from({ length: 4 }, (_, index) => ({
|
||||
identifier: `00000${index + 1}.SZ`, code: `00000${index + 1}`, name: `移动样本${index + 1}`,
|
||||
streak: index + 1, change: 9.9, price: 10 + index, sector: "机器人",
|
||||
first_time: "09:35", last_time: "14:20", open_times: index, turnover_rate: 8 + index,
|
||||
amount: 300000000, seal_amount: 50000000, reason: "产业链催化与资金承接",
|
||||
}));
|
||||
return { trade_date: "2026-07-30", observed_at: "2026-07-30T15:00:00+08:00", carried_forward: false, message: "", overview: { up_count: 2960, down_count: 1980, flat_count: 112, limit_up: 68, limit_down: 4, broken: 24, seal_rate: 73.9, amount: 1628000000000 }, items };
|
||||
}
|
||||
|
||||
async function mockMarket(page) {
|
||||
await page.route("**/api/market/summary", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify(summary()) }));
|
||||
await page.route("**/api/review/alerts?*", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [], unread_count: 0, as_of: "2026-07-30" }) }));
|
||||
await page.route("**/api/market/workspaces/*", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify(pool()) }));
|
||||
}
|
||||
|
||||
async function assertViewport(page, width, height) {
|
||||
await page.setViewportSize({ width, height });
|
||||
await expect(page.getByLabel("切换行情页面")).toBeVisible();
|
||||
const metrics = await page.evaluate(() => {
|
||||
const visible = (element) => {
|
||||
const style = getComputedStyle(element);
|
||||
const rect = element.getBoundingClientRect();
|
||||
return style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
|
||||
};
|
||||
const undersized = [...document.querySelectorAll("button")]
|
||||
.filter(visible)
|
||||
.map((element) => ({ label: element.getAttribute("aria-label") || element.textContent.trim(), rect: element.getBoundingClientRect() }))
|
||||
.filter((item) => item.rect.width < 43.5 || item.rect.height < 43.5)
|
||||
.map((item) => item.label);
|
||||
return { overflow: document.documentElement.scrollWidth - innerWidth, undersized };
|
||||
});
|
||||
expect(metrics.overflow).toBeLessThanOrEqual(0);
|
||||
expect(metrics.undersized).toEqual([]);
|
||||
}
|
||||
|
||||
test("mobile information architecture, touch targets and responsive tables", async ({ page }) => {
|
||||
const consoleErrors = [];
|
||||
page.on("console", (message) => { if (message.type() === "error" && !message.text().includes("401 (Unauthorized)")) consoleErrors.push(message.text()); });
|
||||
await mockMarket(page);
|
||||
await authenticate(page);
|
||||
await page.setViewportSize({ width: 320, height: 568 });
|
||||
await page.getByLabel("切换行情页面").selectOption("pool");
|
||||
await expect(page.getByRole("heading", { name: "涨停池" })).toBeVisible();
|
||||
await expect(page.locator(".mobile-date-control .date-input")).toHaveValue("2026-07-30");
|
||||
await expect(page.getByRole("navigation", { name: "移动端主导航" }).getByText("行情", { exact: true })).toHaveAttribute("aria-current", "page");
|
||||
await expect(page.locator(".data-table tbody tr")).toHaveCount(4);
|
||||
await expect(page.locator(".data-table tbody td").filter({ hasText: "移动样本1" })).toHaveAttribute("data-label", "股票");
|
||||
expect(await page.locator(".data-table tbody tr").first().evaluate((row) => row.scrollWidth <= row.clientWidth)).toBe(true);
|
||||
|
||||
for (const viewport of [[320, 568], [375, 667], [390, 844], [430, 932], [768, 1024], [844, 390]]) {
|
||||
await assertViewport(page, viewport[0], viewport[1]);
|
||||
}
|
||||
for (const viewport of [[1024, 768], [1280, 720], [1440, 900], [1536, 864], [1920, 1080], [3840, 2160]]) {
|
||||
await page.setViewportSize({ width: viewport[0], height: viewport[1] });
|
||||
await expect(page.getByLabel("切换行情页面")).toBeHidden();
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(0);
|
||||
}
|
||||
await page.setViewportSize({ width: 320, height: 568 });
|
||||
await page.screenshot({ path: path.join(evidence, "pool-light-320x568.jpg"), type: "jpeg", quality: 82 });
|
||||
await page.getByRole("button", { name: "夜间" }).click();
|
||||
await page.setViewportSize({ width: 844, height: 390 });
|
||||
await page.screenshot({ path: path.join(evidence, "pool-dark-landscape-844x390.jpg"), type: "jpeg", quality: 82 });
|
||||
expect(consoleErrors).toEqual([]);
|
||||
});
|
||||
|
||||
test("keyboard focus, dialog return and reduced motion remain available", async ({ page }) => {
|
||||
await mockMarket(page);
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await authenticate(page);
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.keyboard.press("Tab");
|
||||
await expect(page.getByRole("link", { name: "跳到主要内容" })).toBeFocused();
|
||||
await page.keyboard.press("Enter");
|
||||
await expect(page.locator("#main-content")).toBeFocused();
|
||||
const search = page.getByRole("button", { name: "全局搜索" });
|
||||
await search.focus();
|
||||
await search.click();
|
||||
await expect(page.getByRole("dialog", { name: "全局搜索" })).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(search).toBeFocused();
|
||||
await expect(page.locator(".emotion-dot")).toHaveCSS("animation-duration", "0.001s");
|
||||
});
|
||||
|
||||
test("all sixteen workspaces remain reachable without page overflow", async ({ page }) => {
|
||||
await page.route("**/api/market/summary", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify(summary()) }));
|
||||
await page.route("**/api/review/alerts?*", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [], unread_count: 0, as_of: "2026-07-30" }) }));
|
||||
await page.route("**/api/market/workspaces/*", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify({ trade_date: "", message: "移动端空态验收" }) }));
|
||||
await page.route("**/api/market/insights/*", (route) => route.fulfill({ contentType: "application/json", body: JSON.stringify({ trade_date: "", message: "移动端空态验收" }) }));
|
||||
await authenticate(page);
|
||||
await page.setViewportSize({ width: 320, height: 568 });
|
||||
const keys = ["emotion", "pool", "broken", "limit-down", "yesterday", "performance", "ladder", "rotation", "auction", "themes", "popularity", "dragon-list", "screener", "mentor", "heaven", "review"];
|
||||
for (const key of keys) {
|
||||
await page.goto(`/workspace/${key}`);
|
||||
await expect(page.locator(".page-frame")).toBeVisible();
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth - innerWidth), key).toBeLessThanOrEqual(0);
|
||||
await expect(page.locator(".mobile-date-control .date-input")).toBeVisible();
|
||||
if (key === "review") {
|
||||
await expect(page.getByRole("button", { name: "提醒中心" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "复盘助手" })).toBeVisible();
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user