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}
+
{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