@akinon/pz-theme 2.0.157-rc.13 → 2.0.158-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.
@@ -33,20 +33,19 @@ export type PropertyType = (typeof CUSTOM_PROPERTY_TYPES)[number];
33
33
  * The subset whose value provably survives the full round trip — panel write,
34
34
  * persistence, and resolution back into a prop.
35
35
  *
36
- * The two excluded members are excluded for reasons that were verified, not
37
- * guessed: `multiselect` and `localetext` are stored UNBUCKETED by the editor
38
- * (`theme-basic-properties.tsx:1547-1548` passes arrays and locale objects
39
- * through without the per-breakpoint wrapper). `getResponsiveValue` then
40
- * rejects an array with a devWarn and returns the fallback, and a locale
41
- * object has no `desktop` key so it does too. They can never deliver a
42
- * value until a per-type resolver exists.
36
+ * The three excluded members are excluded for reasons that were verified, not
37
+ * guessed:
43
38
  *
44
- * `richtext` is the one type with such a resolver. The editor's rich text
45
- * control writes per language (`{ tr: '<p>…</p>', en: … }`, unbucketed, like
46
- * `localetext`), so `useThemeSetting` reads the page's language out of the
47
- * map before the breakpoint read, then passes the html through
48
- * `sanitizeRichText`, a DOM-free allowlist, so a block can hand it to
49
- * `dangerouslySetInnerHTML` on the server as well as the client.
39
+ * - `multiselect` and `localetext` are stored UNBUCKETED by the editor
40
+ * (`theme-basic-properties.tsx:1547-1548` passes arrays and locale objects
41
+ * through without the per-breakpoint wrapper). `getResponsiveValue` then
42
+ * rejects an array with a devWarn and returns the fallback, and a locale
43
+ * object has no `desktop` key so it does too. They can never deliver a
44
+ * value until a per-type resolver exists.
45
+ * - `richtext` has no safe renderer here. The allowlist sanitizer this repo
46
+ * already owns parses by assigning to `innerHTML` on a live element, so the
47
+ * sink is its first act — and pz-theme renders server-side, where there is
48
+ * no `document` at all.
50
49
  *
51
50
  * Declaring one of these throws at define time rather than shipping a control
52
51
  * that silently shows the default forever.
@@ -60,11 +59,12 @@ export const SUPPORTED_PROPERTY_TYPES: readonly PropertyType[] = [
60
59
  'color',
61
60
  'image',
62
61
  'file',
63
- 'richtext',
64
62
  'typography',
65
63
  'icon'
66
64
  ];
67
65
 
68
- export const isSupportedPropertyType = (type: unknown): type is PropertyType =>
66
+ export const isSupportedPropertyType = (
67
+ type: unknown
68
+ ): type is PropertyType =>
69
69
  typeof type === 'string' &&
70
70
  (SUPPORTED_PROPERTY_TYPES as readonly string[]).includes(type);
@@ -1,7 +1,6 @@
1
1
  'use client';
2
2
 
3
3
  import { getResponsiveValue } from '../utils';
4
- import { sanitizeRichText } from '../utils/sanitize-rich-text';
5
4
  import { devWarn } from '../utils/dev-warn';
6
5
  import {
7
6
  isLocalizedValue,
@@ -239,11 +238,6 @@ export function useThemeSetting<
239
238
 
240
239
  const defaults = splitDefault(declaration, valueLocale);
241
240
 
242
- const finish = (value: unknown): TValue =>
243
- (declaration.type === 'richtext'
244
- ? sanitizeRichText(typeof value === 'string' ? value : '')
245
- : value) as TValue;
246
-
247
241
  if (!context) {
248
242
  // A missing block context is never an error on its own: a developer
249
243
  // previewing the component in a storybook or a unit test should see the
@@ -260,7 +254,7 @@ export function useThemeSetting<
260
254
  `unreachable and the declared default is used instead. Render this ` +
261
255
  `component through a block registered with registerCustomBlocks().`
262
256
  );
263
- return finish(defaults.page);
257
+ return defaults.page as TValue;
264
258
  }
265
259
 
266
260
  // Reported on every render rather than once from an effect: the collection
@@ -277,17 +271,7 @@ export function useThemeSetting<
277
271
  // attribute keys that were never declared. `node` rather than `block` so a
278
272
  // setting declared inside a custom SECTION reads the section's own
279
273
  // properties, which is where the editor writes it.
280
- const authored = context.node.properties?.[key];
281
-
282
- // The rich text control writes per language — `{ tr: '<p>…</p>', en: … }` —
283
- // and the editor stores that map as it is, with no breakpoint key: the same
284
- // shape that keeps `localetext` out (see `property-types.ts`). Read the
285
- // page's language out of it first; what is left is a string or a breakpoint
286
- // map, which the responsive read below already understands.
287
- const stored =
288
- declaration.type === 'richtext'
289
- ? resolveLocalizedValue(authored, valueLocale)
290
- : authored;
274
+ const stored = context.node.properties?.[key];
291
275
 
292
276
  // The sentinel again, for the same reason: handing `getResponsiveValue` an
293
277
  // undefined fallback would resolve an undeclared default to `''`, so a
@@ -296,15 +280,15 @@ export function useThemeSetting<
296
280
  const resolved = getResponsiveValue(stored, breakpoint, NOT_AUTHORED);
297
281
 
298
282
  if (resolved === NOT_AUTHORED) {
299
- return finish(resolveAuthoredDefault(defaults.page, breakpoint));
283
+ return resolveAuthoredDefault(defaults.page, breakpoint) as TValue;
300
284
  }
301
285
 
302
286
  // The default-language text as the editor seeded it when the block was
303
287
  // placed, never changed since: still the author's copy, so it follows the
304
288
  // page's language. Anything the merchant typed is theirs and shows as typed.
305
289
  if (defaults.perLocale && resolved === defaults.base) {
306
- return finish(defaults.page);
290
+ return defaults.page as TValue;
307
291
  }
308
292
 
309
- return finish(resolved);
293
+ return resolved as TValue;
310
294
  }
@@ -4,28 +4,14 @@ import { useEffect } from 'react';
4
4
  import { useLocalization } from '@akinon/next/hooks';
5
5
 
6
6
  import { isTrustedEditorEvent, postToEditor } from './editor-bridge';
7
- import { useValueLocale } from './utils/localized-value';
8
7
 
9
8
  /**
10
9
  * The storefront half of the editor's Language dropdown.
11
10
  *
12
11
  * The editor starts that dropdown with English only and fills it from the
13
- * `LOCALE_UPDATE` frame the preview posts (`{ locale, locales, defaultLocale }`);
14
- * when the merchant picks another language it posts `LOCALE_UPDATE` back and
15
- * expects the preview to follow.
16
- *
17
- * The frame carries the storefront's default language as well. The editor's
18
- * per-language text controls store a plain string in that language and a map
19
- * in every other, so an editor that never heard it assumed English and gave
20
- * every Turkish-default store a stray `en` key (ZERO-5296).
21
- *
22
- * It is posted BEFORE the document reports ready (`IFRAME_READY`): the bridge
23
- * is a child of the placeholder wrapper, whose own effect posts that frame,
24
- * and React runs a child's effects first. The editor relies on the order — it
25
- * holds every per-language write until the preview is ready, and takes
26
- * "ready without languages" to mean a storefront that will never send them.
27
- *
28
- * For a long time the only code answering either direction
12
+ * `LOCALE_UPDATE` frame the preview posts (`{ locale, locales }`); when the
13
+ * merchant picks another language it posts `LOCALE_UPDATE` back and expects the
14
+ * preview to follow. For a long time the only code answering either direction
29
15
  * was the app-template's `LanguageSelect` — brand code. A brand whose header no
30
16
  * longer renders that component (a header composed through ChromeSlot, a
31
17
  * custom header section, a project older than the template) left the editor
@@ -68,7 +54,6 @@ export interface EditorLocaleOption {
68
54
  interface LocaleState {
69
55
  locale: string;
70
56
  locales: EditorLocaleOption[];
71
- defaultLocale?: string;
72
57
  setLocale: (locale: string) => void;
73
58
  }
74
59
 
@@ -101,22 +86,15 @@ const toEditorLocales = (locales: unknown): EditorLocaleOption[] =>
101
86
  : entry.value
102
87
  }));
103
88
 
104
- const announcementKey = ({
105
- locale,
106
- locales,
107
- defaultLocale
108
- }: Omit<LocaleState, 'setLocale'>) =>
109
- JSON.stringify([locale, locales, defaultLocale ?? null]);
89
+ const announcementKey = (locale: string, locales: EditorLocaleOption[]) =>
90
+ JSON.stringify([locale, locales]);
110
91
 
111
- const announce = ({ locale, locales, defaultLocale }: LocaleState) => {
112
- const key = announcementKey({ locale, locales, defaultLocale });
92
+ const announce = ({ locale, locales }: LocaleState) => {
93
+ const key = announcementKey(locale, locales);
113
94
  if (key === lastAnnounced) return;
114
95
  lastAnnounced = key;
115
96
 
116
- postToEditor({
117
- type: 'LOCALE_UPDATE',
118
- data: { locale, locales, defaultLocale }
119
- });
97
+ postToEditor({ type: 'LOCALE_UPDATE', data: { locale, locales } });
120
98
  };
121
99
 
122
100
  const handleEditorMessage = (event: MessageEvent) => {
@@ -137,7 +115,7 @@ const handleEditorMessage = (event: MessageEvent) => {
137
115
 
138
116
  // The editor is already showing `requested`. Recording it as announced keeps
139
117
  // the switch from being echoed back when the new locale reaches the page.
140
- lastAnnounced = announcementKey({ ...current, locale: requested });
118
+ lastAnnounced = announcementKey(requested, current.locales);
141
119
  current.setLocale(requested);
142
120
  };
143
121
 
@@ -148,7 +126,6 @@ const handleEditorMessage = (event: MessageEvent) => {
148
126
  */
149
127
  export function EditorLocaleBridge(): null {
150
128
  const { locale, locales, setLocale } = useLocalization();
151
- const { defaultLocale } = useValueLocale();
152
129
 
153
130
  useEffect(() => {
154
131
  if (!isFramed()) return;
@@ -170,14 +147,9 @@ export function EditorLocaleBridge(): null {
170
147
  useEffect(() => {
171
148
  if (!isFramed() || !locale) return;
172
149
 
173
- current = {
174
- locale,
175
- locales: toEditorLocales(locales),
176
- defaultLocale,
177
- setLocale
178
- };
150
+ current = { locale, locales: toEditorLocales(locales), setLocale };
179
151
  announce(current);
180
- }, [locale, locales, defaultLocale, setLocale]);
152
+ }, [locale, locales, setLocale]);
181
153
 
182
154
  return null;
183
155
  }
@@ -1166,9 +1166,6 @@ export default function ThemePlaceholderWrapper({
1166
1166
  Tells the editor which languages this storefront has and follows its
1167
1167
  language switch. Every placeholder renders one; they share a single
1168
1168
  listener and a single announcement (see editor-locale-bridge.tsx).
1169
- A child of this wrapper on purpose: its effect, and so its frame,
1170
- runs before the effect below that posts IFRAME_READY, and the editor
1171
- counts on hearing the languages before the ready report.
1172
1169
  */}
1173
1170
  <EditorLocaleBridge />
1174
1171
  {themeBackgroundCSS && (
@@ -0,0 +1,96 @@
1
+ import React from 'react';
2
+ import { act, render, screen } from '@testing-library/react';
3
+
4
+ import {
5
+ __resetOrderActionRulesForTests,
6
+ allowsLineReturn,
7
+ allowsOrderCancel,
8
+ registerOrderActionRules,
9
+ useOrderActionRules
10
+ } from '../order-action-rules';
11
+
12
+ /**
13
+ * The brand's say over the order actions a storefront offers (ZERO-5302):
14
+ * asked only about what commerce already allows; with no rule, commerce's
15
+ * flags decide.
16
+ */
17
+
18
+ const line = (status: string) => ({ id: 1, status: { value: status } });
19
+ const order = { id: 7, number: 'O-7' };
20
+
21
+ afterEach(() => __resetOrderActionRulesForTests());
22
+
23
+ describe('order action rules', () => {
24
+ it('leaves the decision to commerce when no brand registered rules', () => {
25
+ expect(allowsLineReturn({}, line('500'), order)).toBe(true);
26
+ expect(allowsOrderCancel({}, order)).toBe(true);
27
+ });
28
+
29
+ it('lets a brand narrow returns and cancellations', () => {
30
+ const rules = {
31
+ returnLine: (item: { status?: { value?: string } | null }) =>
32
+ item.status?.value === '550',
33
+ cancelOrder: () => false
34
+ };
35
+
36
+ expect(allowsLineReturn(rules, line('550'), order)).toBe(true);
37
+ expect(allowsLineReturn(rules, line('500'), order)).toBe(false);
38
+ expect(allowsOrderCancel(rules, order)).toBe(false);
39
+ });
40
+
41
+ it('gives a rule that throws no say', () => {
42
+ const rules = {
43
+ returnLine: () => {
44
+ throw new Error('brand bug');
45
+ },
46
+ cancelOrder: () => {
47
+ throw new Error('brand bug');
48
+ }
49
+ };
50
+
51
+ expect(allowsLineReturn(rules, line('550'), order)).toBe(true);
52
+ expect(allowsOrderCancel(rules, order)).toBe(true);
53
+ });
54
+
55
+ it('renders a reader again when the rules are registered or dropped', () => {
56
+ const Reader = () => {
57
+ const rules = useOrderActionRules();
58
+ return (
59
+ <span data-testid="reader">
60
+ {allowsLineReturn(rules, line('500'), order) ? 'offers' : 'withholds'}
61
+ </span>
62
+ );
63
+ };
64
+ render(<Reader />);
65
+ expect(screen.getByTestId('reader')).toHaveTextContent('offers');
66
+
67
+ let unregister = () => {};
68
+ act(() => {
69
+ unregister = registerOrderActionRules({ returnLine: () => false });
70
+ });
71
+ expect(screen.getByTestId('reader')).toHaveTextContent('withholds');
72
+
73
+ act(() => unregister());
74
+ expect(screen.getByTestId('reader')).toHaveTextContent('offers');
75
+ });
76
+
77
+ it('keeps rules registered later when an older registration unregisters', () => {
78
+ const first = registerOrderActionRules({ returnLine: () => false });
79
+ const second = { returnLine: () => true };
80
+ registerOrderActionRules(second);
81
+
82
+ first();
83
+
84
+ expect(allowsLineReturn(second, line('500'), order)).toBe(true);
85
+ const Reader = () => {
86
+ const rules = useOrderActionRules();
87
+ return (
88
+ <span data-testid="current">
89
+ {rules === second ? 'second' : 'other'}
90
+ </span>
91
+ );
92
+ };
93
+ render(<Reader />);
94
+ expect(screen.getByTestId('current')).toHaveTextContent('second');
95
+ });
96
+ });
@@ -0,0 +1,115 @@
1
+ import { useSyncExternalStore } from 'react';
2
+
3
+ /**
4
+ * Brand hook for the order actions a storefront offers (ZERO-5302).
5
+ *
6
+ * Commerce says what it allows: a line it will take back is flagged
7
+ * `is_refundable`, an order it will cancel `is_cancellable`. Whether the
8
+ * storefront offers that return or that cancellation is a brand decision on
9
+ * top — from which statuses, for how long after the order, whether the action
10
+ * is switched on at all — so the order blocks never learn it. A brand
11
+ * registers its rules from its own code (the pattern of the search-term
12
+ * transform), and the blocks ask them only about what commerce already
13
+ * allows. No registration, or a rule left out, and commerce's flags decide.
14
+ * A rule that throws has no say: a brand bug must not take the page down.
15
+ *
16
+ * Rules may be registered again when what they depend on arrives (a brand
17
+ * reading its settings from a widget, say); every block reading them through
18
+ * `useOrderActionRules` renders again.
19
+ */
20
+
21
+ export interface OrderActionLine {
22
+ id?: number | string;
23
+ status?: { value?: string; label?: string } | null;
24
+ created_date?: string | null;
25
+ is_refundable?: boolean;
26
+ is_cancellable?: boolean;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export interface OrderActionOrder {
31
+ id?: number | string;
32
+ number?: string;
33
+ created_date?: string | null;
34
+ status?: { value?: string; label?: string } | null;
35
+ [key: string]: unknown;
36
+ }
37
+
38
+ export interface OrderActionRules {
39
+ /**
40
+ * Whether a return may start from this line. Asked only about a line
41
+ * commerce flags refundable and that has no request open on it.
42
+ */
43
+ returnLine?: (line: OrderActionLine, order: OrderActionOrder) => boolean;
44
+ /**
45
+ * Whether the order may be cancelled from the storefront. Asked only about
46
+ * an order commerce flags cancellable.
47
+ */
48
+ cancelOrder?: (order: OrderActionOrder) => boolean;
49
+ }
50
+
51
+ const NO_RULES: OrderActionRules = {};
52
+
53
+ let current: OrderActionRules = NO_RULES;
54
+ const listeners = new Set<() => void>();
55
+
56
+ const notify = () => listeners.forEach((listener) => listener());
57
+
58
+ /** Register the brand's rules. Returns an unregister function. */
59
+ export const registerOrderActionRules = (
60
+ rules: OrderActionRules
61
+ ): (() => void) => {
62
+ current = rules;
63
+ notify();
64
+ return () => {
65
+ if (current !== rules) return;
66
+ current = NO_RULES;
67
+ notify();
68
+ };
69
+ };
70
+
71
+ const subscribe = (listener: () => void) => {
72
+ listeners.add(listener);
73
+ return () => {
74
+ listeners.delete(listener);
75
+ };
76
+ };
77
+
78
+ const getSnapshot = () => current;
79
+ const getServerSnapshot = () => NO_RULES;
80
+
81
+ /** The rules in force; the caller renders again when they are replaced. */
82
+ export const useOrderActionRules = (): OrderActionRules =>
83
+ useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
84
+
85
+ /** Whether the brand lets a return start from a line commerce allows. */
86
+ export const allowsLineReturn = (
87
+ rules: OrderActionRules,
88
+ line: OrderActionLine,
89
+ order: OrderActionOrder
90
+ ): boolean => {
91
+ if (!rules.returnLine) return true;
92
+ try {
93
+ return rules.returnLine(line, order) !== false;
94
+ } catch {
95
+ return true;
96
+ }
97
+ };
98
+
99
+ /** Whether the brand lets an order commerce allows be cancelled. */
100
+ export const allowsOrderCancel = (
101
+ rules: OrderActionRules,
102
+ order: OrderActionOrder
103
+ ): boolean => {
104
+ if (!rules.cancelOrder) return true;
105
+ try {
106
+ return rules.cancelOrder(order) !== false;
107
+ } catch {
108
+ return true;
109
+ }
110
+ };
111
+
112
+ /** Test-only: drop the registered rules between cases. */
113
+ export const __resetOrderActionRulesForTests = (): void => {
114
+ current = NO_RULES;
115
+ };
@@ -1,118 +0,0 @@
1
- import React from 'react';
2
- import { render, screen } from '@testing-library/react';
3
- import { ItemTemplateList } from '../blocks/item-template';
4
- import { useThemeProduct } from '../blocks/product-block-utils';
5
- import { useThemeRowScope } from '../blocks/row-scope';
6
- import blockRendererRegistry from '../blocks/block-renderer-registry';
7
- import { resetCustomCatalog } from '../custom/catalog';
8
- import { resetCustomCollection } from '../custom/collect';
9
- import { isCustomThemeType, registerCustomBlocks } from '../custom';
10
- import type { Block } from '../theme-block';
11
-
12
- const READER = 'custom:acme:row-reader';
13
-
14
- function RowReader() {
15
- const scope = useThemeRowScope();
16
- const product = useThemeProduct();
17
-
18
- return (
19
- <span
20
- data-testid="reader"
21
- data-list={scope?.listType ?? ''}
22
- data-index={scope?.index ?? ''}
23
- data-number={String(scope?.row.number ?? '')}
24
- data-product={String(product?.pk ?? '')}
25
- />
26
- );
27
- }
28
-
29
- const template = (): Block =>
30
- ({
31
- id: 'tpl',
32
- type: 'group',
33
- label: 'Row',
34
- order: 0,
35
- hidden: false,
36
- value: '',
37
- properties: { role: { desktop: 'item-template' } },
38
- styles: {},
39
- blocks: [
40
- {
41
- id: 'tpl-reader',
42
- type: READER,
43
- label: 'Reader',
44
- order: 0,
45
- hidden: false,
46
- value: '',
47
- properties: {},
48
- styles: {},
49
- blocks: []
50
- }
51
- ]
52
- } as Block);
53
-
54
- const list = (type: string): Block =>
55
- ({
56
- id: 'list',
57
- type,
58
- label: 'List',
59
- order: 0,
60
- hidden: false,
61
- value: '',
62
- properties: {},
63
- styles: {},
64
- blocks: [template()]
65
- } as Block);
66
-
67
- const renderRows = (type: string, items: Record<string, unknown>[]) =>
68
- render(
69
- <ItemTemplateList
70
- block={list(type)}
71
- items={items}
72
- placeholderId="p1"
73
- sectionId="s1"
74
- />
75
- );
76
-
77
- beforeAll(() => {
78
- registerCustomBlocks('acme', { 'row-reader': RowReader });
79
- });
80
-
81
- afterAll(() => {
82
- const map = (
83
- blockRendererRegistry as unknown as { renderers: Map<string, unknown> }
84
- ).renderers;
85
- Array.from(map.keys())
86
- .filter(isCustomThemeType)
87
- .forEach((key) => map.delete(key));
88
- resetCustomCatalog();
89
- resetCustomCollection();
90
- });
91
-
92
- describe('the row scope', () => {
93
- it('hands every repeated row to the blocks inside it, on any list', () => {
94
- renderRows('order-list', [{ number: 'ORD-1' }, { number: 'ORD-2' }]);
95
-
96
- const readers = screen.getAllByTestId('reader');
97
- expect(readers).toHaveLength(2);
98
- expect(readers[0]).toHaveAttribute('data-list', 'order-list');
99
- expect(readers[0]).toHaveAttribute('data-index', '0');
100
- expect(readers[0]).toHaveAttribute('data-number', 'ORD-1');
101
- expect(readers[1]).toHaveAttribute('data-number', 'ORD-2');
102
- expect(readers[0]).toHaveAttribute('data-product', '');
103
- });
104
-
105
- it('keeps the product scope on product lists next to the row scope', () => {
106
- renderRows('product-grid', [{ pk: 7, name: 'Sneaker' }]);
107
-
108
- const reader = screen.getByTestId('reader');
109
- expect(reader).toHaveAttribute('data-list', 'product-grid');
110
- expect(reader).toHaveAttribute('data-product', '7');
111
- });
112
-
113
- it('is closed outside a repeated row', () => {
114
- render(<RowReader />);
115
-
116
- expect(screen.getByTestId('reader')).toHaveAttribute('data-list', '');
117
- });
118
- });
@@ -1,54 +0,0 @@
1
- import React from 'react';
2
- import {
3
- registerOrderLineActions,
4
- renderOrderLineActions
5
- } from '../order-line-actions';
6
-
7
- const context = {
8
- order: { id: 7, number: '20920397829', created_date: '2026-10-01' },
9
- line: {
10
- key: '1',
11
- name: 'Product',
12
- image: '',
13
- href: '',
14
- price: '100',
15
- total: 100,
16
- quantity: 1,
17
- item: { id: 1, status: { value: '550', label: 'Delivered' } }
18
- },
19
- isDesigner: false
20
- };
21
-
22
- describe('order line actions registry', () => {
23
- it('renders nothing until a brand registers', () => {
24
- expect(renderOrderLineActions(context)).toBeNull();
25
- });
26
-
27
- it('hands the line and order to the registered renderer', () => {
28
- const renderer = jest.fn(() => <span>action</span>);
29
- const unregister = registerOrderLineActions(renderer);
30
-
31
- expect(renderOrderLineActions(context)).toEqual(<span>action</span>);
32
- expect(renderer).toHaveBeenCalledWith(context);
33
-
34
- unregister();
35
- expect(renderOrderLineActions(context)).toBeNull();
36
- });
37
-
38
- it('swallows a throwing renderer so a brand bug cannot break the line', () => {
39
- const unregister = registerOrderLineActions(() => {
40
- throw new Error('boom');
41
- });
42
-
43
- expect(renderOrderLineActions(context)).toBeNull();
44
- unregister();
45
- });
46
-
47
- it('only forgets the renderer it was asked to forget', () => {
48
- const first = registerOrderLineActions(() => 'first');
49
- registerOrderLineActions(() => 'second');
50
-
51
- first();
52
- expect(renderOrderLineActions(context)).toBe('second');
53
- });
54
- });