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
195 changes: 171 additions & 24 deletions playwright-tests/helpers/friendStatusHelpers.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,177 @@
const { expect } = require('@playwright/test');
const { getInjectedTransaction } = require('./injectHelpers');

const FriendStatus = Object.freeze({
BLOCKED: 0,
OTHER: 1,
CONNECTION: 2,
});

async function waitForFriendStatusTransaction(page) {
await page.waitForEvent('console', {
timeout: 60_000,
predicate: (msg) => /update_toll_required transaction successfully processed/i.test(msg.text()),
const FRIEND_STATUS_LABELS = Object.freeze({
[FriendStatus.BLOCKED]: 'Blocked',
[FriendStatus.OTHER]: 'Tolled',
[FriendStatus.CONNECTION]: 'Connection',
});

const FRIEND_STATUS_STATE_TIMEOUT = 15_000;
const FRIEND_STATUS_SETTLEMENT_TIMEOUT = 60_000;
const USABLE_FRIEND_STATUS_STATES = new Set(['ready', 'offline', 'failed', 'pending']);

function isFriendStatusInjection(response) {
return getInjectedTransaction(response.request())?.type === 'update_toll_required';
}

async function readFriendStatusState(page) {
const modal = page.locator('#friendModal');
const declaredState = await modal.getAttribute('data-status-state');
if (USABLE_FRIEND_STATUS_STATES.has(declaredState)) {
return declaredState;
}

const firstStatusInput = page.locator('#friendForm input[name="friendStatus"]').first();
if (await firstStatusInput.isEnabled()) {
return 'ready';
}

const refreshMessage = page.locator('#friendStatusRefreshMessage');
if (await refreshMessage.isVisible()) {
const message = await refreshMessage.textContent();
if (/offline/i.test(message)) return 'offline';
if (/could not refresh|failed/i.test(message)) return 'failed';
if (/pending/i.test(message)) return 'pending';
}

return 'checking';
}

async function waitForFriendStatusState(page) {
let currentState = 'checking';

await expect.poll(async () => {

Check failure on line 50 in playwright-tests/helpers/friendStatusHelpers.js

View workflow job for this annotation

GitHub Actions / e2e

[chromium] › tests/videoCall.e2e.test.js:229:5 › Video Call Tests › invite flow: sender calls recipient

6) [chromium] › tests/videoCall.e2e.test.js:229:5 › Video Call Tests › invite flow: sender calls recipient, recipient invites two others Error: Contact Status did not finish refreshing expect(received).not.toBe(expected) // Object.is equality Expected: not "checking" Call Log: - Timeout 15000ms exceeded while waiting on the predicate at ../helpers/friendStatusHelpers.js:50 48 | let currentState = 'checking'; 49 | > 50 | await expect.poll(async () => { | ^ 51 | currentState = await readFriendStatusState(page); 52 | return currentState; 53 | }, { at waitForFriendStatusState (/home/runner/work/client-testing/client-testing/playwright-tests/helpers/friendStatusHelpers.js:50:3) at updateFriendStatus (/home/runner/work/client-testing/client-testing/playwright-tests/helpers/friendStatusHelpers.js:148:17) at setFriendStatusInChat (/home/runner/work/client-testing/client-testing/playwright-tests/helpers/friendStatusHelpers.js:194:3) at /home/runner/work/client-testing/client-testing/playwright-tests/tests/videoCall.e2e.test.js:277:21 at /home/runner/work/client-testing/client-testing/playwright-tests/tests/videoCall.e2e.test.js:256:13
currentState = await readFriendStatusState(page);
return currentState;
}, {
message: 'Contact Status did not finish refreshing',
timeout: FRIEND_STATUS_STATE_TIMEOUT,
}).not.toBe('checking');

return currentState;
}

function readStatusFromButtonClass(className) {
const match = className.match(/(?:^|\s)status-([012])(?:\s|$)/);
return match ? Number(match[1]) : null;
}

async function openFriendStatusModal(page, openButton) {
const modal = page.locator('#friendModal');
await openButton.click();

if (await modal.isVisible()) {
return 'open';
}

const pendingToast = page.locator('.toast.warning.show', {
hasText: /pending transaction.*friend status/i,
});
await expect(pendingToast).toBeVisible({ timeout: 5_000 }).catch(() => {
throw new Error('Contact Status did not open and no pending-status warning was shown');
});
return 'pending';
}

async function closeFriendStatusModal(page) {
await page.locator('#closeFriendModal').click();
await expect(page.locator('#friendModal')).not.toHaveClass(/active/);
}

function friendStatusUnavailableError(state, status) {
const requestedStatus = FRIEND_STATUS_LABELS[status] || String(status);
return new Error(`Cannot change Contact Status to ${requestedStatus} while the modal is ${state}`);
}

async function getAcceptedFriendStatusTxid(response, status) {
const result = await response.json();
const success = result?.result?.success ?? result?.success;
if (success !== true) {
const reason = result?.result?.reason || result?.reason || 'unknown reason';
throw new Error(`Contact Status ${FRIEND_STATUS_LABELS[status]} injection failed: ${reason}`);
}

const txid = result?.result?.txId || result?.result?.txid || result?.txId || result?.txid;
if (!txid) {
throw new Error(`Contact Status ${FRIEND_STATUS_LABELS[status]} injection returned no transaction ID`);
}

return txid;
}

async function waitForFriendStatusSettlement(page, status, txid) {
let response;
try {
response = await page.waitForResponse(async (candidate) => {
const url = candidate.url();
const matchesTransaction = url.includes(`/transaction/${txid}`)
|| (url.includes('/collector/api/transaction') && url.includes(`appReceiptId=${txid}`));
if (!matchesTransaction) {
return false;
}

try {
const body = await candidate.json();
return body?.transaction?.success === true || body?.transaction?.success === false;
} catch {
return false;
}
}, { timeout: FRIEND_STATUS_SETTLEMENT_TIMEOUT });
} catch {
throw new Error(`Contact Status ${FRIEND_STATUS_LABELS[status]} did not settle within 60 seconds`);
}

const result = await response.json();
if (result.transaction?.success !== true) {
const reason = result.transaction?.reason || 'unknown reason';
throw new Error(`Contact Status ${FRIEND_STATUS_LABELS[status]} failed to settle: ${reason}`);
}
}

async function updateFriendStatus(page, openButton, status) {
const openResult = await openFriendStatusModal(page, openButton);
if (openResult === 'pending') {
const currentStatus = readStatusFromButtonClass(await openButton.getAttribute('class') || '');
if (currentStatus === status) {
return;
}
throw friendStatusUnavailableError('pending', status);
}

const state = await waitForFriendStatusState(page);
const checkedStatus = Number(await page
.locator('#friendForm input[name="friendStatus"]:checked')
.getAttribute('value'));

if (checkedStatus === status) {
await closeFriendStatusModal(page);
return;
}

if (state !== 'ready') {
throw friendStatusUnavailableError(state, status);
}

const statusInput = page.locator(`#friendForm input[name="friendStatus"][value="${status}"]`);
const submitButton = page.locator('#friendForm button[type="submit"]');
await statusInput.check();
await expect(submitButton).toBeEnabled();

const injectionPromise = page.waitForResponse(isFriendStatusInjection);
await submitButton.click();
const txid = await getAcceptedFriendStatusTxid(await injectionPromise, status);

const settlementPromise = waitForFriendStatusSettlement(page, status, txid);
await expect(page.locator('#friendModal')).not.toHaveClass(/active/);
await settlementPromise;
await expect(openButton).toHaveClass(new RegExp(`\\bstatus-${status}\\b`));
}

async function setFriendStatus(page, username, status) {
Expand All @@ -20,14 +181,8 @@
await expect(page.locator('#contactsScreen.active')).toBeVisible();
await page.locator('#contactsList .chat-name', { hasText: username }).click();
await expect(page.locator('#contactInfoModal.active')).toBeVisible();
await page.locator('#addFriendButtonContactInfo').click();
await expect(page.locator('#friendModal.active')).toBeVisible();
await page.locator(`#friendForm input[type=radio][value="${status}"]`).check();

await Promise.all([
waitForFriendStatusTransaction(page),
page.locator('#friendForm button[type="submit"]').click(),
]);
await updateFriendStatus(page, page.locator('#addFriendButtonContactInfo'), status);

await page.locator('#closeContactInfoModal').click();
await expect(page.locator('#contactInfoModal')).not.toHaveClass(/active/);
Expand All @@ -36,31 +191,23 @@
async function setFriendStatusInChat(page, status) {
// Use the chat header button when the caller already has the relevant chat
// modal open and wants to keep working in that conversation.
await page.locator('#addFriendButtonChat').click();
await expect(page.locator('#friendModal.active')).toBeVisible();
await page.locator(`#friendForm input[type=radio][value="${status}"]`).check();

await Promise.all([
waitForFriendStatusTransaction(page),
page.locator('#friendForm button[type="submit"]').click(),
]);

await expect(page.locator('#friendModal')).not.toHaveClass(/active/);
await updateFriendStatus(page, page.locator('#addFriendButtonChat'), status);
}

async function getCurrentFriendStatus(page, username) {
// Open the friend modal just long enough to read the selected radio value,
// Open the friend modal just long enough to read the refreshed radio value,
// then return the page to its previous modal-free state.
await page.locator('#switchToContacts').click();
await expect(page.locator('#contactsScreen.active')).toBeVisible();
await page.locator('#contactsList .chat-name', { hasText: username }).click();
await expect(page.locator('#contactInfoModal.active')).toBeVisible();
await page.locator('#addFriendButtonContactInfo').click();
await expect(page.locator('#friendModal.active')).toBeVisible();
await waitForFriendStatusState(page);

const checked = await page.locator('#friendForm input[type=radio]:checked').getAttribute('value');
const checked = await page.locator('#friendForm input[name="friendStatus"]:checked').getAttribute('value');

await page.locator('#closeFriendModal').click();
await closeFriendStatusModal(page);
await page.locator('#closeContactInfoModal').click();
await expect(page.locator('#contactInfoModal')).not.toHaveClass(/active/);

Expand Down
14 changes: 14 additions & 0 deletions playwright-tests/helpers/injectHelpers.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
function getInjectedTransaction(request) {
if (request.method() !== 'POST' || !request.url().endsWith('/inject')) {
return null;
}

try {
const body = request.postDataJSON();
return typeof body.tx === 'string' ? JSON.parse(body.tx) : body.tx;
} catch {
return null;
}
}

async function holdNextInject(page, responseBody) {
await page.evaluate((mockResponseBody) => {
if (!window.__injectMockOriginalFetch) {
Expand Down Expand Up @@ -84,5 +97,6 @@ function failedInjectResponse(reason = 'forced_inject_failure') {

module.exports = {
failedInjectResponse,
getInjectedTransaction,
holdNextInject,
};
10 changes: 8 additions & 2 deletions playwright-tests/helpers/localStorageHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ function getMessagesBetweenUsers(localStorageObj, ownerUsername, contactUsername
return [];
}

function getUserAuthoredMessagesBetweenUsers(localStorageObj, ownerUsername, contactUsername) {
return getMessagesBetweenUsers(localStorageObj, ownerUsername, contactUsername)
.filter(message => message.type !== 'update_toll_required' && typeof message.message === 'string');
}

module.exports = {
getLocalStorage,
getNetidFromAccounts,
Expand All @@ -150,5 +155,6 @@ module.exports = {
getUserContactMessages,
countUserMessages,
findContactByUsername,
getMessagesBetweenUsers
};
getMessagesBetweenUsers,
getUserAuthoredMessagesBetweenUsers
};
39 changes: 38 additions & 1 deletion playwright-tests/helpers/userHelpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

async function createUser(page, username) {
await page.goto('', { waitUntil: 'networkidle' });
await expect(page.locator('#welcomeScreen')).toBeVisible();

Check failure on line 5 in playwright-tests/helpers/userHelpers.js

View workflow job for this annotation

GitHub Actions / e2e

[chromium] › tests/reactions.e2e.test.js:161:3 › Message reactions › expanded emoji picker sets a custom reaction and surfaces it as active

2) [chromium] › tests/reactions.e2e.test.js:161:3 › Message reactions › expanded emoji picker sets a custom reaction and surfaces it as active Error: Timed out 5000ms waiting for expect(locator).toBeVisible() Locator: locator('#welcomeScreen') Expected: visible Received: hidden Call log: - expect.toBeVisible with timeout 5000ms - waiting for locator('#welcomeScreen') 9 × locator resolved to <div id="welcomeScreen" class="welcome-screen is-booting">…</div> - unexpected value "hidden" at ../helpers/userHelpers.js:5 3 | async function createUser(page, username) { 4 | await page.goto('', { waitUntil: 'networkidle' }); > 5 | await expect(page.locator('#welcomeScreen')).toBeVisible(); | ^ 6 | await expect(page.locator('#createAccountButton')).toBeVisible(); 7 | await page.click('#createAccountButton'); 8 | await expect(page.locator('#createAccountModal')).toBeVisible(); at createUser (/home/runner/work/client-testing/client-testing/playwright-tests/helpers/userHelpers.js:5:50) at createAndSignInUser (/home/runner/work/client-testing/client-testing/playwright-tests/helpers/userHelpers.js:18:5) at Object.users (/home/runner/work/client-testing/client-testing/playwright-tests/tests/reactions.e2e.test.js:31:7)

Check failure on line 5 in playwright-tests/helpers/userHelpers.js

View workflow job for this annotation

GitHub Actions / e2e

[chromium] › tests/friendStatus.e2e.test.js:297:5 › Friend Status E2E › Stale Connection -> Blocked: rejected send refreshes local block

1) [chromium] › tests/friendStatus.e2e.test.js:297:5 › Friend Status E2E › Stale Connection -> Blocked: rejected send refreshes local block Error: Timed out 5000ms waiting for expect(locator).toBeVisible() Locator: locator('#welcomeScreen') Expected: visible Received: hidden Call log: - expect.toBeVisible with timeout 5000ms - waiting for locator('#welcomeScreen') 9 × locator resolved to <div id="welcomeScreen" class="welcome-screen is-booting">…</div> - unexpected value "hidden" at ../helpers/userHelpers.js:5 3 | async function createUser(page, username) { 4 | await page.goto('', { waitUntil: 'networkidle' }); > 5 | await expect(page.locator('#welcomeScreen')).toBeVisible(); | ^ 6 | await expect(page.locator('#createAccountButton')).toBeVisible(); 7 | await page.click('#createAccountButton'); 8 | await expect(page.locator('#createAccountModal')).toBeVisible(); at createUser (/home/runner/work/client-testing/client-testing/playwright-tests/helpers/userHelpers.js:5:50) at createAndSignInUser (/home/runner/work/client-testing/client-testing/playwright-tests/helpers/userHelpers.js:18:5) at Object.users (/home/runner/work/client-testing/client-testing/playwright-tests/tests/friendStatus.e2e.test.js:79:13)
await expect(page.locator('#createAccountButton')).toBeVisible();
await page.click('#createAccountButton');
await expect(page.locator('#createAccountModal')).toBeVisible();
Expand All @@ -25,6 +25,41 @@
await expect(appName.trim()).toBe(username);
}

function signInAccountCard(page, username) {
return page.locator(`#signInAccountList .sign-in-account-item[data-username="${username}"]`);
}

async function unlockDevice(page, password) {
const unlockModal = page.locator('#unlockModal.active');
await expect(unlockModal).toBeVisible();
await unlockModal.locator('#password').fill(password);
await unlockModal.locator('#unlockForm button[type="submit"]').click();
await expect(unlockModal).not.toBeVisible();
}

async function signInWithAccountCard(page, username, options = {}) {
const { expectedAccountUsernames = [username], lockPassword = '' } = options;

await expect(page.locator('#welcomeScreen')).toBeVisible();
const signInButton = page.locator('#signInButton');
await expect(signInButton).toBeVisible();
await signInButton.click();

if (lockPassword) {
await unlockDevice(page, lockPassword);
}

if (expectedAccountUsernames.length > 1) {
for (const accountUsername of expectedAccountUsernames) {
await expect(signInAccountCard(page, accountUsername)).toBeVisible();
}
await signInAccountCard(page, username).click();
}

await expect(page.locator('#chatsScreen.active')).toBeVisible({ timeout: 20_000 });
await expect(page.locator('.app-name')).toHaveText(username);
}

// creates a unique username based on the browser name and current timestamp
function generateUsername(browserName) {
const browserInitial = browserName[0];
Expand All @@ -36,5 +71,7 @@
module.exports = {
createAndSignInUser,
generateUsername,
createUser
createUser,
signInWithAccountCard,
unlockDevice
};
18 changes: 12 additions & 6 deletions playwright-tests/tests/attachments.e2e.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

function waitForNamedDownload(page, expectedFileName) {
return page.waitForEvent('download', {
predicate: download => download.suggestedFilename() === expectedFileName
});
}

// Helper to create test files with unique names based on test info to avoid conflicts in parallel runs
async function createTestFile(baseFileName, sizeInMB = 0.5, type = 'image/png', uniqueId) {
// Generate a unique filename by adding the unique ID before the extension
Expand Down Expand Up @@ -280,8 +286,8 @@ test.describe('File Attachment Tests', () => {
const fileName = fileInfo.fileName;
const testFilePath = fileInfo.filePath;

// Start waiting for download before clicking
const downloadPromise = page.waitForEvent('download');
// Start waiting for the named download before clicking
const downloadPromise = waitForNamedDownload(page, fileName);

// Open New Chat
await page.click('#newChatButton');
Expand Down Expand Up @@ -392,8 +398,8 @@ test.describe('File Attachment Tests', () => {
const recipientPage = await testRecipient.context.newPage();

try {
// Start waiting for download before clicking
const downloadPromise = recipientPage.waitForEvent('download');
// Start waiting for the named download before clicking
const downloadPromise = waitForNamedDownload(recipientPage, fileName);

// Sign in as recipient
await recipientPage.goto('');
Expand Down Expand Up @@ -548,7 +554,7 @@ test.describe('File Attachment Tests', () => {
await expect(attachmentLink).toBeVisible({ timeout: 15000 });

// Set up download listener for this specific attachment
const downloadPromise = recipientPage.waitForEvent('download');
const downloadPromise = waitForNamedDownload(recipientPage, attachment.fileName);

// Click to download
await attachmentLink.click();
Expand Down Expand Up @@ -692,7 +698,7 @@ test.describe('File Attachment Tests', () => {
await expect(receivedAttachment).toHaveText(fileName);

// Test download on third user's side
const downloadPromise = thirdUserPage.waitForEvent('download');
const downloadPromise = waitForNamedDownload(thirdUserPage, fileName);
await receivedAttachment.click();
await expect(thirdUserPage.locator('#imageAttachmentContextMenu .context-menu-option[data-action="save"]')).toBeVisible();
await thirdUserPage.click('#imageAttachmentContextMenu .context-menu-option[data-action="save"]');
Expand Down
Loading
Loading