From recorded actions to a reusable testing language#
The suite grew from recorded browser interactions into a library of named operations. This chapter follows the engineering decisions that made the later agent workflow possible, then connects them to the complete helper walkthroughs.
1. The application constraints#
I needed a testing approach that could work across our organization’s web applications, including legacy applications. In many cases, changing the frontend to add test hooks or improve its accessibility was outside the scope of the work. I therefore concentrated on the testing layer: reusable interactions, application-specific workflows, and documentation that could grow with the code. The stories, tests, and screenshots could also provide a rough draft of a user manual for the web application.
The work supported three related outcomes: exercising existing applications, documenting their workflows, and producing a useful first draft for a user manual. The stories supply the task and purpose; tests and screenshots supply steps and visual examples. A person still reviews the draft for the intended audience and missing context.
2. A recording as the starting point#
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByRole('option', { name: 'Test Account' }).locator('span').click();
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByRole('option', { name: 'Active' }).locator('span').click();
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByRole('option', { name: 'In Process' }).locator('span').click();
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByText('Stop Investment').click();
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByText('Cashout', { exact: true }).click();
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByText('ACH Failed', { exact: true }).click();
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByRole('option', { name: 'Holding - Eligible' }).locator('span').click();
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByRole('option', { name: 'Holding - Not Eligible' }).locator('span').click();
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByText('Admin Stop').click();
await page.getByRole('textbox', { name: 'Search' }).click();
await page.locator('#mat-mdc-form-field-label-12').getByText('Status').click();
await page.getByText('All').click();
await page.getByRole('heading', { name: 'Summary (Totals: 100)' }).click();
await page.locator('app-offering-enrollment-summary').getByText('Active').click();
await page.locator('app-offering-enrollment-summary').getByText('In Process').click();
await page.getByRole('columnheader', { name: 'Account ID' }).click();
await page.getByRole('columnheader', { name: 'Investor' }).click();
await page.getByRole('columnheader', { name: 'Monthly Withdrawal' }).click();
await page.getByRole('columnheader', { name: 'Pending for Withdrawal' }).click();
await page.getByRole('columnheader', { name: 'ACH Processing' }).click();
await page.getByRole('columnheader', { name: 'Awaiting Share Issuance' }).click();
The browser recording captures useful evidence: the control’s label, several observed options, and the action sequence. It also repeats the same selector and depends on a numbered Material-generated ID and a nested span. Changes to rendered structure can break several recorded lines at once.
Clicking through options also does not establish that filtering produced the correct records. This is a useful place to distinguish a recorded action from a test with an explicit expected result.
This longer sequence shows both the repeated selector and the changing values. The agent chapter later returns to recordings as input for stories and helper-based tests.
3. POM principles in a function-based implementation#
The suite applies the central Page Object Model idea: keep knowledge of how a page works behind a reusable API. That API is primarily exported functions grouped into domain and component modules. This is a function-based implementation of POM principles.
export async function viewFirstCITCorpDetails(page: Page): Promise<void> {
const visibilityButton = page.getByRole('button').filter({ hasText: 'visibility' });
await visibilityButton.first().click();
await page.waitForLoadState('networkidle');
}
The test can ask to view corporation details while this module owns the button lookup and navigation wait. A locator repair has one implementation point. This particular helper deliberately chooses the first matching button, so its contract is “open the first result,” not “open a particular corporation.”
The raw browser calls live inside this helper. The complete test walkthrough shows the resulting workflow-level calls in its test bodies.
The responsibilities form this structure:
Test scenario
-> page/domain workflow functions
-> reusable component and assertion functions
-> Playwright locators and browser interactions
Shared configuration supplies expected labels, columns, and options.
This diagram describes responsibilities, not a mandatory call chain: a spec may call a shared assertion or component helper directly.
4. Centralize setup and recurring UI difficulties#
The setup harness gives the test author one operation for opening a session and reaching a starting screen.
test.beforeAll(async ({ browser }) => {
({ context, page } = await setupTestPage(browser, {
menuItem: 'Account',
subMenuItem: 'Proof of Address'
}));
});
test.afterAll(async () => {
await page?.close();
await context?.close();
});
setupTestPage creates a browser context and page, applies common viewport and timeout settings, logs in as the super administrator, and attempts the requested sidebar navigation. The caller provides the destination and retains the returned resources for cleanup.
I centralized setup in this harness, though its Settings-specific fallback and fixed delay still needed attention when adapting it to other workflows.
Once setup has established the page, a recurring interaction can stay at the workflow level:
test('Navigate back to list', async () => {
await safeClick(page, 'Back');
await verifyHeadingPresence(page, 'Proof of Address', false);
await takeNamedScreenshot(page, 'proof_of_address', 'back_to_list');
await takeDocScreenshot(page, 'Investors', 'proof-of-address-back-to-list');
});
safeClick collects repeated interaction handling behind a named function. Its implementation accepts options for exact matching, fallback roles, timeouts, multiple matches, and overlays. Those mechanisms cope with the existing UI. Checking the resulting screen is a separate responsibility from clicking the control.
Centralizing this behavior makes fixes reusable. Broad fallbacks and retries still require review to ensure they target the intended control and preserve failures when a required action cannot complete.
6. Move common values out of individual scenarios#
The next layer was shared configuration and machine-readable references. The runnable status lists, column names, and navigation values are exported from TypeScript configuration, while JSON describes helper APIs for agent use. The examples below retain that distinction.
ProofOfAddress: ['All', 'Pending Approval', 'Need More Info', 'Denied', 'Approved'],
test('Test Status dropdown options', async () => {
await testDropdownOptions(page, 'Status', statusOptions.ProofOfAddress);
await takeNamedScreenshot(page, 'community', 'poa_status_dropdown');
});
The admin Proof of Address spec imports and uses the same statusOptions.ProofOfAddress key. This demonstrates reuse across two portals while other exports, such as communityPortalDropdowns and transferSoloDropdowns, preserve portal-specific expectations.
The useful improvement is the ownership of expectations: a shared business option list has one configuration entry instead of being copied into every scenario. It still needs review when application behavior changes; updating the expected values to match a defect would weaken the test.
7. JSON helper references as context for agents#
There is a concrete JSON artifact suitable for the next stage: function-definitions.json. It describes helper APIs, parameters, usage scenarios, and related functions. This is a machine-readable helper reference, distinct from the TypeScript runtime configuration above.
"loginAsSuperAdmin": {
"definition": "Logs into the application using super admin credentials from environment variables and navigates to the dashboard",
"parameters": [
{
"name": "page",
"type": "Page",
"description": "Playwright page instance",
"required": true
}
],
This deliberately partial fragment is not a standalone JSON document. The complete entry also includes a process description, return type, usage scenarios, common patterns, and related helpers.
I used documented helper functions to guide agents when drafting tests. This JSON reference gave them function names, parameters, and usage examples to consult, and needed to stay in step with changes to the helpers.
The email and address walkthroughs show complete illustrative JSON files and their consumers. In the suite itself, TypeScript holds the runtime expectations shown above, while function-definitions.json describes helper APIs for agents.
The later workflow can now build on these ingredients: record a user journey, identify its behavior, map that behavior to known helpers and configuration, generate readable scenarios, and inspect actual failures to improve the shared implementation.