@akinon/pz-theme 2.0.163-rc.0 → 2.0.163

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.
Files changed (41) hide show
  1. package/CHANGELOG.md +7 -50
  2. package/package.json +4 -4
  3. package/src/__tests__/commerce-channel-frozen.test.ts +12 -1
  4. package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
  5. package/src/__tests__/section-list-identity.test.tsx +113 -0
  6. package/src/blocks/__tests__/favourites-grid-impression.test.tsx +217 -0
  7. package/src/blocks/__tests__/group-block-list-click.test.tsx +90 -0
  8. package/src/blocks/__tests__/text-block.test.tsx +6 -34
  9. package/src/blocks/favourites-grid-block.tsx +16 -18
  10. package/src/blocks/group-block.tsx +14 -2
  11. package/src/blocks/item-template.tsx +1 -0
  12. package/src/blocks/order-detail-block.tsx +95 -113
  13. package/src/blocks/product-block-utils.tsx +5 -1
  14. package/src/blocks/product-recommendations-block.tsx +45 -13
  15. package/src/blocks/text-block.tsx +2 -6
  16. package/src/custom/README.md +219 -100
  17. package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
  18. package/src/custom/collect.ts +9 -4
  19. package/src/custom/property-types.ts +15 -15
  20. package/src/custom/use-theme-setting.ts +5 -21
  21. package/src/editor-locale-bridge.tsx +11 -39
  22. package/src/hooks/__tests__/use-list-impression.test.tsx +228 -0
  23. package/src/hooks/__tests__/use-section-seen.test.tsx +55 -2
  24. package/src/hooks/use-list-impression.ts +116 -0
  25. package/src/hooks/use-section-seen.ts +22 -2
  26. package/src/sections/__tests__/posts-slider-select-item.test.tsx +205 -0
  27. package/src/sections/__tests__/section-wrapper-list.test.tsx +148 -0
  28. package/src/sections/posts-slider-section.tsx +62 -2
  29. package/src/sections/section-wrapper.tsx +32 -2
  30. package/src/theme-block.tsx +6 -0
  31. package/src/theme-placeholder-wrapper.tsx +0 -3
  32. package/src/theme-section-data-context.tsx +33 -0
  33. package/src/theme-section.tsx +60 -2
  34. package/src/utils/__tests__/section-impressions.test.ts +68 -0
  35. package/src/utils/iterator-price-shapes.test.ts +13 -0
  36. package/src/utils/iterator-utils.ts +2 -1
  37. package/src/utils/section-impressions.ts +37 -0
  38. package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
  39. package/src/blocks/order-line-actions.ts +0 -45
  40. package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
  41. package/src/utils/sanitize-rich-text.ts +0 -215
package/CHANGELOG.md CHANGED
@@ -1,55 +1,12 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.163-rc.0
4
-
5
- ### Patch Changes
6
-
7
- - 8e040731e: 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
- - Updated dependencies [86187b1c2]
12
- - Updated dependencies [08390a87]
13
- - Updated dependencies [0cf9ea239]
14
- - Updated dependencies [324f97d55]
15
- - Updated dependencies [4c3465ae]
16
- - Updated dependencies [51ea06888]
17
- - Updated dependencies
18
- - Updated dependencies [549da545]
19
- - Updated dependencies [2fea41430]
20
- - Updated dependencies [b55acb768]
21
- - Updated dependencies [760258c1c]
22
- - Updated dependencies [143be2b9d]
23
- - Updated dependencies [7889b08fe]
24
- - Updated dependencies [9f8cd3bc5]
25
- - Updated dependencies [d635d345]
26
- - Updated dependencies [bfafa3f49]
27
- - Updated dependencies [d860c917]
28
- - Updated dependencies [ac6cada1]
29
- - Updated dependencies [e90c0e02]
30
- - Updated dependencies [dc7f9ca5]
31
- - Updated dependencies [57d7eb305]
32
- - Updated dependencies [0145dc3f]
33
- - Updated dependencies [d99a6a7d5]
34
- - Updated dependencies [92086b29]
35
- - Updated dependencies [227b7408]
36
- - Updated dependencies [9db81a714]
37
- - Updated dependencies [591e345e1]
38
- - Updated dependencies [4de5303c5]
39
- - Updated dependencies [95b139dc1]
40
- - Updated dependencies [1d00f2d06]
41
- - Updated dependencies [4ac7b2a1e]
42
- - Updated dependencies [4998a9631]
43
- - Updated dependencies [804d2bd6c]
44
- - Updated dependencies [3909d3224]
45
- - Updated dependencies [6a3d8a631]
46
- - Updated dependencies [e18836b20]
47
- - Updated dependencies [b1111d7b1]
48
- - Updated dependencies [1595d458e]
49
- - Updated dependencies [dca128112]
50
- - Updated dependencies [9a7b0937]
51
- - @akinon/next@2.0.163-rc.0
52
- - @akinon/theme-contract@2.0.163-rc.0
3
+ ## 2.0.163
4
+
5
+ ### Patch Changes
6
+
7
+ - 2c4fae70: ZERO-5305: Report product lists as GA4 lists on the commerce channel. Collection sections, product sliders and recommendation strips send `view_item_list` once seen and `select_item` with the list id, list name and the card's position; the favourites grid reports when it is seen rather than on mount; `section.seen` carries the section label and its product count. Nothing is emitted inside the editor.
8
+ - @akinon/next@2.0.163
9
+ - @akinon/theme-contract@2.0.163
53
10
 
54
11
  ## 2.0.162
55
12
 
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.163-rc.0",
4
+ "version": "2.0.163",
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.163-rc.0",
11
+ "@akinon/next": "2.0.163",
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.163-rc.0",
16
+ "@akinon/theme-contract": "2.0.163",
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.163-rc.0",
24
+ "@akinon/next": "2.0.163",
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",
@@ -30,11 +30,22 @@ const FROZEN_COMMERCE_CALL_SITES: Record<string, number> = {
30
30
  'blocks/auth-form-block.tsx': 3,
31
31
  'blocks/basket-items-block.tsx': 1,
32
32
  'blocks/favorite-button-block.tsx': 1,
33
- 'blocks/favourites-grid-block.tsx': 3,
33
+ // view_item_list moved to hooks/use-list-impression.ts (ZERO-5305): the
34
+ // grid reports when it is seen, as every other list does.
35
+ 'blocks/favourites-grid-block.tsx': 2,
34
36
  'blocks/group-block.tsx': 1,
35
37
  'blocks/product-grid-block.tsx': 2,
36
38
  'blocks/product-recommendations-block.tsx': 1,
37
39
  'blocks/search-box-block.tsx': 1,
40
+ // view_item_list for the product rows that are not a listing grid — a
41
+ // collection carousel, a recommendations strip (ZERO-5305). An existing GA4
42
+ // verb, not a new moment: a storefront bridge maps it as it maps the
43
+ // listing grid's.
44
+ 'hooks/use-list-impression.ts': 1,
45
+ // select_item for a product slider's card (ZERO-5305): the same verb the
46
+ // composed card's anchor reports from group-block, for the slides the
47
+ // slider draws itself.
48
+ 'sections/posts-slider-section.tsx': 1,
38
49
  // One call, four page-scope actions behind it (view_item, view_cart,
39
50
  // begin_checkout, purchase) — the action is a variable there.
40
51
  'theme-placeholder-wrapper.tsx': 1
@@ -28,7 +28,6 @@ const SETTINGS_LOCALES = [
28
28
  const mockLocalization = {
29
29
  locale: 'tr',
30
30
  locales: SETTINGS_LOCALES as Array<Record<string, unknown>>,
31
- defaultLocaleValue: 'tr',
32
31
  setLocale: jest.fn()
33
32
  };
34
33
 
@@ -107,7 +106,6 @@ beforeEach(() => {
107
106
  postMessage.mockClear();
108
107
  mockLocalization.locale = 'tr';
109
108
  mockLocalization.locales = SETTINGS_LOCALES;
110
- mockLocalization.defaultLocaleValue = 'tr';
111
109
  mockLocalization.setLocale = jest.fn();
112
110
  delete (window as { __iframeReadySent?: boolean }).__iframeReadySent;
113
111
  });
@@ -146,8 +144,7 @@ describe('inside the editor', () => {
146
144
  { value: 'en', label: 'English' },
147
145
  { value: 'tr', label: 'Türkçe' },
148
146
  { value: 'de', label: 'de' }
149
- ],
150
- defaultLocale: 'tr'
147
+ ]
151
148
  }
152
149
  })
153
150
  ]);
@@ -157,34 +154,6 @@ describe('inside the editor', () => {
157
154
  expect(targetOrigin).toBe(EDITOR_ORIGIN);
158
155
  });
159
156
 
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
-
188
157
  it("follows the editor's switch exactly once, with several placeholders mounted", () => {
189
158
  render(
190
159
  <>
@@ -0,0 +1,113 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+
4
+ jest.mock('next/navigation', () => ({
5
+ usePathname: () => '/'
6
+ }));
7
+
8
+ jest.mock('react-multi-carousel', () => ({
9
+ __esModule: true,
10
+ default: ({ children }: { children?: React.ReactNode }) => (
11
+ <div>{children}</div>
12
+ )
13
+ }));
14
+
15
+ const wrapperProps: Array<Record<string, unknown>> = [];
16
+ jest.mock('../sections/section-wrapper', () => ({
17
+ __esModule: true,
18
+ default: (
19
+ props: Record<string, unknown> & { children?: React.ReactNode }
20
+ ) => {
21
+ wrapperProps.push(props);
22
+ return (
23
+ <section data-section-id={(props.section as { id: string }).id}>
24
+ {props.children}
25
+ </section>
26
+ );
27
+ }
28
+ }));
29
+
30
+ import ThemeSection from '../theme-section';
31
+ import { ThemeSettingsProvider } from '../theme-settings-context';
32
+
33
+ /**
34
+ * A section with a renderer of its own (a product slider) is drawn through
35
+ * `SectionWrapper`, not the generic branch's <section>. ThemeSection hands the
36
+ * wrapper what it needs to report the section as the generic branch does
37
+ * (ZERO-5305): the product count, so a product row is never taken for a
38
+ * banner, and the rows with the list they are — the collection, named after
39
+ * the section's label.
40
+ */
41
+
42
+ const ROWS = [
43
+ { pk: 1, base_code: 'B1', name: 'Polo', price: '1499' },
44
+ { pk: 2, base_code: 'B2', name: 'Shirt', price: '2499' }
45
+ ];
46
+
47
+ const slider = (blocks: unknown[] = []) =>
48
+ ({
49
+ id: 'section-1',
50
+ type: 'flash-deals-slider',
51
+ name: 'section-1',
52
+ label: 'Picks Slider',
53
+ order: 3,
54
+ hidden: false,
55
+ properties: {},
56
+ styles: {},
57
+ dataSourceId: 'datasource-1',
58
+ dataSource: {
59
+ id: 'datasource-1',
60
+ type: 'collection',
61
+ details: { collection: { data: { products: ROWS } } }
62
+ },
63
+ blocks
64
+ } as never);
65
+
66
+ const renderSection = (section: never) =>
67
+ render(
68
+ <ThemeSettingsProvider value={null}>
69
+ <ThemeSection
70
+ section={section}
71
+ placeholderId="home-body"
72
+ currentBreakpoint="desktop"
73
+ isDesigner={false}
74
+ />
75
+ </ThemeSettingsProvider>
76
+ );
77
+
78
+ describe('a section drawn through SectionWrapper (ZERO-5305)', () => {
79
+ beforeEach(() => {
80
+ wrapperProps.length = 0;
81
+ });
82
+
83
+ it('is handed its product count and its rows as a list', () => {
84
+ renderSection(slider());
85
+
86
+ const props = wrapperProps[wrapperProps.length - 1];
87
+ expect(props.productCount).toBe(2);
88
+ expect(props.listProducts).toEqual(ROWS);
89
+ expect(props.listId).toBe('datasource-1');
90
+ expect(props.listName).toBe('Picks Slider');
91
+ });
92
+
93
+ it('lists nothing when a grid inside reports its own impressions', () => {
94
+ renderSection(
95
+ slider([
96
+ {
97
+ id: 'grid',
98
+ type: 'product-grid',
99
+ label: 'Grid',
100
+ properties: {},
101
+ styles: {},
102
+ order: 0,
103
+ hidden: false,
104
+ blocks: []
105
+ }
106
+ ])
107
+ );
108
+
109
+ const props = wrapperProps[wrapperProps.length - 1];
110
+ expect(props.listProducts).toBeNull();
111
+ expect(props.productCount).toBe(2);
112
+ });
113
+ });
@@ -0,0 +1,217 @@
1
+ import React from 'react';
2
+ import { act, render } from '@testing-library/react';
3
+
4
+ import FavouritesGridBlock from '../favourites-grid-block';
5
+ import { COMMERCE_EVENT_NAME } from '../../utils/analytics';
6
+ import { LIST_IMPRESSION_DWELL_MS } from '../../hooks/use-list-impression';
7
+
8
+ /**
9
+ * The favourites grid reports its favourites as a list when the grid is seen
10
+ * (ZERO-5305), as every other list does — not when it mounts. A grid kept on
11
+ * a page only for its empty state, its list laid out with `display: none`
12
+ * beside a favourites strip, reported the favourites a second time.
13
+ */
14
+
15
+ jest.mock('next-auth/react', () => ({
16
+ useSession: () => ({ data: null, status: 'authenticated' })
17
+ }));
18
+
19
+ jest.mock('@akinon/next/hooks', () => ({
20
+ __esModule: true,
21
+ useLocalization: () => ({ locale: 'tr', currency: 'try' })
22
+ }));
23
+
24
+ jest.mock('@akinon/next/redux/hooks', () => ({
25
+ __esModule: true,
26
+ useAppSelector: () => undefined,
27
+ useAppDispatch: () => jest.fn()
28
+ }));
29
+
30
+ const favourites = {
31
+ results: [
32
+ {
33
+ pk: 11,
34
+ product: {
35
+ pk: 101,
36
+ base_code: 'B101',
37
+ name: 'Shirt',
38
+ absolute_url: '/gomlek/',
39
+ price: '3999.90',
40
+ currency_type: 'try'
41
+ }
42
+ },
43
+ {
44
+ pk: 12,
45
+ product: {
46
+ pk: 102,
47
+ base_code: 'B102',
48
+ name: 'Polo',
49
+ absolute_url: '/polo/',
50
+ price: '2499.00',
51
+ currency_type: 'try'
52
+ }
53
+ }
54
+ ]
55
+ };
56
+
57
+ // Every `@akinon/next/data/client/*` subpath maps to the same mock file.
58
+ jest.mock('@akinon/next/data/client/account', () => ({
59
+ __esModule: true,
60
+ useGetFavoritesQuery: () => ({ data: mockFavourites(), isLoading: false }),
61
+ useRemoveFavoriteMutation: () => [jest.fn(), { isLoading: false }],
62
+ useAddProductMutation: () => [jest.fn(), { isLoading: false }],
63
+ basketApi: { util: { invalidateTags: jest.fn() } }
64
+ }));
65
+
66
+ let current = favourites;
67
+ const mockFavourites = () => current;
68
+
69
+ class RecordingObserver {
70
+ static instances: RecordingObserver[] = [];
71
+ callback: IntersectionObserverCallback;
72
+ observed: Element[] = [];
73
+
74
+ constructor(callback: IntersectionObserverCallback) {
75
+ this.callback = callback;
76
+ RecordingObserver.instances.push(this);
77
+ }
78
+
79
+ observe(node: Element) {
80
+ this.observed.push(node);
81
+ }
82
+
83
+ unobserve() {}
84
+ disconnect() {}
85
+ takeRecords() {
86
+ return [];
87
+ }
88
+
89
+ cross() {
90
+ act(() => {
91
+ this.callback(
92
+ [
93
+ {
94
+ isIntersecting: true,
95
+ intersectionRatio: 1,
96
+ target: this.observed[0]
97
+ } as unknown as IntersectionObserverEntry
98
+ ],
99
+ this as unknown as IntersectionObserver
100
+ );
101
+ });
102
+ }
103
+ }
104
+
105
+ const block = {
106
+ id: 'favourites-grid-1',
107
+ type: 'favourites-grid',
108
+ label: 'Favourites Grid',
109
+ order: 0,
110
+ hidden: false,
111
+ properties: {},
112
+ styles: {},
113
+ blocks: []
114
+ };
115
+
116
+ describe('favourites-grid list impression (ZERO-5305)', () => {
117
+ let received: Array<Record<string, unknown>>;
118
+ let originalObserver: unknown;
119
+ const onCommerce = (event: Event) =>
120
+ received.push((event as CustomEvent<Record<string, unknown>>).detail);
121
+ const lists = () => received.filter((d) => d.action === 'view_item_list');
122
+ const latest = () =>
123
+ RecordingObserver.instances[RecordingObserver.instances.length - 1];
124
+
125
+ beforeEach(() => {
126
+ jest.useFakeTimers();
127
+ current = favourites;
128
+ received = [];
129
+ window.addEventListener(COMMERCE_EVENT_NAME, onCommerce);
130
+ RecordingObserver.instances = [];
131
+ originalObserver = (window as unknown as Record<string, unknown>)
132
+ .IntersectionObserver;
133
+ (window as unknown as Record<string, unknown>).IntersectionObserver =
134
+ RecordingObserver;
135
+ });
136
+
137
+ afterEach(() => {
138
+ window.removeEventListener(COMMERCE_EVENT_NAME, onCommerce);
139
+ (window as unknown as Record<string, unknown>).IntersectionObserver =
140
+ originalObserver;
141
+ jest.useRealTimers();
142
+ });
143
+
144
+ it('reports nothing until the grid is seen, then the favourites as one list', () => {
145
+ const { container } = render(
146
+ <FavouritesGridBlock
147
+ block={block}
148
+ placeholderId="p1"
149
+ sectionId="s1"
150
+ isDesigner={false}
151
+ />
152
+ );
153
+
154
+ expect(lists()).toEqual([]);
155
+ expect(latest().observed[0]).toBe(
156
+ container.querySelector('[data-favourites-grid]')
157
+ );
158
+
159
+ latest().cross();
160
+ act(() => {
161
+ jest.advanceTimersByTime(LIST_IMPRESSION_DWELL_MS);
162
+ });
163
+
164
+ expect(lists()).toHaveLength(1);
165
+ expect(lists()[0]).toMatchObject({ currency: 'try' });
166
+ expect(lists()[0].items).toEqual([
167
+ expect.objectContaining({
168
+ id: '101',
169
+ name: 'Shirt',
170
+ index: 0,
171
+ listId: 'favourites',
172
+ listName: 'Favourites'
173
+ }),
174
+ expect.objectContaining({
175
+ id: '102',
176
+ name: 'Polo',
177
+ index: 1,
178
+ listId: 'favourites',
179
+ listName: 'Favourites'
180
+ })
181
+ ]);
182
+ });
183
+
184
+ it('reports the new list once a favourite is removed', () => {
185
+ const { rerender } = render(
186
+ <FavouritesGridBlock
187
+ block={block}
188
+ placeholderId="p1"
189
+ sectionId="s1"
190
+ isDesigner={false}
191
+ />
192
+ );
193
+ latest().cross();
194
+ act(() => {
195
+ jest.advanceTimersByTime(LIST_IMPRESSION_DWELL_MS);
196
+ });
197
+
198
+ current = { results: [favourites.results[1]] };
199
+ rerender(
200
+ <FavouritesGridBlock
201
+ block={block}
202
+ placeholderId="p1"
203
+ sectionId="s1"
204
+ isDesigner={false}
205
+ />
206
+ );
207
+ latest().cross();
208
+ act(() => {
209
+ jest.advanceTimersByTime(LIST_IMPRESSION_DWELL_MS);
210
+ });
211
+
212
+ expect(lists()).toHaveLength(2);
213
+ expect(lists()[1].items).toEqual([
214
+ expect.objectContaining({ id: '102', index: 0 })
215
+ ]);
216
+ });
217
+ });
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import { fireEvent, render } from '@testing-library/react';
3
+
4
+ jest.mock('../../theme-block', () => ({
5
+ __esModule: true,
6
+ default: () => null
7
+ }));
8
+
9
+ import GroupBlock from '../group-block';
10
+ import { ThemeItemScopeProvider } from '../product-block-utils';
11
+ import { ThemeSectionListProvider } from '../../theme-section-data-context';
12
+ import { COMMERCE_EVENT_NAME } from '../../utils/analytics';
13
+
14
+ /**
15
+ * A product card's click names the list the card sits in — the section's
16
+ * carousel, not the product's catalogue group — and its place in it
17
+ * (ZERO-5305), the identity the row's view_item_list carries.
18
+ */
19
+
20
+ const card = () =>
21
+ ({
22
+ block: {
23
+ id: 'card',
24
+ type: 'group',
25
+ label: 'card',
26
+ value: '',
27
+ properties: { tag: 'a', href: '#product' },
28
+ styles: {},
29
+ order: 0,
30
+ hidden: false,
31
+ blocks: []
32
+ },
33
+ placeholderId: 'home-body',
34
+ sectionId: 'sec_new_in',
35
+ isDesigner: false
36
+ } as any);
37
+
38
+ const PRODUCT = { pk: 7, base_code: 'B7', name: 'Polo', price: '1499' };
39
+
40
+ describe('a product card click (ZERO-5305)', () => {
41
+ let received: Array<Record<string, unknown>>;
42
+ const onCommerce = (event: Event) =>
43
+ received.push((event as CustomEvent<Record<string, unknown>>).detail);
44
+
45
+ beforeEach(() => {
46
+ received = [];
47
+ window.addEventListener(COMMERCE_EVENT_NAME, onCommerce);
48
+ });
49
+
50
+ afterEach(() => {
51
+ window.removeEventListener(COMMERCE_EVENT_NAME, onCommerce);
52
+ });
53
+
54
+ it('reports the section’s list and the card’s place in it', () => {
55
+ const { container } = render(
56
+ <ThemeSectionListProvider listId="collection-12" listName="New Arrivals">
57
+ <ThemeItemScopeProvider product={PRODUCT as never} index={3}>
58
+ <GroupBlock {...card()} />
59
+ </ThemeItemScopeProvider>
60
+ </ThemeSectionListProvider>
61
+ );
62
+
63
+ fireEvent.click(container.querySelector('a') as HTMLElement);
64
+
65
+ expect(received).toHaveLength(1);
66
+ expect(received[0]).toMatchObject({ action: 'select_item' });
67
+ expect(received[0].items).toEqual([
68
+ expect.objectContaining({
69
+ baseCode: 'B7',
70
+ index: 3,
71
+ listId: 'collection-12',
72
+ listName: 'New Arrivals'
73
+ })
74
+ ]);
75
+ });
76
+
77
+ it('still reports the product outside a section list', () => {
78
+ const { container } = render(
79
+ <ThemeItemScopeProvider product={PRODUCT as never}>
80
+ <GroupBlock {...card()} />
81
+ </ThemeItemScopeProvider>
82
+ );
83
+
84
+ fireEvent.click(container.querySelector('a') as HTMLElement);
85
+
86
+ expect(received[0].items).toEqual([
87
+ expect.not.objectContaining({ listId: expect.anything() })
88
+ ]);
89
+ });
90
+ });
@@ -2,31 +2,9 @@ import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
3
  import TextBlock from '../text-block';
4
4
 
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
- });
5
+ // The mocked useLocalization (jest/__mocks__/akinon-next-hooks.js) returns
6
+ // locale: 'en-us'. process.env.NEXT_PUBLIC_DEFAULT_LOCALE is unset in the
7
+ // jest env, so the component's defaultLocale falls back to 'en'.
30
8
 
31
9
  const renderBlock = (value: unknown, styles?: Record<string, unknown>) => {
32
10
  const block: any = { id: 'b1', type: 'text', value, styles };
@@ -56,16 +34,10 @@ describe('TextBlock value-shape resolution', () => {
56
34
  );
57
35
  });
58
36
 
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' });
37
+ it('falls back to defaultLocale ("en") when active locale is absent', () => {
38
+ const { container } = renderBlock({ en: 'EN', fr: 'FR' });
67
39
  expect((container.querySelector('div') as HTMLDivElement).innerHTML).toBe(
68
- 'TR'
40
+ 'EN'
69
41
  );
70
42
  });
71
43