diff --git a/package-lock.json b/package-lock.json index c35d6a417ca..001dba45bb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48174,13 +48174,6 @@ "win32" ] }, - "node_modules/rrweb-cssom": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", - "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", - "dev": true, - "license": "MIT" - }, "node_modules/run-applescript": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", @@ -51210,26 +51203,6 @@ "node": ">=14.0.0" } }, - "node_modules/tldts": { - "version": "6.1.61", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.61.tgz", - "integrity": "sha512-rv8LUyez4Ygkopqn+M6OLItAOT9FF3REpPQDkdMx5ix8w4qkuE7Vo2o/vw1nxKQYmJDV8JpAMJQr1b+lTKf0FA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tldts-core": "^6.1.61" - }, - "bin": { - "tldts": "bin/cli.js" - } - }, - "node_modules/tldts-core": { - "version": "6.1.61", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.61.tgz", - "integrity": "sha512-In7VffkDWUPgwa+c9picLUxvb0RltVwTkSgMNFgvlGSWveCzGBemBqTsgJCL4EDFWZ6WH0fKTsot6yNhzy3ZzQ==", - "dev": true, - "license": "MIT" - }, "node_modules/tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", @@ -65637,7 +65610,6 @@ "@types/react-dom": "18.3.0", "eslint": "8.57", "eslint-config-next": "14.2.5", - "jsdom": "25.0.1", "typescript": "5.4.5" } }, @@ -65656,61 +65628,6 @@ "node": ">=18" } }, - "packages/samples/headless-ssr-commerce/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/cssstyle": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", - "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "rrweb-cssom": "^0.7.1" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "packages/samples/headless-ssr-commerce/node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -65725,95 +65642,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "packages/samples/headless-ssr-commerce/node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-encoding": "^3.1.1" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dev": true, - "license": "MIT", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/jsdom": { - "version": "25.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", - "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssstyle": "^4.1.0", - "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.0", - "html-encoding-sniffer": "^4.0.0", - "http-proxy-agent": "^7.0.2", - "https-proxy-agent": "^7.0.5", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.12", - "parse5": "^7.1.2", - "rrweb-cssom": "^0.7.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^5.0.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0", - "ws": "^8.18.0", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "canvas": "^2.11.2" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "packages/samples/headless-ssr-commerce/node_modules/nwsapi": { - "version": "2.2.13", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", - "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", - "dev": true, - "license": "MIT" - }, "packages/samples/headless-ssr-commerce/node_modules/playwright": { "version": "1.45.3", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz", @@ -65844,114 +65672,6 @@ "node": ">=18" } }, - "packages/samples/headless-ssr-commerce/node_modules/tough-cookie": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", - "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^6.1.32" - }, - "engines": { - "node": ">=16" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "iconv-lite": "0.6.3" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/whatwg-url": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", - "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "packages/samples/headless-ssr-commerce/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "packages/samples/headless-ssr-commerce/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "packages/samples/headless-ssr/app-router": { "name": "@coveo/headless-ssr-samples-app-router", "version": "0.0.0", diff --git a/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts b/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts index 34dc7594eb0..0bedc4f4807 100644 --- a/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts +++ b/packages/atomic/src/components/search/atomic-external/e2e/atomic-external.e2e.ts @@ -18,11 +18,12 @@ test.describe('when modifying state of a component (search box) that is a child await external.searchBox.press('Enter'); }); - test("other components' state under the same atomic-external should be affected", async ({ - external, - }) => { - await expect(external.querySummary).toHaveText(/hello/); - }); + test.fixme( + "other components' state under the same atomic-external should be affected", + async ({external}) => { + await expect(external.querySummary).toHaveText(/hello/); + } + ); test.fixme( "other components' state under the linked atomic-search-interface should be affected", diff --git a/packages/headless-react/src/ssr-commerce/index.ts b/packages/headless-react/src/ssr-commerce/index.ts index f93b491861b..fbb8accebff 100644 --- a/packages/headless-react/src/ssr-commerce/index.ts +++ b/packages/headless-react/src/ssr-commerce/index.ts @@ -1,4 +1,5 @@ export * from '@coveo/headless/ssr-commerce'; +export {buildProviderWithDefinition} from './providers.js'; export type {ReactCommerceEngineDefinition} from './commerce-engine.js'; export {MissingEngineProviderError} from '../errors.js'; export {defineCommerceEngine} from './commerce-engine.js'; diff --git a/packages/headless-react/src/ssr-commerce/providers.tsx b/packages/headless-react/src/ssr-commerce/providers.tsx new file mode 100644 index 00000000000..6b9e15388d9 --- /dev/null +++ b/packages/headless-react/src/ssr-commerce/providers.tsx @@ -0,0 +1,103 @@ +'use client'; + +import { + InferHydratedState, + InferStaticState, + NavigatorContext, +} from '@coveo/headless/ssr-commerce'; +import {PropsWithChildren, useEffect, useState} from 'react'; +import {defineCommerceEngine} from './commerce-engine.js'; + +interface WithDefinitionProps { + staticState: InferStaticState; + navigatorContext: NavigatorContext; +} + +type LooseDefinition = { + setNavigatorContextProvider: unknown; + build: unknown; + hydrateStaticState: unknown; + fetchStaticState: unknown; + HydratedStateProvider: unknown; + StaticStateProvider: unknown; +}; + +type RealDefinition = + | ReturnType['recommendationEngineDefinition'] + | ReturnType['listingEngineDefinition'] + | ReturnType['searchEngineDefinition'] + | ReturnType['standaloneEngineDefinition']; + +export function buildProviderWithDefinition(looseDefinition: LooseDefinition) { + return function WrappedProvider({ + staticState, + navigatorContext, + children, + }: PropsWithChildren) { + const definition = looseDefinition as RealDefinition; + type RecommendationHydratedState = InferHydratedState; + const [hydratedState, setHydratedState] = useState< + RecommendationHydratedState | undefined + >(undefined); + + definition.setNavigatorContextProvider(() => navigatorContext); + + useEffect(() => { + const {searchActions, controllers} = staticState; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const hydrateControllers: Record = {}; + + if ('cart' in controllers) { + hydrateControllers.cart = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + initialState: {items: (controllers as any).cart.state.items}, + }; + } + + if ('context' in controllers) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hydrateControllers.context = (controllers as any).context.state; + } + + definition + .hydrateStaticState({ + searchActions, + controllers: { + ...controllers, + ...hydrateControllers, + }, + }) + .then(({engine, controllers}) => { + setHydratedState({engine, controllers}); + }); + }, [staticState]); + + if (hydratedState) { + return ( + + {children} + + ); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const StaticStateProviderWithAnyControllers = (looseDefinition as any) + .StaticStateProvider as React.ComponentType<{ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + controllers: any; + children: React.ReactNode; + }>; + + return ( + + {children} + + ); + }; +} diff --git a/packages/headless/src/app/commerce-ssr-engine/types/common.ts b/packages/headless/src/app/commerce-ssr-engine/types/common.ts index 918cba55ade..fe449f46df9 100644 --- a/packages/headless/src/app/commerce-ssr-engine/types/common.ts +++ b/packages/headless/src/app/commerce-ssr-engine/types/common.ts @@ -210,11 +210,11 @@ export type RecommendationOnlyControllerDefinitionWithProps< > = ControllerDefinitionWithProps & RecommendationOnlyController; -export type UniversalControllerDefinitionWithoutProps< +export type NonRecommendationControllerDefinitionWithoutProps< TController extends Controller, > = ControllerDefinitionWithoutProps & UniversalController; -export type UniversalControllerDefinitionWithProps< +export type NonRecommendationControllerDefinitionWithProps< TController extends Controller, TProps, > = ControllerDefinitionWithProps & UniversalController; diff --git a/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts b/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts index 903c64708c6..ac1c12864ea 100644 --- a/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts +++ b/packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithProps} from '../../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithProps} from '../../../../app/commerce-ssr-engine/types/common.js'; import {Cart, buildCart, CartInitialState} from './headless-cart.js'; export type {CartState, CartItem, CartProps} from './headless-cart.js'; @@ -9,7 +9,10 @@ export interface CartBuildProps { } export interface CartDefinition - extends UniversalControllerDefinitionWithProps {} + extends NonRecommendationControllerDefinitionWithProps< + Cart, + CartBuildProps + > {} /** * Defines a `Cart` controller instance. diff --git a/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts b/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts index 3743e9d83b0..dbacda42a17 100644 --- a/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts +++ b/packages/headless/src/controllers/commerce/context/headless-context.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { Context, buildContext, @@ -11,7 +11,10 @@ export type {ContextState, Context, ContextProps} from './headless-context.js'; export type {View, UserLocation, ContextOptions}; export interface ContextDefinition - extends UniversalControllerDefinitionWithProps {} + extends NonRecommendationControllerDefinitionWithProps< + Context, + ContextOptions + > {} /** * Defines a `Context` controller instance. diff --git a/packages/headless/src/controllers/commerce/field-suggestions/headless-field-suggestions-generator.ssr.ts b/packages/headless/src/controllers/commerce/field-suggestions/headless-field-suggestions-generator.ssr.ts index c339b8d4762..ee89bd6b5bc 100644 --- a/packages/headless/src/controllers/commerce/field-suggestions/headless-field-suggestions-generator.ssr.ts +++ b/packages/headless/src/controllers/commerce/field-suggestions/headless-field-suggestions-generator.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { FieldSuggestionsGenerator, buildFieldSuggestionsGenerator, @@ -17,7 +17,7 @@ export type {GeneratedFieldSuggestionsControllers} from './headless-field-sugges export type {FieldSuggestionsGenerator}; export interface FieldSuggestionsGeneratorDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines the `FieldSuggestionsGenerator` controller for the purpose of server-side rendering. diff --git a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts index 46c09400d18..bf6941c388e 100644 --- a/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts +++ b/packages/headless/src/controllers/commerce/instant-products/headless-instant-products.ssr.ts @@ -1,4 +1,4 @@ -import {SearchAndListingControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { InstantProducts, InstantProductsProps, @@ -12,7 +12,7 @@ export type { export type {InstantProducts, InstantProductsProps}; export interface InstantProductsDefinition - extends SearchAndListingControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines the `InstantProducts` controller for the purpose of server-side rendering. @@ -28,6 +28,7 @@ export function defineInstantProducts( return { listing: true, search: true, + standalone: true, build: (engine) => buildInstantProducts(engine, props), }; } diff --git a/packages/headless/src/controllers/commerce/product-view/headless-product-view.ssr.ts b/packages/headless/src/controllers/commerce/product-view/headless-product-view.ssr.ts index 5882b96c6bb..8a7210c3fe6 100644 --- a/packages/headless/src/controllers/commerce/product-view/headless-product-view.ssr.ts +++ b/packages/headless/src/controllers/commerce/product-view/headless-product-view.ssr.ts @@ -1,5 +1,5 @@ import {CommerceEngine} from '../../../app/commerce-engine/commerce-engine.js'; -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { buildController, Controller, @@ -10,7 +10,7 @@ import { } from './headless-product-view.js'; export interface ProductViewDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines a `ProductView` controller instance. diff --git a/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts b/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts index 72aac7cee78..9db774096f1 100644 --- a/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts +++ b/packages/headless/src/controllers/commerce/recent-queries-list/headless-recent-queries-list.ssr.ts @@ -1,4 +1,4 @@ -import {SearchAndListingControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import { RecentQueriesList, RecentQueriesListProps, @@ -13,7 +13,7 @@ export type { export type {RecentQueriesList, RecentQueriesListProps}; export interface RecentQueriesListDefinition - extends SearchAndListingControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines the `RecentQueriesList` controller for the purpose of server-side rendering. @@ -30,6 +30,7 @@ export function defineRecentQueriesList( return { search: true, listing: true, + standalone: true, build: (engine) => buildRecentQueriesList(engine, props), }; } diff --git a/packages/headless/src/controllers/commerce/standalone-search-box/headless-standalone-search-box.ssr.ts b/packages/headless/src/controllers/commerce/standalone-search-box/headless-standalone-search-box.ssr.ts index c7d5c780426..f992138e3d6 100644 --- a/packages/headless/src/controllers/commerce/standalone-search-box/headless-standalone-search-box.ssr.ts +++ b/packages/headless/src/controllers/commerce/standalone-search-box/headless-standalone-search-box.ssr.ts @@ -1,4 +1,4 @@ -import {UniversalControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; +import {NonRecommendationControllerDefinitionWithoutProps} from '../../../app/commerce-ssr-engine/types/common.js'; import {StandaloneSearchBoxProps} from '../../standalone-search-box/headless-standalone-search-box.js'; import { StandaloneSearchBox, @@ -10,7 +10,7 @@ export type {StandaloneSearchBoxState} from './headless-standalone-search-box.js export type {StandaloneSearchBox, StandaloneSearchBoxProps}; export interface StandaloneSearchBoxDefinition - extends UniversalControllerDefinitionWithoutProps {} + extends NonRecommendationControllerDefinitionWithoutProps {} /** * Defines the `StandaloneSearchBox` controller for the purpose of server-side rendering. diff --git a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx index ec0c9bae99e..c5cefdea131 100644 --- a/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/(listing)/[category]/page.tsx @@ -5,8 +5,10 @@ import ContextDropdown from '@/components/context-dropdown'; import FacetGenerator from '@/components/facets/facet-generator'; import Pagination from '@/components/pagination'; import ProductList from '@/components/product-list'; -import ListingProvider from '@/components/providers/listing-provider'; -import RecommendationProvider from '@/components/providers/recommendation-provider'; +import { + ListingProvider, + RecommendationProvider, +} from '@/components/providers/providers'; import PopularBought from '@/components/recommendations/popular-bought'; import PopularViewed from '@/components/recommendations/popular-viewed'; import Sort from '@/components/sort'; diff --git a/packages/samples/headless-ssr-commerce/app/cart/page.tsx b/packages/samples/headless-ssr-commerce/app/cart/page.tsx index bc994c0db1e..5a448a8a870 100644 --- a/packages/samples/headless-ssr-commerce/app/cart/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/cart/page.tsx @@ -1,9 +1,12 @@ import * as externalCartAPI from '@/actions/external-cart-api'; import Cart from '@/components/cart'; import ContextDropdown from '@/components/context-dropdown'; -import RecommendationProvider from '@/components/providers/recommendation-provider'; -import SearchProvider from '@/components/providers/search-provider'; +import { + RecommendationProvider, + StandaloneProvider, +} from '@/components/providers/providers'; import PopularBought from '@/components/recommendations/popular-bought'; +import StandaloneSearchBox from '@/components/standalone-search-box'; import { recommendationEngineDefinition, searchEngineDefinition, @@ -39,12 +42,13 @@ export default async function Search() { ['popularBought'] ); return ( - -
+
+
- + ); } diff --git a/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx b/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx index 2208ae33475..e0a8cb8db28 100644 --- a/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/products/[productId]/page.tsx @@ -1,18 +1,18 @@ import * as externalCartAPI from '@/actions/external-cart-api'; import ContextDropdown from '@/components/context-dropdown'; -import ProductPage from '@/components/pages/product-page'; -import StandaloneProvider from '@/components/providers/standalone-provider'; +import {StandaloneProvider} from '@/components/providers/providers'; import StandaloneSearchBox from '@/components/standalone-search-box'; import {searchEngineDefinition} from '@/lib/commerce-engine'; import {NextJsNavigatorContext} from '@/lib/navigatorContextProvider'; import {defaultContext} from '@/utils/context'; import {headers} from 'next/headers'; -import {Suspense} from 'react'; export default async function ProductDescriptionPage({ params, + searchParams, }: { params: {productId: string}; + searchParams: Promise<{[key: string]: string | string[] | undefined}>; }) { // Sets the navigator context provider to use the newly created `navigatorContext` before fetching the app static state const navigatorContext = new NextJsNavigatorContext(headers()); @@ -35,6 +35,11 @@ export default async function ProductDescriptionPage({ }, }, }); + + const resolvedSearchParams = await searchParams; + const price = Number(resolvedSearchParams.price) ?? NaN; + const name = resolvedSearchParams.name ?? params.productId; + return ( Product description page - Loading...

}> - -
+

+ {name} ({params.productId}) - ${price} +

+
); } diff --git a/packages/samples/headless-ssr-commerce/app/search/page.tsx b/packages/samples/headless-ssr-commerce/app/search/page.tsx index c3917a7bce0..50b32f3310e 100644 --- a/packages/samples/headless-ssr-commerce/app/search/page.tsx +++ b/packages/samples/headless-ssr-commerce/app/search/page.tsx @@ -3,7 +3,7 @@ import BreadcrumbManager from '@/components/breadcrumb-manager'; import ContextDropdown from '@/components/context-dropdown'; import FacetGenerator from '@/components/facets/facet-generator'; import ProductList from '@/components/product-list'; -import SearchProvider from '@/components/providers/search-provider'; +import {SearchProvider} from '@/components/providers/providers'; import SearchBox from '@/components/search-box'; import ShowMore from '@/components/show-more'; import Summary from '@/components/summary'; diff --git a/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx b/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx deleted file mode 100644 index 58efd70d0ae..00000000000 --- a/packages/samples/headless-ssr-commerce/components/hydration-metadata.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { - ListingHydratedState, - ListingStaticState, - SearchHydratedState, - SearchStaticState, -} from '@/lib/commerce-engine'; -import {FunctionComponent} from 'react'; - -export interface HydrationMetadataProps { - staticState: SearchStaticState | ListingStaticState; - hydratedState?: SearchHydratedState | ListingHydratedState; -} - -export const HydrationMetadata: FunctionComponent = ({ - staticState, - hydratedState, -}) => { - return ( - <> -
- Hydrated:{' '} - -
- - Rendered page with{' '} - { - (hydratedState ?? staticState).controllers.productList.state.products - .length - }{' '} - products - -
- Items in cart:{' '} - {(hydratedState ?? staticState).controllers.cart.state.items.length} -
-
- Rendered on{' '} - - {new Date().toISOString()} - -
- - ); -}; diff --git a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx b/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx deleted file mode 100644 index 79232674ef2..00000000000 --- a/packages/samples/headless-ssr-commerce/components/pages/product-page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -'use client'; - -import { - standaloneEngineDefinition, - StandaloneHydratedState, - StandaloneStaticState, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {useSearchParams} from 'next/navigation'; -import {useEffect, useState} from 'react'; - -interface IProductPageProps { - staticState: StandaloneStaticState; - navigatorContext: NavigatorContext; - productId: string; -} - -export default function ProductPage(props: IProductPageProps) { - const [hydratedState, setHydratedState] = useState< - StandaloneHydratedState | undefined - >(undefined); - - const {staticState, navigatorContext, productId} = props; - - const searchParams = useSearchParams(); - - const price = Number(searchParams.get('price')) ?? NaN; - const name = searchParams.get('name') ?? productId; - - // Setting the navigator context provider also in client-side before hydrating the application - standaloneEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - standaloneEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - controllers: { - cart: { - initialState: {items: staticState.controllers.cart.state.items}, - }, - context: staticState.controllers.context.state, - }, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - const viewController = hydratedState?.controllers.productView; - - useEffect(() => { - viewController?.view({name, productId, price}); - }, [viewController, productId, name, price]); - - return ( - <> -

- {name} ({productId}) - ${price} -

-
- - ); -} diff --git a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx deleted file mode 100644 index 918919891fe..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/listing-provider.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client'; - -import { - listingEngineDefinition, - ListingHydratedState, - ListingStaticState, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; -import {HydrationMetadata} from '../hydration-metadata'; - -interface ListingPageProps { - staticState: ListingStaticState; - navigatorContext: NavigatorContext; -} - -export default function ListingProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - ListingHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - listingEngineDefinition.setNavigatorContextProvider(() => navigatorContext); - - useEffect(() => { - listingEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - controllers: { - cart: { - initialState: {items: staticState.controllers.cart.state.items}, - }, - context: staticState.controllers.context.state, - }, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {children} - - - ); - } else { - return ( - - {children} - - ); - } -} diff --git a/packages/samples/headless-ssr-commerce/components/providers/providers.tsx b/packages/samples/headless-ssr-commerce/components/providers/providers.tsx new file mode 100644 index 00000000000..0df7111416e --- /dev/null +++ b/packages/samples/headless-ssr-commerce/components/providers/providers.tsx @@ -0,0 +1,29 @@ +'use client'; + +import { + listingEngineDefinition, + recommendationEngineDefinition, + searchEngineDefinition, + standaloneEngineDefinition, +} from '@/lib/commerce-engine'; +import {buildProviderWithDefinition} from '@coveo/headless-react/ssr-commerce'; + +// Wraps listing pages to provide context for listing-specific hooks +export const ListingProvider = buildProviderWithDefinition( + listingEngineDefinition +); + +// Wraps search pages to provide context for search-specific hooks +export const SearchProvider = buildProviderWithDefinition( + searchEngineDefinition +); + +// Wraps recommendations, whether in a standalone, search, or listing page +export const RecommendationProvider = buildProviderWithDefinition( + recommendationEngineDefinition +); + +// Used for components that don’t require triggering a search or product fetch (e.g., cart pages, standalone search box) +export const StandaloneProvider = buildProviderWithDefinition( + standaloneEngineDefinition +); diff --git a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx deleted file mode 100644 index 6ea9c228236..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/recommendation-provider.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; - -import { - recommendationEngineDefinition, - RecommendationHydratedState, - RecommendationStaticState, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; - -interface RecommendationProviderProps { - staticState: RecommendationStaticState; - navigatorContext: NavigatorContext; -} - -export default function RecommendationProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - RecommendationHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - recommendationEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - recommendationEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } -} diff --git a/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx deleted file mode 100644 index d8726cdca2a..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/search-provider.tsx +++ /dev/null @@ -1,72 +0,0 @@ -'use client'; - -import { - SearchHydratedState, - SearchStaticState, - searchEngineDefinition, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; -import {HydrationMetadata} from '../hydration-metadata'; - -interface SearchPageProps { - staticState: SearchStaticState; - navigatorContext: NavigatorContext; -} - -export default function SearchProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - SearchHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - searchEngineDefinition.setNavigatorContextProvider(() => navigatorContext); - - useEffect(() => { - searchEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - controllers: { - cart: { - initialState: {items: staticState.controllers.cart.state.items}, - }, - context: staticState.controllers.context.state, - }, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - // controllers.popularBoughtRecs.refresh(); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {children} - - - ); - } else { - return ( - - {children} - - - ); - } -} diff --git a/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx b/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx deleted file mode 100644 index d7764a08f5a..00000000000 --- a/packages/samples/headless-ssr-commerce/components/providers/standalone-provider.tsx +++ /dev/null @@ -1,68 +0,0 @@ -'use client'; - -import { - StandaloneHydratedState, - StandaloneStaticState, - standaloneEngineDefinition, -} from '@/lib/commerce-engine'; -import {NavigatorContext} from '@coveo/headless-react/ssr-commerce'; -import {PropsWithChildren, useEffect, useState} from 'react'; - -interface StandalonePageProps { - staticState: StandaloneStaticState; - navigatorContext: NavigatorContext; -} - -export default function StandaloneProvider({ - staticState, - navigatorContext, - children, -}: PropsWithChildren) { - const [hydratedState, setHydratedState] = useState< - StandaloneHydratedState | undefined - >(undefined); - - // Setting the navigator context provider also in client-side before hydrating the application - standaloneEngineDefinition.setNavigatorContextProvider( - () => navigatorContext - ); - - useEffect(() => { - standaloneEngineDefinition - .hydrateStaticState({ - searchActions: staticState.searchActions, - controllers: { - cart: { - initialState: {items: staticState.controllers.cart.state.items}, - }, - context: staticState.controllers.context.state, - }, - }) - .then(({engine, controllers}) => { - setHydratedState({engine, controllers}); - - // Refreshing recommendations in the browser after hydrating the state in the client-side - // Recommendation refresh in the server is not supported yet. - // controllers.popularBoughtRecs.refresh(); - }); - }, [staticState]); - - if (hydratedState) { - return ( - - {children} - - ); - } else { - return ( - - {children} - - ); - } -} diff --git a/packages/samples/headless-ssr-commerce/e2e/cart/cart.spec.ts b/packages/samples/headless-ssr-commerce/e2e/cart/cart.spec.ts index 34ed42720ae..9513a9d5f92 100644 --- a/packages/samples/headless-ssr-commerce/e2e/cart/cart.spec.ts +++ b/packages/samples/headless-ssr-commerce/e2e/cart/cart.spec.ts @@ -1,4 +1,3 @@ -import {JSDOM} from 'jsdom'; import {test, expect} from './cart.fixture'; test.describe('default', () => { @@ -221,69 +220,3 @@ test.describe('default', () => { }); }); }); - -test.describe('ssr', () => { - const numItemsInCart = 0; // Define the numResults variable - const numItemsInCartMsg = `Items in cart: ${numItemsInCart}`; - - test(`renders page in SSR as expected`, async ({page}) => { - const responsePromise = page.waitForResponse('**/cart'); - await page.goto('/cart'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - expect(dom.window.document.querySelector('#cart-msg')?.textContent).toBe( - numItemsInCartMsg - ); - - expect(dom.window.document.querySelectorAll('ul#cart li').length).toBe( - numItemsInCart - ); - expect( - ( - dom.window.document.querySelector( - '#hydrated-indicator' - ) as HTMLInputElement - )?.checked - ).toBe(false); - }); - - test(`renders page in CSR as expected`, async ({page, cart, hydrated}) => { - await page.goto('/cart'); - await expect(hydrated.hydratedCartMessage).toHaveText(numItemsInCartMsg); - - expect(await cart.items.all()).toHaveLength(numItemsInCart); - - expect(await hydrated.hydratedIndicator).toBe(true); - }); - - test('renders product list in SSR and then in CSR', async ({ - page, - cart, - hydrated, - }) => { - const responsePromise = page.waitForResponse('**/cart'); - await page.goto('/cart'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - const ssrTimestamp = Date.parse( - dom.window.document.querySelector('#timestamp')!.textContent || '' - ); - - const hydratedTimestamp = Date.parse( - (await hydrated.hydratedTimestamp.textContent()) || '' - ); - - expect(ssrTimestamp).not.toBeNaN(); - await expect(hydrated.hydratedCartMessage).toHaveText(numItemsInCartMsg); - expect(await cart.items.all()).toHaveLength(numItemsInCart); - expect(hydratedTimestamp).toBeGreaterThan(ssrTimestamp); - }); -}); diff --git a/packages/samples/headless-ssr-commerce/e2e/listing/listing.spec.ts b/packages/samples/headless-ssr-commerce/e2e/listing/listing.spec.ts index 551f677eb74..64bed901937 100644 --- a/packages/samples/headless-ssr-commerce/e2e/listing/listing.spec.ts +++ b/packages/samples/headless-ssr-commerce/e2e/listing/listing.spec.ts @@ -1,4 +1,3 @@ -import {JSDOM} from 'jsdom'; import {test, expect} from './listing.fixture'; test.describe('default', () => { @@ -76,81 +75,3 @@ test.describe('default', () => { }); }); }); - -test.describe('ssr', () => { - const numResults = 9; // Define the numResults variable - const numResultsMsg = `Rendered page with ${numResults} products`; - - test(`renders page in SSR as expected`, async ({page}) => { - const responsePromise = page.waitForResponse('**/surf-accessories'); - await page.goto('/surf-accessories'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - expect( - dom.window.document.querySelector('#hydrated-msg')?.textContent - ).toBe(numResultsMsg); - - expect( - dom.window.document.querySelectorAll('[aria-label="Product List"] li') - .length - ).toBe(numResults); - - expect( - (dom.window.document.querySelector('#sorts-select') as HTMLSelectElement) - .selectedOptions[0]?.textContent - ).toBe('Relevance'); - - expect( - ( - dom.window.document.querySelector( - '#hydrated-indicator' - ) as HTMLInputElement - )?.checked - ).toBe(false); - }); - - test(`renders page in CSR as expected`, async ({ - page, - search, - sort, - hydrated, - }) => { - await page.goto('/surf-accessories'); - - await expect(hydrated.hydratedMessage).toHaveText(numResultsMsg); - expect(await search.productItems).toHaveLength(numResults); - expect(await sort.selectedOption.textContent()).toBe('Relevance'); - expect(await hydrated.hydratedIndicator).toBe(true); - }); - - test('renders product list in SSR and then in CSR', async ({ - page, - search, - hydrated, - }) => { - const responsePromise = page.waitForResponse('**/surf-accessories'); - await page.goto('/surf-accessories'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - const ssrTimestamp = Date.parse( - dom.window.document.querySelector('#timestamp')!.textContent || '' - ); - - const hydratedTimestamp = Date.parse( - (await hydrated.hydratedTimestamp.textContent()) || '' - ); - - expect(ssrTimestamp).not.toBeNaN(); - await expect(hydrated.hydratedMessage).toHaveText(numResultsMsg); - expect(await search.productItems).toHaveLength(numResults); - expect(hydratedTimestamp).toBeGreaterThan(ssrTimestamp); - }); -}); diff --git a/packages/samples/headless-ssr-commerce/e2e/search/search.spec.ts b/packages/samples/headless-ssr-commerce/e2e/search/search.spec.ts index fddee69787d..6c1ec777157 100644 --- a/packages/samples/headless-ssr-commerce/e2e/search/search.spec.ts +++ b/packages/samples/headless-ssr-commerce/e2e/search/search.spec.ts @@ -1,4 +1,3 @@ -import {JSDOM} from 'jsdom'; import {test, expect} from './search.fixture'; test.describe('default', () => { @@ -112,85 +111,3 @@ test.describe('default', () => { }); }); }); -test.describe('ssr', () => { - const numResults = 9; // Define the numResults variable - const numResultsMsg = `Rendered page with ${numResults} products`; - - test(`renders page in SSR as expected`, async ({page}) => { - const responsePromise = page.waitForResponse('**/search'); - await page.goto('/search'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - expect( - dom.window.document.querySelector('#hydrated-msg')?.textContent - ).toBe(numResultsMsg); - - expect( - dom.window.document.querySelectorAll('[aria-label="Product List"] li') - .length - ).toBe(numResults); - expect( - ( - dom.window.document.querySelector( - '#hydrated-indicator' - ) as HTMLInputElement - )?.checked - ).toBe(false); - }); - - test(`renders page in CSR as expected`, async ({page, search, hydrated}) => { - await page.goto('/search'); - await expect(hydrated.hydratedMessage).toHaveText(numResultsMsg); - - expect(await search.productItems).toHaveLength(numResults); - expect(await hydrated.hydratedIndicator).toBe(true); - }); - - test('renders product list in SSR and then in CSR', async ({ - page, - search, - hydrated, - }) => { - const responsePromise = page.waitForResponse('**/search'); - await page.goto('/search'); - - const response = await responsePromise; - const responseBody = await response.text(); - - const dom = new JSDOM(responseBody); - - const ssrTimestamp = Date.parse( - dom.window.document.querySelector('#timestamp')!.textContent || '' - ); - - const hydratedTimestamp = Date.parse( - (await hydrated.hydratedTimestamp.textContent()) || '' - ); - - expect(ssrTimestamp).not.toBeNaN(); - await expect(hydrated.hydratedMessage).toHaveText(numResultsMsg); - expect(await search.productItems).toHaveLength(numResults); - expect(hydratedTimestamp).toBeGreaterThan(ssrTimestamp); - }); - - test('after submitting a query, should change results', async ({ - page, - search, - facet, - }) => { - await page.goto('/search'); - const initialProducts = await search.productList.textContent(); - - await search.searchBox.fill('shoes'); - await search.searchButton.click(); - - await facet.facetLoading.waitFor({state: 'visible'}); - await facet.facetLoading.waitFor({state: 'hidden'}); - - expect(await search.productList.textContent()).not.toEqual(initialProducts); - }); -}); diff --git a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts index ad6db361dff..ef7b21019a3 100644 --- a/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts +++ b/packages/samples/headless-ssr-commerce/lib/commerce-engine.ts @@ -1,8 +1,4 @@ -import { - defineCommerceEngine, - InferStaticState, - InferHydratedState, -} from '@coveo/headless-react/ssr-commerce'; +import {defineCommerceEngine} from '@coveo/headless-react/ssr-commerce'; import engineConfig from './commerce-engine-config'; export const engineDefinition = defineCommerceEngine(engineConfig); @@ -36,29 +32,3 @@ export const { useFacetGenerator, useBreadcrumbManager, } = engineDefinition.controllers; - -export type ListingStaticState = InferStaticState< - typeof listingEngineDefinition ->; -export type ListingHydratedState = InferHydratedState< - typeof listingEngineDefinition ->; - -export type SearchStaticState = InferStaticState; -export type SearchHydratedState = InferHydratedState< - typeof searchEngineDefinition ->; - -export type RecommendationStaticState = InferStaticState< - typeof recommendationEngineDefinition ->; -export type RecommendationHydratedState = InferHydratedState< - typeof recommendationEngineDefinition ->; - -export type StandaloneStaticState = InferStaticState< - typeof standaloneEngineDefinition ->; -export type StandaloneHydratedState = InferHydratedState< - typeof standaloneEngineDefinition ->; diff --git a/packages/samples/headless-ssr-commerce/package.json b/packages/samples/headless-ssr-commerce/package.json index 718070a2460..1d64c81e5a4 100644 --- a/packages/samples/headless-ssr-commerce/package.json +++ b/packages/samples/headless-ssr-commerce/package.json @@ -23,7 +23,6 @@ "@playwright/test": "1.45.3", "eslint": "8.57", "eslint-config-next": "14.2.5", - "jsdom": "25.0.1", "typescript": "5.4.5" } }