diff --git a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts index ae2003367bf..a5825bd428c 100644 --- a/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts +++ b/packages/react-composites/src/composites/CallComposite/MockCallAdapter.ts @@ -120,7 +120,7 @@ export class _MockCallAdapter implements CallAdapter { } /* @conditional-compile-remove(together-mode) */ setTogetherModeSceneSize(width: number, height: number): void { - throw Error(`Setting Together Mode scene to width ${width} and height ${height} is not implemented`); + return; } disposeStreamView(): Promise { return Promise.resolve(); diff --git a/packages/react-composites/tests/app/lib/MockCallAdapter.ts b/packages/react-composites/tests/app/lib/MockCallAdapter.ts index 9a46040ba17..d3c39710d37 100644 --- a/packages/react-composites/tests/app/lib/MockCallAdapter.ts +++ b/packages/react-composites/tests/app/lib/MockCallAdapter.ts @@ -125,7 +125,7 @@ export class MockCallAdapter implements CallAdapter { } /* @conditional-compile-remove(together-mode) */ setTogetherModeSceneSize(width: number, height: number): void { - throw Error(`Setting Together Mode width ${width} and height: ${height} not implemented`); + return; } /* @conditional-compile-remove(together-mode) */ disposeTogetherModeStreamView(): Promise { diff --git a/packages/react-composites/tests/browser/call/hermetic/TogetherModeOverlay.test.ts b/packages/react-composites/tests/browser/call/hermetic/TogetherModeOverlay.test.ts new file mode 100644 index 00000000000..7ca6c549a88 --- /dev/null +++ b/packages/react-composites/tests/browser/call/hermetic/TogetherModeOverlay.test.ts @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + // addScreenshareStream, + addTogetherModeStream, + addVideoStream, + buildUrlWithMockAdapter, + defaultMockCallAdapterState, + defaultMockRemoteParticipant, + test +} from './fixture'; +import { expect } from '@playwright/test'; +import { + dataUiId, + // dragToRight, + // existsOnPage, + isTestProfileMobile, + pageClick, + stableScreenshot, + waitForSelector +} from '../../common/utils'; +import { IDS } from '../../common/constants'; +import { CallKind } from '@azure/communication-calling'; +// import type { MockCallState } from '../../../common'; + +/* @conditional-compile-remove(together-mode) */ +test.describe('Confirm Start Together layout view option ', async () => { + test('Confirm together mode is not shown in ACS Call', async ({ page, serverUrl }, testInfo) => { + test.skip(isTestProfileMobile(testInfo)); + // Remote Participants is ACS Identity + const vasily = defaultMockRemoteParticipant('Vasily'); + const paul = defaultMockRemoteParticipant('Paul'); + const participants = [vasily, paul]; + addVideoStream(vasily, true); + addVideoStream(paul, true); + // Local Participant is ACS Identity + const initialState = defaultMockCallAdapterState(participants); + if (initialState.call) { + initialState.call.togetherMode.isActive = true; + } + await page.goto( + buildUrlWithMockAdapter(serverUrl, initialState, { + newControlBarExperience: 'true' + }) + ); + + await waitForSelector(page, dataUiId(IDS.moreButton)); + await pageClick(page, dataUiId(IDS.moreButton)); + await page.locator('button:has-text("View")').click(); + expect(await stableScreenshot(page)).toMatchSnapshot('together-mode-view-option-hidden-in-acs-call.png'); + }); + + test('Confirm together mode is enabled for Teams Call', async ({ page, serverUrl }, testInfo) => { + test.skip(isTestProfileMobile(testInfo)); + // Remote Participant is Teams Identity + const vasily = defaultMockRemoteParticipant('Vasily', true); + const paul = defaultMockRemoteParticipant('Paul', true); + const participants = [vasily, paul]; + addVideoStream(vasily, true); + addVideoStream(paul, true); + // Local Participant is Teams Identity + const initialState = defaultMockCallAdapterState(participants); + initialState.userId = { kind: 'microsoftTeamsUser', microsoftTeamsUserId: `8:orgid:localUser` }; + initialState.isTeamsCall = true; + if (initialState.call) { + initialState.call.togetherMode.isActive = true; + initialState.call.kind = 'TeamsCall' as CallKind; + } + await page.goto( + buildUrlWithMockAdapter(serverUrl, initialState, { + newControlBarExperience: 'true' + }) + ); + + await waitForSelector(page, dataUiId(IDS.moreButton)); + await pageClick(page, dataUiId(IDS.moreButton)); + await page.locator('button:has-text("View")').click(); + expect(await stableScreenshot(page)).toMatchSnapshot('together-mode-view-option-in-teams-call.png'); + }); + + test('Confirm together mode is enabled for Teams Meeting', async ({ page, serverUrl }, testInfo) => { + test.skip(isTestProfileMobile(testInfo)); + // Remote Participant is Teams Identity + const vasily = defaultMockRemoteParticipant('Vasily', true); + const paul = defaultMockRemoteParticipant('Paul', true); + const participants = [vasily, paul]; + addVideoStream(vasily, true); + addVideoStream(paul, true); + // Local Participant is Teams Identity + const initialState = defaultMockCallAdapterState(participants); + initialState.userId = { kind: 'microsoftTeamsUser', microsoftTeamsUserId: `8:orgid:localUser` }; + initialState.isTeamsMeeting = true; + if (initialState.call) { + initialState.call.togetherMode.isActive = true; + initialState.call.kind = 'TeamsCall' as CallKind; + } + await page.goto( + buildUrlWithMockAdapter(serverUrl, initialState, { + newControlBarExperience: 'true' + }) + ); + + await waitForSelector(page, dataUiId(IDS.moreButton)); + await pageClick(page, dataUiId(IDS.moreButton)); + await page.locator('button:has-text("View")').click(); + expect(await stableScreenshot(page)).toMatchSnapshot('together-mode-view-option-in-teams-meeting.png'); + }); +}); + +test.describe('Confirm Together Mode Stream signaling events', async () => { + test.only('Confirm raiseHand icon and display Name in together mode layout', async ({ + page, + serverUrl + }, testInfo) => { + test.skip(isTestProfileMobile(testInfo)); + const paul = defaultMockRemoteParticipant('Paul Bridges', true); + const vasily = defaultMockRemoteParticipant('Vasily'); + const participants = [paul, vasily]; + addVideoStream(vasily, true); + vasily.raisedHand = { raisedHandOrderPosition: 1 }; + addVideoStream(vasily, true); + const initialState = defaultMockCallAdapterState(participants); + + if (initialState.call?.togetherMode) { + initialState.call.togetherMode.isActive = true; + addTogetherModeStream(initialState.call.togetherMode.streams, true); + initialState.call.togetherMode.seatingPositions = { + '8:acs:Vasily-id': { left: 0, top: 0, width: 200, height: 200 } + }; + } + initialState.isTeamsCall = true; + if (initialState.call) { + initialState.call.kind = 'TeamsCall' as CallKind; + } + await page.goto( + buildUrlWithMockAdapter(serverUrl, initialState, { + newControlBarExperience: 'true' + }) + ); + + const id = `together-mode-participant-8:acs:Vasily-id`; + await waitForSelector(page, dataUiId(IDS.moreButton)); + await pageClick(page, dataUiId(IDS.moreButton)); + await page.locator('button:has-text("View")').click(); + await page.locator('button:has-text("Together mode")').click(); + await waitForSelector(page, dataUiId(IDS.togetherModeStream)); + await waitForSelector(page, dataUiId(id)); + expect(await stableScreenshot(page)).toMatchSnapshot('together-mode-view-raisehand-icon.png'); + }); + + test.only('Confirm spotlight icon and display Name in together mode layout', async ({ + page, + serverUrl + }, testInfo) => { + test.skip(isTestProfileMobile(testInfo)); + const paul = defaultMockRemoteParticipant('Paul Bridges', true); + const vasily = defaultMockRemoteParticipant('Vasily'); + const participants = [paul, vasily]; + addVideoStream(vasily, true); + vasily.spotlight = { spotlightedOrderPosition: 1 }; + addVideoStream(vasily, true); + const initialState = defaultMockCallAdapterState(participants); + + if (initialState.call?.togetherMode) { + initialState.call.togetherMode.isActive = true; + addTogetherModeStream(initialState.call.togetherMode.streams, true); + initialState.call.togetherMode.seatingPositions = { + '8:acs:Vasily-id': { left: 0, top: 0, width: 200, height: 200 } + }; + } + initialState.isTeamsCall = true; + if (initialState.call) { + initialState.call.kind = 'TeamsCall' as CallKind; + } + await page.goto( + buildUrlWithMockAdapter(serverUrl, initialState, { + newControlBarExperience: 'true' + }) + ); + + const id = `together-mode-participant-8:acs:Vasily-id`; + await waitForSelector(page, dataUiId(IDS.moreButton)); + await pageClick(page, dataUiId(IDS.moreButton)); + await page.locator('button:has-text("View")').click(); + await page.locator('button:has-text("Together mode")').click(); + await waitForSelector(page, dataUiId(IDS.togetherModeStream)); + await waitForSelector(page, dataUiId(id)); + expect(await stableScreenshot(page)).toMatchSnapshot('together-mode-view-spotlight-icon.png'); + }); + + test.only('Confirm mute icon and display Name in together mode layout', async ({ page, serverUrl }, testInfo) => { + test.skip(isTestProfileMobile(testInfo)); + const paul = defaultMockRemoteParticipant('Paul Bridges', true); + const vasily = defaultMockRemoteParticipant('Vasily'); + const participants = [paul, vasily]; + addVideoStream(vasily, true); + vasily.spotlight = { spotlightedOrderPosition: 1 }; + vasily.isMuted = true; + addVideoStream(vasily, true); + const initialState = defaultMockCallAdapterState(participants); + + if (initialState.call?.togetherMode) { + initialState.call.togetherMode.isActive = true; + addTogetherModeStream(initialState.call.togetherMode.streams, true); + initialState.call.togetherMode.seatingPositions = { + '8:acs:Vasily-id': { left: 0, top: 0, width: 200, height: 200 } + }; + } + initialState.isTeamsCall = true; + if (initialState.call) { + initialState.call.kind = 'TeamsCall' as CallKind; + } + await page.goto( + buildUrlWithMockAdapter(serverUrl, initialState, { + newControlBarExperience: 'true' + }) + ); + + const id = `together-mode-participant-8:acs:Vasily-id`; + await waitForSelector(page, dataUiId(IDS.moreButton)); + await pageClick(page, dataUiId(IDS.moreButton)); + await page.locator('button:has-text("View")').click(); + await page.locator('button:has-text("Together mode")').click(); + await waitForSelector(page, dataUiId(IDS.togetherModeStream)); + await waitForSelector(page, dataUiId(id)); + expect(await stableScreenshot(page)).toMatchSnapshot('together-mode-view-mute-icon.png'); + }); + + test.only('Confirm only icons show when seating width is 100px in together mode layout', async ({ + page, + serverUrl + }, testInfo) => { + test.skip(isTestProfileMobile(testInfo)); + const paul = defaultMockRemoteParticipant('Paul Bridges', true); + const vasily = defaultMockRemoteParticipant('Vasily'); + const participants = [paul, vasily]; + addVideoStream(vasily, true); + vasily.spotlight = { spotlightedOrderPosition: 1 }; + vasily.isMuted = true; + vasily.raisedHand = { raisedHandOrderPosition: 1 }; + addVideoStream(vasily, true); + const initialState = defaultMockCallAdapterState(participants); + + if (initialState.call?.togetherMode) { + initialState.call.togetherMode.isActive = true; + addTogetherModeStream(initialState.call.togetherMode.streams, true); + initialState.call.togetherMode.seatingPositions = { + '8:acs:Vasily-id': { left: 0, top: 0, width: 100, height: 100 } + }; + } + initialState.isTeamsCall = true; + if (initialState.call) { + initialState.call.kind = 'TeamsCall' as CallKind; + } + + await page.goto( + buildUrlWithMockAdapter(serverUrl, initialState, { + newControlBarExperience: 'true' + }) + ); + + const id = `together-mode-participant-8:acs:Vasily-id`; + await waitForSelector(page, dataUiId(IDS.moreButton)); + await pageClick(page, dataUiId(IDS.moreButton)); + await page.locator('button:has-text("View")').click(); + await page.locator('button:has-text("Together mode")').click(); + await waitForSelector(page, dataUiId(IDS.togetherModeStream)); + await waitForSelector(page, dataUiId(id)); + expect(await stableScreenshot(page)).toMatchSnapshot('together-mode-view-icons-only.png'); + }); +}); diff --git a/packages/react-composites/tests/browser/call/hermetic/fixture.ts b/packages/react-composites/tests/browser/call/hermetic/fixture.ts index c96685fe223..2b3de90a43f 100644 --- a/packages/react-composites/tests/browser/call/hermetic/fixture.ts +++ b/packages/react-composites/tests/browser/call/hermetic/fixture.ts @@ -14,7 +14,12 @@ import type { } from '../../../common'; import type { CallKind, DominantSpeakersInfo, ParticipantRole } from '@azure/communication-calling'; import type { ParticipantCapabilities } from '@azure/communication-calling'; -import { CallState, CapabilitiesFeatureState } from '@internal/calling-stateful-client'; +import { + CallFeatureStreamState, + CallState, + CapabilitiesFeatureState, + TogetherModeStreamsState +} from '@internal/calling-stateful-client'; const SERVER_URL = 'http://localhost'; const APP_DIR = path.join(__dirname, '../../../app/call'); @@ -56,7 +61,8 @@ export function defaultMockCallAdapterState( role?: ParticipantRole, isRoomsCall?: boolean, callEndReasonSubCode?: number, - isReactionCapability?: boolean + isReactionCapability?: boolean, + isTeamsUser?: boolean ): MockCallAdapterState { const remoteParticipants: Record = {}; participants?.forEach((p) => { @@ -94,7 +100,7 @@ export function defaultMockCallAdapterState( /* @conditional-compile-remove(together-mode) */ togetherMode: { isActive: false, streams: {}, seatingPositions: {} }, pptLive: { isActive: false }, - role: role ?? 'Unknown', + role: 'Presenter', dominantSpeakers: dominantSpeakers, totalParticipantCount: Object.values(remoteParticipants).length > 0 ? Object.values(remoteParticipants).length + 1 : undefined, @@ -133,7 +139,9 @@ export function defaultMockCallAdapterState( } } : undefined, - userId: { kind: 'communicationUser', communicationUserId: '1' }, + userId: isTeamsUser + ? { kind: 'microsoftTeamsUser', microsoftTeamsUserId: '8:orgid:localUser' } + : { kind: 'communicationUser', communicationUserId: '8:orgid:localUser' }, devices: { isSpeakerSelectionAvailable: true, selectedCamera: { id: 'camera1', name: '1st Camera', deviceType: 'UsbCamera' }, @@ -166,9 +174,14 @@ export function defaultMockCallAdapterState( * * Use this to add participants to state created via {@link defaultCallAdapterState}. */ -export function defaultMockRemoteParticipant(displayName?: string): MockRemoteParticipantState { +export function defaultMockRemoteParticipant( + displayName?: string, + isTeamsUser: boolean = false +): MockRemoteParticipantState { return { - identifier: { kind: 'communicationUser', communicationUserId: `8:acs:${displayName}-id` }, + identifier: isTeamsUser + ? { kind: 'microsoftTeamsUser', microsoftTeamsUserId: `8:orgid:${displayName}-id` } + : { kind: 'communicationUser', communicationUserId: `8:acs:${displayName}-id` }, state: 'Connected', videoStreams: { 1: { @@ -262,6 +275,32 @@ export function addScreenshareStream( addDummyView(streams[0], isReceiving, scalingMode); } +/** + * Add a screenshare stream to {@link MockRemoteParticipantState}. + * + * Use to add video to participant created via {@link defaultMockRemoteParticipant}. + */ +export function addTogetherModeStream( + togetherModeStreamState: TogetherModeStreamsState, + isReceiving: boolean, + scalingMode?: 'Stretch' | 'Crop' | 'Fit' +): void { + const togetherModeStream = + togetherModeStreamState.mainVideoStream || + ({ + feature: 'togetherMode', + mediaStreamType: 'Video', + isAvailable: true, + isReceiving: true + } as CallFeatureStreamState); + // if (!togetherModeStream) { + // throw new Error(`Expected togetherMode Stream to be active`); + // } + if (togetherModeStreamState.mainVideoStream) { + addDummyView(togetherModeStreamState.mainVideoStream, isReceiving, scalingMode); + } +} + /** * Add a dummy view to a stream that will be replaced by an actual {@link HTMLElement} by the test app. * diff --git a/packages/react-composites/tests/browser/common/constants.ts b/packages/react-composites/tests/browser/common/constants.ts index 046fca0955d..cec9f8ee4d3 100644 --- a/packages/react-composites/tests/browser/common/constants.ts +++ b/packages/react-composites/tests/browser/common/constants.ts @@ -44,7 +44,8 @@ export const IDS = { reactionButtonSubMenu: 'reaction-sub-menu', reactionMobileDrawerMenuItem: 'reaction-mobile-drawer-menu-item', cameraButton: 'call-composite-camera-button', - microphoneButton: 'call-composite-microphone-button' + microphoneButton: 'call-composite-microphone-button', + togetherModeStream: 'together-mode-layout' }; export const spokenLanguageStrings = [