diff --git a/packages/e2e-tests/package-lock.json b/packages/e2e-tests/package-lock.json index 7e083ff08..0a27e3029 100644 --- a/packages/e2e-tests/package-lock.json +++ b/packages/e2e-tests/package-lock.json @@ -13,12 +13,14 @@ "electron-chromedriver": "23.3.13", "get-port": "^5.1.1", "jest": "^29.4.2", + "luxon": "^3.4.4", "path-browserify": "^1.0.1", "selenium-webdriver": "^4.8.0", "yargs": "^17.3.1" }, "devDependencies": { "@types/jest": "^29.2.6", + "@types/luxon": "^3.4.2", "@types/selenium-webdriver": "^4.1.10", "babel-jest": "^29.3.1", "dotenv": "16.4.5", @@ -1444,6 +1446,12 @@ "@types/node": "*" } }, + "node_modules/@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "dev": true + }, "node_modules/@types/node": { "version": "12.20.46", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.46.tgz", @@ -5213,6 +5221,14 @@ "node": ">=8" } }, + "node_modules/luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -7272,6 +7288,12 @@ "@types/node": "*" } }, + "@types/luxon": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", + "integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==", + "dev": true + }, "@types/node": { "version": "12.20.46", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.46.tgz", @@ -9960,6 +9982,11 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, + "luxon": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index b487a97f8..e22508b26 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -22,6 +22,7 @@ "devDependencies": { "@quiet/eslint-config": "^2.0.2-alpha.0", "@types/jest": "^29.2.6", + "@types/luxon": "^3.4.2", "@types/selenium-webdriver": "^4.1.10", "babel-jest": "^29.3.1", "lint-staged": "^15.2.2", @@ -38,6 +39,7 @@ "electron-chromedriver": "23.3.13", "get-port": "^5.1.1", "jest": "^29.4.2", + "luxon": "^3.4.4", "path-browserify": "^1.0.1", "selenium-webdriver": "^4.8.0", "yargs": "^17.3.1" diff --git a/packages/e2e-tests/src/enums.ts b/packages/e2e-tests/src/enums.ts index 976aea691..eb8e76216 100644 --- a/packages/e2e-tests/src/enums.ts +++ b/packages/e2e-tests/src/enums.ts @@ -7,3 +7,26 @@ export enum UploadedFileType { IMAGE = 'IMAGE', FILE = 'FILE', } + +export enum FileDownloadStatus { + QUEUED = 'QUEUED', + DOWNLOADING = 'DOWNLOADING', + DOWNLOADING_CAN_CANCEL = 'DOWNLOADING_CAN_CANCEL', + COMPLETED = 'COMPLETED', + CANCELED = 'CANCELED', + DOWNLOAD_FILE = 'DOWNLOAD_FILE', +} + +export enum SettingsModalTabName { + INVITE = 'invite', + ABOUT = 'about', + NOTIFICATIONS = 'notifications', + QR_CODE = 'qr-code', + LEAVE_COMMUNITY = 'leave-community', +} + +export enum PhotoExt { + GIF = 'gif', + JPEG = 'jpeg', + PNG = 'png', +} diff --git a/packages/e2e-tests/src/selectors.ts b/packages/e2e-tests/src/selectors.ts index 438e5c054..b0d2ec2bf 100644 --- a/packages/e2e-tests/src/selectors.ts +++ b/packages/e2e-tests/src/selectors.ts @@ -1,9 +1,10 @@ -import { By, Key, type ThenableWebDriver, type WebElement, until, error } from 'selenium-webdriver' +import { By, Key, type ThenableWebDriver, type WebElement, until } from 'selenium-webdriver' import { BuildSetup, logAndReturnError, promiseWithRetries, sleep, type BuildSetupInit } from './utils' import path from 'path' -import { UploadedFileType, X_DATA_TESTID } from './enums' +import { FileDownloadStatus, PhotoExt, SettingsModalTabName, UploadedFileType, X_DATA_TESTID } from './enums' import { MessageIds, RetryConfig } from './types' import { createLogger } from './logger' +import { DateTime } from 'luxon' const logger = createLogger('selectors') @@ -82,7 +83,12 @@ export class App { } get saveStateButton() { - return this.driver.wait(until.elementLocated(By.xpath('//div[@data-testid="save-state-button"]'))) + return this.driver.wait( + until.elementLocated(By.xpath('//div[@data-testid="save-state-button"]')), + 10_000, + `Save state button couldn't be located within timeout`, + 500 + ) } async closeUpdateModalIfPresent() { @@ -96,7 +102,12 @@ export class App { } async waitForSavedState() { - const dataSaved = this.driver.wait(until.elementLocated(By.xpath('//div[@data-is-saved="true"]'))) + const dataSaved = this.driver.wait( + until.elementLocated(By.xpath('//div[@data-is-saved="true"]')), + 20_000, + `State couldn't be saved within timeout`, + 500 + ) return await dataSaved } } @@ -108,7 +119,12 @@ export class StartingLoadingPanel { } get element() { - return this.driver.wait(until.elementLocated(By.xpath('//div[@data-testid="startingPanelComponent"]'))) + return this.driver.wait( + until.elementLocated(By.xpath('//div[@data-testid="startingPanelComponent"]')), + 15_000, + `Loading panel element couldn't be located within timeout`, + 500 + ) } } @@ -119,12 +135,32 @@ export class WarningModal { this.driver = driver } + async isReady(): Promise { + await this.driver.wait( + until.elementIsVisible(this.titleElement), + 15_000, + `Warning modal couldn't be seen within timeout`, + 500 + ) + return true + } + get titleElement() { - return this.driver.wait(until.elementLocated(By.xpath('//h3[@data-testid="warningModalTitle"]'))) + return this.driver.wait( + until.elementLocated(By.xpath('//h3[@data-testid="warningModalTitle"]')), + 10_000, + `Warning modal title element couldn't be located within timeout`, + 500 + ) } async close() { - const submitButton = await this.driver.findElement(By.xpath('//button[@data-testid="warningModalSubmit"]')) + const submitButton = await this.driver.wait( + this.driver.findElement(By.xpath('//button[@data-testid="warningModalSubmit"]')), + 10_000, + `Warning modal couldn't be closed within timeout`, + 500 + ) await submitButton.click() } } @@ -136,7 +172,37 @@ export class JoiningLoadingPanel { } get element() { - return this.driver.wait(until.elementLocated(By.xpath('//div[@data-testid="joiningPanelComponent"]'))) + return this.driver.wait( + this.driver.findElement(By.xpath('//div[@data-testid="joiningPanelComponent"]')), + 15_000, + `Joining loading panel element couldn't be located within timeout`, + 500 + ) + } + + async waitForJoinToComplete() { + const panel = await this.element + await this.driver.wait( + until.elementIsVisible(panel), + 15_000, + `Loading panel element couldn't be seen within timeout`, + 500 + ) + + try { + await this.driver.wait( + until.elementIsNotVisible(panel), + 300_000, + `Loading panel element didn't disappear within timeout`, + 5_000 + ) + } catch (e) { + if (e.message.includes('stale element reference')) { + logger.warn(`Join loading panel disappeared and we couldn't get visibility information. This is fine.`) + } else { + throw e + } + } } } @@ -147,17 +213,33 @@ export class ChannelContextMenu { } async openMenu() { - const menu = this.driver.wait(until.elementLocated(By.xpath('//div[@data-testid="channelContextMenuButton"]'))) + const menu = this.driver.wait( + until.elementLocated(By.xpath('//div[@data-testid="channelContextMenuButton"]')), + 15_000, + `Channel context menu couldn't be located within timeout`, + 500 + ) await menu.click() } async openDeletionChannelModal() { - const tab = this.driver.wait(until.elementLocated(By.xpath('//div[@data-testid="contextMenuItemDelete"]'))) + const tab = this.driver.wait( + until.elementLocated(By.xpath('//div[@data-testid="contextMenuItemDelete"]')), + 15_000, + `Channel context menu channel deletion modal couldn't be located within timeout`, + 500 + ) await tab.click() } + // TODO: replace sleep async deleteChannel() { - const button = this.driver.wait(until.elementLocated(By.xpath('//button[@data-testid="deleteChannelButton"]'))) + const button = this.driver.wait( + until.elementLocated(By.xpath('//button[@data-testid="deleteChannelButton"]')), + 20_000, + `Channel deletion button couldn't be located within timeout`, + 500 + ) await button.click() await sleep(5000) } @@ -170,45 +252,89 @@ export class UserProfileContextMenu { this.driver = driver } + get menuElement() { + return this.driver.wait( + until.elementLocated(By.xpath(`//*[text()='Profile']`)), + 10_000, + `User profile menu couldn't be found within timeout`, + 500 + ) + } + + get editProfileMenuElement() { + return this.driver.wait( + until.elementLocated(By.xpath(`//*[text()='Edit profile']`)), + 10_000, + `User profile edit menu couldn't be found within timeout`, + 500 + ) + } + + async isMenuReady() { + await this.driver.wait( + until.elementIsVisible(this.menuElement), + 15_000, + `User profile menu wasn't ready within timeout`, + 500 + ) + return true + } + + async isEditProfileMenuReady() { + await this.driver.wait( + until.elementIsVisible(this.editProfileMenuElement), + 15_000, + `User profile edit menu wasn't ready within timeout`, + 500 + ) + return true + } + async openMenu() { const button = await this.driver.wait( until.elementLocated(By.xpath('//div[@data-testid="user-profile-menu-button"]')), - 20000, + 20_000, 'Context menu button not found', 500 ) - await this.driver.wait(until.elementIsVisible(button), 20000, 'Context menu button never became visible', 500) + await this.driver.wait(until.elementIsVisible(button), 20_000, 'Context menu button never became visible', 500) await button.click() + await this.isMenuReady() } async back(dataTestid: X_DATA_TESTID) { const button = await this.driver.wait( until.elementLocated(By.xpath(`//button[@data-testid="${dataTestid}"]`)), - 20000, + 20_000, `Context back button with data-testid ${dataTestid} not found`, 500 ) - logger.info('clicking back button') - // await this.driver.executeScript('arguments[0].click();', button) + await this.driver.wait( + until.elementIsVisible(button), + 5_000, + `Context back button with data-testid ${dataTestid} not visibile`, + 500 + ) await button.click() } async openEditProfileMenu() { const button = await this.driver.wait( until.elementLocated(By.xpath('//div[@data-testid="contextMenuItemEdit profile"]')), - 20000, + 20_000, 'Edit Profile button not found', 500 ) await this.driver.wait(until.elementIsVisible(button), 20000, 'Edit Profile button never became visible', 500) await button.click() + await this.isEditProfileMenuReady() } async uploadPhoto(fileName: string) { const input = await this.driver.wait( until.elementLocated(By.xpath('//input[@data-testid="user-profile-edit-photo-input"]')), - 10000, + 10_000, 'Edit Photo button not found', 500 ) @@ -229,14 +355,34 @@ export class UserProfileContextMenu { } async waitForPhoto(): Promise { - await sleep(3000) - const photoElement = await this.driver.wait(until.elementLocated(By.className('UserProfilePanel-profilePhoto'))) + const photoElement = await this.driver.wait( + until.elementLocated(By.className('UserProfileContextMenuprofilePhoto')), + 30_000, + 'Profile photo element never located', + 500 + ) return photoElement } - async getProfilePhotoSrc(): Promise { - const photoElement = await this.waitForPhoto() - return photoElement.getAttribute('src') + async getProfilePhotoSrc(ext: PhotoExt): Promise { + return await this.driver.wait( + async () => { + let i = 0 + while (i < 5) { + const photoElement = await this.waitForPhoto() + const src = await photoElement.getAttribute('src') + + if (src.includes(`image/${ext}`)) { + return src + } + i++ + } + throw new Error(`Failed to find image with data type ${ext} after 5 tries`) + }, + 15_000, + `Failed to find image with data type ${ext} within timeout`, + 500 + ) } } @@ -247,24 +393,68 @@ export class RegisterUsernameModal { } get element() { - return this.driver.wait(until.elementLocated(By.xpath("//h3[text()='Register a username']"))) + return this.driver.wait( + until.elementLocated(By.xpath("//h3[text()='Register a username']")), + 15_000, + `Username registration modal couldn't be located within timeout`, + 500 + ) } get elementUsernameTaken() { - return this.driver.wait(until.elementLocated(By.xpath("//h6[text()='Username taken']"))) + return this.driver.wait( + until.elementLocated(By.xpath("//h6[text()='Username taken']")), + 15_000, + `Username taken registration modal couldn't be located within timeout`, + 500 + ) } get error() { - return this.driver.wait(until.elementLocated(By.xpath("//p[text()='Username already taken.']"))) + return this.driver.wait( + until.elementLocated(By.xpath("//p[text()='Username already taken.']")), + 15_000, + `Username taken error modal couldn't be located within timeout`, + 500 + ) + } + + get usernameInput() { + return this.driver.wait( + this.driver.findElement(By.xpath('//input[@name="userName"]')), + 10_000, + `Username input couldn't be found within timeout`, + 500 + ) + } + + async isReady(): Promise { + await this.driver.wait( + until.elementIsVisible(this.element), + 10_000, + `Username registration modal wasn't ready within timeout`, + 500 + ) + return true + } + + async isUsernameTakenReady(): Promise { + await this.driver.wait( + until.elementIsVisible(this.elementUsernameTaken), + 10_000, + `Username taken registration modal wasn't ready within timeout`, + 500 + ) + return true } async typeUsername(username: string) { - const usernameInput = await this.driver.findElement(By.xpath('//input[@name="userName"]')) + const usernameInput = await this.usernameInput await usernameInput.sendKeys(username) } async clearInput() { - const usernameInput = await this.driver.findElement(By.xpath('//input[@name="userName"]')) + const usernameInput = await this.usernameInput if (process.platform === 'darwin') { await usernameInput.sendKeys(Key.COMMAND + 'a') await usernameInput.sendKeys(Key.DELETE) @@ -275,15 +465,26 @@ export class RegisterUsernameModal { } async submit() { - const submitButton = await this.driver.findElement(By.xpath('//button[text()="Register"]')) + const submitButton = await this.driver.wait( + this.driver.findElement(By.xpath('//button[text()="Register"]')), + 10_000, + `Username registration submit button couldn't be found within timeout`, + 500 + ) await submitButton.click() } async submitUsernameTaken() { - const submitButton = await this.driver.findElement(By.xpath('//button[text()="Continue"]')) + const submitButton = await this.driver.wait( + this.driver.findElement(By.xpath('//button[text()="Continue"]')), + 10_000, + `Username taken registration submit button couldn't be found within timeout`, + 500 + ) await submitButton.click() } } + export class JoinCommunityModal { private readonly driver: ThenableWebDriver constructor(driver: ThenableWebDriver) { @@ -291,21 +492,51 @@ export class JoinCommunityModal { } get element() { - return this.driver.wait(until.elementLocated(By.xpath("//h3[text()='Join community']"))) + return this.driver.wait( + until.elementLocated(By.xpath("//h3[text()='Join community']")), + 10_000, + `Join community modal couldn't be found within timeout`, + 500 + ) + } + + async isReady(): Promise { + await this.driver.wait( + until.elementIsVisible(this.element), + 10_000, + `Join community modal wasn't ready within timeout`, + 500 + ) + return true } async switchToCreateCommunity() { - const link = this.driver.findElement(By.linkText('create a new community')) + const link = await this.driver.wait( + this.driver.findElement(By.linkText('create a new community')), + 10_000, + `Create community button couldn't be found within timeout`, + 500 + ) await link.click() } async typeCommunityInviteLink(inviteLink: string) { - const communityNameInput = await this.driver.findElement(By.xpath('//input[@placeholder="Invite link"]')) + const communityNameInput = await this.driver.wait( + this.driver.findElement(By.xpath('//input[@placeholder="Invite link"]')), + 10_000, + `Invite link input couldn't be found within timeout`, + 500 + ) await communityNameInput.sendKeys(inviteLink) } async submit() { - const continueButton = await this.driver.findElement(By.xpath('//button[@data-testid="continue-joinCommunity"]')) + const continueButton = await this.driver.wait( + this.driver.findElement(By.xpath('//button[@data-testid="continue-joinCommunity"]')), + 10_000, + `Join community continue button couldn't be found within timeout`, + 500 + ) await continueButton.click() } } @@ -316,16 +547,41 @@ export class CreateCommunityModal { } get element() { - return this.driver.findElement(By.xpath("//h3[text()='Create your community']")) + return this.driver.wait( + this.driver.findElement(By.xpath("//h3[text()='Create your community']")), + 10_000, + `Create community modal couldn't be found within timeout`, + 500 + ) + } + + async isReady(): Promise { + await this.driver.wait( + until.elementIsVisible(await this.element), + 10_000, + `Create community modal wasn't ready within timeout`, + 500 + ) + return true } async typeCommunityName(name: string) { - const communityNameInput = await this.driver.findElement(By.xpath('//input[@placeholder="Community name"]')) + const communityNameInput = await this.driver.wait( + this.driver.findElement(By.xpath('//input[@placeholder="Community name"]')), + 10_000, + `Community name input couldn't be found within timeout`, + 500 + ) await communityNameInput.sendKeys(name) } async submit() { - const continueButton = await this.driver.findElement(By.xpath('//button[@data-testid="continue-createCommunity"]')) + const continueButton = await this.driver.wait( + this.driver.findElement(By.xpath('//button[@data-testid="continue-createCommunity"]')), + 10_000, + `Create community submit button couldn't be found within timeout`, + 500 + ) await continueButton.click() } } @@ -338,27 +594,77 @@ export class Channel { } get title() { - return this.driver.findElement(By.xpath(`//span[text()="#${this.name}}"]`)) + return this.driver.wait( + until.elementLocated(By.xpath(`//*[@data-testid='channelTitle']`)), + 10_000, + `Channel title element for ${this.name} couldn't be found within timeout`, + 500 + ) } get messagesList() { - return this.driver.findElement(By.xpath('//ul[@id="messages-scroll"]')) + return this.driver.wait( + until.elementLocated(By.xpath('//ul[@id="messages-scroll"]')), + 10_000, + `Channel message list element for ${this.name} couldn't be found within timeout`, + 500 + ) + } + + async isReady(timeoutMs = 15_000): Promise { + await this.driver.wait( + until.elementIsVisible(this.element), + timeoutMs, + `Channel ${this.name} wasn't ready within timeout`, + 500 + ) + return true + } + + async isOpen(): Promise { + const titleElement = await this.driver.wait( + until.elementIsVisible(await this.title), + 15_000, + `Channel title element for ${this.name} couldn't be seen within timeout`, + 500 + ) + return (await titleElement.getText()) === `#${this.name}` + } + + async isMessageInputReady(): Promise { + await this.driver.wait( + until.elementIsVisible(this.messageInput), + 15_000, + `Channel message input element for ${this.name} couldn't be seen within timeout`, + 500 + ) + return true } async waitForUserMessageByText(username: string, messageContent: string) { logger.info(`Waiting for user "${username}" message "${messageContent}"`) - return this.driver.wait(async () => { - const messages = await this.getUserMessages(username) - for (const element of messages) { - const text = await element.getText() - logger.info(`Potential message with text: ${text}`) - if (text.includes(messageContent)) { - logger.info(`Found message with matching text ${text}`) - return element + return this.driver.wait( + async () => { + const startTime = DateTime.utc().toMillis() + const endTime = startTime + 20_000 + while (DateTime.utc().toMillis() < endTime) { + const messages = await this.getUserMessages(username) + for (const element of messages) { + const text = await element.getText() + logger.info(`Potential message with text: ${text}`) + if (text.includes(messageContent)) { + logger.info(`Found message with matching text ${text}`) + return element + } + } + await sleep(500) } - } - throw logAndReturnError(`No message found for user ${username} and message content ${messageContent}`) - }) + throw logAndReturnError(`No message found for user ${username} and message content ${messageContent}`) + }, + 30_000, + `Message in channel ${this.name} couldn't be found within timeout`, + 500 + ) } async waitForUserMessageByFilename( @@ -367,17 +673,27 @@ export class Channel { fileType: UploadedFileType ): Promise { logger.info(`Waiting for user "${username}" message with uploaded file "${filename}"`) - return this.driver.wait(async () => { - const messages = await this.getUserMessages(username) - for (const element of messages) { - const filenameElement = await this.getUploadedFilenameElementByType(filename, fileType, element) - if (filenameElement != null) { - logger.info(`Found message with matching filename ${filename}`) - return element + return this.driver.wait( + async () => { + const startTime = DateTime.utc().toMillis() + const endTime = startTime + 30_000 + while (DateTime.utc().toMillis() < endTime) { + const messages = await this.getUserMessages(username) + for (const element of messages) { + const filenameElement = await this.getUploadedFilenameElementByType(filename, fileType, element) + if (filenameElement != null) { + logger.info(`Found message with matching filename ${filename}`) + return element + } + } + await sleep(500) } - } - throw logAndReturnError(`No message found for user ${username} and filename ${filename}`) - }) + throw logAndReturnError(`No message found for user ${username} and filename ${filename}`) + }, + 45_000, + `Message for uploaded file ${filename} in channel ${this.name} couldn't be found within timeout`, + 500 + ) } private async getUploadedFilenameElementByType( @@ -403,15 +719,19 @@ export class Channel { baseElement: WebElement ): Promise { try { - const filenameComponentElement = await await this.driver.wait( + const filenameComponentElement = await this.driver.wait( baseElement.findElement(By.xpath(`//*[@class='FileComponentfilename']`)), - 45_000 + 20_000, + `Filename parent component for uploaded file ${filename} in channel ${this.name} couldn't be found within timeout`, + 500 ) const parsedPath = path.parse(filename) // this is split because we print the message as multiple lines and contains doesn't return true when searching the full filename const filenameElement = await this.driver.wait( filenameComponentElement.findElement(By.xpath(`//h5[contains(text(), "${parsedPath.name}")]`)), - 45_000 + 15_000, + `Filename component with correct filename for uploaded file ${filename} in channel ${this.name} couldn't be found within timeout`, + 500 ) if ((await filenameElement.getText()) === filename) { return filenameElement @@ -432,7 +752,9 @@ export class Channel { try { const filenameElement = await this.driver.wait( baseElement.findElement(By.xpath(`//p[text()='${filename}']`)), - 45_000 + 15_000, + `Filename component for uploaded image ${filename} in channel ${this.name} couldn't be found within timeout`, + 500 ) return filenameElement } catch (e) { @@ -445,26 +767,45 @@ export class Channel { } get getAllMessages() { - return this.driver.wait(until.elementsLocated(By.xpath('//*[contains(@data-testid, "userMessages-")]'))) + return this.driver.wait( + until.elementsLocated(By.xpath('//*[contains(@data-testid, "userMessages-")]')), + 15_000, + `All messages in channel ${this.name} couldn't be found within timeout`, + 500 + ) } get element() { - return this.driver.wait(until.elementLocated(By.xpath('//p[@data-testid="general-link-text"]'))) + return this.driver.wait( + until.elementLocated(By.xpath(`//p[@data-testid="${this.name}-link-text"]`)), + 60_000, + `Link for channel ${this.name} couldn't be found within timeout`, + 500 + ) } get messageInput() { - return this.driver.wait(until.elementLocated(By.xpath('//*[@data-testid="messageInput"]'))) + return this.driver.wait( + until.elementLocated(By.xpath('//*[@data-testid="messageInput"]')), + 15_000, + `Message input for channel ${this.name} couldn't be found within timeout`, + 500 + ) } get uploadFileInput() { - return this.driver.wait(this.driver.findElement(By.xpath('//*[@data-testid="uploadFileInput"]')), 15_000) + return this.driver.wait( + this.driver.findElement(By.xpath('//*[@data-testid="uploadFileInput"]')), + 15_000, + `File upload button for channel ${this.name} couldn't be found within timeout`, + 500 + ) } async sendMessage(message: string, username: string): Promise { const sendMessageInput = await this.messageInput await sendMessageInput.sendKeys(message) await sendMessageInput.sendKeys(Key.ENTER) - await sleep(5000) return this.getMessageIdsByText(message, username) } @@ -478,20 +819,63 @@ export class Channel { await uploadFileInput.sendKeys(filePath) const sendMessageInput = await this.messageInput await sendMessageInput.sendKeys(Key.ENTER) - await sleep(5_000) return this.getMessageIdsByFile(filename, fileType, username) } async cancelFileDownload(messageIds: MessageIds): Promise { try { const messageElement = await this.waitForMessageContentById(messageIds.messageId) - const downloadingElement = await this.driver.wait( - messageElement.findElement(By.xpath(`//p[text()='Downloading...']`)), - 45_000 - ) - await downloadingElement.click() - await sleep(10_000) - await this.driver.wait(messageElement.findElement(By.xpath(`//p[text()='Download file']`)), 45_000) + let statusElement: WebElement | undefined = undefined + try { + statusElement = await this.waitForFileDownloadStatus(FileDownloadStatus.QUEUED, messageElement, 15_000) + } catch (e) { + logger.warn( + `Couldn't find a queued status element for this file, this is likely because it is already downloading...` + ) + } + + let endTime = DateTime.utc().toMillis() + 90_000 + while (DateTime.utc().toMillis() < endTime) { + try { + statusElement = await this.waitForFileDownloadStatus(FileDownloadStatus.DOWNLOADING, messageElement, 15_000) + break + } catch (e) { + logger.warn(`Couldn't find status element with downloading status`) + } + + try { + statusElement = await this.waitForFileDownloadStatus( + FileDownloadStatus.DOWNLOADING_CAN_CANCEL, + messageElement, + 15_000 + ) + break + } catch (e) { + logger.warn(`Couldn't find status element with downloading cancelable status`) + } + } + + if (statusElement == null) { + throw new Error(`File didn't start downloading within a reasonable time`) + } + + await statusElement.click() + endTime = DateTime.utc().toMillis() + 90_000 + while (DateTime.utc().toMillis() < endTime) { + try { + statusElement = await this.waitForFileDownloadStatus(FileDownloadStatus.CANCELED, messageElement, 15_000) + break + } catch (e) { + logger.warn(`Couldn't find status element with canceled status`) + } + + try { + statusElement = await this.waitForFileDownloadStatus(FileDownloadStatus.DOWNLOAD_FILE, messageElement, 15_000) + break + } catch (e) { + logger.warn(`Couldn't find status element with download file status`) + } + } return true } catch (e) { logger.error(`Error occurred while canceling download`, e) @@ -532,7 +916,7 @@ export class Channel { } let testId = await messageElement.getAttribute('data-testid') - logger.info(`Data Test ID for message content: ${testId}`) + logger.info(`Data Test ID for (parent) message content: ${testId}`) let testIdSplit = testId.split('-') const parentMessageId = testIdSplit[testIdSplit.length - 1] @@ -551,41 +935,91 @@ export class Channel { } } - async getUserMessages(username: string) { + async getMessageIdsByFileAndId( + messageIds: MessageIds, + filename: string, + fileType: UploadedFileType, + username: string + ): Promise { + const messageElement = await this.waitForUserMessageByFilename(username, filename, fileType) + if (!messageElement) { + throw logAndReturnError(`No message element found for filename ${filename}`) + } + + let testId = await messageElement.getAttribute('data-testid') + logger.info(`Data Test ID for (parent) message content: ${testId}`) + let testIdSplit = testId.split('-') + const parentMessageId = testIdSplit[testIdSplit.length - 1] + + const contentElement = await this.waitForMessageContentByFilenameAndId(messageIds, filename, fileType) + if (contentElement == null) { + throw logAndReturnError(`No message content element found for filename ${filename}`) + } + + testId = await contentElement.getAttribute('data-testid') + logger.info(`Data Test ID for message content: ${testId}`) + testIdSplit = testId.split('-') + const messageId = testIdSplit[testIdSplit.length - 1] + return { + messageId, + parentMessageId, + } + } + + async getUserMessages(username: string): Promise { return await this.driver.wait( - until.elementsLocated(By.xpath(`//*[contains(@data-testid, "userMessages-${username}")]`)) + until.elementsLocated(By.xpath(`//*[contains(@data-testid, "userMessages-${username}")]`)), + 15_000, + `Messages for user ${username} in channel ${this.name} couldn't be found within timeout`, + 500 ) } - async getUserMessagesFull(username: string) { + async getUserMessagesFull(username: string): Promise { return await this.driver.wait( - until.elementsLocated(By.xpath(`//*[contains(@data-testid, "userMessagesWrapper-${username}")]`)) + until.elementsLocated(By.xpath(`//*[contains(@data-testid, "userMessagesWrapper-${username}")]`)), + 15_000, + `All messages for user ${username} in channel ${this.name} couldn't be found within timeout`, + 500 ) } async getAtleastNumUserMessages(username: string, num: number): Promise { - return await this.driver.wait(async (): Promise => { - const messages = await this.getUserMessages(username) - return messages.length >= num ? messages : null - }) + return await this.driver.wait( + async (): Promise => { + const messages = await this.getUserMessages(username) + return messages.length >= num ? messages : null + }, + 20_000, + `At least ${num} messages for user ${username} in channel ${this.name} couldn't be found within timeout`, + 500 + ) } async waitForLabel(username: string, label: string) { logger.info(`Waiting for user's "${username}" label "${label}" label`) - await this.driver.wait(async () => { - const labels = await this.driver.findElements(By.xpath(`//*[contains(@data-testid, "userLabel-${username}")]`)) - const properLabels = labels.filter(async labelElement => { - const labelText = await labelElement.getText() - return labelText === label - }) - return properLabels.length > 0 - }) + await this.driver.wait( + async () => { + const labels = await this.driver.findElements(By.xpath(`//*[contains(@data-testid, "userLabel-${username}")]`)) + const properLabels = labels.filter(async labelElement => { + const labelText = await labelElement.getText() + return labelText === label + }) + return properLabels.length > 0 + }, + 15_000, + `Message label ${label} for user ${username} in channel ${this.name} couldn't be found within timeout`, + 500 + ) } async waitForAvatar(username: string, messageId: string): Promise { logger.info(`Waiting for user's avatar with username ${username} for message with ID ${messageId}`) const avatarElement = await this.driver.wait( - this.driver.findElement(By.xpath(`//*[contains(@data-testid, "userAvatar-${username}-${messageId}")]`)) + this.driver.findElement(By.xpath(`//*[contains(@data-testid, "userAvatar-${username}-${messageId}")]`)), + 15_000, + `Avatar for user ${username} in channel ${this.name} couldn't be found within timeout`, + 500 ) if (avatarElement) { logger.info(`Found user's avatar with username ${username} for message with ID ${messageId}`) @@ -598,7 +1032,10 @@ export class Channel { async waitForDateLabel(username: string, messageId: string): Promise { logger.info(`Waiting for date for message with ID ${messageId}`) const dateElement = await this.driver.wait( - this.driver.findElement(By.xpath(`//*[contains(@data-testid, "messageDateLabel-${username}-${messageId}")]`)) + this.driver.findElement(By.xpath(`//*[contains(@data-testid, "messageDateLabel-${username}-${messageId}")]`)), + 15_000, + `Message date label for user ${username} in channel ${this.name} couldn't be found within timeout`, + 500 ) if (dateElement) { logger.info(`Found date label for message with ID ${messageId}`) @@ -611,7 +1048,10 @@ export class Channel { async waitForMessageContentById(messageId: string): Promise { logger.info(`Waiting for content for message with ID ${messageId}`) const messageContentElement = await this.driver.wait( - this.driver.findElement(By.xpath(`//*[contains(@data-testid, "messagesGroupContent-${messageId}")]`)) + this.driver.findElement(By.xpath(`//*[contains(@data-testid, "messagesGroupContent-${messageId}")]`)), + 15_000, + `Message content element for message ID ${messageId} in channel ${this.name} couldn't be found within timeout`, + 500 ) if (messageContentElement) { logger.info(`Found content for message with ID ${messageId}`) @@ -624,7 +1064,10 @@ export class Channel { async waitForMessageContentByText(messageContent: string, messageElement: WebElement): Promise { logger.info(`Waiting for content for message with text ${messageContent}`) const messageContentElements = await this.driver.wait( - messageElement.findElements(By.xpath(`//*[contains(@data-testid, "messagesGroupContent-")]`)) + messageElement.findElements(By.xpath(`//*[contains(@data-testid, "messagesGroupContent-")]`)), + 15_000, + `Message content element for text ${messageContent} in channel ${this.name} couldn't be found within timeout`, + 500 ) for (const element of messageContentElements) { logger.info(await element.getId()) @@ -645,72 +1088,189 @@ export class Channel { messageElement: WebElement ): Promise { logger.info(`Waiting for file content for message with filename ${filename} and type ${fileType}`) + await this.getUploadedFilenameElementByType(filename, fileType, messageElement) const messageContentElements = await this.driver.wait( messageElement.findElements(By.xpath(`//*[contains(@data-testid, "messagesGroupContent-")]`)), - 45_000 + 45_000, + `Message content element for filename ${filename} in channel ${this.name} couldn't be found within timeout`, + 500 ) + for (const element of messageContentElements) { - logger.info(await element.getId()) - logger.info(`Testing content for type ${fileType}`) - let containerElements: WebElement[] = [] + const result = await this.testContentByFilename(filename, fileType, element) + if (result != null) { + return result + } + } + + throw logAndReturnError(`Failed to find content for message with filename ${filename} and type ${fileType}`) + } + + async waitForMessageContentByFilenameAndId( + messageIds: MessageIds, + filename: string, + fileType: UploadedFileType + ): Promise { + logger.info( + `Waiting for file content for message with filename ${filename} and type ${fileType} and ID ${messageIds.messageId}` + ) + const messageContentElement = await this.waitForMessageContentById(messageIds.messageId) + await this.getUploadedFilenameElementByType(filename, fileType, messageContentElement) + const result = await this.testContentByFilename(filename, fileType, messageContentElement) + if (result != null) { + return result + } + + throw logAndReturnError(`Failed to find content for message with filename ${filename} and type ${fileType}`) + } + + // class="UploadedImagePlaceholderplaceholderIcon" + // class="UploadedImagePlaceholderplaceholder" + + private async testContentByFilename( + filename: string, + fileType: UploadedFileType, + testableMessageContentElement: WebElement + ): Promise { + logger.info(`Testing content for type ${fileType}`) + let containerElements: WebElement[] = [] + switch (fileType) { + case UploadedFileType.IMAGE: + // wait for the downloading placeholder to appear and then disappear + try { + const placeholderElement = await this.driver.wait( + this.driver.findElement(By.xpath(`//*[@class='UploadedImagePlaceholderplaceholder']`)), + 10_000, + `Image placeholder element for ${filename} in channel ${this.name} couldn't be found within timeout`, + 500 + ) + await this.driver.wait( + until.elementIsNotVisible(placeholderElement), + 30_000, + `Image placeholder element for ${filename} in channel ${this.name} didn't disappear within timeout`, + 500 + ) + } catch (e) { + logger.warn( + `The image placeholder element never became visible, this is likely because the download completed too quickly...` + ) + } + + containerElements = await this.driver.wait( + testableMessageContentElement.findElements(By.xpath(`//*[@class='UploadedImagecontainer']`)), + 15_000, + `Image container elements in channel ${this.name} couldn't be found within timeout`, + 500 + ) + break + case UploadedFileType.FILE: + containerElements = await this.driver.wait( + testableMessageContentElement.findElements(By.xpath(`//*[contains(@data-testid, "-fileComponent")]`)), + 15_000, + `File container elements for ${filename} in channel ${this.name} couldn't be found within timeout`, + 500 + ) + break + } + + for (const container of containerElements) { + logger.info(`Testing uploaded file container ${await container.getId()}`) + const filenameElement = await this.getUploadedFilenameElementByType(filename, fileType, container) + if (filenameElement == null) { + continue + } + + let contentElement: WebElement | undefined = undefined switch (fileType) { case UploadedFileType.IMAGE: - containerElements = await this.driver.wait( - element.findElements(By.xpath(`//*[@class='UploadedImagecontainer']`)), - 45_000 + contentElement = await this.driver.wait( + container.findElement(By.xpath(`//img[@class='UploadedImageimage']`)), + 15_000, + `Image element for ${filename} in channel ${this.name} couldn't be found within timeout`, + 500 ) break case UploadedFileType.FILE: - containerElements = await this.driver.wait( - element.findElements(By.xpath(`//*[contains(@data-testid, "-fileComponent")]`)), - 45_000 + contentElement = await this.driver.wait( + container.findElement(By.xpath(`//img[@class='FileComponentactionIcon']`)), + 15_000, + `File element for ${filename} in channel ${this.name} couldn't be found within timeout`, + 500 ) break } - for (const container of containerElements) { - logger.info(`Testing uploaded file container ${await container.getId()}`) - const filenameElement = await this.getUploadedFilenameElementByType(filename, fileType, container) - if (filenameElement == null) { - continue - } - - let contentElement: WebElement | undefined = undefined - switch (fileType) { - case UploadedFileType.IMAGE: - contentElement = await this.driver.wait( - container.findElement(By.xpath(`//img[@class='UploadedImageimage']`)), - 45_000 - ) - break - case UploadedFileType.FILE: - contentElement = await this.driver.wait( - container.findElement(By.xpath(`//img[@class='FileComponentactionIcon']`)), - 45_000 - ) - break - } - - if (contentElement != null && (await contentElement.isDisplayed())) { - logger.info(`Found content element for message with filename ${filename} and type ${fileType}`) - return element - } + if (contentElement != null) { + await this.driver.wait( + until.elementIsVisible(contentElement), + 30_000, + `Image/file content element for ${filename} in channel ${this.name} couldn't be seen within timeout`, + 500 + ) + logger.info(`Found content element for message with filename ${filename} and type ${fileType}`) + return testableMessageContentElement } } - throw logAndReturnError(`Failed to find content for message with filename ${filename} and type ${fileType}`) + return undefined + } + + async waitForFileDownloadStatus( + status: FileDownloadStatus, + messageElement: WebElement, + timeoutMs = 45_000 + ): Promise { + let locatorString: string | undefined = undefined + switch (status) { + case FileDownloadStatus.QUEUED: + locatorString = 'Queued for download' + break + case FileDownloadStatus.DOWNLOADING: + locatorString = 'Downloading...' + break + case FileDownloadStatus.DOWNLOADING_CAN_CANCEL: + locatorString = 'Cancel download' + break + case FileDownloadStatus.COMPLETED: + locatorString = 'Show in folder' + break + case FileDownloadStatus.CANCELED: + locatorString = 'Canceled' + break + case FileDownloadStatus.DOWNLOAD_FILE: + locatorString = 'Download file' + break + default: + throw new Error(`Unknown status type ${status}`) + } + return await this.driver.wait( + messageElement.findElement(By.xpath(`//p[text()='${locatorString!}']`)), + timeoutMs, + `File download status element with text ${locatorString} in channel ${this.name} couldn't be found within timeout`, + 500 + ) } async waitForLabelsNotPresent(username: string) { logger.info(`Waiting for user's "${username}" label to not be present`) - await this.driver.wait(async () => { - const labels = await this.driver.findElements(By.xpath(`//*[contains(@data-testid, "userLabel-${username}")]`)) - return labels.length === 0 - }) + await this.driver.wait( + async () => { + const labels = await this.driver.findElements(By.xpath(`//*[contains(@data-testid, "userLabel-${username}")]`)) + return labels.length === 0 + }, + 15_000, + `User name label ${username} in channel ${this.name} didn't disappear within timeout`, + 500 + ) } async getMessage(text: string) { - return await this.driver.wait(until.elementLocated(By.xpath(`//span[contains(text(),"${text}")]`))) + return await this.driver.wait( + until.elementLocated(By.xpath(`//span[contains(text(),"${text}")]`)), + 15_000, + `Message with text ${text} in channel ${this.name} couldn't be found within timeout`, + 500 + ) } } @@ -721,7 +1281,12 @@ export class Sidebar { } async getChannelList() { - const channels = await this.driver.findElements(By.xpath('//*[contains(@data-testid, "link-text")]')) + const channels = await this.driver.wait( + this.driver.findElements(By.xpath('//*[contains(@data-testid, "link-text")]')), + 15_000, + `Sidebar channel list couldn't be found within timeout`, + 500 + ) return channels } @@ -740,10 +1305,15 @@ export class Sidebar { async waitForChannelsNum(num: number) { logger.info(`Waiting for ${num} channels`) - return this.driver.wait(async () => { - const channels = await this.getChannelList() - return channels.length === num - }) + return this.driver.wait( + async () => { + const channels = await this.getChannelList() + return channels.length === num + }, + 15_000, + `Sidebar channel list length couldn't be determined within timeout`, + 500 + ) } async waitForChannels(channelsNames: Array) { @@ -752,23 +1322,51 @@ export class Sidebar { expect(names).toEqual(expect.arrayContaining(channelsNames)) } - async openSettings() { - const button = await this.driver.findElement(By.xpath('//span[@data-testid="settings-panel-button"]')) + async openSettings(): Promise { + const button = await this.driver.wait( + this.driver.findElement(By.xpath('//span[@data-testid="settings-panel-button"]')), + 10_000, + `Community settings button couldn't be found within timeout`, + 500 + ) await button.click() return new Settings(this.driver) } - async switchChannel(name: string) { - const channelLink = await this.driver.wait(until.elementLocated(By.xpath(`//div[@data-testid="${name}-link"]`))) + async switchChannel(name: string): Promise { + const channelLink = await this.driver.wait( + until.elementLocated(By.xpath(`//div[@data-testid="${name}-link"]`)), + 10_000, + `Channel link button for ${name} couldn't be found within timeout`, + 500 + ) await channelLink.click() + const channel = new Channel(this.driver, name) + await channel.isOpen() + return channel } - async addNewChannel(name: string) { - const button = await this.driver.findElement(By.xpath('//button[@data-testid="addChannelButton"]')) + async addNewChannel(name: string): Promise { + const button = await this.driver.wait( + this.driver.findElement(By.xpath('//button[@data-testid="addChannelButton"]')), + 5_000, + `Add channel button couldn't be found within timeout`, + 500 + ) await button.click() - const channelNameInput = await this.driver.findElement(By.xpath('//input[@name="channelName"]')) + const channelNameInput = await this.driver.wait( + this.driver.findElement(By.xpath('//input[@name="channelName"]')), + 5_000, + `Add channel name input field couldn't be found within timeout`, + 500 + ) await channelNameInput.sendKeys(name) - const channelNameButton = await this.driver.findElement(By.xpath('//button[@data-testid="channelNameSubmit"]')) + const channelNameButton = await this.driver.wait( + this.driver.findElement(By.xpath('//button[@data-testid="channelNameSubmit"]')), + 5_000, + `Add channel submit button couldn't be found within timeout`, + 500 + ) await channelNameButton.click() return new Channel(this.driver, name) } @@ -783,15 +1381,21 @@ export class UpdateModal { get element() { logger.info('Waiting for update modal root element') return this.driver.wait( - until.elementLocated(By.xpath("//h3[text()='Software update']/ancestor::div[contains(@class,'MuiModal-root')]")) + until.elementLocated(By.xpath("//h3[text()='Software update']/ancestor::div[contains(@class,'MuiModal-root')]")), + 15_000, + `Update modal couldn't be found within timeout`, + 500 ) } async close() { const updateModalRootElement = await this.element logger.info('Found update modal root element') - const closeButton = await updateModalRootElement.findElement( - By.xpath("//*[self::div[@data-testid='ModalActions']]/button") + const closeButton = await this.driver.wait( + updateModalRootElement.findElement(By.xpath("//*[self::div[@data-testid='ModalActions']]/button")), + 10_000, + `Update modal close button couldn't be found within timeout`, + 500 ) try { @@ -817,13 +1421,32 @@ export class Settings { } get element() { - return this.driver.wait(until.elementLocated(By.xpath("//p[text()='Community Settings']"))) + return this.driver.wait( + until.elementLocated(By.xpath("//p[text()='Community Settings']")), + 15_000, + `Settings modal couldn't be found within timeout`, + 500 + ) + } + + async isReady(): Promise { + await this.driver.wait( + until.elementIsVisible(this.element), + 10_000, + `Settings modal wasn't ready within timeout`, + 500 + ) + return true } async getVersion() { - await this.switchTab('about') - await sleep(500) - const textWebElement = await this.driver.findElement(By.xpath('//p[contains(text(),"Version")]')) + await this.switchTab(SettingsModalTabName.ABOUT) + const textWebElement = await this.driver.wait( + this.driver.findElement(By.xpath('//p[contains(text(),"Version")]')), + 10_000, + `App version couldn't be found within timeout`, + 500 + ) const text = await textWebElement.getText() const version = this.formatVersionText(text) @@ -839,26 +1462,47 @@ export class Settings { } async openLeaveCommunityModal() { - const tab = await this.driver.wait(until.elementLocated(By.xpath('//p[@data-testid="leave-community-tab"]'))) - await tab.click() + await this.switchTab(SettingsModalTabName.LEAVE_COMMUNITY) } async leaveCommunityButton() { - const button = await this.driver.wait(until.elementLocated(By.xpath('//button[text()="Leave community"]'))) + const button = await this.driver.wait( + until.elementLocated(By.xpath('//button[text()="Leave community"]')), + 15_000, + `Leave community button couldn't be found within timeout`, + 500 + ) await button.click() } - async switchTab(name: string) { - const tab = await this.driver.findElement(By.xpath(`//div[@data-testid='${name}-settings-tab']`)) + async switchTab(name: SettingsModalTabName) { + const tab = await this.driver.wait( + this.driver.findElement(By.xpath(`//div[@data-testid='${name}-settings-tab']`)), + 15_000, + `Settings tab button for ${name} couldn't be found within timeout`, + 500 + ) await tab.click() + await this.waitForTabToBeReady(name) } async invitationLink() { - const unlockButton = await this.driver.findElement(By.xpath('//button[@data-testid="show-invitation-link"]')) + const unlockButton = await this.driver.wait( + until.elementLocated(By.xpath('//button[@data-testid="show-invitation-link"]')), + 10_000, + `Show invitation link button couldn't be found within timeout`, + 500 + ) + await this.driver.wait(until.elementIsVisible(unlockButton), 10_000) await unlockButton.click() - return await this.driver.findElement(By.xpath("//p[@data-testid='invitation-link']")) + return await this.driver.wait( + this.driver.findElement(By.xpath("//p[@data-testid='invitation-link']")), + 10_000, + `Unhidden invitation link element couldn't be found within timeout`, + 500 + ) } async closeTabThenModal() { @@ -867,16 +1511,60 @@ export class Settings { } async close() { - const closeButton = await this.driver.findElement(By.xpath('//div[@data-testid="close-settings-button"]')) + const closeButton = await this.driver.wait( + this.driver.findElement(By.xpath('//div[@data-testid="close-settings-button"]')), + 10_000, + `Settings close button couldn't be found within timeout`, + 500 + ) await closeButton.click() } async closeTab() { - const closeTabButton = await this.driver - .findElement(By.xpath('//div[@data-testid="close-tab-button-box"]')) - .findElement(By.css('button')) + const closeTabButton = await this.driver.wait( + this.driver.findElement(By.xpath('//div[@data-testid="close-tab-button-box"]')).findElement(By.css('button')), + 10_000, + `Settings tab close button couldn't be found within timeout`, + 500 + ) await closeTabButton.click() } + + private async waitForTabToBeReady(tabName: SettingsModalTabName) { + let locator: string | undefined = undefined + switch (tabName) { + case SettingsModalTabName.INVITE: + locator = "//*[@data-testid='invite-a-friend']" + break + case SettingsModalTabName.ABOUT: + locator = "//div[contains(@class, 'Abouttitle')]" + break + case SettingsModalTabName.LEAVE_COMMUNITY: + locator = "//div[contains(@class, 'LeaveCommunitytitleContainer')]" + break + case SettingsModalTabName.NOTIFICATIONS: + locator = "//div[contains(@class, 'Notificationstitle')]" + break + case SettingsModalTabName.QR_CODE: + locator = "//div[contains(@class, 'QRCodetextWrapper')]" + break + default: + throw new Error(`Can't wait for unknown tab ${tabName}`) + } + + const result = await this.driver.wait( + this.driver.findElement(By.xpath(locator!)), + 15_000, + `Settings tab ${tabName} wasn't ready within timeout`, + 500 + ) + await this.driver.wait( + until.elementIsVisible(result), + 10_000, + `Settings tab ${tabName} wasn't visible within timeout`, + 500 + ) + } } export class DebugModeModal { @@ -887,11 +1575,21 @@ export class DebugModeModal { } get element() { - return this.driver.wait(until.elementLocated(By.xpath("//h3[text()='App is running in debug mode']")), 5000) + return this.driver.wait( + until.elementLocated(By.xpath("//h3[text()='App is running in debug mode']")), + 5000, + `Debug modal couldn't be found within timeout`, + 500 + ) } get button() { - return this.driver.wait(until.elementLocated(By.xpath("//button[text()='Understand']")), 5000) + return this.driver.wait( + until.elementLocated(By.xpath("//button[text()='Understand']")), + 5000, + `Debug modal understand button couldn't be found within timeout`, + 500 + ) } async close() { @@ -899,7 +1597,12 @@ export class DebugModeModal { let button try { logger.info('Closing debug modal') - await this.element.isDisplayed() + await this.driver.wait( + until.elementIsVisible(this.element), + 15_000, + `Debug modal couldn't be seen within timeout`, + 500 + ) logger.info('Debug modal title is displayed') button = await this.button logger.info('Debug modal button is displayed') diff --git a/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts b/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts index cdb384472..e74497d3e 100644 --- a/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts +++ b/packages/e2e-tests/src/tests/backwardsCompatibility.test.ts @@ -5,11 +5,12 @@ import { CreateCommunityModal, DebugModeModal, JoinCommunityModal, + JoiningLoadingPanel, RegisterUsernameModal, Sidebar, } from '../selectors' import { MessageIds } from '../types' -import { BACKWARD_COMPATIBILITY_BASE_VERSION, BuildSetup, copyInstallerFile, downloadInstaller, sleep } from '../utils' +import { BACKWARD_COMPATIBILITY_BASE_VERSION, BuildSetup, copyInstallerFile, downloadInstaller } from '../utils' import { createLogger } from '../logger' const logger = createLogger('backwardsCompatibility') @@ -46,11 +47,9 @@ describe('Backwards Compatibility', () => { beforeEach(async () => { logger.info(`░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ${expect.getState().currentTestName}`) - await sleep(1_000) }) afterAll(async () => { - await sleep(5_000) await ownerAppNewVersion?.close() await ownerAppNewVersion?.cleanup() await ownerAppOldVersion?.close() @@ -77,48 +76,48 @@ describe('Backwards Compatibility', () => { 'Owner sees "join community" modal and switches to "create community" modal', async () => { const joinModal = new JoinCommunityModal(ownerAppOldVersion.driver) - const isJoinModal = await joinModal.element.isDisplayed() - expect(isJoinModal).toBeTruthy() + expect(await joinModal.isReady()).toBeTruthy() await joinModal.switchToCreateCommunity() } ) itif(process.platform == 'linux')('Owner submits valid community name', async () => { const createModal = new CreateCommunityModal(ownerAppOldVersion.driver) - const isCreateModal = await createModal.element.isDisplayed() - expect(isCreateModal).toBeTruthy() + expect(await createModal.isReady()).toBeTruthy() await createModal.typeCommunityName(communityName) await createModal.submit() }) itif(process.platform == 'linux')('Owner sees "register username" modal and submits valid username', async () => { const registerModal = new RegisterUsernameModal(ownerAppOldVersion.driver) - const isRegisterModal = await registerModal.element.isDisplayed() - expect(isRegisterModal).toBeTruthy() + expect(await registerModal.isReady()).toBeTruthy() await registerModal.typeUsername(ownerUsername) await registerModal.submit() }) + itif(process.platform == 'linux')('Owner waits for join to complete', async () => { + const joinPanel = new JoiningLoadingPanel(ownerAppOldVersion.driver) + await joinPanel.waitForJoinToComplete() + }) + itif(process.platform == 'linux')('Owner registers successfully and sees general channel', async () => { generalChannel = new Channel(ownerAppOldVersion.driver, 'general') - const isGeneralChannel = await generalChannel.element.isDisplayed() + expect(await generalChannel.isReady()).toBeTruthy() + const generalChannelText = await generalChannel.element.getText() - expect(isGeneralChannel).toBeTruthy() expect(generalChannelText).toEqual('# general') }) itif(process.platform == 'linux')(`Verify version - ${BACKWARD_COMPATIBILITY_BASE_VERSION}`, async () => { const settingsModal = await new Sidebar(ownerAppOldVersion.driver).openSettings() - const isSettingsModal = await settingsModal.element.isDisplayed() - expect(isSettingsModal).toBeTruthy() + expect(await settingsModal.isReady()).toBeTruthy() const settingVersion = await settingsModal.getVersion() expect(settingVersion).toEqual(BACKWARD_COMPATIBILITY_BASE_VERSION) await settingsModal.close() }) itif(process.platform == 'linux')('Owner sends a message in the general channel', async () => { - const isMessageInput = await generalChannel.messageInput.isDisplayed() - expect(isMessageInput).toBeTruthy() + expect(await generalChannel.isMessageInputReady()).toBeTruthy() generalChannelMessageIds = await generalChannel.sendMessage(ownerMessages[0], ownerUsername) }) @@ -138,8 +137,7 @@ describe('Backwards Compatibility', () => { itif(process.platform == 'linux')('Owner sends a message in second channel', async () => { secondChannel = new Channel(ownerAppOldVersion.driver, newChannelName) - const isMessageInput = await secondChannel.messageInput.isDisplayed() - expect(isMessageInput).toBeTruthy() + expect(await secondChannel.isMessageInputReady()).toBeTruthy() secondChannelMessageIds = await secondChannel.sendMessage(ownerMessages[1], ownerUsername) }) @@ -161,7 +159,6 @@ describe('Backwards Compatibility', () => { ) itif(process.platform == 'linux')('User closes the old app', async () => { await ownerAppOldVersion.close() - await sleep(5_000) }) // ________________________________ @@ -183,18 +180,16 @@ describe('Backwards Compatibility', () => { itif(process.platform == 'linux')('Owener sees general channel', async () => { logger.info('New version', 3) generalChannel = new Channel(ownerAppNewVersion.driver, 'general') - const isGeneralChannel = await generalChannel.element.isDisplayed() + expect(await generalChannel.isReady()).toBeTruthy() + const generalChannelText = await generalChannel.element.getText() - expect(isGeneralChannel).toBeTruthy() expect(generalChannelText).toEqual('# general') }) itif(process.platform == 'linux')('Confirm that the opened app is the latest version', async () => { logger.info('New version', 4) - await sleep(10_000) const settingsModal = await new Sidebar(ownerAppNewVersion.driver).openSettings() - const isSettingsModal = await settingsModal.element.isDisplayed() - expect(isSettingsModal).toBeTruthy() + expect(await settingsModal.isReady()).toBeTruthy() const settingVersion = await settingsModal.getVersion() const envVersion = ownerAppNewVersion.buildSetup.getVersionFromEnv() expect(settingVersion).toEqual(envVersion) @@ -203,11 +198,11 @@ describe('Backwards Compatibility', () => { itif(process.platform == 'linux')('Check number of messages on second channel', async () => { logger.info('New version', 5) - await sleep(2_000) sidebar = new Sidebar(ownerAppNewVersion.driver) await sidebar.switchChannel(newChannelName) - await sleep(5_000) secondChannel = new Channel(ownerAppNewVersion.driver, newChannelName) + expect(await secondChannel.isReady()).toBeTruthy() + const currentMessages = await secondChannel.getUserMessages(ownerUsername) expect(currentMessages.length).toEqual(messagesToCompare.length) }) diff --git a/packages/e2e-tests/src/tests/invitationLink.test.ts b/packages/e2e-tests/src/tests/invitationLink.test.ts index 348b2b4d7..2404712e2 100644 --- a/packages/e2e-tests/src/tests/invitationLink.test.ts +++ b/packages/e2e-tests/src/tests/invitationLink.test.ts @@ -7,12 +7,13 @@ import { App, Sidebar, WarningModal, + JoiningLoadingPanel, } from '../selectors' import { composeInvitationDeepUrl, parseInvitationLink, userJoinedMessage } from '@quiet/common' import { execSync } from 'child_process' import { type SupportedPlatformDesktop } from '@quiet/types' import { createLogger } from '../logger' -import { sleep } from '../utils' +import { SettingsModalTabName } from '../enums' const logger = createLogger('invitationLink') @@ -37,7 +38,6 @@ describe('New user joins using invitation link while having app opened', () => { beforeEach(async () => { logger.info(`░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ${expect.getState().currentTestName}`) - await sleep(1000) }) afterAll(async () => { @@ -56,16 +56,14 @@ describe('New user joins using invitation link while having app opened', () => { it('JoinCommunityModal - owner switches to create community', async () => { logger.info('Invitation Link', 4) const joinModal = new JoinCommunityModal(ownerApp.driver) - const isJoinModal = await joinModal.element.isDisplayed() - expect(isJoinModal).toBeTruthy() + expect(await joinModal.isReady()).toBeTruthy() await joinModal.switchToCreateCommunity() }) it('CreateCommunityModal - owner creates his community', async () => { logger.info('Invitation Link', 5) const createModal = new CreateCommunityModal(ownerApp.driver) - const isCreateModal = await createModal.element.isDisplayed() - expect(isCreateModal).toBeTruthy() + expect(await createModal.isReady()).toBeTruthy() await createModal.typeCommunityName(communityName) await createModal.submit() }) @@ -73,8 +71,7 @@ describe('New user joins using invitation link while having app opened', () => { it('RegisterUsernameModal - owner has registered', async () => { logger.info('Invitation Link', 6) const registerModal = new RegisterUsernameModal(ownerApp.driver) - const isRegisterModal = await registerModal.element.isDisplayed() - expect(isRegisterModal).toBeTruthy() + expect(await registerModal.isReady()).toBeTruthy() await registerModal.typeUsername(ownerUsername) await registerModal.submit() }) @@ -82,19 +79,17 @@ describe('New user joins using invitation link while having app opened', () => { it('Owner sees general channel', async () => { logger.info('Invitation Link', 8) const generalChannel = new Channel(ownerApp.driver, 'general') - const isGeneralChannel = await generalChannel.element.isDisplayed() + expect(await generalChannel.isReady()) + const generalChannelText = await generalChannel.element.getText() - expect(isGeneralChannel).toBeTruthy() expect(generalChannelText).toEqual('# general') }) it('Owner opens the settings tab and gets an invitation code', async () => { logger.info('Invitation Link', 9) const settingsModal = await new Sidebar(ownerApp.driver).openSettings() - const isSettingsModal = await settingsModal.element.isDisplayed() - expect(isSettingsModal).toBeTruthy() - await settingsModal.switchTab('invite') - await sleep(1000) + expect(await settingsModal.isReady()).toBeTruthy() + await settingsModal.switchTab(SettingsModalTabName.INVITE) const invitationLinkElement = await settingsModal.invitationLink() invitationLink = await invitationLinkElement.getText() logger.info('Received invitation link:', invitationLink) @@ -131,8 +126,9 @@ describe('New user joins using invitation link while having app opened', () => { it.skip('Guest sees modal with warning about invalid code, closes it', async () => { // Fix when modals ordering is fixed (joining modal hiddes warning modal) const warningModal = new WarningModal(guestApp.driver) + expect(await warningModal.isReady()).toBeTruthy() + const titleElement = await warningModal.titleElement - expect(titleElement.isDisplayed()).toBeTruthy() expect(titleElement.getText()).toEqual('Invalid link') await warningModal.close() }) @@ -157,18 +153,12 @@ describe('New user joins using invitation link while having app opened', () => { logger.info('Guest opened invitation link') }) - it('Guest is redirected to UsernameModal', async () => { - logger.info('Invitation Link', 15) - logger.info('Guest sees username modal') - const registerModal = new RegisterUsernameModal(guestApp.driver) - const isRegisterModalDisplayed = await registerModal.element.isDisplayed() - expect(isRegisterModalDisplayed).toBeTruthy() - }) - - it('Guest submits username', async () => { + it('Guest is redirected to UsernameModal and submits username', async () => { logger.info('Invitation Link', 16) logger.info('Guest submits username') const registerModal = new RegisterUsernameModal(guestApp.driver) + expect(await registerModal.isReady()).toBeTruthy() + await registerModal.typeUsername(joiningUserUsername) await registerModal.submit() }) @@ -184,18 +174,23 @@ describe('New user joins using invitation link while having app opened', () => { }) } + it('Guest waits to join the community', async () => { + const joinPanel = new JoiningLoadingPanel(guestApp.driver) + await joinPanel.waitForJoinToComplete() + }) + it('Guest joined a community and sees general channel', async () => { logger.info('Invitation Link', 20) logger.info('guest sees general channel') const generalChannel = new Channel(guestApp.driver, 'general') - await generalChannel.element.isDisplayed() + expect(await generalChannel.isReady()).toBeTruthy() }) it('Owner sees that guest joined community', async () => { logger.info('Invitation Link', 21) const generalChannel = new Channel(ownerApp.driver, 'general') - await generalChannel.element.isDisplayed() + expect(await generalChannel.isReady()).toBeTruthy() const messageIds = await generalChannel.getMessageIdsByText( `@${joiningUserUsername} has joined and will be registered soon. 🎉 Learn more`, diff --git a/packages/e2e-tests/src/tests/multipleClients.test.ts b/packages/e2e-tests/src/tests/multipleClients.test.ts index 88d273254..a01faf1d4 100644 --- a/packages/e2e-tests/src/tests/multipleClients.test.ts +++ b/packages/e2e-tests/src/tests/multipleClients.test.ts @@ -7,6 +7,7 @@ import { CreateCommunityModal, DebugModeModal, JoinCommunityModal, + JoiningLoadingPanel, RegisterUsernameModal, Sidebar, } from '../selectors' @@ -14,7 +15,7 @@ import { promiseWithRetries, sleep } from '../utils' import { MessageIds, UserTestData } from '../types' import { createLogger } from '../logger' import * as path from 'path' -import { UploadedFileType } from '../enums' +import { SettingsModalTabName, UploadedFileType } from '../enums' import { BIG_FILE_SIZE, TEST_BIG_FILE_NAME, @@ -87,7 +88,6 @@ describe('Multiple Clients', () => { beforeEach(async () => { logger.info(`░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ${expect.getState().currentTestName}`) - await sleep(1000) }) describe('Stages:', () => { @@ -98,38 +98,40 @@ describe('Multiple Clients', () => { it('Owner sees "join community" modal and switches to "create community" modal', async () => { const joinModal = new JoinCommunityModal(users.owner.app.driver) - const isJoinModal = await joinModal.element.isDisplayed() - expect(isJoinModal).toBeTruthy() + expect(await joinModal.isReady()).toBeTruthy() await joinModal.switchToCreateCommunity() }) it('Owner submits valid community name', async () => { const createModal = new CreateCommunityModal(users.owner.app.driver) - const isCreateModal = await createModal.element.isDisplayed() - expect(isCreateModal).toBeTruthy() + expect(await createModal.isReady()).toBeTruthy() await createModal.typeCommunityName(communityName) await createModal.submit() }) it('Owner sees "register username" modal and submits valid username', async () => { const registerModal = new RegisterUsernameModal(users.owner.app.driver) - const isRegisterModal = await registerModal.element.isDisplayed() - expect(isRegisterModal).toBeTruthy() + expect(await registerModal.isReady()).toBeTruthy() await registerModal.typeUsername(users.owner.username) await registerModal.submit() }) + it('Owner waits to join', async () => { + const joinPanel = new JoiningLoadingPanel(users.owner.app.driver) + await joinPanel.waitForJoinToComplete() + }) + it('Owner registers successfully and sees general channel', async () => { generalChannelOwner = new Channel(users.owner.app.driver, generalChannelName) - const isGeneralChannel = await generalChannelOwner.element.isDisplayed() + expect(await generalChannelOwner.isReady()).toBeTruthy() + expect(await generalChannelOwner.isOpen()).toBeTruthy() + const generalChannelText = await generalChannelOwner.element.getText() - expect(isGeneralChannel).toBeTruthy() expect(generalChannelText).toEqual('# general') }) it('Owner sends a message', async () => { - const isMessageInput = await generalChannelOwner.messageInput.isDisplayed() - expect(isMessageInput).toBeTruthy() + expect(await generalChannelOwner.isMessageInputReady()).toBeTruthy() await generalChannelOwner.sendMessage(users.owner.messages[0], users.owner.username) }) @@ -141,11 +143,8 @@ describe('Multiple Clients', () => { it('Owner opens the settings tab and gets an invitation link', async () => { const settingsModal = await new Sidebar(users.owner.app.driver).openSettings() - const isSettingsModal = await settingsModal.element.isDisplayed() - expect(isSettingsModal).toBeTruthy() - await sleep(2000) - await settingsModal.switchTab('invite') - await sleep(2000) + expect(await settingsModal.isReady()).toBeTruthy() + await settingsModal.switchTab(SettingsModalTabName.INVITE) const invitationLinkElement = await settingsModal.invitationLink() invitationLink = await invitationLinkElement.getText() expect(invitationLink).not.toBeUndefined() @@ -164,8 +163,7 @@ describe('Multiple Clients', () => { it('First user submits invitation code received from owner', async () => { logger.info('new user - 3') const joinCommunityModal = new JoinCommunityModal(users.user1.app.driver) - const isJoinCommunityModal = await joinCommunityModal.element.isDisplayed() - expect(isJoinCommunityModal).toBeTruthy() + expect(await joinCommunityModal.isReady()).toBeTruthy() logger.info({ invitationLink }) await joinCommunityModal.typeCommunityInviteLink(invitationLink) await joinCommunityModal.submit() @@ -175,22 +173,26 @@ describe('Multiple Clients', () => { const app = users.user1.app logger.info('new user - 5') const registerModal = new RegisterUsernameModal(app.driver) - const isRegisterModal = await registerModal.element.isDisplayed() - expect(isRegisterModal).toBeTruthy() + expect(await registerModal.isReady()).toBeTruthy() await registerModal.clearInput() await registerModal.typeUsername(users.user1.username) await registerModal.submit() logger.time(`[${app.name}] '${users.user1.username}' joining community time`) }) + it('First user waits to join', async () => { + const joinPanel = new JoiningLoadingPanel(users.user1.app.driver) + await joinPanel.waitForJoinToComplete() + }) + it('First user joins successfully sees general channel and sends a message', async () => { logger.info('new user - 7') const app = users.user1.app const loadNewUser = async () => { generalChannelUser1 = new Channel(app.driver, generalChannelName) - await generalChannelUser1.element.isDisplayed() - const isMessageInput2 = await generalChannelUser1.messageInput.isDisplayed() - expect(isMessageInput2).toBeTruthy() + expect(await generalChannelUser1.isReady()).toBeTruthy() + expect(await generalChannelUser1.isOpen()).toBeTruthy() + expect(await generalChannelUser1.isMessageInputReady()).toBeTruthy() logger.timeEnd(`[${app.name}] '${users.user1.username}' joining community time`) } @@ -201,10 +203,6 @@ describe('Multiple Clients', () => { await app.open() } await promiseWithRetries(loadNewUser(), failureReason, retryConfig, onTimeout) - - logger.info('FETCHING CHANNEL MESSAGES!') - await sleep(20000) - await generalChannelUser1.sendMessage(users.user1.messages[0], users.user1.username) }) @@ -215,15 +213,10 @@ describe('Multiple Clients', () => { it('First user opens the settings tab and copies updated invitation link', async () => { const settingsModal = await new Sidebar(users.user1.app.driver).openSettings() - const isSettingsModal = await settingsModal.element.isDisplayed() - expect(isSettingsModal).toBeTruthy() - await sleep(2000) - await settingsModal.switchTab('invite') - await sleep(2000) + expect(await settingsModal.isReady()).toBeTruthy() + await settingsModal.switchTab(SettingsModalTabName.INVITE) const invitationLinkElement = await settingsModal.invitationLink() - await sleep(2000) invitationLink = await invitationLinkElement.getText() - await sleep(2000) logger.info(`${invitationLink} copied from non owner`) expect(invitationLink).not.toBeUndefined() await settingsModal.closeTabThenModal() @@ -233,7 +226,6 @@ describe('Multiple Clients', () => { describe('Owner Leaves', () => { it('Owner goes offline', async () => { await users.owner.app.close() - await sleep(30000) }) it(`First user sends a message`, async () => { @@ -257,8 +249,7 @@ describe('Multiple Clients', () => { it('Second user starts to join when owner is offline', async () => { const app = users.user3.app const joinCommunityModal = new JoinCommunityModal(app.driver) - const isJoinCommunityModal = await joinCommunityModal.element.isDisplayed() - expect(isJoinCommunityModal).toBeTruthy() + expect(await joinCommunityModal.isReady()).toBeTruthy() logger.info({ invitationLink }) await joinCommunityModal.typeCommunityInviteLink(invitationLink) await joinCommunityModal.submit() @@ -267,24 +258,26 @@ describe('Multiple Clients', () => { it('Second user submits non-valid, duplicated username', async () => { logger.info('duplicated user - 1') const registerModal = new RegisterUsernameModal(users.user3.app.driver) - const isRegisterModal = await registerModal.element.isDisplayed() - expect(isRegisterModal).toBeTruthy() + expect(await registerModal.isReady()).toBeTruthy() await registerModal.clearInput() await registerModal.typeUsername(users.user1.username) await registerModal.submit() logger.time(`[${users.user3.app.name}] '${users.user1.username}' duplicated joining community time`) }) + it('Second user waits to join', async () => { + const joinPanel = new JoiningLoadingPanel(users.user3.app.driver) + await joinPanel.waitForJoinToComplete() + }) + it('Second user submits valid username', async () => { - const app = users.user3.app logger.info('duplicated user - 2') - const registerModal = new RegisterUsernameModal(app.driver) - const isRegisterModal = await registerModal.elementUsernameTaken.isDisplayed() - expect(isRegisterModal).toBeTruthy() + const registerModal = new RegisterUsernameModal(users.user3.app.driver) + expect(await registerModal.isUsernameTakenReady()).toBeTruthy() await registerModal.clearInput() await registerModal.typeUsername(users.user3.username) await registerModal.submitUsernameTaken() - logger.time(`[${app.name}] '${users.user3.username}' joining community time`) + logger.time(`[${users.user3.app.name}] '${users.user3.username}' joining community time`) }) it('Second user sees general channel', async () => { @@ -292,9 +285,9 @@ describe('Multiple Clients', () => { const app = users.user3.app const loadNewUser = async () => { generalChannelUser3 = new Channel(app.driver, generalChannelName) - await generalChannelUser3.element.isDisplayed() - const isMessageInput = await generalChannelUser3.messageInput.isDisplayed() - expect(isMessageInput).toBeTruthy() + expect(await generalChannelUser3.isReady()).toBeTruthy() + expect(await generalChannelUser3.isOpen()).toBeTruthy() + expect(await generalChannelUser3.isMessageInputReady()).toBeTruthy() logger.timeEnd(`[${app.name}] '${users.user3.username}' joining community time`) } @@ -308,8 +301,6 @@ describe('Multiple Clients', () => { }) it('Second user can send a message, they see their message tagged as "unregistered"', async () => { - logger.info('Second guest FETCHING CHANNEL MESSAGES!') - await sleep(15000) await generalChannelUser3.sendMessage(users.user3.messages[0], users.user3.username) generalChannelUser3 = new Channel(users.user3.app.driver, generalChannelName) await generalChannelUser3.waitForLabel(users.user3.username, 'Unregistered') @@ -322,6 +313,7 @@ describe('Multiple Clients', () => { }) describe('Second User Registers', () => { + // TODO: add check for number of messages it('Owner goes back online', async () => { await users.owner.app.openWithRetries() const debugModal = new DebugModeModal(users.owner.app.driver) @@ -355,9 +347,8 @@ describe('Multiple Clients', () => { it('Owner sends message in second channel', async () => { secondChannelOwner = new Channel(users.owner.app.driver, newChannelName) - const isMessageInput = await secondChannelOwner.messageInput.isDisplayed() - expect(isMessageInput).toBeTruthy() - await sleep(5000) + expect(await secondChannelOwner.isReady()).toBeTruthy() + expect(await secondChannelOwner.isMessageInputReady()).toBeTruthy() await secondChannelOwner.sendMessage(users.owner.messages[1], users.owner.username) }) @@ -365,7 +356,6 @@ describe('Multiple Clients', () => { sidebarUser1 = new Sidebar(users.user1.app.driver) await sidebarUser1.switchChannel(newChannelName) secondChannelUser1 = new Channel(users.user1.app.driver, newChannelName) - await sleep(2000) await secondChannelUser1.getMessageIdsByText(users.owner.messages[1], users.owner.username) }) }) @@ -377,23 +367,35 @@ describe('Multiple Clients', () => { await channelContextMenuOwner.openDeletionChannelModal() await channelContextMenuOwner.deleteChannel() const channels = await sidebarOwner.getChannelList() + expect(await generalChannelOwner.isOpen()).toBeTruthy() expect(channels.length).toEqual(1) }) it('User sees info about channel deletion in general channel', async () => { - await sleep(5000) + expect(await generalChannelUser1.isOpen()).toBeTruthy() await generalChannelUser1.getMessageIdsByText( `@${users.owner.username} deleted #${newChannelName}`, users.owner.username ) }) + it('User sees that the channel is missing in the sidebar', async () => { + const channels = await sidebarUser1.getChannelList() + expect(channels.length).toEqual(1) + }) + it('User can create channel with the same name and is fresh channel', async () => { await sidebarUser1.addNewChannel(newChannelName) await sidebarUser1.switchChannel(newChannelName) const messages = await secondChannelUser1.getUserMessages(users.user1.username) expect(messages.length).toEqual(1) - await sleep(2000) + expect(await secondChannelUser1.isReady()).toBeTruthy() + const channels = await sidebarUser1.getChannelList() + expect(channels.length).toEqual(2) + }) + + it('Owner sees the recreated second channel', async () => { + expect(await secondChannelOwner.isReady()).toBeTruthy() const channels = await sidebarOwner.getChannelList() expect(channels.length).toEqual(2) }) @@ -403,22 +405,27 @@ describe('Multiple Clients', () => { it('Leave community', async () => { logger.info('TEST 2') const settingsModal = await new Sidebar(users.user1.app.driver).openSettings() - const isSettingsModal = await settingsModal.element.isDisplayed() - expect(isSettingsModal).toBeTruthy() - await settingsModal.switchTab('leave-community') - await sleep(2000) + expect(await settingsModal.isReady()).toBeTruthy() + await settingsModal.switchTab(SettingsModalTabName.LEAVE_COMMUNITY) await settingsModal.leaveCommunityButton() }) // Delete general channel while guest is absent it('Owner recreates general channel', async () => { logger.info('TEST 3') - await sleep(10000) - const isGeneralChannel = await generalChannelOwner.messageInput.isDisplayed() - expect(isGeneralChannel).toBeTruthy() + expect(await generalChannelOwner.isReady()).toBeTruthy() + expect(await generalChannelOwner.isOpen()).toBeTruthy() + expect(await generalChannelOwner.isMessageInputReady()).toBeTruthy() await channelContextMenuOwner.openMenu() await channelContextMenuOwner.openDeletionChannelModal() await channelContextMenuOwner.deleteChannel() + }) + + it('Owner sees recreated general channel', async () => { + logger.info('TEST 3') + expect(await generalChannelOwner.isReady()).toBeTruthy() + expect(await generalChannelOwner.isOpen()).toBeTruthy() + expect(await generalChannelOwner.isMessageInputReady()).toBeTruthy() const channels = await sidebarOwner.getChannelList() expect(channels.length).toEqual(2) }) @@ -431,8 +438,7 @@ describe('Multiple Clients', () => { const debugModal = new DebugModeModal(users.user1.app.driver) await debugModal.close() const joinCommunityModal = new JoinCommunityModal(users.user1.app.driver) - const isJoinCommunityModal = await joinCommunityModal.element.isDisplayed() - expect(isJoinCommunityModal).toBeTruthy() + expect(await joinCommunityModal.isReady()).toBeTruthy() await joinCommunityModal.typeCommunityInviteLink(invitationLink) await joinCommunityModal.submit() }) @@ -440,18 +446,25 @@ describe('Multiple Clients', () => { it('Guest registers new username', async () => { logger.info('TEST 5') const registerModal2 = new RegisterUsernameModal(users.user1.app.driver) - const isRegisterModal2 = await registerModal2.element.isDisplayed() - expect(isRegisterModal2).toBeTruthy() + expect(await registerModal2.isReady()).toBeTruthy() await registerModal2.typeUsername(users.user2.username) await registerModal2.submit() logger.time(`[${users.user1.app.name}] '${users.user2.username}' joining community time`) }) + it('Guest waits to join', async () => { + const joinPanel = new JoiningLoadingPanel(users.user1.app.driver) + await joinPanel.waitForJoinToComplete() + }) + // Check correct channels replication + // TODO: add check for number of messages it('User sees information about recreation general channel and see correct amount of messages', async () => { logger.info('TEST 6') generalChannelUser1 = new Channel(users.user1.app.driver, generalChannelName) - await generalChannelUser1.element.isDisplayed() + expect(await generalChannelUser1.isReady()).toBeTruthy() + expect(await generalChannelUser1.isOpen()).toBeTruthy() + expect(await generalChannelUser1.isMessageInputReady()).toBeTruthy() logger.timeEnd(`[${users.user1.app.name}] '${users.user2.username}' joining community time`) await sleep(10000) @@ -469,20 +482,20 @@ describe('Multiple Clients', () => { it('Guest sends a message after rejoining community as a new user and it is visible', async () => { logger.info('TEST 7') generalChannelUser1 = new Channel(users.user1.app.driver, generalChannelName) - await generalChannelUser1.element.isDisplayed() - const isMessageInput2 = await generalChannelUser1.messageInput.isDisplayed() - expect(isMessageInput2).toBeTruthy() - await sleep(5000) + expect(await generalChannelUser1.isReady()).toBeTruthy() + expect(await generalChannelUser1.isMessageInputReady()).toBeTruthy() await generalChannelUser1.sendMessage(users.user2.messages[0], users.user2.username) }) }) describe('Uploading and downloading files', () => { + let imageMessageIds: MessageIds | undefined = undefined + let fileMessageIds: MessageIds | undefined = undefined let largeFileMessageIds: MessageIds | undefined = undefined it('Owner uploads an image', async () => { const uploadFilePath = path.resolve(UPLOAD_FILE_DIR, TEST_IMAGE_FILE_NAME) - await generalChannelOwner.uploadFile( + imageMessageIds = await generalChannelOwner.uploadFile( TEST_IMAGE_FILE_NAME, uploadFilePath, UploadedFileType.IMAGE, @@ -491,8 +504,9 @@ describe('Multiple Clients', () => { }) it('Guest sees uploaded image', async () => { - await sleep(10_000) - await generalChannelUser1.getMessageIdsByFile( + expect(imageMessageIds).toBeDefined() + await generalChannelUser1.getMessageIdsByFileAndId( + imageMessageIds!, TEST_IMAGE_FILE_NAME, UploadedFileType.IMAGE, users.owner.username @@ -501,7 +515,7 @@ describe('Multiple Clients', () => { it('Owner uploads a file', async () => { const uploadFilePath = path.resolve(UPLOAD_FILE_DIR, TEST_FILE_NAME) - await generalChannelOwner.uploadFile( + fileMessageIds = await generalChannelOwner.uploadFile( TEST_FILE_NAME, uploadFilePath, UploadedFileType.FILE, @@ -510,13 +524,19 @@ describe('Multiple Clients', () => { }) it('Guest sees uploaded file and it downloads', async () => { - await generalChannelUser1.getMessageIdsByFile(TEST_FILE_NAME, UploadedFileType.FILE, users.owner.username) + expect(fileMessageIds).toBeDefined() + await generalChannelUser1.getMessageIdsByFileAndId( + fileMessageIds!, + TEST_FILE_NAME, + UploadedFileType.FILE, + users.owner.username + ) }) it('Owner uploads a large file', async () => { const uploadFilePath = path.resolve(UPLOAD_FILE_DIR, TEST_BIG_FILE_NAME) createArbitraryFile(uploadFilePath, BIG_FILE_SIZE) - await generalChannelOwner.uploadFile( + largeFileMessageIds = await generalChannelOwner.uploadFile( TEST_BIG_FILE_NAME, uploadFilePath, UploadedFileType.FILE, @@ -525,7 +545,9 @@ describe('Multiple Clients', () => { }) it(`Guest sees uploaded large file`, async () => { - largeFileMessageIds = await generalChannelUser1.getMessageIdsByFile( + expect(largeFileMessageIds).toBeDefined() + await generalChannelUser1.getMessageIdsByFileAndId( + largeFileMessageIds!, TEST_BIG_FILE_NAME, UploadedFileType.FILE, users.owner.username @@ -541,7 +563,6 @@ describe('Multiple Clients', () => { describe('Guest Closes App', () => { it('Owner closes app', async () => { await users.owner.app.close({ forceSaveState: true }) - await sleep(30000) }) it('Guest closes app', async () => { @@ -551,14 +572,14 @@ describe('Multiple Clients', () => { it('Owner re-opens app', async () => { await users.owner.app?.openWithRetries() - await sleep(30000) }) it('Owner sends another message after guest left the app and it is visible', async () => { logger.info('TEST 10') generalChannelOwner = new Channel(users.owner.app.driver, generalChannelName) - const isMessageInput = await generalChannelOwner.messageInput.isDisplayed() - expect(isMessageInput).toBeTruthy() + expect(await generalChannelOwner.isReady()).toBeTruthy() + expect(await generalChannelOwner.isOpen()).toBeTruthy() + expect(await generalChannelOwner.isMessageInputReady()).toBeTruthy() const messageIds = await generalChannelOwner.sendMessage(users.owner.messages[2], users.owner.username) }) }) diff --git a/packages/e2e-tests/src/tests/oneClient.test.ts b/packages/e2e-tests/src/tests/oneClient.test.ts index 0eb06034c..07af93d16 100644 --- a/packages/e2e-tests/src/tests/oneClient.test.ts +++ b/packages/e2e-tests/src/tests/oneClient.test.ts @@ -12,9 +12,8 @@ import getPort from 'get-port' import { fork } from 'child_process' import path from 'path' import { createLogger } from '../logger' -import { sleep } from '../utils' -import { UploadedFileType } from '../enums' -import { TEST_IMAGE_FILE_NAME, UPLOAD_FILE_DIR } from '../uploadFile.const' +import { SettingsModalTabName, UploadedFileType } from '../enums' +import { TEST_FILE_NAME, TEST_IMAGE_FILE_NAME, UPLOAD_FILE_DIR } from '../uploadFile.const' const logger = createLogger('oneClient') @@ -28,6 +27,9 @@ describe('One Client', () => { const generalChannelName = 'general' const ownerUserName = 'testuser' + const firstCommunityName = 'testcommunity' + const secondCommunityName = 'testcommunity-redux' + beforeAll(async () => { app = new App() await app.open() @@ -40,7 +42,6 @@ describe('One Client', () => { beforeEach(async () => { logger.info(`░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ${expect.getState().currentTestName}`) - await sleep(200) }) describe('User opens app for the first time', () => { @@ -51,57 +52,49 @@ describe('One Client', () => { }) it('User sees "join community" page and switches to "create community" view by clicking on the link', async () => { - const joinModal = new JoinCommunityModal(app.driver) - const isJoinModal = await joinModal.element.isDisplayed() - expect(isJoinModal).toBeTruthy() + const debugModal = new DebugModeModal(app.driver) + await debugModal.close() - if (!isJoinModal) { - const generalChannel = new Channel(app.driver, generalChannelName) - const isGeneralChannel = await generalChannel.element.isDisplayed() + const joinModal = new JoinCommunityModal(app.driver) + expect(await joinModal.isReady()).toBeTruthy() - expect(isGeneralChannel).toBeTruthy() - } else { - await joinModal.switchToCreateCommunity() - } + await joinModal.switchToCreateCommunity() }) it('User is on "Create community" page, enters valid community name and presses the button', async () => { const createModal = new CreateCommunityModal(app.driver) - const isCreateModal = await createModal.element.isDisplayed() - expect(isCreateModal).toBeTruthy() - await createModal.typeCommunityName('testcommunity') + expect(await createModal.isReady()).toBeTruthy() + + await createModal.typeCommunityName(firstCommunityName) await createModal.submit() }) it('User sees "register username" page, enters the valid name and submits by clicking on the button', async () => { const registerModal = new RegisterUsernameModal(app.driver) - const isRegisterModal = await registerModal.element.isDisplayed() + expect(await registerModal.isReady()).toBeTruthy() - expect(isRegisterModal).toBeTruthy() - logger.info('Registration - vefore typeUsername') + logger.info('Registration - before typeUsername') await registerModal.typeUsername(ownerUserName) logger.info('Registration - before submit') await registerModal.submit() logger.info('Registration - after submit') }) - it.skip('User waits for the modal JoiningLoadingPanel to disappear', async () => { + it('User waits for the modal JoiningLoadingPanel to disappear', async () => { const loadingPanelCommunity = new JoiningLoadingPanel(app.driver) - const isLoadingPanelCommunity = await loadingPanelCommunity.element.isDisplayed() - expect(isLoadingPanelCommunity).toBeTruthy() + await loadingPanelCommunity.waitForJoinToComplete() }) it('User sees general channel', async () => { generalChannel = new Channel(app.driver, generalChannelName) - const isGeneralChannel = await generalChannel.element.isDisplayed() + expect(await generalChannel.isReady()).toBeTruthy() + const generalChannelText = await generalChannel.element.getText() - expect(isGeneralChannel).toBeTruthy() expect(generalChannelText).toEqual(`# ${generalChannelName}`) }) it('User sends a message', async () => { - const isMessageInput = await generalChannel.messageInput.isDisplayed() - expect(isMessageInput).toBeTruthy() + expect(await generalChannel.isMessageInputReady()).toBeTruthy() await generalChannel.sendMessage('this shows up as sent', ownerUserName) }) }) @@ -132,8 +125,7 @@ describe('One Client', () => { it('User sees "general channel" page', async () => { const generalChannel = new Channel(app.driver, 'general') - const isGeneralChannel = await generalChannel.element.isDisplayed() - expect(isGeneralChannel).toBeTruthy() + expect(await generalChannel.isReady()).toBeTruthy() }) }) } @@ -141,10 +133,9 @@ describe('One Client', () => { describe('User leaves community and recreates it', () => { it('Leave community', async () => { const settingsModal = await new Sidebar(app.driver).openSettings() - const isSettingsModal = await settingsModal.element.isDisplayed() - expect(isSettingsModal).toBeTruthy() - await settingsModal.switchTab('leave-community') - await sleep(2000) + expect(await settingsModal.isReady()).toBeTruthy() + + await settingsModal.switchTab(SettingsModalTabName.LEAVE_COMMUNITY) await settingsModal.leaveCommunityButton() }) @@ -153,56 +144,44 @@ describe('One Client', () => { await debugModal.close() const joinModal = new JoinCommunityModal(app.driver) - const isJoinModal = await joinModal.element.isDisplayed() - expect(isJoinModal).toBeTruthy() - - if (!isJoinModal) { - const generalChannel = new Channel(app.driver, generalChannelName) - const isGeneralChannel = await generalChannel.element.isDisplayed() + expect(await joinModal.isReady()).toBeTruthy() - expect(isGeneralChannel).toBeTruthy() - } else { - await joinModal.switchToCreateCommunity() - } + await joinModal.switchToCreateCommunity() }) it('User is on "Create community" page, enters new valid community name and presses the button', async () => { const createModal = new CreateCommunityModal(app.driver) - const isCreateModal = await createModal.element.isDisplayed() - expect(isCreateModal).toBeTruthy() - await createModal.typeCommunityName('testcommunity1') + expect(await createModal.isReady()).toBeTruthy() + await createModal.typeCommunityName(secondCommunityName) await createModal.submit() }) it('User sees "register username" page, enters the valid name and submits by clicking on the button', async () => { const registerModal = new RegisterUsernameModal(app.driver) - const isRegisterModal = await registerModal.element.isDisplayed() - expect(isRegisterModal).toBeTruthy() - logger.info('Registration - vefore typeUsername') + expect(await registerModal.isReady()).toBeTruthy() + logger.info('Registration - before typeUsername') await registerModal.typeUsername(ownerUserName) logger.info('Registration - before submit') await registerModal.submit() logger.info('Registration - after submit') }) - it.skip('User waits for the modal JoiningLoadingPanel to disappear', async () => { + it('User waits for the modal JoiningLoadingPanel to disappear', async () => { const loadingPanelCommunity = new JoiningLoadingPanel(app.driver) - const isLoadingPanelCommunity = await loadingPanelCommunity.element.isDisplayed() - expect(isLoadingPanelCommunity).toBeTruthy() + await loadingPanelCommunity.waitForJoinToComplete() }) it('User sees general channel', async () => { generalChannel = new Channel(app.driver, generalChannelName) - const isGeneralChannel = await generalChannel.element.isDisplayed() + expect(await generalChannel.isReady()) + const generalChannelText = await generalChannel.element.getText() - expect(isGeneralChannel).toBeTruthy() expect(generalChannelText).toEqual(`# ${generalChannelName}`) }) it('User sends a message', async () => { - const isMessageInput = await generalChannel.messageInput.isDisplayed() - expect(isMessageInput).toBeTruthy() + expect(await generalChannel.isMessageInputReady()).toBeTruthy() await generalChannel.sendMessage('this shows up as sent again', ownerUserName) }) }) @@ -214,8 +193,8 @@ describe('One Client', () => { }) it('Owner uploads a non-image file', async () => { - const uploadFilePath = path.resolve(UPLOAD_FILE_DIR, TEST_IMAGE_FILE_NAME) - await generalChannel.uploadFile(TEST_IMAGE_FILE_NAME, uploadFilePath, UploadedFileType.FILE, ownerUserName) + const uploadFilePath = path.resolve(UPLOAD_FILE_DIR, TEST_FILE_NAME) + await generalChannel.uploadFile(TEST_FILE_NAME, uploadFilePath, UploadedFileType.FILE, ownerUserName) }) }) }) diff --git a/packages/e2e-tests/src/tests/userProfile.test.ts b/packages/e2e-tests/src/tests/userProfile.test.ts index 66b714398..dca77bb7d 100644 --- a/packages/e2e-tests/src/tests/userProfile.test.ts +++ b/packages/e2e-tests/src/tests/userProfile.test.ts @@ -5,14 +5,14 @@ import { Channel, CreateCommunityModal, JoinCommunityModal, + JoiningLoadingPanel, RegisterUsernameModal, Sidebar, UserProfileContextMenu, } from '../selectors' import { createLogger } from '../logger' import { EXPECTED_IMG_SRC_GIF, EXPECTED_IMG_SRC_JPEG, EXPECTED_IMG_SRC_PNG } from '../profilePhoto.const' -import { sleep } from '../utils' -import { X_DATA_TESTID } from '../enums' +import { PhotoExt, SettingsModalTabName, X_DATA_TESTID } from '../enums' import { UserTestData } from '../types' const logger = createLogger('userProfile') @@ -51,7 +51,6 @@ describe('User Profile Feature', () => { beforeEach(async () => { logger.info(`░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ ${expect.getState().currentTestName}`) - await sleep(1_000) }) it('Owner opens the app', async () => { @@ -60,38 +59,34 @@ describe('User Profile Feature', () => { it('Owner sees "join community" modal and switches to "create community" modal', async () => { const joinModal = new JoinCommunityModal(users.owner.app.driver) - const isJoinModal = await joinModal.element.isDisplayed() - expect(isJoinModal).toBeTruthy() + expect(await joinModal.isReady()).toBeTruthy() await joinModal.switchToCreateCommunity() }) it('Owner submits valid community name', async () => { const createModal = new CreateCommunityModal(users.owner.app.driver) - const isCreateModal = await createModal.element.isDisplayed() - expect(isCreateModal).toBeTruthy() + expect(await createModal.isReady()).toBeTruthy() await createModal.typeCommunityName(communityName) await createModal.submit() }) it('Owner sees "register username" modal and submits valid username', async () => { const registerModal = new RegisterUsernameModal(users.owner.app.driver) - const isRegisterModal = await registerModal.element.isDisplayed() - expect(isRegisterModal).toBeTruthy() + expect(await registerModal.isReady()).toBeTruthy() await registerModal.typeUsername(users.owner.username) await registerModal.submit() }) it('Owner registers successfully and sees general channel', async () => { generalChannelOwner = new Channel(users.owner.app.driver, 'general') - const isGeneralChannel = await generalChannelOwner.element.isDisplayed() + expect(await generalChannelOwner.isReady()) + const generalChannelText = await generalChannelOwner.element.getText() - expect(isGeneralChannel).toBeTruthy() expect(generalChannelText).toEqual('# general') }) it('Owner sends a message', async () => { - const isMessageInput = await generalChannelOwner.messageInput.isDisplayed() - expect(isMessageInput).toBeTruthy() + expect(await generalChannelOwner.isMessageInputReady()).toBeTruthy() await generalChannelOwner.sendMessage(users.owner.messages[0], users.owner.username) }) @@ -103,11 +98,13 @@ describe('User Profile Feature', () => { await menu.openEditProfileMenu() await menu.uploadJPEGPhoto() - const imgSrc = await menu.getProfilePhotoSrc() + const imgSrc = await menu.getProfilePhotoSrc(PhotoExt.JPEG) expect(imgSrc).toEqual(EXPECTED_IMG_SRC_JPEG) await menu.back(X_DATA_TESTID.EDIT_PROFILE) + await menu.isMenuReady() await menu.back(X_DATA_TESTID.PROFILE) + await generalChannelOwner.isMessageInputReady() } catch (e) { logger.error('Failed to set JPEG profile photo', e) throw e @@ -122,11 +119,13 @@ describe('User Profile Feature', () => { await menu.openEditProfileMenu() await menu.uploadGIFPhoto() - const imgSrc = await menu.getProfilePhotoSrc() + const imgSrc = await menu.getProfilePhotoSrc(PhotoExt.GIF) expect(imgSrc).toEqual(EXPECTED_IMG_SRC_GIF) await menu.back(X_DATA_TESTID.EDIT_PROFILE) + await menu.isMenuReady() await menu.back(X_DATA_TESTID.PROFILE) + await generalChannelOwner.isMessageInputReady() } catch (e) { logger.error('Failed to set GIF profile photo', e) throw e @@ -141,11 +140,13 @@ describe('User Profile Feature', () => { await menu.openEditProfileMenu() await menu.uploadPNGPhoto() - const imgSrc = await menu.getProfilePhotoSrc() + const imgSrc = await menu.getProfilePhotoSrc(PhotoExt.PNG) expect(imgSrc).toEqual(EXPECTED_IMG_SRC_PNG) await menu.back(X_DATA_TESTID.EDIT_PROFILE) + await menu.isMenuReady() await menu.back(X_DATA_TESTID.PROFILE) + await generalChannelOwner.isMessageInputReady() } catch (e) { logger.error('Failed to set PNG profile photo', e) throw e @@ -154,14 +155,10 @@ describe('User Profile Feature', () => { it('Owner opens the settings tab and gets an invitation link', async () => { const settingsModal = await new Sidebar(users.owner.app.driver).openSettings() - const isSettingsModal = await settingsModal.element.isDisplayed() - expect(isSettingsModal).toBeTruthy() - await sleep(2000) - await settingsModal.switchTab('invite') - await sleep(2000) + expect(await settingsModal.isReady()).toBeTruthy() + await settingsModal.switchTab(SettingsModalTabName.INVITE) const invitationLinkElement = await settingsModal.invitationLink() invitationLink = await invitationLinkElement.getText() - await sleep(2000) expect(invitationLink).not.toBeUndefined() logger.info('Received invitation link:', invitationLink) await settingsModal.closeTabThenModal() @@ -173,26 +170,28 @@ describe('User Profile Feature', () => { it('First user submits invitation link received from owner', async () => { const joinCommunityModal = new JoinCommunityModal(users.user1.app.driver) - const isJoinCommunityModal = await joinCommunityModal.element.isDisplayed() - expect(isJoinCommunityModal).toBeTruthy() + expect(await joinCommunityModal.isReady()).toBeTruthy() await joinCommunityModal.typeCommunityInviteLink(invitationLink) await joinCommunityModal.submit() }) it('First user submits valid username', async () => { const registerModal = new RegisterUsernameModal(users.user1.app.driver) - const isRegisterModal = await registerModal.element.isDisplayed() - expect(isRegisterModal).toBeTruthy() + expect(await registerModal.isReady()).toBeTruthy() await registerModal.clearInput() await registerModal.typeUsername(users.user1.username) await registerModal.submit() }) + it('First user waits to join the community', async () => { + const joinPanel = new JoiningLoadingPanel(users.user1.app.driver) + await joinPanel.waitForJoinToComplete() + }) + it('First user joins successfully sees general channel', async () => { generalChannelUser1 = new Channel(users.user1.app.driver, 'general') - await generalChannelUser1.element.isDisplayed() - const isMessageInput2 = await generalChannelUser1.messageInput.isDisplayed() - expect(isMessageInput2).toBeTruthy() + expect(await generalChannelUser1.isReady()).toBeTruthy() + expect(await generalChannelUser1.isMessageInputReady()).toBeTruthy() }) it("First user sees owner's message with profile photo", async () => { @@ -201,13 +200,13 @@ describe('User Profile Feature', () => { if (!elem) { fail('Failed to find at least 2 messages') } - await users.user1.app.driver.wait(until.elementIsVisible(elem)) + await users.user1.app.driver.wait(until.elementIsVisible(elem), 10_000) const text = await elem.getText() expect(text).toEqual(users.owner.messages[0]) const fullMessages = await generalChannelUser1.getUserMessagesFull(users.owner.username) const img = await fullMessages[1].findElement(By.tagName('img')) - await users.user1.app.driver.wait(until.elementIsVisible(img)) + await users.user1.app.driver.wait(until.elementIsVisible(img), 10_000) const imgSrc = await img.getAttribute('src') expect(imgSrc).toEqual(EXPECTED_IMG_SRC_PNG) }) diff --git a/packages/e2e-tests/src/utils.ts b/packages/e2e-tests/src/utils.ts index b8a727fe3..7c1100f2b 100644 --- a/packages/e2e-tests/src/utils.ts +++ b/packages/e2e-tests/src/utils.ts @@ -370,6 +370,11 @@ export class Timeout { } public clear(): void { + if (this.id == null) { + logger.warn(`Timeout already cleared!`) + return + } + clearTimeout(this.id as NodeJS.Timeout) this.id = undefined } @@ -384,6 +389,7 @@ export const promiseWithTimeout = async ( const timeout = new Timeout() try { const result: T = await timeout.wrap(promise, timeoutMs, reason) + timeout.clear() return result } catch (e) { if (e.message === reason) {