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,180 +1,192 @@
|
|||||||
import { test, expect, Page } from './fixtures';
|
import {
|
||||||
|
test,
|
||||||
|
expect,
|
||||||
|
createAdminContext,
|
||||||
|
createItem,
|
||||||
|
sellItem,
|
||||||
|
uniqueSuffix,
|
||||||
|
uniqueEmail,
|
||||||
|
PASSWORD,
|
||||||
|
StorefrontPage,
|
||||||
|
FavoritePrompt
|
||||||
|
} from './fixtures';
|
||||||
|
|
||||||
const PASSWORD = 'supersecret123';
|
|
||||||
// The storefront runs against a shared database that is never reset, so every
|
// The storefront runs against a shared database that is never reset, so every
|
||||||
// name has to be unique to this run or a rerun would match the last one's rows.
|
// name has to be unique to this run or a rerun would match the last one's rows.
|
||||||
const RUN = `ff${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
const RUN = `ff${uniqueSuffix()}`;
|
||||||
const KEPT = `Kept ${RUN}`;
|
const KEPT = `Kept ${RUN}`;
|
||||||
const OTHER = `Other ${RUN}`;
|
const OTHER = `Other ${RUN}`;
|
||||||
// Its own item because this run marks it sold, and the suite is fullyParallel:
|
// Its own item because this run marks it sold, and the suite is fullyParallel:
|
||||||
// mutating an item the other tests read would make them race.
|
// mutating an item the other tests read would make them race.
|
||||||
const SELLS = `Sells ${RUN}`;
|
const SELLS = `Sells ${RUN}`;
|
||||||
|
|
||||||
const uniqueEmail = () => `favfilter-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}@example.com`;
|
|
||||||
|
|
||||||
test.beforeAll(async ({ playwright }) => {
|
test.beforeAll(async ({ playwright }) => {
|
||||||
const api = await playwright.request.newContext({ baseURL: 'http://localhost:5173' });
|
const api = await createAdminContext(playwright);
|
||||||
// Two items, priced apart so one test can prove favorites combines with the
|
// Priced apart so one test can prove favorites combines with the price filter
|
||||||
// price filter rather than replacing it.
|
// rather than replacing it.
|
||||||
for (const [name, price] of [[KEPT, '60'], [OTHER, '900'], [SELLS, '70']] as const) {
|
await createItem(api, { name: KEPT, price: '60' });
|
||||||
const res = await api.post('/api/admin/items', {
|
await createItem(api, { name: OTHER, price: '900' });
|
||||||
multipart: { name, description: '', price, category_id: '', tags: '[]' }
|
await createItem(api, { name: SELLS, price: '70' });
|
||||||
});
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
await api.dispose();
|
await api.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
async function register(page: Page) {
|
/**
|
||||||
await page.goto('/register');
|
* Favorites an item and settles the opt-in modal that follows.
|
||||||
await page.getByRole('textbox', { name: 'Email' }).fill(uniqueEmail());
|
*
|
||||||
await page.getByRole('textbox', { name: 'First name' }).fill('Test');
|
* The alert opt-in is offered on every favorite until it is accepted, so it is
|
||||||
await page.getByRole('textbox', { name: 'Last name' }).fill('Customer');
|
* always there to decline. Clicked rather than probed with isVisible(): that
|
||||||
await page.getByLabel('Password').fill(PASSWORD);
|
* check does not wait, so it loses the race with the modal appearing and leaves
|
||||||
await page.getByRole('button', { name: 'Create account' }).click();
|
* it open to block everything the test does next. Its wrapper also goes on
|
||||||
// Registering now closes the auth modal and returns to the page behind it, so
|
* intercepting pointer events while it fades out, hence waiting for it to go.
|
||||||
// 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.
|
async function favorite(
|
||||||
await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible({ timeout: 20000 });
|
storefront: StorefrontPage,
|
||||||
|
favoritePrompt: FavoritePrompt,
|
||||||
|
itemName: string
|
||||||
|
) {
|
||||||
|
await storefront.addToFavoritesButton(itemName).click();
|
||||||
|
await favoritePrompt.declineAlerts();
|
||||||
|
await expect(favoritePrompt.declineAlertsButton).toBeHidden();
|
||||||
|
await expect(storefront.removeFromFavoritesButton(itemName)).toBeVisible();
|
||||||
}
|
}
|
||||||
|
|
||||||
// The session is still resolving for a moment after a remount, and the
|
|
||||||
// favorites filter deliberately waits it out rather than guessing. Waiting for
|
|
||||||
// the account link is what a real customer sees settle.
|
|
||||||
async function gotoStorefrontSignedIn(page: Page) {
|
|
||||||
await page.goto('/');
|
|
||||||
await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function favorite(page: Page, itemName: string) {
|
|
||||||
await page.getByRole('button', { name: `Add ${itemName} to favorites` }).click();
|
|
||||||
// The alert opt-in is offered on every favorite until it is accepted, so it
|
|
||||||
// is always there to decline. Clicked rather than probed with isVisible():
|
|
||||||
// that check does not wait, so it loses the race with the modal appearing and
|
|
||||||
// leaves it open to block everything the test does next.
|
|
||||||
const decline = page.getByRole('dialog').getByRole('button', { name: 'No thanks' });
|
|
||||||
await decline.click();
|
|
||||||
// Its wrapper goes on intercepting pointer events while it fades out.
|
|
||||||
await expect(decline).toBeHidden();
|
|
||||||
await expect(page.getByRole('button', { name: `Remove ${itemName} from favorites` })).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openFilters(page: Page) {
|
|
||||||
await page.getByRole('button', { name: /Filters/ }).click();
|
|
||||||
await expect(favoritesSwitch(page)).toBeVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
const favoritesSwitch = (page: Page) => page.getByRole('switch', { name: 'Only my favorites' });
|
|
||||||
|
|
||||||
test.describe('Filtering the storefront by favorites', () => {
|
test.describe('Filtering the storefront by favorites', () => {
|
||||||
test('narrows the grid to favorited items and puts it in the URL', async ({ page }) => {
|
test('narrows the grid to favorited items and puts it in the URL', async ({
|
||||||
await register(page);
|
page,
|
||||||
await gotoStorefrontSignedIn(page);
|
customer,
|
||||||
await favorite(page, KEPT);
|
storefront,
|
||||||
|
filterDrawer,
|
||||||
|
favoritePrompt
|
||||||
|
}) => {
|
||||||
|
await storefront.gotoSignedIn();
|
||||||
|
await favorite(storefront, favoritePrompt, KEPT);
|
||||||
|
|
||||||
await openFilters(page);
|
await storefront.openFilters();
|
||||||
await favoritesSwitch(page).click();
|
await filterDrawer.waitForOpen();
|
||||||
|
await filterDrawer.favoritesOnlySwitch.click();
|
||||||
// Close the drawer before reading the grid behind it, as the other filter
|
// Close the drawer before reading the grid behind it, as the other filter
|
||||||
// tests do.
|
// tests do.
|
||||||
await page.getByRole('button', { name: 'Close' }).click();
|
await filterDrawer.close();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: `Remove ${KEPT} from favorites` })).toBeVisible();
|
await expect(storefront.removeFromFavoritesButton(KEPT)).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: `Add ${OTHER} to favorites` })).toBeHidden();
|
await expect(storefront.addToFavoritesButton(OTHER)).toBeHidden();
|
||||||
// In the URL so the view can be linked, bookmarked, and reloaded.
|
// In the URL so the view can be linked, bookmarked, and reloaded.
|
||||||
await expect(page).toHaveURL(/favorites=1/);
|
await expect(page).toHaveURL(/favorites=1/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('survives a reload, since the URL is the source of truth', async ({ page }) => {
|
test('survives a reload, since the URL is the source of truth', async ({
|
||||||
await register(page);
|
page,
|
||||||
await gotoStorefrontSignedIn(page);
|
customer,
|
||||||
await favorite(page, KEPT);
|
storefront,
|
||||||
|
favoritePrompt
|
||||||
|
}) => {
|
||||||
|
await storefront.gotoSignedIn();
|
||||||
|
await favorite(storefront, favoritePrompt, KEPT);
|
||||||
|
|
||||||
await page.goto('/?favorites=1');
|
await page.goto('/?favorites=1');
|
||||||
await expect(page.getByRole('button', { name: `Remove ${KEPT} from favorites` })).toBeVisible();
|
await expect(storefront.removeFromFavoritesButton(KEPT)).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: `Add ${OTHER} to favorites` })).toBeHidden();
|
await expect(storefront.addToFavoritesButton(OTHER)).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows a removable chip that restores the full catalogue', async ({ page }) => {
|
test('shows a removable chip that restores the full catalogue', async ({
|
||||||
await register(page);
|
page,
|
||||||
await gotoStorefrontSignedIn(page);
|
customer,
|
||||||
await favorite(page, KEPT);
|
storefront,
|
||||||
|
favoritePrompt
|
||||||
|
}) => {
|
||||||
|
await storefront.gotoSignedIn();
|
||||||
|
await favorite(storefront, favoritePrompt, KEPT);
|
||||||
|
|
||||||
await page.goto('/?favorites=1');
|
await page.goto('/?favorites=1');
|
||||||
await expect(page.getByRole('button', { name: `Add ${OTHER} to favorites` })).toBeHidden();
|
await expect(storefront.addToFavoritesButton(OTHER)).toBeHidden();
|
||||||
|
|
||||||
const chips = page.getByRole('group', { name: 'Active filters' });
|
await expect(storefront.activeFilters).toContainText('My favorites');
|
||||||
await expect(chips).toContainText('My favorites');
|
await storefront.removeFilterChip('My favorites').click();
|
||||||
await chips.getByRole('button', { name: 'Remove filter My favorites' }).click();
|
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: `Add ${OTHER} to favorites` })).toBeVisible();
|
await expect(storefront.addToFavoritesButton(OTHER)).toBeVisible();
|
||||||
await expect(page).not.toHaveURL(/favorites/);
|
await expect(page).not.toHaveURL(/favorites/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('combines with the price filter rather than replacing it', async ({ page }) => {
|
test('combines with the price filter rather than replacing it', async ({
|
||||||
await register(page);
|
page,
|
||||||
await gotoStorefrontSignedIn(page);
|
customer,
|
||||||
await favorite(page, KEPT);
|
storefront,
|
||||||
await favorite(page, OTHER);
|
favoritePrompt
|
||||||
|
}) => {
|
||||||
|
await storefront.gotoSignedIn();
|
||||||
|
await favorite(storefront, favoritePrompt, KEPT);
|
||||||
|
await favorite(storefront, favoritePrompt, OTHER);
|
||||||
|
|
||||||
// Both are favorited; only one is under the price cap.
|
// Both are favorited; only one is under the price cap.
|
||||||
await page.goto('/?favorites=1&max_price=50000');
|
await page.goto('/?favorites=1&max_price=50000');
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: `Remove ${KEPT} from favorites` })).toBeVisible();
|
await expect(storefront.removeFromFavoritesButton(KEPT)).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: `Remove ${OTHER} from favorites` })).toBeHidden();
|
await expect(storefront.removeFromFavoritesButton(OTHER)).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prompts a signed-out visitor to sign in, then applies the filter', async ({ page }) => {
|
test('prompts a signed-out visitor to sign in, then applies the filter', async ({
|
||||||
await page.goto('/');
|
page,
|
||||||
await openFilters(page);
|
storefront,
|
||||||
await favoritesSwitch(page).click();
|
filterDrawer,
|
||||||
|
authModal
|
||||||
|
}) => {
|
||||||
|
await storefront.goto();
|
||||||
|
await storefront.openFilters();
|
||||||
|
await filterDrawer.waitForOpen();
|
||||||
|
await filterDrawer.favoritesOnlySwitch.click();
|
||||||
|
|
||||||
// The same inline prompt the heart and Add to Cart use, rather than an
|
// The same inline prompt the heart and Add to Cart use, rather than an
|
||||||
// empty grid implying the visitor has no favorites.
|
// empty grid implying the visitor has no favorites.
|
||||||
const prompt = page.getByRole('dialog', { name: /Create an account/ });
|
await expect(authModal.registerDialog).toBeVisible();
|
||||||
await expect(prompt).toBeVisible();
|
|
||||||
|
|
||||||
await prompt.getByRole('textbox', { name: 'Email' }).fill(uniqueEmail());
|
await authModal.fillRegistration({
|
||||||
await page.getByRole('textbox', { name: 'First name' }).fill('Test');
|
email: uniqueEmail('favfilter'),
|
||||||
await page.getByRole('textbox', { name: 'Last name' }).fill('Customer');
|
password: PASSWORD,
|
||||||
await prompt.getByLabel('Password').fill(PASSWORD);
|
firstName: 'Test',
|
||||||
await prompt.getByRole('button', { name: 'Create account' }).click();
|
lastName: 'Customer'
|
||||||
|
});
|
||||||
|
await authModal.submitRegistration();
|
||||||
|
|
||||||
// Signing in resolves the gate and the filter applies on its own — the
|
// Signing in resolves the gate and the filter applies on its own — the
|
||||||
// customer never sets it twice. A brand-new account has no favorites yet.
|
// customer never sets it twice. A brand-new account has no favorites yet.
|
||||||
await expect(page.getByText('No items match these filters')).toBeVisible();
|
await expect(storefront.noMatchesNotice).toBeVisible({ timeout: 20000 });
|
||||||
await expect(page).toHaveURL(/favorites=1/);
|
await expect(page).toHaveURL(/favorites=1/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('explains itself when a favorites link is opened without a session', async ({ page }) => {
|
test('explains itself when a favorites link is opened without a session', async ({
|
||||||
|
page,
|
||||||
|
storefront
|
||||||
|
}) => {
|
||||||
// A bookmarked filtered view whose session has since expired. The grid must
|
// A bookmarked filtered view whose session has since expired. The grid must
|
||||||
// not claim there are no matching items, which would read as "you have no
|
// not claim there are no matching items, which would read as "you have no
|
||||||
// favorites" rather than "we do not know who you are".
|
// favorites" rather than "we do not know who you are".
|
||||||
await page.goto('/?favorites=1');
|
await page.goto('/?favorites=1');
|
||||||
|
|
||||||
await expect(page.getByText('Sign in to see the items you have favorited')).toBeVisible();
|
await expect(storefront.favoritesNeedSignInNotice).toBeVisible();
|
||||||
await expect(page.getByText('No items match these filters')).toBeHidden();
|
await expect(storefront.noMatchesNotice).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keeps showing a favorite after it sells', async ({ page, playwright }) => {
|
test('keeps showing a favorite after it sells', async ({
|
||||||
await register(page);
|
page,
|
||||||
await gotoStorefrontSignedIn(page);
|
playwright,
|
||||||
await favorite(page, SELLS);
|
customer,
|
||||||
|
storefront,
|
||||||
|
favoritePrompt
|
||||||
|
}) => {
|
||||||
|
await storefront.gotoSignedIn();
|
||||||
|
await favorite(storefront, favoritePrompt, SELLS);
|
||||||
|
|
||||||
const api = await playwright.request.newContext({ baseURL: 'http://localhost:5173' });
|
const api = await createAdminContext(playwright);
|
||||||
const items = await (await api.get('/api/items')).json();
|
const items = await (await api.get('/api/items')).json();
|
||||||
const sells = items.find((item: { name: string }) => item.name === SELLS);
|
const sells = items.find((item: { name: string }) => item.name === SELLS);
|
||||||
expect(await (await api.post(`/api/admin/items/${sells.id}/mark-sold`)).ok()).toBeTruthy();
|
await sellItem(api, sells.id);
|
||||||
await api.dispose();
|
await api.dispose();
|
||||||
|
|
||||||
await page.goto('/?favorites=1');
|
await page.goto('/?favorites=1');
|
||||||
|
|
||||||
// Hiding it would make an item the customer curated vanish without
|
// Hiding it would make an item the customer curated vanish without
|
||||||
// explanation, right after they were emailed to say it had sold.
|
// explanation, right after they were emailed to say it had sold.
|
||||||
await expect(page.getByRole('button', { name: `Remove ${SELLS} from favorites` })).toBeVisible();
|
await expect(storefront.removeFromFavoritesButton(SELLS)).toBeVisible();
|
||||||
// Scoped to this item's cell: the ribbon sits outside the card, and other
|
await expect(storefront.gridCell(SELLS)).toContainText('SOLD');
|
||||||
// sold items from earlier runs are on the same page.
|
|
||||||
await expect(page.locator('.ant-col').filter({ hasText: SELLS })).toContainText('SOLD');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,83 +1,55 @@
|
|||||||
import { test, expect, Page } from './fixtures';
|
import { test, expect, createAdminContext, createItem, uniqueSuffix, uniqueEmail, PASSWORD } from './fixtures';
|
||||||
|
|
||||||
const PASSWORD = 'supersecret123';
|
const ITEM = `Favoritable ${uniqueSuffix()}`;
|
||||||
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`;
|
|
||||||
|
|
||||||
test.beforeAll(async ({ playwright }) => {
|
test.beforeAll(async ({ playwright }) => {
|
||||||
const api = await playwright.request.newContext({ baseURL: 'http://localhost:5173' });
|
const api = await createAdminContext(playwright);
|
||||||
const res = await api.post('/api/admin/items', {
|
await createItem(api, { name: ITEM, price: '60' });
|
||||||
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();
|
|
||||||
await api.dispose();
|
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.describe('Favoriting items', () => {
|
||||||
test('a signed-out visitor is prompted to sign in, and the favorite completes', async ({ page }) => {
|
// Drives the registration form rather than taking the `customer` fixture:
|
||||||
await page.goto('/');
|
// the claim here is that the favorite a signed-out visitor asked for survives
|
||||||
await heart(page, ITEM).first().click();
|
// 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.
|
// Same inline prompt Add to Cart already uses.
|
||||||
await expect(page.getByRole('dialog')).toBeVisible();
|
await expect(authModal.registerDialog).toBeVisible();
|
||||||
|
|
||||||
const email = uniqueEmail();
|
await authModal.fillRegistration({
|
||||||
await page.getByRole('dialog').getByRole('textbox', { name: 'Email' }).fill(email);
|
email: uniqueEmail('fav'),
|
||||||
await page.getByRole('textbox', { name: 'First name' }).fill('Test');
|
password: PASSWORD,
|
||||||
await page.getByRole('textbox', { name: 'Last name' }).fill('Customer');
|
firstName: 'Test',
|
||||||
await page.getByRole('dialog').getByLabel('Password').fill(PASSWORD);
|
lastName: 'Customer'
|
||||||
await page.getByRole('dialog').getByRole('button', { name: 'Create account' }).click();
|
});
|
||||||
|
await authModal.submitRegistration();
|
||||||
|
|
||||||
// The favorite the visitor originally asked for is applied on success.
|
// 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 }) => {
|
test('offers the alert opt-in with a reason, and it is not the marketing consent', async ({
|
||||||
const email = uniqueEmail();
|
page,
|
||||||
await register(page, email);
|
customer,
|
||||||
|
storefront,
|
||||||
|
favoritePrompt
|
||||||
|
}) => {
|
||||||
|
await storefront.gotoSignedIn();
|
||||||
|
await storefront.favoriteToggle(ITEM).first().click();
|
||||||
|
|
||||||
await gotoStorefrontSignedIn(page);
|
await expect(favoritePrompt.dialog).toBeVisible();
|
||||||
await heart(page, ITEM).first().click();
|
await expect(favoritePrompt.dialog).toContainText('Want to know if this sells?');
|
||||||
|
|
||||||
const prompt = page.getByRole('dialog');
|
|
||||||
await expect(prompt).toBeVisible();
|
|
||||||
await expect(prompt).toContainText('Want to know if this sells?');
|
|
||||||
// The reason for asking has to be given, not just the ask.
|
// The reason for asking has to be given, not just the ask.
|
||||||
await expect(prompt).toContainText(/one of a kind/i);
|
await expect(favoritePrompt.dialog).toContainText(/one of a kind/i);
|
||||||
await expect(prompt).toContainText(/separate from any marketing/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();
|
await expect(page.getByText('We will let you know')).toBeVisible();
|
||||||
|
|
||||||
const me = await (await page.request.get('/api/customers/me')).json();
|
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);
|
expect(me.marketing_consent).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('declining the opt-in still keeps the favorite', async ({ page }) => {
|
test('declining the opt-in still keeps the favorite', async ({
|
||||||
const email = uniqueEmail();
|
page,
|
||||||
await register(page, email);
|
customer,
|
||||||
|
storefront,
|
||||||
|
favoritePrompt
|
||||||
|
}) => {
|
||||||
|
await storefront.gotoSignedIn();
|
||||||
|
await storefront.favoriteToggle(ITEM).first().click();
|
||||||
|
await favoritePrompt.declineAlerts();
|
||||||
|
|
||||||
await gotoStorefrontSignedIn(page);
|
await expect(storefront.removeFromFavoritesButton(ITEM)).toBeVisible();
|
||||||
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();
|
|
||||||
|
|
||||||
const favorites = await (await page.request.get('/api/customers/me/favorites')).json();
|
const favorites = await (await page.request.get('/api/customers/me/favorites')).json();
|
||||||
expect(favorites).toHaveLength(1);
|
expect(favorites).toHaveLength(1);
|
||||||
@@ -103,49 +77,39 @@ test.describe('Favoriting items', () => {
|
|||||||
expect(me.favorite_alerts).toBe(false);
|
expect(me.favorite_alerts).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('unfavoriting removes it', async ({ page }) => {
|
test('unfavoriting removes it', async ({ page, customer, storefront, favoritePrompt }) => {
|
||||||
const email = uniqueEmail();
|
await storefront.gotoSignedIn();
|
||||||
await register(page, email);
|
await storefront.favoriteToggle(ITEM).first().click();
|
||||||
|
await favoritePrompt.declineAlerts();
|
||||||
|
await expect(storefront.removeFromFavoritesButton(ITEM)).toBeVisible();
|
||||||
|
|
||||||
await gotoStorefrontSignedIn(page);
|
await storefront.removeFromFavoritesButton(ITEM).click();
|
||||||
await heart(page, ITEM).first().click();
|
await expect(storefront.addToFavoritesButton(ITEM)).toBeVisible();
|
||||||
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();
|
|
||||||
|
|
||||||
const favorites = await (await page.request.get('/api/customers/me/favorites')).json();
|
const favorites = await (await page.request.get('/api/customers/me/favorites')).json();
|
||||||
expect(favorites).toEqual([]);
|
expect(favorites).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('favorites survive a reload', async ({ page }) => {
|
test('favorites survive a reload', async ({ page, customer, storefront, favoritePrompt }) => {
|
||||||
const email = uniqueEmail();
|
await storefront.gotoSignedIn();
|
||||||
await register(page, email);
|
await storefront.favoriteToggle(ITEM).first().click();
|
||||||
|
await favoritePrompt.declineAlerts();
|
||||||
await gotoStorefrontSignedIn(page);
|
await expect(storefront.removeFromFavoritesButton(ITEM)).toBeVisible();
|
||||||
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();
|
|
||||||
|
|
||||||
// Proves it was stored server-side rather than held in component state.
|
// Proves it was stored server-side rather than held in component state.
|
||||||
await page.reload();
|
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 }) => {
|
test('the account page can turn the alerts off again', async ({ page, customer, accountModal }) => {
|
||||||
const email = uniqueEmail();
|
|
||||||
await register(page, email);
|
|
||||||
await page.request.put('/api/customers/me/favorite-alerts', { data: { enabled: true } });
|
await page.request.put('/api/customers/me/favorite-alerts', { data: { enabled: true } });
|
||||||
|
|
||||||
await page.goto('/account');
|
await accountModal.open();
|
||||||
// Scoped to the account modal. The storefront renders behind it and has a
|
await expect(
|
||||||
// theme switch of its own, so an unscoped switch locator only picks the
|
accountModal.dialog.getByText('Email me when an item I favorited is sold')
|
||||||
// right control by DOM accident.
|
).toBeVisible();
|
||||||
const account = page.getByRole('dialog', { name: 'My Account' });
|
|
||||||
await expect(account.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();
|
await expect(page.getByText('Turned off')).toBeVisible();
|
||||||
|
|
||||||
const me = await (await page.request.get('/api/customers/me')).json();
|
const me = await (await page.request.get('/api/customers/me')).json();
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ import { StorefrontPage } from './pages/StorefrontPage';
|
|||||||
import { AdminPage } from './pages/AdminPage';
|
import { AdminPage } from './pages/AdminPage';
|
||||||
import { PasswordResetPages } from './pages/PasswordResetPages';
|
import { PasswordResetPages } from './pages/PasswordResetPages';
|
||||||
import { FilterDrawer } from './pages/FilterDrawer';
|
import { FilterDrawer } from './pages/FilterDrawer';
|
||||||
|
import { FavoritePrompt } from './pages/FavoritePrompt';
|
||||||
|
import { OrdersPage } from './pages/OrdersPage';
|
||||||
|
import { AdminInventory } from './pages/AdminInventory';
|
||||||
import { uniqueEmail } from './support/api';
|
import { uniqueEmail } from './support/api';
|
||||||
|
|
||||||
// Re-exported so specs can import everything from here — expect, Page,
|
// Re-exported so specs can import everything from here — expect, Page,
|
||||||
@@ -16,6 +19,16 @@ import { uniqueEmail } from './support/api';
|
|||||||
// The explicit `test` below wins over the star export.
|
// The explicit `test` below wins over the star export.
|
||||||
export * from '@playwright/test';
|
export * from '@playwright/test';
|
||||||
export * from './support/api';
|
export * from './support/api';
|
||||||
|
export { Header } from './pages/Header';
|
||||||
|
export { AuthModal } from './pages/AuthModal';
|
||||||
|
export { AccountModal } from './pages/AccountModal';
|
||||||
|
export { StorefrontPage } from './pages/StorefrontPage';
|
||||||
|
export { AdminPage } from './pages/AdminPage';
|
||||||
|
export { PasswordResetPages } from './pages/PasswordResetPages';
|
||||||
|
export { FilterDrawer } from './pages/FilterDrawer';
|
||||||
|
export { FavoritePrompt } from './pages/FavoritePrompt';
|
||||||
|
export { OrdersPage } from './pages/OrdersPage';
|
||||||
|
export { AdminInventory } from './pages/AdminInventory';
|
||||||
|
|
||||||
const NYC_OUTPUT = path.resolve(__dirname, '..', '..', '.nyc_output');
|
const NYC_OUTPUT = path.resolve(__dirname, '..', '..', '.nyc_output');
|
||||||
const collectingCoverage = process.env.COVERAGE === 'true';
|
const collectingCoverage = process.env.COVERAGE === 'true';
|
||||||
@@ -42,6 +55,9 @@ interface Pages {
|
|||||||
admin: AdminPage;
|
admin: AdminPage;
|
||||||
passwordReset: PasswordResetPages;
|
passwordReset: PasswordResetPages;
|
||||||
filterDrawer: FilterDrawer;
|
filterDrawer: FilterDrawer;
|
||||||
|
favoritePrompt: FavoritePrompt;
|
||||||
|
orders: OrdersPage;
|
||||||
|
adminInventory: AdminInventory;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Data {
|
interface Data {
|
||||||
@@ -98,6 +114,15 @@ export const test = base.extend<Pages & Data & { collectCoverage: void }>({
|
|||||||
filterDrawer: async ({ page }, use) => {
|
filterDrawer: async ({ page }, use) => {
|
||||||
await use(new FilterDrawer(page));
|
await use(new FilterDrawer(page));
|
||||||
},
|
},
|
||||||
|
favoritePrompt: async ({ page }, use) => {
|
||||||
|
await use(new FavoritePrompt(page));
|
||||||
|
},
|
||||||
|
orders: async ({ page }, use) => {
|
||||||
|
await use(new OrdersPage(page));
|
||||||
|
},
|
||||||
|
adminInventory: async ({ page }, use) => {
|
||||||
|
await use(new AdminInventory(page));
|
||||||
|
},
|
||||||
|
|
||||||
adminApi: async ({ playwright, baseURL }, use) => {
|
adminApi: async ({ playwright, baseURL }, use) => {
|
||||||
const context = await playwright.request.newContext({ baseURL });
|
const context = await playwright.request.newContext({ baseURL });
|
||||||
|
|||||||
@@ -1,27 +1,8 @@
|
|||||||
import { test, expect, Page } from './fixtures';
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
const PASSWORD = 'supersecret123';
|
|
||||||
|
|
||||||
const uniqueEmail = () => `orders-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}@example.com`;
|
|
||||||
|
|
||||||
// The generous wait is the same one the other account specs use: registration is
|
|
||||||
// a bcrypt round-trip rather than a render, and runs past Playwright's 5s
|
|
||||||
// default when the suite's workers all register at once.
|
|
||||||
async function registerCustomer(page: Page): Promise<string> {
|
|
||||||
const email = uniqueEmail();
|
|
||||||
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();
|
|
||||||
await expect(page.getByRole('button', { name: 'My Account' })).toBeVisible({ timeout: 20000 });
|
|
||||||
return email;
|
|
||||||
}
|
|
||||||
|
|
||||||
test.describe('Order history has a page of its own', () => {
|
test.describe('Order history has a page of its own', () => {
|
||||||
test('a signed-out visitor is sent to sign in', async ({ page }) => {
|
test('a signed-out visitor is sent to sign in', async ({ page, orders }) => {
|
||||||
await page.goto('/orders');
|
await orders.goto();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/login/, { timeout: 20000 });
|
await expect(page).toHaveURL(/\/login/, { timeout: 20000 });
|
||||||
});
|
});
|
||||||
@@ -29,43 +10,46 @@ test.describe('Order history has a page of its own', () => {
|
|||||||
// A page, not a modal: no dialog, and the storefront is not rendered behind
|
// A page, not a modal: no dialog, and the storefront is not rendered behind
|
||||||
// it. Putting /orders in MODAL_ROUTES would quietly undo the whole change,
|
// it. Putting /orders in MODAL_ROUTES would quietly undo the whole change,
|
||||||
// and this is what would catch it.
|
// and this is what would catch it.
|
||||||
test('renders as a page rather than a modal over the storefront', async ({ page }) => {
|
test('renders as a page rather than a modal over the storefront', async ({
|
||||||
await registerCustomer(page);
|
customer,
|
||||||
await page.goto('/orders');
|
orders,
|
||||||
|
header
|
||||||
|
}) => {
|
||||||
|
await orders.goto();
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Order History' })).toBeVisible();
|
await expect(orders.heading).toBeVisible({ timeout: 20000 });
|
||||||
await expect(page.getByRole('dialog')).toHaveCount(0);
|
await expect(orders.anyDialog).toHaveCount(0);
|
||||||
await expect(page.getByRole('heading', { name: 'Redefined Designs' })).toBeHidden();
|
await expect(header.siteTitle).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('says so when there are no orders, rather than showing an empty table', async ({ page }) => {
|
test('says so when there are no orders, rather than showing an empty table', async ({
|
||||||
await registerCustomer(page);
|
customer,
|
||||||
await page.goto('/orders');
|
orders
|
||||||
|
}) => {
|
||||||
|
await orders.goto();
|
||||||
|
|
||||||
await expect(page.getByText('No orders yet')).toBeVisible();
|
await expect(orders.noOrdersNotice).toBeVisible({ timeout: 20000 });
|
||||||
await expect(page.getByRole('button', { name: 'Continue Shopping' })).toBeVisible();
|
await expect(orders.continueShoppingButton).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Back to Shop returns to the storefront', async ({ page }) => {
|
test('Back to Shop returns to the storefront', async ({ page, customer, orders, header }) => {
|
||||||
await registerCustomer(page);
|
await orders.goto();
|
||||||
await page.goto('/orders');
|
await expect(orders.heading).toBeVisible({ timeout: 20000 });
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Back to Shop' }).click();
|
await orders.backToShopButton.click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/$/);
|
await expect(page).toHaveURL(/\/$/);
|
||||||
await expect(page.getByRole('heading', { name: 'Redefined Designs' })).toBeVisible();
|
await expect(header.siteTitle).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
// The account view is where a customer looks for their orders, so the route
|
// The account view is where a customer looks for their orders, so the route
|
||||||
// out of it is the part that has to keep working now the table has gone.
|
// out of it is the part that has to keep working now the table has gone.
|
||||||
test('My Account links to it', async ({ page }) => {
|
test('My Account links to it', async ({ page, customer, accountModal, orders }) => {
|
||||||
await registerCustomer(page);
|
await accountModal.open();
|
||||||
await page.goto('/account');
|
|
||||||
|
|
||||||
await page.getByRole('dialog', { name: 'My Account' })
|
await accountModal.orderHistoryButton.click();
|
||||||
.getByRole('button', { name: 'View order history' }).click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(/\/orders/);
|
await expect(page).toHaveURL(/\/orders/);
|
||||||
await expect(page.getByRole('heading', { name: 'Order History' })).toBeVisible();
|
await expect(orders.heading).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ export class AccountModal {
|
|||||||
readonly closeButton: Locator;
|
readonly closeButton: Locator;
|
||||||
readonly resendVerificationButton: Locator;
|
readonly resendVerificationButton: Locator;
|
||||||
readonly themeSwitches: Locator;
|
readonly themeSwitches: Locator;
|
||||||
|
/** The second switch in the modal; the first is the theme. */
|
||||||
|
readonly favoriteAlertsSwitch: Locator;
|
||||||
readonly notVerifiedNotice: Locator;
|
readonly notVerifiedNotice: Locator;
|
||||||
|
|
||||||
readonly firstName: Locator;
|
readonly firstName: Locator;
|
||||||
@@ -44,6 +46,7 @@ export class AccountModal {
|
|||||||
this.closeButton = this.dialog.getByRole('button', { name: 'Close' });
|
this.closeButton = this.dialog.getByRole('button', { name: 'Close' });
|
||||||
this.resendVerificationButton = this.dialog.getByRole('button', { name: 'Send it again' });
|
this.resendVerificationButton = this.dialog.getByRole('button', { name: 'Send it again' });
|
||||||
this.themeSwitches = this.dialog.getByRole('switch');
|
this.themeSwitches = this.dialog.getByRole('switch');
|
||||||
|
this.favoriteAlertsSwitch = this.dialog.getByRole('switch').last();
|
||||||
this.notVerifiedNotice = this.dialog.getByText('Email not verified');
|
this.notVerifiedNotice = this.dialog.getByText('Email not verified');
|
||||||
|
|
||||||
this.firstName = this.dialog.getByLabel('First name', { exact: true });
|
this.firstName = this.dialog.getByLabel('First name', { exact: true });
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The admin inventory tab: the item table and the form that adds to it.
|
||||||
|
*
|
||||||
|
* Rows are located by the item's name rather than by index. The table is shared
|
||||||
|
* with every other run's items and is paginated and sortable, so an index means
|
||||||
|
* a different row depending on what else exists.
|
||||||
|
*/
|
||||||
|
export class AdminInventory {
|
||||||
|
readonly addItemButton: Locator;
|
||||||
|
readonly name: Locator;
|
||||||
|
readonly price: Locator;
|
||||||
|
readonly category: Locator;
|
||||||
|
readonly saveButton: Locator;
|
||||||
|
|
||||||
|
constructor(private readonly page: Page) {
|
||||||
|
this.addItemButton = page.getByRole('button', { name: 'Add Item' });
|
||||||
|
this.name = page.getByLabel('Name');
|
||||||
|
this.price = page.getByLabel('Price (USD)');
|
||||||
|
this.category = page.getByLabel('Category', { exact: true });
|
||||||
|
this.saveButton = page.getByRole('button', { name: 'Save', exact: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
row(itemName: string): Locator {
|
||||||
|
return this.page.getByRole('row').filter({ hasText: itemName });
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The status chip in an item's row — PENDING, AVAILABLE, RESERVED, SOLD. */
|
||||||
|
status(itemName: string, status: string): Locator {
|
||||||
|
return this.row(itemName).getByText(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
publishButton(itemName: string): Locator {
|
||||||
|
return this.row(itemName).getByRole('button', { name: 'Publish' });
|
||||||
|
}
|
||||||
|
|
||||||
|
unpublishButton(itemName: string): Locator {
|
||||||
|
return this.row(itemName).getByRole('button', { name: 'Unpublish' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The preview drawer, opened by clicking an item's name.
|
||||||
|
*
|
||||||
|
* It renders the item as the storefront card will, which is the question an
|
||||||
|
* admin is asking of a pending item — a customer never sees the pending state.
|
||||||
|
*/
|
||||||
|
previewDrawer(itemName: string): Locator {
|
||||||
|
return this.page.getByRole('dialog', { name: `Preview: ${itemName}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
async openPreview(itemName: string): Promise<void> {
|
||||||
|
await this.page.getByRole('button', { name: itemName }).click();
|
||||||
|
}
|
||||||
|
|
||||||
|
async openItemForm(): Promise<void> {
|
||||||
|
await this.addItemButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The modal offered when a customer favorites an item: an opt-in to hear if it
|
||||||
|
* sells, with the reason for asking.
|
||||||
|
*
|
||||||
|
* `getByRole('dialog')` unqualified is deliberate and safe — the storefront
|
||||||
|
* shows one modal at a time, and a signed-out visitor clicking the heart gets
|
||||||
|
* the auth prompt instead, which is a different object.
|
||||||
|
*
|
||||||
|
* The opt-in is deliberately not the marketing consent. Accepting item alerts
|
||||||
|
* must not sign anyone up for marketing, which is why the tests read both flags
|
||||||
|
* back off the API rather than trusting the copy.
|
||||||
|
*/
|
||||||
|
export class FavoritePrompt {
|
||||||
|
readonly dialog: Locator;
|
||||||
|
readonly acceptAlertsButton: Locator;
|
||||||
|
readonly declineAlertsButton: Locator;
|
||||||
|
|
||||||
|
constructor(page: Page) {
|
||||||
|
this.dialog = page.getByRole('dialog');
|
||||||
|
this.acceptAlertsButton = this.dialog.getByRole('button', { name: 'Yes, email me' });
|
||||||
|
this.declineAlertsButton = this.dialog.getByRole('button', { name: 'No thanks' });
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptAlerts(): Promise<void> {
|
||||||
|
await this.acceptAlertsButton.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Declining keeps the favorite; only the alerts are refused. */
|
||||||
|
async declineAlerts(): Promise<void> {
|
||||||
|
await this.declineAlertsButton.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Locator, Page } from '@playwright/test';
|
import { Locator, Page, expect } from '@playwright/test';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The storefront's filter drawer.
|
* The storefront's filter drawer.
|
||||||
@@ -16,12 +16,14 @@ export class FilterDrawer {
|
|||||||
readonly maximumPrice: Locator;
|
readonly maximumPrice: Locator;
|
||||||
readonly closeButton: Locator;
|
readonly closeButton: Locator;
|
||||||
readonly clearAllButton: Locator;
|
readonly clearAllButton: Locator;
|
||||||
|
readonly favoritesOnlySwitch: Locator;
|
||||||
|
|
||||||
constructor(private readonly page: Page) {
|
constructor(private readonly page: Page) {
|
||||||
this.minimumPrice = page.getByLabel('Minimum price');
|
this.minimumPrice = page.getByLabel('Minimum price');
|
||||||
this.maximumPrice = page.getByLabel('Maximum price');
|
this.maximumPrice = page.getByLabel('Maximum price');
|
||||||
this.closeButton = page.getByRole('button', { name: 'Close' });
|
this.closeButton = page.getByRole('button', { name: 'Close' });
|
||||||
this.clearAllButton = page.getByRole('button', { name: 'Clear all' });
|
this.clearAllButton = page.getByRole('button', { name: 'Clear all' });
|
||||||
|
this.favoritesOnlySwitch = page.getByRole('switch', { name: 'Only my favorites' });
|
||||||
}
|
}
|
||||||
|
|
||||||
category(name: string): Locator {
|
category(name: string): Locator {
|
||||||
@@ -45,6 +47,16 @@ export class FilterDrawer {
|
|||||||
if (maximum !== undefined) await this.maximumPrice.fill(maximum);
|
if (maximum !== undefined) await this.maximumPrice.fill(maximum);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the drawer and waits for its contents.
|
||||||
|
*
|
||||||
|
* The wait is the action's contract: the drawer animates in, and a click
|
||||||
|
* landing mid-animation hits the backdrop instead of the control.
|
||||||
|
*/
|
||||||
|
async waitForOpen(): Promise<void> {
|
||||||
|
await expect(this.favoritesOnlySwitch).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
await this.closeButton.click();
|
await this.closeButton.click();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Locator, Page } from '@playwright/test';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Order history, which is a page rather than a modal.
|
||||||
|
*
|
||||||
|
* That distinction is the point of the specs using this: /orders is
|
||||||
|
* deliberately not in MODAL_ROUTES, so there is no dialog and the storefront is
|
||||||
|
* not rendered behind it. Putting it back would quietly undo that, which is why
|
||||||
|
* the absence of a dialog is asserted rather than assumed.
|
||||||
|
*/
|
||||||
|
export class OrdersPage {
|
||||||
|
readonly heading: Locator;
|
||||||
|
readonly noOrdersNotice: Locator;
|
||||||
|
readonly continueShoppingButton: Locator;
|
||||||
|
readonly backToShopButton: Locator;
|
||||||
|
readonly anyDialog: Locator;
|
||||||
|
|
||||||
|
constructor(private readonly page: Page) {
|
||||||
|
this.heading = page.getByRole('heading', { name: 'Order History' });
|
||||||
|
this.noOrdersNotice = page.getByText('No orders yet');
|
||||||
|
this.continueShoppingButton = page.getByRole('button', { name: 'Continue Shopping' });
|
||||||
|
this.backToShopButton = page.getByRole('button', { name: 'Back to Shop' });
|
||||||
|
this.anyDialog = page.getByRole('dialog');
|
||||||
|
}
|
||||||
|
|
||||||
|
async goto(): Promise<void> {
|
||||||
|
await this.page.goto('/orders');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,10 @@ export class StorefrontPage {
|
|||||||
* all" of its own, so an unscoped one matches both.
|
* all" of its own, so an unscoped one matches both.
|
||||||
*/
|
*/
|
||||||
readonly activeFilters: Locator;
|
readonly activeFilters: Locator;
|
||||||
|
/** Shown instead of the grid when filters match nothing. */
|
||||||
|
readonly noMatchesNotice: Locator;
|
||||||
|
/** Shown instead of that when a favorites filter is set with no session. */
|
||||||
|
readonly favoritesNeedSignInNotice: Locator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The three things the catalogue can say instead of listing items. Named
|
* The three things the catalogue can say instead of listing items. Named
|
||||||
@@ -40,6 +44,8 @@ export class StorefrontPage {
|
|||||||
this.filtersButton = page.getByRole('button', { name: /Filters/ });
|
this.filtersButton = page.getByRole('button', { name: /Filters/ });
|
||||||
this.privacyPolicyLink = page.getByRole('link', { name: 'Privacy Policy' });
|
this.privacyPolicyLink = page.getByRole('link', { name: 'Privacy Policy' });
|
||||||
this.activeFilters = page.getByRole('group', { name: 'Active filters' });
|
this.activeFilters = page.getByRole('group', { name: 'Active filters' });
|
||||||
|
this.noMatchesNotice = page.getByText('No items match these filters');
|
||||||
|
this.favoritesNeedSignInNotice = page.getByText('Sign in to see the items you have favorited');
|
||||||
|
|
||||||
this.emptyNotice = page.getByText('No items yet');
|
this.emptyNotice = page.getByText('No items yet');
|
||||||
this.loadFailureNotice = page.getByText("Couldn't load items");
|
this.loadFailureNotice = page.getByText("Couldn't load items");
|
||||||
@@ -75,15 +81,56 @@ export class StorefrontPage {
|
|||||||
return this.card(name).getByRole('button', { name: 'Add to Cart' });
|
return this.card(name).getByRole('button', { name: 'Add to Cart' });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The heart. Named for what it does rather than what it looks like. */
|
/**
|
||||||
|
* The heart, in whichever state it is currently in.
|
||||||
|
*
|
||||||
|
* Located page-wide rather than inside the card: the storefront paginates as
|
||||||
|
* items accumulate, and the control is named for the item anyway, so scoping
|
||||||
|
* to a card buys nothing and breaks when the card is on another page.
|
||||||
|
*/
|
||||||
favoriteToggle(name: string): Locator {
|
favoriteToggle(name: string): Locator {
|
||||||
return this.card(name).getByRole('button', { name: /favorite/i });
|
return this.page.getByRole('button', { name: new RegExp(`(Add|Remove) ${name}`) });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The two settled states, named separately because the tests assert on the
|
||||||
|
* transition between them — a favorite that took is a "Remove" control, and
|
||||||
|
* one that did not is still an "Add".
|
||||||
|
*/
|
||||||
|
addToFavoritesButton(name: string): Locator {
|
||||||
|
return this.page.getByRole('button', { name: `Add ${name} to favorites` });
|
||||||
|
}
|
||||||
|
|
||||||
|
removeFromFavoritesButton(name: string): Locator {
|
||||||
|
return this.page.getByRole('button', { name: `Remove ${name} from favorites` });
|
||||||
}
|
}
|
||||||
|
|
||||||
async openFilters(): Promise<void> {
|
async openFilters(): Promise<void> {
|
||||||
await this.filtersButton.click();
|
await this.filtersButton.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item's whole grid cell rather than its card.
|
||||||
|
*
|
||||||
|
* The SOLD ribbon is rendered outside the card, so a test asserting on it has
|
||||||
|
* to reach the cell — and it has to be scoped to this item, because sold items
|
||||||
|
* from earlier runs share the page.
|
||||||
|
*/
|
||||||
|
gridCell(name: string): Locator {
|
||||||
|
return this.page.locator('.ant-col').filter({ hasText: name });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The availability segment.
|
||||||
|
*
|
||||||
|
* antd's Segmented hides the real radio behind a styled label, so the input is
|
||||||
|
* found by role but cannot be clicked. The label carries a title attribute,
|
||||||
|
* which is the same handle this suite uses for antd Select options.
|
||||||
|
*/
|
||||||
|
async chooseAvailability(label: string): Promise<void> {
|
||||||
|
await this.page.getByTitle(label, { exact: true }).click();
|
||||||
|
}
|
||||||
|
|
||||||
removeFilterChip(name: string): Locator {
|
removeFilterChip(name: string): Locator {
|
||||||
return this.page.getByRole('button', { name: `Remove filter ${name}` });
|
return this.page.getByRole('button', { name: `Remove filter ${name}` });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,88 @@
|
|||||||
import { test, expect } from './fixtures';
|
import { test, expect, uniqueSuffix, publishItem } from './fixtures';
|
||||||
|
|
||||||
const suffix = () => `s${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
/**
|
||||||
|
* Creates an item and leaves it as it arrives — pending.
|
||||||
// Creates an item and leaves it as it arrives — pending. Deliberately does not
|
*
|
||||||
// publish, unlike the other specs' fixtures, because the staged state is what
|
* Deliberately does not publish, unlike the shared `createItem` helper, because
|
||||||
// is being tested here.
|
* the staged state is the thing under test here. The status assertion is the
|
||||||
async function createStagedItem(page: import('@playwright/test').Page, name: string) {
|
* whole premise: creating something does not publish it.
|
||||||
const created = await page.request.post('/api/admin/items', {
|
*/
|
||||||
|
async function createStagedItem(
|
||||||
|
request: import('@playwright/test').APIRequestContext,
|
||||||
|
name: string
|
||||||
|
): Promise<number> {
|
||||||
|
const created = await request.post('/api/admin/items', {
|
||||||
multipart: { name, description: '', price: '55', category_id: '', tags: '[]' }
|
multipart: { name, description: '', price: '55', category_id: '', tags: '[]' }
|
||||||
});
|
});
|
||||||
expect(created.ok()).toBeTruthy();
|
expect(created.ok()).toBeTruthy();
|
||||||
const body = await created.json();
|
const body = await created.json();
|
||||||
// The whole premise: creating something does not publish it.
|
|
||||||
expect(body.status).toBe('pending');
|
expect(body.status).toBe('pending');
|
||||||
return body.id as number;
|
return body.id as number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rowFor = (page: import('@playwright/test').Page, name: string) =>
|
|
||||||
page.getByRole('row').filter({ hasText: name });
|
|
||||||
|
|
||||||
test.describe('Staging an item until it is published', () => {
|
test.describe('Staging an item until it is published', () => {
|
||||||
test('a new item is held back from the storefront until published', async ({ page }) => {
|
test('a new item is held back from the storefront until published', async ({
|
||||||
const name = `Staged ${suffix()}`;
|
page,
|
||||||
await createStagedItem(page, name);
|
storefront,
|
||||||
|
admin,
|
||||||
|
adminInventory
|
||||||
|
}) => {
|
||||||
|
const name = `Staged ${uniqueSuffix()}`;
|
||||||
|
await createStagedItem(page.request, name);
|
||||||
|
|
||||||
// Not in the catalogue while pending.
|
// Not in the catalogue while pending.
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
await expect(page.getByText(name)).toHaveCount(0);
|
await expect(page.getByText(name)).toHaveCount(0);
|
||||||
|
|
||||||
// It is in the admin, marked as pending.
|
// It is in the admin, marked as pending.
|
||||||
await page.goto('/admin');
|
await admin.goto();
|
||||||
const row = rowFor(page, name);
|
await expect(adminInventory.row(name)).toBeVisible();
|
||||||
await expect(row).toBeVisible();
|
await expect(adminInventory.status(name, 'PENDING')).toBeVisible();
|
||||||
await expect(row.getByText('PENDING')).toBeVisible();
|
|
||||||
|
|
||||||
await row.getByRole('button', { name: 'Publish' }).click();
|
await adminInventory.publishButton(name).click();
|
||||||
await expect(row.getByText('AVAILABLE')).toBeVisible();
|
await expect(adminInventory.status(name, 'AVAILABLE')).toBeVisible();
|
||||||
|
|
||||||
// And now a customer can see it.
|
// And now a customer can see it.
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
await expect(page.getByText(name)).toBeVisible();
|
await expect(page.getByText(name)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('publishing can be undone while nobody is holding the item', async ({ page }) => {
|
test('publishing can be undone while nobody is holding the item', async ({
|
||||||
const name = `Staged ${suffix()}`;
|
page,
|
||||||
const id = await createStagedItem(page, name);
|
storefront,
|
||||||
expect((await page.request.post(`/api/admin/items/${id}/mark-available`)).ok()).toBeTruthy();
|
admin,
|
||||||
|
adminInventory
|
||||||
|
}) => {
|
||||||
|
const name = `Staged ${uniqueSuffix()}`;
|
||||||
|
const id = await createStagedItem(page.request, name);
|
||||||
|
await publishItem(page.request, id);
|
||||||
|
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
await expect(page.getByText(name)).toBeVisible();
|
await expect(page.getByText(name)).toBeVisible();
|
||||||
|
|
||||||
await page.goto('/admin');
|
await admin.goto();
|
||||||
const row = rowFor(page, name);
|
await adminInventory.unpublishButton(name).click();
|
||||||
await row.getByRole('button', { name: 'Unpublish' }).click();
|
await expect(adminInventory.status(name, 'PENDING')).toBeVisible();
|
||||||
await expect(row.getByText('PENDING')).toBeVisible();
|
|
||||||
|
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
await expect(page.getByText(name)).toHaveCount(0);
|
await expect(page.getByText(name)).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The two features together, which is the reason for wanting both: a staged
|
// The two features together, which is the reason for wanting both: a staged
|
||||||
// item is previewed as it will look once live, because a customer never sees
|
// item is previewed as it will look once live, because a customer never sees
|
||||||
// the pending state and "how will this look" is the question being asked.
|
// the pending state and "how will this look" is the question being asked.
|
||||||
test('a pending item previews as it will look once published', async ({ page }) => {
|
test('a pending item previews as it will look once published', async ({
|
||||||
const name = `Staged ${suffix()}`;
|
page,
|
||||||
await createStagedItem(page, name);
|
admin,
|
||||||
|
adminInventory
|
||||||
|
}) => {
|
||||||
|
const name = `Staged ${uniqueSuffix()}`;
|
||||||
|
await createStagedItem(page.request, name);
|
||||||
|
|
||||||
await page.goto('/admin');
|
await admin.goto();
|
||||||
await page.getByRole('button', { name }).click();
|
await adminInventory.openPreview(name);
|
||||||
|
|
||||||
const drawer = page.getByRole('dialog', { name: `Preview: ${name}` });
|
const drawer = adminInventory.previewDrawer(name);
|
||||||
await expect(drawer).toBeVisible();
|
await expect(drawer).toBeVisible();
|
||||||
await expect(drawer.getByText('$55.00')).toBeVisible();
|
await expect(drawer.getByText('$55.00')).toBeVisible();
|
||||||
// The live card's action, not a pending placeholder.
|
// The live card's action, not a pending placeholder.
|
||||||
|
|||||||
@@ -1,44 +1,21 @@
|
|||||||
import { test, expect } from './fixtures';
|
import { test, expect, createAdminContext, createItem, sellItem, uniqueSuffix } from './fixtures';
|
||||||
|
|
||||||
// The storefront shows every item ever seeded and the e2e database is not reset
|
// The storefront shows every item ever seeded and the e2e database is not reset
|
||||||
// between runs, so every fixture carries a unique run id and assertions name
|
// between runs, so every fixture carries a unique run id and assertions name
|
||||||
// only the items this run created.
|
// only the items this run created.
|
||||||
const RUN = `s${Date.now().toString(36)}${Math.random().toString(36).slice(2, 7)}`;
|
const RUN = `s${uniqueSuffix()}`;
|
||||||
|
|
||||||
const NAMES = {
|
const NAMES = {
|
||||||
available: `Available piece ${RUN}`,
|
available: `Available piece ${RUN}`,
|
||||||
sold: `Sold piece ${RUN}`
|
sold: `Sold piece ${RUN}`
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scoped to the item's own grid cell: the SOLD ribbon sits outside the card,
|
|
||||||
// and other runs' sold items share the page.
|
|
||||||
const cell = (page: import('@playwright/test').Page, name: string) =>
|
|
||||||
page.locator('.ant-col').filter({ hasText: name });
|
|
||||||
|
|
||||||
// antd Segmented hides the real radio input behind a styled label, so the input
|
|
||||||
// is found by role but cannot be clicked. The label carries a title attribute,
|
|
||||||
// which is the same handle this suite already uses for antd Select options.
|
|
||||||
// The input is still the right thing to assert checked-ness on: toBeChecked
|
|
||||||
// does not require visibility.
|
|
||||||
async function chooseAvailability(page: import('@playwright/test').Page, label: string) {
|
|
||||||
await page.getByTitle(label, { exact: true }).click();
|
|
||||||
}
|
|
||||||
|
|
||||||
test.beforeAll(async ({ playwright }) => {
|
test.beforeAll(async ({ playwright }) => {
|
||||||
const api = await playwright.request.newContext({ baseURL: 'http://localhost:5173' });
|
const api = await createAdminContext(playwright);
|
||||||
|
|
||||||
for (const name of [NAMES.available, NAMES.sold]) {
|
await createItem(api, { name: NAMES.available, price: '250.00' });
|
||||||
const res = await api.post('/api/admin/items', {
|
const sold = await createItem(api, { name: NAMES.sold, price: '250.00' });
|
||||||
multipart: { name, description: '', price: '250.00' }
|
await sellItem(api, sold.id);
|
||||||
});
|
|
||||||
expect(res.ok()).toBeTruthy();
|
|
||||||
const { id } = await res.json();
|
|
||||||
// Items arrive pending since #90, so publishing is what makes them public.
|
|
||||||
expect((await api.post(`/api/admin/items/${id}/mark-available`)).ok()).toBeTruthy();
|
|
||||||
if (name === NAMES.sold) {
|
|
||||||
expect((await api.post(`/api/admin/items/${id}/mark-sold`)).ok()).toBeTruthy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await api.dispose();
|
await api.dispose();
|
||||||
});
|
});
|
||||||
@@ -46,68 +23,68 @@ test.beforeAll(async ({ playwright }) => {
|
|||||||
test.describe('Filtering the storefront by availability', () => {
|
test.describe('Filtering the storefront by availability', () => {
|
||||||
// The change customers actually see. Asserted on a bare visit rather than on
|
// The change customers actually see. Asserted on a bare visit rather than on
|
||||||
// a parameter, because the default is what changed for everyone.
|
// a parameter, because the default is what changed for everyone.
|
||||||
test('hides sold pieces by default', async ({ page }) => {
|
test('hides sold pieces by default', async ({ page, storefront }) => {
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
|
|
||||||
await expect(cell(page, NAMES.available)).toBeVisible();
|
await expect(storefront.gridCell(NAMES.available)).toBeVisible();
|
||||||
await expect(cell(page, NAMES.sold)).toHaveCount(0);
|
await expect(storefront.gridCell(NAMES.sold)).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('All brings them back, ribbon and all', async ({ page }) => {
|
test('All brings them back, ribbon and all', async ({ page, storefront }) => {
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
|
|
||||||
await chooseAvailability(page, 'All');
|
await storefront.chooseAvailability('All');
|
||||||
|
|
||||||
await expect(cell(page, NAMES.sold)).toBeVisible();
|
await expect(storefront.gridCell(NAMES.sold)).toBeVisible();
|
||||||
await expect(cell(page, NAMES.sold)).toContainText('SOLD');
|
await expect(storefront.gridCell(NAMES.sold)).toContainText('SOLD');
|
||||||
await expect(cell(page, NAMES.available)).toBeVisible();
|
await expect(storefront.gridCell(NAMES.available)).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Sold shows only the sold ones', async ({ page }) => {
|
test('Sold shows only the sold ones', async ({ page, storefront }) => {
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
|
|
||||||
await chooseAvailability(page, 'Sold');
|
await storefront.chooseAvailability('Sold');
|
||||||
|
|
||||||
await expect(cell(page, NAMES.sold)).toBeVisible();
|
await expect(storefront.gridCell(NAMES.sold)).toBeVisible();
|
||||||
await expect(cell(page, NAMES.available)).toHaveCount(0);
|
await expect(storefront.gridCell(NAMES.available)).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The filter is view state that belongs in the URL, like every other filter
|
// The filter is view state that belongs in the URL, like every other filter
|
||||||
// here, so a chosen view can be linked and survives a reload.
|
// here, so a chosen view can be linked and survives a reload.
|
||||||
test('the choice survives a reload, because it lives in the URL', async ({ page }) => {
|
test('the choice survives a reload, because it lives in the URL', async ({ page, storefront }) => {
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
await chooseAvailability(page, 'All');
|
await storefront.chooseAvailability('All');
|
||||||
await expect(cell(page, NAMES.sold)).toBeVisible();
|
await expect(storefront.gridCell(NAMES.sold)).toBeVisible();
|
||||||
|
|
||||||
await page.reload();
|
await page.reload();
|
||||||
|
|
||||||
await expect(cell(page, NAMES.sold)).toBeVisible();
|
await expect(storefront.gridCell(NAMES.sold)).toBeVisible();
|
||||||
await expect(page.getByRole('radio', { name: 'All' })).toBeChecked();
|
await expect(page.getByRole('radio', { name: 'All' })).toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Not sold is the default, so it is held as "no preference" rather than as an
|
// Not sold is the default, so it is held as "no preference" rather than as an
|
||||||
// explicit list. Putting it in the URL would make the default look like a
|
// explicit list. Putting it in the URL would make the default look like a
|
||||||
// choice somebody made, and would show it in the Filters (N) count.
|
// choice somebody made, and would show it in the Filters (N) count.
|
||||||
test('returning to Not sold leaves no status in the URL', async ({ page }) => {
|
test('returning to Not sold leaves no status in the URL', async ({ page, storefront }) => {
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
await chooseAvailability(page, 'All');
|
await storefront.chooseAvailability('All');
|
||||||
await expect(page).toHaveURL(/status=/);
|
await expect(page).toHaveURL(/status=/);
|
||||||
|
|
||||||
await chooseAvailability(page, 'Not sold');
|
await storefront.chooseAvailability('Not sold');
|
||||||
|
|
||||||
await expect(page).not.toHaveURL(/status=/);
|
await expect(page).not.toHaveURL(/status=/);
|
||||||
await expect(cell(page, NAMES.sold)).toHaveCount(0);
|
await expect(storefront.gridCell(NAMES.sold)).toHaveCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
// The control sits beside the Filters button rather than inside the drawer,
|
// The control sits beside the Filters button rather than inside the drawer,
|
||||||
// so its state must not be counted as one of the drawer's filters.
|
// so its state must not be counted as one of the drawer's filters.
|
||||||
test('does not inflate the Filters count', async ({ page }) => {
|
test('does not inflate the Filters count', async ({ page, storefront }) => {
|
||||||
await page.goto('/');
|
await storefront.goto();
|
||||||
await chooseAvailability(page, 'Sold');
|
await storefront.chooseAvailability('Sold');
|
||||||
|
|
||||||
// Matched loosely and asserted on the text, because antd's icon contributes
|
// Matched loosely and asserted on the text, because antd's icon contributes
|
||||||
// its own aria-label to the button's accessible name. The text is the part
|
// its own aria-label to the button's accessible name. The text is the part
|
||||||
// that would gain a "(1)" if status were counted as a drawer filter.
|
// that would gain a "(1)" if status were counted as a drawer filter.
|
||||||
await expect(page.getByRole('button', { name: /Filters/ })).toHaveText('Filters');
|
await expect(storefront.filtersButton).toHaveText('Filters');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user