From a568275d9b4da75d4e5409c8e16011b456e83390 Mon Sep 17 00:00:00 2001 From: fkwp Date: Thu, 10 Sep 2026 14:47:28 +0200 Subject: [PATCH] Cover the audio menu's surfaces before joining and during a call MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lobby footer reads the same device behaviours as the in-call one, so the menu already reaches every platform before joining, and stays gated to desktop during a call. These are the checks that hold it there: the view model offers the menu pre-join on desktop, Android and iOS, and the microphone button stands alone with no chevron once a call starts on mobile. Spec: FEATURES_SPEC/2026-09_Audio_Quick_Menu.md, slice 5 — AC18, AC19. Co-Authored-By: Claude Opus 5 (1M context) --- playwright/mobile/audio-menu-mobile.spec.ts | 35 +++++++++++++++++++++ src/components/CallFooter.stories.tsx | 34 ++++++++++++-------- src/components/CallFooterViewModel.test.ts | 22 ++++++++++++- 3 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 playwright/mobile/audio-menu-mobile.spec.ts diff --git a/playwright/mobile/audio-menu-mobile.spec.ts b/playwright/mobile/audio-menu-mobile.spec.ts new file mode 100644 index 000000000..1cc78b534 --- /dev/null +++ b/playwright/mobile/audio-menu-mobile.spec.ts @@ -0,0 +1,35 @@ +/* +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 { expect, test } from "@playwright/test"; + +test("no audio menu during a call on mobile", async ({ page }) => { + await page.goto("/"); + await page.getByTestId("home_callName").fill("Audio menu"); + await page.getByTestId("home_displayName").fill("Mobile"); + await page.getByTestId("home_go").click(); + await expect(page.getByTestId("lobby_joinCall")).toBeVisible(); + + // Before joining, the microphone chevron exists and opens the full menu. + const chevron = page.getByRole("button", { name: "Microphone" }); + await expect(chevron).toBeVisible(); + await chevron.click(); + // On mobile the menu is a drawer named after its title rather than headed + // by it. + const menu = page.getByRole("menu", { name: "Audio controls" }); + await expect(menu).toBeVisible(); + await expect(menu.getByRole("meter")).toBeVisible(); + await expect(menu.getByRole("slider")).toBeVisible(); + await page.keyboard.press("Escape"); + await expect(menu).not.toBeVisible(); + + // During the call, the microphone button stands alone: no chevron, no menu. + await page.getByTestId("lobby_joinCall").click(); + await expect(page.getByTestId("incall_mute")).toBeVisible(); + await expect(page.getByTestId("incall_leave")).toBeVisible(); + await expect(chevron).toHaveCount(0); +}); diff --git a/src/components/CallFooter.stories.tsx b/src/components/CallFooter.stories.tsx index f16b079e5..a4c62223c 100644 --- a/src/components/CallFooter.stories.tsx +++ b/src/components/CallFooter.stories.tsx @@ -361,10 +361,29 @@ export const MobileLayout: Story = { }, }; +/** Devices and controls of the audio menu, as the lobby offers them. */ +const audioMenuArgs = { + audioOptions: [ + { label: { type: "name", name: "MacBook Pro Microphone" }, id: "1" }, + { label: { type: "name", name: "Jabra Evolve 65" }, id: "2" }, + ], + selectedAudio: "1", + selectAudioButtonOption: fn(), + audioOutputOptions: [ + { label: { type: "default", name: "MacBook Pro Speakers" }, id: "" }, + { label: { type: "name", name: "Jabra Evolve 65" }, id: "2" }, + ], + selectedAudioOutput: "", + selectAudioOutputOption: fn(), + soundEffectVolume: 0.5, + setSoundEffectVolume: fn(), +} satisfies Partial; + export const Lobby: Story = { ...Default, args: { ...Default.args, + ...audioMenuArgs, showLogo: false, openSettings: undefined, layout: null, @@ -379,6 +398,7 @@ export const LobbyMobile: Story = { ...Default, args: { ...Default.args, + ...audioMenuArgs, showLogo: false, layout: null, @@ -429,19 +449,7 @@ export const WithAudioMenu: Story = { args: { ...Default.args, audioEnabled: true, - audioOptions: [ - { label: { type: "name", name: "MacBook Pro Microphone" }, id: "1" }, - { label: { type: "name", name: "Jabra Evolve 65" }, id: "2" }, - ], - selectedAudio: "1", - selectAudioButtonOption: fn(), - audioOutputOptions: [ - { label: { type: "default", name: "MacBook Pro Speakers" }, id: "" }, - { label: { type: "name", name: "Jabra Evolve 65" }, id: "2" }, - ], - selectedAudioOutput: "", - selectAudioOutputOption: fn(), - setSoundEffectVolume: fn(), + ...audioMenuArgs, }, play: async ({ args, canvasElement }) => { const canvas = within(canvasElement); diff --git a/src/components/CallFooterViewModel.test.ts b/src/components/CallFooterViewModel.test.ts index e568eac3c..31b72058f 100644 --- a/src/components/CallFooterViewModel.test.ts +++ b/src/components/CallFooterViewModel.test.ts @@ -17,7 +17,10 @@ import type { AudioOutputDeviceLabel, DeviceLabel, } from "../state/MediaDevices"; -import { createCallFooterViewModel } from "./CallFooterViewModel"; +import { + createCallFooterViewModel, + createLobbyFooterViewModel, +} from "./CallFooterViewModel"; import { HeaderStyle } from "../UrlParams"; import { type FooterSnapshot } from "./CallFooter"; import { type ViewModel } from "../state/ViewModel"; @@ -148,6 +151,23 @@ describe("createCallFooterViewModel", () => { expect(vm.setSoundEffectVolume$.value).toBeUndefined(); }); + it("audio menu is present pre-join on every platform", () => { + for (const platform of ["desktop", "android", "ios"]) { + platformMock.mockReturnValue(platform); + const vm = createLobbyFooterViewModel( + testScope(), + mockMuteStates(), + twoOutputsMediaDevices, + undefined, + undefined, + false, + ); + expect(vm.audioOutputOptions$.value).toHaveLength(2); + expect(vm.selectAudioOutputOption$.value).toBeDefined(); + expect(vm.setSoundEffectVolume$.value).toBeDefined(); + } + }); + it("reads and writes the sound-effect volume setting on desktop", () => { const vm = createVm("desktop", gridLayout); expect(vm.soundEffectVolume$.value).toBe(0.5);