diff --git a/server/app/shows/[show_id]/rundown/[rundown_id]/RundownAssets.tsx b/server/app/shows/[show_id]/rundown/[rundown_id]/RundownAssets.tsx index 92a1d494b..f44a63458 100644 --- a/server/app/shows/[show_id]/rundown/[rundown_id]/RundownAssets.tsx +++ b/server/app/shows/[show_id]/rundown/[rundown_id]/RundownAssets.tsx @@ -2,6 +2,7 @@ import { Asset, Rundown } from "@badger/prisma/types"; import React, { ReactNode, useMemo, useState, useTransition } from "react"; +import Link from "next/link"; import classNames from "classnames"; import Button from "@badger/components/button"; import { @@ -71,8 +72,17 @@ function AssetsCategory({ const [isUploadOpen, setIsUploadOpen] = useState(false); return ( -
-
setExpanded((v) => !v)} role="button"> +
+
setExpanded((v) => !v)} + role="button" + data-testid={`asset-category-header-${category}`} + aria-label={`Expand ${category}`} + aria-expanded={isExpanded} + > {isExpanded ? ( ) : ( @@ -81,7 +91,10 @@ function AssetsCategory({

{category}

{isExpanded && ( -
+
{assets .sort((a, b) => a.order - b.order) .map((asset) => { @@ -121,6 +134,7 @@ function AssetsCategory({ return (
+ {(asset.media.state === MediaState.Ready || + asset.media.state === MediaState.ReadyWithWarnings) && ( + + + + )}
); })} diff --git a/server/e2e/rundownAssetDownload.spec.ts b/server/e2e/rundownAssetDownload.spec.ts new file mode 100644 index 000000000..82e5974ba --- /dev/null +++ b/server/e2e/rundownAssetDownload.spec.ts @@ -0,0 +1,104 @@ +import { readFileSync } from "fs"; +import * as path from "node:path"; +import { test, expect, fileToDataTransfer } from "./lib"; + +test("download asset from rundown", async ({ showPage }) => { + const testFileName = "download_test_file.txt"; + const testFileContent = readFileSync( + path.join(__dirname, "testdata", testFileName), + ); + + // Navigate to the show page (handled by showPage fixture) + + // Create a new rundown + await showPage.getByRole("button", { name: "New Rundown" }).click(); + await expect(showPage.getByTestId("name-rundown")).toBeVisible(); + await showPage.getByTestId("name-rundown").fill("Download Test Rundown"); + await showPage.getByTestId("create-rundown").click(); + await expect(showPage.getByLabel("Name")).toHaveValue(""); // Wait for form to clear + await showPage.locator("body").press("Escape"); // Close the creation form + + // Navigate into the rundown + await showPage.getByRole("link", { name: "Edit Rundown" }).click(); + await showPage.waitForURL(/.*\/shows\/.*\/rundown\/.*/); + + // Create a new asset category + const categoryName = "Test Assets For Download"; + await showPage.getByRole("button", { name: "New Category" }).click(); + await showPage.getByPlaceholder("Stills").fill(categoryName); + await showPage.getByRole("button", { name: "Create" }).click(); + // Click the category header to expand it + const categoryHeaderButton = showPage.getByRole("button", { + name: categoryName, + }); + await categoryHeaderButton.click(); + + await showPage + .getByRole("button", { + name: "Upload new asset", + }) + .click(); + await showPage.getByText("Upload file").click(); + + const uploadPromise = showPage.waitForResponse(async (response) => { + // tusd returns 201 or 204 on successful chunk/final upload + return ( + response.url().startsWith("http://localhost:1080/files") && + (response.status() === 201 || response.status() === 204) + ); + }); + + await showPage + .getByText("Drop files here, or click to select") + .dispatchEvent("drop", { + dataTransfer: await fileToDataTransfer( + showPage, + testFileContent, + testFileName, + "text/plain", + ), + }); + + await uploadPromise; // upload complete + + // The category gets collapsed when the upload completes + await expect( + showPage.getByRole("button", { + expanded: false, + name: `Expand ${categoryName}`, + }), + ).toBeVisible(); + + // After upload, the category is collapsed. Locate its header and click to expand. + const categoryHeader = showPage.getByTestId( + "asset-category-header-" + categoryName, + ); + await categoryHeader.click({ timeout: 10_000 }); + + // Now that it's clicked, the content area should eventually become visible. + const categoryContent = showPage.getByTestId( + "asset-category-content-" + categoryName, + ); + + // Now, locate the asset item within this visible content area. + // The expect(...).toBeVisible() on assetItemLocator will also wait for categoryContent to be visible. + // The asset row itself doesn't have a data-testid, so we find it by its text content. + const assetItemLocator = categoryContent + .locator('div:has-text("' + testFileName + '")') + .first(); + await expect(assetItemLocator).toBeVisible({ timeout: 30_000 }); // Allow time for asset processing + + // Then, locate and check the download button within the visible asset item. + const downloadButton = assetItemLocator.locator( + '[data-testid^="RundownAssets.downloadButton."]', + ); + await expect(downloadButton).toBeVisible({ timeout: 15000 }); // Allow time for button to appear (media state ready) + + // Start waiting for the download event BEFORE clicking the button + const downloadPromise = showPage.waitForEvent("download"); + await downloadButton.click(); + const download = await downloadPromise; + + // Assert the downloaded file name + expect(download.suggestedFilename()).toBe(`1 - ${testFileName}`); +}); diff --git a/server/e2e/showItems.spec.ts b/server/e2e/showItems.spec.ts index d0098d4f1..6f14e732f 100644 --- a/server/e2e/showItems.spec.ts +++ b/server/e2e/showItems.spec.ts @@ -321,9 +321,46 @@ test("asset upload failure (BDGR-54)", async ({ showPage }) => { }); await req; - await showPage.getByRole("button", { name: "Expand Test Assets" }).click(); - await expect(showPage.getByTestId("RundownAssets.loadFailed")).toBeVisible({ - timeout: 30_000, + async function ensureCategoryIsExpanded( + page: typeof showPage, + categoryTestIdSuffix: string, + timeout: number = 5000, + ) { + const categoryHeader = page.getByTestId( + `asset-category-header-${categoryTestIdSuffix}`, + ); + const categoryContent = page.getByTestId( + `asset-category-content-${categoryTestIdSuffix}`, + ); + + const isExpanded = await categoryHeader.getAttribute("aria-expanded"); + + if (isExpanded !== "true") { + await categoryHeader.click(); + await expect(categoryHeader).toHaveAttribute("aria-expanded", "true", { + timeout, + }); + } + // After ensuring it's expanded (or was already), check content visibility + await expect(categoryContent).toBeVisible({ timeout }); + return categoryContent; // Return for chaining locators + } + + // Use expect().toPass() for robustly checking the icon, re-expanding category if needed. + await expect(async () => { + const categoryContent = await ensureCategoryIsExpanded( + showPage, + "Test Assets", + 5000, + ); + + const failedAssetIcon = categoryContent + .getByTestId("asset-row-__FAIL__smpte_bars_15s.mp4") + .getByTestId("RundownAssets.loadFailed"); + + await expect(failedAssetIcon).toBeVisible({ timeout: 1000 }); + }).toPass({ + timeout: 30_000, // Overall timeout for the entire block to succeed }); }); diff --git a/server/e2e/testdata/download_test_file.txt b/server/e2e/testdata/download_test_file.txt new file mode 100644 index 000000000..f05587fdc --- /dev/null +++ b/server/e2e/testdata/download_test_file.txt @@ -0,0 +1 @@ +This is a test file for download functionality. diff --git a/yarn.lock b/yarn.lock index 76ec0119a..ecb50f6a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10466,14 +10466,14 @@ __metadata: linkType: hard "import-in-the-middle@npm:^1.13.1, import-in-the-middle@npm:^1.8.1": - version: 1.14.2 - resolution: "import-in-the-middle@npm:1.14.2" + version: 1.14.0 + resolution: "import-in-the-middle@npm:1.14.0" dependencies: acorn: "npm:^8.14.0" acorn-import-attributes: "npm:^1.9.5" cjs-module-lexer: "npm:^1.2.2" module-details-from-path: "npm:^1.0.3" - checksum: 10/45934b366d7f344e1cbfb6141ed93d3c2ced7021d2dd49b0e2474bab4f571e11f7f377c0f510f03e2d4ba61074d64b9f04677497d3f14106c8cc6f44c749f068 + checksum: 10/c0b73a5637c0c7ec0ab19d5687a571dc864d90e0603dd45e28939624707def244a5dae402d7dccc1f5fc5b54ffcf18313a071d13048c390a495696d6b9f290fc languageName: node linkType: hard