GienTech Wealth Connect Runbook
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.

14
screens
50
manual cases
3
engines
35
seeded transactions
1
file to deploy
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.

ConcernAngular buildConnect (this build)
FrameworkAngular 18, MaterialReact 18, Vite, Tailwind, Flowbite React
Test idspremium-errorbo-packages-div-error-premium
DropdownsMaterial overlay (mat-select)Native <select> in a styled wrapper
Date fieldsMaterial datepicker, typed M/D/YYYYreact-datepicker in a portal, typed dd/MM/yyyy
TablesMaterial table.sticky-actions table, last column pinned
Validation stateError text onlyText and data-error / data-variant / data-disabled on the field wrapper
Login route#/sign-in#/bcp/login, the portal's path
SessionComponent memoryComponent memory + a sessionStorage flag that survives reload
Transactions7 rows, per account35 rows, a filterable, paginated page of its own
Stored datesUTC 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

WhatWhere it livesSurvives a reload?
Accounts, balances, transactions, payees, policies, claims, noticesReact state (StoreContext)No — every load is the pristine seed
Failed sign-in count and lockoutReact stateNo
“Signed in” flagsessionStorage (connect-for-bank.authed)Yes, within the tab
Theme, drafts, filtersComponent stateNo

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.

/bcp/login
h1-bcp-login-header-app-name

/dashboard
h1-dashboard

/accounts

/accounts/:id
search · direction

/transactions
filters · pager · modal

/transfer
3-step wizard

/payees

/policies

/policies/:id

/premium

/claims

/claims/new
datepicker · upload

/claims/:id

/profile

/notifications

Figure 3.1 — Routes and the screens behind them. Everything under the shell redirects to /bcp/login when the session flag is absent.
RouteReadiness idWhat a test does here
/bcp/loginbo-packages-h1-bcp-login-header-app-nameSign in, lockout, the field state machine
/dashboardbo-packages-h1-dashboardDerived totals, quick actions, recent transactions
/accountsbo-packages-h1-accountsThe sticky table, badges, balances in their own currency
/accounts/:idbo-packages-h1-account-detailSearch and direction filter over the account's history
/transactionsbo-packages-h1-transactionsDrafted filters, the datepicker range, pagination, the modal
/transferbo-packages-h1-transferFour refusal rules, the fee, the confirmation step
/payeesbo-packages-h1-payeesFormat and duplicate rules, data-variant on the form
/policies, /policies/:id…-h1-policies, …-h1-policy-detailStatus filter, lapse warning, expansion panels
/premiumbo-packages-h1-premiumDormant, SGD-only, partial, reinstatement
/claims, /claims/new, /claims/:id…-h1-claims, …-h1-claim-new, …-h1-claim-detailFive stacked rules, the dependent dropdown, the upload
/profilebo-packages-h1-profileEmail and phone validation, the name reaching the app bar
/notificationsbo-packages-h1-notificationsThe 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.

bo-packages

role
input · select · button · row · td · badge · div · h1

context
transfer · premium · claim · transactions

element
amount · from-account · view-transaction

identity
an id from the data, when the element is one of many

Figure 4.1 — Composition. bo-packages-row-transaction-35: role row, context transaction, identity 35. The identity is the transaction's id, never its position in the table.

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

  1. Deterministic. No random suffixes, no timestamps. The same element has the same id on every run and on every machine.
  2. 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.
  3. 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.

AttributeValuesDrives (Tailwind)Set when
data-errortrue / falsedata-[error=true]:border-danger, …:placeholder-dangerA required field is left empty on blur; the amount is refused
data-variantnormal / succeed / faileddata-[variant=succeed]:border-success, data-[variant=failed]:border-errorA submit is refused or accepted; typing again returns to normal
data-disabledtrue / falsedata-[disabled=true]:bg-neutral-300, …:cursor-not-allowedThe account is locked after three failed sign-ins

blur while empty

type

submit refused

type

submit accepted

type

3rd failed sign-in

normal

error

failed

succeed

disabled

Figure 5.1 — The wrapper's states. error is per field and immediate; failed / succeed are set by a submit and apply to every field on the form; disabled is terminal for the session.

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.
input[data-testid=…-datepicker-transactions-from]Testinput[data-testid=…-datepicker-transactions-from]Testclick()mount .react-datepickerexpect(calendar).toBeVisible()click .react-datepicker__navigation--next (until the month matches)click .react-datepicker__day:not(--outside-month) "1"value = 01/09/2026, popup unmountsexpect(calendar).toBeHidden()
Figure 6.1 — Picking from the calendar. The suite's ReactDatepicker driver does this, bounded to 24 month hops so a wrong target fails with a message instead of looping.
// 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.

edits are a DRAFT

Apply

Reset

Filter bar
search · status · from · to

Applied filters

Filtered rows
newest first

Page N of ceil(rows / 10)

Everything cleared,
page 1

Figure 7.1 — Nothing filters until Apply. A test that types a status and asserts the count without clicking Apply is asserting the wrong thing — and there is a test that proves it.

Facts the tests are built on

FactValue
Total rows35
By statusCompleted 25 · Pending 7 · Failed 3
By accountsav 18 · cur 13 · eur 2 · usd 2
Date range2026-08-14 → 2026-09-10
Rows dated 1–5 September inclusive11
Page size · pages10 · 4 (the last page holds 5)
First row on page 1id 35, TXN-2026-000135, 2026-09-10, Pending
Last row on page 4id 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

ElementId
Filter barbo-packages-transactions-filter-bar
Search / statusbo-packages-input-transactions-search / bo-packages-select-transactions-status
From / to datesbo-packages-datepicker-transactions-from / …-to
Apply / Resetbo-packages-transactions-button-apply / …-reset
Result countbo-packages-transactions-span-result-count
Table, header cellsbo-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 statebo-packages-p-transactions-empty
Modalbo-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.

#RuleScreenMessage appears in
1Three wrong passwords lock the accountLogin…-p-bcp-login-error, …-toast-error-message; wrappers data-disabled=true
2The error never says which half was wrongLoginsame
3A transfer above the balance is refused, naming the balanceTransferbo-packages-div-error-transfer
4An unverified payee needs an acknowledgementTransfersame; checkbox …-checkbox-transfer-ack-unverified
5Payee account number must match 123-45678-9Payeesbo-packages-div-error-payee; wrappers data-variant=failed
6A duplicate payee account number is refusedPayeessame
7A dormant account cannot pay a premium (checked first)Premiumbo-packages-div-error-premium
8Premiums are collected in SGD onlyPremiumsame
9Partial premiums refused; a lapsed policy owes twoPremiumsame; …-div-premium-outstanding shows the doubled figure
10Paying in full reinstates a lapsed policyPremium → Policies…-badge-policy-status-pol3 reads Active
11A lapsed policy provides no coverClaim formbo-packages-div-error-claim
12An incident dated in the future is refusedClaim formsame
13Claims over SGD 1,000 need a document (1,000 exactly does not)Claim formsame; hint …-p-claim-document-hint
14A claim cannot exceed the sum assuredClaim formsame
15The declaration must be tickedClaim formsame; …-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

src/pages/connect

Shared with the Angular suite

business-rules.ts

test-data.ts

Payee / Claim builders

date · money utils

ConnectBasePage
bo() · fieldWrapper() · expectLoaded()

AppShell
sidebar · user menu · toasts

ReactDatepicker
type() · pick()

16 page objects

connect-fixtures.ts
signedIn · page objects · pageErrors

tests/connect
88 tests · 8 files

Figure 9.1 — The suite's shape. Nothing in the shared layer changed for the port; everything under src/pages/connect is new.

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

PathHolds
src/pages/connect/connect-base.page.tsThe base class above
src/pages/connect/components/app-shell.component.tsSidebar navigation, user menu, notification badge, toast accessors
src/pages/connect/components/react-datepicker.component.tstype(), pick(), clear(), format()
src/pages/connect/*.page.tsLogin, dashboard, accounts (+ detail), transactions, transfer, payees, policies (+ detail), premium, claims (+ new, detail), profile, notifications
src/fixtures/connect-fixtures.tsThe custom test: one fixture per page object, signedIn, pageErrors
src/data/connect-data.tsCredentials and the transaction facts of Chapter 7
tests/connect/*.spec.tslogin · 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.

  1. Page object. ConnectPayeesPage already has addPayee(), errorMessage, rows and nameWrapper. Nothing to add.
  2. Rule. payeeRules.duplicateAccount exists in the shared file.
  3. Data. PayeeBuilder.valid().withAccountNumber(payees.verifiedLandlord.account) — valid in every respect except the one under test.
  4. 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-01Sign in with valid credentials
PreSigned out; app opened at /#/bcp/login.
1Open the applicationLogin card shows “Welcome to” and “GienTech Wealth”; Log in button is disabled
2Type amara in Global IDButton still disabled
3Type Passw0rd! in PasswordButton becomes enabled
4Click Log inDashboard loads; h1 reads “Good morning, Amara Devi”; app bar shows the name; URL ends #/dashboard
Idsinput-global-id · input-password · form-provider-form-button-verify-login-button · h1-dashboard · appbar-user-name
TC-AUTH-02Required field marks itself on blur
PreSigned out.
1Click into Global ID, then click elsewhere without typingThe Global ID wrapper turns red: data-error="true"
2Type one characterdata-error returns to "false"
Idsinput-global-id · the wrapper found by containment (div-label)
TC-AUTH-03Wrong password is refused without saying which half was wrong
PreSigned out.
1Sign 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"
2Sign in with nobody / WrongPassword!Same wording — the message never distinguishes an unknown ID from a wrong password
3Type in the password fieldBoth wrappers return to data-variant="normal"
Idstoast-error-message · p-bcp-login-error · div-label
TC-AUTH-04Three failures lock the account
PreSigned out.
1Fail sign-in twiceMessage counts down: 2 remaining, then 1
2Fail a third time“Your account is locked after 3 failed attempts. Call 1800 248 2888 to unlock.”
3Inspect the formBoth inputs disabled; wrappers data-disabled="true"; Log in disabled
4Enter the correct passwordNothing can be submitted — the lock holds for the session
Idsinput-global-id · input-password · form-provider-form-button-verify-login-button
TC-AUTH-05Password visibility toggle
PreSigned out.
1Inspect the password inputtype="password"
2Click the eye buttontype="text"; button aria-pressed="true"
3Click it againtype="password"
Idsbutton-toggle-password
TC-AUTH-06Forgot password points to a branch
PreSigned out.
1Click Forgot passwordInfo toast: “Password resets are handled at a branch.”
Idsbutton-button-forgot-password · toast-info-message
TC-AUTH-07Deep links are guarded
PreSigned out.
1Open /#/transactions directlyRedirected to /#/bcp/login
2Sign in, then open /#/bcp/login directlyRedirected to /#/dashboard — an authed user never sees the card
Idsh1-bcp-login-header-app-name · h1-dashboard
TC-AUTH-08Log out
PreSigned in.
1Open the user menu (app bar, right)Menu shows My profile and Log out
2Click Log outLogin card; app bar gone; a deep link to /#/dashboard now redirects back to login
Idsappbar-user-menu · appbar-menu-logout · appbar

Dashboard

TC-DASH-01Summary cards are derived correctly
PreSigned in, dashboard.
1Read Total BalanceSGD 60,520.55 — the two SGD accounts only; USD and EUR are not converted
2Read Total Sum AssuredSGD 700,000 — active policies only; the lapsed motor policy is excluded
3Read Next Premium Due2026-09-28 — the earliest due date among active policies
4Read Pending Transactions7
Idscard-total-balance-value · card-total-cover-value · card-next-premium-due-value · card-pending-transactions-value
TC-DASH-02Recent transactions preview
PreSigned in, dashboard.
1Scroll to Recent transactionsFive rows, newest first; the first is TXN-2026-000135
2Scroll the table horizontally on a narrow windowThe Actions column stays pinned to the right edge
3Click View on the first rowTransactions page opens with the detail modal for that row already showing
Idstable-recent-transactions · row-recent-transaction-<id> · button-view-transaction-<id> · modal-transaction-detail
TC-DASH-03Quick actions and links
PreSigned in, dashboard.
1Click each quick action in turnTransfer money → /transfer; Pay a premium → /premium; File a claim → /claims/new; Manage payees → /payees
2Return and click See all accounts / See all policies / View allAccounts, Policies and Transactions pages respectively
Idsbutton-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-01Account list
PreSigned in.
1Open Accounts from the sidebarFour rows; each shows name, number, type, a status badge and the balance in its own currency (USD 6,250.75, EUR 9,840.20)
2Inspect the dormant accountRow is present with a Dormant badge — it is shown, not hidden
Idsnav-accounts · table-accounts · row-account-<id> · badge-account-status-<id> · td-account-balance-<id>
TC-ACC-02Account detail facts
PreSigned in, Accounts.
1Click Open on Everyday SavingsDetail shows name, number 0012-3456-7890, Savings, Active, Orchard, GNTCSGSG, opened 2019-04-11, balance SGD 18,420.55
Idsbutton-open-account-sav · dd-account-detail-<key>
TC-ACC-03History filters
PreSigned in, Everyday Savings detail.
1Read the history18 rows
2Set Direction to Credit8 rows; SP Utilities is gone
3Set Direction to Debit10 rows
4Set Direction back to All18 rows
5Type Salary in the search box1 row (search filters as you type here — unlike the Transactions page)
6Type SAL-88213Same single row, found by reference
7Type nothing-like-thisNo rows; “No transactions match this search.”
Idsselect-txn-direction · input-txn-search · table-transaction-history · p-txn-empty

Transactions

TC-TXN-01Default view and ordering
PreSigned in.
1Open Transactions from the sidebar“35 transactions”; ten rows; “Page 1 of 4”; Prev disabled
2Read the dates down the pageDescending; same-day rows have the higher id first (row 35 above 34)
Idsnav-transactions · transactions-span-result-count · table-transactions · transactions-span-page-summary · transactions-button-prev
TC-TXN-02Pagination
PreSigned in, Transactions.
1Click NextPage 2 of 4; Prev enabled
2Click page 4Five rows; Next disabled; last row is CLM-30288 dated 2026-08-14
3Click page 3Page button 3 carries aria-current="page"; button 1 does not
Idstransactions-button-next · transactions-button-page-<n> · transactions-button-prev
TC-TXN-03Filters are drafted until Apply
PreSigned in, Transactions.
1Choose Status = Failed. Do NOT click ApplyCount still reads 35 transactions
2Click Apply3 transactions; every visible badge reads Failed
3Choose Pending, click Apply7 transactions
Idsselect-transactions-status · transactions-button-apply · badge-status-<id>
TC-TXN-04Search
PreSigned in, Transactions.
1Type SAL-88213, Apply1 row, the salary credit
2Type invoice, Apply3 rows (search is case-insensitive over reference and description)
Idsinput-transactions-search · transactions-button-apply
TC-TXN-05Date range, typed
PreSigned in, Transactions.
1Type 01/09/2026 into From, press EnterField shows 01/09/2026; calendar closed
2Type 05/09/2026 into To, press Enter, click Apply11 transactions; every date between 2026-09-01 and 2026-09-05
Idsdatepicker-transactions-from · datepicker-transactions-to
TC-TXN-06Date range, picked from the calendar
PreSigned in, Transactions.
1Click the From fieldA react-datepicker popup opens — inside #datepicker-portal, not inside the filter bar
2Navigate to September 2026 if needed and click 1Field reads 01/09/2026
3Pick 5 in the To field, click Apply11 transactions
Ids.react-datepicker · .react-datepicker__navigation--next · .react-datepicker__day (exclude --outside-month)
TC-TXN-07Reset
PreSigned in, Transactions with search invoice + status Failed applied.
1ObserveNo rows; “No transactions match these filters.”
2Click ResetSearch empty, Status All, dates cleared, 35 transactions, page 1
Idstransactions-button-reset · p-transactions-empty
TC-TXN-08Detail modal
PreSigned in, Transactions.
1Click View on TXN-2026-000135Modal shows reference, date 2026-09-10, account 0022-8899-1010, amount −SGD 2,600.00, Pending, description, initiator j.tan, approver —
2Click ×Modal closes
3Open it again, press EscapeModal closes
4Open it again, click the dark backdropModal closes
Idsbutton-view-transaction-35 · modal-transaction-detail · modal-transaction-detail-value-<field> · modal-button-close

Transfer

TC-TRF-01Transfer between own accounts
PreSigned in.
1Open Transfer; From = Everyday Savings; To my own account; To = Business Current; amount 250; reference “Monthly top-up”; ContinueConfirmation shows From, To, SGD 250.00, Fee SGD 0.00, Total debited SGD 250.00
2Click Confirm Transfer“Transfer Successful!”; message names Business Current; reference matches TFR-nnnnnn
3Click Done, open AccountsEveryday Savings balance is SGD 18,170.55
Idsselect-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-02Transfer to a payee carries a fee
PreSigned in, Transfer.
1Choose To a saved payee; Payee = Lim Wei Jie; amount 400; ContinueFee SGD 0.50; Total debited SGD 400.50
2Confirm, Done, open AccountsEveryday Savings is SGD 18,020.05 — amount plus fee
Idsradio-transfer-kind-payee · select-transfer-payee · dd-transfer-confirm-fee · p-transfer-confirm-total
TC-TRF-03Insufficient funds names the balance
PreSigned in, Transfer.
1Own account, Business Current, amount 19420.55, Continue“Insufficient funds. Everyday Savings holds SGD 18420.55.” — no confirmation panel
2Payee Lim Wei Jie, amount 18420.55 (exactly the balance), ContinueRefused for the same reason — the SGD 0.50 fee tips it over
Idsdiv-error-transfer · div-transfer-confirm (absent)
TC-TRF-04Unverified payee needs an acknowledgement
PreSigned in, Transfer.
1Payee = Lim Wei JieNo acknowledgement checkbox
2Payee = Kaur Renovations (unverified)Checkbox appears
3Amount 200, Continue without ticking“Acknowledge the unverified payee before continuing.”
4Tick the box, ContinueConfirmation panel; To = Kaur Renovations
Idscheckbox-transfer-ack-unverified · div-error-transfer
TC-TRF-05Zero amount, reference cap, Back
PreSigned in, Transfer.
1Amount 0, Continue“Enter an amount greater than zero.”; the amount wrapper shows data-error="true"
2Type 50 X characters into the referenceField holds 35; hint reads 35/35
3Amount 75, Continue, then BackForm returns with 75 still in the amount field
Idsinput-transfer-amount · hint-transfer-reference · transfer-button-back
TC-TRF-06Scheduled date from the calendar
PreSigned in, Transfer.
1Click the Transfer on fieldCalendar opens in the portal; dates before today are disabled
2Pick a date two weeks ahead, fill the rest, ContinueConfirmation panel shows
Idsdatepicker-transfer-date

Payees

TC-PAY-01Payee register
PreSigned in.
1Open PayeesThree rows; Lim Wei Jie Verified Yes, Kaur Renovations Verified No, Kaur's nickname shows “—”
Idstable-payees · td-payee-verified-<id> · td-payee-nickname-<id>
TC-PAY-02Add a payee
PreSigned in, Payees.
1Name 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
2Inspect the formFields cleared; the name wrapper shows data-variant="succeed"
Idsinput-payee-name · select-payee-bank · input-payee-account · input-payee-nickname · payee-button-add · div-success-payee
TC-PAY-03Refusals
PreSigned in, Payees.
1Leave the name empty, valid bank and account, Add“Enter the payee name.”; wrappers data-variant="failed"
2Name filled, leave Bank at “Choose a bank”, Add“Choose a bank from the list.”
3Account 12345678, Add“Account number must look like 123-45678-9.”
4Account 1234-56789-0, AddSame — the groups are the wrong length
5Account 123-45678-9 (Lim Wei Jie's), Add“A payee with that account number already exists.”; still three rows
Idsdiv-error-payee · table-payees

Policies and premiums

TC-POL-01Policy list and filter
PreSigned in.
1Open PoliciesThree rows with product, number, type, status badge, premium
2Filter = LapsedOnly DriveSafe Comprehensive
3Filter = ActiveTwo rows; the lapsed one is gone
4Filter = AllThree rows
Idsselect-policy-status-filter · row-policy-<id> · badge-policy-status-<id>
TC-POL-02Policy detail
PreSigned in, Policies.
1View PruLife SecureSum Assured SGD 500,000; Premium Monthly SGD 312.40; Next Due 2026-09-28; Life Assured Amara Devi; Beneficiary Ravi Devi; no lapse warning
2Expand What this covers, then What it does not coverBenefit and exclusion lists appear (they are absent from the DOM until expanded)
3Back to policies; View DriveSafe ComprehensiveRed warning: “This policy has lapsed…a claim cannot be filed against it…”
Idsbutton-open-policy-<id> · dd-policy-detail-<key> · button-policy-panel-benefits · ul-policy-panel-benefits · div-policy-lapsed-warning
TC-PRM-01Outstanding amount
PreSigned in, Pay premium.
1Policy = PruLife SecureOutstanding SGD 312.40
2Policy = DriveSafe Comprehensive (Lapsed)Outstanding SGD 290.00 — twice the quarterly premium
Idsselect-premium-policy · div-premium-outstanding
TC-PRM-02Pay a premium in full
PreSigned in, Pay premium.
1PruLife 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
2Back to policies, then AccountsEveryday Savings is SGD 18,108.15
Idsselect-premium-from-account · input-premium-amount · premium-button-pay · span-premium-receipt · span-premium-next-due
TC-PRM-03Refusals, in order
PreSigned in, Pay premium, PruLife Secure.
1From = 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
2From = Multi-Currency EUR (active), 312.40, Pay“Premiums are collected in SGD. Choose an SGD account.”
3From = Everyday Savings, 100, Pay“Partial payments are not accepted. SGD 312.40 is outstanding on this policy.”
4Policy DriveSafe, 145, PayPartial — SGD 290.00 is outstanding
5Amount empty, Pay“Enter the amount you are paying.”
Idsdiv-error-premium
TC-PRM-04Reinstate a lapsed policy
PreSigned in, Pay premium.
1DriveSafe Comprehensive, Everyday Savings, 290, PayPremium Paid
2Back to policiesDriveSafe's badge now reads Active
Idspremium-button-done · badge-policy-status-pol3

Claims

TC-CLM-01Claim register and detail
PreSigned in.
1Open ClaimsTwo rows: CLM-30288 Approved, CLM-31904 Assessing
2View CLM-31904Reference, Assessing, Health — day surgery, SGD 3,250.00, incident 2026-08-18, submitted 2026-08-20, reason, “Documents: discharge-summary.pdf, invoice.pdf”
Idstable-claims · badge-claim-status-<id> · button-open-claim-<id> · dd-claim-detail-<key> · p-claim-detail-documents
TC-CLM-02File a claim under the threshold
PreSigned in, Claims → File a claim.
1Policy 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
2Click View my claimsA new first row with that reference and status Submitted
3View itIncident date is the date typed — not the day before
Idsselect-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-03Claim types depend on the policy
PreSigned in, File a claim.
1Policy = GreatCare Hospital, open the type listHospitalisation · Day surgery · Outpatient · Critical illness
2Policy = PruLife SecureType resets; list is Death benefit · Terminal illness · Total permanent disability
3Policy = DriveSafe ComprehensiveAccident damage · Theft · Third party liability · Windscreen
Idsselect-claim-type · option-claim-type-<value>
TC-CLM-04Large claim needs a document
PreSigned in, File a claim, otherwise valid.
1Amount 3500, no document, Submit“A supporting document is required for claims over SGD 1,000.”
2Amount 1000, SubmitAccepted — exactly 1,000 is not “over”
3New claim: amount 3500, attach a PDF, SubmitFile name listed under Attach a document; claim submitted
4View my claims → View the new claimDocuments: <the file name>
Idsinput-claim-file (hidden — set files directly) · ul-claim-documents · li-claim-document-<n> · p-claim-document-hint
TC-CLM-05Refusals
PreSigned in, File a claim.
1Policy DriveSafe (lapsed), any type, Submit“DriveSafe Comprehensive has lapsed and provides no cover. Pay the outstanding premium before claiming.”
2GreatCare, no type, Submit“Choose the type of claim.”
3Type chosen, no date, Submit“Enter the date of the incident.”
4Incident 30 days in the future, Submit“The incident date cannot be in the future.”
5Amount 0, Submit“Enter the amount you are claiming.”
6Amount 200001 with a document, Submit“The amount claimed exceeds the sum assured of SGD 200,000.”
7Valid claim, declaration unticked, Submit“Tick the declaration to submit this claim.”
Idsdiv-error-claim
TC-CLM-06Cancel
PreSigned in, File a claim.
1Type an amount, click CancelBack on the register; still two rows
Idsclaim-button-cancel · table-claims

Profile and notifications

TC-PROF-01Profile
PreSigned in.
1Open My profile from the user menuName Amara Devi, email amara.devi@example.sg, phone +65 9123 4567; claim alerts ticked
2Change the name to Amara D. Devi, Save“Your details have been updated.”; app bar shows Amara D. Devi
3Email not-an-email, Save“Enter a valid email address.”; the email wrapper data-error="true"
4Phone 123, Save“Enter a valid mobile number.”
5Phone +65 8123 4567, SaveSaved
Idsappbar-menu-profile · input-profile-name · input-profile-email · input-profile-phone · profile-button-save · div-error-profile · div-success-profile
TC-NOTIF-01Notifications record events, newest first
PreSigned in, fresh session.
1Open the bell“Nothing to show yet.”; no badge on the bell
2Add a payee, then transfer SGD 60 to Business CurrentBell badge reads 2
3Open the bellTwo notices; the first is the transfer (amount, destination, TFR reference), the second the payee
4Click Clear allEmpty state; badge gone
Idsappbar-notifications · appbar-notifications-badge · li-notice-<index> · notifications-button-clear · p-notifications-empty

End-to-end journeys

E2E-01Reinstate a lapsed policy, then claim against it
PreSigned in.
1Policies → View DriveSafe ComprehensiveLapse warning shown
2Pay premium → DriveSafe, Everyday Savings, 290 → PayPremium Paid
3Back to policiesDriveSafe badge Active
4Claims → File a claim → DriveSafe, Accident damage, a past date, 900, declaration → SubmitClaim Submitted (the same claim was impossible in step 1)
Idsdiv-policy-lapsed-warning · policy-button-pay-premium · badge-policy-status-pol3 · claims-button-new · span-claim-reference
E2E-02Add a payee, pay them, and see three witnesses agree
PreSigned in.
1Payees → add Jane Tan / DBS / 234-56789-0Row added, Verified No
2Transfer → payee Jane Tan (the new p4), amount 275Acknowledgement checkbox required; tick it; Continue; Confirm
3Note the TFR reference; Done
4AccountsEveryday Savings is SGD 18,145.05 (275 + 0.50 fee)
5BellFirst notice carries the reference and Jane Tan's name
Idspayee-button-add · select-transfer-payee · checkbox-transfer-ack-unverified · span-transfer-reference · td-account-balance-sav · li-notice-0
E2E-03Large claim with evidence, end to end
PreSigned in.
1Dashboard → File a claimClaim form
2GreatCare, Day surgery, past date, 3500, attach discharge-summary.pdf, declaration → SubmitClaim Submitted, reference CLM-nnnnnn
3View my claimsNew row, Submitted
4View itStatus Submitted; Documents: discharge-summary.pdf
Idsbutton-quick-file-claim · input-claim-file · claim-button-done · dd-claim-detail-status · p-claim-detail-documents
E2E-04A premium leaves the account it came from
PreSigned in.
1Dashboard → Pay a premium → PruLife Secure, Everyday Savings, 312.40 → PayReceipt PRM-nnnnnn
2Accounts → Open Everyday SavingsBalance SGD 18,108.15
3Search the history for the receipt numberOne Debit row, “GienTech premium — PruLife Secure”
Idsspan-premium-receipt · input-txn-search · td-history-description-<id>
E2E-05Log out abandons work in progress
PreSigned in.
1Transfer → type amount 999 (do not continue)
2User menu → Log outLogin card
3Open /#/transfer directlyRedirected to login; nothing of the draft survives
Idsappbar-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
1The Log in button is disabled until both fields have text, and disables again when one is clearedtoBeDisabled() / toBeEnabled() on the verbatim submit id
2Leaving Global ID empty on blur turns its wrapper red — and typing turns it backfieldWrapper(), toHaveAttribute('data-error', …)
3Three wrong passwords lock every control on the carda loop in the page object, data-disabled, toBeDisabled()
4The dashboard's Total Balance excludes the USD and EUR accountsdashboardTotals from the shared data; assert the exact string
5Filtering Transactions to Failed shows 3 rows — but only after Applyassert the count before and after clicking Apply
6Picking 1–5 September in the calendar popups yields 11 rowsReactDatepicker.pick(), the portal, --outside-month
7Page 4 holds five rows, Next is disabled there, and the last row is CLM-30288the pager ids; rows.last()
8A transfer to the unverified contractor is refused until the box is ticked, then charged SGD 0.50the acknowledgement checkbox; the confirmation panel's fee cell
9Paying SGD 290 on the lapsed motor policy turns its badge Active on the policy listtwo page objects in one test; the doubled outstanding amount
10A SGD 3,500 claim is refused without a document and accepted with one — and the document survives into the claim detailsetInputFiles() 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

npm run app:connect:build

npm test

npm run deploy:connect

apps/connect-for-bank/src

app/connect-for-bank.html
committed

connect-* projects

https://connect-for-bank.129.126.127.126.sslip.io

Figure 13.1 — One artefact feeds both the suite and the server. What is tested is what is deployed.

Server layout

PieceWhere
Document/var/www/connect-for-bank/index.html
Vhostszz-connect-for-bank-ssl.conf (443, DocumentRoot, deflate) and zz-connect-for-bank.conf (80 → 301)
CertificateLet's Encrypt via the default :80 webroot; certbot's timer renews
Logs/var/log/apache2/connect-for-bank-{access,error}.log
Provisioningtools/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)

ElementId
App bar, title, logobo-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)

ElementId
Required marker (present only when required)bo-packages-div-is-required
State-carrying wrapperbo-packages-div-labeldata-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

ElementId
Gradient shell, logo blockbo-packages-div, bo-packages-div-2
Card wrappersbo-packages-div-show-toast, bo-packages-div-show-toast-2, bo-packages-div-bcp-login-header-welcome-message, …-welcome-message-2
Welcome, app namebo-packages-span-bcp-login-header-welcome-message, bo-packages-h1-bcp-login-header-app-name
Form, form bodybo-packages-form-provider-form-bcp-login-input-label-global-id, bo-packages-form-provider-form-div-bcp-login-input-label-global-id
Global IDbo-packages-label-global-id, bo-packages-input-global-id
Password, togglebo-packages-label-password, bo-packages-input-password, bo-packages-button-toggle-password
Inline errorbo-packages-p-bcp-login-error
Submitbo-packages-form-provider-form-button-verify-login-button
Forgot passwordbo-packages-div-forgot-password, bo-packages-button-button-forgot-password
Demo credentials linebo-packages-p-bcp-login-demo-credentials

Dashboard

ElementId
Titlebo-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

ElementId
Listbo-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 factsbo-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

ElementId
Formbo-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

ElementId
Registerbo-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

ElementId
Listbo-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
Detailbo-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
Premiumbo-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

ElementId
Registerbo-packages-h1-claims, …-claims-button-new, …-table-claims, …-row-claim-<id>, …-td-claim-reference-<id>, …-badge-claim-status-<id>, …-button-open-claim-<id>
Formbo-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

ElementId
Profilebo-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
Notificationsbo-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.

AccountIdStatusBalanceWhy it exists
Everyday SavingssavActiveSGD 18,420.55Default funding account, 18 transactions
Business CurrentcurActiveSGD 42,100.00The own-account transfer destination
Multi-Currency USDusdDormantUSD 6,250.75Proves the dormancy rule
Multi-Currency EUReurActiveEUR 9,840.20Proves the SGD-only rule — active but not SGD
PolicyIdStatusPremiumSum assuredClaim types
PruLife Securepol1ActiveMonthly 312.40, due 2026-09-28500,000Death benefit · Terminal illness · Total permanent disability
GreatCare Hospitalpol2ActiveAnnual 940.00, due 2027-01-15200,000Hospitalisation · Day surgery · Outpatient · Critical illness
DriveSafe Comprehensivepol3LapsedQuarterly 145.00 — owes 290.0080,000Accident damage · Theft · Third party liability · Windscreen
PayeeIdBank · accountVerified
Lim Wei Jie (Landlord)p1DBS · 123-45678-9Yes
Sunrise Childcarep2OCBC · 552-11009-3Yes
Kaur Renovationsp3UOB · 901-77321-5No — proves the acknowledgement rule
ClaimIdPolicyAmountStatus
CLM-30288c1pol31,480.00Approved
CLM-31904c2pol23,250.00Assessing (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

CommandPurpose
npm run test:connectThe React target's suite on Chromium
npm run test:connect:allChromium, Firefox and WebKit
npx playwright test --project=connect-chromium --uiUI mode against this target
npm run app:connect:devVite dev server with hot reload
npm run app:connect:buildRebuild the single-file bundle into app/
npm run deploy:connect:provisionCreate the vhost and certificate (once)
npm run deploy:connectPush the bundle and verify the live URL
npm run docs:connectRebuild this book's standalone HTML
npm run deploy:connect-runbookPush 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.