Skip to content
This repository was archived by the owner on Jun 15, 2026. It is now read-only.

Commit e05ecae

Browse files
CopilotRMCampos
andauthored
feat: Integrate Cypress for E2E authentication testing (#39)
* Initial plan * feat: add Cypress E2E testing for auth flows Agent-Logs-Url: https://github.com/RMCampos/tasknote/sessions/f3b4e747-3e09-4b6c-8b4c-336cf45b59c4 Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> * fix: use regex intercepts and unique emails in Cypress auth tests Agent-Logs-Url: https://github.com/RMCampos/tasknote/sessions/2a1225d9-9412-459e-bc97-93303454a461 Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> * fix: use PUT method for sign-up intercepts in Cypress auth tests Agent-Logs-Url: https://github.com/RMCampos/tasknote/sessions/bda14dfc-a30d-4c01-aa74-e8c2690072e3 Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> * chore: update tools.md file to include sql command --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RMCampos <2219519+RMCampos@users.noreply.github.com> Co-authored-by: Ricardo Campos <ricardompcampos@gmail.com>
1 parent 44b29d9 commit e05ecae

10 files changed

Lines changed: 1919 additions & 11 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ client.pnp.js
77

88
# testing
99
client/coverage
10+
client/cypress/videos
11+
client/cypress/screenshots
1012

1113
# production
1214
client/build

client/cypress.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineConfig } from 'cypress';
2+
3+
export default defineConfig({
4+
e2e: {
5+
baseUrl: 'http://localhost:5000',
6+
specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
7+
supportFile: 'cypress/support/e2e.ts',
8+
videosFolder: 'cypress/videos',
9+
screenshotsFolder: 'cypress/screenshots',
10+
viewportWidth: 1280,
11+
viewportHeight: 720,
12+
setupNodeEvents(on, config) {
13+
return config;
14+
}
15+
}
16+
});

client/cypress/e2e/auth.cy.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/**
2+
* E2E tests for authentication flows: User Signup and Login.
3+
*
4+
* These tests cover the primary user journeys for account creation
5+
* and logging in to the TaskNote application.
6+
*/
7+
describe('Authentication', () => {
8+
/**
9+
* User Signup (Registration) flow
10+
*/
11+
describe('Signup', () => {
12+
beforeEach(() => {
13+
cy.visit('/register');
14+
});
15+
16+
it('displays the registration form', () => {
17+
cy.contains('h2', 'Create account').should('be.visible');
18+
cy.get('input[name="email"]').should('be.visible');
19+
cy.get('input[name="password"]').should('be.visible');
20+
cy.get('input[name="passwordAgain"]').should('be.visible');
21+
cy.get('button[type="submit"]').should('be.visible').and('contain', 'Create account');
22+
});
23+
24+
it('shows a validation error when the form is submitted empty', () => {
25+
cy.get('button[type="submit"]').click();
26+
cy.get('.alert-danger').should('be.visible');
27+
});
28+
29+
it('shows a validation error when passwords do not match', () => {
30+
cy.get('input[name="email"]').type('testuser@example.com');
31+
cy.get('input[name="password"]').type('Password1!');
32+
cy.get('input[name="passwordAgain"]').type('DifferentPass1!');
33+
cy.get('button[type="submit"]').click();
34+
cy.get('.alert-danger').should('be.visible');
35+
});
36+
37+
it('navigates to the login page via the login link', () => {
38+
cy.contains('a', 'Login').first().click();
39+
cy.url().should('include', '/login');
40+
});
41+
42+
it('navigates back to the landing page via the back-to-home link', () => {
43+
cy.contains('a', 'Back to home').click();
44+
cy.url().should('eq', `${Cypress.config('baseUrl')}/`);
45+
});
46+
47+
it('submits the signup form and shows a confirmation message on success', () => {
48+
cy.intercept({ method: 'PUT', url: /\/auth\/sign-up/ }, {
49+
statusCode: 201,
50+
body: { message: 'User created' }
51+
}).as('signUp');
52+
53+
cy.get('input[name="email"]').type(`newuser${Date.now()}@example.com`);
54+
cy.get('input[name="password"]').type('Password1!');
55+
cy.get('input[name="passwordAgain"]').type('Password1!');
56+
cy.get('button[type="submit"]').click();
57+
58+
cy.wait('@signUp');
59+
cy.get('.alert-success').should('be.visible');
60+
});
61+
62+
it('displays an error alert when the API returns an error on signup', () => {
63+
cy.intercept({ method: 'PUT', url: /\/auth\/sign-up/ }, {
64+
statusCode: 409,
65+
body: { message: 'Email already in use' }
66+
}).as('signUpFail');
67+
68+
cy.get('input[name="email"]').type(`existing${Date.now()}@example.com`);
69+
cy.get('input[name="password"]').type('Password1!');
70+
cy.get('input[name="passwordAgain"]').type('Password1!');
71+
cy.get('button[type="submit"]').click();
72+
73+
cy.wait('@signUpFail');
74+
cy.get('.alert-danger').should('be.visible');
75+
});
76+
});
77+
78+
/**
79+
* User Login flow
80+
*/
81+
describe('Login', () => {
82+
beforeEach(() => {
83+
cy.visit('/login');
84+
});
85+
86+
it('displays the login form', () => {
87+
cy.contains('h2', 'Login').should('be.visible');
88+
cy.get('input[name="email"]').should('be.visible');
89+
cy.get('input[name="password"]').should('be.visible');
90+
cy.get('button[type="submit"]').should('be.visible').and('contain', 'Login');
91+
});
92+
93+
it('shows a validation error when the form is submitted empty', () => {
94+
cy.get('button[type="submit"]').click();
95+
cy.get('.alert-danger').should('be.visible');
96+
});
97+
98+
it('navigates to the register page via the create account link', () => {
99+
cy.contains('a', 'Create account').click();
100+
cy.url().should('include', '/register');
101+
});
102+
103+
it('navigates back to the landing page via the back-to-home link', () => {
104+
cy.contains('a', 'Back to home').click();
105+
cy.url().should('eq', `${Cypress.config('baseUrl')}/`);
106+
});
107+
108+
it('navigates to the reset password page via the forgot password link', () => {
109+
cy.contains('a', 'Forgot your password?').click();
110+
cy.url().should('include', '/reset-password');
111+
});
112+
113+
it('redirects to home after a successful login', () => {
114+
cy.intercept({ method: 'POST', url: /\/auth\/sign-in/ }, {
115+
statusCode: 200,
116+
body: { token: 'fake-jwt-token', email: 'user@example.com' }
117+
}).as('signIn');
118+
119+
cy.get('input[name="email"]').type('user@example.com');
120+
cy.get('input[name="password"]').type('Password1!');
121+
cy.get('button[type="submit"]').click();
122+
123+
cy.wait('@signIn');
124+
cy.url().should('include', '/home');
125+
});
126+
127+
it('displays an error alert when the API returns an error on login', () => {
128+
cy.intercept({ method: 'POST', url: /\/auth\/sign-in/ }, {
129+
statusCode: 401,
130+
body: { message: 'Invalid credentials' }
131+
}).as('signInFail');
132+
133+
cy.get('input[name="email"]').type('user@example.com');
134+
cy.get('input[name="password"]').type('wrongpassword');
135+
cy.get('button[type="submit"]').click();
136+
137+
cy.wait('@signInFail');
138+
cy.get('.alert-danger').should('be.visible');
139+
});
140+
});
141+
});

client/cypress/support/commands.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Custom Cypress commands.
2+
// Add any project-wide custom commands here.
3+
4+
// Prevent TypeScript errors for cy.login usage
5+
declare global {
6+
// eslint-disable-next-line @typescript-eslint/no-namespace
7+
namespace Cypress {
8+
interface Chainable {
9+
/**
10+
* Custom command to log in via the UI.
11+
* @param email - User email address
12+
* @param password - User password
13+
*/
14+
login(email: string, password: string): Chainable<void>;
15+
}
16+
}
17+
}
18+
19+
Cypress.Commands.add('login', (email: string, password: string) => {
20+
cy.visit('/login');
21+
cy.get('input[name="email"]').clear().type(email);
22+
cy.get('input[name="password"]').clear().type(password);
23+
cy.get('button[type="submit"]').click();
24+
});
25+
26+
export {};

client/cypress/support/e2e.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Support file for Cypress E2E tests.
2+
// This file is loaded before each spec file.
3+
4+
import './commands';

client/cypress/tsconfig.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"lib": ["ESNext", "dom"],
5+
"module": "ESNext",
6+
"moduleResolution": "bundler",
7+
"types": ["cypress", "node"],
8+
"esModuleInterop": true,
9+
"allowSyntheticDefaultImports": true,
10+
"strict": false,
11+
"skipLibCheck": true
12+
},
13+
"include": [
14+
"./**/*.ts",
15+
"./**/*.tsx"
16+
]
17+
}

client/eslint.config.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default [
2121
{
2222
ignores: [
2323
'**/__test__/*',
24+
'cypress/**',
2425
'**/assets/*',
2526
'**/*.scss',
2627
'**/*.css',

0 commit comments

Comments
 (0)