diff --git a/playwright.config.ts b/playwright.config.ts index 40e15a3..4258e34 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -50,6 +50,13 @@ export default defineConfig({ use: { browserName: "chromium", viewport: { width: 1280, height: 720 }, // Desktop viewport + launchOptions: { + args: [ + "--disable-web-security", + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream", + ], + }, }, }, @@ -59,6 +66,13 @@ export default defineConfig({ use: { browserName: "chromium", ...devices["Pixel 5"], // Use predefined mobile device + launchOptions: { + args: [ + "--disable-web-security", + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream", + ], + }, }, testIgnore: "./playwright/specs/13-settings-keybinds.spec.ts", }, diff --git a/playwright/PageObjects/CallControls.ts b/playwright/PageObjects/CallElements/CallControls.ts similarity index 97% rename from playwright/PageObjects/CallControls.ts rename to playwright/PageObjects/CallElements/CallControls.ts index 71b3d00..51801db 100644 --- a/playwright/PageObjects/CallControls.ts +++ b/playwright/PageObjects/CallElements/CallControls.ts @@ -1,4 +1,4 @@ -import MainPage from "./MainPage"; +import MainPage from "../MainPage"; import { type Locator, type Page } from "@playwright/test"; export class CallControls extends MainPage { diff --git a/playwright/PageObjects/CallElements/CallScreen.ts b/playwright/PageObjects/CallElements/CallScreen.ts new file mode 100644 index 0000000..814c7e3 --- /dev/null +++ b/playwright/PageObjects/CallElements/CallScreen.ts @@ -0,0 +1,263 @@ +import MainPage from "../MainPage"; +import { type Locator, type Page, expect } from "@playwright/test"; + +export class CallScreen extends MainPage { + readonly callCollapseExpandButton: Locator; + readonly callDeafenButton: Locator; + readonly callEndButton: Locator; + readonly callFullscreenButton: Locator; + readonly callMuteButton: Locator; + readonly callParticipant: Locator; + readonly callParticipantConnecting: Locator; + readonly callParticipantShaking: Locator; + readonly callScreen: Locator; + readonly callScreenTopbar: Locator; + readonly callSettingsButton: Locator; + readonly callStreamButton: Locator; + readonly callVideoButton: Locator; + readonly callVolumeMixerButton: Locator; + readonly localUserVideo: Locator; + readonly participantDeafenButton: Locator; + readonly participantMuteButton: Locator; + readonly participantProfilePicture: Locator; + readonly participantProfilePictureIdenticon: Locator; + readonly participantProfilePictureImage: Locator; + readonly participantUser: Locator; + readonly participantVideo: Locator; + readonly participantWithoutVideo: Locator; + readonly participants: Locator; + readonly remoteUserVideo: Locator; + readonly usersInCallText: Locator; + + constructor( + public readonly page: Page, + public readonly viewport: string, + ) { + super(page, viewport); + this.callScreen = this.page.getByTestId("call-screen"); + this.callCollapseExpandButton = this.callScreen.getByTestId( + "button-call-collapse-expand", + ); + this.callDeafenButton = this.callScreen.getByTestId("button-call-deafen"); + this.callEndButton = this.callScreen.getByTestId("button-call-end"); + this.callFullscreenButton = this.callScreen.getByTestId( + "button-call-fullscreen", + ); + this.callMuteButton = this.callScreen.getByTestId("button-call-mute"); + this.callParticipant = this.callScreen.getByTestId("call-participant"); + this.callParticipantConnecting = this.page.getByText("Connecting..."); + this.callParticipantShaking = this.page.locator(".shaking-participant"); + this.callScreenTopbar = this.callScreen.getByTestId("topbar"); + this.callSettingsButton = this.callScreen.getByTestId( + "button-call-settings", + ); + this.callStreamButton = this.callScreen.getByTestId("button-call-stream"); + this.callVideoButton = this.callScreen.getByTestId("button-call-video"); + this.callVolumeMixerButton = this.callScreen.getByTestId( + "button-call-volume-mixer", + ); + this.localUserVideo = this.callScreen.getByTestId("local-user-video"); + this.participantDeafenButton = this.callScreen.getByTestId( + "button-participant-deafen", + ); + this.participantMuteButton = this.callScreen.getByTestId( + "button-participant-mute", + ); + this.participantProfilePicture = this.page.getByTestId( + "participant-profile-picture", + ); + this.participantProfilePictureIdenticon = + this.participantProfilePicture.locator(".identicon"); + this.participantProfilePictureImage = + this.participantProfilePicture.locator("img"); + this.participantUser = this.page.getByTestId("participant-user"); + this.participantVideo = this.page.getByTestId("participant-video"); + this.participantWithoutVideo = this.page.getByTestId( + "participant-without-video", + ); + this.participants = this.callScreen.locator("#participants"); + this.remoteUserVideo = this.callScreen.getByTestId("remote-user-video"); + this.usersInCallText = this.callScreen.getByTestId("text-users-in-call"); + } + + async clickOnStreamButton() { + await this.callStreamButton.click(); + await expect(this.callStreamButton).toHaveCSS( + "border-bottom-color", + "rgb(77, 77, 255)", + ); + await expect(this.callStreamButton).toHaveAttribute( + "data-tooltip", + "Stream", + ); + } + + async collapseCall() { + await expect(this.callCollapseExpandButton).toHaveAttribute( + "data-tooltip", + "Less Space", + ); + await this.callCollapseExpandButton.click(); + await expect(this.callScreen).not.toHaveClass(/.*\bexpanded\b.*/); + await expect(this.callCollapseExpandButton).toHaveAttribute( + "data-tooltip", + "More Space", + ); + } + + async deafenCall() { + await this.callDeafenButton.click(); + await expect(this.callDeafenButton).toHaveCSS( + "background-color", + "color(srgb 0.978824 0.297647 0.396471)", + ); + await expect(this.callDeafenButton).toHaveAttribute( + "data-tooltip", + "Deafen", + ); + } + + async disableVideo() { + await expect(this.callVideoButton).toHaveAttribute( + "data-tooltip", + "Disable Video", + ); + await this.callVideoButton.click(); + await expect(this.callVideoButton).toHaveCSS( + "background-color", + /rgb\(249, 56, 84\)|color\(srgb 0.978824 0.297647 0.396471\)/, + ); + + await expect(this.callVideoButton).toHaveAttribute( + "data-tooltip", + "Enable Video", + ); + } + + async enableVideo() { + await expect(this.callVideoButton).toHaveAttribute( + "data-tooltip", + "Enable Video", + ); + await this.callVideoButton.click(); + await expect(this.callVideoButton).toHaveCSS( + "background-color", + "rgb(33, 38, 58)", + ); + await expect(this.callVideoButton).toHaveAttribute( + "data-tooltip", + "Disable Video", + ); + } + + async endCall() { + await expect(this.callEndButton).toHaveAttribute("data-tooltip", "End"); + await expect(this.callEndButton).toHaveCSS( + "background-color", + "rgb(249, 56, 84)", + ); + await this.callEndButton.click(); + await this.callScreen.waitFor({ state: "detached" }); + } + + async expandCall() { + await expect(this.callCollapseExpandButton).toHaveAttribute( + "data-tooltip", + "More Space", + ); + await this.callCollapseExpandButton.click(); + await expect(this.callScreen).toHaveClass(/.*\bexpanded\b.*/); + await expect(this.callCollapseExpandButton).toHaveAttribute( + "data-tooltip", + "Less Space", + ); + } + + async enterFullScreenMode() { + await expect(this.callFullscreenButton).toHaveAttribute( + "data-tooltip", + "Fullscreen", + ); + await this.callFullscreenButton.click(); + const isFullscreen = await this.page.evaluate(() => { + return document.fullscreenElement !== null; + }); + expect(isFullscreen).toBeTruthy(); + } + async exitFullScreenMode() { + await expect(this.callFullscreenButton).toHaveAttribute( + "data-tooltip", + "Fullscreen", + ); + await this.callFullscreenButton.click(); + const isFullscreen = await this.page.evaluate(() => { + return document.fullscreenElement !== null; + }); + expect(isFullscreen).toBeFalsy(); + } + + async muteCall() { + await expect(this.callMuteButton).toHaveAttribute("data-tooltip", "Mute"); + await this.callMuteButton.click(); + await expect(this.callMuteButton).toHaveCSS( + "background-color", + "color(srgb 0.978824 0.297647 0.396471)", + ); + await expect(this.callMuteButton).toHaveAttribute("data-tooltip", "Unmute"); + } + + async openCallSettings() { + await this.callSettingsButton.click(); + } + + async openCallVolumeMixer() { + await this.callVolumeMixerButton.click(); + } + + async undeafenCall() { + await this.callDeafenButton.click(); + await expect(this.callDeafenButton).toHaveCSS( + "background-color", + "rgb(35, 41, 62)", + ); + await expect(this.callDeafenButton).toHaveAttribute( + "data-tooltip", + "Deafen", + ); + } + + async unmuteCall() { + await expect(this.callMuteButton).toHaveAttribute("data-tooltip", "Unmute"); + await this.callMuteButton.click(); + await expect(this.callMuteButton).toHaveCSS( + "background-color", + "rgb(35, 41, 62)", + ); + await expect(this.callMuteButton).toHaveAttribute("data-tooltip", "Mute"); + } + + async validateCallScreenContents( + localUserImgSrc: string, + remoteUserImgSrc: string, + ) { + // Validate local user contents + await expect(this.participantProfilePictureImage.first()).toHaveAttribute( + "src", + localUserImgSrc, + ); + + // Validate remote user profile picture + await expect(this.participantProfilePictureImage.last()).toHaveAttribute( + "src", + remoteUserImgSrc, + ); + + // Validate number of users in call displayed in label + await expect(this.usersInCallText).toHaveText("(2) users in the call"); + } + + async validateUserIsConnecting() { + await expect(this.callParticipantConnecting).toBeVisible(); + await expect(this.callParticipantShaking).toBeVisible(); + } +} diff --git a/playwright/PageObjects/CallSettings.ts b/playwright/PageObjects/CallElements/CallSettings.ts similarity index 99% rename from playwright/PageObjects/CallSettings.ts rename to playwright/PageObjects/CallElements/CallSettings.ts index 4bde91b..6f46ec2 100644 --- a/playwright/PageObjects/CallSettings.ts +++ b/playwright/PageObjects/CallElements/CallSettings.ts @@ -1,4 +1,4 @@ -import MainPage from "./MainPage"; +import MainPage from "../MainPage"; import { type Locator, type Page } from "@playwright/test"; export class CallSettings extends MainPage { diff --git a/playwright/PageObjects/CallElements/IncomingCall.ts b/playwright/PageObjects/CallElements/IncomingCall.ts new file mode 100644 index 0000000..717b2dc --- /dev/null +++ b/playwright/PageObjects/CallElements/IncomingCall.ts @@ -0,0 +1,86 @@ +import MainPage from "../MainPage"; +import { type Locator, type Page, expect } from "@playwright/test"; + +export class IncomingCall extends MainPage { + readonly buttonAcceptVideoIncomingCall: Locator; + readonly buttonAcceptVoiceIncomingCall: Locator; + readonly buttonDeclineIncomingCall: Locator; + readonly incomingCallModal: Locator; + readonly incomingCallProfilePicture: Locator; + readonly incomingCallProfilePictureIdenticon: Locator; + readonly incomingCallProfilePictureImage: Locator; + readonly incomingCallStatus: Locator; + readonly incomingCallStatusIndicator: Locator; + readonly incomingCallUsername: Locator; + + constructor( + public readonly page: Page, + public readonly viewport: string, + ) { + super(page, viewport); + this.incomingCallModal = this.page.locator("#incoming-call"); + this.buttonAcceptVideoIncomingCall = this.incomingCallModal.getByRole( + "button", + { + name: "Video", + }, + ); + this.buttonAcceptVoiceIncomingCall = this.incomingCallModal.getByRole( + "button", + { + name: "Voice", + }, + ); + this.buttonDeclineIncomingCall = this.incomingCallModal.getByRole( + "button", + { + name: "Decline", + exact: true, + }, + ); + this.incomingCallProfilePicture = this.incomingCallModal.getByTestId( + "friend-profile-picture", + ); + this.incomingCallProfilePictureIdenticon = + this.incomingCallProfilePicture.locator(".identicon img"); + this.incomingCallProfilePictureImage = + this.incomingCallProfilePicture.locator("img"); + this.incomingCallStatus = this.incomingCallModal.getByText( + "status from second user", + ); // Temporarily hardcoded + this.incomingCallStatusIndicator = + this.incomingCallModal.getByTestId("status-indicator"); + this.incomingCallUsername = this.incomingCallModal.getByText("ChatUserB"); // Temporarily hardcoded + } + + async acceptAudioIncomingCall() { + await this.buttonAcceptVoiceIncomingCall.click(); + } + + async acceptVideoIncomingCall() { + await this.buttonAcceptVideoIncomingCall.click(); + } + + async denyIncomingCall() { + await this.buttonDeclineIncomingCall.click(); + } + + async validateIncomingCallModal( + username: string, + status: string, + imageSrc: string, + ) { + await expect(this.incomingCallUsername).toHaveText(username); + await expect(this.incomingCallStatus).toHaveText(status); + await expect(this.incomingCallStatusIndicator).toHaveClass( + /.*\bonline\b.*/, + ); + await expect(this.incomingCallProfilePictureImage).toHaveAttribute( + "src", + imageSrc, + ); + await expect(this.buttonAcceptVideoIncomingCall).toBeVisible(); + await expect(this.buttonAcceptVoiceIncomingCall).toBeVisible(); + await expect(this.buttonDeclineIncomingCall).toBeVisible(); + } +} diff --git a/playwright/PageObjects/CallScreen.ts b/playwright/PageObjects/CallScreen.ts deleted file mode 100644 index 4933d92..0000000 --- a/playwright/PageObjects/CallScreen.ts +++ /dev/null @@ -1,78 +0,0 @@ -import MainPage from "./MainPage"; -import { type Locator, type Page } from "@playwright/test"; - -export class CallScreen extends MainPage { - readonly callCollapseExpandButton: Locator; - readonly callDeafenButton: Locator; - readonly callEndButton: Locator; - readonly callFullscreenButton: Locator; - readonly callMuteButton: Locator; - readonly callParticipant: Locator; - readonly callScreen: Locator; - readonly callScreenTopbar: Locator; - readonly callSettingsButton: Locator; - readonly callStreamButton: Locator; - readonly callVideoButton: Locator; - readonly callVolumeMixerButton: Locator; - readonly localUserVideo: Locator; - readonly participantDeafenButton: Locator; - readonly participantMuteButton: Locator; - readonly participantProfilePicture: Locator; - readonly participantProfilePictureIdenticon: Locator; - readonly participantProfilePictureImage: Locator; - readonly participantUser: Locator; - readonly participantVideo: Locator; - readonly participantWithoutVideo: Locator; - readonly participants: Locator; - readonly remoteUserVideo: Locator; - readonly usersInCallText: Locator; - - constructor( - public readonly page: Page, - public readonly viewport: string, - ) { - super(page, viewport); - this.callCollapseExpandButton = this.callScreen.getByTestId( - "button-call-collapse-expand", - ); - this.callDeafenButton = this.callScreen.getByTestId("button-call-deafen"); - this.callEndButton = this.callScreen.getByTestId("button-call-end"); - this.callFullscreenButton = this.callScreen.getByTestId( - "button-call-fullscreen", - ); - this.callMuteButton = this.callScreen.getByTestId("button-call-mute"); - this.callParticipant = this.callScreen.getByTestId("call-participant"); - this.callScreen = this.page.getByTestId("call-screen"); - this.callScreenTopbar = this.callScreen.getByTestId("topbar"); - this.callSettingsButton = this.callScreen.getByTestId( - "button-call-settings", - ); - this.callStreamButton = this.callScreen.getByTestId("button-call-stream"); - this.callVideoButton = this.callScreen.getByTestId("button-call-video"); - this.callVolumeMixerButton = this.callScreen.getByTestId( - "button-call-volume-mixer", - ); - this.localUserVideo = this.callScreen.getByTestId("local-user-video"); - this.participantDeafenButton = this.callScreen.getByTestId( - "button-participant-deafen", - ); - this.participantMuteButton = this.callScreen.getByTestId( - "button-participant-mute", - ); - this.participantProfilePicture = this.page.getByTestId( - "participant-profile-picture", - ); - this.participantProfilePictureIdenticon = - this.participantProfilePicture.locator(".identicon"); - this.participantProfilePictureImage = - this.participantProfilePicture.locator("img"); - this.participantUser = this.page.getByTestId("participant-user"); - this.participantVideo = this.page.getByTestId("participant-video"); - this.participantWithoutVideo = this.page.getByTestId( - "participant-without-video", - ); - this.participants = this.callScreen.locator("#participants"); - this.remoteUserVideo = this.callScreen.getByTestId("remote-user-video"); - this.usersInCallText = this.callScreen.getByTestId("text-users-in-call"); - } -} diff --git a/playwright/PageObjects/ChatsMain.ts b/playwright/PageObjects/ChatsMain.ts index 217822a..ac76798 100644 --- a/playwright/PageObjects/ChatsMain.ts +++ b/playwright/PageObjects/ChatsMain.ts @@ -365,6 +365,13 @@ export class ChatsMainPage extends MainPage { this.uploadFilesSelectedSingle.locator(".file-preview-image"); } + async clickOnAudioCallButton() { + if (this.viewport === "mobile-chrome") { + await this.clickOnHamburgerMobileButton(); + } + await this.buttonChatCall.click(); + } + async clickOnFavoriteButton() { if (this.viewport === "mobile-chrome") { await this.clickOnHamburgerMobileButton(); @@ -412,6 +419,16 @@ export class ChatsMainPage extends MainPage { await this.chatbarInput.click(); } + async exitCallSettings(): Promise { + if (this.viewport === "mobile-chrome") { + await this.page + .getByTestId("button-show-controls") + .click({ force: true }); + } else { + await this.chatbarInput.click({ force: true }); + } + } + async exitPinMessagesContainer() { if (this.viewport === "mobile-chrome") { await this.page diff --git a/playwright/specs/03-friends-two-instances.spec.ts b/playwright/specs/03-friends-two-instances.spec.ts index 4950b12..6537ae0 100644 --- a/playwright/specs/03-friends-two-instances.spec.ts +++ b/playwright/specs/03-friends-two-instances.spec.ts @@ -10,6 +10,8 @@ import { SettingsMessages } from "playwright/PageObjects/Settings/SettingsMessag import { EmojiPicker } from "playwright/PageObjects/ChatsElements/EmojiPicker"; import { GifPicker } from "playwright/PageObjects/ChatsElements/GifPicker"; import { StickerPicker } from "playwright/PageObjects/ChatsElements/StickerPicker"; +import { CallScreen } from "playwright/PageObjects/CallElements/CallScreen"; +import { IncomingCall } from "playwright/PageObjects/CallElements/IncomingCall"; const username = "ChatUserA"; const usernameTwo = "ChatUserB"; @@ -1868,6 +1870,132 @@ test.describe("Two instances tests - Friends and Chats", () => { await stickerPickerSecond.navigateThroughStickerCategories("The Garden"); await stickerPickerSecond.navigateThroughStickerCategories("Sassy Toons"); }); + + test("Videocall testing between two users - mute, unmute, fullscreen, expand/collapse call", async ({ + firstUserContext, + secondUserContext, + }) => { + // Declare constants required from the fixtures + const context1 = firstUserContext.context; + const page1 = firstUserContext.page; + const page2 = secondUserContext.page; + const viewport = firstUserContext.viewport; + const friendsScreenFirst = new FriendsScreen(page1, viewport); + const friendsScreenSecond = new FriendsScreen(page2, viewport); + const chatsMainPageFirst = new ChatsMainPage(page1, viewport); + const chatsMainPageSecond = new ChatsMainPage(page2, viewport); + const settingsProfileFirst = new SettingsProfile(page1, viewport); + const settingsProfileSecond = new SettingsProfile(page2, viewport); + let lastMessageSent: Locator; + let lastMessageReceived: Locator; + + // Setup accounts for testing + await setupChats( + chatsMainPageFirst, + chatsMainPageSecond, + context1, + friendsScreenFirst, + friendsScreenSecond, + page1, + ); + + // Second user uploads a profile picture + await chatsMainPageSecond.goToSettings(); + await page2.waitForURL("/settings/profile"); + await settingsProfileSecond.hideSidebarOnMobileView(); + await settingsProfileSecond.uploadProfilePicture( + "playwright/assets/logo.jpg", + ); + + const profilePictureUserB = + await settingsProfileSecond.getProfileImageSource(); + await settingsProfileSecond.goToChat(); + + // First user uploads a profile picture + await chatsMainPageFirst.goToSettings(); + await page1.waitForURL("/settings/profile"); + await settingsProfileFirst.hideSidebarOnMobileView(); + await settingsProfileFirst.uploadProfilePicture( + "playwright/assets/banner.jpg", + ); + + const profilePictureUserA = + await settingsProfileFirst.getProfileImageSource(); + await settingsProfileFirst.goToChat(); + + // Send message from second user to first user + const firstMessage = "hey I am gonna call you now"; + await chatsMainPageSecond.sendMessage(firstMessage); + lastMessageSent = await chatsMainPageSecond.getLastMessageLocal(); + lastMessageReceived = await chatsMainPageFirst.getLastMessageRemote(); + await expect(lastMessageSent).toHaveText(firstMessage); + await expect(lastMessageReceived).toHaveText(firstMessage); + + // Second user calls the first user + await chatsMainPageSecond.clickOnAudioCallButton(); + + // Validate outgoing call modal displayed + const callScreenSecondUser = new CallScreen(page2, viewport); + await expect(callScreenSecondUser.callScreen).toBeVisible(); + + // Validate incoming call modal displayed + const incomingCallFirstUser = new IncomingCall(page1, viewport); + await incomingCallFirstUser.validateIncomingCallModal( + usernameTwo, + "status from second user", + profilePictureUserB, + ); + + // Validate user is connecting displays and then accept incoming call + await callScreenSecondUser.validateUserIsConnecting(); + await incomingCallFirstUser.acceptAudioIncomingCall(); + + // Validate incoming call modal is closed + await incomingCallFirstUser.incomingCallModal.waitFor({ + state: "detached", + }); + await callScreenSecondUser.callParticipantConnecting.waitFor({ + state: "detached", + }); + const callScreenFirstUser = new CallScreen(page1, viewport); + await expect(callScreenFirstUser.callScreen).toBeVisible(); + + // With second user validate contents from call screen + await callScreenSecondUser.validateCallScreenContents( + profilePictureUserB, + profilePictureUserA, + ); + + // With first user validate contents from call screen + await callScreenFirstUser.validateCallScreenContents( + profilePictureUserA, + profilePictureUserB, + ); + + // With first user validate all buttons are working correctly + await callScreenSecondUser.unmuteCall(); + await callScreenSecondUser.muteCall(); + await callScreenSecondUser.deafenCall(); + await callScreenSecondUser.undeafenCall(); + await callScreenSecondUser.clickOnStreamButton(); + await callScreenSecondUser.expandCall(); + await callScreenSecondUser.collapseCall(); + + // Executing full screen mode validations only on desktop viewport since exit button appears outside of mobile viewport + if (callScreenSecondUser.viewport === "desktop-chrome") { + await callScreenSecondUser.enterFullScreenMode(); + await callScreenSecondUser.exitFullScreenMode(); + } + + // With second user continue validating buttons are working correctly + await callScreenSecondUser.enableVideo(); + await page2.waitForTimeout(10000); + await callScreenSecondUser.disableVideo(); + await callScreenSecondUser.openCallVolumeMixer(); + await callScreenSecondUser.openCallSettings(); + await chatsMainPageSecond.exitCallSettings(); + await callScreenSecondUser.endCall(); + }); }); async function setupChats(