@q9labsai/chalk-react-native 4.1.12 → 4.1.13

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 (90) hide show
  1. package/dist/android.js +2 -2
  2. package/dist/{chunk-FVHH65FR.js → chunk-4NLA35HE.js} +1 -1
  3. package/dist/{chunk-LZ67SXGB.js → chunk-MK6OO4V7.js} +6 -0
  4. package/dist/client.js +1 -1
  5. package/dist/index.js +2 -2
  6. package/dist/ios-pad.js +2 -2
  7. package/dist/ios-phone.js +2 -2
  8. package/dist/ios.js +2 -2
  9. package/dist/macos.js +2 -2
  10. package/package.json +4 -4
  11. package/src/callkit/callkit.test.ts +12 -10
  12. package/src/space-client/create-native-space-client.ts +8 -0
  13. package/dist/test-support/test-renderer.d.ts +0 -20
  14. package/dist/test-support/test-renderer.d.ts.map +0 -1
  15. package/src/android/connection-service.test.ts +0 -114
  16. package/src/callkit/resolve-space-callkit-options.test.ts +0 -68
  17. package/src/client.test.ts +0 -12
  18. package/src/clipboard.test.ts +0 -79
  19. package/src/components/Chalk.test.tsx +0 -223
  20. package/src/components/EmbeddedWhiteboard.test.tsx +0 -92
  21. package/src/components/Entrance.test.tsx +0 -26
  22. package/src/components/EntranceView.android.test.tsx +0 -100
  23. package/src/components/EntranceView.ios-pad.test.tsx +0 -160
  24. package/src/components/EntranceView.ios-phone.test.tsx +0 -137
  25. package/src/components/EntranceView.test.tsx +0 -15
  26. package/src/components/ReactionPicker.test.tsx +0 -19
  27. package/src/components/RtcVideoView.test.tsx +0 -12
  28. package/src/components/SpaceView.android.test.tsx +0 -11
  29. package/src/components/SpaceView.ios-pad.test.tsx +0 -11
  30. package/src/components/SpaceView.ios-phone.test.tsx +0 -11
  31. package/src/components/SpaceView.macos.test.tsx +0 -11
  32. package/src/components/SpaceView.shared.test.tsx +0 -116
  33. package/src/components/SpaceView.test.tsx +0 -15
  34. package/src/components/android-connection-service-controller.test.ts +0 -80
  35. package/src/components/episode-diagnostic-render-observer.test.ts +0 -24
  36. package/src/components/logo/Logo.test.tsx +0 -144
  37. package/src/components/native-animation-controller.test.ts +0 -47
  38. package/src/components/native-chat.test.ts +0 -22
  39. package/src/components/native-entrance/entrance-controller-store.test.ts +0 -75
  40. package/src/components/native-media-visibility.test.ts +0 -77
  41. package/src/components/native-space-callkit-controller.test.ts +0 -62
  42. package/src/components/native-space-view/AppearanceSettings.test.tsx +0 -56
  43. package/src/components/native-space-view/ParticipantActionSheet.test.tsx +0 -18
  44. package/src/components/native-space-view/SettingsSheet.test.tsx +0 -17
  45. package/src/components/native-space-view/SpaceActionMenu.test.tsx +0 -126
  46. package/src/components/native-space-view/SpaceBottomDock.android.test.tsx +0 -61
  47. package/src/components/native-space-view/SpaceChatSheet.test.tsx +0 -18
  48. package/src/components/native-space-view/SpaceMoreSheet.test.tsx +0 -15
  49. package/src/components/native-space-view/SpacePanelSheet.test.tsx +0 -15
  50. package/src/components/native-space-view/SpacePeopleSheet.test.tsx +0 -13
  51. package/src/components/native-space-view/SpaceSurfacePrimitives.test.tsx +0 -12
  52. package/src/components/native-space-view/SpaceTopBar.android.test.tsx +0 -50
  53. package/src/components/native-space-view/SpaceWhiteboardSurface.test.tsx +0 -85
  54. package/src/components/native-space-view/diagnostics.test.ts +0 -31
  55. package/src/components/native-space-view/format-space-duration.test.ts +0 -13
  56. package/src/components/native-space-view/native-space-grid-keys.test.ts +0 -11
  57. package/src/components/native-space-view/native-space-page-store.test.ts +0 -23
  58. package/src/components/native-space-view/space-chat-attachments.test.ts +0 -33
  59. package/src/components/native-space-view/space-progressive-surface-helpers.test.ts +0 -24
  60. package/src/components/native-space-view/useSpaceViewController.contract.test.ts +0 -10
  61. package/src/components/native-space-view/useSpaceViewController.test.ts +0 -106
  62. package/src/components/native-space-view/useSpaceViewDerived.test.ts +0 -58
  63. package/src/components/native-space-view/useSpaceViewPanels.controlled.test.ts +0 -40
  64. package/src/components/native-space-view/useSpaceViewPanels.test.ts +0 -73
  65. package/src/components/space-lifecycle.test.ts +0 -19
  66. package/src/context/space-client-context.test.tsx +0 -10
  67. package/src/diagnostics.test.ts +0 -29
  68. package/src/hooks/entrance-preview-store.test.ts +0 -79
  69. package/src/hooks/space-hooks.test.ts +0 -76
  70. package/src/hooks/useLayout.test.ts +0 -16
  71. package/src/invites.test.ts +0 -51
  72. package/src/public-surface.test.ts +0 -26
  73. package/src/runtime.test.ts +0 -83
  74. package/src/space-client/cloudflare-rtk-native.test.ts +0 -118
  75. package/src/space-client/create-native-space-client.test.ts +0 -300
  76. package/src/telemetry.test.ts +0 -143
  77. package/src/test-support/test-renderer.test.ts +0 -9
  78. package/src/test-support/test-renderer.ts +0 -28
  79. package/src/ui/appearance.test.ts +0 -61
  80. package/src/ui/native-appearance-context.controlled.test.tsx +0 -41
  81. package/src/ui/native-appearance-context.test.tsx +0 -14
  82. package/src/ui/native-theme.test.ts +0 -34
  83. package/src/ui/native-theme.test.tsx +0 -14
  84. package/src/ui/participant-avatar.test.ts +0 -29
  85. package/src/ui/theme-tokens.test.ts +0 -14
  86. package/src/ui/theme.test.ts +0 -36
  87. package/src/utils/build-chalk-invite-link.test.ts +0 -12
  88. package/src/utils/ios-simulator.test.ts +0 -96
  89. package/src/utils/native-space-layout.test.ts +0 -170
  90. package/src/whiteboard/embedded-whiteboard-assets.test.ts +0 -31
@@ -1,76 +0,0 @@
1
- import type { SpaceClient, SpaceSnapshot } from "@q9labsai/chalk-client";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
-
4
- const state = vi.hoisted(() => ({ client: null as SpaceClient | null }));
5
-
6
- vi.mock("../context/space-client-context", () => ({
7
- useSpaceClient: () => {
8
- if (!state.client) throw new Error("missing client");
9
- return state.client;
10
- },
11
- }));
12
-
13
- vi.mock("react", async (importOriginal) => {
14
- const react = await importOriginal<typeof import("react")>();
15
- return {
16
- ...react,
17
- useCallback: <T>(callback: T) => callback,
18
- useSyncExternalStore: (_subscribe: unknown, getSnapshot: () => unknown) => getSnapshot(),
19
- };
20
- });
21
-
22
- import { useCan, useChat, useConnection, useMedia, useParticipants, useReactions, useSelf, useWhiteboard } from "./space-hooks";
23
-
24
- describe("SpaceSnapshot hooks", () => {
25
- beforeEach(() => {
26
- state.client = client(snapshot());
27
- });
28
-
29
- it("returns the exact stable slices from SpaceSnapshot", () => {
30
- const current = state.client!.getSnapshot();
31
-
32
- expect(useConnection()).toBe(current.connection);
33
- expect(useSelf()).toBe(current.self);
34
- expect(useParticipants()).toBe(current.participants);
35
- expect(useMedia()).toBe(current.media);
36
- expect(useChat()).toBe(current.chat);
37
- expect(useReactions()).toBe(current.reactions);
38
- expect(useWhiteboard()).toBe(current.whiteboard);
39
- });
40
-
41
- it("derives capabilities from the self slice", () => {
42
- expect(useCan("sendChat")).toBe(true);
43
- expect(useCan("endEpisode")).toBe(false);
44
- });
45
- });
46
-
47
- function client(current: SpaceSnapshot): SpaceClient {
48
- return {
49
- getSnapshot: () => current,
50
- subscribe: () => () => undefined,
51
- } as SpaceClient;
52
- }
53
-
54
- function snapshot(): SpaceSnapshot {
55
- const capabilities = ["sendChat"] as const;
56
- return {
57
- connection: { status: "live", episode: null, lastError: null },
58
- self: { participantId: "participant-1", displayName: "Ada", role: "collaborator", capabilities, handRaised: false, can: (capability) => capabilities.includes(capability as (typeof capabilities)[number]) },
59
- participants: { roster: [], admissionQueue: [] },
60
- media: {
61
- devices: { microphones: [], cameras: [], speakers: [] },
62
- selection: { microphone: null, camera: null, speaker: null },
63
- local: {
64
- microphone: { source: "microphone", state: "disabled", track: null },
65
- camera: { source: "camera", state: "disabled", track: null },
66
- screen: { source: "screen", state: "disabled", track: null },
67
- },
68
- remote: [],
69
- screenShare: { source: "screen", state: "disabled", track: null },
70
- incomingRequests: [],
71
- },
72
- chat: { status: "idle", messages: [], pendingSends: [], readReceipts: [], unreadCount: 0, pagination: { cursor: null, hasOlder: false, historyTruncated: false }, lastError: null },
73
- reactions: { active: [] },
74
- whiteboard: { open: false, engine: { status: "unsubscribed", sceneId: null, revision: null, presenting: false, error: null } },
75
- };
76
- }
@@ -1,16 +0,0 @@
1
- // @vitest-environment happy-dom
2
-
3
- import { describe, expect, it } from "vitest";
4
-
5
- import { act, renderHook } from "../test-support/test-renderer";
6
- import { useLayout } from "./useLayout";
7
-
8
- describe("useLayout", () => {
9
- it("synchronizes a controlled layout without replacing the hook", () => {
10
- const { result, rerender } = renderHook(({ layout }) => useLayout({ layout }), { initialProps: { layout: "grid" as const } });
11
-
12
- expect(result.current.layout).toBe("grid");
13
- act(() => rerender({ layout: "presentation" }));
14
- expect(result.current.layout).toBe("presentation");
15
- });
16
- });
@@ -1,51 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { getClipboardInviteSuggestion, parseSpaceInviteLink } from "./invites";
3
-
4
- describe("parseSpaceInviteLink", () => {
5
- const inviteToken = "cspi1.key.payload.signature";
6
-
7
- it("accepts Chalk https invite links", () => {
8
- expect(parseSpaceInviteLink(`https://chalk.q9labs.ai/space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
9
- });
10
-
11
- it("accepts bare Chalk hostnames", () => {
12
- expect(parseSpaceInviteLink(`chalkmeet.com/space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
13
- });
14
-
15
- it("accepts native Chalk deep links with the canonical path", () => {
16
- expect(parseSpaceInviteLink(`chalk://space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
17
- expect(parseSpaceInviteLink(`ai.q9labs.chalk.mobile://space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
18
- });
19
-
20
- it("accepts loopback HTTP invite links for local development", () => {
21
- expect(parseSpaceInviteLink(`http://127.0.0.1:3070/space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
22
- expect(parseSpaceInviteLink(`http://localhost:3070/space/design-lab#spaceInviteToken=${inviteToken}`)).toEqual({ slug: "design-lab", spaceInviteToken: inviteToken });
23
- });
24
-
25
- it("rejects legacy paths, missing slugs, and invalid tokens", () => {
26
- expect(parseSpaceInviteLink(`https://chalk.q9labs.ai/legacy/${inviteToken}`)).toBeNull();
27
- expect(parseSpaceInviteLink(`https://chalk.q9labs.ai/space#spaceInviteToken=${inviteToken}`)).toBeNull();
28
- expect(parseSpaceInviteLink("https://chalk.q9labs.ai/space/design-lab#invite=legacy")).toBeNull();
29
- expect(parseSpaceInviteLink("https://attacker.test/space/design-lab#spaceInviteToken=cspi1.key.payload.signature")).toBeNull();
30
- expect(parseSpaceInviteLink(`http://chalkmeet.com/space/design-lab#spaceInviteToken=${inviteToken}`)).toBeNull();
31
- expect(parseSpaceInviteLink(`http://192.168.1.10/space/design-lab#spaceInviteToken=${inviteToken}`)).toBeNull();
32
- expect(parseSpaceInviteLink("https://chalk.q9labs.ai/space/design-lab#spaceInviteToken=ABC123")).toBeNull();
33
- expect(parseSpaceInviteLink(`https://chalk.q9labs.ai/space/design-lab#spaceInviteToken=${`cspi1.${"k".repeat(170)}.${"p".repeat(170)}.${"s".repeat(170)}`}`)).toBeNull();
34
- });
35
- });
36
-
37
- describe("getClipboardInviteSuggestion", () => {
38
- const inviteLink = "https://chalkmeet.com/space/design-lab#spaceInviteToken=cspi1.key.payload.signature";
39
-
40
- it("returns a copied Chalk invite link when it differs from the current input", () => {
41
- expect(getClipboardInviteSuggestion(inviteLink, "")).toBe(inviteLink);
42
- });
43
-
44
- it("hides the suggestion when the input already matches the clipboard", () => {
45
- expect(getClipboardInviteSuggestion(inviteLink, inviteLink)).toBeNull();
46
- });
47
-
48
- it("ignores non-invite clipboard values", () => {
49
- expect(getClipboardInviteSuggestion("ABC123", "")).toBeNull();
50
- });
51
- });
@@ -1,26 +0,0 @@
1
- import type { ClientEventMap } from "@q9labsai/chalk-client";
2
- import type { ThemeAppearance as ReactThemeAppearance, ThemePalette as ReactThemePalette, ThemeTexture as ReactThemeTexture } from "../../react/src/components/theme";
3
- import type { ChalkProps, ChalkThemeTokens, SpaceLayout } from "./components/Chalk";
4
- import type { EntranceProps, EntranceSettings } from "./components/Entrance";
5
- import type { ThemeAppearance, ThemePalette, ThemeTexture } from "./ui/appearance";
6
- import { expectTypeOf, test } from "vitest";
7
-
8
- test("the turnkey and entrance props use the ratified names", () => {
9
- expectTypeOf<ChalkProps>().toMatchTypeOf<{
10
- readonly entrance?: boolean;
11
- readonly defaults?: { readonly microphone?: boolean; readonly camera?: boolean };
12
- readonly features?: object;
13
- readonly layout?: SpaceLayout;
14
- readonly onOpenDiagnostics?: () => void;
15
- }>();
16
- expectTypeOf<EntranceProps>().toMatchTypeOf<{
17
- readonly defaults?: { readonly microphone?: boolean; readonly camera?: boolean };
18
- readonly onCancel?: () => void;
19
- }>();
20
- expectTypeOf<EntranceSettings>().toEqualTypeOf<{ readonly displayName: string; readonly microphone: boolean; readonly camera: boolean }>();
21
- expectTypeOf<NonNullable<ChalkProps["onError"]>>().toEqualTypeOf<(event: ClientEventMap["error"]) => void>();
22
- expectTypeOf<keyof ChalkThemeTokens>().toEqualTypeOf<"canvas" | "chrome" | "surface" | "stage" | "text" | "mutedText" | "line" | "accent" | "accentText" | "positive" | "danger" | "dangerSurface" | "focus" | "shadow">();
23
- expectTypeOf<ThemePalette>().toEqualTypeOf<ReactThemePalette>();
24
- expectTypeOf<ThemeTexture>().toEqualTypeOf<ReactThemeTexture>();
25
- expectTypeOf<ThemeAppearance>().toEqualTypeOf<ReactThemeAppearance>();
26
- });
@@ -1,83 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { canUseLocalHostBootstrap, getMetroHostFromScriptUrl, isConfiguredLocalApiUrl, isDeviceLocalUrl, resolveAppRuntimeUrl, resolveDeviceLocalUrl, shouldAutoReadClipboard } from "./runtime";
3
-
4
- vi.mock("react-native", () => ({
5
- NativeModules: { SourceCode: { scriptURL: null } },
6
- Platform: {
7
- OS: "ios",
8
- Version: "17.0",
9
- constants: { reactNativeVersion: { major: 0, minor: 83, patch: 2 } },
10
- },
11
- }));
12
-
13
- describe("mobile runtime helpers", () => {
14
- it("extracts the Metro host from a device script URL", () => {
15
- expect(getMetroHostFromScriptUrl("http://192.168.18.245:8081/index.bundle?platform=android")).toBe("192.168.18.245");
16
- expect(getMetroHostFromScriptUrl(null)).toBeNull();
17
- });
18
-
19
- it("rewrites localhost URLs to the Metro host for device builds", () => {
20
- const scriptUrl = "http://192.168.18.245:8081/index.bundle?platform=android";
21
-
22
- expect(resolveDeviceLocalUrl("http://localhost:8080", scriptUrl)).toBe("http://192.168.18.245:8080");
23
- expect(resolveDeviceLocalUrl("ws://127.0.0.1:8080/ws", scriptUrl)).toBe("ws://192.168.18.245:8080/ws");
24
- expect(resolveDeviceLocalUrl("https://chalk-api.q9labs.ai", scriptUrl)).toBe("https://chalk-api.q9labs.ai");
25
- });
26
-
27
- it("falls back to production URLs when release builds still carry localhost env values", () => {
28
- expect(resolveDeviceLocalUrl("http://localhost:8080", null, "https://chalk-api.q9labs.ai")).toBe("https://chalk-api.q9labs.ai");
29
- expect(resolveDeviceLocalUrl("ws://127.0.0.1:8080/ws", undefined, "wss://chalk-ws.q9labs.ai/ws")).toBe("wss://chalk-ws.q9labs.ai/ws");
30
- });
31
-
32
- it("preserves localhost URLs when the dev bundle is also served from localhost", () => {
33
- const scriptUrl = "http://localhost:8081/index.bundle?platform=ios";
34
-
35
- expect(resolveDeviceLocalUrl("http://localhost:8080", scriptUrl, "https://chalk-api.q9labs.ai")).toBe("http://localhost:8080");
36
- expect(resolveDeviceLocalUrl("ws://127.0.0.1:8080/ws", scriptUrl, "wss://chalk-ws.q9labs.ai/ws")).toBe("ws://127.0.0.1:8080/ws");
37
- });
38
-
39
- it("hard-forces production URLs when release builds disallow device-local hosts", () => {
40
- expect(
41
- resolveAppRuntimeUrl({
42
- configuredUrl: "http://localhost:8080",
43
- scriptUrl: "file:///android_asset/index.android.bundle",
44
- fallbackUrl: "https://chalk-api.q9labs.ai",
45
- allowDeviceLocal: false,
46
- }),
47
- ).toBe("https://chalk-api.q9labs.ai");
48
-
49
- expect(
50
- resolveAppRuntimeUrl({
51
- configuredUrl: "ws://localhost:8080/ws",
52
- scriptUrl: "file:///android_asset/index.android.bundle",
53
- fallbackUrl: "wss://chalk-ws.q9labs.ai/ws",
54
- allowDeviceLocal: false,
55
- }),
56
- ).toBe("wss://chalk-ws.q9labs.ai/ws");
57
- });
58
-
59
- it("detects local device URLs", () => {
60
- expect(isDeviceLocalUrl("http://localhost:8080")).toBe(true);
61
- expect(isDeviceLocalUrl("ws://127.0.0.1:8080/ws")).toBe(true);
62
- expect(isDeviceLocalUrl("https://chalk-api.q9labs.ai")).toBe(false);
63
- });
64
-
65
- it("detects configured local api urls even outside dev mode", () => {
66
- expect(isConfiguredLocalApiUrl("http://localhost:8080")).toBe(true);
67
- expect(isConfiguredLocalApiUrl(" ws://127.0.0.1:8080/ws ")).toBe(true);
68
- expect(isConfiguredLocalApiUrl("https://chalk-api.q9labs.ai")).toBe(false);
69
- expect(isConfiguredLocalApiUrl(undefined)).toBe(false);
70
- });
71
-
72
- it("only allows host bootstrap for resolved local runtime urls", () => {
73
- expect(canUseLocalHostBootstrap("http://localhost:8080", true)).toBe(true);
74
- expect(canUseLocalHostBootstrap("https://chalk-api.q9labs.ai", true)).toBe(false);
75
- expect(canUseLocalHostBootstrap("http://localhost:8080", false)).toBe(false);
76
- });
77
-
78
- it("skips eager clipboard reads on the iOS simulator", () => {
79
- expect(shouldAutoReadClipboard({ platform: "ios", isSimulator: true })).toBe(false);
80
- expect(shouldAutoReadClipboard({ platform: "ios", isSimulator: false })).toBe(true);
81
- expect(shouldAutoReadClipboard({ platform: "android", isSimulator: false })).toBe(true);
82
- });
83
- });
@@ -1,118 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const initialize = vi.hoisted(() => vi.fn());
4
-
5
- vi.mock("@cloudflare/realtimekit-react-native", () => ({ default: { init: initialize } }));
6
-
7
- import { createNativeRealtimeKitClient } from "./cloudflare-rtk-native";
8
-
9
- describe("native RealtimeKit adapter", () => {
10
- beforeEach(() => {
11
- vi.clearAllMocks();
12
- });
13
-
14
- it("initializes, joins, leaves, and projects native participants", async () => {
15
- const selfOn = vi.fn();
16
- const selfOff = vi.fn();
17
- const joinedOn = vi.fn();
18
- const joinedOff = vi.fn();
19
- const joinRoom = vi.fn().mockResolvedValue(undefined);
20
- const leaveRoom = vi.fn().mockResolvedValue(undefined);
21
- const audioTrack = { kind: "audio" };
22
- const videoTrack = { kind: "video" };
23
- const participant = {
24
- id: "native-participant",
25
- userId: "user",
26
- customParticipantId: "custom-participant",
27
- audioEnabled: true,
28
- videoEnabled: false,
29
- screenShareEnabled: false,
30
- audioTrack,
31
- videoTrack: null,
32
- screenShareTracks: {},
33
- };
34
- const nativeClient = {
35
- joinRoom,
36
- leaveRoom,
37
- self: {
38
- peerId: "peer",
39
- audioEnabled: true,
40
- videoEnabled: false,
41
- screenShareEnabled: false,
42
- audioTrack,
43
- videoTrack,
44
- screenShareTracks: {},
45
- enableAudio: vi.fn().mockResolvedValue(undefined),
46
- enableVideo: vi.fn().mockResolvedValue(undefined),
47
- enableScreenShare: vi.fn().mockResolvedValue(undefined),
48
- disableAudio: vi.fn().mockResolvedValue(undefined),
49
- disableVideo: vi.fn().mockResolvedValue(undefined),
50
- disableScreenShare: vi.fn().mockResolvedValue(undefined),
51
- on: selfOn,
52
- off: selfOff,
53
- },
54
- participants: {
55
- joined: {
56
- toArray: vi.fn(() => [participant]),
57
- on: joinedOn,
58
- off: joinedOff,
59
- },
60
- },
61
- };
62
- initialize.mockResolvedValue(nativeClient);
63
-
64
- const onError = vi.fn();
65
- const connection = await createNativeRealtimeKitClient({ authToken: "rtk-token", onError });
66
- await connection.join();
67
- await connection.leave();
68
-
69
- expect(initialize).toHaveBeenCalledWith({ authToken: "rtk-token", defaults: { audio: false, video: false }, onError });
70
- expect(joinRoom).toHaveBeenCalledOnce();
71
- expect(leaveRoom).toHaveBeenCalledOnce();
72
- expect(connection.self.peerId).toBe("peer");
73
- expect(connection.self.audioTrack).toBe(audioTrack);
74
- expect(connection.participants.joined.list()).toEqual([expect.objectContaining({ id: "native-participant", customParticipantId: "custom-participant", audioTrack })]);
75
- });
76
-
77
- it("bridges lifecycle event subscriptions and removes them", async () => {
78
- const selfOn = vi.fn();
79
- const selfOff = vi.fn();
80
- const joinedOn = vi.fn();
81
- const joinedOff = vi.fn();
82
- initialize.mockResolvedValue({
83
- joinRoom: vi.fn(),
84
- leaveRoom: vi.fn(),
85
- self: {
86
- peerId: "peer",
87
- audioEnabled: false,
88
- videoEnabled: false,
89
- screenShareEnabled: false,
90
- audioTrack: null,
91
- videoTrack: null,
92
- screenShareTracks: {},
93
- enableAudio: vi.fn(),
94
- enableVideo: vi.fn(),
95
- enableScreenShare: vi.fn(),
96
- disableAudio: vi.fn(),
97
- disableVideo: vi.fn(),
98
- disableScreenShare: vi.fn(),
99
- on: selfOn,
100
- off: selfOff,
101
- },
102
- participants: { joined: { toArray: vi.fn(() => []), on: joinedOn, off: joinedOff } },
103
- });
104
-
105
- const connection = await createNativeRealtimeKitClient({ authToken: "rtk-token", onError: vi.fn() });
106
- const listener = vi.fn();
107
- const unsubscribeAudio = connection.self.onAudioUpdate(listener);
108
- const unsubscribeJoined = connection.participants.joined.onJoined(listener);
109
-
110
- unsubscribeAudio();
111
- unsubscribeJoined();
112
-
113
- expect(selfOn).toHaveBeenCalledWith("audioUpdate", listener);
114
- expect(selfOff).toHaveBeenCalledWith("audioUpdate", listener);
115
- expect(joinedOn).toHaveBeenCalledWith("participantJoined", expect.any(Function));
116
- expect(joinedOff).toHaveBeenCalledWith("participantJoined", expect.any(Function));
117
- });
118
- });
@@ -1,300 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const createSpaceClientForPlatform = vi.hoisted(() => vi.fn());
4
- const addAppStateListener = vi.hoisted(() => vi.fn());
5
- const createV1SyncClient = vi.hoisted(() => vi.fn());
6
- const createCloudflareSFUHTTPTransport = vi.hoisted(() => vi.fn(() => ({})));
7
- const CloudflareRTKClient = vi.hoisted(() =>
8
- vi.fn(
9
- class MockCloudflareRTKClient {
10
- setLocalPublicationTarget = vi.fn();
11
- observeLocalPublications = vi.fn();
12
- observeRemotePublications = vi.fn();
13
- start = vi.fn();
14
- stop = vi.fn();
15
- restart = vi.fn();
16
- prepareLocalTrack = vi.fn();
17
- clearPreparedLocalTrack = vi.fn();
18
- getSnapshot = vi.fn();
19
- subscribe = vi.fn();
20
- },
21
- ),
22
- );
23
- const CloudflareSFUClient = vi.hoisted(() =>
24
- vi.fn(
25
- class MockCloudflareSFUClient {
26
- setLocalPublicationTarget = vi.fn();
27
- observeLocalPublications = vi.fn();
28
- observeRemotePublications = vi.fn();
29
- start = vi.fn();
30
- stop = vi.fn();
31
- restart = vi.fn();
32
- prepareLocalTrack = vi.fn();
33
- clearPreparedLocalTrack = vi.fn();
34
- getSnapshot = vi.fn();
35
- subscribe = vi.fn();
36
- },
37
- ),
38
- );
39
-
40
- vi.mock("@q9labsai/chalk-client", () => ({
41
- AsyncStorageV1PendingTargetStore: vi.fn(),
42
- CloudflareRTKClient,
43
- CloudflareSFUClient,
44
- createChalkChatFileHttpTransport: vi.fn(),
45
- createChalkWhiteboardV1Client: vi.fn(),
46
- createChalkWhiteboardV1FileHttpTransport: vi.fn(),
47
- createCloudflareSFUHTTPTransport,
48
- createReactNativeSyncLifecycle: vi.fn(() => ({})),
49
- createReactNativeWebSocketFactory: vi.fn(() => ({})),
50
- createV1SyncClient,
51
- }));
52
- vi.mock("@q9labsai/chalk-client/effect", () => ({ createSpaceClientForPlatform }));
53
- vi.mock("@cloudflare/react-native-webrtc", () => ({
54
- RTCPeerConnection: vi.fn(),
55
- mediaDevices: {
56
- getDisplayMedia: vi.fn(),
57
- getUserMedia: vi.fn(),
58
- },
59
- }));
60
- vi.mock("react-native", () => ({
61
- AppState: {
62
- addEventListener: addAppStateListener,
63
- currentState: "active",
64
- },
65
- }));
66
-
67
- import { createNativeSpaceClient } from "./create-native-space-client";
68
-
69
- describe("native client creation", () => {
70
- beforeEach(() => {
71
- vi.clearAllMocks();
72
- createSpaceClientForPlatform.mockReturnValue(spaceClient());
73
- addAppStateListener.mockReturnValue({ remove: vi.fn() });
74
- });
75
-
76
- it("allows multiple native WebSocket attempts before Sync startup expires", () => {
77
- createNativeSpaceClient(options());
78
-
79
- expect(createSpaceClientForPlatform.mock.calls[0]?.[1]).toEqual(expect.objectContaining({ syncStartupTimeoutMs: 30_000 }));
80
- });
81
-
82
- it("preserves an explicit consumer Sync startup budget", () => {
83
- createNativeSpaceClient({ ...options(), syncStartupTimeoutMs: 45_000 });
84
-
85
- expect(createSpaceClientForPlatform.mock.calls[0]?.[1]).toEqual(expect.objectContaining({ syncStartupTimeoutMs: 45_000 }));
86
- });
87
-
88
- it("derives the canonical Sync origin from the default API origin", () => {
89
- createNativeSpaceClient({ ...options(), baseUrl: undefined, syncUrl: undefined });
90
-
91
- expect(createSpaceClientForPlatform.mock.calls[0]?.[1]).toEqual(expect.objectContaining({ apiBaseUrl: "https://api.chalkmeet.com", syncUrl: "wss://sync.chalkmeet.com/v1/sync" }));
92
- });
93
-
94
- it("selects the native RealtimeKit adapter for RTK access grants", () => {
95
- createNativeSpaceClient(options());
96
- const platform = createSpaceClientForPlatform.mock.calls[0]?.[1] as {
97
- readonly dependencies: { readonly createMediaClient: (input: unknown) => unknown };
98
- };
99
-
100
- platform.dependencies.createMediaClient({
101
- access: {
102
- subject: { tenantId: "tenant", spaceId: "space", episodeId: "episode", participantId: "participant", participantGeneration: 1 },
103
- media: { provider: "cloudflare_rtk", clientPayload: { providerSubject: "provider-subject", token: "rtk-token" } },
104
- },
105
- credential: vi.fn(),
106
- onFailure: vi.fn(),
107
- onScreenEnded: vi.fn(),
108
- });
109
-
110
- expect(CloudflareRTKClient).toHaveBeenCalledWith(expect.objectContaining({ authToken: "rtk-token", participantId: "participant", clientFactory: expect.any(Function) }));
111
- expect(createCloudflareSFUHTTPTransport).not.toHaveBeenCalled();
112
- });
113
-
114
- it("passes the closed access callback to the client", () => {
115
- const getAccess = vi.fn();
116
- createNativeSpaceClient({ ...options(), getAccess });
117
-
118
- expect(createSpaceClientForPlatform.mock.calls[0]?.[0]).toEqual(expect.objectContaining({ getAccess }));
119
- });
120
-
121
- it("forwards Space join spans into the correlated diagnostic timeline", () => {
122
- const journey = {
123
- context: { journeyId: "journey", rootJourneyId: "journey", traceparent: "00-11111111111111111111111111111111-2222222222222222-01" },
124
- headers: {},
125
- recordDiagnostic: vi.fn(),
126
- recordRtcSummary: vi.fn(),
127
- recordSyncFrame: vi.fn(),
128
- };
129
-
130
- createNativeSpaceClient({ ...options(), telemetry: journey });
131
- const platform = createSpaceClientForPlatform.mock.calls[0]?.[1] as { readonly onConnectionDiagnostic: (event: unknown) => void };
132
- platform.onConnectionDiagnostic({
133
- event: "space_join_span",
134
- state: "live",
135
- epoch: 2,
136
- step: "start_media",
137
- spanId: "space-join-span-2",
138
- parentSpanId: "space-join-span-1",
139
- outcome: "succeeded",
140
- durationMs: 12,
141
- code: "media_start_failed",
142
- });
143
-
144
- expect(journey.recordDiagnostic).toHaveBeenCalledWith({
145
- category: "connection",
146
- code: "space.join.start_media.succeeded",
147
- phase: "media",
148
- state: "succeeded",
149
- metricValue: 12,
150
- attributes: {
151
- span_id: "space-join-span-2",
152
- parent_span_id: "space-join-span-1",
153
- step: "start_media",
154
- outcome: "succeeded",
155
- epoch: 2,
156
- failure_code: "media_start_failed",
157
- },
158
- });
159
- });
160
-
161
- it("passes telemetry context into the production Sync constructor", () => {
162
- const journey = {
163
- context: {
164
- journeyId: "journey",
165
- rootJourneyId: "journey",
166
- traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
167
- tracestate: "chalk=native",
168
- },
169
- headers: {},
170
- recordDiagnostic: vi.fn(),
171
- recordRtcSummary: vi.fn(),
172
- recordSyncFrame: vi.fn(),
173
- };
174
-
175
- createNativeSpaceClient({ ...options(), telemetry: journey });
176
- const platform = createSpaceClientForPlatform.mock.calls[0]?.[1] as {
177
- readonly telemetry: typeof journey.context;
178
- readonly dependencies: { readonly createSyncClient: (input: unknown) => unknown };
179
- };
180
- expect(platform.telemetry).toEqual(journey.context);
181
- platform.dependencies.createSyncClient({
182
- access: { subject: { tenantId: "tenant", episodeId: "episode", participantId: "participant" } },
183
- token: async () => "token",
184
- media: {},
185
- telemetry: platform.telemetry,
186
- });
187
-
188
- expect(createV1SyncClient).toHaveBeenCalledWith(expect.objectContaining({ telemetry: journey.context }));
189
- });
190
-
191
- it("revalidates access when the app returns to the foreground", () => {
192
- createNativeSpaceClient(options());
193
- const platform = createSpaceClientForPlatform.mock.calls[0]?.[1] as {
194
- readonly dependencies: { readonly subscribeForeground: (listener: () => void) => () => void };
195
- };
196
-
197
- const refresh = vi.fn();
198
- const unsubscribe = platform.dependencies.subscribeForeground(refresh);
199
- const stateListener = addAppStateListener.mock.calls[0]?.[1] as (state: string) => void;
200
-
201
- stateListener("background");
202
- expect(refresh).not.toHaveBeenCalled();
203
- stateListener("active");
204
- expect(refresh).toHaveBeenCalledOnce();
205
-
206
- unsubscribe();
207
- expect(addAppStateListener.mock.results[0]?.value.remove).toHaveBeenCalledOnce();
208
- });
209
-
210
- it("bridges verified media credentials to telemetry and clears them at terminal teardown", async () => {
211
- const client = spaceClient();
212
- createSpaceClientForPlatform.mockReturnValue(client);
213
- const setAuthenticatedTelemetryHeaders = vi.fn();
214
- const journey = {
215
- context: { journeyId: "journey", rootJourneyId: "journey", traceparent: "00-11111111111111111111111111111111-2222222222222222-01" },
216
- headers: { "x-chalk-journey-id": "journey" },
217
- recordDiagnostic: vi.fn(),
218
- recordRtcSummary: vi.fn(),
219
- recordSyncFrame: vi.fn(),
220
- setAuthenticatedTelemetryHeaders,
221
- };
222
- createNativeSpaceClient({ ...options(), telemetry: journey });
223
- const platform = createSpaceClientForPlatform.mock.calls[0]?.[1] as {
224
- readonly dependencies: { readonly createMediaClient: (input: unknown) => unknown };
225
- };
226
- const credential = vi.fn().mockResolvedValueOnce("media-one").mockResolvedValueOnce("media-two").mockResolvedValueOnce("media-three");
227
- platform.dependencies.createMediaClient({
228
- access: { subject: { tenantId: "tenant", episodeId: "episode", participantId: "participant" }, media: { clientPayload: {} } },
229
- credential,
230
- onFailure: vi.fn(),
231
- onScreenEnded: vi.fn(),
232
- });
233
-
234
- const transport = createCloudflareSFUHTTPTransport.mock.calls.at(-1)?.[0] as { readonly credential: () => Promise<string> };
235
- await expect(transport.credential()).resolves.toBe("media-one");
236
- await expect(transport.credential()).resolves.toBe("media-two");
237
- expect(setAuthenticatedTelemetryHeaders).toHaveBeenNthCalledWith(1, { Authorization: "Bearer media-one" });
238
- expect(setAuthenticatedTelemetryHeaders).toHaveBeenNthCalledWith(2, { Authorization: "Bearer media-two" });
239
- expect(journey.recordDiagnostic).not.toHaveBeenCalledWith(expect.objectContaining({ attributes: expect.stringContaining("media-") }));
240
- expect(JSON.stringify(journey.recordDiagnostic.mock.calls)).not.toContain("media-one");
241
-
242
- client.setStatus("left");
243
- expect(setAuthenticatedTelemetryHeaders).toHaveBeenLastCalledWith(undefined);
244
- await expect(transport.credential()).resolves.toBe("media-three");
245
- client.dispose();
246
- expect(setAuthenticatedTelemetryHeaders).toHaveBeenLastCalledWith(undefined);
247
- expect(credential).toHaveBeenCalledTimes(3);
248
- });
249
- });
250
-
251
- function options() {
252
- return {
253
- space: "native-space",
254
- getAccess: vi.fn(),
255
- baseUrl: "https://api.chalk.test",
256
- syncUrl: "wss://sync.chalk.test/v1/sync",
257
- };
258
- }
259
-
260
- function spaceClient() {
261
- const snapshot = {
262
- connection: { status: "idle", episode: null, lastError: null },
263
- self: { participantId: null, displayName: null, role: null, capabilities: [], handRaised: false, can: () => false },
264
- participants: { roster: [], admissionQueue: [] },
265
- media: {
266
- devices: { microphones: [], cameras: [], speakers: [] },
267
- selection: { microphone: null, camera: null, speaker: null },
268
- local: {
269
- microphone: { source: "microphone", state: "unavailable", track: null },
270
- camera: { source: "camera", state: "unavailable", track: null },
271
- screen: { source: "screen", state: "unavailable", track: null },
272
- },
273
- remote: [],
274
- screenShare: { source: "screen", state: "unavailable", track: null },
275
- incomingRequests: [],
276
- },
277
- chat: { status: "idle", messages: [], pendingSends: [], readReceipts: [], unreadCount: 0, pagination: { cursor: null, hasOlder: false, historyTruncated: false }, lastError: null },
278
- reactions: { active: [] },
279
- whiteboard: { open: false, engine: { status: "unsubscribed", sceneId: null, revision: null, presenting: false, error: null } },
280
- } as const;
281
- let currentSnapshot = snapshot;
282
- const listeners = new Set<() => void>();
283
- return {
284
- media: {},
285
- chat: { files: {} },
286
- participants: {},
287
- reactions: {},
288
- whiteboard: { transport: () => null },
289
- getSnapshot: () => currentSnapshot,
290
- subscribe: (listener: () => void) => {
291
- listeners.add(listener);
292
- return () => listeners.delete(listener);
293
- },
294
- setStatus: (status: "idle" | "failed" | "left") => {
295
- currentSnapshot = { ...currentSnapshot, connection: { ...currentSnapshot.connection, status } };
296
- listeners.forEach((listener) => listener());
297
- },
298
- dispose: vi.fn(),
299
- };
300
- }