Skip to content

Commit 08802f4

Browse files
authored
Merge pull request #115 from GTBitsOfGood/luciano/more-tests
Add Tests
2 parents 3d9fab1 + 8421ce6 commit 08802f4

12 files changed

Lines changed: 638 additions & 23 deletions

‎src/app/rides/RideCard.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,10 @@ export function RideCard({
133133
!CHAT_DISABLED_STATUSES.has(route.status);
134134

135135
return (
136-
<div className={`${styles.rideCard} ${styles.rideCardDriverNew}`}>
136+
<div
137+
className={`${styles.rideCard} ${styles.rideCardDriverNew}`}
138+
data-testid="ride-card"
139+
>
137140
<div className={styles.rideCardDriverBody}>
138141
{studentName && (
139142
<p className={styles.rideCardStudentName}>{studentName}</p>
@@ -248,7 +251,10 @@ export function RideCard({
248251
const canCancel = CANCELLABLE_STATUSES.has(route.status);
249252

250253
return (
251-
<div className={`${styles.rideCard} ${styles.rideCardStudent}`}>
254+
<div
255+
className={`${styles.rideCard} ${styles.rideCardStudent}`}
256+
data-testid="ride-card"
257+
>
252258
{/* Left section */}
253259
<div className={styles.rideCardStudentBody}>
254260
<div className={styles.rideCardPickupDropoffNew}>

‎tests/e2e/README.md‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# E2E: the ride flow, two ways
22

3-
One user journey, request → schedule → start → pickup → dropoff, run twice:
3+
Ride journeys, each run twice (`api` HTTP / `ui` Chromium):
4+
5+
| Spec | Journey |
6+
| -------- | ------------------------------------------------------------------ |
7+
| `routeA` | student creates, admin schedules, lists agree (stops at Scheduled) |
8+
| `routeB` | scheduled ride → driver start → pickup → dropoff |
9+
| `routeC` | student cancellation requires confirmation and propagates state |
10+
| `routeE` | driver marks an en-route ride as no-show |
411

512
| Project | How | Tells you |
613
| ------- | -------------------------- | ----------------------------------------- |

‎tests/e2e/fixtures.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ export const test = base.extend<Fixtures>({
142142
pageAs: async ({ browser, diag }, provide) => {
143143
const open: BrowserContext[] = [];
144144
await provide(async (key) => {
145-
const context = await browser.newContext({ baseURL: E2E.baseURL });
145+
const context = await browser.newContext({
146+
baseURL: E2E.baseURL,
147+
timezoneId: "America/New_York",
148+
});
146149
await context.addCookies([
147150
{
148151
name: E2E.sessionCookieName,

‎tests/e2e/routeA.api.spec.ts‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* Route A, UI-agnostic. Student creates a ride, admin schedules it, then
3+
* student / admin / driver lists agree on status and assignment.
4+
*
5+
* Stops at Scheduled — the full lifecycle (start → pickup → dropoff) lives
6+
* in routeB.api.spec.ts.
7+
*/
8+
import { test, expect } from "./fixtures";
9+
import { PERSONAS, LOCATIONS, VEHICLE } from "./seed";
10+
11+
const HOUR = 60 * 60 * 1000;
12+
const MINUTE = 60 * 1000;
13+
14+
type Embedded = { _id: string; firstName?: string; lastName?: string };
15+
type RouteJson = {
16+
_id: string;
17+
status: string;
18+
pickupLocation: string;
19+
dropoffLocation: string;
20+
student?: Embedded | string;
21+
driver?: Embedded | string;
22+
vehicle?: { _id: string; name?: string; licensePlate?: string } | string;
23+
};
24+
25+
function idOf(value: Embedded | string | undefined): string {
26+
if (!value) return "";
27+
return typeof value === "string" ? value : String(value._id);
28+
}
29+
30+
function asRoutes(body: unknown): RouteJson[] {
31+
return Array.isArray(body) ? (body as RouteJson[]) : [];
32+
}
33+
34+
test("route A: student creates, admin schedules, lists stay consistent", async ({
35+
apiAs,
36+
}) => {
37+
const student = await apiAs("student");
38+
const admin = await apiAs("admin");
39+
const driver = await apiAs("driver");
40+
41+
// Distinct locations/time from routeB.api.spec.ts so parallel runs don't collide.
42+
const pickup = new Date(Date.now() + 30 * HOUR);
43+
const create = await student.post("/api/routes", {
44+
data: {
45+
pickupLocation: LOCATIONS.exhibitionHall.id,
46+
dropoffLocation: LOCATIONS.studentCenter.id,
47+
scheduledPickupTime: pickup.toISOString(),
48+
pickupWindowStart: new Date(pickup.getTime() - 30 * MINUTE).toISOString(),
49+
pickupWindowEnd: new Date(pickup.getTime() + 30 * MINUTE).toISOString(),
50+
},
51+
});
52+
expect(create.status(), await create.text()).toBe(201);
53+
const created = (await create.json()) as RouteJson;
54+
const routeId = String(created._id);
55+
expect(created.status).toBe("Requested");
56+
57+
const studentList = await student.get("/api/routes");
58+
expect(studentList.status()).toBe(200);
59+
const studentRide = asRoutes(await studentList.json()).find(
60+
(r) => String(r._id) === routeId,
61+
);
62+
expect(studentRide, "ride missing from student list").toBeTruthy();
63+
expect(studentRide!.status).toBe("Requested");
64+
expect(String(studentRide!.pickupLocation)).toBe(LOCATIONS.exhibitionHall.id);
65+
expect(String(studentRide!.dropoffLocation)).toBe(LOCATIONS.studentCenter.id);
66+
67+
const adminList = await admin.get("/api/routes");
68+
expect(adminList.status()).toBe(200);
69+
const adminRide = asRoutes(await adminList.json()).find(
70+
(r) => String(r._id) === routeId,
71+
);
72+
expect(adminRide, "ride missing from admin list").toBeTruthy();
73+
expect(adminRide!.status).toBe("Requested");
74+
75+
const scheduled = await admin.post("/api/routes/schedule", {
76+
data: {
77+
routeId,
78+
driverId: PERSONAS.driver.id,
79+
vehicleId: VEHICLE.id,
80+
},
81+
});
82+
expect(scheduled.status(), await scheduled.text()).toBe(200);
83+
expect(((await scheduled.json()) as RouteJson).status).toBe("Scheduled");
84+
85+
const driverList = await driver.get(
86+
`/api/routes?driver=${PERSONAS.driver.id}`,
87+
);
88+
expect(driverList.status()).toBe(200);
89+
const driverRide = asRoutes(await driverList.json()).find(
90+
(r) => String(r._id) === routeId,
91+
);
92+
expect(driverRide, "ride missing from driver list").toBeTruthy();
93+
expect(driverRide!.status).toBe("Scheduled");
94+
expect(idOf(driverRide!.driver)).toBe(PERSONAS.driver.id);
95+
expect(idOf(driverRide!.vehicle)).toBe(VEHICLE.id);
96+
97+
const readBack = await student.get(`/api/routes?id=${routeId}`);
98+
expect(readBack.status()).toBe(200);
99+
const studentView = (await readBack.json()) as RouteJson;
100+
expect(studentView.status).toBe("Scheduled");
101+
expect(idOf(studentView.driver)).toBe(PERSONAS.driver.id);
102+
expect(idOf(studentView.vehicle)).toBe(VEHICLE.id);
103+
});

‎tests/e2e/routeA.ui.spec.ts‎

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/**
2+
* Route A through the pages. Sessions are minted, so CAS is not involved.
3+
*
4+
* student /rides/new fills the form Requested
5+
* student /rides ride is in this/next week
6+
* admin /admin?tab=Rides assigns driver + vehicle Scheduled
7+
* driver /rides Tomorrow tab shows it
8+
* student /rides/:id scheduled driver/vehicle
9+
*
10+
* Stops at Scheduled — the full lifecycle lives in routeB.ui.spec.ts.
11+
*/
12+
import type { Page } from "@playwright/test";
13+
import { test, expect } from "./fixtures";
14+
import { PERSONAS, LOCATIONS, VEHICLE } from "./seed";
15+
import { estDateDaysFromNow } from "./routeTestHelpers";
16+
17+
const PICKUP = LOCATIONS.techSquare;
18+
const DROPOFF = LOCATIONS.studentCenter;
19+
const PICKUP_TIME = "2:00 PM";
20+
21+
/** TimeInput is a text box with a listbox; typing then Enter commits the first match. */
22+
async function pickTime(input: ReturnType<Page["locator"]>, label: string) {
23+
await input.fill(label);
24+
await input.press("Enter");
25+
await expect(input).toHaveValue(label);
26+
}
27+
28+
function isEstSaturday(): boolean {
29+
return (
30+
new Intl.DateTimeFormat("en-US", {
31+
timeZone: "America/New_York",
32+
weekday: "short",
33+
}).format(new Date()) === "Sat"
34+
);
35+
}
36+
37+
test("route A: student creates, admin schedules (UI)", async ({
38+
pageAs,
39+
diag,
40+
}) => {
41+
test.setTimeout(90_000);
42+
43+
const student = await pageAs("student");
44+
45+
// ── Student requests a ride ────────────────────────────────────────────
46+
await student.goto("/rides/new");
47+
await expect(
48+
student.getByRole("heading", { name: "Create Ride" }),
49+
).toBeVisible();
50+
51+
const today = estDateDaysFromNow(0);
52+
const tomorrow = estDateDaysFromNow(1);
53+
if (tomorrow.getMonth() !== today.getMonth()) {
54+
await student.locator('button[type="button"]:has(svg)').nth(1).click();
55+
}
56+
await student
57+
.getByRole("button", { name: String(tomorrow.getDate()), exact: true })
58+
.click();
59+
60+
const times = student.getByPlaceholder("hh:mm");
61+
await pickTime(times.nth(0), PICKUP_TIME);
62+
await pickTime(student.locator("#pickup-window-from"), "1:30 PM");
63+
await pickTime(student.locator("#pickup-window-to"), "2:30 PM");
64+
65+
const selects = student.locator("select");
66+
await selects.nth(0).selectOption({ label: PICKUP.name });
67+
await selects.nth(1).selectOption({ label: DROPOFF.name });
68+
69+
const created = student.waitForResponse(
70+
(r) => r.url().endsWith("/api/routes") && r.request().method() === "POST",
71+
);
72+
await student.getByRole("button", { name: "Submit" }).click();
73+
const createRes = await created;
74+
expect(createRes.status(), await createRes.text()).toBe(201);
75+
const { _id: routeId } = (await createRes.json()) as { _id: string };
76+
await expect(student).toHaveURL(/\/rides$/);
77+
78+
// Tomorrow is next week when today is Saturday (weeks start Sunday EST).
79+
if (isEstSaturday()) {
80+
await student.getByText("Next Week", { exact: true }).click();
81+
}
82+
83+
const studentCard = student.getByTestId("ride-card").filter({
84+
hasText: PICKUP_TIME,
85+
});
86+
await expect(studentCard).toHaveCount(1);
87+
await expect(
88+
studentCard.getByText("Requested", { exact: true }),
89+
).toBeVisible();
90+
91+
// ── Admin sees it and schedules ────────────────────────────────────────
92+
const admin = await pageAs("admin");
93+
await admin.goto("/admin?tab=Rides");
94+
const row = admin
95+
.getByRole("row")
96+
.filter({ hasText: PERSONAS.student.lastName })
97+
.filter({ hasText: PICKUP.name })
98+
.filter({ hasText: DROPOFF.name })
99+
.filter({ hasText: PICKUP_TIME });
100+
await expect(row).toHaveCount(1);
101+
102+
await row.getByText("Select driver").click();
103+
await admin
104+
.getByRole("menuitem", {
105+
name: `${PERSONAS.driver.lastName}, ${PERSONAS.driver.firstName}`,
106+
})
107+
.click();
108+
await row.getByText("Select vehicle").click();
109+
await admin
110+
.getByRole("menuitem", {
111+
name: `${VEHICLE.name} (${VEHICLE.licensePlate})`,
112+
})
113+
.click();
114+
115+
const scheduled = admin.waitForResponse((r) =>
116+
r.url().endsWith("/api/routes/schedule"),
117+
);
118+
await row.getByRole("button", { name: "Assign" }).click();
119+
const scheduleRes = await scheduled;
120+
expect(scheduleRes.status(), await scheduleRes.text()).toBe(200);
121+
await expect(row).toHaveCount(0);
122+
123+
// ── Driver sees it on Tomorrow ─────────────────────────────────────────
124+
const driver = await pageAs("driver");
125+
await driver.goto("/rides");
126+
await driver.getByText("Tomorrow", { exact: true }).click();
127+
const driverCard = driver
128+
.getByTestId("ride-card")
129+
.filter({ hasText: PICKUP.name })
130+
.filter({ hasText: DROPOFF.name });
131+
await expect(driverCard).toHaveCount(1);
132+
await expect(
133+
driverCard.getByText("Scheduled", { exact: true }),
134+
).toBeVisible();
135+
136+
// ── Student sees scheduled assignment ──────────────────────────────────
137+
await student.goto(`/rides/${routeId}`);
138+
await expect(
139+
student
140+
.getByRole("heading", { name: "Ride Details" })
141+
.locator("xpath=following-sibling::span[1]"),
142+
).toHaveText("Scheduled");
143+
await expect(student.getByText(PICKUP.name)).toBeVisible();
144+
await expect(student.getByText(DROPOFF.name)).toBeVisible();
145+
await expect(student.getByText(VEHICLE.name)).toBeVisible();
146+
await expect(student.getByText(VEHICLE.licensePlate)).toBeVisible();
147+
await expect(
148+
student.getByText(
149+
`${PERSONAS.driver.firstName} ${PERSONAS.driver.lastName}`,
150+
),
151+
).toBeVisible();
152+
153+
expect(diag.apiFailures, diag.summary()).toHaveLength(0);
154+
});
Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
2-
* The ride flow, UI-agnostic. Every step is an HTTP call; if this is red the
3-
* backend is broken regardless of what the pages do.
2+
* Route B, UI-agnostic. Every step is an HTTP call; if this is red the backend
3+
* is broken regardless of what the pages do.
44
*
55
* student POST /api/routes Requested
66
* admin POST /api/routes/schedule Scheduled
@@ -14,9 +14,7 @@ import { PERSONAS, LOCATIONS, VEHICLE } from "./seed";
1414
const HOUR = 60 * 60 * 1000;
1515
const MINUTE = 60 * 1000;
1616

17-
test("ride: request → schedule → start → pickup → dropoff", async ({
18-
apiAs,
19-
}) => {
17+
test("route B: driver start → pickup → dropoff", async ({ apiAs }) => {
2018
const student = await apiAs("student");
2119
const admin = await apiAs("admin");
2220
const driver = await apiAs("driver");
@@ -60,10 +58,15 @@ test("ride: request → schedule → start → pickup → dropoff", async ({
6058
await step(driver, "/api/routes/pickup", { routeId }, "Pickedup");
6159
await step(driver, "/api/routes/complete", { routeId }, "Completed");
6260

63-
// The student sees the finished ride.
64-
const readBack = await student.get(`/api/routes?id=${routeId}`);
65-
expect(readBack.status()).toBe(200);
66-
expect(((await readBack.json()) as { status: string }).status).toBe(
67-
"Completed",
68-
);
61+
// Student, driver, and admin all see the completed final state.
62+
for (const [label, who] of [
63+
["student", student],
64+
["driver", driver],
65+
["admin", admin],
66+
] as const) {
67+
const readBack = await who.get(`/api/routes?id=${routeId}`);
68+
const body = await readBack.text();
69+
expect(readBack.status(), `${label}: ${body}`).toBe(200);
70+
expect((JSON.parse(body) as { status: string }).status).toBe("Completed");
71+
}
6972
});

0 commit comments

Comments
 (0)