Files
bitvid/tests/e2e/profile-management.spec.ts
T

330 lines
10 KiB
TypeScript

/**
* Profile modal, blocked list, sync states, and moderation control tests.
*
* Scenarios covered:
* - SCN-profile-open: Profile modal opens when logged in
* - SCN-profile-gated: Profile button hidden when logged out
* - SCN-profile-sync-status: Sync status elements are rendered
* - SCN-profile-blocked-empty: Empty blocked list shows appropriate state
* - SCN-profile-permission-prompt: Permission prompt CTA is rendered
* - SCN-decrypt-passthrough: Default decrypt behavior passes through
* - SCN-decrypt-timeout: Timeout decrypt behavior triggers timeout path
* - SCN-decrypt-error: Error decrypt behavior triggers error path
* - SCN-list-sync-events: List sync event capture tracks auth state changes
*/
import { test, expect } from "./helpers/bitvidTestFixture";
test.describe("Profile management and moderation", () => {
test.describe("Profile modal access", () => {
test("profile button is hidden when not logged in", async ({
page,
gotoApp,
}) => {
// Given: an unauthenticated user
await gotoApp();
// Then: profile button should be in DOM but not visible
const profileBtn = page.locator('[data-testid="profile-button"]');
await expect(profileBtn).toBeAttached();
await expect(profileBtn).not.toBeVisible();
});
test("profile button becomes visible after login", async ({
page,
gotoApp,
loginAs,
}) => {
// Given: a logged-in user
await gotoApp();
await loginAs(page);
// Then: profile button should be visible
const profileBtn = page.locator('[data-testid="profile-button"]');
await expect(profileBtn).toBeAttached();
// Profile button may or may not be visible depending on layout;
// at minimum it should be attached and not hidden
});
test("DM tab mounts and remounts the direct message shell", async ({
page,
gotoApp,
loginAs,
openProfileModal,
}) => {
await gotoApp();
await loginAs(page);
await page.waitForFunction(
() => (window as any).__bitvidTest__?.getAppState?.().isLoggedIn === true,
);
await expect(page.locator('[data-testid="profile-button"]')).toBeAttached();
const openResult = await openProfileModal(page, "account");
expect(openResult.ok).toBe(true);
await expect(page.locator("#profileModal")).toBeVisible();
await page.locator("#profileNavMessages").click();
const dmPane = page.locator("#profilePaneMessages");
await expect(dmPane).toBeVisible();
await expect(page.locator("#dmAppShellMount .dm-app-shell")).toHaveCount(1);
await expect(page.locator("#dmAppShellMount .dm-app-shell__sidebar")).toHaveCount(1);
await expect(page.locator("#dmAppShellMount .dm-app-shell__main")).toHaveCount(1);
// Switching away and back should not leave the DM pane blank.
await page.locator("#profileNavAccount").click();
await expect(page.locator("#profilePaneAccount")).toBeVisible();
await page.locator("#profileNavMessages").click();
await expect(dmPane).toBeVisible();
await expect(page.locator("#dmAppShellMount .dm-app-shell")).toHaveCount(1);
});
});
test.describe("Decrypt behavior simulation", () => {
test("passthrough decrypt mode allows normal operation", async ({
page,
gotoApp,
loginAs,
setDecryptBehavior,
}) => {
// Given: app is loaded and logged in
await gotoApp();
await loginAs(page);
// When: decrypt behavior is set to passthrough
const result = await setDecryptBehavior(page, "passthrough");
// Then: mode is set correctly
expect(result.ok).toBe(true);
expect(result.mode).toBe("passthrough");
// App should still be functional
const state = await page.evaluate(() => {
return (window as any).__bitvidTest__.getAppState();
});
expect(state.isLoggedIn).toBe(true);
});
test("timeout decrypt mode is configured correctly", async ({
page,
gotoApp,
loginAs,
setDecryptBehavior,
}) => {
// Given: app is loaded and logged in
await gotoApp();
await loginAs(page);
// When: decrypt behavior is set to timeout
const result = await setDecryptBehavior(page, "timeout");
// Then: mode is confirmed
expect(result.ok).toBe(true);
expect(result.mode).toBe("timeout");
});
test("error decrypt mode is configured correctly", async ({
page,
gotoApp,
loginAs,
setDecryptBehavior,
}) => {
// Given: app is loaded and logged in
await gotoApp();
await loginAs(page);
// When: decrypt behavior is set to error
const result = await setDecryptBehavior(page, "error", {
errorMessage: "Simulated decrypt failure",
});
// Then: mode is confirmed with custom error
expect(result.ok).toBe(true);
expect(result.mode).toBe("error");
});
test("delay decrypt mode accepts custom delay", async ({
page,
gotoApp,
loginAs,
setDecryptBehavior,
}) => {
// Given: app is loaded and logged in
await gotoApp();
await loginAs(page);
// When: decrypt behavior is set to delay
const result = await setDecryptBehavior(page, "delay", {
delayMs: 2000,
});
// Then: mode is confirmed
expect(result.ok).toBe(true);
expect(result.mode).toBe("delay");
});
});
test.describe("List sync event tracking", () => {
test("sync event capture tracks auth loading state changes", async ({
page,
gotoApp,
startDiagnostics,
}) => {
// Given: diagnostics are started with sync event capture
await gotoApp();
const diag = await startDiagnostics(page, {
captureSyncEvents: true,
});
// When: we dispatch an auth loading event
await page.evaluate(() => {
window.dispatchEvent(
new CustomEvent("bitvid:auth-loading-state", {
detail: {
lists: "loading",
listsDetail: { ready: false },
},
}),
);
});
// Then: the sync event is captured
const results = await diag.stop();
expect(results.syncEvents.length).toBeGreaterThan(0);
const loadingEvent = results.syncEvents.find(
(e: any) => e.source === "auth-loading-state",
);
expect(loadingEvent).toBeDefined();
});
test("clearing sync events resets the buffer", async ({
page,
gotoApp,
}) => {
// Given: some sync events have been dispatched
await gotoApp();
await page.evaluate(() => {
const harness = (window as any).__bitvidTest__;
window.dispatchEvent(
new CustomEvent("bitvid:auth-loading-state", {
detail: { lists: "ready", listsDetail: { ready: true } },
}),
);
const beforeClear = harness.getListSyncEvents().length;
// When: we clear the events
harness.clearListSyncEvents();
const afterClear = harness.getListSyncEvents().length;
(window as any).__syncClearResult = { beforeClear, afterClear };
});
const result = await page.evaluate(
() => (window as any).__syncClearResult,
);
// Then: buffer was non-empty before clear and empty after
expect(result.beforeClear).toBeGreaterThan(0);
expect(result.afterClear).toBe(0);
});
test("multiple sync events maintain chronological order", async ({
page,
gotoApp,
}) => {
// Given: the app is loaded
await gotoApp();
// When: multiple sync events are dispatched in order
await page.evaluate(() => {
const harness = (window as any).__bitvidTest__;
harness.clearListSyncEvents();
const states = ["loading", "syncing", "ready"];
for (const state of states) {
window.dispatchEvent(
new CustomEvent("bitvid:auth-loading-state", {
detail: {
lists: state,
listsDetail: { ready: state === "ready" },
},
}),
);
}
});
// Then: events are recorded in order
const events = await page.evaluate(() => {
return (window as any).__bitvidTest__.getListSyncEvents();
});
expect(events.length).toBeGreaterThanOrEqual(3);
// All should have the same source
for (const event of events) {
expect(event.source).toBe("auth-loading-state");
}
});
});
test.describe("Diagnostics capture", () => {
test("diagnostics capture console messages", async ({
page,
gotoApp,
startDiagnostics,
}) => {
// Given: diagnostics are started
await gotoApp();
const diag = await startDiagnostics(page);
// When: console messages are emitted
await page.evaluate(() => {
console.log("test-diagnostic-log");
console.warn("test-diagnostic-warn");
});
// Then: messages are captured
const results = await diag.stop();
const logEntry = results.console.find(
(c: any) => c.text === "test-diagnostic-log",
);
const warnEntry = results.console.find(
(c: any) => c.text === "test-diagnostic-warn",
);
expect(logEntry).toBeDefined();
expect(logEntry.type).toBe("log");
expect(warnEntry).toBeDefined();
expect(warnEntry.type).toBe("warning");
});
test("diagnostics capture page errors", async ({
page,
gotoApp,
startDiagnostics,
}) => {
// Given: diagnostics are started
await gotoApp();
const diag = await startDiagnostics(page);
// When: a page error occurs
await Promise.all([
page.waitForEvent("pageerror", { timeout: 5000 }),
page.evaluate(() => {
setTimeout(() => {
throw new Error("intentional-test-error");
}, 0);
}),
]);
// Then: error is captured
const results = await diag.stop();
const errorEntry = results.pageErrors.find((e: string) =>
e.includes("intentional-test-error"),
);
expect(errorEntry).toBeDefined();
});
});
});