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
69 changes: 69 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 3 additions & 17 deletions packages/atomic/.storybook/main.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ const virtualOpenApiModules: PluginImpl = () => {
resolveId(id) {
console.log('resolveId', id);
if (id.startsWith('virtual:open-api-coveo')) {
console.log('resolveId', id);
return id;
}
return null;
Expand All @@ -25,25 +24,12 @@ const virtualOpenApiModules: PluginImpl = () => {
'virtual:open-api-coveo',
'https://platform.cloud.coveo.com/api-docs'
);
console.log('load', id);
if (virtualModules.has(id)) {
return virtualModules.get(id);
}

try {
console.log(`Fetching OpenAPI spec from ${id}`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
}
const content = await response.json();
const moduleContent = `export default ${JSON.stringify(content, null, 2)};`;
virtualModules.set(id, moduleContent);
return moduleContent;
} catch (error) {
console.error(`Error fetching OpenAPI spec from ${url}:`, error);
throw error;
}
const moduleContent = `export default await (await fetch("${url}")).json();`;
virtualModules.set(id, moduleContent);
return moduleContent;
}
return null;
},
Expand Down
1 change: 1 addition & 0 deletions packages/atomic/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@
"@types/puppeteer": "7.0.4",
"@vitest/browser": "3.2.4",
"@wc-toolkit/storybook-helpers": "9.0.1",
"ajv": "8.17.1",
"axe-core": "4.10.3",
"cypress": "13.7.3",
"cypress-axe": "1.6.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import type {Meta, StoryObj as Story} from '@storybook/web-components-vite';
import {getStorybookHelpers} from '@wc-toolkit/storybook-helpers';
import {html} from 'lit';
import {HttpResponse, http} from 'msw';
import {baseSearchResponse} from '@/storybook-utils/api/search';
import {MockSearchApi} from '@/storybook-utils/api/search/mock';
import {parameters} from '@/storybook-utils/common/common-meta-parameters';
import {wrapInRecommendationInterface} from '@/storybook-utils/search/recs-interface-wrapper';

const mockedSearchApi = new MockSearchApi();

mockedSearchApi.searchEndpoint.modifyBaseResponse((response) => ({
...response,
results: response.results.slice(0, 30),
totalCount: 30,
totalCountFiltered: 30,
}));

const {decorator, play} = wrapInRecommendationInterface();
const {events, args, argTypes, template} = getStorybookHelpers(
'atomic-recs-list',
Expand All @@ -23,6 +31,12 @@ const meta: Meta = {
actions: {
handles: events,
},
msw: {
handlers: [...mockedSearchApi.handlers],
},
},
beforeEach: () => {
mockedSearchApi.searchEndpoint.flushQueuedResponses();
},
args,
argTypes,
Expand Down Expand Up @@ -116,37 +130,26 @@ export const RecsAsCarousel: Story = {

export const NotEnoughRecsForCarousel: Story = {
name: 'Not enough recommendations for carousel',
parameters: {
msw: {
handlers: [
http.post('**/search/v2', () => {
return HttpResponse.json({
...baseSearchResponse,
totalCount: 3,
totalCountFiltered: 3,
});
}),
],
},
beforeEach: () => {
mockedSearchApi.searchEndpoint.enqueueNextResponse((response) => ({
...response,
results: response.results.slice(0, 3),
totalCount: 3,
totalCountFiltered: 3,
}));
},
play,
};

export const NoRecommendations: Story = {
name: 'No recommendations',
parameters: {
msw: {
handlers: [
http.post('**/search/v2', () => {
return HttpResponse.json({
...baseSearchResponse,
totalCount: 0,
totalCountFiltered: 0,
results: [],
});
}),
],
},
beforeEach: async () => {
mockedSearchApi.searchEndpoint.enqueueNextResponse((response) => ({
...response,
totalCount: 0,
totalCountFiltered: 0,
results: [],
}));
},
play,
};
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ test.describe('with a carousel', () => {

await recsList.prevButton.click();
await recsList.prevButton.click();
await expect(recsList.indicators.nth(2)).toHaveAttribute(
await expect(recsList.indicators.last()).toHaveAttribute(
'part',
'indicator active-indicator'
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {Meta, StoryObj as Story} from '@storybook/web-components-vite';
import {getStorybookHelpers} from '@wc-toolkit/storybook-helpers';
import {html} from 'lit/static-html.js';
import {HttpResponse, http} from 'msw';
import {baseSearchResponse} from '@/storybook-utils/api/search';
import {baseSearchResponse} from '@/storybook-utils/api/search/mock';
import {parameters} from '@/storybook-utils/common/common-meta-parameters';
import {wrapInSearchInterface} from '@/storybook-utils/search/search-interface-wrapper';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import oapiSearch from 'virtual:open-api-coveo/SearchApi?group=public';
import {fromOpenApi} from '@mswjs/source/open-api';
import {delay, HttpResponse, http} from 'msw';
import type {OpenAPIV3} from 'openapi-types';
import {baseSearchResponse} from '@/storybook-utils/api/search';
import {baseSearchResponse} from '@/storybook-utils/api/search/mock';

Object.assign(
oapiSearch.paths['/rest/search/v2'].post.responses[200].content[
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type {Meta, StoryObj as Story} from '@storybook/web-components-vite';
import {getStorybookHelpers} from '@wc-toolkit/storybook-helpers';
import {html} from 'lit/static-html.js';
import {MockSearchApi} from '@/storybook-utils/api/search/mock';
import {parameters} from '@/storybook-utils/common/common-meta-parameters';
import {
playExecuteFirstSearch,
wrapInSearchInterface,
} from '@/storybook-utils/search/search-interface-wrapper';
import {wrapInSearchInterface} from '@/storybook-utils/search/search-interface-wrapper';

const searchApiHarness = new MockSearchApi();

const {decorator, play} = wrapInSearchInterface();
const {events, args, argTypes, template} = getStorybookHelpers(
Expand All @@ -25,10 +25,27 @@ const meta: Meta = {
actions: {
handles: events,
},
msw: {
handlers: [...searchApiHarness.handlers],
},
},
args,
argTypes,

beforeEach: () => {
searchApiHarness.searchEndpoint.flushQueuedResponses();
searchApiHarness.searchEndpoint.enqueueNextResponse((response) => ({
...response,
results: response.results.slice(0, 40),
}));
searchApiHarness.searchEndpoint.enqueueNextResponse((response) => ({
...response,
results: response.results.slice(40, 80),
}));
searchApiHarness.searchEndpoint.enqueueNextResponse((response) => ({
...response,
results: response.results.slice(80),
}));
},
play,
};

Expand Down Expand Up @@ -198,8 +215,4 @@ export const InPage: Story = {
</atomic-layout-section>
</atomic-search-layout>`,
],
play: async (context) => {
await play(context);
await playExecuteFirstSearch(context);
},
};
70 changes: 70 additions & 0 deletions packages/atomic/storybook-utils/api/_base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type {ValidateFunction} from 'ajv';
import {type HttpHandler, HttpResponse, http} from 'msw';
export abstract class MockApi {
abstract get handlers(): HttpHandler[];
}

type HttpMethod = 'GET' | 'POST';
export class EndpointHarness<TResponse extends {}> {
private nextResponses: ((response: TResponse) => TResponse)[] = [];
private baseResponse: Readonly<TResponse>;
private initialBaseResponse: Readonly<TResponse>;
constructor(
private method: HttpMethod,
private path: string,
initialBaseResponse: TResponse,
private schemaValidator?: ValidateFunction<unknown>
) {
this.initialBaseResponse = Object.freeze(initialBaseResponse);
this.baseResponse = Object.freeze(structuredClone(initialBaseResponse));
}

modifyBaseResponse(modifier: (base: TResponse) => TResponse) {
this.baseResponse = Object.freeze(modifier(this.baseResponse));
}

resetBaseResponse() {
this.modifyBaseResponse(() => this.initialBaseResponse);
}

enqueueNextResponse(responseMiddleware: (response: TResponse) => TResponse) {
this.nextResponses.push(responseMiddleware);
}

flushQueuedResponses() {
this.nextResponses.length = 0;
}

getNextResponse(): HttpResponse<TResponse> {
const responseCandidate =
this.nextResponses.shift()?.(this.baseResponse) ?? this.baseResponse;
this.validateResponse(responseCandidate);
return HttpResponse.json(responseCandidate);
}

validateResponse<TResponse extends {}>(
responseCandidate: TResponse
): true | never {
if (!this.schemaValidator) {
console.warn(
`No schema validator provided for endpoint [${this.method} ${this.path}]. Skipping validation.`
);
return true;
}
const isValid = this.schemaValidator(responseCandidate);
if (!isValid) {
console.error('Response validation errors:', this.schemaValidator.errors);
throw new Error(
`The response does not conform to the expected schema for endpoint [${this.method} ${this.path}].`
);
}
return isValid;
}

generateHandler() {
return http[this.method.toLowerCase() as Lowercase<HttpMethod>]<TResponse>(
this.path,
() => this.getNextResponse()
);
}
}
Loading
Loading