mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2026-03-09 00:10:31 -07:00
Centralize and standardize E2E test timeouts
Extracted timeout values into features/supports/timeouts.ts and replaced hardcoded timeouts in step definitions with named constants. This ensures consistent timeout handling across local and CI environments, reduces duplication, and clarifies intent. Also improved workspace update logic to check watch-fs state before restart and cleaned up related log marker handling.
This commit is contained in:
parent
cce7152a2e
commit
9be4ef64f7
8 changed files with 141 additions and 84 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<IWorkspace | null>;
|
||||
}, 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<string, unknown> }) => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
36
features/supports/timeouts.ts
Normal file
36
features/supports/timeouts.ts
Normal file
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue