@akinon/pz-theme 2.0.43-rc.0 → 2.0.44

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 (37) hide show
  1. package/CHANGELOG.md +14 -31
  2. package/package.json +4 -4
  3. package/src/__tests__/checkout-pieces.test.tsx +420 -0
  4. package/src/__tests__/checkout-slot-coverage.test.ts +181 -0
  5. package/src/__tests__/chrome-empty-zone.test.tsx +3 -3
  6. package/src/__tests__/code-owned-region.test.tsx +30 -6
  7. package/src/__tests__/composable-region.test.tsx +481 -0
  8. package/src/__tests__/part-styles.test.ts +24 -4
  9. package/src/__tests__/registry-parity.test.ts +3 -0
  10. package/src/__tests__/route-slug.test.ts +11 -0
  11. package/src/blocks/__tests__/checkout-hosted-parts.test.ts +203 -0
  12. package/src/blocks/__tests__/checkout-outlet-blocks.test.tsx +623 -0
  13. package/src/blocks/block-renderer-registry.tsx +6 -0
  14. package/src/blocks/checkout-flow-outlet-block.tsx +104 -0
  15. package/src/blocks/checkout-outlet-base.tsx +241 -0
  16. package/src/blocks/checkout-steps-outlet-block.tsx +57 -0
  17. package/src/blocks/checkout-summary-outlet-block.tsx +43 -0
  18. package/src/chrome/checkout-piece.tsx +145 -0
  19. package/src/chrome/checkout-pieces-provider.tsx +28 -0
  20. package/src/chrome/checkout-pieces.ts +173 -0
  21. package/src/chrome/chrome-fallback.tsx +8 -26
  22. package/src/chrome/chrome-slot.tsx +25 -26
  23. package/src/chrome/code-owned-region.tsx +21 -9
  24. package/src/chrome/composable-region-client.tsx +228 -0
  25. package/src/chrome/composable-region.tsx +58 -0
  26. package/src/chrome/composition-source.ts +34 -0
  27. package/src/chrome/composition-state.ts +49 -0
  28. package/src/chrome/live-checkout-page-context.tsx +3 -2
  29. package/src/chrome/placeholder-slugs.ts +14 -4
  30. package/src/chrome/route-slug.ts +6 -3
  31. package/src/hooks/use-designer-features.tsx +41 -10
  32. package/src/index.ts +6 -0
  33. package/src/utils/__tests__/checkout-safety.test.ts +374 -0
  34. package/src/utils/checkout-safety.ts +177 -0
  35. package/src/utils/index.ts +47 -8
  36. package/src/utils/part-styles.ts +4 -0
  37. package/src/utils/visibility-rules.ts +31 -8
@@ -62,11 +62,11 @@ describe('ChromeFallback empty zone in the designer', () => {
62
62
  value: { postMessage }
63
63
  });
64
64
 
65
- render(<ChromeFallback slug="checkout-summary-body" emptyZone />);
65
+ render(<ChromeFallback slug="checkout-bottom-body" emptyZone />);
66
66
 
67
67
  // A frame from the editor is what proves its origin to the bridge;
68
68
  // without one there is no trusted target and the message is dropped.
69
- pushTheme([], 'checkout-summary-body');
69
+ pushTheme([], 'checkout-bottom-body');
70
70
 
71
71
  fireEvent.click(
72
72
  screen.getByRole('button', { name: /edit in theme editor/i })
@@ -76,7 +76,7 @@ describe('ChromeFallback empty zone in the designer', () => {
76
76
  const [envelope, targetOrigin] = postMessage.mock.calls[0];
77
77
  expect(envelope).toMatchObject({
78
78
  type: 'REQUEST_CHROME_OVERRIDE',
79
- data: { slug: 'checkout-summary-body' }
79
+ data: { slug: 'checkout-bottom-body' }
80
80
  });
81
81
  expect(envelope.protocolVersion).toBeGreaterThan(0);
82
82
  expect(targetOrigin).toBe(EDITOR_ORIGIN);
@@ -4,10 +4,14 @@ import { render, screen, fireEvent } from '@testing-library/react';
4
4
  import { CodeOwnedRegion } from '../chrome/code-owned-region';
5
5
 
6
6
  /**
7
- * The checkout flow offers no takeover affordance on purpose, but silence
8
- * reads as breakage: a designer hovering it sees the same nothing a bug would
9
- * produce. These tests pin the explainer that fills that gap — and pin that
10
- * it never leaks to visitors and never blocks the flow it wraps.
7
+ * Some regions offer no takeover on purpose, but silence reads as breakage: a
8
+ * designer hovering one sees the same nothing a bug would produce. These tests
9
+ * pin the explainer that fills that gap — and pin that it never leaks to
10
+ * visitors and never blocks what it wraps.
11
+ *
12
+ * The reference app's checkout no longer mounts this: its body IS composable,
13
+ * so it takes `ComposableRegion` and a real affordance. The template app's
14
+ * checkout, which has no body zone at all, still does.
11
15
  */
12
16
 
13
17
  describe('CodeOwnedRegion in the designer', () => {
@@ -18,7 +22,7 @@ describe('CodeOwnedRegion in the designer', () => {
18
22
  });
19
23
  });
20
24
 
21
- it('explains the region on hover and says where the composable zones are', () => {
25
+ it('explains the region on hover without promising outlet placement', () => {
22
26
  render(
23
27
  <CodeOwnedRegion label="checkout flow">
24
28
  <div data-testid="flow">payment steps</div>
@@ -31,12 +35,32 @@ describe('CodeOwnedRegion in the designer', () => {
31
35
 
32
36
  const note = screen.getByText(/checkout flow is code-owned/i);
33
37
  expect(note).toBeInTheDocument();
34
- expect(note.textContent).toMatch(/zones above and below/i);
38
+ expect(note.textContent).toMatch(/is code-owned/i);
39
+ expect(note.textContent).toMatch(/internals cannot be edited here/i);
40
+ // The default used to promise placement through outlet blocks, which is
41
+ // false for any mount without a composable body zone — and outlets only
42
+ // register inside one. A mount that has the zone passes its own note.
43
+ expect(note.textContent).not.toMatch(/outlet blocks/i);
35
44
 
36
45
  fireEvent.mouseLeave(screen.getByTestId('flow').parentElement!);
37
46
  expect(screen.queryByText(/code-owned/i)).not.toBeInTheDocument();
38
47
  });
39
48
 
49
+ it('lets the mounting layout replace the default sentence with its own note', () => {
50
+ render(
51
+ <CodeOwnedRegion label="checkout flow" note="Compose the Checkout Body zone.">
52
+ <div data-testid="flow">payment steps</div>
53
+ </CodeOwnedRegion>
54
+ );
55
+
56
+ fireEvent.mouseEnter(screen.getByTestId('flow').parentElement!);
57
+
58
+ expect(
59
+ screen.getByText('Compose the Checkout Body zone.')
60
+ ).toBeInTheDocument();
61
+ expect(screen.queryByText(/is code-owned —/)).not.toBeInTheDocument();
62
+ });
63
+
40
64
  it('never intercepts the pointer, so the flow stays usable in the preview', () => {
41
65
  render(
42
66
  <CodeOwnedRegion label="checkout flow">
@@ -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(partAttrs('option', { selected: true, disabled: true })[
101
- 'data-state'
102
- ]).toBe('selected disabled');
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('routing a block\'s own styles to its real root', () => {
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
  });