Primary and mailing addresses: one workflow, several helper generations#
Address entry repeated across forms: street, apartment or suite, city, state, postal code, and sometimes a separate mailing address. I moved those steps into shared functions so the test could ask to fill an address. A field-level change then had a shared implementation to update.
This walkthrough starts with the complete fillPrimaryAndMailingAddresses function, then follows the configurable fillPrimaryAndMailingAddressesCITCorp version through inline and JSON calls. The larger test examples use sample data and are explicitly illustrative.
The original operation behind the short call#
export async function fillPrimaryAndMailingAddresses(page: Page, useMailingAddress = false): Promise<void> {
await page.locator('#mat-input-6').fill('123 NW Madison St');
await page.locator('#mat-input-7').fill('Unit 101');
await page.locator('#mat-input-8').fill('Portland');
// Click the first "State"
await page.getByText("State", { exact: true }).nth(0).click();
await page.getByRole('option', { name: 'Alabama' }).click();
await page.locator('#mat-input-9').fill('97204');
if (useMailingAddress) {
await page.locator('#mat-input-10').fill('342 SW Madison St.');
await page.locator('#mat-input-11').fill('1002');
await page.locator('#mat-input-12').fill('Portland');
// Handle mailing address state dropdown
await page.getByRole("combobox", { name: "State" }).nth(1).click();
await page.waitForTimeout(1000); // Wait for animation
// Select Alabama from the dropdown
await page.keyboard.press('ArrowDown'); // Move to first option
await page.keyboard.press('Enter'); // Select it
await page.waitForTimeout(1000); // Wait for selection to register
// 1. Locate the second address block
const mailingSection = page.locator("app-company-address").nth(1);
// 2. Within that block, find "Zip Code" and fill
await mailingSection.getByLabel("Zip Code").fill("97204");
// or
await mailingSection.getByRole("textbox", { name: "Zip Code" }).fill("97204");
}
}
The primary fields are always filled. Passing true enters the mailing-address branch as well. In that branch, the function locates the second app-company-address component to distinguish its Zip Code field from the primary address’s field.
An illustrative call, with an authenticated page already on the address form, is:
import { fillPrimaryAndMailingAddresses } from '../../HelperFiles/components/address/address-helper';
// Fill primary fields using the helper's embedded values.
await fillPrimaryAndMailingAddresses(page);
// Alternatively, also fill the mailing address.
await fillPrimaryAndMailingAddresses(page, true);
These are two alternative calls. In a real scenario, choose the branch needed for that form. The helper fills fields; submission and checking persisted values belong to subsequent workflow steps.
Centralization made the test shorter, but the implementation still uses numbered Material input IDs, embedded values, and fixed waits. It even fills the mailing Zip Code twice through two selectors. Keeping the complete function visible makes that distinction concrete: the first improvement was giving repeated work one home; improving the selectors and data API was further work.
Accept address data without changing the scenario’s purpose#
The CIT Corporation variant accepts a PrimaryAddress object with a nested mailing address. Here is its complete implementation, including the defaults and both branches:
export async function fillPrimaryAndMailingAddressesCITCorp(
page: Page,
useMailingAddress: boolean = false,
justRequired: boolean = false,
{
primaryStreetName = 'Test Street Ave',
primaryAptNumber = 'Unit 101',
primaryCityName = 'Portland',
primaryStateName = 'Alabama',
primaryZipCode = '97204',
mailingAddress: {
mailingStreetName = '342 SW Madison St.',
mailingAptNumber = '1002',
mailingCityName = 'Portland',
mailingStateName = 'Alabama',
mailingZipCode = '97204',
} = {} // Default for mailingAddress if not provided
}: PrimaryAddress = {}
): Promise<void> {
console.log('[fillPrimaryAndMailingAddressesCITCorp] Entering function.');
if (justRequired) {
console.log(`[fillPrimaryAndMailingAddressesCITCorp] justRequired: ${justRequired}`);
// Fill minimal primary address
console.log('Filling Street Name...');
await fillStreetName(page, primaryStreetName);
console.log('Filling City...');
const cityInput = page.getByLabel('City', { exact: true }).nth(0);
await cityInput.waitFor({ state: 'visible', timeout: 5000 });
await cityInput.fill(primaryCityName);
console.log('Filling State...');
const stateDropdown = page.getByLabel('State', { exact: true }).nth(0);
await stateDropdown.waitFor({ state: 'visible', timeout: 5000 });
await stateDropdown.click();
await page.waitForTimeout(3000);
const primaryStateOption = page.getByRole('option', { name: primaryStateName });
await primaryStateOption.click();
console.log('Filling Zip Code...');
const zipInput = page.getByLabel('Zip Code', { exact: true }).nth(0);
await zipInput.waitFor({ state: 'visible', timeout: 5000 });
await zipInput.fill(primaryZipCode);
console.log('Completed filling minimal required fields for the primary address.');
} else {
console.log('Filling full primary address...');
await fillStreetName(page, primaryStreetName);
// Apt / Suite
const suiteAptInput = page.getByLabel('Suite/Apt Number', { exact: true }).nth(0);
await suiteAptInput.waitFor({ state: 'visible', timeout: 5000 });
await suiteAptInput.fill(primaryAptNumber);
// City
const cityInput = page.getByLabel('City', { exact: true }).nth(0);
await cityInput.waitFor({ state: 'visible', timeout: 5000 });
await cityInput.fill(primaryCityName);
// State
const stateDropdown = page.getByLabel('State', { exact: true }).nth(0);
await stateDropdown.waitFor({ state: 'visible', timeout: 5000 });
await stateDropdown.click();
await page.waitForTimeout(3000);
const stateOption = page.getByRole('option', { name: primaryStateName });
await stateOption.click();
// Zip
const zipInput = page.getByLabel('Zip Code', { exact: true }).nth(0);
await zipInput.waitFor({ state: 'visible', timeout: 5000 });
await zipInput.fill(primaryZipCode);
// If we also want to fill mailing address
if (useMailingAddress) {
console.log('Filling mailing address...');
const mailingAddressSection = page.locator('app-company-address').nth(1);
// Street
const mailingStreet = mailingAddressSection.getByLabel('Street Name', { exact: true });
await mailingStreet.waitFor({ state: 'visible', timeout: 5000 });
await mailingStreet.fill(mailingStreetName);
// Apt
const mailingSuite = mailingAddressSection.getByLabel('Suite/Apt Number', { exact: true });
await mailingSuite.waitFor({ state: 'visible', timeout: 5000 });
await mailingSuite.fill(mailingAptNumber);
// City
const mailingCity = mailingAddressSection.getByLabel('City', { exact: true });
await mailingCity.waitFor({ state: 'visible', timeout: 5000 });
await mailingCity.fill(mailingCityName);
// State
const mailingStateDropdown = mailingAddressSection.getByLabel('State', { exact: true });
await mailingStateDropdown.waitFor({ state: 'visible', timeout: 5000 });
await mailingStateDropdown.click();
await page.waitForTimeout(3000);
const mailingStateOption = page.getByRole('option', { name: mailingStateName });
await mailingStateOption.click();
// Zip
const mailingZip = mailingAddressSection.getByLabel('Zip Code', { exact: true });
await mailingZip.waitFor({ state: 'visible', timeout: 5000 });
await mailingZip.fill(mailingZipCode);
console.log('Completed filling mailing address.');
}
}
}
The argument order is page, useMailingAddress, justRequired, then the address values. The source’s destructuring provides defaults for omitted values and for the nested mailing-address object.
Arguments after |
Branch taken |
|---|---|
|
Fill the full primary address, including suite/apartment. |
|
Fill the full primary address and the mailing address. |
|
Fill only the minimal primary-address fields. |
|
Still fill only minimal primary fields; the mailing branch is inside |
The full-address branch scopes mailing fields to the second address component. Primary fields use their first matching label. State options are located on the page after opening the corresponding control, which accommodates an option list rendered outside the address component.
The related fillPrimaryAndMailingAddressesAllApproaches function composes fallback field helpers but keeps its values embedded. Its name does not mean it accepts the configurable CIT Corporation signature.
A complete test with inline address values#
This illustrative spec uses CIT_CORP_ADDRESS_TEST_URL as an example environment variable for an address form that is ready to edit after login. Any prerequisite record creation or navigation must be supplied by the application’s setup if the form cannot be opened directly.
import { test } from '@playwright/test';
import { loginAsSuperAdmin } from '../../HelperFiles/cit-app-helpers';
import { fillPrimaryAndMailingAddressesCITCorp } from '../../HelperFiles/components/address/address-helper';
const addressFormUrl = process.env.CIT_CORP_ADDRESS_TEST_URL;
if (!addressFormUrl) {
throw new Error('Set CIT_CORP_ADDRESS_TEST_URL to the address form');
}
test.describe('CIT Corporation address entry', () => {
test.beforeEach(async ({ page }) => {
await loginAsSuperAdmin(page);
await page.goto(addressFormUrl);
});
test('Enter separate primary and mailing addresses', async ({ page }) => {
await fillPrimaryAndMailingAddressesCITCorp(page, true, false, {
primaryStreetName: '123 Example Avenue',
primaryAptNumber: 'Suite 100',
primaryCityName: 'Portland',
primaryStateName: 'Oregon',
primaryZipCode: '97204',
mailingAddress: {
mailingStreetName: '456 Example Street',
mailingAptNumber: 'Suite 200',
mailingCityName: 'Portland',
mailingStateName: 'Oregon',
mailingZipCode: '97205'
}
});
});
test('Enter only the required primary fields', async ({ page }) => {
await fillPrimaryAndMailingAddressesCITCorp(page, false, true, {
primaryStreetName: '123 Example Avenue',
primaryCityName: 'Portland',
primaryStateName: 'Oregon',
primaryZipCode: '97204'
});
});
});
These examples exercise form entry. A test of saving an organization would continue with the application’s submit helper and checks against the saved record. The function does not make those assertions on the caller’s behalf.
The same call with JSON configuration#
An illustrative address.json at the suite root holds the flags and values together:
{
"useMailingAddress": true,
"justRequired": false,
"values": {
"primaryStreetName": "123 Example Avenue",
"primaryAptNumber": "Suite 100",
"primaryCityName": "Portland",
"primaryStateName": "Oregon",
"primaryZipCode": "97204",
"mailingAddress": {
"mailingStreetName": "456 Example Street",
"mailingAptNumber": "Suite 200",
"mailingCityName": "Portland",
"mailingStateName": "Oregon",
"mailingZipCode": "97205"
}
}
}
Postal codes stay strings, preserving formatting and leading zeroes. The configuration corresponds to the existing PrimaryAddress and MailingAddress interfaces.
The complete alternative spec uses the same setup and moves the data out of its test body. It assumes execution from the suite root:
import { test } from '@playwright/test';
import { readFileSync } from 'node:fs';
import { loginAsSuperAdmin } from '../../HelperFiles/cit-app-helpers';
import { fillPrimaryAndMailingAddressesCITCorp } from '../../HelperFiles/components/address/address-helper';
import type { PrimaryAddress } from '../../HelperFiles/types/address';
type AddressConfig = {
useMailingAddress: boolean;
justRequired: boolean;
values: PrimaryAddress;
};
const config: AddressConfig = JSON.parse(
readFileSync('address.json', 'utf8')
);
const addressFormUrl = process.env.CIT_CORP_ADDRESS_TEST_URL;
if (!addressFormUrl) {
throw new Error('Set CIT_CORP_ADDRESS_TEST_URL to the address form');
}
test.describe('Configured CIT Corporation address entry', () => {
test.beforeEach(async ({ page }) => {
await loginAsSuperAdmin(page);
await page.goto(addressFormUrl);
});
test('Enter the configured primary and mailing addresses', async ({ page }) => {
await fillPrimaryAndMailingAddressesCITCorp(
page,
config.useMailingAddress,
config.justRequired,
config.values
);
});
});
The type describes the expected configuration to TypeScript; it does not perform runtime JSON validation. The sample file is a checked-in fixture for this illustration. Most importantly, the basic two-argument helper has not acquired a fictional JSON parameter: the loader passes the values to the variant that already accepts them.
Reuse this explanation later#
The helper and configuration tour locates these modules in the suite. Docs as code follows the address functions into their generated developer reference. The agent workflow builds on this same distinction between a known operation and the data supplied to it.
Continue to named screenshots, or return to Functions explained.