Skip to content

Commit 5be1f14

Browse files
committed
feat(e2e): enhance Cypress configuration and add baseline snapshot functionality
- Added a new command `sessionViaApi` to facilitate authenticated sessions in Cypress tests. - Introduced a `cy:baseline` script for generating full-page PNG snapshots of public routes. - Updated Cypress configuration to include environment variables for API URL and authentication secret. - Modified screenshot handling to store baseline images separately and disabled failure screenshots. - Created a new test file for baseline snapshots of static and dynamic pages. - Updated `.gitignore` to exclude unnecessary screenshots directory.
1 parent 06ed0dd commit 5be1f14

9 files changed

Lines changed: 166 additions & 4 deletions

File tree

apps/e2e/.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
cypress/downloads/
2-
cypress/screenshots/
32
cypress/videos/

apps/e2e/cypress.config.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
*
1010
* Browser time in specs can align with backend seed caps via `@nbw/config`
1111
* (`DEFAULT_SEED_DATA_TIME_CAP`, `SEED_E2E_BROWSER_CLOCK_MS`).
12+
*
13+
* Authenticated flows: set backend `E2E_AUTH_SECRET` (development) and the same
14+
* value in Cypress (`E2E_AUTH_SECRET` or `CYPRESS_E2E_AUTH_SECRET`), then call
15+
* `cy.sessionViaApi()` (see `cypress/support/commands.ts`).
1216
*/
1317
import { defineConfig } from 'cypress';
1418

@@ -18,9 +22,22 @@ const baseUrl =
1822
export default defineConfig({
1923
e2e: {
2024
baseUrl,
25+
env: {
26+
API_URL:
27+
process.env.CYPRESS_API_URL ??
28+
process.env.API_URL ??
29+
'http://localhost:4000/v1',
30+
E2E_AUTH_SECRET:
31+
process.env.CYPRESS_E2E_AUTH_SECRET ??
32+
process.env.E2E_AUTH_SECRET ??
33+
'',
34+
},
2135
supportFile: 'cypress/support/e2e.ts',
2236
specPattern: 'cypress/e2e/**/*.cy.ts',
2337
video: false,
24-
screenshotOnRunFailure: true,
38+
// Baseline full-page PNGs live under cypress/baseline/ (tracked). Avoid dumping
39+
// failure screenshots into the same tree as route baselines.
40+
screenshotOnRunFailure: false,
41+
screenshotsFolder: 'cypress/baseline',
2542
},
2643
});

apps/e2e/cypress/baseline/.gitkeep

Whitespace-only changes.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Full-page screenshots for public routes (committed under cypress/baseline/).
3+
*
4+
* Prerequisites: frontend (and usually API) running; regenerate after UI changes:
5+
* cd apps/e2e && bun run cy:baseline
6+
*
7+
* Dynamic routes (/song/:id, /blog/:id, …) use CYPRESS_* env vars when set; otherwise skipped.
8+
*/
9+
import { SEED_E2E_BROWSER_CLOCK_MS } from '@nbw/config';
10+
11+
const VIEWPORT = { width: 1280, height: 720 } as const;
12+
13+
type PageTarget = { path: string; file: string };
14+
15+
const STATIC_PAGES: PageTarget[] = [
16+
{ path: '/', file: 'page-home' },
17+
{ path: '/about', file: 'page-about' },
18+
{ path: '/contact', file: 'page-contact' },
19+
{ path: '/search', file: 'page-search' },
20+
{ path: '/upload', file: 'page-upload' },
21+
{ path: '/my-songs', file: 'page-my-songs' },
22+
{ path: '/help', file: 'page-help' },
23+
{ path: '/blog', file: 'page-blog' },
24+
{ path: '/login', file: 'page-login' },
25+
{ path: '/login/email', file: 'page-login-email' },
26+
{ path: '/logout', file: 'page-logout' },
27+
{ path: '/privacy', file: 'page-privacy' },
28+
{ path: '/terms', file: 'page-terms' },
29+
{ path: '/guidelines', file: 'page-guidelines' },
30+
{
31+
path: '/__cypress_unknown_route__/nbw',
32+
file: 'page-not-found',
33+
},
34+
];
35+
36+
function optionalPage(
37+
envName: string,
38+
pathSuffix: string,
39+
file: string,
40+
): PageTarget | null {
41+
const id = Cypress.env(envName) as string | undefined;
42+
if (!id || typeof id !== 'string') {
43+
return null;
44+
}
45+
return { path: `${pathSuffix}/${id}`, file };
46+
}
47+
48+
describe('Page baseline snapshots', () => {
49+
beforeEach(() => {
50+
cy.viewport(VIEWPORT.width, VIEWPORT.height);
51+
cy.clock(SEED_E2E_BROWSER_CLOCK_MS, ['Date']);
52+
});
53+
54+
for (const { path, file } of STATIC_PAGES) {
55+
it(file, () => {
56+
cy.visit(path, { failOnStatusCode: false });
57+
cy.get('body', { timeout: 45_000 }).should('be.visible');
58+
cy.wait(800);
59+
cy.screenshot(file, { capture: 'fullPage', overwrite: true });
60+
});
61+
}
62+
63+
const dynamicPages: PageTarget[] = [
64+
optionalPage('SNAPSHOT_SONG_ID', '/song', 'page-song-detail'),
65+
optionalPage('SNAPSHOT_BLOG_ID', '/blog', 'page-blog-detail'),
66+
optionalPage('SNAPSHOT_HELP_ID', '/help', 'page-help-detail'),
67+
].filter((p): p is PageTarget => p !== null);
68+
69+
for (const { path, file } of dynamicPages) {
70+
it(file, () => {
71+
cy.visit(path, { failOnStatusCode: false });
72+
cy.get('body', { timeout: 45_000 }).should('be.visible');
73+
cy.wait(800);
74+
cy.screenshot(file, { capture: 'fullPage', overwrite: true });
75+
});
76+
}
77+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { DEFAULT_E2E_SEED_USER_EMAIL, E2E_AUTH_HEADER } from '@nbw/config';
2+
3+
Cypress.Commands.add(
4+
'sessionViaApi',
5+
(overrides?: { email?: string; userId?: string }) => {
6+
const apiRoot = (Cypress.env('API_URL') as string | undefined)?.replace(
7+
/\/$/,
8+
'',
9+
);
10+
if (!apiRoot) {
11+
throw new Error(
12+
'Cypress env API_URL is missing (e.g. http://localhost:4000/v1)',
13+
);
14+
}
15+
const secret = Cypress.env('E2E_AUTH_SECRET') as string | undefined;
16+
if (!secret) {
17+
throw new Error(
18+
'Cypress env E2E_AUTH_SECRET is required for sessionViaApi (must match backend E2E_AUTH_SECRET)',
19+
);
20+
}
21+
22+
const email = overrides?.email?.trim();
23+
const userId = overrides?.userId?.trim();
24+
if (email && userId) {
25+
throw new Error('sessionViaApi: pass at most one of email or userId');
26+
}
27+
28+
const body = email
29+
? { email }
30+
: userId
31+
? { userId }
32+
: { email: DEFAULT_E2E_SEED_USER_EMAIL };
33+
34+
cy.request({
35+
method: 'POST',
36+
url: `${apiRoot}/auth/e2e/session`,
37+
headers: { [E2E_AUTH_HEADER]: secret },
38+
body,
39+
failOnStatusCode: true,
40+
}).then((res) => {
41+
const { access_token, refresh_token } = res.body as {
42+
access_token: string;
43+
refresh_token: string;
44+
};
45+
cy.setCookie('token', access_token, { path: '/' });
46+
cy.setCookie('refresh_token', refresh_token, { path: '/' });
47+
});
48+
},
49+
);

apps/e2e/cypress/support/e2e.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
// Global setup for Cypress e2e runs (hooks and commands can be added here).
1+
import './commands';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export {};
2+
3+
declare global {
4+
namespace Cypress {
5+
interface Chainable {
6+
/**
7+
* `POST /v1/auth/e2e/session` then sets `token` and `refresh_token` cookies
8+
* on the spec origin (`baseUrl`). Requires backend `NODE_ENV=development`,
9+
* non-empty `E2E_AUTH_SECRET`, and Cypress env `E2E_AUTH_SECRET` + `API_URL`.
10+
* Defaults to the first seeded user email when no overrides are passed.
11+
*/
12+
sessionViaApi(overrides?: {
13+
email?: string;
14+
userId?: string;
15+
}): Chainable<void>;
16+
}
17+
}
18+
}

apps/e2e/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010
"start": "exit 0",
1111
"dev": "exit 0",
1212
"cy:open": "cypress open",
13-
"cy:run": "cypress run"
13+
"cy:run": "cypress run",
14+
"cy:baseline": "cypress run --spec cypress/e2e/snapshots/pages.cy.ts"
1415
},
1516
"dependencies": {
1617
"@nbw/config": "workspace:*"

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"format": "prettier --write .",
6868
"cy:open": "cd apps/e2e && bun run cy:open",
6969
"test:cy": "cd apps/e2e && bun run cy:run",
70+
"cy:baseline": "cd apps/e2e && bun run cy:baseline",
7071
"prepare": "husky",
7172
"lint-staged": "lint-staged"
7273
},

0 commit comments

Comments
 (0)