diff --git a/.github/workflows/insomnia-test.yml b/.github/workflows/insomnia-test.yml index 39ebb017fc..5f73e4bdbe 100644 --- a/.github/workflows/insomnia-test.yml +++ b/.github/workflows/insomnia-test.yml @@ -102,8 +102,8 @@ jobs: fail-fast: false matrix: # To scale up, add indices here and bump shardTotal to match. - shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] - shardTotal: [16] + shardIndex: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24] + shardTotal: [24] steps: - name: Checkout repo uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 diff --git a/packages/insomnia-test/pages/auth-tab.page.ts b/packages/insomnia-test/pages/auth-tab.page.ts index 39eb1a90af..e2814c489b 100644 --- a/packages/insomnia-test/pages/auth-tab.page.ts +++ b/packages/insomnia-test/pages/auth-tab.page.ts @@ -244,23 +244,26 @@ export class AuthTabComponent extends TabPanelPage { reapply: () => this.setCodeMirrorValue(editor, value), }; await edit.reapply!(); - await this.waitForAuthFieldsPersisted([edit]); + // Verify every field written so far, not just this one: its save can + // overwrite an earlier field from a stale copy of the auth config. + await this.waitForAuthFieldsPersisted([...edits, edit]); edits.push(edit); } if (fields.signatureMethod) { const signatureMethod = fields.signatureMethod; - const selectMethod = () => - this.page + const selectMethod = async () => { + await this.page .locator(this.TABPANEL) .locator("#Signature-Method") .selectOption(signatureMethod); + }; await selectMethod(); const edit: AuthFieldEdit = { field: "signatureMethod", value: signatureMethod, reapply: selectMethod, }; - await this.waitForAuthFieldsPersisted([edit]); + await this.waitForAuthFieldsPersisted([...edits, edit]); edits.push(edit); } // A later field's debounced save can overwrite an earlier one from a diff --git a/packages/insomnia-test/pages/cloud-sync.page.ts b/packages/insomnia-test/pages/cloud-sync.page.ts index 54d065c18d..8fe7b1cf02 100644 --- a/packages/insomnia-test/pages/cloud-sync.page.ts +++ b/packages/insomnia-test/pages/cloud-sync.page.ts @@ -317,11 +317,13 @@ export class CloudSyncPage extends BasePage { * @param branch - The local branch to delete */ async deleteLocalBranch(branch: string): Promise { - await this.branchesDialog + const row = this.branchesDialog .getByLabel("Branches list", { exact: true }) - .getByLabel(branch, { exact: true }) - .getByRole("button", { name: "Delete" }) - .dblclick(); + .getByLabel(branch, { exact: true }); + await row.getByRole("button", { name: "Delete" }).click(); + // A dblclick can fire its 2nd click before React re-renders the + // button into its "Confirm" state, so wait for it explicitly. + await row.getByRole("button", { name: "Confirm" }).click(); } /** @@ -347,11 +349,13 @@ export class CloudSyncPage extends BasePage { * @param branch - The local branch to merge in */ async mergeBranch(branch: string): Promise { - await this.branchesDialog + const row = this.branchesDialog .getByLabel("Branches list", { exact: true }) - .getByLabel(branch, { exact: true }) - .getByRole("button", { name: "Merge" }) - .dblclick(); + .getByLabel(branch, { exact: true }); + await row.getByRole("button", { name: "Merge" }).click(); + // A dblclick can fire its 2nd click before React re-renders the + // button into its "Confirm" state, so wait for it explicitly. + await row.getByRole("button", { name: "Confirm" }).click(); } /** diff --git a/packages/insomnia-test/pages/mcp-client.page.ts b/packages/insomnia-test/pages/mcp-client.page.ts index d0ebf05fe5..eeeab5518a 100644 --- a/packages/insomnia-test/pages/mcp-client.page.ts +++ b/packages/insomnia-test/pages/mcp-client.page.ts @@ -38,15 +38,25 @@ export class McpClientPage extends RequestPage { */ async getTools(): Promise { // The sidebar is populated asynchronously once the server answers - // tools/list, so wait for the first row before reading the list. - await expect(this.page.locator(this.TOOL_ROW).first()).toBeVisible({ - timeout: DEFAULT_TIMEOUT, - }); - return this.page - .locator(this.TOOL_ROW) - .evaluateAll((rows) => - rows.map((row) => row.getAttribute("aria-label") ?? ""), - ); + // tools/list, and it can re-render (briefly empty) while it settles, + // so poll until a non-empty list of names has been read. + let tools: string[] = []; + await expect + .poll( + async () => { + tools = ( + await this.page + .locator(this.TOOL_ROW) + .evaluateAll((rows) => + rows.map((row) => row.getAttribute("aria-label") ?? ""), + ) + ).filter(Boolean); + return tools.length; + }, + { timeout: DEFAULT_TIMEOUT }, + ) + .toBeGreaterThan(0); + return tools; } /** diff --git a/packages/insomnia-test/pages/socket-io-request.page.ts b/packages/insomnia-test/pages/socket-io-request.page.ts index 14c6b0e7bd..8dec89a1a6 100644 --- a/packages/insomnia-test/pages/socket-io-request.page.ts +++ b/packages/insomnia-test/pages/socket-io-request.page.ts @@ -1,3 +1,6 @@ +import { expect } from "@playwright/test"; + +import { DEFAULT_TIMEOUT } from "../misc/constants"; import type { RequestHeader,SocketIOMessage } from "../models/socket-io-request"; import { RequestPage } from "./request.page"; @@ -5,6 +8,19 @@ export class SocketIORequestPage extends RequestPage { protected readonly urlBarId = "websocket-url-bar"; protected readonly persistedDbFile = "insomnia.SocketIORequest.db"; + /** + * Clicks Connect and waits for the button to flip to "Disconnect", so a + * following send doesn't race the handshake and get dropped. + */ + async connect(): Promise { + await super.connect(); + await expect( + this.page + .locator(this.PANE) + .getByRole("button", { name: "Disconnect", exact: true }), + ).toBeVisible({ timeout: DEFAULT_TIMEOUT }); + } + /** * Reads the full Socket.IO request state from the UI (url, headers, and * the composed message).