Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/webapp/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ module.exports = defineConfig({
name: 'Chromium',
use: {
...devices['Desktop Chrome'],
permissions: ['notifications'],
permissions: ['notifications', 'clipboard-read', 'clipboard-write'],
launchOptions: {
args: [
'--use-fake-device-for-media-stream', // Provide fake devices for audio & video device input
Expand Down
20 changes: 15 additions & 5 deletions apps/webapp/src/script/components/UserList/UserList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
*
*/

import {ChangeEvent, useCallback, useMemo, useState} from 'react';
import {ChangeEvent, useCallback, useId, useMemo, useState} from 'react';

import cx from 'classnames';
import {container} from 'tsyringe';
Expand Down Expand Up @@ -157,6 +157,8 @@ export const UserList = ({
[highlightedUserIds, isSelectable, isSelfVerified, mode, noSelfInteraction, selectedUsers, teamState],
);

const adminsHeaderId = useId();
const membersHeaderId = useId();
let content;

const showRoles = !!conversation;
Expand Down Expand Up @@ -188,12 +190,16 @@ export const UserList = ({
<>
{(admins.length > 0 || showEmptyAdmin) && (
<>
<h3 className="user-list__header" data-uie-name="label-conversation-admins">
<h3 id={adminsHeaderId} className="user-list__header" data-uie-name="label-conversation-admins">
{t('searchListAdmins', {count: adminCount})}
</h3>

{admins.length > 0 && (
<ul className={cx('search-list', cssClasses)} data-uie-name="list-admins">
<ul
className={cx('search-list', cssClasses)}
data-uie-name="list-admins"
aria-labelledby={adminsHeaderId}
>
{admins.slice(0, maxShownUsers).map(user => renderListItem(user))}
</ul>
)}
Expand All @@ -208,11 +214,15 @@ export const UserList = ({

{members.length > 0 && maxShownUsers > admins.length && (
<>
<h3 className="user-list__header" data-uie-name="label-conversation-members">
<h3 id={membersHeaderId} className="user-list__header" data-uie-name="label-conversation-members">
{t('searchListMembers', {count: memberCount})}
</h3>

<ul className={cx('search-list', cssClasses)} data-uie-name="list-members">
<ul
className={cx('search-list', cssClasses)}
data-uie-name="list-members"
aria-labelledby={membersHeaderId}
>
{members.slice(0, maxShownUsers - admins.length).map(user => renderListItem(user))}
</ul>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,6 @@ export const CallingCell = ({
answerCall={answerCall}
call={call}
callActions={callActions}
call1To1StartedAlert={call1To1StartedAlert}
isFullUi={isFullUi}
isMuted={isMuted}
isConnecting={isConnecting}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ interface CallingControlsProps {
answerCall: () => void;
call: Call;
callActions: CallActions;
call1To1StartedAlert: string;
isDetachedWindow: boolean;
isFullUi?: boolean;
isMuted?: boolean;
Expand All @@ -61,7 +60,6 @@ export const CallingControls = ({
answerCall,
call,
callActions,
call1To1StartedAlert,
isFullUi,
isMuted,
isConnecting,
Expand Down Expand Up @@ -176,8 +174,8 @@ export const CallingControls = ({
className="call-ui__button call-ui__button--red call-ui__button--large"
onClick={() => (isIncoming ? callActions.reject(call) : callActions.leave(call))}
onBlur={() => clearShowAlert()}
title={!isGroup && showAlert ? call1To1StartedAlert : t('videoCallOverlayHangUp')}
aria-label={!isGroup && showAlert ? call1To1StartedAlert : t('videoCallOverlayHangUp')}
title={t('videoCallOverlayHangUp')}
aria-label={t('videoCallOverlayHangUp')}
type="button"
data-uie-name="do-call-controls-call-decline"
>
Expand Down
7 changes: 6 additions & 1 deletion apps/webapp/test/e2e_tests/pageManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ import {SettingsPage} from './webapp/pages/settings.page';
import {SingleSignOnPage} from './webapp/pages/singleSignOn.page';
import {StartUIPage} from './webapp/pages/startUI.page';
import {WelcomePage} from './webapp/pages/welcome.page';
import {GuestLinkPasswordModal} from './webapp/modals/guestLinkPassword.modal';
import {ConversationJoinPage} from './webapp/pages/conversationJoin.page';

export const webAppPath = process.env.WEBAPP_URL ?? '';

Expand Down Expand Up @@ -166,7 +168,7 @@ export class PageManager {
this.getOrCreate('webapp.pages.audioVideoSettings', () => new AudioVideoSettingsPage(this.page)),
outgoingConnection: () =>
this.getOrCreate('webapp.pages.outgoingConnection', () => new OutgoingConnectionPage(this.page)),
guestOptions: () => this.getOrCreate('webapp.pages.guestOptions', () => new GuestOptionsPage(this.page)),
guestOptions: () => this.getOrCreate('webapp.pages.guestOptions', () => GuestOptionsPage(this.page)),
deleteAccount: () => this.getOrCreate('webapp.pages.deleteAccount', () => new DeleteAccountPage(this.page)),
groupCreation: () => this.getOrCreate('webapp.pages.groupCreation', () => new GroupCreationPage(this.page)),
historyInfo: () => this.getOrCreate('webapp.pages.infoHostory', () => new HistoryInfoPage(this.page)),
Expand All @@ -182,6 +184,7 @@ export class PageManager {
emailVerification: () =>
this.getOrCreate('webapp.pages.verification', () => new EmailVerificationPage(this.page)),
setUsername: () => this.getOrCreate('webapp.pages.setUsername', () => new SetUsernamePage(this.page)),
conversationJoin: () => this.getOrCreate('webapp.pages.conversationJoin', () => ConversationJoinPage(this.page)),
},
modals: {
dataShareConsent: () =>
Expand All @@ -207,6 +210,8 @@ export class PageManager {
cellsFileDetailView: () =>
this.getOrCreate('webapp.modals.cellsFileDetailView', () => new CellsFileDetailViewModal(this.page)),
optionModal: () => this.getOrCreate('webapp.modals.optionModal', () => new OptionModal(this.page)),
guestLinkPassword: () =>
this.getOrCreate('webapp.modals.guestLinkPassword', () => new GuestLinkPasswordModal(this.page)),
},
components: {
contactList: () => this.getOrCreate('webapp.components.ContactList', () => new ContactList(this.page)),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Wire
* Copyright (C) 2025 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {Locator, Page} from '@playwright/test';

import {BaseModal} from './base.modal';

/** Modal shown when a link for guests to join a group conversation with a password is created */
export class GuestLinkPasswordModal extends BaseModal {
readonly setPasswordInput: Locator;
readonly confirmPasswordInput: Locator;

constructor(page: Page) {
super(page, 'modal-template-guest-link-password');

this.setPasswordInput = this.modal.getByRole('textbox', {name: 'Set password'});
this.confirmPasswordInput = this.modal.getByRole('textbox', {name: 'Confirm password'});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export class ConversationPage {
readonly itemPendingRequest: Locator;
readonly ignoreButton: Locator;
readonly cancelRequest: Locator;
readonly mentionSuggestions: Locator;

readonly getImageAltText = (user: User) => `Image from ${user.fullName}`;

Expand Down Expand Up @@ -104,6 +105,7 @@ export class ConversationPage {
this.itemPendingRequest = page.getByTestId('item-pending-requests');
this.ignoreButton = page.getByTestId('do-ignore');
this.cancelRequest = page.getByTestId('do-cancel-request');
this.mentionSuggestions = page.getByRole('listbox').getByTestId('item-mention-suggestion');
}

getImageLocator(user: User): Locator {
Expand Down Expand Up @@ -158,6 +160,12 @@ export class ConversationPage {
await this.messageInput.pressSequentially(message, {delay: 100});
}

async mentionUser(userFullName: string, searchQuery?: string) {
const textToType = searchQuery ? `@${searchQuery}` : `@${userFullName.slice(0, 3)}`;
await this.messageInput.pressSequentially(textToType);
await this.mentionSuggestions.filter({hasText: userFullName}).click({timeout: 5000});
}

async replyToMessage(message: Locator) {
await message.hover();
await message.getByRole('group').getByTestId('do-reply-message').click();
Expand All @@ -174,12 +182,8 @@ export class ConversationPage {
}

async sendMessageWithUserMention(userFullName: string, messageText?: string) {
await this.messageInput.fill(`@`);
await this.page
.getByTestId('item-mention-suggestion')
.getByTestId('status-name')
.filter({hasText: userFullName})
.click({timeout: 1000});
await this.messageInput.fill(''); // Clear the input initially
await this.mentionUser(userFullName);

if (messageText) {
await this.messageInput.pressSequentially(messageText);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,14 @@
*/

import {Locator, Page} from '@playwright/test';
import {GuestOptionsPage} from './guestOptions.page';

export class ConversationDetailsPage {
readonly page: Page;

readonly groupAdmins: Locator;
readonly groupMembers: Locator;

readonly addPeopleButton: Locator;
readonly conversationDetails: Locator;
readonly guestOptionsButton: Locator;
Expand All @@ -38,9 +42,12 @@ export class ConversationDetailsPage {

constructor(page: Page) {
this.page = page;
this.conversationDetails = page.locator('#conversation-details');

this.groupAdmins = this.conversationDetails.getByRole('list', {name: 'Group Admins'}).getByRole('listitem');
this.groupMembers = this.conversationDetails.getByRole('list', {name: 'Group Members'}).getByRole('listitem');

this.addPeopleButton = page.getByTestId('go-add-people');
this.conversationDetails = page.locator('#conversation-details');
this.guestOptionsButton = this.conversationDetails.locator('[data-uie-name="go-guest-options"]');
this.selfDeletingMessageButton = this.conversationDetails.getByRole('button', {name: 'Self-deleting messages'});
this.archiveButton = this.conversationDetails.getByTestId('do-archive');
Expand Down Expand Up @@ -135,6 +142,17 @@ export class ConversationDetailsPage {
await selfDeletingMessagesPanel.getByRole('button', {name: 'Go back'}).click();
}

/** Opens the guests panel, creates a link for guests to join the group, closes the panel and returns the created link */
async createGuestLink(options?: Parameters<ReturnType<typeof GuestOptionsPage>['createLink']>[0]) {
await this.guestOptionsButton.click();

const guestOptionsPage = GuestOptionsPage(this.page);
const link = await guestOptionsPage.createLink(options);

await guestOptionsPage.backButton.click();
return link;
}

async addServiceToConversation(serviceName: string) {
// Click on the Services/Apps tab
const servicesTab = this.page.locator('#add-participants').getByTestId('do-add-services');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Page} from 'playwright/test';

/** Page shown to users using a guest link to join a group conversation */
export const ConversationJoinPage = (page: Page) => {
const joinAsGuest = async (name: string) => {
await page.getByRole('textbox', {name: 'Your name'}).fill(name);
// It's necessary to specify a position since the text contains a link which would be clicked instead of the checkbox
await page.getByText(/I accept .* terms of use/i).click({position: {x: 0, y: 8}});
await page.getByRole('button', {name: 'Join as Temporary Guest'}).click();
};

return {
joinAsGuest,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -111,13 +111,17 @@ export class ConversationListPage {
* @param options.protocol Only locate conversations matching this protocol (mls only works for 1on1 conversations as groups still use proteus) - Default: "mls"
*/
getConversationLocator(conversationName: string, options?: {protocol?: 'mls' | 'proteus'}) {
const conversation = this.page.getByTestId('item-conversation').filter({hasText: conversationName});
let conversation = this.page.getByTestId('item-conversation').filter({hasText: conversationName});

if (options?.protocol) {
return conversation.and(this.page.locator(`[data-protocol="${options.protocol}"]`));
conversation = conversation.and(this.page.locator(`[data-protocol="${options.protocol}"]`));
}

return conversation;
return Object.assign(conversation, {
unreadIndicator: conversation.getByTitle('Unread message'),
mutedIndicator: conversation.getByTitle('Muted conversation'),
mentionIndicator: conversation.getByTitle('Unread mention'),
});
}

async openContextMenu(conversationName: string) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,44 @@
*
*/

import {Page, Locator} from '@playwright/test';

export class GuestOptionsPage {
readonly page: Page;

readonly createLinkButton: Locator;
readonly inviteLink: Locator;
readonly copyLinkButton: Locator;

constructor(page: Page) {
this.page = page;

this.createLinkButton = page.locator('[data-uie-name="do-create-link"]');
this.inviteLink = page.locator('[data-uie-name="status-invite-link"]');
this.copyLinkButton = page.locator('[data-uie-name="do-copy-link"]');
}

async clickCreateLinkButton() {
await this.createLinkButton.click();
}

async getInviteLink() {
return this.inviteLink.textContent();
}

async clickCopyLinkButton() {
await this.copyLinkButton.click();
}
}
import {Page} from '@playwright/test';
import {GuestLinkPasswordModal} from '../modals/guestLinkPassword.modal';
import {ConfirmModal} from '../modals/confirm.modal';

export const GuestOptionsPage = (page: Page) => {
const panel = page.getByRole('complementary').filter({has: page.getByRole('heading', {name: 'Guests'})});
const createPasswordModal = new GuestLinkPasswordModal(page);

const backButton = panel.getByRole('button', {name: 'Go back'});
const passwordSecuredRadioButton = panel.getByRole('radiogroup').getByText('Password secured');
const notPasswordSecuredRadioButton = panel.getByRole('radiogroup').getByText('Not password secured');
const createLinkButton = panel.getByRole('button', {name: 'Create link'});

const guestLink = panel.getByRole('button', {name: /https:\/\/.+\/conversation-join\//});

const createLink = async (options?: {password?: string}) => {
if (options?.password) {
await passwordSecuredRadioButton.click();
} else {
await notPasswordSecuredRadioButton.click();
}

await createLinkButton.click();

if (options?.password) {
await createPasswordModal.setPasswordInput.fill(options.password);
await createPasswordModal.confirmPasswordInput.fill(options.password);
await createPasswordModal.actionButton.click();

// After the link was created a second modal to copy the password will open
await new ConfirmModal(page).actionButton.click();
}

return await guestLink.textContent();
};

return {
backButton,
createLink,
};
};
Loading