mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-28 08:22:24 +00:00
330 lines
10 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|