diff --git a/cypress.config.js b/cypress.config.js index 082535a4..3ec98bd0 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,73 +1,62 @@ -const { defineConfig } = require("cypress"); -const net = require("net"); +const { defineConfig } = require('cypress') +const net = require('net') -const DEFAULT_DB_HOST = - process.env.CYPRESS_DB_HOST ?? process.env.MARIADB_HOST ?? "127.0.0.1"; -const DEFAULT_DB_PORT = Number( - process.env.CYPRESS_DB_PORT ?? process.env.MARIADB_PORT ?? 3306, -); -const DEFAULT_DB_WAIT_TIMEOUT_MS = Number( - process.env.CYPRESS_DB_WAIT_TIMEOUT ?? 30_000, -); +const DEFAULT_DB_HOST = process.env.CYPRESS_DB_HOST ?? process.env.MARIADB_HOST ?? '127.0.0.1' +const DEFAULT_DB_PORT = Number(process.env.CYPRESS_DB_PORT ?? process.env.MARIADB_PORT ?? 3306) +const DEFAULT_DB_WAIT_TIMEOUT_MS = Number(process.env.CYPRESS_DB_WAIT_TIMEOUT ?? 30_000) -const RETRY_DELAY_MS = 1_000; -const SOCKET_TIMEOUT_MS = 2_000; +const RETRY_DELAY_MS = 1_000 +const SOCKET_TIMEOUT_MS = 2_000 /** * @param {{ host?: string; port?: number; timeoutMs?: number }} [options] * @param {{ dbHost?: string; dbPort?: number; dbWaitTimeoutMs?: number }} [env] */ async function waitForDatabase(options = {}, env = {}) { - const host = options.host ?? env.dbHost ?? DEFAULT_DB_HOST; - const port = Number(options.port ?? env.dbPort ?? DEFAULT_DB_PORT); - const timeoutMs = Number( - options.timeoutMs ?? env.dbWaitTimeoutMs ?? DEFAULT_DB_WAIT_TIMEOUT_MS, - ); + const host = options.host ?? env.dbHost ?? DEFAULT_DB_HOST + const port = Number(options.port ?? env.dbPort ?? DEFAULT_DB_PORT) + const timeoutMs = Number(options.timeoutMs ?? env.dbWaitTimeoutMs ?? DEFAULT_DB_WAIT_TIMEOUT_MS) if (!Number.isFinite(port)) { - throw new Error(`Invalid database port provided for waitForDbHealthy: ${port}`); + throw new Error(`Invalid database port provided for waitForDbHealthy: ${port}`) } - const deadline = Date.now() + timeoutMs; + const deadline = Date.now() + timeoutMs return new Promise((resolve, reject) => { const attemptConnection = () => { - const socket = net.createConnection({ host, port }); + const socket = net.createConnection({ host, port }) const cleanup = () => { - socket.removeAllListeners(); - }; + socket.removeAllListeners() + } - const scheduleRetry = (error) => { - cleanup(); - socket.destroy(); + const scheduleRetry = error => { + cleanup() + socket.destroy() if (Date.now() >= deadline) { - reject( - new Error( - `Timed out waiting for MariaDB at ${host}:${port} (${error.message})`, - ), - ); - return; + reject(new Error(`Timed out waiting for MariaDB at ${host}:${port} (${error.message})`)) + return } - setTimeout(attemptConnection, RETRY_DELAY_MS); - }; + setTimeout(attemptConnection, RETRY_DELAY_MS) + } - socket.once("connect", () => { - cleanup(); - socket.end(); - resolve(null); - }); + socket.once('connect', () => { + cleanup() + socket.end() + resolve(null) + }) - socket.once("error", scheduleRetry); + socket.once('error', scheduleRetry) socket.setTimeout(SOCKET_TIMEOUT_MS, () => { - scheduleRetry(new Error("Connection attempt timed out")); - }); - }; + scheduleRetry(new Error('Connection attempt timed out')) + }) + } - attemptConnection(); - }); + attemptConnection() + }) } module.exports = defineConfig({ @@ -86,17 +75,14 @@ module.exports = defineConfig({ return config }, baseUrl: 'http://localhost:5173/', - specPattern: [ - 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', - 'cypress/e2e/**/*.spec.{js,jsx,ts,tsx}', - ], + specPattern: ['cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', 'cypress/e2e/**/*.spec.{js,jsx,ts,tsx}'], }, env: { - databaseResetUrl: "http://localhost:4000/test/reset-test-database", + databaseResetUrl: 'http://localhost:4000/test/reset-test-database', dbHost: DEFAULT_DB_HOST, dbPort: DEFAULT_DB_PORT, dbWaitTimeoutMs: DEFAULT_DB_WAIT_TIMEOUT_MS, }, viewportHeight: 900, viewportWidth: 1200, -}); +}) diff --git a/cypress/e2e/asAdmin.cy.js b/cypress/e2e/asAdmin.cy.js index c6611d9f..1f9fefbb 100644 --- a/cypress/e2e/asAdmin.cy.js +++ b/cypress/e2e/asAdmin.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Open each page, table view and detail view, and check at least some correct text appears', () => { diff --git a/cypress/e2e/delete.spec.ts b/cypress/e2e/delete.spec.ts index d4898ff1..700cd4bb 100644 --- a/cypress/e2e/delete.spec.ts +++ b/cypress/e2e/delete.spec.ts @@ -21,8 +21,7 @@ const createRegion = (regionTarget: DeleteTarget, name: string) => { describe('Delete flow', () => { before('Wait for database and reset', () => { - cy.task('waitForDbHealthy') - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) beforeEach('Login as delete-capable coordinator', () => { @@ -38,26 +37,28 @@ describe('Delete flow', () => { const name = regionName('Success') - cy.wrap(null).then(() => createRegion(regionTarget, name)).then(createdId => { - cy.intercept('DELETE', `**/region/${createdId}`).as('deleteRegion') + cy.wrap(null) + .then(() => createRegion(regionTarget, name)) + .then(createdId => { + cy.intercept('DELETE', `**/region/${createdId}`).as('deleteRegion') - cy.on('window:confirm', message => { - expect(message).to.eq(regionTarget.confirmText) - return true + cy.on('window:confirm', message => { + expect(message).to.eq(regionTarget.confirmText) + return true + }) + + cy.get(regionTarget.deleteButton).click() + + cy.wait('@deleteRegion').its('response.statusCode').should('be.oneOf', [200]) + + cy.contains('Deleted item successfully.').should('be.visible') + + cy.url().should('include', '/region') + cy.contains(name).should('not.exist') + + cy.visit(`/region/${createdId}`) + cy.contains('Error loading data') }) - - cy.get(regionTarget.deleteButton).click() - - cy.wait('@deleteRegion').its('response.statusCode').should('be.oneOf', [200]) - - cy.contains('Deleted item successfully.').should('be.visible') - - cy.url().should('include', '/region') - cy.contains(name).should('not.exist') - - cy.visit(`/region/${createdId}`) - cy.contains('Error loading data') - }) }) }) @@ -70,33 +71,35 @@ describe('Delete flow', () => { const name = regionName('Cancel') - cy.wrap(null).then(() => createRegion(regionTarget, name)).then(createdId => { - let deleteCallCount = 0 - cy.intercept('DELETE', `**/region/${createdId}`, req => { - deleteCallCount += 1 - req.reply({ statusCode: 500 }) - }).as('deleteRegionCancel') + cy.wrap(null) + .then(() => createRegion(regionTarget, name)) + .then(createdId => { + let deleteCallCount = 0 + cy.intercept('DELETE', `**/region/${createdId}`, req => { + deleteCallCount += 1 + req.reply({ statusCode: 500 }) + }).as('deleteRegionCancel') - cy.on('window:confirm', message => { - expect(message).to.eq(regionTarget.confirmText) - return false + cy.on('window:confirm', message => { + expect(message).to.eq(regionTarget.confirmText) + return false + }) + + cy.get(regionTarget.deleteButton).click() + + cy.then(() => { + expect(deleteCallCount).to.eq(0) + }) + + cy.contains('Deleted item successfully.').should('not.exist') + cy.url().should('match', /\/region\/\d+$/) + cy.contains(name).should('be.visible') + + cy.visit('/region') + cy.contains(name).should('be.visible') + cy.visit(`/region/${createdId}`) + cy.contains(name).should('be.visible') }) - - cy.get(regionTarget.deleteButton).click() - - cy.then(() => { - expect(deleteCallCount).to.eq(0) - }) - - cy.contains('Deleted item successfully.').should('not.exist') - cy.url().should('match', /\/region\/\d+$/) - cy.contains(name).should('be.visible') - - cy.visit('/region') - cy.contains(name).should('be.visible') - cy.visit(`/region/${createdId}`) - cy.contains(name).should('be.visible') - }) }) }) @@ -110,29 +113,31 @@ describe('Delete flow', () => { const name = regionName('Failure') const failureMessage = 'Test delete failure' - cy.wrap(null).then(() => createRegion(regionTarget, name)).then(createdId => { - cy.intercept('DELETE', `**/region/${createdId}`, { - statusCode: 500, - body: { message: failureMessage }, - }).as('deleteRegionFailure') + cy.wrap(null) + .then(() => createRegion(regionTarget, name)) + .then(createdId => { + cy.intercept('DELETE', `**/region/${createdId}`, { + statusCode: 500, + body: { message: failureMessage }, + }).as('deleteRegionFailure') - cy.on('window:confirm', message => { - expect(message).to.eq(regionTarget.confirmText) - return true + cy.on('window:confirm', message => { + expect(message).to.eq(regionTarget.confirmText) + return true + }) + + cy.get(regionTarget.deleteButton).click() + + cy.wait('@deleteRegionFailure').its('response.statusCode').should('eq', 500) + + cy.contains(failureMessage).should('be.visible') + cy.contains('Deleted item successfully.').should('not.exist') + cy.url().should('match', /\/region\/\d+$/) + cy.contains(name).should('be.visible') + + cy.visit('/region') + cy.contains(name).should('be.visible') }) - - cy.get(regionTarget.deleteButton).click() - - cy.wait('@deleteRegionFailure').its('response.statusCode').should('eq', 500) - - cy.contains(failureMessage).should('be.visible') - cy.contains('Deleted item successfully.').should('not.exist') - cy.url().should('match', /\/region\/\d+$/) - cy.contains(name).should('be.visible') - - cy.visit('/region') - cy.contains(name).should('be.visible') - }) }) }) }) diff --git a/cypress/e2e/locality.cy.js b/cypress/e2e/locality.cy.js index 91d5dc3f..7e9b1b14 100644 --- a/cypress/e2e/locality.cy.js +++ b/cypress/e2e/locality.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Adding species in Locality -> Species tab for an existing locality', () => { @@ -144,7 +144,6 @@ describe('Adding species in Locality -> Species tab for an existing locality', ( cy.contains('Genus Amblycoptus belongs to family Soricidae, not Bovidae.') }) - it('minus action only marks the clicked species row as removed', () => { cy.visit(`/locality/20920?tab=2`) cy.contains('Lantian-Shuijiazui') diff --git a/cypress/e2e/login.cy.js b/cypress/e2e/login.cy.js index 9000a514..5f26941a 100644 --- a/cypress/e2e/login.cy.js +++ b/cypress/e2e/login.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('User redirecting after logging in works', () => { diff --git a/cypress/e2e/misc.cy.js b/cypress/e2e/misc.cy.js index a64de728..90ff583a 100644 --- a/cypress/e2e/misc.cy.js +++ b/cypress/e2e/misc.cy.js @@ -26,7 +26,7 @@ const doesNotHaveStagingMode = viewUrl => { } before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Test individual features across the app', () => { diff --git a/cypress/e2e/occurrence-edit.cy.ts b/cypress/e2e/occurrence-edit.cy.ts index a219a4cd..4a5bcce3 100644 --- a/cypress/e2e/occurrence-edit.cy.ts +++ b/cypress/e2e/occurrence-edit.cy.ts @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Occurrence editing', () => { diff --git a/cypress/e2e/person.cy.js b/cypress/e2e/person.cy.js index 0fcbd1cc..ba22ed61 100644 --- a/cypress/e2e/person.cy.js +++ b/cypress/e2e/person.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Creating a person', () => { diff --git a/cypress/e2e/reference.cy.js b/cypress/e2e/reference.cy.js index fc410936..91cbdf8a 100644 --- a/cypress/e2e/reference.cy.js +++ b/cypress/e2e/reference.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Creating a journal', () => { diff --git a/cypress/e2e/region.cy.js b/cypress/e2e/region.cy.js index 927382d5..768c04b5 100644 --- a/cypress/e2e/region.cy.js +++ b/cypress/e2e/region.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Creating a region', () => { diff --git a/cypress/e2e/sequence_edit.cy.ts b/cypress/e2e/sequence_edit.cy.ts index 3587d23e..9a591f84 100644 --- a/cypress/e2e/sequence_edit.cy.ts +++ b/cypress/e2e/sequence_edit.cy.ts @@ -2,8 +2,7 @@ describe('Sequence edit behavior', () => { before('Wait for database and reset state', () => { - cy.task('waitForDbHealthy') - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) beforeEach('Login as admin', () => { diff --git a/cypress/e2e/species.cy.js b/cypress/e2e/species.cy.js index 261bf7c2..bd6c399b 100644 --- a/cypress/e2e/species.cy.js +++ b/cypress/e2e/species.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Creating a species', () => { @@ -100,7 +100,7 @@ describe('Editing a species', () => { describe('Taxonomy checks work', () => { before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) beforeEach('Login as admin', () => { diff --git a/cypress/e2e/timeBound.cy.js b/cypress/e2e/timeBound.cy.js index 73800ba5..7b7858a4 100644 --- a/cypress/e2e/timeBound.cy.js +++ b/cypress/e2e/timeBound.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Creating time bound', () => { diff --git a/cypress/e2e/timeUnit.cy.js b/cypress/e2e/timeUnit.cy.js index cf582b58..86b607b4 100644 --- a/cypress/e2e/timeUnit.cy.js +++ b/cypress/e2e/timeUnit.cy.js @@ -2,7 +2,7 @@ const buildTimeUnitName = (base = 'Testing Time Unit Creation') => `${base} ${Date.now()}-${Math.floor(Math.random() * 1e6)}` before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Creating a time unit', () => { @@ -317,30 +317,30 @@ describe('Deleting a time unit', () => { const deletePath = `**/time-unit/${slug}` cy.intercept('DELETE', deletePath).as('deleteTimeUnit') - cy.fixture('login').its('deleteTargets').then(deleteTargets => { - const confirmText = deleteTargets.find(target => target.entity === 'TimeUnit') - ?.confirmText - const expectedConfirm = - confirmText || - 'Are you sure you want to delete this item? This operation cannot be undone.' + cy.fixture('login') + .its('deleteTargets') + .then(deleteTargets => { + const confirmText = deleteTargets.find(target => target.entity === 'TimeUnit')?.confirmText + const expectedConfirm = + confirmText || 'Are you sure you want to delete this item? This operation cannot be undone.' - cy.on('window:confirm', message => { - expect(message).to.eq(expectedConfirm) - return true + cy.on('window:confirm', message => { + expect(message).to.eq(expectedConfirm) + return true + }) + + cy.get('[id=delete-button]').click() + + cy.wait('@deleteTimeUnit').its('response.statusCode').should('be.oneOf', [200, 204]) + + cy.contains('Deleted item successfully.').should('be.visible') + + cy.url().should('include', '/time-unit') + cy.contains(displayName).should('not.exist') + + cy.visit(`/time-unit/${slug}`) + cy.contains('Error loading data') }) - - cy.get('[id=delete-button]').click() - - cy.wait('@deleteTimeUnit').its('response.statusCode').should('be.oneOf', [200, 204]) - - cy.contains('Deleted item successfully.').should('be.visible') - - cy.url().should('include', '/time-unit') - cy.contains(displayName).should('not.exist') - - cy.visit(`/time-unit/${slug}`) - cy.contains('Error loading data') - }) }) }) }) diff --git a/cypress/e2e/ui.cy.js b/cypress/e2e/ui.cy.js index 2f5976f2..514fd5a0 100644 --- a/cypress/e2e/ui.cy.js +++ b/cypress/e2e/ui.cy.js @@ -4,7 +4,7 @@ */ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Button Tests', () => { diff --git a/cypress/e2e/updateLogNavigation.cy.ts b/cypress/e2e/updateLogNavigation.cy.ts index 5570cd8c..31bed148 100644 --- a/cypress/e2e/updateLogNavigation.cy.ts +++ b/cypress/e2e/updateLogNavigation.cy.ts @@ -1,12 +1,8 @@ /// describe('Update log navigation', () => { - before('Wait for database to become ready', () => { - cy.task('waitForDbHealthy') - }) - before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) beforeEach('Login as admin', () => { diff --git a/cypress/e2e/userPage.cy.js b/cypress/e2e/userPage.cy.js index c278b832..5d513563 100644 --- a/cypress/e2e/userPage.cy.js +++ b/cypress/e2e/userPage.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Change password form validates passwords correctly and shows errors to the user', () => { diff --git a/cypress/e2e/userRights.cy.js b/cypress/e2e/userRights.cy.js index 7012a8d7..275efd84 100644 --- a/cypress/e2e/userRights.cy.js +++ b/cypress/e2e/userRights.cy.js @@ -1,5 +1,5 @@ before('Reset database', () => { - cy.request(Cypress.env('databaseResetUrl')) + cy.resetDatabase() }) describe('Broadly test what different user rights see', () => { diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e9d47d91..6be6430a 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -25,13 +25,16 @@ // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) Cypress.Commands.add('login', username => { - cy.clearLocalStorage() + cy.clearAllLocalStorage() + cy.clearAllSessionStorage() + cy.clearCookies() + cy.intercept('POST', '**/user/login').as('loginRequest') cy.visit('/login') - cy.get('[data-cy="login-button"]', { timeout: 10000 }).should('be.visible').click() - cy.get('[data-cy="username-basic"]').type(username) - cy.get('[data-cy="password-basic"]').type('test') - cy.get('[data-cy="login-button"]').click() - cy.contains(`${username}`) + cy.get('[data-cy="username-basic"]', { timeout: 10000 }).should('be.visible').clear().type(username) + cy.get('[data-cy="password-basic"]').should('be.visible').clear().type('test', { log: false }) + cy.get('[data-cy="login-button"]').should('be.visible').click() + cy.wait('@loginRequest').its('response.statusCode').should('eq', 200) + cy.location('pathname', { timeout: 10000 }).should('not.eq', '/login') }) Cypress.Commands.add('loginAsDeleteCoordinator', () => { @@ -50,7 +53,8 @@ Cypress.Commands.add('pageForbidden', url => { }) Cypress.Commands.add('resetDatabase', () => { - cy.task('resetDatabase') + cy.task('waitForDbHealthy') + cy.request(Cypress.env('databaseResetUrl')).its('status').should('eq', 200) }) // use this once you have edited some data and want to save it