Skip to content

Commit d81c14d

Browse files
voidmatchaclaude
andcommitted
test: repair never-passable login tests and tighten the timer flush
The required password input blocks empty or cleared submissions client-side, so the missing-password error and the rate limiter were unreachable from the UI (the CI failure videos show the browser's native validation bubble at the moment of the assertion): - assert the native validation guard (input.password:invalid) for the missing-password case and rename the test to match the actual behavior - refill the password before every attempt in the rate limiter test so all 15 attempts actually POST, and mark it test.slow() since each attempt now costs a real argon2 hash - replace the 3x Promise.resolve() microtask drain in heart.test.ts with a single real-macrotask yield via jest.requireActual("timers") .setImmediate, which drains the queue deterministically Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent ec595ea commit d81c14d

2 files changed

Lines changed: 16 additions & 10 deletions

File tree

‎test/e2e/login.test.ts‎

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ describe("login", ["--disable-workspace-trust", "--auth", "password"], {}, () =>
2020
expect(await codeServerPage.isEditorVisible()).toBe(true)
2121
})
2222

23-
test("should see an error message for missing password", async ({ codeServerPage }) => {
23+
test("should block submission when password is missing", async ({ codeServerPage }) => {
2424
// Skip entering password
2525
// Click the submit button and login
2626
await codeServerPage.page.click(".submit")
27-
await codeServerPage.page.waitForLoadState("networkidle")
28-
await expect(codeServerPage.page.locator("text=Missing password")).toBeVisible()
27+
// The password input has the `required` attribute, so the browser blocks
28+
// the submission client-side and the server-side "Missing password" error
29+
// is unreachable from the UI - assert the native validation guard instead.
30+
await expect(codeServerPage.page.locator("input.password:invalid")).toBeVisible()
2931
})
3032

3133
test("should see an error message for incorrect password", async ({ codeServerPage }) => {
@@ -38,13 +40,17 @@ describe("login", ["--disable-workspace-trust", "--auth", "password"], {}, () =>
3840
})
3941

4042
test("should hit the rate limiter for too many unsuccessful logins", async ({ codeServerPage }) => {
41-
// Type in password
42-
await codeServerPage.page.fill(".password", "password123")
43+
// 15 sequential POSTs each trigger a server-side argon2 hash - triple the
44+
// timeout so the test does not flake on slow CI runners.
45+
test.slow()
4346
// Click the submit button and login
4447
// The current RateLimiter allows 2 logins per minute plus
4548
// 12 logins per hour for a total of 14
4649
// See: src/node/routes/login.ts
4750
for (let i = 1; i <= 14; i++) {
51+
// The page re-renders after each failed attempt, clearing the (required)
52+
// password field - refill it so every click actually submits the form.
53+
await codeServerPage.page.fill(".password", "password123")
4854
await codeServerPage.page.click(".submit")
4955
await codeServerPage.page.waitForLoadState("networkidle")
5056
// We double-check that the correct error message shows
@@ -54,6 +60,7 @@ describe("login", ["--disable-workspace-trust", "--auth", "password"], {}, () =>
5460

5561
// The 15th should fail for a different reason:
5662
// login rate
63+
await codeServerPage.page.fill(".password", "password123")
5764
await codeServerPage.page.click(".submit")
5865
await codeServerPage.page.waitForLoadState("networkidle")
5966
await expect(codeServerPage.page.locator("text=Login rate limited!")).toBeVisible()

‎test/unit/node/heart.test.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,10 @@ describe("heartbeatTimer", () => {
116116
await heart.beat()
117117
jest.advanceTimersByTime(60 * 1000)
118118
// advanceTimersByTime fires the timer callback synchronously, but the
119-
// callback awaits isActive() — drain the microtask queue so its rejection
120-
// handler (which logs the warning) actually runs before we assert.
121-
await Promise.resolve()
122-
await Promise.resolve()
123-
await Promise.resolve()
119+
// callback awaits isActive(), so its rejection handler (which logs the
120+
// warning) runs on the microtask queue. Yield one real macrotask turn
121+
// (fake timers stub the global setImmediate) to drain it fully first.
122+
await new Promise((resolve) => jest.requireActual("timers").setImmediate(resolve))
124123

125124
expect(mockIsActive).toHaveBeenCalled()
126125
expect(logger.warn).toHaveBeenCalledWith(errorMsg)

0 commit comments

Comments
 (0)