Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,46 @@ test.describe('Broker Protection communications', () => {
await dbp.doesInputValueEqual('#full-state', 'District Of Columbia');
});

test('fillForm with state-aware generated ZIP and default city', async ({ page }, workerInfo) => {
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
await dbp.enabled();
await dbp.navigatesTo('form.html');
await dbp.receivesInlineAction({
state: {
action: {
actionType: 'fillForm',
id: '3',
selector: '.ahm',
elements: [{ type: '$generated_zip_code$', selector: '#user_zip_code', useState: true }],
},
data: { extractedProfile: { state: 'AK' } },
},
});
const response = await dbp.collector.waitForMessage('actionCompleted');
dbp.isSuccessMessage(response);
await dbp.doesInputValueEqual('#user_zip_code', '99501');
});

test('fillForm with state-aware generated ZIP and matching city', async ({ page }, workerInfo) => {
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
await dbp.enabled();
await dbp.navigatesTo('form.html');
await dbp.receivesInlineAction({
state: {
action: {
actionType: 'fillForm',
id: '3',
selector: '.ahm',
elements: [{ type: '$generated_zip_code$', selector: '#user_zip_code', useState: true }],
},
data: { extractedProfile: { city: 'Juneau', state: 'AK' } },
},
});
const response = await dbp.collector.waitForMessage('actionCompleted');
dbp.isSuccessMessage(response);
await dbp.doesInputValueEqual('#user_zip_code', '99801');
});

test('fillForm with select containing numbers', async ({ page }, workerInfo) => {
const dbp = BrokerProtectionPage.create(page, workerInfo.project.use);
await dbp.enabled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ export class BrokerProtectionPage {
}

/**
* @param {{state: {action: Record<string, any>}}} action
* @param {{state: {action: Record<string, any>; data?: Record<string, any>}}} action
* @return {Promise<void>}
*/
async receivesInlineAction(action) {
Expand Down
22 changes: 11 additions & 11 deletions injected/src/features/broker-protection/actions/fill-form.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { getElement, generateRandomInt } from '../utils/utils.js';
import { getElement, generateRandomInt, hasOwn } from '../utils/utils.js';
import { ErrorResponse, SuccessResponse } from '../types.js';
import { generatePhoneNumber, generateZipCode, generateStreetAddress } from './generators.js';
import { states } from '../comparisons/constants.js';

/**
* @param {Record<string, any>} action
* @param {Record<string, any>} userData
* @param {import('../types.js').FillFormAction} action
* @param {import('../types.js').BrokerProtectionProfile} userData
* @param {Document | HTMLElement} root
* @return {import('../types.js').ActionResponse}
*/
Expand Down Expand Up @@ -36,8 +36,8 @@ export function fillForm(action, userData, root = document) {
/**
* Try to fill form elements. Collecting results + warnings for reporting.
* @param {HTMLElement} root
* @param {{selector: string; type: string; min?: string; max?: string;}[]} elements
* @param {Record<string, any>} data
* @param {import('../types.js').FillFormElement[]} elements
* @param {import('../types.js').BrokerProtectionProfile} data
* @return {({result: true} | {result: false; error: string})[]}
*/
export function fillMany(root, elements, data) {
Expand All @@ -55,7 +55,7 @@ export function fillMany(root, elements, data) {
} else if (element.type === '$generated_phone_number$') {
results.push(setValueForInput(inputElem, generatePhoneNumber()));
} else if (element.type === '$generated_zip_code$') {
results.push(setValueForInput(inputElem, generateZipCode()));
results.push(setValueForInput(inputElem, generateZipCode(element.useState ? data : null)));
} else if (element.type === '$generated_random_number$') {
if (!element.min || !element.max) {
results.push({
Expand All @@ -81,7 +81,7 @@ export function fillMany(root, elements, data) {

// This is a composite of existing (but separate) city and state fields
} else if (element.type === 'cityState') {
if (!Object.prototype.hasOwnProperty.call(data, 'city') || !Object.prototype.hasOwnProperty.call(data, 'state')) {
if (!hasOwn(data, 'city') || !hasOwn(data, 'state')) {
results.push({
result: false,
error: `element found with selector '${element.selector}', but data didn't contain the keys 'city' and 'state'`,
Expand All @@ -90,7 +90,7 @@ export function fillMany(root, elements, data) {
}
results.push(setValueForInput(inputElem, data.city + ', ' + data.state));
} else if (element.type === 'fullState') {
if (!Object.prototype.hasOwnProperty.call(data, 'state')) {
if (!hasOwn(data, 'state')) {
results.push({
result: false,
error: `element found with selector '${element.selector}', but data didn't contain the key 'state'`,
Expand All @@ -100,7 +100,7 @@ export function fillMany(root, elements, data) {

const state = data.state;

if (!Object.prototype.hasOwnProperty.call(states, state)) {
if (!hasOwn(states, state)) {
results.push({
result: false,
error: `element found with selector '${element.selector}', but data contained an invalid 'state' abbreviation`,
Expand All @@ -115,7 +115,7 @@ export function fillMany(root, elements, data) {
if (isElementTypeOptional(element.type)) {
continue;
}
if (!Object.prototype.hasOwnProperty.call(data, element.type)) {
if (!hasOwn(data, element.type)) {
results.push({
result: false,
error: `element found with selector '${element.selector}', but data didn't contain the key '${element.type}'`,
Expand All @@ -129,7 +129,7 @@ export function fillMany(root, elements, data) {
});
continue;
}
results.push(setValueForInput(inputElem, data[element.type]));
results.push(setValueForInput(inputElem, /** @type {string} */ (data[element.type])));
}
}

Expand Down
31 changes: 27 additions & 4 deletions injected/src/features/broker-protection/actions/generators.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { generateRandomInt } from '../utils/utils.js';
import { generateRandomInt, getOwn, hasOwn } from '../utils/utils.js';
import STATE_CITY_ZIPS from './state-city-zips.json' with { type: 'json' };

export function generatePhoneNumber() {
/**
Expand All @@ -14,9 +15,31 @@ export function generatePhoneNumber() {
return `${areaCode}${exchangeCode}${lineNumber}`;
}

export function generateZipCode() {
const zipCode = generateRandomInt(10000, 99999).toString();
return zipCode;
/**
* @return {string} Random 5-digit ZIP code.
*/
const generateRandomZipCode = () => generateRandomInt(10000, 99999).toString();

/**
* Generate a 5-digit ZIP code. When state and city are provided, looks up a
* real ZIP from a curated city list. If the city is not in the list, falls
* back to the default (largest) city in the state. If the state is unknown,
* returns a random 5-digit number.
*
* @param {{state?: unknown; city?: unknown} | null} [params]
* @return {string}
*/
export function generateZipCode(params) {
const { state, city } = params ?? {};
const stateUpperCase = typeof state === 'string' ? state.toUpperCase() : null;
if (!stateUpperCase || !hasOwn(STATE_CITY_ZIPS, stateUpperCase)) {
return generateRandomZipCode();
}

const stateCities = STATE_CITY_ZIPS[stateUpperCase];
const cityUpperCase = typeof city === 'string' ? city.toUpperCase() : null;
const zips = getOwn(stateCities, cityUpperCase) || getOwn(stateCities, stateCities._default);
return zips?.[generateRandomInt(0, zips.length - 1)] ?? generateRandomZipCode();
}

export function generateStreetAddress() {
Expand Down
Loading
Loading