Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions e2e/cloud/admin-users-console.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { AuthTemplateSlug, ConnectionName, IntegrationSlug } from "@executor-js/
import { scenario } from "../src/scenario";
import { Api, Browser, Target } from "../src/services";
import { accountIdOf, forBrowser, joinOrg } from "./support/session";
import { visit } from "../src/surfaces/browser";

const api = composePluginApi([openApiHttpPlugin()] as const);
type Client = HttpApiClient.ForApi<typeof api>;
Expand Down Expand Up @@ -126,7 +127,7 @@ scenario(
let slug = "";

await step("Land in the workspace and canonicalize onto the org slug", async () => {
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), {
timeout: 30_000,
});
Expand Down Expand Up @@ -323,7 +324,7 @@ scenario(
let slug = "";

await step("Sign in as a plain member of the same workspace", async () => {
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), {
timeout: 30_000,
});
Expand All @@ -345,7 +346,7 @@ scenario(
await step(
"Reaching the URL directly says no access, not an empty workspace",
async () => {
await page.goto(`/${slug}/users`, { waitUntil: "networkidle" });
await visit(page, `/${slug}/users`);
// The denial is the assertion: an empty table here would misreport
// a populated workspace as having no users.
await page
Expand Down
3 changes: 2 additions & 1 deletion e2e/cloud/auth-routing-flow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Effect } from "effect";

import { scenario } from "../src/scenario";
import { Browser, Target } from "../src/services";
import { settle } from "../src/surfaces/browser";

scenario(
"Auth routing · signed out → login → onboarding → dashboard, one browser session",
Expand Down Expand Up @@ -58,7 +59,7 @@ scenario(
await step("Create the first org → canonical dashboard at /<slug>", async () => {
const orgName = "Flow Test Org";
const orgNameInput = page.getByPlaceholder("Northwind Labs");
await page.waitForLoadState("networkidle");
await settle(page);
await orgNameInput.fill(orgName);
await page.waitForTimeout(250);
if ((await orgNameInput.inputValue()) !== orgName) {
Expand Down
5 changes: 3 additions & 2 deletions e2e/cloud/billing-trial-checkout-stale.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Effect } from "effect";

import { scenario } from "../src/scenario";
import { Autumn, Billing, Browser, Target } from "../src/services";
import { visit } from "../src/surfaces/browser";

scenario(
"Billing · completing the trial checkout shows the new plan without a reload",
Expand Down Expand Up @@ -51,12 +52,12 @@ scenario(
// "/undefined/billing/plans" — the billing fetches fire under the bogus
// slug, fail, and are never refetched (autumn-js staleTime 60s), leaving
// the plans grid empty forever.
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), {
timeout: 30_000,
});
const slug = new URL(page.url()).pathname.split("/").filter(Boolean)[0];
await page.goto(`/${slug}/billing/plans`, { waitUntil: "networkidle" });
await visit(page, `/${slug}/billing/plans`);
await page.getByRole("heading", { name: "Choose a plan" }).waitFor();
await startTrial.waitFor();
});
Expand Down
7 changes: 4 additions & 3 deletions e2e/cloud/connect-panel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Effect } from "effect";

import { scenario } from "../src/scenario";
import { Browser, Target } from "../src/services";
import { visit, settle } from "../src/surfaces/browser";

scenario(
"Connect · the agent-connect panel gives working copy for both transports",
Expand All @@ -17,7 +18,7 @@ scenario(

yield* browser.session(identity, async ({ page, step }) => {
await step("Open the Integrations page", async () => {
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
await page.getByText("Connect an agent").first().waitFor();
});

Expand All @@ -35,7 +36,7 @@ scenario(

await step("Switch to Standard I/O", async () => {
await page.getByRole("tab", { name: "Standard I/O" }).click();
await page.waitForLoadState("networkidle");
await settle(page);
});
const stdioCommand = await command();
expect(stdioCommand, "the command changed for stdio").not.toBe(httpCommand);
Expand All @@ -45,7 +46,7 @@ scenario(

await step("Switch back to Remote HTTP", async () => {
await page.getByRole("tab", { name: "Remote HTTP" }).click();
await page.waitForLoadState("networkidle");
await settle(page);
});
expect(await command(), "the HTTP command is restored").toContain("--transport http");
});
Expand Down
5 changes: 3 additions & 2 deletions e2e/cloud/connection-modal-oauth-abandon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { serveOAuthTestServer } from "@executor-js/sdk/testing";

import { scenario } from "../src/scenario";
import { Api, Browser, Target } from "../src/services";
import { visit, settle } from "../src/surfaces/browser";

const api = composePluginApi([openApiHttpPlugin()] as const);

Expand Down Expand Up @@ -98,7 +99,7 @@ scenario(
const connecting = dialog.getByRole("button", { name: "Connecting…" });

await step("Open the integration and start a new connection", async () => {
await page.goto(`/integrations/${integration}`, { waitUntil: "networkidle" });
await visit(page, `/integrations/${integration}`);
await addConnection.click();
// The registered app is auto-selected, so the OAuth connect button is
// present and enabled.
Expand Down Expand Up @@ -133,7 +134,7 @@ scenario(
async () => {
await addConnection.click();
await dialog.waitFor({ state: "visible", timeout: 15_000 });
await page.waitForLoadState("networkidle");
await settle(page);

// The guarantee: the reopened modal is reset. Before the fix it stays
// wedged on "Connecting…" (the abandoned flow's busy state survived the
Expand Down
3 changes: 2 additions & 1 deletion e2e/cloud/logout-stale-session.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { Page } from "playwright";

import { scenario } from "../src/scenario";
import { Api, Browser, Target } from "../src/services";
import { settle } from "../src/surfaces/browser";

/** The display-only identity cookie the SSR gate mints (non-HttpOnly). */
const HINT_COOKIE = "executor-auth-hint";
Expand Down Expand Up @@ -145,7 +146,7 @@ scenario(
await page.waitForURL((url) => url.toString() !== before, {
timeout: 15_000,
});
await page.waitForLoadState("networkidle");
await settle(page);
});

const shown = (await page.locator("body").innerText()).trim();
Expand Down
7 changes: 4 additions & 3 deletions e2e/cloud/mcp-browser-approval-org-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { composePluginApi } from "@executor-js/api/server";
import { scenario } from "../src/scenario";
import { Api, Browser, Mcp, Target } from "../src/services";
import { type McpBrowserApproval, parseBrowserApproval } from "../src/surfaces/mcp";
import { visit } from "../src/surfaces/browser";

const coreApi = composePluginApi([] as const);

Expand Down Expand Up @@ -168,21 +169,21 @@ scenario(
mcpSession.awaitResume(approval.executionId),
browser.session(identity, async ({ page, step }) => {
await step("Land in the original organization", async () => {
await page.goto(`/${orgA.slug}`, { waitUntil: "networkidle" });
await visit(page, `/${orgA.slug}`);
await expectOrgShell(page, orgA);
});

await step("The browser session is switched to another organization", async () => {
await setWorkosSessionCookie(page, target.baseUrl, sessionB);
await page.goto(`/${orgB.slug}`, { waitUntil: "networkidle" });
await visit(page, `/${orgB.slug}`);
await expectOrgShell(page, orgB);
});

await step("Open the original organization's approval URL and approve", async () => {
const loadRequest = page.waitForRequest(approvalApiRequest(approval, "GET"), {
timeout: 30_000,
});
await page.goto(approval.approvalUrl, { waitUntil: "networkidle" });
await visit(page, approval.approvalUrl);
expect(
(await loadRequest).headers()["x-executor-organization"],
"loading the approval page scopes the paused execution lookup to org A",
Expand Down
3 changes: 2 additions & 1 deletion e2e/cloud/mcp-browser-resume-page.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import { Api, Browser, Mcp, Target } from "../src/services";
import { parseBrowserApproval } from "../src/surfaces/mcp";
import type { Identity } from "../src/target";
import { visit } from "../src/surfaces/browser";

const coreApi = composePluginApi([] as const);

Expand Down Expand Up @@ -224,7 +225,7 @@
),
browser.session(identity, async ({ page, step }) => {
await step("Open the paused execution approval page", async () => {
await page.goto(pathWithSearch(approval.approvalUrl), { waitUntil: "networkidle" });
await visit(page, pathWithSearch(approval.approvalUrl));
await page.getByText("User approval required").waitFor();
});

Expand All @@ -246,7 +247,7 @@

await step("Approve the paused tool call", async () => {
await page.getByRole("button", { name: "Approve" }).click();
await page.getByText("Approve sent").waitFor();

Check failure on line 250 in e2e/cloud/mcp-browser-resume-page.test.ts

View workflow job for this annotation

GitHub Actions / E2E (cloud 8of16)

[cloud] cloud/mcp-browser-resume-page.test.ts > MCP approval · browser resume page approves a paused execution through the UI

TimeoutError: locator.waitFor: Timeout 30000ms exceeded. Call log: - waiting for getByText('Approve sent') to be visible ❯ cloud/mcp-browser-resume-page.test.ts:250:54 ❯ step src/surfaces/browser.ts:276:15 ❯ cloud/mcp-browser-resume-page.test.ts:248:15 ❯ src/surfaces/browser.ts:288:13
});
}),
],
Expand Down
5 changes: 3 additions & 2 deletions e2e/cloud/member-invite-seat-limit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { AccountHttpApi } from "@executor-js/api";

import { scenario } from "../src/scenario";
import { Api, Billing, Browser, Target } from "../src/services";
import { visit } from "../src/surfaces/browser";

// apps/cloud/src/extensions/billing/plans.ts → MEMBER_LIMITS.free
const FREE_MEMBER_SEATS = 3;
Expand All @@ -45,7 +46,7 @@ scenario(
let slug = "";

await step("Land in the app and canonicalize onto the org slug", async () => {
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), {
timeout: 30_000,
});
Expand All @@ -54,7 +55,7 @@ scenario(
});

await step("Open the organization members page", async () => {
await page.goto(`/${slug}/org`, { waitUntil: "networkidle" });
await visit(page, `/${slug}/org`);
await page.getByRole("button", { name: "Invite member" }).waitFor();
});

Expand Down
11 changes: 5 additions & 6 deletions e2e/cloud/oauth-callback-org-scope.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { serveOAuthTestServer } from "@executor-js/sdk/testing";
import { scenario } from "../src/scenario";
import { Api, Browser, Target } from "../src/services";
import type { Identity } from "../src/target";
import { visit } from "../src/surfaces/browser";

const api = composePluginApi([openApiHttpPlugin()] as const);

Expand Down Expand Up @@ -212,13 +213,13 @@ scenario(
await installSameWindowOAuthPopup(page);

await step("Land in the original organization", async () => {
await page.goto(`/${orgA.slug}`, { waitUntil: "networkidle" });
await visit(page, `/${orgA.slug}`);
await expectOrgShell(page, orgA);
});

await step("The browser session is switched to another organization", async () => {
await setWorkosSessionCookie(page, target.baseUrl, sessionB);
await page.goto(`/${orgB.slug}`, { waitUntil: "networkidle" });
await visit(page, `/${orgB.slug}`);
await expectOrgShell(page, orgB);
});

Expand All @@ -232,9 +233,7 @@ scenario(
// refetch, and leave "Connect with OAuth" disabled forever (the flake
// this step used to have). Waiting for org A's shell before opening
// the modal makes every modal fetch run under the settled org A scope.
await page.goto(`/${orgA.slug}/integrations/${String(integration)}`, {
waitUntil: "networkidle",
});
await visit(page, `/${orgA.slug}/integrations/${String(integration)}`);
await page.getByRole("button", { name: new RegExp(escapeRegExp(orgA.name)) }).waitFor({
timeout: 30_000,
});
Expand Down Expand Up @@ -286,7 +285,7 @@ scenario(
await step("The provider returns to the OAuth callback", async () => {
const callbackUrl = await submitProviderLoginFromPage(page);
callback = new URL(callbackUrl);
const response = await page.goto(callbackUrl, { waitUntil: "networkidle" });
const response = await visit(page, callbackUrl);
expect(response?.status(), "the callback renders its popup result page").toBe(200);
});

Expand Down
3 changes: 2 additions & 1 deletion e2e/cloud/onboarding-mcp-url.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Effect } from "effect";

import { scenario } from "../src/scenario";
import { Browser, Target } from "../src/services";
import { visit } from "../src/surfaces/browser";

scenario(
"Onboarding · the MCP setup step hands the user their org-scoped MCP server URL",
Expand All @@ -19,7 +20,7 @@ scenario(
await step(
"A fresh user without an org lands on the create-org onboarding page",
async () => {
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
// Step 1 of 2 — the org-name input is the landmark that proves we're on onboarding.
await page.getByPlaceholder("Northwind Labs").waitFor();
},
Expand Down
7 changes: 4 additions & 3 deletions e2e/cloud/org-api-keys-console.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Effect } from "effect";
import { scenario } from "../src/scenario";
import { Browser, Target } from "../src/services";
import { forBrowser, joinOrg } from "./support/session";
import { visit } from "../src/surfaces/browser";

declare global {
interface Window {
Expand All @@ -45,7 +46,7 @@ scenario(
let slug = "";

await step("Land in the workspace and open the API keys page", async () => {
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), {
timeout: 30_000,
});
Expand Down Expand Up @@ -120,12 +121,12 @@ scenario(
// ── The plain member's view ───────────────────────────────────────────
yield* browser.session(forBrowser(member), async ({ page, step }) => {
await step("A plain member is not shown the Organization keys section", async () => {
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), {
timeout: 30_000,
});
const slug = new URL(page.url()).pathname.split("/").filter(Boolean)[0] ?? "";
await page.goto(`/${slug}/api-keys`, { waitUntil: "networkidle" });
await visit(page, `/${slug}/api-keys`);
// The personal half renders for everyone…
await page
.getByRole("heading", { name: "Personal keys" })
Expand Down
7 changes: 4 additions & 3 deletions e2e/cloud/org-delete.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Effect } from "effect";

import { scenario } from "../src/scenario";
import { Browser, Target } from "../src/services";
import { visit, settle } from "../src/surfaces/browser";

scenario(
"Organizations · an admin deletes the organization from settings",
Expand All @@ -22,7 +23,7 @@ scenario(
const ORG = "Doomed Org";

await step("Fresh user creates an org via onboarding", async () => {
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
await page.getByPlaceholder("Northwind Labs").fill(ORG);
await page.getByRole("button", { name: "Create organization" }).click();
await page.getByText("Connect your MCP client").waitFor();
Expand All @@ -32,13 +33,13 @@ scenario(
await page.waitForURL((url) => /^\/[a-z0-9-]+\/?$/.test(url.pathname), {
timeout: 30_000,
});
await page.waitForLoadState("networkidle");
await settle(page);
});

const slug = new URL(page.url()).pathname.split("/").filter(Boolean)[0]!;

await step("Open Organization settings and find the danger zone", async () => {
await page.goto(`/${slug}/org`, { waitUntil: "networkidle" });
await visit(page, `/${slug}/org`);
// The admin-only danger zone renders (a member would not see it).
await page.getByText("Permanently delete this organization").waitFor();
});
Expand Down
7 changes: 4 additions & 3 deletions e2e/cloud/org-last-visited.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Effect } from "effect";

import { scenario } from "../src/scenario";
import { Browser, Target } from "../src/services";
import { visit } from "../src/surfaces/browser";

const CLOUD_ORIGIN_HEADERS = (baseUrl: string) => ({ origin: new URL(baseUrl).origin });

Expand Down Expand Up @@ -64,7 +65,7 @@ scenario(

yield* browser.session(inB, async ({ page, step }) => {
await step("Work in org A by its slug URL (the session still pins org B)", async () => {
await page.goto(`/${slugA}`, { waitUntil: "networkidle" });
await visit(page, `/${slugA}`);
await page.getByText("Integrations").first().waitFor({ timeout: 30_000 });
// The client records the viewed org once /account/me confirms it.
await page.waitForFunction(
Expand All @@ -75,7 +76,7 @@ scenario(
});

await step("A bare entry (`/`) returns to org A, not the session's org B", async () => {
await page.goto("/", { waitUntil: "networkidle" });
await visit(page, "/");
await page.waitForURL(
(url) => url.pathname === `/${slugA}` || url.pathname === `/${slugA}/`,
{
Expand All @@ -86,7 +87,7 @@ scenario(
});

await step("A bare deep link keeps its path while landing in org A", async () => {
await page.goto("/policies", { waitUntil: "networkidle" });
await visit(page, "/policies");
await page.waitForURL((url) => url.pathname === `/${slugA}/policies`, { timeout: 30_000 });
await page.getByText("Policies").first().waitFor({ timeout: 30_000 });
});
Expand Down
Loading
Loading