@q9labsai/chalk-react-native 4.1.11 → 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 (103) hide show
  1. package/dist/android.js +4 -2
  2. package/dist/{chunk-MMBE2BVK.js → chunk-4NLA35HE.js} +649 -407
  3. package/dist/{chunk-LZ67SXGB.js → chunk-MK6OO4V7.js} +6 -0
  4. package/dist/client.js +1 -1
  5. package/dist/components/Entrance.d.ts.map +1 -1
  6. package/dist/components/logo/Logo.d.ts +11 -0
  7. package/dist/components/logo/Logo.d.ts.map +1 -0
  8. package/dist/components/native-space-view/SpaceTopBar.android.d.ts.map +1 -1
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +4 -2
  12. package/dist/ios-pad.js +4 -2
  13. package/dist/ios-phone.js +4 -2
  14. package/dist/ios.js +4 -2
  15. package/dist/macos.js +4 -2
  16. package/package.json +4 -4
  17. package/src/callkit/callkit.test.ts +12 -10
  18. package/src/components/Entrance.tsx +2 -1
  19. package/src/components/logo/Logo.tsx +309 -0
  20. package/src/components/native-space-view/SpaceTopBar.android.tsx +2 -1
  21. package/src/index.ts +2 -0
  22. package/src/space-client/create-native-space-client.ts +8 -0
  23. package/dist/components/LogoElements.d.ts +0 -6
  24. package/dist/components/LogoElements.d.ts.map +0 -1
  25. package/dist/test-support/test-renderer.d.ts +0 -20
  26. package/dist/test-support/test-renderer.d.ts.map +0 -1
  27. package/src/android/connection-service.test.ts +0 -114
  28. package/src/callkit/resolve-space-callkit-options.test.ts +0 -68
  29. package/src/client.test.ts +0 -12
  30. package/src/clipboard.test.ts +0 -79
  31. package/src/components/Chalk.test.tsx +0 -223
  32. package/src/components/EmbeddedWhiteboard.test.tsx +0 -92
  33. package/src/components/Entrance.test.tsx +0 -19
  34. package/src/components/EntranceView.android.test.tsx +0 -100
  35. package/src/components/EntranceView.ios-pad.test.tsx +0 -160
  36. package/src/components/EntranceView.ios-phone.test.tsx +0 -137
  37. package/src/components/EntranceView.test.tsx +0 -15
  38. package/src/components/LogoElements.test.tsx +0 -60
  39. package/src/components/LogoElements.tsx +0 -119
  40. package/src/components/ReactionPicker.test.tsx +0 -19
  41. package/src/components/RtcVideoView.test.tsx +0 -12
  42. package/src/components/SpaceView.android.test.tsx +0 -11
  43. package/src/components/SpaceView.ios-pad.test.tsx +0 -11
  44. package/src/components/SpaceView.ios-phone.test.tsx +0 -11
  45. package/src/components/SpaceView.macos.test.tsx +0 -11
  46. package/src/components/SpaceView.shared.test.tsx +0 -116
  47. package/src/components/SpaceView.test.tsx +0 -15
  48. package/src/components/android-connection-service-controller.test.ts +0 -80
  49. package/src/components/episode-diagnostic-render-observer.test.ts +0 -24
  50. package/src/components/native-animation-controller.test.ts +0 -47
  51. package/src/components/native-chat.test.ts +0 -22
  52. package/src/components/native-entrance/entrance-controller-store.test.ts +0 -75
  53. package/src/components/native-media-visibility.test.ts +0 -77
  54. package/src/components/native-space-callkit-controller.test.ts +0 -62
  55. package/src/components/native-space-view/AppearanceSettings.test.tsx +0 -56
  56. package/src/components/native-space-view/ParticipantActionSheet.test.tsx +0 -18
  57. package/src/components/native-space-view/SettingsSheet.test.tsx +0 -17
  58. package/src/components/native-space-view/SpaceActionMenu.test.tsx +0 -126
  59. package/src/components/native-space-view/SpaceBottomDock.android.test.tsx +0 -61
  60. package/src/components/native-space-view/SpaceChatSheet.test.tsx +0 -18
  61. package/src/components/native-space-view/SpaceMoreSheet.test.tsx +0 -15
  62. package/src/components/native-space-view/SpacePanelSheet.test.tsx +0 -15
  63. package/src/components/native-space-view/SpacePeopleSheet.test.tsx +0 -13
  64. package/src/components/native-space-view/SpaceSurfacePrimitives.test.tsx +0 -12
  65. package/src/components/native-space-view/SpaceTopBar.android.test.tsx +0 -43
  66. package/src/components/native-space-view/SpaceWhiteboardSurface.test.tsx +0 -85
  67. package/src/components/native-space-view/diagnostics.test.ts +0 -31
  68. package/src/components/native-space-view/format-space-duration.test.ts +0 -13
  69. package/src/components/native-space-view/native-space-grid-keys.test.ts +0 -11
  70. package/src/components/native-space-view/native-space-page-store.test.ts +0 -23
  71. package/src/components/native-space-view/space-chat-attachments.test.ts +0 -33
  72. package/src/components/native-space-view/space-progressive-surface-helpers.test.ts +0 -24
  73. package/src/components/native-space-view/useSpaceViewController.contract.test.ts +0 -10
  74. package/src/components/native-space-view/useSpaceViewController.test.ts +0 -106
  75. package/src/components/native-space-view/useSpaceViewDerived.test.ts +0 -58
  76. package/src/components/native-space-view/useSpaceViewPanels.controlled.test.ts +0 -40
  77. package/src/components/native-space-view/useSpaceViewPanels.test.ts +0 -73
  78. package/src/components/space-lifecycle.test.ts +0 -19
  79. package/src/context/space-client-context.test.tsx +0 -10
  80. package/src/diagnostics.test.ts +0 -29
  81. package/src/hooks/entrance-preview-store.test.ts +0 -79
  82. package/src/hooks/space-hooks.test.ts +0 -76
  83. package/src/hooks/useLayout.test.ts +0 -16
  84. package/src/invites.test.ts +0 -51
  85. package/src/public-surface.test.ts +0 -26
  86. package/src/runtime.test.ts +0 -83
  87. package/src/space-client/cloudflare-rtk-native.test.ts +0 -118
  88. package/src/space-client/create-native-space-client.test.ts +0 -300
  89. package/src/telemetry.test.ts +0 -143
  90. package/src/test-support/test-renderer.test.ts +0 -9
  91. package/src/test-support/test-renderer.ts +0 -28
  92. package/src/ui/appearance.test.ts +0 -61
  93. package/src/ui/native-appearance-context.controlled.test.tsx +0 -41
  94. package/src/ui/native-appearance-context.test.tsx +0 -14
  95. package/src/ui/native-theme.test.ts +0 -34
  96. package/src/ui/native-theme.test.tsx +0 -14
  97. package/src/ui/participant-avatar.test.ts +0 -29
  98. package/src/ui/theme-tokens.test.ts +0 -14
  99. package/src/ui/theme.test.ts +0 -36
  100. package/src/utils/build-chalk-invite-link.test.ts +0 -12
  101. package/src/utils/ios-simulator.test.ts +0 -96
  102. package/src/utils/native-space-layout.test.ts +0 -170
  103. package/src/whiteboard/embedded-whiteboard-assets.test.ts +0 -31
@@ -3,6 +3,7 @@ import { HugeiconsIcon } from "@hugeicons/react-native";
3
3
  import { Image, Platform, StyleSheet, Text, View } from "react-native";
4
4
  import { Theme } from "../../ui/theme";
5
5
  import { useNativeTheme } from "../../ui/native-theme";
6
+ import { Logo } from "../logo/Logo";
6
7
 
7
8
  export interface SpaceTopBarProps {
8
9
  spaceName: string;
@@ -16,7 +17,7 @@ export function SpaceTopBarAndroid({ spaceName, participantCount, formattedDurat
16
17
  return (
17
18
  <View style={[styles.topBar, { backgroundColor: theme.colors.darkCanvas, borderColor: theme.colors.border }]}>
18
19
  <View style={styles.topBarLeft}>
19
- {logoUrl ? <Image accessibilityLabel="Chalk" source={{ uri: logoUrl }} style={styles.logo} /> : null}
20
+ {logoUrl ? <Image accessibilityLabel="Chalk" source={{ uri: logoUrl }} style={styles.logo} /> : <Logo height={24} variant="mark" />}
20
21
  <View style={[styles.connectionDot, { backgroundColor: theme.colors.success }]} />
21
22
  <Text style={[styles.topBarSpaceName, { color: theme.colors.foreground }]} numberOfLines={1}>
22
23
  {spaceName}
package/src/index.ts CHANGED
@@ -2,6 +2,8 @@ export { Chalk } from "./components/Chalk";
2
2
  export type { ChalkFeatures, ChalkProps, ChalkTheme, ChalkThemeTokens, SpaceLayout } from "./components/Chalk";
3
3
  export { Entrance } from "./components/Entrance";
4
4
  export type { EntranceProps, EntranceSettings } from "./components/Entrance";
5
+ export { Logo } from "./components/logo/Logo";
6
+ export type { LogoMotion, LogoProps, LogoVariant } from "./components/logo/Logo";
5
7
  export { ChalkProvider, useSpaceClient } from "./context/space-client-context";
6
8
  export type { ChalkProviderProps } from "./context/space-client-context";
7
9
  export { useCan, useChat, useConnection, useMedia, useParticipants, useReactions, useSelf, useWhiteboard } from "./hooks/space-hooks";
@@ -144,6 +144,7 @@ function createMediaClient(apiBaseURL: string, input: MediaFactoryInput, fetch:
144
144
  });
145
145
  return createRTKConnectionMediaClient(client);
146
146
  }
147
+ const replaceMediaConnection = input.replaceMediaConnection;
147
148
  const client = new CloudflareSFUClient({
148
149
  bootstrap: input.access.media.clientPayload,
149
150
  participantId: subject.participantId,
@@ -156,6 +157,13 @@ function createMediaClient(apiBaseURL: string, input: MediaFactoryInput, fetch:
156
157
  participantId: subject.participantId,
157
158
  fetch,
158
159
  }),
160
+ replaceMediaConnection: replaceMediaConnection
161
+ ? async () => {
162
+ const replacement = await replaceMediaConnection();
163
+ if (replacement.provider !== "cloudflare_sfu") throw new TypeError("The Cloudflare SFU adapter requires a Cloudflare SFU access grant");
164
+ return replacement.clientPayload;
165
+ }
166
+ : undefined,
159
167
  peerConnectionFactory: (configuration) => {
160
168
  const connection = new RTCPeerConnection(configuration);
161
169
  observePeerConnection?.(connection as unknown as RtcPeerConnection);
@@ -1,6 +0,0 @@
1
- interface LogoElementsProps {
2
- size?: number;
3
- }
4
- export declare function LogoElements({ size }: LogoElementsProps): React.JSX.Element;
5
- export {};
6
- //# sourceMappingURL=LogoElements.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LogoElements.d.ts","sourceRoot":"","sources":["../../src/components/LogoElements.tsx"],"names":[],"mappings":"AASA,UAAU,iBAAiB;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,wBAAgB,YAAY,CAAC,EAAE,IAAS,EAAE,EAAE,iBAAiB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAyGhF"}
@@ -1,20 +0,0 @@
1
- import type { ComponentType, ReactNode } from "react";
2
- type RenderHookOptions<Props> = {
3
- readonly initialProps?: Props;
4
- readonly wrapper?: ComponentType<{
5
- readonly children?: ReactNode;
6
- }>;
7
- };
8
- type RenderHookResult<Result, Props> = {
9
- readonly result: {
10
- readonly current: Result;
11
- };
12
- readonly rerender: (props?: Props) => void;
13
- readonly unmount: () => void;
14
- };
15
- type RenderHook = <Result, Props = undefined>(callback: (props: Props) => Result, options?: RenderHookOptions<Props>) => RenderHookResult<Result, Props>;
16
- type Act = (callback: () => void | Promise<void>) => void | Promise<void>;
17
- type WaitFor = (callback: () => void | Promise<void>) => Promise<void>;
18
- export declare const act: Act, renderHook: RenderHook, waitFor: WaitFor;
19
- export {};
20
- //# sourceMappingURL=test-renderer.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"test-renderer.d.ts","sourceRoot":"","sources":["../../src/test-support/test-renderer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEtD,KAAK,iBAAiB,CAAC,KAAK,IAAI;IAC9B,QAAQ,CAAC,YAAY,CAAC,EAAE,KAAK,CAAC;IAC9B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;QAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC,CAAC;CACrE,CAAC;AAEF,KAAK,gBAAgB,CAAC,MAAM,EAAE,KAAK,IAAI;IACrC,QAAQ,CAAC,MAAM,EAAE;QAAE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;CAC9B,CAAC;AAEF,KAAK,UAAU,GAAG,CAAC,MAAM,EAAE,KAAK,GAAG,SAAS,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,CAAC,KAAK,CAAC,KAAK,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AACzJ,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAC1E,KAAK,OAAO,GAAG,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAQvE,eAAO,MAAQ,GAAG,OAAE,UAAU,cAAE,OAAO,SAAmB,CAAC"}
@@ -1,114 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
-
3
- const nativeModule = {
4
- addListener: vi.fn(),
5
- endCall: vi.fn(async () => true),
6
- isSupported: vi.fn(async () => true),
7
- registerPhoneAccount: vi.fn(async () => true),
8
- removeListeners: vi.fn(),
9
- setActive: vi.fn(async () => true),
10
- startCall: vi.fn(async () => true),
11
- };
12
-
13
- const subscriptions = new Map<string, (payload: unknown) => void>();
14
- const platform = { OS: "android" };
15
-
16
- class MockNativeEventEmitter {
17
- addListener(eventName: string, listener: (payload: unknown) => void) {
18
- subscriptions.set(eventName, listener);
19
- return {
20
- remove: () => {
21
- subscriptions.delete(eventName);
22
- },
23
- };
24
- }
25
- }
26
-
27
- vi.mock("react-native", () => ({
28
- NativeEventEmitter: MockNativeEventEmitter,
29
- NativeModules: {
30
- ChalkAndroidConnectionService: nativeModule,
31
- },
32
- Platform: platform,
33
- }));
34
-
35
- describe("android connection service bridge", () => {
36
- beforeEach(() => {
37
- vi.resetModules();
38
- vi.clearAllMocks();
39
- subscriptions.clear();
40
- platform.OS = "android";
41
- });
42
-
43
- it("no-ops when Android native support is unavailable", async () => {
44
- platform.OS = "ios";
45
-
46
- const { addAndroidConnectionServiceListener, ensureAndroidConnectionServiceRegistered, endAndroidConnectionServiceCall, isAndroidConnectionServiceSupported, setAndroidConnectionServiceActive, startAndroidConnectionServiceCall } = await import("./connection-service");
47
-
48
- expect(await isAndroidConnectionServiceSupported()).toBe(false);
49
- expect(await ensureAndroidConnectionServiceRegistered()).toBe(false);
50
- expect(
51
- await startAndroidConnectionServiceCall({
52
- callId: "space-1:1",
53
- displayName: "Host",
54
- spaceId: "space-1",
55
- spaceName: "Daily Standup",
56
- }),
57
- ).toBe(false);
58
- expect(await setAndroidConnectionServiceActive("space-1:1")).toBe(false);
59
- expect(await endAndroidConnectionServiceCall("space-1:1")).toBe(false);
60
-
61
- const unsubscribe = addAndroidConnectionServiceListener(() => {
62
- throw new Error("listener should not be called on iOS");
63
- });
64
- unsubscribe();
65
-
66
- expect(nativeModule.isSupported).not.toHaveBeenCalled();
67
- expect(nativeModule.registerPhoneAccount).not.toHaveBeenCalled();
68
- expect(nativeModule.startCall).not.toHaveBeenCalled();
69
- expect(nativeModule.setActive).not.toHaveBeenCalled();
70
- expect(nativeModule.endCall).not.toHaveBeenCalled();
71
- });
72
-
73
- it("passes through Android native module calls and emits disconnect events", async () => {
74
- const { addAndroidConnectionServiceListener, ensureAndroidConnectionServiceRegistered, endAndroidConnectionServiceCall, isAndroidConnectionServiceSupported, setAndroidConnectionServiceActive, startAndroidConnectionServiceCall } = await import("./connection-service");
75
-
76
- expect(await isAndroidConnectionServiceSupported()).toBe(true);
77
- expect(await ensureAndroidConnectionServiceRegistered()).toBe(true);
78
- expect(
79
- await startAndroidConnectionServiceCall({
80
- callId: "space-1:2",
81
- displayName: "Guest",
82
- hasVideo: false,
83
- spaceId: "space-1",
84
- spaceName: "Board Review",
85
- }),
86
- ).toBe(true);
87
- expect(await setAndroidConnectionServiceActive("space-1:2")).toBe(true);
88
- expect(await endAndroidConnectionServiceCall("space-1:2", { reason: "remote", label: "Space ended" })).toBe(true);
89
-
90
- const listener = vi.fn();
91
- const unsubscribe = addAndroidConnectionServiceListener(listener);
92
- const disconnectPayload = {
93
- callId: "space-1:2",
94
- reason: "local",
95
- type: "disconnect",
96
- };
97
-
98
- subscriptions.get("ChalkAndroidConnectionServiceEvent")?.(disconnectPayload);
99
- unsubscribe();
100
-
101
- expect(nativeModule.isSupported).toHaveBeenCalledTimes(1);
102
- expect(nativeModule.registerPhoneAccount).toHaveBeenCalledTimes(1);
103
- expect(nativeModule.startCall).toHaveBeenCalledWith({
104
- callId: "space-1:2",
105
- displayName: "Guest",
106
- hasVideo: false,
107
- spaceId: "space-1",
108
- spaceName: "Board Review",
109
- });
110
- expect(nativeModule.setActive).toHaveBeenCalledWith("space-1:2");
111
- expect(nativeModule.endCall).toHaveBeenCalledWith("space-1:2", "remote", "Space ended");
112
- expect(listener).toHaveBeenCalledWith(disconnectPayload);
113
- });
114
- });
@@ -1,68 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { resolveSpaceCallKitOptions } from "./resolve-space-callkit-options";
3
-
4
- describe("resolveSpaceCallKitOptions", () => {
5
- it("returns null when CallKit is disabled", () => {
6
- expect(
7
- resolveSpaceCallKitOptions({
8
- callKit: false,
9
- hasVideo: true,
10
- spaceId: "space-123",
11
- spaceName: "Design Review",
12
- }),
13
- ).toBeNull();
14
- });
15
-
16
- it("fills sensible defaults for enabled Spaces", () => {
17
- expect(
18
- resolveSpaceCallKitOptions({
19
- callKit: true,
20
- hasVideo: true,
21
- spaceId: "space-123",
22
- spaceName: "Design Review",
23
- }),
24
- ).toEqual({
25
- appName: "Chalk",
26
- displayName: "Design Review",
27
- handle: "space-123",
28
- handleType: "generic",
29
- hasVideo: true,
30
- includesCallsInRecents: false,
31
- maximumCallGroups: 1,
32
- maximumCallsPerCallGroup: 1,
33
- iconTemplateImageName: undefined,
34
- ringtoneSound: undefined,
35
- });
36
- });
37
-
38
- it("prefers explicit overrides from the caller", () => {
39
- expect(
40
- resolveSpaceCallKitOptions({
41
- callKit: {
42
- appName: "Chalk Meet",
43
- displayName: "Board Space",
44
- handle: "board-space",
45
- handleType: "emailAddress",
46
- hasVideo: false,
47
- includesCallsInRecents: true,
48
- maximumCallGroups: 2,
49
- maximumCallsPerCallGroup: 3,
50
- ringtoneSound: "ring.caf",
51
- },
52
- hasVideo: true,
53
- spaceId: "space-123",
54
- }),
55
- ).toEqual({
56
- appName: "Chalk Meet",
57
- displayName: "Board Space",
58
- handle: "board-space",
59
- handleType: "emailAddress",
60
- hasVideo: false,
61
- includesCallsInRecents: true,
62
- maximumCallGroups: 2,
63
- maximumCallsPerCallGroup: 3,
64
- iconTemplateImageName: undefined,
65
- ringtoneSound: "ring.caf",
66
- });
67
- });
68
- });
@@ -1,12 +0,0 @@
1
- import type { GetAccess, SpaceClient } from "@q9labsai/chalk-client";
2
- import { expectTypeOf, test } from "vitest";
3
-
4
- import type { NativeSpaceClientOptions } from "./client";
5
-
6
- test("the native client subpath exposes the advanced-construction contract", () => {
7
- expectTypeOf<NativeSpaceClientOptions>().toMatchTypeOf<{
8
- readonly space: string;
9
- readonly getAccess: GetAccess;
10
- }>();
11
- expectTypeOf<typeof import("./client").createNativeSpaceClient>().returns.toEqualTypeOf<SpaceClient>();
12
- });
@@ -1,79 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { createClipboardTextStore } from "./clipboard-store";
3
-
4
- vi.mock("react-native", () => ({
5
- AppState: { addEventListener: vi.fn(() => ({ remove: vi.fn() })) },
6
- NativeModules: { ChalkRuntimeInfo: { isSimulator: false } },
7
- Platform: { OS: "ios" },
8
- }));
9
-
10
- describe("clipboard invite suggestion hook", () => {
11
- it("exposes the extracted clipboard suggestion hook", async () => {
12
- const { useClipboardInviteSuggestion } = await import("./clipboard");
13
-
14
- expect(typeof useClipboardInviteSuggestion).toBe("function");
15
- });
16
- });
17
-
18
- describe("createClipboardTextStore", () => {
19
- it("refreshes on subscription and active app-state events, then stops after unsubscribe", async () => {
20
- let appStateListener: ((nextState: string) => void) | undefined;
21
- const removeAppStateListener = vi.fn();
22
- const clipboard = {
23
- hasStringAsync: vi.fn().mockResolvedValue(true),
24
- getStringAsync: vi.fn().mockResolvedValue("chalk invite"),
25
- };
26
- const store = createClipboardTextStore({
27
- clipboard,
28
- shouldReadClipboard: true,
29
- subscribeToAppState: (listener) => {
30
- appStateListener = listener;
31
- return { remove: removeAppStateListener };
32
- },
33
- });
34
- const listener = vi.fn();
35
-
36
- const unsubscribe = store.subscribe(listener);
37
- await Promise.resolve();
38
- await Promise.resolve();
39
-
40
- expect(store.getSnapshot()).toBe("chalk invite");
41
- expect(listener).toHaveBeenCalledOnce();
42
-
43
- appStateListener?.("active");
44
- await Promise.resolve();
45
- await Promise.resolve();
46
-
47
- expect(clipboard.hasStringAsync).toHaveBeenCalledTimes(2);
48
-
49
- unsubscribe();
50
- expect(removeAppStateListener).toHaveBeenCalledOnce();
51
- appStateListener?.("active");
52
- await Promise.resolve();
53
- await Promise.resolve();
54
-
55
- expect(clipboard.hasStringAsync).toHaveBeenCalledTimes(2);
56
- });
57
-
58
- it("does not read the clipboard when the platform policy disables automatic reads", async () => {
59
- const clipboard = {
60
- hasStringAsync: vi.fn(),
61
- getStringAsync: vi.fn(),
62
- };
63
- const subscribeToAppState = vi.fn();
64
- const store = createClipboardTextStore({
65
- clipboard,
66
- shouldReadClipboard: false,
67
- subscribeToAppState,
68
- });
69
-
70
- const unsubscribe = store.subscribe(vi.fn());
71
- await Promise.resolve();
72
-
73
- expect(store.getSnapshot()).toBeNull();
74
- expect(clipboard.hasStringAsync).not.toHaveBeenCalled();
75
- expect(subscribeToAppState).not.toHaveBeenCalled();
76
-
77
- unsubscribe();
78
- });
79
- });
@@ -1,223 +0,0 @@
1
- // @vitest-environment happy-dom
2
- import type { SpaceClient, SpaceSnapshot } from "@q9labsai/chalk-client";
3
- import { createRequire } from "node:module";
4
- import { join } from "node:path";
5
- import type { ReactNode } from "react";
6
- import { afterEach, describe, expect, it, vi } from "vitest";
7
-
8
- const testingLibrary = createRequire(import.meta.url)(join(process.cwd(), "../react/node_modules", "@testing-library/react")) as {
9
- readonly act: (callback: () => void | Promise<void>) => void | Promise<void>;
10
- readonly cleanup: () => void;
11
- readonly render: (element: ReactNode) => { readonly container: HTMLElement; readonly unmount: () => void; readonly rerender: (element: ReactNode) => void };
12
- };
13
- const { act, cleanup, render } = testingLibrary;
14
-
15
- const nativeClientFactory = vi.hoisted(() => vi.fn());
16
- const spaceViewSpy = vi.hoisted(() => vi.fn(() => <div data-testid="space" />));
17
-
18
- vi.mock("react-native", () => ({
19
- StyleSheet: { create: <T,>(styles: T) => styles },
20
- View: ({ children }: { readonly children?: ReactNode }) => children,
21
- Text: ({ children }: { readonly children?: ReactNode }) => children,
22
- Pressable: ({ children }: { readonly children?: ReactNode }) => children,
23
- }));
24
- vi.mock("../space-client/create-native-space-client", () => ({ createNativeSpaceClient: nativeClientFactory }));
25
- vi.mock("./SpaceView", () => ({ SpaceView: spaceViewSpy }));
26
- vi.mock("./Entrance", () => ({ Entrance: () => <div data-testid="entrance" /> }));
27
-
28
- import { Chalk } from "./Chalk";
29
-
30
- afterEach(() => {
31
- cleanup();
32
- nativeClientFactory.mockReset();
33
- spaceViewSpy.mockClear();
34
- });
35
-
36
- describe("Chalk", () => {
37
- it("fires joined and left only for the specified connection transitions", () => {
38
- const client = createClient("joining");
39
- const onJoined = vi.fn();
40
- const onLeft = vi.fn();
41
- render(<Chalk client={client.client} entrance={false} onJoined={onJoined} onLeft={onLeft} />);
42
-
43
- act(() => client.setStatus("live"));
44
- act(() => client.setStatus("reconnecting"));
45
- act(() => client.setStatus("live"));
46
- act(() => client.setStatus("left"));
47
-
48
- expect(onJoined).toHaveBeenCalledTimes(1);
49
- expect(onLeft).toHaveBeenCalledTimes(1);
50
- });
51
-
52
- it("does not fire joined for an already-live supplied client", () => {
53
- const client = createClient("live");
54
- const onJoined = vi.fn();
55
- render(<Chalk client={client.client} entrance={false} onJoined={onJoined} />);
56
-
57
- expect(onJoined).not.toHaveBeenCalled();
58
- });
59
-
60
- it("forwards the optional diagnostics action to the live Space", () => {
61
- const client = createClient("live");
62
- const onOpenDiagnostics = vi.fn();
63
- render(<Chalk client={client.client} onOpenDiagnostics={onOpenDiagnostics} />);
64
-
65
- expect(spaceViewSpy).toHaveBeenCalledWith(expect.objectContaining({ onOpenDiagnostics }), undefined);
66
- });
67
-
68
- it("forwards canonical client events and cleans their subscriptions up", () => {
69
- const client = createClient("live");
70
- const handlers = new Map<string, (event: unknown) => void>();
71
- const unsubscribe = vi.fn();
72
- const eventClient = client.client as { on: (event: string, handler: unknown) => () => void };
73
- eventClient.on = vi.fn((event, handler) => {
74
- handlers.set(event, handler as (event: unknown) => void);
75
- return unsubscribe;
76
- });
77
- const onError = vi.fn();
78
- const view = render(<Chalk client={client.client} onEpisodeEnded={vi.fn()} onError={onError} onParticipantJoined={vi.fn()} onParticipantLeft={vi.fn()} onScreenShareStarted={vi.fn()} onScreenShareStopped={vi.fn()} />);
79
-
80
- const error = { error: { code: "client.internal_error", message: "test", recoverable: true } };
81
- handlers.get("error")?.(error);
82
- expect(onError).toHaveBeenCalledWith(error);
83
-
84
- view.unmount();
85
- expect(unsubscribe).toHaveBeenCalledTimes(6);
86
- });
87
-
88
- it("leaves an owned client before disposing it on unmount", async () => {
89
- const client = createClient("joining");
90
- let resolveLeave: (() => void) | undefined;
91
- client.client.leave = vi.fn(
92
- () =>
93
- new Promise<void>((resolve) => {
94
- resolveLeave = resolve;
95
- }),
96
- );
97
- nativeClientFactory.mockReturnValue(client.client);
98
-
99
- const view = render(<Chalk entrance={false} getAccess={vi.fn()} space="space-1" />);
100
- view.unmount();
101
-
102
- expect(client.client.leave).toHaveBeenCalledTimes(1);
103
- expect(client.client.dispose).not.toHaveBeenCalled();
104
- await act(async () => resolveLeave?.());
105
- expect(client.client.dispose).toHaveBeenCalledTimes(1);
106
- });
107
-
108
- it("keeps an owned SpaceClient when getAccess changes identity and uses the latest callback", async () => {
109
- const client = createClient("idle");
110
- const firstAccess = vi.fn(async () => ({ token: "first" }));
111
- const latestAccess = vi.fn(async () => ({ token: "latest" }));
112
- nativeClientFactory.mockReturnValue(client.client);
113
-
114
- const view = render(<Chalk entrance getAccess={firstAccess} space="space-1" />);
115
- view.rerender(<Chalk entrance getAccess={latestAccess} space="space-1" />);
116
-
117
- expect(nativeClientFactory).toHaveBeenCalledTimes(1);
118
- const access = nativeClientFactory.mock.calls[0]?.[0].getAccess as (input: unknown) => Promise<unknown>;
119
- await expect(access({})).resolves.toEqual({ token: "latest" });
120
- expect(firstAccess).not.toHaveBeenCalled();
121
- expect(latestAccess).toHaveBeenCalledTimes(1);
122
- });
123
-
124
- it("omits an absent display name when joining without Entrance", async () => {
125
- const client = createClient("idle");
126
- render(<Chalk client={client.client} entrance={false} />);
127
-
128
- await act(async () => undefined);
129
- expect(client.client.join).toHaveBeenCalledWith({ microphone: true, camera: true });
130
- });
131
-
132
- it("retries automatic join when an owned client is replaced for a new Space", async () => {
133
- const first = createClient("idle");
134
- const replacement = createClient("idle");
135
- nativeClientFactory.mockReturnValueOnce(first.client).mockReturnValueOnce(replacement.client);
136
-
137
- const view = render(<Chalk entrance={false} getAccess={vi.fn()} space="space-1" />);
138
- await act(async () => undefined);
139
- expect(first.client.join).toHaveBeenCalledWith({ microphone: true, camera: true });
140
-
141
- view.rerender(<Chalk entrance={false} getAccess={vi.fn()} space="space-2" />);
142
- await act(async () => undefined);
143
-
144
- expect(nativeClientFactory).toHaveBeenCalledTimes(2);
145
- expect(replacement.client.join).toHaveBeenCalledWith({ microphone: true, camera: true });
146
- });
147
-
148
- it("treats a replacement client's initial failure as an Entrance failure", () => {
149
- const first = createClient("live");
150
- const replacement = createClient("failed");
151
- nativeClientFactory.mockReturnValueOnce(first.client).mockReturnValueOnce(replacement.client);
152
-
153
- const view = render(<Chalk entrance getAccess={vi.fn()} space="space-1" />);
154
- view.rerender(<Chalk entrance getAccess={vi.fn()} space="space-2" />);
155
-
156
- expect(view.container.querySelector('[data-testid="entrance"]')).not.toBeNull();
157
- });
158
-
159
- it("keeps the active Space visible during reconnecting and never after left", () => {
160
- const client = createClient("live");
161
- const view = render(<Chalk client={client.client} />);
162
-
163
- expect(view.container.querySelector('[data-testid="space"]')).not.toBeNull();
164
- act(() => client.setStatus("reconnecting"));
165
- expect(view.container.querySelector('[data-testid="space"]')).not.toBeNull();
166
- act(() => client.setStatus("left"));
167
- expect(view.container.querySelector('[data-testid="space"]')).toBeNull();
168
- expect(view.container.textContent).toContain("You have left this Space.");
169
- });
170
- });
171
-
172
- function createClient(status: SpaceSnapshot["connection"]["status"]): { readonly client: SpaceClient; readonly setStatus: (nextStatus: SpaceSnapshot["connection"]["status"]) => void } {
173
- let snapshot = createSnapshot(status);
174
- const listeners = new Set<() => void>();
175
- const client = {
176
- getSnapshot: () => snapshot,
177
- subscribe: (listener: () => void) => {
178
- listeners.add(listener);
179
- return () => listeners.delete(listener);
180
- },
181
- on: vi.fn(() => () => undefined),
182
- join: vi.fn(async () => undefined),
183
- leave: vi.fn(async () => undefined),
184
- dispose: vi.fn(),
185
- endEpisode: vi.fn(async () => undefined),
186
- media: {},
187
- chat: {},
188
- participants: {},
189
- reactions: {},
190
- whiteboard: {},
191
- } as unknown as SpaceClient;
192
- return {
193
- client,
194
- setStatus(nextStatus) {
195
- snapshot = { ...snapshot, connection: { ...snapshot.connection, status: nextStatus } };
196
- listeners.forEach((listener) => listener());
197
- },
198
- };
199
- }
200
-
201
- function createSnapshot(status: SpaceSnapshot["connection"]["status"]): SpaceSnapshot {
202
- const capabilities = [] as const;
203
- return {
204
- connection: { status, episode: null, lastError: null },
205
- self: { participantId: null, displayName: null, role: null, capabilities, handRaised: false, can: () => false },
206
- participants: { roster: [], admissionQueue: [] },
207
- media: {
208
- devices: { microphones: [], cameras: [], speakers: [] },
209
- selection: { microphone: null, camera: null, speaker: null },
210
- local: {
211
- microphone: { source: "microphone", state: "disabled", track: null },
212
- camera: { source: "camera", state: "disabled", track: null },
213
- screen: { source: "screen", state: "disabled", track: null },
214
- },
215
- remote: [],
216
- screenShare: { source: "screen", state: "disabled", track: null },
217
- incomingRequests: [],
218
- },
219
- chat: { status: "idle", messages: [], pendingSends: [], readReceipts: [], unreadCount: 0, pagination: { cursor: null, hasOlder: false, historyTruncated: false }, lastError: null },
220
- reactions: { active: [] },
221
- whiteboard: { open: false, engine: { status: "unsubscribed", sceneId: null, revision: null, presenting: false, error: null } },
222
- };
223
- }
@@ -1,92 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- const nativeTheme = vi.hoisted(() => ({
4
- colorScheme: "light" as const,
5
- colors: {
6
- dangerSurfaceOverlay94: "#danger",
7
- info: "#info",
8
- lightMutedText: "#muted",
9
- lightSubtleText: "#subtle",
10
- lightSurface: "#surface",
11
- lightText: "#text",
12
- onDark: "#on-dark",
13
- },
14
- }));
15
- const controllerOptions = vi.hoisted(() => [] as unknown[]);
16
-
17
- vi.mock("react", () => ({
18
- useCallback: <T,>(callback: T) => callback,
19
- useEffect: (effect: () => void) => effect(),
20
- useMemo: <T,>(factory: () => T) => factory(),
21
- useRef: <T,>(current: T) => ({ current }),
22
- useState: <T,>(initial: T | (() => T)): readonly [T, () => void] => [typeof initial === "function" ? (initial as () => T)() : initial, () => undefined],
23
- }));
24
- vi.mock("@q9labsai/chalk-whiteboard/embedded", () => ({
25
- ChalkWhiteboardController: class {
26
- constructor(options: unknown) {
27
- controllerOptions.push(options);
28
- }
29
-
30
- rendererReloaded(): void {}
31
-
32
- setCapabilities(): void {}
33
-
34
- setViewport(): void {}
35
-
36
- start(): void {}
37
-
38
- stop(): void {}
39
- },
40
- }));
41
- vi.mock("react-native", () => ({
42
- ActivityIndicator: "ActivityIndicator",
43
- Pressable: "Pressable",
44
- StyleSheet: { create: <Styles,>(styles: Styles) => styles },
45
- Text: "Text",
46
- View: "View",
47
- useWindowDimensions: () => ({ width: 320, height: 640, scale: 2 }),
48
- }));
49
- vi.mock("react-native-webview", () => ({ default: "WebView" }));
50
- vi.mock("../ui/native-theme", () => ({ useNativeTheme: () => nativeTheme }));
51
- vi.mock("../whiteboard/embedded-whiteboard-assets", () => ({
52
- isEmbeddedWhiteboardNavigationAllowed: () => true,
53
- rendererURLWithContext: (url: string) => url,
54
- resolveEmbeddedWhiteboardRendererURL: async () => "file:///android_asset/chalk-whiteboard/index.html",
55
- }));
56
-
57
- import type { ChalkEmbeddedWhiteboardTransport } from "@q9labsai/chalk-whiteboard/embedded";
58
- import { EmbeddedWhiteboard, embeddedWhiteboardWebViewSecurityPolicy } from "./EmbeddedWhiteboard";
59
-
60
- describe("EmbeddedWhiteboard WebView policy", () => {
61
- it("keeps the offline renderer isolated from files, networks, storage, and cookies", () => {
62
- expect(embeddedWhiteboardWebViewSecurityPolicy).toEqual({
63
- allowFileAccess: true,
64
- allowFileAccessFromFileURLs: false,
65
- allowUniversalAccessFromFileURLs: false,
66
- cacheEnabled: false,
67
- domStorageEnabled: false,
68
- javaScriptCanOpenWindowsAutomatically: false,
69
- javaScriptEnabled: true,
70
- mixedContentMode: "never",
71
- setSupportMultipleWindows: false,
72
- sharedCookiesEnabled: false,
73
- thirdPartyCookiesEnabled: false,
74
- });
75
- });
76
-
77
- it("uses the active NativeTheme colors throughout its loading surface", () => {
78
- controllerOptions.length = 0;
79
- const rendered = EmbeddedWhiteboard({
80
- canClear: false,
81
- canDraw: true,
82
- journeyId: "journey-test",
83
- transport: {} as ChalkEmbeddedWhiteboardTransport,
84
- });
85
- const children = rendered.props.children as readonly [{ readonly props: { readonly color: string } }, { readonly props: { readonly style: readonly unknown[] } }];
86
-
87
- expect(rendered.props.style).toContainEqual({ backgroundColor: "#surface" });
88
- expect(children[0]?.props.color).toBe("#info");
89
- expect(children[1]?.props.style).toContainEqual({ color: "#muted" });
90
- expect(controllerOptions[0]).toMatchObject({ theme: "light" });
91
- });
92
- });