Skip to content

Commit afc0584

Browse files
committed
test(davinci-client): e2e tests for agreements, single checkbox, rich text
1 parent 9f93d5e commit afc0584

11 files changed

Lines changed: 139 additions & 86 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,5 @@ GEMINI.md
100100

101101
# Polaris
102102
.polaris-setup-progress.json
103+
.polaris
104+
.playwright-mcp

e2e/davinci-app/components/boolean.ts

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,37 +4,39 @@
44
* This software may be modified and distributed under the terms
55
* of the MIT license. See the LICENSE file for details.
66
*/
7-
import type { ValidatedBooleanCollector, Updater } from '@forgerock/davinci-client/types';
7+
import type {
8+
ValidatedBooleanCollector,
9+
Updater,
10+
Validator,
11+
} from '@forgerock/davinci-client/types';
12+
import { dotToCamelCase } from '../helper.js';
813

914
/**
1015
* Creates a single checkbox and attaches it to the form
11-
* @param {HTMLFormElement} formEl - The form element to attach the checkboxes to
16+
* @param {HTMLFormElement} formEl - The form element to attach the checkbox to
1217
* @param {ValidatedBooleanCollector} collector - Contains the configuration
1318
* @param {Updater} updater - Function to call when selection changes
1419
*/
1520
export default function booleanComponent(
1621
formEl: HTMLFormElement,
1722
collector: ValidatedBooleanCollector,
1823
updater: Updater<ValidatedBooleanCollector>,
24+
validator: Validator<ValidatedBooleanCollector>,
1925
) {
20-
// Create a container for the checkboxes
26+
const collectorKey = dotToCamelCase(collector.output.key);
27+
28+
// Create a container for the checkbox
2129
const containerDiv = document.createElement('div');
2230
containerDiv.className = 'single-checkbox-container';
2331

24-
// Create a heading/label for the checkbox group
25-
const groupLabel = document.createElement('div');
26-
groupLabel.textContent = collector.output.label || 'Single Checkbox';
27-
groupLabel.className = 'single-checkbox-label';
28-
containerDiv.appendChild(groupLabel);
29-
30-
// Create checkboxes for each option
32+
// Create a single checkbox
3133
const wrapper = document.createElement('div');
3234
wrapper.className = 'checkbox-wrapper';
3335

3436
const checkbox = document.createElement('input');
3537
checkbox.type = 'checkbox';
36-
checkbox.id = collector.output.key;
37-
checkbox.name = collector.output.key || 'single-checkbox-field';
38+
checkbox.id = collectorKey;
39+
checkbox.name = collectorKey || 'single-checkbox-field';
3840
checkbox.checked = collector.output.value;
3941
checkbox.value = 'checked';
4042

@@ -44,8 +46,25 @@ export default function booleanComponent(
4446

4547
// Add event listener to handle single-select behavior
4648
checkbox.addEventListener('change', (event) => {
47-
const target = event.target as HTMLInputElement;
48-
updater(target.checked);
49+
const checked = (event.target as HTMLInputElement).checked;
50+
const result = validator(checked);
51+
const errorEl = formEl?.querySelector(`.${collectorKey}-error`);
52+
53+
// Keep collector state aligned with the current UI value
54+
const updateError = updater(checked);
55+
if (updateError && 'error' in updateError) {
56+
console.error(updateError.error.message);
57+
}
58+
59+
// Validate the input
60+
if (Array.isArray(result) && result.length && !errorEl) {
61+
const errorEl = document.createElement('div');
62+
errorEl.className = `${collectorKey}-error`;
63+
errorEl.innerText = result.join(', ');
64+
formEl?.querySelector(`#${collectorKey}`)?.after(errorEl);
65+
} else {
66+
formEl.querySelector(`.${collectorKey}-error`)?.remove();
67+
}
4968
});
5069

5170
wrapper.appendChild(checkbox);

e2e/davinci-app/components/text.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function textComponent(
1616
formEl: HTMLFormElement,
1717
collector: TextCollector | ValidatedTextCollector,
1818
updater: Updater<TextCollector | ValidatedTextCollector>,
19-
validator: Validator,
19+
validator: Validator<ValidatedTextCollector>,
2020
) {
2121
const collectorKey = dotToCamelCase(collector.output.key);
2222
const label = document.createElement('label');

e2e/davinci-app/main.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,12 @@ const urlParams = new URLSearchParams(window.location.search);
300300
} else if (collector.type === 'MultiSelectCollector') {
301301
multiValueComponent(formEl, collector, davinciClient.update(collector));
302302
} else if (collector.type === 'ValidatedBooleanCollector') {
303-
booleanComponent(formEl, collector, davinciClient.update(collector));
303+
booleanComponent(
304+
formEl,
305+
collector,
306+
davinciClient.update(collector),
307+
davinciClient.validate(collector),
308+
);
304309
}
305310
});
306311

e2e/davinci-app/server-configs.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,16 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
4646
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
4747
},
4848
},
49-
'60de77d5-dd2c-41ef-8c40-f8bb2381a359': {
50-
clientId: '60de77d5-dd2c-41ef-8c40-f8bb2381a359',
49+
/**
50+
* Form Fields
51+
*/
52+
'e4ef2896-8d90-4abd-bf0f-7b8034995927': {
53+
clientId: 'e4ef2896-8d90-4abd-bf0f-7b8034995927',
5154
redirectUri: window.location.origin + '/',
5255
scope: 'openid profile email name revoke',
5356
serverConfig: {
5457
wellknown:
55-
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
58+
'https://auth.pingone.ca/356a254c-cba3-4ade-be1a-860136e8df01/as/.well-known/openid-configuration',
5659
},
5760
},
5861
/**

e2e/davinci-suites/src/form-fields.test.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ import { asyncEvents } from './utils/async-events.js';
1010

1111
test('Should render form fields', async ({ page }) => {
1212
const { navigate } = asyncEvents(page);
13-
await navigate('/?clientId=60de77d5-dd2c-41ef-8c40-f8bb2381a359');
13+
await navigate('/?clientId=e4ef2896-8d90-4abd-bf0f-7b8034995927');
1414

15-
await expect(page.getByText('Select Test Form')).toBeVisible();
15+
await expect(page.getByText('Select Form Fields Test Form')).toBeVisible();
1616
await page.getByRole('button', { name: 'Form Fields' }).click();
1717

1818
await expect(page.getByText('Form Fields Test')).toBeVisible();
@@ -34,6 +34,32 @@ test('Should render form fields', async ({ page }) => {
3434
await page.locator('#phone-number-input-1').fill('1234567890');
3535
await page.locator('#extension-input-1').fill('7890');
3636

37+
// Rich text should render a link
38+
await expect(page.getByRole('link', { name: 'Ping Identity' })).toBeVisible();
39+
await expect(page.getByRole('link', { name: 'Ping Identity' })).toHaveAttribute(
40+
'href',
41+
'https://www.pingidentity.com',
42+
);
43+
44+
// Agreement title and content should be visible
45+
await expect(page.getByRole('heading', { name: 'Terms of Service Agreement' })).toBeVisible();
46+
await expect(
47+
page.getByText(
48+
'This is example agreement text, you can edit this text in the agreements section.',
49+
),
50+
).toBeVisible();
51+
52+
// Single checkbox default value
53+
await expect(page.locator('#single-checkbox-field')).not.toBeChecked();
54+
await expect(page.getByText('I agree to the Terms and Conditions')).toBeVisible();
55+
56+
// Toggle the single checkbox and assert that it is optional by the abscence of an error message
57+
await page.locator('#single-checkbox-field').check();
58+
await expect(page.locator('#single-checkbox-field')).toBeChecked();
59+
await page.locator('#single-checkbox-field').uncheck();
60+
await expect(page.locator('#single-checkbox-field')).not.toBeChecked();
61+
await expect(page.locator('.single-checkbox-field-error')).not.toBeAttached();
62+
3763
await expect(page.getByRole('button', { name: 'Flow Button' })).toBeVisible();
3864
await expect(page.getByRole('button', { name: 'Flow Link' })).toBeVisible();
3965

@@ -53,20 +79,20 @@ test('Should render form fields', async ({ page }) => {
5379
'checkbox-field-key': ['option1 value', 'option2 value'],
5480
'dropdown-field-key': 'dropdown-option2-value',
5581
'radio-group-key': 'option2 value',
56-
'single-checkbox-field': false,
5782
'combobox-field-key': ['option1 value', 'option3 value'],
5883
'phone-field': {
5984
phoneNumber: '1234567890',
6085
countryCode: 'GB',
6186
extension: '7890', // Tests PhoneNumberExtensionCollector
6287
},
88+
'single-checkbox-field': false,
6389
});
6490
});
6591

6692
test('should render form validation fields', async ({ page }) => {
67-
await page.goto('http://localhost:5829/?clientId=60de77d5-dd2c-41ef-8c40-f8bb2381a359');
93+
await page.goto('http://localhost:5829/?clientId=e4ef2896-8d90-4abd-bf0f-7b8034995927');
6894

69-
await expect(page.getByText('Select Test Form')).toBeVisible();
95+
await expect(page.getByText('Select Form Fields Test Form')).toBeVisible();
7096

7197
await page.getByRole('button', { name: 'Form Validation' }).click();
7298

@@ -80,4 +106,9 @@ test('should render form validation fields', async ({ page }) => {
80106

81107
await page.getByRole('textbox', { name: 'Email Address' }).fill('abc@email.com');
82108
await expect(page.getByText('Not a valid email')).not.toBeVisible();
109+
110+
// Toggle the single checkbox to assert error message
111+
await page.locator('#single-checkbox-field').check();
112+
await page.locator('#single-checkbox-field').uncheck();
113+
await expect(page.getByText('Select the checkbox to continue.')).toBeVisible();
83114
});

packages/davinci-client/api-report/davinci-client.api.md

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -284,13 +284,11 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
284284
resume: (input: {
285285
continueToken: string;
286286
}) => Promise<InternalErrorResponse | NodeStates>;
287-
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode>;
287+
start: <QueryParams extends OutgoingQueryParams = OutgoingQueryParams>(options?: StartOptions<QueryParams> | undefined) => Promise<ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode>;
288288
update: <T extends SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors>(collector: T) => Updater<T>;
289289
validate: (collector: SingleValueCollectors | ObjectValueCollectors | MultiValueCollectors | AutoCollectors) => Validator;
290290
pollStatus: (collector: PollingCollector) => Poller;
291291
getClient: () => {
292-
status: "start";
293-
} | {
294292
action: string;
295293
collectors: Collectors[];
296294
description?: string;
@@ -302,22 +300,22 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
302300
description?: string;
303301
name?: string;
304302
status: "error";
303+
} | {
304+
status: "failure";
305+
} | {
306+
status: "start";
305307
} | {
306308
authorization?: {
307309
code?: string;
308310
state?: string;
309311
};
310312
status: "success";
311-
} | {
312-
status: "failure";
313313
} | null;
314314
getCollectors: () => Collectors[];
315315
getError: () => DaVinciError | null;
316316
getErrorCollectors: () => CollectorErrors[];
317-
getNode: () => ContinueNode | ErrorNode | StartNode | SuccessNode | FailureNode;
317+
getNode: () => ContinueNode | ErrorNode | FailureNode | StartNode | SuccessNode;
318318
getServer: () => {
319-
status: "start";
320-
} | {
321319
_links?: Links;
322320
id?: string;
323321
interactionId?: string;
@@ -335,20 +333,22 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
335333
} | {
336334
_links?: Links;
337335
eventName?: string;
336+
href?: string;
338337
id?: string;
339338
interactionId?: string;
340339
interactionToken?: string;
341-
href?: string;
342-
session?: string;
343-
status: "success";
340+
status: "failure";
341+
} | {
342+
status: "start";
344343
} | {
345344
_links?: Links;
346345
eventName?: string;
347-
href?: string;
348346
id?: string;
349347
interactionId?: string;
350348
interactionToken?: string;
351-
status: "failure";
349+
href?: string;
350+
session?: string;
351+
status: "success";
352352
} | null;
353353
cache: {
354354
getLatestResponse: () => ((state: RootState< {
@@ -382,14 +382,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
382382
} & Omit<{
383383
requestId: string;
384384
data?: unknown;
385-
error?: SerializedError | FetchBaseQueryError | undefined;
385+
error?: FetchBaseQueryError | SerializedError | undefined;
386386
endpointName: string;
387387
startedTimeStamp: number;
388388
fulfilledTimeStamp?: number;
389389
}, "data" | "fulfilledTimeStamp"> & Required<Pick<{
390390
requestId: string;
391391
data?: unknown;
392-
error?: SerializedError | FetchBaseQueryError | undefined;
392+
error?: FetchBaseQueryError | SerializedError | undefined;
393393
endpointName: string;
394394
startedTimeStamp: number;
395395
fulfilledTimeStamp?: number;
@@ -406,7 +406,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
406406
} & {
407407
requestId: string;
408408
data?: unknown;
409-
error?: SerializedError | FetchBaseQueryError | undefined;
409+
error?: FetchBaseQueryError | SerializedError | undefined;
410410
endpointName: string;
411411
startedTimeStamp: number;
412412
fulfilledTimeStamp?: number;
@@ -423,14 +423,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
423423
} & Omit<{
424424
requestId: string;
425425
data?: unknown;
426-
error?: SerializedError | FetchBaseQueryError | undefined;
426+
error?: FetchBaseQueryError | SerializedError | undefined;
427427
endpointName: string;
428428
startedTimeStamp: number;
429429
fulfilledTimeStamp?: number;
430430
}, "error"> & Required<Pick<{
431431
requestId: string;
432432
data?: unknown;
433-
error?: SerializedError | FetchBaseQueryError | undefined;
433+
error?: FetchBaseQueryError | SerializedError | undefined;
434434
endpointName: string;
435435
startedTimeStamp: number;
436436
fulfilledTimeStamp?: number;
@@ -477,14 +477,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
477477
} & Omit<{
478478
requestId: string;
479479
data?: unknown;
480-
error?: SerializedError | FetchBaseQueryError | undefined;
480+
error?: FetchBaseQueryError | SerializedError | undefined;
481481
endpointName: string;
482482
startedTimeStamp: number;
483483
fulfilledTimeStamp?: number;
484484
}, "data" | "fulfilledTimeStamp"> & Required<Pick<{
485485
requestId: string;
486486
data?: unknown;
487-
error?: SerializedError | FetchBaseQueryError | undefined;
487+
error?: FetchBaseQueryError | SerializedError | undefined;
488488
endpointName: string;
489489
startedTimeStamp: number;
490490
fulfilledTimeStamp?: number;
@@ -501,7 +501,7 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
501501
} & {
502502
requestId: string;
503503
data?: unknown;
504-
error?: SerializedError | FetchBaseQueryError | undefined;
504+
error?: FetchBaseQueryError | SerializedError | undefined;
505505
endpointName: string;
506506
startedTimeStamp: number;
507507
fulfilledTimeStamp?: number;
@@ -518,14 +518,14 @@ export function davinci<ActionType extends ActionTypes = ActionTypes>(input: {
518518
} & Omit<{
519519
requestId: string;
520520
data?: unknown;
521-
error?: SerializedError | FetchBaseQueryError | undefined;
521+
error?: FetchBaseQueryError | SerializedError | undefined;
522522
endpointName: string;
523523
startedTimeStamp: number;
524524
fulfilledTimeStamp?: number;
525525
}, "error"> & Required<Pick<{
526526
requestId: string;
527527
data?: unknown;
528-
error?: SerializedError | FetchBaseQueryError | undefined;
528+
error?: FetchBaseQueryError | SerializedError | undefined;
529529
endpointName: string;
530530
startedTimeStamp: number;
531531
fulfilledTimeStamp?: number;
@@ -1187,8 +1187,8 @@ value: Record<string, unknown>;
11871187
}, string>;
11881188

11891189
// @public
1190-
export const nodeCollectorReducer: Reducer<(ValidatedTextCollector | ValidatedBooleanCollector | ValidatedPasswordCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | TextCollector | PasswordCollector | SingleSelectCollector | UnknownCollector | IdpCollector | FlowCollector | SubmitCollector | ReadOnlyCollector | RichTextCollector | QrCodeCollector | AgreementCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector"> | MultiSelectCollector)[]> & {
1191-
getInitialState: () => (ValidatedTextCollector | ValidatedBooleanCollector | ValidatedPasswordCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | ProtectCollector | FidoRegistrationCollector | FidoAuthenticationCollector | PollingCollector | TextCollector | PasswordCollector | SingleSelectCollector | UnknownCollector | IdpCollector | FlowCollector | SubmitCollector | ReadOnlyCollector | RichTextCollector | QrCodeCollector | AgreementCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector"> | MultiSelectCollector)[];
1190+
export const nodeCollectorReducer: Reducer<(TextCollector | SingleSelectCollector | PasswordCollector | ValidatedPasswordCollector | ValidatedBooleanCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | UnknownCollector | IdpCollector | FlowCollector | SubmitCollector | ReadOnlyCollector | RichTextCollector | QrCodeCollector | AgreementCollector | MultiSelectCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[]> & {
1191+
getInitialState: () => (TextCollector | SingleSelectCollector | PasswordCollector | ValidatedPasswordCollector | ValidatedBooleanCollector | ValidatedTextCollector | ProtectCollector | PollingCollector | FidoRegistrationCollector | FidoAuthenticationCollector | DeviceAuthenticationCollector | DeviceRegistrationCollector | PhoneNumberCollector | PhoneNumberExtensionCollector | UnknownCollector | IdpCollector | FlowCollector | SubmitCollector | ReadOnlyCollector | RichTextCollector | QrCodeCollector | AgreementCollector | MultiSelectCollector | ActionCollector<"ActionCollector"> | SingleValueCollector<"SingleValueCollector">)[];
11921192
};
11931193

11941194
// @public (undocumented)
@@ -1670,9 +1670,7 @@ export type SingleCheckboxField = {
16701670
key: string;
16711671
label: string;
16721672
required: boolean;
1673-
validation?: {
1674-
errorMessage: string;
1675-
};
1673+
errorMessage?: string;
16761674
};
16771675

16781676
// @public (undocumented)

0 commit comments

Comments
 (0)