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>
This commit is contained in:
Copilot
2026-05-06 17:21:17 -03:00
committed by GitHub
co-authored by copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> RMCampos rmcampos
parent bccd30c1a9
commit 1aa8ded9b3
10 changed files with 1911 additions and 3 deletions
+2
View File
@@ -7,6 +7,8 @@ client.pnp.js
# testing
client/coverage
client/cypress/videos
client/cypress/screenshots
# production
client/build
+16
View File
@@ -0,0 +1,16 @@
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:5000',
specPattern: 'cypress/e2e/**/*.cy.{ts,tsx}',
supportFile: 'cypress/support/e2e.ts',
videosFolder: 'cypress/videos',
screenshotsFolder: 'cypress/screenshots',
viewportWidth: 1280,
viewportHeight: 720,
setupNodeEvents(on, config) {
return config;
}
}
});
+141
View File
@@ -0,0 +1,141 @@
/**
* E2E tests for authentication flows: User Signup and Login.
*
* These tests cover the primary user journeys for account creation
* and logging in to the TaskNote application.
*/
describe('Authentication', () => {
/**
* User Signup (Registration) flow
*/
describe('Signup', () => {
beforeEach(() => {
cy.visit('/register');
});
it('displays the registration form', () => {
cy.contains('h2', 'Create account').should('be.visible');
cy.get('input[name="email"]').should('be.visible');
cy.get('input[name="password"]').should('be.visible');
cy.get('input[name="passwordAgain"]').should('be.visible');
cy.get('button[type="submit"]').should('be.visible').and('contain', 'Create account');
});
it('shows a validation error when the form is submitted empty', () => {
cy.get('button[type="submit"]').click();
cy.get('.alert-danger').should('be.visible');
});
it('shows a validation error when passwords do not match', () => {
cy.get('input[name="email"]').type('testuser@example.com');
cy.get('input[name="password"]').type('Password1!');
cy.get('input[name="passwordAgain"]').type('DifferentPass1!');
cy.get('button[type="submit"]').click();
cy.get('.alert-danger').should('be.visible');
});
it('navigates to the login page via the login link', () => {
cy.contains('a', 'Login').first().click();
cy.url().should('include', '/login');
});
it('navigates back to the landing page via the back-to-home link', () => {
cy.contains('a', 'Back to home').click();
cy.url().should('eq', `${Cypress.config('baseUrl')}/`);
});
it('submits the signup form and shows a confirmation message on success', () => {
cy.intercept({ method: 'PUT', url: /\/auth\/sign-up/ }, {
statusCode: 201,
body: { message: 'User created' }
}).as('signUp');
cy.get('input[name="email"]').type(`newuser${Date.now()}@example.com`);
cy.get('input[name="password"]').type('Password1!');
cy.get('input[name="passwordAgain"]').type('Password1!');
cy.get('button[type="submit"]').click();
cy.wait('@signUp');
cy.get('.alert-success').should('be.visible');
});
it('displays an error alert when the API returns an error on signup', () => {
cy.intercept({ method: 'PUT', url: /\/auth\/sign-up/ }, {
statusCode: 409,
body: { message: 'Email already in use' }
}).as('signUpFail');
cy.get('input[name="email"]').type(`existing${Date.now()}@example.com`);
cy.get('input[name="password"]').type('Password1!');
cy.get('input[name="passwordAgain"]').type('Password1!');
cy.get('button[type="submit"]').click();
cy.wait('@signUpFail');
cy.get('.alert-danger').should('be.visible');
});
});
/**
* User Login flow
*/
describe('Login', () => {
beforeEach(() => {
cy.visit('/login');
});
it('displays the login form', () => {
cy.contains('h2', 'Login').should('be.visible');
cy.get('input[name="email"]').should('be.visible');
cy.get('input[name="password"]').should('be.visible');
cy.get('button[type="submit"]').should('be.visible').and('contain', 'Login');
});
it('shows a validation error when the form is submitted empty', () => {
cy.get('button[type="submit"]').click();
cy.get('.alert-danger').should('be.visible');
});
it('navigates to the register page via the create account link', () => {
cy.contains('a', 'Create account').click();
cy.url().should('include', '/register');
});
it('navigates back to the landing page via the back-to-home link', () => {
cy.contains('a', 'Back to home').click();
cy.url().should('eq', `${Cypress.config('baseUrl')}/`);
});
it('navigates to the reset password page via the forgot password link', () => {
cy.contains('a', 'Forgot your password?').click();
cy.url().should('include', '/reset-password');
});
it('redirects to home after a successful login', () => {
cy.intercept({ method: 'POST', url: /\/auth\/sign-in/ }, {
statusCode: 200,
body: { token: 'fake-jwt-token', email: 'user@example.com' }
}).as('signIn');
cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="password"]').type('Password1!');
cy.get('button[type="submit"]').click();
cy.wait('@signIn');
cy.url().should('include', '/home');
});
it('displays an error alert when the API returns an error on login', () => {
cy.intercept({ method: 'POST', url: /\/auth\/sign-in/ }, {
statusCode: 401,
body: { message: 'Invalid credentials' }
}).as('signInFail');
cy.get('input[name="email"]').type('user@example.com');
cy.get('input[name="password"]').type('wrongpassword');
cy.get('button[type="submit"]').click();
cy.wait('@signInFail');
cy.get('.alert-danger').should('be.visible');
});
});
});
+26
View File
@@ -0,0 +1,26 @@
// Custom Cypress commands.
// Add any project-wide custom commands here.
// Prevent TypeScript errors for cy.login usage
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Custom command to log in via the UI.
* @param email - User email address
* @param password - User password
*/
login(email: string, password: string): Chainable<void>;
}
}
}
Cypress.Commands.add('login', (email: string, password: string) => {
cy.visit('/login');
cy.get('input[name="email"]').clear().type(email);
cy.get('input[name="password"]').clear().type(password);
cy.get('button[type="submit"]').click();
});
export {};
+4
View File
@@ -0,0 +1,4 @@
// Support file for Cypress E2E tests.
// This file is loaded before each spec file.
import './commands';
+17
View File
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ESNext",
"lib": ["ESNext", "dom"],
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["cypress", "node"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": false,
"skipLibCheck": true
},
"include": [
"./**/*.ts",
"./**/*.tsx"
]
}
+1
View File
@@ -21,6 +21,7 @@ export default [
{
ignores: [
'**/__test__/*',
'cypress/**',
'**/assets/*',
'**/*.scss',
'**/*.css',
+1692 -1
View File
File diff suppressed because it is too large Load Diff
+5 -1
View File
@@ -41,7 +41,10 @@
"test:no-watch": "vitest --watch=false",
"test:coverage": "vitest run --coverage --watch=false",
"lint": "./node_modules/.bin/eslint",
"lint:fix": "./node_modules/.bin/eslint --fix"
"lint:fix": "./node_modules/.bin/eslint --fix",
"cypress:open": "cypress open",
"cypress:run": "cypress run",
"cypress:run:headless": "cypress run --headless"
},
"eslintConfig": {
"extends": [
@@ -73,6 +76,7 @@
"@types/react": "^19.2.14",
"@types/react-router-bootstrap": "^0.26.8",
"@vitest/coverage-v8": "^4.1.5",
"cypress": "^15.14.2",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0",
+7 -1
View File
@@ -180,4 +180,10 @@ mvn test -P test -Dtest=YourTestClassName
# For a single class method
mvn test -P test -Dtest=YourTestClassName#method
```
```
## Running SQL query from the CLI using docker
```bash
docker exec -it tasknote-db psql -U tasknoteuser -d tasknote -c "SET search_path TO tasknote; SELECT * FROM users;
```