@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.
- package/CHANGELOG.md +7 -50
- package/package.json +4 -4
- package/src/__tests__/commerce-channel-frozen.test.ts +12 -1
- package/src/__tests__/editor-locale-bridge.test.tsx +1 -32
- package/src/__tests__/section-list-identity.test.tsx +113 -0
- package/src/blocks/__tests__/favourites-grid-impression.test.tsx +217 -0
- package/src/blocks/__tests__/group-block-list-click.test.tsx +90 -0
- package/src/blocks/__tests__/text-block.test.tsx +6 -34
- package/src/blocks/favourites-grid-block.tsx +16 -18
- package/src/blocks/group-block.tsx +14 -2
- package/src/blocks/item-template.tsx +1 -0
- package/src/blocks/order-detail-block.tsx +95 -113
- package/src/blocks/product-block-utils.tsx +5 -1
- package/src/blocks/product-recommendations-block.tsx +45 -13
- package/src/blocks/text-block.tsx +2 -6
- package/src/custom/README.md +219 -100
- package/src/custom/__tests__/use-theme-setting.test.tsx +0 -97
- package/src/custom/collect.ts +9 -4
- package/src/custom/property-types.ts +15 -15
- package/src/custom/use-theme-setting.ts +5 -21
- package/src/editor-locale-bridge.tsx +11 -39
- package/src/hooks/__tests__/use-list-impression.test.tsx +228 -0
- package/src/hooks/__tests__/use-section-seen.test.tsx +55 -2
- package/src/hooks/use-list-impression.ts +116 -0
- package/src/hooks/use-section-seen.ts +22 -2
- package/src/sections/__tests__/posts-slider-select-item.test.tsx +205 -0
- package/src/sections/__tests__/section-wrapper-list.test.tsx +148 -0
- package/src/sections/posts-slider-section.tsx +62 -2
- package/src/sections/section-wrapper.tsx +32 -2
- package/src/theme-block.tsx +6 -0
- package/src/theme-placeholder-wrapper.tsx +0 -3
- package/src/theme-section-data-context.tsx +33 -0
- package/src/theme-section.tsx +60 -2
- package/src/utils/__tests__/section-impressions.test.ts +68 -0
- package/src/utils/iterator-price-shapes.test.ts +13 -0
- package/src/utils/iterator-utils.ts +2 -1
- package/src/utils/section-impressions.ts +37 -0
- package/src/blocks/__tests__/order-line-actions.test.tsx +0 -54
- package/src/blocks/order-line-actions.ts +0 -45
- package/src/utils/__tests__/sanitize-rich-text.test.ts +0 -80
- 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
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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(
|
|
60
|
-
|
|
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
|
-
'
|
|
40
|
+
'EN'
|
|
69
41
|
);
|
|
70
42
|
});
|
|
71
43
|
|