Skip to content

Commit 5a911ec

Browse files
committed
test: enhance e2e tests by waiting for TRPC responses to improve reliability
1 parent 9c40ea3 commit 5a911ec

6 files changed

Lines changed: 179 additions & 52 deletions

File tree

e2e/articles.spec.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -341,11 +341,15 @@ test.describe("Authenticated Feed Page (Articles)", () => {
341341
timeout: 15000,
342342
});
343343

344-
// Click bookmark button
344+
// Wait for TRPC bookmark mutation response
345+
const bookmarkResponsePromise = page.waitForResponse(
346+
(response) =>
347+
response.url().includes("/api/trpc/") &&
348+
response.url().includes("bookmark") &&
349+
response.status() === 200,
350+
);
345351
await page.getByRole("button", { name: "Save" }).click();
346-
347-
// Wait for network request to complete
348-
await page.waitForTimeout(1000);
352+
await bookmarkResponsePromise;
349353

350354
// Button text should change to "Saved" - add explicit timeout for slow mobile browsers
351355
await expect(page.getByRole("button", { name: "Saved" })).toBeVisible({

e2e/editor.spec.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -807,6 +807,12 @@ test.describe("Publish Flow", () => {
807807
await page.locator(SELECTORS.editorContent).click();
808808
await page.keyboard.type(articleContent);
809809

810+
// Wait for auto-save to complete before opening modal
811+
await expect(page.locator("nav >> text=/Saved .*/")).toBeVisible({
812+
timeout: 15000,
813+
});
814+
await page.waitForTimeout(300); // Allow state to settle
815+
810816
// Wait for Publish button to be enabled
811817
const publishButton = page.locator('nav button:has-text("Publish")');
812818
await expect(publishButton).toBeEnabled({ timeout: 10000 });
@@ -861,8 +867,11 @@ test.describe("Publish Flow", () => {
861867
await page.locator(SELECTORS.linkUrlInput).fill("https://github.com");
862868
await page.locator(SELECTORS.linkTitleInput).fill("GitHub Link");
863869

864-
// Wait for state to update
865-
await page.waitForTimeout(500);
870+
// Wait for auto-save to complete before opening modal
871+
await expect(page.locator("nav >> text=/Saved .*/")).toBeVisible({
872+
timeout: 15000,
873+
});
874+
await page.waitForTimeout(300); // Allow state to settle
866875

867876
// Click Publish button in nav
868877
await page.locator('nav button:has-text("Publish")').click();
@@ -968,8 +977,10 @@ test.describe("Publish Flow", () => {
968977
"Content for scheduled article test here with enough text to pass validation",
969978
);
970979

971-
// Wait for body content to register (debounce)
972-
await page.waitForTimeout(2000);
980+
// Wait for auto-save to complete
981+
await expect(page.locator("nav >> text=/Saved .*/")).toBeVisible({
982+
timeout: 15000,
983+
});
973984

974985
// Expand More Options
975986
await page.locator(SELECTORS.moreOptionsButton).click();
@@ -993,8 +1004,8 @@ test.describe("Publish Flow", () => {
9931004
const dateString = futureDate.toISOString().slice(0, 16);
9941005
await page.locator(SELECTORS.datetimeInput).fill(dateString);
9951006

996-
// Wait for state to update
997-
await page.waitForTimeout(500);
1007+
// Wait for state to settle after date input
1008+
await page.waitForTimeout(300);
9981009

9991010
// Click Publish button in nav
10001011
await page.locator('nav button:has-text("Publish")').click();

e2e/my-posts.spec.ts

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,23 @@ import { articleExcerpt } from "./constants";
55

66
type TabName = "Drafts" | "Scheduled" | "Published";
77

8-
async function openTab(page: Page, tabName: TabName) {
8+
async function openTab(
9+
page: Page,
10+
tabName: TabName,
11+
isMobile: boolean = false,
12+
) {
913
await page.goto("http://localhost:3000/my-posts");
1014
await page.waitForLoadState("domcontentloaded");
11-
await page.getByRole("link", { name: tabName }).click();
15+
16+
// Mobile renders tabs as a select dropdown, desktop uses links
17+
if (isMobile) {
18+
const tabSelect = page.locator("select#tabs");
19+
await expect(tabSelect).toBeVisible({ timeout: 10000 });
20+
await tabSelect.selectOption({ label: tabName });
21+
} else {
22+
await page.getByRole("link", { name: tabName }).click();
23+
}
24+
1225
const slug = tabName.toLowerCase();
1326
await page.waitForURL(`http://localhost:3000/my-posts?tab=${slug}`);
1427
await expect(page).toHaveURL(new RegExp(`\\/my-posts\\?tab=${slug}`));
@@ -52,38 +65,57 @@ test.describe("Authenticated my-posts Page", () => {
5265

5366
test("Tabs for different type of posts should be visible", async ({
5467
page,
68+
isMobile,
5569
}) => {
5670
await page.goto("http://localhost:3000/my-posts");
5771

58-
await expect(page.getByRole("link", { name: "Drafts" })).toBeVisible();
59-
await expect(page.getByRole("link", { name: "Scheduled" })).toBeVisible();
60-
await expect(page.getByRole("link", { name: "Published" })).toBeVisible();
72+
// Mobile renders tabs as a select dropdown, desktop uses links
73+
if (isMobile) {
74+
const tabSelect = page.locator("select#tabs");
75+
await expect(tabSelect).toBeVisible({ timeout: 10000 });
76+
// Verify the select has the correct options
77+
await expect(tabSelect.locator('option:has-text("Drafts")')).toBeVisible();
78+
await expect(
79+
tabSelect.locator('option:has-text("Scheduled")'),
80+
).toBeVisible();
81+
await expect(
82+
tabSelect.locator('option:has-text("Published")'),
83+
).toBeVisible();
84+
} else {
85+
await expect(page.getByRole("link", { name: "Drafts" })).toBeVisible();
86+
await expect(page.getByRole("link", { name: "Scheduled" })).toBeVisible();
87+
await expect(page.getByRole("link", { name: "Published" })).toBeVisible();
88+
}
6189
});
6290

6391
test("Different article tabs should correctly display articles matching that type", async ({
6492
page,
93+
isMobile,
6594
}) => {
6695
await page.goto("http://localhost:3000/my-posts");
6796

68-
await expect(page.getByRole("link", { name: "Drafts" })).toBeVisible();
69-
await expect(page.getByRole("link", { name: "Scheduled" })).toBeVisible();
70-
await expect(page.getByRole("link", { name: "Published" })).toBeVisible();
97+
// Check tab visibility - on mobile these are in a select dropdown
98+
if (!isMobile) {
99+
await expect(page.getByRole("link", { name: "Drafts" })).toBeVisible();
100+
await expect(page.getByRole("link", { name: "Scheduled" })).toBeVisible();
101+
await expect(page.getByRole("link", { name: "Published" })).toBeVisible();
102+
}
71103

72-
await openTab(page, "Published");
104+
await openTab(page, "Published", isMobile);
73105
await expect(
74106
page.getByRole("heading", { name: "Published Article" }),
75107
).toBeVisible({ timeout: 15000 });
76108
await expect(page.getByText(articleExcerpt)).toBeVisible();
77109

78-
await openTab(page, "Scheduled");
110+
await openTab(page, "Scheduled", isMobile);
79111
await expect(
80112
page.getByRole("heading", { name: "Scheduled Article" }),
81113
).toBeVisible({ timeout: 15000 });
82114
await expect(
83115
page.getByText("This is an excerpt for a scheduled article."),
84116
).toBeVisible();
85117

86-
await openTab(page, "Drafts");
118+
await openTab(page, "Drafts", isMobile);
87119
await expect(
88120
page.getByRole("heading", { name: "Draft Article", exact: true }),
89121
).toBeVisible({ timeout: 15000 });
@@ -96,10 +128,11 @@ test.describe("Authenticated my-posts Page", () => {
96128

97129
test("User should close delete modal with Cancel button", async ({
98130
page,
131+
isMobile,
99132
}) => {
100133
const title = "Published Article";
101134
await page.goto("http://localhost:3000/my-posts");
102-
await openTab(page, "Published");
135+
await openTab(page, "Published", isMobile);
103136
await openDeleteModal(page, title);
104137

105138
const closeButton = page.getByRole("button", { name: "Cancel" });
@@ -110,10 +143,13 @@ test.describe("Authenticated my-posts Page", () => {
110143
).toBeHidden();
111144
});
112145

113-
test("User should close delete modal with Close button", async ({ page }) => {
146+
test("User should close delete modal with Close button", async ({
147+
page,
148+
isMobile,
149+
}) => {
114150
const title = "Published Article";
115151
await page.goto("http://localhost:3000/my-posts");
116-
await openTab(page, "Published");
152+
await openTab(page, "Published", isMobile);
117153
await openDeleteModal(page, title);
118154

119155
const closeButton = page.getByRole("button", { name: "Close" });
@@ -124,7 +160,7 @@ test.describe("Authenticated my-posts Page", () => {
124160
).toBeHidden();
125161
});
126162

127-
test("User should delete published article", async ({ page }) => {
163+
test("User should delete published article", async ({ page, isMobile }) => {
128164
const article = {
129165
id: "test-id-for-deletion",
130166
title: "Article to be deleted",
@@ -134,7 +170,7 @@ test.describe("Authenticated my-posts Page", () => {
134170
};
135171
await createArticle(article);
136172
await page.goto("http://localhost:3000/my-posts");
137-
await openTab(page, "Published");
173+
await openTab(page, "Published", isMobile);
138174
await expect(page.getByRole("link", { name: article.title })).toBeVisible();
139175
await openDeleteModal(page, article.title);
140176

e2e/notifications.spec.ts

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,16 @@ test.describe("Notifications Page", () => {
7878
type: 0,
7979
});
8080

81+
// Wait for TRPC notification response to complete
82+
const responsePromise = page.waitForResponse(
83+
(response) =>
84+
response.url().includes("/api/trpc/") &&
85+
response.url().includes("notification") &&
86+
response.status() === 200,
87+
);
8188
await page.goto("http://localhost:3000/notifications");
82-
// Wait for notifications to load
83-
await page.waitForLoadState("domcontentloaded");
89+
await responsePromise;
90+
8491
await expect(
8592
page.getByRole("button", { name: "Mark all as read" }),
8693
).toBeVisible({ timeout: 15000 });
@@ -96,27 +103,38 @@ test.describe("Notifications Page", () => {
96103
type: 0,
97104
});
98105

106+
// Wait for TRPC notification response to complete
107+
const responsePromise = page.waitForResponse(
108+
(response) =>
109+
response.url().includes("/api/trpc/") &&
110+
response.url().includes("notification") &&
111+
response.status() === 200,
112+
);
99113
await page.goto("http://localhost:3000/notifications");
100-
// Wait for notifications to load
101-
await page.waitForLoadState("domcontentloaded");
114+
await responsePromise;
102115

103116
// Wait for notification to appear
104117
await page.waitForSelector('button[title="Mark as read"]', {
105118
timeout: 15000,
106119
});
107120

108-
// Click mark as read button
121+
// Click mark as read button and wait for mutation response
122+
const markReadResponsePromise = page.waitForResponse(
123+
(response) =>
124+
response.url().includes("/api/trpc/") &&
125+
response.url().includes("notification") &&
126+
response.status() === 200,
127+
);
109128
await page.locator('button[title="Mark as read"]').first().click();
110-
111-
// Wait for the notification to disappear or the count to decrease
112-
await page.waitForTimeout(1000);
129+
await markReadResponsePromise;
113130
});
114131
});
115132

116133
test.describe("Notification Creation Flow", () => {
117134
test.beforeEach(async () => {
118-
// Clear notifications before each test to avoid strict mode violations
135+
// Clear notifications for both users before each test to avoid strict mode violations
119136
await clearNotifications(E2E_USER_ONE_ID);
137+
await clearNotifications(E2E_USER_TWO_ID);
120138
});
121139

122140
test("Should create notification when user comments on another user's post", async ({
@@ -151,7 +169,18 @@ test.describe("Notifications Page", () => {
151169

152170
// Now log in as user one and check notifications
153171
await loggedInAsUserOne(page);
154-
await page.goto("http://localhost:3000/notifications");
172+
173+
// Wait for TRPC notification response to complete
174+
const responsePromise = page.waitForResponse(
175+
(response) =>
176+
response.url().includes("/api/trpc/") &&
177+
response.url().includes("notification") &&
178+
response.status() === 200,
179+
);
180+
await page.goto("http://localhost:3000/notifications", {
181+
waitUntil: "commit",
182+
});
183+
await responsePromise;
155184

156185
// Should see notification from user two
157186
await expect(
@@ -203,12 +232,19 @@ test.describe("Notifications Page", () => {
203232
timeout: 15000,
204233
});
205234

206-
// Click reply on the first comment
207-
await page.getByRole("button", { name: "Reply" }).first().click();
235+
// Find the comment container that has the original comment text and click its first reply button
236+
const commentContainer = page
237+
.locator("article")
238+
.filter({ hasText: originalComment })
239+
.first();
240+
await commentContainer
241+
.getByRole("button", { name: "Reply" })
242+
.first()
243+
.click();
208244

209245
// Wait for reply editor to expand
210246
await page.waitForTimeout(500);
211-
// Focus the reply editor and type
247+
// Focus the reply editor and type - find the editor within the comment's reply section
212248
await page.locator(".ProseMirror").last().click();
213249
const replyText = `Reply to trigger notification ${randomUUID()}`;
214250
await page.keyboard.type(replyText);
@@ -223,8 +259,18 @@ test.describe("Notifications Page", () => {
223259

224260
// Log back in as user one and check for notification
225261
await loggedInAsUserOne(page);
226-
await page.goto("http://localhost:3000/notifications");
227-
await page.waitForLoadState("domcontentloaded");
262+
263+
// Wait for TRPC notification response to complete
264+
const notificationResponsePromise = page.waitForResponse(
265+
(response) =>
266+
response.url().includes("/api/trpc/") &&
267+
response.url().includes("notification") &&
268+
response.status() === 200,
269+
);
270+
await page.goto("http://localhost:3000/notifications", {
271+
waitUntil: "commit",
272+
});
273+
await notificationResponsePromise;
228274

229275
await expect(page.getByText("E2E Test User Two").first()).toBeVisible({
230276
timeout: 15000,

0 commit comments

Comments
 (0)