From 50a71335e6f194d845dd7012f30b3366fd4488ba Mon Sep 17 00:00:00 2001 From: Matiss Janis Aboltins Date: Thu, 12 Jan 2023 22:43:32 +0000 Subject: [PATCH] test: bare-bones e2e testing with playwright (#416) --- .github/actions/setup/action.yml | 2 +- .github/workflows/e2e-test.yml | 41 +++++++ package.json | 1 + packages/desktop-client/.gitignore | 1 + packages/desktop-client/README.md | 22 +++- .../e2e/page-models/account-page.js | 114 ++++++++++++++++++ .../e2e/page-models/configuration-page.js | 10 ++ .../e2e/page-models/navigation.js | 15 +++ .../desktop-client/e2e/transactions.test.js | 84 +++++++++++++ packages/desktop-client/package.json | 2 + packages/desktop-client/playwright.config.js | 9 ++ packages/loot-design/src/components/common.js | 1 + .../loot-design/src/components/sidebar.js | 1 + yarn.lock | 22 ++++ 14 files changed, 323 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/e2e-test.yml create mode 100644 packages/desktop-client/e2e/page-models/account-page.js create mode 100644 packages/desktop-client/e2e/page-models/configuration-page.js create mode 100644 packages/desktop-client/e2e/page-models/navigation.js create mode 100644 packages/desktop-client/e2e/transactions.test.js create mode 100644 packages/desktop-client/playwright.config.js diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index ab8fca8dc5..653fda0c92 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -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' diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 0000000000..b9bb0e0655 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -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 diff --git a/package.json b/package.json index a6bf48b1a4..4338085c6f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/desktop-client/.gitignore b/packages/desktop-client/.gitignore index e30abb299c..ddec6994d2 100644 --- a/packages/desktop-client/.gitignore +++ b/packages/desktop-client/.gitignore @@ -5,6 +5,7 @@ node_modules # testing coverage +test-results # production build diff --git a/packages/desktop-client/README.md b/packages/desktop-client/README.md index c484095576..d061bfb07f 100644 --- a/packages/desktop-client/README.md +++ b/packages/desktop-client/README.md @@ -1 +1,21 @@ -Actual on the web \ No newline at end of file +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 +``` diff --git a/packages/desktop-client/e2e/page-models/account-page.js b/packages/desktop-client/e2e/page-models/account-page.js new file mode 100644 index 0000000000..88bdfbc1db --- /dev/null +++ b/packages/desktop-client/e2e/page-models/account-page.js @@ -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'); + } + } +} diff --git a/packages/desktop-client/e2e/page-models/configuration-page.js b/packages/desktop-client/e2e/page-models/configuration-page.js new file mode 100644 index 0000000000..0121636540 --- /dev/null +++ b/packages/desktop-client/e2e/page-models/configuration-page.js @@ -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(); + } +} diff --git a/packages/desktop-client/e2e/page-models/navigation.js b/packages/desktop-client/e2e/page-models/navigation.js new file mode 100644 index 0000000000..839997652f --- /dev/null +++ b/packages/desktop-client/e2e/page-models/navigation.js @@ -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); + } +} diff --git a/packages/desktop-client/e2e/transactions.test.js b/packages/desktop-client/e2e/transactions.test.js new file mode 100644 index 0000000000..5c96ca63de --- /dev/null +++ b/packages/desktop-client/e2e/transactions.test.js @@ -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: '' + }); + }); +}); diff --git a/packages/desktop-client/package.json b/packages/desktop-client/package.json index d32e590eaa..48a15647f5 100644 --- a/packages/desktop-client/package.json +++ b/packages/desktop-client/package.json @@ -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": [ diff --git a/packages/desktop-client/playwright.config.js b/packages/desktop-client/playwright.config.js new file mode 100644 index 0000000000..d8647df28d --- /dev/null +++ b/packages/desktop-client/playwright.config.js @@ -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' + } +}; diff --git a/packages/loot-design/src/components/common.js b/packages/loot-design/src/components/common.js index dbffd6ca4f..9c6252fa5d 100644 --- a/packages/loot-design/src/components/common.js +++ b/packages/loot-design/src/components/common.js @@ -938,6 +938,7 @@ export function Modal({ bare onClick={e => onClose()} style={{ padding: '10px 10px' }} + aria-label="Close" > diff --git a/packages/loot-design/src/components/sidebar.js b/packages/loot-design/src/components/sidebar.js index 290b5801e2..425e4ed9b1 100644 --- a/packages/loot-design/src/components/sidebar.js +++ b/packages/loot-design/src/components/sidebar.js @@ -483,6 +483,7 @@ const MenuButton = withRouter(function MenuButton({ history }) { }} activeStyle={{ color: colors.p7 }} onClick={() => setMenuOpen(true)} + aria-label="Menu" >