Files
tidarr/e2e/tests/queue.spec.ts
cl.st 8755db625d Feat/retry post processing (#899)
* feat(front): add retry post processing button

* fix test
2026-09-19 17:42:00 +02:00

527 lines
15 KiB
TypeScript

import { expect } from "@playwright/test";
import { test } from "../test-isolation";
import { mockItemOutputSSE, mockSSEPayload } from "./utils/mock";
import { runSearch } from "./utils/search";
test("Queue: Should be able to pause-resume the queue", async ({ page }) => {
// Track API calls
let resumeCalled = false;
let pauseCalled = false;
await page.route("**/queue/resume", async (route) => {
resumeCalled = true;
route.continue();
});
await page.route("**/queue/pause", async (route) => {
pauseCalled = true;
route.continue();
});
await mockItemOutputSSE(page, "high");
await page.goto("/");
// Add an item to the queue
await runSearch("Nirvana", page);
await page.getByRole("tab", { name: "Albums" }).first().click();
await page.getByRole("button", { name: "Get album" }).nth(2).click();
// Open processing list
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
await page.locator("button.MuiFab-circular").click();
// Wait for pause button to appear (it should be in "resume" state - showing PlayArrow icon)
await page.waitForTimeout(500);
// Find and click the resume button (when paused, the button shows PlayArrow)
const pauseButton = page.getByRole("button", { name: "Pause" });
await expect(pauseButton).toBeVisible();
await pauseButton.click();
// Verify the resume API was called
await page.waitForTimeout(500);
expect(pauseCalled).toBe(true);
const resumeButton = page.getByRole("button", { name: "Resume" });
await expect(resumeButton).toBeVisible();
await resumeButton.click();
// Verify the resume API was called
await page.waitForTimeout(500);
expect(resumeCalled).toBe(true);
});
test("Queue: Should load queue status from SSE on mount", async ({ page }) => {
const mockData = [
{
id: "1",
title: "Nevermind",
artist: "Nirvana",
type: "album",
quality: "high",
status: "queue_download",
loading: false,
},
];
// isPaused comes from SSE payload, not from a separate REST call
await page.route("**/stream-processing", async (route) => {
await route.fulfill({
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
body: `data: ${mockSSEPayload(mockData)}\n\n`,
});
});
await page.goto("/");
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
await page.locator("button.MuiFab-circular").click();
await page.waitForSelector('[aria-label="Processing table"]', {
state: "visible",
timeout: 5000,
});
// Queue is loaded and not paused — pause button visible
await expect(page.getByRole("button", { name: "Pause" })).toBeVisible();
await page.route("**/stream-processing", (route) => route.continue());
});
test("Queue: Should hide finished items by default and toggle visibility", async ({
page,
}) => {
const mockData = [
{
id: "1",
title: "In Utero",
artist: "Nirvana",
type: "album",
quality: "high",
status: "finished",
loading: false,
},
{
id: "2",
title: "Nevermind",
artist: "Nirvana",
type: "album",
quality: "high",
status: "queue_download",
loading: false,
},
];
await page.route("**/stream-processing", async (route) => {
await route.fulfill({
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
body: `data: ${mockSSEPayload(mockData)}\n\n`,
});
});
await page.goto("/");
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
await page.locator("button.MuiFab-circular").click();
await page.waitForSelector('[aria-label="Processing table"]', {
state: "visible",
timeout: 5000,
});
// Finished item hidden by default (not in finished table, which is not rendered)
await expect(page.getByLabel("Processing table")).not.toContainText(
"In Utero",
);
await expect(page.getByLabel("Finished table")).not.toBeVisible();
// Non-finished item visible in processing table
await expect(page.getByLabel("Processing table")).toContainText("Nevermind");
// Toggle button shows count
const showButton = page.getByRole("button", { name: "Show finished (1)" });
await expect(showButton).toBeVisible();
// Clear finished button is also visible
await expect(
page.getByRole("button", { name: "Clear finished" }),
).toBeVisible();
await showButton.click();
// Finished table now visible with item
await expect(page.getByLabel("Finished table")).toBeVisible();
await expect(page.getByLabel("Finished table")).toContainText("In Utero");
// Button label changed
await expect(
page.getByRole("button", { name: "Hide finished" }),
).toBeVisible();
// Toggle back
await page.getByRole("button", { name: "Hide finished" }).click();
await expect(page.getByLabel("Finished table")).not.toBeVisible();
// Clean up
await page.route("**/stream-processing", (route) => route.continue());
});
test("Queue: Should display warning color when paused", async ({ page }) => {
const mockData = [
{
id: "1",
title: "Nevermind",
artist: "Nirvana",
type: "album",
quality: "high",
status: "queue_download",
loading: false,
},
];
// isPaused comes from SSE payload
await page.route("**/stream-processing", async (route) => {
await route.fulfill({
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
body: `data: ${JSON.stringify({ items: mockData, isPaused: true, batchCount: 0, batchResumeAt: null })}\n\n`,
});
});
await page.goto("/");
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
// Check if the FAB has warning color (orange/yellow when paused)
const fab = page.locator("button.MuiFab-circular");
await expect(fab).toHaveClass(/MuiFab-warning/);
await page.route("**/stream-processing", (route) => route.continue());
});
test("Queue: Should paginate with show more button when list exceeds 50 items", async ({
page,
}) => {
const mockData = Array.from({ length: 60 }, (_, i) => ({
id: String(i + 1),
title: `Album ${i + 1}`,
artist: "Artist",
type: "album",
quality: "high",
status: "queue_download",
loading: false,
}));
await page.route("**/stream-processing", async (route) => {
await route.fulfill({
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
body: `data: ${mockSSEPayload(mockData)}\n\n`,
});
});
await page.goto("/");
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
await page.locator("button.MuiFab-circular").click();
await page.waitForSelector('[aria-label="Processing table"]', {
state: "visible",
timeout: 5000,
});
// Only first 50 items visible
await expect(page.getByLabel("Processing table")).toContainText("Album 1");
await expect(page.getByLabel("Processing table")).toContainText("Album 50");
await expect(page.getByLabel("Processing table")).not.toContainText(
"Album 51",
);
// Show more button visible with correct count
const showMoreButton = page.getByRole("button", {
name: "Show more (10 remaining)",
});
await expect(showMoreButton).toBeVisible();
// Click show more reveals remaining items
await showMoreButton.click();
await expect(page.getByLabel("Processing table")).toContainText("Album 51");
await expect(showMoreButton).not.toBeVisible();
await page.route("**/stream-processing", (route) => route.continue());
});
test("Queue: Should filter items by keyword on title and artist", async ({
page,
}) => {
const mockData = [
{
id: "1",
title: "Nevermind",
artist: "Nirvana",
type: "album",
quality: "high",
status: "queue_download",
loading: false,
},
{
id: "2",
title: "In Utero",
artist: "Nirvana",
type: "album",
quality: "high",
status: "queue_download",
loading: false,
},
{
id: "3",
title: "OK Computer",
artist: "Radiohead",
type: "album",
quality: "high",
status: "queue_download",
loading: false,
},
];
await page.route("**/stream-processing", async (route) => {
await route.fulfill({
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
body: `data: ${mockSSEPayload(mockData)}\n\n`,
});
});
await page.goto("/");
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
await page.locator("button.MuiFab-circular").click();
await page.waitForSelector('[aria-label="Processing table"]', {
state: "visible",
timeout: 5000,
});
// All items visible initially
await expect(page.getByLabel("Processing table")).toContainText("Nevermind");
await expect(page.getByLabel("Processing table")).toContainText("In Utero");
await expect(page.getByLabel("Processing table")).toContainText(
"OK Computer",
);
// Filter by title
await page.getByPlaceholder("Filter by title or artist…").fill("Nevermind");
await expect(page.getByLabel("Processing table")).toContainText("Nevermind");
await expect(page.getByLabel("Processing table")).not.toContainText(
"In Utero",
);
await expect(page.getByLabel("Processing table")).not.toContainText(
"OK Computer",
);
// Filter by artist
await page.getByPlaceholder("Filter by title or artist…").fill("Nirvana");
await expect(page.getByLabel("Processing table")).toContainText("Nevermind");
await expect(page.getByLabel("Processing table")).toContainText("In Utero");
await expect(page.getByLabel("Processing table")).not.toContainText(
"OK Computer",
);
// Clear filter restores all items
await page.getByPlaceholder("Filter by title or artist…").fill("");
await expect(page.getByLabel("Processing table")).toContainText(
"OK Computer",
);
await page.route("**/stream-processing", (route) => route.continue());
});
test("Queue: Should retry post-processing without re-downloading for error items", async ({
page,
}) => {
let retryPostProcessingCalled = false;
let retryPostProcessingBody: { id: string } | undefined;
await page.route("**/retry-post-processing", async (route) => {
retryPostProcessingCalled = true;
retryPostProcessingBody = route.request().postDataJSON() as {
id: string;
};
await route.fulfill({ status: 204 });
});
const mockData = [
{
id: "1",
title: "In Utero",
artist: "Nirvana",
type: "album",
quality: "high",
status: "error",
errorStage: "post_processing",
loading: false,
},
];
await page.route("**/stream-processing", async (route) => {
await route.fulfill({
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
body: `data: ${mockSSEPayload(mockData)}\n\n`,
});
});
await page.goto("/");
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
await page.locator("button.MuiFab-circular").click();
await page.waitForSelector('[aria-label="Processing table"]', {
state: "visible",
timeout: 5000,
});
// Post-processing errors only show the post-processing retry button
const retryMoveButton = page.getByTestId("btn-retry-post-processing");
await expect(retryMoveButton).toBeVisible();
await expect(retryMoveButton).toHaveText("Retry");
await retryMoveButton.click();
await page.waitForTimeout(300);
expect(retryPostProcessingCalled).toBe(true);
expect(retryPostProcessingBody?.id).toBe("1");
await page.route("**/stream-processing", (route) => route.continue());
});
test("Queue: Should only show the download retry button for download errors", async ({
page,
}) => {
const mockData = [
{
id: "1",
title: "In Utero",
artist: "Nirvana",
type: "album",
quality: "high",
status: "error",
errorStage: "download",
loading: false,
},
];
await page.route("**/stream-processing", async (route) => {
await route.fulfill({
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
body: `data: ${mockSSEPayload(mockData)}\n\n`,
});
});
await page.goto("/");
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
await page.locator("button.MuiFab-circular").click();
await page.waitForSelector('[aria-label="Processing table"]', {
state: "visible",
timeout: 5000,
});
await expect(
page.getByRole("button", { name: "Retry download" }),
).toBeVisible();
await expect(page.getByTestId("btn-retry-post-processing")).not.toBeVisible();
await page.route("**/stream-processing", (route) => route.continue());
});
test("Queue: Terminal dialog should show full-label retry buttons for error items", async ({
page,
}) => {
let retryPostProcessingCalled = false;
await page.route("**/retry-post-processing", async (route) => {
retryPostProcessingCalled = true;
await route.fulfill({ status: 204 });
});
const mockData = [
{
id: "1",
title: "In Utero",
artist: "Nirvana",
type: "album",
quality: "high",
status: "error",
errorStage: "post_processing",
loading: false,
},
];
await page.route("**/stream-processing", async (route) => {
await route.fulfill({
status: 200,
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
},
body: `data: ${mockSSEPayload(mockData)}\n\n`,
});
});
await page.goto("/");
await expect(page.locator("button.MuiFab-circular")).toBeVisible();
await page.locator("button.MuiFab-circular").click();
await page.waitForSelector('[aria-label="Processing table"]', {
state: "visible",
timeout: 5000,
});
await page.getByTestId("btn-console").click();
// Full labels, no reliance on tooltips
const removeButton = page.getByRole("button", { name: "Remove" });
await expect(removeButton).toBeVisible();
const retryDownloadButton = page.getByRole("button", {
name: "Retry download",
});
await expect(retryDownloadButton).toBeVisible();
const retryPostProcessingButton = page.getByTestId(
"btn-dialog-retry-post-processing",
);
await expect(retryPostProcessingButton).toHaveText("Retry post processing");
await retryPostProcessingButton.click();
await page.waitForTimeout(300);
expect(retryPostProcessingCalled).toBe(true);
await page.route("**/stream-processing", (route) => route.continue());
});