diff --git a/.eslintrc.js b/.eslintrc.js index 3322e35..5bb1d0c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,7 +9,7 @@ module.exports = { extends: ['airbnb-base', 'airbnb-typescript/base', 'prettier', 'plugin:@typescript-eslint/recommended'], - plugins: ['mocha', 'more', '@typescript-eslint'], + plugins: ['mocha', 'more', '@typescript-eslint', 'perfectionist'], parser: '@typescript-eslint/parser', parserOptions: { project: ['./tsconfig.json'] }, @@ -54,6 +54,36 @@ module.exports = { 'import/order': 'off', 'no-useless-catch': 'off', + 'perfectionist/sort-imports': 'error', + 'perfectionist/sort-named-imports': 'error', + 'perfectionist/sort-classes': [ + 'error', + { + partitionByComment: true, + }, + ], + 'perfectionist/sort-union-types': [ + 'error', + { + // This ensures null/undefined come after other types for better readability + groups: [ + 'named', + 'keyword', + 'operator', + 'literal', + 'function', + 'import', + 'conditional', + 'object', + 'tuple', + 'intersection', + 'union', + 'nullish', + ], + } + ], + + quotes: [ 'error', 'single', diff --git a/.gitignore b/.gitignore index 19ff287..80bf8e8 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,5 @@ output/ dist/ .eslintcache *-difference.png +*-current*.png +*-diff*.png diff --git a/eslint.config.mjs b/eslint.config.mjs index 9a033b2..f5e4e6d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,5 +1,5 @@ import eslint from '@eslint/js'; -import tseslint from 'typescript-eslint'; +import tseslint from '@typescript-eslint/eslint-plugin'; import globals from 'globals'; export default tseslint.config( diff --git a/global.setup.ts b/global.setup.ts index a779208..3d83252 100644 --- a/global.setup.ts +++ b/global.setup.ts @@ -1,9 +1,10 @@ import { readdirSync, rm } from 'fs-extra'; +import { isEmpty } from 'lodash'; import { homedir } from 'os'; import { join } from 'path'; + import { MULTI_PREFIX, NODE_ENV } from './tests/automation/setup/open'; import { isLinux, isMacOS } from './tests/os_utils'; -import { isEmpty } from 'lodash'; const getDirectoriesOfSessionDataPath = (source: string) => readdirSync(source, { withFileTypes: true }) diff --git a/package.json b/package.json index 1649382..69022bc 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "eslint-plugin-import": "^2.28.0", "eslint-plugin-mocha": "^10.1.0", "eslint-plugin-more": "^1.0.5", + "eslint-plugin-perfectionist": "^4.15.0", "fs-extra": "^11.1.1", "lodash": "^4.17.21", "prettier": "^3.0.1", diff --git a/playwright.config.ts b/playwright.config.ts index a274990..e48316b 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from '@playwright/test'; +import dotenv from 'dotenv'; import { toNumber } from 'lodash'; -import dotenv from 'dotenv'; import { screenshotFolder } from './tests/automation/constants/variables'; dotenv.config(); diff --git a/screenshots/Change-avatar/avatar-updated-blue-darwin.jpeg b/screenshots/Change-avatar/avatar-updated-blue-darwin.jpeg index 0d83659..cba87b7 100644 --- a/screenshots/Change-avatar/avatar-updated-blue-darwin.jpeg +++ b/screenshots/Change-avatar/avatar-updated-blue-darwin.jpeg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02b450cb82f4004714d796c79a45787329fc1dc68f6c371795bac9664e107f1e -size 944 +oid sha256:e9069447a34a67f2695f041f31e4902b72651c4162d733086635ea689fb7492b +size 1262 diff --git a/screenshots/Change-avatar/avatar-updated-blue-linux.jpeg b/screenshots/Change-avatar/avatar-updated-blue-linux.jpeg index ca272e6..3f21393 100644 --- a/screenshots/Change-avatar/avatar-updated-blue-linux.jpeg +++ b/screenshots/Change-avatar/avatar-updated-blue-linux.jpeg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22c4440e25da54a94ee286bcf20916d2a9e11e5db4236b46d76d84eb4b8c2f10 -size 882 +oid sha256:cd339a84c915a8d2fb312166f96ce4c833ee36206b552b5d83430da53355d0bb +size 1255 diff --git a/screenshots/Profile-picture-syncs/avatar-updated-blue-darwin.jpeg b/screenshots/Profile-picture-syncs/avatar-updated-blue-darwin.jpeg index 0d83659..cba87b7 100644 --- a/screenshots/Profile-picture-syncs/avatar-updated-blue-darwin.jpeg +++ b/screenshots/Profile-picture-syncs/avatar-updated-blue-darwin.jpeg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02b450cb82f4004714d796c79a45787329fc1dc68f6c371795bac9664e107f1e -size 944 +oid sha256:e9069447a34a67f2695f041f31e4902b72651c4162d733086635ea689fb7492b +size 1262 diff --git a/screenshots/Profile-picture-syncs/avatar-updated-blue-linux.jpeg b/screenshots/Profile-picture-syncs/avatar-updated-blue-linux.jpeg index 581a066..3f21393 100644 --- a/screenshots/Profile-picture-syncs/avatar-updated-blue-linux.jpeg +++ b/screenshots/Profile-picture-syncs/avatar-updated-blue-linux.jpeg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:afd7a7acb7fcd504f311c3258d64c0940bc5f0f429b59c5e613df291f91dfd58 -size 964 +oid sha256:cd339a84c915a8d2fb312166f96ce4c833ee36206b552b5d83430da53355d0bb +size 1255 diff --git a/screenshots/landing-page-states/new-account-darwin.png b/screenshots/landing-page-states/new-account-darwin.png deleted file mode 100644 index b1843cc..0000000 --- a/screenshots/landing-page-states/new-account-darwin.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9d0da82b7711b507b623243fa6fc43de1272a2efe402228fd4f899dbb37d2bbd -size 95444 diff --git a/screenshots/landing-page-states/new-account-linux.png b/screenshots/landing-page-states/new-account-linux.png deleted file mode 100644 index 6e22594..0000000 --- a/screenshots/landing-page-states/new-account-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f73c838496f090bc81d5d958febb45c8da18db5a99647147735d61b22731e9d3 -size 53875 diff --git a/screenshots/landing-page-states/restored-account-darwin.png b/screenshots/landing-page-states/restored-account-darwin.png deleted file mode 100644 index b522c91..0000000 --- a/screenshots/landing-page-states/restored-account-darwin.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5af2a09db18e7161950f986d6f9aad001d3098dae95d4e5d2f703bad39712d69 -size 52581 diff --git a/screenshots/landing-page-states/restored-account-linux.png b/screenshots/landing-page-states/restored-account-linux.png deleted file mode 100644 index 69e3e88..0000000 --- a/screenshots/landing-page-states/restored-account-linux.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d1ecac5cd73fa59eb7baa3e30313b15edfffb5f14b76145fce00ecbccbd69fd6 -size 29276 diff --git a/sessionReporter.ts b/sessionReporter.ts index e153a06..4c6e4ef 100644 --- a/sessionReporter.ts +++ b/sessionReporter.ts @@ -7,6 +7,7 @@ import type { TestError, TestResult, } from '@playwright/test/reporter'; + import chalk from 'chalk'; import { Dictionary, groupBy, isString, mean, sortBy } from 'lodash'; @@ -74,14 +75,14 @@ function printFailedTestLogs() { } class SessionReporter implements Reporter { - private startTime = 0; + private allResults: Array = []; private allTestsCount = 0; - private allResults: Array = []; - private countWorkers = 1; + private startTime = 0; + onBegin(config: FullConfig, suite: Suite) { this.allTestsCount = suite.allTests().length; this.countWorkers = config.workers; @@ -92,6 +93,56 @@ class SessionReporter implements Reporter { this.startTime = Date.now(); } + onEnd(result: FullResult) { + console.log( + chalk.bgWhiteBright.black( + `\n\n\n\t\tFinished the run: ${result.status}, count of tests run: ${ + this.allResults.length + }, took ${Math.floor( + (Date.now() - this.startTime) / (60 * 1000), + )} minute(s)`, + ), + ); + const { allFailedSoFar, allPassedSoFar, partiallyPassed } = + this.groupResultsByTestName(); + + sortByTitle(allPassedSoFar).forEach((m) => formatGroupedByResults(m)); + sortByTitle(partiallyPassed).forEach((m) => formatGroupedByResults(m)); + sortByTitle(allFailedSoFar).forEach((m) => formatGroupedByResults(m)); + } + + onError?(error: TestError) { + console.info('global error:', error); + } + + onStdErr?( + chunk: Buffer | string, + test: TestCase | void, + _result: TestResult | void, + ) { + if (printOngoingTestLogs()) { + process.stdout.write( + `"${test ? `${chalk.cyanBright(test.title)}` : ''}":err: ${ + isString(chunk) ? chunk : chunk.toString('utf-8') + }`, + ); + } + } + + onStdOut?( + chunk: Buffer | string, + test: TestCase | void, + _result: TestResult | void, + ) { + if (printOngoingTestLogs()) { + process.stdout.write( + `"${test ? `${chalk.cyanBright(test.title)}` : ''}": ${ + isString(chunk) ? chunk : chunk.toString('utf-8') + }`, + ); + } + } + onTestBegin(test: TestCase, result: TestResult) { console.log( chalk.magenta( @@ -200,56 +251,6 @@ class SessionReporter implements Reporter { ), }; } - - onEnd(result: FullResult) { - console.log( - chalk.bgWhiteBright.black( - `\n\n\n\t\tFinished the run: ${result.status}, count of tests run: ${ - this.allResults.length - }, took ${Math.floor( - (Date.now() - this.startTime) / (60 * 1000), - )} minute(s)`, - ), - ); - const { allFailedSoFar, allPassedSoFar, partiallyPassed } = - this.groupResultsByTestName(); - - sortByTitle(allPassedSoFar).forEach((m) => formatGroupedByResults(m)); - sortByTitle(partiallyPassed).forEach((m) => formatGroupedByResults(m)); - sortByTitle(allFailedSoFar).forEach((m) => formatGroupedByResults(m)); - } - - onStdOut?( - chunk: string | Buffer, - test: void | TestCase, - _result: void | TestResult, - ) { - if (printOngoingTestLogs()) { - process.stdout.write( - `"${test ? `${chalk.cyanBright(test.title)}` : ''}": ${ - isString(chunk) ? chunk : chunk.toString('utf-8') - }`, - ); - } - } - - onStdErr?( - chunk: string | Buffer, - test: void | TestCase, - _result: void | TestResult, - ) { - if (printOngoingTestLogs()) { - process.stdout.write( - `"${test ? `${chalk.cyanBright(test.title)}` : ''}":err: ${ - isString(chunk) ? chunk : chunk.toString('utf-8') - }`, - ); - } - } - - onError?(error: TestError) { - console.info('global error:', error); - } } export default SessionReporter; diff --git a/tests/automation/call_checks.spec.ts b/tests/automation/call_checks.spec.ts index 9bf75d3..d4216a9 100644 --- a/tests/automation/call_checks.spec.ts +++ b/tests/automation/call_checks.spec.ts @@ -8,7 +8,7 @@ test_Alice_1W_Bob_1W( 'Voice calls', async ({ alice, aliceWindow1, bob, bobWindow1 }) => { await createContact(aliceWindow1, bobWindow1, alice, bob); - await makeVoiceCall(aliceWindow1, bobWindow1, alice, bob); + await makeVoiceCall(aliceWindow1, bobWindow1); // In the receivers window, the message is 'Call in progress' await waitForTestIdWithText( bobWindow1, diff --git a/tests/automation/community_tests.spec.ts b/tests/automation/community_tests.spec.ts index 28cf523..694ca49 100644 --- a/tests/automation/community_tests.spec.ts +++ b/tests/automation/community_tests.spec.ts @@ -1,19 +1,20 @@ import { testCommunityName } from './constants/community'; +import { Conversation, HomeScreen } from './locators'; import { test_Alice_1W_Bob_1W, test_Alice_2W } from './setup/sessionTest'; import { joinCommunity } from './utilities/join_community'; import { sendMessage } from './utilities/message'; import { replyTo } from './utilities/reply_message'; import { sendMedia } from './utilities/send_media'; -import { clickOnTestIdWithText } from './utilities/utils'; +import { clickOn, clickOnWithText } from './utilities/utils'; test_Alice_2W('Join community', async ({ aliceWindow1, aliceWindow2 }) => { await joinCommunity(aliceWindow1); - await clickOnTestIdWithText(aliceWindow1, 'scroll-to-bottom-button'); + await clickOn(aliceWindow1, Conversation.scrollToBottomButton); await sendMessage(aliceWindow1, 'Hello, community!'); // Check linked device for community - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, testCommunityName, ); }); @@ -29,15 +30,16 @@ test_Alice_1W_Bob_1W( // waitForLoadingAnimationToFinish(aliceWindow1, 'loading-spinner'), // waitForLoadingAnimationToFinish(bobWindow1, 'loading-spinner'), // ]); - await Promise.all([ - clickOnTestIdWithText(aliceWindow1, 'scroll-to-bottom-button'), - clickOnTestIdWithText(bobWindow1, 'scroll-to-bottom-button'), - ]); + await Promise.all( + [aliceWindow1, bobWindow1].map((window) => + clickOn(window, Conversation.scrollToBottomButton), + ), + ); await sendMessage(aliceWindow1, testMessage); // Check linked device for community - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, testCommunityName, ); await sendMedia( diff --git a/tests/automation/constants/variables.ts b/tests/automation/constants/variables.ts index 471638f..198d235 100644 --- a/tests/automation/constants/variables.ts +++ b/tests/automation/constants/variables.ts @@ -1,8 +1,8 @@ import { - DMTimeOption, DisappearActions, DisappearGroupType, DisappearType, + DMTimeOption, MediaType, } from '../types/testing'; @@ -42,7 +42,7 @@ export const mediaArray = [ type DisappearingOption = { timeOption: DMTimeOption; - disappearingMessagesType: DisappearType | DisappearGroupType; + disappearingMessagesType: DisappearGroupType | DisappearType; disappearAction: DisappearActions; }; diff --git a/tests/automation/create_user.spec.ts b/tests/automation/create_user.spec.ts index cebc6a9..e093043 100644 --- a/tests/automation/create_user.spec.ts +++ b/tests/automation/create_user.spec.ts @@ -1,30 +1,36 @@ import { sleepFor } from '../promise_utils'; +import { Global, LeftPane, Settings } from './locators'; import { newUser } from './setup/new_user'; import { sessionTestOneWindow } from './setup/sessionTest'; -import { - clickOnTestIdWithText, - waitForTestIdWithText, -} from './utilities/utils'; +import { clickOn, waitForTestIdWithText } from './utilities/utils'; sessionTestOneWindow('Create User', async ([window]) => { // Create User const userA = await newUser(window, 'Alice', false); // Open profile tab - await clickOnTestIdWithText(window, 'leftpane-primary-avatar'); + await clickOn(window, LeftPane.profileButton); await sleepFor(100, true); // check username matches - await waitForTestIdWithText(window, 'your-profile-name', userA.userName); + await waitForTestIdWithText( + window, + Settings.displayName.selector, + userA.userName, + ); // check Account ID matches - await waitForTestIdWithText(window, 'your-account-id', userA.accountid); + await waitForTestIdWithText( + window, + Settings.accountId.selector, + userA.accountid, + ); // exit profile modal - await clickOnTestIdWithText(window, 'modal-close-button'); + await clickOn(window, Global.modalCloseButton); // go to settings section - await clickOnTestIdWithText(window, 'settings-section'); + await clickOn(window, LeftPane.settingsButton); // check recovery phrase matches - await clickOnTestIdWithText(window, 'recovery-password-settings-menu-item'); + await clickOn(window, Settings.recoveryPasswordMenuItem); await waitForTestIdWithText( window, - 'recovery-password-seed-modal', + Settings.recoveryPasswordContainer.selector, userA.recoveryPassword, ); }); diff --git a/tests/automation/delete_account.spec.ts b/tests/automation/delete_account.spec.ts index e5a16cc..fa0a372 100644 --- a/tests/automation/delete_account.spec.ts +++ b/tests/automation/delete_account.spec.ts @@ -1,22 +1,24 @@ import { Page } from '@playwright/test'; + +import { englishStrippedStr } from '../localization/englishStrippedStr'; import { sleepFor } from '../promise_utils'; +import { Global, HomeScreen, LeftPane, Onboarding, Settings } from './locators'; import { forceCloseAllWindows } from './setup/closeWindows'; import { newUser } from './setup/new_user'; import { openApp } from './setup/open'; +import { recoverFromSeed } from './setup/recovery_using_seed'; import { sessionTestTwoWindows } from './setup/sessionTest'; import { createContact } from './utilities/create_contact'; import { sendNewMessage } from './utilities/send_message'; import { - clickOnElement, + clickOn, clickOnMatchingText, - clickOnTestIdWithText, + clickOnWithText, hasElementBeenDeleted, typeIntoInput, waitForElement, waitForLoadingAnimationToFinish, } from './utilities/utils'; -import { recoverFromSeed } from './setup/recovery_using_seed'; -import { englishStrippedStr } from '../localization/englishStrippedStr'; sessionTestTwoWindows( 'Delete account from swarm', @@ -36,17 +38,17 @@ sessionTestTwoWindows( ]); // Delete all data from device // Click on settings tab - await clickOnTestIdWithText(windowA, 'settings-section'); + await clickOn(windowA, LeftPane.settingsButton); // Click on clear all data - await clickOnTestIdWithText( + await clickOnWithText( windowA, - 'clear-data-settings-menu-item', + Settings.clearDataMenuItem, englishStrippedStr('sessionClearData').toString(), ); // Select entire account - await clickOnTestIdWithText( + await clickOnWithText( windowA, - 'label-device_and_network', + Settings.clearDeviceAndNetworkRadial, englishStrippedStr('clearDeviceAndNetwork').toString(), ); // Confirm deletion by clicking Clear, twice @@ -66,15 +68,15 @@ sessionTestTwoWindows( restoringWindows = await openApp(1); // not using sessionTest here as we need to close and reopen one of the window const [restoringWindow] = restoringWindows; // Sign in with deleted account and check that nothing restores - await clickOnTestIdWithText(restoringWindow, 'existing-account-button'); + await clickOn(restoringWindow, Onboarding.iHaveAnAccountButton); // Fill in recovery phrase await typeIntoInput( restoringWindow, - 'recovery-phrase-input', + Onboarding.recoveryPhraseInput.selector, userA.recoveryPassword, ); // Enter display name - await clickOnTestIdWithText(restoringWindow, 'continue-button'); + await clickOn(restoringWindow, Global.continueButton); await waitForLoadingAnimationToFinish( restoringWindow, 'loading-animation', @@ -82,11 +84,11 @@ sessionTestTwoWindows( await typeIntoInput( restoringWindow, - 'display-name-input', + Onboarding.displayNameInput.selector, userA.userName, ); // Click continue - await clickOnTestIdWithText(restoringWindow, 'continue-button'); + await clickOn(restoringWindow, Global.continueButton); await sleepFor(5000, true); // just to allow any messages from our swarm to show up // Need to verify that no conversation is found at all @@ -94,15 +96,15 @@ sessionTestTwoWindows( await hasElementBeenDeleted( restoringWindow, 'data-testid', - 'conversation-list-item', + HomeScreen.conversationItemName.selector, ); - await clickOnTestIdWithText(restoringWindow, 'new-conversation-button'); // Expect contacts list to be empty + await clickOn(restoringWindow, HomeScreen.plusButton); // Expect contacts list to be empty await hasElementBeenDeleted( restoringWindow, 'data-testid', - 'module-conversation__user_profile', + Global.contactItem.selector, 10000, ); } finally { @@ -126,11 +128,11 @@ sessionTestTwoWindows( await createContact(windowA, windowB, userA, userB); // Delete all data from device // Click on settings tab - await clickOnTestIdWithText(windowA, 'settings-section'); + await clickOn(windowA, LeftPane.settingsButton); // Click on clear all data - await clickOnTestIdWithText( + await clickOnWithText( windowA, - 'clear-data-settings-menu-item', + Settings.clearDataMenuItem, englishStrippedStr('sessionClearData').toString(), ); // Keep 'Clear Device only' selection @@ -143,9 +145,6 @@ sessionTestTwoWindows( windowA, englishStrippedStr('clear').toString(), ); - await waitForLoadingAnimationToFinish(windowA, 'loading-spinner'); - // Wait for window to close and reopen - // await windowA.close(); restoringWindows = await openApp(1); const [restoringWindow] = restoringWindows; // Sign in with deleted account and check that nothing restores @@ -155,20 +154,16 @@ sessionTestTwoWindows( await waitForElement( restoringWindow, 'data-testid', - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName.selector, 10000, userB.userName, ); // Check if contact is available in contacts section - await clickOnElement({ - window: restoringWindow, - strategy: 'data-testid', - selector: 'new-conversation-button', - }); + await clickOn(restoringWindow, HomeScreen.plusButton); await waitForElement( restoringWindow, 'data-testid', - 'module-conversation__user__profile-name', + Global.contactItem.selector, 1000, userB.userName, ); diff --git a/tests/automation/disappearing_message_checks.spec.ts b/tests/automation/disappearing_message_checks.spec.ts index 3a4c3e4..fec8a6b 100644 --- a/tests/automation/disappearing_message_checks.spec.ts +++ b/tests/automation/disappearing_message_checks.spec.ts @@ -7,6 +7,12 @@ import { mediaArray, testLink, } from './constants/variables'; +import { + Conversation, + ConversationSettings, + Global, + HomeScreen, +} from './locators'; import { test_Alice_1W_Bob_1W } from './setup/sessionTest'; import { createContact } from './utilities/create_contact'; import { joinCommunity } from './utilities/join_community'; @@ -19,8 +25,8 @@ import { } from './utilities/send_media'; import { setDisappearingMessages } from './utilities/set_disappearing_messages'; import { - clickOnElement, - clickOnTestIdWithText, + clickOn, + clickOnWithText, formatTimeOption, hasElementBeenDeleted, hasTextMessageBeenDeleted, @@ -52,7 +58,7 @@ mediaArray.forEach(({ mediaType, path, attachmentType }) => { await Promise.all([ waitForTestIdWithText( aliceWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSetYou') .withArgs({ time: formattedTime, @@ -62,7 +68,7 @@ mediaArray.forEach(({ mediaType, path, attachmentType }) => { ), waitForTestIdWithText( bobWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSet') .withArgs({ time: formattedTime, @@ -110,7 +116,7 @@ test_Alice_1W_Bob_1W( await Promise.all([ waitForTestIdWithText( aliceWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSetYou') .withArgs({ time: formattedTime, @@ -120,7 +126,7 @@ test_Alice_1W_Bob_1W( ), waitForTestIdWithText( bobWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSet') .withArgs({ time: formattedTime, @@ -132,11 +138,7 @@ test_Alice_1W_Bob_1W( ]); await typeIntoInput(aliceWindow1, 'message-input-text-area', longText); await sleepFor(100); - await clickOnElement({ - window: aliceWindow1, - strategy: 'data-testid', - selector: 'send-message-button', - }); + await clickOn(aliceWindow1, Conversation.sendMessageButton); await waitForSentTick(aliceWindow1, longText); await waitForTextMessage(bobWindow1, longText); // Wait 30 seconds for long text to disappear @@ -159,7 +161,7 @@ test_Alice_1W_Bob_1W( await Promise.all([ waitForTestIdWithText( aliceWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSetYou') .withArgs({ time: formattedTime, @@ -169,7 +171,7 @@ test_Alice_1W_Bob_1W( ), waitForTestIdWithText( bobWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSet') .withArgs({ time: formattedTime, @@ -213,7 +215,7 @@ test_Alice_1W_Bob_1W( await Promise.all([ waitForTestIdWithText( aliceWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSetYou') .withArgs({ time: formattedTime, @@ -223,7 +225,7 @@ test_Alice_1W_Bob_1W( ), waitForTestIdWithText( bobWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSet') .withArgs({ time: formattedTime, @@ -236,24 +238,24 @@ test_Alice_1W_Bob_1W( await joinCommunity(aliceWindow1); // To stop the layout shift await sleepFor(500); - await clickOnTestIdWithText(aliceWindow1, 'conversation-options-avatar'); - await clickOnTestIdWithText(aliceWindow1, 'invite-contacts-menu-option'); + await clickOn(aliceWindow1, Conversation.conversationSettingsIcon); + await clickOn(aliceWindow1, ConversationSettings.inviteContactsOption); await waitForTestIdWithText( aliceWindow1, 'modal-heading', englishStrippedStr('membersInvite').toString(), ); - await clickOnTestIdWithText(aliceWindow1, 'contact', bob.userName); - await clickOnTestIdWithText(aliceWindow1, 'session-confirm-ok-button'); + await clickOnWithText(aliceWindow1, Global.contactItem, bob.userName); + await clickOn(aliceWindow1, Global.confirmButton); // For lack of a unique ID we use native Playwright methods await aliceWindow1 .getByTestId('invite-contacts-dialog') .getByTestId('modal-close-button') .click(); - await clickOnTestIdWithText(aliceWindow1, 'modal-close-button'); - await clickOnTestIdWithText( + await clickOn(aliceWindow1, Global.modalCloseButton); + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); await Promise.all([ @@ -307,7 +309,7 @@ test_Alice_1W_Bob_1W( await Promise.all([ waitForTestIdWithText( aliceWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSetYou') .withArgs({ time: formattedTime, @@ -317,7 +319,7 @@ test_Alice_1W_Bob_1W( ), waitForTestIdWithText( bobWindow1, - 'disappear-control-message', + Conversation.disappearingControlMessage.selector, englishStrippedStr('disappearingMessagesSet') .withArgs({ time: formattedTime, @@ -327,7 +329,7 @@ test_Alice_1W_Bob_1W( .toString(), ), ]); - await makeVoiceCall(aliceWindow1, bobWindow1, alice, bob); + await makeVoiceCall(aliceWindow1, bobWindow1); // In the receivers window, the message is 'Call in progress' await Promise.all([ waitForTestIdWithText( diff --git a/tests/automation/disappearing_messages.spec.ts b/tests/automation/disappearing_messages.spec.ts index 6f80c5c..a173ac2 100644 --- a/tests/automation/disappearing_messages.spec.ts +++ b/tests/automation/disappearing_messages.spec.ts @@ -1,6 +1,7 @@ import { englishStrippedStr } from '../localization/englishStrippedStr'; import { sleepFor } from '../promise_utils'; import { defaultDisappearingOptions } from './constants/variables'; +import { Conversation, HomeScreen } from './locators'; import { test_Alice_2W, test_Alice_2W_Bob_1W, @@ -11,9 +12,10 @@ import { sendMessage } from './utilities/message'; import { sendNewMessage } from './utilities/send_message'; import { setDisappearingMessages } from './utilities/set_disappearing_messages'; import { + clickOn, clickOnElement, clickOnMatchingText, - clickOnTestIdWithText, + clickOnWithText, doesTextIncludeString, formatTimeOption, hasElementBeenDeleted, @@ -42,9 +44,9 @@ test_Alice_2W_Bob_1W( // Create Contact await createContact(aliceWindow1, bobWindow1, alice, bob); // Click on conversation in linked device - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); @@ -76,11 +78,7 @@ test_Alice_2W_Bob_1W( const message = 'Forcing window to front'; await typeIntoInput(bobWindow1, 'message-input-text-area', message); // click up arrow (send) - await clickOnElement({ - window: bobWindow1, - strategy: 'data-testid', - selector: 'send-message-button', - }); + await clickOn(bobWindow1, Conversation.sendMessageButton); await sleepFor(10000); await hasTextMessageBeenDeleted(bobWindow1, testMessage); }, @@ -106,9 +104,9 @@ test_Alice_2W_Bob_1W( await createContact(aliceWindow1, bobWindow1, alice, bob); // Click on conversation in linked device - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); await setDisappearingMessages( @@ -160,9 +158,9 @@ test_group_Alice_2W_Bob_1W_Charlie_1W( .toString(); const testMessage = 'Testing disappearing messages in groups'; - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); await setDisappearingMessages(aliceWindow1, [ @@ -213,9 +211,9 @@ test_Alice_2W( // Open Note to self conversation await sendNewMessage(aliceWindow1, alice.accountid, testMessage); // Check messages are syncing across linked devices - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, englishStrippedStr('noteToSelf').toString(), ); await waitForTextMessage(aliceWindow2, testMessage); @@ -235,8 +233,8 @@ test_Alice_2W( await sendMessage(aliceWindow1, testMessageDisappear); await waitForTextMessage(aliceWindow2, testMessageDisappear); await Promise.all([ - hasTextMessageBeenDeleted(aliceWindow1, testMessageDisappear), - hasTextMessageBeenDeleted(aliceWindow2, testMessageDisappear), + hasTextMessageBeenDeleted(aliceWindow1, testMessageDisappear, 10_000), + hasTextMessageBeenDeleted(aliceWindow2, testMessageDisappear, 10_000), ]); }, ); @@ -250,9 +248,9 @@ test_Alice_2W_Bob_1W( const formattedTime = formatTimeOption(timeOption); await createContact(aliceWindow1, bobWindow1, alice, bob); // Click on conversation on linked device - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); // Set disappearing messages to on @@ -301,13 +299,9 @@ test_Alice_2W_Bob_1W( waitForTextMessage(bobWindow1, testMessage), waitForTextMessage(aliceWindow2, testMessage), ]); - await clickOnTestIdWithText( - aliceWindow1, - 'conversation-options-avatar', - undefined, - undefined, - 1000, - ); + await clickOn(aliceWindow1, Conversation.conversationSettingsIcon, { + maxWait: 1_000, + }); await clickOnElement({ window: aliceWindow1, strategy: 'data-testid', diff --git a/tests/automation/enforce_localized_str.spec.ts b/tests/automation/enforce_localized_str.spec.ts index a2617ea..ec9dc63 100644 --- a/tests/automation/enforce_localized_str.spec.ts +++ b/tests/automation/enforce_localized_str.spec.ts @@ -206,7 +206,7 @@ function getExpectedStringFromKey( case 'callsVoiceAndVideoBeta': return 'Voice and Video Calls (Beta)'; case 'callsVoiceAndVideoModalDescription': - return 'Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls.'; + return 'Your IP is visible to your call partner and a Session Foundation server while using beta calls.'; case 'blockDescription': return 'Are you sure you want to block {name}? Blocked users cannot send you message requests, group invites or call you.'; case 'noteToSelfHide': @@ -240,7 +240,7 @@ function getExpectedStringFromKey( case 'qrView': return 'View QR'; case 'recoveryPasswordView': - return 'View Password'; + return 'View Recovery Password'; case 'deleteConversationDescription': return 'Are you sure you want to delete your conversation with {name}? This will permanently delete all messages and attachments.'; case 'manageMembers': @@ -249,6 +249,17 @@ function getExpectedStringFromKey( return 'Without your recovery password, you cannot load your account on new devices. We strongly recommend you save your recovery password in a safe and secure place before continuing.'; case 'recoveryPasswordHidePermanentlyDescription2': return 'Are you sure you want to permanently hide your recovery password on this device? This cannot be undone.'; + case 'enter': + return 'Enter'; + case 'onboardingAccountCreated': + return 'Account Created'; + case 'onboardingBubbleWelcomeToSession': + return 'Welcome to Session {emoji}'; + case 'conversationsNone': + return "You don't have any conversations yet"; + case 'onboardingHitThePlusButton': + return 'Hit the plus button to start a chat, create a group, or join an official community!'; + default: // returning null means we don't have an expected string yet for this key. // This will make the test fail diff --git a/tests/automation/group_disappearing_messages.spec.ts b/tests/automation/group_disappearing_messages.spec.ts index 4fed712..7ebe98f 100644 --- a/tests/automation/group_disappearing_messages.spec.ts +++ b/tests/automation/group_disappearing_messages.spec.ts @@ -56,7 +56,7 @@ mediaArray.forEach(({ mediaType, path }) => { waitForTestIdWithText(bobWindow1, 'audio-player'), waitForTestIdWithText(charlieWindow1, 'audio-player'), ]); - await sleepFor(30000); + await sleepFor(10000); await Promise.all([ hasElementBeenDeleted(bobWindow1, 'data-testid', 'audio-player'), hasElementBeenDeleted(charlieWindow1, 'data-testid', 'audio-player'), @@ -68,8 +68,8 @@ mediaArray.forEach(({ mediaType, path }) => { waitForTextMessage(bobWindow1, testMessage), waitForTextMessage(charlieWindow1, testMessage), ]); - // Wait 30 seconds for image to disappear - await sleepFor(30000); + // Wait 10 seconds for image to disappear + await sleepFor(10000); await Promise.all([ hasTextMessageBeenDeleted(bobWindow1, testMessage), hasTextMessageBeenDeleted(charlieWindow1, testMessage), diff --git a/tests/automation/group_testing.spec.ts b/tests/automation/group_testing.spec.ts index 7bdebf2..54b171e 100644 --- a/tests/automation/group_testing.spec.ts +++ b/tests/automation/group_testing.spec.ts @@ -1,5 +1,11 @@ import { englishStrippedStr } from '../localization/englishStrippedStr'; import { doForAll, sleepFor } from '../promise_utils'; +import { + Conversation, + ConversationSettings, + Global, + HomeScreen, +} from './locators'; import { createGroup } from './setup/create_group'; import { newUser } from './setup/new_user'; import { @@ -11,9 +17,10 @@ import { createContact } from './utilities/create_contact'; import { leaveGroup } from './utilities/leave_group'; import { renameGroup } from './utilities/rename_group'; import { + clickOn, clickOnElement, clickOnMatchingText, - clickOnTestIdWithText, + clickOnWithText, grabTextFromElement, typeIntoInput, waitForMatchingText, @@ -54,17 +61,10 @@ test_group_Alice_1W_Bob_1W_Charlie_1W_Dracula_1W( draculaWindow1, groupCreated, }) => { - // Check config messages in all windows - await sleepFor(1000); await createContact(aliceWindow1, draculaWindow1, alice, dracula); - await clickOnElement({ - window: aliceWindow1, - strategy: 'data-testid', - selector: 'message-section', - }); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); await clickOnElement({ @@ -98,14 +98,10 @@ test_group_Alice_1W_Bob_1W_Charlie_1W_Dracula_1W( }, [aliceWindow1, bobWindow1, charlieWindow1], ); - await clickOnElement({ - window: draculaWindow1, - strategy: 'data-testid', - selector: 'message-section', - }); - await clickOnTestIdWithText( + await clickOn(draculaWindow1, Global.backButton); + await clickOnWithText( draculaWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); }, @@ -138,14 +134,14 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( // Click on conversation options // Check to see that you can't change group name to empty string // Click on edit group name - await clickOnTestIdWithText(aliceWindow1, 'conversation-options-avatar'); - await clickOnTestIdWithText(aliceWindow1, 'edit-group-name'); - await clickOnTestIdWithText(aliceWindow1, 'clear-group-info-name-button'); - await waitForTestIdWithText(aliceWindow1, 'error-message'); + await clickOn(aliceWindow1, Conversation.conversationSettingsIcon); + await clickOn(aliceWindow1, ConversationSettings.editGroupButton); + await clickOn(aliceWindow1, ConversationSettings.clearGroupNameButton); + await waitForTestIdWithText(aliceWindow1, Global.errorMessage.selector); const actualError = await grabTextFromElement( aliceWindow1, 'data-testid', - 'error-message', + Global.errorMessage.selector, ); if (actualError !== expectedError) { throw new Error( @@ -156,7 +152,7 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( aliceWindow1, englishStrippedStr('cancel').toString(), ); - await clickOnTestIdWithText(aliceWindow1, 'modal-close-button'); + await clickOn(aliceWindow1, Global.modalCloseButton); }, ); @@ -173,9 +169,9 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( }) => { // in windowA we should be able to mentions bob and userC - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); await typeIntoInput(aliceWindow1, 'message-input-text-area', '@'); @@ -192,17 +188,17 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( charlie.userName, ); // ALice tags Bob - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'mentions-popup-row', + Conversation.mentionsPopup, bob.userName, ); await waitForMatchingText(bobWindow1, 'You'); // in windowB we should be able to mentions alice and charlie - await clickOnTestIdWithText( + await clickOnWithText( bobWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); await typeIntoInput(bobWindow1, 'message-input-text-area', '@'); @@ -219,17 +215,17 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( charlie.userName, ); // Bob tags Charlie - await clickOnTestIdWithText( + await clickOnWithText( bobWindow1, - 'mentions-popup-row', + Conversation.mentionsPopup, charlie.userName, ); await waitForMatchingText(charlieWindow1, 'You'); // in charlieWindow1 we should be able to mentions alice and userB - await clickOnTestIdWithText( + await clickOnWithText( charlieWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); await typeIntoInput(charlieWindow1, 'message-input-text-area', '@'); @@ -246,9 +242,9 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( bob.userName, ); // Charlie tags Alice - await clickOnTestIdWithText( + await clickOnWithText( charlieWindow1, - 'mentions-popup-row', + Conversation.mentionsPopup, alice.userName, ); await waitForMatchingText(aliceWindow1, 'You'); diff --git a/tests/automation/input_validations.spec.ts b/tests/automation/input_validations.spec.ts index 1d015ea..d8f7fd4 100644 --- a/tests/automation/input_validations.spec.ts +++ b/tests/automation/input_validations.spec.ts @@ -1,7 +1,8 @@ import { englishStrippedStr } from '../localization/englishStrippedStr'; +import { Global, Onboarding } from './locators'; import { sessionTestOneWindow } from './setup/sessionTest'; import { - clickOnTestIdWithText, + clickOn, grabTextFromElement, typeIntoInput, waitForTestIdWithText, @@ -35,9 +36,9 @@ import { }, ].forEach(({ testName, incorrectSeed, expectedError }) => { sessionTestOneWindow(`Seed validation: "${testName}"`, async ([window]) => { - await clickOnTestIdWithText(window, 'existing-account-button'); + await clickOn(window, Onboarding.iHaveAnAccountButton); await typeIntoInput(window, 'recovery-phrase-input', incorrectSeed); - await clickOnTestIdWithText(window, 'continue-button'); + await clickOn(window, Global.continueButton); await waitForTestIdWithText(window, 'error-message'); const actualError = await grabTextFromElement( window, @@ -71,14 +72,14 @@ import { sessionTestOneWindow( `Display name validation: "${testName}"`, async ([window]) => { - await clickOnTestIdWithText(window, 'create-account-button'); + await clickOn(window, Onboarding.createAccountButton); await typeIntoInput(window, 'display-name-input', displayName); - await clickOnTestIdWithText(window, 'continue-button'); - await waitForTestIdWithText(window, 'error-message'); + await clickOn(window, Global.continueButton); + await waitForTestIdWithText(window, Global.errorMessage.selector); const actualError = await grabTextFromElement( window, 'data-testid', - 'error-message', + Global.errorMessage.selector, ); if (testName === 'No name') { console.log('Expected failure: see SES-2832'); diff --git a/tests/automation/landing_page.spec.ts b/tests/automation/landing_page.spec.ts index 7b071df..9338ec4 100644 --- a/tests/automation/landing_page.spec.ts +++ b/tests/automation/landing_page.spec.ts @@ -1,33 +1,70 @@ +import { englishStrippedStr } from '../localization/englishStrippedStr'; import { test_Alice_2W } from './setup/sessionTest'; -import { compareScreenshot, waitForElement } from './utilities/utils'; - -// TODO: Normalize screenshot dimensions before comparison to handle different pixel densities (e.g. with sharp) -// This would fix MacBook Retina (2x) vs M4 Mac Mini (1x) pixel density differences (1000x1584 vs 500x792) -// Alternatives: -// - Try to set deviceScaleFactor: 1 in Playwright context to force consistent scaling -// - Record pixel density dependent screenshots +import { + hasElementPoppedUpThatShouldnt, + waitForElement, +} from './utilities/utils'; test_Alice_2W( `Landing page states`, - async ({ aliceWindow1, aliceWindow2 }, testInfo) => { - const os = process.platform; - console.log('OS:', os); - const [landingPage, restoredPage] = await Promise.all([ + async ({ aliceWindow1, aliceWindow2 }, _testInfo) => { + await Promise.all([ waitForElement(aliceWindow1, 'class', 'session-conversation'), waitForElement(aliceWindow2, 'class', 'session-conversation'), ]); - await compareScreenshot( - landingPage, - `${testInfo.title}`, - 'new-account', - os, + // Check that the account created has all the required strings displayed + await Promise.all( + [ + englishStrippedStr('onboardingAccountCreated'), + englishStrippedStr('onboardingBubbleWelcomeToSession').withArgs({ + emoji: '👋', + }), + englishStrippedStr('conversationsNone'), + englishStrippedStr('onboardingHitThePlusButton'), + ].map(async (builder) => + waitForElement( + aliceWindow1, + 'data-testid', + 'empty-msg-view-account-created', + 1000, + builder.toString(), + ), + ), + ); + + // Check that the account restored has all the required strings displayed + await Promise.all( + [ + englishStrippedStr('conversationsNone'), + englishStrippedStr('onboardingHitThePlusButton'), + ].map(async (builder) => + waitForElement( + aliceWindow2, + 'data-testid', + 'empty-msg-view-welcome', + 1000, + builder.toString(), + ), + ), ); - await compareScreenshot( - restoredPage, - `${testInfo.title}`, - 'restored-account', - os, + + // Make sure the "account created" part is not visible on the restored window + await Promise.all( + [ + englishStrippedStr('onboardingAccountCreated'), + englishStrippedStr('onboardingBubbleWelcomeToSession').withArgs({ + emoji: '👋', + }), + ].map(async (builder) => + hasElementPoppedUpThatShouldnt( + aliceWindow2, + 'data-testid', + 'empty-msg-view-account-created', + + builder.toString(), + ), + ), ); }, ); diff --git a/tests/automation/linked_device_group.spec.ts b/tests/automation/linked_device_group.spec.ts index da8dd8a..73587ce 100644 --- a/tests/automation/linked_device_group.spec.ts +++ b/tests/automation/linked_device_group.spec.ts @@ -1,5 +1,14 @@ import type { Page } from '@playwright/test'; + import { englishStrippedStr } from '../localization/englishStrippedStr'; +import { + Conversation, + ConversationSettings, + Global, + HomeScreen, + LeftPane, + Settings, +} from './locators'; import { openApp } from './setup/open'; import { recoverFromSeed } from './setup/recovery_using_seed'; import { @@ -9,9 +18,9 @@ import { import { leaveGroup } from './utilities/leave_group'; import { checkModalStrings, + clickOn, clickOnMatchingText, - clickOnTestIdWithText, - waitForLoadingAnimationToFinish, + clickOnWithText, waitForTestIdWithText, } from './utilities/utils'; @@ -36,9 +45,9 @@ test_group_Alice_2W_Bob_1W_Charlie_1W( // Check for user A for control message that userC left group // await sleepFor(1000); // Click on group - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); await waitForTestIdWithText( @@ -51,9 +60,9 @@ test_group_Alice_2W_Bob_1W_Charlie_1W( .toString(), ); // Check for linked device (userA) - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); await waitForTestIdWithText( @@ -88,26 +97,26 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( // Does group appear? await waitForTestIdWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName.selector, groupCreated.userName, ); // Check group for members, conversation name and messages - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); // Check header name await waitForTestIdWithText( aliceWindow2, - 'header-conversation-name', + Conversation.conversationHeader.selector, groupCreated.userName, ); // Check for group members - await clickOnTestIdWithText(aliceWindow2, 'conversation-options-avatar'); + await clickOn(aliceWindow2, Conversation.conversationSettingsIcon); // Check right panel has correct name await waitForTestIdWithText(aliceWindow2, 'group-name'); - await clickOnTestIdWithText(aliceWindow2, 'manage-members-menu-option'); + await clickOn(aliceWindow2, ConversationSettings.manageMembersOption); await waitForTestIdWithText( aliceWindow2, 'modal-heading', @@ -117,40 +126,49 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( await Promise.all([ waitForTestIdWithText( aliceWindow2, - 'contact', + Global.contactItem.selector, englishStrippedStr('you').toString(), ), - waitForTestIdWithText(aliceWindow2, 'contact', bob.userName), - waitForTestIdWithText(aliceWindow2, 'contact', charlie.userName), + waitForTestIdWithText( + aliceWindow2, + Global.contactItem.selector, + bob.userName, + ), + waitForTestIdWithText( + aliceWindow2, + Global.contactItem.selector, + charlie.userName, + ), ]); }, ); async function clearDataOnWindow(window: Page) { - await clickOnTestIdWithText(window, 'settings-section'); + await clickOn(window, LeftPane.settingsButton); // Click on clear data option on left pane - await clickOnTestIdWithText( + await clickOnWithText( window, - 'clear-data-settings-menu-item', + Settings.clearDataMenuItem, englishStrippedStr('sessionClearData').toString(), ); await checkModalStrings( window, englishStrippedStr('clearDataAll').toString(), englishStrippedStr('clearDataAllDescription').toString(), + 'deleteAccountModal', ); - await clickOnTestIdWithText( + await clickOnWithText( window, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('clear').toString(), ); await checkModalStrings( window, englishStrippedStr('clearDataAll').toString(), englishStrippedStr('clearDeviceDescription').toString(), + 'deleteAccountModal', ); await clickOnMatchingText(window, englishStrippedStr('clear').toString()); - await waitForLoadingAnimationToFinish(window, 'loading-spinner'); } // Delete device data > Restore account @@ -164,36 +182,44 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( // Does group appear? await waitForTestIdWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName.selector, groupCreated.userName, ); // Check group for members, conversation name and messages - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); // Check header name await waitForTestIdWithText( aliceWindow2, - 'header-conversation-name', + Conversation.conversationHeader.selector, groupCreated.userName, ); // Check for group members - await clickOnTestIdWithText(aliceWindow2, 'conversation-options-avatar'); - await clickOnTestIdWithText(aliceWindow2, 'manage-members-menu-option'); + await clickOn(aliceWindow2, Conversation.conversationSettingsIcon); + await clickOn(aliceWindow2, ConversationSettings.manageMembersOption); // Check for You, Bob and Charlie await Promise.all([ waitForTestIdWithText( aliceWindow2, - 'contact', + Global.contactItem.selector, englishStrippedStr('you').toString(), ), - waitForTestIdWithText(aliceWindow2, 'contact', bob.userName), - waitForTestIdWithText(aliceWindow2, 'contact', charlie.userName), + waitForTestIdWithText( + aliceWindow2, + Global.contactItem.selector, + bob.userName, + ), + waitForTestIdWithText( + aliceWindow2, + Global.contactItem.selector, + charlie.userName, + ), ]); - await clickOnTestIdWithText(aliceWindow2, 'session-confirm-cancel-button'); - await clickOnTestIdWithText(aliceWindow2, 'modal-close-button'); + await clickOn(aliceWindow2, Global.cancelButton); + await clickOn(aliceWindow2, Global.modalCloseButton); // Delete device data on alicewindow2 await clearDataOnWindow(aliceWindow2); const [restoredWindow] = await openApp(1); @@ -201,40 +227,45 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( // Does group appear? await waitForTestIdWithText( restoredWindow, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName.selector, groupCreated.userName, ); // Check group for members, conversation name and messages - await clickOnTestIdWithText( + await clickOnWithText( restoredWindow, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); // Check header name await waitForTestIdWithText( restoredWindow, - 'header-conversation-name', + Conversation.conversationHeader.selector, groupCreated.userName, ); // Check for group members - await clickOnTestIdWithText(restoredWindow, 'conversation-options-avatar'); - await clickOnTestIdWithText(restoredWindow, 'manage-members-menu-option'); + await clickOn(restoredWindow, Conversation.conversationSettingsIcon); + await clickOn(restoredWindow, ConversationSettings.manageMembersOption); // Check for You, Bob and Charlie await Promise.all([ waitForTestIdWithText( restoredWindow, - 'contact', + Global.contactItem.selector, englishStrippedStr('you').toString(), ), - waitForTestIdWithText(restoredWindow, 'contact', bob.userName), - waitForTestIdWithText(restoredWindow, 'contact', charlie.userName), + waitForTestIdWithText( + restoredWindow, + Global.contactItem.selector, + bob.userName, + ), + waitForTestIdWithText( + restoredWindow, + Global.contactItem.selector, + charlie.userName, + ), ]); // Do it all again - await clickOnTestIdWithText( - restoredWindow, - 'session-confirm-cancel-button', - ); - await clickOnTestIdWithText(restoredWindow, 'modal-close-button'); + await clickOn(restoredWindow, Global.cancelButton); + await clickOn(restoredWindow, Global.modalCloseButton); // Delete device data on restoredWindow await clearDataOnWindow(restoredWindow); const [restoredWindow2] = await openApp(1); @@ -242,33 +273,41 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( // Does group appear? await waitForTestIdWithText( restoredWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName.selector, groupCreated.userName, ); // Check group for members, conversation name and messages - await clickOnTestIdWithText( + await clickOnWithText( restoredWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, groupCreated.userName, ); // Check header name await waitForTestIdWithText( restoredWindow2, - 'header-conversation-name', + Conversation.conversationHeader.selector, groupCreated.userName, ); // Check for group members - await clickOnTestIdWithText(restoredWindow2, 'conversation-options-avatar'); - await clickOnTestIdWithText(restoredWindow2, 'manage-members-menu-option'); + await clickOn(restoredWindow2, Conversation.conversationSettingsIcon); + await clickOn(restoredWindow2, ConversationSettings.manageMembersOption); // Check for You, Bob and Charlie await Promise.all([ waitForTestIdWithText( restoredWindow2, - 'contact', + Global.contactItem.selector, englishStrippedStr('you').toString(), ), - waitForTestIdWithText(restoredWindow2, 'contact', bob.userName), - waitForTestIdWithText(restoredWindow2, 'contact', charlie.userName), + waitForTestIdWithText( + restoredWindow2, + Global.contactItem.selector, + bob.userName, + ), + waitForTestIdWithText( + restoredWindow2, + Global.contactItem.selector, + charlie.userName, + ), ]); }, ); diff --git a/tests/automation/linked_device_requests.spec.ts b/tests/automation/linked_device_requests.spec.ts index 1c41ec9..6284459 100644 --- a/tests/automation/linked_device_requests.spec.ts +++ b/tests/automation/linked_device_requests.spec.ts @@ -1,11 +1,19 @@ import { englishStrippedStr } from '../localization/englishStrippedStr'; import { sleepFor } from '../promise_utils'; +import { + Conversation, + Global, + HomeScreen, + LeftPane, + Settings, +} from './locators'; import { test_Alice_2W_Bob_1W } from './setup/sessionTest'; import { sendMessage } from './utilities/message'; import { sendNewMessage } from './utilities/send_message'; import { checkModalStrings, - clickOnTestIdWithText, + clickOn, + clickOnWithText, waitForMatchingText, waitForTestIdWithText, waitForTextMessage, @@ -18,17 +26,17 @@ test_Alice_2W_Bob_1W( const testReply = `${alice.userName} accepting message request from ${bob.userName}`; await sendNewMessage(bobWindow1, alice.accountid, testMessage); // Accept request in aliceWindow1 - await clickOnTestIdWithText(aliceWindow1, 'message-request-banner'); - await clickOnTestIdWithText(aliceWindow2, 'message-request-banner'); - await clickOnTestIdWithText( + await clickOn(aliceWindow1, HomeScreen.messageRequestBanner); + await clickOn(aliceWindow2, HomeScreen.messageRequestBanner); + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); - await clickOnTestIdWithText(aliceWindow1, 'accept-message-request'); + await clickOn(aliceWindow1, Conversation.acceptMessageRequestButton); await waitForTestIdWithText( aliceWindow1, - 'message-request-response-message', + Conversation.messageRequestAcceptControlMessage.selector, englishStrippedStr('messageRequestYouHaveAccepted') .withArgs({ name: bob.userName, @@ -45,10 +53,11 @@ test_Alice_2W_Bob_1W( ); await sendMessage(aliceWindow1, testReply); await waitForTextMessage(bobWindow1, testReply); - await clickOnTestIdWithText(aliceWindow2, 'new-conversation-button'); + await clickOn(aliceWindow2, Global.backButton); + await clickOn(aliceWindow2, HomeScreen.plusButton); await waitForTestIdWithText( aliceWindow2, - 'module-conversation__user__profile-name', + Global.contactItem.selector, bob.userName, ); }, @@ -60,37 +69,29 @@ test_Alice_2W_Bob_1W( const testMessage = `${bob.userName} sending message request to ${alice.userName}`; await sendNewMessage(bobWindow1, alice.accountid, testMessage); // Decline request in aliceWindow1 - await clickOnTestIdWithText(aliceWindow1, 'message-request-banner'); - await clickOnTestIdWithText( + await clickOn(aliceWindow1, HomeScreen.messageRequestBanner); + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); - await clickOnTestIdWithText(aliceWindow2, 'message-request-banner'); + await clickOn(aliceWindow2, HomeScreen.messageRequestBanner); await waitForTestIdWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName.selector, bob.userName, ); await sleepFor(1000); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'delete-message-request', + Conversation.deleteMessageRequestButton, englishStrippedStr('delete').toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('delete').toString(), ); - - // Note: this test is broken currently but this is a known issue. - // It happens because we have a race condition between the update from libsession and the update from the swarm, both with the same seqno. - // See SES-1563 - console.info( - 'This test is subject to a race condition and so is most of the times, broken. See SES-2518', - ); - await waitForMatchingText( aliceWindow1, englishStrippedStr('messageRequestsNonePending').toString(), @@ -109,18 +110,15 @@ test_Alice_2W_Bob_1W( // send a message to Bob to Alice await sendNewMessage(bobWindow1, alice.accountid, `${testMessage}`); // Check the message request banner appears and click on it - await clickOnTestIdWithText(aliceWindow1, 'message-request-banner'); + await clickOn(aliceWindow1, HomeScreen.messageRequestBanner); // Select message request from Bob - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); // Block Bob - await clickOnTestIdWithText( - aliceWindow1, - 'decline-and-block-message-request', - ); + await clickOn(aliceWindow1, Conversation.blockMessageRequestButton); // Check modal strings await checkModalStrings( aliceWindow1, @@ -130,27 +128,29 @@ test_Alice_2W_Bob_1W( .toString(), ); // Confirm block - await clickOnTestIdWithText(aliceWindow1, 'session-confirm-ok-button'); + await clickOn(aliceWindow1, Global.confirmButton); // Need to wait for the blocked status to sync await sleepFor(2000); // Check blocked status in blocked contacts list - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); - await clickOnTestIdWithText( + await clickOn(aliceWindow1, LeftPane.settingsButton); + await clickOn(aliceWindow1, Settings.conversationsMenuItem); + await clickOn(aliceWindow1, Settings.blockedContactsButton); + await waitForTestIdWithText( aliceWindow1, - 'conversations-settings-menu-item', + Global.contactItem.selector, + bob.userName, ); - await clickOnTestIdWithText(aliceWindow1, 'reveal-blocked-user-settings'); - await waitForTestIdWithText(aliceWindow1, 'contact', bob.userName); // Check that the blocked contacts is on alicewindow2 // Check blocked status in blocked contacts list await sleepFor(5000); - await clickOnTestIdWithText(aliceWindow2, 'settings-section'); - await clickOnTestIdWithText( + await clickOn(aliceWindow2, LeftPane.settingsButton); + await clickOn(aliceWindow2, Settings.conversationsMenuItem); + await clickOn(aliceWindow2, Settings.blockedContactsButton); + await waitForTestIdWithText( aliceWindow2, - 'conversations-settings-menu-item', + Global.contactItem.selector, + bob.userName, ); - await clickOnTestIdWithText(aliceWindow2, 'reveal-blocked-user-settings'); - await waitForTestIdWithText(aliceWindow2, 'contact', bob.userName); await waitForMatchingText(aliceWindow2, bob.userName); }, ); diff --git a/tests/automation/linked_device_user.spec.ts b/tests/automation/linked_device_user.spec.ts index ea3ddec..7714017 100644 --- a/tests/automation/linked_device_user.spec.ts +++ b/tests/automation/linked_device_user.spec.ts @@ -1,7 +1,15 @@ /* eslint-disable no-await-in-loop */ -import { Page, expect } from '@playwright/test'; +import { expect, Page } from '@playwright/test'; + import { englishStrippedStr } from '../localization/englishStrippedStr'; import { sleepFor } from '../promise_utils'; +import { + Conversation, + Global, + HomeScreen, + LeftPane, + Settings, +} from './locators'; import { forceCloseAllWindows } from './setup/closeWindows'; import { newUser } from './setup/new_user'; import { @@ -14,10 +22,11 @@ import { linkedDevice } from './utilities/linked_device'; import { sendMessage } from './utilities/message'; import { checkModalStrings, + clickOn, clickOnElement, clickOnMatchingText, - clickOnTestIdWithText, clickOnTextMessage, + clickOnWithText, doWhileWithMax, hasElementBeenDeleted, hasTextMessageBeenDeleted, @@ -34,21 +43,21 @@ sessionTestOneWindow('Link a device', async ([aliceWindow1]) => { try { const userA = await newUser(aliceWindow1, 'Alice'); aliceWindow2 = await linkedDevice(userA.recoveryPassword); // not using fixture here as we want to check the behavior finely - await clickOnTestIdWithText(aliceWindow1, 'leftpane-primary-avatar'); + await clickOn(aliceWindow1, LeftPane.profileButton); // Verify Username await waitForTestIdWithText( aliceWindow1, - 'your-profile-name', + Settings.displayName.selector, userA.userName, ); // Verify Account ID await waitForTestIdWithText( aliceWindow1, - 'your-account-id', + Settings.accountId.selector, userA.accountid, ); // exit profile modal - await clickOnTestIdWithText(aliceWindow1, 'modal-close-button'); + await clickOn(aliceWindow1, Global.modalCloseButton); // You're almost finished isn't displayed const errorDesc = 'Should not be found'; try { @@ -78,19 +87,20 @@ test_Alice_2W( 'Changed username syncs', async ({ aliceWindow1, aliceWindow2 }) => { const newUsername = 'Tiny bubble'; - await clickOnTestIdWithText(aliceWindow1, 'leftpane-primary-avatar'); + await clickOn(aliceWindow1, LeftPane.profileButton); // Click on pencil icon - await clickOnTestIdWithText(aliceWindow1, 'edit-profile-icon'); + await clickOn(aliceWindow1, Settings.displayName); // Replace old username with new username - await typeIntoInput(aliceWindow1, 'profile-name-input', newUsername); + await typeIntoInput( + aliceWindow1, + Settings.displayNameInput.selector, + newUsername, + ); // Press enter to confirm change - await clickOnElement({ - window: aliceWindow1, - strategy: 'data-testid', - selector: 'save-button-profile-update', - }); - // Wait for loading animation - await waitForLoadingAnimationToFinish(aliceWindow1, 'loading-spinner'); + await clickOnMatchingText( + aliceWindow1, + englishStrippedStr('save').toString(), + ); // Check username change in window B // Click on profile settings in window B @@ -100,12 +110,12 @@ test_Alice_2W( 500, 'waiting for updated username in profile dialog', async () => { - await clickOnTestIdWithText(aliceWindow2, 'leftpane-primary-avatar'); + await clickOn(aliceWindow2, LeftPane.profileButton); // Verify username has changed to new username try { await waitForTestIdWithText( aliceWindow2, - 'your-profile-name', + Settings.displayName.selector, newUsername, 100, ); @@ -128,20 +138,20 @@ test_Alice_2W( test_Alice_2W( 'Profile picture syncs', async ({ aliceWindow1, aliceWindow2 }, testinfo) => { - await clickOnTestIdWithText(aliceWindow1, 'leftpane-primary-avatar'); + await clickOn(aliceWindow1, LeftPane.profileButton); // Click on current profile picture - await waitForTestIdWithText( - aliceWindow1, - 'copy-button-profile-update', - englishStrippedStr('copy').toString(), - ); - - await clickOnTestIdWithText(aliceWindow1, 'image-upload-section'); - await clickOnTestIdWithText(aliceWindow1, 'image-upload-click'); + await clickOn(aliceWindow1, Settings.displayName); + await clickOn(aliceWindow1, Settings.imageUploadSection); + await clickOn(aliceWindow1, Settings.imageUploadClick); // allow for the image to be resized before we try to save it await sleepFor(500); - await clickOnTestIdWithText(aliceWindow1, 'save-button-profile-update'); - await waitForTestIdWithText(aliceWindow1, 'loading-spinner'); + await clickOn(aliceWindow1, Settings.saveProfileUpdateButton); + await waitForLoadingAnimationToFinish(aliceWindow1, 'loading-spinner'); + await clickOnMatchingText( + aliceWindow1, + englishStrippedStr('save').toString(), + ); + await clickOn(aliceWindow1, Global.modalCloseButton); if (testinfo.config.updateSnapshots === 'all') { await sleepFor(15000, true); // long time to be sure a poll happened when we want to update the snapshot @@ -150,7 +160,7 @@ test_Alice_2W( } const leftpaneAvatarContainer = await waitForTestIdWithText( aliceWindow2, - 'leftpane-primary-avatar', + LeftPane.profileButton.selector, ); const start = Date.now(); let correctScreenshot = false; @@ -205,9 +215,9 @@ test_Alice_2W_Bob_1W( await createContact(aliceWindow1, bobWindow1, alice, bob); await sendMessage(aliceWindow1, messageToDelete); // Navigate to conversation on linked device and for message from user A to user B - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); await Promise.all([ @@ -219,9 +229,9 @@ test_Alice_2W_Bob_1W( aliceWindow1, englishStrippedStr('delete').toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('delete').toString(), ); await waitForTestIdWithText( @@ -231,11 +241,11 @@ test_Alice_2W_Bob_1W( .withArgs({ count: 1 }) .toString(), ); - await hasTextMessageBeenDeleted(aliceWindow1, messageToDelete, 6000); + await hasTextMessageBeenDeleted(aliceWindow1, messageToDelete, 6_000); // linked device for deleted message // Waiting for message to be removed // Check for linked device - await hasTextMessageBeenDeleted(aliceWindow2, messageToDelete, 10000); + await hasTextMessageBeenDeleted(aliceWindow2, messageToDelete, 30_000); // Still should exist for user B await waitForMatchingText(bobWindow1, messageToDelete); }, @@ -248,9 +258,9 @@ test_Alice_2W_Bob_1W( await createContact(aliceWindow1, bobWindow1, alice, bob); await sendMessage(aliceWindow1, unsentMessage); // Navigate to conversation on linked device and for message from user A to user B - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); await Promise.all([ @@ -296,16 +306,16 @@ test_Alice_2W_Bob_1W( await createContact(aliceWindow1, bobWindow1, alice, bob); await sendMessage(aliceWindow1, testMessage); // Navigate to conversation on linked device and check for message from user A to user B - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, - true, + { rightButton: true }, ); // Select block - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'context-menu-item', + Global.contextMenuItem, englishStrippedStr('block').toString(), ); // Check modal strings @@ -316,35 +326,31 @@ test_Alice_2W_Bob_1W( .withArgs({ name: bob.userName }) .toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow2, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('block').toString(), ); // Verify the user was moved to the blocked contact list await waitForMatchingPlaceholder( aliceWindow1, - 'message-input-text-area', + Conversation.messageInput.selector, englishStrippedStr('blockBlockedDescription').toString(), ); - // reveal-blocked-user-settings is not updated once opened // Check linked device for blocked contact in settings screen // Click on settings tab - await clickOnTestIdWithText(aliceWindow2, 'settings-section'); - await clickOnTestIdWithText( - aliceWindow2, - 'conversations-settings-menu-item', - ); + await clickOn(aliceWindow2, LeftPane.settingsButton); + await clickOn(aliceWindow2, Settings.conversationsMenuItem); // a conf sync job can take 30s (if the last one failed) + 10s polling to show a change on a linked device. - await clickOnTestIdWithText( + await clickOn(aliceWindow2, Settings.blockedContactsButton, { + maxWait: 50_000, + }); + // Check if user B is in blocked contact list + await waitForTestIdWithText( aliceWindow2, - 'reveal-blocked-user-settings', - undefined, - undefined, - 50000, + Global.contactItem.selector, + bob.userName, ); - // Check if user B is in blocked contact list - await waitForTestIdWithText(aliceWindow2, 'contact', bob.userName); }, ); @@ -353,57 +359,48 @@ test_Alice_2W_Bob_1W( async ({ alice, aliceWindow1, aliceWindow2, bob, bobWindow1 }) => { // Create contact and send new message await createContact(aliceWindow1, bobWindow1, alice, bob); - // Confirm contact by checking Messages tab (name should appear in list) - await Promise.all([ - clickOnTestIdWithText(aliceWindow1, 'message-section'), - clickOnTestIdWithText(bobWindow1, 'message-section'), - clickOnTestIdWithText(aliceWindow2, 'message-section'), - ]); - await Promise.all([ - clickOnElement({ - window: aliceWindow1, - strategy: 'data-testid', - selector: 'new-conversation-button', - }), - clickOnElement({ - window: bobWindow1, - strategy: 'data-testid', - selector: 'new-conversation-button', - }), - clickOnElement({ - window: aliceWindow2, - strategy: 'data-testid', - selector: 'new-conversation-button', - }), - ]); + await clickOn(bobWindow1, Global.backButton); + await Promise.all( + [aliceWindow1, aliceWindow2, bobWindow1].map((w) => + clickOnElement({ + window: w, + strategy: 'data-testid', + selector: 'new-conversation-button', + }), + ), + ); await Promise.all([ waitForTestIdWithText( aliceWindow1, - 'module-conversation__user__profile-name', + Global.contactItem.selector, bob.userName, ), waitForTestIdWithText( bobWindow1, - 'module-conversation__user__profile-name', + Global.contactItem.selector, alice.userName, ), waitForTestIdWithText( aliceWindow2, - 'module-conversation__user__profile-name', + Global.contactItem.selector, bob.userName, ), ]); + await Promise.all( + [aliceWindow1, aliceWindow2, bobWindow1].map((w) => + clickOn(w, Global.backButton), + ), + ); // Delete contact - await clickOnTestIdWithText(aliceWindow1, 'message-section'); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, - true, + { rightButton: true }, ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'context-menu-item', + Global.contextMenuItem, englishStrippedStr('conversationsDelete').toString(), ); await checkModalStrings( @@ -413,48 +410,38 @@ test_Alice_2W_Bob_1W( .withArgs({ name: bob.userName }) .toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('delete').toString(), ); - // Need to close 'New Conversation' screen - await clickOnTestIdWithText(aliceWindow2, 'new-conversation-button'); // Check if conversation is deleted // Need to wait for deletion to propagate to linked device - await Promise.all([ - hasElementBeenDeleted( - aliceWindow1, - 'data-testid', - 'module-conversation__user__profile-name', - 1000, - bob.userName, - ), - hasElementBeenDeleted( - aliceWindow2, - 'data-testid', - 'module-conversation__user__profile-name', - 10000, - bob.userName, + await Promise.all( + [aliceWindow1, aliceWindow2].map((w) => + hasElementBeenDeleted( + w, + 'data-testid', + HomeScreen.conversationItemName.selector, + 10_000, + bob.userName, + ), ), - ]); + ); }, ); test_Alice_2W( 'Hide note to self syncs', async ({ alice, aliceWindow1, aliceWindow2 }) => { - await clickOnTestIdWithText(aliceWindow1, 'new-conversation-button'); - await clickOnTestIdWithText( - aliceWindow1, - 'chooser-new-conversation-button', - ); + await clickOn(aliceWindow1, HomeScreen.plusButton); + await clickOn(aliceWindow1, HomeScreen.newMessageOption); await typeIntoInput( aliceWindow1, - 'new-session-conversation', + HomeScreen.newMessageAccountIDInput.selector, alice.accountid, ); - await clickOnTestIdWithText(aliceWindow1, 'next-new-conversation-button'); + await clickOn(aliceWindow1, HomeScreen.newMessageNextButton); await waitForTestIdWithText( aliceWindow1, 'header-conversation-name', @@ -465,18 +452,18 @@ test_Alice_2W( await sleepFor(1000); await waitForTestIdWithText( aliceWindow2, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName.selector, englishStrippedStr('noteToSelf').toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, englishStrippedStr('noteToSelf').toString(), - true, + { rightButton: true }, ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'context-menu-item', + Global.contextMenuItem, englishStrippedStr('noteToSelfHide').toString(), ); await checkModalStrings( @@ -484,9 +471,9 @@ test_Alice_2W( englishStrippedStr('noteToSelfHide').toString(), englishStrippedStr('noteToSelfHideDescription').toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('hide').toString(), ); // Check linked device for hidden note to self @@ -495,15 +482,15 @@ test_Alice_2W( hasElementBeenDeleted( aliceWindow1, 'data-testid', - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName.selector, 5000, englishStrippedStr('noteToSelf').toString(), ), hasElementBeenDeleted( aliceWindow2, 'data-testid', - 'module-conversation__user__profile-name', - 10000, + HomeScreen.conversationItemName.selector, + 15_000, englishStrippedStr('noteToSelf').toString(), ), ]); diff --git a/tests/automation/locators/index.ts b/tests/automation/locators/index.ts new file mode 100644 index 0000000..7db611a --- /dev/null +++ b/tests/automation/locators/index.ts @@ -0,0 +1,210 @@ +/* eslint-disable @typescript-eslint/lines-between-class-members */ +import { DataTestId, StrategyExtractionObj } from '../types/testing'; + +abstract class Locator { + protected static className(selector: string): StrategyExtractionObj { + return { + strategy: 'class', + selector, + } as const; + } + + protected static hasText(selector: string): StrategyExtractionObj { + return { + strategy: ':has-text', + selector, + } as const; + } + + protected static testId(selector: DataTestId) { + return { + strategy: 'data-testid', + selector, + } as const; + } +} + +export class Onboarding extends Locator { + static readonly createAccountButton = this.testId('create-account-button'); + static readonly displayNameInput = this.testId('display-name-input'); + static readonly iHaveAnAccountButton = this.testId('existing-account-button'); + static readonly recoveryPhraseInput = this.testId('recovery-phrase-input'); +} + +export class LeftPane extends Locator { + static readonly profileButton = this.testId('leftpane-primary-avatar'); + static readonly settingsButton = this.testId('settings-section'); + static readonly themeButton = this.testId('theme-section'); +} + +export class HomeScreen extends Locator { + // New Conversation + static readonly createGroupOption = this.testId('chooser-new-group'); + static readonly inviteAFriendOption = this.testId('chooser-invite-friend'); + static readonly joinCommunityOption = this.testId('chooser-new-community'); + static readonly newMessageOption = this.testId( + 'chooser-new-conversation-button', + ); + // New Message + static readonly newMessageAccountIDInput = this.testId( + 'new-session-conversation', + ); + static readonly newMessageNextButton = this.testId( + 'next-new-conversation-button', + ); + // Create Group + static readonly createGroupCreateButton = this.testId('create-group-button'); + static readonly createGroupGroupName = this.testId('new-closed-group-name'); + // Invite A Friend + static readonly inviteAFriendCopyButton = this.testId( + 'copy-button-account-id', + ); + // Join Community + static readonly joinCommunityButton = this.testId('join-community-button'); + static readonly joinCommunityInput = this.testId( + 'join-community-conversation', + ); + // Home Screen items + static readonly conversationItemName = this.testId( + 'module-conversation__user__profile-name', + ); + static readonly messageRequestBanner = this.testId('message-request-banner'); + static readonly plusButton = this.testId('new-conversation-button'); + static readonly revealRecoveryPhraseButton = this.testId( + 'reveal-recovery-phrase', + ); + static readonly setNicknameButton = this.testId( + 'set-nickname-confirm-button', + ); +} + +export class Conversation extends Locator { + static readonly acceptMessageRequestButton = this.testId( + 'accept-message-request', + ); + static readonly blockMessageRequestButton = this.testId( + 'decline-and-block-message-request', + ); + static readonly callButton = this.testId('call-button'); + static readonly conversationHeader = this.testId('header-conversation-name'); + static readonly conversationSettingsIcon = this.testId( + 'conversation-options-avatar', + ); + static readonly deleteMessageRequestButton = this.testId( + 'delete-message-request', + ); + static readonly disappearingControlMessage = this.testId( + 'disappear-control-message', + ); + static readonly endCallButton = this.testId('end-call'); + static readonly endVoiceMessageButton = this.testId('end-voice-message'); + static readonly mentionsPopup = this.testId('mentions-popup-row'); + static readonly messageContent = this.testId('message-content'); + + static readonly messageInput = this.testId('message-input-text-area'); + static readonly messageRequestAcceptControlMessage = this.testId( + 'message-request-response-message', + ); + static readonly microphoneButton = this.testId('microphone-button'); + static readonly scrollToBottomButton = this.testId('scroll-to-bottom-button'); + static readonly sendMessageButton = this.testId('send-message-button'); +} + +export class ConversationSettings extends Locator { + static readonly clearGroupNameButton = this.testId( + 'clear-group-info-name-button', + ); + static readonly disappearingMessagesOption = this.testId( + 'disappearing-messages-menu-option', + ); + static readonly editGroupButton = this.testId('edit-group-name'); + static readonly inviteContactsOption = this.testId( + 'invite-contacts-menu-option', + ); + static readonly manageMembersOption = this.testId( + 'manage-members-menu-option', + ); +} + +export class Settings extends Locator { + // Profile + static readonly accountId = this.testId('your-account-id'); + static readonly displayName = this.testId('your-profile-name'); + // Update Profile Information + static readonly displayNameInput = this.testId( + 'update-profile-info-name-input', + ); + static readonly imageUploadClick = this.testId('image-upload-click'); + static readonly imageUploadSection = this.testId('image-upload-section'); + static readonly saveProfileUpdateButton = this.testId( + 'save-button-profile-update', + ); + // Menu items + static readonly clearDataMenuItem = this.testId( + 'clear-data-settings-menu-item', + ); + static readonly conversationsMenuItem = this.testId( + 'conversations-settings-menu-item', + ); + static readonly messageRequestsMenuItem = this.testId( + 'message-requests-settings-menu-item', + ); + static readonly privacyMenuItem = this.testId('privacy-settings-menu-item'); + static readonly recoveryPasswordMenuItem = this.testId( + 'recovery-password-settings-menu-item', + ); + // Privacy + static readonly changePasswordSettingsButton = this.testId( + 'change-password-settings-button', + ); + static readonly confirmPasswordInput = this.testId('password-input-confirm'); + static readonly enableCalls = this.testId('enable-calls-settings-row'); + static readonly enableMicrophone = this.testId( + 'enable-microphone-settings-row', + ); + static readonly enableReadReceipts = this.testId( + 'enable-read-receipts-settings-row', + ); + static readonly passwordInput = this.testId('password-input'); + static readonly reConfirmPasswordInput = this.testId( + 'password-input-reconfirm', + ); + static readonly setPasswordButton = this.testId('set-password-button'); + static readonly setPasswordSettingsButton = this.testId( + 'set-password-settings-button', + ); + // Conversations + static readonly blockedContactsButton = this.testId( + 'blocked-contacts-settings-row', + ); + static readonly unblockButton = this.testId('unblock-button-settings-screen'); + // Recovery Password + static readonly hideRecoveryPasswordButton = this.testId( + 'hide-recovery-password-settings-button', + ); + static readonly recoveryPasswordContainer = this.testId( + 'recovery-password-seed-modal', + ); + static readonly recoveryPasswordQRCode = this.testId( + 'session-recovery-password', + ); + // Clear Data + static readonly clearDeviceAndNetworkRadial = this.testId( + 'label-device_and_network', + ); +} + +export class Global extends Locator { + static readonly backButton = this.testId('back-button'); + static readonly cancelButton = this.testId('session-confirm-cancel-button'); + static readonly confirmButton = this.testId('session-confirm-ok-button'); + static readonly contactItem = this.testId( + 'module-contact-name__profile-name', + ); + static readonly contextMenuItem = this.testId('context-menu-item'); + static readonly continueButton = this.testId('continue-button'); + static readonly errorMessage = this.testId('error-message'); + static readonly modalBackButton = this.testId('modal-back-button'); + static readonly modalCloseButton = this.testId('modal-close-button'); + static readonly toast = this.testId('session-toast'); +} diff --git a/tests/automation/message_checks.spec.ts b/tests/automation/message_checks.spec.ts index 36797e2..bb31894 100644 --- a/tests/automation/message_checks.spec.ts +++ b/tests/automation/message_checks.spec.ts @@ -2,6 +2,12 @@ import { englishStrippedStr } from '../localization/englishStrippedStr'; import { sleepFor } from '../promise_utils'; import { testCommunityName } from './constants/community'; import { longText, mediaArray, testLink } from './constants/variables'; +import { + Conversation, + ConversationSettings, + Global, + HomeScreen, +} from './locators'; import { newUser } from './setup/new_user'; import { sessionTestTwoWindows, @@ -18,10 +24,11 @@ import { trustUser, } from './utilities/send_media'; import { + clickOn, clickOnElement, clickOnMatchingText, - clickOnTestIdWithText, clickOnTextMessage, + clickOnWithText, hasTextMessageBeenDeleted, measureSendingTime, typeIntoInput, @@ -121,25 +128,25 @@ test_Alice_1W_Bob_1W( async ({ alice, aliceWindow1, bob, bobWindow1 }) => { await createContact(aliceWindow1, bobWindow1, alice, bob); await joinCommunity(aliceWindow1); - await clickOnTestIdWithText(aliceWindow1, 'conversation-options-avatar'); - await clickOnTestIdWithText(aliceWindow1, 'invite-contacts-menu-option'); + await clickOn(aliceWindow1, Conversation.conversationSettingsIcon); + await clickOn(aliceWindow1, ConversationSettings.inviteContactsOption); await waitForTestIdWithText( aliceWindow1, 'modal-heading', englishStrippedStr('membersInvite').toString(), ); - await clickOnTestIdWithText(aliceWindow1, 'contact', bob.userName); - await clickOnTestIdWithText(aliceWindow1, 'session-confirm-ok-button'); + await clickOnWithText(aliceWindow1, Global.contactItem, bob.userName); + await clickOn(aliceWindow1, Global.confirmButton); // For lack of a unique ID we use native Playwright methods await aliceWindow1 .getByTestId('invite-contacts-dialog') - .getByTestId('modal-close-button') + .getByTestId(Global.modalCloseButton.selector) .click(); // Close UCS modal - await clickOnTestIdWithText(aliceWindow1, 'modal-close-button'); - await clickOnTestIdWithText( + await clickOn(aliceWindow1, Global.modalCloseButton); + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); await Promise.all([ diff --git a/tests/automation/message_requests.spec.ts b/tests/automation/message_requests.spec.ts index 86fb331..21d22f2 100644 --- a/tests/automation/message_requests.spec.ts +++ b/tests/automation/message_requests.spec.ts @@ -1,11 +1,19 @@ import { englishStrippedStr } from '../localization/englishStrippedStr'; +import { + Conversation, + Global, + HomeScreen, + LeftPane, + Settings, +} from './locators'; import { test_Alice_1W_Bob_1W } from './setup/sessionTest'; import { sendMessage } from './utilities/message'; import { sendNewMessage } from './utilities/send_message'; import { checkModalStrings, + clickOn, clickOnMatchingText, - clickOnTestIdWithText, + clickOnWithText, waitForMatchingText, waitForTestIdWithText, } from './utilities/utils'; @@ -18,15 +26,15 @@ test_Alice_1W_Bob_1W( // send a message to User B from User A await sendNewMessage(aliceWindow1, bob.accountid, `${testMessage}`); // Check the message request banner appears and click on it - await clickOnTestIdWithText(bobWindow1, 'message-request-banner'); + await clickOn(bobWindow1, HomeScreen.messageRequestBanner); // Select message request from User A - await clickOnTestIdWithText( + await clickOnWithText( bobWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, alice.userName, ); // Check that using the accept button has intended use - await clickOnTestIdWithText(bobWindow1, 'accept-message-request'); + await clickOn(bobWindow1, Conversation.acceptMessageRequestButton); // Check config message of message request acceptance await waitForTestIdWithText( bobWindow1, @@ -52,11 +60,11 @@ test_Alice_1W_Bob_1W( // send a message to User B from User A await sendNewMessage(aliceWindow1, bob.accountid, `${testMessage}`); // Check the message request banner appears and click on it - await clickOnTestIdWithText(bobWindow1, 'message-request-banner'); + await clickOn(bobWindow1, HomeScreen.messageRequestBanner); // Select message request from User A - await clickOnTestIdWithText( + await clickOnWithText( bobWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, alice.userName, ); await sendMessage(bobWindow1, testReply); @@ -85,17 +93,17 @@ test_Alice_1W_Bob_1W( // send a message to User B from User A await sendNewMessage(aliceWindow1, bob.accountid, `${testMessage}`); // Check the message request banner appears and click on it - await clickOnTestIdWithText(bobWindow1, 'message-request-banner'); + await clickOn(bobWindow1, HomeScreen.messageRequestBanner); // Select message request from User A - await clickOnTestIdWithText( + await clickOnWithText( bobWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, alice.userName, ); - await clickOnTestIdWithText( + await clickOnWithText( bobWindow1, - 'delete-message-request', + Conversation.deleteMessageRequestButton, englishStrippedStr('delete').toString(), ); // Confirm decline @@ -104,9 +112,9 @@ test_Alice_1W_Bob_1W( englishStrippedStr('delete').toString(), englishStrippedStr('messageRequestsDelete').toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( bobWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('delete').toString(), ); // Check config message of message request acceptance @@ -124,7 +132,7 @@ test_Alice_1W_Bob_1W( // send a message to User B from User A await sendNewMessage(aliceWindow1, bob.accountid, `${testMessage}`); // Check the message request banner appears and click on it - await clickOnTestIdWithText(bobWindow1, 'message-request-banner'); + await clickOn(bobWindow1, HomeScreen.messageRequestBanner); // Select 'Clear All' button await clickOnMatchingText( bobWindow1, @@ -136,17 +144,17 @@ test_Alice_1W_Bob_1W( englishStrippedStr('clearAll').toString(), englishStrippedStr('messageRequestsClearAllExplanation').toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( bobWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('clear').toString(), ); // Navigate back to message request folder to check - await clickOnTestIdWithText(bobWindow1, 'settings-section'); + await clickOn(bobWindow1, LeftPane.settingsButton); - await clickOnTestIdWithText( + await clickOnWithText( bobWindow1, - 'message-requests-settings-menu-item', + Settings.messageRequestsMenuItem, englishStrippedStr('sessionMessageRequests').toString(), ); // Check config message of message request acceptance diff --git a/tests/automation/password.spec.ts b/tests/automation/password.spec.ts index af22dad..125326b 100644 --- a/tests/automation/password.spec.ts +++ b/tests/automation/password.spec.ts @@ -1,15 +1,16 @@ import { Page } from '@playwright/test'; +import { englishStrippedStr } from '../localization/englishStrippedStr'; import { sleepFor } from '../promise_utils'; +import { Global, LeftPane, Settings } from './locators'; import { test_Alice_1W_no_network } from './setup/sessionTest'; import { + clickOn, clickOnMatchingText, - clickOnTestIdWithText, hasElementPoppedUpThatShouldnt, typeIntoInput, waitForTestIdWithText, } from './utilities/utils'; -import { englishStrippedStr } from '../localization/englishStrippedStr'; const testPassword = '123456'; const newTestPassword = '789101112'; @@ -20,7 +21,7 @@ async function expectRecoveryPhraseToBeVisible( ) { await waitForTestIdWithText( window, - 'recovery-password-seed-modal', + Settings.recoveryPasswordContainer.selector, recoveryPhrase, 1000, ); @@ -28,58 +29,73 @@ async function expectRecoveryPhraseToBeVisible( test_Alice_1W_no_network('Set Password', async ({ alice, aliceWindow1 }) => { // Click on settings tab - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); + await clickOn(aliceWindow1, LeftPane.settingsButton); // Click on privacy - await clickOnTestIdWithText(aliceWindow1, 'privacy-settings-menu-item'); + await clickOn(aliceWindow1, Settings.privacyMenuItem); // Click set password - await clickOnTestIdWithText(aliceWindow1, 'set-password-button'); + await clickOn(aliceWindow1, Settings.setPasswordSettingsButton); // Enter password - await typeIntoInput(aliceWindow1, 'password-input', testPassword); + await typeIntoInput( + aliceWindow1, + Settings.passwordInput.selector, + testPassword, + ); // Confirm password - await typeIntoInput(aliceWindow1, 'password-input-confirm', testPassword); - // Click Done - await clickOnMatchingText( + await typeIntoInput( aliceWindow1, - englishStrippedStr('save').toString(), + Settings.confirmPasswordInput.selector, + testPassword, ); + await clickOn(aliceWindow1, Settings.setPasswordButton); // Check toast notification await waitForTestIdWithText( aliceWindow1, - 'session-toast', + Global.toast.selector, englishStrippedStr('passwordSetDescriptionToast').toString(), ); // Click on settings tab await sleepFor(300, true); - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); - await clickOnTestIdWithText( - aliceWindow1, - 'recovery-password-settings-menu-item', - ); + await clickOn(aliceWindow1, Global.modalCloseButton); + await clickOn(aliceWindow1, LeftPane.settingsButton); + await clickOn(aliceWindow1, Settings.recoveryPasswordMenuItem); await sleepFor(300, true); // Type password into input field and validate it - await typeIntoInput(aliceWindow1, 'password-input', testPassword); + await typeIntoInput( + aliceWindow1, + Settings.passwordInput.selector, + testPassword, + ); // Click Done await clickOnMatchingText( aliceWindow1, - englishStrippedStr('done').toString(), + englishStrippedStr('enter').toString(), ); // check that the seed is visible now await expectRecoveryPhraseToBeVisible(aliceWindow1, alice.recoveryPassword); - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); + await clickOn(aliceWindow1, Global.modalCloseButton); + await clickOn(aliceWindow1, LeftPane.settingsButton); + await clickOn(aliceWindow1, Settings.privacyMenuItem); // Change password - await clickOnTestIdWithText(aliceWindow1, 'change-password-settings-button'); + await clickOn(aliceWindow1, Settings.changePasswordSettingsButton); // Enter old password - await typeIntoInput(aliceWindow1, 'password-input', testPassword); + await typeIntoInput( + aliceWindow1, + Settings.passwordInput.selector, + testPassword, + ); // Enter new password - await typeIntoInput(aliceWindow1, 'password-input-confirm', newTestPassword); - // await window.keyboard.press('Tab'); + await typeIntoInput( + aliceWindow1, + Settings.confirmPasswordInput.selector, + newTestPassword, + ); // Confirm new password await typeIntoInput( aliceWindow1, - 'password-input-reconfirm', + Settings.reConfirmPasswordInput.selector, newTestPassword, ); // Press enter on keyboard @@ -87,7 +103,7 @@ test_Alice_1W_no_network('Set Password', async ({ alice, aliceWindow1 }) => { // Check toast notification for 'changed password' await waitForTestIdWithText( aliceWindow1, - 'session-toast', + Global.toast.selector, englishStrippedStr('passwordChangedDescriptionToast').toString(), ); }); @@ -97,85 +113,76 @@ test_Alice_1W_no_network( async ({ alice: { recoveryPassword }, aliceWindow1 }) => { // Check if incorrect password works // Click on settings tab - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); + await clickOn(aliceWindow1, LeftPane.settingsButton); // Click on privacy - await clickOnMatchingText( - aliceWindow1, - englishStrippedStr('sessionPrivacy').toString(), - ); + await clickOn(aliceWindow1, Settings.privacyMenuItem); // Click set password - await clickOnTestIdWithText(aliceWindow1, 'set-password-button'); + await clickOn(aliceWindow1, Settings.setPasswordSettingsButton); // Enter password - await typeIntoInput(aliceWindow1, 'password-input', testPassword); + await typeIntoInput( + aliceWindow1, + Settings.passwordInput.selector, + testPassword, + ); // Confirm password - await typeIntoInput(aliceWindow1, 'password-input-confirm', testPassword); - // Click Done - await clickOnMatchingText( + await typeIntoInput( aliceWindow1, - englishStrippedStr('save').toString(), + Settings.confirmPasswordInput.selector, + testPassword, ); + await clickOn(aliceWindow1, Settings.setPasswordButton); // Click on recovery phrase tab - await sleepFor(100); - - // Click on settings tab - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); - await clickOnTestIdWithText( + await sleepFor(5000); + await clickOn(aliceWindow1, Global.modalBackButton); + await clickOn(aliceWindow1, Settings.recoveryPasswordMenuItem); + // Type password into input field + await typeIntoInput( aliceWindow1, - 'recovery-password-settings-menu-item', + Settings.passwordInput.selector, + testPassword, ); - // Type password into input field - await typeIntoInput(aliceWindow1, 'password-input', testPassword); // Confirm the password - await clickOnTestIdWithText(aliceWindow1, 'session-confirm-ok-button'); + await clickOn(aliceWindow1, Global.confirmButton); // this should print the recovery phrase await expectRecoveryPhraseToBeVisible(aliceWindow1, recoveryPassword); - // move away from the settings tab (otherwise the settings doesn't lock right away) - await clickOnTestIdWithText(aliceWindow1, 'message-section'); - - // Click on settings tab - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); + await clickOn(aliceWindow1, Global.modalBackButton); await sleepFor(500); // Click on recovery phrase tab - await clickOnTestIdWithText( + await clickOn(aliceWindow1, Settings.recoveryPasswordMenuItem); + // Try with incorrect password + await typeIntoInput( aliceWindow1, - 'recovery-password-settings-menu-item', + Settings.passwordInput.selector, + newTestPassword, ); - // Try with incorrect password - await typeIntoInput(aliceWindow1, 'password-input', newTestPassword); // Confirm the password - await clickOnTestIdWithText(aliceWindow1, 'session-confirm-ok-button'); + await clickOn(aliceWindow1, Global.confirmButton); // this should NOT print the recovery phrase await hasElementPoppedUpThatShouldnt( aliceWindow1, 'data-testid', - 'recovery-password-seed-modal', + Settings.recoveryPasswordContainer.selector, recoveryPassword, ); // Incorrect password below input showing? await waitForTestIdWithText( aliceWindow1, - 'error-message', + Global.errorMessage.selector, englishStrippedStr('passwordIncorrect').toString(), ); - await clickOnTestIdWithText(aliceWindow1, 'modal-close-button'); + await clickOn(aliceWindow1, Global.modalCloseButton); await sleepFor(100); // Click on recovery phrase tab - await clickOnTestIdWithText( - aliceWindow1, - 'recovery-password-settings-menu-item', - ); + await clickOn(aliceWindow1, Settings.recoveryPasswordMenuItem); // No password entered - await clickOnMatchingText( - aliceWindow1, - englishStrippedStr('done').toString(), - ); + await clickOn(aliceWindow1, Global.confirmButton); // Banner should ask for password to be entered await waitForTestIdWithText( aliceWindow1, - 'error-message', + Global.errorMessage.selector, englishStrippedStr('passwordIncorrect').toString(), ); }, diff --git a/tests/automation/setup/closeWindows.ts b/tests/automation/setup/closeWindows.ts index a4a358a..357cd79 100644 --- a/tests/automation/setup/closeWindows.ts +++ b/tests/automation/setup/closeWindows.ts @@ -1,4 +1,5 @@ import { Page } from '@playwright/test'; + import { sleepFor } from '../../promise_utils'; export const forceCloseAllWindows = async (windows: Array) => { diff --git a/tests/automation/setup/create_group.ts b/tests/automation/setup/create_group.ts index 21ba3e7..898a991 100644 --- a/tests/automation/setup/create_group.ts +++ b/tests/automation/setup/create_group.ts @@ -1,16 +1,19 @@ import { Page } from '@playwright/test'; + import { englishStrippedStr } from '../../localization/englishStrippedStr'; +import { sortByPubkey } from '../../pubkey'; +import { HomeScreen } from '../locators'; import { Group, User } from '../types/testing'; import { sendMessage } from '../utilities/message'; import { sendNewMessage } from '../utilities/send_message'; import { + clickOn, clickOnMatchingText, - clickOnTestIdWithText, + clickOnWithText, typeIntoInput, waitForTestIdWithText, waitForTextMessages, } from '../utilities/utils'; -import { sortByPubkey } from '../../pubkey'; export const createGroup = async ( userName: string, @@ -50,21 +53,21 @@ export const createGroup = async ( userOne.accountid, `${messageCA} Time: ${Date.now()}`, ); - // Focus screen on window C to allow user C to become contact - await clickOnTestIdWithText(windowC, 'messages-container'); - // wait for user C to be contact before moving to create group - // Create group with existing contact and session ID (of non-contact) // Click new closed group tab - await clickOnTestIdWithText(windowA, 'new-conversation-button'); - await clickOnTestIdWithText(windowA, 'chooser-new-group'); + await clickOn(windowA, HomeScreen.plusButton); + await clickOn(windowA, HomeScreen.createGroupOption); // Enter group name - await typeIntoInput(windowA, 'new-closed-group-name', group.userName); + await typeIntoInput( + windowA, + HomeScreen.createGroupGroupName.selector, + group.userName, + ); // Select user B await clickOnMatchingText(windowA, userTwo.userName); // Select user C await clickOnMatchingText(windowA, userThree.userName); // Click Next - await clickOnTestIdWithText(windowA, 'create-group-button'); + await clickOn(windowA, HomeScreen.createGroupCreateButton); // Check group was successfully created await clickOnMatchingText(windowB, group.userName); await waitForTestIdWithText( @@ -83,24 +86,12 @@ export const createGroup = async ( .withArgs({ name: firstUser, other_name: secondUser }) .toString(), ); - // Click on message section - await Promise.all([ - clickOnTestIdWithText(windowB, 'message-section'), - clickOnTestIdWithText(windowC, 'message-section'), - ]); // Click on test group - await Promise.all([ - clickOnTestIdWithText( - windowB, - 'module-conversation__user__profile-name', - group.userName, - ), - clickOnTestIdWithText( - windowC, - 'module-conversation__user__profile-name', - group.userName, + await Promise.all( + [windowB, windowC].map((w) => + clickOnWithText(w, HomeScreen.conversationItemName, group.userName), ), - ]); + ); // Make sure the empty state is in windowB & windowC await Promise.all([ waitForTestIdWithText( @@ -142,8 +133,5 @@ export const createGroup = async ( // windowC must see the message from A and the message from B await waitForTextMessages(windowC, [msgAToGroup, msgBToGroup]); - // Focus screen - // await clickOnTestIdWithText(windowB, 'scroll-to-bottom-button'); - return { userName, userOne, userTwo, userThree }; }; diff --git a/tests/automation/setup/new_user.ts b/tests/automation/setup/new_user.ts index 5653922..c59bec5 100644 --- a/tests/automation/setup/new_user.ts +++ b/tests/automation/setup/new_user.ts @@ -1,9 +1,17 @@ import { Page } from '@playwright/test'; import chalk from 'chalk'; + +import { + Global, + HomeScreen, + LeftPane, + Onboarding, + Settings, +} from '../locators'; import { User } from '../types/testing'; import { checkPathLight, - clickOnTestIdWithText, + clickOn, grabTextFromElement, typeIntoInput, waitForTestIdWithText, @@ -15,13 +23,16 @@ export const newUser = async ( awaitOnionPath = true, ): Promise => { // Create User - await clickOnTestIdWithText(window, 'create-account-button'); + await clickOn(window, Onboarding.createAccountButton); // Input username = testuser - await typeIntoInput(window, 'display-name-input', userName); - await clickOnTestIdWithText(window, 'continue-button'); + await typeIntoInput(window, Onboarding.displayNameInput.selector, userName); + await clickOn(window, Global.continueButton); // save recovery phrase - await clickOnTestIdWithText(window, 'reveal-recovery-phrase'); - await waitForTestIdWithText(window, 'recovery-password-seed-modal'); + await clickOn(window, HomeScreen.revealRecoveryPhraseButton); + await waitForTestIdWithText( + window, + Settings.recoveryPasswordContainer.selector, + ); const recoveryPassword = await grabTextFromElement( window, 'data-testid', @@ -30,22 +41,23 @@ export const newUser = async ( // const recoveryPhrase = await window.innerText( // '[data-testid=recovery-password-seed-modal]', // ); - // await clickOnTestIdWithText(window, 'modal-close-button'); - await clickOnTestIdWithText(window, 'leftpane-primary-avatar'); + await clickOn(window, Global.modalCloseButton); + await clickOn(window, LeftPane.profileButton); // Save Account ID to a variable - let accountid = await window.innerText('[data-testid=your-account-id]'); - accountid = accountid.replace(/(\r\n|\n|\r)/gm, ''); // remove the new line in the Account ID as it is rendered with one forced + let accountid = await window.innerText( + `[data-testid=${Settings.accountId.selector}]`, + ); + accountid = accountid.replace(/[^0-9a-fA-F]/g, ''); // keep only hex characters console.log( `${userName}: Account ID: "${chalk.blue( accountid, )}" and Recovery password: "${chalk.green(recoveryPassword)}"`, ); - await clickOnTestIdWithText(window, 'modal-close-button'); + await clickOn(window, Global.modalCloseButton); if (awaitOnionPath) { await checkPathLight(window); } - await clickOnTestIdWithText(window, 'message-section'); return { userName, accountid, recoveryPassword }; }; diff --git a/tests/automation/setup/open.ts b/tests/automation/setup/open.ts index b31b8ff..4f98ba4 100644 --- a/tests/automation/setup/open.ts +++ b/tests/automation/setup/open.ts @@ -1,5 +1,4 @@ import { _electron as electron } from '@playwright/test'; - import chalk from 'chalk'; import { isEmpty } from 'lodash'; import { join } from 'path'; @@ -25,8 +24,10 @@ const openElectronAppOnly = async (multi: string) => { process.env.NODE_APP_INSTANCE = `${MULTI_PREFIX}-devprod-${uniqueId}-${process.env.MULTI}`; process.env.NODE_ENV = NODE_ENV; process.env.SESSION_DEBUG = '1'; - process.env.LOCAL_DEVNET_SEED_URL = 'http://sesh-net.local:1280'; + process.env.LOCAL_DEVNET_SEED_URL = 'http://seed2.getsession.org:38157/'; + // process.env.LOCAL_DEVNET_SEED_URL = 'http://sesh-net:1280' + console.info(` ${process.env.LOCAL_DEVNET_SEED_URL}`); console.info(` NON CI RUN`); console.info(' NODE_ENV', process.env.NODE_ENV); console.info(' NODE_APP_INSTANCE', process.env.NODE_APP_INSTANCE); @@ -36,6 +37,7 @@ const openElectronAppOnly = async (multi: string) => { args: [ join(getAppRootPath(), 'ts', 'mains', 'main_node.js'), '--disable-gpu', + '--force-device-scale-factor=1', // Normalizes Retina and non-Retina mac screens ], }); return electronApp; diff --git a/tests/automation/setup/recovery_using_seed.ts b/tests/automation/setup/recovery_using_seed.ts index 50b95b9..de3f0a0 100644 --- a/tests/automation/setup/recovery_using_seed.ts +++ b/tests/automation/setup/recovery_using_seed.ts @@ -1,15 +1,17 @@ import { Page } from '@playwright/test'; + +import { Global, Onboarding } from '../locators'; import { - clickOnTestIdWithText, + clickOn, doesElementExist, typeIntoInput, waitForLoadingAnimationToFinish, } from '../utilities/utils'; export async function recoverFromSeed(window: Page, recoveryPhrase: string) { - await clickOnTestIdWithText(window, 'existing-account-button'); + await clickOn(window, Onboarding.iHaveAnAccountButton); await typeIntoInput(window, 'recovery-phrase-input', recoveryPhrase); - await clickOnTestIdWithText(window, 'continue-button'); + await clickOn(window, Global.continueButton); await waitForLoadingAnimationToFinish(window, 'loading-animation'); const displayName = await doesElementExist( window, diff --git a/tests/automation/setup/sessionTest.ts b/tests/automation/setup/sessionTest.ts index ab701e2..3b4e2c2 100644 --- a/tests/automation/setup/sessionTest.ts +++ b/tests/automation/setup/sessionTest.ts @@ -2,14 +2,15 @@ /* eslint-disable @typescript-eslint/array-type */ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/naming-convention */ -import { Page, TestInfo, test } from '@playwright/test'; +import { Page, test, TestInfo } from '@playwright/test'; +import chalk from 'chalk'; + import { Group, User } from '../types/testing'; import { linkedDevice } from '../utilities/linked_device'; import { forceCloseAllWindows } from './closeWindows'; import { createGroup } from './create_group'; import { newUser } from './new_user'; import { openApp } from './open'; -import chalk from 'chalk'; // This is not ideal, most of our test needs to open a specific number of windows and close them once the test is done or failed. // This file contains a bunch of utility function to use to open those windows and clean them afterwards. @@ -105,8 +106,8 @@ type LessThan< * This type can cause type checking performance issues, so only use it with small values. */ type NumericRange = - | Exclude> - | Exclude, LessThan>; + | Exclude, LessThan> + | Exclude>; function sessionTestGeneric< UserCount extends 1 | 2 | 3 | 4, diff --git a/tests/automation/switching_theme.spec.ts b/tests/automation/switching_theme.spec.ts index 86f9c25..67e0701 100644 --- a/tests/automation/switching_theme.spec.ts +++ b/tests/automation/switching_theme.spec.ts @@ -1,6 +1,8 @@ import { expect } from '@playwright/test'; + +import { LeftPane } from './locators'; import { test_Alice_1W_no_network } from './setup/sessionTest'; -import { clickOnTestIdWithText } from './utilities/utils'; +import { clickOn } from './utilities/utils'; test_Alice_1W_no_network('Switch themes', async ({ aliceWindow1 }) => { // Create @@ -9,7 +11,7 @@ test_Alice_1W_no_network('Switch themes', async ({ aliceWindow1 }) => { await expect(darkThemeColor).toHaveCSS('background-color', 'rgb(27, 27, 27)'); // Click theme button and change to dark theme - await clickOnTestIdWithText(aliceWindow1, 'theme-section'); + await clickOn(aliceWindow1, LeftPane.themeButton); // Check background colour of background to verify dark theme const lightThemeColor = aliceWindow1.locator('.inbox.index'); await expect(lightThemeColor).toHaveCSS( @@ -18,7 +20,7 @@ test_Alice_1W_no_network('Switch themes', async ({ aliceWindow1 }) => { ); // Toggle back to light theme - await clickOnTestIdWithText(aliceWindow1, 'theme-section'); + await clickOn(aliceWindow1, LeftPane.themeButton); // Check background colour again await expect(darkThemeColor).toHaveCSS('background-color', 'rgb(27, 27, 27)'); }); diff --git a/tests/automation/test.spec.ts b/tests/automation/test.spec.ts index 167c617..9017713 100644 --- a/tests/automation/test.spec.ts +++ b/tests/automation/test.spec.ts @@ -1,6 +1,7 @@ +import { Onboarding } from './locators'; import { sessionTestOneWindow } from './setup/sessionTest'; -import { clickOnTestIdWithText } from './utilities/utils'; +import { clickOn } from './utilities/utils'; sessionTestOneWindow('Tiny test', async ([windowA]) => { - await clickOnTestIdWithText(windowA, 'create-account-button'); + await clickOn(windowA, Onboarding.createAccountButton); }); diff --git a/tests/automation/types/landing_page_states.ts b/tests/automation/types/landing_page_states.ts deleted file mode 100644 index e7166d0..0000000 --- a/tests/automation/types/landing_page_states.ts +++ /dev/null @@ -1 +0,0 @@ -export type ElementState = 'new-account' | 'restored-account'; diff --git a/tests/automation/types/testing.ts b/tests/automation/types/testing.ts index 0ef2b8d..c7c2d33 100644 --- a/tests/automation/types/testing.ts +++ b/tests/automation/types/testing.ts @@ -13,24 +13,24 @@ export type Group = { userThree: User; }; -export type ConversationType = '1:1' | 'group' | 'community' | 'note-to-self'; +export type ConversationType = '1:1' | 'community' | 'group' | 'note-to-self'; export type DMTimeOption = + | 'disappear-off-option' + | 'input-10-seconds' | 'time-option-0-seconds' - | 'time-option-5-seconds' + | 'time-option-1-days' + | 'time-option-1-hours' | 'time-option-10-seconds' + | 'time-option-12-hours' + | 'time-option-14-days' + | 'time-option-30-minutes' | 'time-option-30-seconds' - | 'time-option-60-seconds' | 'time-option-5-minutes' - | 'time-option-30-minutes' - | 'time-option-1-hours' + | 'time-option-5-seconds' | 'time-option-6-hours' - | 'time-option-12-hours' - | 'time-option-1-days' - | 'time-option-7-days' - | 'time-option-14-days' - | 'input-10-seconds' - | 'disappear-off-option'; + | 'time-option-60-seconds' + | 'time-option-7-days'; type DisappearOpts1o1 = [ '1:1', @@ -73,121 +73,129 @@ export type WithMaxWait = { maxWait?: number }; export type WithRightButton = { rightButton?: boolean }; export type LoaderType = 'loading-animation' | 'loading-spinner'; -export type MediaType = 'image' | 'video' | 'audio' | 'file'; -export type Strategy = 'data-testid' | 'class' | ':has-text'; +export type MediaType = 'audio' | 'file' | 'image' | 'video'; +export type Strategy = ':has-text' | 'class' | 'data-testid'; -// Would be good to find a way to sort those with prettier -// TODO sort with eslint plugin perfectionist export type DataTestId = - | 'session-id-signup' - | 'display-name-input' - | 'recovery-password-seed-modal' - | 'path-light-container' - | 'new-conversation-button' + | DMTimeOption + | 'accept-message-request' + | 'audio-player' + | 'back-button' + | 'blocked-contacts-settings-row' + | 'call-button' + | 'call-notification-answered-a-call' + | 'call-notification-started-call' + | 'change-password-settings-button' + | 'chooser-invite-friend' + | 'chooser-new-community' | 'chooser-new-conversation-button' - | 'new-session-conversation' - | 'next-new-conversation-button' - | 'control-message' - | 'disappear-control-message' - | 'disappearing-messages-indicator' - | 'conversation-options-avatar' - | 'settings-section' + | 'chooser-new-group' | 'clear-data-settings-menu-item' - | 'message-requests-settings-menu-item' - | 'restore-using-recovery' - | 'reveal-recovery-phrase' - | 'recovery-phrase-input' + | 'clear-group-info-name-button' + | 'contact' + | 'context-menu-item' + | 'continue-button' | 'continue-session-button' - | 'label-device_and_network' - | 'message-request-banner' - | 'module-conversation__user__profile-name' + | 'control-message' + | 'conversation-options-avatar' + | 'conversations-settings-menu-item' + | 'copy-button-account-id' + | 'copy-button-profile-update' + | 'create-account-button' + | 'create-group-button' + | 'decline-and-block-message-request' | 'delete-message-request' - | 'session-confirm-ok-button' - | 'dropdownitem-5-seconds' + | 'disappear-after-read-option' + | 'disappear-after-send-option' + | 'disappear-control-message' + | 'disappear-messages-type-and-time' + | 'disappear-set-button' + | 'disappear-set-button' | 'disappearing-messages-dropdown' - | 'session-toast' - | 'accept-message-request' - | 'set-nickname-confirm-button' - | 'nickname-input' - | 'three-dots-conversation-options' - | 'message-section' - | 'conversations-settings-menu-item' - | 'reveal-blocked-user-settings' - | 'unblock-button-settings-screen' - | 'leftpane-primary-avatar' - | 'edit-profile-icon' + | 'disappearing-messages-indicator' + | 'disappearing-messages-menu-option' + | 'display-name-input' + | 'dropdownitem-5-seconds' | 'edit-group-name' - | 'profile-name-input' - | 'image-upload-section' - | 'save-button-profile-update' - | 'modal-close-button' - | 'send-message-button' - | 'message-input-text-area' - | 'end-voice-message' - | 'microphone-button' - | 'enable-microphone' - | 'theme-section' - | 'call-button' - | 'enable-calls' + | 'edit-profile-icon' + | 'empty-conversation-notification' + | 'enable-calls-settings-row' + | 'enable-microphone-settings-row' + | 'enable-read-receipts-settings-row' | 'end-call' - | 'privacy-settings-menu-item' - | 'set-password-button' - | 'password-input' - | 'password-input-confirm' - | 'change-password-settings-button' - | 'password-input-reconfirm' - | 'recovery-password-settings-menu-item' - | 'messages-container' - | 'chooser-new-group' - | 'new-closed-group-name' - | 'create-group-button' - | 'link-device' - | 'update-group-info-name-input' + | 'end-voice-message' + | 'error-message' + | 'existing-account-button' | 'group-name' + | 'group-update-message' | 'header-conversation-name' - | 'copy-button-profile-update' + | 'hide-recovery-password-settings-button' + | 'image-upload-click' + | 'image-upload-section' + | 'invite-contacts-menu-option' + | 'join-community-button' + | 'join-community-conversation' + | 'label-device_and_network' + | 'leave-group-button' + | 'leftpane-primary-avatar' + | 'link-device' + | 'link-preview-image' + | 'link-preview-title' | 'loading-spinner' - | 'empty-conversation-notification' - | 'your-profile-name' + | 'manage-members-menu-option' | 'mentions-popup-row' - | 'enable-read-receipts' - | 'disappear-set-button' - | 'disappear-after-read-option' - | 'disappearing-messages-menu-option' - | 'disappear-after-send-option' - | 'disappear-set-button' | 'message-content' - | 'group-update-message' + | 'message-input-text-area' + | 'message-request-banner' | 'message-request-response-message' - | 'image-upload-click' - | 'leave-group-button' - | 'create-account-button' - | 'continue-button' - | 'existing-account-button' - | 'context-menu-item' + | 'message-requests-settings-menu-item' + | 'messages-container' + | 'microphone-button' + | 'modal-back-button' + | 'modal-close-button' | 'modal-description' - | DMTimeOption - | `input-${DMTimeOption}` - | 'disappear-messages-type-and-time' - | 'hide-recovery-password-button' - | 'chooser-new-community' - | 'join-community-conversation' - | 'join-community-button' - | 'scroll-to-bottom-button' - | 'decline-and-block-message-request' - | 'contact' | 'modal-heading' - | 'call-notification-answered-a-call' - | 'call-notification-started-call' - | 'audio-player' - | 'chooser-invite-friend' - | 'your-account-id' - | 'copy-button-account-id' - | 'link-preview-image' - | 'link-preview-title' - | 'error-message' - | 'manage-members-menu-option' + | 'module-contact-name__profile-name' + | 'module-conversation__user__profile-name' + | 'new-closed-group-name' + | 'new-conversation-button' + | 'new-session-conversation' + | 'next-new-conversation-button' + | 'nickname-input' + | 'password-input-confirm' + | 'password-input-reconfirm' + | 'password-input' + | 'path-light-container' + | 'privacy-settings-menu-item' + | 'profile-name-input' + | 'recovery-password-seed-modal' + | 'recovery-password-settings-menu-item' + | 'recovery-phrase-input' + | 'restore-using-recovery' + | 'reveal-recovery-phrase' + | 'save-button-profile-update' + | 'scroll-to-bottom-button' + | 'send-message-button' | 'session-confirm-cancel-button' + | 'session-confirm-ok-button' + | 'session-id-signup' | 'session-recovery-password' - | 'invite-contacts-menu-option' - | 'clear-group-info-name-button'; + | 'session-toast' + | 'set-nickname-confirm-button' + | 'set-password-button' + | 'set-password-settings-button' + | 'settings-section' + | 'theme-section' + | 'unblock-button-settings-screen' + | 'update-group-info-name-input' + | 'update-profile-info-name-input' + | 'your-account-id' + | 'your-profile-name' + | `input-${DMTimeOption}`; + +export type ModalId = + | 'blockOrUnblockModal' + | 'confirmModal' + | 'deleteAccountModal' + | 'hideRecoveryPasswordModal' + | 'userSettingsModal'; diff --git a/tests/automation/user_actions.spec.ts b/tests/automation/user_actions.spec.ts index c71aba0..4347e34 100644 --- a/tests/automation/user_actions.spec.ts +++ b/tests/automation/user_actions.spec.ts @@ -1,6 +1,14 @@ import { expect } from '@playwright/test'; + import { englishStrippedStr } from '../localization/englishStrippedStr'; import { sleepFor } from '../promise_utils'; +import { + Conversation, + Global, + HomeScreen, + LeftPane, + Settings, +} from './locators'; import { newUser } from './setup/new_user'; import { sessionTestTwoWindows, @@ -9,15 +17,17 @@ import { test_Alice_2W, } from './setup/sessionTest'; import { createContact } from './utilities/create_contact'; -import { sendMessage } from './utilities/message'; +import { sendMessage, waitForReadTick } from './utilities/message'; import { checkModalStrings, + clickOn, clickOnElement, clickOnMatchingText, - clickOnTestIdWithText, + clickOnWithText, doesElementExist, hasElementBeenDeleted, typeIntoInput, + waitForLoadingAnimationToFinish, waitForMatchingText, waitForTestIdWithText, } from './utilities/utils'; @@ -33,13 +43,14 @@ sessionTestTwoWindows('Create contact', async ([windowA, windowB]) => { // Navigate to contacts tab in User B's window await waitForTestIdWithText( windowB, - 'message-request-response-message', + Conversation.messageRequestAcceptControlMessage.selector, englishStrippedStr('messageRequestYouHaveAccepted') .withArgs({ name: userA.userName, }) .toString(), ); + await clickOn(windowB, Global.backButton); await Promise.all([ clickOnElement({ window: windowA, @@ -53,16 +64,8 @@ sessionTestTwoWindows('Create contact', async ([windowA, windowB]) => { }), ]); await Promise.all([ - waitForTestIdWithText( - windowA, - 'module-conversation__user__profile-name', - userB.userName, - ), - waitForTestIdWithText( - windowB, - 'module-conversation__user__profile-name', - userA.userName, - ), + waitForTestIdWithText(windowA, Global.contactItem.selector, userB.userName), + waitForTestIdWithText(windowB, Global.contactItem.selector, userA.userName), ]); }); @@ -72,25 +75,25 @@ test_Alice_1W_Bob_1W( // Create contact and send new message await createContact(aliceWindow1, bobWindow1, alice, bob); // Check to see if User B is a contact - await clickOnTestIdWithText(aliceWindow1, 'new-conversation-button'); + await clickOn(aliceWindow1, HomeScreen.plusButton); await waitForTestIdWithText( aliceWindow1, - 'module-conversation__user__profile-name', + Global.contactItem.selector, bob.userName, ); // he is a contact, close the new conversation button tab as there is no right click allowed on it - await clickOnTestIdWithText(aliceWindow1, 'new-conversation-button'); + await clickOn(aliceWindow1, Global.backButton); // then right click on the contact conversation list item to show the menu - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, - true, + { rightButton: true }, ); // Select block - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'context-menu-item', + Global.contextMenuItem, englishStrippedStr('block').toString(), ); // Check modal strings @@ -101,25 +104,22 @@ test_Alice_1W_Bob_1W( .withArgs({ name: bob.userName }) .toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('block').toString(), ); // Verify the user was moved to the blocked contact list // Click on settings tab - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); + await clickOn(aliceWindow1, LeftPane.settingsButton); // click on settings section 'conversation' - await clickOnTestIdWithText( - aliceWindow1, - 'conversations-settings-menu-item', - ); + await clickOn(aliceWindow1, Settings.conversationsMenuItem); // Navigate to blocked users tab' - await clickOnTestIdWithText(aliceWindow1, 'reveal-blocked-user-settings'); + await clickOn(aliceWindow1, Settings.blockedContactsButton); // select the contact to unblock by clicking on it by name - await clickOnTestIdWithText(aliceWindow1, 'contact', bob.userName); + await clickOnWithText(aliceWindow1, Global.contactItem, bob.userName); // Unblock user by clicking on unblock - await clickOnTestIdWithText(aliceWindow1, 'unblock-button-settings-screen'); + await clickOn(aliceWindow1, Settings.unblockButton); // make sure the confirm dialogs shows up await checkModalStrings( aliceWindow1, @@ -127,11 +127,12 @@ test_Alice_1W_Bob_1W( englishStrippedStr('blockUnblockName') .withArgs({ name: bob.userName }) .toString(), + 'blockOrUnblockModal', ); // click on the unblock button - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('blockUnblock').toString(), ); // make sure no blocked contacts are listed @@ -145,47 +146,49 @@ test_Alice_1W_Bob_1W( test_Alice_1W_no_network('Change username', async ({ aliceWindow1 }) => { const newUsername = 'Tiny bubble'; // Open Profile - await clickOnTestIdWithText(aliceWindow1, 'leftpane-primary-avatar'); + await clickOn(aliceWindow1, LeftPane.profileButton); // Click on current username to open edit field - await clickOnTestIdWithText(aliceWindow1, 'edit-profile-icon'); + await clickOn(aliceWindow1, Settings.displayName); // Type in new username - await typeIntoInput(aliceWindow1, 'profile-name-input', newUsername); - // Press enter to confirm username input - await aliceWindow1.keyboard.press('Enter'); - // Wait for Copy button to appear to verify username change - await aliceWindow1.isVisible(`'${englishStrippedStr('copy').toString()}'`); - // verify name change - expect(await aliceWindow1.innerText('[data-testid=your-profile-name]')).toBe( + await typeIntoInput( + aliceWindow1, + Settings.displayNameInput.selector, newUsername, ); + await clickOnMatchingText( + aliceWindow1, + englishStrippedStr('save').toString(), + ); + await sleepFor(1000); + // verify name change + expect( + await aliceWindow1.innerText( + `[data-testid=${Settings.displayName.selector}]`, + ), + ).toBe(newUsername); // Exit profile modal - await clickOnTestIdWithText(aliceWindow1, 'modal-close-button'); + await clickOn(aliceWindow1, Global.modalCloseButton); }); -// TODO: Normalize screenshot dimensions before comparison to handle different pixel densities (e.g. with sharp) -// This would fix MacBook Retina (2x) vs M4 Mac Mini (1x) pixel density differences (56x56 vs 28x28) -// Alternatives: -// - Try to set deviceScaleFactor: 1 in Playwright context to force consistent scaling -// - Record pixel density dependent screenshots - test_Alice_1W_no_network( 'Change avatar', async ({ aliceWindow1 }, testInfo) => { // Open profile - await clickOnTestIdWithText(aliceWindow1, 'leftpane-primary-avatar'); + await clickOn(aliceWindow1, LeftPane.profileButton); // Click on current profile picture - await waitForTestIdWithText( - aliceWindow1, - 'copy-button-profile-update', - englishStrippedStr('copy').toString(), - ); + await clickOn(aliceWindow1, Settings.displayName); - await clickOnTestIdWithText(aliceWindow1, 'image-upload-section'); - await clickOnTestIdWithText(aliceWindow1, 'image-upload-click'); + await clickOn(aliceWindow1, Settings.imageUploadSection); + await clickOn(aliceWindow1, Settings.imageUploadClick); // allow for the image to be resized before we try to save it await sleepFor(500); - await clickOnTestIdWithText(aliceWindow1, 'save-button-profile-update'); - await waitForTestIdWithText(aliceWindow1, 'loading-spinner'); + await clickOn(aliceWindow1, Settings.saveProfileUpdateButton); + await waitForLoadingAnimationToFinish(aliceWindow1, 'loading-spinner'); + await clickOnMatchingText( + aliceWindow1, + englishStrippedStr('save').toString(), + ); + await clickOn(aliceWindow1, Global.modalCloseButton); // if we were asked to update the snapshots, make sure we wait for the change to be received before taking a screenshot. if (testInfo.config.updateSnapshots === 'all') { await sleepFor(15000); @@ -195,7 +198,7 @@ test_Alice_1W_no_network( await sleepFor(500); const leftpaneAvatarContainer = await waitForTestIdWithText( aliceWindow1, - 'leftpane-primary-avatar', + LeftPane.profileButton.selector, ); const start = Date.now(); let correctScreenshot = false; @@ -235,16 +238,11 @@ test_Alice_1W_Bob_1W( const nickname = 'new nickname for Bob'; await createContact(aliceWindow1, bobWindow1, alice, bob); - await clickOnElement({ - window: aliceWindow1, - strategy: 'data-testid', - selector: 'message-section', - }); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, - true, + { rightButton: true }, ); await clickOnMatchingText( aliceWindow1, @@ -254,9 +252,9 @@ test_Alice_1W_Bob_1W( await typeIntoInput(aliceWindow1, 'nickname-input', nickname); await sleepFor(100); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'set-nickname-confirm-button', + HomeScreen.setNicknameButton, englishStrippedStr('save').toString(), ); await sleepFor(1000); @@ -287,44 +285,41 @@ test_Alice_1W_Bob_1W( await clickOnElement({ window: aliceWindow1, strategy: 'data-testid', - selector: 'settings-section', - }); - await clickOnElement({ - window: aliceWindow1, - strategy: 'data-testid', - selector: 'enable-read-receipts', + selector: LeftPane.settingsButton.selector, }); + await clickOn(aliceWindow1, Settings.privacyMenuItem); await clickOnElement({ window: aliceWindow1, strategy: 'data-testid', - selector: 'message-section', + selector: Settings.enableReadReceipts.selector, }); - await clickOnTestIdWithText( + await clickOn(aliceWindow1, Global.modalCloseButton); + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, ); await clickOnElement({ window: bobWindow1, strategy: 'data-testid', - selector: 'settings-section', - }); - await clickOnElement({ - window: bobWindow1, - strategy: 'data-testid', - selector: 'enable-read-receipts', + selector: LeftPane.settingsButton.selector, }); + await clickOn(bobWindow1, Settings.privacyMenuItem); + await clickOnElement({ window: bobWindow1, strategy: 'data-testid', - selector: 'message-section', + selector: Settings.enableReadReceipts.selector, }); - await clickOnTestIdWithText( + await clickOn(bobWindow1, Global.modalCloseButton); + await sendMessage(aliceWindow1, 'Testing read receipts'); + await clickOn(bobWindow1, Global.backButton); + await clickOnWithText( bobWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, alice.userName, ); - await sendMessage(aliceWindow1, 'Testing read receipts'); + await waitForReadTick(aliceWindow1, 'Testing read receipts'); }, ); @@ -333,46 +328,41 @@ test_Alice_1W_Bob_1W( async ({ aliceWindow1, bobWindow1, alice, bob }) => { // Create contact and send new message await createContact(aliceWindow1, bobWindow1, alice, bob); - // Confirm contact by checking Messages tab (name should appear in list) - await Promise.all([ - clickOnTestIdWithText(aliceWindow1, 'message-section'), - clickOnTestIdWithText(bobWindow1, 'message-section'), - ]); - await Promise.all([ - clickOnElement({ - window: aliceWindow1, - strategy: 'data-testid', - selector: 'new-conversation-button', - }), - clickOnElement({ - window: bobWindow1, - strategy: 'data-testid', - selector: 'new-conversation-button', - }), - ]); + await clickOn(bobWindow1, Global.backButton); + await Promise.all( + [aliceWindow1, bobWindow1].map((w) => + clickOnElement({ + window: w, + strategy: 'data-testid', + selector: 'new-conversation-button', + }), + ), + ); await Promise.all([ waitForTestIdWithText( aliceWindow1, - 'module-conversation__user__profile-name', + Global.contactItem.selector, bob.userName, ), waitForTestIdWithText( bobWindow1, - 'module-conversation__user__profile-name', + Global.contactItem.selector, alice.userName, ), ]); + await Promise.all( + [aliceWindow1, bobWindow1].map((w) => clickOn(w, Global.backButton)), + ); // Delete contact - await clickOnTestIdWithText(aliceWindow1, 'message-section'); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, bob.userName, - true, + { rightButton: true }, ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'context-menu-item', + Global.contextMenuItem, englishStrippedStr('conversationsDelete').toString(), ); await checkModalStrings( @@ -382,16 +372,16 @@ test_Alice_1W_Bob_1W( .withArgs({ name: bob.userName }) .toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('delete').toString(), ); // Check if conversation is deleted await hasElementBeenDeleted( aliceWindow1, 'data-testid', - 'module-conversation__user__profile-name', + Global.contactItem.selector, 1000, bob.userName, ); @@ -401,12 +391,9 @@ test_Alice_1W_Bob_1W( test_Alice_2W( 'Hide recovery password', async ({ aliceWindow1, aliceWindow2 }) => { - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); - await clickOnTestIdWithText( - aliceWindow1, - 'recovery-password-settings-menu-item', - ); - await clickOnTestIdWithText(aliceWindow1, 'hide-recovery-password-button'); + await clickOn(aliceWindow1, LeftPane.settingsButton); + await clickOn(aliceWindow1, Settings.recoveryPasswordMenuItem); + await clickOn(aliceWindow1, Settings.hideRecoveryPasswordButton); // Check first modal await checkModalStrings( aliceWindow1, @@ -414,10 +401,11 @@ test_Alice_2W( englishStrippedStr( 'recoveryPasswordHidePermanentlyDescription1', ).toString(), + 'hideRecoveryPasswordModal', ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('theContinue').toString(), ); await checkModalStrings( @@ -426,36 +414,37 @@ test_Alice_2W( englishStrippedStr( 'recoveryPasswordHidePermanentlyDescription2', ).toString(), + 'hideRecoveryPasswordModal', ); // Click yes - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('yes').toString(), ); await doesElementExist( aliceWindow1, 'data-testid', - 'recovery-password-settings-menu-item', + Settings.recoveryPasswordMenuItem.selector, ); // Check linked device if Recovery Password is still visible (it should be) - await clickOnTestIdWithText(aliceWindow2, 'settings-section'); + await clickOn(aliceWindow2, LeftPane.settingsButton); await waitForTestIdWithText( aliceWindow2, - 'recovery-password-settings-menu-item', + Settings.recoveryPasswordMenuItem.selector, ); }, ); test_Alice_1W_no_network('Invite a friend', async ({ aliceWindow1, alice }) => { - await clickOnTestIdWithText(aliceWindow1, 'new-conversation-button'); - await clickOnTestIdWithText(aliceWindow1, 'chooser-invite-friend'); + await clickOn(aliceWindow1, HomeScreen.plusButton); + await clickOn(aliceWindow1, HomeScreen.inviteAFriendOption); await waitForTestIdWithText(aliceWindow1, 'your-account-id', alice.accountid); - await clickOnTestIdWithText(aliceWindow1, 'copy-button-account-id'); + await clickOn(aliceWindow1, HomeScreen.inviteAFriendCopyButton); // Toast await waitForTestIdWithText( aliceWindow1, - 'session-toast', + Global.toast.selector, englishStrippedStr('copied').toString(), ); // Wait for copy to resolve @@ -469,19 +458,17 @@ test_Alice_1W_no_network('Invite a friend', async ({ aliceWindow1, alice }) => { englishStrippedStr('shareAccountIdDescriptionCopied').toString(), ); // To exit invite a friend - await clickOnTestIdWithText(aliceWindow1, 'new-conversation-button'); - // To create note to self - await clickOnTestIdWithText(aliceWindow1, 'new-conversation-button'); + await clickOn(aliceWindow1, Global.backButton); // New message - await clickOnTestIdWithText(aliceWindow1, 'chooser-new-conversation-button'); - await clickOnTestIdWithText(aliceWindow1, 'new-session-conversation'); + await clickOn(aliceWindow1, HomeScreen.newMessageOption); + await clickOn(aliceWindow1, HomeScreen.newMessageAccountIDInput); const isMac = process.platform === 'darwin'; await aliceWindow1.keyboard.press(`${isMac ? 'Meta' : 'Control'}+V`); - await clickOnTestIdWithText(aliceWindow1, 'next-new-conversation-button'); + await clickOn(aliceWindow1, HomeScreen.newMessageNextButton); // Did the copied text create note to self? await waitForTestIdWithText( aliceWindow1, - 'header-conversation-name', + Conversation.conversationHeader.selector, englishStrippedStr('noteToSelf').toString(), ); }); @@ -489,31 +476,28 @@ test_Alice_1W_no_network('Invite a friend', async ({ aliceWindow1, alice }) => { test_Alice_1W_no_network( 'Hide note to self', async ({ aliceWindow1, alice }) => { - await clickOnTestIdWithText(aliceWindow1, 'new-conversation-button'); - await clickOnTestIdWithText( - aliceWindow1, - 'chooser-new-conversation-button', - ); + await clickOn(aliceWindow1, HomeScreen.plusButton); + await clickOn(aliceWindow1, HomeScreen.newMessageOption); await typeIntoInput( aliceWindow1, 'new-session-conversation', alice.accountid, ); - await clickOnTestIdWithText(aliceWindow1, 'next-new-conversation-button'); + await clickOn(aliceWindow1, HomeScreen.newMessageNextButton); await waitForTestIdWithText( aliceWindow1, - 'header-conversation-name', + Conversation.conversationHeader.selector, englishStrippedStr('noteToSelf').toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, englishStrippedStr('noteToSelf').toString(), - true, + { rightButton: true }, ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'context-menu-item', + Global.contextMenuItem, englishStrippedStr('noteToSelfHide').toString(), ); await checkModalStrings( @@ -521,9 +505,9 @@ test_Alice_1W_no_network( englishStrippedStr('noteToSelfHide').toString(), englishStrippedStr('noteToSelfHideDescription').toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( aliceWindow1, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('hide').toString(), ); await hasElementBeenDeleted( @@ -537,24 +521,30 @@ test_Alice_1W_no_network( ); test_Alice_1W_no_network('Toggle password', async ({ aliceWindow1 }) => { - await clickOnTestIdWithText(aliceWindow1, 'settings-section'); - await clickOnTestIdWithText( + await clickOn(aliceWindow1, LeftPane.settingsButton); + await clickOn(aliceWindow1, Settings.recoveryPasswordMenuItem); + await waitForTestIdWithText( aliceWindow1, - 'recovery-password-settings-menu-item', + Settings.recoveryPasswordContainer.selector, ); - await waitForTestIdWithText(aliceWindow1, 'recovery-password-seed-modal'); await clickOnMatchingText( aliceWindow1, englishStrippedStr('qrView').toString(), ); // Wait for QR code to be visible - await waitForTestIdWithText(aliceWindow1, 'session-recovery-password'); + await waitForTestIdWithText( + aliceWindow1, + Settings.recoveryPasswordQRCode.selector, + ); // Then toggle back to text seed password await clickOnMatchingText( aliceWindow1, englishStrippedStr('recoveryPasswordView').toString(), ); - await waitForTestIdWithText(aliceWindow1, 'recovery-password-seed-modal'); + await waitForTestIdWithText( + aliceWindow1, + Settings.recoveryPasswordContainer.selector, + ); }); test_Alice_2W( diff --git a/tests/automation/utilities/create_contact.ts b/tests/automation/utilities/create_contact.ts index 28c6adc..4b69332 100644 --- a/tests/automation/utilities/create_contact.ts +++ b/tests/automation/utilities/create_contact.ts @@ -1,8 +1,10 @@ import { Page } from '@playwright/test'; + +import { HomeScreen } from '../locators'; import { User } from '../types/testing'; import { replyTo } from './reply_message'; import { sendNewMessage } from './send_message'; -import { clickOnElement, clickOnTestIdWithText } from './utils'; +import { clickOnElement, clickOnWithText } from './utils'; export const createContact = async ( windowA: Page, @@ -19,9 +21,9 @@ export const createContact = async ( strategy: 'data-testid', selector: 'message-request-banner', }); - await clickOnTestIdWithText( + await clickOnWithText( windowB, - 'module-conversation__user__profile-name', + HomeScreen.conversationItemName, userA.userName, ); await clickOnElement({ diff --git a/tests/automation/utilities/join_community.ts b/tests/automation/utilities/join_community.ts index f31c0d9..d6dd074 100644 --- a/tests/automation/utilities/join_community.ts +++ b/tests/automation/utilities/join_community.ts @@ -1,16 +1,22 @@ import { Page } from '@playwright/test'; + import { testCommunityLink } from '../constants/community'; +import { HomeScreen } from '../locators'; import { - clickOnTestIdWithText, + clickOn, typeIntoInput, waitForLoadingAnimationToFinish, } from './utils'; export const joinCommunity = async (window: Page) => { - await clickOnTestIdWithText(window, 'new-conversation-button'); - await clickOnTestIdWithText(window, 'chooser-new-community'); + await clickOn(window, HomeScreen.plusButton); + await clickOn(window, HomeScreen.joinCommunityOption); // The follow two test tags are pending implementation - await typeIntoInput(window, 'join-community-conversation', testCommunityLink); - await clickOnTestIdWithText(window, 'join-community-button'); + await typeIntoInput( + window, + HomeScreen.joinCommunityInput.selector, + testCommunityLink, + ); + await clickOn(window, HomeScreen.joinCommunityButton); await waitForLoadingAnimationToFinish(window, 'loading-spinner'); }; diff --git a/tests/automation/utilities/leave_group.ts b/tests/automation/utilities/leave_group.ts index a6defcf..4cd2750 100644 --- a/tests/automation/utilities/leave_group.ts +++ b/tests/automation/utilities/leave_group.ts @@ -1,24 +1,27 @@ import { Page } from '@playwright/test'; + +import { englishStrippedStr } from '../../localization/englishStrippedStr'; +import { Conversation, Global } from '../locators'; import { Group } from '../types/testing'; import { + clickOn, clickOnMatchingText, - clickOnTestIdWithText, + clickOnWithText, hasElementBeenDeleted, } from './utils'; -import { englishStrippedStr } from '../../localization/englishStrippedStr'; export const leaveGroup = async (window: Page, group: Group) => { // go to three dots menu - await clickOnTestIdWithText(window, 'conversation-options-avatar'); + await clickOn(window, Conversation.conversationSettingsIcon); // Select Leave Group await clickOnMatchingText( window, englishStrippedStr('groupLeave').toString(), ); // Confirm leave group - await clickOnTestIdWithText( + await clickOnWithText( window, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('leave').toString(), ); // check config message diff --git a/tests/automation/utilities/message.ts b/tests/automation/utilities/message.ts index 7bb03bd..f42dff4 100644 --- a/tests/automation/utilities/message.ts +++ b/tests/automation/utilities/message.ts @@ -1,4 +1,5 @@ import { Page } from '@playwright/test'; + // eslint-disable-next-line import/no-cycle import { clickOnElement, typeIntoInput } from './utils'; @@ -17,6 +18,21 @@ export const waitForSentTick = async (window: Page, message: string) => { ); }; +export const waitForReadTick = async (window: Page, message: string) => { + // wait for confirmation tick to send reply message + const selc = `css=[data-testid=message-content]:has-text("${message}"):has([data-testid=msg-status][data-testtype=read])`; + console.info('waiting for read tick of message: ', message); + + const tickMessageRead = await window.waitForSelector(selc, { + timeout: 30000, + }); + console.info( + 'found the tick of message read: ', + message, + Boolean(tickMessageRead), + ); +}; + export const sendMessage = async (window: Page, message: string) => { // type into message input box await typeIntoInput(window, 'message-input-text-area', message); diff --git a/tests/automation/utilities/rename_group.ts b/tests/automation/utilities/rename_group.ts index 741e1e7..8aca644 100644 --- a/tests/automation/utilities/rename_group.ts +++ b/tests/automation/utilities/rename_group.ts @@ -1,12 +1,14 @@ import { Page } from '@playwright/test'; + +import { englishStrippedStr } from '../../localization/englishStrippedStr'; +import { Conversation, ConversationSettings, Global } from '../locators'; import { + clickOn, clickOnMatchingText, - clickOnTestIdWithText, typeIntoInput, waitForMatchingText, waitForTestIdWithText, } from './utils'; -import { englishStrippedStr } from '../../localization/englishStrippedStr'; export const renameGroup = async ( window: Page, @@ -14,13 +16,13 @@ export const renameGroup = async ( newGroupName: string, ) => { await clickOnMatchingText(window, oldGroupName); - await clickOnTestIdWithText(window, 'conversation-options-avatar'); - await clickOnTestIdWithText(window, 'edit-group-name'); + await clickOn(window, Conversation.conversationSettingsIcon); + await clickOn(window, ConversationSettings.editGroupButton); await typeIntoInput(window, 'update-group-info-name-input', newGroupName); await window.keyboard.press('Enter'); await clickOnMatchingText(window, englishStrippedStr('save').toString()); await waitForTestIdWithText(window, 'group-name', newGroupName); - await clickOnTestIdWithText(window, 'modal-close-button'); + await clickOn(window, Global.modalCloseButton); // Check config message await waitForMatchingText( window, diff --git a/tests/automation/utilities/reply_message.ts b/tests/automation/utilities/reply_message.ts index b257d1f..0c3046c 100644 --- a/tests/automation/utilities/reply_message.ts +++ b/tests/automation/utilities/reply_message.ts @@ -1,4 +1,5 @@ import { Page } from '@playwright/test'; + import { englishStrippedStr } from '../../localization/englishStrippedStr'; import { sleepFor } from '../../promise_utils'; import { Strategy } from '../types/testing'; diff --git a/tests/automation/utilities/send_media.ts b/tests/automation/utilities/send_media.ts index 6533f6e..a9ce17f 100644 --- a/tests/automation/utilities/send_media.ts +++ b/tests/automation/utilities/send_media.ts @@ -1,17 +1,20 @@ import { Page } from '@playwright/test'; + import { englishStrippedStr } from '../../localization/englishStrippedStr'; import { sleepFor } from '../../promise_utils'; +import { Conversation, Global, Settings } from '../locators'; +import { MediaType } from '../types/testing'; +import { waitForSentTick } from './message'; import { checkModalStrings, + clickOn, clickOnElement, clickOnMatchingText, - clickOnTestIdWithText, + clickOnWithText, typeIntoInput, waitForLoadingAnimationToFinish, waitForTestIdWithText, } from './utils'; -import { MediaType } from '../types/testing'; -import { waitForSentTick } from './message'; export const sendMedia = async ( window: Page, @@ -30,13 +33,13 @@ export const sendMedia = async ( }; export const sendVoiceMessage = async (window: Page) => { - await clickOnTestIdWithText(window, 'microphone-button'); - await clickOnTestIdWithText(window, 'session-toast'); - await clickOnTestIdWithText(window, 'enable-microphone'); - await clickOnTestIdWithText(window, 'message-section'); - await clickOnTestIdWithText(window, 'microphone-button'); + await clickOn(window, Conversation.microphoneButton); + await clickOn(window, Global.toast); + await clickOn(window, Settings.enableMicrophone); + await clickOn(window, Global.modalCloseButton); + await clickOn(window, Conversation.microphoneButton); await sleepFor(5000); - await clickOnTestIdWithText(window, 'end-voice-message'); + await clickOn(window, Conversation.endVoiceMessageButton); await sleepFor(4000); await clickOnElement({ window, @@ -56,7 +59,9 @@ export const sendLinkPreview = async (window: Page, testLink: string) => { strategy: 'data-testid', selector: 'send-message-button', }); - await clickOnTestIdWithText(window, 'message-content', testLink, true); + await clickOnWithText(window, Conversation.messageContent, testLink, { + rightButton: true, + }); // Need to copy link to clipboard, as the enable link preview modal // doesn't pop up if manually typing link (needs to be pasted) // Need to have a nth(0) here to account for Copy Account ID, Appium was getting confused @@ -73,9 +78,9 @@ export const sendLinkPreview = async (window: Page, testLink: string) => { englishStrippedStr('copied').toString(), ); // click on the toast and wait for it to be closed to avoid the layout shift - await clickOnTestIdWithText(window, 'session-toast'); + await clickOn(window, Global.toast); await sleepFor(1000); - await clickOnTestIdWithText(window, 'message-input-text-area'); + await clickOn(window, Conversation.messageInput); const isMac = process.platform === 'darwin'; await window.keyboard.press(`${isMac ? 'Meta' : 'Control'}+V`); await checkModalStrings( @@ -83,9 +88,9 @@ export const sendLinkPreview = async (window: Page, testLink: string) => { englishStrippedStr('linkPreviewsEnable').toString(), englishStrippedStr('linkPreviewsFirstDescription').toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( window, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('enable').toString(), ); await waitForLoadingAnimationToFinish(window, 'loading-spinner'); @@ -126,9 +131,9 @@ export const trustUser = async ( }) .toString(), ); - await clickOnTestIdWithText( + await clickOnWithText( window, - 'session-confirm-ok-button', + Global.confirmButton, englishStrippedStr('yes').toString(), ); }; diff --git a/tests/automation/utilities/send_message.ts b/tests/automation/utilities/send_message.ts index 01a5ed9..a3769b2 100644 --- a/tests/automation/utilities/send_message.ts +++ b/tests/automation/utilities/send_message.ts @@ -1,17 +1,19 @@ import { Page } from '@playwright/test'; + +import { HomeScreen } from '../locators'; import { sendMessage } from './message'; -import { clickOnTestIdWithText, typeIntoInput } from './utils'; +import { clickOn, typeIntoInput } from './utils'; export const sendNewMessage = async ( window: Page, sessionid: string, message: string, ) => { - await clickOnTestIdWithText(window, 'new-conversation-button'); - await clickOnTestIdWithText(window, 'chooser-new-conversation-button'); + await clickOn(window, HomeScreen.plusButton); + await clickOn(window, HomeScreen.newMessageOption); // Enter session ID of USER B await typeIntoInput(window, 'new-session-conversation', sessionid); // click next - await clickOnTestIdWithText(window, 'next-new-conversation-button'); + await clickOn(window, HomeScreen.newMessageNextButton); await sendMessage(window, message); }; diff --git a/tests/automation/utilities/set_disappearing_messages.ts b/tests/automation/utilities/set_disappearing_messages.ts index 71fdf2f..7f5b184 100644 --- a/tests/automation/utilities/set_disappearing_messages.ts +++ b/tests/automation/utilities/set_disappearing_messages.ts @@ -1,21 +1,22 @@ import { Page } from '@playwright/test'; + +import { englishStrippedStr } from '../../localization/englishStrippedStr'; +import { Conversation, ConversationSettings } from '../locators'; import { ConversationType, DataTestId, DisappearOptions, } from '../types/testing'; -import { englishStrippedStr } from '../../localization/englishStrippedStr'; +import { isChecked } from './checked'; import { checkModalStrings, + clickOn, clickOnElement, clickOnMatchingText, - clickOnTestIdWithText, - doWhileWithMax, formatTimeOption, waitForElement, waitForTestIdWithText, } from './utils'; -import { isChecked } from './checked'; export const setDisappearingMessages = async ( windowA: Page, @@ -28,30 +29,14 @@ export const setDisappearingMessages = async ( windowB?: Page, ) => { const enforcedType: ConversationType = conversationType; - await doWhileWithMax(5000, 1000, 'setDisappearingMessages', async () => { - try { - await clickOnTestIdWithText( - windowA, - 'conversation-options-avatar', - undefined, - undefined, - 1000, - ); - await clickOnElement({ - window: windowA, - strategy: 'data-testid', - selector: 'disappearing-messages-menu-option', - maxWait: 100, - }); - return true; - } catch (e) { - console.log( - 'setDisappearingMessages doWhileWithMax action threw:', - e.message, - ); - - return false; - } + await clickOn(windowA, Conversation.conversationSettingsIcon, { + maxWait: 5_000, + }); + await clickOnElement({ + window: windowA, + strategy: 'data-testid', + selector: ConversationSettings.disappearingMessagesOption.selector, + maxWait: 5_000, }); if (enforcedType === '1:1') { diff --git a/tests/automation/utilities/utils.ts b/tests/automation/utilities/utils.ts index c0e4db6..ed7428f 100644 --- a/tests/automation/utilities/utils.ts +++ b/tests/automation/utilities/utils.ts @@ -3,11 +3,13 @@ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable no-await-in-loop */ import { ElementHandle, Page } from '@playwright/test'; + import { sleepFor } from '../../promise_utils'; import { - DMTimeOption, DataTestId, + DMTimeOption, LoaderType, + ModalId, Strategy, StrategyExtractionObj, WithMaxWait, @@ -15,10 +17,14 @@ import { WithRightButton, } from '../types/testing'; import { sendMessage } from './message'; -import fs from 'fs'; -import path from 'path'; -import { screenshotFolder } from '../constants/variables'; -import type { ElementState } from '../types/landing_page_states'; + +type ElementOptions = { + maxWait?: number; + rightButton?: boolean; +}; + +// TODO Unify element interaction functions to use locator objects the way clickOn and clickOnWithText do +// Remaining functions to migrate: waitForElement, typeIntoInput, grabTextFromElement etc. // WAIT FOR FUNCTIONS @@ -165,7 +171,7 @@ export async function waitForLoadingAnimationToFinish( loader: LoaderType, maxWait?: number, ) { - let loadingAnimation: ElementHandle | undefined; + let loadingAnimation: ElementHandle | undefined; await waitForElement(window, 'data-testid', `${loader}`, maxWait); @@ -242,19 +248,72 @@ export async function checkPathLight(window: Page, maxWait?: number) { // ACTIONS +/** + * Clicks on an element using a locator object + * @param window - Playwright page instance + * @param locator - Element locator with strategy and selector + * @param options - Optional element interaction configuration + */ +export async function clickOn( + window: Page, + locator: StrategyExtractionObj, + options?: ElementOptions, +) { + let builtSelector: string; + + if (locator.strategy === 'class') { + builtSelector = `css=.${locator.selector}`; + } else { + builtSelector = `css=[${locator.strategy}=${locator.selector}]`; + } + + const sharedOpts = { timeout: options?.maxWait, strict: true }; + await window.click( + builtSelector, + options?.rightButton ? { ...sharedOpts, button: 'right' } : sharedOpts, + ); +} + +/** + * Clicks on an element that contains specific text + * @param window - Playwright page instance + * @param locator - Element locator with strategy and selector + * @param text - Text content to match within the element + * @param options - Optional element interaction configuration + */ +export async function clickOnWithText( + window: Page, + locator: StrategyExtractionObj, + text: string, + options?: ElementOptions, +) { + let builtSelector: string; + + if (locator.strategy === 'class') { + builtSelector = `css=.${locator.selector}:has-text("${text.replace( + /"/g, + '\\"', + )}")`; + } else { + builtSelector = `css=[${locator.strategy}=${ + locator.selector + }]:has-text("${text.replace(/"/g, '\\"')}")`; + } + + const sharedOpts = { timeout: options?.maxWait, strict: true }; + await window.click( + builtSelector, + options?.rightButton ? { ...sharedOpts, button: 'right' } : sharedOpts, + ); +} +// Legacy wrapper for backwards compatibility export async function clickOnElement({ window, maxWait, rightButton, ...obj }: WithPage & StrategyExtractionObj & WithMaxWait & WithRightButton) { - const builtSelector = `css=[${obj.strategy}=${obj.selector}]`; - console.info(`clickOnElement: looking for selector ${builtSelector}`); - const sharedOpts = { timeout: maxWait }; - await window.click( - builtSelector, - rightButton ? { ...sharedOpts, button: 'right' } : sharedOpts, - ); + return clickOn(window, obj, { maxWait, rightButton }); } export async function lookForPartialTestId( @@ -276,8 +335,6 @@ export async function lookForPartialTestId( return builtSelector; } -// - export async function clickOnMatchingText( window: Page, text: string, @@ -293,35 +350,6 @@ export async function clickOnMatchingText( ); } -export async function clickOnTestIdWithText( - window: Page, - dataTestId: DataTestId, - text?: string, - rightButton?: boolean, - maxWait?: number, -) { - const sharedOpts = { timeout: maxWait, strict: true }; - console.info( - `clickOnTestIdWithText with testId:${dataTestId} and text:${ - text || 'none' - }, rightButton:${!!rightButton}`, - ); - - const builtSelector = !text - ? `css=[data-testid=${dataTestId}]` - : `css=[data-testid=${dataTestId}]:has-text("${text}")`; - - await window.click( - builtSelector, - rightButton ? { ...sharedOpts, button: 'right' } : sharedOpts, - ); - console.info( - `clickOnTestIdWithText:clicked! testId:${dataTestId} and text:${ - text || 'none' - }`, - ); -} - export async function clickOnTextMessage( window: Page, text: string, @@ -349,7 +377,9 @@ export async function typeIntoInput( console.info(`typeIntoInput testId: ${dataTestId} : "${text}"`); const builtSelector = `css=[data-testid=${dataTestId}]`; // the new input made with onboarding element needs a click to reveal the input in the DOM - await clickOnTestIdWithText(window, dataTestId); + // Convert DataTestId to locator object for clickOn + const locator = { strategy: 'data-testid' as const, selector: dataTestId }; + await clickOn(window, locator); // reset the content to be empty before typing into the input await window.fill(builtSelector, ''); return window.type(builtSelector, text); @@ -390,7 +420,7 @@ export async function hasElementBeenDeleted( ) { const start = Date.now(); - let el: ElementHandle | undefined; + let el: ElementHandle | undefined; do { try { el = await waitForElement(window, strategy, selector, maxWait, text); @@ -421,7 +451,7 @@ export async function hasTextMessageBeenDeleted( maxWait: number = 5000, ) { await doWhileWithMax( - 15000, + maxWait, 500, 'waiting for text message to be deleted', async () => { @@ -501,13 +531,28 @@ export async function checkModalStrings( window: Page, expectedHeading: string, expectedDescription: string, + modalId?: ModalId, ) { - const heading = await waitForElement(window, 'data-testid', 'modal-heading'); - const description = await waitForElement( - window, - 'data-testid', - 'modal-description', - ); + let modalSelector = '[data-modal-id]'; // Base selector for modals + + // If a specific modal ID is provided, target that one + if (modalId) { + modalSelector = `[data-modal-id="${modalId}"]`; + } + + // Find the target modal + const targetModal = window.locator(modalSelector).first(); + + // Wait for the modal to be visible + await targetModal.waitFor({ state: 'visible' }); + + // Get elements within this specific modal + const heading = targetModal.locator('[data-testid="modal-heading"]'); + const description = targetModal.locator('[data-testid="modal-description"]'); + + // Wait for these elements to be visible + await heading.waitFor({ state: 'visible' }); + await description.waitFor({ state: 'visible' }); const headingText = await heading.innerText(); const descriptionText = await description.innerText(); @@ -531,63 +576,3 @@ export function formatTimeOption(option: DMTimeOption) { const formattedTime = timePart.replace(/-/g, ' '); return formattedTime; } - -async function deleteDifferenceFile( - fileFolder: string, - fileName: string, - os: string, -) { - const filePath = path.join( - screenshotFolder, - fileFolder, - `${fileName}-${os}-difference.png`, - ); - - if (fs.existsSync(filePath)) { - // Delete the file if it exists - fs.unlinkSync(filePath); - console.log(`Deleted difference file at: ${filePath}`); - } else { - console.log(`No difference file found at: ${filePath}, proceeding...`); - } -} - -export async function compareScreenshot( - element: ElementHandle, - testTitle: string, - elementState: ElementState, - os: string, -) { - const formattedTitle = testTitle.toLowerCase().replace(/\s+/g, '-'); - await deleteDifferenceFile(formattedTitle, elementState, os); - - const elementScreenshot = await element.screenshot(); - const folderPath = path.join(screenshotFolder, `${formattedTitle}`); - - // If folder doesn't exist, create folder - if (!fs.existsSync(folderPath)) { - fs.mkdirSync(folderPath, { recursive: true }); - } - const previousScreenshotFilePath = path.join( - folderPath, - `${elementState}-${os}.png`, - ); - // If screenshot does not exist, save it to the folder - if (!fs.existsSync(previousScreenshotFilePath)) { - fs.writeFileSync(previousScreenshotFilePath, elementScreenshot); - } - // If screenshot does exist, compare it to previous screenshot in the folder - const previousScreenshot = fs.readFileSync(previousScreenshotFilePath); - const diffFilePath = path.join( - folderPath, - `${elementState}-${os}-difference.png`, - ); - // If screenshots are different, then create a difference screenshot - if (!elementScreenshot.equals(previousScreenshot)) { - // If elements do not match, then take the elementScreenshot and save it to same folder but with a new name of 'difference.png' - fs.writeFileSync(diffFilePath, elementScreenshot); - throw new Error( - `Screenshots do not match, see ${screenshotFolder} > ${testTitle} folder > \n\t\t diff: ${diffFilePath}\n\t\t previous: ${previousScreenshotFilePath}`, - ); - } -} diff --git a/tests/automation/utilities/voice_call.ts b/tests/automation/utilities/voice_call.ts index 537819b..af23ed4 100644 --- a/tests/automation/utilities/voice_call.ts +++ b/tests/automation/utilities/voice_call.ts @@ -1,55 +1,41 @@ import { Page } from '@playwright/test'; + import { englishStrippedStr } from '../../localization/englishStrippedStr'; import { sleepFor } from '../../promise_utils'; -import { User } from '../types/testing'; -import { - checkModalStrings, - clickOnMatchingText, - clickOnTestIdWithText, -} from './utils'; +import { Conversation, Global, Settings } from '../locators'; +import { checkModalStrings, clickOn, clickOnMatchingText } from './utils'; export const makeVoiceCall = async ( callerWindow: Page, receiverWindow: Page, - caller: User, - receiver: User, ) => { - await clickOnTestIdWithText(callerWindow, 'call-button'); - await clickOnTestIdWithText(callerWindow, 'session-toast'); - await clickOnTestIdWithText(callerWindow, 'enable-calls'); + await clickOn(callerWindow, Conversation.callButton); + await clickOn(callerWindow, Global.toast); + await clickOn(callerWindow, Settings.enableCalls); await checkModalStrings( callerWindow, englishStrippedStr('callsVoiceAndVideoBeta').toString(), englishStrippedStr('callsVoiceAndVideoModalDescription').toString(), + 'confirmModal', ); - await clickOnTestIdWithText(callerWindow, 'session-confirm-ok-button'); - await clickOnTestIdWithText(callerWindow, 'message-section'); - await clickOnTestIdWithText( - callerWindow, - 'module-conversation__user__profile-name', - receiver.userName, - ); - await clickOnTestIdWithText(callerWindow, 'call-button'); + await clickOn(callerWindow, Global.confirmButton); + await clickOn(callerWindow, Global.modalCloseButton); + await clickOn(callerWindow, Conversation.callButton); // Enable calls in window B - await clickOnTestIdWithText(receiverWindow, 'session-toast'); - await clickOnTestIdWithText(receiverWindow, 'enable-calls'); - // Getting wrong strings from locales.ts file + await clickOn(receiverWindow, Global.toast); + await clickOn(receiverWindow, Settings.enableCalls); await checkModalStrings( receiverWindow, englishStrippedStr('callsVoiceAndVideoBeta').toString(), englishStrippedStr('callsVoiceAndVideoModalDescription').toString(), + 'confirmModal', ); - await clickOnTestIdWithText(receiverWindow, 'session-confirm-ok-button'); + await clickOn(receiverWindow, Global.confirmButton); await clickOnMatchingText( receiverWindow, englishStrippedStr('accept').toString(), ); - await clickOnTestIdWithText(receiverWindow, 'message-section'); - await clickOnTestIdWithText( - receiverWindow, - 'module-conversation__user__profile-name', - caller.userName, - ); + await clickOn(receiverWindow, Global.modalCloseButton); await sleepFor(5000); - await clickOnTestIdWithText(callerWindow, 'end-call'); + await clickOn(callerWindow, Conversation.endCallButton); }; diff --git a/tests/localization/constants.ts b/tests/localization/constants.ts index fd7dc29..dd2717d 100644 --- a/tests/localization/constants.ts +++ b/tests/localization/constants.ts @@ -9,6 +9,8 @@ export enum LOCALE_DEFAULTS { token_name_short = 'SESH', usd_name_short = 'USD', app_pro = 'Session Pro', + session_foundation = 'Session Foundation', + pro = 'Pro', } export const rtlLocales = ['ar', 'fa', 'he', 'ps', 'ur']; diff --git a/tests/localization/locales.ts b/tests/localization/locales.ts index 94b497a..e842e2e 100644 --- a/tests/localization/locales.ts +++ b/tests/localization/locales.ts @@ -41,7 +41,7 @@ type WithStoreVariant = {storevariant: string}; type WithMin = {min: string}; type WithMax = {max: string}; -export type TokenSimpleNoArgs = +export type TokenSimpleNoArgs = 'about' | 'accept' | 'accountIDCopy' | @@ -81,12 +81,15 @@ export type TokenSimpleNoArgs = 'appIconEnableIconAndName' | 'appIconSelect' | 'appIconSelectionTitle' | + 'appName' | 'appNameCalculator' | 'appNameMeetingSE' | 'appNameNews' | 'appNameNotes' | 'appNameStocks' | 'appNameWeather' | + 'appPro' | + 'appProBadge' | 'appearanceAutoDarkMode' | 'appearanceHideMenuBar' | 'appearanceLanguage' | @@ -198,6 +201,7 @@ export type TokenSimpleNoArgs = 'cameraGrantAccessDescription' | 'cameraGrantAccessQr' | 'cancel' | + 'cancelPlan' | 'change' | 'changePasswordFail' | 'changePasswordModalDescription' | @@ -286,6 +290,8 @@ export type TokenSimpleNoArgs = 'copy' | 'create' | 'creatingCall' | + 'currentPassword' | + 'currentPlan' | 'cut' | 'darkMode' | 'databaseErrorClearDataWarning' | @@ -449,12 +455,16 @@ export type TokenSimpleNoArgs = 'hideOthers' | 'image' | 'images' | + 'important' | 'incognitoKeyboard' | 'incognitoKeyboardDescription' | 'info' | 'invalidShortcut' | 'join' | 'later' | + 'launchOnStartDescriptionDesktop' | + 'launchOnStartDesktop' | + 'launchOnStartupDisabledDesktop' | 'learnMore' | 'leave' | 'leaving' | @@ -472,6 +482,7 @@ export type TokenSimpleNoArgs = 'linkPreviewsSendModalDescription' | 'linkPreviewsTurnedOff' | 'linkPreviewsTurnedOffDescription' | + 'links' | 'loadAccount' | 'loadAccountProgressMessage' | 'loading' | @@ -484,7 +495,9 @@ export type TokenSimpleNoArgs = 'lockAppStatus' | 'lockAppUnlock' | 'lockAppUnlocked' | + 'logs' | 'manageMembers' | + 'managePro' | 'max' | 'media' | 'membersAddAccountIdOrOns' | @@ -534,7 +547,10 @@ export type TokenSimpleNoArgs = 'modalMessageCharacterDisplayTitle' | 'modalMessageCharacterTooLongTitle' | 'modalMessageTooLongTitle' | + 'networkName' | + 'newPassword' | 'next' | + 'nextSteps' | 'nicknameEnter' | 'nicknameErrorShorter' | 'nicknameRemove' | @@ -607,6 +623,7 @@ export type TokenSimpleNoArgs = 'open' | 'openSurvey' | 'other' | + 'oxenFoundation' | 'password' | 'passwordChange' | 'passwordChangeShortDescription' | @@ -630,8 +647,8 @@ export type TokenSimpleNoArgs = 'passwordSetShortDescription' | 'passwordStrengthCharLength' | 'passwordStrengthIncludeNumber' | - 'passwordStrengthIncludesLetter' | 'passwordStrengthIncludesLowercase' | + 'passwordStrengthIncludesSymbol' | 'passwordStrengthIncludesUppercase' | 'passwordStrengthIndicator' | 'passwordStrengthIndicatorDescription' | @@ -675,31 +692,68 @@ export type TokenSimpleNoArgs = 'pinConversation' | 'pinUnpin' | 'pinUnpinConversation' | + 'plusLoadsMore' | 'preferences' | 'preview' | 'previewNotification' | + 'pro' | 'proActivated' | + 'proAllSet' | 'proAlreadyPurchased' | 'proAnimatedDisplayPicture' | 'proAnimatedDisplayPictureCallToActionDescription' | 'proAnimatedDisplayPictureFeature' | 'proAnimatedDisplayPictureModalDescription' | + 'proAnimatedDisplayPictures' | + 'proAnimatedDisplayPicturesDescription' | 'proAnimatedDisplayPicturesNonProModalDescription' | 'proBadge' | + 'proBadgeVisible' | + 'proBadges' | + 'proBadgesDescription' | 'proCallToActionLongerMessages' | 'proCallToActionPinnedConversations' | 'proCallToActionPinnedConversationsMoreThan' | + 'proExpired' | + 'proExpiredDescription' | + 'proExpiringSoon' | + 'proFaq' | + 'proFaqDescription' | 'proFeatureListAnimatedDisplayPicture' | 'proFeatureListLargerGroups' | 'proFeatureListLoadsMore' | 'proFeatureListLongerMessages' | 'proFeatureListPinnedConversations' | + 'proFeatures' | 'proGroupActivated' | 'proGroupActivatedDescription' | + 'proImportantDescription' | 'proIncreasedAttachmentSizeFeature' | 'proIncreasedMessageLengthFeature' | + 'proLargerGroups' | + 'proLargerGroupsDescription' | + 'proLongerMessages' | + 'proLongerMessagesDescription' | 'proMessageInfoFeatures' | + 'proPlanNotFound' | + 'proPlanNotFoundDescription' | + 'proPlanRecover' | + 'proPlanRenew' | + 'proPlanRenewStart' | + 'proPlanRenewSupport' | + 'proPlanRestored' | + 'proPlanRestoredDescription' | + 'proRefundDescription' | + 'proRefundRequestSessionSupport' | + 'proRefunding' | + 'proRequestedRefund' | 'proSendMore' | + 'proSettings' | + 'proStats' | + 'proStatsTooltip' | + 'proSupportDescription' | + 'proUnlimitedPins' | + 'proUnlimitedPinsDescription' | 'proUserProfileModalCallToAction' | 'profile' | 'profileDisplayPicture' | @@ -750,7 +804,9 @@ export type TokenSimpleNoArgs = 'remove' | 'removePasswordFail' | 'removePasswordModalDescription' | + 'renew' | 'reply' | + 'requestRefund' | 'resend' | 'resolving' | 'restart' | @@ -784,6 +840,8 @@ export type TokenSimpleNoArgs = 'sessionAppearance' | 'sessionClearData' | 'sessionConversations' | + 'sessionDownloadUrl' | + 'sessionFoundation' | 'sessionHelp' | 'sessionInviteAFriend' | 'sessionMessageRequests' | @@ -799,12 +857,15 @@ export type TokenSimpleNoArgs = 'sessionNotifications' | 'sessionPermissions' | 'sessionPrivacy' | + 'sessionProBeta' | 'sessionRecoveryPassword' | 'sessionSettings' | 'set' | 'setCommunityDisplayPicture' | 'setPasswordModalDescription' | + 'settingsCannotChangeDesktop' | 'settingsRestartDescription' | + 'settingsStartCategoryDesktop' | 'share' | 'shareAccountIdDescription' | 'shareAccountIdDescriptionCopied' | @@ -817,6 +878,7 @@ export type TokenSimpleNoArgs = 'showNoteToSelf' | 'showNoteToSelfDescription' | 'spellChecker' | + 'stakingRewardPool' | 'stickers' | 'strength' | 'supportDescription' | @@ -825,7 +887,10 @@ export type TokenSimpleNoArgs = 'theContinue' | 'theDefault' | 'theError' | + 'theReturn' | 'themePreview' | + 'tokenNameLong' | + 'tokenNameShort' | 'tooltipBlindedIdCommunities' | 'translate' | 'tray' | @@ -847,6 +912,8 @@ export type TokenSimpleNoArgs = 'updateGroupInformationDescription' | 'updateGroupInformationEnterShorterDescription' | 'updateNewVersion' | + 'updatePlan' | + 'updatePlanTwo' | 'updateProfileInformation' | 'updateProfileInformationDescription' | 'updateReleaseNotes' | @@ -858,6 +925,8 @@ export type TokenSimpleNoArgs = 'urlCopy' | 'urlOpen' | 'urlOpenBrowser' | + 'urlOpenDescriptionAlternative' | + 'usdNameShort' | 'useFastMode' | 'video' | 'videoErrorPlay' | @@ -1018,13 +1087,49 @@ export type TokensSimpleAndArgs = { notificationsMutedFor: WithTimeLarge, notificationsMutedForTime: WithDateTime, notificationsSystem: WithMessageCount & WithConversationCount, + onDevice: { device_type: string }, + onDeviceDescription: { device_type: string, platform_account: string }, onboardingBubbleCreatingAnAccountIsEasy: WithEmoji, onboardingBubbleWelcomeToSession: WithEmoji, + openStoreWebsite: { platform_store: string }, passwordErrorLength: WithMin & WithMax, + plusLoadsMoreDescription: WithIcon, + proAllSetDescription: WithDate, + proAutoRenewTime: WithTime, + proBilledAnnually: { price: string }, + proBilledMonthly: { price: string }, + proBilledQuarterly: { price: string }, + proDiscountTooltip: { percent: string }, + proExpiringSoonDescription: WithTime, + proExpiringTime: WithTime, + proPercentOff: { percent: string }, + proPlanActivatedAuto: WithDate & { current_plan: string }, + proPlanActivatedAutoShort: WithDate & { current_plan: string }, + proPlanActivatedNotAuto: WithDate, + proPlanExpireDate: WithDate, + proPlanPlatformRefund: { platform_store: string, platform_account: string }, + proPlanPlatformRefundLong: { platform_store: string }, + proPlanRenewDesktop: { platform_store: string }, + proPlanRenewDesktopLinked: { platform_store: string }, + proPlanRenewDesktopStore: { platform_store: string, platform_account: string }, + proPlanSignUp: { platform_store: string, platform_account: string }, + proPriceOneMonth: { monthly_price: string }, + proPriceThreeMonths: { monthly_price: string }, + proPriceTwelveMonths: { monthly_price: string }, + proRefundNextSteps: { platform_account: string }, + proRefundRequestStorePolicies: { platform_account: string }, + proRefundSupport: { platform_account: string, platform_store: string }, + proRefundingDescription: { platform_account: string, platform_store: string }, + proTosPrivacy: WithIcon, + proUpdatePlanDescription: WithDate & { current_plan: string, selected_plan: string }, + proUpdatePlanExpireDescription: WithDate & { selected_plan: string }, + processingRefundRequest: { platform_account: string }, rateSessionModalDescription: WithStoreVariant, + refundPlanNonOriginatorApple: { platform_account: string }, remainingCharactersOverTooltip: WithCount, screenshotTaken: WithName, searchMatchesNoneSpecific: WithQuery, + sessionNetworkDataPrice: WithDateTime, sessionNetworkDescription: WithIcon, systemInformationDesktop: WithInformation, tooltipAccountIdVisible: WithName, @@ -1033,7 +1138,8 @@ export type TokensSimpleAndArgs = { updateVersion: WithVersion, updated: WithRelativeTime, urlOpenDescription: WithUrl, - sessionNetworkDataPrice: WithDateTime + viaStoreWebsite: { platform_store: string }, + viaStoreWebsiteDescription: { platform_account: string, platform_store: string } }; export type TokensPluralAndArgs = { @@ -1059,13 +1165,17 @@ export type TokensPluralAndArgs = { messageNewYouveGot: WithCount, messageNewYouveGotGroup: WithGroupName & WithCount, modalMessageCharacterDisplayDescription: WithLimit & WithCount, + proBadgesSent: WithCount & { total: string }, + proGroupsUpgraded: WithCount & { total: string }, + proLongerMessagesSent: WithCount & { total: string }, + proPinnedConversations: WithCount & { total: string }, promotionFailed: WithCount, promotionFailedDescription: WithCount, remainingCharactersTooltip: WithCount, searchMatches: WithFoundCount & WithCount }; -export type TokenSimpleWithArgs = +export type TokenSimpleWithArgs = 'accountIdShare' | 'adminMorePromotedToAdmin' | 'adminPromoteDescription' | @@ -1209,13 +1319,49 @@ export type TokenSimpleWithArgs = 'notificationsMutedFor' | 'notificationsMutedForTime' | 'notificationsSystem' | + 'onDevice' | + 'onDeviceDescription' | 'onboardingBubbleCreatingAnAccountIsEasy' | 'onboardingBubbleWelcomeToSession' | + 'openStoreWebsite' | 'passwordErrorLength' | + 'plusLoadsMoreDescription' | + 'proAllSetDescription' | + 'proAutoRenewTime' | + 'proBilledAnnually' | + 'proBilledMonthly' | + 'proBilledQuarterly' | + 'proDiscountTooltip' | + 'proExpiringSoonDescription' | + 'proExpiringTime' | + 'proPercentOff' | + 'proPlanActivatedAuto' | + 'proPlanActivatedAutoShort' | + 'proPlanActivatedNotAuto' | + 'proPlanExpireDate' | + 'proPlanPlatformRefund' | + 'proPlanPlatformRefundLong' | + 'proPlanRenewDesktop' | + 'proPlanRenewDesktopLinked' | + 'proPlanRenewDesktopStore' | + 'proPlanSignUp' | + 'proPriceOneMonth' | + 'proPriceThreeMonths' | + 'proPriceTwelveMonths' | + 'proRefundNextSteps' | + 'proRefundRequestStorePolicies' | + 'proRefundSupport' | + 'proRefundingDescription' | + 'proTosPrivacy' | + 'proUpdatePlanDescription' | + 'proUpdatePlanExpireDescription' | + 'processingRefundRequest' | 'rateSessionModalDescription' | + 'refundPlanNonOriginatorApple' | 'remainingCharactersOverTooltip' | 'screenshotTaken' | 'searchMatchesNoneSpecific' | + 'sessionNetworkDataPrice' | 'sessionNetworkDescription' | 'systemInformationDesktop' | 'tooltipAccountIdVisible' | @@ -1224,9 +1370,10 @@ export type TokenSimpleWithArgs = 'updateVersion' | 'updated' | 'urlOpenDescription' | - 'sessionNetworkDataPrice' + 'viaStoreWebsite' | + 'viaStoreWebsiteDescription' -export type TokenPluralWithArgs = +export type TokenPluralWithArgs = 'adminSendingPromotion' | 'clearDataErrorDescription' | 'deleteMessage' | @@ -1249,6 +1396,10 @@ export type TokenPluralWithArgs = 'messageNewYouveGot' | 'messageNewYouveGotGroup' | 'modalMessageCharacterDisplayDescription' | + 'proBadgesSent' | + 'proGroupsUpgraded' | + 'proLongerMessagesSent' | + 'proPinnedConversations' | 'promotionFailed' | 'promotionFailedDescription' | 'remainingCharactersTooltip' | @@ -1375,6 +1526,9 @@ export const simpleDictionaryNoArgs: Record< appIconSelectionTitle: { en: "Icon", }, + appName: { + en: "Session", + }, appNameCalculator: { en: "Calculator", }, @@ -1393,6 +1547,12 @@ export const simpleDictionaryNoArgs: Record< appNameWeather: { en: "Weather", }, + appPro: { + en: "Session Pro", + }, + appProBadge: { + en: "Session Pro Badge", + }, appearanceAutoDarkMode: { en: "Auto Dark Mode", }, @@ -1700,7 +1860,7 @@ export const simpleDictionaryNoArgs: Record< en: "Voice and Video Calls (Beta)", }, callsVoiceAndVideoModalDescription: { - en: "Your IP is visible to your call partner and a Session Technology Foundation server while using beta calls.", + en: "Your IP is visible to your call partner and a Session Foundation server while using beta calls.", }, callsVoiceAndVideoToggleDescription: { en: "Enables voice and video calls to and from other users.", @@ -1726,6 +1886,9 @@ export const simpleDictionaryNoArgs: Record< cancel: { en: "Cancel", }, + cancelPlan: { + en: "Cancel Plan", + }, change: { en: "Change", }, @@ -1931,7 +2094,7 @@ export const simpleDictionaryNoArgs: Record< en: "Enter Key", }, conversationsEnterDescription: { - en: "Function of the enter key when typing in a conversation.", + en: "Define how the Enter and Shift+Enter keys function in conversations.", }, conversationsEnterNewLine: { en: "SHIFT + ENTER sends a message, ENTER starts a new line.", @@ -1990,6 +2153,12 @@ export const simpleDictionaryNoArgs: Record< creatingCall: { en: "Creating Call", }, + currentPassword: { + en: "Current Password", + }, + currentPlan: { + en: "Current Plan", + }, cut: { en: "Cut", }, @@ -2479,6 +2648,9 @@ export const simpleDictionaryNoArgs: Record< images: { en: "images", }, + important: { + en: "Important", + }, incognitoKeyboard: { en: "Incognito Keyboard", }, @@ -2497,6 +2669,15 @@ export const simpleDictionaryNoArgs: Record< later: { en: "Later", }, + launchOnStartDescriptionDesktop: { + en: "Launch Session automatically when your computer starts up.", + }, + launchOnStartDesktop: { + en: "Launch on Startup", + }, + launchOnStartupDisabledDesktop: { + en: "This setting is managed by your system on Linux. To enable automatic startup, add Session to your startup applications in system settings.", + }, learnMore: { en: "Learn More", }, @@ -2548,6 +2729,9 @@ export const simpleDictionaryNoArgs: Record< linkPreviewsTurnedOffDescription: { en: "Session must contact linked websites to generate previews of links you send and receive.

You can turn them on in Session's settings.", }, + links: { + en: "Links", + }, loadAccount: { en: "Load Account", }, @@ -2584,9 +2768,15 @@ export const simpleDictionaryNoArgs: Record< lockAppUnlocked: { en: "Session is unlocked", }, + logs: { + en: "Logs", + }, manageMembers: { en: "Manage Members", }, + managePro: { + en: "Manage Pro", + }, max: { en: "Max", }, @@ -2734,9 +2924,18 @@ export const simpleDictionaryNoArgs: Record< modalMessageTooLongTitle: { en: "Message Too Long", }, + networkName: { + en: "Session Network", + }, + newPassword: { + en: "New Password", + }, next: { en: "Next", }, + nextSteps: { + en: "Next Steps", + }, nicknameEnter: { en: "Enter nickname", }, @@ -2953,6 +3152,9 @@ export const simpleDictionaryNoArgs: Record< other: { en: "Other", }, + oxenFoundation: { + en: "Oxen Foundation", + }, password: { en: "Password", }, @@ -3022,12 +3224,12 @@ export const simpleDictionaryNoArgs: Record< passwordStrengthIncludeNumber: { en: "Includes a number", }, - passwordStrengthIncludesLetter: { - en: "Includes a letter", - }, passwordStrengthIncludesLowercase: { en: "Includes a lowercase letter", }, + passwordStrengthIncludesSymbol: { + en: "Includes a symbol", + }, passwordStrengthIncludesUppercase: { en: "Includes a uppercase letter", }, @@ -3157,6 +3359,9 @@ export const simpleDictionaryNoArgs: Record< pinUnpinConversation: { en: "Unpin Conversation", }, + plusLoadsMore: { + en: "Plus Loads More...", + }, preferences: { en: "Preferences", }, @@ -3166,9 +3371,15 @@ export const simpleDictionaryNoArgs: Record< previewNotification: { en: "Preview Notification", }, + pro: { + en: "Pro", + }, proActivated: { en: "Activated", }, + proAllSet: { + en: "You're all set!", + }, proAlreadyPurchased: { en: "You’ve already got", }, @@ -3184,11 +3395,26 @@ export const simpleDictionaryNoArgs: Record< proAnimatedDisplayPictureModalDescription: { en: "users can upload GIFs", }, + proAnimatedDisplayPictures: { + en: "Animated Display Pictures", + }, + proAnimatedDisplayPicturesDescription: { + en: "Set animated GIFs and WebP images as your display picture.", + }, proAnimatedDisplayPicturesNonProModalDescription: { en: "Upload GIFs with", }, proBadge: { - en: "Session Pro Badge", + en: "Pro Badge", + }, + proBadgeVisible: { + en: "Show Session Pro badge to other users", + }, + proBadges: { + en: "Badges", + }, + proBadgesDescription: { + en: "Show your support for Session with an exclusive badge next to your display name.", }, proCallToActionLongerMessages: { en: "Want to send longer messages? Send more text and unlock premium features with Session Pro", @@ -3199,6 +3425,21 @@ export const simpleDictionaryNoArgs: Record< proCallToActionPinnedConversationsMoreThan: { en: "Want more than 5 pins? Organize your chats and unlock premium features with Session Pro", }, + proExpired: { + en: "Expired", + }, + proExpiredDescription: { + en: "Unfortunately, your Pro plan has expired. Renew to keep accessing the exclusive perks and features of Session Pro.", + }, + proExpiringSoon: { + en: "Expiring Soon", + }, + proFaq: { + en: "Pro FAQ", + }, + proFaqDescription: { + en: "Find answers to common questions in the Session FAQ.", + }, proFeatureListAnimatedDisplayPicture: { en: "Upload GIF and WebP display pictures", }, @@ -3214,24 +3455,96 @@ export const simpleDictionaryNoArgs: Record< proFeatureListPinnedConversations: { en: "Pin unlimited conversations", }, + proFeatures: { + en: "Pro Features", + }, proGroupActivated: { en: "Group Activated", }, proGroupActivatedDescription: { en: "This group has expanded capacity! It can support up to 300 members because a group admin has", }, + proImportantDescription: { + en: "Requesting a refund is final. If approved, your Pro plan will be canceled immediately and you will lose access to all Pro features.", + }, proIncreasedAttachmentSizeFeature: { en: "Increased Attachment Size", }, proIncreasedMessageLengthFeature: { en: "Increased Message Length", }, + proLargerGroups: { + en: "Larger Groups", + }, + proLargerGroupsDescription: { + en: "Groups you are an admin in are automatically upgraded to support 300 members.", + }, + proLongerMessages: { + en: "Longer Messages", + }, + proLongerMessagesDescription: { + en: "You can send messages up to 10,000 characters in all conversations.", + }, proMessageInfoFeatures: { en: "This message used the following Session Pro features:", }, + proPlanNotFound: { + en: "Pro Plan Not Found", + }, + proPlanNotFoundDescription: { + en: "No active plan was found for your account. If you believe this is a mistake, please reach out to Session support for assistance.", + }, + proPlanRecover: { + en: "Recover Pro Plan", + }, + proPlanRenew: { + en: "Renew Pro Plan", + }, + proPlanRenewStart: { + en: "Renew your Session Pro plan to start using powerful Session Pro features again.", + }, + proPlanRenewSupport: { + en: "Your Session Pro plan has been renewed! Thank you for supporting the Session Network.", + }, + proPlanRestored: { + en: "Pro Plan Restored", + }, + proPlanRestoredDescription: { + en: "A valid plan for Session Pro was detected and your Pro status has been restored!", + }, + proRefundDescription: { + en: "We’re sorry to see you go. Here's what you need to know before requesting a refund.", + }, + proRefundRequestSessionSupport: { + en: "Your refund request will be handled by Session Support.

Request a refund by hitting the button below and completing the refund request form.

While Session Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume.", + }, + proRefunding: { + en: "Refunding Pro", + }, + proRequestedRefund: { + en: "Refund Requested", + }, proSendMore: { en: "Send more with", }, + proSettings: { + en: "Pro Settings", + }, + proStats: { + en: "Your Pro Stats", + }, + proStatsTooltip: { + en: "Pro stats reflect usage on this device and may appear differently on linked devices", + }, + proSupportDescription: { + en: "Need help with your Pro plan? Submit a request to the support team.", + }, + proUnlimitedPins: { + en: "Unlimited Pins", + }, + proUnlimitedPinsDescription: { + en: "Organize all your chats with unlimited pinned conversations.", + }, proUserProfileModalCallToAction: { en: "Want to get more out of Session? Upgrade to Session Pro for a more powerful messaging experience.", }, @@ -3359,7 +3672,7 @@ export const simpleDictionaryNoArgs: Record< en: "Enter your recovery password to load your account. If you haven't saved it, you can find it in your app settings.", }, recoveryPasswordView: { - en: "View Password", + en: "View Recovery Password", }, recoveryPasswordVisibility: { en: "Recovery Password Visibility", @@ -3382,9 +3695,15 @@ export const simpleDictionaryNoArgs: Record< removePasswordModalDescription: { en: "Remove your current password for Session. Locally stored data will be re-encrypted with a randomly generated key, stored on your device.", }, + renew: { + en: "Renew", + }, reply: { en: "Reply", }, + requestRefund: { + en: "Request Refund", + }, resend: { en: "Resend", }, @@ -3484,6 +3803,12 @@ export const simpleDictionaryNoArgs: Record< sessionConversations: { en: "Conversations", }, + sessionDownloadUrl: { + en: "https://getsession.org/download", + }, + sessionFoundation: { + en: "Session Foundation", + }, sessionHelp: { en: "Help", }, @@ -3529,6 +3854,9 @@ export const simpleDictionaryNoArgs: Record< sessionPrivacy: { en: "Privacy", }, + sessionProBeta: { + en: "Session Pro Beta", + }, sessionRecoveryPassword: { en: "Recovery Password", }, @@ -3544,9 +3872,15 @@ export const simpleDictionaryNoArgs: Record< setPasswordModalDescription: { en: "Set a password for Session. Locally stored data will be encrypted with this password. You will be asked to enter this password each time Session starts.", }, + settingsCannotChangeDesktop: { + en: "Cannot Update Setting", + }, settingsRestartDescription: { en: "You must restart Session to apply your new settings.", }, + settingsStartCategoryDesktop: { + en: "Startup", + }, share: { en: "Share", }, @@ -3583,6 +3917,9 @@ export const simpleDictionaryNoArgs: Record< spellChecker: { en: "Spell Checker", }, + stakingRewardPool: { + en: "Staking Reward Pool", + }, stickers: { en: "Stickers", }, @@ -3607,9 +3944,18 @@ export const simpleDictionaryNoArgs: Record< theError: { en: "Error", }, + theReturn: { + en: "Return", + }, themePreview: { en: "Theme Preview", }, + tokenNameLong: { + en: "Session Token", + }, + tokenNameShort: { + en: "SESH", + }, tooltipBlindedIdCommunities: { en: "Blinded IDs are used in communities to reduce spam and increase privacy", }, @@ -3673,6 +4019,12 @@ export const simpleDictionaryNoArgs: Record< updateNewVersion: { en: "A new version of Session is available, tap to update", }, + updatePlan: { + en: "Update Plan", + }, + updatePlanTwo: { + en: "Two ways to update your plan:", + }, updateProfileInformation: { en: "Update Profile Information", }, @@ -3706,6 +4058,12 @@ export const simpleDictionaryNoArgs: Record< urlOpenBrowser: { en: "This will open in your browser.", }, + urlOpenDescriptionAlternative: { + en: "Links will open in your browser.", + }, + usdNameShort: { + en: "USD", + }, useFastMode: { en: "Use Fast Mode", }, @@ -4186,18 +4544,123 @@ export const simpleDictionaryWithArgs: Record< }, notificationsSystem: { en: "{message_count} new messages in {conversation_count} conversations", + }, + onDevice: { + en: "On your {device_type} device", + }, + onDeviceDescription: { + en: "Open this Session account on an {device_type} device logged into the {platform_account} you originally signed up with. Then, change your plan via the Session Pro settings.", }, onboardingBubbleCreatingAnAccountIsEasy: { en: "Creating an account is instant, free, and anonymous {emoji}", }, onboardingBubbleWelcomeToSession: { en: "Welcome to Session {emoji}", + }, + openStoreWebsite: { + en: "Open {platform_store} Website", }, passwordErrorLength: { en: "Password must be between {min} and {max} characters long", + }, + plusLoadsMoreDescription: { + en: "New features coming soon to Pro. Discover what's next on the Pro Roadmap {icon}", + }, + proAllSetDescription: { + en: "Your Session Pro plan was updated! You will be billed when your current Pro plan is automatically renewed on {date}.", + }, + proAutoRenewTime: { + en: "Pro auto-renewing in {time}", + }, + proBilledAnnually: { + en: "{price} Billed Annually", + }, + proBilledMonthly: { + en: "{price} Billed Monthly", + }, + proBilledQuarterly: { + en: "{price} Billed Quarterly", + }, + proDiscountTooltip: { + en: "Your current plan is already discounted by {percent}% of the full Session Pro price.", + }, + proExpiringSoonDescription: { + en: "Your Pro plan is expiring in {time}. Update your plan to keep accessing the exclusive perks and features of Session Pro.", + }, + proExpiringTime: { + en: "Pro expiring in {time}", + }, + proPercentOff: { + en: "{percent}% Off", + }, + proPlanActivatedAuto: { + en: "Your Session Pro plan is active!

Your plan will automatically renew for another {current_plan} on {date}. Updates to your plan take effect when Pro is next renewed.", + }, + proPlanActivatedAutoShort: { + en: "Your Session Pro plan is active!

Your plan will automatically renew for another {current_plan} on {date}.", + }, + proPlanActivatedNotAuto: { + en: "Your Session Pro plan will expire on {date}.

Update your plan now to ensure uninterrupted access to exclusive Pro features.", + }, + proPlanExpireDate: { + en: "Your Session Pro plan will expire on {date}.", + }, + proPlanPlatformRefund: { + en: "Because you originally signed up for Session Pro via the {platform_store} Store, you'll need to use the same {platform_account} to request a refund.", + }, + proPlanPlatformRefundLong: { + en: "Because you originally signed up for Session Pro via the {platform_store} Store, your refund request will be processed by Session Support.

Request a refund by hitting the button below and completing the refund request form.

While Session Support strives to process refund requests within 24-72 hours, processing may take longer during times of high request volume.", + }, + proPlanRenewDesktop: { + en: "Currently, Pro plans can only be purchased and renewed via the {platform_store} or {platform_store} Stores. Because you are using Session Desktop, you're not able to renew your plan here.

Session Pro developers are working hard on alternative payment options to allow users to purchase Pro plans outside of the {platform_store} and {platform_store} Stores. Pro Roadmap", + }, + proPlanRenewDesktopLinked: { + en: "Renew your plan in the Session Pro settings on a linked device with Session installed via the {platform_store} or {platform_store} Store.", + }, + proPlanRenewDesktopStore: { + en: "Renew your plan on the {platform_store} website using the {platform_account} you signed up for Pro with.", + }, + proPlanSignUp: { + en: "Because you originally signed up for Session Pro via the {platform_store} Store, you'll need to use your {platform_account} to update your plan.", + }, + proPriceOneMonth: { + en: "1 Month - {monthly_price} / Month", + }, + proPriceThreeMonths: { + en: "3 Months - {monthly_price} / Month", + }, + proPriceTwelveMonths: { + en: "12 Months - {monthly_price} / Month", + }, + proRefundNextSteps: { + en: "{platform_account} is now processing your refund request. This typically takes 24-48 hours. Depending on their decision, you may see your Pro status change in Session.", + }, + proRefundRequestStorePolicies: { + en: "Your refund request will be handled exclusively by {platform_account} through the {platform_account} website.

Due to {platform_account} refund policies, Session developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued.", + }, + proRefundSupport: { + en: "Please contact {platform_account} for further updates on your refund request. Due to {platform_account} refund policies, Session developers have no ability to influence the outcome of refund requests.

{platform_store} Refund Support", + }, + proRefundingDescription: { + en: "Refunds for Session Pro plans are handled exclusively by {platform_account} through the {platform_store} Store.

Due to {platform_account} refund policies, Session developers have no ability to influence the outcome of refund requests. This includes whether the request is approved or denied, as well as whether a full or partial refund is issued.", + }, + proTosPrivacy: { + en: "By updating, you agree to the Session Pro Terms of Service {icon} and Privacy Policy {icon}", + }, + proUpdatePlanDescription: { + en: "You are currently on the {current_plan} Plan. Are you sure you want to switch to the {selected_plan} Plan?

By updating, your plan will automatically renew on {date} for an additional {selected_plan} of Pro access.", + }, + proUpdatePlanExpireDescription: { + en: "Your plan will expire on {date}.

By updating, your plan will automatically renew on {date} for an additional {selected_plan} of Pro access.", + }, + processingRefundRequest: { + en: "{platform_account} is processing your refund request", }, rateSessionModalDescription: { en: "We're glad you're enjoying Session, if you have a moment, rating us in the {storevariant} helps others discover private, secure messaging!", + }, + refundPlanNonOriginatorApple: { + en: "Because you originally signed up for Session Pro via a different {platform_account}, you'll need to use that {platform_account} to update your plan.", }, remainingCharactersOverTooltip: { en: "Reduce message length by {count}", @@ -4207,6 +4670,9 @@ export const simpleDictionaryWithArgs: Record< }, searchMatchesNoneSpecific: { en: "No results found for {query}", + }, + sessionNetworkDataPrice: { + en: "Price data powered by CoinGecko
Accurate at {date_time}", }, sessionNetworkDescription: { en: "Messages are sent using the Session Network. The network is comprised of nodes incentivized with Session Token, which keeps Session decentralized and secure. Learn More {icon}", @@ -4232,8 +4698,11 @@ export const simpleDictionaryWithArgs: Record< urlOpenDescription: { en: "Are you sure you want to open this URL in your browser?

{url}", }, - sessionNetworkDataPrice: { - en: "Price data powered by CoinGecko
Accurate at {date_time}", + viaStoreWebsite: { + en: "Via the {platform_store} website", + }, + viaStoreWebsiteDescription: { + en: "Change your plan using the {platform_account} you used to sign up with, via the {platform_store} website.", }, } as const; @@ -4370,6 +4839,30 @@ export const pluralsDictionaryWithArgs = { other: "Messages have a character limit of {limit} characters. You have {count} characters remaining." }, }, + proBadgesSent: { + en:{ + one: "{total} Pro Badge Sent", + other: "{total} Pro Badges Sent" + }, + }, + proGroupsUpgraded: { + en:{ + one: "{total} Group Upgraded", + other: "{total} Groups Upgraded" + }, + }, + proLongerMessagesSent: { + en:{ + one: "{total} Longer Message Sent", + other: "{total} Longer Messages Sent" + }, + }, + proPinnedConversations: { + en:{ + one: "{total} Pinned Conversation", + other: "{total} Pinned Conversations" + }, + }, promotionFailed: { en:{ one: "Promotion Failed", diff --git a/yarn.lock b/yarn.lock index 41ee1c7..692d8cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -29,6 +29,13 @@ dependencies: eslint-visitor-keys "^3.3.0" +"@eslint-community/eslint-utils@^4.7.0": + version "4.8.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz#0e3b5e45566d1bce1ec47d8aae2fc2ad77ad0894" + integrity sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q== + dependencies: + eslint-visitor-keys "^3.4.3" + "@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": version "4.6.2" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" @@ -230,6 +237,15 @@ "@typescript-eslint/visitor-keys" "6.2.1" debug "^4.3.4" +"@typescript-eslint/project-service@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.42.0.tgz#636eb3418b6c42c98554dce884943708bf41a583" + integrity sha512-vfVpLHAhbPjilrabtOSNcUDmBboQNrJUiNAGoImkZKnMjs2TIcWG33s4Ds0wY3/50aZmTMqJa6PiwkwezaAklg== + dependencies: + "@typescript-eslint/tsconfig-utils" "^8.42.0" + "@typescript-eslint/types" "^8.42.0" + debug "^4.3.4" + "@typescript-eslint/scope-manager@6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.2.1.tgz#b6f43a867b84e5671fe531f2b762e0b68f7cf0c4" @@ -238,6 +254,19 @@ "@typescript-eslint/types" "6.2.1" "@typescript-eslint/visitor-keys" "6.2.1" +"@typescript-eslint/scope-manager@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.42.0.tgz#36016757bc85b46ea42bae47b61f9421eddedde3" + integrity sha512-51+x9o78NBAVgQzOPd17DkNTnIzJ8T/O2dmMBLoK9qbY0Gm52XJcdJcCl18ExBMiHo6jPMErUQWUv5RLE51zJw== + dependencies: + "@typescript-eslint/types" "8.42.0" + "@typescript-eslint/visitor-keys" "8.42.0" + +"@typescript-eslint/tsconfig-utils@8.42.0", "@typescript-eslint/tsconfig-utils@^8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.42.0.tgz#21a3e74396fd7443ff930bc41b27789ba7e9236e" + integrity sha512-kHeFUOdwAJfUmYKjR3CLgZSglGHjbNTi1H8sTYRYV2xX6eNz4RyJ2LIgsDLKf8Yi0/GL1WZAC/DgZBeBft8QAQ== + "@typescript-eslint/type-utils@6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-6.2.1.tgz#8eb8a2cccdf39cd7cf93e02bd2c3782dc90b0525" @@ -253,6 +282,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.2.1.tgz#7fcdeceb503aab601274bf5e210207050d88c8ab" integrity sha512-528bGcoelrpw+sETlyM91k51Arl2ajbNT9L4JwoXE2dvRe1yd8Q64E4OL7vHYw31mlnVsf+BeeLyAZUEQtqahQ== +"@typescript-eslint/types@8.42.0", "@typescript-eslint/types@^8.34.1", "@typescript-eslint/types@^8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.42.0.tgz#ae15c09cebda20473772902033328e87372db008" + integrity sha512-LdtAWMiFmbRLNP7JNeY0SqEtJvGMYSzfiWBSmx+VSZ1CH+1zyl8Mmw1TT39OrtsRvIYShjJWzTDMPWZJCpwBlw== + "@typescript-eslint/typescript-estree@6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.2.1.tgz#2af6e90c1e91cb725a5fe1682841a3f74549389e" @@ -266,6 +300,22 @@ semver "^7.5.4" ts-api-utils "^1.0.1" +"@typescript-eslint/typescript-estree@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.42.0.tgz#593c3af87d4462252c0d7239d1720b84a1b56864" + integrity sha512-ku/uYtT4QXY8sl9EDJETD27o3Ewdi72hcXg1ah/kkUgBvAYHLwj2ofswFFNXS+FL5G+AGkxBtvGt8pFBHKlHsQ== + dependencies: + "@typescript-eslint/project-service" "8.42.0" + "@typescript-eslint/tsconfig-utils" "8.42.0" + "@typescript-eslint/types" "8.42.0" + "@typescript-eslint/visitor-keys" "8.42.0" + debug "^4.3.4" + fast-glob "^3.3.2" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^2.1.0" + "@typescript-eslint/utils@6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.2.1.tgz#2aa4279ec13053d05615bcbde2398e1e8f08c334" @@ -279,6 +329,16 @@ "@typescript-eslint/typescript-estree" "6.2.1" semver "^7.5.4" +"@typescript-eslint/utils@^8.34.1": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.42.0.tgz#95f8e0c697ff2f7da5f72e16135011f878d815c0" + integrity sha512-JnIzu7H3RH5BrKC4NoZqRfmjqCIS1u3hGZltDYJgkVdqAezl4L9d1ZLw+36huCujtSBSAirGINF/S4UxOcR+/g== + dependencies: + "@eslint-community/eslint-utils" "^4.7.0" + "@typescript-eslint/scope-manager" "8.42.0" + "@typescript-eslint/types" "8.42.0" + "@typescript-eslint/typescript-estree" "8.42.0" + "@typescript-eslint/visitor-keys@6.2.1": version "6.2.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.2.1.tgz#442e7c09fe94b715a54ebe30e967987c3c41fbf4" @@ -287,6 +347,14 @@ "@typescript-eslint/types" "6.2.1" eslint-visitor-keys "^3.4.1" +"@typescript-eslint/visitor-keys@8.42.0": + version "8.42.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.42.0.tgz#87c6caaa1ac307bc73a87c1fc469f88f0162f27e" + integrity sha512-3WbiuzoEowaEn8RSnhJBrxSwX8ULYE9CXaPepS2C2W3NSA5NNIvBaslpBSBElPq0UGr0xVJlXFWOAKIkyylydQ== + dependencies: + "@typescript-eslint/types" "8.42.0" + eslint-visitor-keys "^4.2.1" + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -434,6 +502,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +brace-expansion@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7" + integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ== + dependencies: + balanced-match "^1.0.0" + braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" @@ -441,6 +516,13 @@ braces@^3.0.2: dependencies: fill-range "^7.0.1" +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -769,6 +851,15 @@ eslint-plugin-more@^1.0.5: resolved "https://registry.yarnpkg.com/eslint-plugin-more/-/eslint-plugin-more-1.0.5.tgz#667bffc2a64bde2d48b98c8faa111e213b2f873f" integrity sha512-zjDza5jeNBHWf8ZezyW2Llk99abndcGlSz9GIKgVOGwISx0m+f4QoZAapjSmUjKSxHvmOa7Lt68Pk8XbRzWb7w== +eslint-plugin-perfectionist@^4.15.0: + version "4.15.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-perfectionist/-/eslint-plugin-perfectionist-4.15.0.tgz#320029162b0ec439af522d5b146903f0e47bfbd4" + integrity sha512-pC7PgoXyDnEXe14xvRUhBII8A3zRgggKqJFx2a82fjrItDs1BSI7zdZnQtM2yQvcyod6/ujmzb7ejKPx8lZTnw== + dependencies: + "@typescript-eslint/types" "^8.34.1" + "@typescript-eslint/utils" "^8.34.1" + natural-orderby "^5.0.0" + eslint-scope@^7.2.2: version "7.2.2" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" @@ -794,6 +885,16 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4 resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f" integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw== +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-visitor-keys@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" + integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== + eslint@^8.46.0: version "8.46.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552" @@ -897,6 +998,17 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.2: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -935,6 +1047,13 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + find-up@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -1472,6 +1591,14 @@ micromatch@^4.0.4: braces "^3.0.2" picomatch "^2.3.1" +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + mimic-response@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -1489,6 +1616,13 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" +minimatch@^9.0.4: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -1514,6 +1648,11 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +natural-orderby@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/natural-orderby/-/natural-orderby-5.0.0.tgz#bb655f669ee9c84e82cdc6cddbba25eb263cd9f4" + integrity sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg== + normalize-url@^6.0.1: version "6.1.0" resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" @@ -1820,6 +1959,11 @@ semver@^7.3.2, semver@^7.5.4: dependencies: lru-cache "^6.0.0" +semver@^7.6.0: + version "7.7.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.2.tgz#67d99fdcd35cec21e6f8b87a7fd515a33f982b58" + integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA== + serialize-error@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-7.0.1.tgz#f1360b0447f61ffb483ec4157c737fab7d778e18" @@ -1938,6 +2082,11 @@ ts-api-utils@^1.0.1: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d" integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A== +ts-api-utils@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" + integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== + tsconfig-paths@^3.14.2: version "3.14.2" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz#6e32f1f79412decd261f92d633a9dc1cfa99f088"