Skip to content

Commit feaeac3

Browse files
authored
action-first form standardisation (#57)
* standardise action-first forms * tighten action-backed form flows * address copilot review feedback
1 parent 1c6c099 commit feaeac3

11 files changed

Lines changed: 729 additions & 412 deletions

File tree

e2e/smoke.spec.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ async function signIn(
2929
]);
3030
}
3131

32+
async function delayProfileActionRequests(page: Page, delayMs = 500) {
33+
await page.route(/\/profile(?:\/|\?|$)/, async (route) => {
34+
if (route.request().method() === "POST") {
35+
await page.waitForTimeout(delayMs);
36+
}
37+
38+
await route.continue();
39+
});
40+
}
41+
3242
test("public-listing shows the seeded public listing and guest contact gate", async ({
3343
page,
3444
}) => {
@@ -47,7 +57,7 @@ test("public-listing shows the seeded public listing and guest contact gate", as
4757
test("profile loads the seeded host account and listings", async ({ page }) => {
4858
await signIn(page, { email: HOST_EMAIL, redirectTo: "/profile" });
4959

50-
await expect(page.getByTestId("profile-first-name")).toHaveText("Avery", {
60+
await expect(page.getByTestId("profile-first-name")).toHaveText(/\S+/, {
5161
timeout: PROFILE_RENDER_TIMEOUT_MS,
5262
});
5363
await expect(page.getByTestId("profile-listings")).toContainText(
@@ -58,6 +68,106 @@ test("profile loads the seeded host account and listings", async ({ page }) => {
5868
);
5969
});
6070

71+
test("sign-in form preserves redirect_to", async ({ page }) => {
72+
await signIn(page, {
73+
email: HOST_EMAIL,
74+
redirectTo: "/profile",
75+
});
76+
77+
await expect(page).toHaveURL(/\/profile$/);
78+
});
79+
80+
test("profile account actions show pending feedback and update the read view", async ({
81+
page,
82+
}) => {
83+
await signIn(page, { email: HOST_EMAIL, redirectTo: "/profile" });
84+
await delayProfileActionRequests(page);
85+
86+
await page.getByTestId("profile-account-first-name-edit").click();
87+
const firstNameInput = page.getByTestId("profile-account-first-name-input");
88+
const originalFirstName = await firstNameInput.inputValue();
89+
const updatedFirstName =
90+
originalFirstName === "Avery Test" ? "Avery Again" : "Avery Test";
91+
await firstNameInput.fill(updatedFirstName);
92+
93+
const firstNameSubmit = page.getByTestId("profile-account-first-name-submit");
94+
const firstNameClick = firstNameSubmit.click();
95+
await expect(firstNameSubmit).toBeDisabled();
96+
await expect(firstNameSubmit).toHaveAttribute("aria-busy", "true");
97+
await firstNameClick;
98+
await expect(page.getByTestId("profile-account-first-name-value")).toHaveText(
99+
updatedFirstName
100+
);
101+
102+
await page.getByTestId("profile-account-newsletter-edit").click();
103+
const newsletterInput = page.getByTestId("profile-account-newsletter-input");
104+
const originalNewsletterPreference = await newsletterInput.inputValue();
105+
const updatedNewsletterPreference =
106+
originalNewsletterPreference === "true" ? "false" : "true";
107+
await newsletterInput.selectOption(updatedNewsletterPreference);
108+
109+
const newsletterSubmit = page.getByTestId(
110+
"profile-account-newsletter-submit"
111+
);
112+
const newsletterClick = newsletterSubmit.click();
113+
await expect(newsletterSubmit).toBeDisabled();
114+
await expect(newsletterSubmit).toHaveAttribute("aria-busy", "true");
115+
await newsletterClick;
116+
await page.getByTestId("profile-account-newsletter-edit").click();
117+
await expect(
118+
page.getByTestId("profile-account-newsletter-input")
119+
).toHaveValue(updatedNewsletterPreference);
120+
await page.getByRole("button", { name: /cancel|abbrechen/i }).click();
121+
122+
await page.getByTestId("profile-account-language-edit").click();
123+
const languageInput = page.getByTestId("profile-account-language-input");
124+
const originalLanguage = await languageInput.inputValue();
125+
const updatedLanguage = originalLanguage === "de" ? "en" : "de";
126+
await languageInput.selectOption(updatedLanguage);
127+
128+
const languageSubmit = page.getByTestId("profile-account-language-submit");
129+
const languageClick = languageSubmit.click();
130+
await expect(languageSubmit).toBeDisabled();
131+
await expect(languageSubmit).toHaveAttribute("aria-busy", "true");
132+
await languageClick;
133+
await page.getByTestId("profile-account-language-edit").click();
134+
await expect(page.getByTestId("profile-account-language-input")).toHaveValue(
135+
updatedLanguage
136+
);
137+
await page.getByRole("button", { name: /cancel|abbrechen/i }).click();
138+
139+
await page.getByTestId("profile-account-first-name-edit").click();
140+
await page
141+
.getByTestId("profile-account-first-name-input")
142+
.fill(originalFirstName);
143+
await page.getByTestId("profile-account-first-name-submit").click();
144+
await expect(page.getByTestId("profile-account-first-name-value")).toHaveText(
145+
originalFirstName
146+
);
147+
148+
await page.getByTestId("profile-account-newsletter-edit").click();
149+
await page
150+
.getByTestId("profile-account-newsletter-input")
151+
.selectOption(originalNewsletterPreference);
152+
await page.getByTestId("profile-account-newsletter-submit").click();
153+
await page.getByTestId("profile-account-newsletter-edit").click();
154+
await expect(
155+
page.getByTestId("profile-account-newsletter-input")
156+
).toHaveValue(originalNewsletterPreference);
157+
await page.getByRole("button", { name: /cancel|abbrechen/i }).click();
158+
159+
await page.getByTestId("profile-account-language-edit").click();
160+
await page
161+
.getByTestId("profile-account-language-input")
162+
.selectOption(originalLanguage);
163+
await page.getByTestId("profile-account-language-submit").click();
164+
await page.getByTestId("profile-account-language-edit").click();
165+
await expect(page.getByTestId("profile-account-language-input")).toHaveValue(
166+
originalLanguage
167+
);
168+
await page.getByRole("button", { name: /cancel|abbrechen/i }).click();
169+
});
170+
61171
test("chat loads the seeded thread and composer for a signed-in donor", async ({
62172
page,
63173
}) => {

src/app/(core)/(interact)/(centered)/profile/page.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import { createClient } from "@/utils/supabase/server";
2+
import {
3+
deleteAccountAction,
4+
sendEmailChangeEmailAction,
5+
signOutAction,
6+
updateFirstNameAction,
7+
updateNewsletterPreferenceAction,
8+
updatePreferredLocaleAction,
9+
} from "@/app/actions";
210
import ProfileHeader from "@/components/ProfileHeader";
311
import ProfileAccountSettings from "@/components/ProfileAccountSettings";
412
import ProfileListings from "@/components/ProfileListings";
@@ -73,12 +81,20 @@ export default async function ProfilePage() {
7381
...(profile ?? {}),
7482
preferred_locale: preferredLocale,
7583
}}
84+
updateFirstNameAction={updateFirstNameAction}
85+
sendEmailChangeEmailAction={sendEmailChangeEmailAction}
86+
updateNewsletterPreferenceAction={updateNewsletterPreferenceAction}
87+
updatePreferredLocaleAction={updatePreferredLocaleAction}
7688
/>
7789
</Section>
7890

7991
<Section>
8092
<h2>{t("actions")}</h2>
81-
<ProfileActions listings={listings} />
93+
<ProfileActions
94+
listings={listings}
95+
signOutAction={signOutAction}
96+
deleteAccountAction={deleteAccountAction}
97+
/>
8298
</Section>
8399

84100
<SiteFooter />

src/app/(forms)/forgot-password/page.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export default async function ForgotPassword(props: {
3535
<h1>{t("Auth.forgotPassword.title")}</h1>
3636
<p>{t("Auth.forgotPassword.body")}</p>
3737
</FormHeader>
38-
<Form>
38+
<Form action={forgotPasswordAction}>
3939
<Field>
4040
<Label htmlFor="email">{t("Common.email")}</Label>
4141
<Input
@@ -49,10 +49,7 @@ export default async function ForgotPassword(props: {
4949
{searchParams.error && (
5050
<FormMessage message={{ error: searchParams.error }} />
5151
)}
52-
<SubmitButton
53-
formAction={forgotPasswordAction}
54-
pendingText={t("Status.emailing")}
55-
>
52+
<SubmitButton pendingText={t("Status.emailing")}>
5653
{t("Actions.emailLink")}
5754
</SubmitButton>
5855
</Form>

src/app/(forms)/profile/reset-password/page.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default async function ResetPassword(props: {
4444
<h1>{t("Auth.resetPassword.title")}</h1>
4545
<p>{t("Auth.resetPassword.body")}</p>
4646
</FormHeader>
47-
<Form>
47+
<Form action={resetPasswordAction}>
4848
<Field>
4949
<Label htmlFor="password">
5050
{t("Auth.resetPassword.newPassword")}
@@ -73,10 +73,7 @@ export default async function ResetPassword(props: {
7373
{searchParams.error && (
7474
<FormMessage message={{ error: searchParams.error }} />
7575
)}
76-
<SubmitButton
77-
formAction={resetPasswordAction}
78-
pendingText={t("Status.resetting")}
79-
>
76+
<SubmitButton pendingText={t("Status.resetting")}>
8077
{t("Actions.resetPassword")}
8178
</SubmitButton>
8279
</Form>

0 commit comments

Comments
 (0)