diff --git a/features/cucumber.config.js b/features/cucumber.config.js index 141e5a6b..39017be0 100644 --- a/features/cucumber.config.js +++ b/features/cucumber.config.js @@ -1,3 +1,5 @@ +const isCI = Boolean(process.env.CI); + module.exports = { default: { require: [ @@ -9,9 +11,11 @@ module.exports = { formatOptions: { snippetInterface: 'async-await', }, - // Global default timeout - maximum allowed: Local 5s, CI 10s (no more!) - timeout: process.env.CI ? 10000 : 5000, paths: ['features/*.feature'], + // Global timeout for all steps + // Local: 5s, CI: 10s (exactly 2x local) + // Individual steps should NOT specify custom timeouts unless they have special needs + timeout: isCI ? 10000 : 5000, // Parallel execution disabled due to OOM issues on Windows // Each scenario still gets isolated test-artifacts/{scenarioSlug}/ directory // parallel: 2, diff --git a/features/stepDefinitions/agent.ts b/features/stepDefinitions/agent.ts index dd459cb0..17f035b1 100644 --- a/features/stepDefinitions/agent.ts +++ b/features/stepDefinitions/agent.ts @@ -8,6 +8,7 @@ import path from 'path'; import type { ISettingFile } from '../../src/services/database/interface'; import { MockOpenAIServer } from '../supports/mockOpenAI'; import { getSettingsPath } from '../supports/paths'; +import { PLAYWRIGHT_SHORT_TIMEOUT } from '../supports/timeouts'; import type { ApplicationWorld } from './application'; // Backoff configuration for retries @@ -202,7 +203,7 @@ Then('I should see {int} messages in chat history', async function(this: Applica await backOff( async () => { // Wait for at least one message to exist - await currentWindow.waitForSelector(messageSelector, { timeout: 5000 }); + await currentWindow.waitForSelector(messageSelector, { timeout: PLAYWRIGHT_SHORT_TIMEOUT }); // Count current messages const messages = currentWindow.locator(messageSelector); diff --git a/features/stepDefinitions/application.ts b/features/stepDefinitions/application.ts index 2c8ce13e..875b65d4 100644 --- a/features/stepDefinitions/application.ts +++ b/features/stepDefinitions/application.ts @@ -8,6 +8,7 @@ import { windowDimension, WindowNames } from '../../src/services/windows/WindowP import { MockOAuthServer } from '../supports/mockOAuthServer'; import { MockOpenAIServer } from '../supports/mockOpenAI'; import { getPackedAppPath, makeSlugPath } from '../supports/paths'; +import { PLAYWRIGHT_TIMEOUT } from '../supports/timeouts'; import { captureScreenshot } from '../supports/webContentsViewHelper'; /** @@ -301,7 +302,7 @@ AfterStep(async function(this: ApplicationWorld, { pickle, pickleStep, result }) // Timeout is a symptom, not the disease. Fix the root cause. // Read docs/Testing.md section "Key E2E Testing Patterns" point 6 before attempting any changes. // Maximum allowed timeouts: Local 5s, CI 10s (exactly 2x local, no more) -When('I launch the TidGi application', { timeout: process.env.CI ? 10000 : 5000 }, async function(this: ApplicationWorld) { +When('I launch the TidGi application', async function(this: ApplicationWorld) { // For E2E tests on dev mode, use the packaged test version with NODE_ENV environment variable baked in const packedAppPath = getPackedAppPath(); @@ -362,13 +363,10 @@ When('I launch the TidGi application', { timeout: process.env.CI ? 10000 : 5000 }, // Set cwd to repo root; scenario isolation is handled via --test-scenario argument cwd: process.cwd(), - // Align Electron launch timeout with step definition (max 10s in CI, 5s locally) - timeout: process.env.CI ? 10000 : 5000, + timeout: PLAYWRIGHT_TIMEOUT, }); - // Wait for first window - aligned with maximum timeout rules: Local 5s, CI 10s - const windowTimeout = process.env.CI ? 10000 : 5000; - this.mainWindow = await this.app.firstWindow({ timeout: windowTimeout }); + this.mainWindow = await this.app.firstWindow({ timeout: PLAYWRIGHT_TIMEOUT }); this.currentWindow = this.mainWindow; } catch (error) { throw new Error( diff --git a/features/stepDefinitions/browserView.ts b/features/stepDefinitions/browserView.ts index 5d08444f..e9b253d1 100644 --- a/features/stepDefinitions/browserView.ts +++ b/features/stepDefinitions/browserView.ts @@ -71,7 +71,7 @@ Then('I should see {string} in the browser view DOM', async function(this: Appli }); }); -Then('the browser view should be loaded and visible', { timeout: 15000 }, async function(this: ApplicationWorld) { +Then('the browser view should be loaded and visible', async function(this: ApplicationWorld) { if (!this.app) { throw new Error('Application not launched'); } @@ -150,7 +150,7 @@ When('I click on {string} elements in browser view with selectors:', async funct } }); -Then('I wait for {string} element in browser view with selector {string}', { timeout: 15000 }, async function( +Then('I wait for {string} element in browser view with selector {string}', async function( this: ApplicationWorld, elementComment: string, selector: string, @@ -314,7 +314,7 @@ When('I open tiddler {string} in browser view', async function(this: Application * Create a new tiddler with title and optional tags via TiddlyWiki UI. * This step handles all the UI interactions: click add button, set title, add tags, and confirm. */ -When('I create a tiddler {string} with tag {string} in browser view', { timeout: 20000 }, async function( +When('I create a tiddler {string} with tag {string} in browser view', async function( this: ApplicationWorld, tiddlerTitle: string, tagName: string, @@ -361,7 +361,7 @@ When('I create a tiddler {string} with tag {string} in browser view', { timeout: /** * Create a new tiddler with title and custom field via TiddlyWiki UI. */ -When('I create a tiddler {string} with field {string} set to {string} in browser view', { timeout: 20000 }, async function( +When('I create a tiddler {string} with field {string} set to {string} in browser view', async function( this: ApplicationWorld, tiddlerTitle: string, fieldName: string, diff --git a/features/stepDefinitions/ui.ts b/features/stepDefinitions/ui.ts index 7b458a08..45a7ac49 100644 --- a/features/stepDefinitions/ui.ts +++ b/features/stepDefinitions/ui.ts @@ -1,6 +1,7 @@ import { DataTable, Then, When } from '@cucumber/cucumber'; import { parseDataTableRows } from '../supports/dataTable'; import { getWikiTestRootPath } from '../supports/paths'; +import { PLAYWRIGHT_SHORT_TIMEOUT, PLAYWRIGHT_TIMEOUT } from '../supports/timeouts'; import type { ApplicationWorld } from './application'; When('I wait for {float} seconds', async function(seconds: number) { @@ -17,14 +18,13 @@ When('I wait for {float} seconds for {string}', async function(seconds: number, When('I wait for the page to load completely', async function(this: ApplicationWorld) { const currentWindow = this.currentWindow; - // Maximum timeout rule: CI 10s, local 5s - await currentWindow?.waitForLoadState('networkidle', { timeout: process.env.CI ? 10000 : 5000 }); + await currentWindow?.waitForLoadState('networkidle', { timeout: PLAYWRIGHT_TIMEOUT }); }); Then('I should see a(n) {string} element with selector {string}', async function(this: ApplicationWorld, elementComment: string, selector: string) { const currentWindow = this.currentWindow; try { - await currentWindow?.waitForSelector(selector, { timeout: 10000 }); + await currentWindow?.waitForSelector(selector, { timeout: PLAYWRIGHT_TIMEOUT }); const isVisible = await currentWindow?.isVisible(selector); if (!isVisible) { throw new Error(`Element "${elementComment}" with selector "${selector}" is not visible`); @@ -51,7 +51,7 @@ Then('I should see {string} elements with selectors:', async function(this: Appl // Check all elements in parallel for better performance await Promise.all(dataRows.map(async ([elementComment, selector]) => { try { - await currentWindow.waitForSelector(selector, { timeout: 10000 }); + await currentWindow.waitForSelector(selector, { timeout: PLAYWRIGHT_TIMEOUT }); const isVisible = await currentWindow.isVisible(selector); if (!isVisible) { errors.push(`Element "${elementComment}" with selector "${selector}" is not visible`); @@ -149,7 +149,7 @@ When('I click on a(n) {string} element with selector {string}', async function(t } try { - await targetWindow.waitForSelector(selector, { timeout: 10000 }); + await targetWindow.waitForSelector(selector, { timeout: PLAYWRIGHT_TIMEOUT }); const isVisible = await targetWindow.isVisible(selector); if (!isVisible) { throw new Error(`Element "${elementComment}" with selector "${selector}" is not visible`); @@ -178,7 +178,7 @@ When('I click on {string} elements with selectors:', async function(this: Applic // Click elements sequentially (not in parallel) to maintain order and avoid race conditions for (const [elementComment, selector] of dataRows) { try { - await targetWindow.waitForSelector(selector, { timeout: 10000 }); + await targetWindow.waitForSelector(selector, { timeout: PLAYWRIGHT_TIMEOUT }); const isVisible = await targetWindow.isVisible(selector); if (!isVisible) { errors.push(`Element "${elementComment}" with selector "${selector}" is not visible`); @@ -203,7 +203,7 @@ When('I right-click on a(n) {string} element with selector {string}', async func } try { - await targetWindow.waitForSelector(selector, { timeout: 10000 }); + await targetWindow.waitForSelector(selector, { timeout: PLAYWRIGHT_TIMEOUT }); const isVisible = await targetWindow.isVisible(selector); if (!isVisible) { throw new Error(`Element "${elementComment}" with selector "${selector}" is not visible`); @@ -245,7 +245,7 @@ When('I type {string} in {string} element with selector {string}', async functio const actualText = text.replace('{tmpDir}', getWikiTestRootPath(this)); try { - await currentWindow.waitForSelector(selector, { timeout: 10000 }); + await currentWindow.waitForSelector(selector, { timeout: PLAYWRIGHT_TIMEOUT }); const element = currentWindow.locator(selector); await element.fill(actualText); } catch (error) { @@ -277,7 +277,7 @@ When('I type in {string} elements with selectors:', async function(this: Applica const actualText = text.replace('{tmpDir}', getWikiTestRootPath(this)); try { - await currentWindow.waitForSelector(selector, { timeout: 10000 }); + await currentWindow.waitForSelector(selector, { timeout: PLAYWRIGHT_TIMEOUT }); const element = currentWindow.locator(selector); await element.fill(actualText); } catch (error) { @@ -297,7 +297,7 @@ When('I clear text in {string} element with selector {string}', async function(t } try { - await currentWindow.waitForSelector(selector, { timeout: 10000 }); + await currentWindow.waitForSelector(selector, { timeout: PLAYWRIGHT_TIMEOUT }); const element = currentWindow.locator(selector); await element.clear(); } catch (error) { @@ -429,7 +429,7 @@ When('I select {string} from MUI Select with test id {string}', async function(t try { // Find the hidden input element with the test-id const inputSelector = `input[data-testid="${testId}"]`; - await currentWindow.waitForSelector(inputSelector, { timeout: 10000 }); + await currentWindow.waitForSelector(inputSelector, { timeout: PLAYWRIGHT_TIMEOUT }); // Try to click using Playwright's click on the div with role="combobox" // According to your HTML structure, the combobox is a sibling of the input @@ -465,7 +465,7 @@ When('I select {string} from MUI Select with test id {string}', async function(t await currentWindow.waitForTimeout(500); // Wait for the menu to appear - await currentWindow.waitForSelector('[role="listbox"]', { timeout: 5000 }); + await currentWindow.waitForSelector('[role="listbox"]', { timeout: PLAYWRIGHT_SHORT_TIMEOUT }); // Try to click on the option with the specified value (data-value attribute) // If not found, try to find by text content @@ -506,7 +506,7 @@ When('I select {string} from MUI Select with test id {string}', async function(t } // Wait for the menu to close - await currentWindow.waitForSelector('[role="listbox"]', { state: 'hidden', timeout: 5000 }); + await currentWindow.waitForSelector('[role="listbox"]', { state: 'hidden', timeout: PLAYWRIGHT_SHORT_TIMEOUT }); } catch (error) { throw new Error(`Failed to select option "${optionValue}" from MUI Select with test id "${testId}": ${String(error)}`); } @@ -536,7 +536,7 @@ When('I print DOM structure of element with selector {string}', async function(t } try { - await currentWindow.waitForSelector(selector, { timeout: 5000 }); + await currentWindow.waitForSelector(selector, { timeout: PLAYWRIGHT_SHORT_TIMEOUT }); const elementInfo = await currentWindow.evaluate((sel) => { const element = document.querySelector(sel); diff --git a/features/stepDefinitions/wiki.ts b/features/stepDefinitions/wiki.ts index 4c6dd91b..5b1211c9 100644 --- a/features/stepDefinitions/wiki.ts +++ b/features/stepDefinitions/wiki.ts @@ -6,6 +6,7 @@ import path from 'path'; import type { IWikiWorkspace, IWorkspace } from '../../src/services/workspaces/interface'; import { parseDataTableRows } from '../supports/dataTable'; import { getLogPath, getSettingsPath, getWikiTestRootPath, getWikiTestWikiPath } from '../supports/paths'; +import { LOG_MARKER_WAIT_TIMEOUT } from '../supports/timeouts'; // Scenario-specific paths are computed via helper functions import type { ApplicationWorld } from './application'; @@ -301,42 +302,45 @@ Then('file {string} should exist in {string}', async function(this: ApplicationW } }); -Then('file {string} should not exist in {string}', { timeout: process.env.CI ? 10000 : 5000 }, async function(this: ApplicationWorld, fileName: string, simpleDirectoryPath: string) { - // Replace {tmpDir} with wiki test root (not wiki subfolder) - let directoryPath = simpleDirectoryPath.replace('{tmpDir}', getWikiTestRootPath(this)); +Then( + 'file {string} should not exist in {string}', + async function(this: ApplicationWorld, fileName: string, simpleDirectoryPath: string) { + // Replace {tmpDir} with wiki test root (not wiki subfolder) + let directoryPath = simpleDirectoryPath.replace('{tmpDir}', getWikiTestRootPath(this)); - // Resolve symlinks on all platforms to handle sub-wikis correctly - if (await fs.pathExists(directoryPath)) { - try { - directoryPath = fs.realpathSync(directoryPath); - } catch { - // If realpathSync fails, continue with the original path + // Resolve symlinks on all platforms to handle sub-wikis correctly + if (await fs.pathExists(directoryPath)) { + try { + directoryPath = fs.realpathSync(directoryPath); + } catch { + // If realpathSync fails, continue with the original path + } } - } - const filePath = path.join(directoryPath, fileName); + const filePath = path.join(directoryPath, fileName); - try { - await backOff( - async () => { - if (!(await fs.pathExists(filePath))) { - return; - } - throw new Error('File still exists'); - }, - BACKOFF_OPTIONS, - ); - } catch { - throw new Error( - `File "${fileName}" should not exist but was found in directory: ${directoryPath}`, - ); - } -}); + try { + await backOff( + async () => { + if (!(await fs.pathExists(filePath))) { + return; + } + throw new Error('File still exists'); + }, + BACKOFF_OPTIONS, + ); + } catch { + throw new Error( + `File "${fileName}" should not exist but was found in directory: ${directoryPath}`, + ); + } + }, +); /** * Verify that a workspace in settings.json has a specific property set to a specific value */ -Then('settings.json should have workspace {string} with {string} set to {string}', { timeout: 10000 }, async function( +Then('settings.json should have workspace {string} with {string} set to {string}', async function( this: ApplicationWorld, workspaceName: string, propertyName: string, @@ -416,7 +420,7 @@ Then('settings.json should have workspace {string} with {string} set to {string} /** * Verify that a workspace in settings.json has a property array that contains a specific value */ -Then('settings.json should have workspace {string} with {string} containing {string}', { timeout: 10000 }, async function( +Then('settings.json should have workspace {string} with {string} containing {string}', async function( this: ApplicationWorld, workspaceName: string, propertyName: string, @@ -559,11 +563,9 @@ async function clearGitTestData(scenarioRoot?: string) { * Read docs/Testing.md section "Key E2E Testing Patterns" point 6 before attempting any changes. * Maximum allowed timeouts: Local 5s, CI 10s (exactly 2x local, no more) */ -Then('I wait for {string} log marker {string}', { timeout: process.env.CI ? 10 * 1000 : 5 * 1000 }, async function(this: ApplicationWorld, description: string, marker: string) { +Then('I wait for {string} log marker {string}', async function(this: ApplicationWorld, description: string, marker: string) { // Search in all log files using '*' pattern (includes TidGi-, wiki-, and workspace-named logs like WikiRenamed-) - // Internal wait timeout: Local 3s, CI 6s (to fit within step timeout) - const waitTimeout = process.env.CI ? 6000 : 3000; - await waitForLogMarker(this, marker, `Log marker "${marker}" not found. Expected: ${description}`, waitTimeout, '*'); + await waitForLogMarker(this, marker, `Log marker "${marker}" not found. Expected: ${description}`, LOG_MARKER_WAIT_TIMEOUT, '*'); }); /** @@ -576,21 +578,19 @@ Then('I wait for {string} log marker {string}', { timeout: process.env.CI ? 10 * * | watch-fs stabilized after restart| [test-id-WATCH_FS_STABILIZED] | * | SSE ready after restart | [test-id-SSE_READY] | */ -Then('I wait for log markers:', { timeout: process.env.CI ? 10 * 1000 : 5 * 1000 }, async function(this: ApplicationWorld, dataTable: DataTable) { +Then('I wait for log markers:', async function(this: ApplicationWorld, dataTable: DataTable) { const rows = dataTable.raw(); const dataRows = parseDataTableRows(rows, 2); if (dataRows[0]?.length !== 2) { throw new Error('Table must have exactly 2 columns: | description | marker |'); } - - const waitTimeout = process.env.CI ? 6000 : 3000; const errors: string[] = []; // Wait for markers sequentially to maintain order for (const [description, marker] of dataRows) { try { - await waitForLogMarker(this, marker, `Log marker "${marker}" not found. Expected: ${description}`, waitTimeout, '*'); + await waitForLogMarker(this, marker, `Log marker "${marker}" not found. Expected: ${description}`, LOG_MARKER_WAIT_TIMEOUT, '*'); } catch (error) { errors.push(`Failed to find log marker "${marker}" (${description}): ${error instanceof Error ? error.message : String(error)}`); } @@ -940,7 +940,7 @@ When('I create a new wiki workspace with name {string}', async function(this: Ap /** * Restart a workspace wiki worker */ -When('I restart workspace {string}', { timeout: STEP_TIMEOUT }, async function(this: ApplicationWorld, workspaceName: string) { +When('I restart workspace {string}', async function(this: ApplicationWorld, workspaceName: string) { if (!this.app) throw new Error('Application is not available'); const settingsPath = getSettingsPath(this); @@ -998,7 +998,7 @@ When('I restart workspace {string}', { timeout: STEP_TIMEOUT }, async function(t * | enableFileSystemWatch | true | * | syncOnInterval | false | */ -When('I update workspace {string} settings:', { timeout: 60000 }, async function(this: ApplicationWorld, workspaceName: string, dataTable: DataTable) { +When('I update workspace {string} settings:', async function(this: ApplicationWorld, workspaceName: string, dataTable: DataTable) { if (!this.app) { throw new Error('Application is not available'); } @@ -1073,6 +1073,20 @@ When('I update workspace {string} settings:', { timeout: 60000 }, async function throw new Error(`No workspace found with name: ${workspaceName}`); } + // If enableFileSystemWatch is being changed, check current state BEFORE updating + let watchFsCurrentlyEnabled = false; + if ('enableFileSystemWatch' in settingsUpdate) { + const currentWorkspace = await this.app.evaluate(async ({ BrowserWindow }, workspaceId: string) => { + const windows = BrowserWindow.getAllWindows(); + const mainWindow = windows.find(win => !win.isDestroyed() && win.webContents && win.webContents.getURL().includes('index.html')); + if (!mainWindow) return null; + + return await mainWindow.webContents.executeJavaScript(`window.service.workspace.get(${JSON.stringify(workspaceId)})`) as Promise; + }, targetWorkspaceId); + + watchFsCurrentlyEnabled = currentWorkspace !== null && isWikiWorkspace(currentWorkspace) && currentWorkspace.enableFileSystemWatch; + } + // Update workspace settings via main window await this.app.evaluate(async ({ BrowserWindow }, { workspaceId, updates }: { workspaceId: string; updates: Record }) => { const windows = BrowserWindow.getAllWindows(); @@ -1098,23 +1112,17 @@ When('I update workspace {string} settings:', { timeout: 60000 }, async function // If enableFileSystemWatch was changed, we need to restart the wiki for it to take effect // The wiki worker reads this config at startup, so changes don't apply until restart if ('enableFileSystemWatch' in settingsUpdate) { - // First, wait for the wiki to be fully started before attempting restart - // This prevents conflicts if the wiki is still initializing - // Wait for WATCH_FS since it indicates wiki worker is ready, or SSE_READY if watch is disabled - try { - await waitForLogMarker(this, '[test-id-WATCH_FS_STABILIZED]', 'watch-fs not ready before restart', 30000); - } catch { - // If watch-fs is disabled initially, wait for SSE instead - await waitForLogMarker(this, '[test-id-SSE_READY]', 'SSE not ready before restart', 30000); + // Only wait for watch-fs if it was enabled before the update + // If it was disabled, wiki is ready immediately without watch-fs markers + if (watchFsCurrentlyEnabled) { + await waitForLogMarker(this, '[test-id-WATCH_FS_STABILIZED]', 'watch-fs not ready before restart', LOG_MARKER_WAIT_TIMEOUT); } - // Only clear watch-fs related log markers to ensure we wait for fresh ones after restart - // Don't clear other markers like git-init-complete that won't appear again + // Clear log markers to ensure we wait for fresh ones after restart await clearLogLinesContaining(this, '[test-id-WATCH_FS_STABILIZED]'); - await clearLogLinesContaining(this, '[test-id-SSE_READY]'); // Restart the wiki - await this.app.evaluate(async ({ BrowserWindow }, workspaceId: string) => { + const restartResult = await this.app.evaluate(async ({ BrowserWindow }, workspaceId: string) => { const windows = BrowserWindow.getAllWindows(); const mainWindow = windows.find(win => !win.isDestroyed() && win.webContents && win.webContents.getURL().includes('index.html')); @@ -1122,20 +1130,31 @@ When('I update workspace {string} settings:', { timeout: 60000 }, async function throw new Error('Main window not found'); } - await mainWindow.webContents.executeJavaScript(` + const result = await mainWindow.webContents.executeJavaScript(` (async () => { const workspace = await window.service.workspace.get(${JSON.stringify(workspaceId)}); - if (workspace) { + if (!workspace) { + return { success: false, error: 'Workspace not found' }; + } + try { await window.service.wiki.restartWiki(workspace); + return { success: true }; + } catch (error) { + return { success: false, error: error.message }; } })(); - `); + `) as Promise<{ success: boolean; error?: string }>; + return result; }, targetWorkspaceId); + if (!restartResult.success) { + throw new Error(`Failed to restart wiki: ${restartResult.error ?? 'Unknown error'}`); + } + // Wait for wiki to restart and watch-fs to stabilize // Only wait if enableFileSystemWatch was set to true if (settingsUpdate.enableFileSystemWatch === true) { - await waitForLogMarker(this, '[test-id-WATCH_FS_STABILIZED]', 'watch-fs did not stabilize after restart', 30000); + await waitForLogMarker(this, '[test-id-WATCH_FS_STABILIZED]', 'watch-fs did not stabilize after restart', LOG_MARKER_WAIT_TIMEOUT); } } }); diff --git a/features/supports/timeouts.ts b/features/supports/timeouts.ts new file mode 100644 index 00000000..c428509d --- /dev/null +++ b/features/supports/timeouts.ts @@ -0,0 +1,36 @@ +/** + * Centralized timeout configuration for E2E tests + * + * IMPORTANT: Most steps should NOT specify custom timeouts! + * CucumberJS global timeout is configured in cucumber.config.js: + * - Local: 5 seconds + * - CI: 10 seconds (exactly 2x local) + * + * Only special operations (like complex browser view UI interactions) should + * specify custom timeouts at the step level, with clear comments explaining why. + * + * If an operation times out, it indicates a performance issue that should be fixed, + * not a timeout that should be increased. + */ + +const isCI = Boolean(process.env.CI); + +/** + * Timeout for Playwright waitForSelector and similar operations + * These are internal timeouts for finding elements, not Cucumber step timeouts + * Local: 5s, CI: 10s + */ +export const PLAYWRIGHT_TIMEOUT = isCI ? 10000 : 5000; + +/** + * Shorter timeout for operations that should be very fast + * Local: 3s, CI: 6s + */ +export const PLAYWRIGHT_SHORT_TIMEOUT = isCI ? 6000 : 3000; + +/** + * Timeout for waiting log markers + * Internal wait should be shorter than step timeout to allow proper error reporting + * Local: 3s, CI: 6s + */ +export const LOG_MARKER_WAIT_TIMEOUT = isCI ? 6000 : 3000; diff --git a/scripts/start-e2e-app.ts b/scripts/start-e2e-app.ts index 3075e10c..ee702999 100644 --- a/scripts/start-e2e-app.ts +++ b/scripts/start-e2e-app.ts @@ -13,7 +13,7 @@ import { getPackedAppPath } from '../features/supports/paths'; */ function getMostRecentScenarioName(): string | undefined { const testArtifactsDir = path.resolve(process.cwd(), 'test-artifacts'); - + if (!fs.existsSync(testArtifactsDir)) { return undefined; } @@ -63,4 +63,3 @@ child.on('error', error => { console.error('Failed to start TidGi app:', error); process.exit(1); }); -