feat: harden recording lifecycle and refresh fnOS UI
This commit is contained in:
@@ -0,0 +1,502 @@
|
||||
import { expect, test, type Page, type TestInfo } from "@playwright/test";
|
||||
|
||||
const now = "2026-08-02T12:00:00Z";
|
||||
const room = {
|
||||
id: "room-1",
|
||||
platform: 0,
|
||||
platformName: "哔哩哔哩",
|
||||
sourceUrl: "https://live.example/123",
|
||||
originalLiveRoomUrl: "https://live.example/123",
|
||||
normalizedUrl: "https://live.example/123",
|
||||
roomId: "123456",
|
||||
title: "夏日音乐直播间",
|
||||
anchorName: "示例主播",
|
||||
isPinned: true,
|
||||
isPriority: false,
|
||||
overrides: {},
|
||||
effectiveSettings: {
|
||||
preferredQuality: "origin",
|
||||
outputFormat: 0,
|
||||
saveMode: 1,
|
||||
recordingTemplate: 0,
|
||||
segmentDurationMinutes: 30,
|
||||
enableAutoReconnect: true,
|
||||
reconnectDelayMaxSeconds: 60,
|
||||
readWriteTimeoutMilliseconds: 30000,
|
||||
enableDanmakuRecording: true,
|
||||
danmakuIncludeNonChatEvents: false,
|
||||
danmakuMinPollIntervalMilliseconds: 1000,
|
||||
danmakuRetryDelayMaxSeconds: 30
|
||||
},
|
||||
isEnabled: true,
|
||||
availabilityStatus: 2,
|
||||
currentRecordingState: 2,
|
||||
lastAutoStartDecisionCode: "started",
|
||||
lastAutoStartDecisionSummary: "已自动开始录制",
|
||||
lastAutoStartDecisionDetail: "直播状态确认后创建录制会话。",
|
||||
lastAutoStartDecisionAt: now,
|
||||
lastCheckedAt: now,
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
};
|
||||
|
||||
const task = {
|
||||
id: "task-1",
|
||||
liveRoomId: room.id,
|
||||
recordSessionId: "session-12345678",
|
||||
segmentIndex: 1,
|
||||
liveRoomTitle: room.title,
|
||||
platform: 0,
|
||||
roomId: room.roomId,
|
||||
status: 4,
|
||||
preferredQuality: "origin",
|
||||
outputFormat: 0,
|
||||
outputFilePath: "/volume1/录制/示例主播/2026-08-02/分片-001.mp4",
|
||||
createdAt: now,
|
||||
startedAt: now,
|
||||
endedAt: now,
|
||||
durationSeconds: 1800,
|
||||
uploadStatus: 0
|
||||
};
|
||||
|
||||
const session = {
|
||||
id: "session-12345678",
|
||||
liveRoomId: room.id,
|
||||
liveRoomTitle: room.title,
|
||||
platform: 0,
|
||||
roomId: room.roomId,
|
||||
status: 4,
|
||||
preferredQuality: "origin",
|
||||
outputFormat: 0,
|
||||
saveMode: 1,
|
||||
activeSegmentIndex: 0,
|
||||
segmentCount: 1,
|
||||
createdAt: now,
|
||||
startedAt: now,
|
||||
endedAt: now,
|
||||
totalFileSizeBytes: 8_589_934_592,
|
||||
totalDanmakuMessageCount: 2680,
|
||||
uploadedSegmentCount: 0,
|
||||
failedUploadSegmentCount: 0,
|
||||
uploadingSegmentCount: 0,
|
||||
tasks: [task]
|
||||
};
|
||||
|
||||
const uploadTask = {
|
||||
recordTaskId: task.id,
|
||||
recordSessionId: session.id,
|
||||
liveRoomId: room.id,
|
||||
liveRoomTitle: room.title,
|
||||
platform: room.platform,
|
||||
roomId: room.roomId,
|
||||
segmentIndex: 1,
|
||||
outputFormat: 0,
|
||||
filePath: task.outputFilePath,
|
||||
fileSizeBytes: 8_589_934_592,
|
||||
uploadStatus: 0,
|
||||
uploadProgressPercent: 0,
|
||||
uploadAttemptCount: 0,
|
||||
createdAt: now
|
||||
};
|
||||
|
||||
const dashboard = {
|
||||
activeRecordingCount: 1,
|
||||
liveRoomCount: 3,
|
||||
offlineRoomCount: 5,
|
||||
totalRoomCount: 8,
|
||||
todayRecordingSeconds: 12600,
|
||||
todayDataBytes: 32_212_254_720,
|
||||
todayDanmakuCount: 12860,
|
||||
activeSessionCount: 1,
|
||||
recentErrorCount: 2,
|
||||
currentErrorCount: 0,
|
||||
storageStatus: {
|
||||
isEnabled: true,
|
||||
isAvailable: true,
|
||||
hasEnoughSpace: true,
|
||||
message: "空间充足",
|
||||
checkedPath: "/volume1/录制/示例主播/2026-08-02",
|
||||
totalBytes: 1_000_000_000_000,
|
||||
usedBytes: 750_000_000_000,
|
||||
availableBytes: 250_000_000_000,
|
||||
requiredBytes: 100_000_000_000,
|
||||
tier: "Green",
|
||||
usagePercent: 75,
|
||||
freePercent: 25,
|
||||
greenThresholdPercent: 30,
|
||||
redThresholdPercent: 10
|
||||
},
|
||||
recentSessions: [session],
|
||||
topRooms: [{ liveRoomId: room.id, title: room.title, anchorName: room.anchorName, platformName: room.platformName, roomId: room.roomId, sessionCount: 1, totalDurationSeconds: 12600 }],
|
||||
pendingTranscodeCount: 2,
|
||||
pendingUploadCount: 3,
|
||||
queuedDataBytes: 4_294_967_296
|
||||
};
|
||||
|
||||
async function mockApi(page: Page) {
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("live-recorder-token", "e2e-token");
|
||||
localStorage.setItem("live-recorder-user", JSON.stringify({
|
||||
userId: "e2e-user",
|
||||
username: "tester",
|
||||
displayName: "界面测试"
|
||||
}));
|
||||
});
|
||||
|
||||
await page.route(/^http:\/\/127\.0\.0\.1:47173\/api\//, async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const path = url.pathname;
|
||||
let body: unknown = {};
|
||||
|
||||
if (path === "/api/dashboard") body = dashboard;
|
||||
else if (path === "/api/live-rooms") body = [room];
|
||||
else if (path === "/api/record-sessions/page") body = {
|
||||
items: [session],
|
||||
totalCount: 1,
|
||||
skip: 0,
|
||||
take: 20,
|
||||
totalSessionCount: 1,
|
||||
activeSessionCount: 0,
|
||||
totalTaskCount: 1,
|
||||
totalDanmakuCount: session.totalDanmakuMessageCount
|
||||
};
|
||||
else if (path === "/api/record-sessions") body = [session];
|
||||
else if (path === "/api/record-tasks/upload-status") body = {
|
||||
items: [uploadTask],
|
||||
totalCount: 1,
|
||||
notUploadedCount: 1,
|
||||
failedArtifactCount: 0,
|
||||
succeededCount: 0,
|
||||
failedCount: 0,
|
||||
queuedCount: 0,
|
||||
uploadingCount: 0,
|
||||
waitingRetryCount: 0
|
||||
};
|
||||
else if (path === "/api/settings") body = {};
|
||||
else if (path.includes("/record-sessions/stream")) {
|
||||
await route.fulfill({ status: 200, contentType: "text/event-stream", body: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(body) });
|
||||
});
|
||||
}
|
||||
|
||||
async function expectNoDocumentOverflow(page: Page) {
|
||||
await expect.poll(() => page.evaluate(() => ({
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
clientWidth: document.documentElement.clientWidth
|
||||
}))).toEqual(await page.evaluate(() => ({
|
||||
scrollWidth: document.documentElement.clientWidth,
|
||||
clientWidth: document.documentElement.clientWidth
|
||||
})));
|
||||
}
|
||||
|
||||
async function capture(page: Page, testInfo: TestInfo, name: string) {
|
||||
await page.screenshot({ path: testInfo.outputPath(`${name}.png`), fullPage: true });
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
page.on("pageerror", (error) => console.error("[browser pageerror]", error.message));
|
||||
page.on("console", (message) => {
|
||||
if (message.type() === "error") console.error("[browser console]", message.text());
|
||||
});
|
||||
await mockApi(page);
|
||||
});
|
||||
|
||||
test("dashboard keeps storage semantics and the shell within the viewport", async ({ page }, testInfo) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { name: "仪表盘" })).toBeVisible();
|
||||
const storage = page.getByTestId("storage-capacity");
|
||||
await expect(storage).toContainText("75.0%");
|
||||
await expect(storage).toContainText("已使用");
|
||||
await expect(storage).toContainText("25.0%");
|
||||
await expectNoDocumentOverflow(page);
|
||||
await capture(page, testInfo, "dashboard");
|
||||
});
|
||||
|
||||
test("live room table, drawer and dialog retain their final actions", async ({ page }, testInfo) => {
|
||||
await page.goto("/live-rooms");
|
||||
await expect(page.getByRole("heading", { name: "直播间列表" })).toBeVisible();
|
||||
await expectNoDocumentOverflow(page);
|
||||
|
||||
if ((page.viewportSize()?.width ?? 0) >= 768) {
|
||||
const actionHeader = page.getByRole("columnheader", { name: "操作" }).last();
|
||||
await expect(actionHeader).toBeVisible();
|
||||
const box = await actionHeader.boundingBox();
|
||||
expect(box && box.x + box.width).toBeLessThanOrEqual((page.viewportSize()?.width ?? 0) + 1);
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: /^查看/ }).first().click();
|
||||
const drawerFooter = page.locator(".right-drawer__footer");
|
||||
await expect(drawerFooter).toBeVisible();
|
||||
const footerBox = await drawerFooter.boundingBox();
|
||||
expect(footerBox && footerBox.y + footerBox.height).toBeLessThanOrEqual((page.viewportSize()?.height ?? 0) + 1);
|
||||
await page.getByRole("button", { name: "关闭", exact: true }).click();
|
||||
await expect(drawerFooter).toBeHidden();
|
||||
|
||||
await page.getByRole("button", { name: "新增直播间" }).click();
|
||||
const dialogFooter = page.locator(".el-dialog__footer");
|
||||
await expect(dialogFooter).toBeVisible();
|
||||
const dialogFooterBox = await dialogFooter.boundingBox();
|
||||
expect(dialogFooterBox && dialogFooterBox.y + dialogFooterBox.height).toBeLessThanOrEqual((page.viewportSize()?.height ?? 0) + 1);
|
||||
await page.getByRole("button", { name: "取消", exact: true }).click();
|
||||
await expect(dialogFooter).toBeHidden();
|
||||
await capture(page, testInfo, "live-rooms");
|
||||
});
|
||||
|
||||
test("mobile live room list paginates large collections", async ({ page }) => {
|
||||
test.skip((page.viewportSize()?.width ?? 0) >= 768, "mobile-only pagination contract");
|
||||
const manyRooms = Array.from({ length: 25 }, (_, index) => ({
|
||||
...room,
|
||||
id: `room-${index + 1}`,
|
||||
roomId: String(100000 + index + 1),
|
||||
title: `分页直播间 ${index + 1}`,
|
||||
anchorName: `分页主播 ${index + 1}`
|
||||
}));
|
||||
await page.route("**/api/live-rooms", async (route) => {
|
||||
await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(manyRooms) });
|
||||
});
|
||||
|
||||
await page.goto("/live-rooms");
|
||||
await expect(page.locator(".room-card")).toHaveCount(12);
|
||||
await expect(page.getByText("分页直播间 1", { exact: true })).toBeVisible();
|
||||
await page.locator(".mobile-room-pagination .btn-next").click();
|
||||
await expect(page.getByText("分页直播间 13", { exact: true })).toBeVisible();
|
||||
await expect(page.locator(".room-card")).toHaveCount(12);
|
||||
});
|
||||
|
||||
test("record task cards and tables expose one primary action", async ({ page }, testInfo) => {
|
||||
await page.goto("/record-tasks");
|
||||
await expect(page.getByRole("heading", { name: "录制任务" })).toBeVisible();
|
||||
await expect(page.getByPlaceholder(/搜索直播间/)).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /查看/ }).first()).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /更多/ }).first()).toBeVisible();
|
||||
await expectNoDocumentOverflow(page);
|
||||
await capture(page, testInfo, "record-tasks");
|
||||
});
|
||||
|
||||
test("record sessions paginate on the server and bound the rendered page", async ({ page }) => {
|
||||
const allSessions = Array.from({ length: 200 }, (_, index) => ({
|
||||
...session,
|
||||
id: `session-${index + 1}`,
|
||||
liveRoomTitle: `分页录制会话 ${index + 1}`,
|
||||
roomId: String(200000 + index + 1),
|
||||
tasks: [{ ...task, id: `task-${index + 1}`, recordSessionId: `session-${index + 1}` }]
|
||||
}));
|
||||
const requests: Array<{ skip: number; take: number }> = [];
|
||||
|
||||
await page.route("**/api/record-sessions/page**", async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
const skip = Number(url.searchParams.get("skip") || 0);
|
||||
const take = Number(url.searchParams.get("take") || 20);
|
||||
requests.push({ skip, take });
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: allSessions.slice(skip, skip + take),
|
||||
totalCount: allSessions.length,
|
||||
skip,
|
||||
take,
|
||||
totalSessionCount: allSessions.length,
|
||||
activeSessionCount: 0,
|
||||
totalTaskCount: allSessions.length,
|
||||
totalDanmakuCount: allSessions.length * session.totalDanmakuMessageCount
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/record-tasks");
|
||||
const expectedPageSize = (page.viewportSize()?.width ?? 0) < 768 ? 12 : 20;
|
||||
await expect.poll(() => requests.length).toBeGreaterThan(0);
|
||||
expect(requests[0]).toEqual({ skip: 0, take: expectedPageSize });
|
||||
await expect(page.getByText("当前页 " + expectedPageSize + " / 共 200")).toBeVisible();
|
||||
await expect(page.locator((page.viewportSize()?.width ?? 0) < 768 ? ".session-card" : ".session-panel"))
|
||||
.toHaveCount(expectedPageSize);
|
||||
const renderedNodeCount = await page.evaluate(() => document.getElementsByTagName("*").length);
|
||||
expect(renderedNodeCount).toBeLessThan(5_000);
|
||||
|
||||
await page.locator(".session-pagination .btn-next").click();
|
||||
await expect.poll(() => requests.some((item) => item.skip === expectedPageSize)).toBeTruthy();
|
||||
await expect(page.getByText(`分页录制会话 ${expectedPageSize + 1}`, { exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("mobile record session refresh keeps the current page and scroll position", async ({ page }) => {
|
||||
test.skip((page.viewportSize()?.width ?? 0) >= 768, "mobile refresh contract");
|
||||
|
||||
await page.addInitScript(() => {
|
||||
class MockEventSource {
|
||||
onopen: ((event: Event) => void) | null = null;
|
||||
onerror: ((event: Event) => void) | null = null;
|
||||
private listeners = new Map<string, Array<(event: Event) => void>>();
|
||||
|
||||
constructor() {
|
||||
(window as any).__recordSessionEventSource = this;
|
||||
window.setTimeout(() => this.onopen?.(new Event("open")), 0);
|
||||
}
|
||||
|
||||
addEventListener(type: string, listener: EventListenerOrEventListenerObject) {
|
||||
const callback = typeof listener === "function"
|
||||
? listener
|
||||
: (event: Event) => listener.handleEvent(event);
|
||||
this.listeners.set(type, [...(this.listeners.get(type) ?? []), callback]);
|
||||
}
|
||||
|
||||
emit(type: string) {
|
||||
this.listeners.get(type)?.forEach((listener) => listener(new MessageEvent(type, { data: "{}" })));
|
||||
}
|
||||
|
||||
close() {}
|
||||
}
|
||||
|
||||
Object.defineProperty(window, "EventSource", { configurable: true, value: MockEventSource });
|
||||
(window as any).__emitRecordSessionRefresh = () =>
|
||||
(window as any).__recordSessionEventSource?.emit("refresh");
|
||||
});
|
||||
|
||||
const allSessions = Array.from({ length: 60 }, (_, index) => ({
|
||||
...session,
|
||||
id: `refresh-session-${index + 1}`,
|
||||
liveRoomTitle: `刷新录制会话 ${index + 1}`,
|
||||
tasks: [{ ...task, id: `refresh-task-${index + 1}`, recordSessionId: `refresh-session-${index + 1}` }]
|
||||
}));
|
||||
let requestCount = 0;
|
||||
await page.route("**/api/record-sessions/page**", async (route) => {
|
||||
requestCount++;
|
||||
const url = new URL(route.request().url());
|
||||
const skip = Number(url.searchParams.get("skip") || 0);
|
||||
const take = Number(url.searchParams.get("take") || 12);
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: allSessions.slice(skip, skip + take),
|
||||
totalCount: allSessions.length,
|
||||
skip,
|
||||
take,
|
||||
totalSessionCount: allSessions.length,
|
||||
activeSessionCount: 0,
|
||||
totalTaskCount: allSessions.length,
|
||||
totalDanmakuCount: 0
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/record-tasks");
|
||||
await expect(page.locator(".session-card")).toHaveCount(12);
|
||||
const main = page.locator(".app-main");
|
||||
await main.evaluate((element) => { element.scrollTop = element.scrollHeight; });
|
||||
const scrollTopBefore = await main.evaluate((element) => element.scrollTop);
|
||||
expect(scrollTopBefore).toBeGreaterThan(500);
|
||||
|
||||
await page.evaluate(() => (window as any).__emitRecordSessionRefresh());
|
||||
await expect.poll(() => requestCount).toBeGreaterThan(1);
|
||||
await expect(page.locator(".session-card")).toHaveCount(12);
|
||||
const scrollTopAfter = await main.evaluate((element) => element.scrollTop);
|
||||
expect(Math.abs(scrollTopAfter - scrollTopBefore)).toBeLessThanOrEqual(2);
|
||||
});
|
||||
|
||||
test("upload tasks use cards on mobile and never require table horizontal scrolling", async ({ page }, testInfo) => {
|
||||
await page.goto("/upload-tasks");
|
||||
await expect(page.getByRole("heading", { name: "上传任务" })).toBeVisible();
|
||||
await expect(page.getByPlaceholder(/搜索直播间/)).toBeVisible();
|
||||
await expect(page.getByText(task.outputFilePath, { exact: true })).toBeVisible();
|
||||
await expectNoDocumentOverflow(page);
|
||||
|
||||
if ((page.viewportSize()?.width ?? 0) < 1280) {
|
||||
await expect(page.locator(".upload-item-card")).toBeVisible();
|
||||
await expect(page.locator(".upload-table")).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "查看详情" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "立即上传" })).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByRole("columnheader", { name: "操作" })).toBeVisible();
|
||||
const tableScroll = page.locator(".upload-table .el-scrollbar__wrap");
|
||||
await expect(tableScroll).toBeVisible();
|
||||
const dimensions = await tableScroll.evaluate((element) => ({
|
||||
clientWidth: element.clientWidth,
|
||||
scrollWidth: element.scrollWidth
|
||||
}));
|
||||
expect(dimensions.scrollWidth).toBeLessThanOrEqual(dimensions.clientWidth + 1);
|
||||
const uploadButton = page.getByRole("button", { name: "上传", exact: true });
|
||||
await expect(uploadButton).toBeVisible();
|
||||
const uploadButtonBox = await uploadButton.boundingBox();
|
||||
expect(uploadButtonBox && uploadButtonBox.x + uploadButtonBox.width)
|
||||
.toBeLessThanOrEqual((page.viewportSize()?.width ?? 0) + 1);
|
||||
}
|
||||
|
||||
await capture(page, testInfo, "upload-tasks");
|
||||
});
|
||||
|
||||
test("mobile upload polling keeps cards and scroll position while progress updates", async ({ page }) => {
|
||||
test.skip((page.viewportSize()?.width ?? 0) >= 768, "mobile polling contract");
|
||||
|
||||
const allItems = Array.from({ length: 60 }, (_, index) => ({
|
||||
...uploadTask,
|
||||
recordTaskId: `upload-task-${index + 1}`,
|
||||
segmentIndex: index + 1,
|
||||
liveRoomTitle: `轮询直播间 ${index + 1}`,
|
||||
uploadStatus: 3,
|
||||
uploadProgressPercent: 10
|
||||
}));
|
||||
let requestCount = 0;
|
||||
let inFlight = 0;
|
||||
let maxInFlight = 0;
|
||||
|
||||
await page.route("**/api/record-tasks/upload-status**", async (route) => {
|
||||
requestCount++;
|
||||
inFlight++;
|
||||
maxInFlight = Math.max(maxInFlight, inFlight);
|
||||
const url = new URL(route.request().url());
|
||||
const skip = Number(url.searchParams.get("skip") || 0);
|
||||
const take = Number(url.searchParams.get("take") || 50);
|
||||
if (requestCount > 1) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
const progress = requestCount > 1 ? 42 : 10;
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: allItems.slice(skip, skip + take).map((item) => ({ ...item, uploadProgressPercent: progress })),
|
||||
totalCount: allItems.length,
|
||||
notUploadedCount: 0,
|
||||
failedArtifactCount: 0,
|
||||
succeededCount: 0,
|
||||
failedCount: 0,
|
||||
queuedCount: 0,
|
||||
uploadingCount: allItems.length,
|
||||
waitingRetryCount: 0
|
||||
})
|
||||
});
|
||||
inFlight--;
|
||||
});
|
||||
|
||||
await page.goto("/upload-tasks");
|
||||
await expect(page.locator(".upload-item-card")).toHaveCount(12);
|
||||
await expect(page.getByText("10.0% · 第 0 次").first()).toBeVisible();
|
||||
|
||||
const main = page.locator(".app-main");
|
||||
await main.evaluate((element) => { element.scrollTop = element.scrollHeight; });
|
||||
const scrollTopBefore = await main.evaluate((element) => element.scrollTop);
|
||||
expect(scrollTopBefore).toBeGreaterThan(500);
|
||||
|
||||
await expect.poll(() => requestCount, { timeout: 10_000 }).toBeGreaterThan(1);
|
||||
await expect(page.locator(".upload-item-card")).toHaveCount(12);
|
||||
await expect(page.locator(".el-skeleton")).toHaveCount(0);
|
||||
await expect(page.getByText("42.0% · 第 0 次").first()).toBeVisible();
|
||||
|
||||
const scrollTopAfter = await main.evaluate((element) => element.scrollTop);
|
||||
expect(Math.abs(scrollTopAfter - scrollTopBefore)).toBeLessThanOrEqual(2);
|
||||
expect(maxInFlight).toBe(1);
|
||||
});
|
||||
|
||||
test("settings layer advanced controls without clipping the quick bar", async ({ page }, testInfo) => {
|
||||
await page.goto("/settings/recording");
|
||||
await expect(page.getByRole("heading", { name: "系统设置" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "保留清理" })).toBeHidden();
|
||||
await page.getByText("高级设置", { exact: true }).click();
|
||||
await expect(page.getByRole("heading", { name: "保留清理" })).toBeVisible();
|
||||
await expectNoDocumentOverflow(page);
|
||||
await capture(page, testInfo, "settings");
|
||||
});
|
||||
Reference in New Issue
Block a user