diff --git a/cypress.config.js b/cypress.config.js index a873102..d938268 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -6,10 +6,11 @@ module.exports = defineConfig({ viewportWidth: 1280, viewportHeight:720, e2e: { + baseUrl: "http://localhost:5173", specPattern: "cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}", }, includeShadowDom: true, env: { - loginPwd: 'REDACTED-PWD', + // Removed hardcoded password — use cypress.env.json for real credentials } }); diff --git a/cypress/e2e/accountAdmin.cy.js b/cypress/e2e/accountAdmin.cy.js new file mode 100644 index 0000000..32f2329 --- /dev/null +++ b/cypress/e2e/accountAdmin.cy.js @@ -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'); + }); +}); diff --git a/cypress/e2e/files.cy.js b/cypress/e2e/files.cy.js new file mode 100644 index 0000000..570e31e --- /dev/null +++ b/cypress/e2e/files.cy.js @@ -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'); + }); +}); diff --git a/cypress/e2e/login.cy.js b/cypress/e2e/login.cy.js new file mode 100644 index 0000000..4bb626b --- /dev/null +++ b/cypress/e2e/login.cy.js @@ -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'); + }); +}); diff --git a/cypress/e2e/navigation.cy.js b/cypress/e2e/navigation.cy.js new file mode 100644 index 0000000..d8fdc3e --- /dev/null +++ b/cypress/e2e/navigation.cy.js @@ -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'); + }); +}); diff --git a/cypress/fixtures/api/discover.json b/cypress/fixtures/api/discover.json new file mode 100644 index 0000000..d592293 --- /dev/null +++ b/cypress/fixtures/api/discover.json @@ -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 } + ] + } +} diff --git a/cypress/fixtures/api/files.json b/cypress/fixtures/api/files.json new file mode 100644 index 0000000..c27c8d2 --- /dev/null +++ b/cypress/fixtures/api/files.json @@ -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 + } +] diff --git a/cypress/fixtures/api/my-account.json b/cypress/fixtures/api/my-account.json new file mode 100644 index 0000000..0a6119c --- /dev/null +++ b/cypress/fixtures/api/my-account.json @@ -0,0 +1,9 @@ +{ + "username": "testadmin", + "name": "Test Admin", + "is_sso": false, + "created_at": "2025-01-15T10:00:00Z", + "roles": [ + { "code": "admin", "name": "Administrator" } + ] +} diff --git a/cypress/fixtures/api/performance.json b/cypress/fixtures/api/performance.json new file mode 100644 index 0000000..33b9ad8 --- /dev/null +++ b/cypress/fixtures/api/performance.json @@ -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] + } +} diff --git a/cypress/fixtures/api/token.json b/cypress/fixtures/api/token.json new file mode 100644 index 0000000..fe6e0c6 --- /dev/null +++ b/cypress/fixtures/api/token.json @@ -0,0 +1,6 @@ +{ + "access_token": "fake-access-token-for-testing", + "token_type": "bearer", + "expires_in": 3600, + "refresh_token": "fake-refresh-token-for-testing" +} diff --git a/cypress/fixtures/api/traces.json b/cypress/fixtures/api/traces.json new file mode 100644 index 0000000..0c6ae51 --- /dev/null +++ b/cypress/fixtures/api/traces.json @@ -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"] + } + ] +} diff --git a/cypress/fixtures/api/users.json b/cypress/fixtures/api/users.json new file mode 100644 index 0000000..4552cc8 --- /dev/null +++ b/cypress/fixtures/api/users.json @@ -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 + } +] diff --git a/cypress/support/intercept.js b/cypress/support/intercept.js new file mode 100644 index 0000000..2124e45 --- /dev/null +++ b/cypress/support/intercept.js @@ -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'); +}