feat: improve recording recovery and upload workflow
This commit is contained in:
@@ -63,6 +63,7 @@ const session = {
|
||||
id: "session-12345678",
|
||||
liveRoomId: room.id,
|
||||
liveRoomTitle: room.title,
|
||||
anchorName: room.anchorName,
|
||||
platform: 0,
|
||||
roomId: room.roomId,
|
||||
status: 4,
|
||||
@@ -82,6 +83,28 @@ const session = {
|
||||
tasks: [task]
|
||||
};
|
||||
|
||||
const sessionDetail = {
|
||||
session,
|
||||
timeline: {
|
||||
anchorAt: now,
|
||||
totalDurationSeconds: 5400,
|
||||
segments: Array.from({ length: 3 }, (_, index) => ({
|
||||
recordTaskId: `task-${index + 1}`,
|
||||
segmentIndex: index + 1,
|
||||
status: 4,
|
||||
startedAt: new Date(Date.parse(now) + index * 1_800_000).toISOString(),
|
||||
endedAt: new Date(Date.parse(now) + (index + 1) * 1_800_000).toISOString(),
|
||||
offsetSeconds: index * 1800,
|
||||
durationSeconds: 1800,
|
||||
label: `/volume1/录制/示例主播/2026-08-02/分片-${String(index + 1).padStart(3, "0")}.mp4`,
|
||||
detail: "录制完成"
|
||||
})),
|
||||
events: [],
|
||||
heatBuckets: []
|
||||
},
|
||||
logs: []
|
||||
};
|
||||
|
||||
const uploadTask = {
|
||||
recordTaskId: task.id,
|
||||
recordSessionId: session.id,
|
||||
@@ -99,6 +122,29 @@ const uploadTask = {
|
||||
createdAt: now
|
||||
};
|
||||
|
||||
const recordingFailure = {
|
||||
recordTaskId: "failed-task-1",
|
||||
recordSessionId: "failed-session-1",
|
||||
liveRoomId: room.id,
|
||||
liveRoomTitle: room.title,
|
||||
roomId: room.roomId,
|
||||
platformName: room.platformName,
|
||||
segmentIndex: 7,
|
||||
failureKind: "ReadableFragment",
|
||||
failureLabel: "异常退出分片",
|
||||
recommendedAction: "文件可以读取。确认内容有效后,将它转为待上传任务。",
|
||||
errorMessage: "FFmpeg 异常退出;原始文件会保留,不会被修复流程覆盖。",
|
||||
filePath: "/volume1/录制/示例主播/2026-08-02/一个用于验证窄屏换行的很长文件名-007.mp4",
|
||||
fileSizeBytes: 4_294_967_296,
|
||||
durationSeconds: 1789,
|
||||
fileExists: true,
|
||||
canAccept: true,
|
||||
canRepair: false,
|
||||
canRetryRoom: false,
|
||||
isRepairing: false,
|
||||
createdAt: now
|
||||
};
|
||||
|
||||
const dashboard = {
|
||||
activeRecordingCount: 1,
|
||||
liveRoomCount: 3,
|
||||
@@ -160,6 +206,7 @@ async function mockApi(page: Page) {
|
||||
totalTaskCount: 1,
|
||||
totalDanmakuCount: session.totalDanmakuMessageCount
|
||||
};
|
||||
else if (path === `/api/record-sessions/${session.id}`) body = sessionDetail;
|
||||
else if (path === "/api/record-sessions") body = [session];
|
||||
else if (path === "/api/record-tasks/upload-status") body = {
|
||||
items: [uploadTask],
|
||||
@@ -170,7 +217,18 @@ async function mockApi(page: Page) {
|
||||
failedCount: 0,
|
||||
queuedCount: 0,
|
||||
uploadingCount: 0,
|
||||
waitingRetryCount: 0
|
||||
waitingRetryCount: 0,
|
||||
matchingRetryableCount: 0,
|
||||
queueHealth: { state: "Healthy", isPaused: false }
|
||||
};
|
||||
else if (path === "/api/recovery/recording-failures") body = {
|
||||
items: [recordingFailure],
|
||||
totalCount: 1
|
||||
};
|
||||
else if (path === "/api/recovery") body = {
|
||||
storage: dashboard.storageStatus,
|
||||
liveRooms: [],
|
||||
finalizations: []
|
||||
};
|
||||
else if (path === "/api/settings") body = {};
|
||||
else if (path.includes("/record-sessions/stream")) {
|
||||
@@ -204,23 +262,117 @@ test.beforeEach(async ({ page }) => {
|
||||
await mockApi(page);
|
||||
});
|
||||
|
||||
test("login preserves the product hierarchy without viewport overflow", async ({ page }, testInfo) => {
|
||||
await page.addInitScript(() => localStorage.clear());
|
||||
await page.goto("/login");
|
||||
await expect(page.getByRole("heading", { name: /让每一次开播/ })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "欢迎回来" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /登录控制台/ })).toBeVisible();
|
||||
await expectNoDocumentOverflow(page);
|
||||
await capture(page, testInfo, "login");
|
||||
});
|
||||
|
||||
test("dashboard keeps storage semantics and the shell within the viewport", async ({ page }, testInfo) => {
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { name: "仪表盘" })).toBeVisible();
|
||||
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%");
|
||||
if ((page.viewportSize()?.width ?? 0) <= 768) {
|
||||
const avatarGeometry = await page.locator(".app-user-btn").evaluate((button) => {
|
||||
const avatar = button.querySelector<SVGSVGElement>(".app-user-btn__avatar")!;
|
||||
const circle = avatar.querySelector<SVGCircleElement>("circle")!;
|
||||
const buttonRect = button.getBoundingClientRect();
|
||||
const avatarRect = avatar.getBoundingClientRect();
|
||||
const circleRect = circle.getBoundingClientRect();
|
||||
return {
|
||||
tagName: avatar.tagName.toLowerCase(),
|
||||
buttonDelta: Math.abs(buttonRect.width - buttonRect.height),
|
||||
avatarDelta: Math.abs(avatarRect.width - avatarRect.height),
|
||||
circleDelta: Math.abs(circleRect.width - circleRect.height)
|
||||
};
|
||||
});
|
||||
expect(avatarGeometry.tagName).toBe("svg");
|
||||
expect(avatarGeometry.buttonDelta).toBeLessThanOrEqual(1);
|
||||
expect(avatarGeometry.avatarDelta).toBeLessThanOrEqual(1);
|
||||
expect(avatarGeometry.circleDelta).toBeLessThanOrEqual(1);
|
||||
}
|
||||
await expectNoDocumentOverflow(page);
|
||||
await capture(page, testInfo, "dashboard");
|
||||
});
|
||||
|
||||
test("mobile navigation animates route and active state without a tap frame", async ({ page }, testInfo) => {
|
||||
test.skip((page.viewportSize()?.width ?? 0) > 768, "mobile navigation contract");
|
||||
await page.emulateMedia({ reducedMotion: "no-preference" });
|
||||
await page.goto("/");
|
||||
await expect(page.getByRole("heading", { name: "运行中心" })).toBeVisible();
|
||||
await expect(page.locator(".app-main")).toBeVisible();
|
||||
await expect(page.locator(".app-bottom-nav button[aria-current='page']")).toHaveCount(1);
|
||||
|
||||
await page.evaluate(() => {
|
||||
(window as Window & { __pageSwapObserved?: boolean }).__pageSwapObserved = false;
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
if (mutations.some((mutation) =>
|
||||
mutation.target instanceof HTMLElement && mutation.target.className.includes("page-swap-")
|
||||
)) {
|
||||
(window as Window & { __pageSwapObserved?: boolean }).__pageSwapObserved = true;
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
observer.observe(document.querySelector(".app-main")!, {
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["class"]
|
||||
});
|
||||
});
|
||||
|
||||
const recordNavigation = page.locator(".app-bottom-nav").getByRole("button", { name: "录制", exact: true });
|
||||
const tapHighlight = await recordNavigation.evaluate((element) =>
|
||||
getComputedStyle(element).webkitTapHighlightColor
|
||||
);
|
||||
expect(tapHighlight).toBe("rgba(0, 0, 0, 0)");
|
||||
|
||||
await recordNavigation.click();
|
||||
await expect(page).toHaveURL(/\/live-rooms$/);
|
||||
await expect(recordNavigation).toHaveClass(/is-active/);
|
||||
await expect(page.locator(".app-bottom-nav button[aria-current='page']")).toHaveCount(1);
|
||||
await expect.poll(() => page.evaluate(() =>
|
||||
(window as Window & { __pageSwapObserved?: boolean }).__pageSwapObserved
|
||||
)).toBeTruthy();
|
||||
const activeIndicator = await recordNavigation.locator(".app-bottom-nav__icon").evaluate((element) => ({
|
||||
background: getComputedStyle(element).backgroundColor,
|
||||
duration: getComputedStyle(element).transitionDuration
|
||||
}));
|
||||
expect(activeIndicator.background).not.toBe("rgba(0, 0, 0, 0)");
|
||||
expect(activeIndicator.duration).not.toBe("0s");
|
||||
await capture(page, testInfo, "mobile-navigation-active");
|
||||
});
|
||||
|
||||
test("dark mode teleported overlays inherit dark surfaces and borders", async ({ page }) => {
|
||||
await page.addInitScript(() => localStorage.setItem("live-recorder-ui-theme", "dark"));
|
||||
await page.goto("/");
|
||||
await expect(page.locator("html")).toHaveAttribute("data-theme", "dark");
|
||||
|
||||
await page.locator(".app-user-btn").click();
|
||||
const dropdown = page.locator(".el-dropdown__popper:visible").first();
|
||||
await expect(dropdown).toBeVisible();
|
||||
const colors = await dropdown.evaluate((element) => ({
|
||||
background: getComputedStyle(element).backgroundColor,
|
||||
border: getComputedStyle(element).borderTopColor,
|
||||
rootBorder: getComputedStyle(document.documentElement).getPropertyValue("--el-border-color-light").trim()
|
||||
}));
|
||||
expect(colors.background).toBe("rgb(23, 34, 53)");
|
||||
expect(colors.border).toBe("rgb(44, 60, 85)");
|
||||
expect(colors.rootBorder).toBe("#2c3c55");
|
||||
});
|
||||
|
||||
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) {
|
||||
if ((page.viewportSize()?.width ?? 0) > 768) {
|
||||
const actionHeader = page.getByRole("columnheader", { name: "操作" }).last();
|
||||
await expect(actionHeader).toBeVisible();
|
||||
const box = await actionHeader.boundingBox();
|
||||
@@ -228,10 +380,38 @@ test("live room table, drawer and dialog retain their final actions", async ({ p
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: /^查看/ }).first().click();
|
||||
const drawer = page.locator(".right-drawer");
|
||||
const drawerFooter = page.locator(".right-drawer__footer");
|
||||
await expect(drawerFooter).toBeVisible();
|
||||
if ((page.viewportSize()?.width ?? 0) <= 768) {
|
||||
const viewportWidth = page.viewportSize()?.width ?? 0;
|
||||
await expect.poll(async () => {
|
||||
const box = await drawer.boundingBox();
|
||||
return Boolean(box && box.x >= -1 && box.x + box.width <= viewportWidth + 1);
|
||||
}).toBe(true);
|
||||
if (viewportWidth <= 640) {
|
||||
const box = await drawer.boundingBox();
|
||||
expect(Math.round(box?.width ?? 0)).toBe(viewportWidth);
|
||||
}
|
||||
const drawerBody = page.locator(".right-drawer__body");
|
||||
const bodyOverflow = await drawerBody.evaluate((element) => ({
|
||||
clientWidth: element.clientWidth,
|
||||
scrollWidth: element.scrollWidth,
|
||||
overflowY: getComputedStyle(element).overflowY
|
||||
}));
|
||||
expect(bodyOverflow.scrollWidth).toBeLessThanOrEqual(bodyOverflow.clientWidth);
|
||||
expect(bodyOverflow.overflowY).toBe("auto");
|
||||
await drawerBody.evaluate((element) => { element.scrollTop = element.scrollHeight; });
|
||||
const lastDetailRow = page.locator(".detail-panel__descriptions tr").last();
|
||||
await expect(lastDetailRow).toBeVisible();
|
||||
const lastRowBox = await lastDetailRow.boundingBox();
|
||||
const fixedFooterBox = await drawerFooter.boundingBox();
|
||||
expect(lastRowBox && fixedFooterBox && lastRowBox.y + lastRowBox.height)
|
||||
.toBeLessThanOrEqual((fixedFooterBox?.y ?? 0) + 1);
|
||||
}
|
||||
const footerBox = await drawerFooter.boundingBox();
|
||||
expect(footerBox && footerBox.y + footerBox.height).toBeLessThanOrEqual((page.viewportSize()?.height ?? 0) + 1);
|
||||
await capture(page, testInfo, "live-room-drawer");
|
||||
await page.getByRole("button", { name: "关闭", exact: true }).click();
|
||||
await expect(drawerFooter).toBeHidden();
|
||||
|
||||
@@ -246,7 +426,7 @@ test("live room table, drawer and dialog retain their final actions", async ({ p
|
||||
});
|
||||
|
||||
test("mobile live room list paginates large collections", async ({ page }) => {
|
||||
test.skip((page.viewportSize()?.width ?? 0) >= 768, "mobile-only pagination contract");
|
||||
test.skip((page.viewportSize()?.width ?? 0) > 768, "mobile-only pagination contract");
|
||||
const manyRooms = Array.from({ length: 25 }, (_, index) => ({
|
||||
...room,
|
||||
id: `room-${index + 1}`,
|
||||
@@ -269,6 +449,13 @@ test("mobile live room list paginates large collections", async ({ page }) => {
|
||||
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.getByText("主播 · 示例主播", { exact: true })).toBeVisible();
|
||||
if ((page.viewportSize()?.width ?? 0) <= 768) {
|
||||
const tapHighlight = await page.getByRole("button", { name: "刷新列表" }).evaluate((element) =>
|
||||
getComputedStyle(element).webkitTapHighlightColor
|
||||
);
|
||||
expect(tapHighlight).toBe("rgba(0, 0, 0, 0)");
|
||||
}
|
||||
await expect(page.getByPlaceholder(/搜索直播间/)).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /查看/ }).first()).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: /更多/ }).first()).toBeVisible();
|
||||
@@ -276,7 +463,7 @@ test("record task cards and tables expose one primary action", async ({ page },
|
||||
await capture(page, testInfo, "record-tasks");
|
||||
});
|
||||
|
||||
test("record sessions paginate on the server and bound the rendered page", async ({ page }) => {
|
||||
test("record sessions paginate on the server and bound the rendered page", async ({ page }, testInfo) => {
|
||||
const allSessions = Array.from({ length: 200 }, (_, index) => ({
|
||||
...session,
|
||||
id: `session-${index + 1}`,
|
||||
@@ -308,11 +495,11 @@ test("record sessions paginate on the server and bound the rendered page", async
|
||||
});
|
||||
|
||||
await page.goto("/record-tasks");
|
||||
const expectedPageSize = (page.viewportSize()?.width ?? 0) < 768 ? 12 : 20;
|
||||
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"))
|
||||
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);
|
||||
@@ -320,10 +507,24 @@ test("record sessions paginate on the server and bound the rendered page", async
|
||||
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();
|
||||
if ((page.viewportSize()?.width ?? 0) <= 768) {
|
||||
await expect(page.locator(".session-pagination__mobile .el-button")).toHaveCount(2);
|
||||
const pageCount = Math.ceil(allSessions.length / expectedPageSize);
|
||||
const middlePage = Math.ceil(pageCount / 2);
|
||||
for (let pageNumber = 2; pageNumber < middlePage; pageNumber++) {
|
||||
await page.locator(".session-pagination__mobile .btn-next").click();
|
||||
await expect(page.getByText(`分页录制会话 ${pageNumber * expectedPageSize + 1}`, { exact: true })).toBeVisible();
|
||||
}
|
||||
await expect(page.locator(".session-pagination__position")).toContainText(`${middlePage}/ ${pageCount}`);
|
||||
await expect(page.locator(".session-pagination__mobile .el-button")).toHaveCount(2);
|
||||
await expectNoDocumentOverflow(page);
|
||||
await page.locator(".session-pagination__mobile").scrollIntoViewIfNeeded();
|
||||
await capture(page, testInfo, "record-session-middle-page");
|
||||
}
|
||||
});
|
||||
|
||||
test("mobile record session refresh keeps the current page and scroll position", async ({ page }) => {
|
||||
test.skip((page.viewportSize()?.width ?? 0) >= 768, "mobile refresh contract");
|
||||
test.skip((page.viewportSize()?.width ?? 0) > 768, "mobile refresh contract");
|
||||
|
||||
await page.addInitScript(() => {
|
||||
class MockEventSource {
|
||||
@@ -428,8 +629,99 @@ test("upload tasks use cards on mobile and never require table horizontal scroll
|
||||
await capture(page, testInfo, "upload-tasks");
|
||||
});
|
||||
|
||||
test("paused uploads expose retry controls without overflowing the viewport", async ({ page }, testInfo) => {
|
||||
await page.route("**/api/record-tasks/upload-status**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [{
|
||||
...uploadTask,
|
||||
uploadStatus: 5,
|
||||
uploadAttemptCount: 3,
|
||||
uploadErrorMessage: "OpenList 登录请求过多,队列已全局暂停,当前任务次数不会继续消耗。"
|
||||
}],
|
||||
totalCount: 1,
|
||||
notUploadedCount: 0,
|
||||
failedArtifactCount: 0,
|
||||
succeededCount: 0,
|
||||
failedCount: 0,
|
||||
queuedCount: 0,
|
||||
uploadingCount: 0,
|
||||
waitingRetryCount: 1,
|
||||
matchingRetryableCount: 1,
|
||||
queueHealth: {
|
||||
state: "RateLimited",
|
||||
isPaused: true,
|
||||
reason: "OpenList 登录请求触发限流。队列暂停期间不会发起新的上传请求,也不会消耗单任务重试额度。",
|
||||
retryAt: "2026-08-02T12:15:00Z"
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto("/upload-tasks");
|
||||
await expect(page.getByText("OpenList 正在限流", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "验证并恢复队列" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "重试全部匹配" })).toBeVisible();
|
||||
const retryLabel = (page.viewportSize()?.width ?? 0) < 1280 ? "立即重试" : "重试";
|
||||
await expect(page.getByRole("button", { name: retryLabel, exact: true })).toBeVisible();
|
||||
await expectNoDocumentOverflow(page);
|
||||
await capture(page, testInfo, "upload-queue-paused");
|
||||
});
|
||||
|
||||
test("recording failure recovery adapts its actions and long paths to each viewport", async ({ page }, testInfo) => {
|
||||
await page.goto("/recovery");
|
||||
await expect(page.getByRole("heading", { name: "恢复中心" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "录制失败产物" })).toBeVisible();
|
||||
await expect(page.getByText(recordingFailure.filePath, { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "确认有效" })).toBeVisible();
|
||||
|
||||
if ((page.viewportSize()?.width ?? 0) <= 767) {
|
||||
await expect(page.locator(".failure-card")).toHaveCount(1);
|
||||
await expect(page.getByRole("columnheader", { name: "操作" })).toHaveCount(0);
|
||||
} else {
|
||||
await expect(page.locator(".failure-card")).toHaveCount(0);
|
||||
await expect(page.getByRole("columnheader", { name: "操作" })).toBeVisible();
|
||||
}
|
||||
|
||||
await expectNoDocumentOverflow(page);
|
||||
await capture(page, testInfo, "recording-failure-recovery");
|
||||
});
|
||||
|
||||
test("mobile session details keep segment actions visible and left aligned", async ({ page }, testInfo) => {
|
||||
test.skip((page.viewportSize()?.width ?? 0) > 768, "mobile session detail contract");
|
||||
|
||||
await page.goto("/record-tasks");
|
||||
const sessionCard = page.locator(".session-card").first();
|
||||
await expect(sessionCard).toBeVisible();
|
||||
await sessionCard.locator(":scope > .data-card__actions").getByRole("button", { name: "查看", exact: true }).click();
|
||||
|
||||
await expect(page.getByRole("heading", { name: "会话详情" })).toBeVisible();
|
||||
await expect(page.locator(".segment-card")).toHaveCount(3);
|
||||
await expect(page.getByRole("columnheader", { name: "操作" })).toHaveCount(0);
|
||||
|
||||
const firstCard = page.locator(".segment-card").first();
|
||||
const firstActions = firstCard.locator(".segment-card__actions");
|
||||
const cardBox = await firstCard.boundingBox();
|
||||
const actionsBox = await firstActions.boundingBox();
|
||||
expect(cardBox).not.toBeNull();
|
||||
expect(actionsBox).not.toBeNull();
|
||||
expect(actionsBox!.x - cardBox!.x).toBeLessThanOrEqual(20);
|
||||
|
||||
const lastActions = page.locator(".segment-card__actions").last();
|
||||
await lastActions.scrollIntoViewIfNeeded();
|
||||
await expect(lastActions.getByRole("button", { name: "查看分片" })).toBeVisible();
|
||||
const actionsBottom = await lastActions.boundingBox();
|
||||
const bottomNav = await page.locator(".app-bottom-nav").boundingBox();
|
||||
expect(actionsBottom).not.toBeNull();
|
||||
expect(bottomNav).not.toBeNull();
|
||||
expect(actionsBottom!.y + actionsBottom!.height).toBeLessThanOrEqual(bottomNav!.y + 1);
|
||||
await capture(page, testInfo, "mobile-session-detail");
|
||||
});
|
||||
|
||||
test("mobile upload polling keeps cards and scroll position while progress updates", async ({ page }) => {
|
||||
test.skip((page.viewportSize()?.width ?? 0) >= 768, "mobile polling contract");
|
||||
test.skip((page.viewportSize()?.width ?? 0) > 768, "mobile polling contract");
|
||||
|
||||
const allItems = Array.from({ length: 60 }, (_, index) => ({
|
||||
...uploadTask,
|
||||
@@ -466,7 +758,9 @@ test("mobile upload polling keeps cards and scroll position while progress updat
|
||||
failedCount: 0,
|
||||
queuedCount: 0,
|
||||
uploadingCount: allItems.length,
|
||||
waitingRetryCount: 0
|
||||
waitingRetryCount: 0,
|
||||
matchingRetryableCount: 0,
|
||||
queueHealth: { state: "Healthy", isPaused: false }
|
||||
})
|
||||
});
|
||||
inFlight--;
|
||||
|
||||
Reference in New Issue
Block a user