snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
import { Account, AccountSectionsHref } from './account';
|
||||
|
||||
export class AccountAccessibility extends Account {
|
||||
protected readonly route = AccountSectionsHref.accessibilityAndAppearance;
|
||||
|
||||
protected readonly title = 'Accessibility & appearance';
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Account, AccountSectionsHref } from './account';
|
||||
|
||||
export class AccountFeaturePreview extends Account {
|
||||
protected readonly route = AccountSectionsHref.featurePreview;
|
||||
|
||||
protected readonly title = 'Feature preview';
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Account, AccountSectionsHref } from './account';
|
||||
|
||||
export class AccountOmnichannel extends Account {
|
||||
protected readonly route = AccountSectionsHref.omnichannel;
|
||||
|
||||
protected readonly title = 'Omnichannel';
|
||||
}
|
||||
107
apps/meteor/tests/e2e/page-objects/account-profile.ts
Normal file
107
apps/meteor/tests/e2e/page-objects/account-profile.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Account, AccountSectionsHref } from './account';
|
||||
import { DeleteAccountModal } from './fragments';
|
||||
|
||||
export class AccountProfile extends Account {
|
||||
protected readonly route = AccountSectionsHref.profile;
|
||||
|
||||
protected readonly title = 'Profile';
|
||||
|
||||
readonly deleteAccountModal: DeleteAccountModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.deleteAccountModal = new DeleteAccountModal(page);
|
||||
}
|
||||
|
||||
get inputName(): Locator {
|
||||
return this.page.locator('//label[contains(text(), "Name")]/..//input');
|
||||
}
|
||||
|
||||
get inputAvatarLink(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Use URL for avatar' });
|
||||
}
|
||||
|
||||
get btnSetAvatarLink(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Add URL', exact: true });
|
||||
}
|
||||
|
||||
get inputUsername(): Locator {
|
||||
return this.page.locator('//label[contains(text(), "Username")]/..//input');
|
||||
}
|
||||
|
||||
// TODO: remove this locator
|
||||
get btnSubmit(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save changes', exact: true });
|
||||
}
|
||||
|
||||
get avatarFileInput(): Locator {
|
||||
return this.page.locator('.avatar-file-input');
|
||||
}
|
||||
|
||||
get userAvatarEditor(): Locator {
|
||||
return this.page.getByAltText('profile picture');
|
||||
}
|
||||
|
||||
get emailTextInput(): Locator {
|
||||
return this.page.locator('//label[contains(text(), "Email")]/..//input');
|
||||
}
|
||||
|
||||
get inputStatusText(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Status' });
|
||||
}
|
||||
|
||||
get selectClearStatusAfter(): Locator {
|
||||
return this.page.getByLabel('Clear status after');
|
||||
}
|
||||
|
||||
async chooseClearStatusAfter(option: string): Promise<void> {
|
||||
await this.selectClearStatusAfter.click();
|
||||
await this.page.getByRole('option', { name: new RegExp(option) }).click();
|
||||
}
|
||||
|
||||
get preferencesSoundAccordionOption(): Locator {
|
||||
return this.page.locator('h2:has-text("Sound")');
|
||||
}
|
||||
|
||||
get preferencesCallRingerVolumeSlider(): Locator {
|
||||
return this.page.getByRole('slider', { name: 'Call Ringer Volume' });
|
||||
}
|
||||
|
||||
get btnClose(): Locator {
|
||||
return this.page.locator('role=navigation >> role=button[name=Close]');
|
||||
}
|
||||
|
||||
get inputImageFile(): Locator {
|
||||
return this.page.locator('input[type=file]');
|
||||
}
|
||||
|
||||
getAccordionItemByName(name: string): Locator {
|
||||
return this.page.getByRole('button', { name, exact: true });
|
||||
}
|
||||
|
||||
getCheckboxByLabelText(name: string): Locator {
|
||||
return this.page.locator('label', { has: this.page.getByRole('checkbox', { name }) });
|
||||
}
|
||||
|
||||
get btnSaveChanges(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save changes', exact: true });
|
||||
}
|
||||
|
||||
get profileHeading(): Locator {
|
||||
return this.page.getByRole('heading', { name: 'Profile', exact: true });
|
||||
}
|
||||
|
||||
get btnDeleteMyAccount(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Delete my account' });
|
||||
}
|
||||
|
||||
private getErrorAlertByText(text: string): Locator {
|
||||
return this.page.getByRole('alert').filter({ hasText: text });
|
||||
}
|
||||
|
||||
get errorInvalidUrl(): Locator {
|
||||
return this.getErrorAlertByText('Invalid image URL');
|
||||
}
|
||||
}
|
||||
103
apps/meteor/tests/e2e/page-objects/account-security.ts
Normal file
103
apps/meteor/tests/e2e/page-objects/account-security.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Account, AccountSectionsHref } from './account';
|
||||
import { EnterPasswordModal } from './fragments/modals';
|
||||
|
||||
export class AccountSecurity extends Account {
|
||||
protected readonly route = AccountSectionsHref.security;
|
||||
|
||||
protected readonly title = 'Security';
|
||||
|
||||
private readonly enterPasswordModal: EnterPasswordModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.enterPasswordModal = new EnterPasswordModal(page);
|
||||
}
|
||||
|
||||
get inputNewPassword() {
|
||||
return this.page.getByRole('textbox', { name: 'New password' });
|
||||
}
|
||||
|
||||
private get inputConfirmPassword() {
|
||||
return this.page.getByRole('textbox', { name: 'Confirm password' });
|
||||
}
|
||||
|
||||
private get btnExpandE2EESection() {
|
||||
return this.page.getByRole('button', { name: 'End-to-end encryption' });
|
||||
}
|
||||
|
||||
private get btnResetE2EEPassword() {
|
||||
return this.page.getByRole('button', { name: 'Reset E2EE password' });
|
||||
}
|
||||
|
||||
private get inputNewE2EEPassword() {
|
||||
return this.page.getByRole('textbox', { name: 'New E2EE password' });
|
||||
}
|
||||
|
||||
private get inputConfirmNewE2EEPassword() {
|
||||
return this.page.getByRole('textbox', { name: 'Confirm new E2EE password' });
|
||||
}
|
||||
|
||||
get securityHeader(): Locator {
|
||||
return this.page.getByRole('main').getByRole('heading', { level: 1, name: 'Security', exact: true });
|
||||
}
|
||||
|
||||
get securityPasswordSection(): Locator {
|
||||
return this.page.locator('[role="button"]:has-text("Password")');
|
||||
}
|
||||
|
||||
get security2FASection(): Locator {
|
||||
return this.page.locator('[role="button"]:has-text("Two Factor Authentication")');
|
||||
}
|
||||
|
||||
get securityE2EEncryptionSection(): Locator {
|
||||
return this.page.locator('[role="button"]:has-text("End-to-end encryption")');
|
||||
}
|
||||
|
||||
get securityE2EEncryptionResetKeyButton(): Locator {
|
||||
return this.page.locator("role=button[name='Reset E2EE password']");
|
||||
}
|
||||
|
||||
get securityE2EEncryptionSavePasswordButton(): Locator {
|
||||
return this.page.locator("role=button[name='Save changes']");
|
||||
}
|
||||
|
||||
get email2FASwitch(): Locator {
|
||||
return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Two-factor authentication via email' }) });
|
||||
}
|
||||
|
||||
get totp2FASwitch(): Locator {
|
||||
return this.page.locator('label', { has: this.page.getByRole('checkbox', { name: 'Two-factor authentication via TOTP' }) });
|
||||
}
|
||||
|
||||
get required2faModalSetUpButton(): Locator {
|
||||
return this.page.locator('dialog >> button');
|
||||
}
|
||||
|
||||
async changePassword(newPassword: string, confirmPassword: string, currentPassword: string) {
|
||||
await this.inputNewPassword.fill(newPassword);
|
||||
await this.inputConfirmPassword.fill(confirmPassword);
|
||||
await this.saveChangesButton.click();
|
||||
await this.enterPasswordModal.enterPassword(currentPassword);
|
||||
}
|
||||
|
||||
async resetE2EEPassword() {
|
||||
await this.btnExpandE2EESection.click();
|
||||
await this.btnResetE2EEPassword.click();
|
||||
await this.toastMessage.dismissToast('success');
|
||||
// Logged out
|
||||
}
|
||||
|
||||
async setE2EEPassword(newPassword: string) {
|
||||
await this.btnExpandE2EESection.click();
|
||||
await this.inputNewE2EEPassword.fill(newPassword);
|
||||
await this.inputConfirmNewE2EEPassword.fill(newPassword);
|
||||
await this.saveChangesButton.click();
|
||||
await this.toastMessage.dismissToast('success');
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.sidebar.close();
|
||||
}
|
||||
}
|
||||
49
apps/meteor/tests/e2e/page-objects/account-tokens.ts
Normal file
49
apps/meteor/tests/e2e/page-objects/account-tokens.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { Account, AccountSectionsHref } from './account';
|
||||
|
||||
export class AccountTokens extends Account {
|
||||
protected readonly route = AccountSectionsHref.tokens;
|
||||
|
||||
protected readonly title = 'Personal Access Tokens';
|
||||
|
||||
get inputToken(): Locator {
|
||||
return this.page.locator('[data-qa="PersonalTokenField"]');
|
||||
}
|
||||
|
||||
get tokensTableEmpty(): Locator {
|
||||
return this.page.locator('//h3[contains(text(), "No results found")]');
|
||||
}
|
||||
|
||||
get btnTokensAdd(): Locator {
|
||||
return this.page.locator('role=button[name="Add"]');
|
||||
}
|
||||
|
||||
get tokenAddedModal(): Locator {
|
||||
return this.page.locator('role=dialog[name="Personal Access Token successfully generated"]');
|
||||
}
|
||||
|
||||
get btnTokenAddedOk(): Locator {
|
||||
return this.tokenAddedModal.locator('role=button[name="Ok"]');
|
||||
}
|
||||
|
||||
get tokensRows(): Locator {
|
||||
return this.page.locator('table tbody tr');
|
||||
}
|
||||
|
||||
tokenInTable(name: string): Locator {
|
||||
return this.page.locator(`tr[qa-token-name="${name}"]`);
|
||||
}
|
||||
|
||||
get btnRegenerateTokenModal(): Locator {
|
||||
return this.page.locator('role=button[name="Regenerate token"]');
|
||||
}
|
||||
|
||||
get removeTokenModal(): Locator {
|
||||
return this.page.locator('role=dialog', { hasText: 'personal access token' });
|
||||
}
|
||||
|
||||
get btnRemoveTokenModal(): Locator {
|
||||
return this.removeTokenModal.getByRole('button', { name: 'Remove' });
|
||||
}
|
||||
}
|
||||
33
apps/meteor/tests/e2e/page-objects/account.manage-devices.ts
Normal file
33
apps/meteor/tests/e2e/page-objects/account.manage-devices.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Page } from 'playwright-core';
|
||||
|
||||
import { Account, AccountSectionsHref } from './account';
|
||||
import { ConfirmLogoutModal, DevicesTable } from './fragments';
|
||||
|
||||
export class AccountManageDevices extends Account {
|
||||
protected readonly route = AccountSectionsHref.manageDevices;
|
||||
|
||||
protected readonly title = 'Manage Devices';
|
||||
|
||||
readonly logoutModal: ConfirmLogoutModal;
|
||||
|
||||
readonly table: DevicesTable;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.logoutModal = new ConfirmLogoutModal(page.getByRole('dialog', { name: 'Log out device' }));
|
||||
this.table = new DevicesTable(page);
|
||||
}
|
||||
|
||||
async getNthDeviceId(nth: number): Promise<string> {
|
||||
const deviceId = await this.pageContent.getByRole('row').nth(nth).getAttribute('aria-label');
|
||||
if (!deviceId) {
|
||||
throw new Error(`Device ID not found for the device ${nth}`);
|
||||
}
|
||||
return deviceId || '';
|
||||
}
|
||||
|
||||
async logoutDeviceById(deviceId: string) {
|
||||
await this.table.getDeviceRowById(deviceId).getByRole('button', { name: 'Logout' }).click();
|
||||
await this.logoutModal.confirmLogout();
|
||||
}
|
||||
}
|
||||
50
apps/meteor/tests/e2e/page-objects/account.ts
Normal file
50
apps/meteor/tests/e2e/page-objects/account.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { AccountSidebar, ToastMessages } from './fragments';
|
||||
import { RoutedPage } from './routed-page';
|
||||
|
||||
export enum AccountSectionsHref {
|
||||
preferences = '/account/preferences',
|
||||
profile = '/account/profile',
|
||||
security = '/account/security',
|
||||
integrations = '/account/integrations',
|
||||
tokens = '/account/tokens',
|
||||
omnichannel = '/account/omnichannel',
|
||||
featurePreview = '/account/feature-preview',
|
||||
accessibilityAndAppearance = '/account/accessibility-and-appearance',
|
||||
manageDevices = '/account/manage-devices',
|
||||
}
|
||||
|
||||
/**
|
||||
* A page under `/account/*`. Subclasses declare `route` and `title` and get `goto()`,
|
||||
* `pageContent` and their ready state for free.
|
||||
*/
|
||||
export abstract class Account extends RoutedPage {
|
||||
protected abstract override readonly route: AccountSectionsHref;
|
||||
|
||||
/** Text of the page's main heading. Identifies the page and scopes `pageContent`. */
|
||||
protected abstract readonly title: string;
|
||||
|
||||
readonly toastMessage: ToastMessages;
|
||||
|
||||
readonly sidebar: AccountSidebar;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.toastMessage = new ToastMessages(page);
|
||||
this.sidebar = new AccountSidebar(page);
|
||||
}
|
||||
|
||||
/** The `main` region of this page, scoped by its heading so it cannot match another route. */
|
||||
get pageContent(): Locator {
|
||||
return this.page.getByRole('main').filter({ has: this.page.getByRole('heading', { name: this.title, exact: true }) });
|
||||
}
|
||||
|
||||
async waitForReady(): Promise<void> {
|
||||
await this.pageContent.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
protected get saveChangesButton() {
|
||||
return this.page.getByRole('button', { name: 'Save changes' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
import { MenuOptions, DevicesTable } from './fragments';
|
||||
import { DeviceInfoFlexTab } from './fragments/flextabs';
|
||||
import { ConfirmLogoutModal } from './fragments/modals';
|
||||
|
||||
export class AdminDeviceManagement extends Admin {
|
||||
readonly deviceInfo: DeviceInfoFlexTab;
|
||||
|
||||
readonly deviceRowMenu: MenuOptions;
|
||||
|
||||
readonly table: DevicesTable;
|
||||
|
||||
readonly logoutModal: ConfirmLogoutModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.deviceInfo = new DeviceInfoFlexTab(page.getByRole('dialog', { name: 'Device Info' }));
|
||||
this.deviceRowMenu = new MenuOptions(page);
|
||||
this.table = new DevicesTable(page);
|
||||
this.logoutModal = new ConfirmLogoutModal(page.getByRole('dialog', { name: 'Log out device' }));
|
||||
}
|
||||
|
||||
protected readonly route = AdminSectionsHref.deviceManagement;
|
||||
|
||||
protected readonly title = 'Device management';
|
||||
|
||||
get upsellModal(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Device management' });
|
||||
}
|
||||
|
||||
get emptyState(): Locator {
|
||||
return this.pageContent.getByRole('heading', { name: 'No results found', exact: true });
|
||||
}
|
||||
|
||||
async searchUserDevice(user: string): Promise<void> {
|
||||
await this.pageContent.getByRole('textbox', { name: 'Search devices or users' }).fill(user);
|
||||
}
|
||||
|
||||
async getUsersDeviceId(username: string): Promise<string> {
|
||||
await this.searchUserDevice(username);
|
||||
const deviceId = await this.pageContent
|
||||
.getByRole('row')
|
||||
.filter({ has: this.page.getByRole('cell', { name: username }) })
|
||||
.first()
|
||||
.getAttribute('aria-label');
|
||||
|
||||
return deviceId || '';
|
||||
}
|
||||
|
||||
async openDeviceOptionsById(deviceId: string) {
|
||||
const row = this.table.getDeviceRowById(deviceId);
|
||||
await row.getByRole('button', { name: 'Options' }).click();
|
||||
}
|
||||
|
||||
async logoutDeviceById(deviceId: string) {
|
||||
await this.openDeviceOptionsById(deviceId);
|
||||
await this.deviceRowMenu.getMenuItem('Log out device').click();
|
||||
await this.logoutModal.confirmLogout();
|
||||
}
|
||||
}
|
||||
69
apps/meteor/tests/e2e/page-objects/admin-email-inboxes.ts
Normal file
69
apps/meteor/tests/e2e/page-objects/admin-email-inboxes.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
export class AdminEmailInboxes extends Admin {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
get btnNewEmailInbox(): Locator {
|
||||
return this.page.locator('role=button[name="New Email Inbox"]');
|
||||
}
|
||||
|
||||
protected readonly route = AdminSectionsHref.emailInboxes;
|
||||
|
||||
protected readonly title = 'Email inboxes';
|
||||
|
||||
get inputName(): Locator {
|
||||
return this.page.locator('input[name="name"]');
|
||||
}
|
||||
|
||||
get inputEmail(): Locator {
|
||||
return this.page.locator('input[name="email"]');
|
||||
}
|
||||
|
||||
// SMTP
|
||||
get inputSmtpServer(): Locator {
|
||||
return this.page.locator('input[name="smtpServer"]');
|
||||
}
|
||||
|
||||
get inputSmtpUsername(): Locator {
|
||||
return this.page.locator('input[name="smtpUsername"]');
|
||||
}
|
||||
|
||||
get inputSmtpPassword(): Locator {
|
||||
return this.page.locator('input[name="smtpPassword"]');
|
||||
}
|
||||
|
||||
get inputSmtpSecure(): Locator {
|
||||
return this.page.locator('label >> text="Connect with SSL/TLS"').first();
|
||||
}
|
||||
|
||||
// IMAP
|
||||
get inputImapServer(): Locator {
|
||||
return this.page.locator('input[name="imapServer"]');
|
||||
}
|
||||
|
||||
get inputImapUsername(): Locator {
|
||||
return this.page.locator('input[name="imapUsername"]');
|
||||
}
|
||||
|
||||
get inputImapPassword(): Locator {
|
||||
return this.page.locator('input[name="imapPassword"]');
|
||||
}
|
||||
|
||||
get inputImapSecure(): Locator {
|
||||
return this.page.locator('label >> text="Connect with SSL/TLS"').last();
|
||||
}
|
||||
|
||||
itemRow(name: string): Locator {
|
||||
return this.page.locator(`td >> text="${name}"`);
|
||||
}
|
||||
|
||||
async deleteEmailInboxByName(name: string): Promise<void> {
|
||||
await this.itemRow(name).click();
|
||||
await this.btnDelete.click();
|
||||
await this.deleteModal.confirmDelete();
|
||||
}
|
||||
}
|
||||
39
apps/meteor/tests/e2e/page-objects/admin-emojis.ts
Normal file
39
apps/meteor/tests/e2e/page-objects/admin-emojis.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
import { AddEmojiFlexTab, EditEmojiFlexTab } from './fragments/flextabs';
|
||||
|
||||
export class AdminEmoji extends Admin {
|
||||
readonly addEmojiFlexTab: AddEmojiFlexTab;
|
||||
|
||||
readonly editEmojiFlexTab: EditEmojiFlexTab;
|
||||
|
||||
protected readonly route = AdminSectionsHref.emoji;
|
||||
|
||||
protected readonly title = 'Emoji';
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.addEmojiFlexTab = new AddEmojiFlexTab(page);
|
||||
this.editEmojiFlexTab = new EditEmojiFlexTab(page);
|
||||
}
|
||||
|
||||
private get inputSearch(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Search' });
|
||||
}
|
||||
|
||||
private get emojiTable() {
|
||||
return this.page.getByRole('table', { name: 'Emoji' });
|
||||
}
|
||||
|
||||
async findEmojiByName(emojiName: string) {
|
||||
await this.inputSearch.fill(emojiName);
|
||||
return this.emojiTable.getByRole('link', { name: emojiName });
|
||||
}
|
||||
|
||||
async deleteEmoji(emojiName: string) {
|
||||
await (await this.findEmojiByName(emojiName)).click();
|
||||
await this.editEmojiFlexTab.delete();
|
||||
await this.deleteModal.confirmDelete();
|
||||
}
|
||||
}
|
||||
13
apps/meteor/tests/e2e/page-objects/admin-engagement.ts
Normal file
13
apps/meteor/tests/e2e/page-objects/admin-engagement.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
export class AdminEngagement extends Admin {
|
||||
protected readonly route = AdminSectionsHref.engagement;
|
||||
|
||||
protected readonly title = 'Engagement';
|
||||
|
||||
get upsellModal(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Engagement dashboard' });
|
||||
}
|
||||
}
|
||||
38
apps/meteor/tests/e2e/page-objects/admin-imports.ts
Normal file
38
apps/meteor/tests/e2e/page-objects/admin-imports.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
export class AdminImports extends Admin {
|
||||
protected readonly route = AdminSectionsHref.import;
|
||||
|
||||
protected readonly title = 'Import';
|
||||
|
||||
override async waitForReady(): Promise<void> {
|
||||
await this.btnImportNewFile.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
get btnImportNewFile(): Locator {
|
||||
return this.page.locator('.rcx-button--primary.rcx-button >> text="Import New File"');
|
||||
}
|
||||
|
||||
async getOptionFileType(option: string): Promise<Locator> {
|
||||
await this.page.locator('.rcx-select').click();
|
||||
return this.page.locator(`.rcx-option__content >> text="${option}"`);
|
||||
}
|
||||
|
||||
get inputFile(): Locator {
|
||||
return this.page.locator('input[type=file]');
|
||||
}
|
||||
|
||||
get btnImport(): Locator {
|
||||
return this.page.locator('.rcx-button--primary.rcx-button >> text="Import"');
|
||||
}
|
||||
|
||||
get btnStartImport(): Locator {
|
||||
return this.page.locator('.rcx-button--primary.rcx-button >> text="Start Importing"');
|
||||
}
|
||||
|
||||
get importStatusTableFirstRowCell(): Locator {
|
||||
return this.page.locator('[data-qa-id="ImportTable"] tbody tr:first-child td >> text="Completed successfully"');
|
||||
}
|
||||
}
|
||||
18
apps/meteor/tests/e2e/page-objects/admin-info.ts
Normal file
18
apps/meteor/tests/e2e/page-objects/admin-info.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
export class AdminInfo extends Admin {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
protected readonly route = AdminSectionsHref.workspace;
|
||||
|
||||
protected readonly title = 'Workspace';
|
||||
|
||||
/** Opens the workspace info page in the embedded layout. */
|
||||
async gotoEmbedded(): Promise<void> {
|
||||
await this.navigateTo(`${AdminSectionsHref.workspace}?layout=embedded`);
|
||||
}
|
||||
}
|
||||
47
apps/meteor/tests/e2e/page-objects/admin-integrations.ts
Normal file
47
apps/meteor/tests/e2e/page-objects/admin-integrations.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
export class AdminIntegrations extends Admin {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
get btnInstructions(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Instructions', exact: true });
|
||||
}
|
||||
|
||||
protected readonly route = AdminSectionsHref.integrations;
|
||||
|
||||
protected readonly title = 'Integrations';
|
||||
|
||||
codeExamplePayload(text: string): Locator {
|
||||
return this.page.locator('code', { hasText: text });
|
||||
}
|
||||
|
||||
get inputName(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Name' });
|
||||
}
|
||||
|
||||
get inputPostToChannel(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Post to Channel' });
|
||||
}
|
||||
|
||||
get inputPostAs(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Post as' });
|
||||
}
|
||||
|
||||
getIntegrationByName(name: string): Locator {
|
||||
return this.page.getByRole('table', { name: 'Integrations table' }).locator('tr', { hasText: name });
|
||||
}
|
||||
|
||||
get inputWebhookUrl(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Webhook URL' });
|
||||
}
|
||||
|
||||
async deleteIntegrationByName(name: string) {
|
||||
await this.getIntegrationByName(name).click();
|
||||
await this.btnDelete.click();
|
||||
await this.deleteModal.confirmDelete();
|
||||
}
|
||||
}
|
||||
7
apps/meteor/tests/e2e/page-objects/admin-mailer.ts
Normal file
7
apps/meteor/tests/e2e/page-objects/admin-mailer.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
export class AdminMailer extends Admin {
|
||||
protected readonly route = AdminSectionsHref.mailer;
|
||||
|
||||
protected readonly title = 'Mailer';
|
||||
}
|
||||
25
apps/meteor/tests/e2e/page-objects/admin-moderation.ts
Normal file
25
apps/meteor/tests/e2e/page-objects/admin-moderation.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
export class AdminModeration extends Admin {
|
||||
protected readonly route = AdminSectionsHref.moderation;
|
||||
|
||||
protected readonly title = 'Moderation';
|
||||
|
||||
get tabReportedMessages(): Locator {
|
||||
return this.page.getByRole('tab', { name: 'Reported messages' });
|
||||
}
|
||||
|
||||
get tabReportedUsers(): Locator {
|
||||
return this.page.getByRole('tab', { name: 'Reported users' });
|
||||
}
|
||||
|
||||
async gotoMessages(): Promise<void> {
|
||||
await this.navigateTo(`${AdminSectionsHref.moderation}/messages`, this.tabReportedMessages);
|
||||
}
|
||||
|
||||
async gotoUsers(): Promise<void> {
|
||||
await this.navigateTo(`${AdminSectionsHref.moderation}/users`, this.tabReportedUsers);
|
||||
}
|
||||
}
|
||||
42
apps/meteor/tests/e2e/page-objects/admin-permissions.ts
Normal file
42
apps/meteor/tests/e2e/page-objects/admin-permissions.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
export class AdminPermissions extends Admin {
|
||||
protected readonly route = AdminSectionsHref.permissions;
|
||||
|
||||
protected readonly title = 'Permissions';
|
||||
|
||||
get btnCreateRole(): Locator {
|
||||
return this.page.locator('button[name="New role"]');
|
||||
}
|
||||
|
||||
openRoleByName(name: string): Locator {
|
||||
return this.page.getByRole('table').getByRole('button', { name });
|
||||
}
|
||||
|
||||
get btnUsersInRole(): Locator {
|
||||
return this.page.getByRole('dialog').getByRole('button', { name: 'Users in role', exact: true });
|
||||
}
|
||||
|
||||
get tableUsersInRole(): Locator {
|
||||
return this.page.getByRole('table');
|
||||
}
|
||||
|
||||
get inputRoom(): Locator {
|
||||
return this.page.locator('input[placeholder="Room"]');
|
||||
}
|
||||
|
||||
get inputUsers(): Locator {
|
||||
return this.page.locator('input[placeholder="Users"]');
|
||||
}
|
||||
|
||||
getUserInRoleRowByUsername(username: string): Locator {
|
||||
return this.tableUsersInRole.locator('tr', { hasText: username });
|
||||
}
|
||||
|
||||
removeUserFromRoleByUsername = async (username: string): Promise<void> => {
|
||||
await this.getUserInRoleRowByUsername(username).getByRole('button', { name: 'Remove' }).click();
|
||||
await this.deleteModal.confirmDelete();
|
||||
};
|
||||
}
|
||||
33
apps/meteor/tests/e2e/page-objects/admin-rooms.ts
Normal file
33
apps/meteor/tests/e2e/page-objects/admin-rooms.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
import { EditAdminRoomFlexTab } from './fragments/flextabs';
|
||||
|
||||
export class AdminRooms extends Admin {
|
||||
readonly editRoom: EditAdminRoomFlexTab;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.editRoom = new EditAdminRoomFlexTab(page.getByRole('dialog', { name: 'Room Information' }));
|
||||
}
|
||||
|
||||
protected readonly route = AdminSectionsHref.rooms;
|
||||
|
||||
protected readonly title = 'Rooms';
|
||||
|
||||
get inputSearchRooms(): Locator {
|
||||
return this.pageContent.getByPlaceholder('Search rooms');
|
||||
}
|
||||
|
||||
getRoomRow(name?: string): Locator {
|
||||
return this.pageContent.getByRole('link', { name });
|
||||
}
|
||||
|
||||
get btnEdit(): Locator {
|
||||
return this.pageContent.getByRole('button', { name: 'Edit' });
|
||||
}
|
||||
|
||||
dropdownFilterRoomType(text = 'All rooms'): Locator {
|
||||
return this.pageContent.getByRole('button', { name: text });
|
||||
}
|
||||
}
|
||||
79
apps/meteor/tests/e2e/page-objects/admin-settings.ts
Normal file
79
apps/meteor/tests/e2e/page-objects/admin-settings.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
/**
|
||||
* A single settings section, e.g. `/admin/settings/Omnichannel`. Its `title` is the section heading, so it gets its ready state from `Admin` like any other page.
|
||||
*/
|
||||
export abstract class AdminSettingsSection extends Admin {
|
||||
protected abstract override readonly route: `${AdminSectionsHref.settings}/${string}`;
|
||||
}
|
||||
|
||||
/** The settings index at `/admin/settings`. Individual sections are `AdminSettingsSection`s. */
|
||||
export class AdminSettings extends Admin {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
}
|
||||
|
||||
get inputSearchSettings(): Locator {
|
||||
return this.page.locator('input[type=search]');
|
||||
}
|
||||
|
||||
protected readonly route = AdminSectionsHref.settings;
|
||||
|
||||
protected readonly title = 'Settings';
|
||||
|
||||
override async waitForReady(): Promise<void> {
|
||||
await this.inputSearchSettings.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
get inputSiteURL(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Site URL' });
|
||||
}
|
||||
|
||||
get btnResetSiteURL(): Locator {
|
||||
return this.page.locator('//label[@title="Site_Url"]//following-sibling::button');
|
||||
}
|
||||
|
||||
get btnAssetsSettings(): Locator {
|
||||
return this.page.locator('[data-qa-id="Assets"] >> role=link[name="Open"]');
|
||||
}
|
||||
|
||||
get btnDeleteAssetsLogo(): Locator {
|
||||
return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> role=button[name="Delete"]');
|
||||
}
|
||||
|
||||
get inputAssetsLogo(): Locator {
|
||||
return this.page.locator('//label[@title="Assets_logo"]/following-sibling::span >> input[type="file"]');
|
||||
}
|
||||
|
||||
get btnFullScreen(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Full Screen', exact: true });
|
||||
}
|
||||
|
||||
get btnExitFullScreen(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Exit Full Screen', exact: true });
|
||||
}
|
||||
|
||||
getSectionHeading(section: string): Locator {
|
||||
return this.page.getByRole('main').getByRole('heading', { level: 1, name: section, exact: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a settings section, e.g. `Message` for `/admin/settings/Message`.
|
||||
*
|
||||
* @param section id of the section, which doubles as its heading
|
||||
* @param until element to wait for, when the section heading is not a strong enough ready signal
|
||||
*/
|
||||
async gotoSection(section: string, until: Locator = this.getSectionHeading(section)): Promise<void> {
|
||||
await this.navigateTo(`${AdminSectionsHref.settings}/${section}`, until);
|
||||
}
|
||||
|
||||
async gotoGeneral(): Promise<void> {
|
||||
await this.gotoSection('General', this.inputSiteURL);
|
||||
}
|
||||
|
||||
async gotoLayout(): Promise<void> {
|
||||
await this.gotoSection('Layout', this.getAccordionBtnByName('Custom CSS'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
|
||||
export class AdminThirdPartyLogin extends Admin {
|
||||
protected readonly route = AdminSectionsHref.thirdPartyLogin;
|
||||
|
||||
protected readonly title = 'Third-party login';
|
||||
|
||||
get btnNewApplication(): Locator {
|
||||
return this.page.getByRole('button', { name: 'New Application', exact: true });
|
||||
}
|
||||
|
||||
get inputRedirectURI(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Redirect URI' });
|
||||
}
|
||||
|
||||
get inputApplicationName(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Application Name' });
|
||||
}
|
||||
|
||||
get inputClientId(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Client ID' });
|
||||
}
|
||||
|
||||
get inputClientSecret(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Client Secret' });
|
||||
}
|
||||
|
||||
get inputAuthUrl(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Authorization URL' });
|
||||
}
|
||||
|
||||
get inputTokenUrl(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Access Token URL' });
|
||||
}
|
||||
|
||||
getThirdPartyAppByName(name: string): Locator {
|
||||
return this.page.getByRole('table', { name: 'Third-party applications table' }).locator('tr', { hasText: name });
|
||||
}
|
||||
|
||||
async deleteThirdPartyAppByName(name: string) {
|
||||
await this.getThirdPartyAppByName(name).click();
|
||||
await this.btnDelete.click();
|
||||
await this.deleteModal.confirmDelete();
|
||||
}
|
||||
}
|
||||
79
apps/meteor/tests/e2e/page-objects/admin-users.ts
Normal file
79
apps/meteor/tests/e2e/page-objects/admin-users.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Admin, AdminSectionsHref } from './admin';
|
||||
import { MenuMoreActions, UserInfoFlexTab, EditUserFlexTab } from './fragments';
|
||||
import { expect } from '../utils/test';
|
||||
|
||||
type UserActions = 'Make Admin' | 'Remove Admin' | 'Activate' | 'Deactivate';
|
||||
|
||||
export class AdminUsers extends Admin {
|
||||
readonly editUser: EditUserFlexTab;
|
||||
|
||||
readonly userInfo: UserInfoFlexTab;
|
||||
|
||||
readonly userRowMenu: MenuMoreActions;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.editUser = new EditUserFlexTab(page);
|
||||
this.userInfo = new UserInfoFlexTab(page);
|
||||
this.userRowMenu = new MenuMoreActions(page);
|
||||
}
|
||||
|
||||
protected readonly route = AdminSectionsHref.users;
|
||||
|
||||
protected readonly title = 'Users';
|
||||
|
||||
override async waitForReady(): Promise<void> {
|
||||
await this.inputSearchUsers.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
get btnNewUser(): Locator {
|
||||
return this.page.getByRole('button', { name: 'New user', exact: true });
|
||||
}
|
||||
|
||||
get btnInvite(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Invite', exact: true });
|
||||
}
|
||||
|
||||
private get inputSearchUsers(): Locator {
|
||||
return this.page.getByRole('textbox', { name: 'Search Users' });
|
||||
}
|
||||
|
||||
get btnMoreActionsMenu(): Locator {
|
||||
return this.page.getByRole('button', { name: 'More actions' });
|
||||
}
|
||||
|
||||
getUserRowByUsername(username: string): Locator {
|
||||
return this.page.locator('tr', { hasText: username }).first();
|
||||
}
|
||||
|
||||
getTabByName(name: 'All' | 'Pending' | 'Active' | 'Deactivated' = 'All'): Locator {
|
||||
return this.page.getByRole('tab', { name });
|
||||
}
|
||||
|
||||
private async openUserActionMenu(username: string): Promise<void> {
|
||||
await this.getUserRowByUsername(username).getByRole('button', { name: 'More actions' }).click();
|
||||
}
|
||||
|
||||
async dispatchUserAction(username: string, action: UserActions) {
|
||||
await this.openUserActionMenu(username);
|
||||
await this.userRowMenu.root.getByRole('menuitem', { name: action }).click();
|
||||
}
|
||||
|
||||
async activatePendingUser(username: string): Promise<void> {
|
||||
await this.getUserRowByUsername(username).getByRole('button', { name: 'Activate', exact: true }).click();
|
||||
}
|
||||
|
||||
async deleteUser(username: string): Promise<void> {
|
||||
await this.searchUser(username);
|
||||
await this.getUserRowByUsername(username).click();
|
||||
await this.userInfo.btnMoreActions.click();
|
||||
await this.userInfo.menuItemDeleteUser.click();
|
||||
}
|
||||
|
||||
async searchUser(username: string): Promise<void> {
|
||||
await this.inputSearchUsers.fill(username);
|
||||
await expect(this.getUserRowByUsername(username)).toHaveCount(1);
|
||||
}
|
||||
}
|
||||
101
apps/meteor/tests/e2e/page-objects/admin.ts
Normal file
101
apps/meteor/tests/e2e/page-objects/admin.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { AdminSidebar, ToastMessages } from './fragments';
|
||||
import { ConfirmDeleteModal } from './fragments/modals';
|
||||
import { RoutedPage } from './routed-page';
|
||||
|
||||
export enum AdminSectionsHref {
|
||||
workspace = '/admin/info',
|
||||
subscription = '/admin/subscription',
|
||||
engagement = '/admin/engagement/users',
|
||||
moderation = '/admin/moderation',
|
||||
rooms = '/admin/rooms',
|
||||
users = '/admin/users',
|
||||
invites = '/admin/invites',
|
||||
userStatus = '/admin/user-status',
|
||||
permissions = '/admin/permissions',
|
||||
deviceManagement = '/admin/device-management',
|
||||
emailInboxes = '/admin/email-inboxes',
|
||||
mailer = '/admin/mailer',
|
||||
thirdPartyLogin = '/admin/third-party-login',
|
||||
integrations = '/admin/integrations',
|
||||
import = '/admin/import',
|
||||
reports = '/admin/analytic-reports',
|
||||
sounds = '/admin/sounds',
|
||||
emoji = '/admin/emoji',
|
||||
settings = '/admin/settings',
|
||||
}
|
||||
|
||||
/**
|
||||
* A page under `/admin/*`. Subclasses declare `route` and `title` and get `goto()`, `pageContent`
|
||||
* and their ready state for free; override `waitForReady()` when the heading is not a strong enough
|
||||
* signal that the page is usable.
|
||||
*/
|
||||
export abstract class Admin extends RoutedPage {
|
||||
protected abstract override readonly route: AdminSectionsHref | `${AdminSectionsHref}/${string}`;
|
||||
|
||||
/** Text of the page's main heading. Identifies the page and scopes `pageContent`. */
|
||||
protected abstract readonly title: string;
|
||||
|
||||
readonly sidebar: AdminSidebar;
|
||||
|
||||
readonly deleteModal: ConfirmDeleteModal;
|
||||
|
||||
readonly toastMessage: ToastMessages;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.sidebar = new AdminSidebar(page);
|
||||
this.deleteModal = new ConfirmDeleteModal(page.getByRole('dialog', { name: 'Are you sure?' }));
|
||||
this.toastMessage = new ToastMessages(page);
|
||||
}
|
||||
|
||||
/** The `main` region of this page, scoped by its heading so it cannot match another route. */
|
||||
get pageContent(): Locator {
|
||||
return this.page.getByRole('main').filter({ has: this.page.getByRole('heading', { name: this.title, exact: true }) });
|
||||
}
|
||||
|
||||
async waitForReady(): Promise<void> {
|
||||
await this.pageContent.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
get btnAdd(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Add', exact: true });
|
||||
}
|
||||
|
||||
get btnBack(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Back', exact: true });
|
||||
}
|
||||
|
||||
get btnSave(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save', exact: true });
|
||||
}
|
||||
|
||||
get btnNew(): Locator {
|
||||
return this.page.getByRole('button', { name: 'New', exact: true });
|
||||
}
|
||||
|
||||
get btnSaveChanges(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save changes', exact: true });
|
||||
}
|
||||
|
||||
get btnDelete(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Delete', exact: true });
|
||||
}
|
||||
|
||||
getAccordionBtnByName(name: string): Locator {
|
||||
return this.page.getByRole('button', { name, exact: true });
|
||||
}
|
||||
|
||||
async adminSectionButton(href: AdminSectionsHref): Promise<Locator> {
|
||||
return this.page.locator(`a[href="${href}"]`);
|
||||
}
|
||||
|
||||
findFileRowByUsername(username: string) {
|
||||
return this.page.locator('tr', { has: this.page.getByRole('cell', { name: username }) });
|
||||
}
|
||||
|
||||
findFileCheckboxByUsername(username: string) {
|
||||
return this.findFileRowByUsername(username).locator('label', { has: this.page.getByRole('checkbox') });
|
||||
}
|
||||
}
|
||||
194
apps/meteor/tests/e2e/page-objects/auth.ts
Normal file
194
apps/meteor/tests/e2e/page-objects/auth.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
import type { FrameLocator, Locator, Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '../utils/test';
|
||||
|
||||
abstract class Main {
|
||||
protected abstract readonly page: Page;
|
||||
|
||||
constructor(protected root: Locator) {}
|
||||
|
||||
/**
|
||||
* Navigates to `url` and waits for this screen to render.
|
||||
*
|
||||
* Auth screens are what an unauthenticated visitor is bounced to, so the url a test navigates to
|
||||
* is usually not the one it lands on — hence the parameter. Pass `until` when the route settles
|
||||
* on something else, e.g. a login iframe replacing the login form.
|
||||
*/
|
||||
async goto(url = '/home', until?: Locator): Promise<void> {
|
||||
await this.page.goto(url);
|
||||
|
||||
if (until) {
|
||||
await expect(until).toBeVisible();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.waitForDisplay();
|
||||
}
|
||||
|
||||
waitForDisplay() {
|
||||
return expect(this.root).toBeVisible();
|
||||
}
|
||||
|
||||
waitForDismissal() {
|
||||
return expect(this.root).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export class Authenticated extends Main {
|
||||
constructor(protected page: Page) {
|
||||
super(page.locator('#main-content'));
|
||||
}
|
||||
}
|
||||
|
||||
class LoginForm {
|
||||
readonly root: Locator;
|
||||
|
||||
constructor(protected page: Page) {
|
||||
this.root = page?.getByRole('form', { name: 'Login' });
|
||||
}
|
||||
|
||||
get username(): Locator {
|
||||
return this.root.locator('role=textbox[name=/username/i]');
|
||||
}
|
||||
|
||||
get inputPassword(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Password', exact: true });
|
||||
}
|
||||
|
||||
get btnLogin(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Login', exact: true });
|
||||
}
|
||||
|
||||
async fillForm(username: string, password: string) {
|
||||
await this.username.fill(username);
|
||||
await this.inputPassword.fill(password);
|
||||
}
|
||||
}
|
||||
|
||||
export class Login extends Main {
|
||||
readonly form: LoginForm;
|
||||
|
||||
constructor(protected page: Page) {
|
||||
super(page.getByRole('main', { name: 'Login', exact: true }));
|
||||
this.form = new LoginForm(page);
|
||||
}
|
||||
|
||||
async login(username: string, password: string) {
|
||||
await this.form.fillForm(username, password);
|
||||
await this.form.btnLogin.click();
|
||||
}
|
||||
|
||||
get loginIframe(): FrameLocator {
|
||||
return this.page.frameLocator('iframe[title="Login"]');
|
||||
}
|
||||
|
||||
get loginIframeForm(): Locator {
|
||||
return this.loginIframe.locator('#login-form');
|
||||
}
|
||||
|
||||
get loginIframeSubmitButton(): Locator {
|
||||
return this.loginIframe.locator('#submit');
|
||||
}
|
||||
|
||||
get loginIframeError(): Locator {
|
||||
return this.loginIframe.locator('#login-error', { hasText: 'Login failed' });
|
||||
}
|
||||
}
|
||||
|
||||
export class Registration extends Main {
|
||||
constructor(protected page: Page) {
|
||||
super(page.getByRole('main'));
|
||||
}
|
||||
|
||||
get btnSendInstructions(): Locator {
|
||||
return this.page.locator('role=button[name="Send instructions"]');
|
||||
}
|
||||
|
||||
get btnReset(): Locator {
|
||||
return this.page.locator('role=button[name="Reset"]');
|
||||
}
|
||||
|
||||
get btnLoginWithSaml(): Locator {
|
||||
return this.page.locator('role=button[name="SAML test login button"]');
|
||||
}
|
||||
|
||||
get btnLoginWithGoogle(): Locator {
|
||||
return this.page.locator('role=button[name="Sign in with Google"]');
|
||||
}
|
||||
|
||||
get btnLoginWithCustomOAuth(): Locator {
|
||||
return this.page.locator('role=button[name="Sign in with Test"]');
|
||||
}
|
||||
|
||||
get goToRegister(): Locator {
|
||||
return this.page.locator('role=link[name="Create an account"]');
|
||||
}
|
||||
|
||||
get btnRegister(): Locator {
|
||||
return this.page.locator('role=button[name="Join your team"]');
|
||||
}
|
||||
|
||||
get btnRegisterConfirmUsername(): Locator {
|
||||
return this.page.locator('role=button[name="Use this username"]');
|
||||
}
|
||||
|
||||
get btnForgotPassword(): Locator {
|
||||
return this.page.locator('role=link[name="Forgot your password?"]');
|
||||
}
|
||||
|
||||
get username(): Locator {
|
||||
return this.page.locator('role=textbox[name=/username/i]');
|
||||
}
|
||||
|
||||
get inputName(): Locator {
|
||||
return this.page.locator('[name=name]');
|
||||
}
|
||||
|
||||
get inputEmail(): Locator {
|
||||
return this.page.locator('role=textbox[name=/Email/]');
|
||||
}
|
||||
|
||||
get inputPassword(): Locator {
|
||||
return this.page.locator('[name=password]');
|
||||
}
|
||||
|
||||
get inputReason(): Locator {
|
||||
return this.page.locator('role=textbox[name=/Reason/]');
|
||||
}
|
||||
|
||||
get inputPasswordConfirm(): Locator {
|
||||
return this.page.locator('[name=passwordConfirmation]');
|
||||
}
|
||||
|
||||
get forgotPasswordEmailCallout(): Locator {
|
||||
return this.page.locator('role=status');
|
||||
}
|
||||
|
||||
get registrationDisabledCallout(): Locator {
|
||||
return this.page.getByRole('status').filter({ hasText: 'New user registration is currently disabled' });
|
||||
}
|
||||
|
||||
/** Opens an invite url, e.g. `/invite/<id>`. */
|
||||
async gotoInvite(inviteId: string): Promise<void> {
|
||||
await this.goto(`/invite/${inviteId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the secret registration url, e.g. `/register/<secret>`.
|
||||
*
|
||||
* Depending on `Accounts_RegistrationForm` the page settles on the register form or on an
|
||||
* invalid-secret callout, and neither renders a `main` landmark - so the caller says which it expects.
|
||||
*/
|
||||
async gotoWithSecret(secret: string, until: Locator): Promise<void> {
|
||||
await this.goto(`/register/${secret}`, until);
|
||||
}
|
||||
|
||||
/** Opens the password reset url for a token. */
|
||||
async gotoResetPassword(token: string): Promise<void> {
|
||||
await this.goto(`/reset-password/${token}`, this.inputPassword);
|
||||
}
|
||||
|
||||
get registrationInvalidUrlCallout(): Locator {
|
||||
return this.page.getByRole('status').filter({ hasText: 'The URL provided is invalid' });
|
||||
}
|
||||
}
|
||||
28
apps/meteor/tests/e2e/page-objects/directory.ts
Normal file
28
apps/meteor/tests/e2e/page-objects/directory.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { RoutedPage } from './routed-page';
|
||||
|
||||
export class Directory extends RoutedPage {
|
||||
protected readonly route = '/directory';
|
||||
|
||||
async waitForReady(): Promise<void> {
|
||||
await this.directoryHeader.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async searchChannel(name: string) {
|
||||
await this.page.getByRole('textbox', { name: 'Search' }).fill(name);
|
||||
}
|
||||
|
||||
getSearchByChannelName(name: string) {
|
||||
return this.page.locator(`role=table >> role=link >> text="${name}"`);
|
||||
}
|
||||
|
||||
async openChannel(name: string) {
|
||||
await this.searchChannel(name);
|
||||
await this.getSearchByChannelName(name).click();
|
||||
}
|
||||
|
||||
private get directoryHeader(): Locator {
|
||||
return this.page.locator('main').getByRole('heading', { name: 'Directory', exact: true });
|
||||
}
|
||||
}
|
||||
51
apps/meteor/tests/e2e/page-objects/encrypted-room.ts
Normal file
51
apps/meteor/tests/e2e/page-objects/encrypted-room.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { EncryptedRoomToolbar, HomeContent } from './fragments';
|
||||
import { Message } from './fragments/message';
|
||||
import { DisableRoomEncryptionModal, EnableRoomEncryptionModal } from './fragments/modals';
|
||||
|
||||
export class EncryptedRoomPage extends HomeContent {
|
||||
readonly toolbar: EncryptedRoomToolbar;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.toolbar = new EncryptedRoomToolbar(page);
|
||||
}
|
||||
|
||||
get encryptedTitle() {
|
||||
return this.page.getByRole('button', { name: '- encrypted' });
|
||||
}
|
||||
|
||||
get encryptionNotReadyIndicator() {
|
||||
return this.page.getByText("You're sending an unencrypted message");
|
||||
}
|
||||
|
||||
get lastMessage() {
|
||||
return new Message(this.lastUserMessage);
|
||||
}
|
||||
|
||||
lastNthMessage(index: number) {
|
||||
return new Message(this.nthMessage(-index - 1));
|
||||
}
|
||||
|
||||
async enableEncryption() {
|
||||
const enableRoomEncryptionModal = new EnableRoomEncryptionModal(this.page);
|
||||
|
||||
await this.toolbar.openMoreOptions();
|
||||
await this.toolbar.menuItemEnableE2EEncryption.click();
|
||||
await enableRoomEncryptionModal.enable();
|
||||
}
|
||||
|
||||
async disableEncryption() {
|
||||
const disableRoomEncryptionModal = new DisableRoomEncryptionModal(this.page);
|
||||
|
||||
await this.toolbar.openMoreOptions();
|
||||
await this.toolbar.menuItemDisableE2EEncryption.click();
|
||||
await disableRoomEncryptionModal.disable();
|
||||
}
|
||||
|
||||
async showExportMessagesTab() {
|
||||
await this.toolbar.openMoreOptions();
|
||||
await this.toolbar.menuItemExportMessages.click();
|
||||
}
|
||||
}
|
||||
137
apps/meteor/tests/e2e/page-objects/fragments/composer.ts
Normal file
137
apps/meteor/tests/e2e/page-objects/fragments/composer.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import type { Locator, Page } from 'playwright-core';
|
||||
|
||||
import { Popup } from './popup';
|
||||
|
||||
class VideoRecorderPopup extends Popup {
|
||||
constructor(root: Locator) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
get btnRecord(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Record' });
|
||||
}
|
||||
|
||||
get btnStopRecording(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Stop Recording' });
|
||||
}
|
||||
|
||||
get btnSend(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Send' });
|
||||
}
|
||||
|
||||
async record() {
|
||||
await this.btnRecord.click();
|
||||
await this.btnStopRecording.waitFor({ state: 'visible' }).then(() => this.btnStopRecording.click());
|
||||
await this.btnSend.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class Composer {
|
||||
readonly videoRecorderPopup: VideoRecorderPopup;
|
||||
|
||||
constructor(protected root: Locator) {
|
||||
this.root = root;
|
||||
this.videoRecorderPopup = new VideoRecorderPopup(this.root.page().getByRole('dialog', { name: 'Video record' }));
|
||||
}
|
||||
|
||||
get inputMessage(): Locator {
|
||||
return this.root.locator('[name="msg"]');
|
||||
}
|
||||
|
||||
get btnJoinRoom(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Join' });
|
||||
}
|
||||
|
||||
get toolbarPrimaryActions(): Locator {
|
||||
return this.root.getByRole('toolbar', { name: 'Composer Primary Actions' });
|
||||
}
|
||||
|
||||
get allPrimaryActions(): Locator {
|
||||
return this.toolbarPrimaryActions.getByRole('button');
|
||||
}
|
||||
|
||||
get btnEmoji(): Locator {
|
||||
return this.toolbarPrimaryActions.getByRole('button', { name: 'Emoji', exact: true });
|
||||
}
|
||||
|
||||
get btnAudioMessage(): Locator {
|
||||
return this.toolbarPrimaryActions.getByRole('button', { name: 'Audio message' });
|
||||
}
|
||||
|
||||
getFileByName(fileName: string): Locator {
|
||||
return this.root.getByRole('group', { name: fileName, exact: true });
|
||||
}
|
||||
|
||||
get groupUploads() {
|
||||
return this.root.getByRole('group', { name: 'Uploads', exact: true });
|
||||
}
|
||||
|
||||
getFilesInComposer(): Locator {
|
||||
return this.groupUploads.getByRole('group', { name: /^(?!Close$)/ });
|
||||
}
|
||||
|
||||
async removeFileByName(fileName: string): Promise<void> {
|
||||
return this.getFileByName(fileName).getByRole('button', { name: 'Remove', exact: true }).click();
|
||||
}
|
||||
|
||||
get btnSend(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Send' });
|
||||
}
|
||||
|
||||
get btnCancel(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Cancel', exact: true });
|
||||
}
|
||||
|
||||
get btnOptionFileUpload(): Locator {
|
||||
return this.toolbarPrimaryActions.getByRole('button', { name: 'Upload file' });
|
||||
}
|
||||
|
||||
get btnVideoMessage(): Locator {
|
||||
return this.toolbarPrimaryActions.getByRole('button', { name: 'Video message' });
|
||||
}
|
||||
|
||||
get btnMenuMoreActions() {
|
||||
return this.toolbarPrimaryActions.getByRole('button', { name: 'More actions' });
|
||||
}
|
||||
|
||||
get btnItalicFormatter(): Locator {
|
||||
return this.toolbarPrimaryActions.getByRole('button', { name: 'Italic', exact: true });
|
||||
}
|
||||
|
||||
get btnBoldFormatter(): Locator {
|
||||
return this.toolbarPrimaryActions.getByRole('button', { name: 'Bold', exact: true });
|
||||
}
|
||||
|
||||
get btnLinkFormatter(): Locator {
|
||||
return this.toolbarPrimaryActions.getByRole('button', { name: 'Link', exact: true });
|
||||
}
|
||||
|
||||
get boxPopup(): Locator {
|
||||
return this.root.locator('[role="menu"][name="ComposerBoxPopup"]');
|
||||
}
|
||||
|
||||
get readOnlyFooter(): Locator {
|
||||
return this.root.getByText('This room is read only');
|
||||
}
|
||||
|
||||
get typingIndicator(): Locator {
|
||||
return this.root.getByRole('status').getByText(/typing/i);
|
||||
}
|
||||
|
||||
get addLinkModal(): Locator {
|
||||
return this.root.page().getByRole('dialog', { name: 'Add link' });
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomComposer extends Composer {
|
||||
constructor(page: Page) {
|
||||
super(page.locator('footer[aria-label="Room composer"]'));
|
||||
}
|
||||
}
|
||||
|
||||
export class ThreadComposer extends Composer {
|
||||
constructor(page: Page) {
|
||||
super(page.locator('footer[aria-label="Thread composer"]'));
|
||||
}
|
||||
}
|
||||
60
apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts
Normal file
60
apps/meteor/tests/e2e/page-objects/fragments/e2ee.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Navbar } from './navbar';
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
abstract class E2EEBanner {
|
||||
constructor(protected root: Locator) {}
|
||||
|
||||
click() {
|
||||
return this.root.click();
|
||||
}
|
||||
|
||||
async waitForDisappearance() {
|
||||
await expect(this.root).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export class SaveE2EEPasswordBanner extends E2EEBanner {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('button', { name: 'Save your new E2EE password' }));
|
||||
}
|
||||
}
|
||||
|
||||
export class EnterE2EEPasswordBanner extends E2EEBanner {
|
||||
constructor(page: Page) {
|
||||
// TODO: there is a typo in the default translation
|
||||
super(page.getByRole('button', { name: 'Enter your E2E password' }));
|
||||
}
|
||||
}
|
||||
|
||||
export class E2EEKeyDecodeFailureBanner extends E2EEBanner {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('button', { name: "Wasn't possible to decode your encryption key to be imported." }));
|
||||
}
|
||||
|
||||
async expectToNotBeVisible() {
|
||||
await expect(this.root).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateE2EEChannel {
|
||||
private readonly navbar: Navbar;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.navbar = new Navbar(page);
|
||||
}
|
||||
|
||||
async create(name: string): Promise<void> {
|
||||
await this.navbar.createEncryptedChannel(name);
|
||||
}
|
||||
|
||||
async createAndStore(name: string, createdChannels: string[]): Promise<void> {
|
||||
await this.create(name);
|
||||
this.store(name, createdChannels);
|
||||
}
|
||||
|
||||
store(name: string, createdChannels: string[]): void {
|
||||
createdChannels.push(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
|
||||
export class EditAdminRoomFlexTab extends FlexTab {
|
||||
constructor(locator: Locator) {
|
||||
super(locator);
|
||||
}
|
||||
|
||||
get roomNameInput(): Locator {
|
||||
return this.root.locator('input[name="roomName"]');
|
||||
}
|
||||
|
||||
get privateLabel(): Locator {
|
||||
return this.root.locator(`label >> text=Private`);
|
||||
}
|
||||
|
||||
get privateInput(): Locator {
|
||||
return this.root.locator('input[name="roomType"]');
|
||||
}
|
||||
|
||||
get roomOwnerInput(): Locator {
|
||||
return this.root.locator('input[name="roomOwner"]');
|
||||
}
|
||||
|
||||
get archivedLabel(): Locator {
|
||||
return this.root.locator('label >> text=Archived');
|
||||
}
|
||||
|
||||
get archivedInput(): Locator {
|
||||
return this.root.locator('input[name="archived"]');
|
||||
}
|
||||
|
||||
get favoriteLabel(): Locator {
|
||||
return this.root.locator('label >> text=Favorite');
|
||||
}
|
||||
|
||||
get favoriteInput(): Locator {
|
||||
return this.root.locator('input[name="favorite"]');
|
||||
}
|
||||
|
||||
get defaultLabel(): Locator {
|
||||
return this.root.locator('label >> text=Default');
|
||||
}
|
||||
|
||||
get defaultInput(): Locator {
|
||||
return this.root.locator('input[name="isDefault"]');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
|
||||
export class AddEmojiFlexTab extends FlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Add New Emoji' }));
|
||||
}
|
||||
}
|
||||
|
||||
export class EditEmojiFlexTab extends FlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Custom Emoji Info' }));
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.btnDelete.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
|
||||
export class AutoTranslateFlexTab extends FlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Auto-Translate' }));
|
||||
}
|
||||
|
||||
get checkboxAutomaticTranslation(): Locator {
|
||||
return this.root.getByRole('checkbox', { name: 'Automatic Translation' });
|
||||
}
|
||||
|
||||
get textAutomaticTranslation(): Locator {
|
||||
return this.root.getByText('Automatic Translation', { exact: true });
|
||||
}
|
||||
|
||||
get textEncryptedRoomCallout(): Locator {
|
||||
return this.root.getByText('Automatic translation not available');
|
||||
}
|
||||
|
||||
async setAutomaticTranslation(enabled: boolean): Promise<void> {
|
||||
if ((await this.checkboxAutomaticTranslation.isChecked()) === enabled) {
|
||||
return;
|
||||
}
|
||||
await this.textAutomaticTranslation.click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
import { Listbox } from '../listbox';
|
||||
import { ConfirmDeleteRoomModal, ConfirmRemoveModal } from '../modals';
|
||||
import { Modal } from '../modals/modal';
|
||||
|
||||
class AddExistingChannelModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Add Existing Channels' }));
|
||||
}
|
||||
|
||||
get inputChannels(): Locator {
|
||||
return this.root.getByRole('textbox');
|
||||
}
|
||||
|
||||
get btnAdd(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Add', exact: true });
|
||||
}
|
||||
|
||||
async confirmAdd() {
|
||||
await this.btnAdd.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class ChannelsFlexTab extends FlexTab {
|
||||
readonly confirmRemoveModal: ConfirmRemoveModal;
|
||||
|
||||
readonly confirmDeleteModal: ConfirmDeleteRoomModal;
|
||||
|
||||
readonly addExistingChannelModal: AddExistingChannelModal;
|
||||
|
||||
readonly listbox: Listbox;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Team Channels' }));
|
||||
this.confirmRemoveModal = new ConfirmRemoveModal(page.getByRole('dialog', { name: 'Are you sure?' }));
|
||||
this.confirmDeleteModal = new ConfirmDeleteRoomModal(page);
|
||||
this.addExistingChannelModal = new AddExistingChannelModal(page);
|
||||
this.listbox = new Listbox(page);
|
||||
}
|
||||
|
||||
get btnAddExisting(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Add Existing', exact: true });
|
||||
}
|
||||
|
||||
get btnCreateNew(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Create new', exact: true });
|
||||
}
|
||||
|
||||
get channelsList(): Locator {
|
||||
return this.root.getByRole('list');
|
||||
}
|
||||
|
||||
getListboxOption(name: string): Locator {
|
||||
return this.listbox.getOption(name);
|
||||
}
|
||||
|
||||
channelOption(name: string) {
|
||||
return this.root.locator('li', { hasText: name });
|
||||
}
|
||||
|
||||
async openChannelOptionMoreActions(name: string) {
|
||||
await this.channelOption(name).hover();
|
||||
await this.channelOption(name).getByRole('button', { name: 'More' }).click();
|
||||
}
|
||||
|
||||
async confirmRemoveChannel() {
|
||||
return this.confirmRemoveModal.confirmRemove();
|
||||
}
|
||||
|
||||
async confirmDeleteRoom() {
|
||||
return this.confirmDeleteModal.confirmDelete();
|
||||
}
|
||||
|
||||
async addExistingChannel(name: string) {
|
||||
await this.btnAddExisting.click();
|
||||
await this.addExistingChannelModal.inputChannels.fill(name);
|
||||
await this.listbox.selectOption(name);
|
||||
await this.addExistingChannelModal.confirmAdd();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
|
||||
export class DeviceInfoFlexTab extends FlexTab {
|
||||
constructor(locator: Locator) {
|
||||
super(locator);
|
||||
}
|
||||
|
||||
get btnLogoutDevice(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Log out device' });
|
||||
}
|
||||
|
||||
getDeviceInfoId(deviceId: string): Locator {
|
||||
return this.root.getByText(deviceId, { exact: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
|
||||
export class OmnichannelEditContactFlexTab extends FlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'contact' }));
|
||||
}
|
||||
|
||||
get inputEmail(): Locator {
|
||||
return this.root.locator('input[name="emails.0.address"]');
|
||||
}
|
||||
|
||||
get inputPhone(): Locator {
|
||||
return this.root.locator('input[name="phones.0.phoneNumber"]');
|
||||
}
|
||||
|
||||
get inputContactManager(): Locator {
|
||||
return this.root.locator('input[name=contactManager]');
|
||||
}
|
||||
|
||||
get btnAddEmail(): Locator {
|
||||
return this.root.locator('role=button[name="Add email"]');
|
||||
}
|
||||
|
||||
get btnAddPhone(): Locator {
|
||||
return this.root.locator('role=button[name="Add phone"]');
|
||||
}
|
||||
|
||||
getErrorMessage(message: string): Locator {
|
||||
return this.root.locator(`role=alert >> text="${message}"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
import { Listbox } from '../listbox';
|
||||
|
||||
export class EditRoomFlexTab extends FlexTab {
|
||||
constructor(locator: Locator) {
|
||||
super(locator);
|
||||
}
|
||||
|
||||
get inputTopic(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Topic' });
|
||||
}
|
||||
|
||||
get inputAnnouncement(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Announcement' });
|
||||
}
|
||||
|
||||
get inputDescription(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Description' });
|
||||
}
|
||||
|
||||
get checkboxReadOnly(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Read-only' });
|
||||
}
|
||||
|
||||
get calloutRetentionPolicy(): Locator {
|
||||
return this.root.getByRole('alert', { name: 'Retention policy warning callout' });
|
||||
}
|
||||
|
||||
get advancedSettingsAccordion(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Advanced settings' });
|
||||
}
|
||||
|
||||
get pruneAccordion(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Prune', exact: true });
|
||||
}
|
||||
|
||||
getMaxAgeLabel(maxAge = '30') {
|
||||
return this.root.getByText(`Maximum message age in days (default: ${maxAge})`);
|
||||
}
|
||||
|
||||
get inputRetentionMaxAge(): Locator {
|
||||
return this.root.locator('input[name="retentionMaxAge"]');
|
||||
}
|
||||
|
||||
get checkboxPruneMessages(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Automatically prune old messages' });
|
||||
}
|
||||
|
||||
get checkboxOverrideGlobalRetention(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Override global retention policy' });
|
||||
}
|
||||
|
||||
get checkboxIgnoreThreads(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Do not prune Threads' });
|
||||
}
|
||||
|
||||
get checkboxChannels(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Channels' });
|
||||
}
|
||||
|
||||
get checkboxDiscussions(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Discussions' });
|
||||
}
|
||||
|
||||
async toggleSidepanelItems() {
|
||||
await this.checkboxChannels.click();
|
||||
await this.checkboxDiscussions.click();
|
||||
}
|
||||
}
|
||||
|
||||
export class EditTeamFlexTab extends EditRoomFlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Edit team' }));
|
||||
}
|
||||
}
|
||||
|
||||
export class OmnichannelEditRoomFlexTab extends EditRoomFlexTab {
|
||||
private readonly tagsListbox: Listbox;
|
||||
|
||||
private readonly slaListbox: Listbox;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Edit Room' }));
|
||||
this.tagsListbox = new Listbox(page);
|
||||
this.slaListbox = new Listbox(page, 'SLA Policy');
|
||||
}
|
||||
|
||||
get inputSLAPolicy(): Locator {
|
||||
return this.root.getByRole('button', { name: 'SLA Policy', exact: true });
|
||||
}
|
||||
|
||||
optionTag(name: string): Locator {
|
||||
return this.tagsListbox.getOption(name);
|
||||
}
|
||||
|
||||
async selectTag(name: string) {
|
||||
await this.tagsListbox.selectOption(name);
|
||||
}
|
||||
|
||||
async selectSLA(name: string) {
|
||||
await this.inputSLAPolicy.click();
|
||||
await this.slaListbox.selectOption(name, true);
|
||||
}
|
||||
|
||||
get inputTags(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Select an option' });
|
||||
}
|
||||
|
||||
getRoomCustomField(label: string): Locator {
|
||||
return this.root.getByRole('group', { name: 'Custom Fields' }).getByLabel(label);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
|
||||
export class EditUserFlexTab extends FlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog'));
|
||||
}
|
||||
|
||||
get btnAddUser(): Locator {
|
||||
return this.root.locator('role=button[name="Add user"]');
|
||||
}
|
||||
|
||||
get btnSaveUser(): Locator {
|
||||
return this.root.locator('role=button[name="Save user"]');
|
||||
}
|
||||
|
||||
get inputUserName(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Username', exact: true });
|
||||
}
|
||||
|
||||
get inputEmail(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Email', exact: true });
|
||||
}
|
||||
|
||||
get inputSetManually(): Locator {
|
||||
return this.root.locator('//label[text()="Set manually"]');
|
||||
}
|
||||
|
||||
get inputPassword(): Locator {
|
||||
return this.root.getByPlaceholder('Password', { exact: true });
|
||||
}
|
||||
|
||||
get inputConfirmPassword(): Locator {
|
||||
return this.root.getByPlaceholder('Confirm password', { exact: true });
|
||||
}
|
||||
|
||||
get joinDefaultChannels(): Locator {
|
||||
return this.root.locator('//label[text()="Join default channels"]');
|
||||
}
|
||||
|
||||
get chipUserRole(): Locator {
|
||||
return this.root.getByRole('button', { name: 'user', exact: true });
|
||||
}
|
||||
|
||||
get setupSmtpLink(): Locator {
|
||||
return this.root.getByRole('link', { name: 'Set up SMTP' });
|
||||
}
|
||||
|
||||
getCustomField(fieldName: string): Locator {
|
||||
return this.root.getByRole('textbox', { name: fieldName, exact: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
|
||||
export class ExportMessagesFlexTab extends FlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Export Messages' }));
|
||||
}
|
||||
|
||||
async exposeMethods() {
|
||||
await this.method.click();
|
||||
}
|
||||
|
||||
async setMethod(optionName: string) {
|
||||
await this.exposeMethods();
|
||||
await this.root.page().getByRole('option', { name: optionName }).click();
|
||||
}
|
||||
|
||||
async exposeOutputFormats() {
|
||||
await this.outputFormat.click();
|
||||
}
|
||||
|
||||
async setOutputFormat(optionName: string) {
|
||||
await this.exposeOutputFormats();
|
||||
await this.root.page().getByRole('option', { name: optionName }).click();
|
||||
}
|
||||
|
||||
getMethodOptionByName(name: string) {
|
||||
return this.root.page().getByRole('option', { name });
|
||||
}
|
||||
|
||||
getOutputFormatOptionByName(name: string) {
|
||||
return this.root.page().getByRole('option', { name });
|
||||
}
|
||||
|
||||
async selectAllMessages() {
|
||||
await this.root
|
||||
.page()
|
||||
.getByRole('button', { name: /Select \d+ messages/ })
|
||||
.click();
|
||||
}
|
||||
|
||||
async downloadMessages() {
|
||||
const [download] = await Promise.all([this.root.page().waitForEvent('download'), this.downloadButton.click()]);
|
||||
return download;
|
||||
}
|
||||
|
||||
async send() {
|
||||
await this.sendButton.click();
|
||||
}
|
||||
|
||||
async setAdditionalEmail(email: string) {
|
||||
await this.inputAdditionalEmails.fill(email);
|
||||
}
|
||||
|
||||
getMessageCheckbox(messageText: string): Locator {
|
||||
return this.root.page().getByRole('listitem').filter({ hasText: messageText }).getByRole('checkbox');
|
||||
}
|
||||
|
||||
get inputUsers() {
|
||||
return this.root.getByRole('combobox', { name: 'To users' });
|
||||
}
|
||||
|
||||
get inputAdditionalEmails() {
|
||||
return this.root.getByRole('textbox', { name: 'To additional emails' });
|
||||
}
|
||||
|
||||
get method() {
|
||||
return this.root.getByTestId('export-messages-method');
|
||||
}
|
||||
|
||||
get outputFormat() {
|
||||
return this.root.page().getByTestId('export-messages-output-format');
|
||||
}
|
||||
|
||||
get downloadButton() {
|
||||
return this.root.getByRole('button', { name: 'Download', exact: true });
|
||||
}
|
||||
|
||||
get sendButton() {
|
||||
return this.root.getByRole('button', { name: 'Send', exact: true });
|
||||
}
|
||||
|
||||
get clearSelectionButton() {
|
||||
return this.root.page().getByRole('button', { name: 'Clear selection' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
import { MenuMore } from '../menu';
|
||||
import { ConfirmDeleteModal } from '../modals';
|
||||
|
||||
export class FilesFlexTab extends FlexTab {
|
||||
readonly menu: MenuMore;
|
||||
|
||||
readonly confirmDeleteModal: ConfirmDeleteModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Files' }));
|
||||
this.menu = new MenuMore(page);
|
||||
this.confirmDeleteModal = new ConfirmDeleteModal(page.getByRole('dialog', { name: 'Are you sure?' }));
|
||||
}
|
||||
|
||||
get fileList() {
|
||||
return this.root.getByRole('list', { name: 'Files list' });
|
||||
}
|
||||
|
||||
getFileByName(name: string) {
|
||||
return this.fileList.getByRole('listitem').filter({ hasText: name });
|
||||
}
|
||||
|
||||
async deleteFile(name: string) {
|
||||
await this.getFileByName(name).getByRole('button', { name: 'More' }).click();
|
||||
await this.menu.selectMenuItem('Delete');
|
||||
await this.confirmDeleteModal.confirmDelete();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { expect } from '../../../utils/test';
|
||||
|
||||
export abstract class FlexTab {
|
||||
/**
|
||||
* @param root should be protected, but for now there are a lot of tests relying on accessing it directly, * so we need to keep it public until we can refactor those tests
|
||||
*/
|
||||
constructor(public root: Locator) {}
|
||||
|
||||
waitForDisplay() {
|
||||
return expect(this.root).toBeVisible();
|
||||
}
|
||||
|
||||
waitForDismissal() {
|
||||
return expect(this.root).not.toBeVisible();
|
||||
}
|
||||
|
||||
private get btnClose() {
|
||||
return this.root.getByRole('button', { name: 'Close', exact: true });
|
||||
}
|
||||
|
||||
get inputName() {
|
||||
return this.root.getByRole('textbox', { name: 'Name', exact: true });
|
||||
}
|
||||
|
||||
get btnSave() {
|
||||
return this.root.getByRole('button', { name: 'Save', exact: true });
|
||||
}
|
||||
|
||||
get btnCancel() {
|
||||
return this.root.getByRole('button', { name: 'Cancel', exact: true });
|
||||
}
|
||||
|
||||
get btnDelete() {
|
||||
return this.root.getByRole('button', { name: 'Delete', exact: true });
|
||||
}
|
||||
|
||||
get btnReset() {
|
||||
return this.root.getByRole('button', { name: 'Reset', exact: true });
|
||||
}
|
||||
|
||||
errorMessage(message: string): Locator {
|
||||
return this.root.locator('[role="alert"]', { hasText: message });
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.btnClose.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.btnSave.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export * from './auto-translate-flextab';
|
||||
export * from './admin-flextab-emoji';
|
||||
export * from './admin-edit-room-flextab';
|
||||
export * from './channels-flextab';
|
||||
export * from './device-info-flextab';
|
||||
export * from './edit-room-flextab';
|
||||
export * from './edit-user-flextab';
|
||||
export * from './edit-contact-flextab';
|
||||
export * from './export-messages-flextab';
|
||||
export * from './files-flextab';
|
||||
export * from './members-flextab';
|
||||
export * from './notification-preferences-flextab';
|
||||
export * from './prune-messages-flextab';
|
||||
export * from './room-info-flextab';
|
||||
export * from './threads-flextab';
|
||||
export * from './search-messages-flextab';
|
||||
export * from './user-info-flextab';
|
||||
@@ -0,0 +1,179 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
import { Listbox } from '../listbox';
|
||||
import { MenuMore } from '../menu';
|
||||
import { ConfirmRemoveModal } from '../modals';
|
||||
import { UserInfoFlexTab } from './user-info-flextab';
|
||||
import { Modal } from '../modals/modal';
|
||||
|
||||
export class ConfirmMuteModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Are you sure' }));
|
||||
}
|
||||
|
||||
private get btnMute() {
|
||||
return this.root.getByRole('button', { name: 'Yes, mute user!' });
|
||||
}
|
||||
|
||||
async confirmMute() {
|
||||
await this.btnMute.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoveTeamMemberModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Removing Member' }));
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.root.getByText('Select the channels you want the user to be removed from');
|
||||
}
|
||||
|
||||
channel(channelName: string) {
|
||||
return this.root.getByText(channelName);
|
||||
}
|
||||
|
||||
channelCheckbox(channelName: string) {
|
||||
return this.root.getByRole('row', { name: channelName }).getByRole('checkbox');
|
||||
}
|
||||
|
||||
async selectChannel(channelName: string) {
|
||||
await this.channel(channelName).click();
|
||||
}
|
||||
|
||||
private get btnContinue() {
|
||||
return this.root.getByRole('button', { name: 'Continue' });
|
||||
}
|
||||
|
||||
async continue() {
|
||||
await this.btnContinue.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
class AddUsersFlexTab extends FlexTab {
|
||||
readonly listbox: Listbox;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Add users' }));
|
||||
this.listbox = new Listbox(page);
|
||||
}
|
||||
|
||||
private btnAddUsers() {
|
||||
return this.root.getByRole('button', { name: 'Add users' });
|
||||
}
|
||||
|
||||
async addUser(username: string) {
|
||||
await this.root.getByRole('combobox', { name: 'Choose users' }).pressSequentially(username);
|
||||
await this.listbox.selectOption(username);
|
||||
await this.btnAddUsers().click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class MembersFlexTab extends FlexTab {
|
||||
readonly listbox: Listbox;
|
||||
|
||||
readonly removeModal: ConfirmRemoveModal;
|
||||
|
||||
readonly removeTeamMemberModal: RemoveTeamMemberModal;
|
||||
|
||||
readonly menu: MenuMore;
|
||||
|
||||
readonly confirmMuteModal: ConfirmMuteModal;
|
||||
|
||||
readonly addUsersFlexTab: AddUsersFlexTab;
|
||||
|
||||
readonly userInfo: UserInfoFlexTab;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Members' }));
|
||||
this.removeModal = new ConfirmRemoveModal(page.getByRole('dialog', { name: 'Confirmation', exact: true }));
|
||||
this.removeTeamMemberModal = new RemoveTeamMemberModal(page);
|
||||
this.listbox = new Listbox(page);
|
||||
this.menu = new MenuMore(page);
|
||||
this.confirmMuteModal = new ConfirmMuteModal(page);
|
||||
this.addUsersFlexTab = new AddUsersFlexTab(page);
|
||||
this.userInfo = new UserInfoFlexTab(page);
|
||||
}
|
||||
|
||||
memberOption(username: string) {
|
||||
return this.root.locator('li', { hasText: username });
|
||||
}
|
||||
|
||||
getMenuItemAction(action: string) {
|
||||
return this.menu.getMenuItem(action);
|
||||
}
|
||||
|
||||
async openMemberInfo(username: string) {
|
||||
await this.memberOption(username).click();
|
||||
}
|
||||
|
||||
async openMoreActions() {
|
||||
await this.root.getByRole('button', { name: 'More' }).click();
|
||||
}
|
||||
|
||||
async openMemberOptionMoreActions(username: string) {
|
||||
await this.memberOption(username).hover();
|
||||
await this.memberOption(username).getByRole('button', { name: 'More' }).click();
|
||||
}
|
||||
|
||||
async addUser(username: string) {
|
||||
await this.root.getByRole('button', { name: 'Add' }).click();
|
||||
await this.addUsersFlexTab.addUser(username);
|
||||
}
|
||||
|
||||
private get btnInviteLink() {
|
||||
return this.root.getByRole('button', { name: 'Invite Link' });
|
||||
}
|
||||
|
||||
async inviteUser() {
|
||||
await this.btnInviteLink.click();
|
||||
}
|
||||
|
||||
async muteUser(username: string) {
|
||||
await this.openMemberOptionMoreActions(username);
|
||||
await this.menu.selectMenuItem('Mute user');
|
||||
await this.confirmMuteModal.confirmMute();
|
||||
}
|
||||
|
||||
async unmuteUser(username: string) {
|
||||
await this.openMemberOptionMoreActions(username);
|
||||
await this.menu.selectMenuItem('Unmute user');
|
||||
}
|
||||
|
||||
async setUserAsModerator(username: string) {
|
||||
await this.openMemberOptionMoreActions(username);
|
||||
await this.menu.selectMenuItem('Set as moderator');
|
||||
}
|
||||
|
||||
async setUserAsOwner(username: string) {
|
||||
await this.openMemberOptionMoreActions(username);
|
||||
await this.menu.selectMenuItem('Set as owner');
|
||||
}
|
||||
|
||||
async showAllUsers() {
|
||||
await this.root.getByRole('button', { name: 'Online' }).click();
|
||||
await this.listbox.selectOption('All');
|
||||
}
|
||||
|
||||
private async ignoreUserAction(action: string, username: string) {
|
||||
await this.openMemberInfo(username);
|
||||
await this.userInfo.openMoreActions();
|
||||
await this.userInfo.menu.selectMenuItem(action);
|
||||
}
|
||||
|
||||
async ignoreUser(username: string) {
|
||||
await this.ignoreUserAction('Ignore', username);
|
||||
}
|
||||
|
||||
async unignoreUser(username: string) {
|
||||
await this.ignoreUserAction('Unignore', username);
|
||||
}
|
||||
|
||||
async confirmRemoveUser() {
|
||||
return this.removeModal.confirmRemove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
import { Listbox } from '../listbox';
|
||||
|
||||
export class NotificationPreferencesFlexTab extends FlexTab {
|
||||
readonly listbox: Listbox;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Notifications Preferences' }));
|
||||
this.listbox = new Listbox(page);
|
||||
}
|
||||
|
||||
getPreferenceByDevice(device: string): Locator {
|
||||
return this.root.locator(`//div[@id="${device}Alert"]`);
|
||||
}
|
||||
|
||||
async selectDropdownById(text: string): Promise<void> {
|
||||
await this.root.locator(`//div[@id="${text}"]`).click();
|
||||
}
|
||||
|
||||
async selectOptionByLabel(text: string): Promise<void> {
|
||||
await this.listbox.selectOption(text);
|
||||
}
|
||||
|
||||
async selectDevice(text: string): Promise<void> {
|
||||
await this.root.getByRole('button', { name: text }).click();
|
||||
}
|
||||
|
||||
async updateDevicePreference(device: string): Promise<void> {
|
||||
await this.selectDevice(device);
|
||||
await this.selectDropdownById(`${device}Alert`);
|
||||
await this.selectOptionByLabel('Mentions');
|
||||
}
|
||||
|
||||
async updateAllNotificationPreferences(): Promise<void> {
|
||||
await this.updateDevicePreference('Desktop');
|
||||
await this.updateDevicePreference('Mobile');
|
||||
await this.updateDevicePreference('Email');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
import { Modal } from '../modals/modal';
|
||||
|
||||
class ConfirmPruneMessageModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Are you sure?' }));
|
||||
}
|
||||
|
||||
async pruneConfirm(): Promise<void> {
|
||||
await this.root.getByRole('button', { name: 'Yes, prune them!' }).click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class PruneMessagesFlexTab extends FlexTab {
|
||||
readonly confirmPruneModal: ConfirmPruneMessageModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Prune Messages' }));
|
||||
this.confirmPruneModal = new ConfirmPruneMessageModal(page);
|
||||
}
|
||||
|
||||
get labelDoNotPrunePinned(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Do not prune pinned messages' });
|
||||
}
|
||||
|
||||
get labelFilesOnly(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Only remove the attached files, keep messages' });
|
||||
}
|
||||
|
||||
async prune(): Promise<void> {
|
||||
await this.root.getByRole('button', { name: 'Prune' }).click();
|
||||
await this.confirmPruneModal.pruneConfirm();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
import { MenuMore } from '../menu';
|
||||
import { ConfirmDeleteTeamModal, ConfirmDeleteRoomModal } from '../modals';
|
||||
import { Modal } from '../modals/modal';
|
||||
|
||||
class ConfirmLeaveRoomModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Confirmation', exact: true }));
|
||||
}
|
||||
|
||||
private get btnLeave() {
|
||||
return this.root.getByRole('button', { name: 'Leave', exact: true });
|
||||
}
|
||||
|
||||
async confirmLeave() {
|
||||
await this.btnLeave.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomInfoFlexTab extends FlexTab {
|
||||
readonly menu: MenuMore;
|
||||
|
||||
readonly confirmLeaveModal: ConfirmLeaveRoomModal;
|
||||
|
||||
readonly confirmDeleteModal: ConfirmDeleteRoomModal;
|
||||
|
||||
constructor(root: Locator) {
|
||||
super(root);
|
||||
this.menu = new MenuMore(root.page());
|
||||
this.confirmLeaveModal = new ConfirmLeaveRoomModal(root.page());
|
||||
this.confirmDeleteModal = new ConfirmDeleteRoomModal(root.page());
|
||||
}
|
||||
|
||||
get btnEdit(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Edit' });
|
||||
}
|
||||
|
||||
get btnLeave(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Leave' });
|
||||
}
|
||||
|
||||
get btnMore(): Locator {
|
||||
return this.root.getByRole('button', { name: 'More' });
|
||||
}
|
||||
|
||||
get optionDelete(): Locator {
|
||||
return this.menu.getMenuItem('Delete');
|
||||
}
|
||||
|
||||
async leaveRoom() {
|
||||
await this.btnLeave.click();
|
||||
await this.confirmLeaveModal.confirmLeave();
|
||||
}
|
||||
|
||||
async deleteRoom() {
|
||||
await this.btnMore.click();
|
||||
await this.menu.selectMenuItem('Delete');
|
||||
await this.confirmDeleteModal.confirmDelete();
|
||||
}
|
||||
}
|
||||
|
||||
class ConfirmConvertIntoChannelModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Confirmation', exact: true }));
|
||||
}
|
||||
|
||||
private get btnConvert() {
|
||||
return this.root.getByRole('button', { name: 'Convert', exact: true });
|
||||
}
|
||||
|
||||
async confirmConvert() {
|
||||
await this.btnConvert.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class TeamInfoFlexTab extends RoomInfoFlexTab {
|
||||
readonly confirmDeleteTeamModal: ConfirmDeleteTeamModal;
|
||||
|
||||
readonly confirmConvertIntoChannelModal: ConfirmConvertIntoChannelModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Team info' }));
|
||||
this.confirmDeleteTeamModal = new ConfirmDeleteTeamModal(page);
|
||||
this.confirmConvertIntoChannelModal = new ConfirmConvertIntoChannelModal(page);
|
||||
}
|
||||
|
||||
async deleteTeam() {
|
||||
await this.btnMore.click();
|
||||
await this.menu.selectMenuItem('Delete');
|
||||
return this.confirmDeleteTeamModal.confirmDelete();
|
||||
}
|
||||
|
||||
async convertIntoChannel() {
|
||||
await this.btnMore.click();
|
||||
await this.menu.selectMenuItem('Convert to Channel');
|
||||
await this.confirmConvertIntoChannelModal.confirmConvert();
|
||||
}
|
||||
}
|
||||
|
||||
export class OmnichannelRoomInfoFlexTab extends RoomInfoFlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Room Information' }));
|
||||
}
|
||||
|
||||
getInfo(value: string): Locator {
|
||||
return this.root.locator(`span >> text="${value}"`);
|
||||
}
|
||||
|
||||
getLabel(label: string): Locator {
|
||||
return this.root.locator(`div >> text="${label}"`);
|
||||
}
|
||||
|
||||
getInfoByLabel(label: string): Locator {
|
||||
return this.root.getByLabel(label);
|
||||
}
|
||||
|
||||
getTagInfoByLabel(label: string): Locator {
|
||||
return this.root.getByRole('list', { name: 'Tags' }).getByText(label, { exact: true });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
|
||||
export class SearchMessagesFlexTab extends FlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Search Messages' }));
|
||||
}
|
||||
|
||||
async search(text: string, { global = false }: { global?: boolean } = {}) {
|
||||
if (global) {
|
||||
await this.root.getByText('Global search').click();
|
||||
}
|
||||
await this.root.getByPlaceholder('Search Messages').fill(text);
|
||||
}
|
||||
|
||||
async getResultItem(messageText: string) {
|
||||
return this.root.getByRole('listitem', { name: messageText });
|
||||
}
|
||||
|
||||
async jumpToMessage(messageText: string) {
|
||||
const message = (await this.getResultItem(messageText)).first();
|
||||
await message.hover();
|
||||
await message.getByRole('button', { name: 'Jump to message' }).click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
|
||||
export class ThreadsFlexTab extends FlexTab {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog'));
|
||||
}
|
||||
|
||||
private get listThreadMessages(): Locator {
|
||||
return this.root.getByRole('list', { name: 'Thread message list' });
|
||||
}
|
||||
|
||||
get lastThreadMessage(): Locator {
|
||||
return this.listThreadMessages.locator('[data-qa-type="message"]').last().locator('[data-qa-type="message-body"]');
|
||||
}
|
||||
|
||||
getThreadMessageByText(messageText: string): Locator {
|
||||
return this.listThreadMessages.getByRole('listitem').getByText(messageText);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { FlexTab } from './flextab';
|
||||
import { MenuMore } from '../menu';
|
||||
|
||||
export class UserInfoFlexTab extends FlexTab {
|
||||
readonly menu: MenuMore;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'User Info' }));
|
||||
this.menu = new MenuMore(page);
|
||||
}
|
||||
|
||||
get btnEdit(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Edit' });
|
||||
}
|
||||
|
||||
get btnMoreActions(): Locator {
|
||||
return this.root.getByRole('button', { name: 'More' });
|
||||
}
|
||||
|
||||
get menuItemDeleteUser(): Locator {
|
||||
return this.menu.root.getByRole('menuitem', { name: 'Delete' });
|
||||
}
|
||||
|
||||
get username(): Locator {
|
||||
return this.root.getByLabel('Username');
|
||||
}
|
||||
|
||||
async openMoreActions() {
|
||||
await this.btnMoreActions.click();
|
||||
}
|
||||
}
|
||||
680
apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
Normal file
680
apps/meteor/tests/e2e/page-objects/fragments/home-content.ts
Normal file
@@ -0,0 +1,680 @@
|
||||
import fs from 'fs/promises';
|
||||
import { resolve, join, relative } from 'node:path';
|
||||
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { RoomComposer, ThreadComposer } from './composer';
|
||||
import { RoomHeader } from './room-header';
|
||||
import { createMediaResponsePromise } from '../../fixtures/responses/mediaResponse';
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
const FIXTURES_PATH = relative(process.cwd(), resolve(__dirname, '../../fixtures/files'));
|
||||
|
||||
export function getFilePath(fileName: string): string {
|
||||
return join(FIXTURES_PATH, fileName);
|
||||
}
|
||||
|
||||
export class HomeContent {
|
||||
protected readonly page: Page;
|
||||
|
||||
readonly header: RoomHeader;
|
||||
|
||||
readonly composer: RoomComposer;
|
||||
|
||||
protected readonly threadComposer: ThreadComposer;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.composer = new RoomComposer(page);
|
||||
this.threadComposer = new ThreadComposer(page);
|
||||
this.header = new RoomHeader(page.locator('main header'));
|
||||
}
|
||||
|
||||
get channelHeader(): Locator {
|
||||
return this.header.root;
|
||||
}
|
||||
|
||||
get burgerButton(): Locator {
|
||||
return this.channelHeader.getByRole('button', { name: 'Open sidebar' });
|
||||
}
|
||||
|
||||
get channelRetentionPolicyWarning(): Locator {
|
||||
return this.page.locator('main').getByRole('alert', { name: 'Retention policy warning banner' });
|
||||
}
|
||||
|
||||
get messagePopupUsers(): Locator {
|
||||
return this.page.locator('role=menu[name="People"]');
|
||||
}
|
||||
|
||||
get mainMessageList(): Locator {
|
||||
return this.page.getByRole('list', { name: 'Message list', exact: true });
|
||||
}
|
||||
|
||||
get threadMessageList(): Locator {
|
||||
return this.page.getByRole('list', { name: 'Thread message list', exact: true });
|
||||
}
|
||||
|
||||
/** The open room, matched by id. Use it to assert a permalink landed in the room it should. */
|
||||
getRoomById(roomId: string): Locator {
|
||||
return this.page.locator(`[data-qa-rc-room="${roomId}"]`);
|
||||
}
|
||||
|
||||
get messageListItems(): Locator {
|
||||
return this.mainMessageList.locator('[role="listitem"][aria-roledescription="message"]');
|
||||
}
|
||||
|
||||
get mainMessageListScroller(): Locator {
|
||||
return this.page.locator('[data-overlayscrollbars]', { has: this.mainMessageList });
|
||||
}
|
||||
|
||||
get threadMessageListScroller(): Locator {
|
||||
return this.page.locator('[data-overlayscrollbars]', { has: this.threadMessageList });
|
||||
}
|
||||
|
||||
get systemMessageListItems(): Locator {
|
||||
return this.mainMessageList.locator('[role="listitem"][aria-roledescription="system message"]');
|
||||
}
|
||||
|
||||
get threadMessageListItems(): Locator {
|
||||
return this.threadMessageList.locator('[role="listitem"][aria-roledescription="thread message"]');
|
||||
}
|
||||
|
||||
get lastUserMessage(): Locator {
|
||||
return this.messageListItems.last();
|
||||
}
|
||||
|
||||
get lastUserThreadMessage(): Locator {
|
||||
return this.threadMessageListItems.last();
|
||||
}
|
||||
|
||||
get lastThreadMessagePreview(): Locator {
|
||||
return this.page.getByRole('listitem').locator('[role="link"][aria-roledescription="thread message preview"]').last();
|
||||
}
|
||||
|
||||
get lastUserMessageDownloadLink(): Locator {
|
||||
return this.lastUserMessage.getByRole('link', { name: 'Download' });
|
||||
}
|
||||
|
||||
nthMessage(index: number): Locator {
|
||||
return this.messageListItems.nth(index);
|
||||
}
|
||||
|
||||
get lastUserMessageBody(): Locator {
|
||||
return this.lastUserMessage.locator('[role="document"][aria-roledescription="message body"]');
|
||||
}
|
||||
|
||||
get lastUserMessageAttachment(): Locator {
|
||||
return this.page.locator('[role="document"][aria-roledescription="message attachment"]').last();
|
||||
}
|
||||
|
||||
get lastUserMessageNotSequential(): Locator {
|
||||
return this.mainMessageList.locator('[role="listitem"][aria-roledescription="message"][data-sequential="false"]').last();
|
||||
}
|
||||
|
||||
get encryptedRoomHeaderIcon(): Locator {
|
||||
return this.page.locator('.rcx-room-header i.rcx-icon--name-key');
|
||||
}
|
||||
|
||||
get lastIgnoredUserMessage(): Locator {
|
||||
return this.lastUserMessageBody.locator('role=button[name="This message was ignored"]');
|
||||
}
|
||||
|
||||
get lastIgnoredThreadMessage(): Locator {
|
||||
return this.lastUserThreadMessage.getByRole('button', { name: 'This message was ignored' });
|
||||
}
|
||||
|
||||
get ignoredThreadMessages(): Locator {
|
||||
return this.threadMessageListItems.getByRole('button', { name: 'This message was ignored' });
|
||||
}
|
||||
|
||||
async joinRoomIfNeeded(): Promise<void> {
|
||||
if (await this.composer.inputMessage.isEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (!(await this.composer.btnJoinRoom.isVisible())) {
|
||||
return;
|
||||
}
|
||||
await this.composer.btnJoinRoom.click();
|
||||
}
|
||||
|
||||
async sendMessage(text: string, enforce = true): Promise<void> {
|
||||
await this.joinRoomIfNeeded();
|
||||
await expect(this.composer.inputMessage).toBeEnabled();
|
||||
await this.composer.inputMessage.fill(text);
|
||||
|
||||
if (!enforce) {
|
||||
await this.composer.btnSend.click();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for the message to settle in the DOM. Transport-agnostic — the
|
||||
// message may be sent via REST (`/api/v1/method.call/sendMessage`) or
|
||||
// via DDP/WS depending on whether DDPSDK is the active dispatcher;
|
||||
// either way Meteor's optimistic insert renders the new list item
|
||||
// before the server confirms, and the `rcx-message--pending` class
|
||||
// drops once the server result lands.
|
||||
if ((await this.messageListItems.count()) > 0) {
|
||||
const lastMessageBeforeSend = await this.messageListItems.last().getAttribute('id');
|
||||
|
||||
await this.composer.btnSend.click();
|
||||
|
||||
await expect.poll(() => this.messageListItems.last().getAttribute('id'), { timeout: 10_000 }).not.toEqual(lastMessageBeforeSend);
|
||||
await expect(this.lastUserMessage).not.toHaveClass(/rcx-message--pending/);
|
||||
} else {
|
||||
// No previous messages, nothing to compare
|
||||
await this.composer.btnSend.click();
|
||||
await expect(this.lastUserMessage).not.toHaveClass(/rcx-message--pending/);
|
||||
}
|
||||
}
|
||||
|
||||
async dispatchSlashCommand(text: string): Promise<void> {
|
||||
await this.joinRoomIfNeeded();
|
||||
await expect(this.composer.inputMessage).toBeEnabled();
|
||||
|
||||
await this.composer.inputMessage.fill('');
|
||||
await this.composer.inputMessage.fill(text);
|
||||
await this.page.keyboard.press('Enter');
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
async forwardMessage(chatName: string) {
|
||||
await this.messageListItems.last().hover();
|
||||
await this.page.locator('role=button[name="Forward message"]').click();
|
||||
|
||||
await this.page.getByRole('textbox', { name: 'Person or Channel', exact: true }).click();
|
||||
await this.page.keyboard.type(chatName);
|
||||
await this.page.locator('#position-container').getByText(chatName).waitFor();
|
||||
await this.page.locator('#position-container').getByText(chatName).click();
|
||||
await this.page.locator('role=button[name="Forward"]').click();
|
||||
}
|
||||
|
||||
// TODO: use modal fragments -----------------------------------------
|
||||
get btnModalCancel(): Locator {
|
||||
return this.page.locator('#modal-root .rcx-button-group--align-end .rcx-button--secondary');
|
||||
}
|
||||
|
||||
private get fileUploadModal(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'File Upload' });
|
||||
}
|
||||
|
||||
get btnModalConfirm(): Locator {
|
||||
return this.page.locator('#modal-root .rcx-button-group--align-end .rcx-button--primary');
|
||||
}
|
||||
|
||||
get btnModalConfirmDelete(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Yes, delete', exact: true });
|
||||
}
|
||||
|
||||
get btnCancelQuotePreview(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Dismiss quoted message' });
|
||||
}
|
||||
|
||||
get getFileDescription(): Locator {
|
||||
return this.lastUserMessage.locator('[role="document"][aria-roledescription="message body"]');
|
||||
}
|
||||
|
||||
get inputFileUploadName(): Locator {
|
||||
return this.fileUploadModal.getByRole('textbox', { name: 'File name' });
|
||||
}
|
||||
|
||||
get btnUpdateFileUpload(): Locator {
|
||||
return this.fileUploadModal.getByRole('button', { name: 'Update' });
|
||||
}
|
||||
|
||||
get btnCancelUpdateFileUpload(): Locator {
|
||||
return this.fileUploadModal.getByRole('button', { name: 'Cancel' });
|
||||
}
|
||||
|
||||
// -----------------------------------------
|
||||
|
||||
getLastMessageByFileName(filename: string): Locator {
|
||||
return this.messageListItems
|
||||
.filter({ has: this.page.getByRole('link', { name: filename }) })
|
||||
.last()
|
||||
.getByRole('link', { name: filename });
|
||||
}
|
||||
|
||||
get lastMessageTextAttachment(): Locator {
|
||||
return this.messageListItems.last().locator('[role="document"][aria-roledescription="message attachment"]');
|
||||
}
|
||||
|
||||
get lastMessageTextAttachmentEqualsText(): Locator {
|
||||
return this.messageListItems.last().locator('.rcx-attachment__details .rcx-message-body');
|
||||
}
|
||||
|
||||
get btnQuoteMessage(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Quote' });
|
||||
}
|
||||
|
||||
get quotePreview(): Locator {
|
||||
return this.page.locator('footer blockquote');
|
||||
}
|
||||
|
||||
get quotedMessage(): Locator {
|
||||
return this.page.getByRole('blockquote');
|
||||
}
|
||||
|
||||
quotedFileDescription(fileDescription: string): Locator {
|
||||
return this.quotedMessage.getByText(fileDescription);
|
||||
}
|
||||
|
||||
quotedFileName(fileName: string): Locator {
|
||||
return this.quotedMessage.getByTitle(fileName);
|
||||
}
|
||||
|
||||
threadMessageQuotedFileDescription(fileDescription: string): Locator {
|
||||
return this.threadQuotedMessage.getByText(fileDescription);
|
||||
}
|
||||
|
||||
threadMessageQuotedFileName(fileName: string): Locator {
|
||||
return this.threadQuotedMessage.getByTitle(fileName);
|
||||
}
|
||||
|
||||
get linkPreview(): Locator {
|
||||
return this.lastUserMessage.getByText('Link Preview');
|
||||
}
|
||||
|
||||
quotedLinkText(name: string): Locator {
|
||||
return this.quotedMessage.getByRole('link', { name });
|
||||
}
|
||||
|
||||
get threadQuotedMessage(): Locator {
|
||||
return this.page.getByRole('dialog').getByRole('blockquote');
|
||||
}
|
||||
|
||||
get threadQuotePreview(): Locator {
|
||||
return this.page.getByRole('dialog').locator('footer blockquote');
|
||||
}
|
||||
|
||||
get lastThreadMessageTextAttachmentEqualsText(): Locator {
|
||||
return this.threadMessageListItems.last().locator('.rcx-attachment__details');
|
||||
}
|
||||
|
||||
get mainThreadMessageText(): Locator {
|
||||
return this.threadMessageListItems.first();
|
||||
}
|
||||
|
||||
get lastThreadMessagePreviewText(): Locator {
|
||||
return this.page.locator('div.messages-box .messages-list [role=link]').last();
|
||||
}
|
||||
|
||||
get lastThreadMessageFileDescription(): Locator {
|
||||
return this.threadMessageListItems.last().locator('[role="document"][aria-roledescription="message body"]');
|
||||
}
|
||||
|
||||
getLastThreadMessageByFileName(filename: string): Locator {
|
||||
return this.threadMessageListItems.last().getByRole('link', { name: filename });
|
||||
}
|
||||
|
||||
// TODO: improve locator specificity
|
||||
get menuMore(): Locator {
|
||||
return this.page.getByRole('menu', { name: 'More', exact: true });
|
||||
}
|
||||
|
||||
get lastThreadMessageTextAttachment(): Locator {
|
||||
return this.threadMessageListItems.last().locator('[role="document"][aria-roledescription="message attachment"]');
|
||||
}
|
||||
|
||||
get btnOptionEditMessage(): Locator {
|
||||
return this.menuMore.getByRole('menuitem', { name: 'Edit', exact: true });
|
||||
}
|
||||
|
||||
get btnOptionDeleteMessage(): Locator {
|
||||
return this.menuMore.getByRole('menuitem', { name: 'Delete', exact: true });
|
||||
}
|
||||
|
||||
get btnOptionPinMessage(): Locator {
|
||||
return this.menuMore.getByRole('menuitem', { name: 'Pin', exact: true });
|
||||
}
|
||||
|
||||
get btnOptionStarMessage(): Locator {
|
||||
return this.menuMore.getByRole('menuitem', { name: 'Star', exact: true });
|
||||
}
|
||||
|
||||
get btnOptionReplyInDm(): Locator {
|
||||
return this.menuMore.getByRole('menuitem', { name: 'Reply in direct message', exact: true });
|
||||
}
|
||||
|
||||
get btnVoiceCall(): Locator {
|
||||
return this.primaryRoomActionsToolbar.getByRole('button', { name: 'Voice call' });
|
||||
}
|
||||
|
||||
get btnContactInformation(): Locator {
|
||||
return this.page.getByRole('button', { name: 'User Info' });
|
||||
}
|
||||
|
||||
get btnContactInfoVoiceCall(): Locator {
|
||||
return this.page.getByRole('group').getByRole('button', { name: 'Voice call' });
|
||||
}
|
||||
|
||||
get btnSendTranscript(): Locator {
|
||||
return this.page.locator('role=button[name="Send transcript"]');
|
||||
}
|
||||
|
||||
get btnSendTranscriptToEmail(): Locator {
|
||||
return this.page.locator('li.rcx-option', { hasText: 'Send via email' });
|
||||
}
|
||||
|
||||
get btnSendTranscriptAsPDF(): Locator {
|
||||
return this.page.locator('li.rcx-option', { hasText: 'Export as PDF' });
|
||||
}
|
||||
|
||||
get btnNewCannedResponse(): Locator {
|
||||
return this.page.locator('.rcx-vertical-bar button:has-text("Create")');
|
||||
}
|
||||
|
||||
get imageGallery(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Image gallery', exact: true });
|
||||
}
|
||||
|
||||
get imageGalleryImage(): Locator {
|
||||
return this.imageGallery.locator('.swiper-zoom-container img');
|
||||
}
|
||||
|
||||
async getGalleryButtonByName(name: string) {
|
||||
return this.imageGallery.locator(`button[name="${name}"]`);
|
||||
}
|
||||
|
||||
async dragAndDropTxtFile({ waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise<void> {
|
||||
const contract = await fs.readFile(getFilePath('any_file.txt'), 'utf-8');
|
||||
const dataTransfer = await this.page.evaluateHandle((contract) => {
|
||||
const data = new DataTransfer();
|
||||
const file = new File([`${contract}`], 'any_file.txt', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
data.items.add(file);
|
||||
return data;
|
||||
}, contract);
|
||||
|
||||
const responsePromise = waitForResponse ? createMediaResponsePromise(this.page) : null;
|
||||
await this.composer.inputMessage.dispatchEvent('dragenter', { dataTransfer });
|
||||
await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer });
|
||||
if (responsePromise) {
|
||||
await responsePromise;
|
||||
}
|
||||
}
|
||||
|
||||
async dragAndDropLstFile({ waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise<void> {
|
||||
const contract = await fs.readFile(getFilePath('lst-test.lst'), 'utf-8');
|
||||
const dataTransfer = await this.page.evaluateHandle((contract) => {
|
||||
const data = new DataTransfer();
|
||||
const file = new File([`${contract}`], 'lst-test.lst', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
data.items.add(file);
|
||||
return data;
|
||||
}, contract);
|
||||
|
||||
const responsePromise = waitForResponse ? createMediaResponsePromise(this.page) : null;
|
||||
await this.composer.inputMessage.dispatchEvent('dragenter', { dataTransfer });
|
||||
await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer });
|
||||
if (responsePromise) {
|
||||
await responsePromise;
|
||||
}
|
||||
}
|
||||
|
||||
async dragAndDropTxtFileToThread({ waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise<void> {
|
||||
const contract = await fs.readFile(getFilePath('any_file.txt'), 'utf-8');
|
||||
const dataTransfer = await this.page.evaluateHandle((contract) => {
|
||||
const data = new DataTransfer();
|
||||
const file = new File([`${contract}`], 'any_file.txt', {
|
||||
type: 'text/plain',
|
||||
});
|
||||
data.items.add(file);
|
||||
return data;
|
||||
}, contract);
|
||||
|
||||
const responsePromise = waitForResponse ? createMediaResponsePromise(this.page) : null;
|
||||
await this.threadComposer.inputMessage.dispatchEvent('dragenter', { dataTransfer });
|
||||
await this.page.locator('[role=dialog][data-qa="DropTargetOverlay"]').dispatchEvent('drop', { dataTransfer });
|
||||
if (responsePromise) {
|
||||
await responsePromise;
|
||||
}
|
||||
}
|
||||
|
||||
async sendMultipleFilesMessage(fileNames: string[], { waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise<void> {
|
||||
await this.page
|
||||
.getByLabel('Room composer')
|
||||
.locator('input[type=file]')
|
||||
.setInputFiles(fileNames.map((name) => getFilePath(name)));
|
||||
|
||||
if (!waitForResponse) {
|
||||
return;
|
||||
}
|
||||
|
||||
await expect(this.composer.btnSend).toBeEnabled();
|
||||
}
|
||||
|
||||
async sendFileMessage(fileName: string, { waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise<void> {
|
||||
const responsePromise = waitForResponse ? createMediaResponsePromise(this.page) : null;
|
||||
await this.page.getByLabel('Room composer').locator('input[type=file]').setInputFiles(getFilePath(fileName));
|
||||
if (responsePromise) {
|
||||
await responsePromise;
|
||||
}
|
||||
}
|
||||
|
||||
async sendFileMessageToThread(fileName: string, { waitForResponse = true }: { waitForResponse?: boolean } = {}): Promise<void> {
|
||||
await this.threadComposer.inputMessage.click();
|
||||
const responsePromise = waitForResponse ? createMediaResponsePromise(this.page) : null;
|
||||
await this.page.getByLabel('Thread composer').locator('input[type=file]').setInputFiles(getFilePath(fileName));
|
||||
if (responsePromise) {
|
||||
await responsePromise;
|
||||
}
|
||||
}
|
||||
|
||||
async openLastMessageMenu(): Promise<void> {
|
||||
await this.lastUserMessage.hover();
|
||||
await this.lastUserMessage.getByRole('button', { name: 'More', exact: true }).waitFor();
|
||||
await this.lastUserMessage.getByRole('button', { name: 'More', exact: true }).click();
|
||||
}
|
||||
|
||||
async openLastThreadMessageMenu(): Promise<void> {
|
||||
await this.lastUserThreadMessage.hover();
|
||||
await this.lastUserThreadMessage.getByRole('button', { name: 'More', exact: true }).waitFor();
|
||||
await this.lastUserThreadMessage.getByRole('button', { name: 'More', exact: true }).click();
|
||||
}
|
||||
|
||||
async toggleAlsoSendThreadToChannel(isChecked: boolean): Promise<void> {
|
||||
await this.page
|
||||
.getByRole('dialog')
|
||||
.locator('label', { has: this.page.getByRole('checkbox', { name: 'Also send to channel' }) })
|
||||
.setChecked(isChecked);
|
||||
}
|
||||
|
||||
get lastSystemMessageBody(): Locator {
|
||||
return this.page.locator('[role=document][aria-roledescription="system message body"]').last();
|
||||
}
|
||||
|
||||
get resumeOnHoldOmnichannelChatButton(): Locator {
|
||||
return this.page.locator('button.rcx-button--primary >> text="Resume"');
|
||||
}
|
||||
|
||||
get primaryRoomActionsToolbar(): Locator {
|
||||
return this.page.getByRole('toolbar', { name: 'Primary Room actions' });
|
||||
}
|
||||
|
||||
get btnVideoCall(): Locator {
|
||||
return this.page.locator('[role=toolbar][aria-label="Primary Room actions"]').getByRole('button', { name: 'Video call' });
|
||||
}
|
||||
|
||||
get btnToolbarOptions(): Locator {
|
||||
return this.primaryRoomActionsToolbar.getByRole('button', { name: 'Options', exact: true });
|
||||
}
|
||||
|
||||
get optionsMenu(): Locator {
|
||||
return this.page.getByRole('menu', { name: 'Options', exact: true });
|
||||
}
|
||||
|
||||
get starredMessagesMenuOption(): Locator {
|
||||
return this.optionsMenu.getByRole('menuitem', { name: 'Starred Messages', exact: true });
|
||||
}
|
||||
|
||||
getVideoConfPopup(name?: string): Locator {
|
||||
return this.page.getByRole('dialog', { name });
|
||||
}
|
||||
|
||||
get btnStartVideoCall(): Locator {
|
||||
return this.getVideoConfPopup().getByRole('button', { name: 'Start call' });
|
||||
}
|
||||
|
||||
get btnVideoConfMic(): Locator {
|
||||
return this.getVideoConfPopup().getByRole('button', { name: 'Mic' });
|
||||
}
|
||||
|
||||
get btnDeclineVideoCall(): Locator {
|
||||
return this.page.locator('.rcx-button--secondary-danger.rcx-button >> text="Decline"');
|
||||
}
|
||||
|
||||
get videoConfMessageBlock(): Locator {
|
||||
return this.page.locator('.rcx-videoconf-message-block');
|
||||
}
|
||||
|
||||
get btnAnonymousSignIn(): Locator {
|
||||
return this.page.locator('footer >> role=button[name="Sign in to start talking"]');
|
||||
}
|
||||
|
||||
get btnAnonymousTalk(): Locator {
|
||||
return this.page.locator('role=button[name="Or talk as anonymous"]');
|
||||
}
|
||||
|
||||
get nextSlideButton(): Locator {
|
||||
return this.page.getByLabel('Next slide');
|
||||
}
|
||||
|
||||
get previousSlideButton(): Locator {
|
||||
return this.page.getByLabel('Previous slide');
|
||||
}
|
||||
|
||||
get currentGalleryImage(): Locator {
|
||||
return this.page.locator('div[class="swiper-slide swiper-slide-active"] img');
|
||||
}
|
||||
|
||||
// TODO: use getSystemMessageByText instead
|
||||
findSystemMessage(text: string): Locator {
|
||||
return this.page.locator(`[role="document"][aria-roledescription="system message body"]`, { hasText: text });
|
||||
}
|
||||
|
||||
getSystemMessageByText(text: string): Locator {
|
||||
return this.page.locator('[role="listitem"][aria-roledescription="system message"]', { hasText: text });
|
||||
}
|
||||
|
||||
getMessageByText(text: string): Locator {
|
||||
return this.page.locator('[role="listitem"][aria-roledescription="message"]', { hasText: text });
|
||||
}
|
||||
|
||||
getLastMessageActionButton(name: string): Locator {
|
||||
return this.lastUserMessage.getByRole('button', { name, exact: true });
|
||||
}
|
||||
|
||||
getLastThreadMessageActionButton(name: string): Locator {
|
||||
return this.lastUserThreadMessage.getByRole('button', { name, exact: true });
|
||||
}
|
||||
|
||||
getMessageById(id: string): Locator {
|
||||
return this.page.locator(`[role="listitem"][aria-roledescription="message"][id="${id}"]`);
|
||||
}
|
||||
|
||||
/** Thread replies carry `aria-roledescription="thread message"`, so `getMessageById` skips them. */
|
||||
getThreadMessageById(id: string): Locator {
|
||||
return this.threadMessageList.locator(`[role="listitem"][id="${id}"]`);
|
||||
}
|
||||
|
||||
async scrollToMessage(messageLocator: Locator, direction: 'up' | 'down' = 'up'): Promise<Locator> {
|
||||
const scroller = this.mainMessageListScroller;
|
||||
const delta = direction === 'up' ? -400 : 400;
|
||||
|
||||
await expect(async () => {
|
||||
await scroller.evaluate((el, d) => el.scrollBy(0, d), delta);
|
||||
await expect(messageLocator.first()).toBeVisible({ timeout: 500 });
|
||||
}).toPass({ timeout: 30000, intervals: [100] });
|
||||
|
||||
return messageLocator;
|
||||
}
|
||||
|
||||
get btnJoinChannel(): Locator {
|
||||
return this.page.getByRole('main').getByRole('button', { name: 'Join channel', exact: true });
|
||||
}
|
||||
|
||||
async waitForChannel(): Promise<void> {
|
||||
await this.page.locator('role=main').waitFor();
|
||||
|
||||
// a room the user cannot preview renders a join screen instead of the header and message list
|
||||
await this.mainMessageList.or(this.btnJoinChannel).first().waitFor();
|
||||
|
||||
if (await this.btnJoinChannel.isVisible()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.page.locator('role=main >> role=heading[level=1]').waitFor();
|
||||
await expect(this.mainMessageList).not.toHaveAttribute('aria-busy', 'true');
|
||||
}
|
||||
|
||||
async waitForThread(): Promise<void> {
|
||||
const messageList = this.page.getByRole('list', { name: 'Thread message list', exact: true });
|
||||
await messageList.waitFor();
|
||||
|
||||
await expect(messageList).not.toHaveAttribute('aria-busy', 'true');
|
||||
}
|
||||
|
||||
async openReplyInThread(): Promise<void> {
|
||||
await this.lastUserMessage.hover();
|
||||
await this.lastUserMessage.getByRole('button', { name: 'Reply in thread' }).waitFor();
|
||||
await this.lastUserMessage.getByRole('button', { name: 'Reply in thread' }).click();
|
||||
}
|
||||
|
||||
async sendMessageInThread(text: string): Promise<void> {
|
||||
await this.threadComposer.inputMessage.fill(text);
|
||||
await this.threadComposer.btnSend.click();
|
||||
}
|
||||
|
||||
async sendMessageInVideoConfPopup(text: string): Promise<void> {
|
||||
await this.page.getByRole('dialog').getByRole('textbox', { name: 'Message' }).fill(text);
|
||||
await this.page.getByRole('dialog').getByRole('button', { name: 'Send', exact: true }).click();
|
||||
}
|
||||
|
||||
async deleteLastMessage(): Promise<void> {
|
||||
await this.lastUserMessage.hover();
|
||||
await this.openLastMessageMenu();
|
||||
await this.btnOptionDeleteMessage.click();
|
||||
await this.btnModalConfirmDelete.click();
|
||||
await expect(this.btnModalConfirmDelete).toBeDisabled();
|
||||
}
|
||||
|
||||
get btnClearSelection() {
|
||||
return this.page.getByRole('button', { name: 'Clear selection' });
|
||||
}
|
||||
|
||||
get contactUnknownCallout() {
|
||||
return this.page.getByRole('status', { name: 'Unknown contact. This contact is not on the contact list.' });
|
||||
}
|
||||
|
||||
get btnDismissContactUnknownCallout() {
|
||||
return this.contactUnknownCallout.getByRole('button', { name: 'Dismiss' });
|
||||
}
|
||||
|
||||
get btnOptionStartDiscussion(): Locator {
|
||||
return this.page.getByRole('menuitem', { name: 'Start a Discussion' });
|
||||
}
|
||||
|
||||
async quoteMessage(quoteText: string, originalMessageText?: string): Promise<void> {
|
||||
await this.lastUserMessage.hover();
|
||||
await this.btnQuoteMessage.click();
|
||||
if (originalMessageText) {
|
||||
await expect(this.quotePreview).toBeVisible();
|
||||
await expect(this.quotePreview).toContainText(originalMessageText);
|
||||
}
|
||||
await this.sendMessage(quoteText);
|
||||
}
|
||||
|
||||
get clearAllUnreadsModal(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Clear all unreads?' });
|
||||
}
|
||||
|
||||
async markAllRoomsAsRead(): Promise<void> {
|
||||
await this.page.keyboard.down('Shift');
|
||||
await this.page.keyboard.press('Escape');
|
||||
await this.page.keyboard.up('Shift');
|
||||
await expect(this.clearAllUnreadsModal).toBeVisible();
|
||||
await this.clearAllUnreadsModal.getByRole('button', { name: 'Yes, clear all!' }).click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { HomeContent } from './home-content';
|
||||
import { OmnichannelTransferChatModal, OmnichannelReturnToQueueModal } from './modals';
|
||||
|
||||
export class HomeOmnichannelContent extends HomeContent {
|
||||
readonly forwardChatModal: OmnichannelTransferChatModal;
|
||||
|
||||
readonly returnToQueueModal: OmnichannelReturnToQueueModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.forwardChatModal = new OmnichannelTransferChatModal(page);
|
||||
this.returnToQueueModal = new OmnichannelReturnToQueueModal(page);
|
||||
}
|
||||
|
||||
get btnReturnToQueue(): Locator {
|
||||
return this.page.locator('role=button[name="Move to the queue"]');
|
||||
}
|
||||
|
||||
get btnTakeChat(): Locator {
|
||||
return this.page.locator('role=button[name="Take it!"]');
|
||||
}
|
||||
|
||||
get btnReturn(): Locator {
|
||||
return this.header.root.getByRole('button', { name: 'Back' });
|
||||
}
|
||||
|
||||
get btnResume(): Locator {
|
||||
return this.page.locator('role=button[name="Resume"]');
|
||||
}
|
||||
|
||||
get infoHeaderName(): Locator {
|
||||
return this.page.locator('.rcx-room-header').getByRole('heading');
|
||||
}
|
||||
|
||||
async selectCannedResponse(cannedResponseName: string): Promise<void> {
|
||||
await this.composer.inputMessage.pressSequentially('!');
|
||||
const popup = this.composer.boxPopup;
|
||||
await popup.waitFor({ state: 'visible' });
|
||||
await this.composer.inputMessage.pressSequentially(cannedResponseName);
|
||||
await popup.getByText(cannedResponseName, { exact: true }).click();
|
||||
}
|
||||
}
|
||||
12
apps/meteor/tests/e2e/page-objects/fragments/index.ts
Normal file
12
apps/meteor/tests/e2e/page-objects/fragments/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export * from './home-content';
|
||||
export * from './home-omnichannel-content';
|
||||
export * from './navbar';
|
||||
export * from './sidebar';
|
||||
export * from './sidepanel';
|
||||
export * from './modals';
|
||||
export * from './toast-messages';
|
||||
export * from './menu';
|
||||
export * from './table';
|
||||
export * from './toolbar';
|
||||
export * from './composer';
|
||||
export * from './flextabs';
|
||||
21
apps/meteor/tests/e2e/page-objects/fragments/listbox.ts
Normal file
21
apps/meteor/tests/e2e/page-objects/fragments/listbox.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Locator, Page } from 'playwright-core';
|
||||
|
||||
export class Listbox {
|
||||
readonly root: Locator;
|
||||
|
||||
constructor(page: Page, name?: string) {
|
||||
/**
|
||||
* Currently, our selects and multiSelects has multiple listboxes in the DOM.
|
||||
* So, to avoid selecting the wrong one, we will always select the last until we fix it.
|
||||
*/
|
||||
this.root = page.getByRole('listbox', { name }).last();
|
||||
}
|
||||
|
||||
async selectOption(name: string, exact?: boolean) {
|
||||
return this.root.getByRole('option', { name, exact }).click();
|
||||
}
|
||||
|
||||
public getOption(name: string): Locator {
|
||||
return this.root.getByRole('option', { name, exact: true });
|
||||
}
|
||||
}
|
||||
50
apps/meteor/tests/e2e/page-objects/fragments/menu.ts
Normal file
50
apps/meteor/tests/e2e/page-objects/fragments/menu.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
export abstract class Menu {
|
||||
constructor(public root: Locator) {}
|
||||
|
||||
waitForDisplay() {
|
||||
return expect(this.root).toBeVisible();
|
||||
}
|
||||
|
||||
waitForDismissal() {
|
||||
return expect(this.root).not.toBeVisible();
|
||||
}
|
||||
|
||||
getMenuItem(itemName: string) {
|
||||
return this.root.getByRole('menuitem', { name: itemName, exact: true });
|
||||
}
|
||||
|
||||
async selectMenuItem(itemName: string, submenuTrigger = false) {
|
||||
await this.getMenuItem(itemName).click();
|
||||
if (!submenuTrigger) {
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MenuMore extends Menu {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('menu', { name: 'More' }));
|
||||
}
|
||||
}
|
||||
|
||||
export class MenuMoreActions extends Menu {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('menu', { name: 'More actions' }));
|
||||
}
|
||||
}
|
||||
|
||||
export class MenuOptions extends Menu {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('menu', { name: 'Options' }));
|
||||
}
|
||||
}
|
||||
|
||||
export class MenuMoveTo extends Menu {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('menu', { name: 'Move to' }));
|
||||
}
|
||||
}
|
||||
17
apps/meteor/tests/e2e/page-objects/fragments/message.ts
Normal file
17
apps/meteor/tests/e2e/page-objects/fragments/message.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
export class Message {
|
||||
constructor(public readonly root: Locator) {}
|
||||
|
||||
get body() {
|
||||
return this.root.locator('[role="document"][aria-roledescription="message body"]');
|
||||
}
|
||||
|
||||
getFileUploadByName(filename: string) {
|
||||
return this.root.getByRole('link', { name: filename });
|
||||
}
|
||||
|
||||
get encryptedIcon() {
|
||||
return this.root.locator('.rcx-icon--name-key');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Locator, Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class AppsModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Modal Example' }));
|
||||
}
|
||||
|
||||
get textInput(): Locator {
|
||||
return this.root.locator('[name="modal_input"]');
|
||||
}
|
||||
|
||||
get textInputErrorMessage(): Locator {
|
||||
return this.root.getByText('Validation failed');
|
||||
}
|
||||
|
||||
get btnSubmit(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Submit' });
|
||||
}
|
||||
|
||||
async submit(inputText: string) {
|
||||
await this.textInput.fill(inputText);
|
||||
await this.btnSubmit.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Locator, Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class ConfirmDeleteModal extends Modal {
|
||||
constructor(root: Locator) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
get btnDelete() {
|
||||
return this.root.getByRole('button', { name: 'Delete' });
|
||||
}
|
||||
|
||||
async confirmDelete() {
|
||||
await this.btnDelete.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfirmDeleteRoomModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Delete' }));
|
||||
}
|
||||
|
||||
get btnDelete() {
|
||||
return this.root.getByRole('button', { name: 'Yes, delete', exact: true });
|
||||
}
|
||||
|
||||
async confirmDelete() {
|
||||
await this.btnDelete.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfirmDeleteDepartmentModal extends ConfirmDeleteModal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Delete Department?' }));
|
||||
}
|
||||
|
||||
get inputConfirmDepartmentName() {
|
||||
return this.root.getByRole('textbox', { name: 'Department name' });
|
||||
}
|
||||
|
||||
async deleteDepartment(departmentName: string) {
|
||||
await this.inputConfirmDepartmentName.fill(departmentName);
|
||||
await this.confirmDelete();
|
||||
}
|
||||
}
|
||||
|
||||
export class ConfirmDeleteTeamModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Delete team', exact: true }));
|
||||
}
|
||||
|
||||
private get btnDelete() {
|
||||
return this.root.getByRole('button', { name: 'Yes, delete', exact: true });
|
||||
}
|
||||
|
||||
async confirmDelete() {
|
||||
await this.btnDelete.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class ConfirmLogoutModal extends Modal {
|
||||
get btnLogout() {
|
||||
return this.root.getByRole('button', { name: 'Log out device' });
|
||||
}
|
||||
|
||||
async confirmLogout() {
|
||||
await this.btnLogout.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class ConfirmRemoveModal extends Modal {
|
||||
constructor(root: Locator) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
get btnRemove() {
|
||||
return this.root.getByRole('button', { name: 'Remove' });
|
||||
}
|
||||
|
||||
async confirmRemove() {
|
||||
await this.btnRemove.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Listbox } from '../listbox';
|
||||
import { Modal } from './modal';
|
||||
|
||||
export abstract class CreateNewModal extends Modal {
|
||||
readonly listbox: Listbox;
|
||||
|
||||
constructor(root: Locator, page: Page) {
|
||||
super(root, page);
|
||||
this.listbox = new Listbox(page);
|
||||
}
|
||||
|
||||
get inputName(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Name' });
|
||||
}
|
||||
|
||||
get checkboxPrivate(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Private' });
|
||||
}
|
||||
|
||||
get checkboxEncrypted(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Encrypted' });
|
||||
}
|
||||
|
||||
get checkboxReadOnly(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Read-only' });
|
||||
}
|
||||
|
||||
get checkboxFederated(): Locator {
|
||||
return this.root.locator('label', { hasText: 'Federated' });
|
||||
}
|
||||
|
||||
get btnCreate(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Create', exact: true });
|
||||
}
|
||||
|
||||
get inputAddMembers(): Locator {
|
||||
return this.root.getByRole('combobox', { name: 'Members' });
|
||||
}
|
||||
|
||||
async addMember(memberName: string): Promise<void> {
|
||||
await this.inputAddMembers.click();
|
||||
await this.inputAddMembers.fill(memberName, { force: true });
|
||||
await this.listbox.selectOption(memberName);
|
||||
await this.inputAddMembers.click();
|
||||
}
|
||||
|
||||
async create() {
|
||||
await this.btnCreate.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateNewChannelModal extends CreateNewModal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Create channel' }), page);
|
||||
}
|
||||
|
||||
get advancedSettingsAccordion(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Advanced settings', exact: true });
|
||||
}
|
||||
|
||||
async inviteUserToChannel(username: string) {
|
||||
await this.inputAddMembers.click();
|
||||
await this.inputAddMembers.fill(username);
|
||||
await this.listbox.selectOption(username);
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateNewDMModal extends CreateNewModal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'New direct message' }), page);
|
||||
}
|
||||
|
||||
get autocompleteUser(): Locator {
|
||||
return this.root.getByRole('combobox', { name: 'Select one or more people to message', exact: true });
|
||||
}
|
||||
|
||||
async inviteUserToDM(username: string) {
|
||||
await this.autocompleteUser.click();
|
||||
await this.autocompleteUser.fill(username);
|
||||
await this.listbox.selectOption(username);
|
||||
await this.page?.keyboard.press('Tab');
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateNewTeamModal extends CreateNewModal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Create team' }), page);
|
||||
}
|
||||
|
||||
get advancedSettingsAccordion(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Advanced settings', exact: true });
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateNewDiscussionModal extends CreateNewModal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Create discussion' }), page);
|
||||
}
|
||||
|
||||
get inputParentRoom(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Parent channel or team' });
|
||||
}
|
||||
|
||||
getParentRoomListItem(name: string): Locator {
|
||||
return this.listbox.getOption(name);
|
||||
}
|
||||
|
||||
get inputMessage(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Message', exact: true });
|
||||
}
|
||||
}
|
||||
|
||||
export class CreateNewCategoryModal extends CreateNewModal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Create category', exact: true }), page);
|
||||
}
|
||||
|
||||
private get btnCreateAndMove() {
|
||||
return this.root.getByRole('button', { name: 'Create and move', exact: true });
|
||||
}
|
||||
|
||||
override async create(hasRoom = false) {
|
||||
await (hasRoom ? this.btnCreateAndMove.click() : this.btnCreate.click());
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Locator, Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class DeleteAccountModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Delete account?' }));
|
||||
}
|
||||
|
||||
get btnDeleteAccount(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Delete Account' });
|
||||
}
|
||||
|
||||
get btnCancel(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Cancel' });
|
||||
}
|
||||
|
||||
async confirmDelete({ waitForDismissal = true } = {}): Promise<void> {
|
||||
await this.btnDeleteAccount.click();
|
||||
if (waitForDismissal) {
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
get deleteAccountDialogMessageWithPassword(): Locator {
|
||||
return this.root.getByText('Enter your password to delete your account. This cannot be undone.');
|
||||
}
|
||||
|
||||
get inputPassword(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Enter your password to delete your account. This cannot be undone.' });
|
||||
}
|
||||
|
||||
get inputErrorMessage(): Locator {
|
||||
return this.root.locator('[role="alert"]', { hasText: 'Invalid password' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class DeleteCategoryModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Delete category', exact: true }), page);
|
||||
}
|
||||
|
||||
private get btnDelete() {
|
||||
return this.root.getByRole('button', { name: 'Delete', exact: true });
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.btnDelete.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { ToastMessages } from '../toast-messages';
|
||||
|
||||
export class DisableRoomEncryptionModal extends Modal {
|
||||
private readonly toastMessages: ToastMessages;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Disable encryption' }));
|
||||
this.toastMessages = new ToastMessages(page);
|
||||
}
|
||||
|
||||
private get disableButton() {
|
||||
return this.root.getByRole('button', { name: 'Disable encryption' });
|
||||
}
|
||||
|
||||
async disable() {
|
||||
await this.disableButton.click();
|
||||
await this.waitForDismissal();
|
||||
await this.toastMessages.dismissToast('success');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { ToastMessages } from '../toast-messages';
|
||||
|
||||
export class EditStatusModal extends Modal {
|
||||
readonly toastMessages: ToastMessages;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Set your status' }), page);
|
||||
this.toastMessages = new ToastMessages(page);
|
||||
}
|
||||
|
||||
private get statusMessageInput() {
|
||||
return this.root.getByRole('textbox', { name: 'Status' });
|
||||
}
|
||||
|
||||
private get statusTypeButton() {
|
||||
return this.root.getByRole('button', { name: 'User status menu' });
|
||||
}
|
||||
|
||||
private get durationSelect() {
|
||||
return this.root.getByLabel('Clear status after');
|
||||
}
|
||||
|
||||
get customDateInput() {
|
||||
return this.root.getByLabel('Expiration date');
|
||||
}
|
||||
|
||||
get customTimeInput() {
|
||||
return this.root.getByLabel('Expiration time');
|
||||
}
|
||||
|
||||
get btnSubmit() {
|
||||
return this.root.getByRole('button', { name: 'Save' });
|
||||
}
|
||||
|
||||
get durationError() {
|
||||
return this.root.getByRole('alert').filter({ hasText: 'Expiration must be in the future' });
|
||||
}
|
||||
|
||||
get durationMissingError() {
|
||||
return this.root.getByRole('alert').filter({ hasText: 'Choose date and time' });
|
||||
}
|
||||
|
||||
async selectStatusType(status: string): Promise<void> {
|
||||
await this.statusTypeButton.click();
|
||||
await this.page!.getByRole('option', { name: status }).click();
|
||||
}
|
||||
|
||||
async setStatusMessage(message: string): Promise<void> {
|
||||
await this.statusMessageInput.fill(message);
|
||||
}
|
||||
|
||||
async selectDuration(duration: string): Promise<void> {
|
||||
await this.durationSelect.click();
|
||||
await this.page!.getByRole('option', { name: new RegExp(duration) }).click();
|
||||
}
|
||||
|
||||
async changeStatusMessage(statusMessage?: string): Promise<void> {
|
||||
if (statusMessage !== undefined) {
|
||||
await this.setStatusMessage(statusMessage);
|
||||
}
|
||||
await this.save();
|
||||
await this.toastMessages.dismissToast();
|
||||
}
|
||||
|
||||
async setStatusWithExpiration({
|
||||
message,
|
||||
statusType,
|
||||
duration,
|
||||
customDate,
|
||||
customTime,
|
||||
}: {
|
||||
message?: string;
|
||||
statusType?: string;
|
||||
duration: string;
|
||||
customDate?: string;
|
||||
customTime?: string;
|
||||
}): Promise<void> {
|
||||
if (statusType) {
|
||||
await this.selectStatusType(statusType);
|
||||
}
|
||||
if (message !== undefined) {
|
||||
await this.setStatusMessage(message);
|
||||
}
|
||||
await this.selectDuration(duration);
|
||||
if (customDate) {
|
||||
await this.customDateInput.fill(customDate);
|
||||
}
|
||||
if (customTime) {
|
||||
await this.customTimeInput.fill(customTime);
|
||||
}
|
||||
await this.save();
|
||||
await this.toastMessages.dismissToast();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { ToastMessages } from '../toast-messages';
|
||||
|
||||
export class EnableRoomEncryptionModal extends Modal {
|
||||
private readonly toastMessages: ToastMessages;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Enable encryption' }));
|
||||
this.toastMessages = new ToastMessages(page);
|
||||
}
|
||||
|
||||
private get enableButton() {
|
||||
return this.root.getByRole('button', { name: 'Enable encryption' });
|
||||
}
|
||||
|
||||
async enable() {
|
||||
await this.enableButton.click();
|
||||
await this.waitForDismissal();
|
||||
await this.toastMessages.dismissToast('success');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class EnterE2EEPasswordModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Enter E2EE password' }));
|
||||
}
|
||||
|
||||
private get passwordInput() {
|
||||
return this.root.getByPlaceholder('Please enter your E2EE password');
|
||||
}
|
||||
|
||||
private get forgotPasswordLink() {
|
||||
return this.root.getByRole('link', { name: 'Forgot E2EE password?' });
|
||||
}
|
||||
|
||||
private get enterE2EEPasswordButton() {
|
||||
return this.root.getByRole('button', { name: 'Enable encryption' });
|
||||
}
|
||||
|
||||
async enterPassword(password: string) {
|
||||
await this.passwordInput.fill(password);
|
||||
await this.enterE2EEPasswordButton.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
|
||||
async forgotPassword() {
|
||||
await this.forgotPasswordLink.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { ToastMessages } from '../toast-messages';
|
||||
|
||||
export class EnterPasswordModal extends Modal {
|
||||
readonly toastMessages: ToastMessages;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Please enter your password' }));
|
||||
this.toastMessages = new ToastMessages(page);
|
||||
}
|
||||
|
||||
private get inputPassword() {
|
||||
return this.root.getByRole('textbox', { name: 'For your security, you must enter your current password to continue' });
|
||||
}
|
||||
|
||||
private get btnVerify() {
|
||||
return this.root.getByRole('button', { name: 'Verify' });
|
||||
}
|
||||
|
||||
async enterPassword(password: string): Promise<void> {
|
||||
await this.inputPassword.fill(password);
|
||||
await this.btnVerify.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class FileUploadModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'File Upload' }));
|
||||
}
|
||||
|
||||
private get inputFileName() {
|
||||
return this.root.getByRole('textbox', { name: 'File name' });
|
||||
}
|
||||
|
||||
private get updateButton() {
|
||||
return this.root.getByRole('button', { name: 'Update' });
|
||||
}
|
||||
|
||||
setName(fileName: string) {
|
||||
return this.inputFileName.fill(fileName);
|
||||
}
|
||||
|
||||
async update() {
|
||||
await this.updateButton.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class FileUploadWarningModal extends Modal {
|
||||
constructor(root: Locator) {
|
||||
super(root);
|
||||
}
|
||||
|
||||
get btnOk() {
|
||||
return this.root.getByRole('button', { name: 'Ok' });
|
||||
}
|
||||
|
||||
get btnSendAnyway() {
|
||||
return this.root.getByRole('button', { name: 'Send anyway' });
|
||||
}
|
||||
|
||||
getContent(text: string) {
|
||||
return this.root.getByText(text);
|
||||
}
|
||||
|
||||
private get btnCancel() {
|
||||
return this.root.getByRole('button', { name: 'Cancel' });
|
||||
}
|
||||
|
||||
async cancel() {
|
||||
await this.btnCancel.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
|
||||
async confirmSend() {
|
||||
await this.btnSendAnyway.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
26
apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts
Normal file
26
apps/meteor/tests/e2e/page-objects/fragments/modals/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export * from './apps-modal';
|
||||
export * from './confirm-delete-modal';
|
||||
export * from './confirm-logout-modal';
|
||||
export * from './confirm-remove-modal';
|
||||
export * from './create-new-modal';
|
||||
export * from './manage-category-modal';
|
||||
export * from './delete-account-modal';
|
||||
export * from './delete-category-modal';
|
||||
export * from './disable-room-encryption-modal';
|
||||
export * from './edit-status-modal';
|
||||
export * from './enable-room-encryption-modal';
|
||||
export * from './enter-e2ee-password-modal';
|
||||
export * from './enter-password-modal';
|
||||
export * from './file-upload-modal';
|
||||
export * from './omnichannel-close-chat-modal';
|
||||
export * from './omnichannel-on-hold-modal';
|
||||
export * from './omnichannel-transfer-chat-modal';
|
||||
export * from './omnichannel-confirm-remove-chat';
|
||||
export * from './omnichannel-contact-review-modal';
|
||||
export * from './omnichannel-delete-contact-modal';
|
||||
export * from './omnichannel-reset-priorities-modal';
|
||||
export * from './omnichannel-return-to-queue-modal';
|
||||
export * from './report-message-modal';
|
||||
export * from './reset-e2ee-password-modal';
|
||||
export * from './save-e2ee-password-modal';
|
||||
export * from './upsell-modal';
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class ManageCategoryModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Manage category', exact: true }), page);
|
||||
}
|
||||
|
||||
private get inputName() {
|
||||
return this.root.getByRole('textbox', { name: 'Name', exact: true });
|
||||
}
|
||||
|
||||
async rename(newName: string): Promise<void> {
|
||||
await this.inputName.fill(newName);
|
||||
await this.save();
|
||||
}
|
||||
}
|
||||
39
apps/meteor/tests/e2e/page-objects/fragments/modals/modal.ts
Normal file
39
apps/meteor/tests/e2e/page-objects/fragments/modals/modal.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '../../../utils/test';
|
||||
|
||||
export abstract class Modal {
|
||||
constructor(
|
||||
protected root: Locator,
|
||||
protected page?: Page,
|
||||
) {
|
||||
this.root = root;
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
waitForDisplay() {
|
||||
return expect(this.root).toBeVisible();
|
||||
}
|
||||
|
||||
waitForDismissal() {
|
||||
return expect(this.root).not.toBeVisible();
|
||||
}
|
||||
|
||||
private get btnClose() {
|
||||
return this.root.getByRole('button', { name: 'Close' });
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.btnClose.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
|
||||
private get btnSave() {
|
||||
return this.root.getByRole('button', { name: 'Save' });
|
||||
}
|
||||
|
||||
async save() {
|
||||
await this.btnSave.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { ToastMessages } from '../toast-messages';
|
||||
|
||||
export class OmnichannelCloseChatModal extends Modal {
|
||||
private readonly toastMessages: ToastMessages;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Wrap up conversation' }));
|
||||
this.toastMessages = new ToastMessages(page);
|
||||
}
|
||||
|
||||
private get inputComment(): Locator {
|
||||
return this.root.locator('input[name="comment"]');
|
||||
}
|
||||
|
||||
private get btnConfirm(): Locator {
|
||||
return this.root.locator('role=button[name="Confirm"]');
|
||||
}
|
||||
|
||||
private get labelPDF(): Locator {
|
||||
return this.root.locator('label[for="transcript-pdf"]');
|
||||
}
|
||||
|
||||
async confirm(comment: string, downloadPDF: boolean): Promise<void> {
|
||||
await this.inputComment.fill(comment);
|
||||
if (downloadPDF) {
|
||||
await this.labelPDF.click();
|
||||
}
|
||||
await this.btnConfirm.click();
|
||||
await this.waitForDismissal();
|
||||
await this.toastMessages.dismissToast('success');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Locator, Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class OmnichannelConfirmRemoveChat extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog'));
|
||||
}
|
||||
|
||||
private get btnConfirmRemove(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Delete' });
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
await this.btnConfirmRemove.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { expect } from '../../../utils/test';
|
||||
import { Listbox } from '../listbox';
|
||||
|
||||
export class OmnichannelContactReviewModal extends Modal {
|
||||
readonly listbox: Listbox;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Review contact' }), page);
|
||||
this.listbox = new Listbox(page);
|
||||
}
|
||||
|
||||
private getFieldByName(name: string): Locator {
|
||||
return this.root.getByLabel(name, { exact: true });
|
||||
}
|
||||
|
||||
async solveConfirmation(field: string, value: string) {
|
||||
await this.getFieldByName(field).click();
|
||||
await this.listbox.selectOption(value);
|
||||
const responsePromise = this.page?.waitForResponse('**/api/v1/omnichannel/contacts.conflicts');
|
||||
await this.save();
|
||||
const responseListener = await responsePromise;
|
||||
expect(responseListener?.status()).toBe(200);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class OmnichannelDeleteContactModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Delete Contact' }));
|
||||
}
|
||||
|
||||
get inputConfirmation(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Confirm Contact Removal' });
|
||||
}
|
||||
|
||||
get btnDelete(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Delete' });
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.btnDelete.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class OmnichannelOnHoldModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Place chat On-Hold' }));
|
||||
}
|
||||
|
||||
private get btnPlaceChatOnHold(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Place chat On-Hold' });
|
||||
}
|
||||
|
||||
async confirm(): Promise<void> {
|
||||
await this.btnPlaceChatOnHold.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class OmnichannelResetPrioritiesModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Reset priorities' }));
|
||||
}
|
||||
|
||||
private get btnResetConfirm() {
|
||||
return this.root.getByRole('button', { name: 'Reset' });
|
||||
}
|
||||
|
||||
async reset() {
|
||||
await this.btnResetConfirm.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class OmnichannelReturnToQueueModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Return back to the Queue' }));
|
||||
}
|
||||
|
||||
private get btnReturnToQueueConfirm() {
|
||||
return this.root.getByRole('button', { name: 'Confirm' });
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
await this.btnReturnToQueueConfirm.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { Listbox } from '../listbox';
|
||||
|
||||
export class OmnichannelTransferChatModal extends Modal {
|
||||
private readonly listbox: Listbox;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Forward chat' }));
|
||||
this.listbox = new Listbox(page);
|
||||
}
|
||||
|
||||
get inputComment(): Locator {
|
||||
return this.root.locator('textarea[name="comment"]');
|
||||
}
|
||||
|
||||
get inputForwardDepartment(): Locator {
|
||||
return this.root.getByLabel('Forward to department').getByRole('textbox');
|
||||
}
|
||||
|
||||
get inputForwardUser(): Locator {
|
||||
return this.root.getByLabel('Forward to user').getByRole('textbox');
|
||||
}
|
||||
|
||||
get btnForward(): Locator {
|
||||
return this.root.locator('role=button[name="Forward"]');
|
||||
}
|
||||
|
||||
async selectDepartment(name: string) {
|
||||
await this.inputForwardDepartment.click();
|
||||
await this.inputForwardDepartment.fill(name);
|
||||
await this.listbox.selectOption(name);
|
||||
}
|
||||
|
||||
async selectUser(name: string, id?: string) {
|
||||
await this.inputForwardUser.click();
|
||||
await this.inputForwardUser.fill(name);
|
||||
await this.listbox.selectOption(id || name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { expect } from '../../../utils/test';
|
||||
import { ToastMessages } from '../toast-messages';
|
||||
|
||||
export class ReportMessageModal extends Modal {
|
||||
readonly toastMessage: ToastMessages;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Report message' }));
|
||||
this.toastMessage = new ToastMessages(page);
|
||||
}
|
||||
|
||||
get inputReportDescription(): Locator {
|
||||
return this.root.getByRole('textbox', { name: 'Report reason' });
|
||||
}
|
||||
|
||||
private get btnSubmitReport(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Report' });
|
||||
}
|
||||
|
||||
private get btnCancelReport(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Cancel' });
|
||||
}
|
||||
|
||||
private get alertInputDescription(): Locator {
|
||||
return this.root.getByRole('alert');
|
||||
}
|
||||
|
||||
async cancelReport(): Promise<void> {
|
||||
await this.btnCancelReport.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
|
||||
async submitReport(description?: string): Promise<void> {
|
||||
if (!description) {
|
||||
await this.btnSubmitReport.click();
|
||||
await expect(this.alertInputDescription).toBeVisible();
|
||||
return;
|
||||
}
|
||||
|
||||
await this.inputReportDescription.fill(description);
|
||||
await this.btnSubmitReport.click();
|
||||
await this.waitForDismissal();
|
||||
await this.toastMessage.waitForDisplay({ type: 'success', message: 'Report has been sent' });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { LoginPage } from '../../login';
|
||||
|
||||
export class ResetE2EEPasswordModal extends Modal {
|
||||
private readonly login: LoginPage;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Reset E2EE password' }));
|
||||
this.login = new LoginPage(page);
|
||||
}
|
||||
|
||||
private get resetE2EEPasswordButton() {
|
||||
return this.root.getByRole('button', { name: 'Reset E2EE password' });
|
||||
}
|
||||
|
||||
async confirmReset() {
|
||||
await this.resetE2EEPasswordButton.click();
|
||||
await this.waitForDismissal();
|
||||
await this.login.waitForIt();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { Page } from 'playwright-core';
|
||||
|
||||
import { Modal } from './modal';
|
||||
import { ToastMessages } from '../toast-messages';
|
||||
|
||||
export class SaveE2EEPasswordModal extends Modal {
|
||||
private readonly toastMessages: ToastMessages;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Save your new E2EE password' }));
|
||||
this.toastMessages = new ToastMessages(page);
|
||||
}
|
||||
|
||||
private get password() {
|
||||
return this.root.getByLabel('Your E2EE password is:').getByRole('code');
|
||||
}
|
||||
|
||||
private get savedPasswordButton() {
|
||||
return this.root.getByRole('button', { name: 'I saved my password' });
|
||||
}
|
||||
|
||||
async getPassword() {
|
||||
return (await this.password.textContent()) ?? '';
|
||||
}
|
||||
|
||||
async confirm() {
|
||||
await this.savedPasswordButton.click();
|
||||
await this.waitForDismissal();
|
||||
await this.toastMessages.dismissToast('success');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { Modal } from './modal';
|
||||
|
||||
export class VoiceCallsUpsellModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Team voice calls' }));
|
||||
}
|
||||
}
|
||||
|
||||
export class OmnichannelUpsellDepartmentsModal extends Modal {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('dialog', { name: 'Departments' }));
|
||||
}
|
||||
}
|
||||
335
apps/meteor/tests/e2e/page-objects/fragments/navbar.ts
Normal file
335
apps/meteor/tests/e2e/page-objects/fragments/navbar.ts
Normal file
@@ -0,0 +1,335 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import {
|
||||
EditStatusModal,
|
||||
CreateNewChannelModal,
|
||||
CreateNewDiscussionModal,
|
||||
CreateNewDMModal,
|
||||
CreateNewTeamModal,
|
||||
CreateNewCategoryModal,
|
||||
} from './modals';
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
export class Navbar {
|
||||
readonly modals: {
|
||||
'Channel': CreateNewChannelModal;
|
||||
'Team': CreateNewTeamModal;
|
||||
'Discussion': CreateNewDiscussionModal;
|
||||
'Direct message': CreateNewDMModal;
|
||||
'editStatus': EditStatusModal;
|
||||
'Category': CreateNewCategoryModal;
|
||||
};
|
||||
|
||||
constructor(private readonly root: Page) {
|
||||
this.modals = {
|
||||
'Channel': new CreateNewChannelModal(root),
|
||||
'Team': new CreateNewTeamModal(root),
|
||||
'Discussion': new CreateNewDiscussionModal(root),
|
||||
'Direct message': new CreateNewDMModal(root),
|
||||
'editStatus': new EditStatusModal(root),
|
||||
'Category': new CreateNewCategoryModal(root),
|
||||
};
|
||||
}
|
||||
|
||||
get btnVoiceAndOmnichannel(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Voice and omnichannel' });
|
||||
}
|
||||
|
||||
get groupHistoryNavigation(): Locator {
|
||||
return this.root.getByRole('group', { name: 'History navigation' });
|
||||
}
|
||||
|
||||
get pagesGroup(): Locator {
|
||||
return this.root.getByRole('group', { name: 'Pages and actions' });
|
||||
}
|
||||
|
||||
get omnichannelGroup(): Locator {
|
||||
return this.root.getByRole('group', { name: 'Omnichannel' });
|
||||
}
|
||||
|
||||
get btnContactCenter(): Locator {
|
||||
return this.omnichannelGroup.getByRole('button', { name: 'Contact Center' });
|
||||
}
|
||||
|
||||
get voiceCallGroup(): Locator {
|
||||
return this.root.getByRole('group', { name: 'Voice call' });
|
||||
}
|
||||
|
||||
get btnNewVoiceCall(): Locator {
|
||||
return this.voiceCallGroup.getByRole('button', { name: 'New voice call' });
|
||||
}
|
||||
|
||||
get btnSwitchOmnichannelStatus(): Locator {
|
||||
return this.omnichannelGroup.getByRole('button', { name: 'answer chats' });
|
||||
}
|
||||
|
||||
get btnHome(): Locator {
|
||||
return this.pagesGroup.getByRole('button', { name: 'Home' });
|
||||
}
|
||||
|
||||
get btnDirectory(): Locator {
|
||||
return this.pagesGroup.getByRole('button', { name: 'Directory' });
|
||||
}
|
||||
|
||||
get btnMarketplace(): Locator {
|
||||
return this.pagesGroup.getByRole('button', { name: 'Marketplace' });
|
||||
}
|
||||
|
||||
get btnMenuPages(): Locator {
|
||||
return this.pagesGroup.getByRole('button', { name: 'Pages' });
|
||||
}
|
||||
|
||||
get btnDisplay(): Locator {
|
||||
return this.pagesGroup.getByRole('button', { name: 'Display' });
|
||||
}
|
||||
|
||||
get menuDisplay(): Locator {
|
||||
return this.root.getByRole('menu', { name: 'Display' });
|
||||
}
|
||||
|
||||
get groupDisplay(): Locator {
|
||||
return this.menuDisplay.getByRole('group', { name: 'Display' });
|
||||
}
|
||||
|
||||
getDisplayMenuItem(mode: 'Extended' | 'Medium' | 'Condensed' | 'Avatars'): Locator {
|
||||
return this.groupDisplay.getByRole('menuitemcheckbox', { name: mode });
|
||||
}
|
||||
|
||||
get groupSortBy(): Locator {
|
||||
return this.menuDisplay.getByRole('group', { name: 'Sort by' });
|
||||
}
|
||||
|
||||
getSortMenuItem(mode: 'Activity' | 'Name'): Locator {
|
||||
return this.groupSortBy.getByRole('menuitemcheckbox', { name: mode });
|
||||
}
|
||||
|
||||
get groupGroupBy(): Locator {
|
||||
return this.menuDisplay.getByRole('group', { name: 'Group by' });
|
||||
}
|
||||
|
||||
getGroupByMenuItem(mode: 'Unread' | 'Favorites' | 'Types'): Locator {
|
||||
return this.groupGroupBy.getByRole('menuitemcheckbox', { name: mode });
|
||||
}
|
||||
|
||||
get btnCreateNew(): Locator {
|
||||
return this.pagesGroup.getByRole('button', { name: 'Create new' });
|
||||
}
|
||||
|
||||
get createNewMenu(): Locator {
|
||||
return this.root.getByRole('menu', { name: 'Create new' });
|
||||
}
|
||||
|
||||
get navbarSearchSection(): Locator {
|
||||
return this.root.getByRole('search');
|
||||
}
|
||||
|
||||
get searchInput(): Locator {
|
||||
return this.navbarSearchSection.getByRole('combobox');
|
||||
}
|
||||
|
||||
get searchList(): Locator {
|
||||
return this.navbarSearchSection.getByRole('listbox', { name: 'Channels' });
|
||||
}
|
||||
|
||||
get workspaceGroup(): Locator {
|
||||
return this.root.getByRole('group', { name: 'Workspace and user preferences' });
|
||||
}
|
||||
|
||||
get btnManageWorkspace(): Locator {
|
||||
return this.workspaceGroup.getByRole('button', { name: 'Manage' });
|
||||
}
|
||||
|
||||
async openManageMenuItem(name: 'Workspace' | 'Omnichannel'): Promise<void> {
|
||||
await this.btnManageWorkspace.click();
|
||||
await this.root.getByRole('menu', { name: 'Manage' }).getByRole('menuitem', { name }).click();
|
||||
}
|
||||
|
||||
get btnUserMenu(): Locator {
|
||||
return this.workspaceGroup.getByRole('button', { name: 'User menu' });
|
||||
}
|
||||
|
||||
get userMenu(): Locator {
|
||||
return this.root.getByRole('menu', { name: 'User menu' });
|
||||
}
|
||||
|
||||
get btnLogout(): Locator {
|
||||
return this.userMenu.getByRole('menuitemcheckbox', { name: 'Logout' });
|
||||
}
|
||||
|
||||
get btnCustomStatus(): Locator {
|
||||
return this.userMenu.getByRole('menuitemcheckbox', { name: 'Custom...' });
|
||||
}
|
||||
|
||||
getUserProfileMenuOption(name: string): Locator {
|
||||
return this.userMenu.getByRole('menuitemcheckbox', { name });
|
||||
}
|
||||
|
||||
createNewMenuItem(name: 'Direct message' | 'Discussion' | 'Channel' | 'Team' | 'Outbound message' | 'Category'): Locator {
|
||||
return this.createNewMenu.getByRole('menuitem', { name });
|
||||
}
|
||||
|
||||
async openCreate(name: 'Direct message' | 'Discussion' | 'Channel' | 'Team' | 'Category'): Promise<void> {
|
||||
await this.btnCreateNew.click();
|
||||
await this.createNewMenuItem(name).click();
|
||||
}
|
||||
|
||||
async openCreateCategory(): Promise<void> {
|
||||
await this.btnCreateNew.click();
|
||||
await this.createNewMenu.getByRole('menuitem', { name: 'Category', exact: true }).click();
|
||||
}
|
||||
|
||||
async logout(): Promise<void> {
|
||||
await this.btnUserMenu.click();
|
||||
return this.btnLogout.click();
|
||||
}
|
||||
|
||||
btnSidebarToggler(closeSidebar?: boolean): Locator {
|
||||
return this.root.getByRole('button', { name: closeSidebar ? 'Close sidebar' : 'Open sidebar' });
|
||||
}
|
||||
|
||||
async openAdminPanel(): Promise<void> {
|
||||
await this.btnManageWorkspace.click();
|
||||
await this.root.getByRole('menuitem', { name: 'Workspace' }).click();
|
||||
await this.root.waitForURL(/\/admin/);
|
||||
}
|
||||
|
||||
async typeSearch(name: string): Promise<void> {
|
||||
return this.searchInput.fill(name);
|
||||
}
|
||||
|
||||
async waitForChannel(): Promise<void> {
|
||||
await this.root.locator('role=main').waitFor();
|
||||
await this.root.locator('role=main >> role=heading[level=1]').waitFor();
|
||||
const messageList = this.root.getByRole('main').getByRole('list', { name: 'Message list', exact: true });
|
||||
await messageList.waitFor();
|
||||
|
||||
await expect(messageList).not.toHaveAttribute('aria-busy', 'true');
|
||||
}
|
||||
|
||||
getSearchRoomByName(name: string): Locator {
|
||||
return this.searchList.getByRole('option', { name }).filter({ has: this.root.getByText(name, { exact: true }) });
|
||||
}
|
||||
|
||||
getSearchItemBadge(name: string): Locator {
|
||||
return this.getSearchRoomByName(name).getByRole('status', { name: 'unread' });
|
||||
}
|
||||
|
||||
async openChat(name: string): Promise<void> {
|
||||
await this.typeSearch(name);
|
||||
await this.getSearchRoomByName(name).waitFor();
|
||||
await this.getSearchRoomByName(name).click();
|
||||
await this.waitForChannel();
|
||||
}
|
||||
|
||||
async setDisplayMode(mode: 'Extended' | 'Medium' | 'Condensed'): Promise<void> {
|
||||
await this.btnDisplay.click();
|
||||
await this.menuDisplay.getByRole('menuitemcheckbox', { name: mode }).click();
|
||||
await this.root.keyboard.press('Escape');
|
||||
}
|
||||
|
||||
async createNew(
|
||||
type: 'Channel' | 'Team',
|
||||
name: string,
|
||||
options?: { private?: boolean; encrypted?: boolean; readOnly?: boolean; federated?: boolean; members?: string[] },
|
||||
): Promise<void> {
|
||||
await this.openCreate(type);
|
||||
await this.modals[type].inputName.fill(name);
|
||||
|
||||
if (options?.private === false) {
|
||||
await this.modals[type].checkboxPrivate.click();
|
||||
}
|
||||
|
||||
if (options?.members && options.members.length > 0) {
|
||||
await Promise.all(options.members.map((member) => this.modals[type].addMember(member)));
|
||||
}
|
||||
|
||||
if (options && ('encrypted' in options || 'readOnly' in options || 'federated' in options)) {
|
||||
await this.modals[type].advancedSettingsAccordion.click();
|
||||
}
|
||||
if (options?.encrypted) {
|
||||
await this.modals[type].checkboxEncrypted.click();
|
||||
}
|
||||
if (options?.readOnly) {
|
||||
await this.modals[type].checkboxReadOnly.click();
|
||||
}
|
||||
if (options?.federated) {
|
||||
await this.modals[type].checkboxFederated.click();
|
||||
}
|
||||
|
||||
await this.modals[type].btnCreate.click();
|
||||
}
|
||||
|
||||
async createNewDM(username: string): Promise<void> {
|
||||
await this.openCreate('Direct message');
|
||||
await this.modals['Direct message'].inviteUserToDM(username);
|
||||
await this.modals['Direct message'].btnCreate.click();
|
||||
}
|
||||
|
||||
async createNewCategory(name: string): Promise<void> {
|
||||
await this.openCreate('Category');
|
||||
await this.modals.Category.inputName.fill(name);
|
||||
await this.modals.Category.create();
|
||||
}
|
||||
|
||||
async createNewDiscussion(parentRoom: string, name: string, message?: string): Promise<void> {
|
||||
await this.openCreate('Discussion');
|
||||
await this.modals.Discussion.inputParentRoom.click();
|
||||
await this.modals.Discussion.inputParentRoom.pressSequentially(parentRoom);
|
||||
await this.modals.Discussion.getParentRoomListItem(parentRoom).click();
|
||||
await this.modals.Discussion.inputName.fill(name);
|
||||
message && (await this.modals.Discussion.inputMessage.fill(message));
|
||||
await this.modals.Discussion.btnCreate.click();
|
||||
}
|
||||
|
||||
async createEncryptedChannel(name: string): Promise<void> {
|
||||
await this.createNew('Channel', name, { encrypted: true });
|
||||
}
|
||||
|
||||
async changeUserStatus(status: 'online' | 'away' | 'busy' | 'invisible' | 'offline' | string): Promise<void> {
|
||||
await this.btnUserMenu.click();
|
||||
await this.getUserProfileMenuOption(status).click();
|
||||
}
|
||||
|
||||
async changeUserCustomStatus(text?: string): Promise<void> {
|
||||
await this.openEditStatusModal();
|
||||
await this.modals.editStatus.changeStatusMessage(text);
|
||||
}
|
||||
|
||||
get editStatusModal(): EditStatusModal {
|
||||
return this.modals.editStatus;
|
||||
}
|
||||
|
||||
async openEditStatusModal(): Promise<void> {
|
||||
await this.btnUserMenu.click();
|
||||
await this.btnCustomStatus.click();
|
||||
}
|
||||
|
||||
async changeUserCustomStatusWithExpiration(options: {
|
||||
message?: string;
|
||||
statusType?: string;
|
||||
duration: string;
|
||||
customDate?: string;
|
||||
customTime?: string;
|
||||
}): Promise<void> {
|
||||
await this.openEditStatusModal();
|
||||
await this.modals.editStatus.setStatusWithExpiration(options);
|
||||
}
|
||||
|
||||
async switchOmnichannelStatus(status: 'offline' | 'online') {
|
||||
const toggleButton = this.btnSwitchOmnichannelStatus;
|
||||
await expect(toggleButton).toBeVisible();
|
||||
|
||||
const expectedTitle = status === 'offline' ? 'Turn on answer chats' : 'Turn off answer chats';
|
||||
|
||||
const currentStatus = await toggleButton.getAttribute('title');
|
||||
if (currentStatus !== expectedTitle) {
|
||||
await toggleButton.click();
|
||||
}
|
||||
|
||||
await expect(toggleButton).toHaveAttribute('title', expectedTitle);
|
||||
}
|
||||
|
||||
getUserStatusBadge(status: 'online' | 'away' | 'busy' | 'offline'): Locator {
|
||||
return this.btnUserMenu.locator(`svg[class*="${status}"]`);
|
||||
}
|
||||
}
|
||||
15
apps/meteor/tests/e2e/page-objects/fragments/popup.ts
Normal file
15
apps/meteor/tests/e2e/page-objects/fragments/popup.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Locator } from 'playwright-core';
|
||||
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
export abstract class Popup {
|
||||
constructor(protected root: Locator) {}
|
||||
|
||||
waitForDisplay() {
|
||||
return expect(this.root).toBeVisible();
|
||||
}
|
||||
|
||||
waitForDismissal() {
|
||||
return expect(this.root).not.toBeVisible();
|
||||
}
|
||||
}
|
||||
42
apps/meteor/tests/e2e/page-objects/fragments/room-header.ts
Normal file
42
apps/meteor/tests/e2e/page-objects/fragments/room-header.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { MenuMoveTo } from './menu';
|
||||
import { CreateNewCategoryModal } from './modals';
|
||||
|
||||
export class RoomHeader {
|
||||
readonly menuMoveTo: MenuMoveTo;
|
||||
|
||||
readonly createCategoryModal: CreateNewCategoryModal;
|
||||
|
||||
constructor(public root: Locator) {
|
||||
this.menuMoveTo = new MenuMoveTo(this.root.page());
|
||||
this.createCategoryModal = new CreateNewCategoryModal(this.root.page());
|
||||
}
|
||||
|
||||
private get btnSelectCategory(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Move to', exact: true });
|
||||
}
|
||||
|
||||
async openCategorySelector(): Promise<void> {
|
||||
await this.btnSelectCategory.click();
|
||||
await this.menuMoveTo.waitForDisplay();
|
||||
}
|
||||
|
||||
async pickCategoryMenuItem(name: string): Promise<void> {
|
||||
await this.openCategorySelector();
|
||||
await this.menuMoveTo.selectMenuItem(name);
|
||||
}
|
||||
|
||||
async checkRoomBelongsToGroup(optionName: string): Promise<boolean> {
|
||||
await this.openCategorySelector();
|
||||
const exists = await this.menuMoveTo.getMenuItem(optionName).isVisible();
|
||||
await this.root.page().keyboard.press('Escape');
|
||||
return exists;
|
||||
}
|
||||
|
||||
async createCategory(name: string): Promise<void> {
|
||||
await this.pickCategoryMenuItem('New category');
|
||||
await this.createCategoryModal.inputName.fill(name);
|
||||
await this.createCategoryModal.create(true);
|
||||
}
|
||||
}
|
||||
256
apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts
Normal file
256
apps/meteor/tests/e2e/page-objects/fragments/sidebar.ts
Normal file
@@ -0,0 +1,256 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { MenuOptions, MenuMoveTo } from './menu';
|
||||
import { DeleteCategoryModal, ManageCategoryModal, CreateNewCategoryModal } from './modals';
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
export abstract class Sidebar {
|
||||
constructor(protected root: Locator) {}
|
||||
|
||||
get btnClose(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Close' });
|
||||
}
|
||||
|
||||
waitForDismissal() {
|
||||
return expect(this.root).not.toBeVisible();
|
||||
}
|
||||
|
||||
waitForDisplay() {
|
||||
return expect(this.root).toBeVisible();
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomSidebar extends Sidebar {
|
||||
readonly menuOptions: MenuOptions;
|
||||
|
||||
readonly menuMoveTo: MenuMoveTo;
|
||||
|
||||
readonly modals: {
|
||||
deleteCategory: DeleteCategoryModal;
|
||||
manageCategory: ManageCategoryModal;
|
||||
createCategory: CreateNewCategoryModal;
|
||||
};
|
||||
|
||||
constructor(protected page: Page) {
|
||||
super(page.getByRole('navigation', { name: 'Sidebar' }));
|
||||
this.modals = {
|
||||
deleteCategory: new DeleteCategoryModal(page),
|
||||
manageCategory: new ManageCategoryModal(page),
|
||||
createCategory: new CreateNewCategoryModal(page),
|
||||
};
|
||||
this.menuOptions = new MenuOptions(page);
|
||||
this.menuMoveTo = new MenuMoveTo(page);
|
||||
}
|
||||
|
||||
get teamCollabFilters(): Locator {
|
||||
return this.root.getByRole('tablist', { name: 'Team collaboration filters' });
|
||||
}
|
||||
|
||||
get omnichannelFilters(): Locator {
|
||||
return this.root.getByRole('tablist', { name: 'Omnichannel filters' });
|
||||
}
|
||||
|
||||
get allTeamCollabFilter(): Locator {
|
||||
return this.teamCollabFilters.getByRole('tab', { name: 'All' });
|
||||
}
|
||||
|
||||
get favoritesTeamCollabFilter(): Locator {
|
||||
return this.teamCollabFilters.getByRole('tab', { name: 'Favorites' });
|
||||
}
|
||||
|
||||
get discussionsTeamCollabFilter(): Locator {
|
||||
return this.teamCollabFilters.getByRole('tab', { name: 'Discussions' });
|
||||
}
|
||||
|
||||
// TODO: fix this filter, workaround due to virtuoso
|
||||
get topChannelList(): Locator {
|
||||
return this.root.getByTestId('virtuoso-top-item-list');
|
||||
}
|
||||
|
||||
get channelsList(): Locator {
|
||||
// TODO: fix this filter, workaround due to virtuoso
|
||||
// return this.sidebar.getByRole('list', { name: 'Channels' }).filter({ has: this.page.getByRole('listitem') });
|
||||
return this.root.getByTestId('virtuoso-item-list');
|
||||
}
|
||||
|
||||
getSidebarItemByName(name: string) {
|
||||
return this.channelsList.getByRole('link', { name }).filter({ has: this.page.getByText(name, { exact: true }) });
|
||||
}
|
||||
|
||||
getFilterItemByName(name: string): Locator {
|
||||
return this.root.getByRole('button', { name }).filter({ has: this.page.getByText(name, { exact: true }) });
|
||||
}
|
||||
|
||||
getSidebarListItem(name: string): Locator {
|
||||
return this.root.getByRole('listitem').filter({ has: this.page.getByText(name, { exact: true }) });
|
||||
}
|
||||
|
||||
get firstCollapser(): Locator {
|
||||
return this.topChannelList.getByRole('group').first().getByRole('button').first();
|
||||
}
|
||||
|
||||
get teamsCollapser(): Locator {
|
||||
return this.root.getByRole('group', { name: 'Collapse Teams' }).first();
|
||||
}
|
||||
|
||||
get channelsCollapser(): Locator {
|
||||
return this.channelsList.getByRole('group', { name: 'Collapse Channels' });
|
||||
}
|
||||
|
||||
get directMessagesCollapser(): Locator {
|
||||
return this.channelsList.getByRole('group', { name: 'Collapse Direct messages' });
|
||||
}
|
||||
|
||||
get firstChannelFromList(): Locator {
|
||||
return this.channelsList.getByRole('listitem').first();
|
||||
}
|
||||
|
||||
async markItemAsUnread(item: Locator): Promise<void> {
|
||||
await item.hover();
|
||||
await item.focus();
|
||||
await item.getByRole('button', { name: 'Options', exact: true }).click();
|
||||
await this.page.getByRole('menuitem', { name: 'Mark Unread' }).click();
|
||||
}
|
||||
|
||||
getCollapseGroupByName(name: string): Locator {
|
||||
return this.root.getByRole('button').filter({ has: this.page.getByRole('heading', { name, exact: true }) });
|
||||
}
|
||||
|
||||
getItemUnreadBadge(item: Locator): Locator {
|
||||
return item.getByRole('status', { name: 'unread' });
|
||||
}
|
||||
|
||||
getBadgeIndicator(name: string, title: string): Locator {
|
||||
return this.getSidebarItemByName(name).getByTitle(title);
|
||||
}
|
||||
|
||||
async selectPriority(name: string, priority: string) {
|
||||
const sidebarItem = this.getSidebarItemByName(name);
|
||||
await sidebarItem.hover();
|
||||
await sidebarItem.focus();
|
||||
await sidebarItem.getByRole('button', { name: 'Options', exact: true }).click();
|
||||
await this.page.getByRole('menuitem', { name: priority }).click();
|
||||
}
|
||||
|
||||
getSidebarListItemByName(name: string): Locator {
|
||||
return this.channelsList.getByRole('listitem').filter({ has: this.getSidebarItemByName(name) });
|
||||
}
|
||||
|
||||
getCategoryCollapser(name: string): Locator {
|
||||
return this.root.getByRole('group', { name: `Collapse ${name}`, exact: true }).first();
|
||||
}
|
||||
|
||||
getCategoryKebab(name: string): Locator {
|
||||
return this.getCategoryCollapser(name).getByRole('button', { name: 'Options', exact: true });
|
||||
}
|
||||
|
||||
async openCategoryMenu(name: string): Promise<void> {
|
||||
await this.getCategoryCollapser(name).hover();
|
||||
await this.getCategoryKebab(name).click();
|
||||
}
|
||||
|
||||
async renameCategory(name: string, newName: string): Promise<void> {
|
||||
await this.openCategoryMenu(name);
|
||||
await this.page.getByRole('menuitemcheckbox', { name: 'Manage', exact: true }).click();
|
||||
await this.modals.manageCategory.rename(newName);
|
||||
}
|
||||
|
||||
async deleteCategory(name: string): Promise<void> {
|
||||
await this.openCategoryMenu(name);
|
||||
await this.page.getByRole('menuitemcheckbox', { name: 'Delete', exact: true }).click();
|
||||
await this.modals.deleteCategory.delete();
|
||||
}
|
||||
|
||||
get moveToOption(): Locator {
|
||||
return this.page.getByRole('menuitem', { name: 'Move to', exact: true });
|
||||
}
|
||||
|
||||
async openRoomMenu(name: string): Promise<void> {
|
||||
const item = this.getSidebarItemByName(name);
|
||||
await item.hover();
|
||||
await item.focus();
|
||||
await item.getByRole('button', { name: 'Options', exact: true }).click();
|
||||
await this.menuOptions.waitForDisplay();
|
||||
}
|
||||
|
||||
/** The open submenu keeps the first Escape, so the kebab menu itself needs a second one. */
|
||||
async closeRoomMenu(): Promise<void> {
|
||||
await this.page.keyboard.press('Escape');
|
||||
await this.page.keyboard.press('Escape');
|
||||
await expect(this.moveToOption).toBeHidden();
|
||||
}
|
||||
|
||||
/** Move a room into a custom category (or to "Favorites") through the kebab "Move to ▸" submenu. */
|
||||
async moveRoomToCategory(roomName: string, categoryName: string): Promise<void> {
|
||||
await this.openRoomMoveToSubmenu(roomName);
|
||||
await this.menuMoveTo.selectMenuItem(categoryName);
|
||||
}
|
||||
|
||||
async removeRoomFromCategory(roomName: string, categoryName: string): Promise<void> {
|
||||
await this.openRoomMoveToSubmenu(roomName);
|
||||
await this.menuMoveTo.selectMenuItem(`Remove from ${categoryName}`);
|
||||
}
|
||||
|
||||
async createCategoryFromRoom(roomName: string, name: string): Promise<void> {
|
||||
await this.openRoomMoveToSubmenu(roomName);
|
||||
await this.menuMoveTo.selectMenuItem('New category');
|
||||
await this.modals.createCategory.inputName.fill(name);
|
||||
await this.modals.createCategory.create(true);
|
||||
}
|
||||
|
||||
async moveRoomToFavorites(roomName: string): Promise<void> {
|
||||
await this.openRoomMoveToSubmenu(roomName);
|
||||
await this.menuMoveTo.selectMenuItem('Favorites');
|
||||
}
|
||||
|
||||
async removeRoomFromFavorites(roomName: string): Promise<void> {
|
||||
await this.openRoomMoveToSubmenu(roomName);
|
||||
await this.menuMoveTo.selectMenuItem('Remove from Favorites');
|
||||
}
|
||||
|
||||
async openRoomMoveToSubmenu(roomName: string): Promise<void> {
|
||||
await this.openRoomMenu(roomName);
|
||||
await this.menuOptions.selectMenuItem('Move to', true);
|
||||
await this.menuMoveTo.waitForDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
export class AdminSidebar extends Sidebar {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('navigation', { name: 'Administration' }));
|
||||
}
|
||||
|
||||
get linkEmoji() {
|
||||
return this.root.getByRole('link', { name: 'Emoji' });
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.btnClose.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class AccountSidebar extends Sidebar {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('navigation', { name: 'Account' }));
|
||||
}
|
||||
|
||||
get linkSecurity(): Locator {
|
||||
return this.root.getByRole('link', { name: 'Security' });
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
await this.btnClose.click();
|
||||
await this.waitForDismissal();
|
||||
}
|
||||
}
|
||||
|
||||
export class OmnichannelSidebar extends Sidebar {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('navigation', { name: 'Omnichannel' }));
|
||||
}
|
||||
|
||||
getSidebarLinkByName(name: string): Locator {
|
||||
return this.root.getByRole('link', { name, exact: true });
|
||||
}
|
||||
}
|
||||
61
apps/meteor/tests/e2e/page-objects/fragments/sidepanel.ts
Normal file
61
apps/meteor/tests/e2e/page-objects/fragments/sidepanel.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
export class Sidepanel {
|
||||
private readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
get sidepanel(): Locator {
|
||||
return this.page.getByRole('tabpanel', { name: 'Side panel' });
|
||||
}
|
||||
|
||||
get sidepanelList(): Locator {
|
||||
return this.sidepanel.getByRole('list', { name: 'Channels' });
|
||||
}
|
||||
|
||||
get firstChannelFromList(): Locator {
|
||||
return this.sidepanelList.getByRole('listitem').first();
|
||||
}
|
||||
|
||||
get unreadCheckbox(): Locator {
|
||||
return this.sidepanel.getByRole('heading').getByRole('checkbox', { name: 'Unread' });
|
||||
}
|
||||
|
||||
get unreadToggleLabel(): Locator {
|
||||
return this.sidepanel.getByRole('heading').locator('label', { hasText: 'Unread' });
|
||||
}
|
||||
|
||||
get sidepanelBackButton(): Locator {
|
||||
return this.sidepanel.getByRole('button', { name: 'Back' });
|
||||
}
|
||||
|
||||
getSidepanelHeader(name: string): Locator {
|
||||
return this.sidepanel.getByRole('heading', { name, exact: true });
|
||||
}
|
||||
|
||||
getTeamItemByName(name: string): Locator {
|
||||
return this.sidepanelList
|
||||
.getByRole('link')
|
||||
.filter({ hasText: name })
|
||||
.filter({ hasNot: this.page.getByRole('button', { name }) });
|
||||
}
|
||||
|
||||
getMainRoomByName(name: string): Locator {
|
||||
return this.getTeamItemByName(name);
|
||||
}
|
||||
|
||||
getItemByName(name: string): Locator {
|
||||
return this.sidepanelList.getByRole('link', { name });
|
||||
}
|
||||
|
||||
getSidepanelItem(name: string, subtitle?: string): Locator {
|
||||
const regex = new RegExp(`${name}.*${subtitle}`);
|
||||
return this.sidepanelList.getByRole('link', { name: regex });
|
||||
}
|
||||
|
||||
getItemUnreadBadge(item: Locator): Locator {
|
||||
return item.getByRole('status', { name: 'unread' });
|
||||
}
|
||||
}
|
||||
43
apps/meteor/tests/e2e/page-objects/fragments/table.ts
Normal file
43
apps/meteor/tests/e2e/page-objects/fragments/table.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
export class Table {
|
||||
constructor(protected root: Locator) {}
|
||||
|
||||
/**
|
||||
* @param fallback also satisfies the wait, for tables the page may legitimately
|
||||
* replace with something else — an empty state, most commonly.
|
||||
*/
|
||||
waitForDisplay(fallback?: Locator) {
|
||||
return expect(fallback ? this.root.or(fallback) : this.root).toBeVisible();
|
||||
}
|
||||
|
||||
findRowByName(name: string): Locator {
|
||||
return this.root.getByRole('row').filter({
|
||||
has: this.root.page().getByText(name, { exact: true }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class DevicesTable extends Table {
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('table', { name: 'Devices' }));
|
||||
}
|
||||
|
||||
getRowsForUsername(username: string): Locator {
|
||||
return this.root.getByRole('row').getByRole('cell', { name: username, exact: true });
|
||||
}
|
||||
|
||||
getDeviceRowById(deviceId: string): Locator {
|
||||
return this.findRowByName(deviceId);
|
||||
}
|
||||
|
||||
getColumnHeaderByName(name: string): Locator {
|
||||
return this.root.getByRole('cell', { name, exact: true });
|
||||
}
|
||||
|
||||
async orderByLastLogin() {
|
||||
await this.getColumnHeaderByName('Last login').click();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
export class ToastMessages {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
private readonly toastByType = {
|
||||
success: this.page.locator('.rcx-toastbar--success'),
|
||||
error: this.page.locator('.rcx-toastbar--error'),
|
||||
};
|
||||
|
||||
async dismissToast(type: 'success' | 'error' = 'success') {
|
||||
await this.toastByType[type].last().getByRole('button', { name: 'Dismiss alert' }).click();
|
||||
await this.page.mouse.move(0, 0);
|
||||
}
|
||||
|
||||
waitForDisplay({ type, message }: { type: 'success' | 'error'; message?: string } = { type: 'success' }) {
|
||||
if (message) {
|
||||
return expect(
|
||||
this.toastByType[type].last().locator(`[role=${type === 'error' ? '"alert"' : '"status"'}]`, {
|
||||
hasText: message,
|
||||
}),
|
||||
).toBeVisible();
|
||||
}
|
||||
|
||||
return expect(this.toastByType[type].last()).toBeVisible();
|
||||
}
|
||||
}
|
||||
212
apps/meteor/tests/e2e/page-objects/fragments/toolbar.ts
Normal file
212
apps/meteor/tests/e2e/page-objects/fragments/toolbar.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { MenuOptions } from './menu';
|
||||
import { OmnichannelCloseChatModal, OmnichannelOnHoldModal } from './modals';
|
||||
|
||||
export abstract class Toolbar {
|
||||
constructor(protected root: Locator) {}
|
||||
|
||||
click() {
|
||||
return this.root.click();
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomToolbar extends Toolbar {
|
||||
readonly menu: MenuOptions;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('toolbar', { name: 'Primary Room actions' }));
|
||||
this.menu = new MenuOptions(page);
|
||||
}
|
||||
|
||||
get btnRoomInfo() {
|
||||
return this.root.getByRole('button', { name: 'Room Information' });
|
||||
}
|
||||
|
||||
get btnMembers() {
|
||||
return this.root.getByRole('button', { name: 'Members' });
|
||||
}
|
||||
|
||||
get btnVideoCall() {
|
||||
return this.root.getByRole('button', { name: 'Video Call' });
|
||||
}
|
||||
|
||||
get btnVoiceCall() {
|
||||
return this.root.getByRole('button', { name: 'Voice call' });
|
||||
}
|
||||
|
||||
get btnUserInfo(): Locator {
|
||||
return this.root.getByRole('button', { name: 'User Info' });
|
||||
}
|
||||
|
||||
get btnDiscussion(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Discussions' });
|
||||
}
|
||||
|
||||
get btnThreads(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Threads' });
|
||||
}
|
||||
|
||||
get btnFiles(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Files' });
|
||||
}
|
||||
|
||||
get btnMoreOptions(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Options' });
|
||||
}
|
||||
|
||||
get btnSearchMessages(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Search Messages' });
|
||||
}
|
||||
|
||||
get btnDisableE2EEncryption(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Disable E2E encryption' });
|
||||
}
|
||||
|
||||
get menuItemExportMessages(): Locator {
|
||||
return this.menu.getMenuItem('Export messages');
|
||||
}
|
||||
|
||||
get menuItemMentions(): Locator {
|
||||
return this.menu.getMenuItem('Mentions');
|
||||
}
|
||||
|
||||
get menuItemStarredMessages(): Locator {
|
||||
return this.menu.getMenuItem('Starred Messages');
|
||||
}
|
||||
|
||||
get menuItemPinnedMessages(): Locator {
|
||||
return this.menu.getMenuItem('Pinned Messages');
|
||||
}
|
||||
|
||||
get menuItemPruneMessages(): Locator {
|
||||
return this.menu.getMenuItem('Prune Messages');
|
||||
}
|
||||
|
||||
get menuItemAutoTranslate(): Locator {
|
||||
return this.menu.getMenuItem('Auto-Translate');
|
||||
}
|
||||
|
||||
get menuItemNotificationsPreferences(): Locator {
|
||||
return this.menu.getMenuItem('Notifications Preferences');
|
||||
}
|
||||
|
||||
get menuItemDisableE2EEncryption(): Locator {
|
||||
return this.menu.getMenuItem('Disable E2E encryption');
|
||||
}
|
||||
|
||||
get menuItemEnableE2EEncryption(): Locator {
|
||||
return this.menu.getMenuItem('Enable E2E encryption');
|
||||
}
|
||||
|
||||
get menuItemFiles(): Locator {
|
||||
return this.menu.getMenuItem('Files');
|
||||
}
|
||||
|
||||
async openRoomInfo() {
|
||||
await this.btnRoomInfo.click();
|
||||
}
|
||||
|
||||
async openMembersTab() {
|
||||
await this.btnMembers.click();
|
||||
}
|
||||
|
||||
async openUserInfo() {
|
||||
await this.btnUserInfo.click();
|
||||
}
|
||||
|
||||
async openMoreOptions() {
|
||||
await this.btnMoreOptions.click();
|
||||
}
|
||||
|
||||
private get btnTeamChannels(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Team Channels' });
|
||||
}
|
||||
|
||||
async openTeamChannels() {
|
||||
await this.btnTeamChannels.click();
|
||||
}
|
||||
|
||||
async waitFor() {
|
||||
await this.root.waitFor({ state: 'visible' });
|
||||
}
|
||||
}
|
||||
|
||||
export class TeamToolbar extends RoomToolbar {
|
||||
private get menuItemTeamMembers() {
|
||||
return this.menu.getMenuItem('Teams Members');
|
||||
}
|
||||
|
||||
private get btnTeamInfo() {
|
||||
return this.root.getByRole('button', { name: 'Team info' });
|
||||
}
|
||||
|
||||
async openTeamMembers() {
|
||||
await this.menuItemTeamMembers.click();
|
||||
}
|
||||
|
||||
async openTeamInfo() {
|
||||
await this.btnTeamInfo.click();
|
||||
}
|
||||
}
|
||||
|
||||
export class EncryptedRoomToolbar extends RoomToolbar {}
|
||||
|
||||
export class OmnichannelRoomToolbar extends RoomToolbar {
|
||||
private get btnContactInfo(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Contact Information' });
|
||||
}
|
||||
|
||||
private get btnCannedResponses(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Canned Responses' });
|
||||
}
|
||||
|
||||
async openContactInfo() {
|
||||
await this.btnContactInfo.click();
|
||||
}
|
||||
|
||||
async openCannedResponses() {
|
||||
await this.btnCannedResponses.click();
|
||||
}
|
||||
}
|
||||
|
||||
export class OmnichannelQuickActionsRoomToolbar extends Toolbar {
|
||||
private closeChatModal: OmnichannelCloseChatModal;
|
||||
|
||||
private onHoldModal: OmnichannelOnHoldModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('toolbar', { name: 'Omnichannel Quick Actions' }));
|
||||
this.closeChatModal = new OmnichannelCloseChatModal(page);
|
||||
this.onHoldModal = new OmnichannelOnHoldModal(page);
|
||||
}
|
||||
|
||||
get btnOnHold(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Place chat On-Hold' });
|
||||
}
|
||||
|
||||
get btnForwardChat(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Forward chat' });
|
||||
}
|
||||
|
||||
get btnEndConversation(): Locator {
|
||||
return this.root.getByRole('button', { name: 'End conversation' });
|
||||
}
|
||||
|
||||
/**
|
||||
* FIXME: This `clickCount` seems a hack for a bad implementation
|
||||
*/
|
||||
async placeChatOnHold() {
|
||||
await this.btnOnHold.click({ clickCount: 2 });
|
||||
await this.onHoldModal.confirm();
|
||||
}
|
||||
|
||||
async forwardChat() {
|
||||
await this.btnForwardChat.click();
|
||||
}
|
||||
|
||||
async closeChat({ comment = 'any_comment', downloadPDF = false } = {}): Promise<void> {
|
||||
await this.btnEndConversation.click();
|
||||
await this.closeChatModal.confirm(comment, downloadPDF);
|
||||
}
|
||||
}
|
||||
31
apps/meteor/tests/e2e/page-objects/fragments/user-card.ts
Normal file
31
apps/meteor/tests/e2e/page-objects/fragments/user-card.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
export class UserCard {
|
||||
readonly root: Locator;
|
||||
|
||||
constructor(protected page: Page) {
|
||||
this.root = page.getByRole('dialog', { name: 'User card', exact: true });
|
||||
}
|
||||
|
||||
waitForDisplay() {
|
||||
return expect(this.root).toBeVisible();
|
||||
}
|
||||
|
||||
waitForDismissal() {
|
||||
return expect(this.root).not.toBeVisible();
|
||||
}
|
||||
|
||||
get btnSeeFullProfile(): Locator {
|
||||
return this.root.getByRole('button', { name: 'See full profile', exact: true });
|
||||
}
|
||||
|
||||
get imgUserCard(): Locator {
|
||||
return this.root.locator('img');
|
||||
}
|
||||
|
||||
async openUserInfo() {
|
||||
await this.btnSeeFullProfile.click();
|
||||
}
|
||||
}
|
||||
399
apps/meteor/tests/e2e/page-objects/fragments/voice-calls.ts
Normal file
399
apps/meteor/tests/e2e/page-objects/fragments/voice-calls.ts
Normal file
@@ -0,0 +1,399 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '../../utils/test';
|
||||
|
||||
const timerToSeconds = (time: string): number => {
|
||||
const [seconds, minutes, hours = '00'] = time.split(':').reverse();
|
||||
if (!seconds || !minutes) {
|
||||
throw new Error('Voice call timer has an invalid time format');
|
||||
}
|
||||
return Number(hours) * 3600 + Number(minutes) * 60 + Number(seconds);
|
||||
};
|
||||
|
||||
export class VoiceCallControls {
|
||||
private readonly _controls: Locator;
|
||||
|
||||
constructor(controls: Locator) {
|
||||
this._controls = controls;
|
||||
}
|
||||
|
||||
get call(): Locator {
|
||||
return this._controls.getByRole('button', { name: 'Call', exact: true });
|
||||
}
|
||||
|
||||
get accept(): Locator {
|
||||
return this._controls.getByRole('button', { name: 'Accept', exact: true });
|
||||
}
|
||||
|
||||
get reject(): Locator {
|
||||
return this._controls.getByRole('button', { name: 'Reject', exact: true });
|
||||
}
|
||||
|
||||
get hangup(): Locator {
|
||||
return this._controls.getByRole('button', { name: 'End call with', exact: false });
|
||||
}
|
||||
|
||||
get cancel(): Locator {
|
||||
return this._controls.getByRole('button', { name: 'Cancel', exact: true });
|
||||
}
|
||||
|
||||
get dialpad(): Locator {
|
||||
return this._controls.getByRole('button', { name: /Dialpad/i });
|
||||
}
|
||||
|
||||
get mute(): Locator {
|
||||
return this._controls.getByRole('button', { name: /Mute/i });
|
||||
}
|
||||
|
||||
get hold(): Locator {
|
||||
return this._controls.getByRole('button', { name: /Hold|Resume/i });
|
||||
}
|
||||
|
||||
get transfer(): Locator {
|
||||
return this._controls.getByRole('button', { name: 'Forward', exact: true });
|
||||
}
|
||||
|
||||
get shareScreen(): Locator {
|
||||
return this._controls.getByRole('button', { name: /Share screen|Stop sharing screen/i });
|
||||
}
|
||||
|
||||
get toggleChat(): Locator {
|
||||
return this._controls.getByRole('button', { name: /Hide chat|Show chat/i });
|
||||
}
|
||||
|
||||
get directMessage(): Locator {
|
||||
return this._controls.getByRole('button', { name: /Direct message/i });
|
||||
}
|
||||
|
||||
get popout(): Locator {
|
||||
return this._controls.getByRole('button', { name: /Open in new window|Return to main window/i });
|
||||
}
|
||||
}
|
||||
|
||||
export class TransferModal {
|
||||
private readonly root: Locator;
|
||||
|
||||
private readonly page: Page;
|
||||
|
||||
constructor(page: Page, root: Locator) {
|
||||
this.page = page;
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
get content(): Locator {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
get input(): Locator {
|
||||
return this.root.getByRole('textbox');
|
||||
}
|
||||
|
||||
get hangUpAndTransfer(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Hang up and transfer call', exact: true });
|
||||
}
|
||||
|
||||
async transferCall(username: string): Promise<void> {
|
||||
// await this.input.click();
|
||||
await this.input.fill(username);
|
||||
// Options have to be gotten from the page since the Options are rendered on a portal
|
||||
await this.page.getByRole('option', { name: username }).getByRole('figure').click();
|
||||
await expect(this.content).toContainText(username);
|
||||
await this.hangUpAndTransfer.click();
|
||||
}
|
||||
}
|
||||
|
||||
export class Widget {
|
||||
private readonly root: Locator;
|
||||
|
||||
private readonly callControls: VoiceCallControls;
|
||||
|
||||
private readonly headerControls: VoiceCallControls;
|
||||
|
||||
private readonly transferModal: TransferModal;
|
||||
|
||||
constructor(private readonly page: Page) {
|
||||
this.transferModal = new TransferModal(page, page.getByRole('dialog', { name: 'Transfer call' }));
|
||||
this.root = page.getByRole('dialog', { name: 'Voice call', exact: false });
|
||||
this.callControls = new VoiceCallControls(this.root.getByRole('group'));
|
||||
this.headerControls = new VoiceCallControls(this.root.getByRole('banner'));
|
||||
}
|
||||
|
||||
get content(): Locator {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
get controls(): VoiceCallControls {
|
||||
return this.callControls;
|
||||
}
|
||||
|
||||
get timer(): Locator {
|
||||
return this.root.getByRole('time');
|
||||
}
|
||||
|
||||
get btnShowCallHere(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Show call here' });
|
||||
}
|
||||
|
||||
async showCallHere(): Promise<void> {
|
||||
await this.btnShowCallHere.click();
|
||||
await expect(this.btnShowCallHere).not.toBeVisible();
|
||||
}
|
||||
|
||||
async getTimerContentInSeconds() {
|
||||
const text = await this.timer.innerText();
|
||||
return timerToSeconds(text);
|
||||
}
|
||||
|
||||
async initiateCall(username?: string): Promise<void> {
|
||||
if (username) {
|
||||
await this.root.getByRole('textbox', { name: 'Enter username or number' }).fill(username);
|
||||
await this.page.getByRole('listbox').getByRole('option', { name: username }).click();
|
||||
}
|
||||
await this.callControls.call.click();
|
||||
await expect(this.callControls.cancel).toBeVisible();
|
||||
}
|
||||
|
||||
async acceptCall(): Promise<void> {
|
||||
await this.callControls.accept.click();
|
||||
await expect(this.callControls.hangup).toBeVisible();
|
||||
}
|
||||
|
||||
async hangup(): Promise<void> {
|
||||
await this.callControls.hangup.click();
|
||||
await expect(this.content).not.toBeVisible();
|
||||
}
|
||||
|
||||
async reject(): Promise<void> {
|
||||
await this.callControls.reject.click();
|
||||
await expect(this.content).not.toBeVisible();
|
||||
}
|
||||
|
||||
async goToDm(): Promise<void> {
|
||||
await this.headerControls.directMessage.click();
|
||||
await expect(this.content).not.toBeVisible();
|
||||
}
|
||||
|
||||
async transferCall(username: string): Promise<void> {
|
||||
await this.callControls.transfer.click();
|
||||
await expect(this.transferModal.content).toBeVisible();
|
||||
await this.transferModal.transferCall(username);
|
||||
await expect(this.transferModal.content).not.toBeVisible();
|
||||
await expect(this.content).not.toBeVisible();
|
||||
}
|
||||
|
||||
async muteSelf(): Promise<void> {
|
||||
await this.callControls.mute.click();
|
||||
await expect(this.callControls.mute).toHaveAttribute('title', 'Unmute');
|
||||
}
|
||||
|
||||
async unmuteSelf(): Promise<void> {
|
||||
await this.callControls.mute.click();
|
||||
await expect(this.callControls.mute).toHaveAttribute('title', 'Mute');
|
||||
}
|
||||
|
||||
async holdSelf(): Promise<void> {
|
||||
await this.callControls.hold.click();
|
||||
await expect(this.callControls.hold).toHaveAttribute('title', 'Resume');
|
||||
}
|
||||
|
||||
async resumeSelf(): Promise<void> {
|
||||
await this.callControls.hold.click();
|
||||
await expect(this.callControls.hold).toHaveAttribute('title', 'Hold');
|
||||
}
|
||||
|
||||
async openDialpad(): Promise<void> {
|
||||
await this.callControls.dialpad.click();
|
||||
await expect(this.callControls.dialpad).toHaveAttribute('title', 'Close dialpad');
|
||||
}
|
||||
|
||||
async closeDialpad(): Promise<void> {
|
||||
await this.callControls.dialpad.click();
|
||||
await expect(this.callControls.dialpad).toHaveAttribute('title', 'Open dialpad');
|
||||
}
|
||||
|
||||
get allScreenShareVideos(): Locator {
|
||||
return this.root.locator('video');
|
||||
}
|
||||
|
||||
async shareScreen(): Promise<void> {
|
||||
await this.callControls.shareScreen.click();
|
||||
await expect(this.callControls.shareScreen).toHaveAttribute('title', 'Stop sharing screen');
|
||||
}
|
||||
|
||||
async stopSharing(): Promise<void> {
|
||||
await this.callControls.shareScreen.click();
|
||||
await expect(this.callControls.shareScreen).toHaveAttribute('title', 'Share screen');
|
||||
}
|
||||
|
||||
async openPopout(): Promise<void> {
|
||||
await this.headerControls.popout.click();
|
||||
}
|
||||
|
||||
peerCard(username: string): Locator {
|
||||
return this.root.getByText(username);
|
||||
}
|
||||
}
|
||||
|
||||
export class RoomSection {
|
||||
private readonly root: Locator;
|
||||
|
||||
public readonly callControls: VoiceCallControls;
|
||||
|
||||
public readonly otherControls: VoiceCallControls;
|
||||
|
||||
constructor(root: Locator) {
|
||||
this.root = root;
|
||||
// Right now there are 2 control groups in the room view, with no discrimination between them
|
||||
this.callControls = new VoiceCallControls(this.root.getByRole('group').first());
|
||||
this.otherControls = new VoiceCallControls(this.root.getByRole('group').last());
|
||||
}
|
||||
|
||||
get content(): Locator {
|
||||
return this.root;
|
||||
}
|
||||
|
||||
get controls(): VoiceCallControls {
|
||||
return this.callControls;
|
||||
}
|
||||
|
||||
get timer(): Locator {
|
||||
return this.root.getByRole('time');
|
||||
}
|
||||
|
||||
get allScreenShareVideos(): Locator {
|
||||
return this.root.locator('video');
|
||||
}
|
||||
|
||||
get btnShowCallHere(): Locator {
|
||||
return this.root.getByRole('button', { name: 'Show call here' });
|
||||
}
|
||||
|
||||
async showCallHere(): Promise<void> {
|
||||
await this.btnShowCallHere.click();
|
||||
await expect(this.btnShowCallHere).not.toBeVisible();
|
||||
}
|
||||
|
||||
async getTimerContentInSeconds() {
|
||||
const text = await this.timer.innerText();
|
||||
return timerToSeconds(text);
|
||||
}
|
||||
|
||||
async hangup(): Promise<void> {
|
||||
await this.callControls.hangup.click();
|
||||
await expect(this.content).not.toBeVisible();
|
||||
}
|
||||
|
||||
async hideChat(): Promise<void> {
|
||||
await expect(this.otherControls.toggleChat).toHaveAccessibleName(/Hide chat/i);
|
||||
await this.otherControls.toggleChat.click();
|
||||
await expect(this.otherControls.toggleChat).toHaveAccessibleName(/Show chat/i);
|
||||
}
|
||||
|
||||
async showChat(): Promise<void> {
|
||||
await expect(this.otherControls.toggleChat).toHaveAccessibleName(/Show chat/i);
|
||||
await this.otherControls.toggleChat.click();
|
||||
await expect(this.otherControls.toggleChat).toHaveAccessibleName(/Hide chat/i);
|
||||
}
|
||||
|
||||
async muteSelf(): Promise<void> {
|
||||
await this.callControls.mute.click();
|
||||
await expect(this.callControls.mute).toHaveAttribute('title', 'Unmute');
|
||||
}
|
||||
|
||||
async unmuteSelf(): Promise<void> {
|
||||
await this.callControls.mute.click();
|
||||
await expect(this.callControls.mute).toHaveAttribute('title', 'Mute');
|
||||
}
|
||||
|
||||
async holdSelf(): Promise<void> {
|
||||
await this.callControls.hold.click();
|
||||
await expect(this.callControls.hold).toHaveAttribute('title', 'Resume');
|
||||
}
|
||||
|
||||
async resumeSelf(): Promise<void> {
|
||||
await this.callControls.hold.click();
|
||||
await expect(this.callControls.hold).toHaveAttribute('title', 'Hold');
|
||||
}
|
||||
|
||||
async shareScreen(): Promise<void> {
|
||||
await this.callControls.shareScreen.click();
|
||||
await expect(this.callControls.shareScreen).toHaveAttribute('title', 'Stop sharing screen');
|
||||
}
|
||||
|
||||
async stopSharing(): Promise<void> {
|
||||
await this.callControls.shareScreen.click();
|
||||
await expect(this.callControls.shareScreen).toHaveAttribute('title', 'Share screen');
|
||||
}
|
||||
|
||||
peerCard(username: string): Locator {
|
||||
return this.root.getByText(username);
|
||||
}
|
||||
|
||||
async openPopout(): Promise<void> {
|
||||
await this.otherControls.popout.click();
|
||||
}
|
||||
}
|
||||
|
||||
export class PopoutPage extends RoomSection {
|
||||
private readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page.getByRole('main', { name: 'Voice call' }));
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
override async hangup(): Promise<void> {
|
||||
const pageClosed = new Promise((resolve) => this.page.on('close', () => resolve(true)));
|
||||
await this.callControls.hangup.click();
|
||||
await expect(pageClosed).resolves.toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
export class VoiceCalls {
|
||||
public readonly widget: Widget;
|
||||
|
||||
public readonly roomSection: RoomSection;
|
||||
|
||||
public popoutPage: PopoutPage | undefined;
|
||||
|
||||
private readonly page: Page;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.widget = new Widget(page);
|
||||
this.roomSection = new RoomSection(page.getByRole('region', { name: 'Voice call' }));
|
||||
}
|
||||
|
||||
get popout(): PopoutPage {
|
||||
if (!this.popoutPage) {
|
||||
throw new Error('Voice call - Popout is not open (call openPopout(Room|Widget) first)');
|
||||
}
|
||||
return this.popoutPage;
|
||||
}
|
||||
|
||||
// Open the popout from the room section
|
||||
async openPopoutRoom(): Promise<void> {
|
||||
const [popout] = await Promise.all([this.page.waitForEvent('popup'), this.roomSection.openPopout()]);
|
||||
if (!popout) {
|
||||
throw new Error('Voice call - Failed to open popout');
|
||||
}
|
||||
await popout.waitForLoadState();
|
||||
popout.on('close', () => {
|
||||
this.popoutPage = undefined;
|
||||
});
|
||||
this.popoutPage = new PopoutPage(popout);
|
||||
}
|
||||
|
||||
// Open the popout from the widget
|
||||
async openPopoutWidget(): Promise<void> {
|
||||
const [popout] = await Promise.all([this.page.waitForEvent('popup'), this.widget.openPopout()]);
|
||||
if (!popout) {
|
||||
throw new Error('Voice call - Failed to open popout');
|
||||
}
|
||||
await popout.waitForLoadState();
|
||||
popout.on('close', () => {
|
||||
this.popoutPage = undefined;
|
||||
});
|
||||
this.popoutPage = new PopoutPage(popout);
|
||||
}
|
||||
}
|
||||
237
apps/meteor/tests/e2e/page-objects/home-channel.ts
Normal file
237
apps/meteor/tests/e2e/page-objects/home-channel.ts
Normal file
@@ -0,0 +1,237 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import {
|
||||
HomeContent,
|
||||
Navbar,
|
||||
Sidepanel,
|
||||
RoomSidebar,
|
||||
ToastMessages,
|
||||
RoomComposer,
|
||||
ThreadComposer,
|
||||
MembersFlexTab,
|
||||
ChannelsFlexTab,
|
||||
NotificationPreferencesFlexTab,
|
||||
AutoTranslateFlexTab,
|
||||
ExportMessagesFlexTab,
|
||||
PruneMessagesFlexTab,
|
||||
SearchMessagesFlexTab,
|
||||
RoomInfoFlexTab,
|
||||
ThreadsFlexTab,
|
||||
EditRoomFlexTab,
|
||||
UserInfoFlexTab,
|
||||
FilesFlexTab,
|
||||
} from './fragments';
|
||||
import { RoomToolbar } from './fragments/toolbar';
|
||||
import { UserCard } from './fragments/user-card';
|
||||
import { VoiceCalls } from './fragments/voice-calls';
|
||||
import { RoutedPage } from './routed-page';
|
||||
|
||||
export class HomeChannel extends RoutedPage {
|
||||
protected readonly route: string = '/home';
|
||||
|
||||
readonly content: HomeContent;
|
||||
|
||||
readonly sidebar: RoomSidebar;
|
||||
|
||||
readonly sidepanel: Sidepanel;
|
||||
|
||||
readonly navbar: Navbar;
|
||||
|
||||
readonly userCard: UserCard;
|
||||
|
||||
private _tabs: {
|
||||
members: MembersFlexTab;
|
||||
userInfo: UserInfoFlexTab;
|
||||
room: RoomInfoFlexTab;
|
||||
editRoom: EditRoomFlexTab;
|
||||
channels: ChannelsFlexTab;
|
||||
notificationPreferences: NotificationPreferencesFlexTab;
|
||||
autoTranslate: AutoTranslateFlexTab;
|
||||
exportMessages: ExportMessagesFlexTab;
|
||||
pruneMessages: PruneMessagesFlexTab;
|
||||
searchMessages: SearchMessagesFlexTab;
|
||||
threads: ThreadsFlexTab;
|
||||
files: FilesFlexTab;
|
||||
};
|
||||
|
||||
readonly roomToolbar: RoomToolbar;
|
||||
|
||||
readonly voiceCalls: VoiceCalls;
|
||||
|
||||
readonly toastMessage: ToastMessages;
|
||||
|
||||
readonly composer: RoomComposer;
|
||||
|
||||
readonly threadComposer: ThreadComposer;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.content = new HomeContent(page);
|
||||
this.sidebar = new RoomSidebar(page);
|
||||
this.sidepanel = new Sidepanel(page);
|
||||
this.navbar = new Navbar(page);
|
||||
this.userCard = new UserCard(page);
|
||||
this._tabs = {
|
||||
members: new MembersFlexTab(page),
|
||||
userInfo: new UserInfoFlexTab(page),
|
||||
room: new RoomInfoFlexTab(page.getByRole('dialog', { name: 'Channel info' })),
|
||||
editRoom: new EditRoomFlexTab(page.getByRole('dialog', { name: 'Edit channel' })),
|
||||
channels: new ChannelsFlexTab(page),
|
||||
notificationPreferences: new NotificationPreferencesFlexTab(page),
|
||||
autoTranslate: new AutoTranslateFlexTab(page),
|
||||
exportMessages: new ExportMessagesFlexTab(page),
|
||||
pruneMessages: new PruneMessagesFlexTab(page),
|
||||
searchMessages: new SearchMessagesFlexTab(page),
|
||||
threads: new ThreadsFlexTab(page),
|
||||
files: new FilesFlexTab(page),
|
||||
};
|
||||
this.roomToolbar = new RoomToolbar(page);
|
||||
this.voiceCalls = new VoiceCalls(page);
|
||||
this.toastMessage = new ToastMessages(page);
|
||||
this.composer = new RoomComposer(page);
|
||||
this.threadComposer = new ThreadComposer(page);
|
||||
}
|
||||
|
||||
get tabs() {
|
||||
return this._tabs;
|
||||
}
|
||||
|
||||
async waitForReady(): Promise<void> {
|
||||
await this.homepageHeader.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
async gotoChannel(name: string): Promise<void> {
|
||||
await this.navigateTo(`/channel/${name}`, () => this.content.waitForChannel());
|
||||
}
|
||||
|
||||
async gotoGroup(name: string): Promise<void> {
|
||||
await this.navigateTo(`/group/${name}`, () => this.content.waitForChannel());
|
||||
}
|
||||
|
||||
/** Opens a channel scrolled to a message. Pass `isThread` when the message lives in a thread. */
|
||||
async gotoChannelMessage(name: string, messageId: string, isThread = false): Promise<void> {
|
||||
await this.navigateTo(`/channel/${name}?msg=${messageId}`, async () => {
|
||||
await this.content.waitForChannel();
|
||||
|
||||
if (isThread) {
|
||||
await this.content.waitForThread();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Opens a thread by the id of the message that started it. */
|
||||
async gotoChannelThread(name: string, threadMessageId: string): Promise<void> {
|
||||
await this.navigateTo(`/channel/${name}/thread/${threadMessageId}`, async () => {
|
||||
await this.content.waitForChannel();
|
||||
await this.content.waitForThread();
|
||||
});
|
||||
}
|
||||
|
||||
/** Opens a channel with the Prune Messages panel open. */
|
||||
async gotoChannelCleanHistory(name: string): Promise<void> {
|
||||
await this.navigateTo(`/channel/${name}/clean-history`, this.tabs.pruneMessages.root);
|
||||
}
|
||||
|
||||
get btnContextualbarClose(): Locator {
|
||||
return this.page.locator('[data-qa="ContextualbarActionClose"]');
|
||||
}
|
||||
|
||||
get userCardToolbar(): Locator {
|
||||
return this.page.locator('[role=toolbar][aria-label="User card actions"]');
|
||||
}
|
||||
|
||||
getRoomHeaderFavoriteBtn(isEnterprise: boolean): Locator {
|
||||
return isEnterprise ? this.btnCategorySelector : this.roomHeaderFavoriteBtn;
|
||||
}
|
||||
|
||||
private get roomHeaderFavoriteBtn(): Locator {
|
||||
return this.page.getByRole('main').getByRole('button', { name: 'Favorite' });
|
||||
}
|
||||
|
||||
private get btnCategorySelector(): Locator {
|
||||
return this.page.getByRole('main').getByRole('button', { name: 'Move to', exact: true });
|
||||
}
|
||||
|
||||
// TODO: this button should name open room info or something instead of the room name
|
||||
getBtnOpenRoomInfo(roomName: string): Locator {
|
||||
return this.page.getByRole('main').getByRole('button', { name: roomName, exact: true });
|
||||
}
|
||||
|
||||
get roomHeaderToolbar(): Locator {
|
||||
return this.page.locator('[role=toolbar][aria-label="Primary Room actions"]');
|
||||
}
|
||||
|
||||
get markUnread(): Locator {
|
||||
return this.page.locator('role=menuitem[name="Mark Unread"]');
|
||||
}
|
||||
|
||||
get dialogEnterE2EEPassword(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Enter E2EE password' });
|
||||
}
|
||||
|
||||
get dialogSaveE2EEPassword(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Save your new E2EE password' });
|
||||
}
|
||||
|
||||
get btnRoomSaveE2EEPassword(): Locator {
|
||||
return this.page.getByRole('main').getByRole('button', { name: 'Save E2EE password' });
|
||||
}
|
||||
|
||||
get btnRoomEnterE2EEPassword(): Locator {
|
||||
return this.page.getByRole('main').getByRole('button', { name: 'Enter your E2E password' });
|
||||
}
|
||||
|
||||
get btnSavedMyPassword(): Locator {
|
||||
return this.dialogSaveE2EEPassword.getByRole('button', { name: 'I saved my password' });
|
||||
}
|
||||
|
||||
get bannerSaveEncryptionPassword(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save your new E2EE password' });
|
||||
}
|
||||
|
||||
get bannerEnterE2EEPassword(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Enter your E2E password' });
|
||||
}
|
||||
|
||||
get audioRecorder(): Locator {
|
||||
return this.page.getByRole('group', { name: 'Audio recorder', exact: true });
|
||||
}
|
||||
|
||||
get homepageHeader(): Locator {
|
||||
return this.page.locator('main').getByRole('heading', { name: 'Home' });
|
||||
}
|
||||
|
||||
get dialogEmojiPicker(): Locator {
|
||||
return this.page.getByRole('dialog', { name: 'Emoji picker' });
|
||||
}
|
||||
|
||||
get scrollerEmojiPicker(): Locator {
|
||||
return this.dialogEmojiPicker.locator('[data-overlayscrollbars]');
|
||||
}
|
||||
|
||||
get btnJoinChannel(): Locator {
|
||||
return this.content.btnJoinChannel;
|
||||
}
|
||||
|
||||
getEmojiPickerTabByName(name: string) {
|
||||
return this.dialogEmojiPicker.locator(`role=tablist >> role=tab[name="${name}"]`);
|
||||
}
|
||||
|
||||
getEmojiByName(name: string) {
|
||||
return this.dialogEmojiPicker.locator(`role=tabpanel >> [data-emoji="${name}"]`);
|
||||
}
|
||||
|
||||
async pickEmoji(emoji: string, section = 'Smileys & People') {
|
||||
await this.composer.btnEmoji.click();
|
||||
await this.getEmojiPickerTabByName(section).click();
|
||||
await this.getEmojiByName(emoji).click();
|
||||
}
|
||||
|
||||
async waitForHome(): Promise<void> {
|
||||
await this.waitForReady();
|
||||
}
|
||||
|
||||
async waitForRoomLoad(): Promise<void> {
|
||||
await this.roomHeaderToolbar.waitFor({ state: 'visible' });
|
||||
}
|
||||
}
|
||||
11
apps/meteor/tests/e2e/page-objects/home-discussion.ts
Normal file
11
apps/meteor/tests/e2e/page-objects/home-discussion.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { RoomInfoFlexTab } from './fragments';
|
||||
import { HomeChannel } from './home-channel';
|
||||
|
||||
export class HomeDiscussion extends HomeChannel {
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.tabs.room = new RoomInfoFlexTab(page.getByRole('dialog', { name: 'Discussion Info' }));
|
||||
}
|
||||
}
|
||||
52
apps/meteor/tests/e2e/page-objects/home-omnichannel.ts
Normal file
52
apps/meteor/tests/e2e/page-objects/home-omnichannel.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { HomeOmnichannelContent, OmnichannelQuickActionsRoomToolbar, OmnichannelRoomToolbar, OmnichannelSidebar } from './fragments';
|
||||
import { OmnichannelEditRoomFlexTab } from './fragments/flextabs';
|
||||
import { OmnichannelRoomInfoFlexTab } from './fragments/flextabs/room-info-flextab';
|
||||
import { HomeChannel } from './home-channel';
|
||||
import {
|
||||
OmnichannelCannedResponses,
|
||||
OmnichannelTranscript,
|
||||
OmnichannelContactCenterContacts,
|
||||
OmnichannelContactCenterChats,
|
||||
} from './omnichannel';
|
||||
|
||||
export class HomeOmnichannel extends HomeChannel {
|
||||
readonly omnisidenav: OmnichannelSidebar;
|
||||
|
||||
readonly transcript: OmnichannelTranscript;
|
||||
|
||||
readonly cannedResponses: OmnichannelCannedResponses;
|
||||
|
||||
readonly contacts: OmnichannelContactCenterContacts;
|
||||
|
||||
readonly chats: OmnichannelContactCenterChats;
|
||||
|
||||
readonly roomInfo: OmnichannelRoomInfoFlexTab;
|
||||
|
||||
readonly editRoomInfo: OmnichannelEditRoomFlexTab;
|
||||
|
||||
readonly quickActionsRoomToolbar: OmnichannelQuickActionsRoomToolbar;
|
||||
|
||||
override readonly content: HomeOmnichannelContent;
|
||||
|
||||
override readonly roomToolbar: OmnichannelRoomToolbar;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.omnisidenav = new OmnichannelSidebar(page);
|
||||
this.transcript = new OmnichannelTranscript(page);
|
||||
this.cannedResponses = new OmnichannelCannedResponses(page);
|
||||
this.contacts = new OmnichannelContactCenterContacts(page);
|
||||
this.chats = new OmnichannelContactCenterChats(page);
|
||||
this.roomInfo = new OmnichannelRoomInfoFlexTab(page);
|
||||
this.quickActionsRoomToolbar = new OmnichannelQuickActionsRoomToolbar(page);
|
||||
this.content = new HomeOmnichannelContent(page);
|
||||
this.roomToolbar = new OmnichannelRoomToolbar(page);
|
||||
this.editRoomInfo = new OmnichannelEditRoomFlexTab(page);
|
||||
}
|
||||
|
||||
get btnContactInfo(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Contact Information' });
|
||||
}
|
||||
}
|
||||
21
apps/meteor/tests/e2e/page-objects/home-team.ts
Normal file
21
apps/meteor/tests/e2e/page-objects/home-team.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
|
||||
import { EditTeamFlexTab, TeamInfoFlexTab, TeamToolbar } from './fragments';
|
||||
import { HomeChannel } from './home-channel';
|
||||
|
||||
export class HomeTeam extends HomeChannel {
|
||||
readonly headerToolbar: TeamToolbar;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.headerToolbar = new TeamToolbar(page);
|
||||
}
|
||||
|
||||
override get tabs() {
|
||||
return {
|
||||
...super.tabs,
|
||||
room: new TeamInfoFlexTab(this.page),
|
||||
editRoom: new EditTeamFlexTab(this.page),
|
||||
};
|
||||
}
|
||||
}
|
||||
26
apps/meteor/tests/e2e/page-objects/index.ts
Normal file
26
apps/meteor/tests/e2e/page-objects/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export * from './account-accessibility';
|
||||
export * from './account-feature-preview';
|
||||
export * from './account-omnichannel';
|
||||
export * from './account-profile';
|
||||
export * from './account-tokens';
|
||||
export * from './account-security';
|
||||
export * from './admin-email-inboxes';
|
||||
export * from './admin-rooms';
|
||||
export * from './admin-users';
|
||||
export * from './admin-settings';
|
||||
export * from './admin-engagement';
|
||||
export * from './admin-info';
|
||||
export * from './admin-integrations';
|
||||
export * from './admin-permissions';
|
||||
export * from './admin-third-party-login';
|
||||
export * from './admin-emojis';
|
||||
export * from './admin-imports';
|
||||
export * from './admin-mailer';
|
||||
export * from './admin-moderation';
|
||||
export * from './admin';
|
||||
export * from './auth';
|
||||
export * from './home-channel';
|
||||
export * from './home-discussion';
|
||||
export * from './home-team';
|
||||
export * from './home-omnichannel';
|
||||
export * from './marketplace';
|
||||
61
apps/meteor/tests/e2e/page-objects/login.ts
Normal file
61
apps/meteor/tests/e2e/page-objects/login.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Page } from '@playwright/test';
|
||||
import type { IUser } from '@rocket.chat/core-typings';
|
||||
import { MongoClient } from 'mongodb';
|
||||
|
||||
import * as constants from '../config/constants';
|
||||
import type { IUserState } from '../fixtures/userStates';
|
||||
import { expect } from '../utils/test';
|
||||
|
||||
export class LoginPage {
|
||||
constructor(protected readonly page: Page) {}
|
||||
|
||||
get loginButton() {
|
||||
return this.page.getByRole('button', { name: 'Login', exact: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Ideally the previous action should ensure the user is logged out and we should just assume to be at the login page
|
||||
* */
|
||||
async waitForIt() {
|
||||
await this.loginButton.waitFor();
|
||||
}
|
||||
|
||||
/** Navigates to `url` and waits for the login screen — for routes that bounce an anonymous visitor. */
|
||||
async goto(url = '/home'): Promise<void> {
|
||||
await this.page.goto(url);
|
||||
await this.waitForIt();
|
||||
}
|
||||
|
||||
protected async waitForLogin() {
|
||||
await expect(this.loginButton).not.toBeVisible();
|
||||
await expect(this.page.getByRole('main')).toBeVisible();
|
||||
}
|
||||
|
||||
async loginByUserState(userState: IUserState, options: { except: string[] } = { except: [] }) {
|
||||
// Creates a login token for the user
|
||||
const connection = await MongoClient.connect(constants.URL_MONGODB);
|
||||
|
||||
await connection
|
||||
.db()
|
||||
.collection<IUser>('users')
|
||||
.updateOne(
|
||||
{ username: userState.data.username },
|
||||
{ $addToSet: { 'services.resume.loginTokens': { when: userState.data.loginExpire, hashedToken: userState.data.hashedToken } } },
|
||||
);
|
||||
|
||||
await connection.close();
|
||||
|
||||
const localStorageItems = userState.state.origins[0].localStorage.filter((item) => options.except.indexOf(item.name) === -1);
|
||||
|
||||
// Injects the login token to the local storage
|
||||
await this.page.evaluate((items) => {
|
||||
items.forEach(({ name, value }) => {
|
||||
window.localStorage.setItem(name, value);
|
||||
});
|
||||
|
||||
require('meteor/accounts-base').Accounts._pollStoredLoginToken();
|
||||
}, localStorageItems);
|
||||
|
||||
await this.waitForLogin();
|
||||
}
|
||||
}
|
||||
51
apps/meteor/tests/e2e/page-objects/marketplace.ts
Normal file
51
apps/meteor/tests/e2e/page-objects/marketplace.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { Locator } from '@playwright/test';
|
||||
|
||||
import { RoutedPage } from './routed-page';
|
||||
|
||||
export class Marketplace extends RoutedPage {
|
||||
protected readonly route = '/marketplace/private';
|
||||
|
||||
async waitForReady(): Promise<void> {
|
||||
await this.btnUploadPrivateApp.waitFor({ state: 'visible' });
|
||||
}
|
||||
|
||||
get btnUploadPrivateApp(): Locator {
|
||||
return this.page.locator('role=button[name="Upload private app"]');
|
||||
}
|
||||
|
||||
get btnInstallPrivateApp(): Locator {
|
||||
return this.page.locator('role=button[name="Install"]');
|
||||
}
|
||||
|
||||
get btnUploadPrivateAppFile(): Locator {
|
||||
return this.page.locator('role=button[name="Browse Files"]');
|
||||
}
|
||||
|
||||
get appStatusTag(): Locator {
|
||||
return this.page.locator('[data-qa-type="app-status-tag"]');
|
||||
}
|
||||
|
||||
get btnConfirmAppUploadModal(): Locator {
|
||||
return this.page.locator('role=button[name="Upload anyway"]');
|
||||
}
|
||||
|
||||
get lastAppRow(): Locator {
|
||||
return this.page.locator('[data-qa-type="app-row"]').last();
|
||||
}
|
||||
|
||||
get appMenu(): Locator {
|
||||
return this.page.getByTitle('More options');
|
||||
}
|
||||
|
||||
get btnEnableApp(): Locator {
|
||||
return this.page.getByRole('menuitem', { name: 'Enable' });
|
||||
}
|
||||
|
||||
get btnDisableApp(): Locator {
|
||||
return this.page.getByRole('menuitem', { name: 'Disable' });
|
||||
}
|
||||
|
||||
get btnConfirmAppUpdate(): Locator {
|
||||
return this.page.locator('role=button[name="Yes"]');
|
||||
}
|
||||
}
|
||||
19
apps/meteor/tests/e2e/page-objects/omnichannel/index.ts
Normal file
19
apps/meteor/tests/e2e/page-objects/omnichannel/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export * from './omnichannel-agents';
|
||||
export * from './omnichannel-departments';
|
||||
export * from './omnichannel-canned-responses';
|
||||
export * from './omnichannel-contact-center';
|
||||
export * from './omnichannel-livechat';
|
||||
export * from './omnichannel-livechat-appearance';
|
||||
export * from './omnichannel-livechat-embedded';
|
||||
export * from './omnichannel-manager';
|
||||
export * from './omnichannel-custom-fields';
|
||||
export * from './omnichannel-units';
|
||||
export * from './omnichannel-monitors';
|
||||
export * from './omnichannel-settings';
|
||||
export * from './omnichannel-business-hours';
|
||||
export * from './omnichannel-tags';
|
||||
export * from './omnichannel-transcript';
|
||||
export * from './omnichannel-triggers';
|
||||
export * from './omnichannel-priorities';
|
||||
export * from './omnichannel-reports';
|
||||
export * from './omnichannel-sla-policies';
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { Locator, Page } from '@playwright/test';
|
||||
|
||||
import { expect } from '../../utils/test';
|
||||
import { OmnichannelSidebar, ToastMessages } from '../fragments';
|
||||
import { ConfirmDeleteModal } from '../fragments/modals';
|
||||
import { Table } from '../fragments/table';
|
||||
import { RoutedPage } from '../routed-page';
|
||||
|
||||
export enum OmnichannelSectionsHref {
|
||||
currentChats = '/omnichannel/current/chats',
|
||||
currentContacts = '/omnichannel/current/contacts',
|
||||
agents = '/omnichannel/agents',
|
||||
managers = '/omnichannel/managers',
|
||||
monitors = '/omnichannel/monitors',
|
||||
departments = '/omnichannel/departments',
|
||||
units = '/omnichannel/units',
|
||||
customFields = '/omnichannel/customfields',
|
||||
cannedResponses = '/omnichannel/canned-responses',
|
||||
triggers = '/omnichannel/triggers',
|
||||
tags = '/omnichannel/tags',
|
||||
priorities = '/omnichannel/priorities',
|
||||
slaPolicies = '/omnichannel/sla-policies',
|
||||
businessHours = '/omnichannel/businessHours',
|
||||
appearance = '/omnichannel/appearance',
|
||||
livechatAppearance = '/omnichannel/livechat-appearance',
|
||||
analytics = '/omnichannel/analytics',
|
||||
reports = '/omnichannel/reports',
|
||||
realtimeMonitoring = '/omnichannel/realtime-monitoring',
|
||||
securityPrivacy = '/omnichannel/security-privacy',
|
||||
}
|
||||
|
||||
/**
|
||||
* A page under `/omnichannel/*`. Subclasses declare `route` and `title` and get
|
||||
* `goto()`, `pageHeader` and `table` for free.
|
||||
*/
|
||||
export abstract class OmnichannelAdmin extends RoutedPage {
|
||||
protected abstract override readonly route: OmnichannelSectionsHref;
|
||||
|
||||
/** Text of the page's main heading. */
|
||||
protected abstract readonly title: string;
|
||||
|
||||
/** Accessible name of the page's main table, when it differs from `title`. */
|
||||
protected readonly tableName?: string;
|
||||
|
||||
protected readonly toastMessage: ToastMessages;
|
||||
|
||||
readonly sidebar: OmnichannelSidebar;
|
||||
|
||||
readonly deleteModal: ConfirmDeleteModal;
|
||||
|
||||
constructor(page: Page) {
|
||||
super(page);
|
||||
this.sidebar = new OmnichannelSidebar(page);
|
||||
this.toastMessage = new ToastMessages(page);
|
||||
this.deleteModal = new ConfirmDeleteModal(page.getByRole('dialog', { name: 'Are you sure?' }));
|
||||
}
|
||||
|
||||
get inputSearch() {
|
||||
return this.page.getByRole('main').getByRole('textbox', { name: 'Search' });
|
||||
}
|
||||
|
||||
get btnSaveChanges(): Locator {
|
||||
return this.page.getByRole('button', { name: 'Save changes' });
|
||||
}
|
||||
|
||||
getButtonByType(type: 'unit' | 'SLA policy' | 'tag' | 'trigger' | 'department' | 'custom field'): Locator {
|
||||
return this.page.locator('header').getByRole('button', { name: `Create ${type}` });
|
||||
}
|
||||
|
||||
async search(text: string) {
|
||||
await this.inputSearch.fill(text);
|
||||
}
|
||||
|
||||
async clearSearch() {
|
||||
await this.inputSearch.fill('');
|
||||
}
|
||||
|
||||
get emptyState() {
|
||||
return this.page.getByRole('group', { name: 'No results found', exact: true });
|
||||
}
|
||||
|
||||
waitForEmptyState() {
|
||||
return expect(this.emptyState).toBeVisible();
|
||||
}
|
||||
|
||||
get pageHeader(): Locator {
|
||||
return this.page.locator('main').getByRole('heading', { name: this.title, exact: true });
|
||||
}
|
||||
|
||||
get table(): Table {
|
||||
return new Table(this.page.getByRole('table', { name: this.tableName ?? this.title }));
|
||||
}
|
||||
|
||||
async waitForReady(): Promise<void> {
|
||||
await this.pageHeader.waitFor({ state: 'visible' });
|
||||
await this.table.waitForDisplay(this.emptyState);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user