@akinon/pz-theme 2.0.67-rc.0 → 2.0.68-beta.0
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 +15 -31
- package/jest/__mocks__/akinon-next-plugin-module.js +18 -0
- package/jest.config.js +2 -0
- package/package.json +4 -4
- package/src/blocks/__tests__/basket-line-image.test.tsx +126 -0
- package/src/blocks/__tests__/basket-line-layout.test.tsx +336 -0
- package/src/blocks/__tests__/basket-line-price.test.tsx +227 -0
- package/src/blocks/__tests__/basket-offers.test.tsx +198 -0
- package/src/blocks/__tests__/product-recommendations-block.test.tsx +169 -0
- package/src/blocks/basket-items-block.tsx +677 -45
- package/src/blocks/basket-summary-block.tsx +636 -92
- package/src/blocks/checkout-summary-block.tsx +8 -3
- package/src/blocks/product-block-utils.tsx +31 -1
- package/src/blocks/product-recommendations-block.tsx +209 -51
- package/src/theme-section-data-context.tsx +32 -0
- package/src/theme-section.tsx +13 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,36 +1,20 @@
|
|
|
1
1
|
# @akinon/pz-theme
|
|
2
2
|
|
|
3
|
-
## 2.0.
|
|
4
|
-
|
|
5
|
-
### Patch Changes
|
|
6
|
-
|
|
7
|
-
- Updated dependencies [
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
- Updated dependencies [57d7eb305]
|
|
19
|
-
- Updated dependencies [d99a6a7d5]
|
|
20
|
-
- Updated dependencies [9db81a714]
|
|
21
|
-
- Updated dependencies [591e345e1]
|
|
22
|
-
- Updated dependencies [4de5303c5]
|
|
23
|
-
- Updated dependencies [95b139dc1]
|
|
24
|
-
- Updated dependencies [1d00f2d06]
|
|
25
|
-
- Updated dependencies [4ac7b2a1e]
|
|
26
|
-
- Updated dependencies [e9598c71]
|
|
27
|
-
- Updated dependencies [4998a9631]
|
|
28
|
-
- Updated dependencies [804d2bd6]
|
|
29
|
-
- Updated dependencies [3909d3224]
|
|
30
|
-
- Updated dependencies [6a3d8a63]
|
|
31
|
-
- Updated dependencies [e18836b20]
|
|
32
|
-
- @akinon/next@2.0.67-rc.0
|
|
33
|
-
- @akinon/theme-contract@2.0.67-rc.0
|
|
3
|
+
## 2.0.68-beta.0
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [cbbbfd75]
|
|
8
|
+
- @akinon/next@2.0.68-beta.0
|
|
9
|
+
- @akinon/theme-contract@2.0.68-beta.0
|
|
10
|
+
|
|
11
|
+
## 2.0.67
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- bf503b3: ZERO-4819: add basket redesign block features and line image fix
|
|
16
|
+
- @akinon/next@2.0.67
|
|
17
|
+
- @akinon/theme-contract@2.0.67
|
|
34
18
|
|
|
35
19
|
## 2.0.66
|
|
36
20
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Mock for @akinon/next/components/plugin-module: the real one dynamically
|
|
2
|
+
// imports brand plugins, which have no place in a unit test. The slot renders
|
|
3
|
+
// a marker so a test can assert where a block placed it.
|
|
4
|
+
const React = require('react');
|
|
5
|
+
|
|
6
|
+
const Component = {
|
|
7
|
+
BasketGiftPack: 'BasketGiftPack',
|
|
8
|
+
CheckoutGiftPack: 'CheckoutGiftPack'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const PluginModule = ({ component }) =>
|
|
12
|
+
React.createElement('div', { 'data-plugin-module': component });
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
__esModule: true,
|
|
16
|
+
default: PluginModule,
|
|
17
|
+
Component
|
|
18
|
+
};
|
package/jest.config.js
CHANGED
|
@@ -30,6 +30,8 @@ module.exports = {
|
|
|
30
30
|
'<rootDir>/jest/__mocks__/akinon-next-modal.js',
|
|
31
31
|
'^@akinon/next/components/price$':
|
|
32
32
|
'<rootDir>/jest/__mocks__/akinon-next-price.js',
|
|
33
|
+
'^@akinon/next/components/plugin-module$':
|
|
34
|
+
'<rootDir>/jest/__mocks__/akinon-next-plugin-module.js',
|
|
33
35
|
'^@akinon/next/components/loader-spinner$':
|
|
34
36
|
'<rootDir>/jest/__mocks__/akinon-next-loader-spinner.js',
|
|
35
37
|
'^@akinon/next/data/client/(.*)$':
|
package/package.json
CHANGED
|
@@ -1,26 +1,26 @@
|
|
|
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.68-beta.0",
|
|
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.68-beta.0",
|
|
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.68-beta.0",
|
|
17
17
|
"clsx": "2.1.1",
|
|
18
18
|
"lottie-web": "^5.13.0",
|
|
19
19
|
"tailwind-merge": "^2.5.4",
|
|
20
20
|
"zod": "4.4.1"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@akinon/next": "2.0.
|
|
23
|
+
"@akinon/next": "2.0.68-beta.0",
|
|
24
24
|
"@testing-library/jest-dom": "5.16.4",
|
|
25
25
|
"@testing-library/react": "16.3.2",
|
|
26
26
|
"@testing-library/user-event": "14.1.1",
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render } from '@testing-library/react';
|
|
3
|
+
import BasketItemsBlock from '../basket-items-block';
|
|
4
|
+
import CheckoutSummaryBlock from '../checkout-summary-block';
|
|
5
|
+
import { getLineImage, groupOrderItems } from '../product-block-utils';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Commerce basket and order payloads carry the product picture only in
|
|
9
|
+
* `product.productimage_set`; `line.image` and `product.image` come back
|
|
10
|
+
* empty. The blocks that read the live query used to look at the two empty
|
|
11
|
+
* fields and draw no image at all, on every brand, in every basket layout.
|
|
12
|
+
*/
|
|
13
|
+
const CDN_IMAGE = 'https://cdn.example.com/products/polo.jpg';
|
|
14
|
+
|
|
15
|
+
const basketLine = {
|
|
16
|
+
id: 1,
|
|
17
|
+
quantity: 1,
|
|
18
|
+
image: null,
|
|
19
|
+
price: '5999.00',
|
|
20
|
+
unit_price: '5999.00',
|
|
21
|
+
total_amount: '5999.00',
|
|
22
|
+
currency_type: 'try',
|
|
23
|
+
attributes: {},
|
|
24
|
+
product: {
|
|
25
|
+
pk: 37727,
|
|
26
|
+
name: 'GANT Erkek Siyah Polo',
|
|
27
|
+
absolute_url: '/polo/',
|
|
28
|
+
productimage_set: [{ image: CDN_IMAGE }]
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let basketData: unknown = { basketitem_set: [basketLine] };
|
|
33
|
+
|
|
34
|
+
jest.mock('@akinon/next/redux/hooks', () => ({
|
|
35
|
+
__esModule: true,
|
|
36
|
+
useAppSelector: () => undefined,
|
|
37
|
+
useAppDispatch: () => jest.fn()
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Every `@akinon/next/data/client/*` subpath maps to the same mock file, so
|
|
41
|
+
// one factory has to serve the basket and the wishlist client alike.
|
|
42
|
+
jest.mock('@akinon/next/data/client/basket', () => ({
|
|
43
|
+
__esModule: true,
|
|
44
|
+
useGetBasketQuery: () => ({ data: basketData, isLoading: false }),
|
|
45
|
+
useUpdateQuantityMutation: () => [jest.fn(), { isLoading: false }],
|
|
46
|
+
useGetFavoritesQuery: () => ({ data: undefined, isLoading: false }),
|
|
47
|
+
useAddFavoriteMutation: () => [jest.fn(), { isLoading: false }],
|
|
48
|
+
useRemoveFavoriteMutation: () => [jest.fn(), { isLoading: false }]
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const renderBlock = (
|
|
52
|
+
Component: React.ComponentType<any>,
|
|
53
|
+
type: string,
|
|
54
|
+
properties: Record<string, unknown> = {}
|
|
55
|
+
) =>
|
|
56
|
+
render(
|
|
57
|
+
<Component
|
|
58
|
+
block={{ id: `${type}-1`, type, properties, styles: {}, blocks: [] }}
|
|
59
|
+
placeholderId="p1"
|
|
60
|
+
sectionId="s1"
|
|
61
|
+
isDesigner={false}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
describe('getLineImage', () => {
|
|
66
|
+
it('falls back through line, product and the product gallery', () => {
|
|
67
|
+
expect(getLineImage({ image: '/line.jpg', product: null })).toBe(
|
|
68
|
+
'/line.jpg'
|
|
69
|
+
);
|
|
70
|
+
expect(
|
|
71
|
+
getLineImage({ image: '', product: { image: '/product.jpg' } })
|
|
72
|
+
).toBe('/product.jpg');
|
|
73
|
+
expect(getLineImage(basketLine)).toBe(CDN_IMAGE);
|
|
74
|
+
expect(
|
|
75
|
+
getLineImage({ product: { productimage_set: [{ url: '/url.jpg' }] } })
|
|
76
|
+
).toBe('/url.jpg');
|
|
77
|
+
expect(getLineImage({ product: { productimage_set: [] } })).toBe('');
|
|
78
|
+
expect(getLineImage(null)).toBe('');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('is what grouped order lines use for their image', () => {
|
|
82
|
+
const [line] = groupOrderItems([
|
|
83
|
+
{ id: 9, price: '10', quantity: 1, product: basketLine.product }
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
expect(line.image).toBe(CDN_IMAGE);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('basket line images', () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
basketData = { basketitem_set: [basketLine] };
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('basket-items draws the gallery image when the line carries no direct one', () => {
|
|
96
|
+
const { container } = renderBlock(BasketItemsBlock, 'basket-items');
|
|
97
|
+
|
|
98
|
+
const img = container.querySelector('[data-part="image"]');
|
|
99
|
+
expect(img).not.toBeNull();
|
|
100
|
+
expect(img?.getAttribute('src')).toBe(CDN_IMAGE);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('basket-items still prefers a direct line image', () => {
|
|
104
|
+
basketData = {
|
|
105
|
+
basketitem_set: [{ ...basketLine, image: '/direct.jpg' }]
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const { container } = renderBlock(BasketItemsBlock, 'basket-items');
|
|
109
|
+
|
|
110
|
+
expect(
|
|
111
|
+
container.querySelector('[data-part="image"]')?.getAttribute('src')
|
|
112
|
+
).toBe('/direct.jpg');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('checkout-summary draws the gallery image for its line list', () => {
|
|
116
|
+
const { container } = renderBlock(
|
|
117
|
+
CheckoutSummaryBlock,
|
|
118
|
+
'checkout-summary',
|
|
119
|
+
{ showItemImage: true }
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const img = container.querySelector('[data-part="image"]');
|
|
123
|
+
expect(img).not.toBeNull();
|
|
124
|
+
expect(img?.getAttribute('src')).toBe(CDN_IMAGE);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { fireEvent, render } from '@testing-library/react';
|
|
3
|
+
import BasketItemsBlock from '../basket-items-block';
|
|
4
|
+
import BasketSummaryBlock from '../basket-summary-block';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The basket page redesign needs the line and the summary to say things the
|
|
8
|
+
* built-in rows never did: attribute rows with their own labels, a quantity
|
|
9
|
+
* select, the remove control under the line, the gift wrap plugin slot, and a
|
|
10
|
+
* voucher / gift card panel that opens on demand below the checkout button.
|
|
11
|
+
* Every default stays exactly what published compositions render today.
|
|
12
|
+
*/
|
|
13
|
+
const updateQuantity = jest.fn(() => ({ unwrap: () => Promise.resolve({}) }));
|
|
14
|
+
const applyVoucher = jest.fn(() => ({
|
|
15
|
+
unwrap: () => Promise.resolve({ total_amount: '1.00' })
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
const basketLine = {
|
|
19
|
+
id: 1,
|
|
20
|
+
quantity: 2,
|
|
21
|
+
price: '5999.00',
|
|
22
|
+
unit_price: '5999.00',
|
|
23
|
+
total_amount: '11998.00',
|
|
24
|
+
currency_type: 'try',
|
|
25
|
+
attributes: {},
|
|
26
|
+
product: {
|
|
27
|
+
pk: 37727,
|
|
28
|
+
name: 'GANT Erkek Siyah Polo',
|
|
29
|
+
absolute_url: '/polo/',
|
|
30
|
+
productimage_set: [{ image: '/polo.jpg' }],
|
|
31
|
+
attributes: { integration_renk_detay: 'Siyah', integration_beden: 'S' },
|
|
32
|
+
attributes_kwargs: {
|
|
33
|
+
integration_beden: { label: 'S', value: 'S' }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let basketData: unknown = {
|
|
39
|
+
basketitem_set: [basketLine],
|
|
40
|
+
total_quantity: 2,
|
|
41
|
+
total_product_amount: '11998.00',
|
|
42
|
+
total_discount_amount: '0.00',
|
|
43
|
+
total_amount: '11998.00',
|
|
44
|
+
voucher_code: null
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Every `@akinon/next/data/client/*` subpath maps to the same mock file, so
|
|
48
|
+
// one factory has to serve the basket and the account client alike.
|
|
49
|
+
jest.mock('@akinon/next/data/client/basket', () => ({
|
|
50
|
+
__esModule: true,
|
|
51
|
+
basketApi: { util: { updateQueryData: () => ({ type: 'noop' }) } },
|
|
52
|
+
useGetBasketQuery: () => ({ data: basketData, isLoading: false }),
|
|
53
|
+
useUpdateQuantityMutation: () => [updateQuantity, { isLoading: false }],
|
|
54
|
+
useApplyVoucherCodeMutation: () => [applyVoucher, { isLoading: false }],
|
|
55
|
+
useRemoveVoucherCodeMutation: () => [jest.fn(), { isLoading: false }],
|
|
56
|
+
useGetBasketOffersQuery: () => ({ data: undefined, isLoading: false }),
|
|
57
|
+
useGetFavoritesQuery: () => ({ data: undefined, isLoading: false }),
|
|
58
|
+
useAddFavoriteMutation: () => [jest.fn(), { isLoading: false }],
|
|
59
|
+
useRemoveFavoriteMutation: () => [jest.fn(), { isLoading: false }]
|
|
60
|
+
}));
|
|
61
|
+
|
|
62
|
+
jest.mock('@akinon/next/redux/hooks', () => ({
|
|
63
|
+
__esModule: true,
|
|
64
|
+
useAppSelector: () => undefined,
|
|
65
|
+
useAppDispatch: () => jest.fn()
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
const renderBlock = (
|
|
69
|
+
Component: React.ComponentType<any>,
|
|
70
|
+
type: string,
|
|
71
|
+
properties: Record<string, unknown> = {},
|
|
72
|
+
isDesigner = false
|
|
73
|
+
) =>
|
|
74
|
+
render(
|
|
75
|
+
<Component
|
|
76
|
+
block={{ id: `${type}-1`, type, properties, styles: {}, blocks: [] }}
|
|
77
|
+
placeholderId="p1"
|
|
78
|
+
sectionId="s1"
|
|
79
|
+
isDesigner={isDesigner}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
updateQuantity.mockClear();
|
|
85
|
+
applyVoucher.mockClear();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('basket-items line layout', () => {
|
|
89
|
+
it('keeps the built-in row unchanged by default', () => {
|
|
90
|
+
const { container } = renderBlock(BasketItemsBlock, 'basket-items');
|
|
91
|
+
|
|
92
|
+
expect(container.querySelector('[data-part="variant-row"]')).toBeNull();
|
|
93
|
+
expect(container.querySelector('[data-part="quantity-select"]')).toBeNull();
|
|
94
|
+
expect(container.querySelector('[data-part="gift-wrap"]')).toBeNull();
|
|
95
|
+
// The remove control still sits in the aside column.
|
|
96
|
+
const actions = container.querySelector('[data-part="actions"]');
|
|
97
|
+
expect(actions?.querySelector('[data-part="remove"]')).not.toBeNull();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('draws labelled attribute rows from the product when the line carries none', () => {
|
|
101
|
+
const { container } = renderBlock(BasketItemsBlock, 'basket-items', {
|
|
102
|
+
showVariant: true,
|
|
103
|
+
variantLayout: 'rows',
|
|
104
|
+
variantRows: 'integration_renk_detay:Renk, integration_beden:Beden',
|
|
105
|
+
variantLabelSuffix: ':'
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const rows = container.querySelectorAll('[data-part="variant-row"]');
|
|
109
|
+
expect(rows).toHaveLength(2);
|
|
110
|
+
expect(rows[0].querySelector('[data-part="variant-label"]')?.textContent).toBe(
|
|
111
|
+
'Renk:'
|
|
112
|
+
);
|
|
113
|
+
expect(rows[0].querySelector('[data-part="variant-value"]')?.textContent).toBe(
|
|
114
|
+
'Siyah'
|
|
115
|
+
);
|
|
116
|
+
expect(rows[1].querySelector('[data-part="variant-value"]')?.textContent).toBe(
|
|
117
|
+
'S'
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('skips attribute rows the payload cannot fill and defaults to a bare label', () => {
|
|
122
|
+
const { container } = renderBlock(BasketItemsBlock, 'basket-items', {
|
|
123
|
+
showVariant: true,
|
|
124
|
+
variantLayout: 'rows',
|
|
125
|
+
variantRows: 'integration_beden, missing_key:Missing'
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const rows = container.querySelectorAll('[data-part="variant-row"]');
|
|
129
|
+
expect(rows).toHaveLength(1);
|
|
130
|
+
expect(rows[0].querySelector('[data-part="variant-label"]')?.textContent).toBe(
|
|
131
|
+
'Integration beden'
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('offers a quantity select that updates the line', () => {
|
|
136
|
+
const { container } = renderBlock(BasketItemsBlock, 'basket-items', {
|
|
137
|
+
quantityControl: 'select',
|
|
138
|
+
quantityMax: 5
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const select = container.querySelector(
|
|
142
|
+
'[data-part="quantity-select"]'
|
|
143
|
+
) as HTMLSelectElement;
|
|
144
|
+
expect(select).not.toBeNull();
|
|
145
|
+
expect(select.value).toBe('2');
|
|
146
|
+
expect(select.querySelectorAll('option')).toHaveLength(5);
|
|
147
|
+
|
|
148
|
+
fireEvent.change(select, { target: { value: '4' } });
|
|
149
|
+
expect(updateQuantity).toHaveBeenCalledWith(
|
|
150
|
+
expect.objectContaining({ product: 37727, quantity: 4 })
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('moves the remove control under the line and mounts the gift wrap slot', () => {
|
|
155
|
+
const { container } = renderBlock(BasketItemsBlock, 'basket-items', {
|
|
156
|
+
removePlacement: 'inline',
|
|
157
|
+
showGiftWrap: true,
|
|
158
|
+
removeIcon: 'text',
|
|
159
|
+
removeLabel: 'Sil'
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const content = container.querySelector('[data-part="content"]');
|
|
163
|
+
expect(content?.querySelector('[data-part="remove"]')?.textContent).toBe(
|
|
164
|
+
'Sil'
|
|
165
|
+
);
|
|
166
|
+
expect(
|
|
167
|
+
container.querySelector('[data-part="actions"] [data-part="remove"]')
|
|
168
|
+
).toBeNull();
|
|
169
|
+
expect(
|
|
170
|
+
content?.querySelector(
|
|
171
|
+
'[data-part="gift-wrap"] [data-plugin-module="BasketGiftPack"]'
|
|
172
|
+
)
|
|
173
|
+
).not.toBeNull();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('formats the unit price with the shared money options', () => {
|
|
177
|
+
const { container } = renderBlock(BasketItemsBlock, 'basket-items', {
|
|
178
|
+
decimals: 'never',
|
|
179
|
+
currencyDisplay: 'custom',
|
|
180
|
+
currencyText: 'TL',
|
|
181
|
+
currencyPosition: 'after'
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(container.querySelector('[data-part="price"]')?.textContent).toBe(
|
|
185
|
+
'5,999 TL'
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('basket-summary panels', () => {
|
|
191
|
+
it('renders the voucher form inline before the button by default', () => {
|
|
192
|
+
const { container } = renderBlock(BasketSummaryBlock, 'basket-summary');
|
|
193
|
+
|
|
194
|
+
const root = container.querySelector('[data-part="root"]') as HTMLElement;
|
|
195
|
+
const children = Array.from(root.children).map((child) =>
|
|
196
|
+
child.getAttribute('data-part')
|
|
197
|
+
);
|
|
198
|
+
expect(children).toEqual(['totals', 'coupon', 'cta']);
|
|
199
|
+
expect(container.querySelector('[data-part="coupon-toggle"]')).toBeNull();
|
|
200
|
+
expect(container.querySelector('[data-part="coupon-form"]')).not.toBeNull();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
it('hides the voucher form behind a header that opens it, below the button', () => {
|
|
204
|
+
const { container } = renderBlock(BasketSummaryBlock, 'basket-summary', {
|
|
205
|
+
couponPresentation: 'collapsible',
|
|
206
|
+
couponPlacement: 'after-cta',
|
|
207
|
+
couponToggleLabel: 'Promosyon Kodu',
|
|
208
|
+
ctaNote: 'Free shipping for members'
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const root = container.querySelector('[data-part="root"]') as HTMLElement;
|
|
212
|
+
const children = Array.from(root.children).map((child) =>
|
|
213
|
+
child.getAttribute('data-part')
|
|
214
|
+
);
|
|
215
|
+
expect(children).toEqual(['totals', 'cta', 'cta-note', 'coupon']);
|
|
216
|
+
|
|
217
|
+
const toggle = container.querySelector(
|
|
218
|
+
'[data-part="coupon-toggle"]'
|
|
219
|
+
) as HTMLButtonElement;
|
|
220
|
+
expect(toggle.textContent).toContain('Promosyon Kodu');
|
|
221
|
+
expect(toggle.getAttribute('aria-expanded')).toBe('false');
|
|
222
|
+
expect(container.querySelector('[data-part="coupon-form"]')).toBeNull();
|
|
223
|
+
|
|
224
|
+
fireEvent.click(toggle);
|
|
225
|
+
expect(toggle.getAttribute('aria-expanded')).toBe('true');
|
|
226
|
+
expect(
|
|
227
|
+
container.querySelector('[data-part="coupon"]')?.getAttribute('data-state')
|
|
228
|
+
).toBe('open');
|
|
229
|
+
expect(container.querySelector('[data-part="coupon-form"]')).not.toBeNull();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('adds a gift card panel that redeems through the voucher endpoint', () => {
|
|
233
|
+
const { container } = renderBlock(BasketSummaryBlock, 'basket-summary', {
|
|
234
|
+
showCoupon: false,
|
|
235
|
+
showGiftCard: true,
|
|
236
|
+
couponPlacement: 'after-cta',
|
|
237
|
+
giftCardToggleLabel: 'Hediye Kartı',
|
|
238
|
+
giftCardApplyLabel: 'Uygula'
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const toggle = container.querySelector(
|
|
242
|
+
'[data-part="gift-card-toggle"]'
|
|
243
|
+
) as HTMLButtonElement;
|
|
244
|
+
expect(toggle.textContent).toContain('Hediye Kartı');
|
|
245
|
+
expect(container.querySelector('[data-part="gift-card-form"]')).toBeNull();
|
|
246
|
+
|
|
247
|
+
fireEvent.click(toggle);
|
|
248
|
+
const number = container.querySelector(
|
|
249
|
+
'[data-part="gift-card-input"]'
|
|
250
|
+
) as HTMLInputElement;
|
|
251
|
+
fireEvent.change(number, { target: { value: '1234 5678' } });
|
|
252
|
+
fireEvent.submit(
|
|
253
|
+
container.querySelector('[data-part="gift-card-form"]') as HTMLFormElement
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
expect(applyVoucher).toHaveBeenCalledWith({ voucher_code: '1234 5678' });
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('prints one discount row per basket campaign when asked to', () => {
|
|
260
|
+
basketData = {
|
|
261
|
+
basketitem_set: [basketLine],
|
|
262
|
+
total_quantity: 2,
|
|
263
|
+
total_product_amount: '11998.00',
|
|
264
|
+
total_discount_amount: '4998.00',
|
|
265
|
+
total_amount: '7000.00',
|
|
266
|
+
voucher_code: 'GANT2026',
|
|
267
|
+
discounts: [
|
|
268
|
+
{ description: 'Sepette %30 İndirim', discount: '2499.00' },
|
|
269
|
+
{ description: 'Kupon Kodu - GANT2026', discount: '2499.00' },
|
|
270
|
+
{ description: 'Sıfır', discount: '0.00' }
|
|
271
|
+
]
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const itemised = renderBlock(BasketSummaryBlock, 'basket-summary', {
|
|
275
|
+
discountRows: 'each',
|
|
276
|
+
discountPrefix: '-',
|
|
277
|
+
decimals: 'never',
|
|
278
|
+
currencyDisplay: 'custom',
|
|
279
|
+
currencyText: 'TL',
|
|
280
|
+
currencyPosition: 'after'
|
|
281
|
+
});
|
|
282
|
+
const labels = Array.from(
|
|
283
|
+
itemised.container.querySelectorAll('[data-basket-summary-row="discount"]')
|
|
284
|
+
).map((row) => row.textContent);
|
|
285
|
+
expect(labels).toEqual([
|
|
286
|
+
'Sepette %30 İndirim-2,499 TL',
|
|
287
|
+
'Kupon Kodu - GANT2026-2,499 TL'
|
|
288
|
+
]);
|
|
289
|
+
itemised.unmount();
|
|
290
|
+
|
|
291
|
+
// The default stays the single summed row.
|
|
292
|
+
const summed = renderBlock(BasketSummaryBlock, 'basket-summary', {
|
|
293
|
+
discountLabel: 'İndirim',
|
|
294
|
+
decimals: 'never',
|
|
295
|
+
currencyDisplay: 'custom',
|
|
296
|
+
currencyText: 'TL',
|
|
297
|
+
currencyPosition: 'after'
|
|
298
|
+
});
|
|
299
|
+
expect(
|
|
300
|
+
Array.from(
|
|
301
|
+
summed.container.querySelectorAll('[data-basket-summary-row="discount"]')
|
|
302
|
+
).map((row) => row.textContent)
|
|
303
|
+
).toEqual(['İndirim-4,998 TL']);
|
|
304
|
+
|
|
305
|
+
basketData = {
|
|
306
|
+
basketitem_set: [basketLine],
|
|
307
|
+
total_quantity: 2,
|
|
308
|
+
total_product_amount: '11998.00',
|
|
309
|
+
total_discount_amount: '0.00',
|
|
310
|
+
total_amount: '11998.00',
|
|
311
|
+
voucher_code: null
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('prints the piece count and the authored shipping row', () => {
|
|
316
|
+
const { container } = renderBlock(BasketSummaryBlock, 'basket-summary', {
|
|
317
|
+
subtotalLabel: '{count} Parça',
|
|
318
|
+
shippingLabel: 'Kargo',
|
|
319
|
+
shippingValue: '0 TL',
|
|
320
|
+
decimals: 'never',
|
|
321
|
+
currencyDisplay: 'custom',
|
|
322
|
+
currencyText: 'TL',
|
|
323
|
+
currencyPosition: 'after'
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
const labels = Array.from(
|
|
327
|
+
container.querySelectorAll('[data-part="row-label"], [data-part="total-label"]')
|
|
328
|
+
).map((node) => node.textContent);
|
|
329
|
+
expect(labels).toEqual(['2 Parça', 'Kargo', 'Total']);
|
|
330
|
+
|
|
331
|
+
const values = Array.from(
|
|
332
|
+
container.querySelectorAll('[data-part="row-value"], [data-part="total-value"]')
|
|
333
|
+
).map((node) => node.textContent);
|
|
334
|
+
expect(values).toEqual(['11,998 TL', '0 TL', '11,998 TL']);
|
|
335
|
+
});
|
|
336
|
+
});
|