Skip to content

Commit e68d530

Browse files
d0x2fclaude
andauthored
ci: run Cypress against Chrome, Firefox, and Edge (#215)
* ci: run Cypress against Chrome, Firefox, and Edge 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 <noreply@anthropic.com> * fix: make Cypress tests pass in Firefox 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8fd972a commit e68d530

8 files changed

Lines changed: 70 additions & 17 deletions

File tree

.github/workflows/CICD.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ jobs:
3737
matrix:
3838
browser:
3939
- chrome
40-
# - firefox
40+
- firefox
41+
- edge
4142
viewport:
4243
[
4344
"viewportWidth=375,viewportHeight=667",
@@ -61,7 +62,7 @@ jobs:
6162
if: always()
6263
uses: actions/upload-artifact@v4
6364
with:
64-
name: videos
65+
name: videos-${{ matrix.browser }}-${{ strategy.job-index }}
6566
path: cypress/videos
6667
retention-days: 5
6768

cypress.config.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,22 @@ export default defineConfig({
55
allowCypressEnv: false,
66
e2e: {
77
baseUrl: "http://localhost:5173",
8+
setupNodeEvents(on, config) {
9+
on('before:browser:launch', (browser, launchOptions) => {
10+
if (browser.name === 'firefox') {
11+
// Firefox doesn't auto-download files; configure it to save
12+
// directly to the Cypress downloads folder without prompting.
13+
launchOptions.preferences['browser.download.folderList'] = 2;
14+
launchOptions.preferences['browser.download.dir'] =
15+
config.downloadsFolder;
16+
launchOptions.preferences[
17+
'browser.helperApps.neverAsk.saveToDisk'
18+
] =
19+
'text/csv,application/octet-stream,application/yaml,text/yaml,text/plain,application/x-yaml';
20+
launchOptions.preferences['pdfjs.disabled'] = true;
21+
}
22+
return launchOptions;
23+
});
24+
},
825
},
926
});

cypress/e2e/Card.cy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ context('Card', () => {
1515
.first()
1616
.find('[data-name=card-author-input]')
1717
.type('Test Author{enter}');
18+
cy.get('[data-name=card]:visible').should('exist');
1819
});
1920

2021
beforeEach(() => {

cypress/e2e/ConnectionLost.cy.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ context('ConnectionLost', () => {
2020
cy.get('[data-name=rank]:visible').should('exist');
2121
});
2222

23+
// Firefox keeps navigator.onLine = false across navigations when the
24+
// offline event was dispatched programmatically. Restoring online state
25+
// here prevents Firebase auth from failing in the next beforeEach or the
26+
// after hook.
27+
afterEach(() => {
28+
cy.window().then((win) => win.dispatchEvent(new Event('online')));
29+
});
30+
2331
it('shows a connection lost alert when the browser goes offline', () => {
2432
cy.window().then((win) => win.dispatchEvent(new Event('offline')));
2533
cy.get('[data-name=error-alert]').should('be.visible');
@@ -34,6 +42,7 @@ context('ConnectionLost', () => {
3442
});
3543

3644
after(() => {
45+
cy.window().then((win) => win.dispatchEvent(new Event('online')));
3746
cy.deleteAllBoards();
3847
});
3948
});

cypress/e2e/Menu.cy.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ context('Menu', () => {
183183

184184
it('csv created_at timestamp is within 24 hours of the current time', () => {
185185
const cardText = 'Timestamp test card';
186+
const date = new Date().toISOString().slice(0, 10);
187+
const csvPath = `cypress/downloads/Test Board Name-${date}.csv`;
186188

187189
cy.get('[data-name=rank]:visible')
188190
.first()
@@ -191,13 +193,12 @@ context('Menu', () => {
191193
.should('have.value', '');
192194
cy.get('[data-name=card]:visible').should('exist');
193195

196+
// Delete any existing file so Firefox doesn't rename the new download.
197+
cy.exec(`rm -f "${csvPath}"`);
194198
cy.get('[data-name=menu-button]').click();
195199
cy.get('[data-name=download-csv-button]').click();
196200

197-
const date = new Date().toISOString().slice(0, 10);
198-
// Use .should() so Cypress retries readFile until the new download
199-
// overwrites a stale CSV from an earlier test in this suite.
200-
cy.readFile(`cypress/downloads/Test Board Name-${date}.csv`)
201+
cy.readFile(csvPath)
201202
.should('include', cardText)
202203
.then((csv) => {
203204
const [, ...rows] = csv.trim().split('\n');

cypress/e2e/OwnerRealtimeSync.cy.js

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,28 @@ context('OwnerRealtimeSync', () => {
3838
// Owner loads the board — with the fix, a Firestore subscription is established
3939
// for owners when open_permission is active
4040
cy.login('owner');
41+
// Capture the session cookie now, before cy.visit() causes the app to call
42+
// /auth again. In Firefox, that re-auth can overwrite __session with a new
43+
// anonymous-user token that doesn't own the board, causing PATCH → 403.
44+
cy.getCookie('__session').as('ownerSession');
4145
cy.visit(boardUrl);
4246
cy.get('[data-name=rank]:visible').should('exist');
4347
cy.get('[data-name=vote-button]:visible').should('exist');
4448

4549
// PATCH the board via REST, bypassing the Svelte store entirely.
4650
// Only the Firestore subscription — not the local store listener — can deliver this update.
47-
cy.request(`/boards/${boardId}`).then(({ body }) => {
48-
let data = body.data;
49-
try {
50-
data = JSON.parse(data);
51-
} catch {}
52-
cy.request({
53-
method: 'PATCH',
54-
url: `/boards/${boardId}`,
55-
body: { ...body, data, voting_open: false },
51+
cy.get('@ownerSession').then((cookie) => {
52+
cy.request(`/boards/${boardId}`).then(({ body }) => {
53+
let data = body.data;
54+
try {
55+
data = JSON.parse(data);
56+
} catch {}
57+
cy.request({
58+
method: 'PATCH',
59+
url: `/boards/${boardId}`,
60+
body: { ...body, data, voting_open: false },
61+
headers: { Cookie: `__session=${cookie.value}` },
62+
});
5663
});
5764
});
5865

cypress/e2e/TemplateRoundTrip.cy.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,11 @@ context(
276276
// Rename the first column — this removes the i18n key from the export
277277
customiseFirstRank({ name: 'My Custom Column' });
278278

279+
// Delete the file from the German-export test so Firefox doesn't save
280+
// the new download under a deduplicated name (e.g. "file (1).yaml").
281+
cy.exec(`rm -f "${DOWNLOAD_FILE}"`);
279282
downloadTemplate();
280283

281-
// The file already exists from the German-export test in this context.
282-
// Use .should() so Cypress retries readFile until the download overwrites it.
283284
cy.readFile(DOWNLOAD_FILE)
284285
.should('include', 'My Custom Column')
285286
.then((text) => {

cypress/support/e2e.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
// Firefox aborts in-flight fetch() calls during navigation and surfaces them
2+
// as "NetworkError when attempting to fetch resource" unhandled rejections.
3+
// Chrome drops the same aborted requests silently. Returning false here tells
4+
// Cypress not to fail the test for this browser-specific behaviour.
5+
Cypress.on('uncaught:exception', (err) => {
6+
// Firefox reports aborted fetch calls during navigation as NetworkError.
7+
if (err.message.includes('NetworkError when attempting to fetch resource')) {
8+
return false;
9+
}
10+
// Firebase auth can fail with this when the browser is in a simulated
11+
// offline state (navigator.onLine = false in Firefox).
12+
if (err.message.includes('auth/network-request-failed')) {
13+
return false;
14+
}
15+
});
16+
117
Cypress.Commands.add('login', (name = 'owner') => {
218
cy.session(
319
name,

0 commit comments

Comments
 (0)