diff --git a/package-lock.json b/package-lock.json index 59fa42dea1a..e69a9f919b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23069,6 +23069,20 @@ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", "license": "MIT" }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -31339,6 +31353,34 @@ } } }, + "node_modules/less/node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "optional": true, + "peer": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -34678,6 +34720,24 @@ "ncp": "bin/ncp" } }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, "node_modules/negotiator": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", @@ -38547,6 +38607,14 @@ "node": ">=0.8.0" } }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -46306,6 +46374,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", diff --git a/packages/atomic/.storybook/main.mts b/packages/atomic/.storybook/main.mts index fe785ebbfb3..2dfd6440d26 100644 --- a/packages/atomic/.storybook/main.mts +++ b/packages/atomic/.storybook/main.mts @@ -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; @@ -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; }, diff --git a/packages/atomic/package.json b/packages/atomic/package.json index 8eb6ae43311..ebc1350afcf 100644 --- a/packages/atomic/package.json +++ b/packages/atomic/package.json @@ -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", diff --git a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.new.stories.tsx b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.new.stories.tsx index bbddff883fd..e2a7ba2e5df 100644 --- a/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.new.stories.tsx +++ b/packages/atomic/src/components/recommendations/atomic-recs-list/atomic-recs-list.new.stories.tsx @@ -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', @@ -23,6 +31,12 @@ const meta: Meta = { actions: { handles: events, }, + msw: { + handlers: [...mockedSearchApi.handlers], + }, + }, + beforeEach: () => { + mockedSearchApi.searchEndpoint.flushQueuedResponses(); }, args, argTypes, @@ -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, }; diff --git a/packages/atomic/src/components/recommendations/atomic-recs-list/e2e/atomic-recs-list.e2e.ts b/packages/atomic/src/components/recommendations/atomic-recs-list/e2e/atomic-recs-list.e2e.ts index f3c5f189ff9..b0f2569cecf 100644 --- a/packages/atomic/src/components/recommendations/atomic-recs-list/e2e/atomic-recs-list.e2e.ts +++ b/packages/atomic/src/components/recommendations/atomic-recs-list/e2e/atomic-recs-list.e2e.ts @@ -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' ); diff --git a/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.new.stories.tsx b/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.new.stories.tsx index 700ade4543b..63f034ba78c 100644 --- a/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.new.stories.tsx +++ b/packages/atomic/src/components/search/atomic-did-you-mean/atomic-did-you-mean.new.stories.tsx @@ -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'; diff --git a/packages/atomic/src/components/search/atomic-generated-answer/e2e/mock-answering-api.ts b/packages/atomic/src/components/search/atomic-generated-answer/e2e/mock-answering-api.ts index 2f7c17f409a..d2db349b23b 100644 --- a/packages/atomic/src/components/search/atomic-generated-answer/e2e/mock-answering-api.ts +++ b/packages/atomic/src/components/search/atomic-generated-answer/e2e/mock-answering-api.ts @@ -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[ diff --git a/packages/atomic/src/components/search/atomic-load-more-results/atomic-load-more-results.new.stories.tsx b/packages/atomic/src/components/search/atomic-load-more-results/atomic-load-more-results.new.stories.tsx index 0251850a5e7..16c7be1787d 100644 --- a/packages/atomic/src/components/search/atomic-load-more-results/atomic-load-more-results.new.stories.tsx +++ b/packages/atomic/src/components/search/atomic-load-more-results/atomic-load-more-results.new.stories.tsx @@ -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( @@ -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, }; @@ -198,8 +215,4 @@ export const InPage: Story = { `, ], - play: async (context) => { - await play(context); - await playExecuteFirstSearch(context); - }, }; diff --git a/packages/atomic/storybook-utils/api/_base.ts b/packages/atomic/storybook-utils/api/_base.ts new file mode 100644 index 00000000000..c4c51281fcd --- /dev/null +++ b/packages/atomic/storybook-utils/api/_base.ts @@ -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 { + private nextResponses: ((response: TResponse) => TResponse)[] = []; + private baseResponse: Readonly; + private initialBaseResponse: Readonly; + constructor( + private method: HttpMethod, + private path: string, + initialBaseResponse: TResponse, + private schemaValidator?: ValidateFunction + ) { + 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 { + const responseCandidate = + this.nextResponses.shift()?.(this.baseResponse) ?? this.baseResponse; + this.validateResponse(responseCandidate); + return HttpResponse.json(responseCandidate); + } + + validateResponse( + 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]( + this.path, + () => this.getNextResponse() + ); + } +} diff --git a/packages/atomic/storybook-utils/api/search.ts b/packages/atomic/storybook-utils/api/search.ts deleted file mode 100644 index 2739c24615c..00000000000 --- a/packages/atomic/storybook-utils/api/search.ts +++ /dev/null @@ -1,81 +0,0 @@ -export const baseSearchResponse = { - totalCount: 120, - totalCountFiltered: 120, - duration: 175, - indexDuration: 18, - requestDuration: 41, - searchUid: '2971cca7-90b4-4b64-97dd-5dac45c1cc43', - pipeline: 'genqatest', - apiVersion: 2, - queryCorrections: [], - basicExpression: 'how to resolve netflix connection with tivo', - advancedExpression: null, - largeExpression: null, - constantExpression: null, - disjunctionExpression: null, - mandatoryExpression: null, - userIdentities: [ - { - name: 'anonymous_user@anonymous.coveo.com', - provider: 'Email Security Provider', - type: 'User', - }, - ], - rankingExpressions: [], - topResults: [], - executionReport: {}, - refinedKeywords: [], - triggers: [], - termsToHighlight: {}, - phrasesToHighlight: {}, - groupByResults: [], - facets: [], - suggestedFacets: [], - categoryFacets: [], - results: [ - { - title: 'Sample Result 1', - excerpt: 'This is a sample result excerpt for testing.', - clickUri: 'https://example.com/search/1', - uniqueId: 'rec-1', - raw: { - systitle: 'Sample Result 1', - sysdescription: 'This is a sample result excerpt for testing.', - sysuri: 'https://example.com/search/1', - }, - }, - { - title: 'Sample Result 2', - excerpt: 'This is another sample result for testing purposes.', - clickUri: 'https://example.com/search/2', - uniqueId: 'rec-2', - raw: { - systitle: 'Sample Result 2', - sysdescription: 'This is another sample result for testing purposes.', - sysuri: 'https://example.com/search/2', - }, - }, - { - title: 'Sample Result 3', - excerpt: 'Third sample result with useful content.', - clickUri: 'https://example.com/search/3', - uniqueId: 'rec-3', - raw: { - systitle: 'Sample Result 3', - sysdescription: 'Third sample result with useful content.', - sysuri: 'https://example.com/search/3', - }, - }, - ], - questionAnswer: { - answerFound: false, - question: '', - answerSnippet: '', - documentId: { - contentIdKey: '', - contentIdValue: '', - }, - score: 0.0, - relatedQuestions: [], - }, -}; diff --git a/packages/atomic/storybook-utils/api/search/mock.ts b/packages/atomic/storybook-utils/api/search/mock.ts new file mode 100644 index 00000000000..5e5688b37a9 --- /dev/null +++ b/packages/atomic/storybook-utils/api/search/mock.ts @@ -0,0 +1,36 @@ +import type {HttpHandler} from 'msw'; +import {EndpointHarness, type MockApi} from '../_base.js'; +import {baseResponse as baseQuerySuggestResponse} from './querySuggest-response.js'; +import { + baseResponse as baseSearchResponse, + validateResponse as validateSearchResponse, +} from './search-response.js'; + +export class MockSearchApi implements MockApi { + readonly searchEndpoint; + readonly querySuggestEndpoint; + + constructor(basePath: string = 'https://:orgId.org.coveo.com') { + this.searchEndpoint = new EndpointHarness( + 'POST', + `${basePath}/rest/search/v2`, + baseSearchResponse, + validateSearchResponse + ); + this.querySuggestEndpoint = new EndpointHarness( + 'POST', + `${basePath}/rest/search/v2/querySuggest`, + baseQuerySuggestResponse + ); + } + + get handlers(): HttpHandler[] { + return [ + this.searchEndpoint.generateHandler(), + this.querySuggestEndpoint.generateHandler(), + ]; + } +} + +// TODO: Remove exports once the concept is fully internalized. +export {baseSearchResponse}; diff --git a/packages/atomic/storybook-utils/api/search/open-api-schema.ts b/packages/atomic/storybook-utils/api/search/open-api-schema.ts new file mode 100644 index 00000000000..456a8215634 --- /dev/null +++ b/packages/atomic/storybook-utils/api/search/open-api-schema.ts @@ -0,0 +1,38 @@ +// biome-ignore-all lint/suspicious/noExplicitAny: imported files without exact typing + temp fix +import searchAPIOpenSpec from 'virtual:open-api-coveo/SearchApi?group=public'; +import {Ajv, type AnySchema} from 'ajv'; + +// Configure AJV with options to handle OpenAPI schemas +const ajv = new Ajv({ + strict: false, // Disable strict mode to allow unknown keywords + allErrors: true, // Get all validation errors, not just the first one + verbose: true, // Include schema and data in validation errors + logger: false, // Mute the log; +}); + +// Add all schemas from the OpenAPI spec to AJV so it can resolve references +const sanitizedComponents = searchAPIOpenSpec.components as any; + +// Make rankingInfo nullable ; SAPI defect, nullable missing + +( + sanitizedComponents.schemas.RestQueryResult as any +).properties.rankingInfo.nullable = true; + +// Handle nullable without type - convert to union with null; SAPI Defect RestQueryParentResult, allOf + nullable; https://github.com/OAI/OpenAPI-Specification/blob/main/proposals/2019-10-31-Clarify-Nullable.md#can-allof-be-used-to-define-a-nullable-subtype-of-a-non-nullable-base-schema-see-1368 +sanitizedComponents.schemas.RestQueryParentResult = { + anyOf: [ + ...(sanitizedComponents.schemas.RestQueryParentResult as any).allOf, + { + type: 'null', + }, + ], +}; + +// Add each schema to AJV with its proper ID +Object.entries(sanitizedComponents.schemas).forEach(([schemaName, schema]) => { + ajv.addSchema(schema as AnySchema, `#/components/schemas/${schemaName}`); +}); + +export const getSchemaValidator = (schemaName: string) => + ajv.compile(sanitizedComponents.schemas[schemaName]); diff --git a/packages/atomic/storybook-utils/api/search/querySuggest-response.ts b/packages/atomic/storybook-utils/api/search/querySuggest-response.ts new file mode 100644 index 00000000000..68e7c227ae9 --- /dev/null +++ b/packages/atomic/storybook-utils/api/search/querySuggest-response.ts @@ -0,0 +1,34 @@ +export const baseResponse = { + completions: [ + { + expression: 'coveo', + score: 100, + executableConfidence: 1.0, + highlighted: '[coveo]', + }, + { + expression: 'coveo platform', + score: 95, + executableConfidence: 0.95, + highlighted: '[coveo] platform', + }, + { + expression: 'coveo search', + score: 90, + executableConfidence: 0.9, + highlighted: '[coveo] search', + }, + { + expression: 'coveo ui kit', + score: 85, + executableConfidence: 0.85, + highlighted: '[coveo] ui kit', + }, + { + expression: 'coveo headless', + score: 80, + executableConfidence: 0.8, + highlighted: '[coveo] headless', + }, + ], +}; diff --git a/packages/atomic/storybook-utils/api/search/search-response.ts b/packages/atomic/storybook-utils/api/search/search-response.ts new file mode 100644 index 00000000000..a392f0a316b --- /dev/null +++ b/packages/atomic/storybook-utils/api/search/search-response.ts @@ -0,0 +1,87 @@ +import {getSchemaValidator} from './open-api-schema.js'; + +const getNthResult = (n: number) => ({ + title: `Sample Result ${n}`, + excerpt: 'This is a sample result excerpt for testing.', + clickUri: `https://example.com/search/${n}`, + uniqueId: `rec-${n}`, + raw: { + systitle: `Sample Result ${n}`, + sysdescription: 'This is a sample result excerpt for testing.', + sysuri: `https://example.com/search/${n}`, + }, + uri: `/example.com/search/${n}`, + printableUri: `https://printable-example.com/search/${n}`, + absentTerms: [], + childResults: [], + totalNumberOfChildResults: 0, + excerptHighlights: [], + parentResult: null, + printableUriHighlights: [], + firstSentences: null, + firstSentencesHighlights: [], + rankingInfo: null, + rating: 3, + score: 0, + percentScore: 42.0, + summary: null, + summaryHighlights: [], + titleHighlights: [], + isUserActionView: false, + isTopResult: false, + isRecommendation: false, + hasMobileHtmlVersion: false, + hasHtmlVersion: false, + flags: '', +}); + +export const baseResponse = { + totalCount: 120, + totalCountFiltered: 120, + duration: 175, + indexDuration: 18, + requestDuration: 41, + index: 'someIndex', + searchUid: '2971cca7-90b4-4b64-97dd-5dac45c1cc43', + pipeline: 'genqatest', + apiVersion: 2, + queryCorrections: [], + basicExpression: 'how to resolve netflix connection with tivo', + advancedExpression: null, + largeExpression: null, + constantExpression: null, + disjunctionExpression: null, + mandatoryExpression: null, + userIdentities: [ + { + name: 'anonymous_user@anonymous.coveo.com', + provider: 'Email Security Provider', + type: 'User', + }, + ], + rankingExpressions: [], + topResults: [], + executionReport: {}, + refinedKeywords: [], + triggers: [], + termsToHighlight: {}, + phrasesToHighlight: {}, + groupByResults: [], + facets: [], + suggestedFacets: [], + categoryFacets: [], + results: Array.from({length: 120}, (_, n) => getNthResult(n)), + questionAnswer: { + answerFound: false, + question: '', + answerSnippet: '', + documentId: { + contentIdKey: '', + contentIdValue: '', + }, + score: 0.0, + relatedQuestions: [], + }, +}; + +export const validateResponse = getSchemaValidator('RestQueryResponse');