2026-09-08 15:05:01 +02:00
|
|
|
/*
|
|
|
|
|
Copyright 2026 Element Creations Ltd.
|
|
|
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
|
|
|
Please see LICENSE in the repository root for full details.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { renderHook } from "@testing-library/react";
|
|
|
|
|
import { createRef } from "react";
|
|
|
|
|
import { describe, expect, test, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
type ElementCallHandle,
|
|
|
|
|
type ElementCallHostBridge,
|
|
|
|
|
useComponentHostBridge,
|
|
|
|
|
} from "./host";
|
|
|
|
|
|
|
|
|
|
describe("useComponentHostBridge", () => {
|
|
|
|
|
test("keeps one identity while the host supplies new objects", () => {
|
|
|
|
|
const { result, rerender } = renderHook(
|
|
|
|
|
({ supplied }: { supplied: ElementCallHostBridge }) =>
|
2026-09-08 17:02:24 +02:00
|
|
|
useComponentHostBridge(supplied, undefined, undefined),
|
2026-09-08 15:05:01 +02:00
|
|
|
{ initialProps: { supplied: {} } },
|
|
|
|
|
);
|
|
|
|
|
const first = result.current;
|
|
|
|
|
rerender({ supplied: { notifyJoined: async () => {} } });
|
|
|
|
|
expect(result.current).toBe(first);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("forwards to whatever the host most recently supplied", async () => {
|
|
|
|
|
const before = vi.fn().mockResolvedValue(undefined);
|
|
|
|
|
const after = vi.fn().mockResolvedValue(undefined);
|
|
|
|
|
const { result, rerender } = renderHook(
|
|
|
|
|
({ supplied }: { supplied: ElementCallHostBridge }) =>
|
2026-09-08 17:02:24 +02:00
|
|
|
useComponentHostBridge(supplied, undefined, undefined),
|
2026-09-08 15:05:01 +02:00
|
|
|
{ initialProps: { supplied: { notifyJoined: before } } },
|
|
|
|
|
);
|
|
|
|
|
rerender({ supplied: { notifyJoined: after } });
|
|
|
|
|
|
|
|
|
|
await result.current.notifyJoined();
|
|
|
|
|
expect(before).not.toHaveBeenCalled();
|
|
|
|
|
expect(after).toHaveBeenCalledOnce();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("is quiet about what the host did not implement", async () => {
|
|
|
|
|
const { result } = renderHook(() =>
|
2026-09-08 17:02:24 +02:00
|
|
|
useComponentHostBridge(undefined, undefined, undefined),
|
2026-09-08 15:05:01 +02:00
|
|
|
);
|
|
|
|
|
await expect(result.current.contentLoaded()).resolves.toBeUndefined();
|
|
|
|
|
await expect(
|
|
|
|
|
result.current.notifyDeviceMute({
|
|
|
|
|
audio_enabled: true,
|
|
|
|
|
video_enabled: false,
|
|
|
|
|
}),
|
|
|
|
|
).resolves.toBeUndefined();
|
|
|
|
|
expect(result.current.supportsReactions).toBe(true);
|
2026-09-08 18:12:40 +02:00
|
|
|
// Starting the user unmuted unasked is something a host has to opt into
|
|
|
|
|
expect(result.current.allowJoinUnmutedViaIntent).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("lets the host allow joining unmuted on the intent", () => {
|
|
|
|
|
const { result, rerender } = renderHook(
|
|
|
|
|
({ supplied }: { supplied: ElementCallHostBridge }) =>
|
|
|
|
|
useComponentHostBridge(supplied, undefined, undefined),
|
|
|
|
|
{ initialProps: { supplied: {} } },
|
|
|
|
|
);
|
|
|
|
|
expect(result.current.allowJoinUnmutedViaIntent).toBe(false);
|
|
|
|
|
|
|
|
|
|
// Read through to whatever the host most recently said
|
|
|
|
|
rerender({ supplied: { allowJoinUnmutedViaIntent: true } });
|
|
|
|
|
expect(result.current.allowJoinUnmutedViaIntent).toBe(true);
|
2026-09-08 15:05:01 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("only has a close when the host has one, since that is a signal", () => {
|
|
|
|
|
const { result, rerender } = renderHook(
|
|
|
|
|
({ supplied }: { supplied: ElementCallHostBridge }) =>
|
2026-09-08 17:02:24 +02:00
|
|
|
useComponentHostBridge(supplied, undefined, undefined),
|
2026-09-08 15:05:01 +02:00
|
|
|
{ initialProps: { supplied: {} } },
|
|
|
|
|
);
|
|
|
|
|
expect(result.current.close).toBeUndefined();
|
|
|
|
|
|
|
|
|
|
const close = vi.fn().mockResolvedValue(undefined);
|
|
|
|
|
rerender({ supplied: { close } });
|
|
|
|
|
expect(result.current.close).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("never offers profile changes, since the account is the host's", () => {
|
|
|
|
|
const { result } = renderHook(() =>
|
2026-09-08 17:02:24 +02:00
|
|
|
useComponentHostBridge(undefined, undefined, undefined),
|
2026-09-08 15:05:01 +02:00
|
|
|
);
|
|
|
|
|
expect(result.current.supportsProfileChanges).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("the handle", () => {
|
|
|
|
|
test("delivers a request to what is listening and resolves on its reply", async () => {
|
|
|
|
|
const ref = createRef<ElementCallHandle>();
|
|
|
|
|
const { result } = renderHook(() =>
|
2026-09-08 17:02:24 +02:00
|
|
|
useComponentHostBridge(undefined, ref, undefined),
|
2026-09-08 15:05:01 +02:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const received = vi.fn();
|
|
|
|
|
result.current.deviceMute$.subscribe(({ data, reply }) => {
|
|
|
|
|
received(data);
|
|
|
|
|
reply({ audio_enabled: data.audio_enabled!, video_enabled: true });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await expect(
|
|
|
|
|
ref.current!.setDeviceMute({ audio_enabled: false }),
|
|
|
|
|
).resolves.toEqual({ audio_enabled: false, video_enabled: true });
|
|
|
|
|
expect(received).toHaveBeenCalledWith({ audio_enabled: false });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("refuses a request nothing in Element Call is listening for", async () => {
|
|
|
|
|
const ref = createRef<ElementCallHandle>();
|
2026-09-08 17:02:24 +02:00
|
|
|
renderHook(() => useComponentHostBridge(undefined, ref, undefined));
|
2026-09-08 15:05:01 +02:00
|
|
|
|
|
|
|
|
await expect(ref.current!.hangUp()).rejects.toThrow(
|
|
|
|
|
"Nothing in Element Call can hang up right now",
|
|
|
|
|
);
|
|
|
|
|
});
|
2026-09-08 17:02:24 +02:00
|
|
|
});
|
2026-09-08 15:05:01 +02:00
|
|
|
|
2026-09-08 17:02:24 +02:00
|
|
|
describe("the theme", () => {
|
|
|
|
|
test("reaches a subscriber that arrives after it was set", () => {
|
2026-09-08 15:05:01 +02:00
|
|
|
const { result } = renderHook(() =>
|
2026-09-08 17:02:24 +02:00
|
|
|
useComponentHostBridge(undefined, undefined, "light"),
|
2026-09-08 15:05:01 +02:00
|
|
|
);
|
|
|
|
|
const names: (string | undefined)[] = [];
|
2026-09-08 17:02:24 +02:00
|
|
|
result.current.themeChange$.subscribe(({ data }) =>
|
|
|
|
|
names.push(data.name),
|
|
|
|
|
);
|
2026-09-08 15:05:01 +02:00
|
|
|
expect(names).toEqual(["light"]);
|
|
|
|
|
});
|
2026-09-08 17:02:24 +02:00
|
|
|
|
|
|
|
|
test("follows the prop", () => {
|
|
|
|
|
const { result, rerender } = renderHook(
|
|
|
|
|
({ theme }: { theme: string | undefined }) =>
|
|
|
|
|
useComponentHostBridge(undefined, undefined, theme),
|
|
|
|
|
{ initialProps: { theme: "light" } },
|
|
|
|
|
);
|
|
|
|
|
const names: (string | undefined)[] = [];
|
|
|
|
|
result.current.themeChange$.subscribe(({ data }) =>
|
|
|
|
|
names.push(data.name),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
rerender({ theme: "dark" });
|
|
|
|
|
expect(names).toEqual(["light", "dark"]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("says nothing when the host leaves the theme to Element Call", () => {
|
|
|
|
|
const { result } = renderHook(() =>
|
|
|
|
|
useComponentHostBridge(undefined, undefined, undefined),
|
|
|
|
|
);
|
|
|
|
|
const names: (string | undefined)[] = [];
|
|
|
|
|
result.current.themeChange$.subscribe(({ data }) =>
|
|
|
|
|
names.push(data.name),
|
|
|
|
|
);
|
|
|
|
|
expect(names).toEqual([]);
|
|
|
|
|
});
|
2026-09-08 15:05:01 +02:00
|
|
|
});
|
|
|
|
|
});
|