Skip to content
Merged
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
10 changes: 9 additions & 1 deletion core/tests/fixtures/catalog/index.ts
Original file line number Diff line number Diff line change
@@ -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<Product> {
Expand All @@ -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<Category[]> {
return this.api.catalog.getCategories(filters);
}

getBrands(filters?: { nameLike?: string; ids?: number[] }): Promise<Brand[]> {
return this.api.catalog.getBrands(filters);
}

async cleanup() {
// no cleanup needed
}
Expand Down
70 changes: 69 additions & 1 deletion core/tests/fixtures/utils/api/catalog/http.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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<Product> => {
const resp = await httpClient
Expand All @@ -37,4 +75,34 @@ export const catalogHttpClient: CatalogApi = {

return resp.data;
},
getCategories: async (filters = {}): Promise<Category[]> => {
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<Brand[]> => {
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;
},
};
16 changes: 16 additions & 0 deletions core/tests/fixtures/utils/api/catalog/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Product>;
getCategories: (filters?: { nameLike?: string; ids?: number[] }) => Promise<Category[]>;
getBrands: (filters?: { nameLike?: string; ids?: number[] }) => Promise<Brand[]>;
}

export { catalogHttpClient } from './http';
24 changes: 21 additions & 3 deletions core/tests/ui/e2e/not-found.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
169 changes: 119 additions & 50 deletions core/tests/ui/e2e/search.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});