@akinon/pz-theme 2.0.157 → 2.0.158-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/package.json +4 -4
- package/src/__tests__/editor-locale-bridge.test.tsx +32 -1
- package/src/__tests__/row-scope.test.tsx +118 -0
- package/src/blocks/__tests__/order-detail-return.test.tsx +70 -5
- package/src/blocks/__tests__/order-line-actions.test.tsx +54 -0
- package/src/blocks/__tests__/order-list-rows.test.tsx +112 -0
- package/src/blocks/__tests__/text-block.test.tsx +34 -6
- package/src/blocks/item-template.tsx +25 -19
- package/src/blocks/order-detail-block.tsx +155 -112
- package/src/blocks/order-line-actions.ts +45 -0
- package/src/blocks/order-list-block.tsx +25 -2
- package/src/blocks/row-scope.tsx +35 -0
- package/src/blocks/text-block.tsx +6 -2
- package/src/chrome/__tests__/page-context-builders.test.ts +14 -1
- package/src/chrome/page-context.ts +4 -0
- package/src/custom/README.md +120 -219
- package/src/custom/__tests__/use-theme-setting.test.tsx +97 -0
- package/src/custom/collect.ts +4 -9
- package/src/custom/property-types.ts +15 -15
- package/src/custom/use-theme-setting.ts +21 -5
- package/src/editor-locale-bridge.tsx +39 -11
- package/src/theme-placeholder-wrapper.tsx +3 -0
- package/src/utils/__tests__/order-action-rules.test.tsx +96 -0
- package/src/utils/__tests__/sanitize-rich-text.test.ts +80 -0
- package/src/utils/order-action-rules.ts +115 -0
- package/src/utils/sanitize-rich-text.ts +215 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,54 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
+
## 2.0.158-rc.14
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 8e040731: ZERO-5235: let brands hang actions on order detail lines
|
|
8
|
+
- c761b9fd: ZERO-5296: fall back to the storefront default language in the text block
|
|
9
|
+
- 17a598da: ZERO-5296: report the storefront default language to the editor
|
|
10
|
+
- 3b05a407: ZERO-5232: allow richtext settings on custom blocks
|
|
11
|
+
- ad5240c4: ZERO-5254: add custom render and theme editor support to pz-rate-review
|
|
12
|
+
- Updated dependencies [86187b1c2]
|
|
13
|
+
- Updated dependencies [0cf9ea239]
|
|
14
|
+
- Updated dependencies [97d29893]
|
|
15
|
+
- Updated dependencies [324f97d55]
|
|
16
|
+
- Updated dependencies [51ea06888]
|
|
17
|
+
- Updated dependencies
|
|
18
|
+
- Updated dependencies [2fea41430]
|
|
19
|
+
- Updated dependencies [b55acb768]
|
|
20
|
+
- Updated dependencies [760258c1c]
|
|
21
|
+
- Updated dependencies [143be2b9d]
|
|
22
|
+
- Updated dependencies [7889b08fe]
|
|
23
|
+
- Updated dependencies [9f8cd3bc5]
|
|
24
|
+
- Updated dependencies [bfafa3f49]
|
|
25
|
+
- Updated dependencies [ac6cada1]
|
|
26
|
+
- Updated dependencies [57d7eb305]
|
|
27
|
+
- Updated dependencies [d99a6a7d5]
|
|
28
|
+
- Updated dependencies [9db81a714]
|
|
29
|
+
- Updated dependencies [591e345e1]
|
|
30
|
+
- Updated dependencies [4de5303c5]
|
|
31
|
+
- Updated dependencies [95b139dc1]
|
|
32
|
+
- Updated dependencies [1d00f2d06]
|
|
33
|
+
- Updated dependencies [4ac7b2a1e]
|
|
34
|
+
- Updated dependencies [4998a9631]
|
|
35
|
+
- Updated dependencies [804d2bd6c]
|
|
36
|
+
- Updated dependencies [3909d3224]
|
|
37
|
+
- Updated dependencies [6a3d8a631]
|
|
38
|
+
- Updated dependencies [e18836b20]
|
|
39
|
+
- Updated dependencies [b1111d7b1]
|
|
40
|
+
- Updated dependencies [1595d458e]
|
|
41
|
+
- Updated dependencies [dca128112]
|
|
42
|
+
- @akinon/next@2.0.158-rc.14
|
|
43
|
+
- @akinon/theme-contract@2.0.158-rc.14
|
|
44
|
+
|
|
3
45
|
## 2.0.157
|
|
4
46
|
|
|
5
47
|
### Patch Changes
|
|
6
48
|
|
|
49
|
+
- 3002c2cc: ZERO-5302: The order detail's return and cancel links offer what commerce allows and what a storefront's order-action rules let through (`registerOrderActionRules`: which lines a return may start from, whether an order may be cancelled). With no rules registered, commerce's `is_refundable` and `is_cancellable` flags decide; the block no longer limits returns to delivered lines on its own.
|
|
7
50
|
- Updated dependencies [219164a3]
|
|
51
|
+
- Updated dependencies [9c778307]
|
|
8
52
|
- @akinon/next@2.0.157
|
|
9
53
|
- @akinon/theme-contract@2.0.157
|
|
10
54
|
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akinon/pz-theme",
|
|
3
3
|
"description": "Theme package for Project Zero Next — ThemePlaceholder system and theme editor infrastructure",
|
|
4
|
-
"version": "2.0.
|
|
4
|
+
"version": "2.0.158-rc.14",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "src/index.ts",
|
|
7
7
|
"scripts": {
|
|
8
8
|
"test": "jest"
|
|
9
9
|
},
|
|
10
10
|
"peerDependencies": {
|
|
11
|
-
"@akinon/next": "2.0.
|
|
11
|
+
"@akinon/next": "2.0.158-rc.14",
|
|
12
12
|
"react": "^18.0.0 || ^19.0.0",
|
|
13
13
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@akinon/theme-contract": "2.0.
|
|
16
|
+
"@akinon/theme-contract": "2.0.158-rc.14",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"react-multi-carousel": "2.8.4",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"zod": "4.4.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@akinon/next": "2.0.
|
|
24
|
+
"@akinon/next": "2.0.158-rc.14",
|
|
25
25
|
"@testing-library/jest-dom": "5.16.4",
|
|
26
26
|
"@testing-library/react": "16.3.2",
|
|
27
27
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -28,6 +28,7 @@ const SETTINGS_LOCALES = [
|
|
|
28
28
|
const mockLocalization = {
|
|
29
29
|
locale: 'tr',
|
|
30
30
|
locales: SETTINGS_LOCALES as Array<Record<string, unknown>>,
|
|
31
|
+
defaultLocaleValue: 'tr',
|
|
31
32
|
setLocale: jest.fn()
|
|
32
33
|
};
|
|
33
34
|
|
|
@@ -106,6 +107,7 @@ beforeEach(() => {
|
|
|
106
107
|
postMessage.mockClear();
|
|
107
108
|
mockLocalization.locale = 'tr';
|
|
108
109
|
mockLocalization.locales = SETTINGS_LOCALES;
|
|
110
|
+
mockLocalization.defaultLocaleValue = 'tr';
|
|
109
111
|
mockLocalization.setLocale = jest.fn();
|
|
110
112
|
delete (window as { __iframeReadySent?: boolean }).__iframeReadySent;
|
|
111
113
|
});
|
|
@@ -144,7 +146,8 @@ describe('inside the editor', () => {
|
|
|
144
146
|
{ value: 'en', label: 'English' },
|
|
145
147
|
{ value: 'tr', label: 'Türkçe' },
|
|
146
148
|
{ value: 'de', label: 'de' }
|
|
147
|
-
]
|
|
149
|
+
],
|
|
150
|
+
defaultLocale: 'tr'
|
|
148
151
|
}
|
|
149
152
|
})
|
|
150
153
|
]);
|
|
@@ -154,6 +157,34 @@ describe('inside the editor', () => {
|
|
|
154
157
|
expect(targetOrigin).toBe(EDITOR_ORIGIN);
|
|
155
158
|
});
|
|
156
159
|
|
|
160
|
+
it('posts the languages before the document reports ready', () => {
|
|
161
|
+
// The editor holds every per-language write until IFRAME_READY and takes
|
|
162
|
+
// "ready without languages" to mean they are never coming, so the order
|
|
163
|
+
// of the two frames is part of the contract.
|
|
164
|
+
render(placeholder('header'));
|
|
165
|
+
|
|
166
|
+
const types = postMessage.mock.calls.map(([message]) => message?.type);
|
|
167
|
+
expect(types).toContain('LOCALE_UPDATE');
|
|
168
|
+
expect(types).toContain('IFRAME_READY');
|
|
169
|
+
expect(types.indexOf('LOCALE_UPDATE')).toBeLessThan(
|
|
170
|
+
types.indexOf('IFRAME_READY')
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("posts the brand's default language, whichever it is", () => {
|
|
175
|
+
// The editor's per-language text controls take the plain-string form to
|
|
176
|
+
// mean this language; left to guess, the editor assumed English.
|
|
177
|
+
mockLocalization.defaultLocaleValue = 'de';
|
|
178
|
+
|
|
179
|
+
render(<EditorLocaleBridge />);
|
|
180
|
+
|
|
181
|
+
expect(localeFrames()).toEqual([
|
|
182
|
+
expect.objectContaining({
|
|
183
|
+
data: expect.objectContaining({ locale: 'tr', defaultLocale: 'de' })
|
|
184
|
+
})
|
|
185
|
+
]);
|
|
186
|
+
});
|
|
187
|
+
|
|
157
188
|
it("follows the editor's switch exactly once, with several placeholders mounted", () => {
|
|
158
189
|
render(
|
|
159
190
|
<>
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { ItemTemplateList } from '../blocks/item-template';
|
|
4
|
+
import { useThemeProduct } from '../blocks/product-block-utils';
|
|
5
|
+
import { useThemeRowScope } from '../blocks/row-scope';
|
|
6
|
+
import blockRendererRegistry from '../blocks/block-renderer-registry';
|
|
7
|
+
import { resetCustomCatalog } from '../custom/catalog';
|
|
8
|
+
import { resetCustomCollection } from '../custom/collect';
|
|
9
|
+
import { isCustomThemeType, registerCustomBlocks } from '../custom';
|
|
10
|
+
import type { Block } from '../theme-block';
|
|
11
|
+
|
|
12
|
+
const READER = 'custom:acme:row-reader';
|
|
13
|
+
|
|
14
|
+
function RowReader() {
|
|
15
|
+
const scope = useThemeRowScope();
|
|
16
|
+
const product = useThemeProduct();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<span
|
|
20
|
+
data-testid="reader"
|
|
21
|
+
data-list={scope?.listType ?? ''}
|
|
22
|
+
data-index={scope?.index ?? ''}
|
|
23
|
+
data-number={String(scope?.row.number ?? '')}
|
|
24
|
+
data-product={String(product?.pk ?? '')}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const template = (): Block =>
|
|
30
|
+
({
|
|
31
|
+
id: 'tpl',
|
|
32
|
+
type: 'group',
|
|
33
|
+
label: 'Row',
|
|
34
|
+
order: 0,
|
|
35
|
+
hidden: false,
|
|
36
|
+
value: '',
|
|
37
|
+
properties: { role: { desktop: 'item-template' } },
|
|
38
|
+
styles: {},
|
|
39
|
+
blocks: [
|
|
40
|
+
{
|
|
41
|
+
id: 'tpl-reader',
|
|
42
|
+
type: READER,
|
|
43
|
+
label: 'Reader',
|
|
44
|
+
order: 0,
|
|
45
|
+
hidden: false,
|
|
46
|
+
value: '',
|
|
47
|
+
properties: {},
|
|
48
|
+
styles: {},
|
|
49
|
+
blocks: []
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
} as Block);
|
|
53
|
+
|
|
54
|
+
const list = (type: string): Block =>
|
|
55
|
+
({
|
|
56
|
+
id: 'list',
|
|
57
|
+
type,
|
|
58
|
+
label: 'List',
|
|
59
|
+
order: 0,
|
|
60
|
+
hidden: false,
|
|
61
|
+
value: '',
|
|
62
|
+
properties: {},
|
|
63
|
+
styles: {},
|
|
64
|
+
blocks: [template()]
|
|
65
|
+
} as Block);
|
|
66
|
+
|
|
67
|
+
const renderRows = (type: string, items: Record<string, unknown>[]) =>
|
|
68
|
+
render(
|
|
69
|
+
<ItemTemplateList
|
|
70
|
+
block={list(type)}
|
|
71
|
+
items={items}
|
|
72
|
+
placeholderId="p1"
|
|
73
|
+
sectionId="s1"
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
beforeAll(() => {
|
|
78
|
+
registerCustomBlocks('acme', { 'row-reader': RowReader });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
afterAll(() => {
|
|
82
|
+
const map = (
|
|
83
|
+
blockRendererRegistry as unknown as { renderers: Map<string, unknown> }
|
|
84
|
+
).renderers;
|
|
85
|
+
Array.from(map.keys())
|
|
86
|
+
.filter(isCustomThemeType)
|
|
87
|
+
.forEach((key) => map.delete(key));
|
|
88
|
+
resetCustomCatalog();
|
|
89
|
+
resetCustomCollection();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('the row scope', () => {
|
|
93
|
+
it('hands every repeated row to the blocks inside it, on any list', () => {
|
|
94
|
+
renderRows('order-list', [{ number: 'ORD-1' }, { number: 'ORD-2' }]);
|
|
95
|
+
|
|
96
|
+
const readers = screen.getAllByTestId('reader');
|
|
97
|
+
expect(readers).toHaveLength(2);
|
|
98
|
+
expect(readers[0]).toHaveAttribute('data-list', 'order-list');
|
|
99
|
+
expect(readers[0]).toHaveAttribute('data-index', '0');
|
|
100
|
+
expect(readers[0]).toHaveAttribute('data-number', 'ORD-1');
|
|
101
|
+
expect(readers[1]).toHaveAttribute('data-number', 'ORD-2');
|
|
102
|
+
expect(readers[0]).toHaveAttribute('data-product', '');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('keeps the product scope on product lists next to the row scope', () => {
|
|
106
|
+
renderRows('product-grid', [{ pk: 7, name: 'Sneaker' }]);
|
|
107
|
+
|
|
108
|
+
const reader = screen.getByTestId('reader');
|
|
109
|
+
expect(reader).toHaveAttribute('data-list', 'product-grid');
|
|
110
|
+
expect(reader).toHaveAttribute('data-product', '7');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('is closed outside a repeated row', () => {
|
|
114
|
+
render(<RowReader />);
|
|
115
|
+
|
|
116
|
+
expect(screen.getByTestId('reader')).toHaveAttribute('data-list', '');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
3
|
import OrderDetailBlock from '../order-detail-block';
|
|
4
|
+
import {
|
|
5
|
+
__resetOrderActionRulesForTests,
|
|
6
|
+
registerOrderActionRules
|
|
7
|
+
} from '../../utils/order-action-rules';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* ZERO-5183 — the order detail is where an easy return starts and where a
|
|
@@ -64,6 +68,8 @@ const texts = (root: ParentNode, part: string) =>
|
|
|
64
68
|
(e.textContent || '').replace(/\s+/g, ' ').trim()
|
|
65
69
|
);
|
|
66
70
|
|
|
71
|
+
afterEach(() => __resetOrderActionRulesForTests());
|
|
72
|
+
|
|
67
73
|
beforeEach(() => {
|
|
68
74
|
order = {
|
|
69
75
|
id: 42,
|
|
@@ -164,7 +170,7 @@ describe('order detail — easy returns', () => {
|
|
|
164
170
|
expect(container.querySelector('[data-part="return-link"]')).toBeNull();
|
|
165
171
|
});
|
|
166
172
|
|
|
167
|
-
it('links
|
|
173
|
+
it('links a refundable line that has no request yet', () => {
|
|
168
174
|
// As commerce answers after a return: the order and its lines stay
|
|
169
175
|
// refundable while the request is open.
|
|
170
176
|
const flagged: Record<string, unknown>[] = (
|
|
@@ -194,15 +200,74 @@ describe('order detail — easy returns', () => {
|
|
|
194
200
|
};
|
|
195
201
|
expect(returnLink()).toBeNull();
|
|
196
202
|
|
|
197
|
-
// Commerce calls a shipped line refundable too
|
|
203
|
+
// Commerce calls a shipped line refundable too, and with no rule from
|
|
204
|
+
// the brand that is what decides (ZERO-5302).
|
|
198
205
|
order = {
|
|
199
206
|
...order,
|
|
200
207
|
orderitem_set: flagged.map((item) =>
|
|
201
|
-
item.id === 3
|
|
202
|
-
? { ...item, status: { value: '500', label: 'Gönderildi' } }
|
|
203
|
-
: item
|
|
208
|
+
item.id === 3 ? { ...item, status: { value: '500' } } : item
|
|
204
209
|
)
|
|
205
210
|
};
|
|
211
|
+
expect(returnLink()).not.toBeNull();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("asks the brand's rules about a line commerce lets go back (ZERO-5302)", () => {
|
|
215
|
+
const asked: string[] = [];
|
|
216
|
+
registerOrderActionRules({
|
|
217
|
+
returnLine: (item) => {
|
|
218
|
+
asked.push(String(item.status?.value));
|
|
219
|
+
return item.status?.value === '550';
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
const returnLink = () =>
|
|
223
|
+
render(
|
|
224
|
+
<OrderDetailBlock
|
|
225
|
+
block={blockOf(ALL_ON) as never}
|
|
226
|
+
placeholderId="p1"
|
|
227
|
+
sectionId="s1"
|
|
228
|
+
isDesigner={false}
|
|
229
|
+
/>
|
|
230
|
+
).container.querySelector('[data-part="return-link"]');
|
|
231
|
+
const withLine3 = (status: string) => ({
|
|
232
|
+
...order,
|
|
233
|
+
orderitem_set: (order?.orderitem_set as Record<string, unknown>[]).map(
|
|
234
|
+
(item) =>
|
|
235
|
+
item.id === 3
|
|
236
|
+
? { ...item, is_refundable: true, status: { value: status } }
|
|
237
|
+
: { ...item, is_refundable: false }
|
|
238
|
+
)
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
order = withLine3('550');
|
|
242
|
+
expect(returnLink()).not.toBeNull();
|
|
243
|
+
|
|
244
|
+
order = withLine3('500');
|
|
206
245
|
expect(returnLink()).toBeNull();
|
|
246
|
+
// Only the line commerce lets go back is put to the brand.
|
|
247
|
+
expect(new Set(asked)).toEqual(new Set(['550', '500']));
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('withholds the cancel link when the brand switches cancellation off (ZERO-5302)', () => {
|
|
251
|
+
order = { ...order, is_cancellable: true };
|
|
252
|
+
const cancelLink = () =>
|
|
253
|
+
render(
|
|
254
|
+
<OrderDetailBlock
|
|
255
|
+
block={
|
|
256
|
+
blockOf({
|
|
257
|
+
...ALL_ON,
|
|
258
|
+
showCancelLink: { desktop: true },
|
|
259
|
+
cancelLinkLabel: { desktop: 'Cancel' }
|
|
260
|
+
}) as never
|
|
261
|
+
}
|
|
262
|
+
placeholderId="p1"
|
|
263
|
+
sectionId="s1"
|
|
264
|
+
isDesigner={false}
|
|
265
|
+
/>
|
|
266
|
+
).container.querySelector('[data-part="cancel-link"]');
|
|
267
|
+
|
|
268
|
+
expect(cancelLink()).not.toBeNull();
|
|
269
|
+
|
|
270
|
+
registerOrderActionRules({ cancelOrder: () => false });
|
|
271
|
+
expect(cancelLink()).toBeNull();
|
|
207
272
|
});
|
|
208
273
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
registerOrderLineActions,
|
|
4
|
+
renderOrderLineActions
|
|
5
|
+
} from '../order-line-actions';
|
|
6
|
+
|
|
7
|
+
const context = {
|
|
8
|
+
order: { id: 7, number: '20920397829', created_date: '2026-10-01' },
|
|
9
|
+
line: {
|
|
10
|
+
key: '1',
|
|
11
|
+
name: 'Product',
|
|
12
|
+
image: '',
|
|
13
|
+
href: '',
|
|
14
|
+
price: '100',
|
|
15
|
+
total: 100,
|
|
16
|
+
quantity: 1,
|
|
17
|
+
item: { id: 1, status: { value: '550', label: 'Delivered' } }
|
|
18
|
+
},
|
|
19
|
+
isDesigner: false
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
describe('order line actions registry', () => {
|
|
23
|
+
it('renders nothing until a brand registers', () => {
|
|
24
|
+
expect(renderOrderLineActions(context)).toBeNull();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('hands the line and order to the registered renderer', () => {
|
|
28
|
+
const renderer = jest.fn(() => <span>action</span>);
|
|
29
|
+
const unregister = registerOrderLineActions(renderer);
|
|
30
|
+
|
|
31
|
+
expect(renderOrderLineActions(context)).toEqual(<span>action</span>);
|
|
32
|
+
expect(renderer).toHaveBeenCalledWith(context);
|
|
33
|
+
|
|
34
|
+
unregister();
|
|
35
|
+
expect(renderOrderLineActions(context)).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('swallows a throwing renderer so a brand bug cannot break the line', () => {
|
|
39
|
+
const unregister = registerOrderLineActions(() => {
|
|
40
|
+
throw new Error('boom');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(renderOrderLineActions(context)).toBeNull();
|
|
44
|
+
unregister();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('only forgets the renderer it was asked to forget', () => {
|
|
48
|
+
const first = registerOrderLineActions(() => 'first');
|
|
49
|
+
registerOrderLineActions(() => 'second');
|
|
50
|
+
|
|
51
|
+
first();
|
|
52
|
+
expect(renderOrderLineActions(context)).toBe('second');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import OrderListBlock from '../order-list-block';
|
|
4
|
+
|
|
5
|
+
const captured: { items: Record<string, unknown>[] } = { items: [] };
|
|
6
|
+
|
|
7
|
+
jest.mock('../item-template', () => ({
|
|
8
|
+
__esModule: true,
|
|
9
|
+
hasItemTemplate: () => true,
|
|
10
|
+
ItemTemplateList: ({ items }: { items: Record<string, unknown>[] }) => {
|
|
11
|
+
captured.items = items;
|
|
12
|
+
return <div data-testid="item-template-list" />;
|
|
13
|
+
}
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock('next-auth/react', () => ({
|
|
17
|
+
useSession: () => ({ data: null, status: 'authenticated' })
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock('@akinon/next/hooks', () => ({
|
|
21
|
+
__esModule: true,
|
|
22
|
+
useLocalization: () => ({ locale: 'tr', currency: 'try' })
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
jest.mock('@akinon/next/redux/hooks', () => ({
|
|
26
|
+
__esModule: true,
|
|
27
|
+
useAppSelector: () => undefined,
|
|
28
|
+
useAppDispatch: () => jest.fn()
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.mock('@akinon/next/data/client/account', () => ({
|
|
32
|
+
__esModule: true,
|
|
33
|
+
useGetOrdersQuery: () => ({
|
|
34
|
+
data: {
|
|
35
|
+
count: 1,
|
|
36
|
+
results: [
|
|
37
|
+
{
|
|
38
|
+
id: 42,
|
|
39
|
+
number: 'ORD-42',
|
|
40
|
+
amount: '100.00',
|
|
41
|
+
created_date: '2026-09-01T10:00:00Z',
|
|
42
|
+
status: { value: '550', label: 'Delivered' },
|
|
43
|
+
orderitem_set: [
|
|
44
|
+
{
|
|
45
|
+
id: 7,
|
|
46
|
+
status: { value: 550, label: 'Delivered' },
|
|
47
|
+
product: {
|
|
48
|
+
sku: 'SKU-1',
|
|
49
|
+
base_code: 'BASE-1',
|
|
50
|
+
name: 'Sneaker',
|
|
51
|
+
image: '/s.jpg'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
status: { value: '400', label: 'Approved' },
|
|
56
|
+
product: { name: 'Boot' }
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
isLoading: false
|
|
63
|
+
})
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
describe('order list rows', () => {
|
|
67
|
+
it('publishes the order items with their product codes and status code', () => {
|
|
68
|
+
render(
|
|
69
|
+
<OrderListBlock
|
|
70
|
+
block={{
|
|
71
|
+
id: 'order-list-1',
|
|
72
|
+
type: 'order-list',
|
|
73
|
+
properties: {},
|
|
74
|
+
styles: {},
|
|
75
|
+
blocks: []
|
|
76
|
+
}}
|
|
77
|
+
placeholderId="p1"
|
|
78
|
+
sectionId="s1"
|
|
79
|
+
/>
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
expect(captured.items).toHaveLength(1);
|
|
83
|
+
expect(captured.items[0]).toMatchObject({
|
|
84
|
+
id: 42,
|
|
85
|
+
number: 'ORD-42',
|
|
86
|
+
createdDate: '2026-09-01T10:00:00Z'
|
|
87
|
+
});
|
|
88
|
+
expect(captured.items[0].items).toEqual([
|
|
89
|
+
{
|
|
90
|
+
id: 7,
|
|
91
|
+
name: 'Sneaker',
|
|
92
|
+
image: '/s.jpg',
|
|
93
|
+
status: 'Delivered',
|
|
94
|
+
statusValue: '550',
|
|
95
|
+
product: {
|
|
96
|
+
sku: 'SKU-1',
|
|
97
|
+
base_code: 'BASE-1',
|
|
98
|
+
name: 'Sneaker',
|
|
99
|
+
image: '/s.jpg'
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 1,
|
|
104
|
+
name: 'Boot',
|
|
105
|
+
image: '',
|
|
106
|
+
status: 'Approved',
|
|
107
|
+
statusValue: '400',
|
|
108
|
+
product: { sku: '', base_code: '', name: 'Boot', image: '' }
|
|
109
|
+
}
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -2,9 +2,31 @@ import React from 'react';
|
|
|
2
2
|
import { render } from '@testing-library/react';
|
|
3
3
|
import TextBlock from '../text-block';
|
|
4
4
|
|
|
5
|
-
// The
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// The page's language and the storefront's default, as the localization
|
|
6
|
+
// provider reports them. Most cases run on an English-default store showing
|
|
7
|
+
// English; the fallback case moves both.
|
|
8
|
+
const mockLocalization = { locale: 'en-us', defaultLocaleValue: 'en-us' };
|
|
9
|
+
|
|
10
|
+
jest.mock('@akinon/next/hooks', () => ({
|
|
11
|
+
__esModule: true,
|
|
12
|
+
useLocalization: () => ({
|
|
13
|
+
t: (path: string) => path,
|
|
14
|
+
setLocale: () => {},
|
|
15
|
+
setCurrency: () => {},
|
|
16
|
+
locale: mockLocalization.locale,
|
|
17
|
+
currency: 'USD',
|
|
18
|
+
locales: [{ value: 'en-us' }, { value: 'tr-tr' }],
|
|
19
|
+
currencies: ['USD'],
|
|
20
|
+
defaultLocaleValue: mockLocalization.defaultLocaleValue,
|
|
21
|
+
defaultCurrencyCode: 'USD',
|
|
22
|
+
localeUrlStrategy: 'hide-default-locale'
|
|
23
|
+
})
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
mockLocalization.locale = 'en-us';
|
|
28
|
+
mockLocalization.defaultLocaleValue = 'en-us';
|
|
29
|
+
});
|
|
8
30
|
|
|
9
31
|
const renderBlock = (value: unknown, styles?: Record<string, unknown>) => {
|
|
10
32
|
const block: any = { id: 'b1', type: 'text', value, styles };
|
|
@@ -34,10 +56,16 @@ describe('TextBlock value-shape resolution', () => {
|
|
|
34
56
|
);
|
|
35
57
|
});
|
|
36
58
|
|
|
37
|
-
it(
|
|
38
|
-
|
|
59
|
+
it("falls back to the storefront's default language when the active one is absent", () => {
|
|
60
|
+
// A Turkish-default store showing German. The entry under `en` is what
|
|
61
|
+
// the block used to show: its default was read from an env variable
|
|
62
|
+
// that falls back to English, not from the storefront (ZERO-5296).
|
|
63
|
+
mockLocalization.locale = 'de-de';
|
|
64
|
+
mockLocalization.defaultLocaleValue = 'tr-tr';
|
|
65
|
+
|
|
66
|
+
const { container } = renderBlock({ en: 'EN', 'tr-tr': 'TR' });
|
|
39
67
|
expect((container.querySelector('div') as HTMLDivElement).innerHTML).toBe(
|
|
40
|
-
'
|
|
68
|
+
'TR'
|
|
41
69
|
);
|
|
42
70
|
});
|
|
43
71
|
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
resolveItemProduct
|
|
16
16
|
} from './item-template-utils';
|
|
17
17
|
import { ThemeItemScopeProvider } from './product-block-utils';
|
|
18
|
+
import { ThemeRowScopeProvider } from './row-scope';
|
|
18
19
|
|
|
19
20
|
export {
|
|
20
21
|
findItemTemplate,
|
|
@@ -120,20 +121,15 @@ export const ItemTemplateList = ({
|
|
|
120
121
|
const scopesProducts = publishesProductItems(block.type);
|
|
121
122
|
// The same slice the iterator applied, so clone N is handed the row it
|
|
122
123
|
// was built from rather than row N.
|
|
123
|
-
const
|
|
124
|
-
? sliceIteratorItems(block, rows).items
|
|
125
|
-
: rows;
|
|
124
|
+
const slicedRows = sliceIteratorItems(block, rows).items;
|
|
126
125
|
|
|
127
126
|
return (
|
|
128
127
|
<>
|
|
129
128
|
{clones.map((clone, index) => {
|
|
130
129
|
const key = clone.id || `${template.id}-item-${index}`;
|
|
130
|
+
const rowData = slicedRows[index] ?? {};
|
|
131
131
|
const row = (
|
|
132
132
|
<ThemeBlock
|
|
133
|
-
// Keyed here so the non-scoped branch is the element it always
|
|
134
|
-
// was: wrapping it in anything, even a fragment, would change the
|
|
135
|
-
// element type at this position and remount every existing row.
|
|
136
|
-
key={key}
|
|
137
133
|
block={clone}
|
|
138
134
|
placeholderId={placeholderId}
|
|
139
135
|
sectionId={sectionId}
|
|
@@ -143,22 +139,32 @@ export const ItemTemplateList = ({
|
|
|
143
139
|
currentBreakpoint={currentBreakpoint}
|
|
144
140
|
/>
|
|
145
141
|
);
|
|
142
|
+
const scoped = scopesProducts
|
|
143
|
+
? resolveItemProduct(block.type, rowData)
|
|
144
|
+
: null;
|
|
146
145
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Context only — the provider renders no element, so the markup a
|
|
152
|
-
// published row produces is unchanged.
|
|
146
|
+
// Context only — the providers render no element, so the markup a
|
|
147
|
+
// published row produces is unchanged. Every list opens the row
|
|
148
|
+
// scope; only product lists open the product scope on top of it.
|
|
153
149
|
return (
|
|
154
|
-
<
|
|
150
|
+
<ThemeRowScopeProvider
|
|
155
151
|
key={key}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
152
|
+
listType={block.type}
|
|
153
|
+
row={rowData}
|
|
154
|
+
index={index}
|
|
159
155
|
>
|
|
160
|
-
{
|
|
161
|
-
|
|
156
|
+
{scopesProducts ? (
|
|
157
|
+
<ThemeItemScopeProvider
|
|
158
|
+
product={
|
|
159
|
+
scoped ?? (isDesigner ? DESIGNER_SAMPLE_PRODUCT : null)
|
|
160
|
+
}
|
|
161
|
+
>
|
|
162
|
+
{row}
|
|
163
|
+
</ThemeItemScopeProvider>
|
|
164
|
+
) : (
|
|
165
|
+
row
|
|
166
|
+
)}
|
|
167
|
+
</ThemeRowScopeProvider>
|
|
162
168
|
);
|
|
163
169
|
})}
|
|
164
170
|
</>
|