mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-08 00:02:15 +01:00
add profile page e2e tests
This commit is contained in:
committed by
Constantin Graf
parent
372c7b02a0
commit
ceec4cbf62
@@ -1,30 +1,187 @@
|
|||||||
import { test, expect } from '../playwright/fixtures';
|
import { test, expect } from '../playwright/fixtures';
|
||||||
import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from '../playwright/config';
|
import { PLAYWRIGHT_BASE_URL, TEST_USER_PASSWORD } from '../playwright/config';
|
||||||
|
import {
|
||||||
|
countEmailsWithSubject,
|
||||||
|
getEmailChangeVerificationUrl,
|
||||||
|
waitForEmailCount,
|
||||||
|
} from './utils/mailpit';
|
||||||
|
import { getCurrentUserViaApi } from './utils/api';
|
||||||
|
import { registerUser } from './utils/members';
|
||||||
import type { Page } from '@playwright/test';
|
import type { Page } from '@playwright/test';
|
||||||
|
|
||||||
async function goToProfilePage(page: Page) {
|
async function goToProfilePage(page: Page) {
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
|
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
|
||||||
}
|
}
|
||||||
|
|
||||||
test('test that user name can be updated', async ({ page }) => {
|
async function saveProfileForm(page: Page): Promise<void> {
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
|
|
||||||
await page.getByLabel('Name', { exact: true }).fill('NEW NAME');
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
page.waitForResponse(
|
||||||
|
(resp) =>
|
||||||
|
resp.url().includes('/user/profile-information') &&
|
||||||
|
resp.request().method() === 'POST'
|
||||||
|
),
|
||||||
page.getByRole('button', { name: 'Save' }).first().click(),
|
page.getByRole('button', { name: 'Save' }).first().click(),
|
||||||
page.waitForResponse('**/user/profile-information'),
|
|
||||||
]);
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('user name can be updated', async ({ page }) => {
|
||||||
|
await goToProfilePage(page);
|
||||||
|
await page.getByLabel('Name', { exact: true }).fill('NEW NAME');
|
||||||
|
await saveProfileForm(page);
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.getByLabel('Name', { exact: true })).toHaveValue('NEW NAME');
|
await expect(page.getByLabel('Name', { exact: true })).toHaveValue('NEW NAME');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('test that user email can be updated', async ({ page }) => {
|
test('timezone change persists across reload', async ({ page }) => {
|
||||||
// this does not work because of email verification currently
|
await goToProfilePage(page);
|
||||||
await page.goto(PLAYWRIGHT_BASE_URL + '/user/profile');
|
await page.getByLabel('Timezone').selectOption('America/New_York');
|
||||||
const emailId = Math.round(Math.random() * 10000);
|
await saveProfileForm(page);
|
||||||
await page.getByLabel('Email').fill(`newemail+${emailId}@test.com`);
|
|
||||||
await page.getByRole('button', { name: 'Save' }).first().click();
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
await expect(page.getByLabel('Email')).toHaveValue(`newemail+${emailId}@test.com`);
|
await expect(page.getByLabel('Timezone')).toHaveValue('America/New_York');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('week-start change persists across reload', async ({ page }) => {
|
||||||
|
await goToProfilePage(page);
|
||||||
|
await page.getByLabel('Start of the week').selectOption('sunday');
|
||||||
|
await saveProfileForm(page);
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.getByLabel('Start of the week')).toHaveValue('sunday');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submitting a new email keeps the current email displayed after reload', async ({
|
||||||
|
page,
|
||||||
|
ctx,
|
||||||
|
}) => {
|
||||||
|
const { email: oldEmail } = await getCurrentUserViaApi(ctx);
|
||||||
|
const newEmail = `newemail+${Date.now()}@test.com`;
|
||||||
|
|
||||||
|
await goToProfilePage(page);
|
||||||
|
await page.getByLabel('Email').fill(newEmail);
|
||||||
|
await saveProfileForm(page);
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Email')).toHaveValue(oldEmail);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('submitting a new email sends a verification email to the new address', async ({
|
||||||
|
page,
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
await goToProfilePage(page);
|
||||||
|
const newEmail = `newemail+${Date.now()}@test.com`;
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(newEmail);
|
||||||
|
await saveProfileForm(page);
|
||||||
|
|
||||||
|
expect(await waitForEmailCount(request, newEmail, 'Verify Email Address', 1)).toBeGreaterThan(
|
||||||
|
0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('mixed-case email is lower-cased before the verification mail is sent', async ({
|
||||||
|
page,
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
await goToProfilePage(page);
|
||||||
|
const stamp = Date.now();
|
||||||
|
const mixedCase = `MixedCase+${stamp}@Example.COM`;
|
||||||
|
const lowerCased = `mixedcase+${stamp}@example.com`;
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(mixedCase);
|
||||||
|
await saveProfileForm(page);
|
||||||
|
|
||||||
|
const verifyUrl = await getEmailChangeVerificationUrl(request, lowerCased);
|
||||||
|
expect(new URL(verifyUrl).searchParams.get('email')).toBe(lowerCased);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('re-submitting the current email does not send a verification email', async ({
|
||||||
|
page,
|
||||||
|
ctx,
|
||||||
|
request,
|
||||||
|
}) => {
|
||||||
|
const { email: currentEmail } = await getCurrentUserViaApi(ctx);
|
||||||
|
const beforeCount = await countEmailsWithSubject(request, currentEmail, 'Verify Email Address');
|
||||||
|
|
||||||
|
await goToProfilePage(page);
|
||||||
|
await page.getByLabel('Email').fill(currentEmail);
|
||||||
|
await saveProfileForm(page);
|
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
const afterCount = await countEmailsWithSubject(request, currentEmail, 'Verify Email Address');
|
||||||
|
expect(afterCount).toBe(beforeCount);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking the verification link swaps the email and shows a success banner', async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await goToProfilePage(page);
|
||||||
|
const newEmail = `verify+${Date.now()}@test.com`;
|
||||||
|
await page.getByLabel('Email').fill(newEmail);
|
||||||
|
await saveProfileForm(page);
|
||||||
|
const verifyUrl = await getEmailChangeVerificationUrl(page.request, newEmail);
|
||||||
|
|
||||||
|
await page.goto(verifyUrl);
|
||||||
|
await page.waitForURL(/\/dashboard/);
|
||||||
|
|
||||||
|
const banner = page.getByTestId('banner');
|
||||||
|
await expect(banner).toBeVisible();
|
||||||
|
await expect(banner).toContainText('Your email address has been updated successfully.');
|
||||||
|
|
||||||
|
await goToProfilePage(page);
|
||||||
|
await expect(page.getByLabel('Email')).toHaveValue(newEmail);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('visiting another user’s verification link is forbidden', async ({ page, browser }) => {
|
||||||
|
await goToProfilePage(page);
|
||||||
|
const newEmail = `victim+${Date.now()}@test.com`;
|
||||||
|
await page.getByLabel('Email').fill(newEmail);
|
||||||
|
await saveProfileForm(page);
|
||||||
|
const verifyUrl = await getEmailChangeVerificationUrl(page.request, newEmail);
|
||||||
|
|
||||||
|
const other = await registerUser(browser, 'Other User', `other+${Date.now()}@test.com`);
|
||||||
|
try {
|
||||||
|
const response = await other.page.goto(verifyUrl);
|
||||||
|
expect(response?.status()).toBe(403);
|
||||||
|
} finally {
|
||||||
|
await other.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a stale verification link from a previous submission is rejected', async ({ page }) => {
|
||||||
|
await goToProfilePage(page);
|
||||||
|
const stamp = Date.now();
|
||||||
|
const olderEmail = `older+${stamp}@test.com`;
|
||||||
|
const newerEmail = `newer+${stamp}@test.com`;
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(olderEmail);
|
||||||
|
await saveProfileForm(page);
|
||||||
|
const staleUrl = await getEmailChangeVerificationUrl(page.request, olderEmail);
|
||||||
|
|
||||||
|
await page.getByLabel('Email').fill(newerEmail);
|
||||||
|
await saveProfileForm(page);
|
||||||
|
|
||||||
|
const response = await page.goto(staleUrl);
|
||||||
|
expect(response?.status()).toBe(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('visiting the verification link while logged out redirects to login', async ({
|
||||||
|
page,
|
||||||
|
browser,
|
||||||
|
}) => {
|
||||||
|
await goToProfilePage(page);
|
||||||
|
const newEmail = `loggedout+${Date.now()}@test.com`;
|
||||||
|
await page.getByLabel('Email').fill(newEmail);
|
||||||
|
await saveProfileForm(page);
|
||||||
|
const verifyUrl = await getEmailChangeVerificationUrl(page.request, newEmail);
|
||||||
|
|
||||||
|
const anonContext = await browser.newContext();
|
||||||
|
try {
|
||||||
|
const anonPage = await anonContext.newPage();
|
||||||
|
await anonPage.goto(verifyUrl);
|
||||||
|
await anonPage.waitForURL(/\/login/);
|
||||||
|
} finally {
|
||||||
|
await anonContext.close();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function createNewApiToken(page) {
|
async function createNewApiToken(page) {
|
||||||
|
|||||||
@@ -788,6 +788,19 @@ export async function createTimeEntryWithTimestampsViaApi(
|
|||||||
// User profile helpers
|
// User profile helpers
|
||||||
// ──────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export async function getCurrentUserViaApi(ctx: TestContext) {
|
||||||
|
const response = await ctx.request.get(`${PLAYWRIGHT_BASE_URL}/api/v1/users/me`);
|
||||||
|
expect(response.status()).toBe(200);
|
||||||
|
const body = await response.json();
|
||||||
|
return body.data as {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
timezone: string;
|
||||||
|
week_start: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateUserProfileViaWeb(
|
export async function updateUserProfileViaWeb(
|
||||||
page: Page,
|
page: Page,
|
||||||
settings: { timezone?: string; week_start?: string }
|
settings: { timezone?: string; week_start?: string }
|
||||||
|
|||||||
@@ -81,3 +81,64 @@ export async function getPasswordResetUrl(
|
|||||||
|
|
||||||
return resetUrlMatch![1].replace(/&/g, '&');
|
return resetUrlMatch![1].replace(/&/g, '&');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count emails matching the given subject sent to the given address.
|
||||||
|
*/
|
||||||
|
export async function countEmailsWithSubject(
|
||||||
|
request: APIRequestContext,
|
||||||
|
recipientEmail: string,
|
||||||
|
subject: string
|
||||||
|
): Promise<number> {
|
||||||
|
const searchResult = await searchEmails(
|
||||||
|
request,
|
||||||
|
`to:${encodeURIComponent(recipientEmail)} subject:"${subject}"`
|
||||||
|
);
|
||||||
|
return searchResult.messages.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Poll Mailpit until the count of matching emails reaches `min`, or 5 attempts
|
||||||
|
* (~2.5s) elapse. Returns the final count.
|
||||||
|
*/
|
||||||
|
export async function waitForEmailCount(
|
||||||
|
request: APIRequestContext,
|
||||||
|
recipientEmail: string,
|
||||||
|
subject: string,
|
||||||
|
min: number
|
||||||
|
): Promise<number> {
|
||||||
|
let count = 0;
|
||||||
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
|
count = await countEmailsWithSubject(request, recipientEmail, subject);
|
||||||
|
if (count >= min) break;
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the email-change verification URL from a Mailpit email sent to the given address.
|
||||||
|
* Retries a few times to allow for email delivery delay.
|
||||||
|
*/
|
||||||
|
export async function getEmailChangeVerificationUrl(
|
||||||
|
request: APIRequestContext,
|
||||||
|
recipientEmail: string
|
||||||
|
): Promise<string> {
|
||||||
|
let searchResult: { messages: Array<{ ID: string }> } = { messages: [] };
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 5; attempt++) {
|
||||||
|
searchResult = await searchEmails(
|
||||||
|
request,
|
||||||
|
`to:${encodeURIComponent(recipientEmail)} subject:"Verify Email Address"`
|
||||||
|
);
|
||||||
|
if (searchResult.messages.length > 0) break;
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
expect(searchResult.messages.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
const message = await getMessage(request, searchResult.messages[0].ID);
|
||||||
|
const verifyUrlMatch = message.HTML.match(/href="([^"]*verify-email-change[^"]*)"/);
|
||||||
|
expect(verifyUrlMatch).toBeTruthy();
|
||||||
|
|
||||||
|
return verifyUrlMatch![1].replace(/&/g, '&');
|
||||||
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export const test = baseTest.extend<
|
|||||||
const email = `john+${Date.now()}_${Math.floor(Math.random() * 10000)}@doe.com`;
|
const email = `john+${Date.now()}_${Math.floor(Math.random() * 10000)}@doe.com`;
|
||||||
const password = TEST_USER_PASSWORD;
|
const password = TEST_USER_PASSWORD;
|
||||||
const name = 'John Doe';
|
const name = 'John Doe';
|
||||||
|
const timezone = await page.evaluate(
|
||||||
|
() => Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
|
);
|
||||||
|
|
||||||
// Use page.context().request() so cookies are automatically shared with the page
|
// Use page.context().request() so cookies are automatically shared with the page
|
||||||
const request = page.context().request;
|
const request = page.context().request;
|
||||||
@@ -64,6 +67,7 @@ export const test = baseTest.extend<
|
|||||||
password,
|
password,
|
||||||
password_confirmation: password,
|
password_confirmation: password,
|
||||||
terms: 'on',
|
terms: 'on',
|
||||||
|
timezone,
|
||||||
},
|
},
|
||||||
maxRedirects: 0,
|
maxRedirects: 0,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user