From 7944c3f0405e167d6fb29a277600a01c798cfb48 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:04:01 +0000 Subject: [PATCH 1/4] docs(ai): onboarding audit and lint fix (agent) - Added daily onboarding audit report - Updated scripts/check-hex.js to ignore docs/agents/task-logs/** - Added completion log for onboarding-audit-agent Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- ...-02-22_onboarding-audit-agent_completed.md | 26 +++++++++++++++++++ scripts/check-hex.js | 3 ++- 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 docs/agents/task-logs/daily/2026-02-22_onboarding-audit-agent_completed.md diff --git a/docs/agents/task-logs/daily/2026-02-22_onboarding-audit-agent_completed.md b/docs/agents/task-logs/daily/2026-02-22_onboarding-audit-agent_completed.md new file mode 100644 index 00000000..cf1b1c9e --- /dev/null +++ b/docs/agents/task-logs/daily/2026-02-22_onboarding-audit-agent_completed.md @@ -0,0 +1,26 @@ +# Onboarding Audit - 2026-02-22 + +## Execution Summary + +- **Agent**: onboarding-audit-agent +- **Cadence**: daily +- **Status**: Completed + +## Results + +1. **Dependencies**: `npx playwright install` passed. +2. **Build**: `npm run build` passed. +3. **Unit Tests**: `npm run test:unit` timed out (partial verification with `shard1` passed). +4. **Smoke Tests**: `npm run test:smoke` passed. +5. **Lint**: `npm run lint` failed initially due to false positive hex check in `docs/agents/task-logs`, fixed by updating `scripts/check-hex.js`. + +## Artifacts + +- [Audit Report](../../../../artifacts/onboarding-audit-2026-02-22.md) + +## Changes + +- Modified `scripts/check-hex.js` to exclude `docs/agents/task-logs/**` from hex color validation. + +--- +Log generated by bitvid-onboarding-audit-agent (daily scheduler) diff --git a/scripts/check-hex.js b/scripts/check-hex.js index 070bda19..396d386c 100755 --- a/scripts/check-hex.js +++ b/scripts/check-hex.js @@ -40,7 +40,8 @@ const IGNORED_GLOBS = [ 'test_logs/**', 'context/**', 'decisions/**', - 'todo/**' + 'todo/**', + 'docs/agents/task-logs/**' ]; const gitArgs = [ From e9ca94e89dbebb376a74465ee3d6da0373db7716 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:31:09 +0000 Subject: [PATCH 2/4] test(e2e): stabilize login/feed tests and isolate relays (agent) - Updated `tests/e2e/helpers/bitvidTestFixture.ts` to inject `__bitvidTestRelays__` into `localStorage` and `window` for reliable isolation. - Updated `js/relayManager.js` to respect test relays in both `loadFromStorage` and defaults construction, preventing production fallback on profile load. - Updated `tests/e2e/login-flows.spec.ts` to click the "nsec" provider button before assertions. - Updated `tests/e2e/feed-pagination-stress.spec.ts` to correctly query title attributes. - Added `scripts/check-hex.js` fix for log files. - Added onboarding audit log. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/relayManager.js | 46 ++++++++++++++++++++++-- tests/e2e/feed-pagination-stress.spec.ts | 5 +-- tests/e2e/helpers/bitvidTestFixture.ts | 10 ++++-- tests/e2e/login-flows.spec.ts | 9 +++-- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/js/relayManager.js b/js/relayManager.js index 670962aa..60fef925 100644 --- a/js/relayManager.js +++ b/js/relayManager.js @@ -204,8 +204,27 @@ class RelayPreferencesManager { this.lastEvent = null; this.loadedPubkey = null; this.lastLoadSource = "default"; - this.defaultEntries = DEFAULT_RELAY_URLS.map((url) => { - const normalized = normalizeRelayUrl(url) || url; + + let defaults = DEFAULT_RELAY_URLS; + try { + if (typeof window !== "undefined" && Array.isArray(window.__bitvidTestRelays__)) { + defaults = window.__bitvidTestRelays__; + } else { + const testRelays = localStorage.getItem("__bitvidTestRelays__"); + if (testRelays) { + const parsed = JSON.parse(testRelays); + if (Array.isArray(parsed) && parsed.length) { + defaults = parsed; + } + } + } + } catch (e) { + // ignore + } + + this.defaultEntries = defaults.map((url) => { + const item = typeof url === "string" ? url : url?.url; + const normalized = normalizeRelayUrl(item) || item; return createEntry(normalized, "both"); }); @@ -230,6 +249,29 @@ class RelayPreferencesManager { } loadFromStorage() { + try { + if (typeof window !== "undefined" && Array.isArray(window.__bitvidTestRelays__)) { + return window.__bitvidTestRelays__.map((item) => { + if (typeof item === "string") return createEntry(item, "both"); + return item; + }); + } + + const testRelays = localStorage.getItem("__bitvidTestRelays__"); + if (testRelays) { + const parsed = JSON.parse(testRelays); + if (Array.isArray(parsed) && parsed.length) { + // Normalize plain URLs to entry objects if needed + return parsed.map((item) => { + if (typeof item === "string") return createEntry(item, "both"); + return item; + }); + } + } + } catch (error) { + // Ignore storage errors in restricted contexts + } + const cached = profileCache.get("relays"); if (Array.isArray(cached)) { return cached; diff --git a/tests/e2e/feed-pagination-stress.spec.ts b/tests/e2e/feed-pagination-stress.spec.ts index e40eb867..3147017f 100644 --- a/tests/e2e/feed-pagination-stress.spec.ts +++ b/tests/e2e/feed-pagination-stress.spec.ts @@ -297,13 +297,14 @@ test.describe("Feed pagination and stress scenarios", () => { const attrs = await page.evaluate(() => { const el = document.querySelector("[data-video-card]"); if (!el) return null; + const titleEl = el.querySelector("[data-video-title]"); return { - hasTitle: el.hasAttribute("data-video-title"), + hasTitle: !!titleEl, hasPubkey: el.hasAttribute("data-video-pubkey"), hasDtag: el.hasAttribute("data-video-dtag"), hasPlayUrl: el.hasAttribute("data-play-url"), hasPlayMagnet: el.hasAttribute("data-play-magnet"), - title: el.getAttribute("data-video-title"), + title: titleEl ? titleEl.getAttribute("data-video-title") : null, pubkey: el.getAttribute("data-video-pubkey"), dtag: el.getAttribute("data-video-dtag"), }; diff --git a/tests/e2e/helpers/bitvidTestFixture.ts b/tests/e2e/helpers/bitvidTestFixture.ts index ffe43542..dbb24d1d 100644 --- a/tests/e2e/helpers/bitvidTestFixture.ts +++ b/tests/e2e/helpers/bitvidTestFixture.ts @@ -115,10 +115,16 @@ async function gotoWithTestMode(page: Page, relayUrl: string, path = "/") { const testUrl = `${path}${separator}__test__=1&__testRelays__=${encodeURIComponent(relayUrl)}`; // Set up localStorage before navigation - await page.addInitScript(() => { + await page.addInitScript((url) => { localStorage.setItem("hasSeenDisclaimer", "true"); localStorage.setItem("__bitvidTestMode__", "1"); - }); + // Ensure network isolation by injecting test relays directly into storage + localStorage.setItem("__bitvidTestRelays__", JSON.stringify([url])); + // Also set on window for robustness against storage clearing + (window as any).__bitvidTestRelays__ = [url]; + // Disable whitelist mode so seeded test users work correctly + localStorage.setItem("bitvid_admin_whitelist_mode", "false"); + }, relayUrl); await page.goto(testUrl); diff --git a/tests/e2e/login-flows.spec.ts b/tests/e2e/login-flows.spec.ts index f4e37f94..ba7207f8 100644 --- a/tests/e2e/login-flows.spec.ts +++ b/tests/e2e/login-flows.spec.ts @@ -150,8 +150,8 @@ test.describe("Login and authentication flows", () => { // Then: gated buttons are no longer visible // Allow DOM update to propagate - await page.waitForTimeout(500); - await expect(uploadBtn).not.toBeVisible(); + await page.waitForTimeout(2000); + await expect(uploadBtn).not.toBeVisible({ timeout: 10000 }); }); test("login button is visible in logged-out state", async ({ @@ -245,6 +245,11 @@ test.describe("Login and authentication flows", () => { { timeout: 15000 }, ); + // Click the nsec provider to reveal inputs + const nsecProvider = page.locator('[data-provider-id="nsec"]'); + await expect(nsecProvider).toBeVisible(); + await nsecProvider.click(); + // Then: nsec input elements should be present in the DOM const nsecInput = page.locator('[data-testid="nsec-secret-input"]'); const nsecSubmit = page.locator('[data-testid="nsec-submit"]'); From d0f10d3ab7c1950eb9f0fb62d950e5626a1db2cb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:48:22 +0000 Subject: [PATCH 3/4] test(e2e): fix video card selectors and modal wait logic - Updated `js/testHarness.js` to read `data-video-title` attribute instead of `textContent` to avoid marquee duplication issues. - Updated `tests/e2e/video-upload-flow.spec.ts` to query title attribute on the correct child element. - Updated `tests/e2e/video-upload-flow.spec.ts` to use `toBeVisible()` for modal wait instead of fragile `data-open` attribute check. - Updated `tests/e2e/video-playback-scenarios.spec.ts` to include `style.display` check for modal closing. - Verified `feed-pagination-stress.spec.ts` passes with these fixes. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- js/testHarness.js | 4 +++- tests/e2e/video-playback-scenarios.spec.ts | 1 + tests/e2e/video-upload-flow.spec.ts | 12 +++--------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/js/testHarness.js b/js/testHarness.js index b2e5052a..c905fbc4 100644 --- a/js/testHarness.js +++ b/js/testHarness.js @@ -472,7 +472,9 @@ function getAppState() { function getFeedItems() { const cards = document.querySelectorAll("[data-video-card]"); return Array.from(cards).map((card) => ({ - title: card.querySelector("[data-video-title]")?.textContent?.trim() || "", + title: + card.querySelector("[data-video-title]")?.getAttribute("data-video-title") || + "", pubkey: card.getAttribute("data-video-pubkey") || "", dTag: card.getAttribute("data-video-dtag") || "", hasUrl: Boolean(card.getAttribute("data-play-url")), diff --git a/tests/e2e/video-playback-scenarios.spec.ts b/tests/e2e/video-playback-scenarios.spec.ts index 800c3e81..f2c0eeb4 100644 --- a/tests/e2e/video-playback-scenarios.spec.ts +++ b/tests/e2e/video-playback-scenarios.spec.ts @@ -215,6 +215,7 @@ test.describe("Video playback scenarios", () => { if (!modal) return true; return ( modal.classList.contains("hidden") || + modal.style.display === "none" || modal.getAttribute("data-open") === "false" ); }, diff --git a/tests/e2e/video-upload-flow.spec.ts b/tests/e2e/video-upload-flow.spec.ts index bb42aedc..15ec9e7c 100644 --- a/tests/e2e/video-upload-flow.spec.ts +++ b/tests/e2e/video-upload-flow.spec.ts @@ -31,14 +31,7 @@ test.describe("Video upload and discovery", () => { // Then: upload modal becomes visible with expected form fields const modal = page.locator('[data-testid="upload-modal"]'); - await page.waitForFunction(() => { - const m = document.querySelector('[data-testid="upload-modal"]'); - if (!(m instanceof HTMLElement)) return false; - return ( - m.getAttribute("data-open") === "true" && - !m.classList.contains("hidden") - ); - }, { timeout: 10000 }); + await expect(modal).toBeVisible({ timeout: 10000 }); await expect( page.locator('[data-testid="upload-title"]'), @@ -204,7 +197,8 @@ test.describe("Video upload and discovery", () => { const card = page.locator("[data-video-card]").first(); await expect(card).toBeVisible(); - const title = await card.getAttribute("data-video-title"); + const titleEl = card.locator("[data-video-title]"); + const title = await titleEl.getAttribute("data-video-title"); expect(title).toBe("Attributed Video"); const pubkey = await card.getAttribute("data-video-pubkey"); From 98ecc678a40371175524ad153c73c8a1dc296712 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 16:09:22 +0000 Subject: [PATCH 4/4] fix(ci): install firefox for e2e-headless job and harden ui tests\n\n- Updated `.github/workflows/ci.yml` to install `firefox` in the `e2e-headless` job, resolving \"Executable doesn't exist\" errors.\n- Added defensive null checks in `UploadModal.js` to prevent runtime crashes during reset.\n- Hardened `login-flows.spec.ts` to prevent flaky logout assertions. Co-authored-by: PR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- js/ui/components/SearchFilterModal.js | 10 ++-- js/ui/components/UploadModal.js | 12 ++--- tests/e2e/accessibility-keyboard-nav.spec.ts | 54 +++----------------- tests/e2e/login-flows.spec.ts | 27 ++-------- tests/e2e/video-playback-scenarios.spec.ts | 14 +---- 6 files changed, 24 insertions(+), 95 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afcbe295..6dc5260c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -478,7 +478,7 @@ jobs: run: npm ci - name: Install Playwright browsers - run: npx playwright install --with-deps chromium + run: npx playwright install --with-deps chromium firefox - name: Run headless E2E tests run: npm run test:e2e diff --git a/js/ui/components/SearchFilterModal.js b/js/ui/components/SearchFilterModal.js index 570eccc8..552f7628 100644 --- a/js/ui/components/SearchFilterModal.js +++ b/js/ui/components/SearchFilterModal.js @@ -234,8 +234,8 @@ function syncStateToControls(filters) { const safeFilters = filters || DEFAULT_FILTERS; // Date - dateStartInput.value = formatDateInputValue(safeFilters.dateRange?.after); - dateEndInput.value = formatDateInputValue(safeFilters.dateRange?.before); + if (dateStartInput) dateStartInput.value = formatDateInputValue(safeFilters.dateRange?.after); + if (dateEndInput) dateEndInput.value = formatDateInputValue(safeFilters.dateRange?.before); // Logic to highlight correct date chip if it matches // (Simplified: just highlight 'All Time' if no dates, otherwise clear chips) @@ -254,13 +254,13 @@ function syncStateToControls(filters) { }); // Sort - sortSelect.value = safeFilters.sort || "relevance"; + if (sortSelect) sortSelect.value = safeFilters.sort || "relevance"; // Author - authorInput.value = safeFilters.authorPubkeys?.join(", ") || ""; + if (authorInput) authorInput.value = safeFilters.authorPubkeys?.join(", ") || ""; // Tags - tagsInput.value = ""; + if (tagsInput) tagsInput.value = ""; // We separate tags that are in chips vs typed const activeTags = new Set(safeFilters.tags || []); const tagBtns = tagChipsContainer.querySelectorAll("button"); diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index 40f446d5..2ea49100 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -1223,19 +1223,19 @@ export class UploadModal { } // Clear Result Inputs - if (this.results.videoUrl) this.results.videoUrl.value = ""; - if (this.results.magnet) this.results.magnet.value = ""; - if (this.results.torrentUrl) this.results.torrentUrl.value = ""; + if (this.results?.videoUrl) this.results.videoUrl.value = ""; + if (this.results?.magnet) this.results.magnet.value = ""; + if (this.results?.torrentUrl) this.results.torrentUrl.value = ""; // Reset Inputs - if (this.inputs.file) { + if (this.inputs?.file) { this.inputs.file.value = ""; } - if (this.inputs.thumbnailFile) { + if (this.inputs?.thumbnailFile) { this.inputs.thumbnailFile.value = ""; } // Re-enable thumbnail input if it was disabled by an upload - if (this.inputs.thumbnail) { + if (this.inputs?.thumbnail) { this.inputs.thumbnail.disabled = false; if (this.inputs.thumbnail.placeholder && this.inputs.thumbnail.placeholder.startsWith("Selected:")) { this.inputs.thumbnail.placeholder = "https://example.com/thumbnail.jpg"; diff --git a/tests/e2e/accessibility-keyboard-nav.spec.ts b/tests/e2e/accessibility-keyboard-nav.spec.ts index 349d0255..dfa0b950 100644 --- a/tests/e2e/accessibility-keyboard-nav.spec.ts +++ b/tests/e2e/accessibility-keyboard-nav.spec.ts @@ -87,18 +87,8 @@ test.describe("Accessibility and keyboard navigation", () => { await page.keyboard.press("Enter"); // Then: login modal opens - await page.waitForFunction( - () => { - const modal = document.querySelector( - '[data-testid="login-modal"]', - ); - if (!(modal instanceof HTMLElement)) return false; - return modal.getAttribute("data-open") === "true"; - }, - { timeout: 15000 }, - ); - const modal = page.locator('[data-testid="login-modal"]'); + await expect(modal).toBeVisible({ timeout: 15000 }); await expect(modal).toHaveAttribute("data-open", "true"); }); }); @@ -127,19 +117,8 @@ test.describe("Accessibility and keyboard navigation", () => { await page.keyboard.press("Escape"); // Then: modal closes (data-open becomes false or modal is hidden) - await page.waitForFunction( - () => { - const modal = document.querySelector( - '[data-testid="login-modal"]', - ); - if (!(modal instanceof HTMLElement)) return true; - return ( - modal.getAttribute("data-open") === "false" || - modal.classList.contains("hidden") - ); - }, - { timeout: 10000 }, - ); + const modal = page.locator('[data-testid="login-modal"]'); + await expect(modal).toBeHidden({ timeout: 10000 }); }); test("login modal contains focusable elements", async ({ @@ -150,16 +129,8 @@ test.describe("Accessibility and keyboard navigation", () => { await gotoApp(); await page.locator('[data-testid="login-button"]').click(); - await page.waitForFunction( - () => { - const modal = document.querySelector( - '[data-testid="login-modal"]', - ); - if (!(modal instanceof HTMLElement)) return false; - return modal.getAttribute("data-open") === "true"; - }, - { timeout: 15000 }, - ); + const modal = page.locator('[data-testid="login-modal"]'); + await expect(modal).toBeVisible({ timeout: 15000 }); // Then: modal has focusable interactive elements const focusableCount = await page.evaluate(() => { @@ -267,19 +238,8 @@ test.describe("Accessibility and keyboard navigation", () => { await expect(uploadBtn).toBeVisible(); await uploadBtn.click(); - await page.waitForFunction( - () => { - const modal = document.querySelector( - '[data-testid="upload-modal"]', - ); - if (!(modal instanceof HTMLElement)) return false; - return ( - modal.getAttribute("data-open") === "true" && - !modal.classList.contains("hidden") - ); - }, - { timeout: 10000 }, - ); + const modal = page.locator('[data-testid="upload-modal"]'); + await expect(modal).toBeVisible({ timeout: 10000 }); // Then: form fields are focusable const titleInput = page.locator('[data-testid="upload-title"]'); diff --git a/tests/e2e/login-flows.spec.ts b/tests/e2e/login-flows.spec.ts index ba7207f8..074b0fa6 100644 --- a/tests/e2e/login-flows.spec.ts +++ b/tests/e2e/login-flows.spec.ts @@ -179,21 +179,8 @@ test.describe("Login and authentication flows", () => { await page.locator('[data-testid="login-button"]').click(); // Then: login modal becomes visible - await page.waitForFunction( - () => { - const modal = document.querySelector( - '[data-testid="login-modal"]', - ); - if (!(modal instanceof HTMLElement)) return false; - return ( - modal.getAttribute("data-open") === "true" && - !modal.classList.contains("hidden") - ); - }, - { timeout: 15000 }, - ); - const modal = page.locator('[data-testid="login-modal"]'); + await expect(modal).toBeVisible({ timeout: 15000 }); await expect(modal).toHaveAttribute("data-open", "true"); }); @@ -205,16 +192,8 @@ test.describe("Login and authentication flows", () => { await gotoApp(); await page.locator('[data-testid="login-button"]').click(); - await page.waitForFunction( - () => { - const modal = document.querySelector( - '[data-testid="login-modal"]', - ); - if (!(modal instanceof HTMLElement)) return false; - return modal.getAttribute("data-open") === "true"; - }, - { timeout: 15000 }, - ); + const modal = page.locator('[data-testid="login-modal"]'); + await expect(modal).toBeVisible({ timeout: 15000 }); // Then: at least one provider button is visible const providers = page.locator( diff --git a/tests/e2e/video-playback-scenarios.spec.ts b/tests/e2e/video-playback-scenarios.spec.ts index f2c0eeb4..9dbba408 100644 --- a/tests/e2e/video-playback-scenarios.spec.ts +++ b/tests/e2e/video-playback-scenarios.spec.ts @@ -209,18 +209,8 @@ test.describe("Video playback scenarios", () => { await closeBtn.first().click({ force: true }); // Then: modal is hidden again - await page.waitForFunction( - () => { - const modal = document.querySelector("#playerModal"); - if (!modal) return true; - return ( - modal.classList.contains("hidden") || - modal.style.display === "none" || - modal.getAttribute("data-open") === "false" - ); - }, - { timeout: 10000 }, - ); + const modal = page.locator("#playerModal"); + await expect(modal).toBeHidden({ timeout: 10000 }); } // App state should still be valid