From 0e04fd9433a402e29e8517382345bf6d2e7e8ae4 Mon Sep 17 00:00:00 2001 From: Valere Date: Tue, 2 Dec 2025 14:14:28 +0100 Subject: [PATCH 1/7] fix: The handset mode overlay is visible a split second for every call --- src/room/EarpieceOverlay.module.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/room/EarpieceOverlay.module.css b/src/room/EarpieceOverlay.module.css index d0757cdb..e007fc44 100644 --- a/src/room/EarpieceOverlay.module.css +++ b/src/room/EarpieceOverlay.module.css @@ -2,7 +2,7 @@ position: fixed; z-index: var(--call-view-overlay-layer); inset: 0; - display: flex; + display: none; flex-direction: column; align-items: center; justify-content: center; @@ -12,6 +12,7 @@ @keyframes fade-in { from { opacity: 0; + display: flex; } to { opacity: 1; From 0ed7194d87175edb277f212a21f747229b219c8d Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 3 Dec 2025 17:10:57 +0100 Subject: [PATCH 2/7] fix: earpiece overlay not showing + playwright test! --- playwright.config.ts | 24 +++- playwright/mobile/create-call-mobile.spec.ts | 112 +++++++++++++++++++ playwright/mobile/fixture-mobile-create.ts | 73 ++++++++++++ src/room/EarpieceOverlay.module.css | 3 +- 4 files changed, 209 insertions(+), 3 deletions(-) create mode 100644 playwright/mobile/create-call-mobile.spec.ts create mode 100644 playwright/mobile/fixture-mobile-create.ts diff --git a/playwright.config.ts b/playwright.config.ts index 7a8ee530..391e746f 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ projects: [ { name: "chromium", + testIgnore: "**/mobile/**", use: { ...devices["Desktop Chrome"], permissions: [ @@ -56,9 +57,9 @@ export default defineConfig({ }, }, }, - { name: "firefox", + testIgnore: "**/mobile/**", use: { ...devices["Desktop Firefox"], ignoreHTTPSErrors: true, @@ -70,6 +71,27 @@ export default defineConfig({ }, }, }, + { + name: "mobile", + testMatch: "**/mobile/**", + use: { + ...devices["Pixel 7"], + ignoreHTTPSErrors: true, + permissions: [ + "clipboard-write", + "clipboard-read", + "microphone", + "camera", + ], + launchOptions: { + args: [ + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream", + "--mute-audio", + ], + }, + }, + }, // No safari for now, until I find a solution to fix `Not allowed to request resource` due to calling // clear http to the homeserver diff --git a/playwright/mobile/create-call-mobile.spec.ts b/playwright/mobile/create-call-mobile.spec.ts new file mode 100644 index 00000000..853294ea --- /dev/null +++ b/playwright/mobile/create-call-mobile.spec.ts @@ -0,0 +1,112 @@ +/* +Copyright 2025 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"; + +import { mobileTest } from "./fixture-mobile-create.ts"; + +test("@mobile Start a new call then leave and show the feedback screen", async ({ + page, +}) => { + await page.goto("/"); + + await page.getByTestId("home_callName").click(); + await page.getByTestId("home_callName").fill("HelloCall"); + await page.getByTestId("home_displayName").click(); + await page.getByTestId("home_displayName").fill("John Doe"); + await page.getByTestId("home_go").click(); + + // await page.pause(); + await expect(page.locator("video")).toBeVisible(); + await expect(page.getByTestId("lobby_joinCall")).toBeVisible(); + + await page.getByRole("button", { name: "Continue in browser" }).click(); + // Join the call + await page.getByTestId("lobby_joinCall").click(); + + // Ensure that the call is connected + await page + .locator("div") + .filter({ hasText: /^HelloCall$/ }) + .click(); + // Check the number of participants + await expect(page.locator("div").filter({ hasText: /^1$/ })).toBeVisible(); + // The tooltip with the name should be visible + await expect(page.getByTestId("name_tag")).toContainText("John Doe"); + + // leave the call + await page.getByTestId("incall_leave").click(); + await expect(page.getByRole("heading")).toContainText( + "John Doe, your call has ended. How did it go?", + ); + await expect(page.getByRole("main")).toContainText( + "Why not finish by setting up a password to keep your account?", + ); + + await expect( + page.getByRole("link", { name: "Not now, return to home screen" }), + ).toBeVisible(); +}); + +mobileTest("Start a new call as widget", async ({ asMobile, browser }) => { + test.slow(); // Triples the timeout + const { creatorPage, inviteLink } = asMobile; + + // test("Show earpiece overlay when output is earpiece", async ({ browser }) => { + // Use reduce motion to disable animations that are making the tests a bit flaky + + // ======== + // ACT: The other user use the invite link to join the call as a guest + // ======== + const guestInviteeContext = await browser.newContext({ + reducedMotion: "reduce", + }); + const guestPage = await guestInviteeContext.newPage(); + await guestPage.goto(inviteLink + "&controlledAudioDevices=true"); + + await guestPage.getByRole("button", { name: "Continue in browser" }).click(); + + await guestPage.getByTestId("joincall_displayName").fill("Invitee"); + await expect(guestPage.getByTestId("joincall_joincall")).toBeVisible(); + await guestPage.getByTestId("joincall_joincall").click(); + await guestPage.getByTestId("lobby_joinCall").click(); + + // ======== + // ASSERT: check that there are two members in the call + // ======== + + // There should be two participants now + await expect( + guestPage.getByTestId("roomHeader_participants_count"), + ).toContainText("2"); + expect(await guestPage.getByTestId("videoTile").count()).toBe(2); + + // Same in creator page + await expect( + creatorPage.getByTestId("roomHeader_participants_count"), + ).toContainText("2"); + expect(await creatorPage.getByTestId("videoTile").count()).toBe(2); + + // TEST: control audio devices from the invitee page + + await guestPage.evaluate(() => { + window.controls.setAvailableAudioDevices([ + { id: "speaker", name: "Speaker", isSpeaker: true }, + { id: "earpiece", name: "Handset", isEarpiece: true }, + { id: "headphones", name: "Headphones" }, + ]); + window.controls.setAudioDevice("earpiece"); + }); + await expect( + guestPage.getByRole("heading", { name: "Handset Mode" }), + ).toBeVisible(); + await expect( + guestPage.getByRole("button", { name: "Back to Speaker Mode" }), + ).toBeVisible(); + + // await guestPage.pause(); +}); diff --git a/playwright/mobile/fixture-mobile-create.ts b/playwright/mobile/fixture-mobile-create.ts new file mode 100644 index 00000000..053335d3 --- /dev/null +++ b/playwright/mobile/fixture-mobile-create.ts @@ -0,0 +1,73 @@ +/* +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 Browser, type Page, test, expect } from "@playwright/test"; + +export interface MobileCreateFixtures { + asMobile: { + creatorPage: Page; + inviteLink: string; + }; +} + +export const mobileTest = test.extend({ + asMobile: async ({ browser }, puse) => { + const fixtures = await createCallAndInvite(browser); + await puse({ + creatorPage: fixtures.page, + inviteLink: fixtures.inviteLink, + }); + }, +}); + +/** + * Create a call and generate an invite link + */ +async function createCallAndInvite( + browser: Browser, +): Promise<{ page: Page; inviteLink: string }> { + const creatorContext = await browser.newContext({ reducedMotion: "reduce" }); + const creatorPage = await creatorContext.newPage(); + + await creatorPage.goto("/"); + + // ======== + // ARRANGE: The first user creates a call as guest, join it, then click the invite button to copy the invite link + // ======== + await creatorPage.getByTestId("home_callName").click(); + await creatorPage.getByTestId("home_callName").fill("Welcome"); + await creatorPage.getByTestId("home_displayName").click(); + await creatorPage.getByTestId("home_displayName").fill("Inviter"); + await creatorPage.getByTestId("home_go").click(); + await expect(creatorPage.locator("video")).toBeVisible(); + + await creatorPage + .getByRole("button", { name: "Continue in browser" }) + .click(); + // join + await creatorPage.getByTestId("lobby_joinCall").click(); + + // Get the invite link + await creatorPage.getByRole("button", { name: "Invite" }).click(); + await expect( + creatorPage.getByRole("heading", { name: "Invite to this call" }), + ).toBeVisible(); + await expect(creatorPage.getByRole("img", { name: "QR Code" })).toBeVisible(); + await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible(); + await expect(creatorPage.getByTestId("modal_inviteLink")).toBeVisible(); + await creatorPage.getByTestId("modal_inviteLink").click(); + + const inviteLink = (await creatorPage.evaluate( + "navigator.clipboard.readText()", + )) as string; + expect(inviteLink).toContain("room/#/"); + + return { + page: creatorPage, + inviteLink, + }; +} diff --git a/src/room/EarpieceOverlay.module.css b/src/room/EarpieceOverlay.module.css index e007fc44..1718b0f2 100644 --- a/src/room/EarpieceOverlay.module.css +++ b/src/room/EarpieceOverlay.module.css @@ -12,7 +12,6 @@ @keyframes fade-in { from { opacity: 0; - display: flex; } to { opacity: 1; @@ -20,6 +19,7 @@ } .overlay[data-show="true"] { + display: flex; animation: fade-in 200ms; } @@ -29,7 +29,6 @@ } to { opacity: 0; - display: none; } } From bcb2b36888abf5d99e6be6cde104f4a12d23c91f Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 3 Dec 2025 17:17:58 +0100 Subject: [PATCH 3/7] keep livekit 1.9.4 as the latest is breaking the dev backend --- dev-backend-docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-backend-docker-compose.yml b/dev-backend-docker-compose.yml index 50498c7a..c7591847 100644 --- a/dev-backend-docker-compose.yml +++ b/dev-backend-docker-compose.yml @@ -47,7 +47,7 @@ services: - ecbackend livekit: - image: livekit/livekit-server:latest + image: livekit/livekit-server:v1.9.4 pull_policy: always hostname: livekit-sfu command: --dev --config /etc/livekit.yaml @@ -67,7 +67,7 @@ services: - ecbackend livekit-1: - image: livekit/livekit-server:latest + image: livekit/livekit-server:v1.9.4 pull_policy: always hostname: livekit-sfu-1 command: --dev --config /etc/livekit.yaml From c7491c3e9747cb0325807b3e9a989c10ab9551d5 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 3 Dec 2025 17:25:32 +0100 Subject: [PATCH 4/7] move fixture to correct folder --- playwright/{mobile => fixtures}/fixture-mobile-create.ts | 0 playwright/mobile/create-call-mobile.spec.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename playwright/{mobile => fixtures}/fixture-mobile-create.ts (100%) diff --git a/playwright/mobile/fixture-mobile-create.ts b/playwright/fixtures/fixture-mobile-create.ts similarity index 100% rename from playwright/mobile/fixture-mobile-create.ts rename to playwright/fixtures/fixture-mobile-create.ts diff --git a/playwright/mobile/create-call-mobile.spec.ts b/playwright/mobile/create-call-mobile.spec.ts index 853294ea..9005d510 100644 --- a/playwright/mobile/create-call-mobile.spec.ts +++ b/playwright/mobile/create-call-mobile.spec.ts @@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details. import { expect, test } from "@playwright/test"; -import { mobileTest } from "./fixture-mobile-create.ts"; +import { mobileTest } from "../fixtures/fixture-mobile-create.ts"; test("@mobile Start a new call then leave and show the feedback screen", async ({ page, From 940c787040b50ab3d1e1eb22010c36a6b7a8af7e Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 4 Dec 2025 10:06:45 +0100 Subject: [PATCH 5/7] review: quick renaming --- playwright/fixtures/fixture-mobile-create.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/playwright/fixtures/fixture-mobile-create.ts b/playwright/fixtures/fixture-mobile-create.ts index 053335d3..3920c978 100644 --- a/playwright/fixtures/fixture-mobile-create.ts +++ b/playwright/fixtures/fixture-mobile-create.ts @@ -15,9 +15,9 @@ export interface MobileCreateFixtures { } export const mobileTest = test.extend({ - asMobile: async ({ browser }, puse) => { + asMobile: async ({ browser }, pUse) => { const fixtures = await createCallAndInvite(browser); - await puse({ + await pUse({ creatorPage: fixtures.page, inviteLink: fixtures.inviteLink, }); From 7a2c1af44b14cce336eb91b3664501fb9a874ed8 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 4 Dec 2025 10:36:57 +0100 Subject: [PATCH 6/7] review: use simple transition instead of keyframe --- src/room/EarpieceOverlay.module.css | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/src/room/EarpieceOverlay.module.css b/src/room/EarpieceOverlay.module.css index 1718b0f2..e53a1974 100644 --- a/src/room/EarpieceOverlay.module.css +++ b/src/room/EarpieceOverlay.module.css @@ -2,39 +2,22 @@ position: fixed; z-index: var(--call-view-overlay-layer); inset: 0; - display: none; + display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--cpd-space-2x); -} - -@keyframes fade-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + transition: opacity 200ms; } .overlay[data-show="true"] { - display: flex; - animation: fade-in 200ms; -} - -@keyframes fade-out { - from { - opacity: 1; - } - to { - opacity: 0; - } + opacity: 1; } .overlay[data-show="false"] { - animation: fade-out 130ms forwards; + opacity: 0; pointer-events: none; + transition-duration: 130ms; } .overlay::before { From 71bf55f3581438245210f229a13f372a18a3f473 Mon Sep 17 00:00:00 2001 From: Valere Date: Thu, 4 Dec 2025 10:37:14 +0100 Subject: [PATCH 7/7] also test that video is muted when earpiece overlay is on --- playwright/mobile/create-call-mobile.spec.ts | 101 ++++++++++--------- 1 file changed, 52 insertions(+), 49 deletions(-) diff --git a/playwright/mobile/create-call-mobile.spec.ts b/playwright/mobile/create-call-mobile.spec.ts index 9005d510..141ffaae 100644 --- a/playwright/mobile/create-call-mobile.spec.ts +++ b/playwright/mobile/create-call-mobile.spec.ts @@ -52,61 +52,64 @@ test("@mobile Start a new call then leave and show the feedback screen", async ( ).toBeVisible(); }); -mobileTest("Start a new call as widget", async ({ asMobile, browser }) => { - test.slow(); // Triples the timeout - const { creatorPage, inviteLink } = asMobile; +mobileTest( + "Test earpiece overlay in controlledAudioDevices mode", + async ({ asMobile, browser }) => { + test.slow(); // Triples the timeout + const { creatorPage, inviteLink } = asMobile; - // test("Show earpiece overlay when output is earpiece", async ({ browser }) => { - // Use reduce motion to disable animations that are making the tests a bit flaky + // ======== + // ACT: The other user use the invite link to join the call as a guest + // ======== + const guestInviteeContext = await browser.newContext({ + reducedMotion: "reduce", + }); + const guestPage = await guestInviteeContext.newPage(); + await guestPage.goto(inviteLink + "&controlledAudioDevices=true"); - // ======== - // ACT: The other user use the invite link to join the call as a guest - // ======== - const guestInviteeContext = await browser.newContext({ - reducedMotion: "reduce", - }); - const guestPage = await guestInviteeContext.newPage(); - await guestPage.goto(inviteLink + "&controlledAudioDevices=true"); + await guestPage + .getByRole("button", { name: "Continue in browser" }) + .click(); - await guestPage.getByRole("button", { name: "Continue in browser" }).click(); + await guestPage.getByTestId("joincall_displayName").fill("Invitee"); + await expect(guestPage.getByTestId("joincall_joincall")).toBeVisible(); + await guestPage.getByTestId("joincall_joincall").click(); + await guestPage.getByTestId("lobby_joinCall").click(); - await guestPage.getByTestId("joincall_displayName").fill("Invitee"); - await expect(guestPage.getByTestId("joincall_joincall")).toBeVisible(); - await guestPage.getByTestId("joincall_joincall").click(); - await guestPage.getByTestId("lobby_joinCall").click(); + // ======== + // ASSERT: check that there are two members in the call + // ======== - // ======== - // ASSERT: check that there are two members in the call - // ======== + // There should be two participants now + await expect( + guestPage.getByTestId("roomHeader_participants_count"), + ).toContainText("2"); + expect(await guestPage.getByTestId("videoTile").count()).toBe(2); - // There should be two participants now - await expect( - guestPage.getByTestId("roomHeader_participants_count"), - ).toContainText("2"); - expect(await guestPage.getByTestId("videoTile").count()).toBe(2); + // Same in creator page + await expect( + creatorPage.getByTestId("roomHeader_participants_count"), + ).toContainText("2"); + expect(await creatorPage.getByTestId("videoTile").count()).toBe(2); - // Same in creator page - await expect( - creatorPage.getByTestId("roomHeader_participants_count"), - ).toContainText("2"); - expect(await creatorPage.getByTestId("videoTile").count()).toBe(2); + // TEST: control audio devices from the invitee page - // TEST: control audio devices from the invitee page + await guestPage.evaluate(() => { + window.controls.setAvailableAudioDevices([ + { id: "speaker", name: "Speaker", isSpeaker: true }, + { id: "earpiece", name: "Handset", isEarpiece: true }, + { id: "headphones", name: "Headphones" }, + ]); + window.controls.setAudioDevice("earpiece"); + }); + await expect( + guestPage.getByRole("heading", { name: "Handset Mode" }), + ).toBeVisible(); + await expect( + guestPage.getByRole("button", { name: "Back to Speaker Mode" }), + ).toBeVisible(); - await guestPage.evaluate(() => { - window.controls.setAvailableAudioDevices([ - { id: "speaker", name: "Speaker", isSpeaker: true }, - { id: "earpiece", name: "Handset", isEarpiece: true }, - { id: "headphones", name: "Headphones" }, - ]); - window.controls.setAudioDevice("earpiece"); - }); - await expect( - guestPage.getByRole("heading", { name: "Handset Mode" }), - ).toBeVisible(); - await expect( - guestPage.getByRole("button", { name: "Back to Speaker Mode" }), - ).toBeVisible(); - - // await guestPage.pause(); -}); + // Should auto-mute the video when earpiece is selected + await expect(guestPage.getByTestId("incall_videomute")).toBeDisabled(); + }, +);