mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-27 09:38:09 -05:00
[TypeScript] Convert test page models to TS (#4218)
* Dummy commit * Delete js snapshots * Move extended expect and test to fixtures * Fix wrong commit * Update VRT * Dummy commit to run GH actions * Convert test page models to TS * Release notes * Fix typecheck errors * New page models to TS * Fix typecheck error * Fix page name * Put awaits on getTableTotals --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
04f8140d26
commit
0504becaf5
@@ -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);
|
||||
|
||||
@@ -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<Locator['waitFor']>) {
|
||||
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' });
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
128
packages/desktop-client/e2e/page-models/budget-page.ts
Normal file
128
packages/desktop-client/e2e/page-models/budget-page.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
@@ -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<Locator['waitFor']>) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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<Locator['waitFor']>) {
|
||||
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);
|
||||
@@ -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');
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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';
|
||||
readonly MONTH_HEADER_DATE_FORMAT = 'MMMM ‘yy';
|
||||
|
||||
constructor(page) {
|
||||
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<Locator['waitFor']>) {
|
||||
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'));
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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<T extends { waitFor: Locator['waitFor'] }>(
|
||||
pageName: keyof typeof ROUTES_BY_PAGE,
|
||||
pageModelFactory: () => T,
|
||||
): Promise<T> {
|
||||
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'];
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Locator['waitFor']>) {
|
||||
await this.overview.waitFor(...options);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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<Locator['waitFor']>) {
|
||||
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');
|
||||
@@ -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' })
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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<SplitsEntry[]>;
|
||||
};
|
||||
|
||||
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<ConditionsEntry | ActionsEntry | SplitsEntry>,
|
||||
rootElement: Locator,
|
||||
fieldFirst = false,
|
||||
) {
|
||||
for (const [idx, entry] of data.entries()) {
|
||||
const { field, op, value } = entry;
|
||||
|
||||
const row = rootElement.getByTestId('editor-row').nth(idx);
|
||||
|
||||
@@ -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');
|
||||
@@ -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<Locator['waitFor']>) {
|
||||
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();
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
6
upcoming-release-notes/4218.md
Normal file
6
upcoming-release-notes/4218.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Convert playwright page models to TypeScript.
|
||||
Reference in New Issue
Block a user