From ce366b5ec18f7457c2bb4d3d0013d4a098051628 Mon Sep 17 00:00:00 2001 From: jordanarldt Date: Fri, 6 Jun 2025 12:39:15 -0500 Subject: [PATCH] Rewrite e2e tests for not-found and search pages --- core/tests/fixtures/catalog/index.ts | 10 +- core/tests/fixtures/utils/api/catalog/http.ts | 70 +++++++- .../tests/fixtures/utils/api/catalog/index.ts | 16 ++ core/tests/ui/e2e/not-found.spec.ts | 24 ++- core/tests/ui/e2e/search.spec.ts | 169 ++++++++++++------ 5 files changed, 234 insertions(+), 55 deletions(-) diff --git a/core/tests/fixtures/catalog/index.ts b/core/tests/fixtures/catalog/index.ts index 7a2ccead5d..78952bbbad 100644 --- a/core/tests/fixtures/catalog/index.ts +++ b/core/tests/fixtures/catalog/index.ts @@ -1,6 +1,6 @@ import { testEnv } from '~/tests/environment'; import { Fixture } from '~/tests/fixtures/fixture'; -import { Product } from '~/tests/fixtures/utils/api/catalog'; +import { Brand, Category, Product } from '~/tests/fixtures/utils/api/catalog'; export class CatalogFixture extends Fixture { getDefaultProduct(): Promise { @@ -13,6 +13,14 @@ export class CatalogFixture extends Fixture { return this.api.catalog.getProductById(testEnv.DEFAULT_PRODUCT_ID); } + getCategories(filters?: { nameLike?: string; ids?: number[] }): Promise { + return this.api.catalog.getCategories(filters); + } + + getBrands(filters?: { nameLike?: string; ids?: number[] }): Promise { + return this.api.catalog.getBrands(filters); + } + async cleanup() { // no cleanup needed } diff --git a/core/tests/fixtures/utils/api/catalog/http.ts b/core/tests/fixtures/utils/api/catalog/http.ts index 2ca0eb663b..f96fd9059f 100644 --- a/core/tests/fixtures/utils/api/catalog/http.ts +++ b/core/tests/fixtures/utils/api/catalog/http.ts @@ -1,9 +1,11 @@ import { z } from 'zod'; +import { testEnv } from '~/tests/environment'; + import { httpClient } from '../client'; import { apiResponseSchema } from '../schema'; -import { CatalogApi, Product } from '.'; +import { Brand, CatalogApi, Category, Product } from '.'; const ProductSchema = z .object({ @@ -29,6 +31,42 @@ const ProductSchema = z }), ); +const CategorySchema = z + .object({ + category_id: z.number(), + parent_id: z.number(), + name: z.string(), + description: z.string(), + url: z.object({ + path: z.string(), + }), + }) + .transform( + (data): Category => ({ + categoryId: data.category_id, + parentId: data.parent_id, + name: data.name, + description: data.description, + path: data.url.path, + }), + ); + +const BrandSchema = z + .object({ + id: z.number(), + name: z.string(), + custom_url: z.object({ + url: z.string(), + }), + }) + .transform( + (data): Brand => ({ + id: data.id, + name: data.name, + path: data.custom_url.url, + }), + ); + export const catalogHttpClient: CatalogApi = { getProductById: async (id: number): Promise => { const resp = await httpClient @@ -37,4 +75,34 @@ export const catalogHttpClient: CatalogApi = { return resp.data; }, + getCategories: async (filters = {}): Promise => { + const trees = await httpClient + .get(`/v3/catalog/trees?channel_id:in=${testEnv.BIGCOMMERCE_CHANNEL_ID ?? 1}`) + .parse(apiResponseSchema(z.array(z.object({ id: z.number() })))); + + const params = new URLSearchParams({ + is_visible: 'true', + 'tree_id:in': trees.data.map((tree) => tree.id).join(','), + ...(filters.ids ? { 'id:in': filters.ids.join(',') } : {}), + ...(filters.nameLike ? { 'name:like': filters.nameLike } : {}), + }); + + const resp = await httpClient + .get(`/v3/catalog/trees/categories?${params}`) + .parse(apiResponseSchema(z.array(CategorySchema))); + + return resp.data; + }, + getBrands: async (filters = {}): Promise => { + const params = new URLSearchParams({ + ...(filters.ids ? { 'id:in': filters.ids.join(',') } : {}), + ...(filters.nameLike ? { 'name:like': filters.nameLike } : {}), + }); + + const resp = await httpClient + .get(`/v3/catalog/brands${params.size ? `?${params}` : ''}`) + .parse(apiResponseSchema(z.array(BrandSchema))); + + return resp.data; + }, }; diff --git a/core/tests/fixtures/utils/api/catalog/index.ts b/core/tests/fixtures/utils/api/catalog/index.ts index cb98bd8c8d..26b464798f 100644 --- a/core/tests/fixtures/utils/api/catalog/index.ts +++ b/core/tests/fixtures/utils/api/catalog/index.ts @@ -8,8 +8,24 @@ export interface Product { readonly path: string; } +export interface Category { + readonly categoryId: number; + readonly parentId: number; + readonly name: string; + readonly description: string; + readonly path: string; +} + +export interface Brand { + readonly id: number; + readonly name: string; + readonly path: string; +} + export interface CatalogApi { getProductById: (id: number) => Promise; + getCategories: (filters?: { nameLike?: string; ids?: number[] }) => Promise; + getBrands: (filters?: { nameLike?: string; ids?: number[] }) => Promise; } export { catalogHttpClient } from './http'; diff --git a/core/tests/ui/e2e/not-found.spec.ts b/core/tests/ui/e2e/not-found.spec.ts index bf5d85e830..782f962a31 100644 --- a/core/tests/ui/e2e/not-found.spec.ts +++ b/core/tests/ui/e2e/not-found.spec.ts @@ -1,7 +1,25 @@ import { expect, test } from '~/tests/fixtures'; +import { getTranslations } from '~/tests/lib/i18n'; -test('404 page', async ({ page }) => { - await page.goto('/unknown-url'); +test.describe('Not Found Page', () => { + test('Displays title, subtitle, and search button', async ({ page }) => { + const t = await getTranslations(); - await expect(page.getByRole('heading', { name: "We couldn't find that page!" })).toBeVisible(); + await page.goto('/unknown-url'); + + await expect(page.getByRole('heading', { name: t('NotFound.title') })).toBeVisible(); + await expect(page.getByText(t('NotFound.subtitle'))).toBeVisible(); + await expect( + page.getByRole('button', { name: t('Components.Header.Icons.search') }), + ).toBeVisible(); + }); + + test('Clicking the search button opens the search bar', async ({ page }) => { + const t = await getTranslations('Components.Header'); + + await page.goto('/unknown-url'); + + await page.getByRole('button', { name: t('Icons.search') }).click(); + await expect(page.getByRole('textbox', { name: t('Search.inputPlaceholder') })).toBeVisible(); + }); }); diff --git a/core/tests/ui/e2e/search.spec.ts b/core/tests/ui/e2e/search.spec.ts index eec5ff2a6c..5e8ccccb85 100644 --- a/core/tests/ui/e2e/search.spec.ts +++ b/core/tests/ui/e2e/search.spec.ts @@ -1,84 +1,153 @@ +import { faker } from '@faker-js/faker'; + import { expect, test } from '~/tests/fixtures'; +import { getTranslations } from '~/tests/lib/i18n'; -const productName = '[Sample] Smith Journal 13'; +test.describe('Search', () => { + test('Typing in the search bar displays quick search results', async ({ page, catalog }) => { + const t = await getTranslations('Components.Header'); + const { name } = await catalog.getDefaultProduct(); -test('Search for a product and press key to view result page', async ({ page }) => { - await page.goto('/'); + await page.goto('/'); + await page.getByRole('button', { name: t('Icons.search') }).click(); + await page.getByPlaceholder(t('Search.inputPlaceholder')).fill(name); + await page.waitForLoadState('networkidle'); - await page.getByLabel('Open search popup').click(); + await expect(page.getByRole('heading', { name: t('Search.categories') })).toBeVisible(); + await expect(page.getByRole('heading', { name: t('Search.brands') })).toBeVisible(); + await expect(page.getByRole('heading', { name: t('Search.products') })).toBeVisible(); - const searchBox = page.getByPlaceholder('Search...'); + const searchResultLocator = page.getByRole('region', { name: t('Search.products') }); - await expect(searchBox).toBeVisible(); + await expect(searchResultLocator.getByRole('link', { name })).toBeVisible(); + }); - await searchBox.fill(productName); - await searchBox.press('Enter'); + test('Typing in the search bar and pressing Enter goes to the Search Results page', async ({ + page, + catalog, + }) => { + const t = await getTranslations(); + const { name } = await catalog.getDefaultProduct(); - await expect(page.getByRole('link', { name: productName })).toBeVisible(); -}); + await page.goto('/'); + await page.getByRole('button', { name: t('Components.Header.Icons.search') }).click(); -test('Search for a product and wait for results', async ({ page }) => { - await page.goto('/'); + const searchInput = page.getByPlaceholder(t('Components.Header.Search.inputPlaceholder')); - await page.getByLabel('Open search popup').click(); + await searchInput.fill(name); + await searchInput.press('Enter'); + await page.waitForLoadState('networkidle'); - const searchBox = page.getByPlaceholder('Search...'); + await expect( + page.getByRole('heading', { name: t('Faceted.Search.searchResults') }), + ).toBeVisible(); + await expect(page.getByRole('link', { name })).toBeVisible(); + }); - await searchBox.fill('Able Brewing System'); + test('Searching by SKU returns the product in the search results', async ({ page, catalog }) => { + const t = await getTranslations('Components.Header'); + const { name, sku } = await catalog.getDefaultProduct(); - await expect(page.getByRole('link', { name: '[Sample] Able Brewing System' })).toBeVisible(); -}); + await page.goto('/'); + await page.getByRole('button', { name: t('Icons.search') }).click(); + await page.getByPlaceholder(t('Search.inputPlaceholder')).fill(sku); + await page.waitForLoadState('networkidle'); -test('Search a product with SKU', async ({ page }) => { - await page.goto('/'); + const searchResultLocator = page.getByRole('region', { name: t('Search.products') }); - await page.getByLabel('Open search popup').click(); + await expect(searchResultLocator.getByRole('link', { name })).toBeVisible(); + }); - const searchBox = page.getByPlaceholder('Search...'); + test('Searching for non-existent product displays no results', async ({ page }) => { + const t = await getTranslations(); + const randomSearchTerm = faker.string.alphanumeric(10); - await searchBox.fill('SM13'); + await page.goto('/'); + await page.getByRole('button', { name: t('Components.Header.Icons.search') }).click(); + await page + .getByPlaceholder(t('Components.Header.Search.inputPlaceholder')) + .fill(randomSearchTerm); - await expect(page.getByRole('link', { name: productName })).toBeVisible(); -}); + await page.waitForLoadState('networkidle'); -test('Search a product category', async ({ page }) => { - await page.goto('/'); + await expect( + page.getByText( + t('Components.Header.Search.noSearchResultsTitle', { term: randomSearchTerm }), + ), + ).toBeVisible(); - await page.getByLabel('Open search popup').click(); + await expect( + page.getByText(t('Components.Header.Search.noSearchResultsSubtitle')), + ).toBeVisible(); + }); - const searchBox = page.getByPlaceholder('Search...'); + test('Searching for non-existent product displays no results on the Search Results page', async ({ + page, + }) => { + const t = await getTranslations(); - await searchBox.fill('OFS'); + await page.goto('/'); + await page.getByRole('button', { name: t('Components.Header.Icons.search') }).click(); - await expect(page.getByRole('link', { name: '[Sample] Utility Caddy' })).toBeVisible(); - await expect(page.getByRole('link', { name: '[Sample] Tiered Wire Basket' })).toBeVisible(); - await expect(page.getByRole('link', { name: '[Sample] Dustpan & Brush' })).toBeVisible(); - await expect(page.getByRole('link', { name: '[Sample] 1 L Le Parfait Jar' })).toBeVisible(); - await expect(page.getByRole('link', { name: '[Sample] Canvas Laundry Cart' })).toBeVisible(); -}); + const searchInput = page.getByPlaceholder(t('Components.Header.Search.inputPlaceholder')); + const randomSearchTerm = faker.string.alphanumeric(10); -test('Search dialog sections', async ({ page }) => { - await page.goto('/'); + await searchInput.fill(randomSearchTerm); + await searchInput.press('Enter'); + await page.waitForLoadState('networkidle'); - await page.getByLabel('Open search popup').click(); + await expect( + page.getByText(t('Faceted.Search.Empty.title', { term: randomSearchTerm })), + ).toBeVisible(); - const searchBox = page.getByPlaceholder('Search...'); + await expect(page.getByText(t('Faceted.Search.Empty.subtitle'))).toBeVisible(); + }); - await searchBox.fill(productName); + test('Searching for a category displays in the search results', async ({ page, catalog }) => { + const t = await getTranslations('Components.Header'); + const categories = await catalog.getCategories(); + const category = categories[0]; - await expect(page.getByRole('heading', { name: 'Categories' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Products' })).toBeVisible(); - await expect(page.getByRole('heading', { name: 'Brands' })).toBeVisible(); -}); + if (!category) { + test.skip(true, 'No categories found in the catalog'); + + return; + } + + const { name } = category; + + await page.goto('/'); + await page.getByRole('button', { name: t('Icons.search') }).click(); + + await page.getByPlaceholder(t('Search.inputPlaceholder')).fill(name); + await page.waitForLoadState('networkidle'); + + const searchResultLocator = page.getByRole('region', { name: t('Search.categories') }); + + await expect(searchResultLocator.getByRole('link', { name })).toBeVisible(); + }); + + test('Searching for a brand displays in the search results', async ({ page, catalog }) => { + const t = await getTranslations('Components.Header'); + const brands = await catalog.getBrands(); + const brand = brands[0]; + + if (!brand) { + test.skip(true, 'No brands found in the catalog'); + + return; + } -test('Search not found', async ({ page }) => { - await page.goto('/'); + const { name } = brand; - await page.getByLabel('Open search popup').click(); + await page.goto('/'); + await page.getByRole('button', { name: t('Icons.search') }).click(); - const searchBox = page.getByPlaceholder('Search...'); + await page.getByPlaceholder(t('Search.inputPlaceholder')).fill(name); + await page.waitForLoadState('networkidle'); - await searchBox.fill('flora & fauna'); + const searchResultLocator = page.getByRole('region', { name: t('Search.brands') }); - await expect(page.getByText('Sorry, no results for "flora & fauna".')).toBeVisible(); + await expect(searchResultLocator.getByRole('link', { name })).toBeVisible(); + }); });