@akinon/pz-theme 2.0.150-beta.0 → 2.0.150

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 CHANGED
@@ -1,12 +1,15 @@
1
1
  # @akinon/pz-theme
2
2
 
3
- ## 2.0.150-beta.0
3
+ ## 2.0.150
4
4
 
5
5
  ### Patch Changes
6
6
 
7
- - Updated dependencies [cbbbfd757]
8
- - @akinon/next@2.0.150-beta.0
9
- - @akinon/theme-contract@2.0.150-beta.0
7
+ - 6532002: ZERO-5207: Correct what pz-theme's notes say each core provides, and pin the collection widget fallback
8
+
9
+ The notes in the server loader, the favourite button, the recommendations block and the reset password check described every gap as a v1 gap. On the latest cores only one remains: `getCollectionWidgetData` (added to the v1 line by ZERO-5207). No core, 1.x or 2.x, has a recommendation or a per-product favourite endpoint, and the reset token query has been on the 1.x line since 1.121.0. A new test pins both halves of the collection widget path: through the core when it exports `getCollectionWidgetData`, straight to the endpoint when it does not. No runtime change.
10
+
11
+ - @akinon/next@2.0.150
12
+ - @akinon/theme-contract@2.0.150
10
13
 
11
14
  ## 2.0.149
12
15
 
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.150-beta.0",
4
+ "version": "2.0.150",
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.150-beta.0",
11
+ "@akinon/next": "2.0.150",
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.150-beta.0",
16
+ "@akinon/theme-contract": "2.0.150",
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.150-beta.0",
24
+ "@akinon/next": "2.0.150",
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",
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Where a collection-backed section's rows come from on the server.
3
+ *
4
+ * A section bound to a product collection is saved as a collection widget
5
+ * (`/product_collection_widgets/<slug>/`), so its regular widget answer is
6
+ * empty and the loader asks for the collection widget instead. A core that
7
+ * exports `getCollectionWidgetData` answers through its cache: every 2.x core,
8
+ * and the 1.x line from ZERO-5207 on. An older 1.x core has no such export, so
9
+ * the loader calls the same endpoint itself, uncached.
10
+ *
11
+ * Both paths must hand the renderer the same rows; only the cache differs.
12
+ */
13
+ jest.mock('server-only', () => ({}), { virtual: true });
14
+
15
+ const getWidgetData = jest.fn();
16
+ const getWidgetSchemaData = jest.fn();
17
+ const getCollectionWidgetData = jest.fn();
18
+ const getPreviewSnapshot = jest.fn();
19
+ const appFetch = jest.fn();
20
+
21
+ /** The core's server data layer, as this test shapes it. */
22
+ const core: Record<string, unknown> = {
23
+ __esModule: true,
24
+ getWidgetData: (...args: unknown[]) => getWidgetData(...args),
25
+ getWidgetSchemaData: (...args: unknown[]) => getWidgetSchemaData(...args),
26
+ getCollectionWidgetData: (...args: unknown[]) =>
27
+ getCollectionWidgetData(...args),
28
+ getPreviewSnapshot: (...args: unknown[]) => getPreviewSnapshot(...args)
29
+ };
30
+
31
+ jest.mock('@akinon/next/data/server', () => core);
32
+
33
+ jest.mock(
34
+ '@akinon/next/utils/app-fetch',
35
+ () => ({
36
+ __esModule: true,
37
+ default: (...args: unknown[]) => appFetch(...args)
38
+ }),
39
+ { virtual: true }
40
+ );
41
+
42
+ jest.mock(
43
+ '@akinon/next/utils/server-variables',
44
+ () => ({ ServerVariables: { locale: 'tr', currency: 'try' } }),
45
+ { virtual: true }
46
+ );
47
+
48
+ import { loadPlaceholderSections } from '../load-placeholder-sections';
49
+
50
+ const SLOT = 'homepage';
51
+ const SECTION = 'collection-slider';
52
+ const SOURCE_ID = 'ds-1';
53
+ const COLLECTION_ENDPOINT = `/product_collection_widgets/${SECTION}/`;
54
+
55
+ const LIVE_ROWS = [
56
+ { pk: 1, name: 'Live one', absolute_url: '/live-one/', price: '199.99' }
57
+ ];
58
+
59
+ /** The collection widget's answer: the bound collection's live rows. */
60
+ const COLLECTION_WIDGET = {
61
+ slug: SECTION,
62
+ attributes: {},
63
+ products: LIVE_ROWS
64
+ };
65
+
66
+ const withCollectionExport = (present: boolean) => {
67
+ core.getCollectionWidgetData = present
68
+ ? (...args: unknown[]) => getCollectionWidgetData(...args)
69
+ : undefined;
70
+ };
71
+
72
+ const loadSection = async () => {
73
+ getWidgetData.mockImplementation(async ({ slug }: { slug: string }) => {
74
+ if (slug === SLOT) {
75
+ return {
76
+ slug: SLOT,
77
+ attributes: {
78
+ theme_editor_placeholder: {
79
+ value: JSON.stringify([{ section_slug: SECTION, order: 0 }])
80
+ }
81
+ }
82
+ };
83
+ }
84
+
85
+ if (slug === 'theme-config') {
86
+ return {
87
+ slug: 'theme-config',
88
+ attributes: {
89
+ theme_dataSources: {
90
+ value: JSON.stringify([
91
+ {
92
+ id: SOURCE_ID,
93
+ name: 'Picked collection',
94
+ type: 'collection',
95
+ details: { collection: { id: 'c-1', products: [] } }
96
+ }
97
+ ])
98
+ }
99
+ }
100
+ };
101
+ }
102
+
103
+ // The section itself is a collection widget: the regular endpoint has
104
+ // nothing for it.
105
+ return null;
106
+ });
107
+
108
+ getWidgetSchemaData.mockResolvedValue({
109
+ schema: {
110
+ [SECTION]: {
111
+ metadata: {
112
+ name: 'Product slider',
113
+ dataSourceId: SOURCE_ID,
114
+ blocks: []
115
+ }
116
+ }
117
+ }
118
+ });
119
+
120
+ const loaded = await loadPlaceholderSections(SLOT);
121
+ return loaded?.sections[0];
122
+ };
123
+
124
+ const collectionEndpointCalls = () =>
125
+ appFetch.mock.calls.filter(([args]) => args?.url === COLLECTION_ENDPOINT);
126
+
127
+ beforeEach(() => {
128
+ getWidgetData.mockReset();
129
+ getWidgetSchemaData.mockReset();
130
+ getCollectionWidgetData.mockReset().mockResolvedValue(COLLECTION_WIDGET);
131
+ getPreviewSnapshot.mockReset().mockResolvedValue(null);
132
+ appFetch.mockReset().mockResolvedValue(COLLECTION_WIDGET);
133
+ });
134
+
135
+ afterAll(() => {
136
+ withCollectionExport(true);
137
+ });
138
+
139
+ describe('a core that exports getCollectionWidgetData', () => {
140
+ beforeEach(() => withCollectionExport(true));
141
+
142
+ it("reads the section's collection widget through the core", async () => {
143
+ const section = await loadSection();
144
+
145
+ expect(getCollectionWidgetData).toHaveBeenCalledWith({ slug: SECTION });
146
+ expect(collectionEndpointCalls()).toHaveLength(0);
147
+ expect(section?.dataSource.details.collection.data).toEqual(
148
+ COLLECTION_WIDGET
149
+ );
150
+ });
151
+ });
152
+
153
+ describe('an older 1.x core without it', () => {
154
+ beforeEach(() => withCollectionExport(false));
155
+
156
+ it('calls the collection widget endpoint itself and renders the same rows', async () => {
157
+ const section = await loadSection();
158
+
159
+ expect(getCollectionWidgetData).not.toHaveBeenCalled();
160
+ expect(collectionEndpointCalls()).toEqual([
161
+ [{ url: COLLECTION_ENDPOINT, locale: 'tr', currency: 'try' }]
162
+ ]);
163
+ expect(section?.dataSource.details.collection.data).toEqual(
164
+ COLLECTION_WIDGET
165
+ );
166
+ });
167
+ });
@@ -111,9 +111,10 @@ const FavoriteButtonBlock = ({
111
111
  const productPk = Number(pickProductField(product, 'pk'));
112
112
  const hasProduct = Number.isFinite(productPk) && productPk > 0;
113
113
 
114
- // The v1 core has no "is this product favourited" endpoint, so the current
115
- // state is derived from the favourites list. It is skipped for guests (the
116
- // endpoint 401s) and in the designer (no shopper session to reflect).
114
+ // No core, 1.x or 2.x, has an "is this product favourited" endpoint, so the
115
+ // current state is derived from the favourites list. It is skipped for
116
+ // guests (the endpoint 401s) and in the designer (no shopper session to
117
+ // reflect).
117
118
  const { data: favorites } = useGetFavoritesQuery(
118
119
  { limit: FAVORITES_PAGE_SIZE },
119
120
  { skip: !isAuthenticated || !hasProduct || isDesigner }
@@ -219,8 +219,8 @@ interface RecommendationCard {
219
219
  /**
220
220
  * Functional PDP/campaign block: a related-products slider.
221
221
  *
222
- * v1 core ships no recommendation endpoint, so the data path is the
223
- * collection/widget one every storefront already uses: the author names a
222
+ * No core, 1.x or 2.x, ships a recommendation endpoint, so the data path is
223
+ * the collection/widget one every storefront already uses: the author names a
224
224
  * collection and `getWidget` returns its products in merchandiser order. The
225
225
  * cards are laid out exactly like the product grid so a recommendation strip
226
226
  * and a PLP card stay visually identical.
@@ -45,11 +45,12 @@ const useUncheckedResetToken: ValidateResetTokenHook = () => UNCHECKED;
45
45
  /**
46
46
  * Read off the module object instead of imported by name.
47
47
  *
48
- * `useGetValidatePasswordResetTokenQuery` arrived with @akinon/next 2.x, and
49
- * most brands are still on the 1.x line. A brand transpiles and type-checks
50
- * this package from `src`, so a named import of an export its core does not
51
- * have fails `next build` before the block can ever run — while a bare runtime
52
- * lookup would leave `undefined` in a hook position and crash the screen.
48
+ * Every 2.x core has `useGetValidatePasswordResetTokenQuery`, but it reached
49
+ * the 1.x line only in 1.121.0 (ZERO-4009), and brands on an older 1.x core
50
+ * still run this package. A brand transpiles and type-checks this package
51
+ * from `src`, so a named import of an export its core does not have fails
52
+ * `next build` before the block can ever run — while a bare runtime lookup
53
+ * would leave `undefined` in a hook position and crash the screen.
53
54
  *
54
55
  * Both are answered here: the name never appears in an import clause, so the
55
56
  * v1 type-checker has nothing to miss, and the fallback above is bound in its
@@ -21,14 +21,16 @@ import { carriesProducts } from './utils/live-rows';
21
21
  const { getWidgetData } = serverData;
22
22
 
23
23
  /**
24
- * v1 cores ship a smaller server data layer: only `getWidgetData` exists,
25
- * `getWidgetSchemaData` and `getCollectionWidgetData` arrived with v2.
24
+ * Older 1.x cores ship a smaller server data layer: every 2.x core has
25
+ * `getWidgetSchemaData` and `getCollectionWidgetData`, but they reached the
26
+ * 1.x line only with ZERO-5194 and ZERO-5207.
26
27
  *
27
- * The endpoints are identical on both backends — v1 just never wrapped them.
28
- * Degrading to "no data" would render every published section *without its
29
- * blocks* (a section's block tree lives in the widget schema, not in the
30
- * widget), so on v1 we call the REST endpoints directly. This is what keeps
31
- * composed pages rendering on the brands still running v1.
28
+ * The endpoints are identical on both backends — those cores just never
29
+ * wrapped them. Degrading to "no data" would render every published section
30
+ * *without its blocks* (a section's block tree lives in the widget schema, not
31
+ * in the widget), so on such a core we call the REST endpoints directly,
32
+ * without the core's cache. This is what keeps composed pages rendering on the
33
+ * brands whose core predates them.
32
34
  */
33
35
  type ServerDataFn = (<T>(args: never) => Promise<T | null>) | undefined;
34
36