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 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/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(() => { 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/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/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}` }, + }); }); }); 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..3140ef0 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -1,3 +1,19 @@ +// 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) => { + // 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') => { cy.session( name,