@akinon/pz-theme 2.0.43-beta.0 → 2.0.43
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 +4 -4
- package/package.json +4 -4
- package/src/__tests__/checkout-pieces.test.tsx +420 -0
- package/src/__tests__/chrome-empty-zone.test.tsx +3 -3
- package/src/__tests__/code-owned-region.test.tsx +30 -6
- package/src/__tests__/composable-region.test.tsx +481 -0
- package/src/__tests__/part-styles.test.ts +24 -4
- package/src/__tests__/registry-parity.test.ts +3 -0
- package/src/__tests__/route-slug.test.ts +11 -0
- package/src/blocks/__tests__/checkout-outlet-blocks.test.tsx +623 -0
- package/src/blocks/block-renderer-registry.tsx +6 -0
- package/src/blocks/checkout-flow-outlet-block.tsx +104 -0
- package/src/blocks/checkout-outlet-base.tsx +241 -0
- package/src/blocks/checkout-steps-outlet-block.tsx +57 -0
- package/src/blocks/checkout-summary-outlet-block.tsx +43 -0
- package/src/chrome/checkout-piece.tsx +145 -0
- package/src/chrome/checkout-pieces-provider.tsx +28 -0
- package/src/chrome/checkout-pieces.ts +173 -0
- package/src/chrome/chrome-fallback.tsx +8 -26
- package/src/chrome/chrome-slot.tsx +25 -26
- package/src/chrome/code-owned-region.tsx +21 -9
- package/src/chrome/composable-region-client.tsx +228 -0
- package/src/chrome/composable-region.tsx +58 -0
- package/src/chrome/composition-source.ts +34 -0
- package/src/chrome/composition-state.ts +49 -0
- package/src/chrome/live-checkout-page-context.tsx +3 -2
- package/src/chrome/placeholder-slugs.ts +14 -4
- package/src/chrome/route-slug.ts +6 -3
- package/src/hooks/use-designer-features.tsx +41 -10
- package/src/index.ts +6 -0
- package/src/utils/__tests__/checkout-safety.test.ts +374 -0
- package/src/utils/checkout-safety.ts +177 -0
- package/src/utils/index.ts +47 -8
- package/src/utils/part-styles.ts +4 -0
- package/src/utils/visibility-rules.ts +31 -8
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { render, screen, fireEvent, act } from '@testing-library/react';
|
|
3
|
+
|
|
4
|
+
import { ComposableRegionClient } from '../chrome/composable-region-client';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The checkout body is composable, but its content — the real payment flow —
|
|
8
|
+
* can never be withdrawn to make room for the takeover affordance the way a
|
|
9
|
+
* ChromeFallback withdraws brand markup. So this region offers the affordance
|
|
10
|
+
* OVER live content it always keeps mounted.
|
|
11
|
+
*
|
|
12
|
+
* These tests pin both halves of that: the affordance behaves exactly like the
|
|
13
|
+
* one on every other page (same message, same trust rules, same composed-state
|
|
14
|
+
* gate), and the children survive every state it can be in — losing them would
|
|
15
|
+
* mean losing a customer's checkout session, mid-3DS in the worst case.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const EDITOR_ORIGIN = 'http://localhost:5174';
|
|
19
|
+
|
|
20
|
+
const pushTheme = (
|
|
21
|
+
sections: unknown[],
|
|
22
|
+
slug = 'checkout-page-body',
|
|
23
|
+
origin = EDITOR_ORIGIN
|
|
24
|
+
) =>
|
|
25
|
+
act(() => {
|
|
26
|
+
fireEvent(
|
|
27
|
+
window,
|
|
28
|
+
new MessageEvent('message', {
|
|
29
|
+
origin,
|
|
30
|
+
data: {
|
|
31
|
+
type: 'UPDATE_THEME',
|
|
32
|
+
data: {
|
|
33
|
+
theme: {
|
|
34
|
+
placeholders: [{ slug, sections }]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/** Stateful stand-in for the flow: its count survives only if it is not remounted. */
|
|
43
|
+
const Counter = () => {
|
|
44
|
+
const [count, setCount] = useState(0);
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<button type="button" onClick={() => setCount((value) => value + 1)}>
|
|
48
|
+
count:{count}
|
|
49
|
+
</button>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** Frames the document without proving an editor — a production embed. */
|
|
54
|
+
const frameDocument = () =>
|
|
55
|
+
Object.defineProperty(window, 'top', {
|
|
56
|
+
configurable: true,
|
|
57
|
+
value: { name: 'embedder' }
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const unframeDocument = () =>
|
|
61
|
+
Object.defineProperty(window, 'top', { configurable: true, value: window });
|
|
62
|
+
|
|
63
|
+
describe('ComposableRegion in the designer', () => {
|
|
64
|
+
beforeAll(() => {
|
|
65
|
+
// The designer is framed AND has proven itself with the editor cookie
|
|
66
|
+
// frame. Both are needed: framing is what lets `postToEditor` reach a
|
|
67
|
+
// parent, the flag is what makes this a designer rather than an embed.
|
|
68
|
+
frameDocument();
|
|
69
|
+
window.__themeEditorDesigner = true;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterAll(() => {
|
|
73
|
+
delete window.__themeEditorDesigner;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('offers the takeover on hover over the live content', () => {
|
|
77
|
+
render(
|
|
78
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
79
|
+
<div data-testid="flow">payment steps</div>
|
|
80
|
+
</ComposableRegionClient>
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Unhovered the area looks untouched — the flow is what the designer is
|
|
84
|
+
// meant to see, not a permanent overlay on top of it.
|
|
85
|
+
expect(screen.getByTestId('flow')).toBeInTheDocument();
|
|
86
|
+
expect(
|
|
87
|
+
screen.queryByRole('button', { name: /edit in theme editor/i })
|
|
88
|
+
).not.toBeInTheDocument();
|
|
89
|
+
|
|
90
|
+
fireEvent.mouseEnter(
|
|
91
|
+
document.querySelector('[data-chrome-region-affordance]')!
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(
|
|
95
|
+
screen.getByText(/checkout-page-body area is not composed yet/i)
|
|
96
|
+
).toBeInTheDocument();
|
|
97
|
+
expect(
|
|
98
|
+
screen.getByRole('button', { name: /edit in theme editor/i })
|
|
99
|
+
).toBeInTheDocument();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('asks the editor to take the area over when the affordance is used', () => {
|
|
103
|
+
// What the designer clicks has to arrive as a contract message addressed
|
|
104
|
+
// to the editor: an unknown type never reaches the editor's handler, and
|
|
105
|
+
// the template picker never opens.
|
|
106
|
+
const postMessage = jest.fn();
|
|
107
|
+
Object.defineProperty(window, 'parent', {
|
|
108
|
+
configurable: true,
|
|
109
|
+
value: { postMessage }
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
render(
|
|
113
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
114
|
+
<div data-testid="flow">payment steps</div>
|
|
115
|
+
</ComposableRegionClient>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// A frame from the editor is what proves its origin to the bridge; without
|
|
119
|
+
// one there is no trusted target and the message is dropped by design.
|
|
120
|
+
pushTheme([]);
|
|
121
|
+
|
|
122
|
+
fireEvent.mouseEnter(
|
|
123
|
+
document.querySelector('[data-chrome-region-affordance]')!
|
|
124
|
+
);
|
|
125
|
+
fireEvent.click(
|
|
126
|
+
screen.getByRole('button', { name: /edit in theme editor/i })
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
expect(postMessage).toHaveBeenCalledTimes(1);
|
|
130
|
+
const [envelope, targetOrigin] = postMessage.mock.calls[0];
|
|
131
|
+
expect(envelope).toMatchObject({
|
|
132
|
+
type: 'REQUEST_CHROME_OVERRIDE',
|
|
133
|
+
data: { slug: 'checkout-page-body' }
|
|
134
|
+
});
|
|
135
|
+
expect(envelope.protocolVersion).toBeGreaterThan(0);
|
|
136
|
+
expect(targetOrigin).toBe(EDITOR_ORIGIN);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('NEVER unmounts the children, in any composition state', () => {
|
|
140
|
+
// The reason this component exists instead of a ChromeFallback mode.
|
|
141
|
+
// A remount here refetches checkout state and would break a 3DS session,
|
|
142
|
+
// so the flow's own state has to survive every transition.
|
|
143
|
+
render(
|
|
144
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
145
|
+
<Counter />
|
|
146
|
+
</ComposableRegionClient>
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
fireEvent.click(screen.getByRole('button', { name: /count:0/i }));
|
|
150
|
+
expect(
|
|
151
|
+
screen.getByRole('button', { name: /count:1/i })
|
|
152
|
+
).toBeInTheDocument();
|
|
153
|
+
|
|
154
|
+
pushTheme([{ id: 'section-abc', type: 'custom' }]);
|
|
155
|
+
expect(
|
|
156
|
+
screen.getByRole('button', { name: /count:1/i })
|
|
157
|
+
).toBeInTheDocument();
|
|
158
|
+
|
|
159
|
+
pushTheme([]);
|
|
160
|
+
expect(
|
|
161
|
+
screen.getByRole('button', { name: /count:1/i })
|
|
162
|
+
).toBeInTheDocument();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('stands down once the area is composed and returns when emptied again', () => {
|
|
166
|
+
render(
|
|
167
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
168
|
+
<div data-testid="flow">payment steps</div>
|
|
169
|
+
</ComposableRegionClient>
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
pushTheme([{ id: 'section-abc', type: 'custom' }]);
|
|
173
|
+
expect(
|
|
174
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
175
|
+
).toBeNull();
|
|
176
|
+
|
|
177
|
+
pushTheme([]);
|
|
178
|
+
expect(
|
|
179
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
180
|
+
).not.toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('KEEPS offering the takeover for a placeholder holding only native sections', () => {
|
|
184
|
+
// A native section never entered the saved order, so the server renders no
|
|
185
|
+
// composition for it — standing down would leave the area with no way in.
|
|
186
|
+
render(
|
|
187
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
188
|
+
<div data-testid="flow">payment steps</div>
|
|
189
|
+
</ComposableRegionClient>
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
pushTheme([{ id: 'layout-checkout-styles', type: 'native' }]);
|
|
193
|
+
|
|
194
|
+
expect(
|
|
195
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
196
|
+
).not.toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('ignores a theme pushed from an untrusted origin', () => {
|
|
200
|
+
// An embedding page must not be able to toggle designer chrome.
|
|
201
|
+
render(
|
|
202
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
203
|
+
<div data-testid="flow">payment steps</div>
|
|
204
|
+
</ComposableRegionClient>
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
pushTheme(
|
|
208
|
+
[{ id: 'section-abc', type: 'custom' }],
|
|
209
|
+
'checkout-page-body',
|
|
210
|
+
'https://evil.example'
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
expect(
|
|
214
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
215
|
+
).not.toBeNull();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('starts silent when the server rendered a composition', () => {
|
|
219
|
+
// The composition already owns the area; offering to take it over again
|
|
220
|
+
// before any frame arrives would flash a CTA over composed content.
|
|
221
|
+
render(
|
|
222
|
+
<ComposableRegionClient slug="checkout-page-body" initiallyComposed>
|
|
223
|
+
<div data-testid="flow">payment steps</div>
|
|
224
|
+
</ComposableRegionClient>
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
expect(screen.getByTestId('flow')).toBeInTheDocument();
|
|
228
|
+
expect(
|
|
229
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
230
|
+
).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('keeps the canvas inert while the flow renders in place', () => {
|
|
234
|
+
// The capture guards that neutralize clicks, submits and Enter live on the
|
|
235
|
+
// outlet target, which exists only once a composition places one. Until
|
|
236
|
+
// then this is what stops a canvas click reaching a real place-order.
|
|
237
|
+
render(
|
|
238
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
239
|
+
<div data-testid="flow">payment steps</div>
|
|
240
|
+
</ComposableRegionClient>
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(
|
|
244
|
+
document.querySelector('[data-chrome-region="checkout-page-body"]')
|
|
245
|
+
).toHaveStyle({ pointerEvents: 'none' });
|
|
246
|
+
// The overlay re-enables itself, so hover detection never depends on the
|
|
247
|
+
// inert wrapper being a hit target.
|
|
248
|
+
expect(
|
|
249
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
250
|
+
).toHaveStyle({ pointerEvents: 'auto' });
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('still explains the inert area once it is composed', () => {
|
|
254
|
+
// The click guard is keyed on being in the designer, not on the area
|
|
255
|
+
// being uncomposed — so a composition that does NOT place the live
|
|
256
|
+
// content elsewhere (an ordinary section, an outlet-free template) leaves
|
|
257
|
+
// it visible and unclickable. Without a note that reads as a broken
|
|
258
|
+
// preview, which is exactly what the code-owned explainer existed to
|
|
259
|
+
// prevent.
|
|
260
|
+
render(
|
|
261
|
+
<ComposableRegionClient
|
|
262
|
+
slug="checkout-page-body"
|
|
263
|
+
composedNote="The checkout flow renders here until an outlet places it"
|
|
264
|
+
>
|
|
265
|
+
<div data-testid="flow">payment steps</div>
|
|
266
|
+
</ComposableRegionClient>
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
pushTheme([{ id: 'section-abc', type: 'custom' }]);
|
|
270
|
+
|
|
271
|
+
const note = document.querySelector('[data-chrome-region-note]');
|
|
272
|
+
expect(note).not.toBeNull();
|
|
273
|
+
// Still inert: explaining the guard must not remove it.
|
|
274
|
+
expect(
|
|
275
|
+
document.querySelector('[data-chrome-region="checkout-page-body"]')
|
|
276
|
+
).toHaveStyle({ pointerEvents: 'none' });
|
|
277
|
+
|
|
278
|
+
fireEvent.mouseEnter(note!);
|
|
279
|
+
|
|
280
|
+
expect(
|
|
281
|
+
screen.getByText(/renders here until an outlet places it/i)
|
|
282
|
+
).toBeInTheDocument();
|
|
283
|
+
// A composed area has nothing left to take over, so no second CTA.
|
|
284
|
+
expect(
|
|
285
|
+
screen.queryByRole('button', { name: /edit in theme editor/i })
|
|
286
|
+
).not.toBeInTheDocument();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('explains a server-composed area too, without a CTA', () => {
|
|
290
|
+
render(
|
|
291
|
+
<ComposableRegionClient slug="checkout-page-body" initiallyComposed>
|
|
292
|
+
<div data-testid="flow">payment steps</div>
|
|
293
|
+
</ComposableRegionClient>
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
const note = document.querySelector('[data-chrome-region-note]');
|
|
297
|
+
expect(note).not.toBeNull();
|
|
298
|
+
|
|
299
|
+
fireEvent.mouseEnter(note!);
|
|
300
|
+
|
|
301
|
+
expect(
|
|
302
|
+
screen.queryByRole('button', { name: /edit in theme editor/i })
|
|
303
|
+
).not.toBeInTheDocument();
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
describe('ComposableRegion framed by a page that is not the editor', () => {
|
|
308
|
+
// pz-next ships `frame-ancestors 'self' … akifast.com … akinoncloud.com`, so
|
|
309
|
+
// a REAL checkout is legitimately embedded in production. Treating any frame
|
|
310
|
+
// as the designer would make the whole payment area pointer-inert behind an
|
|
311
|
+
// interactive scrim: a customer who cannot enter a card, tick an agreement
|
|
312
|
+
// or press Place Order. Nothing here may depend on framing alone.
|
|
313
|
+
beforeAll(() => {
|
|
314
|
+
frameDocument();
|
|
315
|
+
delete window.__themeEditorDesigner;
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('never makes the checkout inert and never shows designer chrome', () => {
|
|
319
|
+
render(
|
|
320
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
321
|
+
<div data-testid="flow">payment steps</div>
|
|
322
|
+
</ComposableRegionClient>
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const wrapper = document.querySelector<HTMLElement>(
|
|
326
|
+
'[data-chrome-region="checkout-page-body"]'
|
|
327
|
+
)!;
|
|
328
|
+
|
|
329
|
+
expect(wrapper.style.pointerEvents).toBe('');
|
|
330
|
+
expect(
|
|
331
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
332
|
+
).toBeNull();
|
|
333
|
+
expect(document.querySelector('[data-chrome-region-note]')).toBeNull();
|
|
334
|
+
|
|
335
|
+
fireEvent.mouseEnter(wrapper);
|
|
336
|
+
|
|
337
|
+
expect(
|
|
338
|
+
screen.queryByRole('button', { name: /edit in theme editor/i })
|
|
339
|
+
).not.toBeInTheDocument();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('is not talked into designer chrome by an embedder pushing a theme', () => {
|
|
343
|
+
// The embedder cannot reach the origin guard, but pin that even an
|
|
344
|
+
// accepted theme frame never doubles as proof of being the designer.
|
|
345
|
+
render(
|
|
346
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
347
|
+
<div data-testid="flow">payment steps</div>
|
|
348
|
+
</ComposableRegionClient>
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
pushTheme([{ id: 'section-abc', type: 'custom' }]);
|
|
352
|
+
|
|
353
|
+
expect(
|
|
354
|
+
document.querySelector<HTMLElement>(
|
|
355
|
+
'[data-chrome-region="checkout-page-body"]'
|
|
356
|
+
)!.style.pointerEvents
|
|
357
|
+
).toBe('');
|
|
358
|
+
expect(
|
|
359
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
360
|
+
).toBeNull();
|
|
361
|
+
expect(document.querySelector('[data-chrome-region-note]')).toBeNull();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('turns designer once the editor proves itself with the cookie frame', () => {
|
|
365
|
+
// The flag rides a postMessage and can land after mount, so the region has
|
|
366
|
+
// to pick the frame up rather than latching "not the designer" forever.
|
|
367
|
+
render(
|
|
368
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
369
|
+
<div data-testid="flow">payment steps</div>
|
|
370
|
+
</ComposableRegionClient>
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
expect(
|
|
374
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
375
|
+
).toBeNull();
|
|
376
|
+
|
|
377
|
+
act(() => {
|
|
378
|
+
fireEvent(
|
|
379
|
+
window,
|
|
380
|
+
new MessageEvent('message', {
|
|
381
|
+
origin: EDITOR_ORIGIN,
|
|
382
|
+
data: { type: 'SET_THEME_EDITOR_COOKIE', data: { cookie: 'x' } }
|
|
383
|
+
})
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
expect(
|
|
388
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
389
|
+
).not.toBeNull();
|
|
390
|
+
expect(
|
|
391
|
+
document.querySelector<HTMLElement>(
|
|
392
|
+
'[data-chrome-region="checkout-page-body"]'
|
|
393
|
+
)!.style.pointerEvents
|
|
394
|
+
).toBe('none');
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('ignores a cookie frame from an untrusted origin', () => {
|
|
398
|
+
render(
|
|
399
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
400
|
+
<div data-testid="flow">payment steps</div>
|
|
401
|
+
</ComposableRegionClient>
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
act(() => {
|
|
405
|
+
fireEvent(
|
|
406
|
+
window,
|
|
407
|
+
new MessageEvent('message', {
|
|
408
|
+
origin: 'https://evil.example',
|
|
409
|
+
data: { type: 'SET_THEME_EDITOR_COOKIE', data: { cookie: 'x' } }
|
|
410
|
+
})
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
expect(
|
|
415
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
416
|
+
).toBeNull();
|
|
417
|
+
expect(
|
|
418
|
+
document.querySelector<HTMLElement>(
|
|
419
|
+
'[data-chrome-region="checkout-page-body"]'
|
|
420
|
+
)!.style.pointerEvents
|
|
421
|
+
).toBe('');
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
describe('ComposableRegion on the live site', () => {
|
|
426
|
+
beforeAll(() => {
|
|
427
|
+
unframeDocument();
|
|
428
|
+
delete window.__themeEditorDesigner;
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('renders the children and no overlay, hovered or not', () => {
|
|
432
|
+
render(
|
|
433
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
434
|
+
<div data-testid="flow">payment steps</div>
|
|
435
|
+
</ComposableRegionClient>
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
expect(screen.getByTestId('flow')).toBeInTheDocument();
|
|
439
|
+
|
|
440
|
+
fireEvent.mouseEnter(screen.getByTestId('flow').parentElement!);
|
|
441
|
+
|
|
442
|
+
expect(
|
|
443
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
444
|
+
).toBeNull();
|
|
445
|
+
expect(
|
|
446
|
+
screen.queryByRole('button', { name: /edit in theme editor/i })
|
|
447
|
+
).not.toBeInTheDocument();
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('renders the children under a server-rendered composition too', () => {
|
|
451
|
+
// Composed or not, the flow is never the thing a composition replaces.
|
|
452
|
+
render(
|
|
453
|
+
<ComposableRegionClient slug="checkout-page-body" initiallyComposed>
|
|
454
|
+
<div data-testid="flow">payment steps</div>
|
|
455
|
+
</ComposableRegionClient>
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
expect(screen.getByTestId('flow')).toBeInTheDocument();
|
|
459
|
+
expect(
|
|
460
|
+
document.querySelector('[data-chrome-region-affordance]')
|
|
461
|
+
).toBeNull();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
it('adds one positioned wrapper and no pointer-events at all', () => {
|
|
465
|
+
// The live output pin: the wrapper replaces the previous code-owned div
|
|
466
|
+
// one-for-one, and nothing about the designer leaks into a visitor's page.
|
|
467
|
+
const { container } = render(
|
|
468
|
+
<ComposableRegionClient slug="checkout-page-body">
|
|
469
|
+
<div data-testid="flow">payment steps</div>
|
|
470
|
+
</ComposableRegionClient>
|
|
471
|
+
);
|
|
472
|
+
|
|
473
|
+
const wrapper = container.querySelector<HTMLElement>(
|
|
474
|
+
'[data-chrome-region="checkout-page-body"]'
|
|
475
|
+
)!;
|
|
476
|
+
|
|
477
|
+
expect(container.children).toHaveLength(1);
|
|
478
|
+
expect(wrapper.style.position).toBe('relative');
|
|
479
|
+
expect(wrapper.style.pointerEvents).toBe('');
|
|
480
|
+
});
|
|
481
|
+
});
|
|
@@ -58,6 +58,14 @@ describe('part style keys', () => {
|
|
|
58
58
|
expect(parsePartStyleKey('dropdown/wobble/border-color')).toBeNull();
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
+
it('recognizes the completed state segment', () => {
|
|
62
|
+
expect(parsePartStyleKey('step/completed/color')).toEqual({
|
|
63
|
+
part: 'step',
|
|
64
|
+
state: 'completed',
|
|
65
|
+
property: 'color'
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
61
69
|
it('round-trips through the builder', () => {
|
|
62
70
|
expect(parsePartStyleKey(buildPartStyleKey('option', 'color'))).toEqual({
|
|
63
71
|
part: 'option',
|
|
@@ -97,9 +105,9 @@ describe('partAttrs', () => {
|
|
|
97
105
|
'data-part': 'option',
|
|
98
106
|
'data-state': 'selected'
|
|
99
107
|
});
|
|
100
|
-
expect(
|
|
101
|
-
'data-state'
|
|
102
|
-
|
|
108
|
+
expect(
|
|
109
|
+
partAttrs('option', { selected: true, disabled: true })['data-state']
|
|
110
|
+
).toBe('selected disabled');
|
|
103
111
|
});
|
|
104
112
|
|
|
105
113
|
it('omits data-state entirely when nothing is on', () => {
|
|
@@ -160,6 +168,18 @@ describe('generated CSS for parts', () => {
|
|
|
160
168
|
expect(css).toContain('[data-part="option"][data-state~="disabled"]');
|
|
161
169
|
});
|
|
162
170
|
|
|
171
|
+
it('emits the completed state as a data-state token only', () => {
|
|
172
|
+
const css = generateThemeCSS(
|
|
173
|
+
sectionWith({ 'step/completed/color': { desktop: '#16a34a' } })
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(css).toContain('[data-part="step"][data-state~="completed"]');
|
|
177
|
+
// No ARIA analog and no pseudo-class: no ARIA attribute honestly means
|
|
178
|
+
// "completed", so the data-state token is the whole contract.
|
|
179
|
+
expect(css).not.toContain('[data-part="step"][aria-');
|
|
180
|
+
expect(css).not.toContain('[data-part="step"]:');
|
|
181
|
+
});
|
|
182
|
+
|
|
163
183
|
it('supports focus and hover on a part', () => {
|
|
164
184
|
const css = generateThemeCSS(
|
|
165
185
|
sectionWith({
|
|
@@ -194,7 +214,7 @@ describe('generated CSS for parts', () => {
|
|
|
194
214
|
});
|
|
195
215
|
});
|
|
196
216
|
|
|
197
|
-
describe(
|
|
217
|
+
describe("routing a block's own styles to its real root", () => {
|
|
198
218
|
const blockWith = (
|
|
199
219
|
styles: Record<string, unknown>,
|
|
200
220
|
stylesTargetRoot?: boolean
|
|
@@ -37,8 +37,11 @@ const EXPECTED_BLOCK_TYPES = [
|
|
|
37
37
|
'basket-summary',
|
|
38
38
|
'button',
|
|
39
39
|
'change-password-form',
|
|
40
|
+
'checkout-flow-outlet',
|
|
40
41
|
'checkout-steps',
|
|
42
|
+
'checkout-steps-outlet',
|
|
41
43
|
'checkout-summary',
|
|
44
|
+
'checkout-summary-outlet',
|
|
42
45
|
'countdown',
|
|
43
46
|
'counter',
|
|
44
47
|
'coupon-list',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PLACEHOLDER_SLUGS } from '../chrome/placeholder-slugs';
|
|
2
2
|
import {
|
|
3
|
+
isPageMountedSlug,
|
|
3
4
|
normalizePath,
|
|
4
5
|
registerRouteSlugRules,
|
|
5
6
|
resetRouteSlugRules,
|
|
@@ -129,11 +130,21 @@ describe('resolveBodySlug', () => {
|
|
|
129
130
|
expect(resolveBodySlug('/orders/completed/abc123')).toBe(
|
|
130
131
|
PLACEHOLDER_SLUGS.orderSuccess
|
|
131
132
|
);
|
|
133
|
+
expect(resolveBodySlug('/orders/checkout')).toBe(
|
|
134
|
+
PLACEHOLDER_SLUGS.checkoutBody
|
|
135
|
+
);
|
|
132
136
|
expect(resolveBodySlug('/stores')).toBe(PLACEHOLDER_SLUGS.stores);
|
|
133
137
|
expect(resolveBodySlug('/address/stores')).toBe(PLACEHOLDER_SLUGS.stores);
|
|
134
138
|
expect(resolveBodySlug('/')).toBe(PLACEHOLDER_SLUGS.homepage);
|
|
135
139
|
});
|
|
136
140
|
|
|
141
|
+
it('keeps the checkout body out of root catch-all mounting', () => {
|
|
142
|
+
// Load-bearing for "the flow can never be lost": the checkout layout
|
|
143
|
+
// mounts this slot itself, and a root catch-all mounting it with
|
|
144
|
+
// `fallback={children}` would make the payment flow replaceable.
|
|
145
|
+
expect(isPageMountedSlug('checkout-page-body')).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
137
148
|
it('returns null for a path with no placeholder', () => {
|
|
138
149
|
expect(resolveBodySlug('/kampanya-bilgilendirme')).toBeNull();
|
|
139
150
|
});
|