Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/nextjs/components/CollectionBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ function SearchPage(props: SearchPageProps) {
<input
{...registerSearchField("query")}
id="search-q"
data-testid="search-input"
defaultValue={term}
key={term}
placeholder={content.search.placeholder}
Expand All @@ -280,6 +281,7 @@ function SearchPage(props: SearchPageProps) {
<button
type="submit"
className="rounded-button button-primary inline-flex h-11 items-center justify-center px-4 text-sm font-medium"
data-testid="search-submit"
>
{content.search.submit}
</button>
Expand All @@ -294,7 +296,7 @@ function SearchPage(props: SearchPageProps) {
</form>

{!term ? null : products.length === 0 ? (
<div className="py-12 text-center">
<div className="py-12 text-center" data-testid="product-results">
<p className="type-body text-on-surface">
{content.search.noResults.replace("{{ terms }}", `\u201c${term}\u201d`)}
</p>
Expand Down
2 changes: 2 additions & 0 deletions examples/nuxt/components/SearchBrowserContent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,15 @@ function visibleFilters(filters: AvailableFilter[]) {
<form method="get" action="/search" class="mt-8 flex max-w-xl gap-3">
<input
v-bind="registerSearchField('query')"
data-testid="search-input"
:value="term"
placeholder="Search products"
class="min-w-0 flex-1 rounded border border-black/15 px-4 py-2 text-base"
/>
<button
type="submit"
class="rounded bg-black px-5 py-2 text-sm font-semibold text-white hover:bg-black/80"
data-testid="search-submit"
>
Search
</button>
Expand Down
6 changes: 3 additions & 3 deletions packages/storefront-e2e/docs/storefront-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ The suite finds some elements with `data-testid` attributes and Playwright `getB

Collection and search pages must expose product cards as accessible links to product detail pages.

The suite reads only the visible `product-card` links inside `product-results`, so product links outside the results, such as recommendations, do not count. The collection test matches a card by its accessible name, and the collection filter test counts the unique product URL paths of the cards.
The suite reads only the visible `product-card` links inside `product-results`, so product links outside the results, such as recommendations, do not count. The collection and search tests match a card by its accessible name, and the collection filter test counts the unique product URL paths of the cards.

## Collection Filters

Expand All @@ -43,9 +43,9 @@ When a probed collection exposes a Storefront API PRICE_RANGE filter, the page m

## Search

The search page must expose a searchbox. A visible button with an accessible name containing Search is supported; otherwise the suite submits the searchbox with Enter.
The search page must expose a search input with `search-input`. If the page has a visible `search-submit` control, the suite clicks it; otherwise it submits the input with Enter.

The suite discovers a live product title, searches for it, and verifies that product appears in visible results.
The suite discovers a live product title, searches for it, and verifies that product appears in visible results. It looks for a visible `product-card` link whose accessible name contains the title inside `product-results` (see Product Results).

## Product Page

Expand Down
14 changes: 10 additions & 4 deletions packages/storefront-e2e/specs/search/search.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,19 @@ async function searchForMatchingProduct(
productTitle: string,
): Promise<boolean> {
await page.goto(searchPath);
const searchbox = page.getByRole("searchbox").first();
const searchbox = page.getByTestId("search-input").filter({ visible: true }).first();
await searchbox.fill(productTitle);
await submitSearch(searchbox);

return hasMatchingProductResult(page, productPathSegment, productTitle);
}

async function submitSearch(searchbox: Locator): Promise<void> {
const form = searchbox.locator("xpath=ancestor::form[1]");
const searchButton = form.getByRole("button", { name: /search/i }).first();
const searchButton = searchbox
.page()
.getByTestId("search-submit")
.filter({ visible: true })
.first();
if (await searchButton.isVisible().catch(() => false)) {
await searchButton.click();
return;
Expand All @@ -53,7 +56,10 @@ async function matchingProductLinkCount(
productTitle: string,
): Promise<number> {
const productTitleMatcher = new RegExp(escapeRegExp(productTitle), "i");
const productLinks = page.getByRole("link", { name: productTitleMatcher });
const productLinks = page
.getByTestId("product-results")
.getByTestId("product-card")
.and(page.getByRole("link", { name: productTitleMatcher }));
const count = Math.min(await productLinks.count(), MAX_LINK_DISCOVERY_PROBES);
let matchingLinkCount = 0;

Expand Down
11 changes: 10 additions & 1 deletion skills/hydrogen-storefront-testing/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ The suite finds a cart line by its product title inside the visible `cart-page`
| `product-results` | The element that holds the product results of a collection or search page |
| `product-card` | The product link of each result |

The suite reads only visible `product-card` links inside `product-results`. The collection test matches a product by the card's accessible name, and the collection filter test counts results by unique product URL path.
The suite reads only visible `product-card` links inside `product-results`. The collection and search tests match a product by the card's accessible name, and the collection filter test counts results by unique product URL path.

### Collection filters

Expand All @@ -69,6 +69,15 @@ The suite reads only visible `product-card` links inside `product-results`. The

Keep the checkbox inside its marked label. The suite clicks the label, expects the checkbox to be checked, and reads the count from the label text, such as `Red (4)`. `collection-filter-apply` is optional: it goes on the control that applies the filters, if the page has one, and the suite clicks it when it is visible. The price range test types into the visible `collection-filter-min-price`, and fails if it is not a labeled spinbutton.

### Search

| Test ID | Put it on |
| --- | --- |
| `search-input` | The search page query input |
| `search-submit` | The search page submit control (optional) |

The suite fills `search-input`, then clicks a visible `search-submit` or presses Enter. Then it looks for the searched product's card in `product-results`.

## How to adopt

1. Find the existing elements in the tables above in your storefront.
Expand Down
3 changes: 2 additions & 1 deletion templates/react-router/app/routes/search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ function SearchHeader({ term }: { term: string }) {
key={term}
{...registerSearchForm("query")}
id="search-q"
data-testid="search-input"
defaultValue={term}
placeholder="Search"
className="peer h-11 w-full ps-10 pe-14"
Expand Down Expand Up @@ -180,7 +181,7 @@ function ProductGrid({ products }: { products: readonly ProductNode[] }) {

function NoResults({ term }: { term: string }) {
return (
<div aria-live="polite">
<div aria-live="polite" data-testid="product-results">
<p className="type-body text-on-surface-secondary">No results for “{term}”</p>
<p className="text-on-surface-secondary mt-2 text-sm">
Check your spelling or try a more general term.
Expand Down
Loading