diff --git a/packages/desktop-client/e2e/budget.mobile.test.ts b/packages/desktop-client/e2e/budget.mobile.test.ts index ac479ea4a0a..51b30fd8ef9 100644 --- a/packages/desktop-client/e2e/budget.mobile.test.ts +++ b/packages/desktop-client/e2e/budget.mobile.test.ts @@ -60,6 +60,9 @@ async function setBudgetAverage( await budgetPage.goToPreviousMonth(); const spentButton = await budgetPage.getButtonForSpent(categoryName); const spent = await spentButton.textContent(); + if (!spent) { + throw new Error('Failed to get spent amount'); + } totalSpent += currencyToAmount(spent) ?? 0; } @@ -280,6 +283,10 @@ budgetTypes.forEach(budgetType => { const lastMonthBudget = await budgetedButton.textContent(); + if (!lastMonthBudget) { + throw new Error('Failed to get last month budget'); + } + await budgetPage.goToNextMonth(); await copyLastMonthBudget(budgetPage, categoryName); diff --git a/packages/desktop-client/e2e/page-models/account-page.js b/packages/desktop-client/e2e/page-models/account-page.ts similarity index 74% rename from packages/desktop-client/e2e/page-models/account-page.js rename to packages/desktop-client/e2e/page-models/account-page.ts index cbdf4efef41..ebbee3f0068 100644 --- a/packages/desktop-client/e2e/page-models/account-page.js +++ b/packages/desktop-client/e2e/page-models/account-page.ts @@ -1,7 +1,33 @@ +import { type Locator, type Page } from '@playwright/test'; + import { CloseAccountModal } from './close-account-modal'; +type TransactionEntry = { + debit?: string; + credit?: string; + account?: string; + payee?: string; + notes?: string; + category?: string; +}; + export class AccountPage { - constructor(page) { + readonly page: Page; + readonly accountName: Locator; + readonly accountBalance: Locator; + readonly addNewTransactionButton: Locator; + readonly newTransactionRow: Locator; + readonly addTransactionButton: Locator; + readonly cancelTransactionButton: Locator; + readonly accountMenuButton: Locator; + readonly transactionTable: Locator; + readonly transactionTableRow: Locator; + readonly filterButton: Locator; + readonly filterSelectTooltip: Locator; + readonly selectButton: Locator; + readonly selectTooltip: Locator; + + constructor(page: Page) { this.page = page; this.accountName = this.page.getByTestId('account-name'); @@ -30,14 +56,14 @@ export class AccountPage { this.selectTooltip = this.page.getByTestId('transactions-select-tooltip'); } - async waitFor() { - await this.transactionTable.waitFor(); + async waitFor(...options: Parameters) { + await this.transactionTable.waitFor(...options); } /** * Enter details of a transaction */ - async enterSingleTransaction(transaction) { + async enterSingleTransaction(transaction: TransactionEntry) { await this.addNewTransactionButton.click(); await this._fillTransactionFields(this.newTransactionRow, transaction); } @@ -53,7 +79,7 @@ export class AccountPage { /** * Create a single transaction */ - async createSingleTransaction(transaction) { + async createSingleTransaction(transaction: TransactionEntry) { await this.enterSingleTransaction(transaction); await this.addEnteredTransaction(); } @@ -61,7 +87,10 @@ export class AccountPage { /** * Create split transactions */ - async createSplitTransaction([rootTransaction, ...transactions]) { + async createSplitTransaction([ + rootTransaction, + ...transactions + ]: TransactionEntry[]) { await this.addNewTransactionButton.click(); // Root transaction @@ -87,7 +116,7 @@ export class AccountPage { await this.cancelTransactionButton.click(); } - async selectNthTransaction(index) { + async selectNthTransaction(index: number) { const row = this.transactionTableRow.nth(index); await row.getByTestId('select').click(); } @@ -96,7 +125,7 @@ export class AccountPage { * Retrieve the data for the nth-transaction. * 0-based index */ - getNthTransaction(index) { + getNthTransaction(index: number) { const row = this.transactionTableRow.nth(index); return this._getTransactionDetails(row); @@ -106,11 +135,9 @@ export class AccountPage { return this._getTransactionDetails(this.newTransactionRow); } - _getTransactionDetails(row) { - const account = row.getByTestId('account'); - + _getTransactionDetails(row: Locator) { return { - ...(account ? { account } : {}), + account: row.getByTestId('account'), payee: row.getByTestId('payee'), notes: row.getByTestId('notes'), category: row.getByTestId('category'), @@ -119,7 +146,7 @@ export class AccountPage { }; } - async clickSelectAction(action) { + async clickSelectAction(action: string | RegExp) { await this.selectButton.click(); await this.selectTooltip.getByRole('button', { name: action }).click(); } @@ -130,16 +157,13 @@ export class AccountPage { async clickCloseAccount() { await this.accountMenuButton.click(); await this.page.getByRole('button', { name: 'Close Account' }).click(); - return new CloseAccountModal( - this.page, - this.page.getByTestId('close-account-modal'), - ); + return new CloseAccountModal(this.page.getByTestId('close-account-modal')); } /** * Open the filtering popover. */ - async filterBy(field) { + async filterBy(field: string | RegExp) { await this.filterButton.click(); await this.filterSelectTooltip.getByRole('button', { name: field }).click(); @@ -149,7 +173,7 @@ export class AccountPage { /** * Filter to a specific note */ - async filterByNote(note) { + async filterByNote(note: string) { const filterTooltip = await this.filterBy('Note'); await this.page.keyboard.type(note); await filterTooltip.applyButton.click(); @@ -158,14 +182,17 @@ export class AccountPage { /** * Remove the nth filter */ - async removeFilter(idx) { + async removeFilter(idx: number) { await this.page .getByRole('button', { name: 'Delete filter' }) .nth(idx) .click(); } - async _fillTransactionFields(transactionRow, transaction) { + async _fillTransactionFields( + transactionRow: Locator, + transaction: TransactionEntry, + ) { if (transaction.debit) { await transactionRow.getByTestId('debit').click(); await this.page.keyboard.type(transaction.debit); @@ -210,8 +237,11 @@ export class AccountPage { } class FilterTooltip { - constructor(page) { - this.page = page; - this.applyButton = page.getByRole('button', { name: 'Apply' }); + readonly locator: Locator; + readonly applyButton: Locator; + + constructor(locator: Locator) { + this.locator = locator; + this.applyButton = locator.getByRole('button', { name: 'Apply' }); } } diff --git a/packages/desktop-client/e2e/page-models/budget-page.js b/packages/desktop-client/e2e/page-models/budget-page.js deleted file mode 100644 index 86b168a2e1d..00000000000 --- a/packages/desktop-client/e2e/page-models/budget-page.js +++ /dev/null @@ -1,89 +0,0 @@ -import { AccountPage } from './account-page'; - -export class BudgetPage { - constructor(page) { - this.page = page; - - this.budgetSummary = page.getByTestId('budget-summary'); - this.budgetTable = page.getByTestId('budget-table'); - this.budgetTableTotals = this.budgetTable.getByTestId('budget-totals'); - } - - async getTableTotals() { - return { - budgeted: parseInt( - await this.budgetTableTotals - .getByTestId(/total-budgeted$/) - .textContent(), - 10, - ), - spent: parseInt( - await this.budgetTableTotals.getByTestId(/total-spent$/).textContent(), - 10, - ), - balance: parseInt( - await this.budgetTableTotals - .getByTestId(/total-leftover$/) - .textContent(), - 10, - ), - }; - } - - async showMoreMonths() { - await this.page.getByTestId('calendar-icon').first().click(); - } - - async getBalanceForRow(idx) { - return Math.round( - parseFloat( - ( - await this.budgetTable - .getByTestId('row') - .nth(idx) - .getByTestId('balance') - .textContent() - ).replace(/,/g, ''), - ) * 100, - ); - } - - async getCategoryNameForRow(idx) { - return this.budgetTable - .getByTestId('row') - .nth(idx) - .getByTestId('category-name') - .textContent(); - } - - async clickOnSpentAmountForRow(idx) { - await this.budgetTable - .getByTestId('row') - .nth(idx) - .getByTestId('category-month-spent') - .click(); - return new AccountPage(this.page); - } - - async transferAllBalance(fromIdx, toIdx) { - const toName = await this.getCategoryNameForRow(toIdx); - - await this.budgetTable - .getByTestId('row') - .nth(fromIdx) - .getByTestId('balance') - .getByTestId(/^budget/) - .click(); - - await this.page - .getByRole('button', { name: 'Transfer to another category' }) - .click(); - - await this.page.getByPlaceholder('(none)').click(); - - await this.page.keyboard.type(toName); - await this.page.keyboard.press('Enter'); - - await this.page.getByRole('button', { name: 'Transfer' }).click(); - } -} diff --git a/packages/desktop-client/e2e/page-models/budget-page.ts b/packages/desktop-client/e2e/page-models/budget-page.ts new file mode 100644 index 00000000000..99f90242950 --- /dev/null +++ b/packages/desktop-client/e2e/page-models/budget-page.ts @@ -0,0 +1,128 @@ +import { type Locator, type Page } from '@playwright/test'; + +import { AccountPage } from './account-page'; + +export class BudgetPage { + readonly page: Page; + readonly budgetSummary: Locator; + readonly budgetTable: Locator; + readonly budgetTableTotals: Locator; + + constructor(page: Page) { + this.page = page; + + this.budgetSummary = page.getByTestId('budget-summary'); + this.budgetTable = page.getByTestId('budget-table'); + this.budgetTableTotals = this.budgetTable.getByTestId('budget-totals'); + } + + async getTotalBudgeted() { + const totalBudgetedText = await this.budgetTableTotals + .getByTestId(/total-budgeted$/) + .textContent(); + + if (!totalBudgetedText) { + throw new Error('Failed to get total budgeted.'); + } + + return parseInt(totalBudgetedText, 10); + } + + async getTotalSpent() { + const totalSpentText = await this.budgetTableTotals + .getByTestId(/total-spent$/) + .textContent(); + + if (!totalSpentText) { + throw new Error('Failed to get total spent.'); + } + + return parseInt(totalSpentText, 10); + } + + async getTotalLeftover() { + const totalLeftoverText = await this.budgetTableTotals + .getByTestId(/total-leftover$/) + .textContent(); + + if (!totalLeftoverText) { + throw new Error('Failed to get total leftover.'); + } + + return parseInt(totalLeftoverText, 10); + } + + async getTableTotals() { + return { + budgeted: await this.getTotalBudgeted(), + spent: await this.getTotalSpent(), + balance: await this.getTotalLeftover(), + }; + } + + async showMoreMonths() { + await this.page.getByTestId('calendar-icon').first().click(); + } + + async getBalanceForRow(idx: number) { + const balanceText = await this.budgetTable + .getByTestId('row') + .nth(idx) + .getByTestId('balance') + .textContent(); + + if (!balanceText) { + throw new Error(`Failed to get balance on row index ${idx}.`); + } + + return Math.round(parseFloat(balanceText.replace(/,/g, '')) * 100); + } + + async getCategoryNameForRow(idx: number) { + const categoryNameText = this.budgetTable + .getByTestId('row') + .nth(idx) + .getByTestId('category-name') + .textContent(); + + if (!categoryNameText) { + throw new Error(`Failed to get category name on row index ${idx}.`); + } + + return categoryNameText; + } + + async clickOnSpentAmountForRow(idx: number) { + await this.budgetTable + .getByTestId('row') + .nth(idx) + .getByTestId('category-month-spent') + .click(); + return new AccountPage(this.page); + } + + async transferAllBalance(fromIdx: number, toIdx: number) { + const toName = await this.getCategoryNameForRow(toIdx); + if (!toName) { + throw new Error(`Unable to get category name of row index ${toIdx}.`); + } + + await this.budgetTable + .getByTestId('row') + .nth(fromIdx) + .getByTestId('balance') + .getByTestId(/^budget/) + .click(); + + await this.page + .getByRole('button', { name: 'Transfer to another category' }) + .click(); + + await this.page.getByPlaceholder('(none)').click(); + + await this.page.keyboard.type(toName); + await this.page.keyboard.press('Enter'); + + await this.page.getByRole('button', { name: 'Transfer' }).click(); + } +} diff --git a/packages/desktop-client/e2e/page-models/close-account-modal.js b/packages/desktop-client/e2e/page-models/close-account-modal.ts similarity index 56% rename from packages/desktop-client/e2e/page-models/close-account-modal.js rename to packages/desktop-client/e2e/page-models/close-account-modal.ts index dc975a1d71f..36b2a062e32 100644 --- a/packages/desktop-client/e2e/page-models/close-account-modal.js +++ b/packages/desktop-client/e2e/page-models/close-account-modal.ts @@ -1,10 +1,15 @@ +import { type Locator, type Page } from '@playwright/test'; + export class CloseAccountModal { - constructor(page, locator) { - this.page = page; + readonly locator: Locator; + readonly page: Page; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); } - async selectTransferAccount(accountName) { + async selectTransferAccount(accountName: string) { await this.locator.getByPlaceholder('Select account...').fill(accountName); await this.page.keyboard.press('Enter'); } diff --git a/packages/desktop-client/e2e/page-models/configuration-page.js b/packages/desktop-client/e2e/page-models/configuration-page.ts similarity index 89% rename from packages/desktop-client/e2e/page-models/configuration-page.js rename to packages/desktop-client/e2e/page-models/configuration-page.ts index 903ec5dfdd9..daa2c9efbac 100644 --- a/packages/desktop-client/e2e/page-models/configuration-page.js +++ b/packages/desktop-client/e2e/page-models/configuration-page.ts @@ -1,8 +1,13 @@ +import { type Locator, type Page } from '@playwright/test'; + import { AccountPage } from './account-page'; import { BudgetPage } from './budget-page'; export class ConfigurationPage { - constructor(page) { + readonly page: Page; + readonly heading: Locator; + + constructor(page: Page) { this.page = page; this.heading = page.getByRole('heading'); @@ -23,7 +28,7 @@ export class ConfigurationPage { return new AccountPage(this.page); } - async importBudget(type, file) { + async importBudget(type: 'YNAB4' | 'nYNAB' | 'Actual', file: string) { const fileChooserPromise = this.page.waitForEvent('filechooser'); await this.page.getByRole('button', { name: 'Import my budget' }).click(); diff --git a/packages/desktop-client/e2e/page-models/custom-report-page.js b/packages/desktop-client/e2e/page-models/custom-report-page.ts similarity index 70% rename from packages/desktop-client/e2e/page-models/custom-report-page.js rename to packages/desktop-client/e2e/page-models/custom-report-page.ts index 493160b5590..2de241afeb3 100644 --- a/packages/desktop-client/e2e/page-models/custom-report-page.js +++ b/packages/desktop-client/e2e/page-models/custom-report-page.ts @@ -1,5 +1,13 @@ +import { type Locator, type Page } from '@playwright/test'; + export class CustomReportPage { - constructor(page) { + readonly page: Page; + readonly pageContent: Locator; + readonly showLegendButton: Locator; + readonly showSummaryButton: Locator; + readonly showLabelsButton: Locator; + + constructor(page: Page) { this.page = page; this.pageContent = page.getByTestId('reports-page'); @@ -14,11 +22,11 @@ export class CustomReportPage { }); } - async selectViz(vizName) { + async selectViz(vizName: string | RegExp) { await this.pageContent.getByRole('button', { name: vizName }).click(); } - async selectMode(mode) { + async selectMode(mode: 'total' | 'time') { switch (mode) { case 'total': await this.pageContent.getByRole('button', { name: 'Total' }).click(); diff --git a/packages/desktop-client/e2e/page-models/mobile-account-page.js b/packages/desktop-client/e2e/page-models/mobile-account-page.ts similarity index 60% rename from packages/desktop-client/e2e/page-models/mobile-account-page.js rename to packages/desktop-client/e2e/page-models/mobile-account-page.ts index 009d3b556bd..4c7e312e776 100644 --- a/packages/desktop-client/e2e/page-models/mobile-account-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-account-page.ts @@ -1,7 +1,18 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileTransactionEntryPage } from './mobile-transaction-entry-page'; export class MobileAccountPage { - constructor(page) { + readonly page: Page; + readonly heading: Locator; + readonly balance: Locator; + readonly noTransactionsMessage: Locator; + readonly searchBox: Locator; + readonly transactionList: Locator; + readonly transactions: Locator; + readonly createTransactionButton: Locator; + + constructor(page: Page) { this.page = page; this.heading = page.getByRole('heading'); @@ -15,21 +26,25 @@ export class MobileAccountPage { }); } - async waitFor() { - await this.transactionList.waitFor(); + async waitFor(...options: Parameters) { + await this.transactionList.waitFor(...options); } /** * Retrieve the balance of the account as a number */ async getBalance() { - return parseInt(await this.balance.textContent(), 10); + const balanceText = await this.balance.textContent(); + if (!balanceText) { + throw new Error('Failed to get balance.'); + } + return parseInt(balanceText, 10); } /** * Search by the given term */ - async searchByText(term) { + async searchByText(term: string) { await this.searchBox.fill(term); } diff --git a/packages/desktop-client/e2e/page-models/mobile-accounts-page.js b/packages/desktop-client/e2e/page-models/mobile-accounts-page.ts similarity index 64% rename from packages/desktop-client/e2e/page-models/mobile-accounts-page.js rename to packages/desktop-client/e2e/page-models/mobile-accounts-page.ts index 365584e73e0..571244535fc 100644 --- a/packages/desktop-client/e2e/page-models/mobile-accounts-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-accounts-page.ts @@ -1,21 +1,27 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileAccountPage } from './mobile-account-page'; export class MobileAccountsPage { - constructor(page) { + readonly page: Page; + readonly accountList: Locator; + readonly accountListItems: Locator; + + constructor(page: Page) { this.page = page; this.accountList = this.page.getByLabel('Account list'); this.accountListItems = this.accountList.getByTestId('account-list-item'); } - async waitFor() { - await this.accountList.waitFor(); + async waitFor(...options: Parameters) { + await this.accountList.waitFor(...options); } /** * Get the name and balance of the nth account */ - async getNthAccount(idx) { + async getNthAccount(idx: number) { const accountRow = this.accountListItems.nth(idx); return { @@ -27,7 +33,7 @@ export class MobileAccountsPage { /** * Click on the n-th account to open it up */ - async openNthAccount(idx) { + async openNthAccount(idx: number) { await this.accountListItems.nth(idx).click(); return new MobileAccountPage(this.page); diff --git a/packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.js b/packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.ts similarity index 62% rename from packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.js rename to packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.ts index e802354013f..8c292271a4f 100644 --- a/packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-balance-menu-modal.ts @@ -1,7 +1,18 @@ +import { type Page, type Locator } from '@playwright/test'; + export class BalanceMenuModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + readonly balanceAmountInput: Locator; + readonly transferToAnotherCategoryButton: Locator; + readonly coverOverspendingButton: Locator; + readonly rolloverOverspendingButton: Locator; + readonly removeOverspendingRolloverButton: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); this.balanceAmountInput = locator.getByTestId('amount-input'); diff --git a/packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.js b/packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.ts similarity index 69% rename from packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.js rename to packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.ts index 774448c6968..5f39f117213 100644 --- a/packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-budget-menu-modal.ts @@ -1,7 +1,19 @@ +import { type Locator, type Page } from '@playwright/test'; + export class BudgetMenuModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + readonly budgetAmountInput: Locator; + readonly copyLastMonthBudgetButton: Locator; + readonly setTo3MonthAverageButton: Locator; + readonly setTo6MonthAverageButton: Locator; + readonly setToYearlyAverageButton: Locator; + readonly applyBudgetTemplateButton: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); this.budgetAmountInput = locator.getByTestId('amount-input'); @@ -26,8 +38,8 @@ export class BudgetMenuModal { await this.heading.getByRole('button', { name: 'Close' }).click(); } - async setBudgetAmount(newAmount) { - await this.budgetAmountInput.fill(String(newAmount)); + async setBudgetAmount(newAmount: string) { + await this.budgetAmountInput.fill(newAmount); await this.budgetAmountInput.blur(); await this.close(); } diff --git a/packages/desktop-client/e2e/page-models/mobile-budget-page.js b/packages/desktop-client/e2e/page-models/mobile-budget-page.ts similarity index 66% rename from packages/desktop-client/e2e/page-models/mobile-budget-page.js rename to packages/desktop-client/e2e/page-models/mobile-budget-page.ts index 3053557e144..daa7804b27d 100644 --- a/packages/desktop-client/e2e/page-models/mobile-budget-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-budget-page.ts @@ -1,3 +1,5 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileAccountPage } from './mobile-account-page'; import { BalanceMenuModal } from './mobile-balance-menu-modal'; import { BudgetMenuModal } from './mobile-budget-menu-modal'; @@ -6,24 +8,47 @@ import { EnvelopeBudgetSummaryModal } from './mobile-envelope-budget-summary-mod import { TrackingBudgetSummaryModal } from './mobile-tracking-budget-summary-modal'; export class MobileBudgetPage { - MONTH_HEADER_DATE_FORMAT = 'MMMM ‘yy'; - - constructor(page) { + readonly MONTH_HEADER_DATE_FORMAT = 'MMMM ‘yy'; + + readonly page: Page; + readonly heading: Locator; + readonly previousMonthButton: Locator; + readonly selectedBudgetMonthButton: Locator; + readonly nextMonthButton: Locator; + readonly budgetPageMenuButton: Locator; + readonly budgetTableHeader: Locator; + readonly toBudgetButton: Locator; + readonly overbudgetedButton: Locator; + readonly savedButton: Locator; + readonly projectedSavingsButton: Locator; + readonly overspentButton: Locator; + readonly budgetedHeaderButton: Locator; + readonly spentHeaderButton: Locator; + readonly budgetTable: Locator; + readonly categoryRows: Locator; + readonly categoryNames: Locator; + readonly categoryGroupRows: Locator; + readonly categoryGroupNames: Locator; + + constructor(page: Page) { this.page = page; - this.#initializePageHeaderLocators(page); - this.#initializeBudgetTableLocators(page); - } + // Page header locators - async determineBudgetType() { - return (await this.#getButtonForEnvelopeBudgetSummary({ - throwIfNotFound: false, - })) !== null - ? 'Envelope' - : 'Tracking'; - } + this.heading = page.getByRole('heading'); + this.previousMonthButton = this.heading.getByRole('button', { + name: 'Previous month', + }); + this.selectedBudgetMonthButton = this.heading.locator('button[data-month]'); + this.nextMonthButton = this.heading.getByRole('button', { + name: 'Next month', + }); + this.budgetPageMenuButton = page.getByRole('button', { + name: 'Budget page menu', + }); + + // Budget table locators - #initializeBudgetTableLocators(page) { this.budgetTableHeader = page.getByTestId('budget-table-header'); // Envelope budget summary buttons @@ -69,25 +94,21 @@ export class MobileBudgetPage { ); } - #initializePageHeaderLocators(page) { - this.heading = page.getByRole('heading'); - this.previousMonthButton = this.heading.getByRole('button', { - name: 'Previous month', - }); - this.selectedBudgetMonthButton = this.heading.locator('button[data-month]'); - this.nextMonthButton = this.heading.getByRole('button', { - name: 'Next month', - }); - this.budgetPageMenuButton = page.getByRole('button', { - name: 'Budget page menu', - }); + async determineBudgetType() { + return (await this.#getButtonForEnvelopeBudgetSummary({ + throwIfNotFound: false, + })) !== null + ? 'Envelope' + : 'Tracking'; } - async waitFor(options) { - await this.budgetTable.waitFor(options); + async waitFor(...options: Parameters) { + await this.budgetTable.waitFor(...options); } - async toggleVisibleColumns(maxAttempts = 3) { + async toggleVisibleColumns({ + maxAttempts = 3, + }: { maxAttempts?: number } = {}) { for (let i = 0; i < maxAttempts; i++) { if (await this.budgetedHeaderButton.isVisible()) { await this.budgetedHeaderButton.click(); @@ -100,55 +121,72 @@ export class MobileBudgetPage { await this.page.waitForTimeout(1000); } - throw new Error('Budgeted/Spent columns could not be located on the page'); + throw new Error('Budgeted/Spent columns could not be located on the page.'); } async getSelectedMonth() { - return await this.heading + const selectedMonth = await this.heading .locator('[data-month]') .getAttribute('data-month'); + + if (!selectedMonth) { + throw new Error('Failed to get the selected month.'); + } + + return selectedMonth; } async openBudgetPageMenu() { await this.budgetPageMenuButton.click(); } - async getCategoryGroupNameForRow(idx) { - return this.categoryGroupNames.nth(idx).textContent(); + async getCategoryGroupNameForRow(idx: number) { + const groupNameText = await this.categoryGroupNames.nth(idx).textContent(); + if (!groupNameText) { + throw new Error(`Failed to get category group name for row ${idx}.`); + } + return groupNameText; } - #getButtonForCategoryGroup(categoryGroupName) { + #getButtonForCategoryGroup(categoryGroupName: string | RegExp) { return this.categoryGroupRows.getByRole('button', { name: categoryGroupName, exact: true, }); } - async openCategoryGroupMenu(categoryGroupName) { + async openCategoryGroupMenu(categoryGroupName: string | RegExp) { const categoryGroupButton = await this.#getButtonForCategoryGroup(categoryGroupName); await categoryGroupButton.click(); } - async getCategoryNameForRow(idx) { - return this.categoryNames.nth(idx).textContent(); + async getCategoryNameForRow(idx: number) { + const categoryNameText = await this.categoryNames.nth(idx).textContent(); + if (!categoryNameText) { + throw new Error(`Failed to get category name for row ${idx}.`); + } + return categoryNameText; } - #getButtonForCategory(categoryName) { + #getButtonForCategory(categoryName: string | RegExp) { return this.categoryRows.getByRole('button', { name: categoryName, exact: true, }); } - async openCategoryMenu(categoryName) { + async openCategoryMenu(categoryName: string | RegExp) { const categoryButton = await this.#getButtonForCategory(categoryName); await categoryButton.click(); - return new CategoryMenuModal(this.page, this.page.getByRole('dialog')); + return new CategoryMenuModal(this.page.getByRole('dialog')); } - async #getButtonForCell(buttonType, categoryName) { + async #getButtonForCell( + buttonType: 'Budgeted' | 'Spent', + categoryName: string, + ) { const buttonSelector = buttonType === 'Budgeted' ? `Open budget menu for ${categoryName} category` @@ -168,43 +206,43 @@ export class MobileBudgetPage { } throw new Error( - `${buttonType} button for category ${categoryName} could not be located on the page`, + `${buttonType} button for category ${categoryName} could not be located on the page.`, ); } - async getButtonForBudgeted(categoryName) { + async getButtonForBudgeted(categoryName: string) { return await this.#getButtonForCell('Budgeted', categoryName); } - async getButtonForSpent(categoryName) { + async getButtonForSpent(categoryName: string) { return await this.#getButtonForCell('Spent', categoryName); } - async openBudgetMenu(categoryName) { + async openBudgetMenu(categoryName: string) { const budgetedButton = await this.getButtonForBudgeted(categoryName); await budgetedButton.click(); - return new BudgetMenuModal(this.page, this.page.getByRole('dialog')); + return new BudgetMenuModal(this.page.getByRole('dialog')); } - async openSpentPage(categoryName) { + async openSpentPage(categoryName: string) { const spentButton = await this.getButtonForSpent(categoryName); await spentButton.click(); return new MobileAccountPage(this.page); } - async openBalanceMenu(categoryName) { + async openBalanceMenu(categoryName: string) { const balanceButton = this.budgetTable.getByRole('button', { name: `Open balance menu for ${categoryName} category`, }); if (await balanceButton.isVisible()) { await balanceButton.click(); - return new BalanceMenuModal(this.page, this.page.getByRole('dialog')); + return new BalanceMenuModal(this.page.getByRole('dialog')); } else { throw new Error( - `Balance button for category ${categoryName} not found or not visible`, + `Balance button for category ${categoryName} not found or not visible.`, ); } } @@ -213,7 +251,11 @@ export class MobileBudgetPage { currentMonth, errorMessage, maxAttempts = 3, - } = {}) { + }: { + currentMonth: string; + errorMessage: string; + maxAttempts: number; + }) { for (let attempt = 0; attempt < maxAttempts; attempt++) { const newMonth = await this.getSelectedMonth(); if (newMonth !== currentMonth) { @@ -225,7 +267,7 @@ export class MobileBudgetPage { throw new Error(errorMessage); } - async goToPreviousMonth({ maxAttempts = 3 } = {}) { + async goToPreviousMonth({ maxAttempts = 3 }: { maxAttempts?: number } = {}) { const currentMonth = await this.getSelectedMonth(); await this.previousMonthButton.click(); @@ -234,7 +276,7 @@ export class MobileBudgetPage { currentMonth, maxAttempts, errorMessage: - 'Failed to navigate to the previous month after maximum attempts', + 'Failed to navigate to the previous month after maximum attempts.', }); } @@ -242,7 +284,7 @@ export class MobileBudgetPage { await this.selectedBudgetMonthButton.click(); } - async goToNextMonth({ maxAttempts = 3 } = {}) { + async goToNextMonth({ maxAttempts = 3 }: { maxAttempts?: number } = {}) { const currentMonth = await this.getSelectedMonth(); await this.nextMonthButton.click(); @@ -251,11 +293,13 @@ export class MobileBudgetPage { currentMonth, maxAttempts, errorMessage: - 'Failed to navigate to the next month after maximum attempts', + 'Failed to navigate to the next month after maximum attempts.', }); } - async #getButtonForEnvelopeBudgetSummary({ throwIfNotFound = true } = {}) { + async #getButtonForEnvelopeBudgetSummary({ + throwIfNotFound = true, + }: { throwIfNotFound?: boolean } = {}) { if (await this.toBudgetButton.isVisible()) { return this.toBudgetButton; } @@ -269,21 +313,23 @@ export class MobileBudgetPage { } throw new Error( - 'Neither “To Budget” nor “Overbudgeted” button could be located on the page', + 'Neither “To Budget” nor “Overbudgeted” button could be located on the page.', ); } async openEnvelopeBudgetSummary() { const budgetSummaryButton = await this.#getButtonForEnvelopeBudgetSummary(); + if (!budgetSummaryButton) { + throw new Error('Envelope budget summary button not found.'); + } await budgetSummaryButton.click(); - return new EnvelopeBudgetSummaryModal( - this.page, - this.page.getByRole('dialog'), - ); + return new EnvelopeBudgetSummaryModal(this.page.getByRole('dialog')); } - async #getButtonForTrackingBudgetSummary({ throwIfNotFound = true } = {}) { + async #getButtonForTrackingBudgetSummary({ + throwIfNotFound = true, + }: { throwIfNotFound?: boolean } = {}) { if (await this.savedButton.isVisible()) { return this.savedButton; } @@ -301,17 +347,17 @@ export class MobileBudgetPage { } throw new Error( - 'None of “Saved”, “Projected savings”, or “Overspent” buttons could be located on the page', + 'None of “Saved”, “Projected savings”, or “Overspent” buttons could be located on the page.', ); } async openTrackingBudgetSummary() { const budgetSummaryButton = await this.#getButtonForTrackingBudgetSummary(); + if (!budgetSummaryButton) { + throw new Error('Tracking budget summary button not found.'); + } await budgetSummaryButton.click(); - return new TrackingBudgetSummaryModal( - this.page, - this.page.getByRole('dialog'), - ); + return new TrackingBudgetSummaryModal(this.page.getByRole('dialog')); } } diff --git a/packages/desktop-client/e2e/page-models/mobile-category-menu-modal.js b/packages/desktop-client/e2e/page-models/mobile-category-menu-modal.ts similarity index 58% rename from packages/desktop-client/e2e/page-models/mobile-category-menu-modal.js rename to packages/desktop-client/e2e/page-models/mobile-category-menu-modal.ts index f02ae03b8db..53319613e50 100644 --- a/packages/desktop-client/e2e/page-models/mobile-category-menu-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-category-menu-modal.ts @@ -1,9 +1,17 @@ +import { type Locator, type Page } from '@playwright/test'; + import { EditNotesModal } from './mobile-edit-notes-modal'; export class CategoryMenuModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + readonly budgetAmountInput: Locator; + readonly editNotesButton: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); this.budgetAmountInput = locator.getByTestId('amount-input'); @@ -17,6 +25,6 @@ export class CategoryMenuModal { async editNotes() { await this.editNotesButton.click(); - return new EditNotesModal(this.page, this.page.getByRole('dialog')); + return new EditNotesModal(this.page.getByRole('dialog')); } } diff --git a/packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.js b/packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.ts similarity index 57% rename from packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.js rename to packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.ts index c9c717df83f..845c8dbfc3f 100644 --- a/packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-edit-notes-modal.ts @@ -1,7 +1,15 @@ +import { type Locator, type Page } from '@playwright/test'; + export class EditNotesModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + readonly textArea: Locator; + readonly saveNotesButton: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); this.textArea = locator.getByRole('textbox'); @@ -12,7 +20,7 @@ export class EditNotesModal { await this.heading.getByRole('button', { name: 'Close' }).click(); } - async updateNotes(notes) { + async updateNotes(notes: string) { await this.textArea.fill(notes); await this.saveNotesButton.click(); } diff --git a/packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.js b/packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.ts similarity index 51% rename from packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.js rename to packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.ts index b0615c04d60..2f6efed0ea0 100644 --- a/packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-envelope-budget-summary-modal.ts @@ -1,7 +1,13 @@ +import { type Locator, type Page } from '@playwright/test'; + export class EnvelopeBudgetSummaryModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); } diff --git a/packages/desktop-client/e2e/page-models/mobile-navigation.js b/packages/desktop-client/e2e/page-models/mobile-navigation.ts similarity index 70% rename from packages/desktop-client/e2e/page-models/mobile-navigation.js rename to packages/desktop-client/e2e/page-models/mobile-navigation.ts index 258ec5a77d4..14fba95cffa 100644 --- a/packages/desktop-client/e2e/page-models/mobile-navigation.js +++ b/packages/desktop-client/e2e/page-models/mobile-navigation.ts @@ -1,3 +1,5 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileAccountPage } from './mobile-account-page'; import { MobileAccountsPage } from './mobile-accounts-page'; import { MobileBudgetPage } from './mobile-budget-page'; @@ -5,8 +7,30 @@ import { MobileReportsPage } from './mobile-reports-page'; import { MobileTransactionEntryPage } from './mobile-transaction-entry-page'; import { SettingsPage } from './settings-page'; +const NAVBAR_ROWS = 3; +const NAV_LINKS_HIDDEN_BY_DEFAULT = [ + 'Reports', + 'Schedules', + 'Payees', + 'Rules', + 'Settings', +]; +const ROUTES_BY_PAGE = { + Budget: '/budget', + Accounts: '/accounts', + Transaction: '/transactions/new', + Reports: '/reports', + Settings: '/settings', +}; + export class MobileNavigation { - constructor(page) { + readonly page: Page; + readonly heading: Locator; + readonly navbar: Locator; + readonly mainContentSelector: string; + readonly navbarSelector: string; + + constructor(page: Page) { this.page = page; this.heading = page.getByRole('heading'); this.navbar = page.getByRole('navigation'); @@ -14,31 +38,23 @@ export class MobileNavigation { this.navbarSelector = '[role=navigation]'; } - static #NAVBAR_ROWS = 3; - static #NAV_LINKS_HIDDEN_BY_DEFAULT = [ - 'Reports', - 'Schedules', - 'Payees', - 'Rules', - 'Settings', - ]; - static #ROUTES_BY_PAGE = { - Budget: '/budget', - Accounts: '/accounts', - Transactions: '/transactions/new', - Reports: '/reports', - Settings: '/settings', - }; - async dragNavbarUp() { const mainContentBoundingBox = await this.page .locator(this.mainContentSelector) .boundingBox(); + if (!mainContentBoundingBox) { + throw new Error('Unable to get bounding box of main content.'); + } + const navbarBoundingBox = await this.page .locator(this.navbarSelector) .boundingBox(); + if (!navbarBoundingBox) { + throw new Error('Unable to get bounding box of navbar.'); + } + await this.page.dragAndDrop(this.navbarSelector, this.mainContentSelector, { sourcePosition: { x: 1, y: 0 }, targetPosition: { @@ -53,39 +69,47 @@ export class MobileNavigation { .locator(this.navbarSelector) .boundingBox(); + if (!boundingBox) { + throw new Error('Unable to get bounding box of navbar.'); + } + await this.page.dragAndDrop(this.navbarSelector, this.navbarSelector, { sourcePosition: { x: 1, y: 0 }, targetPosition: { x: 1, // Only scroll until bottom of screen i.e. bottom of first navbar row. - y: boundingBox.height / MobileNavigation.#NAVBAR_ROWS, + y: boundingBox.height / NAVBAR_ROWS, }, }); } - async hasNavbarState(...states) { + async hasNavbarState(...states: string[]) { if ((await this.navbar.count()) === 0) { // No navbar on page. return false; } const dataNavbarState = await this.navbar.getAttribute('data-navbar-state'); + if (!dataNavbarState) { + throw new Error('Navbar does not have data-navbar-state attribute.'); + } return states.includes(dataNavbarState); } - async navigateToPage(pageName, pageModelFactory) { + async navigateToPage( + pageName: keyof typeof ROUTES_BY_PAGE, + pageModelFactory: () => T, + ): Promise { const pageInstance = pageModelFactory(); - if (this.page.url().endsWith(MobileNavigation.#ROUTES_BY_PAGE[pageName])) { + if (this.page.url().endsWith(ROUTES_BY_PAGE[pageName])) { // Already on the page. return pageInstance; } await this.navbar.waitFor(); - const navbarStates = MobileNavigation.#NAV_LINKS_HIDDEN_BY_DEFAULT.includes( - pageName, - ) + const navbarStates = NAV_LINKS_HIDDEN_BY_DEFAULT.includes(pageName) ? ['default', 'hidden'] : ['hidden']; diff --git a/packages/desktop-client/e2e/page-models/mobile-reports-page.js b/packages/desktop-client/e2e/page-models/mobile-reports-page.js deleted file mode 100644 index 76e317ab3c0..00000000000 --- a/packages/desktop-client/e2e/page-models/mobile-reports-page.js +++ /dev/null @@ -1,11 +0,0 @@ -export class MobileReportsPage { - constructor(page) { - this.page = page; - - this.overview = page.getByTestId('reports-overview'); - } - - async waitFor(options) { - await this.overview.waitFor(options); - } -} diff --git a/packages/desktop-client/e2e/page-models/mobile-reports-page.ts b/packages/desktop-client/e2e/page-models/mobile-reports-page.ts new file mode 100644 index 00000000000..74b5684426c --- /dev/null +++ b/packages/desktop-client/e2e/page-models/mobile-reports-page.ts @@ -0,0 +1,16 @@ +import { type Locator, type Page } from '@playwright/test'; + +export class MobileReportsPage { + readonly page: Page; + readonly overview: Locator; + + constructor(page: Page) { + this.page = page; + + this.overview = page.getByTestId('reports-overview'); + } + + async waitFor(...options: Parameters) { + await this.overview.waitFor(...options); + } +} diff --git a/packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.js b/packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.ts similarity index 51% rename from packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.js rename to packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.ts index f6afab7b209..0a4de8bf281 100644 --- a/packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.js +++ b/packages/desktop-client/e2e/page-models/mobile-tracking-budget-summary-modal.ts @@ -1,7 +1,13 @@ +import { type Locator, type Page } from '@playwright/test'; + export class TrackingBudgetSummaryModal { - constructor(page, locator) { - this.page = page; + readonly page: Page; + readonly locator: Locator; + readonly heading: Locator; + + constructor(locator: Locator) { this.locator = locator; + this.page = locator.page(); this.heading = locator.getByRole('heading'); } diff --git a/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js b/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.ts similarity index 62% rename from packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js rename to packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.ts index b6de03dd92a..8937fd0d187 100644 --- a/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.js +++ b/packages/desktop-client/e2e/page-models/mobile-transaction-entry-page.ts @@ -1,7 +1,16 @@ +import { type Locator, type Page } from '@playwright/test'; + import { MobileAccountPage } from './mobile-account-page'; export class MobileTransactionEntryPage { - constructor(page) { + readonly page: Page; + readonly header: Locator; + readonly amountField: Locator; + readonly transactionForm: Locator; + readonly footer: Locator; + readonly addTransactionButton: Locator; + + constructor(page: Page) { this.page = page; this.header = page.getByRole('heading'); this.transactionForm = page.getByTestId('transaction-form'); @@ -12,11 +21,11 @@ export class MobileTransactionEntryPage { }); } - async waitFor(options) { - await this.transactionForm.waitFor(options); + async waitFor(...options: Parameters) { + await this.transactionForm.waitFor(...options); } - async fillField(fieldLocator, content) { + async fillField(fieldLocator: Locator, content: string) { await fieldLocator.click(); await this.page.locator('css=[role=combobox] input').fill(content); await this.page.keyboard.press('Enter'); diff --git a/packages/desktop-client/e2e/page-models/navigation.js b/packages/desktop-client/e2e/page-models/navigation.ts similarity index 88% rename from packages/desktop-client/e2e/page-models/navigation.js rename to packages/desktop-client/e2e/page-models/navigation.ts index 8e468847cd0..f665a2e8b77 100644 --- a/packages/desktop-client/e2e/page-models/navigation.js +++ b/packages/desktop-client/e2e/page-models/navigation.ts @@ -1,15 +1,25 @@ +import { type Page } from '@playwright/test'; + import { AccountPage } from './account-page'; import { ReportsPage } from './reports-page'; import { RulesPage } from './rules-page'; import { SchedulesPage } from './schedules-page'; import { SettingsPage } from './settings-page'; +type AccountEntry = { + name: string; + balance: number; + offBudget: boolean; +}; + export class Navigation { - constructor(page) { + readonly page: Page; + + constructor(page: Page) { this.page = page; } - async goToAccountPage(accountName) { + async goToAccountPage(accountName: string) { await this.page .getByRole('link', { name: new RegExp(`^${accountName}`) }) .click(); @@ -55,7 +65,7 @@ export class Navigation { return new SettingsPage(this.page); } - async createAccount(data) { + async createAccount(data: AccountEntry) { await this.page.getByRole('button', { name: 'Add account' }).click(); await this.page .getByRole('button', { name: 'Create a local account' }) diff --git a/packages/desktop-client/e2e/page-models/reports-page.js b/packages/desktop-client/e2e/page-models/reports-page.ts similarity index 87% rename from packages/desktop-client/e2e/page-models/reports-page.js rename to packages/desktop-client/e2e/page-models/reports-page.ts index c5c070f3cb8..a499c8692cd 100644 --- a/packages/desktop-client/e2e/page-models/reports-page.js +++ b/packages/desktop-client/e2e/page-models/reports-page.ts @@ -1,7 +1,12 @@ +import { type Locator, type Page } from '@playwright/test'; + import { CustomReportPage } from './custom-report-page'; export class ReportsPage { - constructor(page) { + readonly page: Page; + readonly pageContent: Locator; + + constructor(page: Page) { this.page = page; this.pageContent = page.getByTestId('reports-page'); } diff --git a/packages/desktop-client/e2e/page-models/rules-page.js b/packages/desktop-client/e2e/page-models/rules-page.ts similarity index 74% rename from packages/desktop-client/e2e/page-models/rules-page.js rename to packages/desktop-client/e2e/page-models/rules-page.ts index 28f0c06fb42..be2d359c306 100644 --- a/packages/desktop-client/e2e/page-models/rules-page.js +++ b/packages/desktop-client/e2e/page-models/rules-page.ts @@ -1,5 +1,35 @@ +import { type Locator, type Page } from '@playwright/test'; + +type ConditionsEntry = { + field: string; + op: string; + value: string; +}; + +type ActionsEntry = { + field: string; + op?: string; + value: string; +}; + +type SplitsEntry = { + field: string; + op?: string; + value?: string; +}; + +type RuleEntry = { + conditionsOp?: string | RegExp; + conditions?: ConditionsEntry[]; + actions?: ActionsEntry[]; + splits?: Array; +}; + export class RulesPage { - constructor(page) { + readonly page: Page; + readonly searchBox: Locator; + + constructor(page: Page) { this.page = page; this.searchBox = page.getByPlaceholder('Filter rules...'); } @@ -7,7 +37,7 @@ export class RulesPage { /** * Create a new rule */ - async createRule(data) { + async createRule(data: RuleEntry) { await this.page .getByRole('button', { name: 'Create new rule', @@ -23,7 +53,7 @@ export class RulesPage { * Retrieve the data for the nth-rule. * 0-based index */ - getNthRule(index) { + getNthRule(index: number) { const row = this.page.getByTestId('table').getByTestId('row').nth(index); return { @@ -32,11 +62,11 @@ export class RulesPage { }; } - async searchFor(text) { + async searchFor(text: string) { await this.searchBox.fill(text); } - async _fillRuleFields(data) { + async _fillRuleFields(data: RuleEntry) { if (data.conditionsOp) { await this.page .getByTestId('conditions-op') @@ -76,9 +106,13 @@ export class RulesPage { } } - async _fillEditorFields(data, rootElement, fieldFirst = false) { - for (const idx in data) { - const { field, op, value } = data[idx]; + async _fillEditorFields( + data: Array, + rootElement: Locator, + fieldFirst = false, + ) { + for (const [idx, entry] of data.entries()) { + const { field, op, value } = entry; const row = rootElement.getByTestId('editor-row').nth(idx); diff --git a/packages/desktop-client/e2e/page-models/schedules-page.js b/packages/desktop-client/e2e/page-models/schedules-page.ts similarity index 78% rename from packages/desktop-client/e2e/page-models/schedules-page.js rename to packages/desktop-client/e2e/page-models/schedules-page.ts index 3166b96509a..81ce61c09e5 100644 --- a/packages/desktop-client/e2e/page-models/schedules-page.js +++ b/packages/desktop-client/e2e/page-models/schedules-page.ts @@ -1,5 +1,17 @@ +import { type Locator, type Page } from '@playwright/test'; + +type ScheduleEntry = { + payee?: string; + account?: string; + amount?: number; +}; + export class SchedulesPage { - constructor(page) { + readonly page: Page; + readonly addNewScheduleButton: Locator; + readonly schedulesTableRow: Locator; + + constructor(page: Page) { this.page = page; this.addNewScheduleButton = this.page.getByRole('button', { @@ -11,7 +23,7 @@ export class SchedulesPage { /** * Add a new schedule */ - async addNewSchedule(data) { + async addNewSchedule(data: ScheduleEntry) { await this.addNewScheduleButton.click(); await this._fillScheduleFields(data); @@ -23,7 +35,7 @@ export class SchedulesPage { * Retrieve the row element for the nth-schedule. * 0-based index */ - getNthScheduleRow(index) { + getNthScheduleRow(index: number) { return this.schedulesTableRow.nth(index); } @@ -31,7 +43,7 @@ export class SchedulesPage { * Retrieve the data for the nth-schedule. * 0-based index */ - getNthSchedule(index) { + getNthSchedule(index: number) { const row = this.getNthScheduleRow(index); return { @@ -47,7 +59,7 @@ export class SchedulesPage { * Create a transaction for the nth-schedule. * 0-based index */ - async postNthSchedule(index) { + async postNthSchedule(index: number) { await this._performNthAction(index, 'Post transaction today'); await this.page.waitForTimeout(1000); } @@ -56,12 +68,12 @@ export class SchedulesPage { * Complete the nth-schedule. * 0-based index */ - async completeNthSchedule(index) { + async completeNthSchedule(index: number) { await this._performNthAction(index, 'Complete'); await this.page.waitForTimeout(1000); } - async _performNthAction(index, actionName) { + async _performNthAction(index: number, actionName: string | RegExp) { const row = this.getNthScheduleRow(index); const actions = row.getByTestId('actions'); @@ -69,7 +81,7 @@ export class SchedulesPage { await this.page.getByRole('button', { name: actionName }).click(); } - async _fillScheduleFields(data) { + async _fillScheduleFields(data: ScheduleEntry) { if (data.payee) { await this.page.getByRole('textbox', { name: 'Payee' }).fill(data.payee); await this.page.keyboard.press('Enter'); diff --git a/packages/desktop-client/e2e/page-models/settings-page.js b/packages/desktop-client/e2e/page-models/settings-page.ts similarity index 66% rename from packages/desktop-client/e2e/page-models/settings-page.js rename to packages/desktop-client/e2e/page-models/settings-page.ts index 96045c30043..42c1f329191 100644 --- a/packages/desktop-client/e2e/page-models/settings-page.js +++ b/packages/desktop-client/e2e/page-models/settings-page.ts @@ -1,5 +1,14 @@ +import { type Locator, type Page } from '@playwright/test'; + export class SettingsPage { - constructor(page) { + readonly page: Page; + readonly settings: Locator; + readonly exportDataButton: Locator; + readonly switchBudgetTypeButton: Locator; + readonly advancedSettingsButton: Locator; + readonly experimentalSettingsButton: Locator; + + constructor(page: Page) { this.page = page; this.settings = page.getByTestId('settings'); this.exportDataButton = this.settings.getByRole('button', { @@ -15,24 +24,24 @@ export class SettingsPage { ); } - async waitFor(options) { - await this.settings.waitFor(options); + async waitFor(...options: Parameters) { + await this.settings.waitFor(...options); } async exportData() { await this.exportDataButton.click(); } - async useBudgetType(budgetType) { + async useBudgetType(budgetType: 'Envelope' | 'Tracking') { await this.switchBudgetTypeButton.waitFor(); const buttonText = await this.switchBudgetTypeButton.textContent(); - if (buttonText.includes(budgetType.toLowerCase())) { + if (buttonText?.includes(budgetType.toLowerCase())) { await this.switchBudgetTypeButton.click(); } } - async enableExperimentalFeature(featureName) { + async enableExperimentalFeature(featureName: string) { if (await this.advancedSettingsButton.isVisible()) { await this.advancedSettingsButton.click(); } diff --git a/packages/desktop-client/e2e/transactions.test.ts b/packages/desktop-client/e2e/transactions.test.ts index 1b444b707d5..375605324ef 100644 --- a/packages/desktop-client/e2e/transactions.test.ts +++ b/packages/desktop-client/e2e/transactions.test.ts @@ -40,7 +40,7 @@ test.describe('Transactions', () => { test('by date', async () => { const filterTooltip = await accountPage.filterBy('Date'); - await expect(filterTooltip.page).toMatchThemeScreenshots(); + await expect(filterTooltip.locator).toMatchThemeScreenshots(); // Open datepicker await page.keyboard.press('Space'); @@ -58,7 +58,7 @@ test.describe('Transactions', () => { test('by category', async () => { const filterTooltip = await accountPage.filterBy('Category'); - await expect(filterTooltip.page).toMatchThemeScreenshots(); + await expect(filterTooltip.locator).toMatchThemeScreenshots(); // Type in the autocomplete box const autocomplete = page.getByTestId('autocomplete'); diff --git a/upcoming-release-notes/4218.md b/upcoming-release-notes/4218.md new file mode 100644 index 00000000000..e0128c7b0fe --- /dev/null +++ b/upcoming-release-notes/4218.md @@ -0,0 +1,6 @@ +--- +category: Maintenance +authors: [joel-jeremy] +--- + +Convert playwright page models to TypeScript.