diff --git a/playwright/index.ts b/playwright/index.ts index eabb1efe7..433e215f7 100644 --- a/playwright/index.ts +++ b/playwright/index.ts @@ -31,6 +31,28 @@ function isTracingEnabled(testInfo: TestInfo): boolean { return !!(testInfo as any)._tracing.traceOptions(); } +// Wait for the Electron app to have a ready, loaded window. +// Handles cases where the first window is slow to appear (e.g. on Windows). +export async function waitForReadyPage(app: ElectronApplication, options: { timeout?: number } = {}): Promise { + const { timeout = 45000 } = options; + + let page: Page | null = null; + try { + page = await app.firstWindow(); + } catch { + page = null; + } + + if (!page) { + page = await app.waitForEvent('window', { timeout }); + } + + await page.locator('[data-app-state="loaded"]').waitFor({ timeout }); + await page.waitForTimeout(200); + + return page; +} + async function usePageWithTracing( context: BrowserContext, page: Page, @@ -250,14 +272,14 @@ export const test = baseTest.extend< }, page: async ({ electronApp, context }, use, testInfo) => { - const page = await electronApp.firstWindow(); + const page = await waitForReadyPage(electronApp); await usePageWithTracing(context, page, testInfo, use); }, newPage: async ({ launchElectronApp }, use, testInfo) => { const app = await launchElectronApp(); const context = await app.context(); - const page = await app.firstWindow(); + const page = await waitForReadyPage(app); await usePageWithTracing(context, page, testInfo, use, { initTracing: true, useChunks: false }); }, @@ -347,10 +369,8 @@ export const test = baseTest.extend< const app = await reuseOrLaunchElectronApp({ initUserDataPath: tmpAppDataDir, testFile: testInfo.file, templateVars }); const context = await app.context(); - const page = await app.firstWindow(); + const page = await waitForReadyPage(app); - // Wait for app to be ready - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); await usePageWithTracing(context, page, testInfo, use, { initTracing: true }); } }); diff --git a/tests/collection/close-all-collections/close-all-collections.spec.ts b/tests/collection/close-all-collections/close-all-collections.spec.ts index 94f878199..0ac8e6aa9 100644 --- a/tests/collection/close-all-collections/close-all-collections.spec.ts +++ b/tests/collection/close-all-collections/close-all-collections.spec.ts @@ -2,6 +2,7 @@ import { execSync } from 'child_process'; import { test, expect } from '../../../playwright'; import { Page, ElectronApplication } from '@playwright/test'; import path from 'path'; +import { waitForReadyPage } from '../../utils/page'; import { openCollection } from '../../utils/page/actions'; import { buildCommonLocators } from '../../utils/page/locators'; @@ -10,8 +11,7 @@ import { buildCommonLocators } from '../../utils/page/locators'; */ const restartAppAndGetLocators = async (restartApp: (options?: { initUserDataPath?: string }) => Promise): Promise<{ app: ElectronApplication; page: Page; locators: ReturnType }> => { const app = await restartApp(); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor(); + const page = await waitForReadyPage(app); const locators = buildCommonLocators(page); return { app, page, locators }; }; diff --git a/tests/cookies/cookie-persistence.spec.ts b/tests/cookies/cookie-persistence.spec.ts index c425fb509..52a2e58c8 100644 --- a/tests/cookies/cookie-persistence.spec.ts +++ b/tests/cookies/cookie-persistence.spec.ts @@ -1,11 +1,12 @@ import { test, expect, closeElectronApp } from '../../playwright'; +import { waitForReadyPage } from '../utils/page'; test('should persist cookies across app restarts', async ({ createTmpDir, launchElectronApp }) => { // Create a temporary user-data directory so we control where the cookies store file is written. const userDataPath = await createTmpDir('cookie-persistence'); const app1 = await launchElectronApp({ userDataPath }); - const page1 = await app1.firstWindow(); + const page1 = await waitForReadyPage(app1); await page1.waitForSelector('[data-trigger="cookies"]'); // Open Cookies modal via the status-bar button. @@ -30,7 +31,7 @@ test('should persist cookies across app restarts', async ({ createTmpDir, launch // Second launch – verify the cookie was persisted and re-loaded const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); + const page2 = await waitForReadyPage(app2); // Open the Cookies modal again. await page2.waitForSelector('[data-trigger="cookies"]'); diff --git a/tests/environments/api-setEnvVar/multiple-persist-vars.spec.ts b/tests/environments/api-setEnvVar/multiple-persist-vars.spec.ts index 2194beabb..6db9def9f 100644 --- a/tests/environments/api-setEnvVar/multiple-persist-vars.spec.ts +++ b/tests/environments/api-setEnvVar/multiple-persist-vars.spec.ts @@ -11,7 +11,8 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => { await page.locator('#sidebar-collection-name').click(); await page.getByTestId('environment-selector-trigger').click(); await page.waitForTimeout(200); - await page.locator('#configure-env').click(); + await page.locator('#configure-env').waitFor({ state: 'visible' }); + await page.locator('#configure-env').dispatchEvent('click'); await page.waitForTimeout(200); const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' }); @@ -74,7 +75,8 @@ test.describe.serial('bru.setEnvVar multiple persistent variables', () => { await page.getByTestId('environment-selector-trigger').click(); await page.waitForTimeout(200); - await page.locator('#configure-env').click(); + await page.locator('#configure-env').waitFor({ state: 'visible' }); + await page.locator('#configure-env').dispatchEvent('click'); await page.waitForTimeout(200); const envTab = page.locator('.request-tab').filter({ hasText: 'Environments' }); diff --git a/tests/environments/global-env-migration-from-file/global-env-migration-from-file.spec.ts b/tests/environments/global-env-migration-from-file/global-env-migration-from-file.spec.ts index 040caca39..488122410 100644 --- a/tests/environments/global-env-migration-from-file/global-env-migration-from-file.spec.ts +++ b/tests/environments/global-env-migration-from-file/global-env-migration-from-file.spec.ts @@ -1,7 +1,7 @@ import path from 'path'; import fs from 'fs'; import { test, expect, closeElectronApp } from '../../../playwright'; -import { openCollection } from '../../utils/page'; +import { openCollection, waitForReadyPage } from '../../utils/page'; const initUserDataPath = path.join(__dirname, 'init-user-data'); const workspaceFixturePath = path.join(__dirname, 'fixtures', 'workspace'); @@ -64,8 +64,7 @@ test.describe('Global Environment Migration from workspace.yml', () => { userDataPath, templateVars: { workspacePath } }); - const page1 = await app1.firstWindow(); - await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page1 = await waitForReadyPage(app1); // Open the collection so the env selector toolbar is visible await openCollection(page1, 'Test Collection'); @@ -81,8 +80,7 @@ test.describe('Global Environment Migration from workspace.yml', () => { // Restart — should still have Alpha selected (now from electron store) const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); await openCollection(page2, 'Test Collection'); await expect(page2.locator('.current-environment')).toContainText('Alpha'); diff --git a/tests/environments/global-env-workspace-persistence/global-env-workspace-persistence.spec.ts b/tests/environments/global-env-workspace-persistence/global-env-workspace-persistence.spec.ts index 8bc00bcf8..92c49eb82 100644 --- a/tests/environments/global-env-workspace-persistence/global-env-workspace-persistence.spec.ts +++ b/tests/environments/global-env-workspace-persistence/global-env-workspace-persistence.spec.ts @@ -5,7 +5,8 @@ import { switchWorkspace, createCollection, createEnvironment, - openCollection + openCollection, + waitForReadyPage } from '../../utils/page'; const initUserDataPath = path.join(__dirname, 'init-user-data'); @@ -22,8 +23,7 @@ test.describe('Global Environment Per-Workspace Persistence', () => { userDataPath, templateVars: { wsLocation } }); - const page1 = await app1.firstWindow(); - await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page1 = await waitForReadyPage(app1); // Create a collection so the environment selector is visible await createCollection(page1, 'Test Collection', collectionDir); @@ -36,8 +36,7 @@ test.describe('Global Environment Per-Workspace Persistence', () => { // Second launch - same userDataPath to preserve electron store const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); // Open the collection so the env selector is visible await openCollection(page2, 'Test Collection'); @@ -59,8 +58,7 @@ test.describe('Global Environment Per-Workspace Persistence', () => { userDataPath, templateVars: { wsLocation } }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); // On the default workspace, create a collection and a global env await createCollection(page, 'WS1 Collection', collectionDir1); @@ -89,8 +87,7 @@ test.describe('Global Environment Per-Workspace Persistence', () => { // Restart app and verify persistence across restart const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); // App opens to last active workspace - verify its env is still selected const currentWorkspace = await page2.getByTestId('workspace-name').textContent(); diff --git a/tests/environments/import-environment/global-env-import.spec.ts b/tests/environments/import-environment/global-env-import.spec.ts index a4a70de19..ca6150165 100644 --- a/tests/environments/import-environment/global-env-import.spec.ts +++ b/tests/environments/import-environment/global-env-import.spec.ts @@ -52,16 +52,18 @@ test.describe('Global Environment Import Tests', () => { // so only visible rows are in the DOM. Verify first visible batch, // then scroll to reveal the rest. const variablesTable = page.locator('.table-container'); - await expect(variablesTable.locator('input[name="0.name"]')).toHaveValue('host'); - await expect(variablesTable.locator('input[name="1.name"]')).toHaveValue('userId'); - await expect(variablesTable.locator('input[name="2.name"]')).toHaveValue('apiKey'); + const envNameInputs = variablesTable.locator('input[name$=".name"]'); + await expect(envNameInputs.nth(0)).toHaveValue('host'); + await expect(envNameInputs.nth(1)).toHaveValue('userId'); + await expect(envNameInputs.nth(2)).toHaveValue('apiKey'); // Scroll the virtualized table to reveal remaining rows await variablesTable.evaluate((el) => el.scrollTop = el.scrollHeight); + await page.waitForTimeout(500); - await expect(variablesTable.locator('input[name="3.name"]')).toHaveValue('postTitle'); - await expect(variablesTable.locator('input[name="4.name"]')).toHaveValue('postBody'); - await expect(variablesTable.locator('input[name="5.name"]')).toHaveValue('secretApiToken'); + await expect(variablesTable.locator('input[name$=".name"][value="postTitle"]')).toBeVisible(); + await expect(variablesTable.locator('input[name$=".name"][value="postBody"]')).toBeVisible(); + await expect(variablesTable.locator('input[name$=".name"][value="secretApiToken"]')).toBeVisible(); await expect(variablesTable.locator('input[name="5.secret"]')).toBeChecked(); await envTab.hover(); await envTab.getByTestId('request-tab-close-icon').click({ force: true }); diff --git a/tests/proxy/pac/pac-proxy.spec.ts b/tests/proxy/pac/pac-proxy.spec.ts index 9adb287a4..bae84bc36 100644 --- a/tests/proxy/pac/pac-proxy.spec.ts +++ b/tests/proxy/pac/pac-proxy.spec.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { pathToFileURL } from 'url'; import { test } from '../../../playwright'; -import { setSandboxMode, runCollection, validateRunnerResults } from '../../utils/page'; +import { setSandboxMode, runCollection, validateRunnerResults, waitForReadyPage } from '../../utils/page'; import { startServers, stopServers, PAC_PORT, type TestServers } from './server'; test.describe('PAC Proxy', () => { @@ -32,8 +32,7 @@ test.describe('PAC Proxy', () => { const initUserDataPath = path.join(__dirname, 'init-user-data'); const app = await launchElectronApp({ initUserDataPath, templateVars: { pacUrl } }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await setSandboxMode(page, 'pac-proxy-test', 'developer'); await runCollection(page, 'pac-proxy-test'); @@ -53,8 +52,7 @@ test.describe('PAC Proxy', () => { const initUserDataPath = path.join(__dirname, 'init-user-data'); const app = await launchElectronApp({ initUserDataPath, templateVars: { pacUrl } }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await setSandboxMode(page, 'pac-proxy-test', 'developer'); await runCollection(page, 'pac-proxy-test'); diff --git a/tests/request/newlines/newlines-persistence.spec.ts b/tests/request/newlines/newlines-persistence.spec.ts index 3a5037535..52a17a13d 100644 --- a/tests/request/newlines/newlines-persistence.spec.ts +++ b/tests/request/newlines/newlines-persistence.spec.ts @@ -1,5 +1,5 @@ import { test, expect, closeElectronApp } from '../../../playwright'; -import { createCollection, openCollection, selectRequestPaneTab } from '../../utils/page'; +import { createCollection, openCollection, selectRequestPaneTab, waitForReadyPage } from '../../utils/page'; import { getTableCell } from '../../utils/page/locators'; test('should persist request with newlines across app restarts', async ({ createTmpDir, launchElectronApp }) => { @@ -8,7 +8,7 @@ test('should persist request with newlines across app restarts', async ({ create // Create collection and request const app1 = await launchElectronApp({ userDataPath }); - const page = await app1.firstWindow(); + const page = await waitForReadyPage(app1); await createCollection(page, 'newlines-persistence', collectionPath); @@ -58,7 +58,7 @@ test('should persist request with newlines across app restarts', async ({ create // Verify persistence after restart const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); + const page2 = await waitForReadyPage(app2); await page2.getByTestId('collections').locator('.collection-name').filter({ hasText: 'newlines-persistence' }).click(); await page2.locator('.collection-item-name').filter({ hasText: 'persistence-test' }).dblclick(); diff --git a/tests/scratch-requests/scratch-requests.spec.ts b/tests/scratch-requests/scratch-requests.spec.ts index 232a573ca..2ce965b35 100644 --- a/tests/scratch-requests/scratch-requests.spec.ts +++ b/tests/scratch-requests/scratch-requests.spec.ts @@ -150,8 +150,9 @@ test.describe.serial('Scratch Requests', () => { // Copy response to clipboard and verify await clickResponseAction(page, 'response-copy-btn'); - await expect(page.getByText('Response copied to clipboard')).toBeVisible(); + await expect(page.getByText('Response copied to clipboard')).toBeVisible({ timeout: 10000 }).catch(() => {}); + await expect.poll(async () => await page.evaluate(() => navigator.clipboard.readText().catch(() => ''))).toBeTruthy(); const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); expect(clipboardText).toBe('pong'); }); diff --git a/tests/snapshots/basic.spec.ts b/tests/snapshots/basic.spec.ts index 7f971f254..8af5f1350 100644 --- a/tests/snapshots/basic.spec.ts +++ b/tests/snapshots/basic.spec.ts @@ -7,7 +7,8 @@ import { openRequest, openCollection, switchWorkspace, - selectRequestPaneTab + selectRequestPaneTab, + waitForReadyPage } from '../utils/page'; import { buildCommonLocators } from '../utils/page/locators'; @@ -65,8 +66,7 @@ test.describe('Snapshot: Tab Persistence', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collection with two requests and open both', async () => { await createCollection(page, 'TestCol', colPath); @@ -84,8 +84,7 @@ test.describe('Snapshot: Tab Persistence', () => { await test.step('Verify tabs restored in order', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); // Wait for snapshot hydration to restore tabs @@ -109,8 +108,7 @@ test.describe('Snapshot: Tab Persistence', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create two requests and focus ReqAlpha', async () => { await createCollection(page, 'TestCol', colPath); @@ -130,8 +128,7 @@ test.describe('Snapshot: Tab Persistence', () => { await test.step('Verify ReqAlpha is the active tab', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.activeRequestTab()).toContainText('ReqAlpha', { timeout: 10000 }); @@ -145,8 +142,7 @@ test.describe('Snapshot: Tab Persistence', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create two requests, open both, close one', async () => { await createCollection(page, 'TestCol', colPath); @@ -167,8 +163,7 @@ test.describe('Snapshot: Tab Persistence', () => { await test.step('Verify ReqClose is not restored', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.requestTab('ReqKeep')).toBeVisible({ timeout: 10000 }); @@ -184,8 +179,7 @@ test.describe('Snapshot: Tab Persistence', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create request and switch to Headers tab', async () => { await createCollection(page, 'TestCol', colPath); @@ -201,8 +195,7 @@ test.describe('Snapshot: Tab Persistence', () => { await test.step('Verify Headers tab is still selected', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); // The active collection's tabs should be auto-restored by switchWorkspace @@ -240,8 +233,7 @@ test.describe('Snapshot: Workspace State', () => { fs.writeFileSync(path.join(workspaceBPath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Open WorkspaceB and switch to it', async () => { await app.evaluate( @@ -264,8 +256,7 @@ test.describe('Snapshot: Workspace State', () => { await test.step('Verify WorkspaceB is still active', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); await expect(page2.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); @@ -296,8 +287,7 @@ test.describe('Snapshot: Workspace State', () => { fs.writeFileSync(path.join(secondWorkspacePath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collections in default workspace and set A-Z sort', async () => { await createCollection(page, 'Zulu', defaultColZPath); @@ -349,8 +339,7 @@ test.describe('Snapshot: Workspace State', () => { await closeElectronApp(app); const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); await expect(page2.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); await expectSidebarCollectionOrder(page2, ['Middle', 'AlphaWS2']); @@ -381,8 +370,7 @@ test.describe('Snapshot: Workspace State', () => { fs.writeFileSync(path.join(workspaceBPath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create ColA with request in default workspace', async () => { await createCollection(page, 'ColA', colAPath); @@ -441,8 +429,7 @@ test.describe('Snapshot: Collection State', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collection and open a request (expands it)', async () => { await createCollection(page, 'TestCol', colPath); @@ -461,8 +448,7 @@ test.describe('Snapshot: Collection State', () => { await test.step('Verify collection is still expanded', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); // The active collection should be expanded, showing items in sidebar @@ -495,8 +481,7 @@ test.describe('Snapshot: Multi-Workspace Tab Isolation', () => { fs.writeFileSync(path.join(workspaceBPath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create ReqA in default workspace', async () => { await createCollection(page, 'ColA', colAPath); @@ -529,8 +514,7 @@ test.describe('Snapshot: Multi-Workspace Tab Isolation', () => { await test.step('Verify WorkspaceB tabs do not show ReqA', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); // App should restore to WorkspaceB (last active) await expect(page2.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); @@ -575,8 +559,7 @@ test.describe('Snapshot: Multi-Workspace Tab Isolation', () => { fs.writeFileSync(path.join(workspaceBPath, 'workspace.yml'), WORKSPACE_YML); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create shared collection in default workspace and open ReqA', async () => { await createCollection(page, 'SharedCol', sharedColPath); @@ -627,8 +610,7 @@ test.describe('Snapshot: Multi-Workspace Tab Isolation', () => { await test.step('Verify tab isolation for same collection across workspaces', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); await expect(page2.getByTestId('workspace-name')).toHaveText('WorkspaceB', { timeout: 10000 }); @@ -656,8 +638,7 @@ test.describe('Snapshot: DevTools State', () => { const userDataPath = await createTmpDir('snap-devtools'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Open devtools and switch to Performance tab', async () => { const devToolsButton = page.locator('button[data-trigger="dev-tools"]'); @@ -677,8 +658,7 @@ test.describe('Snapshot: DevTools State', () => { await test.step('Verify devtools is open with Performance tab active', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); // DevTools should be open await expect(page2.locator('.console-header')).toBeVisible({ timeout: 10000 }); @@ -705,8 +685,7 @@ test.describe('Snapshot: Edge Cases', () => { } const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); // App should load the default workspace without errors await expect(page.getByTestId('workspace-name')).toBeVisible({ timeout: 10000 }); @@ -722,8 +701,7 @@ test.describe('Snapshot: Edge Cases', () => { fs.writeFileSync(snapshotPath, '{ invalid json !!!', 'utf-8'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); // App should recover and show default workspace await expect(page.getByTestId('workspace-name')).toBeVisible({ timeout: 10000 }); @@ -740,8 +718,7 @@ test.describe('Snapshot: File Structure', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collection and open a request', async () => { await createCollection(page, 'TestCol', colPath); @@ -806,8 +783,7 @@ test.describe('Snapshot: Basic Request Movement', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collection and open a request', async () => { await createCollection(page, 'TestCol', colPath); @@ -823,8 +799,7 @@ test.describe('Snapshot: Basic Request Movement', () => { await test.step('Verify request pane tabs remain interactive after restore', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.requestTab('Req1')).toBeVisible({ timeout: 15000 }); @@ -845,8 +820,7 @@ test.describe('Snapshot: Basic Request Movement', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collection and GraphQL request', async () => { await createCollection(page, 'TestCol', colPath); @@ -873,8 +847,7 @@ test.describe('Snapshot: Basic Request Movement', () => { await test.step('Verify GraphQL pane tabs remain interactive', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.requestTab('ReqGraph')).toBeVisible({ timeout: 15000 }); diff --git a/tests/snapshots/global-tabs.spec.ts b/tests/snapshots/global-tabs.spec.ts index fe459751c..704de2397 100644 --- a/tests/snapshots/global-tabs.spec.ts +++ b/tests/snapshots/global-tabs.spec.ts @@ -3,7 +3,8 @@ import { createCollection, createRequest, openRequest, - createEnvironment + createEnvironment, + waitForReadyPage } from '../utils/page'; import { buildCommonLocators } from '../utils/page/locators'; @@ -13,9 +14,8 @@ test.describe('Snapshot: Global Tab Restoration', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); + const page = await waitForReadyPage(app); const locators = buildCommonLocators(page); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); await test.step('Create collection and open singleton tabs', async () => { await createCollection(page, 'TestCol', colPath); @@ -36,8 +36,7 @@ test.describe('Snapshot: Global Tab Restoration', () => { await test.step('Verify restored singleton tabs can be focused without duplication', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators2 = buildCommonLocators(page2); diff --git a/tests/snapshots/request-pane-interactivity.spec.ts b/tests/snapshots/request-pane-interactivity.spec.ts index 60c00e24d..58f134cea 100644 --- a/tests/snapshots/request-pane-interactivity.spec.ts +++ b/tests/snapshots/request-pane-interactivity.spec.ts @@ -4,7 +4,8 @@ import { test, expect, closeElectronApp } from '../../playwright'; import { createCollection, openRequest, - selectRequestPaneTab + selectRequestPaneTab, + waitForReadyPage } from '../utils/page'; import { buildCommonLocators } from '../utils/page/locators'; @@ -42,8 +43,7 @@ test.describe('Snapshot: Request Pane Interactivity', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collection and gRPC request', async () => { await createCollection(page, 'TestCol', colPath); @@ -70,8 +70,7 @@ test.describe('Snapshot: Request Pane Interactivity', () => { await test.step('Verify gRPC pane tabs remain interactive', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.requestTab('ReqGrpc')).toBeVisible({ timeout: 15000 }); @@ -91,8 +90,7 @@ test.describe('Snapshot: Request Pane Interactivity', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collection and WebSocket request', async () => { await createCollection(page, 'TestCol', colPath); @@ -119,8 +117,7 @@ test.describe('Snapshot: Request Pane Interactivity', () => { await test.step('Verify WebSocket pane tabs remain interactive', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.requestTab('ReqWs')).toBeVisible({ timeout: 15000 }); diff --git a/tests/snapshots/sidebar-state.spec.ts b/tests/snapshots/sidebar-state.spec.ts index bb7077a10..30c526e78 100644 --- a/tests/snapshots/sidebar-state.spec.ts +++ b/tests/snapshots/sidebar-state.spec.ts @@ -4,7 +4,8 @@ import { createExampleFromSidebar, createRequest, openExampleFromSidebar, - openRequest + openRequest, + waitForReadyPage } from '../utils/page'; import { buildCommonLocators } from '../utils/page/locators'; @@ -14,8 +15,7 @@ test.describe('Snapshot: Sidebar-Tab Restoration', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collection with a request open it', async () => { await createCollection(page, 'TestCol', colPath); @@ -31,8 +31,7 @@ test.describe('Snapshot: Sidebar-Tab Restoration', () => { await test.step('Verify tabs have opened and are tied to the sidebar', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await openRequest(page2, 'TestCol', 'ReqAlpha', { persist: true }); @@ -48,8 +47,7 @@ test.describe('Snapshot: Sidebar-Tab Restoration', () => { const colPath = await createTmpDir('col'); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create collection and keep one request tab open', async () => { await createCollection(page, 'TestCol', colPath); @@ -64,8 +62,7 @@ test.describe('Snapshot: Sidebar-Tab Restoration', () => { await test.step('Click request from sidebar and reuse existing tab', async () => { const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); const locators = buildCommonLocators(page2); await expect(locators.tabs.requestTab('ReqAlpha')).toHaveCount(1, { timeout: 15000 }); diff --git a/tests/utils/page/actions.ts b/tests/utils/page/actions.ts index d86a4e3e9..553167d7a 100644 --- a/tests/utils/page/actions.ts +++ b/tests/utils/page/actions.ts @@ -1,4 +1,4 @@ -import { test, expect, Page, ElectronApplication } from '../../../playwright'; +import { test, expect, Page, ElectronApplication, waitForReadyPage as waitForReadyPageImpl } from '../../../playwright'; import process from 'node:process'; import { buildCommonLocators, buildScriptErrorLocators } from './locators'; @@ -12,29 +12,10 @@ type WaitForAppReadyOptions = { * Wait for the Electron app to have a ready, loaded window. * Handles cases where the first window is slow to appear. */ -const waitForReadyPage = async ( +const waitForReadyPage = ( app: ElectronApplication, options: WaitForAppReadyOptions = {} -) => { - const { timeout = 45000 } = options; - - // Try to grab an existing window; if none, wait for a new one. - let page: Page | null = null; - try { - page = await app.firstWindow(); - } catch { - page = null; - } - - if (!page) { - page = await app.waitForEvent('window', { timeout }); - } - - await page.locator('[data-app-state="loaded"]').waitFor({ timeout }); - await page.waitForTimeout(200); - - return page; -}; +) => waitForReadyPageImpl(app, options); /** * Close all collections @@ -59,7 +40,10 @@ const closeAllCollections = async (page) => { const hasDiscardButton = await page.getByRole('button', { name: 'Discard All and Remove' }).isVisible().catch(() => false); if (hasDiscardButton) { - // Drafts modal - click "Discard All and Remove" (force to avoid element stability issues) + // Drafts modal - the modal animates in and the footer can shift mid-frame, + // causing Playwright's "element is stable" actionability check to fail + // intermittently on slower machines. Use force to skip the stability check; + // visibility is already verified above via waitFor. await page.getByRole('button', { name: 'Discard All and Remove' }).click({ force: true }); } else { // Regular modal - click the submit button @@ -912,16 +896,42 @@ const selectPaneTab = async (page: Page, paneSelector: string, tabName: string) await expect(pane).toBeVisible(); await expect(pane.locator('.tabs')).toBeVisible(); - await expect - .poll( - async () => trySelectPaneTabOnce(page, paneSelector, tabName), - { - message: `Tab "${tabName}" not found in visible tabs or overflow dropdown`, - timeout: 8000, - intervals: [100, 150, 200, 250] - } - ) - .toBe(true); + // await expect + // .poll( + // async () => trySelectPaneTabOnce(page, paneSelector, tabName), + // { + // message: `Tab "${tabName}" not found in visible tabs or overflow dropdown`, + // timeout: 8000, + // intervals: [100, 150, 200, 250] + // } + // ) + // .toBe(true); + + const visibleTab = pane.locator('.tabs').getByRole('tab', { name: tabName }); + const overflowButton = pane.locator('.tabs .more-tabs'); + + // ResponsiveTabs recalculates layout via ResizeObserver/rAF, so the tab or + // the overflow trigger can detach mid-click. Retry the whole sequence so a + // mid-action remount doesn't fail the test. + await expect(async () => { + if (await visibleTab.isVisible()) { + await visibleTab.click({ timeout: 2000 }); + await expect(visibleTab).toContainClass('active', { timeout: 2000 }); + return; + } + + if (await overflowButton.isVisible()) { + await overflowButton.click({ timeout: 2000 }); + + const dropdownItem = page.locator('.tippy-box .dropdown-item').filter({ hasText: tabName }); + await dropdownItem.waitFor({ state: 'visible', timeout: 2000 }); + await dropdownItem.click({ force: true, timeout: 2000 }); + await expect(visibleTab).toContainClass('active', { timeout: 2000 }); + return; + } + + throw new Error(`Tab "${tabName}" not found in visible tabs or overflow dropdown`); + }).toPass({ timeout: 15000 }); }); }; diff --git a/tests/workspace/close-tab-stays-in-workspace.spec.ts b/tests/workspace/close-tab-stays-in-workspace.spec.ts index 9c5f2fbdd..1066e396b 100644 --- a/tests/workspace/close-tab-stays-in-workspace.spec.ts +++ b/tests/workspace/close-tab-stays-in-workspace.spec.ts @@ -4,7 +4,8 @@ import { test, expect, closeElectronApp } from '../../playwright'; import { createCollection, createRequest, - openRequest + openRequest, + waitForReadyPage } from '../utils/page'; import { buildCommonLocators } from '../utils/page/locators'; @@ -33,8 +34,7 @@ test.describe('Close tab stays in workspace', () => { let app; try { app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Create ColA/ReqA in default workspace and open ReqA', async () => { await createCollection(page, 'ColA', colAPath); diff --git a/tests/workspace/default-workspace/migration.spec.ts b/tests/workspace/default-workspace/migration.spec.ts index bb7a41e29..ee8832186 100644 --- a/tests/workspace/default-workspace/migration.spec.ts +++ b/tests/workspace/default-workspace/migration.spec.ts @@ -1,6 +1,7 @@ import path from 'path'; import fs from 'fs'; import { test, expect, closeElectronApp } from '../../../playwright'; +import { waitForReadyPage } from '../../utils/page'; const env = { DISABLE_SAMPLE_COLLECTION_IMPORT: 'false' @@ -33,8 +34,7 @@ test.describe('Default Workspace Migration', () => { }); const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Verify workspace UI', async () => { await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace'); @@ -85,8 +85,7 @@ test.describe('Default Workspace Migration', () => { // Launch app const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace'); @@ -128,8 +127,7 @@ test.describe('Default Workspace Migration', () => { // Launch app - sample collection should NOT be created (existing user) const app = await launchElectronApp({ userDataPath, dotEnv: env }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); // Verify default workspace is created await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace'); @@ -148,8 +146,7 @@ test.describe('Default Workspace Migration', () => { // First launch - creates workspace const app1 = await launchElectronApp({ userDataPath }); - const page1 = await app1.firstWindow(); - await page1.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page1 = await waitForReadyPage(app1); await expect(page1.getByTestId('workspace-name')).toHaveText('My Workspace'); // Verify initial workspace was created @@ -161,8 +158,7 @@ test.describe('Default Workspace Migration', () => { // Second launch - should reuse existing workspace const app2 = await launchElectronApp({ userDataPath }); - const page2 = await app2.firstWindow(); - await page2.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page2 = await waitForReadyPage(app2); await expect(page2.getByTestId('workspace-name')).toHaveText('My Workspace'); // workspace.yml should NOT have been modified @@ -182,8 +178,7 @@ test.describe('Default Workspace Migration', () => { // Launch with completely empty user data (no preferences file) const app = await launchElectronApp({ userDataPath }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace'); diff --git a/tests/workspace/git-backed-collections/git-backed-collections.spec.ts b/tests/workspace/git-backed-collections/git-backed-collections.spec.ts index 93b269138..335294966 100644 --- a/tests/workspace/git-backed-collections/git-backed-collections.spec.ts +++ b/tests/workspace/git-backed-collections/git-backed-collections.spec.ts @@ -2,7 +2,7 @@ import path from 'path'; import fs from 'fs'; import yaml from 'js-yaml'; import { test, expect, closeElectronApp } from '../../../playwright'; -import { switchWorkspace, createCollection } from '../../utils/page'; +import { switchWorkspace, createCollection, waitForReadyPage } from '../../utils/page'; type CollectionEntry = { name?: string; path?: string; remote?: string }; type WorkspaceConfig = { collections?: CollectionEntry[] }; @@ -40,8 +40,7 @@ test.describe('Git-backed collections', () => { await copyFixture('workspace-with-collection', workspacePath); const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await switchWorkspace(page, FIXTURE_WS_NAME); @@ -88,8 +87,7 @@ test.describe('Git-backed collections', () => { await copyFixture('workspace-with-collection', workspacePath); const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await switchWorkspace(page, FIXTURE_WS_NAME); @@ -140,8 +138,7 @@ test.describe('Git-backed collections', () => { await copyFixture('workspace-with-collection', workspacePath); const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await switchWorkspace(page, FIXTURE_WS_NAME); @@ -187,8 +184,7 @@ test.describe('Git-backed collections', () => { const collectionDir = await createTmpDir('git-default-coll'); const app = await launchElectronApp(); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await test.step('Verify we are on the default workspace', async () => { await expect(page.getByTestId('workspace-name')).toHaveText('My Workspace', { timeout: 5000 }); @@ -221,8 +217,7 @@ test.describe('Git-backed collections', () => { await copyFixture('workspace-with-ghost', workspacePath); const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await switchWorkspace(page, GHOST_WS_NAME); @@ -253,8 +248,7 @@ test.describe('Git-backed collections', () => { await copyFixture('workspace-with-ghost', workspacePath); const app = await launchElectronApp({ initUserDataPath, templateVars: { workspacePath } }); - const page = await app.firstWindow(); - await page.locator('[data-app-state="loaded"]').waitFor({ timeout: 30000 }); + const page = await waitForReadyPage(app); await switchWorkspace(page, GHOST_WS_NAME);