e2e: register and repair the settings/explore/social specs
settings.test.ts, explore.test.ts, and social.test.ts weren't matched
by any Playwright project, so they had silently never run — which is
how they rotted. Register them (one project each) and repair the
selectors against the current UI:
- settings: the page was split into sibling sections
(/settings/{profile,security,account}); the spec assumed one page.
Navigate to the right sub-page per test, use the stable section-nav
links + #id input locators (the Vite dev server transiently
double-renders the profile form during hydration, breaking
getByLabel), and wait for hydration before interacting with
fetcher-backed forms and the avatar dropdown.
- explore: setProfileVisibility now targets /settings/profile and
waits for hydration so the visibility save uses the fetcher.
- social: already green once registered.
Also fixes an app bug the specs surfaced: deleting a passkey redirected
to /settings#security, a stale anchor that now resolves to
/settings/profile — so you'd land on the wrong section. It now
redirects to /settings/security.
Verified locally against Postgres/BRouter: green under CI-style
retries (the residual registration flake is the same cold-start class
the rest of the suite has, which is why CI runs retries=2).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
b4067301cc
commit
7a290cd56f
4 changed files with 73 additions and 27 deletions
|
|
@ -23,5 +23,8 @@ export async function action({ request }: Route.ActionArgs) {
|
|||
return data({ error: "Passkey not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
return redirect("/settings#security");
|
||||
// Settings is split into sections now; the bare /settings#security
|
||||
// anchor resolves to /settings/profile. Send the user back to the
|
||||
// security section they were on.
|
||||
return redirect("/settings/security");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import { test, expect, waitForHydration, type CDPSession, type Page } from "./fi
|
|||
import { setupVirtualAuthenticator, registerUser } from "./helpers/auth";
|
||||
|
||||
async function setProfileVisibility(page: Page, value: "public" | "private") {
|
||||
await page.goto("/settings");
|
||||
await page.goto("/settings/profile");
|
||||
// Wait for hydration so the visibility form submits via the fetcher
|
||||
// (and shows the "Profile saved." toast) instead of native-navigating.
|
||||
await waitForHydration(page);
|
||||
await page.locator(`input[type=radio][name=profileVisibility][value=${value}]`).check();
|
||||
await page.getByRole("button", { name: /^Save$/ }).first().click();
|
||||
await expect(page.getByText("Profile saved.")).toBeVisible({ timeout: 10000 });
|
||||
|
|
|
|||
|
|
@ -1,6 +1,14 @@
|
|||
import { test, expect, type CDPSession, type Page } from "./fixtures/test";
|
||||
import { test, expect, waitForHydration, type Page } from "./fixtures/test";
|
||||
import { setupVirtualAuthenticator, removeVirtualAuthenticator, registerFreshUser } from "./helpers/auth";
|
||||
|
||||
// Settings is split into sibling sections, each with a stable URL:
|
||||
// /settings → redirects to /settings/profile
|
||||
// /settings/profile → display name, bio, visibility
|
||||
// /settings/security → passkeys
|
||||
// /settings/account → email change, account deletion
|
||||
// The left-hand section nav lists Profile / Account / Security /
|
||||
// Connected services as links.
|
||||
|
||||
test.describe("Account Settings", () => {
|
||||
test("unauthenticated user is redirected to login", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
|
|
@ -14,9 +22,11 @@ test.describe("Account Settings", () => {
|
|||
|
||||
await page.goto("/settings");
|
||||
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
||||
await expect(page.getByText("Profile")).toBeVisible();
|
||||
await expect(page.getByText("Security")).toBeVisible();
|
||||
await expect(page.getByText("Account")).toBeVisible();
|
||||
// Section nav links (the avatar dropdown is closed, so these names
|
||||
// are unique to the settings section nav).
|
||||
await expect(page.getByRole("link", { name: "Profile" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Security" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Account" })).toBeVisible();
|
||||
|
||||
await removeVirtualAuthenticator(cdp, authenticatorId);
|
||||
});
|
||||
|
|
@ -26,17 +36,20 @@ test.describe("Account Settings", () => {
|
|||
const authenticatorId = await setupVirtualAuthenticator(cdp);
|
||||
const { username } = await registerFreshUser(page, "settings");
|
||||
|
||||
await page.goto("/settings");
|
||||
await page.goto("/settings/profile");
|
||||
await waitForHydration(page);
|
||||
|
||||
await page.getByLabel("Display Name").fill("Test Display Name");
|
||||
await page.getByLabel("Bio").fill("I love hiking!");
|
||||
// Target the inputs by id — robust against the Vite dev server
|
||||
// transiently double-rendering the form during hydration.
|
||||
await page.locator("#displayName").fill("Test Display Name");
|
||||
await page.locator("#bio").fill("I love hiking!");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByText("Profile saved")).toBeVisible({ timeout: 5000 });
|
||||
|
||||
// Reload and verify persistence
|
||||
await page.reload();
|
||||
await expect(page.getByLabel("Display Name")).toHaveValue("Test Display Name");
|
||||
await expect(page.getByLabel("Bio")).toHaveValue("I love hiking!");
|
||||
await expect(page.locator("#displayName")).toHaveValue("Test Display Name");
|
||||
await expect(page.locator("#bio")).toHaveValue("I love hiking!");
|
||||
|
||||
// Verify public profile reflects changes
|
||||
await page.goto(`/users/${username}`);
|
||||
|
|
@ -50,8 +63,8 @@ test.describe("Account Settings", () => {
|
|||
const authenticatorId = await setupVirtualAuthenticator(cdp);
|
||||
await registerFreshUser(page, "settings");
|
||||
|
||||
await page.goto("/settings");
|
||||
// Should show the passkey registered during registration
|
||||
await page.goto("/settings/security");
|
||||
// The passkey registered during sign-up shows as "This device".
|
||||
await expect(page.getByText("This device")).toBeVisible();
|
||||
await expect(page.getByText(/Added/)).toBeVisible();
|
||||
|
||||
|
|
@ -63,7 +76,8 @@ test.describe("Account Settings", () => {
|
|||
const authenticatorId = await setupVirtualAuthenticator(cdp);
|
||||
await registerFreshUser(page, "settings");
|
||||
|
||||
await page.goto("/settings");
|
||||
await page.goto("/settings/security");
|
||||
await waitForHydration(page);
|
||||
|
||||
// Should have 1 passkey from registration
|
||||
const deleteButtons = page.getByRole("button", { name: "Delete" });
|
||||
|
|
@ -71,15 +85,14 @@ test.describe("Account Settings", () => {
|
|||
|
||||
// Add another passkey
|
||||
await page.getByRole("button", { name: "Add Passkey" }).click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// After reload, should have 2 passkeys
|
||||
// After the ceremony + reload, should have 2 passkeys
|
||||
await expect(deleteButtons).toHaveCount(2, { timeout: 5000 });
|
||||
|
||||
// Delete one
|
||||
page.on("dialog", (dialog) => dialog.accept());
|
||||
await deleteButtons.first().click();
|
||||
await expect(page).toHaveURL(/\/settings/, { timeout: 5000 });
|
||||
await expect(page).toHaveURL(/\/settings\/security/, { timeout: 5000 });
|
||||
await expect(page.getByRole("button", { name: "Delete" })).toHaveCount(1);
|
||||
|
||||
await removeVirtualAuthenticator(cdp, authenticatorId);
|
||||
|
|
@ -90,7 +103,8 @@ test.describe("Account Settings", () => {
|
|||
const authenticatorId = await setupVirtualAuthenticator(cdp);
|
||||
await registerFreshUser(page, "settings");
|
||||
|
||||
await page.goto("/settings");
|
||||
await page.goto("/settings/security");
|
||||
await waitForHydration(page);
|
||||
|
||||
// Intercept the confirm dialog to check it contains the warning
|
||||
let dialogMessage = "";
|
||||
|
|
@ -113,8 +127,9 @@ test.describe("Account Settings", () => {
|
|||
const authenticatorId = await setupVirtualAuthenticator(cdp);
|
||||
const { username } = await registerFreshUser(page, "settings");
|
||||
|
||||
// Settings now lives inside the avatar dropdown; opening the
|
||||
// dropdown reveals the menuitem.
|
||||
// Settings lives inside the avatar dropdown; wait for hydration so
|
||||
// the dropdown's open handler is wired before clicking it.
|
||||
await waitForHydration(page);
|
||||
await page.getByRole("navigation").getByRole("button", { name: username }).click();
|
||||
await expect(page.getByRole("menuitem", { name: "Settings" })).toBeVisible();
|
||||
|
||||
|
|
@ -123,7 +138,7 @@ test.describe("Account Settings", () => {
|
|||
|
||||
test("settings link not visible when logged out", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
// Anonymous navbar has neither the avatar nor the Settings link.
|
||||
// Anonymous navbar has neither the avatar nor a Settings link.
|
||||
await expect(page.getByRole("navigation").getByRole("link", { name: "Settings" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
|
|
@ -132,16 +147,17 @@ test.describe("Account Settings", () => {
|
|||
const authenticatorId = await setupVirtualAuthenticator(cdp);
|
||||
const { username } = await registerFreshUser(page, "settings");
|
||||
|
||||
await page.goto("/settings");
|
||||
await page.goto("/settings/account");
|
||||
await waitForHydration(page);
|
||||
|
||||
// Click delete account
|
||||
// Open the delete-account confirmation.
|
||||
await page.getByRole("button", { name: "Delete Account" }).click();
|
||||
|
||||
// Type wrong username — button should be disabled
|
||||
// Wrong username — confirm button disabled
|
||||
await page.getByPlaceholder(username).fill("wrongname");
|
||||
await expect(page.getByRole("button", { name: "Permanently Delete" })).toBeDisabled();
|
||||
|
||||
// Type correct username — button should be enabled
|
||||
// Correct username — confirm button enabled
|
||||
await page.getByPlaceholder(username).fill(username);
|
||||
await expect(page.getByRole("button", { name: "Permanently Delete" })).toBeEnabled();
|
||||
|
||||
|
|
@ -149,7 +165,7 @@ test.describe("Account Settings", () => {
|
|||
await page.getByRole("button", { name: "Permanently Delete" }).click();
|
||||
await expect(page).toHaveURL("/", { timeout: 10000 });
|
||||
|
||||
// Session should be destroyed — nav should show login
|
||||
// Session destroyed — nav shows Sign In again
|
||||
await expect(page.getByRole("navigation").getByRole("link", { name: "Sign In" })).toBeVisible({ timeout: 5000 });
|
||||
|
||||
await removeVirtualAuthenticator(cdp, authenticatorId);
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default defineConfig({
|
|||
},
|
||||
// NOTE: specs only run if a project below matches them — a new
|
||||
// e2e/*.test.ts file MUST be registered here or it silently never
|
||||
// executes (settings/explore/social sat unregistered for months).
|
||||
// executes.
|
||||
projects: [
|
||||
{
|
||||
name: "journal",
|
||||
|
|
@ -90,6 +90,30 @@ export default defineConfig({
|
|||
baseURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "settings",
|
||||
testMatch: "settings.test.ts",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
baseURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "explore",
|
||||
testMatch: "explore.test.ts",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
baseURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "social",
|
||||
testMatch: "social.test.ts",
|
||||
use: {
|
||||
...devices["Desktop Chrome"],
|
||||
baseURL: "http://localhost:3000",
|
||||
},
|
||||
},
|
||||
],
|
||||
webServer: process.env.CI
|
||||
? [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue