From 8877a99cefe48055e77fa2134e8d2b434385319c Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Wed, 8 Jul 2026 19:18:45 +0000 Subject: [PATCH 1/9] fix(site/e2e): fix login helper race condition causing navigation flake The login() helper waited for the post-login URL to match /workspaces via polling, but did not wait for the page to finish loading. When a test immediately navigated elsewhere after login(), the still-loading page could trigger a competing navigation, causing: page.goto: Navigation to "/deployment/users" is interrupted by another navigation to "/" Replace the URL polling with waitForLoadState("domcontentloaded"), which waits for the hard navigation (location.href assignment in LoginPage.tsx) to complete. This is the correct completion signal since login uses a full page reload, not SPA routing. This also removes the coupling between login() and the /workspaces landing page. A dedicated test in login.spec.ts now covers the post-login redirect separately. --- site/e2e/helpers.ts | 4 ++-- site/e2e/tests/login.spec.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 site/e2e/tests/login.spec.ts diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 1acaa8cbdbe..acd5090990f 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -76,11 +76,11 @@ export async function login(page: Page, options: LoginOptions = users.owner) { // biome-ignore lint/suspicious/noExplicitAny: reset the current user (ctx as any)[Symbol.for("currentUser")] = undefined; await ctx.clearCookies(); - await page.goto("/login"); + await page.goto("/login", { waitUntil: "domcontentloaded" }); await page.getByLabel("Email").fill(options.email); await page.getByLabel("Password").fill(options.password); await page.getByRole("button", { name: "Sign In" }).click(); - await expectUrl(page).toHavePathName("/workspaces"); + await page.waitForLoadState("domcontentloaded"); // biome-ignore lint/suspicious/noExplicitAny: update once logged in (ctx as any)[Symbol.for("currentUser")] = options; } diff --git a/site/e2e/tests/login.spec.ts b/site/e2e/tests/login.spec.ts new file mode 100644 index 00000000000..1b237e49a5e --- /dev/null +++ b/site/e2e/tests/login.spec.ts @@ -0,0 +1,13 @@ +import { test } from "@playwright/test"; +import { expectUrl } from "../expectUrl"; +import { login } from "../helpers"; +import { beforeCoderTest } from "../hooks"; + +test.beforeEach(async ({ page }) => { + beforeCoderTest(page); +}); + +test("login redirects to /workspaces", async ({ page }) => { + await login(page); + await expectUrl(page).toHavePathName("/workspaces"); +}); From 542b3822febb6ff1f112bb9e92bd2e43ab6411e8 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Wed, 8 Jul 2026 19:37:58 +0000 Subject: [PATCH 2/9] fix(site/e2e): use waitForURL instead of waitForLoadState in login waitForLoadState("domcontentloaded") resolves immediately on the current page's DOM (the login page) before the hard navigation triggered by location.href even starts. This caused login() to return before the session cookie was set, breaking all downstream tests with "session token not found". Use page.waitForURL(/\/workspaces/) instead. This waits for the actual hard navigation to complete and the new page to fully load (default waitUntil: "load"), which guarantees the session cookie is present. --- site/e2e/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index acd5090990f..74e8aebd4c6 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -80,7 +80,7 @@ export async function login(page: Page, options: LoginOptions = users.owner) { await page.getByLabel("Email").fill(options.email); await page.getByLabel("Password").fill(options.password); await page.getByRole("button", { name: "Sign In" }).click(); - await page.waitForLoadState("domcontentloaded"); + await page.waitForURL(/\/workspaces/); // biome-ignore lint/suspicious/noExplicitAny: update once logged in (ctx as any)[Symbol.for("currentUser")] = options; } From e251ab62d35d7ec7ffca8693050fd853ef6b21f9 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Thu, 9 Jul 2026 13:18:42 +0000 Subject: [PATCH 3/9] fix(site/e2e): remove redundant login redirect test The login() helper already waits for /workspaces via waitForURL, so a separate test asserting the same thing adds no coverage. --- site/e2e/tests/login.spec.ts | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 site/e2e/tests/login.spec.ts diff --git a/site/e2e/tests/login.spec.ts b/site/e2e/tests/login.spec.ts deleted file mode 100644 index 1b237e49a5e..00000000000 --- a/site/e2e/tests/login.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { test } from "@playwright/test"; -import { expectUrl } from "../expectUrl"; -import { login } from "../helpers"; -import { beforeCoderTest } from "../hooks"; - -test.beforeEach(async ({ page }) => { - beforeCoderTest(page); -}); - -test("login redirects to /workspaces", async ({ page }) => { - await login(page); - await expectUrl(page).toHavePathName("/workspaces"); -}); From dde31a8becae0e38e580c05bd78076d1a80dde7a Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Thu, 9 Jul 2026 13:50:00 +0000 Subject: [PATCH 4/9] fix(site/e2e): wait for page title after login to prevent navigation race waitForURL resolves once the URL matches and the document's load event fires. But login triggers a hard nav to "/" (the default redirect), and React Router then does a client-side redirect to "/workspaces". The load event fires for the "/" page load, not the client-side redirect, so waitForURL resolves before the /workspaces page components have mounted. Wait for the page title to match /Workspaces/ after waitForURL. The title is set by the WorkspacesPage component, so it proves React booted, auth resolved, and the page rendered. This closes the window where a subsequent page.goto could be interrupted by pending React rendering. --- site/e2e/helpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 74e8aebd4c6..101c9289587 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -81,6 +81,7 @@ export async function login(page: Page, options: LoginOptions = users.owner) { await page.getByLabel("Password").fill(options.password); await page.getByRole("button", { name: "Sign In" }).click(); await page.waitForURL(/\/workspaces/); + await expect(page).toHaveTitle(/Workspaces/); // biome-ignore lint/suspicious/noExplicitAny: update once logged in (ctx as any)[Symbol.for("currentUser")] = options; } From da9a62cbf8b5f94e1695a9ad8d5e058143631556 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Mon, 13 Jul 2026 12:31:18 +0000 Subject: [PATCH 5/9] docs(site/e2e): explain login wait sequence to prevent regression The toHaveTitle check looks redundant next to waitForURL but is the actual synchronization point, since the client-side redirect from / to /workspaces fires no load event. Document this so the check is not removed as an apparent duplicate. --- site/e2e/helpers.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 101c9289587..dc204e45728 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -80,6 +80,13 @@ export async function login(page: Page, options: LoginOptions = users.owner) { await page.getByLabel("Email").fill(options.email); await page.getByLabel("Password").fill(options.password); await page.getByRole("button", { name: "Sign In" }).click(); + // Sign-in triggers a hard navigation to "/", then React Router + // client-side redirects to "/workspaces" without firing a load event. + // waitForURL alone resolves on the URL change, before WorkspacesPage + // has mounted. The title check is the actual synchronization point: + // it retries until the page component renders. Removing either wait + // reintroduces a navigation race in tests that goto() right after + // login. See https://github.com/coder/internal/issues/1605. await page.waitForURL(/\/workspaces/); await expect(page).toHaveTitle(/Workspaces/); // biome-ignore lint/suspicious/noExplicitAny: update once logged in From 9c2238caef2934dc1a6600842d2f3ea38ea93e17 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Mon, 13 Jul 2026 12:32:05 +0000 Subject: [PATCH 6/9] docs(site/e2e): reference public PR instead of internal issue --- site/e2e/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index dc204e45728..64c463d053b 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -86,7 +86,7 @@ export async function login(page: Page, options: LoginOptions = users.owner) { // has mounted. The title check is the actual synchronization point: // it retries until the page component renders. Removing either wait // reintroduces a navigation race in tests that goto() right after - // login. See https://github.com/coder/internal/issues/1605. + // login. See https://github.com/coder/coder/pull/27107. await page.waitForURL(/\/workspaces/); await expect(page).toHaveTitle(/Workspaces/); // biome-ignore lint/suspicious/noExplicitAny: update once logged in From 3023b7d99452be24d59096fcf396b8dde9c653b2 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Mon, 13 Jul 2026 12:37:43 +0000 Subject: [PATCH 7/9] fix(site/e2e): use exact URL match in login waitForURL The regex /\/workspaces/ is an unanchored partial match, so any URL containing /workspaces anywhere in it would satisfy the wait. A plain string without wildcards resolves against baseURL and requires exact equality, which matches the post-login redirect precisely. --- site/e2e/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 64c463d053b..967f9810891 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -87,7 +87,7 @@ export async function login(page: Page, options: LoginOptions = users.owner) { // it retries until the page component renders. Removing either wait // reintroduces a navigation race in tests that goto() right after // login. See https://github.com/coder/coder/pull/27107. - await page.waitForURL(/\/workspaces/); + await page.waitForURL("/workspaces"); await expect(page).toHaveTitle(/Workspaces/); // biome-ignore lint/suspicious/noExplicitAny: update once logged in (ctx as any)[Symbol.for("currentUser")] = options; From 106557fa6af193ecf05f080682c476ea09e87729 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Mon, 13 Jul 2026 12:48:15 +0000 Subject: [PATCH 8/9] fix(site/e2e): match post-login URL by pathname predicate The exact-string form of waitForURL compares the full URL including any query string, which is stricter than intended. Use the predicate form to compare only the pathname, which precisely matches the post-login redirect regardless of query parameters. --- site/e2e/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 967f9810891..4e9c9099f44 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -87,7 +87,7 @@ export async function login(page: Page, options: LoginOptions = users.owner) { // it retries until the page component renders. Removing either wait // reintroduces a navigation race in tests that goto() right after // login. See https://github.com/coder/coder/pull/27107. - await page.waitForURL("/workspaces"); + await page.waitForURL((url) => url.pathname === "/workspaces"); await expect(page).toHaveTitle(/Workspaces/); // biome-ignore lint/suspicious/noExplicitAny: update once logged in (ctx as any)[Symbol.for("currentUser")] = options; From 6da41ba4c9fda29b461499a5ea00444a8f1e3275 Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Mon, 13 Jul 2026 13:10:38 +0000 Subject: [PATCH 9/9] fix(site/e2e): assert exact page title after login Matches the file's exact-string toHaveTitle convention and catches title drift that a substring regex would miss. --- site/e2e/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 4e9c9099f44..637951887f1 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -88,7 +88,7 @@ export async function login(page: Page, options: LoginOptions = users.owner) { // reintroduces a navigation race in tests that goto() right after // login. See https://github.com/coder/coder/pull/27107. await page.waitForURL((url) => url.pathname === "/workspaces"); - await expect(page).toHaveTitle(/Workspaces/); + await expect(page).toHaveTitle("Workspaces - Coder"); // biome-ignore lint/suspicious/noExplicitAny: update once logged in (ctx as any)[Symbol.for("currentUser")] = options; }