mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 11:42:15 +01:00
Add Mailpit SMTP and refine Playwright tests
This commit is contained in:
53
e2e/utils/mailpit.ts
Normal file
53
e2e/utils/mailpit.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { APIRequestContext } from '@playwright/test';
|
||||
import { MAILPIT_BASE_URL } from '../../playwright/config';
|
||||
|
||||
/**
|
||||
* Search for emails in Mailpit matching the given query.
|
||||
*/
|
||||
export async function searchEmails(
|
||||
request: APIRequestContext,
|
||||
query: string
|
||||
): Promise<{ messages: Array<{ ID: string; Subject: string }> }> {
|
||||
const response = await request.get(`${MAILPIT_BASE_URL}/api/v1/search?query=${query}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the full email message from Mailpit by ID.
|
||||
*/
|
||||
export async function getMessage(
|
||||
request: APIRequestContext,
|
||||
messageId: string
|
||||
): Promise<{ HTML: string; Text: string }> {
|
||||
const response = await request.get(`${MAILPIT_BASE_URL}/api/v1/message/${messageId}`);
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the invitation acceptance URL from a Mailpit email sent to the given address.
|
||||
* Retries a few times to allow for email delivery delay.
|
||||
*/
|
||||
export async function getInvitationAcceptUrl(
|
||||
request: APIRequestContext,
|
||||
recipientEmail: string
|
||||
): Promise<string> {
|
||||
let searchResult: { messages: Array<{ ID: string }> } = { messages: [] };
|
||||
|
||||
// Retry up to 5 times with 500ms delay to allow for email delivery
|
||||
for (let attempt = 0; attempt < 5; attempt++) {
|
||||
searchResult = await searchEmails(
|
||||
request,
|
||||
`to:${encodeURIComponent(recipientEmail)} subject:"Organization Invitation"`
|
||||
);
|
||||
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 acceptUrlMatch = message.HTML.match(/href="([^"]*team-invitations[^"]*)"/);
|
||||
expect(acceptUrlMatch).toBeTruthy();
|
||||
|
||||
return acceptUrlMatch![1].replace(/&/g, '&');
|
||||
}
|
||||
68
e2e/utils/members.ts
Normal file
68
e2e/utils/members.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { expect } from '@playwright/test';
|
||||
import type { Browser, Page } from '@playwright/test';
|
||||
import { PLAYWRIGHT_BASE_URL } from '../../playwright/config';
|
||||
import { getInvitationAcceptUrl } from './mailpit';
|
||||
|
||||
/**
|
||||
* Register a new user in a fresh browser context and return the page + context.
|
||||
*/
|
||||
export async function registerUser(
|
||||
browser: Browser,
|
||||
name: string,
|
||||
email: string
|
||||
): Promise<{ page: Page; close: () => Promise<void> }> {
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto(PLAYWRIGHT_BASE_URL + '/register');
|
||||
await page.getByLabel('Name').fill(name);
|
||||
await page.getByLabel('Email').fill(email);
|
||||
await page.getByLabel('Password', { exact: true }).fill('amazingpassword123');
|
||||
await page.getByLabel('Confirm Password').fill('amazingpassword123');
|
||||
await page.getByLabel('I agree to the Terms of').click();
|
||||
await page.getByRole('button', { name: 'Register' }).click();
|
||||
await page.waitForURL(PLAYWRIGHT_BASE_URL + '/dashboard');
|
||||
|
||||
return { page, close: () => context.close() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Invite a user by email from the members page and accept the invitation
|
||||
* through a second browser session, returning the accepted member to the
|
||||
* members table as a real (non-placeholder) member.
|
||||
*
|
||||
* @param ownerPage – The page of the organization owner who sends the invite
|
||||
* @param browser – Browser instance used to create a second context
|
||||
* @param memberName – Display name for the new user
|
||||
* @param memberEmail – Email address (must not be registered yet)
|
||||
* @param role – Role button label: 'Employee' | 'Manager' | 'Administrator'
|
||||
*/
|
||||
export async function inviteAndAcceptMember(
|
||||
ownerPage: Page,
|
||||
browser: Browser,
|
||||
memberName: string,
|
||||
memberEmail: string,
|
||||
role: 'Employee' | 'Manager' | 'Administrator'
|
||||
): Promise<void> {
|
||||
// 1. Register the second user
|
||||
const secondUser = await registerUser(browser, memberName, memberEmail);
|
||||
|
||||
// 2. Send invitation from the owner
|
||||
await ownerPage.goto(PLAYWRIGHT_BASE_URL + '/members');
|
||||
await ownerPage.getByRole('button', { name: 'Invite Member' }).click();
|
||||
await expect(ownerPage.getByPlaceholder('Member Email')).toBeVisible();
|
||||
await ownerPage.getByLabel('Email').fill(memberEmail);
|
||||
await ownerPage.getByRole('button', { name: role }).click();
|
||||
await Promise.all([
|
||||
ownerPage.getByRole('button', { name: 'Invite Member', exact: true }).click(),
|
||||
expect(ownerPage.getByRole('main')).toContainText(memberEmail),
|
||||
]);
|
||||
|
||||
// 3. Retrieve the acceptance link from Mailpit and accept
|
||||
const acceptUrl = await getInvitationAcceptUrl(secondUser.page.request, memberEmail);
|
||||
await secondUser.page.goto(acceptUrl);
|
||||
await secondUser.page.waitForURL(/dashboard/);
|
||||
|
||||
// 4. Clean up
|
||||
await secondUser.close();
|
||||
}
|
||||
Reference in New Issue
Block a user