Merge pull request #365 from trails-cool/e2e-hydration-and-workers
Stabilize e2e against Vite dev hydration race
This commit is contained in:
commit
ede480e652
6 changed files with 38 additions and 5 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, expect, type CDPSession, type Page } from "./fixtures/test";
|
import { test, expect, waitForHydration, type CDPSession, type Page } from "./fixtures/test";
|
||||||
|
|
||||||
// Virtual authenticator helpers
|
// Virtual authenticator helpers
|
||||||
async function setupVirtualAuthenticator(cdp: CDPSession) {
|
async function setupVirtualAuthenticator(cdp: CDPSession) {
|
||||||
|
|
@ -23,6 +23,7 @@ async function removeVirtualAuthenticator(cdp: CDPSession, authenticatorId: stri
|
||||||
async function registerUser(page: Page, email: string, username: string) {
|
async function registerUser(page: Page, email: string, username: string) {
|
||||||
await page.goto("/auth/register");
|
await page.goto("/auth/register");
|
||||||
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
||||||
|
await waitForHydration(page);
|
||||||
await page.getByLabel("Email").click();
|
await page.getByLabel("Email").click();
|
||||||
await page.getByLabel("Email").fill(email);
|
await page.getByLabel("Email").fill(email);
|
||||||
await page.getByLabel("Username").click();
|
await page.getByLabel("Username").click();
|
||||||
|
|
@ -39,6 +40,7 @@ async function logout(page: Page, accountLabel: string) {
|
||||||
// The account cluster lives inside an avatar dropdown now. Click the
|
// The account cluster lives inside an avatar dropdown now. Click the
|
||||||
// avatar (its aria-label is displayName || username), then click the
|
// avatar (its aria-label is displayName || username), then click the
|
||||||
// Log Out menuitem inside the popup.
|
// Log Out menuitem inside the popup.
|
||||||
|
await waitForHydration(page);
|
||||||
await page.getByRole("navigation").getByRole("button", { name: accountLabel }).click();
|
await page.getByRole("navigation").getByRole("button", { name: accountLabel }).click();
|
||||||
await page.getByRole("menuitem", { name: "Log Out" }).click();
|
await page.getByRole("menuitem", { name: "Log Out" }).click();
|
||||||
await expect(page.getByRole("navigation").getByRole("link", { name: "Sign In" })).toBeVisible({ timeout: 5000 });
|
await expect(page.getByRole("navigation").getByRole("link", { name: "Sign In" })).toBeVisible({ timeout: 5000 });
|
||||||
|
|
@ -65,6 +67,7 @@ test.describe("Passkey Authentication", () => {
|
||||||
|
|
||||||
// Sign in with passkey
|
// Sign in with passkey
|
||||||
await page.goto("/auth/login");
|
await page.goto("/auth/login");
|
||||||
|
await waitForHydration(page);
|
||||||
await page.getByRole("button", { name: /Sign in with Passkey/ }).click();
|
await page.getByRole("button", { name: /Sign in with Passkey/ }).click();
|
||||||
await expect(page).toHaveURL("/", { timeout: 10000 });
|
await expect(page).toHaveURL("/", { timeout: 10000 });
|
||||||
await expect(page.getByRole("navigation").getByRole("button", { name: username })).toBeVisible({ timeout: 5000 });
|
await expect(page.getByRole("navigation").getByRole("button", { name: username })).toBeVisible({ timeout: 5000 });
|
||||||
|
|
@ -77,6 +80,7 @@ test.describe("Passkey Authentication", () => {
|
||||||
const authenticatorId = await setupVirtualAuthenticator(cdp);
|
const authenticatorId = await setupVirtualAuthenticator(cdp);
|
||||||
|
|
||||||
await page.goto("/auth/login");
|
await page.goto("/auth/login");
|
||||||
|
await waitForHydration(page);
|
||||||
await page.getByRole("button", { name: /Sign in with Passkey/ }).click();
|
await page.getByRole("button", { name: /Sign in with Passkey/ }).click();
|
||||||
await expect(page.getByText(/No passkey found/i)).toBeVisible({ timeout: 10000 });
|
await expect(page.getByText(/No passkey found/i)).toBeVisible({ timeout: 10000 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, expect, type CDPSession, type Page } from "./fixtures/test";
|
import { test, expect, waitForHydration, type CDPSession, type Page } from "./fixtures/test";
|
||||||
|
|
||||||
async function setupVirtualAuthenticator(cdp: CDPSession) {
|
async function setupVirtualAuthenticator(cdp: CDPSession) {
|
||||||
await cdp.send("WebAuthn.enable");
|
await cdp.send("WebAuthn.enable");
|
||||||
|
|
@ -17,6 +17,7 @@ async function setupVirtualAuthenticator(cdp: CDPSession) {
|
||||||
async function registerUser(page: Page, email: string, username: string) {
|
async function registerUser(page: Page, email: string, username: string) {
|
||||||
await page.goto("/auth/register");
|
await page.goto("/auth/register");
|
||||||
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
||||||
|
await waitForHydration(page);
|
||||||
await page.getByLabel("Email").fill(email);
|
await page.getByLabel("Email").fill(email);
|
||||||
await page.getByLabel("Username").fill(username);
|
await page.getByLabel("Username").fill(username);
|
||||||
await page.getByRole("checkbox").check();
|
await page.getByRole("checkbox").check();
|
||||||
|
|
|
||||||
|
|
@ -74,5 +74,29 @@ export const test = base.extend({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait until React has hydrated the current page. Vite dev injects scripts
|
||||||
|
* asynchronously, so a button/form is visible (and clickable per Playwright's
|
||||||
|
* actionability check) before its React `onClick`/`onSubmit` is wired up.
|
||||||
|
* Without this, the first click after navigation is a coin flip on a cold
|
||||||
|
* dev server — controlled inputs ignore the keystrokes and submit handlers
|
||||||
|
* never run, so the form posts empty fields or natively GETs to itself.
|
||||||
|
*
|
||||||
|
* Detects hydration by checking for React's `__reactProps$<id>` property
|
||||||
|
* which is attached during commit. CI uses production builds where this
|
||||||
|
* isn't a problem, but the check is cheap and harmless there.
|
||||||
|
*/
|
||||||
|
export async function waitForHydration(page: import("@playwright/test").Page) {
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const el = document.body.querySelector("button, input, form, a");
|
||||||
|
if (!el) return false;
|
||||||
|
return Object.keys(el).some((k) => k.startsWith("__reactProps$"));
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ timeout: 10000 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export { expect };
|
export { expect };
|
||||||
export type { CDPSession, Page } from "@playwright/test";
|
export type { CDPSession, Page } from "@playwright/test";
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, expect, type CDPSession, type Page } from "./fixtures/test";
|
import { test, expect, waitForHydration, type CDPSession, type Page } from "./fixtures/test";
|
||||||
|
|
||||||
async function setupVirtualAuthenticator(cdp: CDPSession) {
|
async function setupVirtualAuthenticator(cdp: CDPSession) {
|
||||||
await cdp.send("WebAuthn.enable");
|
await cdp.send("WebAuthn.enable");
|
||||||
|
|
@ -17,6 +17,7 @@ async function setupVirtualAuthenticator(cdp: CDPSession) {
|
||||||
async function registerUser(page: Page, email: string, username: string) {
|
async function registerUser(page: Page, email: string, username: string) {
|
||||||
await page.goto("/auth/register");
|
await page.goto("/auth/register");
|
||||||
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
||||||
|
await waitForHydration(page);
|
||||||
await page.getByLabel("Email").fill(email);
|
await page.getByLabel("Email").fill(email);
|
||||||
await page.getByLabel("Username").fill(username);
|
await page.getByLabel("Username").fill(username);
|
||||||
await page.getByRole("checkbox").check();
|
await page.getByRole("checkbox").check();
|
||||||
|
|
@ -63,6 +64,7 @@ test.describe("Notifications", () => {
|
||||||
|
|
||||||
// A follows B (auto-accept).
|
// A follows B (auto-accept).
|
||||||
await page.goto(`/users/${bUsername}`);
|
await page.goto(`/users/${bUsername}`);
|
||||||
|
await waitForHydration(page);
|
||||||
await page.getByRole("button", { name: "Follow" }).click();
|
await page.getByRole("button", { name: "Follow" }).click();
|
||||||
await expect(page.getByRole("button", { name: "Unfollow" })).toBeVisible({ timeout: 5000 });
|
await expect(page.getByRole("button", { name: "Unfollow" })).toBeVisible({ timeout: 5000 });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, expect, type CDPSession, type Page } from "./fixtures/test";
|
import { test, expect, waitForHydration, type CDPSession, type Page } from "./fixtures/test";
|
||||||
|
|
||||||
// Reuses the virtual-authenticator + register helpers from auth.test.ts.
|
// Reuses the virtual-authenticator + register helpers from auth.test.ts.
|
||||||
// Inlined rather than factored out to keep this file independently runnable.
|
// Inlined rather than factored out to keep this file independently runnable.
|
||||||
|
|
@ -19,6 +19,7 @@ async function setupVirtualAuthenticator(cdp: CDPSession) {
|
||||||
async function registerUser(page: Page, email: string, username: string) {
|
async function registerUser(page: Page, email: string, username: string) {
|
||||||
await page.goto("/auth/register");
|
await page.goto("/auth/register");
|
||||||
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
||||||
|
await waitForHydration(page);
|
||||||
await page.getByLabel("Email").fill(email);
|
await page.getByLabel("Email").fill(email);
|
||||||
await page.getByLabel("Username").fill(username);
|
await page.getByLabel("Username").fill(username);
|
||||||
await page.getByRole("checkbox").check();
|
await page.getByRole("checkbox").check();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { test, expect, type CDPSession, type Page } from "./fixtures/test";
|
import { test, expect, waitForHydration, type CDPSession, type Page } from "./fixtures/test";
|
||||||
|
|
||||||
// Inline virtual-authenticator + register helpers, mirroring the pattern
|
// Inline virtual-authenticator + register helpers, mirroring the pattern
|
||||||
// in auth.test.ts / public-content.test.ts so this file runs standalone.
|
// in auth.test.ts / public-content.test.ts so this file runs standalone.
|
||||||
|
|
@ -19,6 +19,7 @@ async function setupVirtualAuthenticator(cdp: CDPSession) {
|
||||||
async function registerUser(page: Page, email: string, username: string) {
|
async function registerUser(page: Page, email: string, username: string) {
|
||||||
await page.goto("/auth/register");
|
await page.goto("/auth/register");
|
||||||
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
await expect(page.getByRole("heading", { name: "Register" })).toBeVisible();
|
||||||
|
await waitForHydration(page);
|
||||||
await page.getByLabel("Email").fill(email);
|
await page.getByLabel("Email").fill(email);
|
||||||
await page.getByLabel("Username").fill(username);
|
await page.getByLabel("Username").fill(username);
|
||||||
await page.getByRole("checkbox").check();
|
await page.getByRole("checkbox").check();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue