mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-11 20:44:32 -05:00
✅ (e2e) adding e2e tests for accounts: creating & closing (#695)
This commit is contained in:
committed by
GitHub
parent
42b5208b8b
commit
69a54a16e4
52
packages/desktop-client/e2e/accounts.test.js
Normal file
52
packages/desktop-client/e2e/accounts.test.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
|
||||
test.describe('Accounts', () => {
|
||||
let page;
|
||||
let navigation;
|
||||
let configurationPage;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
navigation = new Navigation(page);
|
||||
configurationPage = new ConfigurationPage(page);
|
||||
|
||||
await page.goto('/');
|
||||
await configurationPage.createTestFile();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test('creates a new account and views the initial balance transaction', async () => {
|
||||
const accountPage = await navigation.createAccount({
|
||||
name: 'New Account',
|
||||
type: 'Checking / Cash',
|
||||
offBudget: false,
|
||||
balance: 100,
|
||||
});
|
||||
|
||||
expect(await accountPage.getNthTransaction(0)).toMatchObject({
|
||||
payee: 'Starting Balance',
|
||||
notes: '',
|
||||
category: 'Starting Balances',
|
||||
debit: '',
|
||||
credit: '100.00',
|
||||
});
|
||||
});
|
||||
|
||||
test('closes an account', async () => {
|
||||
const accountPage = await navigation.goToAccountPage('Roth IRA');
|
||||
|
||||
await expect(accountPage.accountName).toHaveText('Roth IRA');
|
||||
|
||||
const modal = await accountPage.clickCloseAccount();
|
||||
await modal.selectTransferAccount('Vanguard 401k');
|
||||
await modal.closeAccount();
|
||||
|
||||
await expect(accountPage.accountName).toHaveText('Closed: Roth IRA');
|
||||
});
|
||||
});
|
||||
@@ -1,16 +1,22 @@
|
||||
import { CloseAccountModal } from './close-account-modal';
|
||||
|
||||
export class AccountPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
|
||||
this.accountName = this.page.getByTestId('account-name');
|
||||
this.addNewTransactionButton = this.page.getByRole('button', {
|
||||
name: 'Add New'
|
||||
name: 'Add New',
|
||||
});
|
||||
this.newTransactionRow = this.page
|
||||
.getByTestId('new-transaction')
|
||||
.getByTestId('row');
|
||||
this.addTransactionButton = this.page.getByTestId('add-button');
|
||||
this.cancelTransactionButton = this.page.getByRole('button', {
|
||||
name: 'Cancel'
|
||||
name: 'Cancel',
|
||||
});
|
||||
this.menuButton = this.page.getByRole('button', {
|
||||
name: 'Menu',
|
||||
});
|
||||
|
||||
this.transactionTableRow = this.page
|
||||
@@ -40,14 +46,14 @@ export class AccountPage {
|
||||
const transactionRow = this.newTransactionRow.first();
|
||||
await this._fillTransactionFields(transactionRow, {
|
||||
...rootTransaction,
|
||||
category: 'split'
|
||||
category: 'split',
|
||||
});
|
||||
|
||||
// Child transactions
|
||||
for (let i = 0; i < transactions.length; i++) {
|
||||
await this._fillTransactionFields(
|
||||
this.newTransactionRow.nth(i + 1),
|
||||
transactions[i]
|
||||
transactions[i],
|
||||
);
|
||||
|
||||
if (i + 1 < transactions.length) {
|
||||
@@ -71,10 +77,19 @@ export class AccountPage {
|
||||
notes: await row.getByTestId('notes').textContent(),
|
||||
category: await row.getByTestId('category').textContent(),
|
||||
debit: await row.getByTestId('debit').textContent(),
|
||||
credit: await row.getByTestId('credit').textContent()
|
||||
credit: await row.getByTestId('credit').textContent(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the modal for closing the account.
|
||||
*/
|
||||
async clickCloseAccount() {
|
||||
await this.menuButton.click();
|
||||
await this.page.getByRole('button', { name: 'Close Account' }).click();
|
||||
return new CloseAccountModal(this.page.locator('css=[aria-modal]'));
|
||||
}
|
||||
|
||||
async _fillTransactionFields(transactionRow, transaction) {
|
||||
if (transaction.payee) {
|
||||
await transactionRow.getByTestId('payee').click();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export class CloseAccountModal {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async selectTransferAccount(accountName) {
|
||||
await this.page.getByRole('combobox').selectOption({ label: accountName });
|
||||
}
|
||||
|
||||
async closeAccount() {
|
||||
await this.page.getByRole('button', { name: 'Close account' }).click();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
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';
|
||||
|
||||
export class Navigation {
|
||||
constructor(page) {
|
||||
@@ -14,9 +17,57 @@ export class Navigation {
|
||||
return new AccountPage(this.page);
|
||||
}
|
||||
|
||||
async goToReportsPage() {
|
||||
await this.page.getByRole('link', { name: 'Reports' }).click();
|
||||
|
||||
return new ReportsPage(this.page);
|
||||
}
|
||||
|
||||
async goToSchedulesPage() {
|
||||
await this.page.getByRole('link', { name: 'Schedules' }).click();
|
||||
|
||||
return new SchedulesPage(this.page);
|
||||
}
|
||||
|
||||
async goToRulesPage() {
|
||||
const rulesLink = this.page.getByRole('link', { name: 'Rules' });
|
||||
|
||||
// Expand the "more" menu only if it is not already expanded
|
||||
if (!(await rulesLink.isVisible())) {
|
||||
await this.page.getByRole('button', { name: 'More' }).click();
|
||||
}
|
||||
|
||||
await rulesLink.click();
|
||||
|
||||
return new RulesPage(this.page);
|
||||
}
|
||||
|
||||
async goToSettingsPage() {
|
||||
const settingsLink = this.page.getByRole('link', { name: 'Settings' });
|
||||
|
||||
// Expand the "more" menu only if it is not already expanded
|
||||
if (!(await settingsLink.isVisible())) {
|
||||
await this.page.getByRole('button', { name: 'More' }).click();
|
||||
}
|
||||
|
||||
await settingsLink.click();
|
||||
|
||||
return new SettingsPage(this.page);
|
||||
}
|
||||
|
||||
async createAccount(data) {
|
||||
await this.page.getByRole('button', { name: 'Add account' }).click();
|
||||
|
||||
// Fill the form
|
||||
await this.page.getByLabel('Name:').fill(data.name);
|
||||
await this.page.getByLabel('Type:').selectOption({ label: data.type });
|
||||
await this.page.getByLabel('Balance:').fill(String(data.balance));
|
||||
|
||||
if (data.offBudget) {
|
||||
await this.page.getByLabel('Off-budget').click();
|
||||
}
|
||||
|
||||
await this.page.getByRole('button', { name: 'Create' }).click();
|
||||
return new AccountPage(this.page);
|
||||
}
|
||||
}
|
||||
|
||||
17
packages/desktop-client/e2e/page-models/reports-page.js
Normal file
17
packages/desktop-client/e2e/page-models/reports-page.js
Normal file
@@ -0,0 +1,17 @@
|
||||
export class ReportsPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
this.pageContent = page.getByTestId('reports-page');
|
||||
}
|
||||
|
||||
async waitToLoad() {
|
||||
return this.pageContent.getByRole('link', { name: /^Net/ }).waitFor();
|
||||
}
|
||||
|
||||
async getAvailableReportList() {
|
||||
return this.pageContent
|
||||
.getByRole('link')
|
||||
.getByRole('heading')
|
||||
.allTextContents();
|
||||
}
|
||||
}
|
||||
78
packages/desktop-client/e2e/page-models/rules-page.js
Normal file
78
packages/desktop-client/e2e/page-models/rules-page.js
Normal file
@@ -0,0 +1,78 @@
|
||||
export class RulesPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new rule
|
||||
*/
|
||||
async createRule(data) {
|
||||
await this.page
|
||||
.getByRole('button', {
|
||||
name: 'Create new rule',
|
||||
})
|
||||
.click();
|
||||
|
||||
await this._fillRuleFields(data);
|
||||
|
||||
await this.page.getByRole('button', { name: 'Save' }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the data for the nth-rule.
|
||||
* 0-based index
|
||||
*/
|
||||
async getNthRule(index) {
|
||||
const row = this.page.getByTestId('table').getByTestId('row').nth(index);
|
||||
|
||||
return {
|
||||
conditions: await row.getByTestId('conditions').textContent(),
|
||||
actions: await row.getByTestId('actions').textContent(),
|
||||
};
|
||||
}
|
||||
|
||||
async _fillRuleFields(data) {
|
||||
if (data.conditions) {
|
||||
await this._fillEditorFields(
|
||||
data.conditions,
|
||||
this.page.getByTestId('condition-list'),
|
||||
);
|
||||
}
|
||||
|
||||
if (data.actions) {
|
||||
await this._fillEditorFields(
|
||||
data.actions,
|
||||
this.page.getByTestId('action-list'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async _fillEditorFields(data, rootElement) {
|
||||
for (const idx in data) {
|
||||
const { field, op, value } = data[idx];
|
||||
|
||||
const row = rootElement.getByTestId('editor-row').nth(idx);
|
||||
|
||||
if (!(await row.isVisible())) {
|
||||
await rootElement.getByRole('button', { name: 'Add entry' }).click();
|
||||
}
|
||||
|
||||
if (field) {
|
||||
await row.getByRole('button').first().click();
|
||||
await this.page
|
||||
.getByRole('option', { exact: true, name: field })
|
||||
.click();
|
||||
}
|
||||
|
||||
if (op) {
|
||||
await row.getByRole('button', { name: 'is' }).click();
|
||||
await this.page.getByRole('option', { name: op }).click();
|
||||
}
|
||||
|
||||
if (value) {
|
||||
await row.getByRole('combobox').fill(value);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -71,23 +71,17 @@ export class SchedulesPage {
|
||||
|
||||
async _fillScheduleFields(data) {
|
||||
if (data.payee) {
|
||||
await this.page.getByLabel('Payee').click();
|
||||
await this.page.keyboard.type(data.payee);
|
||||
await this.page.getByLabel('Payee').fill(data.payee);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
if (data.account) {
|
||||
await this.page.getByLabel('Account').click();
|
||||
await this.page.keyboard.type(data.account);
|
||||
await this.page.getByLabel('Account').fill(data.account);
|
||||
await this.page.keyboard.press('Enter');
|
||||
}
|
||||
|
||||
if (data.amount) {
|
||||
await this.page.getByLabel('Amount').click();
|
||||
await this.page.keyboard.press('Control+A');
|
||||
await this.page.keyboard.press('Delete');
|
||||
await this.page.keyboard.type(String(data.amount));
|
||||
await this.page.keyboard.press('Enter');
|
||||
await this.page.getByLabel('Amount').fill(String(data.amount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
9
packages/desktop-client/e2e/page-models/settings-page.js
Normal file
9
packages/desktop-client/e2e/page-models/settings-page.js
Normal file
@@ -0,0 +1,9 @@
|
||||
export class SettingsPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
async exportData() {
|
||||
await this.page.getByRole('button', { name: 'Export data' }).click();
|
||||
}
|
||||
}
|
||||
35
packages/desktop-client/e2e/reports.test.js
Normal file
35
packages/desktop-client/e2e/reports.test.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
|
||||
test.describe('Reports', () => {
|
||||
let page;
|
||||
let navigation;
|
||||
let reportsPage;
|
||||
let configurationPage;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
navigation = new Navigation(page);
|
||||
configurationPage = new ConfigurationPage(page);
|
||||
|
||||
await page.goto('/');
|
||||
await configurationPage.createTestFile();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
reportsPage = await navigation.goToReportsPage();
|
||||
await reportsPage.waitToLoad();
|
||||
});
|
||||
|
||||
test('loads net worth and cash flow reports', async () => {
|
||||
const reports = await reportsPage.getAvailableReportList();
|
||||
|
||||
expect(reports).toEqual(['Net Worth', 'Cash Flow']);
|
||||
});
|
||||
});
|
||||
64
packages/desktop-client/e2e/rules.test.js
Normal file
64
packages/desktop-client/e2e/rules.test.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
|
||||
test.describe('Rules', () => {
|
||||
let page;
|
||||
let navigation;
|
||||
let rulesPage;
|
||||
let configurationPage;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
navigation = new Navigation(page);
|
||||
configurationPage = new ConfigurationPage(page);
|
||||
|
||||
await page.goto('/');
|
||||
await configurationPage.createTestFile();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
rulesPage = await navigation.goToRulesPage();
|
||||
});
|
||||
|
||||
test('creates a rule and makes sure it is applied when creating a transaction', async () => {
|
||||
await rulesPage.createRule({
|
||||
conditions: [
|
||||
{
|
||||
field: 'payee',
|
||||
op: 'is',
|
||||
value: 'Fast Internet',
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
field: 'category',
|
||||
value: 'General',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(await rulesPage.getNthRule(0)).toMatchObject({
|
||||
conditions: 'payee is Fast Internet',
|
||||
actions: 'set category to General',
|
||||
});
|
||||
|
||||
const accountPage = await navigation.goToAccountPage('Bank of America');
|
||||
|
||||
await accountPage.createSingleTransaction({
|
||||
payee: 'Fast Internet',
|
||||
debit: '12.34',
|
||||
});
|
||||
|
||||
expect(await accountPage.getNthTransaction(0)).toMatchObject({
|
||||
payee: 'Fast Internet',
|
||||
category: 'General',
|
||||
debit: '12.34',
|
||||
});
|
||||
});
|
||||
});
|
||||
40
packages/desktop-client/e2e/settings.test.js
Normal file
40
packages/desktop-client/e2e/settings.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
import { ConfigurationPage } from './page-models/configuration-page';
|
||||
import { Navigation } from './page-models/navigation';
|
||||
|
||||
test.describe('Settings', () => {
|
||||
let page;
|
||||
let navigation;
|
||||
let settingsPage;
|
||||
let configurationPage;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
page = await browser.newPage();
|
||||
navigation = new Navigation(page);
|
||||
configurationPage = new ConfigurationPage(page);
|
||||
|
||||
await page.goto('/');
|
||||
await configurationPage.createTestFile();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test.beforeEach(async () => {
|
||||
settingsPage = await navigation.goToSettingsPage();
|
||||
});
|
||||
|
||||
test('downloads the export of the budget', async () => {
|
||||
const downloadPromise = page.waitForEvent('download');
|
||||
|
||||
await settingsPage.exportData();
|
||||
|
||||
const download = await downloadPromise;
|
||||
|
||||
expect(await download.suggestedFilename()).toMatch(
|
||||
/^\d{4}-\d{2}-\d{2}-.*.zip$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -356,7 +356,10 @@ let Rule = React.memo(
|
||||
|
||||
<Field width="flex" style={{ padding: '15px 0' }} truncate={false}>
|
||||
<Stack direction="row" align="center">
|
||||
<View style={{ flex: 1, alignItems: 'flex-start' }}>
|
||||
<View
|
||||
style={{ flex: 1, alignItems: 'flex-start' }}
|
||||
data-testid="conditions"
|
||||
>
|
||||
{rule.conditions.map((cond, i) => (
|
||||
<ConditionExpression
|
||||
key={i}
|
||||
@@ -374,7 +377,10 @@ let Rule = React.memo(
|
||||
<ArrowRight color={colors.n4} style={{ width: 12, height: 12 }} />
|
||||
</Text>
|
||||
|
||||
<View style={{ flex: 1, alignItems: 'flex-start' }}>
|
||||
<View
|
||||
style={{ flex: 1, alignItems: 'flex-start' }}
|
||||
data-testid="actions"
|
||||
>
|
||||
{rule.actions.map((action, i) => (
|
||||
<ActionExpression
|
||||
key={i}
|
||||
@@ -446,6 +452,7 @@ let SimpleTable = React.forwardRef(
|
||||
style,
|
||||
]}
|
||||
tabIndex="1"
|
||||
data-testid="table"
|
||||
{...getNavigatorProps(props)}
|
||||
>
|
||||
<View
|
||||
|
||||
@@ -224,7 +224,7 @@ function ReconcileTooltip({ account, onReconcile, onClose }) {
|
||||
|
||||
function MenuButton({ onClick }) {
|
||||
return (
|
||||
<Button bare onClick={onClick}>
|
||||
<Button bare onClick={onClick} aria-label="Menu">
|
||||
<DotsHorizontalTriple
|
||||
width={15}
|
||||
height={15}
|
||||
@@ -724,6 +724,7 @@ const AccountHeader = React.memo(
|
||||
marginRight: 5,
|
||||
marginBottom: 5,
|
||||
}}
|
||||
data-testid="account-name"
|
||||
>
|
||||
{account && account.closed
|
||||
? 'Closed: ' + accountName
|
||||
|
||||
@@ -121,12 +121,22 @@ function EditorButtons({ onAdd, onDelete, style }) {
|
||||
return (
|
||||
<>
|
||||
{onDelete && (
|
||||
<Button bare onClick={onDelete} style={{ padding: 7 }}>
|
||||
<Button
|
||||
bare
|
||||
onClick={onDelete}
|
||||
style={{ padding: 7 }}
|
||||
aria-label="Delete entry"
|
||||
>
|
||||
<SubtractIcon style={{ width: 8, height: 8 }} />
|
||||
</Button>
|
||||
)}
|
||||
{onAdd && (
|
||||
<Button bare onClick={onAdd} style={{ padding: 7 }}>
|
||||
<Button
|
||||
bare
|
||||
onClick={onAdd}
|
||||
style={{ padding: 7 }}
|
||||
aria-label="Add entry"
|
||||
>
|
||||
<AddIcon style={{ width: 10, height: 10 }} />
|
||||
</Button>
|
||||
)}
|
||||
@@ -151,7 +161,7 @@ function FieldError({ type }) {
|
||||
|
||||
function Editor({ error, style, children }) {
|
||||
return (
|
||||
<View style={style}>
|
||||
<View style={style} data-testid="editor-row">
|
||||
<Stack
|
||||
direction="row"
|
||||
align="center"
|
||||
@@ -506,7 +516,7 @@ export function ConditionsList({
|
||||
Add condition
|
||||
</Button>
|
||||
) : (
|
||||
<Stack spacing={2}>
|
||||
<Stack spacing={2} data-testid="condition-list">
|
||||
{conditions.map((cond, i) => {
|
||||
let ops = TYPE_INFO[cond.type].ops;
|
||||
|
||||
@@ -794,7 +804,7 @@ export default function EditRule({
|
||||
Add action
|
||||
</Button>
|
||||
) : (
|
||||
<Stack spacing={2}>
|
||||
<Stack spacing={2} data-testid="action-list">
|
||||
{actions.map((action, i) => (
|
||||
<View key={i}>
|
||||
<ActionEditor
|
||||
|
||||
@@ -79,6 +79,7 @@ function NetWorthCard({ accounts }) {
|
||||
<View style={{ flex: 1 }}>
|
||||
<Block
|
||||
style={[styles.mediumText, { fontWeight: 500, marginBottom: 5 }]}
|
||||
role="heading"
|
||||
>
|
||||
Net Worth
|
||||
</Block>
|
||||
@@ -132,6 +133,7 @@ function CashFlowCard() {
|
||||
<View style={{ flex: 1 }}>
|
||||
<Block
|
||||
style={[styles.mediumText, { fontWeight: 500, marginBottom: 5 }]}
|
||||
role="heading"
|
||||
>
|
||||
Cash Flow
|
||||
</Block>
|
||||
|
||||
@@ -10,7 +10,7 @@ import Overview from './Overview';
|
||||
class Reports extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={{ flex: 1 }} data-testid="reports-page">
|
||||
<Route path="/reports" exact component={Overview} />
|
||||
<Route path="/reports/net-worth" exact component={NetWorth} />
|
||||
<Route path="/reports/cash-flow" exact component={CashFlow} />
|
||||
|
||||
@@ -26,6 +26,7 @@ const Stack = React.forwardRef(
|
||||
children,
|
||||
debug,
|
||||
style,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
@@ -44,6 +45,7 @@ const Stack = React.forwardRef(
|
||||
style,
|
||||
]}
|
||||
innerRef={ref}
|
||||
{...props}
|
||||
>
|
||||
{validChildren.map(({ key, child }, index) => {
|
||||
let isLastChild = validChildren.length === index + 1;
|
||||
|
||||
Reference in New Issue
Block a user