test: bare-bones e2e testing with playwright (#416)

This commit is contained in:
Matiss Janis Aboltins
2023-01-12 22:43:32 +00:00
committed by GitHub
parent 4b90e37186
commit 50a71335e6
14 changed files with 323 additions and 2 deletions

View File

@@ -8,7 +8,7 @@ runs:
with:
node-version: 16.15.0
- name: Cache
uses: actions/cache@v2
uses: actions/cache@v3
id: cache
with:
path: '**/node_modules'

41
.github/workflows/e2e-test.yml vendored Normal file
View File

@@ -0,0 +1,41 @@
name: E2E Tests
on: [pull_request]
env:
GITHUB_PR_NUMBER: ${{github.event.pull_request.number}}
jobs:
test:
name: Run end-to-end tests on Netlify PR preview
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Playwright
run: npx playwright install --with-deps
- name: Set up environment
uses: ./.github/actions/setup
- name: Wait for Pages changed to neutral
uses: fountainhead/action-wait-for-check@v1.0.0
id: wait-for-Netlify
with:
token: ${{ secrets.GITHUB_TOKEN }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
checkName: 'Pages changed - actualbudget'
- name: Waiting for Netlify Preview
if: steps.wait-for-Netlify.outputs.conclusion == 'neutral'
uses: jakepartusch/wait-for-netlify-action@v1.4
id: waitFor200
with:
site_name: 'actualbudget'
max_timeout: 240
- name: Run E2E Tests on Netlify URL
run: yarn e2e
env:
E2E_START_URL: https://deploy-preview-${{env.GITHUB_PR_NUMBER}}--actualbudget.netlify.app
- uses: actions/upload-artifact@v3
if: always()
with:
name: desktop-client-test-results
path: packages/desktop-client/test-results/
retention-days: 30

View File

@@ -27,6 +27,7 @@
"start:browser-frontend": "yarn workspace @actual-app/web start:browser",
"test": "yarn workspaces foreach --parallel --verbose run test",
"test:debug": "yarn workspaces foreach --verbose run test",
"e2e": "yarn workspaces foreach --parallel --verbose run e2e",
"rebuild-electron": "./node_modules/.bin/electron-rebuild -f -m ./packages/loot-core",
"rebuild-node": "yarn workspace loot-core rebuild",
"lint": "yarn workspaces foreach --verbose run lint --max-warnings 0",

View File

@@ -5,6 +5,7 @@ node_modules
# testing
coverage
test-results
# production
build

View File

@@ -1 +1,21 @@
Actual on the web
## E2E tests
E2E (end-to-end) tests use [Playwright](https://playwright.dev/). Running them requires an Actual server to be running either locally or on a remote server.
Running against the local server:
```sh
# Start the development server
yarn start:browser
# Run against the local server (localhost:3001)
yarn e2e
```
Running against a remote server:
```sh
E2E_START_URL=http://my-remote-server.com yarn e2e
```

View File

@@ -0,0 +1,114 @@
export class AccountPage {
constructor(page) {
this.page = page;
this.addNewTransactionButton = this.page.getByRole('button', {
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'
});
this.transactionTableRow = this.page
.getByTestId('table')
.getByTestId('row');
}
/**
* Create a single transaction
*/
async createSingleTransaction(transaction) {
await this.addNewTransactionButton.click();
await this._fillTransactionFields(this.newTransactionRow, transaction);
await this.addTransactionButton.click();
await this.cancelTransactionButton.click();
}
/**
* Create split transactions
*/
async createSplitTransaction([rootTransaction, ...transactions]) {
await this.addNewTransactionButton.click();
// Root transaction
const transactionRow = this.newTransactionRow.first();
await this._fillTransactionFields(transactionRow, {
...rootTransaction,
category: 'split'
});
// Child transactions
for (let i = 0; i < transactions.length; i++) {
await this._fillTransactionFields(
this.newTransactionRow.nth(i + 1),
transactions[i]
);
if (i + 1 < transactions.length) {
await this.page.getByRole('button', { name: 'Add Split' }).click();
}
}
await this.addTransactionButton.click();
await this.cancelTransactionButton.click();
}
/**
* Retrieve the data for the nth-transaction.
* 0-based index
*/
async getNthTransaction(index) {
const row = this.transactionTableRow.nth(index);
return {
payee: await row.getByTestId('payee').textContent(),
notes: await row.getByTestId('notes').textContent(),
category: await row.getByTestId('category').textContent(),
debit: await row.getByTestId('debit').textContent(),
credit: await row.getByTestId('credit').textContent()
};
}
async _fillTransactionFields(transactionRow, transaction) {
if (transaction.payee) {
await transactionRow.getByTestId('payee').click();
await this.page.keyboard.type(transaction.payee);
await this.page.keyboard.press('Tab');
}
if (transaction.notes) {
await transactionRow.getByTestId('notes').click();
await this.page.keyboard.type(transaction.notes);
await this.page.keyboard.press('Tab');
}
if (transaction.category) {
await transactionRow.getByTestId('category').click();
if (transaction.category === 'split') {
await this.page.getByTestId('split-transaction-button').click();
} else {
await this.page.keyboard.type(transaction.category);
await this.page.keyboard.press('Tab');
}
}
if (transaction.debit) {
await transactionRow.getByTestId('debit').click();
await this.page.keyboard.type(transaction.debit);
await this.page.keyboard.press('Tab');
}
if (transaction.credit) {
await transactionRow.getByTestId('credit').click();
await this.page.keyboard.type(transaction.credit);
await this.page.keyboard.press('Tab');
}
}
}

View File

@@ -0,0 +1,10 @@
export class ConfigurationPage {
constructor(page) {
this.page = page;
}
async createTestFile() {
await this.page.getByRole('button', { name: 'Create test file' }).click();
await this.page.getByRole('button', { name: 'Close' }).click();
}
}

View File

@@ -0,0 +1,15 @@
import { AccountPage } from './account-page';
export class Navigation {
constructor(page) {
this.page = page;
}
async goToAccountPage(accountName) {
await this.page
.getByRole('link', { name: new RegExp(`^${accountName}`) })
.click();
return new AccountPage(this.page);
}
}

View File

@@ -0,0 +1,84 @@
import { test, expect } from '@playwright/test';
import { ConfigurationPage } from './page-models/configuration-page';
import { Navigation } from './page-models/navigation';
test.describe('Transactions', () => {
let page;
let navigation;
let accountPage;
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 () => {
accountPage = await navigation.goToAccountPage('Ally Savings');
});
test('creates a test transaction', async () => {
await accountPage.createSingleTransaction({
payee: 'Home Depot',
notes: 'Notes field',
category: 'Food',
debit: '12.34'
});
expect(await accountPage.getNthTransaction(0)).toMatchObject({
payee: 'Home Depot',
notes: 'Notes field',
category: 'Food',
debit: '12.34',
credit: ''
});
});
test('creates a split test transaction', async () => {
await accountPage.createSplitTransaction([
{
payee: 'Krogger',
notes: 'Notes',
debit: '333.33'
},
{
category: 'General',
debit: '222.22'
},
{
debit: '111.11'
}
]);
expect(await accountPage.getNthTransaction(0)).toMatchObject({
payee: 'Krogger',
notes: 'Notes',
category: 'Split',
debit: '333.33',
credit: ''
});
expect(await accountPage.getNthTransaction(1)).toMatchObject({
payee: 'Krogger',
notes: '',
category: 'General',
debit: '222.22',
credit: ''
});
expect(await accountPage.getNthTransaction(2)).toMatchObject({
payee: 'Krogger',
notes: '',
category: 'Categorize',
debit: '111.11',
credit: ''
});
});
});

View File

@@ -9,6 +9,7 @@
"@babel/core": "~7.14.3",
"@jlongster/lively": "0.0.4",
"@jlongster/sentry-metrics-actual": "^0.0.10",
"@playwright/test": "^1.29.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.2",
"@reach/listbox": "^0.11.2",
"@reactions/component": "^2.0.2",
@@ -86,6 +87,7 @@
"watch": "cross-env PORT=3001 node scripts/start.js",
"build": "cross-env INLINE_RUNTIME_CHUNK=false node scripts/build.js",
"build:browser": "cross-env ./bin/build-browser",
"e2e": "npx playwright test e2e --browser=chromium",
"lint": "eslint src"
},
"browserslist": [

View File

@@ -0,0 +1,9 @@
module.exports = {
timeout: 20000, // 20 seconds
retries: 1,
use: {
screenshot: 'on',
browserName: 'chromium',
baseURL: process.env.E2E_START_URL ?? 'http://localhost:3001'
}
};

View File

@@ -938,6 +938,7 @@ export function Modal({
bare
onClick={e => onClose()}
style={{ padding: '10px 10px' }}
aria-label="Close"
>
<Delete width={10} />
</Button>

View File

@@ -483,6 +483,7 @@ const MenuButton = withRouter(function MenuButton({ history }) {
}}
activeStyle={{ color: colors.p7 }}
onClick={() => setMenuOpen(true)}
aria-label="Menu"
>
<DotsHorizontalTriple
width={15}

View File

@@ -55,6 +55,7 @@ __metadata:
"@babel/core": ~7.14.3
"@jlongster/lively": 0.0.4
"@jlongster/sentry-metrics-actual": ^0.0.10
"@playwright/test": ^1.29.1
"@pmmmwh/react-refresh-webpack-plugin": ^0.4.2
"@reach/listbox": ^0.11.2
"@react-aria/focus": ^3.8.0
@@ -3216,6 +3217,18 @@ __metadata:
languageName: node
linkType: hard
"@playwright/test@npm:^1.29.1":
version: 1.29.1
resolution: "@playwright/test@npm:1.29.1"
dependencies:
"@types/node": "*"
playwright-core: 1.29.1
bin:
playwright: cli.js
checksum: c4424ee09f6589a296a3a78402f8c9eac841f3e5f44d96e3a2ba07aaa00d53ee9c10cc96379b304743a9e70f231df0ded0ad2fc197f412ada67b4cf17ee24b0f
languageName: node
linkType: hard
"@pmmmwh/react-refresh-webpack-plugin@npm:^0.4.2":
version: 0.4.3
resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.4.3"
@@ -18205,6 +18218,15 @@ __metadata:
languageName: node
linkType: hard
"playwright-core@npm:1.29.1":
version: 1.29.1
resolution: "playwright-core@npm:1.29.1"
bin:
playwright: cli.js
checksum: e1c8423db4d28f44e5365a353f321b04a07709c8bf26d0c908f06d2c2669f715315170f84ca98a532951c2ac6324f5f48f651dca1abe43092c7637bdb4703303
languageName: node
linkType: hard
"please-upgrade-node@npm:^3.2.0":
version: 3.2.0
resolution: "please-upgrade-node@npm:3.2.0"