Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions server/app/shows/[show_id]/rundown/[rundown_id]/RundownAssets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -71,8 +72,17 @@ function AssetsCategory({
const [isUploadOpen, setIsUploadOpen] = useState(false);

return (
<div className="shadow-md rounded-md bg-light p-2 mb-4">
<div onClick={() => setExpanded((v) => !v)} role="button">
<div
className="shadow-md rounded-md bg-light p-2 mb-4"
data-testid={`asset-category-block-${category}`}
>
<div
onClick={() => setExpanded((v) => !v)}
role="button"
data-testid={`asset-category-header-${category}`}
aria-label={`Expand ${category}`}
aria-expanded={isExpanded}
>
{isExpanded ? (
<IoChevronDown className="inline-block" aria-label="Collapse" />
) : (
Expand All @@ -81,7 +91,10 @@ function AssetsCategory({
<h3 className="inline font-lg font-bold">{category}</h3>
</div>
{isExpanded && (
<div className="flex flex-col">
<div
className="flex flex-col"
data-testid={`asset-category-content-${category}`}
>
{assets
.sort((a, b) => a.order - b.order)
.map((asset) => {
Expand Down Expand Up @@ -121,6 +134,7 @@ function AssetsCategory({
return (
<div
key={asset.id}
data-testid={`asset-row-${asset.name}`}
className={classNames(
"flex flex-row items-center space-x-2",
asset.media.state === "Pending" && "text-mid-dark",
Expand Down Expand Up @@ -157,6 +171,19 @@ function AssetsCategory({
</PopoverContent>
</Popover>
</div>
{(asset.media.state === MediaState.Ready ||
asset.media.state === MediaState.ReadyWithWarnings) && (
<Link
href={`/media/download/${asset.media.id}`}
target="_blank"
data-testid={`RundownAssets.downloadButton.${asset.id}`}
className="ml-2"
>
<Button color="ghost" size="small">
Download
</Button>
</Link>
)}
</div>
);
})}
Expand Down
104 changes: 104 additions & 0 deletions server/e2e/rundownAssetDownload.spec.ts
Original file line number Diff line number Diff line change
@@ -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}`);
});
43 changes: 40 additions & 3 deletions server/e2e/showItems.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
});

Expand Down
1 change: 1 addition & 0 deletions server/e2e/testdata/download_test_file.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a test file for download functionality.
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading