Skip to content

Commit 3b420a6

Browse files
authored
Merge pull request #162 from ForgeRock/form-fields-tests
chore: fix-form-fields-tests
2 parents 6c5ad9d + 64c4723 commit 3b420a6

17 files changed

Lines changed: 268 additions & 92 deletions
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import type { MultiSelectCollector, Updater } from '@forgerock/davinci-client/types';
2+
3+
/**
4+
* Creates a group of checkboxes with single-select behavior (like radio buttons)
5+
* based on the provided data and attaches it to the form
6+
* @param {HTMLFormElement} formEl - The form element to attach the checkboxes to
7+
* @param {SingleSelectCollector} collector - Contains the options and configuration
8+
* @param {Updater} updater - Function to call when selection changes
9+
*/
10+
export default function multiValueComponent(
11+
formEl: HTMLFormElement,
12+
collector: MultiSelectCollector,
13+
updater: Updater,
14+
) {
15+
// Create a container for the checkboxes
16+
const containerDiv = document.createElement('div');
17+
containerDiv.className = 'checkbox-container';
18+
19+
// Create a heading/label for the checkbox group
20+
const groupLabel = document.createElement('div');
21+
groupLabel.textContent = collector.output.label || 'Select an option';
22+
groupLabel.className = 'checkbox-group-label';
23+
containerDiv.appendChild(groupLabel);
24+
25+
const values: string[] = [];
26+
let index = 0;
27+
28+
// Create checkboxes for each option
29+
for (const option of collector.output.options) {
30+
const wrapper = document.createElement('div');
31+
wrapper.className = 'checkbox-wrapper';
32+
33+
index += 1;
34+
35+
const checkbox = document.createElement('input');
36+
checkbox.type = 'checkbox';
37+
checkbox.id = `${collector.output.key}-${index}`;
38+
checkbox.name = collector.output.key || 'checkbox-field';
39+
checkbox.value = option.value;
40+
41+
const label = document.createElement('label');
42+
label.htmlFor = checkbox.id;
43+
label.textContent = option.label;
44+
45+
// Add event listener to handle single-select behavior
46+
checkbox.addEventListener('change', (event) => {
47+
const target = event.target as HTMLInputElement;
48+
49+
// If this checkbox is being checked
50+
if (target.checked) {
51+
values.push(target.value);
52+
} else {
53+
// If this checkbox is being unchecked
54+
const index = values.indexOf(target.value);
55+
if (index > -1) {
56+
values.splice(index, 1);
57+
}
58+
}
59+
console.log(values);
60+
updater(values);
61+
});
62+
63+
wrapper.appendChild(checkbox);
64+
wrapper.appendChild(label);
65+
containerDiv.appendChild(wrapper);
66+
}
67+
68+
// Append the container to the form
69+
formEl.appendChild(containerDiv);
70+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { SingleSelectCollector, Updater } from '@forgerock/davinci-client/types';
2+
3+
/**
4+
* Creates a dropdown component based on the provided data and attaches it to the form
5+
* @param {HTMLFormElement} formEl - The form element to attach the dropdown to
6+
* @param {SingleSelectCollector} collector - Contains the dropdown options and configuration
7+
* @param {Updater} updater - Function to call when selection changes
8+
*/
9+
export default function singleValueComponent(
10+
formEl: HTMLFormElement,
11+
collector: SingleSelectCollector,
12+
updater: Updater,
13+
) {
14+
// Create the label element
15+
const labelEl = document.createElement('label');
16+
labelEl.textContent = collector.output.label || 'Select an option';
17+
labelEl.className = 'dropdown-label';
18+
19+
// Create the select element
20+
const selectEl = document.createElement('select');
21+
selectEl.name = collector.output.key || 'dropdown-field';
22+
selectEl.id = collector.output.key || 'dropdown-field';
23+
24+
// Add all options from the data
25+
for (const option of collector.output.options) {
26+
const optionEl = document.createElement('option');
27+
optionEl.value = option.value;
28+
optionEl.textContent = option.label;
29+
selectEl.appendChild(optionEl);
30+
}
31+
32+
// Add change event listener
33+
selectEl.addEventListener('change', (event) => {
34+
// Properly type the event target
35+
const target = event.target as HTMLSelectElement;
36+
const selectedValue = target.value;
37+
updater(selectedValue);
38+
});
39+
40+
// Append elements to the form
41+
formEl.appendChild(labelEl);
42+
formEl.appendChild(selectEl);
43+
}

e2e/davinci-app/components/text.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import type {
22
TextCollector,
33
ValidatedTextCollector,
44
Updater,
5+
Validator,
56
} from '@forgerock/davinci-client/types';
67
import { dotToCamelCase } from '../helper.js';
78

8-
export default function usernameComponent(
9+
export default function textComponent(
910
formEl: HTMLFormElement,
1011
collector: TextCollector | ValidatedTextCollector,
1112
updater: Updater,
13+
validator: Validator,
1214
) {
1315
const collectorKey = dotToCamelCase(collector.output.key);
1416
const label = document.createElement('label');
@@ -23,10 +25,32 @@ export default function usernameComponent(
2325
formEl?.appendChild(label);
2426
formEl?.appendChild(input);
2527

26-
formEl?.querySelector(`#${collectorKey}`)?.addEventListener('input', (event) => {
27-
const error = updater((event.target as HTMLInputElement).value);
28-
if (error && 'error' in error) {
29-
console.error(error.error.message);
30-
}
31-
});
28+
if (collector.category === 'ValidatedSingleValueCollector') {
29+
formEl?.querySelector(`#${collectorKey}`)?.addEventListener('input', (event) => {
30+
const result = validator((event.target as HTMLInputElement).value);
31+
const errorEl = formEl?.querySelector(`.${collectorKey}-error`);
32+
33+
if (Array.isArray(result) && result.length && !errorEl) {
34+
const errorEl = document.createElement('div');
35+
errorEl.className = `${collectorKey}-error`;
36+
errorEl.innerText = result.join(', ');
37+
formEl?.querySelector(`#${collectorKey}`)?.after(errorEl);
38+
} else if (Array.isArray(result) && result.length) {
39+
return;
40+
} else {
41+
formEl.querySelector(`.${collectorKey}-error`)?.remove();
42+
const error = updater((event.target as HTMLInputElement).value);
43+
if (error && 'error' in error) {
44+
console.error(error.error.message);
45+
}
46+
}
47+
});
48+
} else {
49+
formEl?.querySelector(`#${collectorKey}`)?.addEventListener('input', (event) => {
50+
const error = updater((event.target as HTMLInputElement).value);
51+
if (error && 'error' in error) {
52+
console.error(error.error.message);
53+
}
54+
});
55+
}
3256
}

e2e/davinci-app/main.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
import './style.css';
2+
23
import { Config, FRUser, TokenManager } from '@forgerock/javascript-sdk';
34
import { davinci } from '@forgerock/davinci-client';
45

56
import type { DaVinciConfig, RequestMiddleware } from '@forgerock/davinci-client/types';
67

7-
import usernameComponent from './components/text.js';
8+
import textComponent from './components/text.js';
89
import passwordComponent from './components/password.js';
910
import submitButtonComponent from './components/submit-button.js';
1011
import protect from './components/protect.js';
1112
import flowLinkComponent from './components/flow-link.js';
1213
import socialLoginButtonComponent from './components/social-login-button.js';
1314
import { serverConfigs } from './server-configs.js';
15+
import singleValueComponent from './components/single-value.js';
16+
import multiValueComponent from './components/multi-value.js';
1417

1518
const qs = window.location.search;
1619
const searchParams = new URLSearchParams(qs);
@@ -155,10 +158,11 @@ const urlParams = new URLSearchParams(window.location.search);
155158
davinciClient.update(collector), // Returns an update function for this collector
156159
);
157160
} else if (collector.type === 'TextCollector') {
158-
usernameComponent(
161+
textComponent(
159162
formEl, // You can ignore this; it's just for rendering
160163
collector, // This is the plain object of the collector
161164
davinciClient.update(collector), // Returns an update function for this collector
165+
davinciClient.validate(collector), // Returns a validate function for this collector
162166
);
163167
} else if (collector.type === 'PasswordCollector') {
164168
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
@@ -187,6 +191,10 @@ const urlParams = new URLSearchParams(window.location.search);
187191
}),
188192
renderForm, // Ignore this; it's just for re-rendering the form
189193
);
194+
} else if (collector.type === 'SingleSelectCollector') {
195+
singleValueComponent(formEl, collector, davinciClient.update(collector));
196+
} else if (collector.type === 'MultiSelectCollector') {
197+
multiValueComponent(formEl, collector, davinciClient.update(collector));
190198
}
191199
});
192200

e2e/davinci-app/server-configs.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,13 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
4040
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
4141
},
4242
},
43+
'60de77d5-dd2c-41ef-8c40-f8bb2381a359': {
44+
clientId: '60de77d5-dd2c-41ef-8c40-f8bb2381a359',
45+
redirectUri: window.location.origin + '/',
46+
scope: 'openid profile email name revoke',
47+
serverConfig: {
48+
wellknown:
49+
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
50+
},
51+
},
4352
};

e2e/davinci-app/style.css

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,14 @@ button:focus-visible {
104104
text-decoration: underline;
105105
}
106106

107+
.checkbox-wrapper input,
108+
.checkbox-wrapper label {
109+
display: inline-block;
110+
width: auto;
111+
vertical-align: middle;
112+
margin-right: 0.5em;
113+
}
114+
107115
@media (prefers-color-scheme: light) {
108116
:root {
109117
color: #213547;

e2e/davinci-suites/playwright.config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const config: PlaywrightTestConfig = {
1111
timeout: 30000,
1212
use: {
1313
baseURL,
14+
headless: true,
1415
ignoreHTTPSErrors: true,
1516
geolocation: { latitude: 24.9884, longitude: -87.3459 },
1617
bypassCSP: true,
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
import { asyncEvents } from './utils/async-events.js';
4+
5+
test('Should render form fields', async ({ page }) => {
6+
const { navigate } = asyncEvents(page);
7+
await navigate('/?clientId=60de77d5-dd2c-41ef-8c40-f8bb2381a359');
8+
9+
await expect(page.getByText('Select Test Form')).toBeVisible();
10+
await page.getByRole('button', { name: 'Form Fields' }).click();
11+
12+
await expect(page.getByText('Form Fields Test')).toBeVisible();
13+
await page.getByRole('textbox', { name: 'Text Input Label' }).fill('The input');
14+
15+
await page.locator('#checkbox-field-key-1').check();
16+
await page.locator('#checkbox-field-key-2').check();
17+
18+
await page.locator('#dropdown-field-key').selectOption('dropdown-option1-value');
19+
await page.locator('#dropdown-field-key').selectOption('dropdown-option2-value');
20+
21+
await page.locator('#radio-group-key').selectOption('option2 label');
22+
23+
await page.locator('#combobox-field-key-1').check();
24+
await page.locator('#combobox-field-key-2').check();
25+
await page.locator('#combobox-field-key-3').check();
26+
await page.locator('#combobox-field-key-2').uncheck();
27+
28+
await expect(page.getByRole('button', { name: 'Flow Button' })).toBeVisible();
29+
await expect(page.getByRole('button', { name: 'Flow Link' })).toBeVisible();
30+
31+
const requestPromise = page.waitForRequest(
32+
(request) => request.url().includes('customForm') && request.method() === 'POST',
33+
);
34+
35+
await page.getByRole('button', { name: 'Submit' }).click();
36+
const request = await requestPromise;
37+
38+
const parsedData = JSON.parse(request.postData());
39+
const data = parsedData.parameters.data;
40+
expect(data.actionKey).toBe('submit');
41+
expect(data.formData).toStrictEqual({
42+
'text-input-key': 'The input',
43+
'checkbox-field-key': ['option1 value', 'option2 value'],
44+
'dropdown-field-key': 'dropdown-option2-value',
45+
'radio-group-key': 'option2 value',
46+
'combobox-field-key': ['option1 value', 'option3 value'],
47+
});
48+
});
49+
50+
test('should render form validation fields', async ({ page }) => {
51+
await page.goto('http://localhost:5829/?clientId=60de77d5-dd2c-41ef-8c40-f8bb2381a359');
52+
53+
await expect(page.getByText('Select Test Form')).toBeVisible();
54+
55+
await page.getByRole('button', { name: 'Form Validation' }).click();
56+
57+
await expect(page.getByText('Form Fields Validation')).toBeVisible();
58+
59+
await page.getByRole('textbox', { name: 'Username' }).fill('@#$');
60+
await expect(page.getByText('Must be alphanumeric')).toBeVisible();
61+
62+
await page.getByRole('textbox', { name: 'Email Address' }).fill('abc');
63+
await expect(page.getByText('Not a valid email')).toBeVisible();
64+
65+
await page.getByRole('textbox', { name: 'Email Address' }).fill('abc@email.com');
66+
await expect(page.getByText('Not a valid email')).not.toBeVisible();
67+
});

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

Lines changed: 0 additions & 67 deletions
This file was deleted.

0 commit comments

Comments
 (0)