fix: Regression on default mutestate for voicecall + end-2-end tests

This commit is contained in:
Valere
2026-01-09 12:00:45 +01:00
parent f5f8bb549a
commit a9153f2781
12 changed files with 467 additions and 159 deletions

View File

@@ -17,6 +17,7 @@ import type { MatrixClient } from "matrix-js-sdk";
export type UserBaseFixture = {
mxId: string;
displayName: string;
page: Page;
clientHandle: JSHandle<MatrixClient>;
};
@@ -28,6 +29,7 @@ export type BaseWidgetSetup = {
export interface MyFixtures {
asWidget: BaseWidgetSetup;
callType: "room" | "dm";
}
const PASSWORD = "foobarbaz1!";
@@ -145,25 +147,27 @@ async function registerUser(
}
export const widgetTest = test.extend<MyFixtures>({
asWidget: async ({ browser, context }, pUse) => {
// allow per-test override: `widgetTest.use({ callType: "dm" })`
callType: ["room", { option: true }],
asWidget: async ({ browser, context, callType }, pUse) => {
await context.route(`http://localhost:8081/config.json*`, async (route) => {
await route.fulfill({ json: CONFIG_JSON });
});
const userA = `brooks_${Date.now()}`;
const userB = `whistler_${Date.now()}`;
const brooksDisplayName = `brooks_${Date.now()}`;
const whistlerDisplayName = `whistler_${Date.now()}`;
// Register users
const {
page: ewPage1,
clientHandle: brooksClientHandle,
mxId: brooksMxId,
} = await registerUser(browser, userA);
} = await registerUser(browser, brooksDisplayName);
const {
page: ewPage2,
clientHandle: whistlerClientHandle,
mxId: whistlerMxId,
} = await registerUser(browser, userB);
} = await registerUser(browser, whistlerDisplayName);
// Invite the second user
await ewPage1
@@ -171,37 +175,60 @@ export const widgetTest = test.extend<MyFixtures>({
.getByRole("button", { name: "New conversation" })
.click();
await ewPage1.getByRole("menuitem", { name: "New Room" }).click();
await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room");
await ewPage1.getByRole("button", { name: "Create room" }).click();
await expect(ewPage1.getByText("You created this room.")).toBeVisible();
await expect(ewPage1.getByText("Encryption enabled")).toBeVisible();
if (callType === "room") {
await ewPage1
.getByRole("button", { name: "Invite to this room", exact: true })
.click();
await expect(
ewPage1.getByRole("heading", { name: "Invite to Welcome Room" }),
).toBeVisible();
await ewPage1.getByRole("menuitem", { name: "New Room" }).click();
await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room");
await ewPage1.getByRole("button", { name: "Create room" }).click();
await expect(ewPage1.getByText("You created this room.")).toBeVisible();
await expect(ewPage1.getByText("Encryption enabled")).toBeVisible();
// To get the invite textbox we need to specifically select within the
// dialog, since there is another textbox in the background (the message
// composer). In theory the composer shouldn't be visible to Playwright at
// all because the invite dialog has trapped focus, but the focus trap
// doesn't quite work right on Firefox.
await ewPage1.getByRole("dialog").getByRole("textbox").fill(whistlerMxId);
await ewPage1.getByRole("dialog").getByRole("textbox").click();
await ewPage1.getByRole("button", { name: "Invite" }).click();
await ewPage1
.getByRole("button", { name: "Invite to this room", exact: true })
.click();
await expect(
ewPage1.getByRole("heading", { name: "Invite to Welcome Room" }),
).toBeVisible();
// Accept the invite
await expect(
ewPage2.getByRole("option", { name: "Welcome Room" }),
).toBeVisible();
await ewPage2.getByRole("option", { name: "Welcome Room" }).click();
await ewPage2.getByRole("button", { name: "Accept" }).click();
await expect(
ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }),
).toBeVisible();
// To get the invite textbox we need to specifically select within the
// dialog, since there is another textbox in the background (the message
// composer). In theory the composer shouldn't be visible to Playwright at
// all because the invite dialog has trapped focus, but the focus trap
// doesn't quite work right on Firefox.
await ewPage1.getByRole("dialog").getByRole("textbox").fill(whistlerMxId);
await ewPage1.getByRole("dialog").getByRole("textbox").click();
await ewPage1.getByRole("button", { name: "Invite" }).click();
// Accept the invite
await expect(
ewPage2.getByRole("option", { name: "Welcome Room" }),
).toBeVisible();
await ewPage2.getByRole("option", { name: "Welcome Room" }).click();
await ewPage2.getByRole("button", { name: "Accept" }).click();
await expect(
ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }),
).toBeVisible();
} else if (callType === "dm") {
await ewPage1.getByRole("menuitem", { name: "Start chat" }).click();
await ewPage1.getByRole('textbox', { name: 'Search' }).click();
await ewPage1.getByRole('textbox', { name: 'Search' }).fill(whistlerMxId);
await ewPage1.getByRole("button", { name: "Go" }).click();
// Wait and send the first message to create the DM
await expect(ewPage1.getByText(/Send your first message to invite/)).toBeVisible();
await ewPage1.locator('.mx_BasicMessageComposer_input > div').click();
await ewPage1.getByRole('textbox', { name: 'Send a message…' }).fill('Hello!');
await ewPage1.getByRole("button", { name: "Send message" }).click();
await expect(ewPage1.getByText('This is the beginning of your')).toBeVisible();
// Accept the DM invite from brooks
// This how playwright record selects the DM invite in the room list
await ewPage2.getByRole('option', { name: 'Open room' }).click();
await ewPage2.getByRole('button', { name: 'Start chatting' }).click();
}
// Renamed use to pUse, as a workaround for eslint error that was thinking this use was a react use.
await pUse({
@@ -209,11 +236,13 @@ export const widgetTest = test.extend<MyFixtures>({
mxId: brooksMxId,
page: ewPage1,
clientHandle: brooksClientHandle,
displayName: brooksDisplayName
},
whistler: {
mxId: whistlerMxId,
page: ewPage2,
clientHandle: whistlerClientHandle,
displayName: whistlerDisplayName
},
});
},

View File

@@ -0,0 +1,183 @@
/*
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";
import { widgetTest } from "../fixtures/widget-user.ts";
widgetTest.use({callType: "dm"});
widgetTest("Start a new voice call in DM as widget", async ({ asWidget }) => {
test.slow(); // Triples the timeout
const { brooks, whistler } = asWidget;
await expect(
brooks.page.getByRole("button", { name: "Voice call" }),
).toBeVisible();
await brooks.page.getByRole("button", { name: "Voice call" }).click();
await expect(
brooks.page.getByRole("menuitem", { name: "Element Call" }),
).toBeVisible();
await brooks.page.getByRole("menuitem", { name: "Element Call" }).click();
await expect(
brooks.page
.locator('iframe[title="Element Call"]')
).toBeVisible();
const brooksFrame = brooks.page
.locator('iframe[title="Element Call"]')
.contentFrame();
// We should show a ringing overlay, let's check for that
await expect(brooksFrame.getByText(`Waiting for ${whistler.displayName} to join…`)).toBeVisible();
await expect(whistler.page.getByText('Incoming voice call')).toBeVisible();
await whistler.page.getByRole('button', { name: 'Accept' }).click();
await expect(
whistler.page
.locator('iframe[title="Element Call"]')
).toBeVisible();
const whistlerFrame = whistler.page
.locator('iframe[title="Element Call"]')
.contentFrame();
// ASSERT the button states for whistler (the callee)
{
// The only way to know if it is muted or not is to look at the data-kind attribute..
const videoButton = whistlerFrame.getByTestId('incall_videomute');
// video should be off by default in a voice call
await expect(videoButton).toHaveAttribute("aria-label", /^Start video$/);
const audioButton = whistlerFrame.getByTestId('incall_mute');
// audio should be on for the voice call
await expect(audioButton).toHaveAttribute("aria-label", /^Mute microphone$/);
}
// ASSERT the button states for brools (the caller)
{
// The only way to know if it is muted or not is to look at the data-kind attribute..
const videoButton = brooksFrame.getByTestId('incall_videomute');
// video should be off by default in a voice call
await expect(videoButton).toHaveAttribute("aria-label", /^Start video$/);
const audioButton = brooksFrame.getByTestId('incall_mute');
// audio should be on for the voice call
await expect(audioButton).toHaveAttribute("aria-label", /^Mute microphone$/);
}
// In order to confirm that the call is disconnected we will check that the message composer is shown again.
// So first we need to confirm that it is hidden when in the call.
await expect(whistler.page.locator(".mx_BasicMessageComposer")).not.toBeVisible();
await expect(brooks.page.locator(".mx_BasicMessageComposer")).not.toBeVisible();
// ASSERT hanging up on one side ends the call for both
{
const hangupButton = brooksFrame.getByTestId('incall_leave');
await hangupButton.click();
}
// The widget should be closed on both sides and the timeline should be back on screen
await expect(whistler.page.locator(".mx_BasicMessageComposer")).toBeVisible();
await expect(brooks.page.locator(".mx_BasicMessageComposer")).toBeVisible();
});
widgetTest("Start a new video call in DM as widget", async ({ asWidget, browserName }) => {
test.slow(); // Triples the timeout
const { brooks, whistler } = asWidget;
await expect(
brooks.page.getByRole("button", { name: "Video call" }),
).toBeVisible();
await brooks.page.getByRole("button", { name: "Video call" }).click();
await expect(
brooks.page.getByRole("menuitem", { name: "Element Call" }),
).toBeVisible();
await brooks.page.getByRole("menuitem", { name: "Element Call" }).click();
await expect(
brooks.page
.locator('iframe[title="Element Call"]')
).toBeVisible();
const brooksFrame = brooks.page
.locator('iframe[title="Element Call"]')
.contentFrame();
// We should show a ringing overlay, let's check for that
await expect(brooksFrame.getByText(`Waiting for ${whistler.displayName} to join…`)).toBeVisible();
await expect(whistler.page.getByText('Incoming video call')).toBeVisible();
await whistler.page.getByRole('button', { name: 'Accept' }).click();
await expect(
whistler.page
.locator('iframe[title="Element Call"]')
).toBeVisible();
const whistlerFrame = whistler.page
.locator('iframe[title="Element Call"]')
.contentFrame();
// ASSERT the button states for whistler (the callee)
{
// The only way to know if it is muted or not is to look at the data-kind attribute..
const videoButton = whistlerFrame.getByTestId('incall_videomute');
// video should be on by default in a voice call
await expect(videoButton).toHaveAttribute("aria-label", /^Stop video$/);
const audioButton = whistlerFrame.getByTestId('incall_mute');
// audio should be on for the voice call
await expect(audioButton).toHaveAttribute("aria-label", /^Mute microphone$/);
}
// ASSERT the button states for brools (the caller)
{
// The only way to know if it is muted or not is to look at the data-kind attribute..
const videoButton = brooksFrame.getByTestId('incall_videomute');
// video should be on by default in a voice call
await expect(videoButton).toHaveAttribute("aria-label", /^Stop video$/);
const audioButton = brooksFrame.getByTestId('incall_mute');
// audio should be on for the voice call
await expect(audioButton).toHaveAttribute("aria-label", /^Mute microphone$/);
}
// In order to confirm that the call is disconnected we will check that the message composer is shown again.
// So first we need to confirm that it is hidden when in the call.
await expect(whistler.page.locator(".mx_BasicMessageComposer")).not.toBeVisible();
await expect(brooks.page.locator(".mx_BasicMessageComposer")).not.toBeVisible();
// ASSERT hanging up on one side ends the call for both
{
const hangupButton = brooksFrame.getByTestId('incall_leave');
await hangupButton.click();
}
// The widget should be closed on both sides and the timeline should be back on screen
await expect(whistler.page.locator(".mx_BasicMessageComposer")).toBeVisible();
await expect(brooks.page.locator(".mx_BasicMessageComposer")).toBeVisible();
});