From e522d637daebf808ae75e7ba251b2f6f7659583c Mon Sep 17 00:00:00 2001 From: Dylan McGannon Date: Tue, 5 May 2026 11:16:26 +0100 Subject: [PATCH 1/5] ci: run Cypress against Chrome, Firefox, and Edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uncomments Firefox and adds Edge to the browser matrix, expanding coverage from 2 matrix jobs to 6 (3 browsers × 2 viewports). Also fixes artifact name collision: the hardcoded "videos" name would cause upload-artifact@v4 to fail when multiple matrix jobs ran in parallel; now each job uploads to a unique name using browser + job index. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/CICD.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml index dcad9c6..83b2644 100644 --- a/.github/workflows/CICD.yml +++ b/.github/workflows/CICD.yml @@ -37,7 +37,8 @@ jobs: matrix: browser: - chrome - # - firefox + - firefox + - edge viewport: [ "viewportWidth=375,viewportHeight=667", @@ -61,7 +62,7 @@ jobs: if: always() uses: actions/upload-artifact@v4 with: - name: videos + name: videos-${{ matrix.browser }}-${{ strategy.job-index }} path: cypress/videos retention-days: 5 From 543fedd6d6b511387969abf350f1d882d838c2be Mon Sep 17 00:00:00 2001 From: Dylan McGannon Date: Tue, 5 May 2026 15:25:57 +0100 Subject: [PATCH 2/5] fix: make Cypress tests pass in Firefox MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two root causes: 1. Firefox doesn't auto-download files — it shows a Save dialog unless browser preferences are configured. Add a before:browser:launch hook in cypress.config.js that sets the Firefox download prefs to save directly to config.downloadsFolder. Fixes Menu CSV timestamp test and TemplateRoundTrip custom-column test. 2. In Firefox, cy.visit() causes the app to call /auth, which can issue a new anonymous-user token and overwrite __session with a different UID than the board owner. PATCH then fails with 403. Fix by capturing the owner's __session cookie via cy.getCookie() before the page visit and passing it explicitly as a Cookie header in the PATCH request. Co-Authored-By: Claude Sonnet 4.6 --- cypress.config.js | 17 +++++++++++++++++ cypress/e2e/OwnerRealtimeSync.cy.js | 25 ++++++++++++++++--------- 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/cypress.config.js b/cypress.config.js index 83460c3..e0a6a58 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -5,5 +5,22 @@ export default defineConfig({ allowCypressEnv: false, e2e: { baseUrl: "http://localhost:5173", + setupNodeEvents(on, config) { + on('before:browser:launch', (browser, launchOptions) => { + if (browser.name === 'firefox') { + // Firefox doesn't auto-download files; configure it to save + // directly to the Cypress downloads folder without prompting. + launchOptions.preferences['browser.download.folderList'] = 2; + launchOptions.preferences['browser.download.dir'] = + config.downloadsFolder; + launchOptions.preferences[ + 'browser.helperApps.neverAsk.saveToDisk' + ] = + 'text/csv,application/octet-stream,application/yaml,text/yaml,text/plain,application/x-yaml'; + launchOptions.preferences['pdfjs.disabled'] = true; + } + return launchOptions; + }); + }, }, }); diff --git a/cypress/e2e/OwnerRealtimeSync.cy.js b/cypress/e2e/OwnerRealtimeSync.cy.js index f350b5a..de3a2c2 100644 --- a/cypress/e2e/OwnerRealtimeSync.cy.js +++ b/cypress/e2e/OwnerRealtimeSync.cy.js @@ -38,21 +38,28 @@ context('OwnerRealtimeSync', () => { // Owner loads the board — with the fix, a Firestore subscription is established // for owners when open_permission is active cy.login('owner'); + // Capture the session cookie now, before cy.visit() causes the app to call + // /auth again. In Firefox, that re-auth can overwrite __session with a new + // anonymous-user token that doesn't own the board, causing PATCH → 403. + cy.getCookie('__session').as('ownerSession'); cy.visit(boardUrl); cy.get('[data-name=rank]:visible').should('exist'); cy.get('[data-name=vote-button]:visible').should('exist'); // PATCH the board via REST, bypassing the Svelte store entirely. // Only the Firestore subscription — not the local store listener — can deliver this update. - cy.request(`/boards/${boardId}`).then(({ body }) => { - let data = body.data; - try { - data = JSON.parse(data); - } catch {} - cy.request({ - method: 'PATCH', - url: `/boards/${boardId}`, - body: { ...body, data, voting_open: false }, + cy.get('@ownerSession').then((cookie) => { + cy.request(`/boards/${boardId}`).then(({ body }) => { + let data = body.data; + try { + data = JSON.parse(data); + } catch {} + cy.request({ + method: 'PATCH', + url: `/boards/${boardId}`, + body: { ...body, data, voting_open: false }, + headers: { Cookie: `__session=${cookie.value}` }, + }); }); }); From 29093e2a30c039e9e5cdbdf3b860eadce5a3dd82 Mon Sep 17 00:00:00 2001 From: Dylan McGannon Date: Fri, 8 May 2026 23:12:03 +0100 Subject: [PATCH 3/5] fix: resolve Firefox-specific Cypress failures in multi-browser CI - Suppress Firefox's "NetworkError when attempting to fetch resource" uncaught exception, which fires for fetch calls aborted by navigation (Chrome drops these silently; they are not real test failures) - Delete stale download files before triggering a second download to the same path: Firefox deduplicates filenames (e.g. file (1).csv) rather than overwriting, so cy.readFile was always reading the old content Co-Authored-By: Claude Sonnet 4.6 --- cypress/e2e/Menu.cy.js | 9 +++++---- cypress/e2e/TemplateRoundTrip.cy.js | 5 +++-- cypress/support/e2e.js | 10 ++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cypress/e2e/Menu.cy.js b/cypress/e2e/Menu.cy.js index d24a670..2f9463f 100644 --- a/cypress/e2e/Menu.cy.js +++ b/cypress/e2e/Menu.cy.js @@ -183,6 +183,8 @@ context('Menu', () => { it('csv created_at timestamp is within 24 hours of the current time', () => { const cardText = 'Timestamp test card'; + const date = new Date().toISOString().slice(0, 10); + const csvPath = `cypress/downloads/Test Board Name-${date}.csv`; cy.get('[data-name=rank]:visible') .first() @@ -191,13 +193,12 @@ context('Menu', () => { .should('have.value', ''); cy.get('[data-name=card]:visible').should('exist'); + // Delete any existing file so Firefox doesn't rename the new download. + cy.exec(`rm -f "${csvPath}"`); cy.get('[data-name=menu-button]').click(); cy.get('[data-name=download-csv-button]').click(); - const date = new Date().toISOString().slice(0, 10); - // Use .should() so Cypress retries readFile until the new download - // overwrites a stale CSV from an earlier test in this suite. - cy.readFile(`cypress/downloads/Test Board Name-${date}.csv`) + cy.readFile(csvPath) .should('include', cardText) .then((csv) => { const [, ...rows] = csv.trim().split('\n'); diff --git a/cypress/e2e/TemplateRoundTrip.cy.js b/cypress/e2e/TemplateRoundTrip.cy.js index 7a7cb44..a474ca1 100644 --- a/cypress/e2e/TemplateRoundTrip.cy.js +++ b/cypress/e2e/TemplateRoundTrip.cy.js @@ -276,10 +276,11 @@ context( // Rename the first column — this removes the i18n key from the export customiseFirstRank({ name: 'My Custom Column' }); + // Delete the file from the German-export test so Firefox doesn't save + // the new download under a deduplicated name (e.g. "file (1).yaml"). + cy.exec(`rm -f "${DOWNLOAD_FILE}"`); downloadTemplate(); - // The file already exists from the German-export test in this context. - // Use .should() so Cypress retries readFile until the download overwrites it. cy.readFile(DOWNLOAD_FILE) .should('include', 'My Custom Column') .then((text) => { diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index c5e5951..eb7ef1e 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -1,3 +1,13 @@ +// Firefox aborts in-flight fetch() calls during navigation and surfaces them +// as "NetworkError when attempting to fetch resource" unhandled rejections. +// Chrome drops the same aborted requests silently. Returning false here tells +// Cypress not to fail the test for this browser-specific behaviour. +Cypress.on('uncaught:exception', (err) => { + if (err.message.includes('NetworkError when attempting to fetch resource')) { + return false; + } +}); + Cypress.Commands.add('login', (name = 'owner') => { cy.session( name, From 11f09214ea225fa5b7fdcca32a2e2b63efabf328 Mon Sep 17 00:00:00 2001 From: Dylan McGannon Date: Fri, 8 May 2026 23:23:11 +0100 Subject: [PATCH 4/5] fix: restore online state after each ConnectionLost test in Firefox Firefox sets navigator.onLine = false when the offline event is dispatched programmatically and does not reset it on page navigation. Without cleanup, Firebase auth fails in subsequent beforeEach / after hooks with auth/network-request-failed. Added afterEach to dispatch 'online', guarded the after hook the same way, and extended the uncaught:exception handler to suppress Firebase auth errors that escape during simulated-offline browser state. Co-Authored-By: Claude Sonnet 4.6 --- cypress/e2e/ConnectionLost.cy.js | 9 +++++++++ cypress/support/e2e.js | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/cypress/e2e/ConnectionLost.cy.js b/cypress/e2e/ConnectionLost.cy.js index a2bae19..5ae8611 100644 --- a/cypress/e2e/ConnectionLost.cy.js +++ b/cypress/e2e/ConnectionLost.cy.js @@ -20,6 +20,14 @@ context('ConnectionLost', () => { cy.get('[data-name=rank]:visible').should('exist'); }); + // Firefox keeps navigator.onLine = false across navigations when the + // offline event was dispatched programmatically. Restoring online state + // here prevents Firebase auth from failing in the next beforeEach or the + // after hook. + afterEach(() => { + cy.window().then((win) => win.dispatchEvent(new Event('online'))); + }); + it('shows a connection lost alert when the browser goes offline', () => { cy.window().then((win) => win.dispatchEvent(new Event('offline'))); cy.get('[data-name=error-alert]').should('be.visible'); @@ -34,6 +42,7 @@ context('ConnectionLost', () => { }); after(() => { + cy.window().then((win) => win.dispatchEvent(new Event('online'))); cy.deleteAllBoards(); }); }); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index eb7ef1e..3140ef0 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -3,9 +3,15 @@ // Chrome drops the same aborted requests silently. Returning false here tells // Cypress not to fail the test for this browser-specific behaviour. Cypress.on('uncaught:exception', (err) => { + // Firefox reports aborted fetch calls during navigation as NetworkError. if (err.message.includes('NetworkError when attempting to fetch resource')) { return false; } + // Firebase auth can fail with this when the browser is in a simulated + // offline state (navigator.onLine = false in Firefox). + if (err.message.includes('auth/network-request-failed')) { + return false; + } }); Cypress.Commands.add('login', (name = 'owner') => { From 4b632a5b82bfeaa0751d64802bddb02d802ca42f Mon Sep 17 00:00:00 2001 From: Dylan McGannon Date: Fri, 8 May 2026 23:37:02 +0100 Subject: [PATCH 5/5] test: wait for card to exist after creation in Card before hook Without this assertion the before hook completes as soon as the Enter keystroke fires, before the async createCard API call resolves. Any transient failure in card creation then cascades silently into all 5 tests failing with 'never found [data-name=card]:visible'. Co-Authored-By: Claude Sonnet 4.6 --- cypress/e2e/Card.cy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/e2e/Card.cy.js b/cypress/e2e/Card.cy.js index 6b74397..5a8c728 100644 --- a/cypress/e2e/Card.cy.js +++ b/cypress/e2e/Card.cy.js @@ -15,6 +15,7 @@ context('Card', () => { .first() .find('[data-name=card-author-input]') .type('Test Author{enter}'); + cy.get('[data-name=card]:visible').should('exist'); }); beforeEach(() => {