diff --git a/cypress.config.js b/cypress.config.js index a97668cb..be0cb7e6 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,9 +1,13 @@ const { defineConfig } = require('cypress') const net = require('net') +const http = require('http') +const https = require('https') 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_APP_BASE_URL = process.env.CYPRESS_baseUrl ?? 'http://localhost:5173/' +const DEFAULT_APP_WAIT_TIMEOUT_MS = Number(process.env.CYPRESS_APP_WAIT_TIMEOUT ?? 60_000) const RETRY_DELAY_MS = 1_000 const SOCKET_TIMEOUT_MS = 2_000 @@ -59,6 +63,61 @@ async function waitForDatabase(options = {}, env = {}) { }) } +/** + * @param {{ url?: string; timeoutMs?: number }} [options] + * @param {{ appBaseUrl?: string; appWaitTimeoutMs?: number }} [env] + */ +async function waitForHttpUrl(options = {}, env = {}) { + const url = options.url ?? env.appBaseUrl ?? DEFAULT_APP_BASE_URL + const timeoutMs = Number(options.timeoutMs ?? env.appWaitTimeoutMs ?? DEFAULT_APP_WAIT_TIMEOUT_MS) + const parsedUrl = new URL(url) + const client = parsedUrl.protocol === 'https:' ? https : http + const deadline = Date.now() + timeoutMs + + return new Promise((resolve, reject) => { + const attemptRequest = () => { + const request = client.request( + parsedUrl, + { method: 'GET', timeout: SOCKET_TIMEOUT_MS }, + response => { + response.resume() + + if (response.statusCode && response.statusCode < 500) { + resolve(null) + return + } + + if (Date.now() >= deadline) { + reject(new Error(`Timed out waiting for app at ${url} (status ${response.statusCode ?? 'unknown'})`)) + return + } + + setTimeout(attemptRequest, RETRY_DELAY_MS) + } + ) + + const retry = error => { + request.destroy() + + if (Date.now() >= deadline) { + reject(new Error(`Timed out waiting for app at ${url} (${error.message})`)) + return + } + + setTimeout(attemptRequest, RETRY_DELAY_MS) + } + + request.on('error', retry) + request.on('timeout', () => { + retry(new Error('Connection attempt timed out')) + }) + request.end() + } + + attemptRequest() + }) +} + module.exports = defineConfig({ e2e: { setupNodeEvents(on, config) { @@ -68,6 +127,12 @@ module.exports = defineConfig({ waitForDbHealthy(options) { return waitForDatabase(options, config.env) }, + waitForAppHealthy(options) { + return waitForHttpUrl(options, { + ...config.env, + appBaseUrl: config.baseUrl, + }) + }, }) // It's IMPORTANT to return the config object @@ -88,6 +153,8 @@ module.exports = defineConfig({ dbHost: DEFAULT_DB_HOST, dbPort: DEFAULT_DB_PORT, dbWaitTimeoutMs: DEFAULT_DB_WAIT_TIMEOUT_MS, + appBaseUrl: DEFAULT_APP_BASE_URL, + appWaitTimeoutMs: DEFAULT_APP_WAIT_TIMEOUT_MS, }, viewportHeight: 900, viewportWidth: 1200, diff --git a/cypress/support/commands.js b/cypress/support/commands.js index e7780596..34ce10a1 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -24,6 +24,15 @@ // -- This will overwrite an existing command -- // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +Cypress.Commands.overwrite('visit', (originalFn, url, options) => { + const targetUrl = typeof url === 'string' ? url : url?.url + const resolvedUrl = targetUrl ? new URL(targetUrl, Cypress.config('baseUrl')).toString() : Cypress.config('baseUrl') + + cy.task('waitForAppHealthy', { url: resolvedUrl }) + + return originalFn(url, options) +}) + Cypress.Commands.add('login', username => { cy.clearAllLocalStorage() cy.clearAllSessionStorage() @@ -77,7 +86,7 @@ Cypress.Commands.add('resetDatabase', () => { Cypress.Commands.add('resetDatabaseOnce', () => { const currentSpec = Cypress.spec.name const resetKey = `dbReset_${currentSpec}` - + if (!window[resetKey]) { cy.resetDatabase() window[resetKey] = true @@ -108,7 +117,7 @@ Cypress.Commands.add('loginWithSession', (username) => { // Validate the session is still valid by checking for the username box cy.visit('/') cy.contains('.username-box', username, { timeout: 10000 }).should('be.visible') - } + }, }) }) diff --git a/cypress/support/index.d.ts b/cypress/support/index.d.ts index abed29a7..14c9850c 100644 --- a/cypress/support/index.d.ts +++ b/cypress/support/index.d.ts @@ -13,6 +13,7 @@ declare namespace Cypress { interface Tasks { waitForDbHealthy(options?: WaitForDbHealthyOptions): null + waitForAppHealthy(options?: WaitForAppHealthyOptions): null } } @@ -22,6 +23,11 @@ interface WaitForDbHealthyOptions { timeoutMs?: number } +interface WaitForAppHealthyOptions { + url?: string + timeoutMs?: number +} + interface DeleteTarget { entity: string path: string