test(e2e): convert the favorites, availability and orders specs (#137)
favorites, favorites-filter, sold-filter, orders and pending-publish. Five more copies of "register a customer" and three more of the hardcoded base URL go with them. Two locators that were hiding real knowledge are now named. `gridCell` is the item's whole antd column rather than its card, needed because the SOLD ribbon renders outside the card — three specs reached for `.ant-col` directly to get at it. And `chooseAvailability` goes through the title attribute because antd's Segmented hides the real radio behind a styled label, so the input is found by role and cannot be clicked; that fact was written out twice in comments and is now written once in code. The favorite control is located page-wide rather than within a card. The storefront paginates as items accumulate and the control is named for its item anyway, so scoping to a card bought nothing and broke whenever the card was on another page. favorites-filter keeps its local `favorite()` helper. It is genuinely local — decline the opt-in, wait for the fading modal to stop intercepting pointer events, confirm the heart flipped — and belongs to that file's subject rather than to the storefront. It now takes page objects as parameters instead of reaching for locators itself, which is what a spec-level helper should look like. Two specs still drive the registration form rather than taking the `customer` fixture, and deliberately. Both are about a signed-out visitor being interrupted mid-action — favoriting an item, or switching on the favorites filter — and the claim is that the thing they asked for survives the interruption. Replacing the interruption with an API call would delete the test. Verified: favorites 6/6, favorites-filter 7/7, sold-filter 6/6, orders and pending-publish 8/8. Notably favorites-filter's "keeps showing a favorite after it sells" passes, which had been failing on a strict-mode violation from two items sharing a name across runs. Refs #137
This commit is contained in:
@@ -1,83 +1,55 @@
|
||||
import { test, expect, Page } from './fixtures';
|
||||
import { test, expect, createAdminContext, createItem, uniqueSuffix, uniqueEmail, PASSWORD } from './fixtures';
|
||||
|
||||
const PASSWORD = 'supersecret123';
|
||||
const RUN = `f${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
||||
const ITEM = `Favoritable ${RUN}`;
|
||||
|
||||
const uniqueEmail = () => `fav-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}@example.com`;
|
||||
const ITEM = `Favoritable ${uniqueSuffix()}`;
|
||||
|
||||
test.beforeAll(async ({ playwright }) => {
|
||||
const api = await playwright.request.newContext({ baseURL: 'http://localhost:5173' });
|
||||
const res = await api.post('/api/admin/items', {
|
||||
multipart: { name: ITEM, description: '', price: '60', category_id: '', tags: '[]' }
|
||||
});
|
||||
expect(res.ok()).toBeTruthy();
|
||||
// New items are pending; the storefront only lists published ones.
|
||||
expect((await api.post(`/api/admin/items/${(await res.json()).id}/mark-available`)).ok()).toBeTruthy();
|
||||
const api = await createAdminContext(playwright);
|
||||
await createItem(api, { name: ITEM, price: '60' });
|
||||
await api.dispose();
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
// Going to the storefront remounts the app, so the session is briefly still
|
||||
// resolving. The heart deliberately ignores clicks in that window rather than
|
||||
// wrongly prompting a signed-in customer to sign in, so wait for the header to
|
||||
// show the account link — which is exactly what a real customer sees settle.
|
||||
async function gotoStorefrontSignedIn(page: Page) {
|
||||
await page.goto('/');
|
||||
await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible();
|
||||
}
|
||||
|
||||
// The storefront paginates as items accumulate, so find the card by name.
|
||||
function heart(page: Page, itemName: string) {
|
||||
return page.getByRole('button', { name: new RegExp(`(Add|Remove) ${itemName}`) });
|
||||
}
|
||||
|
||||
test.describe('Favoriting items', () => {
|
||||
test('a signed-out visitor is prompted to sign in, and the favorite completes', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await heart(page, ITEM).first().click();
|
||||
// Drives the registration form rather than taking the `customer` fixture:
|
||||
// the claim here is that the favorite a signed-out visitor asked for survives
|
||||
// being interrupted by signing up, so the interruption has to be real.
|
||||
test('a signed-out visitor is prompted to sign in, and the favorite completes', async ({
|
||||
storefront,
|
||||
authModal
|
||||
}) => {
|
||||
await storefront.goto();
|
||||
await storefront.favoriteToggle(ITEM).first().click();
|
||||
|
||||
// Same inline prompt Add to Cart already uses.
|
||||
await expect(page.getByRole('dialog')).toBeVisible();
|
||||
await expect(authModal.registerDialog).toBeVisible();
|
||||
|
||||
const email = uniqueEmail();
|
||||
await page.getByRole('dialog').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.getByRole('dialog').getByLabel('Password').fill(PASSWORD);
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'Create account' }).click();
|
||||
await authModal.fillRegistration({
|
||||
email: uniqueEmail('fav'),
|
||||
password: PASSWORD,
|
||||
firstName: 'Test',
|
||||
lastName: 'Customer'
|
||||
});
|
||||
await authModal.submitRegistration();
|
||||
|
||||
// The favorite the visitor originally asked for is applied on success.
|
||||
await expect(page.getByRole('button', { name: `Remove ${ITEM} from favorites` })).toBeVisible();
|
||||
await expect(storefront.removeFromFavoritesButton(ITEM)).toBeVisible({ timeout: 20000 });
|
||||
});
|
||||
|
||||
test('offers the alert opt-in with a reason, and it is not the marketing consent', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await register(page, email);
|
||||
test('offers the alert opt-in with a reason, and it is not the marketing consent', async ({
|
||||
page,
|
||||
customer,
|
||||
storefront,
|
||||
favoritePrompt
|
||||
}) => {
|
||||
await storefront.gotoSignedIn();
|
||||
await storefront.favoriteToggle(ITEM).first().click();
|
||||
|
||||
await gotoStorefrontSignedIn(page);
|
||||
await heart(page, ITEM).first().click();
|
||||
|
||||
const prompt = page.getByRole('dialog');
|
||||
await expect(prompt).toBeVisible();
|
||||
await expect(prompt).toContainText('Want to know if this sells?');
|
||||
await expect(favoritePrompt.dialog).toBeVisible();
|
||||
await expect(favoritePrompt.dialog).toContainText('Want to know if this sells?');
|
||||
// The reason for asking has to be given, not just the ask.
|
||||
await expect(prompt).toContainText(/one of a kind/i);
|
||||
await expect(prompt).toContainText(/separate from any marketing/i);
|
||||
await expect(favoritePrompt.dialog).toContainText(/one of a kind/i);
|
||||
await expect(favoritePrompt.dialog).toContainText(/separate from any marketing/i);
|
||||
|
||||
await prompt.getByRole('button', { name: 'Yes, email me' }).click();
|
||||
await favoritePrompt.acceptAlerts();
|
||||
await expect(page.getByText('We will let you know')).toBeVisible();
|
||||
|
||||
const me = await (await page.request.get('/api/customers/me')).json();
|
||||
@@ -86,15 +58,17 @@ test.describe('Favoriting items', () => {
|
||||
expect(me.marketing_consent).toBe(false);
|
||||
});
|
||||
|
||||
test('declining the opt-in still keeps the favorite', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await register(page, email);
|
||||
test('declining the opt-in still keeps the favorite', async ({
|
||||
page,
|
||||
customer,
|
||||
storefront,
|
||||
favoritePrompt
|
||||
}) => {
|
||||
await storefront.gotoSignedIn();
|
||||
await storefront.favoriteToggle(ITEM).first().click();
|
||||
await favoritePrompt.declineAlerts();
|
||||
|
||||
await gotoStorefrontSignedIn(page);
|
||||
await heart(page, ITEM).first().click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'No thanks' }).click();
|
||||
|
||||
await expect(page.getByRole('button', { name: `Remove ${ITEM} from favorites` })).toBeVisible();
|
||||
await expect(storefront.removeFromFavoritesButton(ITEM)).toBeVisible();
|
||||
|
||||
const favorites = await (await page.request.get('/api/customers/me/favorites')).json();
|
||||
expect(favorites).toHaveLength(1);
|
||||
@@ -103,49 +77,39 @@ test.describe('Favoriting items', () => {
|
||||
expect(me.favorite_alerts).toBe(false);
|
||||
});
|
||||
|
||||
test('unfavoriting removes it', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await register(page, email);
|
||||
test('unfavoriting removes it', async ({ page, customer, storefront, favoritePrompt }) => {
|
||||
await storefront.gotoSignedIn();
|
||||
await storefront.favoriteToggle(ITEM).first().click();
|
||||
await favoritePrompt.declineAlerts();
|
||||
await expect(storefront.removeFromFavoritesButton(ITEM)).toBeVisible();
|
||||
|
||||
await gotoStorefrontSignedIn(page);
|
||||
await heart(page, ITEM).first().click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'No thanks' }).click();
|
||||
await expect(page.getByRole('button', { name: `Remove ${ITEM} from favorites` })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: `Remove ${ITEM} from favorites` }).click();
|
||||
await expect(page.getByRole('button', { name: `Add ${ITEM} to favorites` })).toBeVisible();
|
||||
await storefront.removeFromFavoritesButton(ITEM).click();
|
||||
await expect(storefront.addToFavoritesButton(ITEM)).toBeVisible();
|
||||
|
||||
const favorites = await (await page.request.get('/api/customers/me/favorites')).json();
|
||||
expect(favorites).toEqual([]);
|
||||
});
|
||||
|
||||
test('favorites survive a reload', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await register(page, email);
|
||||
|
||||
await gotoStorefrontSignedIn(page);
|
||||
await heart(page, ITEM).first().click();
|
||||
await page.getByRole('dialog').getByRole('button', { name: 'No thanks' }).click();
|
||||
await expect(page.getByRole('button', { name: `Remove ${ITEM} from favorites` })).toBeVisible();
|
||||
test('favorites survive a reload', async ({ page, customer, storefront, favoritePrompt }) => {
|
||||
await storefront.gotoSignedIn();
|
||||
await storefront.favoriteToggle(ITEM).first().click();
|
||||
await favoritePrompt.declineAlerts();
|
||||
await expect(storefront.removeFromFavoritesButton(ITEM)).toBeVisible();
|
||||
|
||||
// Proves it was stored server-side rather than held in component state.
|
||||
await page.reload();
|
||||
await expect(page.getByRole('button', { name: `Remove ${ITEM} from favorites` })).toBeVisible();
|
||||
await expect(storefront.removeFromFavoritesButton(ITEM)).toBeVisible();
|
||||
});
|
||||
|
||||
test('the account page can turn the alerts off again', async ({ page }) => {
|
||||
const email = uniqueEmail();
|
||||
await register(page, email);
|
||||
test('the account page can turn the alerts off again', async ({ page, customer, accountModal }) => {
|
||||
await page.request.put('/api/customers/me/favorite-alerts', { data: { enabled: true } });
|
||||
|
||||
await page.goto('/account');
|
||||
// Scoped to the account modal. The storefront renders behind it and has a
|
||||
// theme switch of its own, so an unscoped switch locator only picks the
|
||||
// right control by DOM accident.
|
||||
const account = page.getByRole('dialog', { name: 'My Account' });
|
||||
await expect(account.getByText('Email me when an item I favorited is sold')).toBeVisible();
|
||||
await accountModal.open();
|
||||
await expect(
|
||||
accountModal.dialog.getByText('Email me when an item I favorited is sold')
|
||||
).toBeVisible();
|
||||
|
||||
await account.getByRole('switch').last().click();
|
||||
await accountModal.favoriteAlertsSwitch.click();
|
||||
await expect(page.getByText('Turned off')).toBeVisible();
|
||||
|
||||
const me = await (await page.request.get('/api/customers/me')).json();
|
||||
|
||||
Reference in New Issue
Block a user