diff --git a/playwright-tests/helpers/friendStatusHelpers.js b/playwright-tests/helpers/friendStatusHelpers.js index f99759f..599a654 100644 --- a/playwright-tests/helpers/friendStatusHelpers.js +++ b/playwright-tests/helpers/friendStatusHelpers.js @@ -1,4 +1,5 @@ const { expect } = require('@playwright/test'); +const { getInjectedTransaction } = require('./injectHelpers'); const FriendStatus = Object.freeze({ BLOCKED: 0, @@ -6,11 +7,171 @@ const FriendStatus = Object.freeze({ 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 () => { + 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) { @@ -20,14 +181,8 @@ async function setFriendStatus(page, username, status) { 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/); @@ -36,20 +191,11 @@ async function setFriendStatus(page, username, status) { 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(); @@ -57,10 +203,11 @@ async function getCurrentFriendStatus(page, username) { 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/); diff --git a/playwright-tests/helpers/injectHelpers.js b/playwright-tests/helpers/injectHelpers.js index 15b9420..65dcbdd 100644 --- a/playwright-tests/helpers/injectHelpers.js +++ b/playwright-tests/helpers/injectHelpers.js @@ -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) { @@ -84,5 +97,6 @@ function failedInjectResponse(reason = 'forced_inject_failure') { module.exports = { failedInjectResponse, + getInjectedTransaction, holdNextInject, }; diff --git a/playwright-tests/helpers/localStorageHelpers.js b/playwright-tests/helpers/localStorageHelpers.js index 4d84577..f6397c8 100644 --- a/playwright-tests/helpers/localStorageHelpers.js +++ b/playwright-tests/helpers/localStorageHelpers.js @@ -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, @@ -150,5 +155,6 @@ module.exports = { getUserContactMessages, countUserMessages, findContactByUsername, - getMessagesBetweenUsers -}; \ No newline at end of file + getMessagesBetweenUsers, + getUserAuthoredMessagesBetweenUsers +}; diff --git a/playwright-tests/helpers/userHelpers.js b/playwright-tests/helpers/userHelpers.js index 3601101..bf68d21 100644 --- a/playwright-tests/helpers/userHelpers.js +++ b/playwright-tests/helpers/userHelpers.js @@ -25,6 +25,41 @@ async function createAndSignInUser(page, username) { 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]; @@ -36,5 +71,7 @@ function generateUsername(browserName) { module.exports = { createAndSignInUser, generateUsername, - createUser + createUser, + signInWithAccountCard, + unlockDevice }; diff --git a/playwright-tests/tests/attachments.e2e.test.js b/playwright-tests/tests/attachments.e2e.test.js index a185b97..f9689cb 100644 --- a/playwright-tests/tests/attachments.e2e.test.js +++ b/playwright-tests/tests/attachments.e2e.test.js @@ -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 @@ -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'); @@ -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(''); @@ -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(); @@ -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"]'); diff --git a/playwright-tests/tests/backup.e2e.test.js b/playwright-tests/tests/backup.e2e.test.js index 3f55295..6cc404d 100644 --- a/playwright-tests/tests/backup.e2e.test.js +++ b/playwright-tests/tests/backup.e2e.test.js @@ -1,5 +1,10 @@ const { test, expect } = require('../fixtures/base'); -const { createAndSignInUser, generateUsername } = require('../helpers/userHelpers'); +const { + createAndSignInUser, + generateUsername, + signInWithAccountCard, + unlockDevice +} = require('../helpers/userHelpers'); const { newContext: createContext } = require('../helpers/toastHelpers'); const path = require('path'); @@ -20,22 +25,52 @@ async function backupAccount(page, backupFilePath, password = '') { await download.saveAs(backupFilePath); } -async function restoreAccount(page, backupFilePath, password = '') { +async function waitForRestoreReload(page) { + const successToast = page.locator('.toast.success.show', { hasText: /\d+ accounts? restored/ }); + await expect(successToast).toBeVisible({ timeout: 15_000 }); + + const navigationPromise = page.waitForNavigation({ waitUntil: 'domcontentloaded' }); + await successToast.locator('.toast-close-btn').click(); + await navigationPromise; + await expect(page.locator('#welcomeScreen')).toBeVisible(); +} + +async function submitRestore(page) { + page.once('dialog', dialog => dialog.accept()); + await page.click('#importForm button[type="submit"]'); + await waitForRestoreReload(page); +} + +async function restoreAccount(page, backupFilePath, options = {}) { + const { + backupPassword = '', + backupLock = '', + deviceLockPassword = '', + overwrite = false + } = options; + await page.goto(''); await expect(page.locator('#welcomeScreen')).toBeVisible(); - await page.click("#openWelcomeMenu") + await page.click('#openWelcomeMenu'); + if (deviceLockPassword) { + await unlockDevice(page, deviceLockPassword); + } await page.click('#welcomeOpenRestore'); await expect(page.locator('#importModal')).toBeVisible(); await page.setInputFiles('#importFile', backupFilePath); - if (password) { - await page.fill('#importPassword', password); + if (backupPassword) { + await page.fill('#importPassword', backupPassword); } - await page.on('dialog', async dialog => { - await dialog.accept(); - }); - await page.click('#importForm button[type="submit"]'); + if (backupLock) { + await page.fill('#backupAccountLock', backupLock); + } + if (overwrite) { + await page.check('#overwriteAccountsCheckbox'); + } + + await submitRestore(page); } async function setLock(page, password) { @@ -82,45 +117,32 @@ async function getProfileName(page) { test.describe('Backup and Restore Scenarios', () => { test.describe('Single Account Basic', () => { let username; - let backupFilePath; test.beforeEach(async ({ browserName }) => { username = generateUsername(browserName); }); test('backup & restore without password', async ({ page, browser }, testInfo) => { - backupFilePath = testInfo.outputPath(path.join('backups', `${username}-no-password.json`)); + const backupFilePath = testInfo.outputPath(path.join('backups', `${username}-no-password.json`)); await createAndSignInUser(page, username); await backupAccount(page, backupFilePath); await page.context().close(); const newContext = await createContext(browser); try { const newPage = await newContext.newPage(); - await newPage.goto(''); - await expect(newPage.locator('#welcomeScreen')).toBeVisible();; - await expect(newPage.locator('#signInButton')).not.toBeVisible(); await restoreAccount(newPage, backupFilePath); - await expect(newPage.locator('#welcomeScreen')).toBeVisible(); - await newPage.click('#signInButton'); - await expect(newPage.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); - await expect(newPage.locator('.app-name')).toHaveText(username); + await signInWithAccountCard(newPage, username); } finally { await newContext.close(); } }); test('backup & restore with password', async ({ page, browser }, testInfo) => { const password = 'supersecretpassword123'; - backupFilePath = testInfo.outputPath(path.join('backups', `${username}-with-password.json`)); + const backupFilePath = testInfo.outputPath(path.join('backups', `${username}-with-password.json`)); await createAndSignInUser(page, username); await backupAccount(page, backupFilePath, password); await page.context().close(); const newContext = await createContext(browser); try { const newPage = await newContext.newPage(); - await newPage.goto(''); - await expect(newPage.locator('#welcomeScreen')).toBeVisible(); - await expect(newPage.locator('#signInButton')).not.toBeVisible(); - await restoreAccount(newPage, backupFilePath, password); - await expect(newPage.locator('#welcomeScreen')).toBeVisible(); - await newPage.click('#signInButton'); - await expect(newPage.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); - await expect(newPage.locator('.app-name')).toHaveText(username); + await restoreAccount(newPage, backupFilePath, { backupPassword: password }); + await signInWithAccountCard(newPage, username); } finally { await newContext.close(); } }); @@ -175,16 +197,10 @@ test.describe('Backup and Restore Scenarios', () => { const newContext = await createContext(browser); try { const newPage = await newContext.newPage(); - await restoreAccount(newPage, backupFilePath, password); - await expect(newPage.locator('#signInButton')).toBeVisible(); - await newPage.click('#signInButton'); - const userDropdown = newPage.locator('#username'); - await expect(userDropdown).toContainText(username1); - await expect(userDropdown).toContainText(username2); - await userDropdown.selectOption(username1); - await newPage.click('#signInForm button[type="submit"]'); - await expect(newPage.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); - await expect(newPage.locator('.app-name')).toHaveText(username1); + await restoreAccount(newPage, backupFilePath, { backupPassword: password }); + await signInWithAccountCard(newPage, username1, { + expectedAccountUsernames: [username1, username2] + }); } finally { await newContext.close(); } }); }); @@ -201,11 +217,9 @@ test.describe('Backup and Restore Scenarios', () => { await createAndSignInUser(page, username2); await page.goto(''); await expect(page.locator('#welcomeScreen')).toBeVisible(); - await page.click('#signInButton'); - const userDropdown = page.locator('#username'); - await userDropdown.selectOption(username1); - await page.click('#signInForm button[type="submit"]'); - await expect(page.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); + await signInWithAccountCard(page, username1, { + expectedAccountUsernames: [username1, username2] + }); await page.click('#toggleSettings'); await expect(page.locator('#settingsModal')).toBeVisible(); await page.click('#openBackupForm'); @@ -222,23 +236,16 @@ test.describe('Backup and Restore Scenarios', () => { const newContext = await createContext(browser); try { const newPage = await newContext.newPage(); - await restoreAccount(newPage, backupFilePath, password); - await expect(newPage.locator('#welcomeScreen')).toBeVisible(); - await newPage.click('#signInButton'); - const restoreUserDropdown = newPage.locator('#username'); - await expect(restoreUserDropdown).toContainText(username1); - await expect(restoreUserDropdown).toContainText(username2); - await restoreUserDropdown.selectOption(username1); - await newPage.click('#signInForm button[type="submit"]'); - await expect(newPage.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); - await expect(newPage.locator('.app-name')).toHaveText(username1); + const restoredUsernames = [username1, username2]; + await restoreAccount(newPage, backupFilePath, { backupPassword: password }); + await signInWithAccountCard(newPage, username1, { + expectedAccountUsernames: restoredUsernames + }); await newPage.goto(''); await expect(newPage.locator('#welcomeScreen')).toBeVisible(); - await newPage.click('#signInButton'); - await restoreUserDropdown.selectOption(username2); - await newPage.click('#signInForm button[type="submit"]'); - await expect(newPage.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); - await expect(newPage.locator('.app-name')).toHaveText(username2); + await signInWithAccountCard(newPage, username2, { + expectedAccountUsernames: restoredUsernames + }); } finally { await newContext.close(); } }); }); @@ -264,19 +271,8 @@ test.describe('Backup and Restore Scenarios', () => { const restoreCtx = await createContext(browser); try { const restorePage = await restoreCtx.newPage(); - await restorePage.goto(''); - await expect(restorePage.locator('#welcomeScreen')).toBeVisible(); - await restorePage.click('#openWelcomeMenu'); - await restorePage.click('#welcomeOpenRestore'); - await expect(restorePage.locator('#importModal')).toBeVisible(); - await restorePage.setInputFiles('#importFile', backupFilePath); - await restorePage.fill('#backupAccountLock', lockPassword); - restorePage.on('dialog', dialog => dialog.accept()); - await restorePage.click('#importForm button[type="submit"]'); - await expect(restorePage.locator('#welcomeScreen')).toBeVisible(); - await restorePage.click('#signInButton'); - await expect(restorePage.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); - await expect(restorePage.locator('.app-name')).toHaveText(username); + await restoreAccount(restorePage, backupFilePath, { backupLock: lockPassword }); + await signInWithAccountCard(restorePage, username); } finally { await restoreCtx.close(); } }); @@ -299,27 +295,13 @@ test.describe('Backup and Restore Scenarios', () => { const username2 = generateUsername(browserName); await createAndSignInUser(restorePage, username2); await setLock(restorePage, lockPassword); - await restorePage.goto(''); - await expect(restorePage.locator('#welcomeScreen')).toBeVisible(); - await restorePage.click('#openWelcomeMenu'); - await restorePage.fill('#password', lockPassword); - await restorePage.click('#unlockForm button[type="submit"]'); - await restorePage.click('#welcomeOpenRestore'); - await expect(restorePage.locator('#importModal')).toBeVisible(); - await restorePage.setInputFiles('#importFile', backupFilePath); - restorePage.on('dialog', dialog => dialog.accept()); - await restorePage.click('#importForm button[type="submit"]'); - await expect(restorePage.locator('#welcomeScreen')).toBeVisible(); - await restorePage.click('#signInButton'); - await restorePage.fill('#password', lockPassword); - await restorePage.click('#unlockForm button[type="submit"]'); - const restoreUserDropdown = restorePage.locator('#username'); - await expect(restoreUserDropdown).toContainText(username1); - await expect(restoreUserDropdown).toContainText(username2); - await restoreUserDropdown.selectOption(username1); - await restorePage.click('#signInForm button[type="submit"]'); - await expect(restorePage.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); - await expect(restorePage.locator('.app-name')).toHaveText(username1); + await restoreAccount(restorePage, backupFilePath, { + deviceLockPassword: lockPassword + }); + await signInWithAccountCard(restorePage, username1, { + expectedAccountUsernames: [username1, username2], + lockPassword + }); } finally { await restoreCtx.close(); } }); @@ -368,34 +350,16 @@ test.describe('Backup and Restore Scenarios', () => { await page.click('#lockForm button[type="submit"]'); await expect(page.locator('.toast.success.show')).toBeVisible({ timeout: 15_000 }); - // Sign out await page.click('#handleSignOutSettings'); await expect(page.locator('#welcomeScreen')).toBeVisible(); - // Start restore flow WITH overwrite using original lock - await page.click('#openWelcomeMenu'); - await expect(page.locator('#unlockModal.active')).toBeVisible(); - await page.fill('#password', newLock); - await page.click('#unlockForm button[type="submit"]'); - await page.click('#welcomeOpenRestore'); - await expect(page.locator('#importModal.active')).toBeVisible(); - await page.setInputFiles('#importFile', backupFilePath); - if (backupPassword) { - await page.fill('#importPassword', backupPassword); - } - await page.fill('#backupAccountLock', originalLock); - await page.check('#overwriteAccountsCheckbox'); - page.on('dialog', dialog => dialog.accept()); - await page.click('#importForm button[type="submit"]'); - await expect(page.locator('#welcomeScreen')).toBeVisible(); - - // Sign in (should succeed with restored account) - await page.click('#signInButton'); - await expect(page.locator('#unlockModal.active')).toBeVisible(); - await page.fill('#password', newLock); - await page.click('#unlockForm button[type="submit"]'); - await expect(page.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); - await expect(page.locator('.app-name')).toHaveText(username); + await restoreAccount(page, backupFilePath, { + backupLock: originalLock, + backupPassword, + deviceLockPassword: newLock, + overwrite: true + }); + await signInWithAccountCard(page, username, { lockPassword: newLock }); } finally { await ctx.close(); } @@ -427,24 +391,14 @@ test.describe('Backup and Restore Scenarios', () => { await setLock(pageB, lockB); await pageB.click('#handleSignOutSettings'); await expect(pageB.locator('#welcomeScreen')).toBeVisible(); - await pageB.click('#openWelcomeMenu'); - await pageB.fill('#password', lockB); - await pageB.click('#unlockForm button[type="submit"]'); - await pageB.click('#welcomeOpenRestore'); - await expect(pageB.locator('#importModal')).toBeVisible(); - await pageB.waitForTimeout(1000); - await pageB.setInputFiles('#importFile', backupFilePath); - await pageB.fill('#backupAccountLock', lockA); - pageB.on('dialog', dialog => dialog.accept()); - await pageB.click('#importForm button[type="submit"]'); - await expect(pageB.locator('#welcomeScreen')).toBeVisible(); - await pageB.click('#signInButton'); - await expect(pageB.locator('#unlockModal.active')).toBeVisible(); - await pageB.fill('#password', lockB); - await pageB.click('#unlockForm button[type="submit"]'); - const dropdown = pageB.locator('#username'); - await expect(dropdown).toContainText(usernameA); - await expect(dropdown).toContainText(existingUsername); + await restoreAccount(pageB, backupFilePath, { + backupLock: lockA, + deviceLockPassword: lockB + }); + await signInWithAccountCard(pageB, usernameA, { + expectedAccountUsernames: [usernameA, existingUsername], + lockPassword: lockB + }); } finally { await ctxB.close(); } }); }); @@ -471,20 +425,13 @@ test.describe('Backup and Restore Scenarios', () => { await page.click('#backupForm button[type="submit"]'); const download = await dl1; await download.saveAs(backupFilePath); await page.click('#closeWelcomeMenu'); - await page.click('#signInButton'); - await expect(page.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); + await signInWithAccountCard(page, username); await updateProfileName(page, modifiedName); await page.click('#toggleSettings'); await page.click('#handleSignOutSettings'); await expect(page.locator('#welcomeScreen')).toBeVisible(); - await page.click('#openWelcomeMenu'); - await page.click('#welcomeOpenRestore'); - await page.setInputFiles('#importFile', backupFilePath); - page.on('dialog', dialog => dialog.accept()); - await page.click('#importForm button[type="submit"]'); - await expect(page.locator('#welcomeScreen')).toBeVisible(); - await page.click('#signInButton'); - await expect(page.locator('#chatsScreen.active')).toBeVisible(); + await restoreAccount(page, backupFilePath); + await signInWithAccountCard(page, username); const current = await getProfileName(page); expect(current).toBe(modifiedName); } finally { await ctx.close(); } }); @@ -507,23 +454,15 @@ test.describe('Backup and Restore Scenarios', () => { await page.click('#backupForm button[type="submit"]'); const download = await dl1; await download.saveAs(backupFilePath); await page.click('#closeWelcomeMenu'); - await page.click('#signInButton'); - await expect(page.locator('#chatsScreen.active')).toBeVisible({ timeout: 15_000 }); + await signInWithAccountCard(page, username); await updateProfileName(page, modifiedName); await page.click('#toggleSettings'); await page.click('#handleSignOutSettings'); await expect(page.locator('#welcomeScreen')).toBeVisible(); - await page.click('#openWelcomeMenu'); - await page.click('#welcomeOpenRestore'); - await page.setInputFiles('#importFile', backupFilePath); - await page.check('#overwriteAccountsCheckbox'); - page.on('dialog', dialog => dialog.accept()); - await page.click('#importForm button[type="submit"]'); - await expect(page.locator('#welcomeScreen')).toBeVisible(); - await page.click('#signInButton'); - await expect(page.locator('#chatsScreen.active')).toBeVisible(); + await restoreAccount(page, backupFilePath, { overwrite: true }); + await signInWithAccountCard(page, username); const current = await getProfileName(page); expect(current).toBe(originalName); } finally { await ctx.close(); } }); }); -}); \ No newline at end of file +}); diff --git a/playwright-tests/tests/friendStatus.e2e.test.js b/playwright-tests/tests/friendStatus.e2e.test.js index 54b2c34..7d5a5f7 100644 --- a/playwright-tests/tests/friendStatus.e2e.test.js +++ b/playwright-tests/tests/friendStatus.e2e.test.js @@ -2,6 +2,7 @@ const { test: base, expect } = require('../fixtures/base'); const { createAndSignInUser, generateUsername } = require('../helpers/userHelpers'); const { getLiberdusBalance } = require('../helpers/walletHelpers'); const { FriendStatus, getCurrentFriendStatus, setFriendStatus } = require('../helpers/friendStatusHelpers'); +const { getInjectedTransaction } = require('../helpers/injectHelpers'); const { sendMessageTo } = require('../helpers/messageHelpers'); const networkParams = require('../helpers/networkParams'); const { newContext } = require('../helpers/toastHelpers'); @@ -15,6 +16,8 @@ const tollStr = tollWei.toString().padStart(19, '0'); const TOLL = (tollStr.slice(0, -18) || '0') + '.' + tollStr.slice(-18); const TOLL_NUM = Number(tollWei) / 1e18; const DEFAULT_TOLL = networkParams.defaultTollLib; +const CHAT_HISTORY_SYNC_URL = /\/account\/[^/]+\/chats\/\d+(?:\?|$)/; +const RECIPIENT_TOLL_STATE_URL = /\/messages\/[^/]+\/toll(?:\?|$)/; async function setToll(page, amount) { await page.click('#toggleSettings'); @@ -33,6 +36,28 @@ async function setToll(page, amount) { await page.click('#closeSettings'); } +async function openChatAndWaitForRecipientState(page, username) { + await page.click('#switchToChats'); + + const tollStateResponsePromise = page.waitForResponse(response => + response.ok() && RECIPIENT_TOLL_STATE_URL.test(response.url()) + ); + + await page.locator('#chatList .chat-name', { hasText: username }).click(); + await expect(page.locator('#chatModal')).toBeVisible(); + + const tollStateResponse = await tollStateResponsePromise; + await tollStateResponse.finished(); +} + +async function pauseChatHistorySync(page) { + await page.route(CHAT_HISTORY_SYNC_URL, route => route.abort()); +} + +async function resumeChatHistorySync(page) { + await page.unroute(CHAT_HISTORY_SYNC_URL); +} + const test = base.extend({ users: async ({ browser, browserName }, use, testInfo) => { const ctxA = await newContext(browser); @@ -93,22 +118,42 @@ test.describe('Friend Status E2E', () => { expect(checkedB).toBe(FriendStatus.CONNECTION); }); - test('Block: User A blocks User B, B cannot message', async ({ users }) => { + test('Setting the current friend status does not inject a transaction', async ({ users }) => { const { a, b } = users; + const friendStatusInjections = []; + const captureFriendStatusInjection = (request) => { + const transaction = getInjectedTransaction(request); + if (transaction?.type === 'update_toll_required') { + friendStatusInjections.push(transaction); + } + }; + + a.page.on('request', captureFriendStatusInjection); + try { + await setFriendStatus(a.page, b.username, FriendStatus.OTHER); + } finally { + a.page.off('request', captureFriendStatusInjection); + } + + expect(friendStatusInjections).toHaveLength(0); + expect(await getCurrentFriendStatus(a.page, b.username)).toBe(FriendStatus.OTHER); + }); + + test('Block: known blocked state rejects without creating a message', async ({ users }) => { + const { a, b } = users; + const message = 'known blocked message'; // User A blocks User B await setFriendStatus(a.page, b.username, FriendStatus.BLOCKED); - // User B should not be able to send a message - // go to contacts tab and back to refresh chat list - await b.page.click('#switchToChats'); - await b.page.locator('#chatList .chat-name', { hasText: a.username }).click(); - await expect(b.page.locator('#chatModal')).toBeVisible(); + // Open the chat and wait until User B has refreshed User A's blocked state. + await openChatAndWaitForRecipientState(b.page, a.username); await expect(b.page.locator('#tollValue')).toHaveText('blocked'); - await b.page.locator('#chatModal .message-input').fill('blocked msg'); + await b.page.locator('#chatModal .message-input').fill(message); await b.page.click('#handleSendMessage'); - // expect an error toast to appear ignore inner text + await expect(b.page.locator('.toast.error.show', { hasText: /You are blocked by this user/i })).toBeVisible({ timeout: 10_000 }); + await expect(b.page.locator('.message.sent', { hasText: message })).toHaveCount(0); }); test('Block: User A blocks User B, B cannot send money', async ({ users }) => { @@ -217,44 +262,67 @@ test.describe('Friend Status E2E', () => { await expect(b.page.locator("#contactInfoX")).toHaveText(x); }); - test('Connection -> Other: Message fails if status changed to require toll', async ({ users }) => { + test('Stale Connection -> Other: rejected send refreshes toll for retry', async ({ users }) => { const { a, b } = users; + const staleMessage = 'stale toll message'; + const retryMessage = 'message with refreshed toll'; - // User A opens chat with B and types a message but does not send - await a.page.click('#switchToChats'); - await a.page.locator('#chatList .chat-name', { hasText: b.username }).click(); - await expect(a.page.locator('#chatModal')).toBeVisible(); - await a.page.fill('#chatModal .message-input', 'pending message'); + // User A opens the chat while the cached recipient state is toll-free. + await openChatAndWaitForRecipientState(a.page, b.username); + await expect(a.page.locator('#tollLabel')).toHaveText('Toll free:'); + await a.page.fill('#chatModal .message-input', staleMessage); + await pauseChatHistorySync(a.page); - // User B sets User A's status to OTHER + // User B changes the status while User A's chat remains open. await setFriendStatus(b.page, a.username, FriendStatus.OTHER); - // User A tries to send the message await a.page.click('#handleSendMessage'); - // Expect an error toast to appear for User A - await expect(a.page.locator('.toast.error.show', { hasText: 'toll' })).toBeVisible({ timeout: 15_000 }); + const tollError = a.page.locator('.toast.error.show', { hasText: 'toll' }); + await expect(tollError).toBeVisible({ timeout: 15_000 }); + await expect(a.page.locator('.message.sent', { hasText: staleMessage })).toHaveAttribute('data-status', 'failed'); + await expect(a.page.locator('#tollValue')).toContainText(networkParams.defaultTollUsd.toFixed(6)); + await resumeChatHistorySync(a.page); + + // The refreshed toll state is used for the next transaction. + await tollError.locator('.toast-close-btn').click(); + await a.page.fill('#chatModal .message-input', retryMessage); + await a.page.click('#handleSendMessage'); + + await b.page.click('#switchToChats'); + await b.page.locator('#chatList .chat-name', { hasText: a.username }).click(); + await expect(b.page.locator('.message.received .message-content', { hasText: retryMessage })).toBeVisible({ timeout: 30_000 }); }); - test('Connection -> Blocked: Message fails if blocked', async ({ users }) => { + test('Stale Connection -> Blocked: rejected send refreshes local block', async ({ users }) => { const { a, b } = users; + const staleMessage = 'stale blocked message'; + const blockedRetryMessage = 'message after blocked refresh'; - // User A opens chat with B and types a message but does not send - await a.page.click('#switchToChats'); - await a.page.locator('#chatList .chat-name', { hasText: b.username }).click(); - await expect(a.page.locator('#chatModal')).toBeVisible(); - await a.page.fill('#chatModal .message-input', 'pending message'); + // User A opens the chat while the cached recipient state is toll-free. + await openChatAndWaitForRecipientState(a.page, b.username); + await expect(a.page.locator('#tollLabel')).toHaveText('Toll free:'); + await a.page.fill('#chatModal .message-input', staleMessage); + await pauseChatHistorySync(a.page); - // User B sets User A's status to BLOCKED + // User B changes the status while User A's chat remains open. await setFriendStatus(b.page, a.username, FriendStatus.BLOCKED); - // User A tries to send the message await a.page.click('#handleSendMessage'); - // Expect an error toast to appear for User A - await expect(a.page.locator('.toast.error.show', { hasText: 'blocked' })).toBeVisible({ timeout: 15_000 }); - // Check that the message is marked as failed - await expect(a.page.locator('.message.sent', { hasText: 'pending message' })).toHaveAttribute('data-status', 'failed'); + const blockedError = a.page.locator('.toast.error.show', { hasText: 'blocked' }); + await expect(blockedError).toBeVisible({ timeout: 15_000 }); + await expect(a.page.locator('.message.sent', { hasText: staleMessage })).toHaveAttribute('data-status', 'failed'); + await expect(a.page.locator('#tollValue')).toHaveText('blocked'); + await resumeChatHistorySync(a.page); + + // The refreshed block is enforced locally without another optimistic message. + await blockedError.locator('.toast-close-btn').click(); + await a.page.fill('#chatModal .message-input', blockedRetryMessage); + await a.page.click('#handleSendMessage'); + + await expect(a.page.locator('.toast.error.show', { hasText: 'blocked' })).toBeVisible({ timeout: 10_000 }); + await expect(a.page.locator('.message.sent', { hasText: blockedRetryMessage })).toHaveCount(0); }); test('Send LIB: status changed to OTHER before submit, error and form persists', async ({ users }) => { diff --git a/playwright-tests/tests/messageSaving.e2e.test.js b/playwright-tests/tests/messageSaving.e2e.test.js index 48f80ac..6c48489 100644 --- a/playwright-tests/tests/messageSaving.e2e.test.js +++ b/playwright-tests/tests/messageSaving.e2e.test.js @@ -1,9 +1,36 @@ const { test: base, expect } = require('../fixtures/base'); const { sendMessageTo, checkReceivedMessage } = require('../helpers/messageHelpers'); const { createAndSignInUser, generateUsername } = require('../helpers/userHelpers'); -const { getLocalStorage, getMessagesBetweenUsers } = require('../helpers/localStorageHelpers'); +const { getLocalStorage, getUserAuthoredMessagesBetweenUsers } = require('../helpers/localStorageHelpers'); const { newContext } = require('../helpers/toastHelpers'); +function expectStoredMessages(actualMessages, expectedMessages, ownerNumber) { + const actual = actualMessages.map(message => ({ + content: message.message, + direction: message.my ? 'sent' : 'received' + })); + const expected = expectedMessages.map(message => ({ + content: message.content, + direction: message.from === ownerNumber ? 'sent' : 'received' + })); + + expect(actual).toHaveLength(expected.length); + expect(actual).toEqual(expect.arrayContaining(expected)); +} + +async function expectRenderedMessages(page, expectedMessages, ownerNumber) { + const messageBubbles = page.locator('#chatModal .messages-list .message:has(.message-content)'); + await expect(messageBubbles).toHaveCount(expectedMessages.length); + + for (let i = 0; i < expectedMessages.length; i++) { + const expectedMessage = expectedMessages[i]; + const expectedDirection = expectedMessage.from === ownerNumber ? 'sent' : 'received'; + const messageBubble = messageBubbles.nth(i); + + await expect(messageBubble).toHaveClass(new RegExp(`\\b${expectedDirection}\\b`)); + await expect(messageBubble.locator('.message-content')).toContainText(expectedMessage.content); + } +} const test = base.extend({ messageUsers: async ({ browserName, browser }, use) => { @@ -23,12 +50,10 @@ const test = base.extend({ createAndSignInUser(pg2, user2) ]); - // Define 4 alternating messages + // Define alternating messages const messages = [ { from: 1, to: 2, content: `Message 1 from ${user1} to ${user2}` }, - { from: 2, to: 1, content: `Message 2 from ${user2} to ${user1}` }, - // { from: 1, to: 2, content: `Message 3 from ${user1} to ${user2}` }, - // { from: 2, to: 1, content: `Message 4 from ${user2} to ${user1}` } + { from: 2, to: 1, content: `Message 2 from ${user2} to ${user1}` } ]; // Exchange messages @@ -71,13 +96,6 @@ test.describe('Message Saving Tests', () => { const { users: { user1, user2 }, messages } = messageUsers; try { - const expectedMessages = [ - messages[0].content, - messages[1].content, - // messages[2].content, - // messages[3].content - ]; - // Explicitly sign out both users // Sign out user1 await user1.page.click('#toggleMenu'); @@ -94,12 +112,20 @@ test.describe('Message Saving Tests', () => { // Check that messages ARE present in localStorage after signing out const user1LocalStorage = await getLocalStorage(user1.page); const user2LocalStorage = await getLocalStorage(user2.page); - const storedUser1Messages = getMessagesBetweenUsers(user1LocalStorage, user1.username, user2.username); - const storedUser2Messages = getMessagesBetweenUsers(user2LocalStorage, user2.username, user1.username); - + const storedUser1Messages = getUserAuthoredMessagesBetweenUsers( + user1LocalStorage, + user1.username, + user2.username + ); + const storedUser2Messages = getUserAuthoredMessagesBetweenUsers( + user2LocalStorage, + user2.username, + user1.username + ); + // When signing out, messages should still be in localStorage - expect(storedUser1Messages.length).toBe(expectedMessages.length); - expect(storedUser2Messages.length).toBe(expectedMessages.length); + expectStoredMessages(storedUser1Messages, messages, 1); + expectStoredMessages(storedUser2Messages, messages, 2); // Sign back in as both users (should automatically sign in from localStorage) // Sign in user1 @@ -119,12 +145,7 @@ test.describe('Message Saving Tests', () => { await chatItem1.click(); await expect(user1.page.locator('#chatModal')).toBeVisible(); - // Explicitly check message count and content for user1 - const user1Messages = await user1.page.locator('#chatModal .messages-list .message').allTextContents(); - expect(user1Messages.length).toBe(expectedMessages.length); - for (let i = 0; i < expectedMessages.length; i++) { - expect(user1Messages[i]).toContain(expectedMessages[i]); - } + await expectRenderedMessages(user1.page, messages, 1); // Also verify user2's messages are still available await user2.page.click('#switchToChats'); @@ -134,12 +155,7 @@ test.describe('Message Saving Tests', () => { await chatItem2.click(); await expect(user2.page.locator('#chatModal')).toBeVisible(); - // Explicitly check message count and content for user2 - const user2Messages = await user2.page.locator('#chatModal .messages-list .message').allTextContents(); - expect(user2Messages.length).toBe(expectedMessages.length); - for (let i = 0; i < expectedMessages.length; i++) { - expect(user2Messages[i]).toContain(expectedMessages[i]); - } + await expectRenderedMessages(user2.page, messages, 2); } finally { await user1.context.close(); @@ -151,13 +167,6 @@ test.describe('Message Saving Tests', () => { const { users: { user1, user2 }, messages } = messageUsers; try { - const expectedMessages = [ - messages[0].content, - messages[1].content, - // messages[2].content, - // messages[3].content - ]; - // Close both users' pages (but keep their contexts) await user1.page.close(); await user2.page.close(); @@ -172,13 +181,21 @@ test.describe('Message Saving Tests', () => { await newPage2.goto(''); await newPage2.waitForSelector('#welcomeScreen', { timeout: 30_000 }); - // Check that messages are NOT present in localStorage before signing in (on welcome screen) + // Check that messages remain in localStorage before signing in const user1LocalStorage = await getLocalStorage(newPage1); const user2LocalStorage = await getLocalStorage(newPage2); - const storedUser1Messages = getMessagesBetweenUsers(user1LocalStorage, user1.username, user2.username); - const storedUser2Messages = getMessagesBetweenUsers(user2LocalStorage, user2.username, user1.username); - expect(storedUser1Messages.length).toBe(expectedMessages.length); - expect(storedUser2Messages.length).toBe(expectedMessages.length); + const storedUser1Messages = getUserAuthoredMessagesBetweenUsers( + user1LocalStorage, + user1.username, + user2.username + ); + const storedUser2Messages = getUserAuthoredMessagesBetweenUsers( + user2LocalStorage, + user2.username, + user1.username + ); + expectStoredMessages(storedUser1Messages, messages, 1); + expectStoredMessages(storedUser2Messages, messages, 2); // Now click sign in for both users await newPage1.click('#signInButton'); @@ -195,12 +212,7 @@ test.describe('Message Saving Tests', () => { await chatItem1.click(); await expect(newPage1.locator('#chatModal')).toBeVisible(); - // Explicitly check message count and content for user1 - const user1Messages = await newPage1.locator('#chatModal .messages-list .message').allTextContents(); - expect(user1Messages.length).toBe(expectedMessages.length); - for (let i = 0; i < expectedMessages.length; i++) { - expect(user1Messages[i]).toContain(expectedMessages[i]); - } + await expectRenderedMessages(newPage1, messages, 1); // Also verify user2's messages are still available await newPage2.click('#switchToChats'); @@ -210,12 +222,7 @@ test.describe('Message Saving Tests', () => { await chatItem2.click(); await expect(newPage2.locator('#chatModal')).toBeVisible(); - // Explicitly check message count and content for user2 - const user2Messages = await newPage2.locator('#chatModal .messages-list .message').allTextContents(); - expect(user2Messages.length).toBe(expectedMessages.length); - for (let i = 0; i < expectedMessages.length; i++) { - expect(user2Messages[i]).toContain(expectedMessages[i]); - } + await expectRenderedMessages(newPage2, messages, 2); } finally { await user1.context.close(); @@ -227,13 +234,6 @@ test.describe('Message Saving Tests', () => { const { users: { user1, user2 }, messages } = messageUsers; try { - const expectedMessages = [ - messages[0].content, - messages[1].content, - // messages[2].content, - // messages[3].content - ]; - // Refresh both users' pages await user1.page.reload(); await expect(user1.page.locator('#welcomeScreen')).toBeVisible({ timeout: 30_000 }); @@ -244,10 +244,18 @@ test.describe('Message Saving Tests', () => { // Check that messages ARE present in localStorage after refreshing const user1LocalStorage = await getLocalStorage(user1.page); const user2LocalStorage = await getLocalStorage(user2.page); - const storedUser1Messages = getMessagesBetweenUsers(user1LocalStorage, user1.username, user2.username); - const storedUser2Messages = getMessagesBetweenUsers(user2LocalStorage, user2.username, user1.username); - expect(storedUser1Messages.length).toBe(expectedMessages.length); - expect(storedUser2Messages.length).toBe(expectedMessages.length); + const storedUser1Messages = getUserAuthoredMessagesBetweenUsers( + user1LocalStorage, + user1.username, + user2.username + ); + const storedUser2Messages = getUserAuthoredMessagesBetweenUsers( + user2LocalStorage, + user2.username, + user1.username + ); + expectStoredMessages(storedUser1Messages, messages, 1); + expectStoredMessages(storedUser2Messages, messages, 2); // sign in await user1.page.click('#signInButton'); @@ -261,12 +269,7 @@ test.describe('Message Saving Tests', () => { await chatItem1.click(); await expect(user1.page.locator('#chatModal')).toBeVisible(); - // Explicitly check message count and content for user1 - const user1Messages = await user1.page.locator('#chatModal .messages-list .message').allTextContents(); - expect(user1Messages.length).toBe(expectedMessages.length); - for (let i = 0; i < expectedMessages.length; i++) { - expect(user1Messages[i]).toContain(expectedMessages[i]); - } + await expectRenderedMessages(user1.page, messages, 1); // Also verify user2's messages are still available await expect(user2.page.locator('#chatsScreen.active')).toBeVisible(); @@ -275,12 +278,7 @@ test.describe('Message Saving Tests', () => { await chatItem2.click(); await expect(user2.page.locator('#chatModal')).toBeVisible(); - // Explicitly check message count and content for user2 - const user2Messages = await user2.page.locator('#chatModal .messages-list .message').allTextContents(); - expect(user2Messages.length).toBe(expectedMessages.length); - for (let i = 0; i < expectedMessages.length; i++) { - expect(user2Messages[i]).toContain(expectedMessages[i]); - } + await expectRenderedMessages(user2.page, messages, 2); } finally { await user1.context.close(); diff --git a/playwright-tests/tests/videoCall.e2e.test.js b/playwright-tests/tests/videoCall.e2e.test.js index e82ad36..3586462 100644 --- a/playwright-tests/tests/videoCall.e2e.test.js +++ b/playwright-tests/tests/videoCall.e2e.test.js @@ -136,8 +136,9 @@ test.describe('Video Call Tests', () => { await expect(a.page.locator('#callScheduleChoiceModal.active')).toBeVisible(); await a.page.click('#openCallScheduleDateBtn'); - const scheduleModal = a.page.locator('#callScheduleDateModal.active'); - await expect(scheduleModal).toBeVisible(); + const dateTimePicker = a.page.locator('#dateTimePickerModal.active'); + await expect(dateTimePicker).toBeVisible(); + await expect(dateTimePicker.locator('#dateTimePickerModalTitle')).toHaveText('Schedule Call'); // Choose a schedule time two hours in the future, rounded to the hour for available options. const scheduleDate = new Date(Date.now() + 2 * 60 * 60 * 1000); @@ -153,21 +154,19 @@ test.describe('Video Call Tests', () => { const hourSelectValue = hour12.toString().padStart(2, '0'); const minuteSelectValue = scheduleDate.getMinutes().toString().padStart(2, '0'); const datePart = `${month}/${day}/${year}`; - // Toast time part is like 2:00:00 PM (with seconds) const timePart = `${hour12}:${minuteSelectValue} ${amPm}`; - await scheduleModal.locator('#callScheduleDate').fill(dateInputValue); - await scheduleModal.locator('#callScheduleHour').selectOption(hourSelectValue); - await scheduleModal.locator('#callScheduleMinute').selectOption(minuteSelectValue); - await scheduleModal.locator('#callScheduleAmPm').selectOption(amPm); + await dateTimePicker.locator('#dateTimePickerDate').fill(dateInputValue); + await dateTimePicker.locator('#dateTimePickerHour').selectOption(hourSelectValue); + await dateTimePicker.locator('#dateTimePickerMinute').selectOption(minuteSelectValue); + await dateTimePicker.locator('#dateTimePickerAmPm').selectOption(amPm); - await scheduleModal.locator('#confirmCallSchedule').click(); + await dateTimePicker.locator('#confirmDateTimePicker').click(); const successToast = a.page.locator('.toast.success.show'); await expect(successToast).toContainText('Call scheduled for', { timeout: 20_000 }); await expect(successToast).toContainText(datePart); - const toastTimePart = `${hour12}:${minuteSelectValue}:00 ${amPm}`; - await expect(successToast).toContainText(toastTimePart); + await expect(successToast).toContainText(timePart); await a.page.waitForSelector('.toast.success.show', { state: 'hidden' }); // Verify the scheduled call message on the sender side.