-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(headless commerce ssr): add SSR FacetGenerator (#4290)
https://coveord.atlassian.net/browse/KIT-3397 In order to make this work, I added an SSR-specific `FacetGenerator` controller. The new controller extends the CSR `FacetGenerator`. It overrides its `state` getter, omits its `facets` getter, and adds a new `getFacetController` method. Instead of returning a list of facet IDs to render, the SSR `FacetGenerator`'s `state` getter returns the full state of each facet. Note that this required me to do some refactoring across all types of Facet controllers in order to extract and export facet state "selector" functions (e.g., `getCoreFacetState`, `getRegularFacetState`). I took the opportunity to use RTK selectors when possible. The idea is that in an SSR scenario, you would use the SSR `FacetGenerator`'s `state` getter as the static state to initially render each facet component in your UI. You would use the `getFacetController` method when the `FacetGenerator` is hydrated to fetch each individual facet controller, and have their components subscribe to their respective states. Under the hood, the `getFacetController` method uses the CSR `FacetGenerator`'s `facets` getter, which relies on a memoized RTK selector that will only rebuild the facet controller if its `type` changes in the engine state (which should not happen unless the configuration changes in the CMH during the user's session). The samples for facets are mostly copied-and-pasted (and adapted for SSR) from #4274 and #4273, so if you've already reviewed these PRs, you can probably skim over those. --------- Co-authored-by: ylakhdar <ylakhdar@coveo.com> Co-authored-by: Alex Prudhomme <78121423+alexprudhomme@users.noreply.github.com>
- Loading branch information
1 parent
ce9f0be
commit 3f957f9
Showing
30 changed files
with
1,607 additions
and
188 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,7 @@ | |
"bloup", | ||
"bpsb", | ||
"btoashim", | ||
"CAPI", | ||
"cfcomment", | ||
"cfpage", | ||
"cfspace", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
164 changes: 164 additions & 0 deletions
164
.../controllers/commerce/core/facets/generator/headless-commerce-facet-generator.ssr.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
import {SolutionType} from '../../../../../app/commerce-ssr-engine/types/common'; | ||
import {AnyFacetResponse} from '../../../../../features/commerce/facets/facet-set/interfaces/response'; | ||
import {CommerceAppState} from '../../../../../state/commerce-app-state'; | ||
import {buildMockCategoryFacetSearch} from '../../../../../test/mock-category-facet-search'; | ||
import {buildMockCommerceFacetRequest} from '../../../../../test/mock-commerce-facet-request'; | ||
import { | ||
buildMockCategoryFacetResponse, | ||
buildMockCommerceDateFacetResponse, | ||
buildMockCommerceNumericFacetResponse, | ||
buildMockCommerceRegularFacetResponse, | ||
} from '../../../../../test/mock-commerce-facet-response'; | ||
import {buildMockCommerceState} from '../../../../../test/mock-commerce-state'; | ||
import { | ||
buildMockSSRCommerceEngine, | ||
MockedCommerceEngine, | ||
} from '../../../../../test/mock-engine-v2'; | ||
import {buildMockFacetSearch} from '../../../../../test/mock-facet-search'; | ||
import {buildProductListing} from '../../../product-listing/headless-product-listing'; | ||
import {buildSearch} from '../../../search/headless-search'; | ||
import {FacetType} from '../headless-core-commerce-facet'; | ||
import {GeneratedFacetControllers} from './headless-commerce-facet-generator'; | ||
import { | ||
buildFacetGenerator, | ||
FacetGenerator, | ||
FacetGeneratorOptions, | ||
} from './headless-commerce-facet-generator.ssr'; | ||
|
||
describe('SSR FacetGenerator', () => { | ||
let engine: MockedCommerceEngine; | ||
let state: CommerceAppState; | ||
let options: FacetGeneratorOptions; | ||
let facetGenerator: FacetGenerator; | ||
let facetsInEngineState: {facetId: string; type: FacetType}[]; | ||
|
||
function initEngine(preloadedState = buildMockCommerceState()) { | ||
engine = buildMockSSRCommerceEngine({...preloadedState}); | ||
} | ||
|
||
function initCommerceFacetGenerator() { | ||
facetGenerator = buildFacetGenerator(engine, options); | ||
} | ||
|
||
function setFacetState(config = facetsInEngineState) { | ||
for (const facet of config) { | ||
const {facetId, type} = facet; | ||
state.facetOrder.push(facetId); | ||
let response: AnyFacetResponse; | ||
switch (type) { | ||
case 'dateRange': | ||
response = buildMockCommerceDateFacetResponse({facetId, type}); | ||
break; | ||
case 'hierarchical': | ||
response = buildMockCategoryFacetResponse({facetId, type}); | ||
break; | ||
case 'numericalRange': | ||
response = buildMockCommerceNumericFacetResponse({facetId, type}); | ||
break; | ||
case 'regular': | ||
default: | ||
response = buildMockCommerceRegularFacetResponse({facetId, type}); | ||
} | ||
if (options.props.solutionType === SolutionType.listing) { | ||
state.productListing.facets.push(response); | ||
} else { | ||
state.commerceSearch.facets.push(response); | ||
} | ||
|
||
state.commerceFacetSet[facet.facetId] = { | ||
request: buildMockCommerceFacetRequest({ | ||
facetId: facet.facetId, | ||
type: facet.type, | ||
}), | ||
}; | ||
if (type === 'regular') { | ||
state.facetSearchSet[facet.facetId] = buildMockFacetSearch(); | ||
} else if (type === 'hierarchical') { | ||
state.categoryFacetSearchSet[facet.facetId] = | ||
buildMockCategoryFacetSearch(); | ||
} | ||
} | ||
} | ||
|
||
beforeEach(() => { | ||
jest.resetAllMocks(); | ||
}); | ||
describe.each([ | ||
{ | ||
solutionType: SolutionType.listing, | ||
buildGeneratedCSRFacetControllersFunction: () => | ||
buildProductListing(engine).facetGenerator().facets, | ||
}, | ||
{ | ||
solutionType: SolutionType.search, | ||
buildGeneratedCSRFacetControllersFunction: () => | ||
buildSearch(engine).facetGenerator().facets, | ||
}, | ||
])( | ||
'when solutionType is $solutionType', | ||
({solutionType, buildGeneratedCSRFacetControllersFunction}) => { | ||
let generatedCSRFacetControllers: GeneratedFacetControllers; | ||
beforeEach(() => { | ||
options = {props: {solutionType}}; | ||
facetsInEngineState = [ | ||
{ | ||
facetId: 'category-facet', | ||
type: 'hierarchical', | ||
}, | ||
{ | ||
facetId: 'date-facet', | ||
type: 'dateRange', | ||
}, | ||
{ | ||
facetId: 'numeric-facet', | ||
type: 'numericalRange', | ||
}, | ||
{ | ||
facetId: 'regular-facet', | ||
type: 'regular', | ||
}, | ||
]; | ||
state = buildMockCommerceState(); | ||
setFacetState(facetsInEngineState); | ||
initEngine(state); | ||
initCommerceFacetGenerator(); | ||
|
||
generatedCSRFacetControllers = | ||
buildGeneratedCSRFacetControllersFunction(); | ||
}); | ||
|
||
it('initialized', () => { | ||
expect(facetGenerator).toBeTruthy(); | ||
}); | ||
it('#state is an array containing the state of each facet', () => { | ||
expect(facetGenerator.state.length).toBe(4); | ||
expect( | ||
facetGenerator.state.map((facet) => ({ | ||
facetId: facet.facetId, | ||
type: facet.type, | ||
})) | ||
).toEqual(facetsInEngineState); | ||
}); | ||
|
||
it('#getFacetController returns facet controller for the given facet id and type', () => { | ||
for (const facetInEngineState of facetsInEngineState) { | ||
const {facetId, type} = facetInEngineState; | ||
const generatedSSRFacetController = facetGenerator.getFacetController( | ||
facetId, | ||
type | ||
); | ||
const generatedCSRFacetController = generatedCSRFacetControllers.find( | ||
(controller) => controller.state.facetId === facetId | ||
); | ||
expect(generatedSSRFacetController).toBeTruthy(); | ||
expect(generatedSSRFacetController?.type).toBe( | ||
generatedCSRFacetController?.type | ||
); | ||
expect(generatedSSRFacetController?.state).toEqual( | ||
generatedCSRFacetController?.state | ||
); | ||
} | ||
}); | ||
} | ||
); | ||
}); |
Oops, something went wrong.