mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
CI has been failing on unstable for weeks, across four unit files and a set of e2e specs. None of it was a product regression: every failure is a test that encodes behavior a later commit deliberately changed. Each is corrected against the spec that superseded it, not adjusted until it passed. Unit: - video-modal-moderation: the fixture set flat `blurThumbnail`/`trustedMuteCount`, but normalizeVideoModerationContext derives shouldShow from `original.blurThumbnail`/`trustedCount`, so the assertion ran against a context whose early return had just deleted the blur it was checking.4525c1c6also made a lone trusted mute a non-blocking ranking signal (its sibling test was renamed to say exactly that; this one was missed). Fixture now mirrors what moderationDecorator emits. - trustBootstrap: asserted a subscription to onWhitelistChange that18e08bf2deliberately removed -- buildTrustedSeeds derives seeds from the super admin, editors and fallback seeds, so a whitelist change cannot alter them. The removal is now pinned so it is not reinstated by reflex. - moderation-service:18e08bf2removed `status.whitelisted ||` from isTrustedReporter (being allowed to publish is not a statement that your reports carry weight). A whitelisted reporter now counts toward the report total but not toward trusted. - playbackService_forcedSource: forced P2P now falls back to a hosted URL when the torrent definitively fails -- playbackService documents the change and its rollback. The old assertion did not merely fail, it HUNG: forced sources skip the start timeout, so the now-reachable hosted attempt waited forever on a <video> that never emits. E2E: - ui-modals: asserted exactly one `.bv-modal` per component page, but profile-modal.html also bundles #storageCorsModal (d7ad1516). Requires at least one and drives the primary, which is what the test's own injected querySelector already used. - coverage-utility-modules: used `author:abc123`, which9afbc0cbcorrectly began rejecting as undecodable. Uses a real pubkey for the success path and pins the rejection separately. - webseed-playback: asserted the FINAL src equals the CDN URL while also tolerating "No playable source found" in its status check -- contradictory, since a failed attempt calls resetVideoElement() and clears src. Headless CI cannot decode the archive.org H.264 sample. Now records src attachments and asserts the URL was attached, which is the behavior under test. - nostr-runtime-coverage: asserted a legacy plaintext session-actor entry reads back as null. Since8692dac6added per-account storage, readV1Entry PURGES the plaintext entry and the reader falls back to the sole remaining encrypted account -- so null no longer describes the security property. Asserts the property directly: no plaintext key survives, in the entry or in raw storage. Also drops the syncAuthUiState share-gate wiring added ind141c316. It was redundant: Application.updateShareNostrAuthState() already fires on init, login, logout, pubkey-change and signer-change and forwards to videoModal.setShareNostrAuthState(). That call short-circuited on its own `if (!this.videoModal?.setShareNostrAuthState) return;` guard purely because the method did not exist -- adding it (d141c316) was the whole fix. The tests now pin that contract instead of a second, competing code path. All 416 unit files, all three CI shards, both DM suites and lint are green. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
312 lines
10 KiB
TypeScript
312 lines
10 KiB
TypeScript
import { expect, test } from "./helpers/instrumentedTest";
|
|
import {
|
|
applyReducedMotion,
|
|
ensureTestAssets,
|
|
failOnConsoleErrors,
|
|
forceOpenModal,
|
|
} from "./helpers/uiTestUtils";
|
|
|
|
test.describe("login modal flows", () => {
|
|
test("preflight check for asset availability", async ({ request }) => {
|
|
const assets = [
|
|
"/assets/svg/default-profile.svg",
|
|
"/components/iframe_forms/iframe-application-form.html",
|
|
];
|
|
|
|
for (const asset of assets) {
|
|
const response = await request.get(asset);
|
|
expect(response.status(), `Asset ${asset} should be available`).toBe(200);
|
|
}
|
|
});
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
await ensureTestAssets(page);
|
|
await applyReducedMotion(page);
|
|
failOnConsoleErrors(page);
|
|
await page.goto("/components/login-modal.html", { waitUntil: "networkidle" });
|
|
|
|
await page.addScriptTag({
|
|
type: "module",
|
|
content: `
|
|
import LoginModalController from "/js/ui/loginModalController.js";
|
|
import {
|
|
prepareStaticModal,
|
|
openStaticModal,
|
|
closeStaticModal,
|
|
} from "/js/ui/components/staticModalAccessibility.js";
|
|
|
|
const modalElement = document.getElementById("loginModal");
|
|
window.__testAuth = {
|
|
lastRequest: null,
|
|
};
|
|
|
|
const authService = {
|
|
requestLogin: async (options) => {
|
|
window.__testAuth.lastRequest = options;
|
|
// For NIP-46, simulate a pending connection so the UI stays visible
|
|
// allowing interaction with the Cancel button.
|
|
if (options.providerId === "nip46") {
|
|
// We must trigger the UI callback to enable the Cancel button
|
|
if (typeof options.onHandshakePrepared === "function") {
|
|
options.onHandshakePrepared({ uri: "nostr:test-uri" });
|
|
}
|
|
await new Promise(() => {});
|
|
}
|
|
return { pubkey: "npub-test" };
|
|
},
|
|
hydrateFromStorage: async () => null,
|
|
on: () => () => {},
|
|
};
|
|
|
|
const providers = [
|
|
{
|
|
id: "nsec",
|
|
label: "Secret key",
|
|
description: "Use a raw key",
|
|
login: async () => {},
|
|
},
|
|
{
|
|
id: "nip46",
|
|
label: "Remote signer",
|
|
description: "Pair a signer",
|
|
login: async () => {},
|
|
},
|
|
{
|
|
id: "nip07",
|
|
label: "Extension",
|
|
description: "Browser extension",
|
|
login: async () => {},
|
|
},
|
|
];
|
|
|
|
const prepareModal = (modal) =>
|
|
prepareStaticModal({ root: modal }) || modal;
|
|
|
|
const openModal = ({ modal, triggerElement } = {}) =>
|
|
openStaticModal(modal, { triggerElement });
|
|
|
|
const closeModal = () => {
|
|
if (modalElement) {
|
|
closeStaticModal(modalElement);
|
|
}
|
|
};
|
|
|
|
window.__loginController = new LoginModalController({
|
|
modalElement,
|
|
providers,
|
|
services: {
|
|
authService,
|
|
// Mock nostrClient for NIP-46 flow
|
|
nostrClient: {
|
|
getRemoteSignerStatus: () => null,
|
|
onRemoteSignerChange: () => () => {},
|
|
getStoredNip46Metadata: () => null,
|
|
},
|
|
},
|
|
helpers: {
|
|
prepareModal,
|
|
openModal,
|
|
closeModal,
|
|
setModalState: (name, isOpen) => {
|
|
window.__testModalState = { name, isOpen };
|
|
},
|
|
},
|
|
});
|
|
|
|
const closeButton = document.getElementById("closeLoginModal");
|
|
if (closeButton) {
|
|
closeButton.addEventListener("click", () => {
|
|
closeModal();
|
|
});
|
|
}
|
|
|
|
window.__loginReady = true;
|
|
`,
|
|
});
|
|
|
|
await page.waitForFunction(() => window.__loginReady === true);
|
|
});
|
|
|
|
test("handles nsec and nip-46 login flows", async ({ page }) => {
|
|
const modal = page.locator("#loginModal");
|
|
|
|
await page.evaluate(() => window.__loginController.openModal());
|
|
await expect(modal).toHaveAttribute("data-open", "true");
|
|
|
|
await page
|
|
.locator('[data-provider-button][data-provider-id="nsec"]')
|
|
.click();
|
|
await expect(page.locator("[data-nsec-secret]")).toBeVisible();
|
|
|
|
await page.locator("[data-nsec-submit]").click();
|
|
const errorMessage = page.locator("[data-nsec-error]");
|
|
await expect(errorMessage).toBeVisible();
|
|
await expect(errorMessage).toHaveText(
|
|
"Paste an nsec, hex key, or mnemonic to continue.",
|
|
);
|
|
|
|
await page.locator("[data-nsec-secret]").fill("nsec-test-secret");
|
|
await page.locator("[data-nsec-submit]").click();
|
|
|
|
await page.waitForFunction(
|
|
() => window.__testAuth?.lastRequest?.providerId === "nsec",
|
|
);
|
|
|
|
await expect(modal).toHaveAttribute("data-open", "false");
|
|
|
|
await page.evaluate(() => window.__loginController.openModal());
|
|
await expect(modal).toHaveAttribute("data-open", "true");
|
|
|
|
await page
|
|
.locator('[data-provider-button][data-provider-id="nip46"]')
|
|
.click();
|
|
await expect(page.locator("[data-nip46-handshake-panel]")).toBeVisible();
|
|
|
|
// Ensure the cancel button is visible and stable before clicking
|
|
const cancelButton = page.locator("[data-nip46-cancel]");
|
|
await expect(cancelButton).toBeVisible();
|
|
await cancelButton.click();
|
|
await expect(page.locator("[data-nip46-handshake-panel]")).toHaveCount(0);
|
|
await expect(modal).toHaveAttribute("data-open", "true");
|
|
|
|
await page.locator("#closeLoginModal").click();
|
|
await expect(modal).toHaveAttribute("data-open", "false");
|
|
|
|
const lastRequest = await page.evaluate(() => window.__testAuth.lastRequest);
|
|
// Since we triggered the NIP-46 flow, the last request should be for nip46
|
|
expect(lastRequest.providerId).toBe("nip46");
|
|
});
|
|
});
|
|
|
|
test.describe("component modal pages", () => {
|
|
const modalPages = [
|
|
"upload-modal",
|
|
"edit-video-modal",
|
|
"delete-video-modal",
|
|
"revert-video-modal",
|
|
"profile-modal",
|
|
"video-modal",
|
|
"lockdown-modal",
|
|
"application-form",
|
|
];
|
|
|
|
const closeSelectors = [
|
|
"#closeModal",
|
|
".modal-close",
|
|
"[data-dismiss]",
|
|
".btn-ghost",
|
|
"[data-cancel]",
|
|
"[data-close]",
|
|
"[data-close-modal]",
|
|
"[data-modal-close]",
|
|
"[data-modal-cancel]",
|
|
];
|
|
|
|
for (const pageName of modalPages) {
|
|
test(`closes ${pageName} modal`, async ({ page }) => {
|
|
await ensureTestAssets(page);
|
|
await applyReducedMotion(page);
|
|
failOnConsoleErrors(page);
|
|
await page.goto(`/components/${pageName}.html`, {
|
|
waitUntil: "networkidle",
|
|
});
|
|
|
|
// A component page may legitimately ship more than one modal root:
|
|
// profile-modal.html also bundles #storageCorsModal, the B2/S3 CORS setup
|
|
// helper added in d7ad1516. Asserting exactly one was incidental to what
|
|
// this test is actually for -- "the page renders a modal that opens and
|
|
// closes" -- so require at least one and drive the primary one, which is
|
|
// what the injected script's document.querySelector(".bv-modal") already
|
|
// operates on.
|
|
const modals = page.locator(".bv-modal");
|
|
expect(await modals.count()).toBeGreaterThan(0);
|
|
const modal = modals.first();
|
|
|
|
// Inject logic to initialize the modal since we are viewing a partial/component in isolation.
|
|
await page.addScriptTag({
|
|
type: "module",
|
|
content: `
|
|
import {
|
|
openStaticModal,
|
|
closeStaticModal,
|
|
} from "/js/ui/components/staticModalAccessibility.js";
|
|
|
|
const modal = document.querySelector(".bv-modal");
|
|
if (modal) {
|
|
// Open the modal using the actual accessibility logic.
|
|
openStaticModal(modal);
|
|
|
|
// Manually wire up close buttons that lack data-dismiss (e.g., .modal-close)
|
|
// since staticModalAccessibility only automatically handles data-dismiss.
|
|
const closeButtons = modal.querySelectorAll(".modal-close, #closeModal");
|
|
closeButtons.forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
closeStaticModal(modal);
|
|
});
|
|
});
|
|
}
|
|
`,
|
|
});
|
|
|
|
await expect(modal).not.toHaveClass(/hidden/);
|
|
await expect(modal).toHaveAttribute("data-open", "true");
|
|
|
|
let closeSelector: string | null = null;
|
|
for (const selector of closeSelectors) {
|
|
// We filter for visible elements because backdrops might be present but invisible/unclickable
|
|
// depending on how they are rendered or if animations are disabled.
|
|
// We prioritize explicit close buttons over generic backdrops.
|
|
const locator = page.locator(selector).first();
|
|
const isPresent = (await locator.count()) > 0;
|
|
// For video-modal, visibility check is flaky in isolation due to opacity transitions
|
|
const isVisible = pageName === "video-modal" ? true : await locator.isVisible();
|
|
|
|
if (isPresent && isVisible) {
|
|
closeSelector = selector;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (closeSelector) {
|
|
if (pageName === "video-modal") {
|
|
// Video modal close button has strict visibility rules (opacity/hover) that fail in isolation.
|
|
// We use evaluate to bypass all visibility checks and force the event.
|
|
await page
|
|
.locator(closeSelector)
|
|
.first()
|
|
.evaluate((node) => node.click());
|
|
} else {
|
|
await page.locator(closeSelector).first().click();
|
|
}
|
|
} else {
|
|
// lockdown-modal is an exception to the rule as it is designed to be inescapable.
|
|
if (pageName !== "lockdown-modal") {
|
|
expect(
|
|
false,
|
|
`Modal page ${pageName} is missing a visible close/cancel control.`,
|
|
).toBeTruthy();
|
|
}
|
|
|
|
// If no close control, manually close it to satisfy the test expectation
|
|
// (though this path implies the test might be incomplete for this modal).
|
|
await page.evaluate(() => {
|
|
const element = document.querySelector(".bv-modal");
|
|
if (element) {
|
|
element.setAttribute("data-open", "false");
|
|
element.classList.add("hidden");
|
|
}
|
|
});
|
|
}
|
|
|
|
await page.waitForFunction(() => {
|
|
const element = document.querySelector(".bv-modal");
|
|
if (!element) {
|
|
return true;
|
|
}
|
|
const dataOpen = element.getAttribute("data-open");
|
|
return dataOpen === "false" || element.classList.contains("hidden");
|
|
});
|
|
});
|
|
}
|
|
});
|