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:
committed by
GitHub
co-authored by
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
RMCampos
rmcampos
parent
bccd30c1a9
commit
1aa8ded9b3
@@ -7,6 +7,8 @@ client.pnp.js
|
||||
|
||||
# testing
|
||||
client/coverage
|
||||
client/cypress/videos
|
||||
client/cypress/screenshots
|
||||
|
||||
# production
|
||||
client/build
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 {};
|
||||
@@ -0,0 +1,4 @@
|
||||
// Support file for Cypress E2E tests.
|
||||
// This file is loaded before each spec file.
|
||||
|
||||
import './commands';
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -21,6 +21,7 @@ export default [
|
||||
{
|
||||
ignores: [
|
||||
'**/__test__/*',
|
||||
'cypress/**',
|
||||
'**/assets/*',
|
||||
'**/*.scss',
|
||||
'**/*.css',
|
||||
|
||||
Generated
+1692
-1
File diff suppressed because it is too large
Load Diff
+5
-1
@@ -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
@@ -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;
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user