@applicaster/zapp-react-native-utils 16.0.0-rc.113 → 16.0.0-rc.115

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.
@@ -3,5 +3,7 @@ import { isTV } from "../reactUtils";
3
3
 
4
4
  export const resolveDefaultAssetUri = (source: string | number): string =>
5
5
  isTV()
6
- ? (source as string)
6
+ ? typeof source === "number"
7
+ ? Image.resolveAssetSource(source)?.uri || ""
8
+ : (source as string)
7
9
  : Image.resolveAssetSource(source as number)?.uri || "";
@@ -60,6 +60,30 @@ describe("openBottomSheetAction custom content", () => {
60
60
  });
61
61
  });
62
62
 
63
+ it("passes themePluginId through to the name sheet", async () => {
64
+ const CustomContent = function CustomContent() {
65
+ return null;
66
+ };
67
+
68
+ await openBottomSheetAction({
69
+ type: "openBottomSheet",
70
+ options: {
71
+ header: { title: "Create New Playlist" },
72
+ themePluginId: "playlist-experience",
73
+ content: {
74
+ component: CustomContent,
75
+ props: { buttonLabel: "Create" },
76
+ },
77
+ },
78
+ } as any);
79
+
80
+ const args = (openBottomSheetModal as jest.Mock).mock.calls[0][0];
81
+
82
+ expect(args.modalBottomSheetContentProps.themePluginId).toBe(
83
+ "playlist-experience"
84
+ );
85
+ });
86
+
63
87
  it("returns Error when neither list data nor content.component is provided", async () => {
64
88
  const result = await openBottomSheetAction({
65
89
  type: "openBottomSheet",
@@ -212,6 +212,7 @@ export async function openBottomSheetAction(
212
212
  ...(content.props || {}),
213
213
  actionContext: context,
214
214
  },
215
+ themePluginId,
215
216
  },
216
217
  });
217
218
 
@@ -45,7 +45,7 @@ describe("playerToolbox", () => {
45
45
 
46
46
  await expect(
47
47
  loadToolbox().registerPlayerToolbox(manager(null))
48
- ).resolves.toBeUndefined();
48
+ ).resolves.toBe(false);
49
49
  });
50
50
 
51
51
  it("does nothing when getXray returns null", async () => {
@@ -53,7 +53,7 @@ describe("playerToolbox", () => {
53
53
 
54
54
  await expect(
55
55
  loadToolbox().registerPlayerToolbox(manager(null))
56
- ).resolves.toBeUndefined();
56
+ ).resolves.toBe(false);
57
57
  });
58
58
 
59
59
  it("swallows a failed registration", async () => {
@@ -62,7 +62,7 @@ describe("playerToolbox", () => {
62
62
 
63
63
  await expect(
64
64
  loadToolbox().registerPlayerToolbox(manager(null))
65
- ).resolves.toBeUndefined();
65
+ ).resolves.toBe(false);
66
66
  });
67
67
 
68
68
  it("seeks to an absolute position, from the end when negative, clamped", async () => {
@@ -397,6 +397,11 @@ export class PlayerManager {
397
397
  return this.manager.removeHandler(event, handler);
398
398
  }
399
399
 
400
+ /** Whether anything handles `event` now, e.g. a mounted PlayerContainer for "toggleFullscreen". */
401
+ public hasHandler(event: string) {
402
+ return this.manager.handlers[event]?.length > 0;
403
+ }
404
+
400
405
  // TODO: should not be
401
406
  public invokeHandler(event, ...args) {
402
407
  return this.manager.invokeHandler(event, ...args);
@@ -1,4 +1,4 @@
1
- import { getXray } from "../../logger";
1
+ import { registerXrayToolbox, XrayToolSpec } from "../../logger/xrayToolbox";
2
2
  import type { PlayerManager } from "./index";
3
3
 
4
4
  /**
@@ -9,25 +9,6 @@ import type { PlayerManager } from "./index";
9
9
  * How the player is shown (video modal mode, mini player) is navigation state: see `nav.*`.
10
10
  */
11
11
 
12
- type ToolSpec = {
13
- name: string;
14
- description: string;
15
- parameters?: Array<{
16
- name: string;
17
- type: "number";
18
- description: string;
19
- }>;
20
- handler: (args: Record<string, unknown>) => unknown;
21
- };
22
-
23
- type RegisterReactToolbox = (toolbox: {
24
- name: string;
25
- description: string;
26
- tools: ToolSpec[];
27
- }) => Promise<void>;
28
-
29
- let registration: Promise<void> | undefined;
30
-
31
12
  const toNumber = (value: unknown) =>
32
13
  typeof value === "number" && Number.isFinite(value) ? value : null;
33
14
 
@@ -44,7 +25,7 @@ const requireSeekable = (player: {
44
25
  return seekable;
45
26
  };
46
27
 
47
- const tools = (manager: PlayerManager): ToolSpec[] => {
28
+ const tools = (manager: PlayerManager): XrayToolSpec[] => {
48
29
  const requirePlayer = () => {
49
30
  const player = manager.getActivePlayer();
50
31
 
@@ -168,25 +149,9 @@ const tools = (manager: PlayerManager): ToolSpec[] => {
168
149
  ];
169
150
  };
170
151
 
171
- export const registerPlayerToolbox = (manager: PlayerManager) => {
172
- if (registration) return registration;
173
-
174
- const register = (
175
- getXray() as
176
- | { registerReactToolbox?: RegisterReactToolbox }
177
- | null
178
- | undefined
179
- )?.registerReactToolbox;
180
-
181
- if (!register) return Promise.resolve();
182
-
183
- registration = register({
152
+ export const registerPlayerToolbox = (manager: PlayerManager) =>
153
+ registerXrayToolbox({
184
154
  name: "player",
185
155
  description: "Inspect and control the active player.",
186
156
  tools: tools(manager),
187
- }).catch(() => {
188
- registration = undefined;
189
157
  });
190
-
191
- return registration;
192
- };
@@ -329,4 +329,64 @@ describe("getEmptyModalBlocksStructure", () => {
329
329
  expect(result.sectionHeader.title).toEqual({ default: {} });
330
330
  expect(result.sectionHeader.actionButtons).toEqual({ default: {} });
331
331
  });
332
+
333
+ it("maps playlist button font family and the new green style keys", () => {
334
+ const result = parseModalBlocksConfiguration(
335
+ {
336
+ "button.title.ios.font_family": "GreycliffCF-Bold",
337
+ "button.title.inactive_font_color": "#111111",
338
+ "button.title.focused_font_color": "#222222",
339
+ "button.title.text_transform": "uppercase",
340
+ "button.background.inactive_color": "#999999",
341
+ "button.background.horizontal_gutter": 6,
342
+ "button.icons.width": 24,
343
+ "button.icons.height": 24,
344
+ },
345
+ "playlist-experience"
346
+ );
347
+
348
+ expect(result.button.title.default).toEqual(
349
+ expect.objectContaining({
350
+ fontFamily: "GreycliffCF-Bold",
351
+ inactiveFontColor: "#111111",
352
+ textTransform: "uppercase",
353
+ })
354
+ );
355
+
356
+ // "focused_" is a state prefix, so this key lands on the focused bucket.
357
+ expect(result.button.title.focused).toEqual(
358
+ expect.objectContaining({
359
+ fontColor: "#222222",
360
+ })
361
+ );
362
+
363
+ expect(result.button.background.default).toEqual(
364
+ expect.objectContaining({
365
+ inactiveColor: "#999999",
366
+ horizontalGutter: 6,
367
+ })
368
+ );
369
+
370
+ expect(result.button.icons.default).toEqual(
371
+ expect.objectContaining({
372
+ width: 24,
373
+ height: 24,
374
+ })
375
+ );
376
+ });
377
+
378
+ it("keeps a font-selector object on the button title", () => {
379
+ const result = parseModalBlocksConfiguration(
380
+ {
381
+ "button.title.ios.font_family": { fontFamily: "GreycliffCF-Bold" },
382
+ },
383
+ "playlist-experience"
384
+ );
385
+
386
+ expect(result.button.title.default).toEqual(
387
+ expect.objectContaining({
388
+ fontFamily: { fontFamily: "GreycliffCF-Bold" },
389
+ })
390
+ );
391
+ });
332
392
  });
@@ -0,0 +1,69 @@
1
+ const xray = require("@applicaster/quick-brick-xray");
2
+
3
+ const toolbox = { name: "test", description: "", tools: [] };
4
+
5
+ const load = (): typeof import("../xrayToolbox") => {
6
+ let module;
7
+
8
+ jest.isolateModules(() => {
9
+ module = require("../xrayToolbox");
10
+ });
11
+
12
+ return module;
13
+ };
14
+
15
+ describe("registerXrayToolbox", () => {
16
+ afterEach(() => {
17
+ delete xray.registerReactToolbox;
18
+ });
19
+
20
+ it("registers once per name", async () => {
21
+ xray.registerReactToolbox = jest.fn().mockResolvedValue(undefined);
22
+ const { registerXrayToolbox } = load();
23
+
24
+ await expect(registerXrayToolbox(toolbox)).resolves.toBe(true);
25
+ await expect(registerXrayToolbox(toolbox)).resolves.toBe(true);
26
+
27
+ expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
28
+ });
29
+
30
+ it("resolves false without a toolbox API", async () => {
31
+ await expect(load().registerXrayToolbox(toolbox)).resolves.toBe(false);
32
+ });
33
+
34
+ it("keeps a failed registration instead of retrying it", async () => {
35
+ xray.registerReactToolbox = jest
36
+ .fn()
37
+ .mockRejectedValue(new Error("XRayToolboxBridge is not available"));
38
+
39
+ const { registerXrayToolbox } = load();
40
+
41
+ await expect(registerXrayToolbox(toolbox)).resolves.toBe(false);
42
+ await expect(registerXrayToolbox(toolbox)).resolves.toBe(false);
43
+
44
+ expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
45
+ });
46
+
47
+ it("registers new tools under the same name again (Fast Refresh)", async () => {
48
+ xray.registerReactToolbox = jest.fn().mockResolvedValue(undefined);
49
+ const { registerXrayToolbox } = load();
50
+
51
+ await registerXrayToolbox(toolbox);
52
+ await registerXrayToolbox({ ...toolbox, tools: [] });
53
+
54
+ expect(xray.registerReactToolbox).toHaveBeenCalledTimes(2);
55
+ });
56
+
57
+ it("stops trying every toolbox once the bridge is missing", async () => {
58
+ xray.registerReactToolbox = jest.fn().mockRejectedValue(new Error("no"));
59
+ const { registerXrayToolbox } = load();
60
+
61
+ await registerXrayToolbox(toolbox);
62
+
63
+ await expect(
64
+ registerXrayToolbox({ ...toolbox, name: "other" })
65
+ ).resolves.toBe(false);
66
+
67
+ expect(xray.registerReactToolbox).toHaveBeenCalledTimes(1);
68
+ });
69
+ });
@@ -0,0 +1,67 @@
1
+ import { getXray } from ".";
2
+
3
+ /** One tool of an X-Ray React toolbox: an agent calls it by name with JSON arguments. */
4
+ export type XrayToolSpec = {
5
+ name: string;
6
+ description: string;
7
+ parameters?: Array<{
8
+ name: string;
9
+ type: "number" | "string";
10
+ description: string;
11
+ }>;
12
+ handler: (args: Record<string, unknown>) => unknown;
13
+ };
14
+
15
+ export type XrayToolbox = {
16
+ name: string;
17
+ description: string;
18
+ tools: XrayToolSpec[];
19
+ };
20
+
21
+ type RegisterReactToolbox = (toolbox: XrayToolbox) => Promise<void>;
22
+
23
+ const registered = new Map<
24
+ string,
25
+ { tools: XrayToolSpec[]; registration: Promise<boolean> }
26
+ >();
27
+
28
+ /** Set once a registration fails: the build has no X-Ray bridge, which cannot appear later. */
29
+ let bridgeMissing = false;
30
+
31
+ /**
32
+ * Registers an X-Ray React toolbox and resolves whether it is registered. The same tools are
33
+ * registered once; new tools under the same name (Fast Refresh re-ran the module) replace them.
34
+ * A build without the X-Ray native bridge (release builds) rejects the first attempt and every
35
+ * later call resolves false at once, so callers on hot paths pay for one attempt only.
36
+ */
37
+ export const registerXrayToolbox = (toolbox: XrayToolbox): Promise<boolean> => {
38
+ const cached = registered.get(toolbox.name);
39
+
40
+ if (cached?.tools === toolbox.tools) return cached.registration;
41
+
42
+ if (bridgeMissing) return Promise.resolve(false);
43
+
44
+ const register = (
45
+ getXray() as
46
+ | { registerReactToolbox?: RegisterReactToolbox }
47
+ | null
48
+ | undefined
49
+ )?.registerReactToolbox;
50
+
51
+ const registration = register
52
+ ? register(toolbox).then(
53
+ () => true,
54
+ () => {
55
+ bridgeMissing = true;
56
+
57
+ return false;
58
+ }
59
+ )
60
+ : Promise.resolve(false);
61
+
62
+ if (!register) bridgeMissing = true;
63
+
64
+ registered.set(toolbox.name, { tools: toolbox.tools, registration });
65
+
66
+ return registration;
67
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The app navigator for the X-Ray toolboxes (nav, screen), which run outside React. The
3
+ * NavigationProvider sets it after each commit; it is a plain module variable, so updating it
4
+ * never renders anything.
5
+ */
6
+ let navigator: QuickBrickAppNavigator | undefined;
7
+
8
+ export const setXrayNavigator = (value: QuickBrickAppNavigator | undefined) => {
9
+ navigator = value;
10
+ };
11
+
12
+ export const getXrayNavigator = () => navigator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "16.0.0-rc.113",
3
+ "version": "16.0.0-rc.115",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -27,7 +27,7 @@
27
27
  },
28
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
29
29
  "dependencies": {
30
- "@applicaster/applicaster-types": "16.0.0-rc.113",
30
+ "@applicaster/applicaster-types": "16.0.0-rc.115",
31
31
  "buffer": "^5.2.1",
32
32
  "camelize": "^1.0.0",
33
33
  "dayjs": "^1.11.10",
@@ -34,6 +34,7 @@ type Props = {
34
34
  type ItemProps = {
35
35
  children: React.ReactNode;
36
36
  index: number;
37
+ testID?: string;
37
38
  };
38
39
 
39
40
  type MeasurementContext = {
@@ -153,11 +154,12 @@ export const ScreenLoadingMeasurements = ({
153
154
  export const ScreenLoadingMeasurementsListItemWrapper = ({
154
155
  children,
155
156
  index,
157
+ testID,
156
158
  }: ItemProps) => {
157
159
  const { onLayout } = React.useContext(MeasurementsSettersContext);
158
160
 
159
161
  return (
160
- <View style={styles.container} onLayout={onLayout?.(index)}>
162
+ <View style={styles.container} onLayout={onLayout?.(index)} testID={testID}>
161
163
  {children}
162
164
  </View>
163
165
  );