Playwright · React 18 · Tailwind · react-datepicker
The GienTech Wealth Connect Runbook
The React training target: how it is built, what its DOM promises to automation, and how the suite that tests it is put together.
Part I
Orientation
Chapter 01
What this target is
GienTech Wealth Connect is the second build of the same bank-and-insurer: the customer, the four accounts, the three policies, the payees, the claims and every business rule are identical to the Angular build. What changed is everything a test actually touches — the framework, the markup, the widgets and the naming of the hooks automation holds on to.
It was built to a training specification: a React app that mimics the structure, styling conventions and, most importantly, the data-testid conventions of an internal bank admin portal, so that trainees practise locators and flows against a realistic DOM before they meet the real one. Everything in it is a mock — no back end, no real credentials, no real customer data.
| Concern | Angular build | Connect (this build) |
| Framework | Angular 18, Material | React 18, Vite, Tailwind, Flowbite React |
| Test ids | premium-error | bo-packages-div-error-premium |
| Dropdowns | Material overlay (mat-select) | Native <select> in a styled wrapper |
| Date fields | Material datepicker, typed M/D/YYYY | react-datepicker in a portal, typed dd/MM/yyyy |
| Tables | Material table | .sticky-actions table, last column pinned |
| Validation state | Error text only | Text and data-error / data-variant / data-disabled on the field wrapper |
| Login route | #/sign-in | #/bcp/login, the portal's path |
| Session | Component memory | Component memory + a sessionStorage flag that survives reload |
| Transactions | 7 rows, per account | 35 rows, a filterable, paginated page of its own |
| Stored dates | UTC day (a known off-by-one) | Local calendar day — the defect is not repeated |
Read this book alongside the main runbook, not instead of it
The main runbook covers Playwright itself — fixtures, locators, assertions, debugging, CI. This one covers this application: what its DOM promises, how the suite for it is shaped, and what to practise on it. Where the two overlap, the main book is referenced by chapter rather than repeated.
Chapter 02
Running it
The application lives at apps/connect-for-bank inside the automation repository, and ships as one self-contained HTML file. There are three ways to have it in front of you.
The live copy
https://connect-for-bank.129.126.127.126.sslip.io — always the committed bundle. Sign in with Global ID amara, password Passw0rd!.
The bundle the suite tests
npm test -- --project=connect-chromium # the static server serves app/connect-for-bank.html on :8123
npm run serve:app # or just serve it, then open http://127.0.0.1:8123/connect-for-bank.html
app/connect-for-bank.html is committed, so the suite and the deploy need no build step. That file is the artefact under test — the same bytes that are deployed.
Working on the application
npm run app:connect:dev # Vite dev server with hot reload on http://localhost:5173
npm run app:connect:build # vite build → single file, copied to app/connect-for-bank.html
The build inlines every script, style and asset (vite-plugin-singlefile), including react-datepicker's stylesheet. The output is ~740 KB and needs nothing from the network.
The state model, and why it matters to a test
| What | Where it lives | Survives a reload? |
| Accounts, balances, transactions, payees, policies, claims, notices | React state (StoreContext) | No — every load is the pristine seed |
| Failed sign-in count and lockout | React state | No |
| “Signed in” flag | sessionStorage (connect-for-bank.authed) | Yes, within the tab |
| Theme, drafts, filters | Component state | No |
A reload is a reset. Navigate by clicking.
Because the data is in memory, page.goto() in the middle of a test throws away every transfer, payee and claim the test made — while the session flag keeps you signed in, which makes the loss easy to miss. Page objects navigate through the sidebar, the app bar and the buttons; open() is an entry point only.
One consequence of the session flag being in sessionStorage: Playwright's storageState cannot carry it (it captures cookies and localStorage), so the suite signs in through the UI in every test. That costs about 300 ms and buys perfect isolation.
Chapter 03
The screens
Fourteen screens, hash-routed so the single file works from any host or from disk. Every screen announces itself with an <h1> carrying bo-packages-h1-<page>; that is the element page objects wait on.
| Route | Readiness id | What a test does here |
/bcp/login | bo-packages-h1-bcp-login-header-app-name | Sign in, lockout, the field state machine |
/dashboard | bo-packages-h1-dashboard | Derived totals, quick actions, recent transactions |
/accounts | bo-packages-h1-accounts | The sticky table, badges, balances in their own currency |
/accounts/:id | bo-packages-h1-account-detail | Search and direction filter over the account's history |
/transactions | bo-packages-h1-transactions | Drafted filters, the datepicker range, pagination, the modal |
/transfer | bo-packages-h1-transfer | Four refusal rules, the fee, the confirmation step |
/payees | bo-packages-h1-payees | Format and duplicate rules, data-variant on the form |
/policies, /policies/:id | …-h1-policies, …-h1-policy-detail | Status filter, lapse warning, expansion panels |
/premium | bo-packages-h1-premium | Dormant, SGD-only, partial, reinstatement |
/claims, /claims/new, /claims/:id | …-h1-claims, …-h1-claim-new, …-h1-claim-detail | Five stacked rules, the dependent dropdown, the upload |
/profile | bo-packages-h1-profile | Email and phone validation, the name reaching the app bar |
/notifications | bo-packages-h1-notifications | The activity log as an independent witness |
Part II
The DOM
Chapter 04
The id convention
Every meaningful element carries a data-testid of the form bo-packages-<role>-<context>-<element>. The prefix is kept verbatim from the portal this mimics because it looks like it comes from an internal component library — it does, there — and the point of training on it is to learn to read a pattern rather than memorise strings.
One helper on each side
// The application — src/testid.js. Every id in the app goes through this.
export const tid = (...parts) =>
['bo-packages', ...parts.filter((p) => p !== undefined && p !== null && p !== '')].join('-');
<tr data-testid={tid('row-transaction', txn.id)}> // bo-packages-row-transaction-35
<Button testid={['button-view-transaction', txn.id]}> // bo-packages-button-view-transaction-35
// The suite — ConnectBasePage. Specs never type the prefix.
protected bo(...parts: (string | number)[]): Locator {
return this.page.getByTestId(['bo-packages', ...parts].join('-'));
}
Three rules the ids keep
- Deterministic. No random suffixes, no timestamps. The same element has the same id on every run and on every machine.
- Derived from data, not position.
row-transaction-35 stays with transaction 35 after a sort, a filter or a new row above it. row-3 would not.
- Roles are literal.
input- is an <input>, select- a <select>, button- a <button>, td- a table cell. A reader of the id knows what to expect before opening DevTools.
The exception: the login card
The login screen's ids are copied from the training specification verbatim rather than composed — bo-packages-form-provider-form-button-verify-login-button, bo-packages-div-bcp-login-header-welcome-message-2. They do not follow the three-part grammar, and some are duplicated on purpose (both field wrappers are bo-packages-div-is-required / bo-packages-div-label), because that is what the portal does. The page object spells them out and says so; Appendix A lists all twenty-two.
Duplicate ids are not a bug here — they are the lesson
When two wrappers share bo-packages-div-label, getByTestId is a strict-mode violation. The way through is containment: the wrapper that has this input. Chapter 5 shows the helper; Chapter 13 of the main runbook explains strict mode.
Chapter 05
The field wrapper: a state machine in data attributes
Every form field is rendered by one component, InputField, whose bordered container carries three attributes. Tailwind's data-[…] variants read those attributes to paint the border, so the visible state and the DOM state are the same thing — an assertion on the attribute is an assertion on what the customer sees.
| Attribute | Values | Drives (Tailwind) | Set when |
data-error | true / false | data-[error=true]:border-danger, …:placeholder-danger | A required field is left empty on blur; the amount is refused |
data-variant | normal / succeed / failed | data-[variant=succeed]:border-success, data-[variant=failed]:border-error | A submit is refused or accepted; typing again returns to normal |
data-disabled | true / false | data-[disabled=true]:bg-neutral-300, …:cursor-not-allowed | The account is locked after three failed sign-ins |
Reaching the wrapper
The wrapper's id is the same on every field (bo-packages-div-label), so it is found by what it contains:
fieldWrapper(control: Locator): Locator {
return this.page.locator('[data-testid="bo-packages-div-label"]', { has: control });
}
get globalIdWrapper(): Locator { return this.fieldWrapper(this.globalIdInput); }
The transitions, as tests
// error: blur an empty required field
await loginPage.globalIdInput.focus();
await loginPage.globalIdInput.blur();
await expect(loginPage.globalIdWrapper).toHaveAttribute('data-error', 'true');
// failed: a refused submit marks BOTH wrappers
await loginPage.signIn('amara', 'WrongPassword!');
await expect(loginPage.globalIdWrapper).toHaveAttribute('data-variant', 'failed');
await expect(loginPage.passwordWrapper).toHaveAttribute('data-variant', 'failed');
// back to normal: typing
await loginPage.passwordInput.fill('retyping');
await expect(loginPage.passwordWrapper).toHaveAttribute('data-variant', 'normal');
// disabled: the lockout
await loginPage.failSignIn(3);
await expect(loginPage.globalIdWrapper).toHaveAttribute('data-disabled', 'true');
await expect(loginPage.globalIdInput).toBeDisabled();
// succeed: the payee form after a valid add
await payeesPage.addPayee(PayeeBuilder.valid().build());
await expect(payeesPage.nameWrapper).toHaveAttribute('data-variant', 'succeed');
The submit button's disabled attribute is driven by the same real state: it is disabled until both login fields are non-empty and re-disables when one is cleared. Assert the attribute, not a class name.
Chapter 06
The widgets
Native selects
Every dropdown is a real <select> inside the field wrapper, with bo-packages-select-<base> on the element and bo-packages-option-<base>-<value> on each option. One call does everything the Material overlay driver needed six lines for:
await page.getByTestId('bo-packages-select-premium-from-account').selectOption('usd');
await page.getByTestId('bo-packages-select-claim-type').selectOption({ label: 'Day surgery' });
The claim-type select is dependent: its options change with the policy and reset to “Choose a type” when the policy changes. Select the policy first.
react-datepicker
Date fields are the genuine library — the same .react-datepicker__* DOM as the portal — wrapped in the field shell. Three facts govern every interaction:
- The popup is a portal. It renders into
#datepicker-portal at the end of <body>, not beside the input. A locator scoped to the filter bar never finds a day cell.
- The format is dd/MM/yyyy. Typing
01/09/2026 and pressing Enter sets 1 September. Typing the American order sets January.
- Neighbouring months are rendered too, greyed. Selecting “1” without excluding
.react-datepicker__day--outside-month can pick the wrong month.
// Both ways in, from src/pages/connect/components/react-datepicker.component.ts
await transactionsPage.typeFromDate(new Date(2026, 8, 1)); // fill + Enter — the fast default
await transactionsPage.pickFromDate(new Date(2026, 8, 1)); // open, navigate, click — covers the widget
The sticky-actions table
Every table sits in a .sticky-actions wrapper; the last column (Actions) is position: sticky; right: 0. Header cells carry bo-packages-th-<column>, rows bo-packages-row-<kind>-<id>. The contract worth asserting is the CSS, not the presence of a column:
await expect(accountsPage.actionsCell('sav')).toHaveCSS('position', 'sticky');
Modal and toasts
The transaction detail modal is bo-packages-modal-transaction-detail, fields …-modal-transaction-detail-value-<name>, closed by bo-packages-modal-button-close, Escape, or the backdrop. Toasts render into one fixed container, bo-packages-toast-container, as bo-packages-toast-<kind> with the text in …-toast-<kind>-message. They dismiss themselves after six seconds — assert promptly.
Buttons
Buttons are Flowbite React's, so the <button> carries group relative flex items-stretch justify-center p-0.5 … and the label sits in a nested <span>. The enabled: / disabled: variants switch with the attribute, not with a second class — toBeDisabled() is the right assertion.
Chapter 07
The transactions page
The page the specification designed to exercise both libraries. Thirty-five deterministic rows, every account, newest first — and among same-day rows the highest id first, so the order is total and never flakes.
Facts the tests are built on
| Fact | Value |
| Total rows | 35 |
| By status | Completed 25 · Pending 7 · Failed 3 |
| By account | sav 18 · cur 13 · eur 2 · usd 2 |
| Date range | 2026-08-14 → 2026-09-10 |
| Rows dated 1–5 September inclusive | 11 |
| Page size · pages | 10 · 4 (the last page holds 5) |
| First row on page 1 | id 35, TXN-2026-000135, 2026-09-10, Pending |
| Last row on page 4 | id 5, CLM-30288, 2026-08-14 |
| Search “invoice” | 3 rows |
These live in src/data/connect-data.ts. They are stated, not computed, on purpose: they are the numbers the seed was designed to produce, and a test that fails because one changed is doing its job.
Ids on this page
| Element | Id |
| Filter bar | bo-packages-transactions-filter-bar |
| Search / status | bo-packages-input-transactions-search / bo-packages-select-transactions-status |
| From / to dates | bo-packages-datepicker-transactions-from / …-to |
| Apply / Reset | bo-packages-transactions-button-apply / …-reset |
| Result count | bo-packages-transactions-span-result-count |
| Table, header cells | bo-packages-table-transactions, bo-packages-th-<column> |
| Row, cells, badge, View | …-row-transaction-<id>, …-td-transaction-<field>-<id>, …-badge-status-<id>, …-button-view-transaction-<id> |
| Pager | …-transactions-pagination, …-button-prev, …-button-next, …-button-page-<n>, …-span-page-summary |
| Empty state | bo-packages-p-transactions-empty |
| Modal | bo-packages-modal-transaction-detail, …-modal-transaction-detail-value-<field>, bo-packages-modal-button-close |
The dashboard's “View” buttons deep-link here as /transactions?view=<id>, which opens the modal on arrival — a second entry point worth a test of its own.
Part III
Testing it
Chapter 08
Business rules, and where they surface
The rules and their wording are identical to the Angular build, so the suite's business-rules.ts is shared. What this table adds is where each rule shows itself in this DOM.
| # | Rule | Screen | Message appears in |
| 1 | Three wrong passwords lock the account | Login | …-p-bcp-login-error, …-toast-error-message; wrappers data-disabled=true |
| 2 | The error never says which half was wrong | Login | same |
| 3 | A transfer above the balance is refused, naming the balance | Transfer | bo-packages-div-error-transfer |
| 4 | An unverified payee needs an acknowledgement | Transfer | same; checkbox …-checkbox-transfer-ack-unverified |
| 5 | Payee account number must match 123-45678-9 | Payees | bo-packages-div-error-payee; wrappers data-variant=failed |
| 6 | A duplicate payee account number is refused | Payees | same |
| 7 | A dormant account cannot pay a premium (checked first) | Premium | bo-packages-div-error-premium |
| 8 | Premiums are collected in SGD only | Premium | same |
| 9 | Partial premiums refused; a lapsed policy owes two | Premium | same; …-div-premium-outstanding shows the doubled figure |
| 10 | Paying in full reinstates a lapsed policy | Premium → Policies | …-badge-policy-status-pol3 reads Active |
| 11 | A lapsed policy provides no cover | Claim form | bo-packages-div-error-claim |
| 12 | An incident dated in the future is refused | Claim form | same |
| 13 | Claims over SGD 1,000 need a document (1,000 exactly does not) | Claim form | same; hint …-p-claim-document-hint |
| 14 | A claim cannot exceed the sum assured | Claim form | same |
| 15 | The declaration must be ticked | Claim form | same; …-checkbox-claim-declaration |
Two rules cannot be reached through this UI and are therefore not tested here: transferring an account to itself (the destination list excludes the source) and choosing a bank that is not on the list (the bank is a closed <select>; the test proves it by leaving the placeholder selected).
Chapter 09
The suite
The base page
export abstract class ConnectBasePage {
protected constructor(protected readonly page: Page, private readonly titleKey: string, private readonly route: string) {}
protected bo(...parts: (string | number)[]): Locator { // composes an id
return this.page.getByTestId(['bo-packages', ...parts].join('-'));
}
get title(): Locator { return this.bo('h1', this.titleKey); } // readiness
async expectLoaded() { await expect(this.title).toBeVisible(); }
fieldWrapper(control: Locator): Locator { /* Chapter 5 */ }
protected select(base: string) { return this.bo('select', base); }
protected input(base: string) { return this.bo('input', base); }
protected errorBox(context: string) { return this.bo('div-error', context); }
}
Unlike the Angular base page, locators are not scoped to a screen root: the ids are globally unique by construction, and React unmounts the outgoing screen synchronously, so there is no outgoing DOM to match against.
Files
| Path | Holds |
src/pages/connect/connect-base.page.ts | The base class above |
src/pages/connect/components/app-shell.component.ts | Sidebar navigation, user menu, notification badge, toast accessors |
src/pages/connect/components/react-datepicker.component.ts | type(), pick(), clear(), format() |
src/pages/connect/*.page.ts | Login, dashboard, accounts (+ detail), transactions, transfer, payees, policies (+ detail), premium, claims (+ new, detail), profile, notifications |
src/fixtures/connect-fixtures.ts | The custom test: one fixture per page object, signedIn, pageErrors |
src/data/connect-data.ts | Credentials and the transaction facts of Chapter 7 |
tests/connect/*.spec.ts | login · dashboard · accounts · transactions · transfer · payees · insurance · platform |
Running it
npm run test:connect # chromium, ~15 s
npm run test:connect:all # chromium + firefox + webkit, 264 runs, ~30 s
npx playwright test --project=connect-chromium tests/connect/transactions.spec.ts
npx playwright test --project=connect-chromium --grep "@smoke"
npx playwright test --project=connect-chromium --ui
# The same suite against the LIVE deployment — the bundle is identical, the transport is not.
CONNECT_APP_URL=https://connect-for-bank.129.126.127.126.sslip.io/ npm run test:connect
npm run test:connect:live # the same, as a script
CONNECT_APP_URL points the connect-* projects at any deployment of the file without touching the Angular projects or the local servers. Run it after every deploy: it is the difference between “the file passed on my machine” and “the site works”.
Tags: every test carries @connect; the build-gating subset carries @smoke. The three projects need no setup project — the session lives in sessionStorage, so each test signs in through the UI.
Chapter 10
Writing a test, start to finish
Requirement: a duplicate payee account number is refused, the register is unchanged, and the form shows the failed state.
- Page object.
ConnectPayeesPage already has addPayee(), errorMessage, rows and nameWrapper. Nothing to add.
- Rule.
payeeRules.duplicateAccount exists in the shared file.
- Data.
PayeeBuilder.valid().withAccountNumber(payees.verifiedLandlord.account) — valid in every respect except the one under test.
- Spec.
import { test, expect } from '@fixtures/connect-fixtures';
import { allPayees, payees } from '@data/test-data';
import { payeeRules } from '@data/business-rules';
import { PayeeBuilder } from '@data/builders/payee.builder';
test('refuses a duplicate account number and marks the form failed @connect', async ({ signedIn, payeesPage }) => {
await signedIn.managePayees();
await payeesPage.expectLoaded();
await payeesPage.addPayee(PayeeBuilder.valid().withAccountNumber(payees.verifiedLandlord.account).build());
await expect(payeesPage.errorMessage).toHaveText(payeeRules.duplicateAccount);
await expect(payeesPage.nameWrapper).toHaveAttribute('data-variant', 'failed');
await expect(payeesPage.rows).toHaveCount(allPayees.length); // what must NOT have happened
});
Four checks before it is done: the title states a requirement; every literal is a fixture, a rule or a builder call; it asserts what happened and what must not have; and it would fail if the rule were deleted.
Chapter 11
Manual test cases to automate
Fifty test cases covering every feature of the application, written the way a manual tester would execute them — preconditions, numbered steps, an expected result per step — and each ending with the ids it needs. The intended use: run one by hand first, then automate it, then compare with the suite's version where one exists.
How to read a case
Ids are given without the bo-packages- prefix; <id> means the entity's own id. Money in expected results is the exact rendered string. Dates are relative (“7 days ago”) so the case stays true; the app's date fields take dd/MM/yyyy. Every case starts from a fresh page load, which resets the data — so cases are independent and can be run in any order.
Coverage by area: authentication 8 · dashboard 3 · accounts 3 · transactions 8 · transfer 6 · payees 3 · policies and premiums 6 · claims 6 · profile and notifications 2 · end-to-end journeys 5.
Authentication
| TC-AUTH-01 | Sign in with valid credentials |
| Pre | Signed out; app opened at /#/bcp/login. |
| 1 | Open the application | Login card shows “Welcome to” and “GienTech Wealth”; Log in button is disabled |
| 2 | Type amara in Global ID | Button still disabled |
| 3 | Type Passw0rd! in Password | Button becomes enabled |
| 4 | Click Log in | Dashboard loads; h1 reads “Good morning, Amara Devi”; app bar shows the name; URL ends #/dashboard |
| Ids | input-global-id · input-password · form-provider-form-button-verify-login-button · h1-dashboard · appbar-user-name |
| TC-AUTH-02 | Required field marks itself on blur |
| Pre | Signed out. |
| 1 | Click into Global ID, then click elsewhere without typing | The Global ID wrapper turns red: data-error="true" |
| 2 | Type one character | data-error returns to "false" |
| Ids | input-global-id · the wrapper found by containment (div-label) |
| TC-AUTH-03 | Wrong password is refused without saying which half was wrong |
| Pre | Signed out. |
| 1 | Sign in with amara / WrongPassword! | Toast and inline message: “That username and password do not match. 2 attempt(s) remaining.” Both wrappers show data-variant="failed" |
| 2 | Sign in with nobody / WrongPassword! | Same wording — the message never distinguishes an unknown ID from a wrong password |
| 3 | Type in the password field | Both wrappers return to data-variant="normal" |
| Ids | toast-error-message · p-bcp-login-error · div-label |
| TC-AUTH-04 | Three failures lock the account |
| Pre | Signed out. |
| 1 | Fail sign-in twice | Message counts down: 2 remaining, then 1 |
| 2 | Fail a third time | “Your account is locked after 3 failed attempts. Call 1800 248 2888 to unlock.” |
| 3 | Inspect the form | Both inputs disabled; wrappers data-disabled="true"; Log in disabled |
| 4 | Enter the correct password | Nothing can be submitted — the lock holds for the session |
| Ids | input-global-id · input-password · form-provider-form-button-verify-login-button |
| TC-AUTH-05 | Password visibility toggle |
| Pre | Signed out. |
| 1 | Inspect the password input | type="password" |
| 2 | Click the eye button | type="text"; button aria-pressed="true" |
| 3 | Click it again | type="password" |
| Ids | button-toggle-password |
| TC-AUTH-06 | Forgot password points to a branch |
| Pre | Signed out. |
| 1 | Click Forgot password | Info toast: “Password resets are handled at a branch.” |
| Ids | button-button-forgot-password · toast-info-message |
| TC-AUTH-07 | Deep links are guarded |
| Pre | Signed out. |
| 1 | Open /#/transactions directly | Redirected to /#/bcp/login |
| 2 | Sign in, then open /#/bcp/login directly | Redirected to /#/dashboard — an authed user never sees the card |
| Ids | h1-bcp-login-header-app-name · h1-dashboard |
| TC-AUTH-08 | Log out |
| Pre | Signed in. |
| 1 | Open the user menu (app bar, right) | Menu shows My profile and Log out |
| 2 | Click Log out | Login card; app bar gone; a deep link to /#/dashboard now redirects back to login |
| Ids | appbar-user-menu · appbar-menu-logout · appbar |
Dashboard
| TC-DASH-01 | Summary cards are derived correctly |
| Pre | Signed in, dashboard. |
| 1 | Read Total Balance | SGD 60,520.55 — the two SGD accounts only; USD and EUR are not converted |
| 2 | Read Total Sum Assured | SGD 700,000 — active policies only; the lapsed motor policy is excluded |
| 3 | Read Next Premium Due | 2026-09-28 — the earliest due date among active policies |
| 4 | Read Pending Transactions | 7 |
| Ids | card-total-balance-value · card-total-cover-value · card-next-premium-due-value · card-pending-transactions-value |
| TC-DASH-02 | Recent transactions preview |
| Pre | Signed in, dashboard. |
| 1 | Scroll to Recent transactions | Five rows, newest first; the first is TXN-2026-000135 |
| 2 | Scroll the table horizontally on a narrow window | The Actions column stays pinned to the right edge |
| 3 | Click View on the first row | Transactions page opens with the detail modal for that row already showing |
| Ids | table-recent-transactions · row-recent-transaction-<id> · button-view-transaction-<id> · modal-transaction-detail |
| TC-DASH-03 | Quick actions and links |
| Pre | Signed in, dashboard. |
| 1 | Click each quick action in turn | Transfer money → /transfer; Pay a premium → /premium; File a claim → /claims/new; Manage payees → /payees |
| 2 | Return and click See all accounts / See all policies / View all | Accounts, Policies and Transactions pages respectively |
| Ids | button-quick-transfer · button-quick-pay-premium · button-quick-file-claim · button-quick-payees · link-see-all-accounts · link-see-all-policies · link-view-all-transactions |
Accounts
| TC-ACC-01 | Account list |
| Pre | Signed in. |
| 1 | Open Accounts from the sidebar | Four rows; each shows name, number, type, a status badge and the balance in its own currency (USD 6,250.75, EUR 9,840.20) |
| 2 | Inspect the dormant account | Row is present with a Dormant badge — it is shown, not hidden |
| Ids | nav-accounts · table-accounts · row-account-<id> · badge-account-status-<id> · td-account-balance-<id> |
| TC-ACC-02 | Account detail facts |
| Pre | Signed in, Accounts. |
| 1 | Click Open on Everyday Savings | Detail shows name, number 0012-3456-7890, Savings, Active, Orchard, GNTCSGSG, opened 2019-04-11, balance SGD 18,420.55 |
| Ids | button-open-account-sav · dd-account-detail-<key> |
| TC-ACC-03 | History filters |
| Pre | Signed in, Everyday Savings detail. |
| 1 | Read the history | 18 rows |
| 2 | Set Direction to Credit | 8 rows; SP Utilities is gone |
| 3 | Set Direction to Debit | 10 rows |
| 4 | Set Direction back to All | 18 rows |
| 5 | Type Salary in the search box | 1 row (search filters as you type here — unlike the Transactions page) |
| 6 | Type SAL-88213 | Same single row, found by reference |
| 7 | Type nothing-like-this | No rows; “No transactions match this search.” |
| Ids | select-txn-direction · input-txn-search · table-transaction-history · p-txn-empty |
Transactions
| TC-TXN-01 | Default view and ordering |
| Pre | Signed in. |
| 1 | Open Transactions from the sidebar | “35 transactions”; ten rows; “Page 1 of 4”; Prev disabled |
| 2 | Read the dates down the page | Descending; same-day rows have the higher id first (row 35 above 34) |
| Ids | nav-transactions · transactions-span-result-count · table-transactions · transactions-span-page-summary · transactions-button-prev |
| TC-TXN-02 | Pagination |
| Pre | Signed in, Transactions. |
| 1 | Click Next | Page 2 of 4; Prev enabled |
| 2 | Click page 4 | Five rows; Next disabled; last row is CLM-30288 dated 2026-08-14 |
| 3 | Click page 3 | Page button 3 carries aria-current="page"; button 1 does not |
| Ids | transactions-button-next · transactions-button-page-<n> · transactions-button-prev |
| TC-TXN-03 | Filters are drafted until Apply |
| Pre | Signed in, Transactions. |
| 1 | Choose Status = Failed. Do NOT click Apply | Count still reads 35 transactions |
| 2 | Click Apply | 3 transactions; every visible badge reads Failed |
| 3 | Choose Pending, click Apply | 7 transactions |
| Ids | select-transactions-status · transactions-button-apply · badge-status-<id> |
| TC-TXN-04 | Search |
| Pre | Signed in, Transactions. |
| 1 | Type SAL-88213, Apply | 1 row, the salary credit |
| 2 | Type invoice, Apply | 3 rows (search is case-insensitive over reference and description) |
| Ids | input-transactions-search · transactions-button-apply |
| TC-TXN-05 | Date range, typed |
| Pre | Signed in, Transactions. |
| 1 | Type 01/09/2026 into From, press Enter | Field shows 01/09/2026; calendar closed |
| 2 | Type 05/09/2026 into To, press Enter, click Apply | 11 transactions; every date between 2026-09-01 and 2026-09-05 |
| Ids | datepicker-transactions-from · datepicker-transactions-to |
| TC-TXN-06 | Date range, picked from the calendar |
| Pre | Signed in, Transactions. |
| 1 | Click the From field | A react-datepicker popup opens — inside #datepicker-portal, not inside the filter bar |
| 2 | Navigate to September 2026 if needed and click 1 | Field reads 01/09/2026 |
| 3 | Pick 5 in the To field, click Apply | 11 transactions |
| Ids | .react-datepicker · .react-datepicker__navigation--next · .react-datepicker__day (exclude --outside-month) |
| TC-TXN-07 | Reset |
| Pre | Signed in, Transactions with search invoice + status Failed applied. |
| 1 | Observe | No rows; “No transactions match these filters.” |
| 2 | Click Reset | Search empty, Status All, dates cleared, 35 transactions, page 1 |
| Ids | transactions-button-reset · p-transactions-empty |
| TC-TXN-08 | Detail modal |
| Pre | Signed in, Transactions. |
| 1 | Click View on TXN-2026-000135 | Modal shows reference, date 2026-09-10, account 0022-8899-1010, amount −SGD 2,600.00, Pending, description, initiator j.tan, approver — |
| 2 | Click × | Modal closes |
| 3 | Open it again, press Escape | Modal closes |
| 4 | Open it again, click the dark backdrop | Modal closes |
| Ids | button-view-transaction-35 · modal-transaction-detail · modal-transaction-detail-value-<field> · modal-button-close |
Transfer
| TC-TRF-01 | Transfer between own accounts |
| Pre | Signed in. |
| 1 | Open Transfer; From = Everyday Savings; To my own account; To = Business Current; amount 250; reference “Monthly top-up”; Continue | Confirmation shows From, To, SGD 250.00, Fee SGD 0.00, Total debited SGD 250.00 |
| 2 | Click Confirm Transfer | “Transfer Successful!”; message names Business Current; reference matches TFR-nnnnnn |
| 3 | Click Done, open Accounts | Everyday Savings balance is SGD 18,170.55 |
| Ids | select-transfer-from-account · radio-transfer-kind-own · select-transfer-to-account · input-transfer-amount · input-transfer-reference · transfer-button-continue · dd-transfer-confirm-<key> · transfer-button-confirm · span-transfer-reference |
| TC-TRF-02 | Transfer to a payee carries a fee |
| Pre | Signed in, Transfer. |
| 1 | Choose To a saved payee; Payee = Lim Wei Jie; amount 400; Continue | Fee SGD 0.50; Total debited SGD 400.50 |
| 2 | Confirm, Done, open Accounts | Everyday Savings is SGD 18,020.05 — amount plus fee |
| Ids | radio-transfer-kind-payee · select-transfer-payee · dd-transfer-confirm-fee · p-transfer-confirm-total |
| TC-TRF-03 | Insufficient funds names the balance |
| Pre | Signed in, Transfer. |
| 1 | Own account, Business Current, amount 19420.55, Continue | “Insufficient funds. Everyday Savings holds SGD 18420.55.” — no confirmation panel |
| 2 | Payee Lim Wei Jie, amount 18420.55 (exactly the balance), Continue | Refused for the same reason — the SGD 0.50 fee tips it over |
| Ids | div-error-transfer · div-transfer-confirm (absent) |
| TC-TRF-04 | Unverified payee needs an acknowledgement |
| Pre | Signed in, Transfer. |
| 1 | Payee = Lim Wei Jie | No acknowledgement checkbox |
| 2 | Payee = Kaur Renovations (unverified) | Checkbox appears |
| 3 | Amount 200, Continue without ticking | “Acknowledge the unverified payee before continuing.” |
| 4 | Tick the box, Continue | Confirmation panel; To = Kaur Renovations |
| Ids | checkbox-transfer-ack-unverified · div-error-transfer |
| TC-TRF-05 | Zero amount, reference cap, Back |
| Pre | Signed in, Transfer. |
| 1 | Amount 0, Continue | “Enter an amount greater than zero.”; the amount wrapper shows data-error="true" |
| 2 | Type 50 X characters into the reference | Field holds 35; hint reads 35/35 |
| 3 | Amount 75, Continue, then Back | Form returns with 75 still in the amount field |
| Ids | input-transfer-amount · hint-transfer-reference · transfer-button-back |
| TC-TRF-06 | Scheduled date from the calendar |
| Pre | Signed in, Transfer. |
| 1 | Click the Transfer on field | Calendar opens in the portal; dates before today are disabled |
| 2 | Pick a date two weeks ahead, fill the rest, Continue | Confirmation panel shows |
| Ids | datepicker-transfer-date |
Payees
| TC-PAY-01 | Payee register |
| Pre | Signed in. |
| 1 | Open Payees | Three rows; Lim Wei Jie Verified Yes, Kaur Renovations Verified No, Kaur's nickname shows “—” |
| Ids | table-payees · td-payee-verified-<id> · td-payee-nickname-<id> |
| TC-PAY-02 | Add a payee |
| Pre | Signed in, Payees. |
| 1 | Name Jane Tan, Bank DBS, Account 234-56789-0, Nickname Plumber; click Add payee | “Payee added. It must be verified before large transfers.”; a fourth row, Verified No, nickname Plumber |
| 2 | Inspect the form | Fields cleared; the name wrapper shows data-variant="succeed" |
| Ids | input-payee-name · select-payee-bank · input-payee-account · input-payee-nickname · payee-button-add · div-success-payee |
| TC-PAY-03 | Refusals |
| Pre | Signed in, Payees. |
| 1 | Leave the name empty, valid bank and account, Add | “Enter the payee name.”; wrappers data-variant="failed" |
| 2 | Name filled, leave Bank at “Choose a bank”, Add | “Choose a bank from the list.” |
| 3 | Account 12345678, Add | “Account number must look like 123-45678-9.” |
| 4 | Account 1234-56789-0, Add | Same — the groups are the wrong length |
| 5 | Account 123-45678-9 (Lim Wei Jie's), Add | “A payee with that account number already exists.”; still three rows |
| Ids | div-error-payee · table-payees |
Policies and premiums
| TC-POL-01 | Policy list and filter |
| Pre | Signed in. |
| 1 | Open Policies | Three rows with product, number, type, status badge, premium |
| 2 | Filter = Lapsed | Only DriveSafe Comprehensive |
| 3 | Filter = Active | Two rows; the lapsed one is gone |
| 4 | Filter = All | Three rows |
| Ids | select-policy-status-filter · row-policy-<id> · badge-policy-status-<id> |
| TC-POL-02 | Policy detail |
| Pre | Signed in, Policies. |
| 1 | View PruLife Secure | Sum Assured SGD 500,000; Premium Monthly SGD 312.40; Next Due 2026-09-28; Life Assured Amara Devi; Beneficiary Ravi Devi; no lapse warning |
| 2 | Expand What this covers, then What it does not cover | Benefit and exclusion lists appear (they are absent from the DOM until expanded) |
| 3 | Back to policies; View DriveSafe Comprehensive | Red warning: “This policy has lapsed…a claim cannot be filed against it…” |
| Ids | button-open-policy-<id> · dd-policy-detail-<key> · button-policy-panel-benefits · ul-policy-panel-benefits · div-policy-lapsed-warning |
| TC-PRM-01 | Outstanding amount |
| Pre | Signed in, Pay premium. |
| 1 | Policy = PruLife Secure | Outstanding SGD 312.40 |
| 2 | Policy = DriveSafe Comprehensive (Lapsed) | Outstanding SGD 290.00 — twice the quarterly premium |
| Ids | select-premium-policy · div-premium-outstanding |
| TC-PRM-02 | Pay a premium in full |
| Pre | Signed in, Pay premium. |
| 1 | PruLife Secure, from Everyday Savings, amount 312.40, Pay Premium | “Premium Paid”; SGD 312.40 paid towards PruLife Secure; receipt PRM-nnnnnn; Next premium due 2026-10-28 |
| 2 | Back to policies, then Accounts | Everyday Savings is SGD 18,108.15 |
| Ids | select-premium-from-account · input-premium-amount · premium-button-pay · span-premium-receipt · span-premium-next-due |
| TC-PRM-03 | Refusals, in order |
| Pre | Signed in, Pay premium, PruLife Secure. |
| 1 | From = Multi-Currency USD (dormant), 312.40, Pay | “Multi-Currency USD is dormant and cannot be used for payments. Reactivate it at a branch.” — dormancy is checked before currency |
| 2 | From = Multi-Currency EUR (active), 312.40, Pay | “Premiums are collected in SGD. Choose an SGD account.” |
| 3 | From = Everyday Savings, 100, Pay | “Partial payments are not accepted. SGD 312.40 is outstanding on this policy.” |
| 4 | Policy DriveSafe, 145, Pay | Partial — SGD 290.00 is outstanding |
| 5 | Amount empty, Pay | “Enter the amount you are paying.” |
| Ids | div-error-premium |
| TC-PRM-04 | Reinstate a lapsed policy |
| Pre | Signed in, Pay premium. |
| 1 | DriveSafe Comprehensive, Everyday Savings, 290, Pay | Premium Paid |
| 2 | Back to policies | DriveSafe's badge now reads Active |
| Ids | premium-button-done · badge-policy-status-pol3 |
Claims
| TC-CLM-01 | Claim register and detail |
| Pre | Signed in. |
| 1 | Open Claims | Two rows: CLM-30288 Approved, CLM-31904 Assessing |
| 2 | View CLM-31904 | Reference, Assessing, Health — day surgery, SGD 3,250.00, incident 2026-08-18, submitted 2026-08-20, reason, “Documents: discharge-summary.pdf, invoice.pdf” |
| Ids | table-claims · badge-claim-status-<id> · button-open-claim-<id> · dd-claim-detail-<key> · p-claim-detail-documents |
| TC-CLM-02 | File a claim under the threshold |
| Pre | Signed in, Claims → File a claim. |
| 1 | Policy GreatCare Hospital; type Day surgery; incident 7 days ago (typed dd/MM/yyyy); amount 600; reason; reimburse to Everyday Savings; tick the declaration; Submit | “Claim Submitted”; reference CLM-nnnnnn |
| 2 | Click View my claims | A new first row with that reference and status Submitted |
| 3 | View it | Incident date is the date typed — not the day before |
| Ids | select-claim-policy · select-claim-type · datepicker-claim-incident-date · input-claim-amount · checkbox-claim-declaration · claim-button-submit · span-claim-reference · claim-button-done |
| TC-CLM-03 | Claim types depend on the policy |
| Pre | Signed in, File a claim. |
| 1 | Policy = GreatCare Hospital, open the type list | Hospitalisation · Day surgery · Outpatient · Critical illness |
| 2 | Policy = PruLife Secure | Type resets; list is Death benefit · Terminal illness · Total permanent disability |
| 3 | Policy = DriveSafe Comprehensive | Accident damage · Theft · Third party liability · Windscreen |
| Ids | select-claim-type · option-claim-type-<value> |
| TC-CLM-04 | Large claim needs a document |
| Pre | Signed in, File a claim, otherwise valid. |
| 1 | Amount 3500, no document, Submit | “A supporting document is required for claims over SGD 1,000.” |
| 2 | Amount 1000, Submit | Accepted — exactly 1,000 is not “over” |
| 3 | New claim: amount 3500, attach a PDF, Submit | File name listed under Attach a document; claim submitted |
| 4 | View my claims → View the new claim | Documents: <the file name> |
| Ids | input-claim-file (hidden — set files directly) · ul-claim-documents · li-claim-document-<n> · p-claim-document-hint |
| TC-CLM-05 | Refusals |
| Pre | Signed in, File a claim. |
| 1 | Policy DriveSafe (lapsed), any type, Submit | “DriveSafe Comprehensive has lapsed and provides no cover. Pay the outstanding premium before claiming.” |
| 2 | GreatCare, no type, Submit | “Choose the type of claim.” |
| 3 | Type chosen, no date, Submit | “Enter the date of the incident.” |
| 4 | Incident 30 days in the future, Submit | “The incident date cannot be in the future.” |
| 5 | Amount 0, Submit | “Enter the amount you are claiming.” |
| 6 | Amount 200001 with a document, Submit | “The amount claimed exceeds the sum assured of SGD 200,000.” |
| 7 | Valid claim, declaration unticked, Submit | “Tick the declaration to submit this claim.” |
| Ids | div-error-claim |
| TC-CLM-06 | Cancel |
| Pre | Signed in, File a claim. |
| 1 | Type an amount, click Cancel | Back on the register; still two rows |
| Ids | claim-button-cancel · table-claims |
Profile and notifications
| TC-PROF-01 | Profile |
| Pre | Signed in. |
| 1 | Open My profile from the user menu | Name Amara Devi, email amara.devi@example.sg, phone +65 9123 4567; claim alerts ticked |
| 2 | Change the name to Amara D. Devi, Save | “Your details have been updated.”; app bar shows Amara D. Devi |
| 3 | Email not-an-email, Save | “Enter a valid email address.”; the email wrapper data-error="true" |
| 4 | Phone 123, Save | “Enter a valid mobile number.” |
| 5 | Phone +65 8123 4567, Save | Saved |
| Ids | appbar-menu-profile · input-profile-name · input-profile-email · input-profile-phone · profile-button-save · div-error-profile · div-success-profile |
| TC-NOTIF-01 | Notifications record events, newest first |
| Pre | Signed in, fresh session. |
| 1 | Open the bell | “Nothing to show yet.”; no badge on the bell |
| 2 | Add a payee, then transfer SGD 60 to Business Current | Bell badge reads 2 |
| 3 | Open the bell | Two notices; the first is the transfer (amount, destination, TFR reference), the second the payee |
| 4 | Click Clear all | Empty state; badge gone |
| Ids | appbar-notifications · appbar-notifications-badge · li-notice-<index> · notifications-button-clear · p-notifications-empty |
End-to-end journeys
| E2E-01 | Reinstate a lapsed policy, then claim against it |
| Pre | Signed in. |
| 1 | Policies → View DriveSafe Comprehensive | Lapse warning shown |
| 2 | Pay premium → DriveSafe, Everyday Savings, 290 → Pay | Premium Paid |
| 3 | Back to policies | DriveSafe badge Active |
| 4 | Claims → File a claim → DriveSafe, Accident damage, a past date, 900, declaration → Submit | Claim Submitted (the same claim was impossible in step 1) |
| Ids | div-policy-lapsed-warning · policy-button-pay-premium · badge-policy-status-pol3 · claims-button-new · span-claim-reference |
| E2E-02 | Add a payee, pay them, and see three witnesses agree |
| Pre | Signed in. |
| 1 | Payees → add Jane Tan / DBS / 234-56789-0 | Row added, Verified No |
| 2 | Transfer → payee Jane Tan (the new p4), amount 275 | Acknowledgement checkbox required; tick it; Continue; Confirm |
| 3 | Note the TFR reference; Done | — |
| 4 | Accounts | Everyday Savings is SGD 18,145.05 (275 + 0.50 fee) |
| 5 | Bell | First notice carries the reference and Jane Tan's name |
| Ids | payee-button-add · select-transfer-payee · checkbox-transfer-ack-unverified · span-transfer-reference · td-account-balance-sav · li-notice-0 |
| E2E-03 | Large claim with evidence, end to end |
| Pre | Signed in. |
| 1 | Dashboard → File a claim | Claim form |
| 2 | GreatCare, Day surgery, past date, 3500, attach discharge-summary.pdf, declaration → Submit | Claim Submitted, reference CLM-nnnnnn |
| 3 | View my claims | New row, Submitted |
| 4 | View it | Status Submitted; Documents: discharge-summary.pdf |
| Ids | button-quick-file-claim · input-claim-file · claim-button-done · dd-claim-detail-status · p-claim-detail-documents |
| E2E-04 | A premium leaves the account it came from |
| Pre | Signed in. |
| 1 | Dashboard → Pay a premium → PruLife Secure, Everyday Savings, 312.40 → Pay | Receipt PRM-nnnnnn |
| 2 | Accounts → Open Everyday Savings | Balance SGD 18,108.15 |
| 3 | Search the history for the receipt number | One Debit row, “GienTech premium — PruLife Secure” |
| Ids | span-premium-receipt · input-txn-search · td-history-description-<id> |
| E2E-05 | Log out abandons work in progress |
| Pre | Signed in. |
| 1 | Transfer → type amount 999 (do not continue) | — |
| 2 | User menu → Log out | Login card |
| 3 | Open /#/transfer directly | Redirected to login; nothing of the draft survives |
| Ids | appbar-menu-logout · h1-bcp-login-header-app-name |
What “done” looks like for an automated version
The same steps, with every expected result turned into a web-first assertion; every literal replaced by a fixture or a rule; navigation by clicking; no sleeps. Where a case checks that something must not have happened — the confirmation panel absent, the register still three rows — the automated version asserts that too. That negative half is what makes a rule test able to detect the rule's deletion.
Chapter 12
Exercises
Ten exercises against this application, in rising order of difficulty. Each names the screen, the thing to prove, and the ids or techniques it needs. Solutions to most already exist in tests/connect — write yours first, then compare.
| # | Prove that… | Needs |
| 1 | The Log in button is disabled until both fields have text, and disables again when one is cleared | toBeDisabled() / toBeEnabled() on the verbatim submit id |
| 2 | Leaving Global ID empty on blur turns its wrapper red — and typing turns it back | fieldWrapper(), toHaveAttribute('data-error', …) |
| 3 | Three wrong passwords lock every control on the card | a loop in the page object, data-disabled, toBeDisabled() |
| 4 | The dashboard's Total Balance excludes the USD and EUR accounts | dashboardTotals from the shared data; assert the exact string |
| 5 | Filtering Transactions to Failed shows 3 rows — but only after Apply | assert the count before and after clicking Apply |
| 6 | Picking 1–5 September in the calendar popups yields 11 rows | ReactDatepicker.pick(), the portal, --outside-month |
| 7 | Page 4 holds five rows, Next is disabled there, and the last row is CLM-30288 | the pager ids; rows.last() |
| 8 | A transfer to the unverified contractor is refused until the box is ticked, then charged SGD 0.50 | the acknowledgement checkbox; the confirmation panel's fee cell |
| 9 | Paying SGD 290 on the lapsed motor policy turns its badge Active on the policy list | two page objects in one test; the doubled outstanding amount |
| 10 | A SGD 3,500 claim is refused without a document and accepted with one — and the document survives into the claim detail | setInputFiles() on the hidden input; three screens |
Two constraints, on purpose
No waitForTimeout, and no page.goto() after the first navigation. The first is a lint error in this repository; the second resets the application. Both are the habits this target exists to break.
Part IV
Operations
Chapter 13
Build, deploy, roll back
Server layout
| Piece | Where |
| Document | /var/www/connect-for-bank/index.html |
| Vhosts | zz-connect-for-bank-ssl.conf (443, DocumentRoot, deflate) and zz-connect-for-bank.conf (80 → 301) |
| Certificate | Let's Encrypt via the default :80 webroot; certbot's timer renews |
| Logs | /var/log/apache2/connect-for-bank-{access,error}.log |
| Provisioning | tools/server/provision-static-site.sh <host> <slug>, idempotent |
npm run deploy:connect:provision # once — vhost + certificate
npm run app:connect:build && npm run deploy:connect # every update: rebuild, scp, verify
Rolling back
git checkout <previous-commit> -- app/connect-for-bank.html
npm run deploy:connect
The bundle is committed, so a rollback is a checkout and a copy; no build is needed. Health is a curl -sI on the URL (200, text/html) and a browser check that /#/bcp/login renders the welcome card.
Appendix A
Test-id catalogue
Every id, by screen. <id> is the entity's own id (sav, pol3, p1, c2, a transaction number); <base> is a field's base name.
Shell (every signed-in screen)
| Element | Id |
| App bar, title, logo | bo-packages-appbar, …-appbar-title, …-logo |
| Notifications bell, badge | …-appbar-notifications, …-appbar-notifications-badge |
| User menu, items | …-appbar-user-menu, …-appbar-user-name, …-appbar-user-menu-items, …-appbar-menu-profile, …-appbar-menu-logout |
| Sidebar links | …-sidebar, …-nav-dashboard, …-nav-accounts, …-nav-transactions, …-nav-transfer, …-nav-payees, …-nav-policies, …-nav-premium, …-nav-claims |
| Main region, shell | …-main, …-div-shell |
| Toasts | …-toast-container, …-toast-<kind>, …-toast-<kind>-message, …-toast-<kind>-dismiss (kind: error · success · info) |
| Datepicker portal | …-datepicker-portal |
Shared field wrapper (every form)
| Element | Id |
| Required marker (present only when required) | bo-packages-div-is-required |
| State-carrying wrapper | bo-packages-div-label — data-error, data-disabled, data-variant |
| Label, hint | …-label-<base>, …-hint-<base> |
| Control | …-input-<base> · …-select-<base> (+ …-option-<base>-<value>) · …-textarea-<base> · …-datepicker-<base> |
| Checkbox | …-div-checkbox-<base> (carries data-error), …-checkbox-<base> |
| Messages | …-div-error-<context>, …-div-success-<context> |
Login — verbatim from the specification
| Element | Id |
| Gradient shell, logo block | bo-packages-div, bo-packages-div-2 |
| Card wrappers | bo-packages-div-show-toast, bo-packages-div-show-toast-2, bo-packages-div-bcp-login-header-welcome-message, …-welcome-message-2 |
| Welcome, app name | bo-packages-span-bcp-login-header-welcome-message, bo-packages-h1-bcp-login-header-app-name |
| Form, form body | bo-packages-form-provider-form-bcp-login-input-label-global-id, bo-packages-form-provider-form-div-bcp-login-input-label-global-id |
| Global ID | bo-packages-label-global-id, bo-packages-input-global-id |
| Password, toggle | bo-packages-label-password, bo-packages-input-password, bo-packages-button-toggle-password |
| Inline error | bo-packages-p-bcp-login-error |
| Submit | bo-packages-form-provider-form-button-verify-login-button |
| Forgot password | bo-packages-div-forgot-password, bo-packages-button-button-forgot-password |
| Demo credentials line | bo-packages-p-bcp-login-demo-credentials |
Dashboard
| Element | Id |
| Title | bo-packages-h1-dashboard |
| Cards | …-card-<key>, …-card-<key>-label, …-card-<key>-value — keys: total-balance · total-cover · next-premium-due · pending-transactions |
| Quick actions | …-card-quick-actions, …-button-quick-transfer, …-button-quick-pay-premium, …-button-quick-file-claim, …-button-quick-payees |
| Account / policy summaries | …-card-accounts, …-div-dash-account-<id>, …-span-dash-account-<id>-balance, …-link-see-all-accounts; …-card-policies, …-div-dash-policy-<id>, …-link-see-all-policies |
| Recent transactions | …-card-recent-transactions, …-table-recent-transactions, …-row-recent-transaction-<id>, …-badge-status-<id>, …-button-view-transaction-<id>, …-link-view-all-transactions |
Accounts and account detail
| Element | Id |
| List | bo-packages-h1-accounts, …-table-accounts, …-row-account-<id>, …-td-account-name-<id>, …-badge-account-status-<id>, …-td-account-balance-<id>, …-button-open-account-<id> |
| Detail facts | bo-packages-h1-account-detail, …-div-account-detail-<key>, …-dd-account-detail-<key> — keys: name · number · type · status · branch · swift · opened · balance |
| History | …-div-txn-filter-bar, …-input-txn-search, …-select-txn-direction, …-table-transaction-history, …-row-history-transaction-<id>, …-td-history-description-<id>, …-td-history-reference-<id>, …-td-history-direction-<id>, …-badge-history-status-<id>, …-p-txn-empty, …-link-back-to-accounts |
Transfer
| Element | Id |
| Form | bo-packages-h1-transfer, …-div-transfer-form, …-select-transfer-from-account, …-div-transfer-kind, …-radio-transfer-kind-own / -payee, …-select-transfer-to-account, …-select-transfer-payee, …-checkbox-transfer-ack-unverified, …-input-transfer-amount, …-datepicker-transfer-date, …-input-transfer-reference, …-hint-transfer-reference, …-div-error-transfer, …-transfer-button-continue |
| Confirm | …-div-transfer-confirm, …-dd-transfer-confirm-<key> (from · to · amount · fee), …-p-transfer-confirm-total, …-transfer-button-confirm, …-transfer-button-back |
| Receipt | …-div-transfer-success, …-h2-transfer-success-title, …-p-transfer-success-message, …-p-transfer-reference, …-span-transfer-reference, …-transfer-button-done |
Payees
| Element | Id |
| Register | bo-packages-h1-payees, …-table-payees, …-row-payee-<id>, …-td-payee-name-<id>, …-td-payee-nickname-<id>, …-td-payee-account-<id>, …-td-payee-verified-<id> |
| Add form | …-div-add-payee, …-input-payee-name, …-select-payee-bank, …-input-payee-account, …-input-payee-nickname, …-div-error-payee, …-div-success-payee, …-payee-button-add |
Policies, policy detail, premium
| Element | Id |
| List | bo-packages-h1-policies, …-select-policy-status-filter, …-table-policies, …-row-policy-<id>, …-td-policy-product-<id>, …-badge-policy-status-<id>, …-button-open-policy-<id>, …-p-policies-empty |
| Detail | bo-packages-h1-policy-detail, …-dd-policy-detail-<key> (product · number · status · type · sum-assured · premium · next-due · insured), …-p-policy-beneficiary, …-div-policy-lapsed-warning, …-div-policy-panel-<key>, …-button-policy-panel-<key>, …-ul-policy-panel-<key> (benefits · exclusions), …-policy-button-pay-premium, …-policy-button-file-claim, …-link-back-to-policies |
| Premium | bo-packages-h1-premium, …-div-premium-form, …-select-premium-policy, …-select-premium-from-account, …-input-premium-amount, …-div-premium-outstanding, …-checkbox-premium-autopay, …-div-error-premium, …-premium-button-pay; receipt …-div-premium-success, …-h2-premium-success-title, …-p-premium-success-message, …-span-premium-receipt, …-span-premium-next-due, …-premium-button-done |
Claims
| Element | Id |
| Register | bo-packages-h1-claims, …-claims-button-new, …-table-claims, …-row-claim-<id>, …-td-claim-reference-<id>, …-badge-claim-status-<id>, …-button-open-claim-<id> |
| Form | bo-packages-h1-claim-new, …-div-claim-form, …-select-claim-policy, …-select-claim-type, …-datepicker-claim-incident-date, …-input-claim-amount, …-textarea-claim-reason, …-select-claim-reimburse-to, …-div-claim-documents, …-claim-button-attach, …-input-claim-file (hidden), …-ul-claim-documents, …-li-claim-document-<n>, …-p-claim-document-hint, …-checkbox-claim-declaration, …-div-error-claim, …-claim-button-submit, …-claim-button-cancel |
| Receipt, detail | …-div-claim-success, …-h2-claim-success-title, …-p-claim-success-message, …-span-claim-reference, …-claim-button-done; bo-packages-h1-claim-detail, …-dd-claim-detail-<key> (reference · status · type · amount · incident · submitted), …-p-claim-detail-reason, …-p-claim-detail-documents, …-link-back-to-claims |
Profile and notifications
| Element | Id |
| Profile | bo-packages-h1-profile, …-div-profile-form, …-input-profile-name, …-input-profile-email, …-input-profile-phone, …-checkbox-profile-alerts, …-div-error-profile, …-div-success-profile, …-profile-button-save |
| Notifications | bo-packages-h1-notifications, …-p-notifications-empty, …-ul-notifications, …-li-notice-<index> (0 is newest), …-notifications-button-clear |
Appendix B
Mock data reference
From apps/connect-for-bank/src/mockData.js. Identical to the Angular seed except for the transaction ledger.
| Account | Id | Status | Balance | Why it exists |
| Everyday Savings | sav | Active | SGD 18,420.55 | Default funding account, 18 transactions |
| Business Current | cur | Active | SGD 42,100.00 | The own-account transfer destination |
| Multi-Currency USD | usd | Dormant | USD 6,250.75 | Proves the dormancy rule |
| Multi-Currency EUR | eur | Active | EUR 9,840.20 | Proves the SGD-only rule — active but not SGD |
| Policy | Id | Status | Premium | Sum assured | Claim types |
| PruLife Secure | pol1 | Active | Monthly 312.40, due 2026-09-28 | 500,000 | Death benefit · Terminal illness · Total permanent disability |
| GreatCare Hospital | pol2 | Active | Annual 940.00, due 2027-01-15 | 200,000 | Hospitalisation · Day surgery · Outpatient · Critical illness |
| DriveSafe Comprehensive | pol3 | Lapsed | Quarterly 145.00 — owes 290.00 | 80,000 | Accident damage · Theft · Third party liability · Windscreen |
| Payee | Id | Bank · account | Verified |
| Lim Wei Jie (Landlord) | p1 | DBS · 123-45678-9 | Yes |
| Sunrise Childcare | p2 | OCBC · 552-11009-3 | Yes |
| Kaur Renovations | p3 | UOB · 901-77321-5 | No — proves the acknowledgement rule |
| Claim | Id | Policy | Amount | Status |
| CLM-30288 | c1 | pol3 | 1,480.00 | Approved |
| CLM-31904 | c2 | pol2 | 3,250.00 | Assessing (two documents) |
Banks offered: DBS · OCBC · UOB · Standard Chartered · Maybank · HSBC. Transaction facts are in Chapter 7. Reference numbers minted in a session are TFR-, PRM- or CLM- followed by six digits.
Appendix C
Commands
| Command | Purpose |
npm run test:connect | The React target's suite on Chromium |
npm run test:connect:all | Chromium, Firefox and WebKit |
npx playwright test --project=connect-chromium --ui | UI mode against this target |
npm run app:connect:dev | Vite dev server with hot reload |
npm run app:connect:build | Rebuild the single-file bundle into app/ |
npm run deploy:connect:provision | Create the vhost and certificate (once) |
npm run deploy:connect | Push the bundle and verify the live URL |
npm run docs:connect | Rebuild this book's standalone HTML |
npm run deploy:connect-runbook | Push this book to its host |
Companion to the GienTech Wealth Automation Runbook. Every id and every count in this book is taken from the application source and the suite it documents; if they disagree, the source is right and this book has a bug.