mirror of
https://github.com/cstaelen/tidarr.git
synced 2026-10-22 11:02:39 -05:00
527 lines
15 KiB
TypeScript
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());
|
|
});
|