Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions test/e2e_tests/pageManager/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import {ErrorModal} from './webapp/modals/error.modal';
import {ExportBackupModal} from './webapp/modals/exportBackup.modal';
import {importBackupModal} from './webapp/modals/importBackup.modal';
import {LeaveConversationModal} from './webapp/modals/leaveConversation.modal';
import {PasswordModal} from './webapp/modals/password.modal';
import {RemoveMemberModal} from './webapp/modals/removeMember.modal';
import {UnableToOpenConversationModal} from './webapp/modals/unableToOpenConversation.modal';
import {UserProfileModal} from './webapp/modals/userProfile.modal';
Expand All @@ -61,6 +62,7 @@ import {ConversationPage} from './webapp/pages/conversation.page';
import {ConversationDetailsPage} from './webapp/pages/conversationDetails.page';
import {ConversationListPage} from './webapp/pages/conversationList.page';
import {DeleteAccountPage} from './webapp/pages/deleteAccount.page';
import {DevicesPage} from './webapp/pages/devices.page';
import {EmailVerificationPage} from './webapp/pages/emailVerification.page';
import {GroupCreationPage} from './webapp/pages/groupCreation.page';
import {GuestOptionsPage} from './webapp/pages/guestOptions.page';
Expand Down Expand Up @@ -175,6 +177,7 @@ export class PageManager {
connectRequest: () => this.getOrCreate('webapp.pages.connectRequest', () => new ConnectRequestPage(this.page)),
calling: () => this.getOrCreate('webapp.pages.calling', () => new CallingPage(this.page)),
settings: () => this.getOrCreate('webapp.pages.settings', () => new SettingsPage(this.page)),
devices: () => this.getOrCreate('webapp.pages.devices', () => new DevicesPage(this.page)),
options: () => this.getOrCreate('webapp.pages.options', () => new OptionsPage(this.page)),
audioVideoSettings: () =>
this.getOrCreate('webapp.pages.audioVideoSettings', () => new AudioVideoSettingsPage(this.page)),
Expand Down Expand Up @@ -224,6 +227,7 @@ export class PageManager {
this.getOrCreate('webapp.modals.marketingConsent', () => new MarketingConsentModal(this.page)),
acknowledge: () => this.getOrCreate('webapp.modals.marketingConsent', () => new AcknowledgeModal(this.page)),
confirm: () => this.getOrCreate('webapp.modals.confirm', () => new ConfirmModal(this.page)),
password: () => this.getOrCreate('webapp.modals.password', () => new PasswordModal(this.page)),
cellsFileDetailView: () =>
this.getOrCreate('webapp.modals.cellsFileDetailView', () => new CellsFileDetailViewModal(this.page)),
cancelRequest: () =>
Expand Down
33 changes: 33 additions & 0 deletions test/e2e_tests/pageManager/webapp/modals/password.modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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';

export class PasswordModal extends BaseModal {
readonly passwordInput: Locator;

constructor(page: Page) {
// For some reason the password modal is rendered using the `modal-template-option` attribute
super(page, 'modal-template-option');

this.passwordInput = this.modal.getByLabel('Password');
}
}
35 changes: 35 additions & 0 deletions test/e2e_tests/pageManager/webapp/pages/devices.page.ts
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';

export class DevicesPage {
readonly page: Page;

readonly proteusId: Locator;
readonly activeDevices: Locator;

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

// The id is not using any label or similar but just multiple paragraphs beneath each other
this.proteusId = page.locator("p:text('Proteus ID') + p");
this.activeDevices = page.getByRole('group', {name: 'Active'}).getByRole('button', {name: /device details/});
}
}
2 changes: 2 additions & 0 deletions test/e2e_tests/pageManager/webapp/pages/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginErrorText: Locator;
readonly publicComputerCheckbox: Locator;

constructor(page: Page) {
this.page = page;
Expand All @@ -36,6 +37,7 @@ export class LoginPage {
this.emailInput = page.locator('[data-uie-name="enter-email"]');
this.passwordInput = page.locator('[data-uie-name="enter-password"]');
this.loginErrorText = page.locator('[data-uie-name="error-message"]');
this.publicComputerCheckbox = page.getByText('This is a public computer');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest to use a better locator. Maybe by role or input type.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@e-maad I wanted to do so as well, ideally locating the checkbox by label. However turns out the checkboxes are absolutely not accessible ^^'
That's also the reason I have to use .click() on the locator instead of the native .check() which I'd prefer. Maybe at some point it would be possible to make the underlying component library accessible.

}

async login(user: Pick<User, 'email' | 'password'>) {
Expand Down
12 changes: 7 additions & 5 deletions test/e2e_tests/pageManager/webapp/pages/settings.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,21 @@ import {Page, Locator} from '@playwright/test';
export class SettingsPage {
readonly page: Page;

readonly audioVideoSettingsButton: Locator;
readonly accountButton: Locator;
readonly devicesButton: Locator;
readonly optionsButton: Locator;
readonly audioVideoButton: Locator;

constructor(page: Page) {
this.page = page;
this.audioVideoSettingsButton = page.locator("[data-uie-name='go-audio-video']");
this.accountButton = page.locator("[data-uie-name='go-account']");
this.optionsButton = page.locator("[data-uie-name='go-options']");
this.accountButton = page.getByRole('button', {name: 'Account'});
this.devicesButton = page.getByRole('button', {name: 'Devices'});
this.optionsButton = page.getByRole('button', {name: 'Options'});
this.audioVideoButton = page.getByRole('button', {name: 'Audio / Video'});
}

async clickAudioVideoSettingsButton() {
await this.audioVideoSettingsButton.click();
await this.audioVideoButton.click();
}

async clickAccountButton() {
Expand Down
225 changes: 224 additions & 1 deletion test/e2e_tests/specs/Authentication/authentication.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,34 @@
*
*/

import {test, expect} from 'test/e2e_tests/test.fixtures';
import {IncomingMessage} from 'node:http';
import https from 'node:https';
import {SecureVersion} from 'node:tls';

import {PageManager, webAppPath} from 'test/e2e_tests/pageManager';
import {test, expect, withLogin} from 'test/e2e_tests/test.fixtures';
import {connectWithUser} from 'test/e2e_tests/utils/userActions';

test.describe('Authentication', () => {
test(
'Verify sign in button is disabled in case of empty credentials',
{tag: ['@TC-3457', '@regression']},
async ({pageManager}) => {
const {pages} = pageManager.webapp;
const {emailInput, passwordInput, signInButton} = pages.login();
await pageManager.openLoginPage();

await expect(signInButton).toBeDisabled();

await emailInput.fill('invalid@email');
await passwordInput.fill('invalidPassword');
await expect(signInButton).not.toBeDisabled();

await passwordInput.clear();
await expect(signInButton).toBeDisabled();
},
);

test(
'I want to be asked to share telemetry data when I log in',
{tag: ['@TC-8780', '@regression']},
Expand All @@ -33,4 +58,202 @@ test.describe('Authentication', () => {
await expect(modals.dataShareConsent().modalTitle).toBeVisible();
},
);

test(
'Verify sign in error appearance in case of suspended team account',
{tag: ['@TC-3468', '@regression']},
async ({pageManager}) => {
const {pages} = pageManager.webapp;
await pageManager.openLoginPage();

const userOfSuspendedTeam = {email: '[email protected]', password: '12345678'};
await pages.login().login(userOfSuspendedTeam);

await expect(pages.login().loginErrorText).toHaveText('This account is no longer authorized to log in');
},
);

test(
'Verify current browser is set as temporary device',
{tag: ['@TC-3460', '@regression']},
async ({pageManager, createUser}) => {
const user = await createUser();
const {pages, components} = pageManager.webapp;

await test.step('Log in with public computer checked', async () => {
await pageManager.openLoginPage();
await pages.login().publicComputerCheckbox.click();
await pages.login().login(user);
await pages.historyInfo().clickConfirmButton();
});

let proteusId: string;
await test.step('Open device settings and get current proteus id', async () => {
await components.conversationSidebar().clickPreferencesButton();
await pages.settings().devicesButton.click();

proteusId = (await pages.devices().proteusId.textContent()) ?? '';
expect(proteusId).toBeTruthy();
});

await test.step('Log out of public computer', async () => {
await pages.settings().accountButton.click();
await pages.account().clickLogoutButton();
});

await test.step('Log in again on non public computer', async () => {
await pageManager.openLoginPage();
await pages.login().login(user);
});

await test.step("Open device settings and ensure the public computer isn't active and the ID was re-generated", async () => {
await components.conversationSidebar().clickPreferencesButton();
await pages.settings().devicesButton.click();

await expect(pages.devices().activeDevices).toHaveCount(0);
await expect(pages.devices().proteusId).not.toContainText(proteusId);
});
},
);

test(
'Verify sign in error appearance in case of wrong credentials',
{tag: ['@TC-3465', '@regression']},
async ({pageManager}) => {
const {pages} = pageManager.webapp;
await pageManager.openLoginPage();

await pages.login().login({email: '[email protected]', password: 'invalid'});

await expect(pages.login().loginErrorText).toHaveText('Please verify your details and try again');
},
);

[
{tag: '@TC-3472', deviceType: 'permanent'},
{tag: '@TC-3473', deviceType: 'temporary'},
].forEach(({deviceType, tag}) => {
test(
`I want to keep my history after refreshing the page on ${deviceType} device`,
{tag: [tag, '@regression']},
async ({pageManager, createTeam}) => {
const {pages} = pageManager.webapp;
const team = await createTeam('Test Team', {withMembers: 1});
const userA = team.owner;
const userB = team.members[0];

await test.step('Log in and connect with user B', async () => {
await pageManager.openLoginPage();

if (deviceType === 'temporary') {
await pages.login().publicComputerCheckbox.click();
await pages.login().login(userA);
await pages.historyInfo().clickConfirmButton();
} else {
await pages.login().login(userA);
}

await connectWithUser(pageManager, userB);
});

await test.step('Send a message', async () => {
await pages.conversationList().openConversation(userB.fullName);
await pages.conversation().sendMessage('Before refresh');
});

await test.step('Ensure message is still visible after page refresh', async () => {
const message = pages.conversation().getMessage({content: 'Before refresh'});
await expect(message).toBeVisible();

await pageManager.refreshPage();

await pages.conversationList().openConversation(userB.fullName);
await expect(message).toBeVisible();
});
},
);
});

// Bug: Connecting using TLSv1.2 should not be allowed but succeeds
test.skip(
'I want to make sure i connect to webapp only through TLS >= 1.3 connection',
{tag: ['@TC-3480', '@regression']},
async () => {
const requestWithTlsVersion = (versions: {min?: SecureVersion; max?: SecureVersion}) =>
new Promise<IncomingMessage>((res, rej) => {
https.get(webAppPath, {minVersion: versions.min, maxVersion: versions.max}, res).on('error', rej);
});

await expect(requestWithTlsVersion({max: 'TLSv1.2'})).rejects.toBeDefined();
await expect(requestWithTlsVersion({min: 'TLSv1.3'})).resolves.toBeDefined();
},
);

test(
'Make sure user does not see data of user of previous sessions on same browser',
{tag: ['@TC-1311', '@regression']},
async ({pageManager, createTeam}) => {
const {pages, components} = pageManager.webapp;
const team = await createTeam('Test Team', {withMembers: 1});
const userA = team.owner;
const userB = team.members[0];

await test.step('Log in with public computer checked', async () => {
await pageManager.openLoginPage();
await pages.login().publicComputerCheckbox.click();
await pages.login().login(userA);
await pages.historyInfo().clickConfirmButton();
});

await test.step('Connect with and send message to userB', async () => {
await connectWithUser(pageManager, userB);
await pages.conversationList().openConversation(userB.fullName);
await pages.conversation().sendMessage('Test message');
});

await test.step('Log out of public computer', async () => {
await components.conversationSidebar().clickPreferencesButton();
await pages.settings().accountButton.click();
await pages.account().clickLogoutButton();
});

await test.step('Log in again', async () => {
await pageManager.openLoginPage();
await pages.login().login(userA);
await pages.historyInfo().clickConfirmButton();
});

await test.step('Verify previously sent message is gone', async () => {
await pages.conversationList().openConversation(userB.fullName);
await expect(pages.conversation().getMessage({content: 'Test message'})).not.toBeAttached();
});
},
);

test(
'Verify session expired info is visible on login page',
{tag: ['@TC-1311', '@regression']},
async ({createPage, createUser}) => {
const user = await createUser();
const device1Pages = PageManager.from(await createPage(withLogin(user))).webapp.pages;

const {
pages: device2Pages,
modals: device2Modals,
components: device2Components,
} = PageManager.from(await createPage(withLogin(user))).webapp;
await device2Pages.historyInfo().clickConfirmButton();

await device2Components.conversationSidebar().clickPreferencesButton();
await device2Pages.settings().devicesButton.click();
await device2Pages.devices().activeDevices.getByRole('button', {name: 'Remove Device'}).click();

await device2Modals.password().passwordInput.fill(user.password);
await device2Modals.password().clickAction();

await expect(
device1Pages.singleSignOn().page.getByText('You were signed out because your device was deleted'),
).toBeVisible();
},
);
});
Loading