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
681 lines
23 KiB
TypeScript
681 lines
23 KiB
TypeScript
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();
|
|
}
|
|
}
|