Add Cypress E2E tests with fixture-based API mocking for UI regression protection
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
29
cypress/e2e/accountAdmin.cy.js
Normal file
29
cypress/e2e/accountAdmin.cy.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { loginWithFixtures } from '../support/intercept';
|
||||
|
||||
describe('Account Management', () => {
|
||||
beforeEach(() => {
|
||||
loginWithFixtures();
|
||||
});
|
||||
|
||||
it('displays user list on account admin page', () => {
|
||||
cy.visit('/account-admin');
|
||||
cy.wait('@getUsers');
|
||||
// Should display users from fixture
|
||||
cy.contains('Test Admin').should('exist');
|
||||
cy.contains('Alice Wang').should('exist');
|
||||
cy.contains('Bob Chen').should('exist');
|
||||
});
|
||||
|
||||
it('shows active/inactive status badges', () => {
|
||||
cy.visit('/account-admin');
|
||||
cy.wait('@getUsers');
|
||||
// The user list should show status indicators
|
||||
cy.contains('testadmin').should('exist');
|
||||
});
|
||||
|
||||
it('navigates to my-account page', () => {
|
||||
cy.visit('/my-account');
|
||||
cy.wait('@getMyAccount');
|
||||
cy.url().should('include', '/my-account');
|
||||
});
|
||||
});
|
||||
55
cypress/e2e/files.cy.js
Normal file
55
cypress/e2e/files.cy.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { loginWithFixtures } from '../support/intercept';
|
||||
|
||||
describe('Files Page', () => {
|
||||
beforeEach(() => {
|
||||
loginWithFixtures();
|
||||
cy.visit('/files');
|
||||
});
|
||||
|
||||
it('displays the file list after login', () => {
|
||||
cy.wait('@getFiles');
|
||||
cy.contains('h2', 'All Files').should('exist');
|
||||
// Should display file names from fixture
|
||||
cy.contains('sample-process.xes').should('exist');
|
||||
cy.contains('filtered-sample').should('exist');
|
||||
cy.contains('production-log.csv').should('exist');
|
||||
});
|
||||
|
||||
it('shows Recently Used section', () => {
|
||||
cy.wait('@getFiles');
|
||||
cy.contains('h2', 'Recently Used').should('exist');
|
||||
});
|
||||
|
||||
it('switches to DISCOVER tab', () => {
|
||||
cy.wait('@getFiles');
|
||||
cy.contains('.nav-item', 'DISCOVER').click();
|
||||
// DISCOVER tab shows filtered file types
|
||||
cy.contains('h2', 'All Files').should('exist');
|
||||
});
|
||||
|
||||
it('switches to COMPARE tab and shows drag zones', () => {
|
||||
cy.wait('@getFiles');
|
||||
cy.contains('.nav-item', 'COMPARE').click();
|
||||
cy.contains('Performance Comparison').should('exist');
|
||||
cy.contains('Drag and drop a file here').should('exist');
|
||||
});
|
||||
|
||||
it('shows Import button on FILES tab', () => {
|
||||
cy.wait('@getFiles');
|
||||
cy.get('#import_btn').should('contain', 'Import');
|
||||
});
|
||||
|
||||
it('can switch between list and grid view', () => {
|
||||
cy.wait('@getFiles');
|
||||
// DataTable (list view) should be visible by default
|
||||
cy.get('table').should('exist');
|
||||
});
|
||||
|
||||
it('double-click file navigates to discover page', () => {
|
||||
cy.wait('@getFiles');
|
||||
// Double-click the first file row in the table
|
||||
// The actual route depends on file type (log→map, log-check→conformance, etc.)
|
||||
cy.get('table tbody tr').first().dblclick();
|
||||
cy.url().should('include', '/discover');
|
||||
});
|
||||
});
|
||||
71
cypress/e2e/login.cy.js
Normal file
71
cypress/e2e/login.cy.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { setupApiIntercepts, loginWithFixtures } from '../support/intercept';
|
||||
|
||||
describe('Login Flow', () => {
|
||||
beforeEach(() => {
|
||||
setupApiIntercepts();
|
||||
});
|
||||
|
||||
it('renders the login form', () => {
|
||||
cy.visit('/login');
|
||||
cy.get('h2').should('contain', 'LOGIN');
|
||||
cy.get('#account').should('exist');
|
||||
cy.get('#password').should('exist');
|
||||
cy.get('#login_btn_main_btn').should('be.disabled');
|
||||
});
|
||||
|
||||
it('login button is disabled when fields are empty', () => {
|
||||
cy.visit('/login');
|
||||
cy.get('#login_btn_main_btn').should('be.disabled');
|
||||
|
||||
// Only username filled — still disabled
|
||||
cy.get('#account').type('testuser');
|
||||
cy.get('#login_btn_main_btn').should('be.disabled');
|
||||
});
|
||||
|
||||
it('login button enables when both fields are filled', () => {
|
||||
cy.visit('/login');
|
||||
cy.get('#account').type('testadmin');
|
||||
cy.get('#password').type('password123');
|
||||
cy.get('#login_btn_main_btn').should('not.be.disabled');
|
||||
});
|
||||
|
||||
it('successful login redirects to /files', () => {
|
||||
cy.visit('/login');
|
||||
cy.get('#account').type('testadmin');
|
||||
cy.get('#password').type('password123');
|
||||
cy.get('#login_btn_main_btn').click();
|
||||
|
||||
cy.wait('@postToken');
|
||||
cy.url().should('include', '/files');
|
||||
});
|
||||
|
||||
it('failed login shows error message', () => {
|
||||
// Override the token intercept to return 401
|
||||
cy.intercept('POST', '/api/oauth/token', {
|
||||
statusCode: 401,
|
||||
body: { detail: 'Incorrect username or password' },
|
||||
}).as('postTokenFail');
|
||||
|
||||
cy.visit('/login');
|
||||
cy.get('#account').type('wronguser');
|
||||
cy.get('#password').type('wrongpass');
|
||||
cy.get('#login_btn_main_btn').click();
|
||||
|
||||
cy.wait('@postTokenFail');
|
||||
cy.contains('Incorrect account or password').should('be.visible');
|
||||
});
|
||||
|
||||
it('toggles password visibility', () => {
|
||||
cy.visit('/login');
|
||||
cy.get('#password').type('secret123');
|
||||
cy.get('#password').should('have.attr', 'type', 'password');
|
||||
|
||||
// Click the eye icon to show password
|
||||
cy.get('label[for="passwordt"] span.cursor-pointer').click();
|
||||
cy.get('#password').should('have.attr', 'type', 'text');
|
||||
|
||||
// Click again to hide
|
||||
cy.get('label[for="passwordt"] span.cursor-pointer').click();
|
||||
cy.get('#password').should('have.attr', 'type', 'password');
|
||||
});
|
||||
});
|
||||
57
cypress/e2e/navigation.cy.js
Normal file
57
cypress/e2e/navigation.cy.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import { loginWithFixtures, setupApiIntercepts } from '../support/intercept';
|
||||
|
||||
describe('Navigation and Routing', () => {
|
||||
it('redirects / to /files when logged in', () => {
|
||||
loginWithFixtures();
|
||||
cy.visit('/');
|
||||
cy.url().should('include', '/files');
|
||||
});
|
||||
|
||||
it('shows 404 page for unknown routes', () => {
|
||||
loginWithFixtures();
|
||||
cy.visit('/nonexistent-page');
|
||||
cy.contains('404').should('exist');
|
||||
});
|
||||
|
||||
it('navbar shows correct view name', () => {
|
||||
loginWithFixtures();
|
||||
cy.visit('/files');
|
||||
cy.wait('@getFiles');
|
||||
cy.get('#nav_bar').should('exist');
|
||||
cy.get('#nav_bar h2').should('contain', 'FILES');
|
||||
});
|
||||
|
||||
it('navbar shows back arrow on non-files pages', () => {
|
||||
loginWithFixtures();
|
||||
cy.visit('/discover/log/1/map');
|
||||
// Back arrow should be visible on discover pages
|
||||
cy.get('#backPage').should('exist');
|
||||
});
|
||||
|
||||
it('navbar tabs are clickable on discover page', () => {
|
||||
loginWithFixtures();
|
||||
cy.visit('/discover/log/1/map');
|
||||
// Discover navbar should show MAP, CONFORMANCE, PERFORMANCE tabs
|
||||
cy.contains('.nav-item', 'MAP').should('exist');
|
||||
cy.contains('.nav-item', 'CONFORMANCE').should('exist');
|
||||
cy.contains('.nav-item', 'PERFORMANCE').should('exist');
|
||||
|
||||
// Click CONFORMANCE tab
|
||||
cy.contains('.nav-item', 'CONFORMANCE').click();
|
||||
cy.url().should('include', '/conformance');
|
||||
|
||||
// Click PERFORMANCE tab
|
||||
cy.contains('.nav-item', 'PERFORMANCE').click();
|
||||
cy.url().should('include', '/performance');
|
||||
|
||||
// Click MAP tab to go back
|
||||
cy.contains('.nav-item', 'MAP').click();
|
||||
cy.url().should('include', '/map');
|
||||
});
|
||||
|
||||
it('login page is accessible at /login', () => {
|
||||
setupApiIntercepts();
|
||||
cy.visit('/login');
|
||||
cy.get('h2').should('contain', 'LOGIN');
|
||||
});
|
||||
});
|
||||
30
cypress/fixtures/api/discover.json
Normal file
30
cypress/fixtures/api/discover.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"stats": {
|
||||
"cases": { "count": 150, "total": 200, "ratio": 0.75 },
|
||||
"traces": { "count": 45, "total": 60, "ratio": 0.75 },
|
||||
"task_instances": { "count": 1200, "total": 1500, "ratio": 0.8 },
|
||||
"tasks": { "count": 12, "total": 15, "ratio": 0.8 },
|
||||
"started_at": "2025-01-01T00:00:00Z",
|
||||
"completed_at": "2025-06-01T00:00:00Z",
|
||||
"case_duration": {
|
||||
"min": 3600,
|
||||
"max": 864000,
|
||||
"average": 172800,
|
||||
"median": 86400
|
||||
}
|
||||
},
|
||||
"graph": {
|
||||
"nodes": [
|
||||
{ "id": "start", "label": "Start", "type": "start" },
|
||||
{ "id": "task_a", "label": "Task A", "type": "task" },
|
||||
{ "id": "task_b", "label": "Task B", "type": "task" },
|
||||
{ "id": "end", "label": "End", "type": "end" }
|
||||
],
|
||||
"edges": [
|
||||
{ "source": "start", "target": "task_a", "count": 150 },
|
||||
{ "source": "task_a", "target": "task_b", "count": 120 },
|
||||
{ "source": "task_b", "target": "end", "count": 120 },
|
||||
{ "source": "task_a", "target": "end", "count": 30 }
|
||||
]
|
||||
}
|
||||
}
|
||||
42
cypress/fixtures/api/files.json
Normal file
42
cypress/fixtures/api/files.json
Normal file
@@ -0,0 +1,42 @@
|
||||
[
|
||||
{
|
||||
"type": "log",
|
||||
"id": 1,
|
||||
"name": "sample-process.xes",
|
||||
"parent": null,
|
||||
"owner": { "username": "testadmin", "name": "Test Admin" },
|
||||
"updated_at": "2025-06-10T14:30:00Z",
|
||||
"accessed_at": "2025-06-12T09:00:00Z",
|
||||
"is_deleted": false
|
||||
},
|
||||
{
|
||||
"type": "filter",
|
||||
"id": 10,
|
||||
"name": "filtered-sample",
|
||||
"parent": { "type": "log", "id": 1, "name": "sample-process.xes" },
|
||||
"owner": { "username": "testadmin", "name": "Test Admin" },
|
||||
"updated_at": "2025-06-11T08:00:00Z",
|
||||
"accessed_at": "2025-06-12T10:00:00Z",
|
||||
"is_deleted": false
|
||||
},
|
||||
{
|
||||
"type": "log",
|
||||
"id": 2,
|
||||
"name": "production-log.csv",
|
||||
"parent": null,
|
||||
"owner": { "username": "user1", "name": "Alice Wang" },
|
||||
"updated_at": "2025-06-09T16:00:00Z",
|
||||
"accessed_at": null,
|
||||
"is_deleted": false
|
||||
},
|
||||
{
|
||||
"type": "log-check",
|
||||
"id": 100,
|
||||
"name": "conformance-check-1",
|
||||
"parent": { "type": "log", "id": 1, "name": "sample-process.xes" },
|
||||
"owner": { "username": "testadmin", "name": "Test Admin" },
|
||||
"updated_at": "2025-06-11T12:00:00Z",
|
||||
"accessed_at": null,
|
||||
"is_deleted": false
|
||||
}
|
||||
]
|
||||
9
cypress/fixtures/api/my-account.json
Normal file
9
cypress/fixtures/api/my-account.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"username": "testadmin",
|
||||
"name": "Test Admin",
|
||||
"is_sso": false,
|
||||
"created_at": "2025-01-15T10:00:00Z",
|
||||
"roles": [
|
||||
{ "code": "admin", "name": "Administrator" }
|
||||
]
|
||||
}
|
||||
18
cypress/fixtures/api/performance.json
Normal file
18
cypress/fixtures/api/performance.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"cycle_time": {
|
||||
"labels": ["Task A", "Task B"],
|
||||
"data": [7200, 3600]
|
||||
},
|
||||
"processing_time": {
|
||||
"labels": ["Task A", "Task B"],
|
||||
"data": [5400, 2700]
|
||||
},
|
||||
"waiting_time": {
|
||||
"labels": ["Task A → Task B", "Task B → End"],
|
||||
"data": [1800, 900]
|
||||
},
|
||||
"frequency": {
|
||||
"labels": ["Task A", "Task B"],
|
||||
"data": [150, 120]
|
||||
}
|
||||
}
|
||||
6
cypress/fixtures/api/token.json
Normal file
6
cypress/fixtures/api/token.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"access_token": "fake-access-token-for-testing",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "fake-refresh-token-for-testing"
|
||||
}
|
||||
16
cypress/fixtures/api/traces.json
Normal file
16
cypress/fixtures/api/traces.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"traces": [
|
||||
{
|
||||
"id": "trace-001",
|
||||
"case_id": "CASE-001",
|
||||
"count": 50,
|
||||
"activities": ["Task A", "Task B", "End"]
|
||||
},
|
||||
{
|
||||
"id": "trace-002",
|
||||
"case_id": "CASE-002",
|
||||
"count": 30,
|
||||
"activities": ["Task A", "End"]
|
||||
}
|
||||
]
|
||||
}
|
||||
26
cypress/fixtures/api/users.json
Normal file
26
cypress/fixtures/api/users.json
Normal file
@@ -0,0 +1,26 @@
|
||||
[
|
||||
{
|
||||
"username": "testadmin",
|
||||
"name": "Test Admin",
|
||||
"is_admin": true,
|
||||
"is_active": true,
|
||||
"is_sso": false,
|
||||
"has_data": true
|
||||
},
|
||||
{
|
||||
"username": "user1",
|
||||
"name": "Alice Wang",
|
||||
"is_admin": false,
|
||||
"is_active": true,
|
||||
"is_sso": false,
|
||||
"has_data": true
|
||||
},
|
||||
{
|
||||
"username": "user2",
|
||||
"name": "Bob Chen",
|
||||
"is_admin": false,
|
||||
"is_active": false,
|
||||
"is_sso": true,
|
||||
"has_data": false
|
||||
}
|
||||
]
|
||||
119
cypress/support/intercept.js
Normal file
119
cypress/support/intercept.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Sets up cy.intercept for all API endpoints using fixture files.
|
||||
* Call setupApiIntercepts() in beforeEach to mock the entire backend.
|
||||
*/
|
||||
export function setupApiIntercepts() {
|
||||
// Auth
|
||||
cy.intercept('POST', '/api/oauth/token', {
|
||||
fixture: 'api/token.json',
|
||||
}).as('postToken');
|
||||
|
||||
// User account
|
||||
cy.intercept('GET', '/api/my-account', {
|
||||
fixture: 'api/my-account.json',
|
||||
}).as('getMyAccount');
|
||||
|
||||
cy.intercept('PUT', '/api/my-account', {
|
||||
statusCode: 200,
|
||||
body: { success: true },
|
||||
}).as('putMyAccount');
|
||||
|
||||
// Files
|
||||
cy.intercept('GET', '/api/files', {
|
||||
fixture: 'api/files.json',
|
||||
}).as('getFiles');
|
||||
|
||||
// Users (account management)
|
||||
cy.intercept('GET', '/api/users', {
|
||||
fixture: 'api/users.json',
|
||||
}).as('getUsers');
|
||||
|
||||
cy.intercept('POST', '/api/users', {
|
||||
statusCode: 200,
|
||||
body: { success: true },
|
||||
}).as('postUser');
|
||||
|
||||
cy.intercept('DELETE', '/api/users/*', {
|
||||
statusCode: 200,
|
||||
body: { success: true },
|
||||
}).as('deleteUser');
|
||||
|
||||
cy.intercept('PUT', '/api/users/*', {
|
||||
statusCode: 200,
|
||||
body: { success: true },
|
||||
}).as('putUser');
|
||||
|
||||
// Discover (map data)
|
||||
cy.intercept('GET', '/api/logs/*/discover', {
|
||||
fixture: 'api/discover.json',
|
||||
}).as('getDiscover');
|
||||
|
||||
cy.intercept('GET', '/api/filters/*/discover', {
|
||||
fixture: 'api/discover.json',
|
||||
}).as('getFilterDiscover');
|
||||
|
||||
// Performance
|
||||
cy.intercept('GET', '/api/logs/*/performance', {
|
||||
fixture: 'api/performance.json',
|
||||
}).as('getPerformance');
|
||||
|
||||
cy.intercept('GET', '/api/filters/*/performance', {
|
||||
fixture: 'api/performance.json',
|
||||
}).as('getFilterPerformance');
|
||||
|
||||
// Traces
|
||||
cy.intercept('GET', '/api/logs/*/traces', {
|
||||
fixture: 'api/traces.json',
|
||||
}).as('getTraces');
|
||||
|
||||
cy.intercept('GET', '/api/filters/*/traces', {
|
||||
fixture: 'api/traces.json',
|
||||
}).as('getFilterTraces');
|
||||
|
||||
// Temp filters
|
||||
cy.intercept('GET', '/api/temp-filters/*/discover', {
|
||||
fixture: 'api/discover.json',
|
||||
}).as('getTempFilterDiscover');
|
||||
|
||||
cy.intercept('GET', '/api/temp-filters/*/traces', {
|
||||
fixture: 'api/traces.json',
|
||||
}).as('getTempFilterTraces');
|
||||
|
||||
// Filter params
|
||||
cy.intercept('GET', '/api/filters/params*', {
|
||||
statusCode: 200,
|
||||
body: {},
|
||||
}).as('getFilterParams');
|
||||
|
||||
cy.intercept('GET', '/api/filters/has-result*', {
|
||||
statusCode: 200,
|
||||
body: false,
|
||||
}).as('getFilterHasResult');
|
||||
|
||||
// Conformance check params
|
||||
cy.intercept('GET', '/api/log-checks/params*', {
|
||||
statusCode: 200,
|
||||
body: {},
|
||||
}).as('getLogCheckParams');
|
||||
|
||||
cy.intercept('GET', '/api/filter-checks/params*', {
|
||||
statusCode: 200,
|
||||
body: {},
|
||||
}).as('getFilterCheckParams');
|
||||
|
||||
// Deletion
|
||||
cy.intercept('DELETE', '/api/deletion/*', {
|
||||
statusCode: 200,
|
||||
body: { success: true },
|
||||
}).as('deleteDeletion');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the luciaToken cookie and isLuciaLoggedIn cookie to simulate
|
||||
* a logged-in state, then sets up all API intercepts.
|
||||
*/
|
||||
export function loginWithFixtures() {
|
||||
setupApiIntercepts();
|
||||
cy.setCookie('luciaToken', 'fake-access-token-for-testing');
|
||||
cy.setCookie('isLuciaLoggedIn', 'true');
|
||||
}
|
||||
Reference in New Issue
Block a user