Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion tests/automation/delete_account.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
20 changes: 20 additions & 0 deletions tests/automation/enforce_localized_str.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,26 @@ 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.';
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';
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
Expand Down
46 changes: 46 additions & 0 deletions tests/automation/linked_device_group.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
clickOn,
clickOnMatchingText,
clickOnWithText,
hasElementBeenDeleted,
waitForTestIdWithText,
} from './utilities/utils';

Expand Down Expand Up @@ -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,
);
}),
);
},
);
5 changes: 4 additions & 1 deletion tests/automation/linked_device_user.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
17 changes: 17 additions & 0 deletions tests/automation/locators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
);
Expand Down Expand Up @@ -149,10 +150,25 @@ 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',
);
// 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',
Expand Down Expand Up @@ -207,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');
Expand Down
112 changes: 112 additions & 0 deletions tests/automation/network_page.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
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,
clickOn,
waitForLoadingAnimationToFinish,
waitForTestIdWithText,
} from './utilities/utils';

test_Alice_1W('Network page values', async ({ aliceWindow1 }) => {
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 "$1.23 USD"
const seshPrice = `$${data.price.usd.toFixed(2)} USD`;

// 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, "$1,234,567 USD"
const marketCap = `$${Math.round(data.price.usd_market_cap).toLocaleString(
'en-US',
)} USD`;

await waitForTestIdWithText(
aliceWindow1,
Settings.seshPrice.selector,
seshPrice,
);
await waitForTestIdWithText(
aliceWindow1,
Settings.stakingRewardPoolAmount.selector,
stakingRewardPool,
);
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,
);
});
60 changes: 60 additions & 0 deletions tests/automation/onboarding.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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]) => {
await clickOn(aliceWindow1, Onboarding.createAccountButton);
await clickOn(aliceWindow1, Global.backButton);
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';
await clickOn(aliceWindow1, Onboarding.iHaveAnAccountButton);
await typeIntoInput(aliceWindow1, 'recovery-phrase-input', seedPhrase);
await clickOn(aliceWindow1, Global.continueButton);
await clickOn(aliceWindow1, Global.backButton);
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
},
);
20 changes: 18 additions & 2 deletions tests/automation/setup/closeWindows.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
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<Page>) => {
return Promise.race([
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) {
// This is fine - process already dead or doesn't exist
}
});
};
20 changes: 20 additions & 0 deletions tests/automation/setup/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> = [];

export function getAppRootPath() {
if (isEmpty(process.env.SESSION_DESKTOP_ROOT)) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -92,3 +104,11 @@ export async function openApp(windowsToCreate: number) {
);
return toRet;
}

export function getTrackedElectronPids(): Array<number> {
return electronPids;
}

export function resetTrackedElectronPids() {
electronPids = [];
}
Loading
Loading