From 75e94c18fa8cd96c191d004626102689101af500 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Mon, 3 Nov 2025 11:24:04 +1100 Subject: [PATCH 1/8] feat: add linked delete group test --- .../automation/enforce_localized_str.spec.ts | 6 +++ tests/automation/linked_device_group.spec.ts | 46 +++++++++++++++++++ tests/automation/locators/index.ts | 1 + tests/automation/types/testing.ts | 1 + 4 files changed, 54 insertions(+) diff --git a/tests/automation/enforce_localized_str.spec.ts b/tests/automation/enforce_localized_str.spec.ts index 98f7a3b..49b65ff 100644 --- a/tests/automation/enforce_localized_str.spec.ts +++ b/tests/automation/enforce_localized_str.spec.ts @@ -263,6 +263,12 @@ function getExpectedStringFromKey( return 'Message Too Long'; case 'modalMessageTooLongDescription': return 'Please shorten your message to {limit} characters or less.'; + case 'groupDelete': + return 'Delete Group'; + case 'groupDeleteDescription': + return 'Are you sure you want to delete {group_name}? This will remove all members and delete all group content.'; + case 'groupDeletedMemberDescription': + return '{group_name} has been deleted by a group admin. You will not be able to send any more messages.'; 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/linked_device_group.spec.ts b/tests/automation/linked_device_group.spec.ts index 73587ce..1d14063 100644 --- a/tests/automation/linked_device_group.spec.ts +++ b/tests/automation/linked_device_group.spec.ts @@ -21,6 +21,7 @@ import { clickOn, clickOnMatchingText, clickOnWithText, + hasElementBeenDeleted, waitForTestIdWithText, } from './utilities/utils'; @@ -311,3 +312,48 @@ test_group_Alice_1W_Bob_1W_Charlie_1W( ]); }, ); + +test_group_Alice_2W_Bob_1W_Charlie_1W( + 'Delete group linked device', + async ({ + aliceWindow1, + aliceWindow2, + bobWindow1, + charlieWindow1, + groupCreated, + }) => { + await clickOn(aliceWindow1, Conversation.conversationSettingsIcon); + await clickOn(aliceWindow1, ConversationSettings.leaveOrDeleteGroupOption); + await checkModalStrings( + aliceWindow1, + englishStrippedStr('groupDelete').toString(), + englishStrippedStr('groupDeleteDescription') + .withArgs({ group_name: groupCreated.userName }) + .toString(), + 'confirmModal', + ); + await clickOn(aliceWindow1, Global.confirmButton); + await Promise.all( + [bobWindow1, charlieWindow1].map(async (w) => { + await waitForTestIdWithText( + w, + 'empty-conversation-control-message', + englishStrippedStr('groupDeletedMemberDescription') + .withArgs({ group_name: groupCreated.userName }) + .toString(), + ); + }), + ); + await Promise.all( + [aliceWindow1, aliceWindow2].map(async (w) => { + await hasElementBeenDeleted( + w, + 'data-testid', + HomeScreen.conversationItemName.selector, + 10_000, + groupCreated.userName, + ); + }), + ); + }, +); diff --git a/tests/automation/locators/index.ts b/tests/automation/locators/index.ts index 5279174..b5ee8f3 100644 --- a/tests/automation/locators/index.ts +++ b/tests/automation/locators/index.ts @@ -121,6 +121,7 @@ export class ConversationSettings extends Locator { static readonly inviteContactsOption = this.testId( 'invite-contacts-menu-option', ); + static readonly leaveOrDeleteGroupOption = this.testId('leave-group-button'); static readonly manageMembersOption = this.testId( 'manage-members-menu-option', ); diff --git a/tests/automation/types/testing.ts b/tests/automation/types/testing.ts index a2cad1d..edcc912 100644 --- a/tests/automation/types/testing.ts +++ b/tests/automation/types/testing.ts @@ -118,6 +118,7 @@ export type DataTestId = | 'dropdownitem-5-seconds' | 'edit-group-name' | 'edit-profile-icon' + | 'empty-conversation-control-message' | 'empty-conversation-notification' | 'enable-calls-settings-row' | 'enable-communities-message-requests-settings-row' From 6757feac833282239b539854593201fbbcfb8ee0 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 5 Nov 2025 10:05:31 +1100 Subject: [PATCH 2/8] feat: add network page fetch test --- .../automation/enforce_localized_str.spec.ts | 2 +- tests/automation/locators/index.ts | 3 ++ tests/automation/network_page.spec.ts | 51 +++++++++++++++++++ tests/automation/types/testing.ts | 4 ++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/automation/network_page.spec.ts diff --git a/tests/automation/enforce_localized_str.spec.ts b/tests/automation/enforce_localized_str.spec.ts index 49b65ff..18daa54 100644 --- a/tests/automation/enforce_localized_str.spec.ts +++ b/tests/automation/enforce_localized_str.spec.ts @@ -263,7 +263,7 @@ function getExpectedStringFromKey( return 'Message Too Long'; case 'modalMessageTooLongDescription': return 'Please shorten your message to {limit} characters or less.'; - case 'groupDelete': + case 'groupDelete': return 'Delete Group'; case 'groupDeleteDescription': return 'Are you sure you want to delete {group_name}? This will remove all members and delete all group content.'; diff --git a/tests/automation/locators/index.ts b/tests/automation/locators/index.ts index b5ee8f3..a16bf7f 100644 --- a/tests/automation/locators/index.ts +++ b/tests/automation/locators/index.ts @@ -150,6 +150,9 @@ export class Settings extends Locator { static readonly messageRequestsMenuItem = this.testId( 'message-requests-settings-menu-item', ); + static readonly networkPageMenuItem = this.testId( + 'session-network-settings-menu-item', + ); static readonly privacyMenuItem = this.testId('privacy-settings-menu-item'); static readonly recoveryPasswordMenuItem = this.testId( 'recovery-password-settings-menu-item', diff --git a/tests/automation/network_page.spec.ts b/tests/automation/network_page.spec.ts new file mode 100644 index 0000000..eff6c51 --- /dev/null +++ b/tests/automation/network_page.spec.ts @@ -0,0 +1,51 @@ +import { LeftPane, Settings } from './locators'; +import { newUser } from './setup/new_user'; +import { sessionTestOneWindow } from './setup/sessionTest'; +import { clickOn, waitForTestIdWithText } from './utilities/utils'; + +function validateNetworkData(data: any): asserts data is { + price: { usd: number; usd_market_cap: number }; + token: { staking_reward_pool: number }; +} { + if ( + typeof data?.price?.usd !== 'number' || + typeof data?.token?.staking_reward_pool !== 'number' || + typeof data?.price?.usd_market_cap !== 'number' + ) { + throw new Error('Network API response missing or invalid numeric fields'); + } +} + +sessionTestOneWindow('Network page values', async ([aliceWindow1]) => { + await newUser(aliceWindow1, 'Alice'); + await clickOn(aliceWindow1, LeftPane.settingsButton); + await clickOn(aliceWindow1, Settings.networkPageMenuItem); + + const response = await fetch('http://networkv1.getsession.org/info'); + if (!response.ok) { + throw new Error(`Network API returned ${response.status}`); + } + const data = await response.json(); + validateNetworkData(data); + + // SESH Price - 2 decimals + const seshPrice = `$${data.price.usd.toFixed(2)} USD`; + + // Staking Reward Pool - whole number with commas + const stakingRewardPool = `${data.token.staking_reward_pool.toLocaleString( + 'en-US', + )} SESH`; + + // Market Cap - round to whole number with commas + const marketCap = `$${Math.round(data.price.usd_market_cap).toLocaleString( + 'en-US', + )} USD`; + + await waitForTestIdWithText(aliceWindow1, 'sent-price', seshPrice); + await waitForTestIdWithText( + aliceWindow1, + 'staking-reward-pool-amount', + stakingRewardPool, + ); + await waitForTestIdWithText(aliceWindow1, 'market-cap-amount', marketCap); +}); diff --git a/tests/automation/types/testing.ts b/tests/automation/types/testing.ts index edcc912..0c1524a 100644 --- a/tests/automation/types/testing.ts +++ b/tests/automation/types/testing.ts @@ -145,6 +145,7 @@ export type DataTestId = | 'link-preview-title' | 'loading-spinner' | 'manage-members-menu-option' + | 'market-cap-amount' | 'mentions-popup-row' | 'message-content' | 'message-input-text-area' @@ -178,15 +179,18 @@ export type DataTestId = | 'save-button-profile-update' | 'scroll-to-bottom-button' | 'send-message-button' + | 'sent-price' | 'session-confirm-cancel-button' | 'session-confirm-ok-button' | 'session-id-signup' + | 'session-network-settings-menu-item' | 'session-recovery-password' | 'session-toast' | 'set-nickname-confirm-button' | 'set-password-button' | 'set-password-settings-button' | 'settings-section' + | 'staking-reward-pool-amount' | 'theme-section' | 'tooltip-character-count' | 'unblock-button-settings-screen' From ff13cfb9b1127ca8ec5748547c90d197715c3738 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 5 Nov 2025 16:47:46 +1100 Subject: [PATCH 3/8] feat: bring in more network page tests --- tests/automation/delete_account.spec.ts | 5 +- .../automation/enforce_localized_str.spec.ts | 6 ++ tests/automation/linked_device_user.spec.ts | 5 +- tests/automation/locators/index.ts | 13 +++ tests/automation/network_page.spec.ts | 97 ++++++++++++++++--- tests/automation/setup/sessionTest.ts | 24 +++++ tests/automation/types/testing.ts | 7 +- tests/automation/user_actions.spec.ts | 5 +- tests/automation/utilities/join_community.ts | 4 +- tests/automation/utilities/send_media.ts | 2 +- tests/automation/utilities/utils.ts | 12 ++- 11 files changed, 159 insertions(+), 21 deletions(-) diff --git a/tests/automation/delete_account.spec.ts b/tests/automation/delete_account.spec.ts index fa0a372..d56da62 100644 --- a/tests/automation/delete_account.spec.ts +++ b/tests/automation/delete_account.spec.ts @@ -60,7 +60,10 @@ sessionTestTwoWindows( windowA, englishStrippedStr('clear').toString(), ); - await waitForLoadingAnimationToFinish(windowA, 'loading-spinner'); + await waitForLoadingAnimationToFinish( + windowA, + Global.loadingSpinner.selector, + ); // await sleepFor(7500); // Wait for window to close and reopen diff --git a/tests/automation/enforce_localized_str.spec.ts b/tests/automation/enforce_localized_str.spec.ts index 18daa54..deae49d 100644 --- a/tests/automation/enforce_localized_str.spec.ts +++ b/tests/automation/enforce_localized_str.spec.ts @@ -269,6 +269,12 @@ function getExpectedStringFromKey( return 'Are you sure you want to delete {group_name}? This will remove all members and delete all group content.'; case 'groupDeletedMemberDescription': return '{group_name} has been deleted by a group admin. You will not be able to send any more messages.'; + case 'urlOpen': + return 'Open URL'; + case 'urlOpenDescription': + return 'Are you sure you want to open this URL in your browser? {url}'; + case 'updated': + return 'Last updated {relative_time} ago'; 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/linked_device_user.spec.ts b/tests/automation/linked_device_user.spec.ts index 7714017..a7e4bfc 100644 --- a/tests/automation/linked_device_user.spec.ts +++ b/tests/automation/linked_device_user.spec.ts @@ -146,7 +146,10 @@ test_Alice_2W( // allow for the image to be resized before we try to save it await sleepFor(500); await clickOn(aliceWindow1, Settings.saveProfileUpdateButton); - await waitForLoadingAnimationToFinish(aliceWindow1, 'loading-spinner'); + await waitForLoadingAnimationToFinish( + aliceWindow1, + Global.loadingSpinner.selector, + ); await clickOnMatchingText( aliceWindow1, englishStrippedStr('save').toString(), diff --git a/tests/automation/locators/index.ts b/tests/automation/locators/index.ts index a16bf7f..a854361 100644 --- a/tests/automation/locators/index.ts +++ b/tests/automation/locators/index.ts @@ -157,6 +157,18 @@ export class Settings extends Locator { static readonly recoveryPasswordMenuItem = this.testId( 'recovery-password-settings-menu-item', ); + // Session Network + static readonly lastUpdatedTimestamp = this.testId('last-updated-timestamp'); + static readonly learnMoreAboutStakingLink = this.testId( + 'learn-about-staking-link', + ); + static readonly learnMoreNetworkLink = this.testId('learn-more-network-link'); + static readonly marketCapAmount = this.testId('market-cap-amount'); + static readonly refreshButton = this.testId('refresh-button'); + static readonly seshPrice = this.testId('sent-price'); + static readonly stakingRewardPoolAmount = this.testId( + 'staking-reward-pool-amount', + ); // Privacy static readonly changePasswordSettingsButton = this.testId( 'change-password-settings-button', @@ -211,6 +223,7 @@ export class Global extends Locator { static readonly contextMenuItem = this.testId('context-menu-item'); static readonly continueButton = this.testId('continue-button'); static readonly errorMessage = this.testId('error-message'); + static readonly loadingSpinner = this.testId('loading-spinner'); 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/network_page.spec.ts b/tests/automation/network_page.spec.ts index eff6c51..97c86a8 100644 --- a/tests/automation/network_page.spec.ts +++ b/tests/automation/network_page.spec.ts @@ -1,7 +1,14 @@ -import { LeftPane, Settings } from './locators'; -import { newUser } from './setup/new_user'; -import { sessionTestOneWindow } from './setup/sessionTest'; -import { clickOn, waitForTestIdWithText } from './utilities/utils'; +import { englishStrippedStr } from '../localization/englishStrippedStr'; +import { sleepFor } from '../promise_utils'; +import { Global, LeftPane, Settings } from './locators'; +import { test_Alice_1W } from './setup/sessionTest'; +import { + assertUrlIsReachable, + checkModalStrings, + clickOn, + waitForLoadingAnimationToFinish, + waitForTestIdWithText, +} from './utilities/utils'; function validateNetworkData(data: any): asserts data is { price: { usd: number; usd_market_cap: number }; @@ -16,8 +23,7 @@ function validateNetworkData(data: any): asserts data is { } } -sessionTestOneWindow('Network page values', async ([aliceWindow1]) => { - await newUser(aliceWindow1, 'Alice'); +test_Alice_1W('Network page values', async ({ aliceWindow1 }) => { await clickOn(aliceWindow1, LeftPane.settingsButton); await clickOn(aliceWindow1, Settings.networkPageMenuItem); @@ -28,24 +34,91 @@ sessionTestOneWindow('Network page values', async ([aliceWindow1]) => { const data = await response.json(); validateNetworkData(data); - // SESH Price - 2 decimals + // SESH Price - 2 decimals "$1.23 USD" const seshPrice = `$${data.price.usd.toFixed(2)} USD`; - // Staking Reward Pool - whole number with commas + // Staking Reward Pool - whole number with commas "1,234,567 SESH" const stakingRewardPool = `${data.token.staking_reward_pool.toLocaleString( 'en-US', )} SESH`; - // Market Cap - round to whole number with commas + // Market Cap - round to whole number with commas, "$1,234,567 USD" const marketCap = `$${Math.round(data.price.usd_market_cap).toLocaleString( 'en-US', )} USD`; - await waitForTestIdWithText(aliceWindow1, 'sent-price', seshPrice); await waitForTestIdWithText( aliceWindow1, - 'staking-reward-pool-amount', + Settings.seshPrice.selector, + seshPrice, + ); + await waitForTestIdWithText( + aliceWindow1, + Settings.stakingRewardPoolAmount.selector, stakingRewardPool, ); - await waitForTestIdWithText(aliceWindow1, 'market-cap-amount', marketCap); + await waitForTestIdWithText( + aliceWindow1, + Settings.marketCapAmount.selector, + marketCap, + ); +}); + +test_Alice_1W('Network page network link', async ({ aliceWindow1 }) => { + const url = 'https://docs.getsession.org/session-network'; + await clickOn(aliceWindow1, LeftPane.settingsButton); + await clickOn(aliceWindow1, Settings.networkPageMenuItem); + await clickOn(aliceWindow1, Settings.learnMoreNetworkLink); + await checkModalStrings( + aliceWindow1, + englishStrippedStr('urlOpen').toString(), + englishStrippedStr('urlOpenDescription').withArgs({ url }).toString(), + 'openUrlModal', + ); + await assertUrlIsReachable(url); +}); + +test_Alice_1W('Network page staking link', async ({ aliceWindow1 }) => { + const url = 'https://docs.getsession.org/session-network/staking'; + await clickOn(aliceWindow1, LeftPane.settingsButton); + await clickOn(aliceWindow1, Settings.networkPageMenuItem); + await clickOn(aliceWindow1, Settings.learnMoreAboutStakingLink); + await checkModalStrings( + aliceWindow1, + englishStrippedStr('urlOpen').toString(), + englishStrippedStr('urlOpenDescription').withArgs({ url }).toString(), + 'openUrlModal', + ); + await assertUrlIsReachable(url); +}); + +test_Alice_1W('Network page refresh', async ({ aliceWindow1 }) => { + const zeroMinAgoText = englishStrippedStr('updated') + .withArgs({ relative_time: '0m' }) + .toString(); + const oneMinAgoText = englishStrippedStr('updated') + .withArgs({ relative_time: '1m' }) + .toString(); + await clickOn(aliceWindow1, LeftPane.settingsButton); + await clickOn(aliceWindow1, Settings.networkPageMenuItem); + await waitForLoadingAnimationToFinish( + aliceWindow1, + Global.loadingSpinner.selector, + ); + await sleepFor(65_000); // Wait 60+5 seconds to ensure timestamp changes to "1m ago" + await waitForTestIdWithText( + aliceWindow1, + Settings.lastUpdatedTimestamp.selector, + oneMinAgoText, + ); + await clickOn(aliceWindow1, Settings.refreshButton); + await waitForLoadingAnimationToFinish( + aliceWindow1, + Global.loadingSpinner.selector, + ); + await waitForTestIdWithText( + aliceWindow1, + Settings.lastUpdatedTimestamp.selector, + zeroMinAgoText, + ); }); diff --git a/tests/automation/setup/sessionTest.ts b/tests/automation/setup/sessionTest.ts index 3b4e2c2..b789ebd 100644 --- a/tests/automation/setup/sessionTest.ts +++ b/tests/automation/setup/sessionTest.ts @@ -224,6 +224,30 @@ export function test_Alice_1W_no_network( ); } +// Need a test for one window with network, mainly for Network Page +export function test_Alice_1W( + testname: string, + testCallback: ( + details: WithAlice & WithAliceWindow1, + testInfo: TestInfo, + ) => Promise, +) { + return sessionTestGeneric( + testname, + 1, + { waitForNetwork: true }, + ({ mainWindows, users }, testInfo) => { + return testCallback( + { + alice: users[0], + aliceWindow1: mainWindows[0], + }, + testInfo, + ); + }, + ); +} + /** * Setup the test with 1 user and 2 windows total: * - Alice with 2 windows. diff --git a/tests/automation/types/testing.ts b/tests/automation/types/testing.ts index 0c1524a..1014127 100644 --- a/tests/automation/types/testing.ts +++ b/tests/automation/types/testing.ts @@ -72,7 +72,6 @@ export type WithPage = { window: Page }; export type WithMaxWait = { maxWait?: number }; export type WithRightButton = { rightButton?: boolean }; -export type LoaderType = 'loading-animation' | 'loading-spinner'; export type MediaType = 'audio' | 'file' | 'image' | 'video'; export type Strategy = ':has-text' | 'class' | 'data-testid'; @@ -138,11 +137,15 @@ export type DataTestId = | 'join-community-button' | 'join-community-conversation' | 'label-device_and_network' + | 'last-updated-timestamp' + | 'learn-about-staking-link' + | 'learn-more-network-link' | 'leave-group-button' | 'leftpane-primary-avatar' | 'link-device' | 'link-preview-image' | 'link-preview-title' + | 'loading-animation' | 'loading-spinner' | 'manage-members-menu-option' | 'market-cap-amount' @@ -174,6 +177,7 @@ export type DataTestId = | 'recovery-password-seed-modal' | 'recovery-password-settings-menu-item' | 'recovery-phrase-input' + | 'refresh-button' | 'restore-using-recovery' | 'reveal-recovery-phrase' | 'save-button-profile-update' @@ -205,4 +209,5 @@ export type ModalId = | 'confirmModal' | 'deleteAccountModal' | 'hideRecoveryPasswordModal' + | 'openUrlModal' | 'userSettingsModal'; diff --git a/tests/automation/user_actions.spec.ts b/tests/automation/user_actions.spec.ts index 4347e34..b676cd7 100644 --- a/tests/automation/user_actions.spec.ts +++ b/tests/automation/user_actions.spec.ts @@ -183,7 +183,10 @@ test_Alice_1W_no_network( // allow for the image to be resized before we try to save it await sleepFor(500); await clickOn(aliceWindow1, Settings.saveProfileUpdateButton); - await waitForLoadingAnimationToFinish(aliceWindow1, 'loading-spinner'); + await waitForLoadingAnimationToFinish( + aliceWindow1, + Global.loadingSpinner.selector, + ); await clickOnMatchingText( aliceWindow1, englishStrippedStr('save').toString(), diff --git a/tests/automation/utilities/join_community.ts b/tests/automation/utilities/join_community.ts index d6dd074..c8f96c6 100644 --- a/tests/automation/utilities/join_community.ts +++ b/tests/automation/utilities/join_community.ts @@ -1,7 +1,7 @@ import { Page } from '@playwright/test'; import { testCommunityLink } from '../constants/community'; -import { HomeScreen } from '../locators'; +import { Global, HomeScreen } from '../locators'; import { clickOn, typeIntoInput, @@ -18,5 +18,5 @@ export const joinCommunity = async (window: Page) => { testCommunityLink, ); await clickOn(window, HomeScreen.joinCommunityButton); - await waitForLoadingAnimationToFinish(window, 'loading-spinner'); + await waitForLoadingAnimationToFinish(window, Global.loadingSpinner.selector); }; diff --git a/tests/automation/utilities/send_media.ts b/tests/automation/utilities/send_media.ts index a9ce17f..ccc7822 100644 --- a/tests/automation/utilities/send_media.ts +++ b/tests/automation/utilities/send_media.ts @@ -93,7 +93,7 @@ export const sendLinkPreview = async (window: Page, testLink: string) => { Global.confirmButton, englishStrippedStr('enable').toString(), ); - await waitForLoadingAnimationToFinish(window, 'loading-spinner'); + await waitForLoadingAnimationToFinish(window, Global.loadingSpinner.selector); await waitForTestIdWithText(window, 'link-preview-image'); await waitForTestIdWithText( window, diff --git a/tests/automation/utilities/utils.ts b/tests/automation/utilities/utils.ts index 78e53ad..343cbbc 100644 --- a/tests/automation/utilities/utils.ts +++ b/tests/automation/utilities/utils.ts @@ -8,7 +8,6 @@ import { sleepFor } from '../../promise_utils'; import { DataTestId, DMTimeOption, - LoaderType, ModalId, Strategy, StrategyExtractionObj, @@ -168,7 +167,7 @@ export async function waitForMatchingPlaceholder( } export async function waitForLoadingAnimationToFinish( window: Page, - loader: LoaderType, + loader: DataTestId, maxWait?: number, ) { let loadingAnimation: ElementHandle | undefined; @@ -581,3 +580,12 @@ export function formatTimeOption(option: DMTimeOption) { const formattedTime = timePart.replace(/-/g, ' '); return formattedTime; } + +export async function assertUrlIsReachable(url: string): Promise { + const response = await fetch(url); + if (response.status !== 200) { + throw new Error( + `Expected status 200 but got ${response.status} for URL: ${url}`, + ); + } +} From 65a689671656915cd8a81f4acd5a061a0eed1158 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 5 Nov 2025 17:31:05 +1100 Subject: [PATCH 4/8] feat: add warning modal tests --- tests/automation/onboarding.spec.ts | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 tests/automation/onboarding.spec.ts diff --git a/tests/automation/onboarding.spec.ts b/tests/automation/onboarding.spec.ts new file mode 100644 index 0000000..c2c5cce --- /dev/null +++ b/tests/automation/onboarding.spec.ts @@ -0,0 +1,63 @@ +import { englishStrippedStr } from '../localization/englishStrippedStr'; +import { Global, Onboarding } from './locators'; +import { sessionTestOneWindow } from './setup/sessionTest'; +import { + checkModalStrings, + clickOn, + clickOnWithText, + typeIntoInput, +} from './utilities/utils'; + +sessionTestOneWindow( + 'Warning modal new account', + async ([aliceWindow1]) => { + // Create User + await clickOn(aliceWindow1, Onboarding.createAccountButton); + // Need to implement a back button on Desktop + await clickOn(aliceWindow1, Global.backButton); + // Expect modal to appear with warning message + await checkModalStrings( + aliceWindow1, + englishStrippedStr('warning').toString(), + englishStrippedStr('onboardingBackAccountCreation').toString(), + 'confirmModal', + ); + await clickOnWithText(aliceWindow1, Global.confirmButton, englishStrippedStr('quitButton').toString()); + // Wait for window to close (confirms restart was triggered) + await aliceWindow1.waitForEvent('close', { timeout: 5000 }); + + // Test ends - app is restarting but we can't verify the aftermath + // Playwright cannot keep track of Electron's `window.restart` IPC call + // so this will have to do + }, +); + +sessionTestOneWindow( + 'Warning modal restore account', + async ([aliceWindow1]) => { + const seedPhrase = + 'eldest fazed hybrid buzzer nasty domestic digit pager unusual purged makeup assorted domestic'; + // Restore user + await clickOn(aliceWindow1, Onboarding.iHaveAnAccountButton); + // Input recovery phrase + await typeIntoInput(aliceWindow1, 'recovery-phrase-input', seedPhrase); + // Click continue to go to loading page + await clickOn(aliceWindow1, Global.continueButton); + // Need to implement a back button on Desktop + await clickOn(aliceWindow1, Global.backButton); + // Expect modal to appear with warning message + await checkModalStrings( + aliceWindow1, + englishStrippedStr('warning').toString(), + englishStrippedStr('onboardingBackLoadAccount').toString(), + 'confirmModal', + ); + await clickOnWithText(aliceWindow1, Global.confirmButton, englishStrippedStr('quitButton').toString()); + // Wait for window to close (confirms restart was triggered) + await aliceWindow1.waitForEvent('close', { timeout: 5000 }); + + // Test ends - app is restarting but we can't verify the aftermath + // Playwright cannot keep track of Electron's `window.restart` IPC call + // so this will have to do + }, +); \ No newline at end of file From 12a6560a7b45226ce7f2bde21bcd18f1c6da0d23 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Wed, 5 Nov 2025 17:34:35 +1100 Subject: [PATCH 5/8] chore: add strings --- .../automation/enforce_localized_str.spec.ts | 8 +++ tests/automation/onboarding.spec.ts | 55 ++++++++++--------- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/tests/automation/enforce_localized_str.spec.ts b/tests/automation/enforce_localized_str.spec.ts index deae49d..9e2da46 100644 --- a/tests/automation/enforce_localized_str.spec.ts +++ b/tests/automation/enforce_localized_str.spec.ts @@ -275,6 +275,14 @@ function getExpectedStringFromKey( return 'Are you sure you want to open this URL in your browser? {url}'; case 'updated': return 'Last updated {relative_time} ago'; + case 'warning': + return 'Warning'; + case 'onboardingBackAccountCreation': + return 'You cannot go back further. In order to cancel your account creation, Session needs to quit.'; + case 'onboardingBackLoadAccount': + return 'You cannot go back further. In order to stop loading your account, Session needs to quit.'; + case 'quitButton': + return 'Quit'; 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/onboarding.spec.ts b/tests/automation/onboarding.spec.ts index c2c5cce..a2505ce 100644 --- a/tests/automation/onboarding.spec.ts +++ b/tests/automation/onboarding.spec.ts @@ -8,29 +8,30 @@ import { typeIntoInput, } from './utilities/utils'; -sessionTestOneWindow( - 'Warning modal new account', - async ([aliceWindow1]) => { - // Create User - await clickOn(aliceWindow1, Onboarding.createAccountButton); - // Need to implement a back button on Desktop - await clickOn(aliceWindow1, Global.backButton); - // Expect modal to appear with warning message - await checkModalStrings( - aliceWindow1, - englishStrippedStr('warning').toString(), - englishStrippedStr('onboardingBackAccountCreation').toString(), - 'confirmModal', - ); - await clickOnWithText(aliceWindow1, Global.confirmButton, englishStrippedStr('quitButton').toString()); - // Wait for window to close (confirms restart was triggered) - await aliceWindow1.waitForEvent('close', { timeout: 5000 }); +sessionTestOneWindow('Warning modal new account', async ([aliceWindow1]) => { + // Create User + await clickOn(aliceWindow1, Onboarding.createAccountButton); + // Need to implement a back button on Desktop + await clickOn(aliceWindow1, Global.backButton); + // Expect modal to appear with warning message + await checkModalStrings( + aliceWindow1, + englishStrippedStr('warning').toString(), + englishStrippedStr('onboardingBackAccountCreation').toString(), + 'confirmModal', + ); + await clickOnWithText( + aliceWindow1, + Global.confirmButton, + englishStrippedStr('quitButton').toString(), + ); + // Wait for window to close (confirms restart was triggered) + await aliceWindow1.waitForEvent('close', { timeout: 5000 }); - // Test ends - app is restarting but we can't verify the aftermath - // Playwright cannot keep track of Electron's `window.restart` IPC call - // so this will have to do - }, -); + // Test ends - app is restarting but we can't verify the aftermath + // Playwright cannot keep track of Electron's `window.restart` IPC call + // so this will have to do +}); sessionTestOneWindow( 'Warning modal restore account', @@ -52,12 +53,16 @@ sessionTestOneWindow( englishStrippedStr('onboardingBackLoadAccount').toString(), 'confirmModal', ); - await clickOnWithText(aliceWindow1, Global.confirmButton, englishStrippedStr('quitButton').toString()); + await clickOnWithText( + aliceWindow1, + Global.confirmButton, + englishStrippedStr('quitButton').toString(), + ); // Wait for window to close (confirms restart was triggered) await aliceWindow1.waitForEvent('close', { timeout: 5000 }); // Test ends - app is restarting but we can't verify the aftermath // Playwright cannot keep track of Electron's `window.restart` IPC call - // so this will have to do + // so this will have to do }, -); \ No newline at end of file +); From d5804c830e184d7e49b732ce9347983065fcf036 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 6 Nov 2025 09:56:21 +1100 Subject: [PATCH 6/8] feat: also kill child processes --- tests/automation/setup/closeWindows.ts | 22 ++++++++++++++++++++-- tests/automation/setup/open.ts | 17 +++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/tests/automation/setup/closeWindows.ts b/tests/automation/setup/closeWindows.ts index 357cd79..a6115ff 100644 --- a/tests/automation/setup/closeWindows.ts +++ b/tests/automation/setup/closeWindows.ts @@ -1,10 +1,28 @@ import { Page } from '@playwright/test'; +import { execSync } from 'child_process'; import { sleepFor } from '../../promise_utils'; +import { getTrackedElectronPids } from './open'; export const forceCloseAllWindows = async (windows: Array) => { - return Promise.race([ + console.log('forceCloseAllWindows called with', windows.length, 'windows'); + + await Promise.race([ Promise.all(windows.map((w) => w.close())), - async () => sleepFor(4000), + sleepFor(4000), ]); + + // Also kill child processes + const pids = getTrackedElectronPids(); + pids.forEach((pid) => { + try { + const killCommand = + process.platform === 'win32' + ? `taskkill /F /T /PID ${pid}` // /T kills child processes on Windows + : `pkill -9 -P ${pid} && kill -9 ${pid}`; // Kill children then parent on Unix + execSync(killCommand, { stdio: 'ignore' }); + } catch (e) { + console.log('Failed to kill PID:', pid, e); + } + }); }; diff --git a/tests/automation/setup/open.ts b/tests/automation/setup/open.ts index 4f98ba4..03cb3aa 100644 --- a/tests/automation/setup/open.ts +++ b/tests/automation/setup/open.ts @@ -7,6 +7,7 @@ import { v4 } from 'uuid'; export const NODE_ENV = 'production'; export const MULTI_PREFIX = 'test-integration-'; const multisAvailable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; +let electronPids: Array = []; export function getAppRootPath() { if (isEmpty(process.env.SESSION_DESKTOP_ROOT)) { @@ -40,6 +41,17 @@ const openElectronAppOnly = async (multi: string) => { '--force-device-scale-factor=1', // Normalizes Retina and non-Retina mac screens ], }); + + // When a test closes a window on purpose, + // the restarted app is considered a child process of the original electronApp. + // However Playwright only tracks the original processes. + // In order to close all Electron windows during teardown + // we need to keep track of the opened PIDs. + const pid = electronApp.process()?.pid; + if (pid) { + electronPids.push(pid); + } + return electronApp; } catch (e) { console.info( @@ -72,6 +84,7 @@ const openAppAndWait = async (multi: string) => { }; export async function openApp(windowsToCreate: number) { + electronPids = []; // Reset if (windowsToCreate >= multisAvailable.length) { throw new Error(`Do you really need ${multisAvailable.length} windows?!`); } @@ -92,3 +105,7 @@ export async function openApp(windowsToCreate: number) { ); return toRet; } + +export function getTrackedElectronPids(): Array { + return electronPids; +} From 115a200171b03d44cf50fc89681f4203f0e621c4 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 6 Nov 2025 09:59:21 +1100 Subject: [PATCH 7/8] chore: remove comments --- tests/automation/onboarding.spec.ts | 8 -------- tests/automation/setup/sessionTest.ts | 1 - 2 files changed, 9 deletions(-) diff --git a/tests/automation/onboarding.spec.ts b/tests/automation/onboarding.spec.ts index a2505ce..a7213fb 100644 --- a/tests/automation/onboarding.spec.ts +++ b/tests/automation/onboarding.spec.ts @@ -9,11 +9,8 @@ import { } from './utilities/utils'; sessionTestOneWindow('Warning modal new account', async ([aliceWindow1]) => { - // Create User await clickOn(aliceWindow1, Onboarding.createAccountButton); - // Need to implement a back button on Desktop await clickOn(aliceWindow1, Global.backButton); - // Expect modal to appear with warning message await checkModalStrings( aliceWindow1, englishStrippedStr('warning').toString(), @@ -38,15 +35,10 @@ sessionTestOneWindow( async ([aliceWindow1]) => { const seedPhrase = 'eldest fazed hybrid buzzer nasty domestic digit pager unusual purged makeup assorted domestic'; - // Restore user await clickOn(aliceWindow1, Onboarding.iHaveAnAccountButton); - // Input recovery phrase await typeIntoInput(aliceWindow1, 'recovery-phrase-input', seedPhrase); - // Click continue to go to loading page await clickOn(aliceWindow1, Global.continueButton); - // Need to implement a back button on Desktop await clickOn(aliceWindow1, Global.backButton); - // Expect modal to appear with warning message await checkModalStrings( aliceWindow1, englishStrippedStr('warning').toString(), diff --git a/tests/automation/setup/sessionTest.ts b/tests/automation/setup/sessionTest.ts index b789ebd..17dfc7b 100644 --- a/tests/automation/setup/sessionTest.ts +++ b/tests/automation/setup/sessionTest.ts @@ -224,7 +224,6 @@ export function test_Alice_1W_no_network( ); } -// Need a test for one window with network, mainly for Network Page export function test_Alice_1W( testname: string, testCallback: ( From 861db0b2ab1fdd258f6bfd86dd4727e19d3fd775 Mon Sep 17 00:00:00 2001 From: Miklos Mandoki Date: Thu, 6 Nov 2025 11:26:14 +1100 Subject: [PATCH 8/8] fix: address PR feedback relegate network api validation to separate util add reset pids function to beginning of test instead of open app remove debug logs skip error logging on pid kill command --- tests/automation/network_page.spec.ts | 14 +-------- tests/automation/setup/closeWindows.ts | 6 ++-- tests/automation/setup/open.ts | 5 +++- tests/automation/setup/sessionTest.ts | 3 +- tests/automation/utilities/network_api.ts | 36 +++++++++++++++++++++++ 5 files changed, 45 insertions(+), 19 deletions(-) create mode 100644 tests/automation/utilities/network_api.ts diff --git a/tests/automation/network_page.spec.ts b/tests/automation/network_page.spec.ts index 97c86a8..a2a0265 100644 --- a/tests/automation/network_page.spec.ts +++ b/tests/automation/network_page.spec.ts @@ -2,6 +2,7 @@ import { englishStrippedStr } from '../localization/englishStrippedStr'; import { sleepFor } from '../promise_utils'; import { Global, LeftPane, Settings } from './locators'; import { test_Alice_1W } from './setup/sessionTest'; +import { validateNetworkData } from './utilities/network_api'; import { assertUrlIsReachable, checkModalStrings, @@ -10,19 +11,6 @@ import { waitForTestIdWithText, } from './utilities/utils'; -function validateNetworkData(data: any): asserts data is { - price: { usd: number; usd_market_cap: number }; - token: { staking_reward_pool: number }; -} { - if ( - typeof data?.price?.usd !== 'number' || - typeof data?.token?.staking_reward_pool !== 'number' || - typeof data?.price?.usd_market_cap !== 'number' - ) { - throw new Error('Network API response missing or invalid numeric fields'); - } -} - test_Alice_1W('Network page values', async ({ aliceWindow1 }) => { await clickOn(aliceWindow1, LeftPane.settingsButton); await clickOn(aliceWindow1, Settings.networkPageMenuItem); diff --git a/tests/automation/setup/closeWindows.ts b/tests/automation/setup/closeWindows.ts index a6115ff..3a69fbc 100644 --- a/tests/automation/setup/closeWindows.ts +++ b/tests/automation/setup/closeWindows.ts @@ -5,8 +5,6 @@ import { sleepFor } from '../../promise_utils'; import { getTrackedElectronPids } from './open'; export const forceCloseAllWindows = async (windows: Array) => { - console.log('forceCloseAllWindows called with', windows.length, 'windows'); - await Promise.race([ Promise.all(windows.map((w) => w.close())), sleepFor(4000), @@ -19,10 +17,10 @@ export const forceCloseAllWindows = async (windows: Array) => { const killCommand = process.platform === 'win32' ? `taskkill /F /T /PID ${pid}` // /T kills child processes on Windows - : `pkill -9 -P ${pid} && kill -9 ${pid}`; // Kill children then parent on Unix + : `pkill -9 -P ${pid}; kill -9 ${pid}`; // Kill children then parent on Unix execSync(killCommand, { stdio: 'ignore' }); } catch (e) { - console.log('Failed to kill PID:', pid, e); + // This is fine - process already dead or doesn't exist } }); }; diff --git a/tests/automation/setup/open.ts b/tests/automation/setup/open.ts index 03cb3aa..dfb42ab 100644 --- a/tests/automation/setup/open.ts +++ b/tests/automation/setup/open.ts @@ -84,7 +84,6 @@ const openAppAndWait = async (multi: string) => { }; export async function openApp(windowsToCreate: number) { - electronPids = []; // Reset if (windowsToCreate >= multisAvailable.length) { throw new Error(`Do you really need ${multisAvailable.length} windows?!`); } @@ -109,3 +108,7 @@ export async function openApp(windowsToCreate: number) { export function getTrackedElectronPids(): Array { return electronPids; } + +export function resetTrackedElectronPids() { + electronPids = []; +} diff --git a/tests/automation/setup/sessionTest.ts b/tests/automation/setup/sessionTest.ts index 17dfc7b..b4116f9 100644 --- a/tests/automation/setup/sessionTest.ts +++ b/tests/automation/setup/sessionTest.ts @@ -10,7 +10,7 @@ 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 { openApp, resetTrackedElectronPids } from './open'; // 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. @@ -46,6 +46,7 @@ function sessionTest>( count: T, ) { return test(testName, async ({}, testinfo) => { + resetTrackedElectronPids(); const windows = await openApp(count); try { diff --git a/tests/automation/utilities/network_api.ts b/tests/automation/utilities/network_api.ts new file mode 100644 index 0000000..e6330d5 --- /dev/null +++ b/tests/automation/utilities/network_api.ts @@ -0,0 +1,36 @@ +import { isFinite } from 'lodash'; + +export type NetworkData = { + price: { usd: number; usd_market_cap: number }; + token: { staking_reward_pool: number }; +}; + +function isObject(value: unknown): value is Record { + return typeof value === 'object' && value !== null; +} + +function isPositiveFiniteNumber(n: unknown): n is number { + return typeof n === 'number' && isFinite(n) && n > 0; +} + +export function validateNetworkData( + data: unknown, +): asserts data is NetworkData { + if (!isObject(data)) { + throw new Error('Invalid network API response: not an object'); + } + + if (!isObject(data.price) || !isObject(data.token)) { + throw new Error('Invalid network API response: missing price or token'); + } + + if ( + !isPositiveFiniteNumber(data.price.usd) || + !isPositiveFiniteNumber(data.price.usd_market_cap) || + !isPositiveFiniteNumber(data.token.staking_reward_pool) + ) { + throw new Error( + 'Invalid network API response: numeric fields must be positive and finite', + ); + } +}