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,65 +1,40 @@
|
||||
import { test, expect, Page } from './fixtures';
|
||||
|
||||
const PASSWORD = 'supersecret123';
|
||||
const uniqueEmail = () => `disable-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}@example.com`;
|
||||
|
||||
async function register(page: Page, email: string) {
|
||||
await page.goto('/register');
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
||||
await page.getByRole('textbox', { name: 'First name' }).fill('Test');
|
||||
await page.getByRole('textbox', { name: 'Last name' }).fill('Customer');
|
||||
await page.getByLabel('Password').fill(PASSWORD);
|
||||
await page.getByRole('button', { name: 'Create account' }).click();
|
||||
// Registering now closes the auth modal and returns to the page behind it, so
|
||||
// the header rather than the URL is what proves the session exists. The wait
|
||||
// is generous because this is a bcrypt round-trip rather than a render.
|
||||
await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible({ timeout: 20000 });
|
||||
}
|
||||
|
||||
async function customerRow(page: Page, email: string) {
|
||||
await page.goto('/admin');
|
||||
await page.getByRole('tab', { name: 'Customers' }).click();
|
||||
const row = page.getByRole('row').filter({ hasText: email });
|
||||
await expect(row).toBeVisible();
|
||||
return row;
|
||||
}
|
||||
import { test, expect, createItem, uniqueSuffix } from './fixtures';
|
||||
|
||||
test.describe('Disabling a customer account', () => {
|
||||
test('an admin can disable an account and the customer is told at sign-in', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await register(page, email);
|
||||
test('an admin can disable an account and the customer is told at sign-in', async ({
|
||||
page,
|
||||
customer,
|
||||
admin,
|
||||
adminCustomers,
|
||||
authModal
|
||||
}) => {
|
||||
await admin.open('Customers');
|
||||
await expect(adminCustomers.row(customer.email)).toBeVisible();
|
||||
await expect(adminCustomers.status(customer.email, 'ACTIVE')).toBeVisible();
|
||||
|
||||
const row = await customerRow(page, email);
|
||||
await expect(row.getByText('ACTIVE')).toBeVisible();
|
||||
|
||||
await row.getByRole('button', { name: 'Disable' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Disable' }).click();
|
||||
await adminCustomers.disable(customer.email);
|
||||
await expect(page.getByText('Account disabled')).toBeVisible();
|
||||
|
||||
const updated = page.getByRole('row').filter({ hasText: email });
|
||||
await expect(updated.getByText('DISABLED')).toBeVisible();
|
||||
await expect(adminCustomers.status(customer.email, 'DISABLED')).toBeVisible();
|
||||
|
||||
// A generic credential error would send a real customer round the
|
||||
// password-reset loop forever.
|
||||
await page.goto('/login');
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
||||
await page.getByLabel('Password').fill(PASSWORD);
|
||||
// Scoped to the modal: the storefront rendered behind it has a "Log in"
|
||||
// button of its own, which is what opened this one.
|
||||
await page.getByRole('dialog', { name: 'Log in' }).getByRole('button', { name: 'Log in' }).click();
|
||||
await authModal.gotoLogIn();
|
||||
await authModal.logIn(customer.email, customer.password);
|
||||
await expect(page.getByText(/disabled/i)).toBeVisible();
|
||||
});
|
||||
|
||||
test('an existing session stops working immediately', async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
await register(page, email);
|
||||
|
||||
test('an existing session stops working immediately', async ({
|
||||
page,
|
||||
request,
|
||||
customer,
|
||||
accountModal
|
||||
}) => {
|
||||
// Still signed in from registration, in this same browser context.
|
||||
await page.goto('/account');
|
||||
await expect(page.getByText(email)).toBeVisible();
|
||||
await accountModal.open();
|
||||
await expect(accountModal.emailText(customer.email)).toBeVisible();
|
||||
|
||||
const customers = await (await request.get('/api/admin/customers')).json();
|
||||
const id = customers.find((c: { email: string }) => c.email === email).id;
|
||||
const id = customers.find((c: { email: string }) => c.email === customer.email).id;
|
||||
await request.post(`/api/admin/customers/${id}/disable`);
|
||||
|
||||
// The cookie is unchanged, so this proves the server rejects it rather
|
||||
@@ -68,51 +43,49 @@ test.describe('Disabling a customer account', () => {
|
||||
await expect(page).toHaveURL(/\/login/);
|
||||
});
|
||||
|
||||
test('re-enabling restores sign-in', async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
await register(page, email);
|
||||
|
||||
test('re-enabling restores sign-in', async ({
|
||||
page,
|
||||
request,
|
||||
customer,
|
||||
admin,
|
||||
adminCustomers,
|
||||
authModal,
|
||||
header
|
||||
}) => {
|
||||
const customers = await (await request.get('/api/admin/customers')).json();
|
||||
const id = customers.find((c: { email: string }) => c.email === email).id;
|
||||
const id = customers.find((c: { email: string }) => c.email === customer.email).id;
|
||||
await request.post(`/api/admin/customers/${id}/disable`);
|
||||
|
||||
const row = await customerRow(page, email);
|
||||
await row.getByRole('button', { name: 'Re-enable' }).click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Re-enable' }).click();
|
||||
await admin.open('Customers');
|
||||
await expect(adminCustomers.row(customer.email)).toBeVisible();
|
||||
await adminCustomers.reEnable(customer.email);
|
||||
await expect(page.getByText('Account re-enabled')).toBeVisible();
|
||||
|
||||
await page.goto('/login');
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
||||
await page.getByLabel('Password').fill(PASSWORD);
|
||||
// Scoped to the modal: the storefront rendered behind it has a "Log in"
|
||||
// button of its own, which is what opened this one.
|
||||
await page.getByRole('dialog', { name: 'Log in' }).getByRole('button', { name: 'Log in' }).click();
|
||||
await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible({ timeout: 20000 });
|
||||
await authModal.gotoLogIn();
|
||||
await authModal.logIn(customer.email, customer.password);
|
||||
await header.waitForSignedIn();
|
||||
});
|
||||
|
||||
test('the confirmation warns that held items will be released', async ({ page, request }) => {
|
||||
const email = uniqueEmail();
|
||||
const itemName = `Held ${Date.now().toString(36)}`;
|
||||
test('the confirmation warns that held items will be released', async ({
|
||||
page,
|
||||
request,
|
||||
customer,
|
||||
admin,
|
||||
adminCustomers
|
||||
}) => {
|
||||
const item = await createItem(request, { name: `Held ${uniqueSuffix()}`, price: '40' });
|
||||
expect((await page.request.post(`/api/cart/items/${item.id}`)).status()).toBe(201);
|
||||
|
||||
const created = await request.post('/api/admin/items', {
|
||||
multipart: { name: itemName, description: '', price: '40', category_id: '', tags: '[]' }
|
||||
});
|
||||
const itemId = (await created.json()).id as number;
|
||||
// New items are pending, and a pending item cannot be added to a cart.
|
||||
expect((await request.post(`/api/admin/items/${itemId}/mark-available`)).ok()).toBeTruthy();
|
||||
|
||||
await register(page, email);
|
||||
expect((await page.request.post(`/api/cart/items/${itemId}`)).status()).toBe(201);
|
||||
|
||||
const row = await customerRow(page, email);
|
||||
await row.getByRole('button', { name: 'Disable' }).click();
|
||||
await admin.open('Customers');
|
||||
await expect(adminCustomers.row(customer.email)).toBeVisible();
|
||||
await adminCustomers.disableButton(customer.email).click();
|
||||
|
||||
// The consequence has to be visible at the moment of the decision.
|
||||
await expect(page.getByText(/1 reserved item/)).toBeVisible();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Disable' }).click();
|
||||
await adminCustomers.confirmDialog.getByRole('button', { name: 'Disable' }).click();
|
||||
await expect(page.getByText('Account disabled')).toBeVisible();
|
||||
|
||||
const item = await (await request.get(`/api/items/${itemId}`)).json();
|
||||
expect(item.status).toBe('available');
|
||||
const released = await (await request.get(`/api/items/${item.id}`)).json();
|
||||
expect(released.status).toBe('available');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user