diff --git a/.github/workflows/meet-e2e.yml b/.github/workflows/meet-e2e.yml index 2c89c8d1a9..d7457bdcfa 100644 --- a/.github/workflows/meet-e2e.yml +++ b/.github/workflows/meet-e2e.yml @@ -29,7 +29,13 @@ concurrency: jobs: e2e: runs-on: ubuntu-latest - name: E2E Tests + # Playwright --shard=N/M splits the suite; each job is self-contained. + strategy: + fail-fast: false + matrix: + shardIndex: [1, 2, 3] + shardTotal: [3] + name: E2E Tests (${{ matrix.shardIndex }}/${{ matrix.shardTotal }}) timeout-minutes: 60 services: @@ -170,12 +176,13 @@ jobs: PORT: 3000 HOST: 0.0.0.0 JWT_SECRET: e2e-test-secret-key-12345 + WEBRTC_LISTEN_IP: 127.0.0.1 WEBRTC_ANNOUNCED_IP: 127.0.0.1 MEDIASOUP_NUM_WORKERS: 1 - name: Run E2E Tests working-directory: ${{ github.workspace }}/suite/meet/e2e - run: yarn test + run: yarn test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} env: BASE_URL: http://meet.test:8000 SFU_URL: http://localhost:3000 @@ -185,9 +192,10 @@ jobs: uses: actions/upload-artifact@v4 if: failure() with: - name: meet-playwright-report + name: meet-playwright-report-shard-${{ matrix.shardIndex }} path: | suite/meet/e2e/playwright-report/ suite/meet/e2e/test-results/ + suite/meet/e2e/results.xml compression-level: 1 retention-days: 7 diff --git a/suite/meet/e2e/fixtures/media.ts b/suite/meet/e2e/fixtures/media.ts index 86964ef9bc..0b5f853040 100644 --- a/suite/meet/e2e/fixtures/media.ts +++ b/suite/meet/e2e/fixtures/media.ts @@ -1,3 +1,7 @@ +/** + * Camera/mic: Chrome --use-fake-device-for-media-stream (see playwright.config). + * Screen share: Chrome has no fake display device — stub getDisplayMedia only. + */ export const STUB_MEDIA_SCRIPT = `(() => { window.localStorage.setItem("mediaPref.autoHideToolbar", "0"); @@ -8,54 +12,31 @@ export const STUB_MEDIA_SCRIPT = `(() => { }); } - function createFakeStream(label) { + navigator.mediaDevices.getDisplayMedia = async () => { const canvas = document.createElement("canvas"); - canvas.width = 1280; - canvas.height = 720; + canvas.width = 640; + canvas.height = 360; const context = canvas.getContext("2d"); let tick = 0; - const draw = () => { - if (!context) { - return; - } + if (!context) return; context.fillStyle = "#111827"; context.fillRect(0, 0, canvas.width, canvas.height); context.fillStyle = "#f9fafb"; - context.font = "32px sans-serif"; - context.fillText(label, 48, 80); - context.font = "20px monospace"; - context.fillText(String(++tick), 48, 128); - requestAnimationFrame(draw); + context.font = "24px sans-serif"; + context.fillText("screen", 24, 48); + context.fillText(String(++tick), 24, 80); }; - draw(); - const stream = canvas.captureStream(12); - - try { - const AudioContextCtor = window.AudioContext || window.webkitAudioContext; - if (AudioContextCtor) { - const audioContext = new AudioContextCtor(); - const oscillator = audioContext.createOscillator(); - const gainNode = audioContext.createGain(); - const destination = audioContext.createMediaStreamDestination(); - gainNode.gain.value = 0.001; - oscillator.connect(gainNode); - gainNode.connect(destination); - oscillator.start(); - for (const track of destination.stream.getAudioTracks()) { - stream.addTrack(track); - } - } - } catch {} - + const intervalId = window.setInterval(draw, 1000 / 12); + for (const track of stream.getVideoTracks()) { + track.addEventListener( + "ended", + () => window.clearInterval(intervalId), + { once: true }, + ); + } return stream; - } - - const userStream = createFakeStream("camera"); - const displayStream = createFakeStream("screen"); - - navigator.mediaDevices.getUserMedia = async () => userStream.clone(); - navigator.mediaDevices.getDisplayMedia = async () => displayStream.clone(); + }; })();`; diff --git a/suite/meet/e2e/fixtures/test.ts b/suite/meet/e2e/fixtures/test.ts index 967f9be7ed..06103b1bbd 100644 --- a/suite/meet/e2e/fixtures/test.ts +++ b/suite/meet/e2e/fixtures/test.ts @@ -5,11 +5,13 @@ import { type BrowserContext, type Page, } from "@playwright/test"; -import * as fs from "node:fs"; -import { MEETINGS_STATE_FILE, type MeetingsState } from "../global-setup"; import { STUB_MEDIA_SCRIPT } from "./media"; import { loginViaApi } from "../helpers/auth"; -import { clearMeetingCreateRateLimit, createMeetingViaApi, type MeetingType } from "../helpers/meeting"; +import { + clearMeetingCreateRateLimit, + createMeetingViaApi, + type MeetingType, +} from "../helpers/meeting"; const isCI = !!process.env.CI; const previewTimeout = isCI ? 45_000 : 20_000; @@ -20,11 +22,6 @@ function appUrl(pathname: string): string { return new URL(pathname, baseURL).toString(); } -function readMeetingsState(): MeetingsState { - const raw = fs.readFileSync(MEETINGS_STATE_FILE, "utf-8"); - return JSON.parse(raw) as MeetingsState; -} - interface Participant { context: BrowserContext; page: Page; @@ -36,8 +33,6 @@ interface Participant { interface TestFixtures { hostPage: Page; - meetingId: string; - restrictedMeetingId: string; createMeeting: (meetingType?: MeetingType) => Promise; createMeetingViaUi: (meetingType?: MeetingType) => Promise; createParticipant: () => Promise; @@ -86,6 +81,22 @@ async function joinFromPreview(page: Page): Promise { await waitForMeetingReady(page); } +/** Host and guest join the same open meeting concurrently. */ +async function joinHostAndGuest( + hostPage: Page, + guest: Participant, + meetingId: string, + guestName: string, +): Promise { + await Promise.all([ + (async () => { + await hostPage.goto(appUrl(`/meet/${meetingId}`)); + await joinFromPreview(hostPage); + })(), + guest.joinAsGuest(meetingId, guestName), + ]); +} + async function createMeetingViaUi( page: Page, meetingType: MeetingType = "open", @@ -158,26 +169,26 @@ export const test = base.extend({ await context.close(); }, - createMeeting: async ({ hostPage }, use) => { + // API-only meeting create so tests do not share rooms across workers. + createMeeting: async ({ playwright }, use) => { + const api = await playwright.request.newContext({ baseURL }); + await loginViaApi(api); + await use(async (meetingType = "open") => { - return createMeetingViaApi(hostPage.request, meetingType); + await clearMeetingCreateRateLimit(api); + return createMeetingViaApi(api, meetingType); }); + + await api.dispose(); }, createMeetingViaUi: async ({ hostPage }, use) => { await use(async (meetingType = "open") => { + await clearMeetingCreateRateLimit(hostPage.request); return createMeetingViaUi(hostPage, meetingType); }); }, - meetingId: async ({}, use) => { - await use(readMeetingsState().openMeetingId); - }, - - restrictedMeetingId: async ({}, use) => { - await use(readMeetingsState().restrictedMeetingId); - }, - createParticipant: async ({ browser }, use) => { const participants: Participant[] = []; @@ -193,9 +204,6 @@ export const test = base.extend({ }, }); -test.beforeEach(async ({ hostPage }) => { - await clearMeetingCreateRateLimit(hostPage.request); -}); - -export { expect, joinFromPreview }; +export { expect, joinFromPreview, joinHostAndGuest }; export { appUrl }; +export type { Participant }; diff --git a/suite/meet/e2e/global-setup.ts b/suite/meet/e2e/global-setup.ts index f2ef2e7419..83f38b89b9 100644 --- a/suite/meet/e2e/global-setup.ts +++ b/suite/meet/e2e/global-setup.ts @@ -1,9 +1,4 @@ -import * as fs from "node:fs"; -import * as path from "node:path"; -import { request } from "@playwright/test"; import type { FullConfig } from "@playwright/test"; -import { loginViaApi } from "./helpers/auth"; -import { createMeetingViaApi } from "./helpers/meeting"; async function waitForService(url: string, name: string): Promise { for (let attempt = 0; attempt < 40; attempt += 1) { @@ -20,13 +15,6 @@ async function waitForService(url: string, name: string): Promise { throw new Error(`${name} did not become ready: ${url}`); } -export const MEETINGS_STATE_FILE = path.join(__dirname, ".state", "meetings.json"); - -export interface MeetingsState { - openMeetingId: string; - restrictedMeetingId: string; -} - export default async function globalSetup(config: FullConfig): Promise { const projectUse = config.projects[0]?.use ?? {}; const baseURL = String( @@ -37,21 +25,7 @@ export default async function globalSetup(config: FullConfig): Promise { ? sfuBaseURL : `${sfuBaseURL.replace(/\/$/, "")}/health`; + // Meetings are created per-test for worker isolation; only health-check here. await waitForService(baseURL, "Frappe Meet"); await waitForService(sfuHealthURL, "SFU server"); - - // Create shared meetings once for the entire test run - const apiContext = await request.newContext({ baseURL }); - await loginViaApi(apiContext); - const openMeetingId = await createMeetingViaApi(apiContext, "open"); - const restrictedMeetingId = await createMeetingViaApi(apiContext, "restricted"); - await apiContext.dispose(); - - const stateDir = path.dirname(MEETINGS_STATE_FILE); - if (!fs.existsSync(stateDir)) { - fs.mkdirSync(stateDir, { recursive: true }); - } - - const state: MeetingsState = { openMeetingId, restrictedMeetingId }; - fs.writeFileSync(MEETINGS_STATE_FILE, JSON.stringify(state, null, 2)); } diff --git a/suite/meet/e2e/helpers/media.ts b/suite/meet/e2e/helpers/media.ts index 86a3ce66d4..7ab5a83ccd 100644 --- a/suite/meet/e2e/helpers/media.ts +++ b/suite/meet/e2e/helpers/media.ts @@ -15,6 +15,19 @@ export async function expectRemoteVideoReceiving( export async function expectVideoReceiving(video: Locator): Promise { await expect(video).toBeVisible({ timeout: 45_000 }); + await video.evaluate(async (element) => { + const videoEl = element as HTMLVideoElement; + videoEl.muted = true; + if (videoEl.paused) { + try { + await videoEl.play(); + } catch { + // Poll below is the real assertion. + } + } + }); + + // Live track + real decode (not metadata-only dimensions). await expect .poll( async () => @@ -23,36 +36,18 @@ export async function expectVideoReceiving(video: Locator): Promise { const quality = videoEl.getVideoPlaybackQuality?.(); const stream = videoEl.srcObject as MediaStream | null; const videoTrack = stream?.getVideoTracks()[0] ?? null; + const decodedFrames = quality?.totalVideoFrames ?? 0; + const hasDecodedPlayback = + decodedFrames > 0 || videoEl.currentTime > 0; return { - currentTime: videoEl.currentTime, - decodedFrames: quality?.totalVideoFrames ?? 0, - height: videoEl.videoHeight, - readyState: videoEl.readyState, + ok: + videoTrack?.readyState === "live" && + videoEl.readyState >= 2 && + hasDecodedPlayback, trackState: videoTrack?.readyState ?? null, - width: videoEl.videoWidth, }; }), { timeout: 45_000 }, ) - .toMatchObject({ - readyState: 4, - trackState: "live", - }); - - await expect - .poll( - async () => - video.evaluate((element) => { - const videoEl = element as HTMLVideoElement; - const quality = videoEl.getVideoPlaybackQuality?.(); - return ( - videoEl.currentTime > 0 && - (quality?.totalVideoFrames ?? 0) > 0 && - videoEl.videoWidth > 0 && - videoEl.videoHeight > 0 - ); - }), - { timeout: 45_000 }, - ) - .toBe(true); + .toMatchObject({ ok: true, trackState: "live" }); } diff --git a/suite/meet/e2e/playwright.config.ts b/suite/meet/e2e/playwright.config.ts index cc8b932720..3c5c58c6ef 100644 --- a/suite/meet/e2e/playwright.config.ts +++ b/suite/meet/e2e/playwright.config.ts @@ -1,19 +1,21 @@ import { defineConfig, devices } from "@playwright/test"; const baseURL = process.env.BASE_URL ?? "http://localhost:8098"; +const isCI = !!process.env.CI; export default defineConfig({ testDir: "./specs", - fullyParallel: false, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: 1, - maxFailures: process.env.CI ? 3 : undefined, - timeout: 60_000, + // CI: one worker per job; --shard=N/M on GH splits the suite automatically. + fullyParallel: !isCI, + forbidOnly: isCI, + retries: isCI ? 2 : 0, + workers: isCI ? 1 : undefined, + maxFailures: isCI ? 3 : undefined, + timeout: isCI ? 90_000 : 60_000, expect: { timeout: 10_000, }, - reporter: process.env.CI + reporter: isCI ? [ ["list"], ["github"], @@ -28,7 +30,7 @@ export default defineConfig({ screenshot: "only-on-failure", viewport: { width: 1440, height: 900 }, actionTimeout: 15_000, - navigationTimeout: 30_000, + navigationTimeout: 30_000, }, projects: [ { @@ -39,8 +41,7 @@ export default defineConfig({ launchOptions: { args: [ "--use-fake-ui-for-media-stream", - "--disable-audio-track-processing", - "--disable-webrtc-apm-in-audio-service", + "--use-fake-device-for-media-stream", "--allow-insecure-localhost", "--autoplay-policy=no-user-gesture-required", `--unsafely-treat-insecure-origin-as-secure=${baseURL}`, diff --git a/suite/meet/e2e/specs/chat.spec.ts b/suite/meet/e2e/specs/chat.spec.ts index f1bc30bf9e..f3eb4deb8b 100644 --- a/suite/meet/e2e/specs/chat.spec.ts +++ b/suite/meet/e2e/specs/chat.spec.ts @@ -1,13 +1,21 @@ -import { test, expect, joinFromPreview } from "../fixtures/test"; +import { test, expect, joinHostAndGuest } from "../fixtures/test"; test.describe("Chat", () => { - test("messages are delivered between host and guest", async ({ hostPage, meetingId, createParticipant }) => { + test("messages are delivered between host and guest", async ({ + hostPage, + createMeeting, + createParticipant, + }) => { + const meetingId = await createMeeting(); const guest = await createParticipant(); const message = `hello-${test.info().parallelIndex}`; - await hostPage.goto(`/meet/${meetingId}`); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, `Guest Chat ${test.info().parallelIndex}`); + await joinHostAndGuest( + hostPage, + guest, + meetingId, + `Guest Chat ${test.info().parallelIndex}`, + ); await hostPage.getByTestId("toolbar-chat").click(); await hostPage.getByTestId("chat-input-wrapper").click(); @@ -22,15 +30,19 @@ test.describe("Chat", () => { test("unread badge appears when chat is closed and clears when opened", async ({ hostPage, - meetingId, + createMeeting, createParticipant, }) => { + const meetingId = await createMeeting(); const guest = await createParticipant(); const message = `unread-${test.info().parallelIndex}`; - await hostPage.goto(`/meet/${meetingId}`); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, `Guest Unread ${test.info().parallelIndex}`); + await joinHostAndGuest( + hostPage, + guest, + meetingId, + `Guest Unread ${test.info().parallelIndex}`, + ); const chatButton = hostPage.getByTestId("toolbar-chat"); const unreadBadge = hostPage.getByTestId("toolbar-chat-unread"); diff --git a/suite/meet/e2e/specs/e2ee.spec.ts b/suite/meet/e2e/specs/e2ee.spec.ts index 7983ff4c74..371572206a 100644 --- a/suite/meet/e2e/specs/e2ee.spec.ts +++ b/suite/meet/e2e/specs/e2ee.spec.ts @@ -1,10 +1,31 @@ import type { Page } from "@playwright/test"; -import { test, expect, joinFromPreview, appUrl } from "../fixtures/test"; +import { + test, + expect, + joinFromPreview, + joinHostAndGuest, + appUrl, +} from "../fixtures/test"; import { expectRemoteVideoReceiving, expectVideoReceiving, } from "../helpers/media"; +async function expectParticipantsAndVideo( + hostPage: Page, + guestPage: Page, + guestName: string, +): Promise { + await expect(hostPage.locator("[data-participant-id]")).toHaveCount(2, { + timeout: 30_000, + }); + await expect(guestPage.locator("[data-participant-id]")).toHaveCount(2, { + timeout: 30_000, + }); + await expectRemoteVideoReceiving(guestPage, "Administrator"); + await expectRemoteVideoReceiving(hostPage, guestName); +} + async function openMeetingAccessSettings(page: Page): Promise { await page.getByTestId("toolbar-more").click(); await page.getByRole("menuitem", { name: "Settings" }).click(); @@ -81,29 +102,22 @@ function capturePageErrors(page: Page, filterPatterns: string[] = []) { } test.describe("E2EE", () => { - test("participants can join an E2EE meeting", async ({ + // Join first so media is healthy, then enable E2EE (avoids host-only avatar on CI). + test("participants keep video after E2EE is enabled", async ({ hostPage, createMeeting, createParticipant, }) => { const meetingId = await createMeeting(); + const guestName = "Guest E2EE"; + const guest = await createParticipant(); - await hostPage.goto(appUrl(`/meet/${meetingId}`)); - await joinFromPreview(hostPage); + await joinHostAndGuest(hostPage, guest, meetingId, guestName); + await expectParticipantsAndVideo(hostPage, guest.page, guestName); await enableE2EEInSettings(hostPage); - const guest = await createParticipant(); - await guest.joinAsGuest(meetingId, "Guest E2EE"); - - await expect(hostPage.locator("[data-participant-id]")).toHaveCount(2, { - timeout: 30_000, - }); - await expect(guest.page.locator("[data-participant-id]")).toHaveCount(2, { - timeout: 30_000, - }); - await expectRemoteVideoReceiving(guest.page, "Administrator"); - await expectRemoteVideoReceiving(hostPage, "Guest E2EE"); + await expectParticipantsAndVideo(hostPage, guest.page, guestName); }); test.describe("heavy coverage", () => { @@ -118,9 +132,7 @@ test.describe("E2EE", () => { const guestName = "Guest Convert E2EE"; const guest = await createParticipant(); - await hostPage.goto(appUrl(`/meet/${meetingId}`)); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, guestName); + await joinHostAndGuest(hostPage, guest, meetingId, guestName); await expectRemoteVideoReceiving(guest.page, "Administrator"); await expectRemoteVideoReceiving(hostPage, guestName); @@ -142,9 +154,7 @@ test.describe("E2EE", () => { const guestName = "Guest Rejoin E2EE"; const guest = await createParticipant(); - await hostPage.goto(appUrl(`/meet/${meetingId}`)); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, guestName); + await joinHostAndGuest(hostPage, guest, meetingId, guestName); await expectRemoteVideoReceiving(guest.page, "Administrator"); await expectRemoteVideoReceiving(hostPage, guestName); @@ -185,9 +195,7 @@ test.describe("E2EE", () => { const guestName = "Guest Reconnect E2EE"; const guest = await createParticipant(); - await hostPage.goto(appUrl(`/meet/${meetingId}`)); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, guestName); + await joinHostAndGuest(hostPage, guest, meetingId, guestName); await expectRemoteVideoReceiving(guest.page, "Administrator"); await expectRemoteVideoReceiving(hostPage, guestName); @@ -226,9 +234,7 @@ test.describe("E2EE", () => { const guestName = "Guest Host Rejoin E2EE"; const guest = await createParticipant(); - await hostPage.goto(appUrl(`/meet/${meetingId}`)); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, guestName); + await joinHostAndGuest(hostPage, guest, meetingId, guestName); await expectRemoteVideoReceiving(guest.page, "Administrator"); await expectRemoteVideoReceiving(hostPage, guestName); @@ -273,9 +279,7 @@ test.describe("E2EE", () => { const guestName = "Guest Screen E2EE"; const guest = await createParticipant(); - await hostPage.goto(appUrl(`/meet/${meetingId}`)); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, guestName); + await joinHostAndGuest(hostPage, guest, meetingId, guestName); await expectRemoteVideoReceiving(guest.page, "Administrator"); await expectRemoteVideoReceiving(hostPage, guestName); @@ -306,9 +310,7 @@ test.describe("E2EE", () => { const guestB = await createParticipant(); const guestC = await createParticipant(); - await hostPage.goto(appUrl(`/meet/${meetingId}`)); - await joinFromPreview(hostPage); - await guestA.joinAsGuest(meetingId, guestAName); + await joinHostAndGuest(hostPage, guestA, meetingId, guestAName); await expectRemoteVideoReceiving(guestA.page, "Administrator"); await expectRemoteVideoReceiving(hostPage, guestAName); diff --git a/suite/meet/e2e/specs/engagement.spec.ts b/suite/meet/e2e/specs/engagement.spec.ts index 6546b00e57..cfd11691ef 100644 --- a/suite/meet/e2e/specs/engagement.spec.ts +++ b/suite/meet/e2e/specs/engagement.spec.ts @@ -1,12 +1,20 @@ -import { test, expect, joinFromPreview } from "../fixtures/test"; +import { test, expect, joinHostAndGuest } from "../fixtures/test"; test.describe("Reactions and raise hand", () => { - test("guest reaction and raised hand are visible to the host", async ({ hostPage, meetingId, createParticipant }) => { + test("guest reaction and raised hand are visible to the host", async ({ + hostPage, + createMeeting, + createParticipant, + }) => { + const meetingId = await createMeeting(); const guest = await createParticipant(); - await hostPage.goto(`/meet/${meetingId}`); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, `Guest Engage ${test.info().parallelIndex}`); + await joinHostAndGuest( + hostPage, + guest, + meetingId, + `Guest Engage ${test.info().parallelIndex}`, + ); await guest.page.getByTestId("toolbar-reactions").click(); await guest.page.getByLabel("Send 👍 reaction").click(); @@ -18,6 +26,8 @@ test.describe("Reactions and raise hand", () => { await expect(hostPage.locator("[aria-label*='has raised their hand']")).toBeVisible(); await hostPage.getByTestId("toolbar-people").click(); - await expect(hostPage.getByTestId("people-panel").locator("[title*='has raised their hand']")).toBeVisible(); + await expect( + hostPage.getByTestId("people-panel").locator("[title*='has raised their hand']"), + ).toBeVisible(); }); }); diff --git a/suite/meet/e2e/specs/host-controls.spec.ts b/suite/meet/e2e/specs/host-controls.spec.ts index 441dfeea4a..97834c4397 100644 --- a/suite/meet/e2e/specs/host-controls.spec.ts +++ b/suite/meet/e2e/specs/host-controls.spec.ts @@ -1,13 +1,16 @@ -import { test, expect, joinFromPreview } from "../fixtures/test"; +import { test, expect, joinHostAndGuest } from "../fixtures/test"; test.describe("Host controls", () => { - test("host can mute a guest participant", async ({ hostPage, meetingId, createParticipant }) => { + test("host can mute a guest participant", async ({ + hostPage, + createMeeting, + createParticipant, + }) => { + const meetingId = await createMeeting(); const guest = await createParticipant(); const guestName = `Guest Mute ${test.info().parallelIndex}`; - await hostPage.goto(`/meet/${meetingId}`); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, guestName); + await joinHostAndGuest(hostPage, guest, meetingId, guestName); const guestSelfTile = guest.page .locator("[data-testid^='participant-tile-guest_']") .first(); @@ -37,17 +40,22 @@ test.describe("Host controls", () => { await expect(guestTileOnHost).toHaveAttribute("data-audio-enabled", "false"); await expect( - guest.page.locator("[data-testid^='participant-tile-guest_'][data-audio-enabled='false']"), + guest.page.locator( + "[data-testid^='participant-tile-guest_'][data-audio-enabled='false']", + ), ).toHaveCount(1); }); - test("host can remove a guest participant", async ({ hostPage, meetingId, createParticipant }) => { + test("host can remove a guest participant", async ({ + hostPage, + createMeeting, + createParticipant, + }) => { + const meetingId = await createMeeting(); const guest = await createParticipant(); const guestName = `Guest Remove ${test.info().parallelIndex}`; - await hostPage.goto(`/meet/${meetingId}`); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, guestName); + await joinHostAndGuest(hostPage, guest, meetingId, guestName); await hostPage.getByTestId("toolbar-people").click(); await expect(hostPage.getByTestId("people-panel")).toBeVisible(); diff --git a/suite/meet/e2e/specs/join-and-leave.spec.ts b/suite/meet/e2e/specs/join-and-leave.spec.ts index 2c3fc6af45..fa91d03ae5 100644 --- a/suite/meet/e2e/specs/join-and-leave.spec.ts +++ b/suite/meet/e2e/specs/join-and-leave.spec.ts @@ -1,7 +1,10 @@ import { test, expect, joinFromPreview } from "../fixtures/test"; test.describe("Joining and leaving", () => { - test("host can join a new meeting and leave it", async ({ hostPage, createMeetingViaUi }) => { + test("host can join a new meeting and leave it", async ({ + hostPage, + createMeetingViaUi, + }) => { const meetingId = await createMeetingViaUi(); await hostPage.goto(`/meet/${meetingId}`); @@ -13,7 +16,11 @@ test.describe("Joining and leaving", () => { await expect(hostPage.getByTestId("home-page")).toBeVisible(); }); - test("guest can join an open meeting from the preview", async ({ createParticipant, meetingId }) => { + test("guest can join an open meeting from the preview", async ({ + createMeeting, + createParticipant, + }) => { + const meetingId = await createMeeting(); const guest = await createParticipant(); await guest.joinAsGuest(meetingId, `Guest ${test.info().parallelIndex}`); diff --git a/suite/meet/e2e/specs/media-controls.spec.ts b/suite/meet/e2e/specs/media-controls.spec.ts index 1f66d32749..35d2e0a4e9 100644 --- a/suite/meet/e2e/specs/media-controls.spec.ts +++ b/suite/meet/e2e/specs/media-controls.spec.ts @@ -1,16 +1,24 @@ -import { test, expect, joinFromPreview } from "../fixtures/test"; +import { test, expect, joinHostAndGuest } from "../fixtures/test"; import { expectRemoteVideoReceiving, expectVideoReceiving, } from "../helpers/media"; test.describe("Media controls", () => { - test("camera and microphone toggles are reflected remotely", async ({ hostPage, meetingId, createParticipant }) => { + test("camera and microphone toggles are reflected remotely", async ({ + hostPage, + createMeeting, + createParticipant, + }) => { + const meetingId = await createMeeting(); const guest = await createParticipant(); - await hostPage.goto(`/meet/${meetingId}`); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, `Guest Media ${test.info().parallelIndex}`); + await joinHostAndGuest( + hostPage, + guest, + meetingId, + `Guest Media ${test.info().parallelIndex}`, + ); await expectRemoteVideoReceiving(guest.page, "Administrator"); await hostPage.getByTestId("toolbar-camera").click(); @@ -22,12 +30,20 @@ test.describe("Media controls", () => { await expect(hostTile).toHaveAttribute("data-video-enabled", "false"); }); - test("screen sharing creates a dedicated remote tile", async ({ hostPage, meetingId, createParticipant }) => { + test("screen sharing creates a dedicated remote tile", async ({ + hostPage, + createMeeting, + createParticipant, + }) => { + const meetingId = await createMeeting(); const guest = await createParticipant(); - await hostPage.goto(`/meet/${meetingId}`); - await joinFromPreview(hostPage); - await guest.joinAsGuest(meetingId, `Guest Screen ${test.info().parallelIndex}`); + await joinHostAndGuest( + hostPage, + guest, + meetingId, + `Guest Screen ${test.info().parallelIndex}`, + ); await hostPage.getByTestId("toolbar-screen-share").click(); diff --git a/suite/meet/e2e/specs/multi-participant.spec.ts b/suite/meet/e2e/specs/multi-participant.spec.ts index 4fbb35beea..7748729ee8 100644 --- a/suite/meet/e2e/specs/multi-participant.spec.ts +++ b/suite/meet/e2e/specs/multi-participant.spec.ts @@ -1,15 +1,26 @@ -import { test, expect, joinFromPreview } from "../fixtures/test"; +import { test, expect, joinFromPreview, appUrl } from "../fixtures/test"; import { expectRemoteVideoReceiving } from "../helpers/media"; test.describe("Multi participant", () => { - test("host and two guests see the same meeting", async ({ hostPage, meetingId, createParticipant }) => { + test("host and two guests see the same meeting", async ({ + hostPage, + createMeeting, + createParticipant, + }) => { + const meetingId = await createMeeting(); const guestOne = await createParticipant(); const guestTwo = await createParticipant(); + const guestOneName = `Guest One ${test.info().parallelIndex}`; + const guestTwoName = `Guest Two ${test.info().parallelIndex}`; - await hostPage.goto(`/meet/${meetingId}`); - await joinFromPreview(hostPage); - await guestOne.joinAsGuest(meetingId, `Guest One ${test.info().parallelIndex}`); - await guestTwo.joinAsGuest(meetingId, `Guest Two ${test.info().parallelIndex}`); + await Promise.all([ + (async () => { + await hostPage.goto(appUrl(`/meet/${meetingId}`)); + await joinFromPreview(hostPage); + })(), + guestOne.joinAsGuest(meetingId, guestOneName), + guestTwo.joinAsGuest(meetingId, guestTwoName), + ]); await expect(hostPage.locator("[data-participant-id]")).toHaveCount(3); await Promise.all([ diff --git a/suite/meet/e2e/specs/restricted-meeting.spec.ts b/suite/meet/e2e/specs/restricted-meeting.spec.ts index b859176a5f..92851dae99 100644 --- a/suite/meet/e2e/specs/restricted-meeting.spec.ts +++ b/suite/meet/e2e/specs/restricted-meeting.spec.ts @@ -1,21 +1,22 @@ -import { test, expect, joinFromPreview } from "../fixtures/test"; +import { test, expect, joinFromPreview, appUrl } from "../fixtures/test"; const lobbyTransitionTimeout = process.env.CI ? 60_000 : 30_000; test.describe("Restricted meeting", () => { test("guest waits for approval and host can admit from people panel", async ({ hostPage, - restrictedMeetingId, + createMeeting, createParticipant, }) => { - const meetingId = restrictedMeetingId; + const meetingId = await createMeeting("restricted"); const guest = await createParticipant(); const guestName = `Guest Restricted ${test.info().parallelIndex}`; - await hostPage.goto(`/meet/${meetingId}`); + // Host must be in the meeting before admit controls appear. + await hostPage.goto(appUrl(`/meet/${meetingId}`)); await joinFromPreview(hostPage); - await guest.page.goto(`/meet/${meetingId}`); + await guest.page.goto(appUrl(`/meet/${meetingId}`)); await expect(guest.page.getByTestId("meeting-preview")).toBeVisible(); const guestNameInput = guest.page.getByPlaceholder("John Doe"); await guestNameInput.fill(guestName); @@ -39,17 +40,17 @@ test.describe("Restricted meeting", () => { test("guest in restricted lobby can't join meeting when rejected", async ({ hostPage, - restrictedMeetingId, + createMeeting, createParticipant, }) => { - const meetingId = restrictedMeetingId; + const meetingId = await createMeeting("restricted"); const guest = await createParticipant(); const guestName = `Guest Rejected ${test.info().parallelIndex}-${test.info().retry}`; - await hostPage.goto(`/meet/${meetingId}`); + await hostPage.goto(appUrl(`/meet/${meetingId}`)); await joinFromPreview(hostPage); - await guest.page.goto(`/meet/${meetingId}`); + await guest.page.goto(appUrl(`/meet/${meetingId}`)); await expect(guest.page.getByTestId("meeting-preview")).toBeVisible(); const guestNameInput = guest.page.getByPlaceholder("John Doe"); await guestNameInput.fill(guestName); @@ -70,7 +71,7 @@ test.describe("Restricted meeting", () => { timeout: lobbyTransitionTimeout, }); - await guest.page.goto(`/meet/${meetingId}`); + await guest.page.goto(appUrl(`/meet/${meetingId}`)); await expect(guest.page.getByTestId("meeting-preview")).toBeVisible(); await guest.page.getByTestId("join-meeting-preview-button").click(); @@ -78,4 +79,4 @@ test.describe("Restricted meeting", () => { guest.page.getByRole("heading", { name: "Waiting to be admitted" }), ).toBeVisible({ timeout: lobbyTransitionTimeout }); }); -}); \ No newline at end of file +});