test(e2e): smoke the real upload -> publish -> feed -> delete pipeline

Existing upload/feed specs seed events straight into the relay, bypassing the
app's own publish path. This spec drives the actual UI: open the upload modal,
switch to External Link mode, fill title + URL, submit -> the app signs and
publishes to the mock relay -> the video appears in the feed; then the owner
deletes it via the gear menu and it leaves the feed.

Closes the launch-readiness coverage gap on the publish lifecycle (the S3/R2
file upload + cleanup are covered by the unit suites). Verified passing on
chromium (2/2).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-17 05:56:38 -04:00
1 parent 7f5dcd7e50
commit a7803c810b
1 file changed
+125
@@ -0,0 +1,125 @@
/**
* Launch-readiness smoke for the REAL publish pipeline.
*
* Every other upload/feed spec seeds events straight into the relay, which
* bypasses the app's own publish path. This spec drives the actual UI:
* open upload modal -> fill title + URL -> submit -> the app signs & publishes
* to the (mock) relay -> the video shows up in the feed -> the owner deletes it.
*
* Scenarios:
* - SCN-publish-url-first: a URL-first upload submitted through the form is
* signed, published to the relay, and appears in the feed with its URL.
* - SCN-publish-then-delete: the owner can delete a self-published video and it
* leaves the feed.
*
* This covers the upload->publish->feed->delete orchestration end to end. The
* S3/R2 *file* upload (multipart, key derivation, cleanup) is exercised by the
* unit suites; this smoke validates the note lifecycle that ties it together.
*/
import { test, expect } from "./helpers/bitvidTestFixture";
async function openUploadAndPublish(
page: any,
{ title, url }: { title: string; url: string },
) {
const opened = await page.evaluate(() =>
(window as any).__bitvidTest__.openUploadModal(),
);
expect(opened?.ok, `openUploadModal: ${JSON.stringify(opened)}`).toBe(true);
const modal = page.locator('[data-testid="upload-modal"]');
await expect(modal).toBeVisible({ timeout: 10000 });
// The modal defaults to "Upload File" (S3) mode; switch to "External Link" so
// the URL field is shown (URL-first publish, no storage needed).
await page.locator("#btn-mode-external").click();
const urlInput = page.locator('[data-testid="upload-url"]');
await expect(urlInput).toBeVisible({ timeout: 10000 });
await page.locator('[data-testid="upload-title"]').fill(title);
await urlInput.fill(url);
await page.locator('[data-testid="upload-submit"]').click();
// The publish path closes the modal on success.
await expect(modal).not.toBeVisible({ timeout: 60000 });
}
test.describe("Upload → publish → feed lifecycle (real pipeline)", () => {
test("a URL-first upload submitted through the form publishes and appears in the feed", async ({
page,
gotoApp,
loginAs,
}) => {
await gotoApp();
await loginAs(page);
const title = `Smoke Publish ${Date.now()}`;
const url = "https://example.com/smoke-publish.mp4";
await openUploadAndPublish(page, { title, url });
// The app signs the note, publishes to the mock relay, and the feed updates.
const item = await page.evaluate(async (t: string) => {
const harness = (window as any).__bitvidTest__;
const deadline = Date.now() + 60000;
while (Date.now() < deadline) {
const items = harness.getFeedItems();
const found = items.find((i: any) => i.title === t);
if (found) return found;
await new Promise((r) => setTimeout(r, 500));
}
return null;
}, title);
expect(item, "published video should appear in the feed").not.toBeNull();
expect(item.hasUrl).toBe(true);
});
test("the owner can delete a self-published video and it leaves the feed", async ({
page,
gotoApp,
loginAs,
}) => {
await gotoApp();
await loginAs(page);
const title = `Smoke Delete ${Date.now()}`;
await openUploadAndPublish(page, {
title,
url: "https://example.com/smoke-delete.mp4",
});
// Wait for the published card to render.
const card = page
.locator("[data-video-card]")
.filter({ has: page.locator(`[data-video-title="${title}"]`) })
.first();
await expect(card).toBeVisible({ timeout: 60000 });
// Owner gear menu -> delete -> confirm.
await card.locator('[aria-label="Video settings"]').click();
const settingsPanel = page.locator('[data-menu="video-settings"]');
await expect(settingsPanel).toBeVisible({ timeout: 60000 });
await settingsPanel.locator('[data-action="delete"]').click();
const deleteModal = page.locator("#deleteVideoModal");
await expect(deleteModal).not.toHaveClass(/hidden/, { timeout: 60000 });
await page.locator("#confirmDeleteVideo").click();
await expect(deleteModal).toHaveClass(/hidden/, { timeout: 60000 });
// The video should no longer be in the feed.
const stillThere = await page.evaluate(async (t: string) => {
const harness = (window as any).__bitvidTest__;
const deadline = Date.now() + 30000;
while (Date.now() < deadline) {
const items = harness.getFeedItems();
if (!items.some((i: any) => i.title === t)) return false;
await new Promise((r) => setTimeout(r, 500));
}
return true;
}, title);
expect(stillThere, "deleted video should leave the feed").toBe(false);
});
});