@q9labsai/chalk-react-native 4.1.12 → 4.1.14

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,22 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { formatChatAttachmentSize, isLatestChatMessageVisible } from "./native-chat";
3
-
4
- describe("native chat viewport", () => {
5
- it("reports the latest message only when the viewport reaches the content bottom", () => {
6
- expect(isLatestChatMessageVisible({ contentHeight: 600, viewportHeight: 300, scrollOffset: 120 })).toBe(false);
7
- expect(isLatestChatMessageVisible({ contentHeight: 600, viewportHeight: 300, scrollOffset: 292 })).toBe(true);
8
- });
9
-
10
- it("does not treat unmeasured content as visible", () => {
11
- expect(isLatestChatMessageVisible({ contentHeight: 0, viewportHeight: 300, scrollOffset: 0 })).toBe(false);
12
- expect(isLatestChatMessageVisible({ contentHeight: 300, viewportHeight: 0, scrollOffset: 0 })).toBe(false);
13
- });
14
- });
15
-
16
- describe("native chat attachment metadata", () => {
17
- it("formats byte lengths for compact attachment rows", () => {
18
- expect(formatChatAttachmentSize(512)).toBe("512 B");
19
- expect(formatChatAttachmentSize(1_536)).toBe("1.5 KB");
20
- expect(formatChatAttachmentSize(2_097_152)).toBe("2.0 MB");
21
- });
22
- });
@@ -1,75 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { EntranceControllerStore } from "./entrance-controller-store";
3
-
4
- function createStore(options: Partial<ConstructorParameters<typeof EntranceControllerStore>[0]> = {}) {
5
- return new EntranceControllerStore({
6
- displayName: "Guest",
7
- initialAudioEnabled: false,
8
- initialVideoEnabled: false,
9
- simulatorMediaDisabled: false,
10
- joinDisabled: false,
11
- onJoin: vi.fn(),
12
- ...options,
13
- });
14
- }
15
-
16
- describe("EntranceControllerStore", () => {
17
- it("toggles media, edits the name, and latches join", () => {
18
- const onJoin = vi.fn();
19
- const store = createStore({ onJoin });
20
- const listener = vi.fn();
21
- const unsubscribe = store.subscribe(listener);
22
-
23
- store.setDisplayName("Host");
24
- store.toggleAudio();
25
- store.toggleVideo();
26
- store.setInputFocused(true);
27
- store.handleJoin();
28
- store.handleJoin();
29
-
30
- expect(store.getSnapshot()).toMatchObject({
31
- displayName: "Host",
32
- audioEnabled: true,
33
- videoEnabled: true,
34
- isInputFocused: true,
35
- isSubmitting: true,
36
- });
37
- expect(onJoin).toHaveBeenCalledTimes(1);
38
- expect(onJoin).toHaveBeenCalledWith({ displayName: "Host", microphoneEnabled: true, cameraEnabled: true });
39
- expect(listener).toHaveBeenCalled();
40
-
41
- unsubscribe();
42
- });
43
-
44
- it("resets the submit latch when joining is re-enabled", () => {
45
- const onJoin = vi.fn();
46
- const store = createStore({ joinDisabled: true, onJoin });
47
-
48
- store.handleJoin();
49
- expect(onJoin).not.toHaveBeenCalled();
50
-
51
- store.update({ simulatorMediaDisabled: false, joinDisabled: false, onJoin });
52
- store.handleJoin();
53
- expect(onJoin).toHaveBeenCalledTimes(1);
54
- });
55
-
56
- it("does not submit a blank display name", () => {
57
- const onJoin = vi.fn();
58
- const store = createStore({ displayName: " ", onJoin });
59
-
60
- store.handleJoin();
61
-
62
- expect(onJoin).not.toHaveBeenCalled();
63
- expect(store.getSnapshot().isSubmitting).toBe(false);
64
- });
65
-
66
- it("disables media and ignores toggles on the iOS simulator", () => {
67
- const store = createStore({ initialAudioEnabled: true, initialVideoEnabled: true });
68
-
69
- store.update({ simulatorMediaDisabled: true, joinDisabled: false, onJoin: vi.fn() });
70
- store.toggleAudio();
71
- store.toggleVideo();
72
-
73
- expect(store.getSnapshot()).toMatchObject({ audioEnabled: false, videoEnabled: false });
74
- });
75
- });
@@ -1,77 +0,0 @@
1
- import type { Participant } from "../internal/core";
2
- import { describe, expect, it } from "vitest";
3
- import { shouldRenderNativeMediaTrack } from "./native-media-visibility";
4
-
5
- function participant(overrides: Partial<Participant> = {}): Participant {
6
- return {
7
- id: overrides.id ?? "participant-1",
8
- displayName: overrides.displayName ?? "Participant",
9
- role: overrides.role ?? "participant",
10
- isLocal: overrides.isLocal ?? false,
11
- videoEnabled: overrides.videoEnabled ?? false,
12
- audioEnabled: overrides.audioEnabled ?? true,
13
- isScreenSharing: overrides.isScreenSharing ?? false,
14
- isSpeaking: overrides.isSpeaking ?? false,
15
- handRaised: overrides.handRaised ?? false,
16
- connectionQuality: overrides.connectionQuality ?? 100,
17
- videoTrack: overrides.videoTrack ?? null,
18
- audioTrack: overrides.audioTrack ?? null,
19
- screenShareTrack: overrides.screenShareTrack ?? null,
20
- screenShareAudioTrack: overrides.screenShareAudioTrack ?? null,
21
- joinedAt: overrides.joinedAt ?? new Date(),
22
- metadata: overrides.metadata ?? {},
23
- };
24
- }
25
-
26
- function track(readyState: "live" | "ended" = "live"): MediaStreamTrack {
27
- return { readyState } as MediaStreamTrack;
28
- }
29
-
30
- describe("shouldRenderNativeMediaTrack", () => {
31
- it("hides video when the participant has turned their camera off even if a stale track object remains", () => {
32
- expect(
33
- shouldRenderNativeMediaTrack({
34
- participant: participant({
35
- videoEnabled: false,
36
- videoTrack: track(),
37
- }),
38
- track: track(),
39
- }),
40
- ).toBe(false);
41
- });
42
-
43
- it("hides video when the track has already ended", () => {
44
- expect(
45
- shouldRenderNativeMediaTrack({
46
- participant: participant({
47
- videoEnabled: true,
48
- videoTrack: track("ended"),
49
- }),
50
- track: track("ended"),
51
- }),
52
- ).toBe(false);
53
- });
54
-
55
- it("still renders live non-participant media such as stage screen share tracks", () => {
56
- expect(
57
- shouldRenderNativeMediaTrack({
58
- participant: null,
59
- track: track(),
60
- }),
61
- ).toBe(true);
62
- });
63
-
64
- it("renders a live screen-share track even when the sharer camera is off", () => {
65
- expect(
66
- shouldRenderNativeMediaTrack({
67
- participant: participant({
68
- videoEnabled: false,
69
- isScreenSharing: true,
70
- screenShareTrack: track(),
71
- }),
72
- track: track(),
73
- mediaKind: "screen-share",
74
- }),
75
- ).toBe(true);
76
- });
77
- });
@@ -1,62 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { SpaceCallKitController, type SpaceCallKitPort, type SpaceCallKitSyncInput } from "./native-space-callkit-controller";
3
-
4
- vi.mock("react-native", () => ({
5
- NativeModules: {},
6
- Platform: { OS: "ios" },
7
- }));
8
-
9
- function createInput(overrides: Partial<SpaceCallKitSyncInput> = {}): SpaceCallKitSyncInput {
10
- return {
11
- callKit: true,
12
- hasVideo: true,
13
- isAudioEnabled: true,
14
- joinNonce: 1,
15
- onEndCall: vi.fn(),
16
- onToggleAudio: vi.fn(async () => false),
17
- phase: "joining",
18
- spaceId: "space-1",
19
- spaceName: "Daily Standup",
20
- ...overrides,
21
- };
22
- }
23
-
24
- describe("SpaceCallKitController", () => {
25
- it("configures, starts, reports, handles mute actions, and ends the native call", async () => {
26
- let eventListener: Parameters<SpaceCallKitPort["addListener"]>[0] | undefined;
27
- const removeListener = vi.fn();
28
- const port: SpaceCallKitPort = {
29
- isSupported: true,
30
- addListener: vi.fn((listener) => {
31
- eventListener = listener;
32
- return { remove: removeListener };
33
- }),
34
- configure: vi.fn(async () => ({ isSupported: true })),
35
- endCall: vi.fn(async () => {}),
36
- reportConnected: vi.fn(async () => {}),
37
- startCall: vi.fn(async () => ({ callUUID: "call-1" })),
38
- };
39
- const controller = new SpaceCallKitController(port);
40
- const input = createInput();
41
-
42
- controller.start();
43
- controller.sync(input);
44
- await Promise.resolve();
45
- expect(port.configure).toHaveBeenCalledOnce();
46
- expect(port.startCall).toHaveBeenCalledOnce();
47
-
48
- controller.sync({ ...input, phase: "live" });
49
- await Promise.resolve();
50
- expect(port.reportConnected).toHaveBeenCalledWith({ callUUID: "call-1" });
51
-
52
- eventListener?.({ callUUID: "call-1", muted: true, type: "setMutedCallAction" });
53
- expect(input.onToggleAudio).toHaveBeenCalledOnce();
54
-
55
- controller.sync({ ...input, phase: "entrance" });
56
- await Promise.resolve();
57
- expect(port.endCall).toHaveBeenCalledWith({ callUUID: "call-1" });
58
-
59
- controller.stop();
60
- expect(removeListener).toHaveBeenCalledOnce();
61
- });
62
- });
@@ -1,56 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, expect, it, vi } from "vitest";
3
-
4
- const appearanceState = vi.hoisted(() => ({
5
- theme: { colors: { ink: "#light-ink", ink2: "#light-muted", ink3: "#light-subtle", line: "#light-line", primary: "#light-primary", surfaceMuted: "#light-surface", success: "#light-success" } },
6
- appearance: { palette: "light", texture: "none", tokens: { stage: "#light-stage", textMuted: "#light-muted" } },
7
- setPalette: vi.fn(),
8
- setTexture: vi.fn(),
9
- }));
10
-
11
- vi.mock("react-native", () => ({ Pressable: "Pressable", StyleSheet: { create: <T,>(styles: T) => styles }, Text: "Text", View: "View" }));
12
- vi.mock("@hugeicons/react-native", () => ({ HugeiconsIcon: "HugeiconsIcon" }));
13
- vi.mock("@hugeicons/core-free-icons/dist/esm/CheckmarkCircle01Icon", () => ({ default: "CheckmarkCircle01Icon" }), { virtual: true });
14
- vi.mock("../../ui/native-appearance-context", () => ({ useNativeAppearance: () => appearanceState }));
15
- vi.mock("../../ui/native-theme", () => ({ useNativeTheme: () => appearanceState.theme }));
16
-
17
- import { AppearanceSettings } from "./AppearanceSettings";
18
-
19
- const source = readFileSync(new URL("./AppearanceSettings.tsx", import.meta.url), "utf8");
20
-
21
- describe("AppearanceSettings", () => {
22
- it("exposes every palette family and texture as an accessible selection", () => {
23
- expect(source).toContain('(["light", "dark"] as const)');
24
- expect(source).toContain("THEME_PALETTES.filter");
25
- expect(source).toContain("THEME_TEXTURES.map");
26
- expect(source).toContain("accessibilityState={{ selected }}");
27
- expect(source).toContain("setPalette");
28
- expect(source).toContain("setTexture");
29
- });
30
-
31
- it("renders palette controls with the active light and dark NativeTheme", () => {
32
- appearanceState.theme.colors = { ...appearanceState.theme.colors, ink2: "#light-muted", line: "#light-line", surfaceMuted: "#light-surface" };
33
- const lightTree = AppearanceSettings();
34
- expect(resolveStyle(findByAccessibilityLabel(lightTree, "Use Chalk Light palette")?.style)).toEqual(expect.arrayContaining([expect.objectContaining({ backgroundColor: "#light-surface", borderColor: "#light-line" })]));
35
-
36
- appearanceState.theme.colors = { ...appearanceState.theme.colors, ink2: "#dark-muted", line: "#dark-line", surfaceMuted: "#dark-surface" };
37
- const darkTree = AppearanceSettings();
38
- expect(resolveStyle(findByAccessibilityLabel(darkTree, "Use Chalk Light palette")?.style)).toEqual(expect.arrayContaining([expect.objectContaining({ backgroundColor: "#dark-surface", borderColor: "#dark-line" })]));
39
- });
40
- });
41
-
42
- function resolveStyle(style: unknown): unknown {
43
- return typeof style === "function" ? (style as (state: { readonly pressed: boolean }) => unknown)({ pressed: false }) : style;
44
- }
45
-
46
- function findByAccessibilityLabel(value: unknown, label: string): { readonly style?: unknown } | undefined {
47
- if (Array.isArray(value)) return value.map((child) => findByAccessibilityLabel(child, label)).find(Boolean);
48
- if (!value || typeof value !== "object") return undefined;
49
- const element = value as { readonly props?: { readonly accessibilityLabel?: unknown; readonly children?: unknown; readonly style?: unknown } };
50
- if (typeof (element as { readonly type?: unknown }).type === "function") {
51
- const component = (element as { readonly type: (props: Record<string, unknown>) => unknown }).type;
52
- return findByAccessibilityLabel(component((element.props ?? {}) as Record<string, unknown>), label);
53
- }
54
- if (element.props?.accessibilityLabel === label) return element.props;
55
- return findByAccessibilityLabel(element.props?.children, label);
56
- }
@@ -1,18 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, expect, it } from "vitest";
3
-
4
- const source = readFileSync(new URL("./ParticipantActionSheet.tsx", import.meta.url), "utf8");
5
-
6
- describe("ParticipantActionSheet", () => {
7
- it("keeps capability-derived actions and explicit destructive styling", () => {
8
- expect(source).toContain("buildParticipantActionDescriptors({");
9
- expect(source).toContain("descriptor.destructive");
10
- expect(source).toContain('accessibilityLabel="Close Participant actions"');
11
- expect(source).toContain("StyleSheet.absoluteFillObject");
12
- expect(source).not.toContain("setParticipantRole");
13
- expect(source).not.toContain("AssignableParticipantRole");
14
- expect(source).not.toContain("role:");
15
- expect(source).not.toContain("paddingBottom: 112");
16
- expect(source).not.toContain("bottom: 94");
17
- });
18
- });
@@ -1,17 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, expect, it } from "vitest";
3
-
4
- const source = readFileSync(new URL("./SettingsSheet.tsx", import.meta.url), "utf8");
5
-
6
- describe("SettingsSheet", () => {
7
- it("exposes accessible tabs and real device toggles", () => {
8
- expect(source).toContain('accessibilityRole="tablist"');
9
- expect(source).toContain('["devices", "appearance", "space"]');
10
- expect(source).toContain("controller.toggleAudio");
11
- expect(source).toContain("controller.toggleVideo");
12
- expect(source).toContain("<AppearanceSettings />");
13
- expect(source).toContain("StyleSheet.absoluteFillObject");
14
- expect(source).not.toContain("marginBottom: 94");
15
- expect(source).not.toContain("bottom: 94");
16
- });
17
- });
@@ -1,126 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, expect, it, vi } from "vitest";
3
-
4
- vi.mock("react-native", () => ({
5
- Alert: { alert: vi.fn() },
6
- FlatList: "FlatList",
7
- Modal: "Modal",
8
- Pressable: "Pressable",
9
- ScrollView: "ScrollView",
10
- StyleSheet: { create: <T,>(styles: T) => styles },
11
- Text: "Text",
12
- TextInput: "TextInput",
13
- View: "View",
14
- }));
15
- vi.mock("../../ui/native-theme", () => ({
16
- useNativeTheme: () => ({ colors: { background: "#0a0a0b", border: "#1c1c1f", card: "#141418", error: "#ef4444", foreground: "#fbffff", mutedForeground: "#71717a", placeholder: "#71717a", primary: "#1bb6a6", primaryForeground: "#ffffff" } }),
17
- }));
18
-
19
- import { selectSpaceReaction, SpaceActionMenu } from "./SpaceActionMenu";
20
-
21
- const source = readFileSync(new URL("./SpaceActionMenu.tsx", import.meta.url), "utf8");
22
-
23
- describe("SpaceActionMenu", () => {
24
- it("shows only capability-backed actions and closes after a layout selection", () => {
25
- const setActionsOpen = vi.fn();
26
- const setLayout = vi.fn();
27
- const controller = actionController({
28
- canInvite: true,
29
- canParticipants: false,
30
- canChat: true,
31
- canScreenShare: false,
32
- canHandRaise: true,
33
- canWhiteboard: false,
34
- canReactions: false,
35
- canSettings: true,
36
- setActionsOpen,
37
- layout: { layout: "grid", setLayout },
38
- });
39
-
40
- const tree = SpaceActionMenu({ controller });
41
- const labels = findText(tree);
42
-
43
- expect(labels).toEqual(expect.arrayContaining(["Invite participants", "Chat", "Raise hand", "Settings", "Grid layout (selected)", "Focus layout", "Presentation layout"]));
44
- expect(labels).not.toContain("Participants");
45
- expect(labels).not.toContain("Open whiteboard");
46
-
47
- const presentation = findElements(tree, "Pressable").find((element) => findText(element).length === 1 && findText(element).includes("Presentation layout"));
48
- const onPress = presentation?.props?.onPress;
49
- if (typeof onPress === "function") onPress();
50
- expect(setLayout).toHaveBeenCalledWith("presentation");
51
- expect(setActionsOpen).toHaveBeenCalledWith(false);
52
- });
53
-
54
- it("routes reaction selection through the controller", () => {
55
- const sendReaction = vi.fn();
56
-
57
- selectSpaceReaction({ sendReaction } as never, "🎉");
58
-
59
- expect(sendReaction).toHaveBeenCalledWith("🎉");
60
- });
61
-
62
- it("shows diagnostics only when its open handler is provided", () => {
63
- const setActionsOpen = vi.fn();
64
- const onOpenDiagnostics = vi.fn();
65
- const controller = actionController({ setActionsOpen });
66
-
67
- expect(findText(SpaceActionMenu({ controller }))).not.toContain("Diagnostics");
68
-
69
- const tree = SpaceActionMenu({ controller, onOpenDiagnostics });
70
- const diagnostics = findElements(tree, "Pressable").find((element) => findText(element).length === 1 && findText(element).includes("Diagnostics"));
71
- const onPress = diagnostics?.props?.onPress;
72
- if (typeof onPress === "function") onPress();
73
-
74
- expect(setActionsOpen).toHaveBeenCalledWith(false);
75
- expect(onOpenDiagnostics).toHaveBeenCalledTimes(1);
76
- });
77
-
78
- it("leaves chat and settings panel ownership to canonical mounted sheets", () => {
79
- expect(source).not.toContain("ChatPanel");
80
- expect(source).not.toContain("<SpacePanel");
81
- });
82
-
83
- it("does not invent a role catalog for customer-defined participant roles", () => {
84
- expect(source).not.toContain("Set collaborator role");
85
- expect(source).not.toContain("Set observer role");
86
- expect(source).not.toContain("setParticipantRole");
87
- });
88
- });
89
-
90
- function actionController(overrides: Record<string, unknown>): never {
91
- return {
92
- actionsOpen: true,
93
- canHandRaise: false,
94
- canInvite: false,
95
- canParticipants: false,
96
- canChat: false,
97
- canScreenShare: false,
98
- canWhiteboard: false,
99
- canReactions: false,
100
- canSettings: false,
101
- handRaised: false,
102
- screenShare: { isLocalSharing: false },
103
- whiteboard: { isOpen: false, toggle: vi.fn() },
104
- layout: { layout: "grid", setLayout: vi.fn() },
105
- handleLeave: vi.fn(),
106
- openPanel: vi.fn(),
107
- setActionsOpen: vi.fn(),
108
- setReactionPickerOpen: vi.fn(),
109
- ...overrides,
110
- } as never;
111
- }
112
-
113
- function findElements(value: unknown, type: string): { readonly props?: Record<string, unknown> }[] {
114
- if (Array.isArray(value)) return value.flatMap((child) => findElements(child, type));
115
- if (!value || typeof value !== "object") return [];
116
- const element = value as { readonly type?: unknown; readonly props?: Record<string, unknown> };
117
- const own = element.type === type ? [element] : [];
118
- return [...own, ...findElements(element.props?.children, type)];
119
- }
120
-
121
- function findText(value: unknown): string[] {
122
- if (typeof value === "string") return [value];
123
- if (Array.isArray(value)) return value.flatMap(findText);
124
- if (!value || typeof value !== "object") return [];
125
- return findText((value as { readonly props?: { readonly children?: unknown } }).props?.children);
126
- }
@@ -1,61 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- vi.mock("@hugeicons/core-free-icons/dist/esm/CallEnd01Icon", () => ({ default: "CallEnd01Icon" }));
4
- vi.mock("@hugeicons/core-free-icons/dist/esm/Chat01Icon", () => ({ default: "Chat01Icon" }));
5
- vi.mock("@hugeicons/core-free-icons/dist/esm/Mic01Icon", () => ({ default: "Mic01Icon" }));
6
- vi.mock("@hugeicons/core-free-icons/dist/esm/MicOff01Icon", () => ({ default: "MicOff01Icon" }));
7
- vi.mock("@hugeicons/core-free-icons/dist/esm/MoreHorizontalIcon", () => ({ default: "MoreHorizontalIcon" }));
8
- vi.mock("@hugeicons/core-free-icons/dist/esm/Video01Icon", () => ({ default: "Video01Icon" }));
9
- vi.mock("@hugeicons/core-free-icons/dist/esm/VideoOffIcon", () => ({ default: "VideoOffIcon" }));
10
- vi.mock("@hugeicons/react-native", () => ({ HugeiconsIcon: "HugeiconsIcon" }));
11
- vi.mock("react-native", () => ({
12
- Platform: { OS: "android" },
13
- Pressable: "Pressable",
14
- StyleSheet: { create: <T,>(styles: T) => styles },
15
- Text: "Text",
16
- View: "View",
17
- }));
18
- vi.mock("../../ui/native-theme", () => ({
19
- useNativeTheme: () => ({ colors: { controlsBackground: "#141418", darkCanvas: "#0a0a0b", error: "#ef4444", onDark: "#fbffff", primary: "#1bb6a6", primaryForeground: "#ffffff", border: "#1c1c1f" } }),
20
- }));
21
-
22
- import { SpaceBottomDockAndroid } from "./SpaceBottomDock.android";
23
-
24
- describe("SpaceBottomDockAndroid", () => {
25
- it("disables both media controls on unsupported simulator media and caps unread badges", () => {
26
- const onToggleAudio = vi.fn();
27
- const onToggleVideo = vi.fn();
28
- const tree = SpaceBottomDockAndroid({
29
- simulatorMediaDisabled: true,
30
- isMuted: false,
31
- isCameraOff: true,
32
- unreadChatCount: 12,
33
- onToggleAudio,
34
- onToggleVideo,
35
- onOpenChat: vi.fn(),
36
- onOpenMore: vi.fn(),
37
- onLeave: vi.fn(),
38
- });
39
-
40
- const buttons = findElements(tree, "Pressable");
41
- expect(buttons).toHaveLength(5);
42
- expect(buttons[0]).toMatchObject({ props: { disabled: true, onPress: onToggleAudio } });
43
- expect(buttons[1]).toMatchObject({ props: { disabled: true, onPress: onToggleVideo } });
44
- expect(findText(tree)).toContain("9+");
45
- });
46
- });
47
-
48
- function findElements(value: unknown, type: string): { readonly props?: Record<string, unknown> }[] {
49
- if (Array.isArray(value)) return value.flatMap((child) => findElements(child, type));
50
- if (!value || typeof value !== "object") return [];
51
- const element = value as { readonly type?: unknown; readonly props?: Record<string, unknown> };
52
- const own = element.type === type ? [element] : [];
53
- return [...own, ...findElements(element.props?.children, type)];
54
- }
55
-
56
- function findText(value: unknown): string[] {
57
- if (typeof value === "string") return [value];
58
- if (Array.isArray(value)) return value.flatMap(findText);
59
- if (!value || typeof value !== "object") return [];
60
- return findText((value as { readonly props?: { readonly children?: unknown } }).props?.children);
61
- }
@@ -1,18 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, expect, it } from "vitest";
3
-
4
- const source = readFileSync(new URL("./SpaceChatSheet.tsx", import.meta.url), "utf8");
5
-
6
- describe("SpaceChatSheet", () => {
7
- it("keeps attachment, retry, and accessible composition flows", () => {
8
- expect(source).toContain('accessibilityLabel="Attach file"');
9
- expect(source).toContain("controller.chatAttachments.map");
10
- expect(source).toContain("controller.removeChatAttachment(index)");
11
- expect(source).toContain("chatAttachmentUploading");
12
- expect(source).toContain("chatAttachmentError");
13
- expect(source).toContain("!controller.chatDraft.trim() && controller.chatAttachments.length === 0");
14
- expect(source).toContain('accessibilityLabel="Send message"');
15
- expect(source).toContain("retryMessage(message.clientMessageId)");
16
- expect(source).toContain('accessibilityRole="link"');
17
- });
18
- });
@@ -1,15 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, expect, it } from "vitest";
3
-
4
- const source = readFileSync(new URL("./SpaceMoreSheet.tsx", import.meta.url), "utf8");
5
-
6
- describe("SpaceMoreSheet", () => {
7
- it("progressively discloses Board, People, Chat, Settings, and Leave", () => {
8
- for (const label of ['label: controller.whiteboard.isOpen ? "Close Board" : "Board"', 'label: "People"', 'label: "Chat"', 'label: "Settings"', ">Leave<"]) {
9
- expect(source).toContain(label);
10
- }
11
- expect(source).toContain("StyleSheet.absoluteFillObject");
12
- expect(source).not.toContain("marginBottom: 94");
13
- expect(source).not.toContain("bottom: 94");
14
- });
15
- });
@@ -1,15 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, expect, it } from "vitest";
3
-
4
- const source = readFileSync(new URL("./SpacePanelSheet.tsx", import.meta.url), "utf8");
5
-
6
- describe("SpacePanelSheet", () => {
7
- it("switches between Chat and People while covering the dock", () => {
8
- expect(source).toContain('controller.panel === "chat"');
9
- expect(source).toContain("<SpaceChatSheet");
10
- expect(source).toContain("<SpacePeopleSheet");
11
- expect(source).toContain("StyleSheet.absoluteFillObject");
12
- expect(source).not.toContain("marginBottom: 94");
13
- expect(source).not.toContain("bottom: 94");
14
- });
15
- });
@@ -1,13 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, expect, it } from "vitest";
3
-
4
- const source = readFileSync(new URL("./SpacePeopleSheet.tsx", import.meta.url), "utf8");
5
-
6
- describe("SpacePeopleSheet", () => {
7
- it("keeps Entrance admission and Participant action controls", () => {
8
- expect(source).toContain(">At the Entrance<");
9
- expect(source).toContain("controller.admitParticipant");
10
- expect(source).toContain("controller.denyAdmission");
11
- expect(source).toContain("accessibilityLabel={`Actions for ${participant.displayName}`}");
12
- });
13
- });
@@ -1,12 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
- import { describe, expect, it } from "vitest";
3
-
4
- const source = readFileSync(new URL("./SpaceSurfacePrimitives.tsx", import.meta.url), "utf8");
5
-
6
- describe("Space surface primitives", () => {
7
- it("keeps accessible close targets and flat deterministic avatars", () => {
8
- expect(source).toContain('accessibilityRole="button"');
9
- expect(source).toContain("getParticipantInitials(name)");
10
- expect(source).toContain("getParticipantColor(name)");
11
- });
12
- });
@@ -1,50 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- vi.mock("@hugeicons/core-free-icons/dist/esm/UserGroupIcon", () => ({ default: "UserGroupIcon" }));
4
- vi.mock("@hugeicons/react-native", () => ({ HugeiconsIcon: "HugeiconsIcon" }));
5
- vi.mock("../logo/Logo", () => ({ Logo: "Logo" }));
6
- vi.mock("react-native", () => ({
7
- Image: "Image",
8
- Platform: { OS: "android" },
9
- StyleSheet: { create: <T,>(styles: T) => styles },
10
- Text: "Text",
11
- View: "View",
12
- }));
13
- vi.mock("../../ui/native-theme", () => ({
14
- useNativeTheme: () => ({ colors: { darkCanvas: "#0a0a0b", border: "#1c1c1f", foreground: "#fbffff", mutedForeground: "#71717a", success: "#22c55e", surface: "#141418" } }),
15
- }));
16
-
17
- import { SpaceTopBarAndroid } from "./SpaceTopBar.android";
18
-
19
- describe("SpaceTopBarAndroid", () => {
20
- it("keeps the space identity, elapsed duration, and participant count visible", () => {
21
- const tree = SpaceTopBarAndroid({ spaceName: "Design space", participantCount: 4, formattedDuration: "1:02", logoUrl: "https://cdn.example.test/logo.png" });
22
-
23
- const images = findElements(tree, "Image");
24
- expect(images).toHaveLength(1);
25
- expect(images[0]).toMatchObject({ props: { accessibilityLabel: "Chalk", source: { uri: "https://cdn.example.test/logo.png" } } });
26
- expect(findText(tree)).toEqual(expect.arrayContaining(["Design space", "1:02", "4"]));
27
- });
28
-
29
- it("uses the SDK mark when no custom logo URL is supplied", () => {
30
- const tree = SpaceTopBarAndroid({ spaceName: "Design space", participantCount: 4, formattedDuration: "1:02" });
31
-
32
- expect(findElements(tree, "Logo")).toMatchObject([{ props: { height: 24, variant: "mark" } }]);
33
- });
34
- });
35
-
36
- function findElements(value: unknown, type: string): { readonly props?: Record<string, unknown> }[] {
37
- if (Array.isArray(value)) return value.flatMap((child) => findElements(child, type));
38
- if (!value || typeof value !== "object") return [];
39
- const element = value as { readonly type?: unknown; readonly props?: Record<string, unknown> };
40
- const own = element.type === type ? [element] : [];
41
- return [...own, ...findElements(element.props?.children, type)];
42
- }
43
-
44
- function findText(value: unknown): string[] {
45
- if (typeof value === "string") return [value];
46
- if (typeof value === "number") return [String(value)];
47
- if (Array.isArray(value)) return value.flatMap(findText);
48
- if (!value || typeof value !== "object") return [];
49
- return findText((value as { readonly props?: { readonly children?: unknown } }).props?.children);
50
- }