Skip to content

Commit f70f946

Browse files
committed
Expand e2e test coverage migrate to API-based data setup
1 parent bbe05ca commit f70f946

21 files changed

+2586
-820
lines changed

e2e/auth.spec.ts

Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
3+
import { getPasswordResetUrl } from './utils/mailpit';
34

45
async function registerNewUser(page, email, password) {
56
await page.goto(PLAYWRIGHT_BASE_URL + '/register');
@@ -35,14 +36,200 @@ test('can register and delete account', async ({ page }) => {
3536
await registerNewUser(page, email, password);
3637
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
3738
await page.getByRole('button', { name: 'Delete Account' }).click();
39+
await expect(page.getByRole('dialog')).toBeVisible();
3840
await page.getByPlaceholder('Password').fill(password);
39-
await page.getByRole('button', { name: 'Delete Account' }).click();
41+
await page.getByRole('dialog').getByRole('button', { name: 'Delete Account' }).click();
4042
await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login');
4143
await page.goto(PLAYWRIGHT_BASE_URL + '/login');
4244
await page.getByLabel('Email').fill(email);
4345
await page.getByLabel('Password').fill(password);
4446
await page.getByRole('button', { name: 'Log in' }).click();
45-
await expect(page.getByRole('paragraph')).toContainText(
47+
await expect(page.getByRole('alert')).toContainText(
4648
'These credentials do not match our records.'
4749
);
4850
});
51+
52+
test('shows error for invalid email on forgot password', async ({ page }) => {
53+
await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password');
54+
55+
// Request password reset with non-existent email
56+
await page.getByLabel('Email').fill('nonexistent@example.com');
57+
await page.getByRole('button', { name: 'Email Password Reset Link' }).click();
58+
59+
// Should show error message
60+
await expect(page.getByText("We can't find a user with that email address.")).toBeVisible();
61+
});
62+
63+
test('shows browser validation for invalid email format on forgot password', async ({ page }) => {
64+
await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password');
65+
66+
// Request password reset with invalid email format
67+
const emailInput = page.getByLabel('Email');
68+
await emailInput.fill('notanemail');
69+
70+
// Check for browser validation - the input should be invalid
71+
const isInvalid = await emailInput.evaluate((el: HTMLInputElement) => !el.validity.valid);
72+
expect(isInvalid).toBe(true);
73+
});
74+
75+
test('shows browser validation for empty email on forgot password', async ({ page }) => {
76+
await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password');
77+
78+
// The email input is required, so it should be invalid when empty
79+
const emailInput = page.getByLabel('Email');
80+
81+
// Check for browser validation - the input should be invalid because it's required and empty
82+
const isInvalid = await emailInput.evaluate((el: HTMLInputElement) => el.validity.valueMissing);
83+
expect(isInvalid).toBe(true);
84+
});
85+
86+
test('can reset password via email link', async ({ page, request }) => {
87+
// First register a new user
88+
const email = `john+${Math.round(Math.random() * 10000)}@doe.com`;
89+
const originalPassword = 'suchagreatpassword123';
90+
const newPassword = 'mynewsecurepassword456';
91+
await registerNewUser(page, email, originalPassword);
92+
93+
// Log out
94+
await page.getByTestId('current_user_button').click();
95+
await page.getByText('Log Out').click();
96+
await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login');
97+
98+
// Request password reset
99+
await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password');
100+
await page.getByLabel('Email').fill(email);
101+
await page.getByRole('button', { name: 'Email Password Reset Link' }).click();
102+
await expect(page.getByText('We have emailed your password reset link.')).toBeVisible();
103+
104+
// Get password reset URL from email
105+
const resetUrl = await getPasswordResetUrl(request, email);
106+
107+
// Navigate to reset page
108+
await page.goto(resetUrl);
109+
110+
// Fill in new password
111+
await page.getByLabel('Password', { exact: true }).fill(newPassword);
112+
await page.getByLabel('Confirm Password').fill(newPassword);
113+
await page.getByRole('button', { name: 'Reset Password' }).click();
114+
115+
// Should redirect to login page after successful reset
116+
await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login');
117+
118+
// Try logging in with new password
119+
await page.getByLabel('Email').fill(email);
120+
await page.getByLabel('Password').fill(newPassword);
121+
await page.getByRole('button', { name: 'Log in' }).click();
122+
await expect(page.getByTestId('dashboard_view')).toBeVisible();
123+
});
124+
125+
test('shows validation error for password mismatch on reset', async ({ page, request }) => {
126+
// First register a new user
127+
const email = `john+${Math.round(Math.random() * 10000)}@doe.com`;
128+
const originalPassword = 'suchagreatpassword123';
129+
await registerNewUser(page, email, originalPassword);
130+
131+
// Log out
132+
await page.getByTestId('current_user_button').click();
133+
await page.getByText('Log Out').click();
134+
await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login');
135+
136+
// Request password reset
137+
await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password');
138+
await page.getByLabel('Email').fill(email);
139+
await page.getByRole('button', { name: 'Email Password Reset Link' }).click();
140+
await expect(page.getByText('We have emailed your password reset link.')).toBeVisible();
141+
142+
// Get password reset URL from email
143+
const resetUrl = await getPasswordResetUrl(request, email);
144+
145+
// Navigate to reset page
146+
await page.goto(resetUrl);
147+
148+
// Fill in mismatched passwords
149+
await page.getByLabel('Password', { exact: true }).fill('newpassword123');
150+
await page.getByLabel('Confirm Password').fill('differentpassword456');
151+
await page.getByRole('button', { name: 'Reset Password' }).click();
152+
153+
// Should show validation error
154+
await expect(page.getByText('The password field confirmation does not match.')).toBeVisible();
155+
});
156+
157+
test('shows validation error for short password on reset', async ({ page, request }) => {
158+
// First register a new user
159+
const email = `john+${Math.round(Math.random() * 10000)}@doe.com`;
160+
const originalPassword = 'suchagreatpassword123';
161+
await registerNewUser(page, email, originalPassword);
162+
163+
// Log out
164+
await page.getByTestId('current_user_button').click();
165+
await page.getByText('Log Out').click();
166+
await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login');
167+
168+
// Request password reset
169+
await page.goto(PLAYWRIGHT_BASE_URL + '/forgot-password');
170+
await page.getByLabel('Email').fill(email);
171+
await page.getByRole('button', { name: 'Email Password Reset Link' }).click();
172+
await expect(page.getByText('We have emailed your password reset link.')).toBeVisible();
173+
174+
// Get password reset URL from email
175+
const resetUrl = await getPasswordResetUrl(request, email);
176+
177+
// Navigate to reset page
178+
await page.goto(resetUrl);
179+
180+
// Fill in short password
181+
await page.getByLabel('Password', { exact: true }).fill('short');
182+
await page.getByLabel('Confirm Password').fill('short');
183+
await page.getByRole('button', { name: 'Reset Password' }).click();
184+
185+
// Should show validation error about minimum length
186+
await expect(page.getByText('must be at least')).toBeVisible();
187+
});
188+
189+
test('shows error for invalid login credentials', async ({ page }) => {
190+
await page.goto(PLAYWRIGHT_BASE_URL + '/login');
191+
await page.getByLabel('Email').fill('nonexistent@example.com');
192+
await page.getByLabel('Password').fill('wrongpassword123');
193+
await page.getByRole('button', { name: 'Log in' }).click();
194+
195+
await expect(
196+
page.getByText('These credentials do not match our records.')
197+
).toBeVisible();
198+
});
199+
200+
test('shows error when registering with existing email', async ({ page }) => {
201+
const email = `john+${Math.round(Math.random() * 10000)}@doe.com`;
202+
const password = 'suchagreatpassword123';
203+
204+
// Register first user
205+
await registerNewUser(page, email, password);
206+
207+
// Log out
208+
await page.getByTestId('current_user_button').click();
209+
await page.getByText('Log Out').click();
210+
await page.waitForURL(PLAYWRIGHT_BASE_URL + '/login');
211+
212+
// Try to register with the same email
213+
await page.goto(PLAYWRIGHT_BASE_URL + '/register');
214+
await page.getByLabel('Name').fill('Another User');
215+
await page.getByLabel('Email').fill(email);
216+
await page.getByLabel('Password', { exact: true }).fill(password);
217+
await page.getByLabel('Confirm Password').fill(password);
218+
await page.getByLabel('I agree to the Terms of').click();
219+
await page.getByRole('button', { name: 'Register' }).click();
220+
221+
// Should show error about email already taken
222+
await expect(page.getByText('The resource already exists.')).toBeVisible();
223+
});
224+
225+
test('shows validation error for weak password on registration', async ({ page }) => {
226+
await page.goto(PLAYWRIGHT_BASE_URL + '/register');
227+
await page.getByLabel('Name').fill('Weak Password User');
228+
await page.getByLabel('Email').fill(`weak+${Math.round(Math.random() * 10000)}@test.com`);
229+
await page.getByLabel('Password', { exact: true }).fill('short');
230+
await page.getByLabel('Confirm Password').fill('short');
231+
await page.getByLabel('I agree to the Terms of').click();
232+
await page.getByRole('button', { name: 'Register' }).click();
233+
234+
await expect(page.getByText('must be at least')).toBeVisible();
235+
});

e2e/calendar.spec.ts

Lines changed: 110 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import { PLAYWRIGHT_BASE_URL } from '../playwright/config';
22
import { test } from '../playwright/fixtures';
33
import { expect } from '@playwright/test';
44
import type { Page } from '@playwright/test';
5-
import { createProject, createBillableProject, createBareTimeEntry } from './utils/reporting';
5+
import {
6+
createBillableProjectViaApi,
7+
createProjectViaApi,
8+
createBareTimeEntryViaApi,
9+
} from './utils/api';
610

711
async function goToCalendar(page: Page) {
812
await page.goto(PLAYWRIGHT_BASE_URL + '/calendar');
@@ -17,11 +21,12 @@ async function goToCalendar(page: Page) {
1721

1822
test('test that changing project in calendar edit modal from non-billable to billable updates billable status', async ({
1923
page,
24+
ctx,
2025
}) => {
2126
const billableProjectName = 'Billable Cal Project ' + Math.floor(1 + Math.random() * 10000);
2227

23-
await createBillableProject(page, billableProjectName);
24-
await createBareTimeEntry(page, 'Test billable calendar', '1h');
28+
await createBillableProjectViaApi(ctx, { name: billableProjectName });
29+
await createBareTimeEntryViaApi(ctx, 'Test billable calendar', '1h');
2530

2631
await goToCalendar(page);
2732

@@ -59,14 +64,15 @@ test('test that changing project in calendar edit modal from non-billable to bil
5964

6065
test('test that changing project in calendar edit modal from billable to non-billable updates billable status', async ({
6166
page,
67+
ctx,
6268
}) => {
6369
const billableProjectName = 'Billable Cal Rev Project ' + Math.floor(1 + Math.random() * 10000);
6470
const nonBillableProjectName =
6571
'NonBillable Cal Rev Project ' + Math.floor(1 + Math.random() * 10000);
6672

67-
await createBillableProject(page, billableProjectName);
68-
await createProject(page, nonBillableProjectName);
69-
await createBareTimeEntry(page, 'Test billable cal reverse', '1h');
73+
await createBillableProjectViaApi(ctx, { name: billableProjectName });
74+
await createProjectViaApi(ctx, { name: nonBillableProjectName });
75+
await createBareTimeEntryViaApi(ctx, 'Test billable cal reverse', '1h');
7076

7177
await goToCalendar(page);
7278

@@ -112,12 +118,13 @@ test('test that changing project in calendar edit modal from billable to non-bil
112118

113119
test('test that opening calendar edit modal for a time entry with manually overridden billable status preserves that status', async ({
114120
page,
121+
ctx,
115122
}) => {
116123
const billableProjectName =
117124
'Billable Cal Persist Project ' + Math.floor(1 + Math.random() * 10000);
118125

119-
await createBillableProject(page, billableProjectName);
120-
await createBareTimeEntry(page, 'Test cal persist override', '1h');
126+
await createBillableProjectViaApi(ctx, { name: billableProjectName });
127+
await createBareTimeEntryViaApi(ctx, 'Test cal persist override', '1h');
121128

122129
await goToCalendar(page);
123130

@@ -186,3 +193,98 @@ test('test that opening calendar edit modal for a time entry with manually overr
186193
const responseBody = await updateResponse.json();
187194
expect(responseBody.data.billable).toBe(false);
188195
});
196+
197+
test('test that calendar page loads and displays time entries', async ({ page, ctx }) => {
198+
await createBareTimeEntryViaApi(ctx, 'Calendar display test', '1h');
199+
200+
await goToCalendar(page);
201+
202+
// Calendar container should be visible
203+
await expect(page.locator('.fc')).toBeVisible();
204+
205+
// The time entry should appear as a calendar event
206+
await expect(
207+
page.locator('.fc-event').filter({ hasText: 'Calendar display test' }).first()
208+
).toBeVisible();
209+
});
210+
211+
test('test that calendar navigation buttons work', async ({ page }) => {
212+
await goToCalendar(page);
213+
await expect(page.locator('.fc')).toBeVisible();
214+
215+
// Click the "next" button to navigate forward
216+
await page.locator('button.fc-next-button').click();
217+
await expect(page.locator('.fc')).toBeVisible();
218+
219+
// Click the "prev" button to navigate back
220+
await page.locator('button.fc-prev-button').click();
221+
await expect(page.locator('.fc')).toBeVisible();
222+
223+
// Navigate forward first so "today" button becomes enabled, then click it
224+
await page.locator('button.fc-next-button').click();
225+
await page.locator('button.fc-today-button').click();
226+
await expect(page.locator('.fc')).toBeVisible();
227+
});
228+
229+
test('test that editing time entry description via calendar modal works', async ({ page, ctx }) => {
230+
const originalDescription = 'Edit me in calendar ' + Math.floor(1 + Math.random() * 10000);
231+
const updatedDescription = 'Updated in calendar ' + Math.floor(1 + Math.random() * 10000);
232+
await createBareTimeEntryViaApi(ctx, originalDescription, '1h');
233+
234+
await goToCalendar(page);
235+
236+
// Click on the time entry event
237+
await page.locator('.fc-event').filter({ hasText: originalDescription }).first().click();
238+
await expect(page.getByRole('dialog')).toBeVisible();
239+
240+
// Update the description (edit modal uses placeholder, not data-testid)
241+
const descriptionInput = page.getByRole('dialog').getByPlaceholder('What did you work on?');
242+
await descriptionInput.fill(updatedDescription);
243+
244+
// Save and verify
245+
const [editResponse] = await Promise.all([
246+
page.waitForResponse(
247+
(response) =>
248+
response.url().includes('/time-entries/') &&
249+
response.request().method() === 'PUT' &&
250+
response.status() === 200
251+
),
252+
page.getByRole('button', { name: 'Update Time Entry' }).click(),
253+
]);
254+
const editBody = await editResponse.json();
255+
expect(editBody.data.description).toBe(updatedDescription);
256+
257+
// Verify the updated description is shown in the calendar UI
258+
await expect(
259+
page.locator('.fc-event').filter({ hasText: updatedDescription }).first()
260+
).toBeVisible();
261+
// Verify the old description is no longer shown
262+
await expect(
263+
page.locator('.fc-event').filter({ hasText: originalDescription })
264+
).not.toBeVisible();
265+
});
266+
267+
test('test that deleting time entry from calendar modal works', async ({ page, ctx }) => {
268+
const description = 'Delete me from calendar ' + Math.floor(1 + Math.random() * 10000);
269+
await createBareTimeEntryViaApi(ctx, description, '1h');
270+
271+
await goToCalendar(page);
272+
273+
// Click on the time entry event
274+
await page.locator('.fc-event').filter({ hasText: description }).first().click();
275+
await expect(page.getByRole('dialog')).toBeVisible();
276+
277+
// Click the delete button
278+
await Promise.all([
279+
page.waitForResponse(
280+
(response) =>
281+
response.url().includes('/time-entries/') &&
282+
response.request().method() === 'DELETE' &&
283+
response.status() === 204
284+
),
285+
page.getByRole('dialog').getByRole('button', { name: 'Delete' }).click(),
286+
]);
287+
288+
// Verify the event is removed from the calendar
289+
await expect(page.locator('.fc-event').filter({ hasText: description })).not.toBeVisible();
290+
});

0 commit comments

Comments
 (0)