Skip to content
Open
1 change: 1 addition & 0 deletions e2e-tests/poms/webapp/callCell.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const callCell = (page: Page) => {

return Object.assign(callCell, {
acceptCallButton: callCell.getByRole('button', {name: 'Accept'}),
declineButton: callCell.getByRole('button', {name: 'Hang up'}),
goFullScreen: page.locator('[data-uie-name="do-maximize-call"]'),
});
};
3 changes: 2 additions & 1 deletion e2e-tests/poms/webapp/conversation.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {User} from '../../actions/createUser';
export const conversation = (page: Page) => {
const conversationTitle = page.getByTestId('status-conversation-title-bar-label');
const startCallButton = page.getByTestId('do-call');

const systemMessages = page.locator('[data-uie-name="item-message"].system-message:not([data-uie-send-status="1"])');
/**
* The attribute 'send-status' will be 1 while the message is being sent, since we only want to assert on sent messages these messages will be excluded. See: {@see StatusTypes}
* Status type -1 ensures that system messages do NOT count as sent messages
Expand Down Expand Up @@ -67,6 +67,7 @@ export const conversation = (page: Page) => {
return {
conversationTitle,
startCallButton,
systemMessages,
sendMessage,
getMessage,
};
Expand Down
5 changes: 4 additions & 1 deletion e2e-tests/poms/webapp/conversationList.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@
import {Page} from '@playwright/test';

export const conversationsList = (page: Page) => {
const items = page.getByTestId('item-conversation');

const getConversation = (conversationName: string, options?: {protocol?: 'mls' | 'proteus'}) => {
let conversation = page.getByTestId('item-conversation').filter({hasText: conversationName});
let conversation = items.filter({hasText: conversationName});

if (options?.protocol) {
conversation = conversation.and(page.locator(`[data-protocol="${options.protocol}"]`));
Expand All @@ -39,6 +41,7 @@ export const conversationsList = (page: Page) => {
const createGroupButton = page.getByTestId('conversation-list-header').getByTestId('go-create-group');

return {
items,
getConversation,
createGroupButton,
};
Expand Down
1 change: 1 addition & 0 deletions e2e-tests/poms/webapp/conversationsSidebar.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export const conversationsSidebar = (page: Page) => {
return {
userAvatar: sidebar.getByTestId('element-avatar-user'),
connectButton: sidebar.getByTestId('go-people'),
archiveButton: sidebar.getByTestId('go-archive'),
};
};
26 changes: 26 additions & 0 deletions e2e-tests/poms/webapp/settings.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Wire
* Copyright (C) 2026 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 {Page} from '@playwright/test';

export const settingsPage = (page: Page) => {
return {
accountButton: page.getByRole('button', {name: 'Account'}),
};
};
276 changes: 276 additions & 0 deletions e2e-tests/specs/regression/menuBar.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
/*
* Wire
* Copyright (C) 2026 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 {MenuItem} from 'electron';

import {conversationsList} from './../../poms/webapp/conversationList.page';

import {connectWithUser} from '../../actions/connectWithUser';
import {type App} from '../../actions/createApp';
import {createGroup} from '../../actions/createGroup';
import {loginUser} from '../../actions/loginUser';
import {test, expect, Page} from '../../fixtures';
import {callCell} from '../../poms/webapp/callCell.page';
import {conversation} from '../../poms/webapp/conversation.page';
import {conversationsSidebar} from '../../poms/webapp/conversationsSidebar.page';
import {loginPage} from '../../poms/webapp/login.page';
import {settingsPage} from '../../poms/webapp/settings.page';

/**
* Triggers an Electron application menu item by matching its label.
* Supports cross-platform variants by accepting either a single string or an array of strings.
* * @param {object} app - The Playwright Electron application instance
* @param {string[]} labels - The menu label(s) to match against (e.g., 'Settings' or ['Preferences', 'Settings'])
* @returns A Promise resolving to the serialized accelerator string of the clicked MenuItem
*/

const triggerApplicationMenu = async (app: App, labels: string[]): Promise<Pick<MenuItem, 'accelerator'>> => {
return await app.evaluate(async ({Menu, BrowserWindow}, targets) => {
const menu = Menu.getApplicationMenu();

const target = menu?.items.flatMap(item => item.submenu?.items ?? []).find(item => targets.includes(item.label));

if (!target) {
throw new Error('Menu item not found');
}

const targetWindow = BrowserWindow.getFocusedWindow() ?? BrowserWindow.getAllWindows()[0];
if (!targetWindow) {
throw new Error('No Electron window found to send the menu event to');
}

// Programmatically trigger the menu item's click action
target.click(target, targetWindow);
return {
accelerator: target.accelerator,
};
}, labels);
};

test.describe('Menu Bar', () => {
test('Open preferences/settings with menu bar', {tag: ['@TC-11010', '@regression']}, async ({app, createUser}) => {
const user = await createUser();
await loginUser(app.page, user);

// Access the native Electron application menu and click the appropriate item
const menuItem = await triggerApplicationMenu(app, ['Preferences', 'Settings']);

expect(menuItem.accelerator).toMatch(/^(Command\+,|Ctrl\+,)$/);
await expect(settingsPage(app.page).accountButton).toBeVisible();
});

test(
'Verify switching to next and previous conversation using menu bar',
{tag: ['@TC-11068', '@regression']},
async ({app, createUser}) => {
const user = await createUser();
await loginUser(app.page, user);

await test.step('Create multiple group conversations', async () => {
await createGroup(app.page, 'Group 1', []);
await createGroup(app.page, 'Group 2', []);
await expect(conversationsList(app.page).items).toHaveCount(2);
});

await test.step('Verify navigation to the next conversation and validate its keyboard shortcut', async () => {
await conversationsList(app.page).getConversation('Group 1').open();
const menuItem = await triggerApplicationMenu(app, ['Next Conversation']);

expect(menuItem.accelerator).toMatch(/^(Alt\+(Cmd|Shift)\+Up)$/);
await expect(conversation(app.page).conversationTitle).toContainText('Group 2');
});

await test.step('Navigate to the previous conversation via the application menu', async () => {
const menuItem = await triggerApplicationMenu(app, ['Previous Conversation']);

expect(menuItem.accelerator).toMatch(/^(Alt\+(Cmd|Shift)\+Down)$/);
await expect(conversation(app.page).conversationTitle).toContainText('Group 1');
});
},
);

test(
'Verify I can create a group conversation with menu bar',
{tag: ['@TC-11066', '@regression']},
async ({app, createUser}) => {
const user = await createUser();
await loginUser(app.page, user);

const menuItem = await triggerApplicationMenu(app, ['Create Group']);

expect(menuItem.accelerator).toBe('CmdOrCtrl+N');
await expect(app.page.getByRole('dialog').getByText('Create group')).toBeVisible();
},
);

test('Sign out with menu bar', {tag: ['@TC-11041', '@regression']}, async ({app, createUser}) => {
const user = await createUser();
await loginUser(app.page, user);

await triggerApplicationMenu(app, ['Log Out']);
await app.page.getByRole('dialog').getByRole('button', {name: 'Log out'}).click();
await expect(loginPage(app.page).loginButton).toBeVisible();
});

test(
'Verify adding people to the conversation with menu bar',
{tag: ['@TC-11046', '@regression']},
async ({app, createUser}) => {
const user = await createUser();
await loginUser(app.page, user);

await createGroup(app.page, 'Test group', []);
await conversationsList(app.page).getConversation('Test group').open();

const menuItem = await triggerApplicationMenu(app, ['Add People...']);

expect(menuItem.accelerator).toBe('Shift+CmdOrCtrl+K');
await expect(app.page.getByRole('complementary').filter({hasText: 'Add participants'})).toBeVisible();
},
);

test(
'Archive a 1:1 and a group conversation with menu bar',
{tag: ['@TC-11067', '@regression']},
async ({app, createUser, createTeam, createPage}) => {
const userB = await createUser();
const {owner: userA} = await createTeam('Test Team', {users: [userB]});
const userAPage = app.page;
const userBPage = await createPage();

await Promise.all([loginUser(userAPage, userA), loginUser(userBPage, userB)]);
await connectWithUser(userAPage, userB);

await createGroup(app.page, 'Test group', []);
await expect(conversationsList(app.page).items).toHaveCount(2);

await test.step('Archive in 1:1 conversation', async () => {
await conversationsList(app.page).getConversation(userB.fullName, {protocol: 'mls'}).open();
const menuItem = await triggerApplicationMenu(app, ['Archive']);

expect(menuItem.accelerator).toBe('CmdOrCtrl+D');
await expect(conversationsList(app.page).getConversation(userB.fullName, {protocol: 'mls'})).not.toBeVisible();
await expect(conversationsList(app.page).items).toHaveCount(1);
});

await test.step('Archive group conversation', async () => {
await conversationsList(userAPage).getConversation('Test group').open();
await triggerApplicationMenu(app, ['Archive']);
await expect(conversationsList(app.page).getConversation(userB.fullName, {protocol: 'mls'})).not.toBeVisible();
await expect(conversationsList(app.page).items).toHaveCount(0);
});

await test.step('Confirm that conversations were moved to archive folder', async () => {
await conversationsSidebar(userAPage).archiveButton.click();
await expect(conversationsList(app.page).items).toHaveCount(2);
});
},
);

const testCases = [
{
name: 'Delete conversation content with menu bar',
tag: '@TC-11042',
menuItem: 'Delete Content...',
verifyDirect: async (page: Page) => {
await expect(page.getByRole('dialog').getByText('Clear content?')).toBeVisible();
await page.getByRole('dialog').getByRole('button', {name: 'Cancel'}).click();
},
verifyGroup: async (page: Page) => {
await expect(page.getByRole('dialog').getByText('Clear content?')).toBeVisible();
},
},
{
name: 'Verify I can ping 1:1 and group conversation using the menu bar',
tag: '@TC-11043',
menuItem: 'Ping',
verifyDirect: async (page: Page) => {
await expect(conversation(page).systemMessages.filter({hasText: 'You pinged'})).toBeVisible();
},
verifyGroup: async (page: Page) => {
await expect(conversation(page).systemMessages.filter({hasText: 'You pinged'})).toBeVisible();
},
},
{
name: 'Verify starting a call via the menu bar',
tag: '@TC-11044',
menuItem: 'Call',
verifyDirect: async (page: Page) => {
await expect(callCell(page)).toBeVisible();
await callCell(page).declineButton.click();
},
verifyGroup: async (page: Page) => {
await expect(callCell(page)).toBeVisible();
},
},
];

testCases.forEach(({name, tag, menuItem, verifyDirect, verifyGroup}) => {
test(name, {tag: [tag, '@regression']}, async ({app, createUser, createTeam, createPage}) => {
const userB = await createUser();
const {owner: userA} = await createTeam('Test Team', {users: [userB]});
const userAPage = app.page;
const userBPage = await createPage();

await Promise.all([loginUser(userAPage, userA), loginUser(userBPage, userB)]);
await connectWithUser(userAPage, userB);

await test.step('User A actions in 1:1 conversation', async () => {
await conversationsList(userAPage).getConversation(userB.fullName, {protocol: 'mls'}).open();
await triggerApplicationMenu(app, [menuItem]);
await verifyDirect(app.page);
});

await test.step('User A actions in group conversation', async () => {
await createGroup(app.page, 'Test group', [userB]);
await conversationsList(userAPage).getConversation('Test group').open();
await triggerApplicationMenu(app, [menuItem]);
await verifyGroup(app.page);
});
});
});

test(
'Verify opening people popover with menu bar in the conversation',
{tag: ['@TC-11045', '@regression']},
async ({app, createUser, createTeam, createPage}) => {
const userB = await createUser();
const {owner: userA} = await createTeam('Test Team', {users: [userB]});
const userAPage = app.page;
const userBPage = await createPage();

await Promise.all([loginUser(userAPage, userA), loginUser(userBPage, userB)]);
await connectWithUser(userAPage, userB);

await test.step('User A can open people popover in 1:1 conversation', async () => {
await conversationsList(userAPage).getConversation(userB.fullName, {protocol: 'mls'}).open();
const menuResult = await triggerApplicationMenu(app, ['People']);
expect(menuResult?.accelerator).toBe('CmdOrCtrl+I');
await expect(app.page.getByTestId('status-profile-picture')).toBeVisible();
});

await test.step('User A can open conversation details in group conversation', async () => {
await createGroup(app.page, 'Test group', [userB]);
await conversationsList(userAPage).getConversation('Test group').open();
await triggerApplicationMenu(app, ['People']);
await expect(app.page.getByTestId('list-users')).toBeVisible();
});
},
);
});
Loading