Skip to content

Commit c147467

Browse files
committed
add playwright smoke tests
1 parent 00d4dd1 commit c147467

14 files changed

Lines changed: 277 additions & 11 deletions

File tree

.github/workflows/validate-app.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,54 @@ jobs:
3030

3131
- name: Build app
3232
run: npm run build
33+
34+
e2e:
35+
runs-on: ubuntu-latest
36+
permissions:
37+
contents: read
38+
39+
steps:
40+
- name: Check out repository
41+
uses: actions/checkout@v4
42+
43+
- name: Set up Node.js
44+
uses: actions/setup-node@v4
45+
with:
46+
node-version: 22
47+
cache: npm
48+
49+
- name: Install dependencies
50+
run: npm ci
51+
52+
- name: Set up Supabase CLI
53+
uses: supabase/setup-cli@v1
54+
with:
55+
version: latest
56+
57+
- name: Start local Supabase stack
58+
run: supabase start
59+
60+
- name: Replay migrations and seed data
61+
run: npm run supabase:reset
62+
63+
- name: Write local app env
64+
shell: bash
65+
run: |
66+
eval "$(supabase status -o env | sed 's/^/export /')"
67+
cat <<EOF > .env.local
68+
NEXT_PUBLIC_SITE_URL=http://127.0.0.1:3000
69+
NEXT_PUBLIC_SUPABASE_URL=${API_URL}
70+
NEXT_PUBLIC_SUPABASE_ANON_KEY=${ANON_KEY}
71+
NEXT_PUBLIC_MAPTILER_API_KEY=playwright-local
72+
NEXT_PUBLIC_PROTOMAPS_API_KEY=playwright-local
73+
NEXT_PUBLIC_TURNSTILE_ENABLED=
74+
EOF
75+
76+
- name: Seed local demo media
77+
run: npm run seed:local-media
78+
79+
- name: Install Playwright browser
80+
run: npx playwright install --with-deps chromium
81+
82+
- name: Run Playwright smoke tests
83+
run: npm run test:e2e

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
# testing
1010
/coverage
11+
/playwright-report
12+
/test-results
1113

1214
# next.js
1315
/.next/

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,24 @@ npm run dev
216216
- Heavy commenting is encouraged to make the codebase accessible to others
217217
- Code formatting is handled by Prettier. Please ensure your code is formatted according to `.prettierrc` before submitting a pull request
218218

219+
### Testing
220+
221+
Peels keeps testing intentionally small:
222+
223+
- `npm run check` for i18n and formatting checks
224+
- `npm run build` for the production build
225+
- `npm run test:e2e` for the headless Playwright smoke suite
226+
227+
The smoke suite covers seeded sign-in, public listing, profile, and chat flows against local Supabase data. It is designed to stay small and high-signal rather than grow into a broad frontend testing matrix.
228+
229+
For the first local Playwright run, install the browser once with:
230+
231+
```bash
232+
npx playwright install chromium
233+
```
234+
235+
Map changes are still a manual smoke-check area. When you touch `/map`, verify that the page loads, listing selection still works, search still works, and nothing obviously breaks in the browser.
236+
219237
### Getting Help
220238

221239
- Check existing [issues](https://github.com/dnywh/peels/issues) for known problems

e2e/smoke.spec.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { expect, test, type Page } from "@playwright/test";
2+
3+
const HOST_EMAIL = "demo-host@peels.local";
4+
const DONOR_EMAIL = "demo-donor@peels.local";
5+
const SEEDED_PASSWORD = "peels-demo-password";
6+
const SEEDED_THREAD_ID = "33333333-3333-4333-8333-333333333333";
7+
8+
async function signIn(
9+
page: Page,
10+
{
11+
email,
12+
redirectTo = "/profile",
13+
}: {
14+
email: string;
15+
redirectTo?: string;
16+
}
17+
) {
18+
await page.goto(`/sign-in?redirect_to=${encodeURIComponent(redirectTo)}`);
19+
20+
await expect(page.getByTestId("sign-in-form")).toBeVisible();
21+
22+
await page.getByLabel("Email").fill(email);
23+
await page.getByLabel("Password").fill(SEEDED_PASSWORD);
24+
25+
await Promise.all([
26+
page.waitForURL((url) => url.pathname === redirectTo),
27+
page.getByTestId("sign-in-submit").click(),
28+
]);
29+
}
30+
31+
test("auth-sign-in redirects a seeded donor into the signed-in experience", async ({
32+
page,
33+
}) => {
34+
await signIn(page, { email: DONOR_EMAIL, redirectTo: "/profile" });
35+
36+
await expect(page).toHaveURL(/\/profile$/);
37+
await expect(page.getByTestId("profile-first-name")).toHaveText("Riley");
38+
});
39+
40+
test("public-listing shows the seeded public listing and guest contact gate", async ({
41+
page,
42+
}) => {
43+
await page.goto("/listings/demo-marrickville-compost");
44+
45+
await expect(
46+
page.getByRole("heading", { name: "Marrickville Neighbourhood Compost" })
47+
).toBeVisible();
48+
await expect(page.getByTestId("listing-guest-cta")).toBeVisible();
49+
await expect(page.getByTestId("listing-sign-in-to-contact")).toHaveAttribute(
50+
"href",
51+
"/sign-in?redirect_to=/listings/demo-marrickville-compost"
52+
);
53+
});
54+
55+
test("profile loads the seeded host account and listings", async ({ page }) => {
56+
await signIn(page, { email: HOST_EMAIL, redirectTo: "/profile" });
57+
58+
await expect(page.getByTestId("profile-first-name")).toHaveText("Avery");
59+
await expect(page.getByTestId("profile-listings")).toContainText(
60+
"Marrickville Neighbourhood Compost"
61+
);
62+
await expect(page.getByTestId("profile-listings")).toContainText(
63+
"Inner West Cafe Compost Pickup"
64+
);
65+
});
66+
67+
test("chat loads the seeded thread and composer for a signed-in donor", async ({
68+
page,
69+
}) => {
70+
await signIn(page, {
71+
email: DONOR_EMAIL,
72+
redirectTo: `/chats/${SEEDED_THREAD_ID}`,
73+
});
74+
75+
await expect(page).toHaveURL(new RegExp(`/chats/${SEEDED_THREAD_ID}$`));
76+
await expect(page.getByTestId("chat-window")).toBeVisible();
77+
await expect(page.getByTestId("chat-message-list")).toContainText(
78+
"Hey Avery, do you take coffee grounds from a small home espresso machine?"
79+
);
80+
await expect(page.getByTestId("chat-message-list")).toContainText(
81+
"Yes, absolutely. Small sealed containers are perfect."
82+
);
83+
await expect(page.getByTestId("chat-composer")).toBeVisible();
84+
await expect(page.getByTestId("chat-composer-input")).toBeVisible();
85+
});

package-lock.json

Lines changed: 64 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"supabase:diff": "supabase db diff",
1515
"i18n:check": "node scripts/check-i18n-messages.mjs",
1616
"check": "npm run i18n:check && npm run format:check",
17+
"test:e2e": "playwright test",
18+
"test:e2e:debug": "playwright test --headed --workers=1",
1719
"format": "prettier --write .",
1820
"format:check": "prettier --check ."
1921
},
@@ -56,6 +58,7 @@
5658
"vaul": "^1.1.2"
5759
},
5860
"devDependencies": {
61+
"@playwright/test": "^1.54.2",
5962
"@pigment-css/nextjs-plugin": "^0.0.30",
6063
"@pigment-css/react": "^0.0.30",
6164
"@types/node": "22.9.0",

playwright.config.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { defineConfig } from "@playwright/test";
2+
3+
const baseURL = "http://127.0.0.1:3000";
4+
5+
export default defineConfig({
6+
testDir: "./e2e",
7+
fullyParallel: false,
8+
workers: 1,
9+
reporter: process.env.CI ? "github" : "list",
10+
use: {
11+
baseURL,
12+
headless: true,
13+
trace: "retain-on-failure",
14+
video: "retain-on-failure",
15+
screenshot: "only-on-failure",
16+
viewport: {
17+
width: 1280,
18+
height: 900,
19+
},
20+
},
21+
webServer: {
22+
command: "npm run dev -- --hostname 127.0.0.1 --port 3000",
23+
url: baseURL,
24+
reuseExistingServer: !process.env.CI,
25+
timeout: 120_000,
26+
},
27+
projects: [
28+
{
29+
name: "chromium",
30+
},
31+
],
32+
});

src/components/ChatComposer/ChatComposer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ function ChatComposer({
5656
return (
5757
<ChatComposerForm>
5858
{error && <FormMessage message={{ error: error }} />}
59-
<ChatComposerInner onSubmit={onSubmit}>
59+
<ChatComposerInner onSubmit={onSubmit} data-testid="chat-composer">
6060
<TextareaComponent
6161
variant="chat"
6262
placeholder={t("Chat.placeholder", {
@@ -66,6 +66,7 @@ function ChatComposer({
6666
onChange={handleMessageChange}
6767
disabled={!isDemo && isSending}
6868
rows={1}
69+
data-testid="chat-composer-input"
6970
/>
7071
<StyledIconButton
7172
type={isDemo ? "button" : "submit"}
@@ -76,6 +77,7 @@ function ChatComposer({
7677
loading={!isDemo && isSending}
7778
loadingLabel={t("Status.sending")}
7879
disabled={isSendDisabled}
80+
data-testid="chat-composer-send"
7981
/>
8082
</ChatComposerInner>
8183
</ChatComposerForm>

src/components/ChatWindow/ChatWindow.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ const ChatWindow = memo(function ChatWindow({
356356
: existingThread.initiator_first_name;
357357

358358
return (
359-
<StyledChatWindow>
359+
<StyledChatWindow data-testid="chat-window">
360360
<ChatHeader
361361
thread={existingThread ? existingThread : undefined}
362362
listing={listing}
@@ -365,9 +365,9 @@ const ChatWindow = memo(function ChatWindow({
365365
isDrawer={isDrawer}
366366
/>
367367

368-
<StyledMessagesContainer>
368+
<StyledMessagesContainer data-testid="chat-message-list">
369369
{messages.length === 0 && (
370-
<EmptyState>
370+
<EmptyState data-testid="chat-empty-state">
371371
<p>{t("Chat.empty")}</p>
372372
</EmptyState>
373373
)}

src/components/ListingCta/ListingCta.jsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ function ListingCta({ viewer, slug, visibility = true, isStub = false }) {
3939

4040
if (viewer === "owner") {
4141
return (
42-
<StyledListingCta>
42+
<StyledListingCta data-testid="listing-owner-cta">
4343
<Button
4444
variant="secondary"
4545
width="full"
@@ -58,7 +58,7 @@ function ListingCta({ viewer, slug, visibility = true, isStub = false }) {
5858
}
5959
if (isStub) {
6060
return (
61-
<StyledListingCta>
61+
<StyledListingCta data-testid="listing-stub-cta">
6262
<PeelsLogo color="quaternary" />
6363
<Text>
6464
<p>{t("Listings.read.stubNote")}</p>
@@ -77,11 +77,12 @@ function ListingCta({ viewer, slug, visibility = true, isStub = false }) {
7777
}
7878

7979
return (
80-
<StyledListingCta>
80+
<StyledListingCta data-testid="listing-guest-cta">
8181
<Button
8282
variant="primary"
8383
width="full"
8484
href={`/sign-in?redirect_to=/listings/${slug}`}
85+
data-testid="listing-sign-in-to-contact"
8586
>
8687
{t("Actions.signInToContact")}
8788
</Button>

0 commit comments

Comments
 (0)