test(e2e): convert the remaining admin specs, completing the POM refactor (#137)
The last nine: admin-taxonomy, admin-save-failures, admin-theme, admin-inline-category, admin-item-preview, admin-disable-customer, admin-reserved-items, admin-inventory-filters, email-templates and admin-email-settings. Every spec in tests/e2e now goes through page objects. Measured rather than asserted: - raw CSS and antd-internal locators in spec files: 0 (was 13) - local `register` helpers: 0 (was 9) - hardcoded http://localhost:5173: 0 (was 3) The antd knowledge that was spread across seven spec files is now in four page objects, each with the reason written next to it. Three of those are things no reader could have guessed from the locator: Segmented hides its real radio behind a styled label, so the input is found by role and cannot be clicked — the title attribute is the handle. The status multi-select renders an invisible role="listbox" shim beside the real list, so getByRole('option') finds something zero-sized; and a selected status renders again as a tag carrying the same title, so an unscoped getByTitle is ambiguous. Matching the visible option class avoids both. The dropdown is also opened only when closed, because antd keeps it open after a selection in multiple mode. Select popups render into a portal at the end of <body>, outside the tab panel they belong to, so a dropdown cannot be found by scoping to the panel. AdminPage.tab gained an `exact` flag for one specific collision: the Emails tab contains a rail that also renders tabs, and "Email verification" contains "Email". Without exact matching, opening the Emails tab is ambiguous with the template inside it. Two helpers stayed local rather than moving into page objects, because they belong to their file's subject rather than to a surface: favorites-filter's `favorite()`, which settles the opt-in modal and waits for its fade before the wrapper stops intercepting pointer events, and admin-theme's `luminance()`, which is a WCAG calculation and not a locator. Both now take page objects as parameters instead of reaching for locators themselves. Verified: 26/26 spec files converted, tsc clean over the whole tree, lint at the 30-warning src baseline with nothing added. Full suite 123 passed / 5 failed; all five pass in a 33/33 serial re-run, which is the load-related flakiness this suite has had throughout and not a change here — the backend hashes passwords with bcryptjs, a pure-JS implementation that blocks the event loop for every request while it runs. Closes #137
This commit is contained in:
@@ -1,44 +1,23 @@
|
||||
import { test, expect } from './fixtures';
|
||||
|
||||
const suffix = () => `t${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
||||
import { test, expect, uniqueSuffix } from './fixtures';
|
||||
|
||||
// Each test leaves the templates as it found them, because they are stored in
|
||||
// admin_settings and would otherwise change the copy a later test reads.
|
||||
async function restore(page: import('@playwright/test').Page, key: string) {
|
||||
await page.request.delete(`/api/admin/email-templates/${key}`);
|
||||
async function restore(request: import('@playwright/test').APIRequestContext, key: string) {
|
||||
await request.delete(`/api/admin/email-templates/${key}`);
|
||||
}
|
||||
|
||||
async function openEmails(page: import('@playwright/test').Page) {
|
||||
await page.goto('/admin');
|
||||
await page.getByRole('tab', { name: 'Emails' }).click();
|
||||
// The rail's first entry, rather than a heading — the tab label is the
|
||||
// heading now, so there is no second one inside the page to wait on.
|
||||
await expect(page.getByRole('tab', { name: /Email verification/ })).toBeVisible();
|
||||
}
|
||||
|
||||
// Opens one template's tab. Only the active tab's editor is mounted, which is
|
||||
// what makes the labels below unambiguous — the previous stacked layout had
|
||||
// every editor on screen at once and a locator for "Save" matched all six.
|
||||
async function openTemplate(page: import('@playwright/test').Page, label: string) {
|
||||
await page.getByRole('tab', { name: new RegExp(label) }).click();
|
||||
await expect(page.getByLabel(`${label} subject`)).toBeVisible();
|
||||
}
|
||||
|
||||
const previewFrame = (page: import('@playwright/test').Page, label: string) =>
|
||||
page.frameLocator(`iframe[title="${label} preview"]`);
|
||||
|
||||
// Serial: these edit one shared stored template, and the suite runs fully
|
||||
// parallel by default — so run concurrently they would race, one asserting a
|
||||
// template is unset while another has just saved it.
|
||||
test.describe.configure({ mode: 'serial' });
|
||||
|
||||
test.describe('Editing the customer emails', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await restore(page, 'passwordReset');
|
||||
test.afterEach(async ({ page, admin, adminEmails }) => {
|
||||
await restore(page.request, 'passwordReset');
|
||||
});
|
||||
|
||||
test('offers every template as a tab, marked default until it is edited', async ({ page }) => {
|
||||
await openEmails(page);
|
||||
test('offers every template as a tab, marked default until it is edited', async ({ page, admin, adminEmails }) => {
|
||||
await admin.open('Emails');
|
||||
|
||||
for (const label of [
|
||||
'Email verification',
|
||||
@@ -48,25 +27,25 @@ test.describe('Editing the customer emails', () => {
|
||||
'Cart reminder',
|
||||
'Email address changed'
|
||||
]) {
|
||||
await expect(page.getByRole('tab', { name: new RegExp(label) })).toBeVisible();
|
||||
await expect(adminEmails.railTab(new RegExp(label))).toBeVisible();
|
||||
}
|
||||
|
||||
// Only a customised template is marked, so which ones have been changed is
|
||||
// visible without opening each one. An untouched template carries nothing.
|
||||
await expect(page.getByRole('tab', { name: /Password reset/ })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: /Password reset.*Customised/ })).toHaveCount(0);
|
||||
await expect(adminEmails.railTab(/Password reset/)).toBeVisible();
|
||||
await expect(adminEmails.customisedTab('Password reset')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('saves a replacement subject and body', async ({ page }) => {
|
||||
const subject = `Reset ${suffix()}`;
|
||||
await openEmails(page);
|
||||
await openTemplate(page, 'Password reset');
|
||||
test('saves a replacement subject and body', async ({ page, admin, adminEmails }) => {
|
||||
const subject = `Reset ${uniqueSuffix()}`;
|
||||
await admin.open('Emails');
|
||||
await adminEmails.openTemplate('Password reset');
|
||||
|
||||
await page.getByLabel('Password reset subject').fill(subject);
|
||||
await adminEmails.subject('Password reset').fill(subject);
|
||||
await page
|
||||
.getByLabel('Password reset body')
|
||||
.fill('Fresh wording. [Choose a new password]({{resetUrl}}).');
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await adminEmails.saveButton.click();
|
||||
|
||||
await expect(page.getByText('Password reset saved')).toBeVisible();
|
||||
|
||||
@@ -79,12 +58,12 @@ test.describe('Editing the customer emails', () => {
|
||||
// The assertion that matters. A body without its link still sends and still
|
||||
// looks fine in the log, so the save has to be refused rather than warned
|
||||
// about — and the admin has to be told which placeholder is missing.
|
||||
test('refuses a body that drops the required placeholder, and says which', async ({ page }) => {
|
||||
await openEmails(page);
|
||||
await openTemplate(page, 'Password reset');
|
||||
test('refuses a body that drops the required placeholder, and says which', async ({ page, admin, adminEmails }) => {
|
||||
await admin.open('Emails');
|
||||
await adminEmails.openTemplate('Password reset');
|
||||
|
||||
await page.getByLabel('Password reset body').fill('Just click the thing in your email.');
|
||||
await page.getByRole('button', { name: 'Save', exact: true }).click();
|
||||
await adminEmails.body('Password reset').fill('Just click the thing in your email.');
|
||||
await adminEmails.saveButton.click();
|
||||
|
||||
await expect(page.getByText('the body must keep {{resetUrl}}')).toBeVisible();
|
||||
|
||||
@@ -94,17 +73,17 @@ test.describe('Editing the customer emails', () => {
|
||||
expect(reset.body).toBeNull();
|
||||
});
|
||||
|
||||
test('restores the built-in copy', async ({ page }) => {
|
||||
test('restores the built-in copy', async ({ page, admin, adminEmails }) => {
|
||||
await page.request.put('/api/admin/email-templates/passwordReset', {
|
||||
data: { subject: 'Temporary', body: 'Temporary [link]({{resetUrl}}).' }
|
||||
});
|
||||
|
||||
await openEmails(page);
|
||||
await admin.open('Emails');
|
||||
// Marked in the rail before it is opened, which is the whole point of the
|
||||
// dot — the stored template above was never touched through the UI.
|
||||
await expect(page.getByRole('tab', { name: /Password reset.*Customised/ })).toBeVisible();
|
||||
await openTemplate(page, 'Password reset');
|
||||
await page.getByRole('button', { name: 'Restore default' }).click();
|
||||
await expect(adminEmails.customisedTab('Password reset')).toBeVisible();
|
||||
await adminEmails.openTemplate('Password reset');
|
||||
await adminEmails.restoreDefaultButton.click();
|
||||
|
||||
await expect(page.getByText('Password reset restored to the default')).toBeVisible();
|
||||
|
||||
@@ -116,14 +95,14 @@ test.describe('Editing the customer emails', () => {
|
||||
});
|
||||
|
||||
test.describe('Previewing the customer emails', () => {
|
||||
test.afterEach(async ({ page }) => {
|
||||
await restore(page, 'passwordReset');
|
||||
test.afterEach(async ({ page, admin, adminEmails }) => {
|
||||
await restore(page.request, 'passwordReset');
|
||||
});
|
||||
|
||||
test('shows the draft being edited, not the stored copy', async ({ page }) => {
|
||||
const wording = `Wording ${suffix()}`;
|
||||
await openEmails(page);
|
||||
await openTemplate(page, 'Password reset');
|
||||
test('shows the draft being edited, not the stored copy', async ({ page, admin, adminEmails }) => {
|
||||
const wording = `Wording ${uniqueSuffix()}`;
|
||||
await admin.open('Emails');
|
||||
await adminEmails.openTemplate('Password reset');
|
||||
|
||||
await page
|
||||
.getByLabel('Password reset body')
|
||||
@@ -131,17 +110,17 @@ test.describe('Previewing the customer emails', () => {
|
||||
|
||||
// Nothing has been saved. The preview still reflects it, which is the whole
|
||||
// point: an admin sees the effect before committing to it.
|
||||
await expect(previewFrame(page, 'Password reset').getByText(wording)).toBeVisible();
|
||||
await expect(adminEmails.preview('Password reset').getByText(wording)).toBeVisible();
|
||||
|
||||
const stored = await (await page.request.get('/api/admin/email-templates')).json();
|
||||
expect(stored.find((t: { key: string }) => t.key === 'passwordReset').body).toBeNull();
|
||||
});
|
||||
|
||||
test('substitutes sample values rather than showing raw placeholders', async ({ page }) => {
|
||||
await openEmails(page);
|
||||
await openTemplate(page, 'Password reset');
|
||||
test('substitutes sample values rather than showing raw placeholders', async ({ page, admin, adminEmails }) => {
|
||||
await admin.open('Emails');
|
||||
await adminEmails.openTemplate('Password reset');
|
||||
|
||||
const frame = previewFrame(page, 'Password reset');
|
||||
const frame = adminEmails.preview('Password reset');
|
||||
await expect(frame.getByRole('link')).toHaveAttribute('href', /reset-password\?token=/);
|
||||
await expect(frame.locator('body')).not.toContainText('{{resetUrl}}');
|
||||
});
|
||||
@@ -149,26 +128,26 @@ test.describe('Previewing the customer emails', () => {
|
||||
// The control that stops an admin putting script into a customer's inbox is
|
||||
// markdown-it's html: false on the server. The preview has to show the same
|
||||
// thing the mailer emits, or it would be reassuring about the wrong output.
|
||||
test('escapes raw HTML exactly as the mailer does', async ({ page }) => {
|
||||
await openEmails(page);
|
||||
await openTemplate(page, 'Password reset');
|
||||
test('escapes raw HTML exactly as the mailer does', async ({ page, admin, adminEmails }) => {
|
||||
await admin.open('Emails');
|
||||
await adminEmails.openTemplate('Password reset');
|
||||
|
||||
await page
|
||||
.getByLabel('Password reset body')
|
||||
.fill('<script>alert(1)</script> [link]({{resetUrl}})');
|
||||
|
||||
await expect(previewFrame(page, 'Password reset').getByText('<script>alert(1)</script>')).toBeVisible();
|
||||
await expect(adminEmails.preview('Password reset').getByText('<script>alert(1)</script>')).toBeVisible();
|
||||
});
|
||||
|
||||
// Appended by the server and not editable, so it has to appear in the preview
|
||||
// of the two templates it belongs to and nowhere else.
|
||||
test('includes the consent footer on a favorite template, and not on others', async ({ page }) => {
|
||||
await openEmails(page);
|
||||
test('includes the consent footer on a favorite template, and not on others', async ({ page, admin, adminEmails }) => {
|
||||
await admin.open('Emails');
|
||||
|
||||
await openTemplate(page, 'Favorited item sold');
|
||||
await expect(previewFrame(page, 'Favorited item sold').getByText(/account page/)).toBeVisible();
|
||||
await adminEmails.openTemplate('Favorited item sold');
|
||||
await expect(adminEmails.preview('Favorited item sold').getByText(/account page/)).toBeVisible();
|
||||
|
||||
await openTemplate(page, 'Password reset');
|
||||
await expect(previewFrame(page, 'Password reset').locator('body')).not.toContainText('account page');
|
||||
await adminEmails.openTemplate('Password reset');
|
||||
await expect(adminEmails.preview('Password reset').locator('body')).not.toContainText('account page');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user