Skip to content

Commit 56a5d92

Browse files
fix(headless-react): fix error when using keys other than their name for WithProps controllers (#4783)
https://coveord.atlassian.net/browse/KIT-3786 ### Problem Right now we have "cart" and "context" hard coded. This means that if users use another key as "cart" for their cart implementation. Lets say we have `myCart: defineCart() ` This condition would fail ` if ('cart' in controllers) { ` ### Solution This PR fixes that problem by adding a _kind property to certain controllers and then changing the condition to `if (controller._kind == Kind.Cart) // where Kind is an enum`
1 parent 8a2cae4 commit 56a5d92

File tree

9 files changed

+152
-62
lines changed

9 files changed

+152
-62
lines changed

packages/headless-react/src/ssr-commerce/providers.tsx

Lines changed: 45 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,30 @@ import {
99
InferControllerStaticStateMapFromDefinitionsWithSolutionType,
1010
InferHydratedState,
1111
NavigatorContext,
12+
ControllerWithKind,
13+
Kind,
1214
SolutionType,
1315
Context,
1416
HydrateStaticStateOptions,
1517
ParameterManager,
16-
Parameters,
18+
Parameters, // Recommendations,
1719
} from '@coveo/headless/ssr-commerce';
1820
import {PropsWithChildren, useEffect, useState} from 'react';
1921
import {ReactCommerceEngineDefinition} from './commerce-engine.js';
2022

2123
type ControllerPropsMap = {[customName: string]: unknown};
2224
type UnknownAction = {type: string};
2325

26+
function getController<T extends Controller>(
27+
controllers: InferControllerStaticStateMapFromDefinitionsWithSolutionType<
28+
ControllerDefinitionsMap<Controller>,
29+
SolutionType
30+
>,
31+
key: string
32+
) {
33+
return controllers[key] as T;
34+
}
35+
2436
export function buildProviderWithDefinition<
2537
TControllers extends ControllerDefinitionsMap<Controller>,
2638
TSolutionType extends SolutionType,
@@ -48,24 +60,40 @@ export function buildProviderWithDefinition<
4860
const {searchActions, controllers} = staticState;
4961
const hydrateArguments: ControllerPropsMap = {};
5062

51-
if ('parameterManager' in controllers) {
52-
hydrateArguments.parameterManager = {
53-
initialState: {
54-
parameters: (
55-
controllers.parameterManager as ParameterManager<Parameters>
56-
).state.parameters,
57-
},
58-
};
59-
}
63+
for (const [key, controller] of Object.entries(controllers)) {
64+
const typedController = controller as ControllerWithKind;
6065

61-
if ('cart' in controllers) {
62-
hydrateArguments.cart = {
63-
initialState: {items: (controllers.cart as Cart).state.items},
64-
};
65-
}
66+
switch (typedController._kind) {
67+
case Kind.Cart: {
68+
const cart = getController<Cart>(controllers, key);
69+
hydrateArguments[key] = {
70+
initialState: {
71+
items: cart.state.items,
72+
},
73+
};
74+
break;
75+
}
76+
case Kind.Context: {
77+
const context = getController<Context>(controllers, key);
78+
hydrateArguments[key] = context.state;
79+
break;
80+
}
6681

67-
if ('context' in controllers) {
68-
hydrateArguments.context = (controllers.context as Context).state;
82+
case Kind.ParameterManager: {
83+
const parameterManager = getController<
84+
ParameterManager<Parameters>
85+
>(controllers, key);
86+
hydrateArguments[key] = {
87+
initialState: {
88+
parameters: parameterManager.state.parameters,
89+
},
90+
};
91+
break;
92+
}
93+
case Kind.Recommendations:
94+
//KIT-3801: Done here
95+
break;
96+
}
6997
}
7098

7199
const args: HydrateStaticStateOptions<UnknownAction> &

packages/headless/src/app/commerce-ssr-engine/common.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
ControllerDefinition,
1212
ControllerDefinitionOption,
1313
ControllerDefinitionsMap,
14+
ControllerWithKind,
1415
EngineStaticState,
1516
InferControllerFromDefinition,
1617
InferControllerPropsFromDefinition,
@@ -19,6 +20,12 @@ import {
1920
SolutionType,
2021
} from './types/common.js';
2122

23+
function hasKindProperty(
24+
controller: Controller | ControllerWithKind
25+
): controller is ControllerWithKind {
26+
return '_kind' in controller;
27+
}
28+
2229
export function createStaticState<TSearchAction extends UnknownAction>({
2330
searchActions,
2431
controllers,
@@ -32,6 +39,7 @@ export function createStaticState<TSearchAction extends UnknownAction>({
3239
return {
3340
controllers: mapObject(controllers, (controller) => ({
3441
state: clone(controller.state),
42+
...(hasKindProperty(controller) && {_kind: controller._kind}),
3543
})) as InferControllerStaticStateMapFromControllers<ControllersMap>,
3644
searchActions: searchActions.map((action) => clone(action)),
3745
};

packages/headless/src/app/commerce-ssr-engine/types/common.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type {
1515
HasOptionalKeys,
1616
} from '../../ssr-engine/types/common.js';
1717
import {SSRCommerceEngine} from '../factories/build-factory.js';
18+
import {Kind} from './kind.js';
1819

1920
export type {
2021
EngineDefinitionBuildResult,
@@ -59,6 +60,10 @@ export interface ControllerDefinitionWithoutProps<
5960
build(engine: SSRCommerceEngine, solutionType?: SolutionType): TController;
6061
}
6162

63+
export interface ControllerWithKind extends Controller {
64+
_kind: Kind;
65+
}
66+
6267
export interface ControllerDefinitionWithProps<
6368
TController extends Controller,
6469
TProps,
@@ -75,7 +80,7 @@ export interface ControllerDefinitionWithProps<
7580
engine: SSRCommerceEngine,
7681
props: TProps,
7782
solutionType?: SolutionType
78-
): TController;
83+
): TController & ControllerWithKind;
7984
}
8085

8186
export interface EngineStaticState<
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export enum Kind {
2+
Cart = 'CART',
3+
Context = 'CONTEXT',
4+
ParameterManager = 'PARAMETER_MANAGER',
5+
Recommendations = 'RECOMMENDATIONS',
6+
}

packages/headless/src/controllers/commerce/context/cart/headless-cart.ssr.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {UniversalControllerDefinitionWithProps} from '../../../../app/commerce-ssr-engine/types/common.js';
2+
import {Kind} from '../../../../app/commerce-ssr-engine/types/kind.js';
23
import {Cart, buildCart, CartInitialState} from './headless-cart.js';
34

45
export type {CartState, CartItem, CartProps} from './headless-cart.js';
@@ -25,7 +26,11 @@ export function defineCart(): CartDefinition {
2526
search: true,
2627
standalone: true,
2728
recommendation: true,
28-
buildWithProps: (engine, props) =>
29-
buildCart(engine, {initialState: props.initialState}),
29+
buildWithProps: (engine, props) => {
30+
return {
31+
...buildCart(engine, {initialState: props.initialState}),
32+
_kind: Kind.Cart,
33+
};
34+
},
3035
};
3136
}

packages/headless/src/controllers/commerce/context/headless-context.ssr.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {UniversalControllerDefinitionWithProps} from '../../../app/commerce-ssr-engine/types/common.js';
2+
import {Kind} from '../../../app/commerce-ssr-engine/types/kind.js';
23
import {
34
Context,
45
buildContext,
@@ -27,6 +28,11 @@ export function defineContext(): ContextDefinition {
2728
search: true,
2829
standalone: true,
2930
recommendation: true,
30-
buildWithProps: (engine, props) => buildContext(engine, {options: props}),
31+
buildWithProps: (engine, props) => {
32+
return {
33+
...buildContext(engine, {options: props}),
34+
_kind: Kind.Context,
35+
};
36+
},
3137
};
3238
}

packages/headless/src/controllers/commerce/recommendations/headless-recommendations.ssr.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export function defineRecommendations(
4343
[recommendationInternalOptionKey]: {
4444
...props.options,
4545
},
46+
//@ts-expect-error fixed in KIT-3801
4647
buildWithProps: (
4748
engine,
4849
options: Omit<RecommendationsOptions, 'slotId'>

packages/headless/src/ssr-commerce.index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ export type {
104104
EngineDefinitionControllersPropsOption,
105105
HydratedState,
106106
OptionsTuple,
107+
ControllerWithKind,
107108
} from './app/commerce-ssr-engine/types/common.js';
109+
export {Kind} from './app/commerce-ssr-engine/types/kind.js';
108110
export type {
109111
EngineDefinition,
110112
InferStaticState,

packages/samples/headless-ssr-commerce/e2e/cart/cart.spec.ts

Lines changed: 70 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -77,27 +77,34 @@ test.describe('default', () => {
7777
test('should increase the quantity', async ({cart}) => {
7878
const item = cart.items.first();
7979

80-
const quantity = parseInt(
81-
(await (await cart.getItemQuantity(item)).textContent()) || ''
82-
);
83-
84-
expect(quantity).toBe(initialItemQuantity + 1);
80+
await expect
81+
.poll(async () => {
82+
return parseInt(
83+
(await (await cart.getItemQuantity(item)).textContent()) || ''
84+
);
85+
})
86+
.toBe(initialItemQuantity + 1);
8587
});
8688

8789
test('should increase the total price', async ({cart}) => {
8890
const item = cart.items.first();
8991

90-
const totalPrice = parseInt(
91-
(await (await cart.getItemTotalPrice(item)).textContent()) || ''
92-
);
93-
94-
expect(totalPrice).toBe(initialItemPrice * (initialItemQuantity + 1));
92+
await expect
93+
.poll(async () => {
94+
return parseInt(
95+
(await (await cart.getItemTotalPrice(item)).textContent()) || ''
96+
);
97+
})
98+
.toBe(initialItemPrice * (initialItemQuantity + 1));
9599
});
96100

97101
test('should increase the cart total', async ({cart}) => {
98-
const total = parseInt((await cart.total.textContent()) || '');
99-
100-
expect(total).toBe(initialCartTotal + initialItemPrice);
102+
await expect
103+
.poll(async () => {
104+
const total = parseInt((await cart.total.textContent()) || '');
105+
return total;
106+
})
107+
.toBe(initialCartTotal + initialItemPrice);
101108
});
102109
});
103110

@@ -124,27 +131,36 @@ test.describe('default', () => {
124131
test('should decrease the quantity', async ({cart}) => {
125132
const item = cart.items.nth(1);
126133

127-
const quantity = parseInt(
128-
(await (await cart.getItemQuantity(item)).textContent()) || ''
129-
);
130-
131-
expect(quantity).toBe(initialItemQuantity - 1);
134+
await expect
135+
.poll(async () => {
136+
const quantity = parseInt(
137+
(await (await cart.getItemQuantity(item)).textContent()) || ''
138+
);
139+
return quantity;
140+
})
141+
.toBe(initialItemQuantity - 1);
132142
});
133143

134144
test('should decrease the total price', async ({cart}) => {
135145
const item = cart.items.nth(1);
136146

137-
const totalPrice = parseInt(
138-
(await (await cart.getItemTotalPrice(item)).textContent()) || ''
139-
);
140-
141-
expect(totalPrice).toBe(initialItemPrice * (initialItemQuantity - 1));
147+
await expect
148+
.poll(async () => {
149+
const totalPrice = parseInt(
150+
(await (await cart.getItemTotalPrice(item)).textContent()) || ''
151+
);
152+
return totalPrice;
153+
})
154+
.toBe(initialItemPrice * (initialItemQuantity - 1));
142155
});
143156

144157
test('should decrease the cart total', async ({cart}) => {
145-
const total = parseInt((await cart.total.textContent()) || '');
146-
147-
expect(total).toBe(initialCartTotal - initialItemPrice);
158+
await expect
159+
.poll(async () => {
160+
const total = parseInt((await cart.total.textContent()) || '');
161+
return total;
162+
})
163+
.toBe(initialCartTotal - initialItemPrice);
148164
});
149165

150166
test('should not remove the item', async ({cart}) => {
@@ -174,15 +190,21 @@ test.describe('default', () => {
174190
});
175191

176192
test('should remove the item', async ({cart}) => {
177-
const cartItemsCount = await cart.items.count();
178-
179-
expect(cartItemsCount).toBe(initialCartItemsCount - 1);
193+
await expect
194+
.poll(async () => {
195+
const cartItemsCount = await cart.items.count();
196+
return cartItemsCount;
197+
})
198+
.toBe(initialCartItemsCount - 1);
180199
});
181200

182201
test('should decrease the cart total', async ({cart}) => {
183-
const total = parseInt((await cart.total.textContent()) || '');
184-
185-
expect(total).toBe(initialCartTotal - initialItemPrice * 1);
202+
await expect
203+
.poll(async () => {
204+
const total = parseInt((await cart.total.textContent()) || '');
205+
return total;
206+
})
207+
.toBe(initialCartTotal - initialItemPrice * 1);
186208
});
187209
});
188210
});
@@ -196,9 +218,11 @@ test.describe('default', () => {
196218
});
197219

198220
test('should remove the item', async ({cart}) => {
199-
const cartItemsCount = await cart.items.count();
200-
201-
expect(cartItemsCount).toBe(initialCartItemsCount - 1);
221+
await expect
222+
.poll(async () => {
223+
return await cart.items.count();
224+
})
225+
.toBe(initialCartItemsCount - 1);
202226
});
203227
});
204228

@@ -208,15 +232,20 @@ test.describe('default', () => {
208232
});
209233

210234
test('should remove all items', async ({cart}) => {
211-
const cartItemsCount = await cart.items.count();
212-
213-
expect(cartItemsCount).toBe(0);
235+
await expect
236+
.poll(async () => {
237+
return await cart.items.count();
238+
})
239+
.toBe(0);
214240
});
215241

216242
test('should set the cart total to 0', async ({cart}) => {
217-
const total = parseInt((await cart.total.textContent()) || '');
218-
219-
expect(total).toBe(0);
243+
await expect
244+
.poll(async () => {
245+
const total = parseInt((await cart.total.textContent()) || '');
246+
return total;
247+
})
248+
.toBe(0);
220249
});
221250
});
222251
});

0 commit comments

Comments
 (0)