From 5da7dd64137f249b649893f607de0db293ad107c Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 9 Apr 2026 15:49:09 +0200 Subject: [PATCH 01/33] Move footer to storybook --- src/button/Button.tsx | 31 ++++ src/button/ReactionToggleButton.tsx | 8 +- src/components/InCallFooter.stories.tsx | 132 ++++++++++++++++ src/components/InCallFooter.tsx | 202 ++++++++++++++++++++++++ src/room/InCallView.tsx | 163 ++++--------------- 5 files changed, 396 insertions(+), 140 deletions(-) create mode 100644 src/components/InCallFooter.stories.tsx create mode 100644 src/components/InCallFooter.tsx diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 407b74cb..175cb22d 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -18,6 +18,7 @@ import { ShareScreenSolidIcon, OverflowHorizontalIcon, OverflowVerticalIcon, + VolumeOnSolidIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; import styles from "./Button.module.css"; @@ -126,6 +127,36 @@ export const EndCallButton: FC = ({ ); }; +interface LoudspeakerButtonProps extends ComponentPropsWithoutRef<"button"> { + size?: "sm" | "lg"; + /** The button will be rendered: + * true: currently in loudspeaker mode, pressing will switch to earpiece (rendered as enabled) + * false: currently in earpiece mode, pressing will switch to loudspeaker (rendered as disabled) + */ + isEarpieceTarget: boolean; +} + +export const LoudspeakerButton: FC = (props) => { + const { t } = useTranslation(); + const label = props.isEarpieceTarget + ? t("settings.devices.handset") + : t("settings.devices.loudspeaker"); + // if the target is the earpice, we are currently in loudspeaker mode. + const enabled = props.isEarpieceTarget; + return ( + + + + ); +}; + interface SettingsButtonProps extends ComponentPropsWithoutRef<"button"> { size?: "sm" | "lg"; } diff --git a/src/button/ReactionToggleButton.tsx b/src/button/ReactionToggleButton.tsx index 28163321..6fe52308 100644 --- a/src/button/ReactionToggleButton.tsx +++ b/src/button/ReactionToggleButton.tsx @@ -165,13 +165,13 @@ export function ReactionPopupMenu({ interface ReactionToggleButtonProps extends ComponentPropsWithoutRef<"button"> { identifier: string; - vm: CallViewModel; + reactionData: Pick; size?: "sm" | "lg"; } export function ReactionToggleButton({ identifier, - vm, + reactionData, ...props }: ReactionToggleButtonProps): ReactNode { const { t } = useTranslation(); @@ -180,8 +180,8 @@ export function ReactionToggleButton({ const [showReactionsMenu, setShowReactionsMenu] = useState(false); const [errorText, setErrorText] = useState(); - const isHandRaised = !!useBehavior(vm.handsRaised$)[identifier]; - const canReact = !useBehavior(vm.reactions$)[identifier]; + const isHandRaised = !!useBehavior(reactionData.handsRaised$)[identifier]; + const canReact = !useBehavior(reactionData.reactions$)[identifier]; useEffect(() => { // Clear whenever the reactions menu state changes. diff --git a/src/components/InCallFooter.stories.tsx b/src/components/InCallFooter.stories.tsx new file mode 100644 index 00000000..336884a4 --- /dev/null +++ b/src/components/InCallFooter.stories.tsx @@ -0,0 +1,132 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { fn } from "storybook/test"; +import { BehaviorSubject } from "rxjs"; +import { type ReactNode } from "react"; + +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { InCallFooter, type InCallFooterProps } from "./InCallFooter"; +import inCallViewStyles from "../room/InCallView.module.css"; + +function InCallFooterWrapper(props: InCallFooterProps): ReactNode { + return ( +
+ +
+ ); +} + +const meta = { + component: InCallFooterWrapper, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + asOverlay: false, + showFooter: true, + showControls: true, + showSettingsButton: true, + showLogo: false, + asPip: false, + gridMode: "grid", + audioEnabled: true, + videoEnabled: true, + sharingScreen: false, + supportsReactions: false, + audioOutputSwitcher: null, + debugTileLayout: false, + tileStoreGeneration: 0, + reactionData: { + handsRaised$: new BehaviorSubject({}), + reactions$: new BehaviorSubject({}), + }, + reactionIdentifier: "@user:example.com:DEVICE", + setGridMode: fn(), + openSettings: fn(), + toggleAudio: fn(), + toggleVideo: fn(), + toggleScreenSharing: fn(), + hangup: fn(), + }, + parameters: { + layout: "fullscreen", + }, + argTypes: { + gridMode: { control: "radio", options: ["grid", "spotlight"] }, + audioOutputSwitcher: { + control: "radio", + options: ["noOutputSwitcher", "earpiece", "speaker"], + mapping: { + noOutputSwitcher: null, + // This is inverersed (speaker<->earpice) because the switcher object stores the target output, not the current one. + earpiece: { targetOutput: "speaker", switch: fn() }, + speaker: { targetOutput: "earpiece", switch: fn() }, + }, + }, + }, +}; + +export const WithLogo: Story = { + ...Default, + args: { + ...Default.args, + showLogo: true, + }, +}; +export const WithAudioOutput: Story = { + ...Default, + args: { + ...Default.args, + audioOutputSwitcher: { targetOutput: "speaker", switch: fn() }, + }, +}; +export const Pip: Story = { + ...Default, + args: { + ...Default.args, + asPip: true, + }, +}; + +export const NoControlsWithLogo: Story = { + ...Default, + args: { + ...Default.args, + showControls: false, + showLogo: true, + }, +}; + +export const DebugData: Story = { + ...Default, + args: { + ...Default.args, + debugTileLayout: true, + tileStoreGeneration: 74, + audioOutputSwitcher: null, + }, +}; +export const MobileLayout: Story = { + ...Default, + args: { + ...Default.args, + showLogo: true, + debugTileLayout: true, + tileStoreGeneration: 74, + audioOutputSwitcher: null, + }, + globals: { + viewport: { value: "mobile2", isRotated: false }, + }, + parameters: { + ...Default.parameters, + }, +}; diff --git a/src/components/InCallFooter.tsx b/src/components/InCallFooter.tsx new file mode 100644 index 00000000..f5ca656c --- /dev/null +++ b/src/components/InCallFooter.tsx @@ -0,0 +1,202 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE in the repository root for full details. +*/ + +import { type FC, type JSX, type Ref, useMemo } from "react"; +import classNames from "classnames"; + +import LogoMark from "../icons/LogoMark.svg?react"; +import LogoType from "../icons/LogoType.svg?react"; +import { + EndCallButton, + MicButton, + VideoButton, + ShareScreenButton, + SettingsButton, + ReactionToggleButton, + LoudspeakerButton, +} from "../button"; +import styles from "../room/InCallView.module.css"; +import { LayoutToggle } from "../room/LayoutToggle"; +import { + type CallViewModel, + type GridMode, +} from "../state/CallViewModel/CallViewModel"; +import { useAppBarSecondaryButton } from "../AppBar"; + +export interface AudioOutputSwitcher { + targetOutput: string; + switch: () => void; +} + +export interface InCallFooterProps { + ref?: Ref; + /* This is needed for WindowMode = "flat" */ + asOverlay: boolean; + showFooter: boolean; + showControls: boolean; + showSettingsButton: boolean; + showLogo: boolean; + asPip: boolean; + gridMode: GridMode; + setGridMode: (mode: GridMode) => void; + openSettings: () => void; + audioEnabled: boolean; + videoEnabled: boolean; + toggleAudio?: () => void; + toggleVideo?: () => void; + sharingScreen: boolean; + toggleScreenSharing?: () => void; + supportsReactions: boolean; + reactionIdentifier: string; + reactionData: Pick; + audioOutputSwitcher: AudioOutputSwitcher | null; + hangup: () => void; + debugTileLayout: boolean; + tileStoreGeneration: number; +} + +export const InCallFooter: FC = ({ + ref, + asOverlay, + showFooter, + showControls, + showSettingsButton, + showLogo, + asPip, + gridMode, + setGridMode, + openSettings, + audioEnabled, + videoEnabled, + toggleAudio, + toggleVideo, + sharingScreen, + toggleScreenSharing, + supportsReactions, + reactionIdentifier, + reactionData, + audioOutputSwitcher, + hangup, + debugTileLayout, + tileStoreGeneration, +}) => { + const buttons: JSX.Element[] = []; + const buttonSize = asPip ? "sm" : "lg"; + + buttons.push( + , + , + ); + + if (toggleScreenSharing !== null) { + buttons.push( + , + ); + } + + if (supportsReactions) { + buttons.push( + , + ); + } + + // In this PR we just move the button to the bottom bar. We do not yet update its appearance + const audioOutputButton = useMemo(() => { + if (audioOutputSwitcher === null) return null; + return ( + + ); + }, [audioOutputSwitcher, buttonSize]); + + if (audioOutputButton) buttons.push(audioOutputButton); + + useAppBarSecondaryButton( + , + ); + + buttons.push( + , + ); + + const logo = ( +
+ {showLogo && ( + <> + + + + )} + {debugTileLayout ? `Tiles generation: ${tileStoreGeneration}` : undefined} +
+ ); + + return ( +
+
+ {showControls && showSettingsButton && !asPip && ( + + )} + + {(!asPip || (!showLogo && !debugTileLayout)) && logo} +
+ + {showControls &&
{buttons}
} + {showControls && !asPip && ( + + )} +
+ ); +}; diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index cf445170..12374d05 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -5,7 +5,6 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial Please see LICENSE in the repository root for full details. */ -import { IconButton, Tooltip } from "@vector-im/compound-web"; import { type MatrixClient, type Room as MatrixRoom } from "matrix-js-sdk"; import { type FC, @@ -25,22 +24,9 @@ import classNames from "classnames"; import { BehaviorSubject, map } from "rxjs"; import { useObservable } from "observable-hooks"; import { logger as rootLogger } from "matrix-js-sdk/lib/logger"; -import { - VoiceCallSolidIcon, - VolumeOnSolidIcon, -} from "@vector-im/compound-design-tokens/assets/web/icons"; import { useTranslation } from "react-i18next"; -import LogoMark from "../icons/LogoMark.svg?react"; -import LogoType from "../icons/LogoType.svg?react"; -import { - EndCallButton, - MicButton, - VideoButton, - ShareScreenButton, - SettingsButton, - ReactionToggleButton, -} from "../button"; +import { SettingsButton } from "../button"; import { Header, LeftNav, RightNav, RoomHeaderInfo } from "../Header"; import { HeaderStyle, useUrlParams } from "../UrlParams"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; @@ -55,7 +41,6 @@ import { useMergedRefs } from "../useMergedRefs"; import { type MuteStates } from "../state/MuteStates"; import { type MatrixInfo } from "./VideoPreview"; import { InviteButton } from "../button/InviteButton"; -import { LayoutToggle } from "./LayoutToggle"; import { type CallViewModel, createCallViewModel$, @@ -106,6 +91,7 @@ import { useTrackProcessorObservable$ } from "../livekit/TrackProcessorContext.t import { type Layout } from "../state/layout-types.ts"; import { ObservableScope } from "../state/ObservableScope.ts"; import { useLatest } from "../useLatest.ts"; +import { InCallFooter } from "../components/InCallFooter.tsx"; const logger = rootLogger.getChild("[InCallView]"); @@ -575,133 +561,38 @@ export const InCallView: FC = ({ matrixRoom.roomId, ); - const buttons: JSX.Element[] = []; - - const buttonSize = layout.type === "pip" ? "sm" : "lg"; - buttons.push( - , - , - ); - if (vm.toggleScreenSharing !== null) { - buttons.push( - , - ); - } - if (supportsReactions) { - buttons.push( - , - ); - } - - // In this PR we just move the button ot the bottom bar. We do not yet update its apperance - const audioOutputButton = useMemo(() => { - if (audioOutputSwitcher === null) return null; - const isEarpieceTarget = audioOutputSwitcher.targetOutput === "earpiece"; - const Icon = isEarpieceTarget ? VoiceCallSolidIcon : VolumeOnSolidIcon; - const label = isEarpieceTarget - ? t("settings.devices.handset") - : t("settings.devices.loudspeaker"); - - return ( - - { - audioOutputSwitcher.switch(); - }} - > - - - - ); - }, [t, audioOutputSwitcher]); - if (audioOutputButton) buttons.push(audioOutputButton); - useAppBarSecondaryButton( , ); - buttons.push( - , - ); - - const logo = ( -
- - - {/* Don't mind this odd placement, it's just a little debug label */} - {debugTileLayout ? `Tiles generation: ${tileStoreGeneration}` : undefined} -
- ); - const footer = ( -
-
- {showControls && - headerStyle !== HeaderStyle.AppBar && - layout.type !== "pip" && ( - - )} - - {headerStyle !== "none" && logo} -
- - {showControls &&
{buttons}
} - {showControls && ( - - )} -
+ asOverlay={windowMode === "flat"} + // TODO this should be computed in the view model! + showFooter={!showFooter || (!showControls && headerStyle === "none")} + showControls={showControls} + showLogo={headerStyle !== HeaderStyle.None} + showSettingsButton={headerStyle !== HeaderStyle.AppBar} + asPip={layout.type === "pip"} + gridMode={gridMode} + setGridMode={setGridMode} + openSettings={openSettings} + audioEnabled={audioEnabled} + videoEnabled={videoEnabled} + toggleAudio={toggleAudio ?? undefined} + toggleVideo={toggleVideo ?? undefined} + sharingScreen={sharingScreen} + toggleScreenSharing={vm.toggleScreenSharing ?? undefined} + supportsReactions={supportsReactions} + reactionIdentifier={`${client.getUserId()}:${client.getDeviceId()}`} + reactionData={vm} + audioOutputSwitcher={audioOutputSwitcher} + hangup={vm.hangup} + debugTileLayout={debugTileLayout} + tileStoreGeneration={tileStoreGeneration} + /> ); - const allConnections = useBehavior(vm.allConnections$); return ( From 9528c4f8377aaf027553d664ef27dd7a38e44649 Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 9 Apr 2026 16:50:04 +0200 Subject: [PATCH 02/33] fix lints --- src/button/ReactionToggleButton.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/button/ReactionToggleButton.test.tsx b/src/button/ReactionToggleButton.test.tsx index c6010562..86954869 100644 --- a/src/button/ReactionToggleButton.test.tsx +++ b/src/button/ReactionToggleButton.test.tsx @@ -37,7 +37,7 @@ function TestComponent({ vm={vm} rtcSession={rtcSession.asMockedSession()} > - + ); From 6be06de1536c0551a066f5280b2f0ea955f545d6 Mon Sep 17 00:00:00 2001 From: Timo K Date: Thu, 9 Apr 2026 17:12:21 +0200 Subject: [PATCH 03/33] fix tests --- src/components/InCallFooter.tsx | 5 ++-- src/room/InCallView.test.tsx | 2 +- src/room/InCallView.tsx | 4 ++- .../__snapshots__/InCallView.test.tsx.snap | 27 +++++++++++++++++-- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/src/components/InCallFooter.tsx b/src/components/InCallFooter.tsx index f5ca656c..8a25bde3 100644 --- a/src/components/InCallFooter.tsx +++ b/src/components/InCallFooter.tsx @@ -93,7 +93,7 @@ export const InCallFooter: FC = ({ key="audio" enabled={audioEnabled} onClick={toggleAudio ?? undefined} - disabled={toggleAudio === null} + disabled={toggleAudio === undefined} data-testid="incall_mute" />, = ({ key="video" enabled={videoEnabled} onClick={toggleVideo ?? undefined} - disabled={toggleVideo === null} + disabled={toggleVideo === undefined} data-testid="incall_videomute" />, ); @@ -137,6 +137,7 @@ export const InCallFooter: FC = ({ return ( audioOutputSwitcher.switch()} isEarpieceTarget={audioOutputSwitcher.targetOutput === "earpiece"} /> ); diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index 0c3be63b..2f797c65 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -197,7 +197,7 @@ describe("InCallView", () => { const { getByRole } = createInCallView({ mediaDevices }); // The button should be visible. When current output is "speaker", // the switcher targets "earpiece", so the tooltip label is "Handset". - const audioOutputBtn = getByRole("button", { name: "Handset" }); + const audioOutputBtn = getByRole("switch", { name: "Handset" }); expect(audioOutputBtn).toBeVisible(); await user.click(audioOutputBtn); diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 12374d05..56cc02e1 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -565,12 +565,14 @@ export const InCallView: FC = ({ , ); + const footerNotNeeded = + showControls === false && headerStyle === HeaderStyle.None; const footer = ( rendering > renders 1`] = ` + -
rendering > renders 1`] = ` d="M4 11a.97.97 0 0 1-.712-.287A.97.97 0 0 1 3 10V4q0-.424.288-.712A.97.97 0 0 1 4 3h6q.424 0 .713.288Q11 3.575 11 4v6q0 .424-.287.713A.97.97 0 0 1 10 11zm5-2V5H5v4zm5 12a.97.97 0 0 1-.713-.288A.97.97 0 0 1 13 20v-6q0-.424.287-.713A.97.97 0 0 1 14 13h6q.424 0 .712.287.288.288.288.713v6q0 .424-.288.712A.97.97 0 0 1 20 21zm5-2v-4h-4v4zM4 21a.97.97 0 0 1-.712-.288A.97.97 0 0 1 3 20v-6q0-.424.288-.713A.97.97 0 0 1 4 13h6q.424 0 .713.287.287.288.287.713v6q0 .424-.287.712A.97.97 0 0 1 10 21zm5-2v-4H5v4zm5-8a.97.97 0 0 1-.713-.287A.97.97 0 0 1 13 10V4q0-.424.287-.712A.97.97 0 0 1 14 3h6q.424 0 .712.288Q21 3.575 21 4v6q0 .424-.288.713A.97.97 0 0 1 20 11zm5-2V5h-4v4z" /> -
+ diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index fe65b68a..7bfe4630 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -173,6 +173,8 @@ export interface CallViewModelOptions { windowSize$?: Behavior<{ width: number; height: number }>; /** The version & compatibility mode of MatrixRTC that we should use. */ matrixRTCMode$?: Behavior; + /** Optional behavior overriding for the screensharing, for testing */ + toggleScreensharing?: () => void; } // Do not play any sounds if the participant count has exceeded this @@ -1506,7 +1508,8 @@ export function createCallViewModel$( * Callback to toggle screen sharing. If null, screen sharing is not possible. */ // reassigned here to make it publicly accessible - const toggleScreenSharing = localMembership.toggleScreenSharing; + const toggleScreenSharing = + options.toggleScreensharing ?? localMembership.toggleScreenSharing; const errors$ = scope.behavior<{ transportError?: ElementCallError; From 3c039be7f2727a16af72a3d58343a31abd8f1908 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 14 Apr 2026 14:11:08 +0200 Subject: [PATCH 21/33] lint --- src/components/CallFooter.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CallFooter.mdx b/src/components/CallFooter.mdx index 4a69939b..a8b960d4 100644 --- a/src/components/CallFooter.mdx +++ b/src/components/CallFooter.mdx @@ -11,9 +11,9 @@ This can be done by using the same filename as the component With the help of Primary, Controls,Stories the overhead is minimal */} -import { Meta,Primary,Controls,Stories,Title,Subtitle } from '@storybook/addon-docs/blocks'; +import { Meta, Primary, Controls, Stories, Title, Subtitle } from '@storybook/addon-docs/blocks'; import * as CallFooterStories from './CallFooter.stories'; - + Call Footer From 2d5de234d2c6a69cd50604742c881cf5757c3775 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 14 Apr 2026 14:26:00 +0200 Subject: [PATCH 22/33] prettier --- src/components/CallFooter.mdx | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/CallFooter.mdx b/src/components/CallFooter.mdx index a8b960d4..b94131a0 100644 --- a/src/components/CallFooter.mdx +++ b/src/components/CallFooter.mdx @@ -1,18 +1,24 @@ -{/* +{/** 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. -*/} +**/} -{/* +{/** This is a custom doc page overwriting the default autodocs tag. This can be done by using the same filename as the component With the help of Primary, Controls,Stories the overhead is minimal -*/} +**/} -import { Meta, Primary, Controls, Stories, Title, Subtitle } from '@storybook/addon-docs/blocks'; -import * as CallFooterStories from './CallFooter.stories'; +import { + Meta, + Primary, + Controls, + Stories, + Title, + Subtitle, +} from "@storybook/addon-docs/blocks"; +import * as CallFooterStories from "./CallFooter.stories"; @@ -28,4 +34,4 @@ The story summary here does not render the mobile layouts correctly. - + From 2ae1df467daa79addd0c603b1c07773363e33e4a Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 14 Apr 2026 14:30:46 +0200 Subject: [PATCH 23/33] knip --- .storybook/main.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.storybook/main.ts b/.storybook/main.ts index 9a3f0b53..977eca73 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -9,7 +9,7 @@ import type { StorybookConfig } from "@storybook/react-vite"; const config: StorybookConfig = { stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], - addons: ["@storybook/addon-docs", "@storybook/addon-vitest"], + addons: ["@storybook/addon-docs"], framework: "@storybook/react-vite", }; export default config; From 57f12d3d1c5dc6af0815d8a593fa4d4c953d4935 Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 14 Apr 2026 16:23:05 +0200 Subject: [PATCH 24/33] fix race in playwright sticky test --- playwright/reconnect.spec.ts | 2 +- playwright/spa-call-sticky.spec.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/playwright/reconnect.spec.ts b/playwright/reconnect.spec.ts index 1a8f2c28..e6d5f715 100644 --- a/playwright/reconnect.spec.ts +++ b/playwright/reconnect.spec.ts @@ -13,7 +13,7 @@ test.skip( 'This test is not working on firefox, page.keyboard.press("Tab") not reliable in headless mode', ); -test("can only interact with header and footer while reconnecting", async ({ +test("can not interact with media tiles while reconnecting (only with header and footer)", async ({ page, }) => { await page.goto("/"); diff --git a/playwright/spa-call-sticky.spec.ts b/playwright/spa-call-sticky.spec.ts index 246b4a73..65cf06ed 100644 --- a/playwright/spa-call-sticky.spec.ts +++ b/playwright/spa-call-sticky.spec.ts @@ -29,6 +29,9 @@ async function setupTwoUserSpaCall( let androlHasSentStickyEvent = false; + const { promise: stickyAndrolPromise, resolve: stickyAndrolResolve } = + Promise.withResolvers(); + await interceptEventSend( page, // This room is not encrypted, so the event is sent in clear @@ -36,6 +39,9 @@ async function setupTwoUserSpaCall( (req) => { androlHasSentStickyEvent = androlHasSentStickyEvent || isStickySend(req.url()); + if (androlHasSentStickyEvent) { + stickyAndrolResolve(); + } }, ); @@ -53,6 +59,8 @@ async function setupTwoUserSpaCall( let pevaraHasSentStickyEvent = false; + const { promise: stickyPevaraPromise, resolve: stickyPevaraResolve } = + Promise.withResolvers(); await interceptEventSend( guestPage, // This room is not encrypted, so the event is sent in clear @@ -60,6 +68,9 @@ async function setupTwoUserSpaCall( (req) => { pevaraHasSentStickyEvent = pevaraHasSentStickyEvent || isStickySend(req.url()); + if (pevaraHasSentStickyEvent) { + stickyPevaraResolve(); + } }, ); @@ -70,7 +81,9 @@ async function setupTwoUserSpaCall( "2_0", ); // Assert both sides have sent sticky membership events + await stickyAndrolPromise; expect(androlHasSentStickyEvent).toEqual(true); + await stickyPevaraPromise; expect(pevaraHasSentStickyEvent).toEqual(true); return { guestPage }; From dd80679ec581c3d63205fadaa8fdae320a77c43b Mon Sep 17 00:00:00 2001 From: Timo K Date: Tue, 14 Apr 2026 18:17:00 +0200 Subject: [PATCH 25/33] test snapshot --- src/room/__snapshots__/InCallView.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap index 716d27d5..e8e06560 100644 --- a/src/room/__snapshots__/InCallView.test.tsx.snap +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -170,7 +170,7 @@ exports[`InCallView > rendering > renders 1`] = ` > -