Skip to content
Merged
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
70 changes: 70 additions & 0 deletions e2e/davinci-app/components/multi-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { MultiSelectCollector, Updater } from '@forgerock/davinci-client/types';

/**
* Creates a group of checkboxes with single-select behavior (like radio buttons)
* based on the provided data and attaches it to the form
* @param {HTMLFormElement} formEl - The form element to attach the checkboxes to
* @param {SingleSelectCollector} collector - Contains the options and configuration
* @param {Updater} updater - Function to call when selection changes
*/
export default function multiValueComponent(
formEl: HTMLFormElement,
collector: MultiSelectCollector,
updater: Updater,
) {
// Create a container for the checkboxes
const containerDiv = document.createElement('div');
containerDiv.className = 'checkbox-container';

// Create a heading/label for the checkbox group
const groupLabel = document.createElement('div');
groupLabel.textContent = collector.output.label || 'Select an option';
groupLabel.className = 'checkbox-group-label';
containerDiv.appendChild(groupLabel);

const values: string[] = [];
let index = 0;

// Create checkboxes for each option
for (const option of collector.output.options) {
const wrapper = document.createElement('div');
wrapper.className = 'checkbox-wrapper';

index += 1;

const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `${collector.output.key}-${index}`;
checkbox.name = collector.output.key || 'checkbox-field';
checkbox.value = option.value;

const label = document.createElement('label');
label.htmlFor = checkbox.id;
label.textContent = option.label;

// Add event listener to handle single-select behavior
checkbox.addEventListener('change', (event) => {
const target = event.target as HTMLInputElement;

// If this checkbox is being checked
if (target.checked) {
values.push(target.value);
} else {
// If this checkbox is being unchecked
const index = values.indexOf(target.value);
if (index > -1) {
values.splice(index, 1);
}
}
console.log(values);
updater(values);
});

wrapper.appendChild(checkbox);
wrapper.appendChild(label);
containerDiv.appendChild(wrapper);
}

// Append the container to the form
formEl.appendChild(containerDiv);
}
43 changes: 43 additions & 0 deletions e2e/davinci-app/components/single-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { SingleSelectCollector, Updater } from '@forgerock/davinci-client/types';

/**
* Creates a dropdown component based on the provided data and attaches it to the form
* @param {HTMLFormElement} formEl - The form element to attach the dropdown to
* @param {SingleSelectCollector} collector - Contains the dropdown options and configuration
* @param {Updater} updater - Function to call when selection changes
*/
export default function singleValueComponent(
formEl: HTMLFormElement,
collector: SingleSelectCollector,
updater: Updater,
) {
// Create the label element
const labelEl = document.createElement('label');
labelEl.textContent = collector.output.label || 'Select an option';
labelEl.className = 'dropdown-label';

// Create the select element
const selectEl = document.createElement('select');
selectEl.name = collector.output.key || 'dropdown-field';
selectEl.id = collector.output.key || 'dropdown-field';

// Add all options from the data
for (const option of collector.output.options) {
const optionEl = document.createElement('option');
optionEl.value = option.value;
optionEl.textContent = option.label;
selectEl.appendChild(optionEl);
}

// Add change event listener
selectEl.addEventListener('change', (event) => {
// Properly type the event target
const target = event.target as HTMLSelectElement;
const selectedValue = target.value;
updater(selectedValue);
});

// Append elements to the form
formEl.appendChild(labelEl);
formEl.appendChild(selectEl);
}
38 changes: 31 additions & 7 deletions e2e/davinci-app/components/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import type {
TextCollector,
ValidatedTextCollector,
Updater,
Validator,
} from '@forgerock/davinci-client/types';
import { dotToCamelCase } from '../helper.js';

export default function usernameComponent(
export default function textComponent(
formEl: HTMLFormElement,
collector: TextCollector | ValidatedTextCollector,
updater: Updater,
validator: Validator,
) {
const collectorKey = dotToCamelCase(collector.output.key);
const label = document.createElement('label');
Expand All @@ -23,10 +25,32 @@ export default function usernameComponent(
formEl?.appendChild(label);
formEl?.appendChild(input);

formEl?.querySelector(`#${collectorKey}`)?.addEventListener('input', (event) => {
const error = updater((event.target as HTMLInputElement).value);
if (error && 'error' in error) {
console.error(error.error.message);
}
});
if (collector.category === 'ValidatedSingleValueCollector') {
formEl?.querySelector(`#${collectorKey}`)?.addEventListener('input', (event) => {
const result = validator((event.target as HTMLInputElement).value);
const errorEl = formEl?.querySelector(`.${collectorKey}-error`);

if (Array.isArray(result) && result.length && !errorEl) {
const errorEl = document.createElement('div');
errorEl.className = `${collectorKey}-error`;
errorEl.innerText = result.join(', ');
formEl?.querySelector(`#${collectorKey}`)?.after(errorEl);
} else if (Array.isArray(result) && result.length) {
return;
} else {
formEl.querySelector(`.${collectorKey}-error`)?.remove();
const error = updater((event.target as HTMLInputElement).value);
if (error && 'error' in error) {
console.error(error.error.message);
}
}
});
} else {
formEl?.querySelector(`#${collectorKey}`)?.addEventListener('input', (event) => {
const error = updater((event.target as HTMLInputElement).value);
if (error && 'error' in error) {
console.error(error.error.message);
}
});
}
}
12 changes: 10 additions & 2 deletions e2e/davinci-app/main.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
import './style.css';

import { Config, FRUser, TokenManager } from '@forgerock/javascript-sdk';
import { davinci } from '@forgerock/davinci-client';

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

import usernameComponent from './components/text.js';
import textComponent from './components/text.js';
import passwordComponent from './components/password.js';
import submitButtonComponent from './components/submit-button.js';
import protect from './components/protect.js';
import flowLinkComponent from './components/flow-link.js';
import socialLoginButtonComponent from './components/social-login-button.js';
import { serverConfigs } from './server-configs.js';
import singleValueComponent from './components/single-value.js';
import multiValueComponent from './components/multi-value.js';

const qs = window.location.search;
const searchParams = new URLSearchParams(qs);
Expand Down Expand Up @@ -155,10 +158,11 @@ const urlParams = new URLSearchParams(window.location.search);
davinciClient.update(collector), // Returns an update function for this collector
);
} else if (collector.type === 'TextCollector') {
usernameComponent(
textComponent(
formEl, // You can ignore this; it's just for rendering
collector, // This is the plain object of the collector
davinciClient.update(collector), // Returns an update function for this collector
davinciClient.validate(collector), // Returns a validate function for this collector
);
} else if (collector.type === 'PasswordCollector') {
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
Expand Down Expand Up @@ -187,6 +191,10 @@ const urlParams = new URLSearchParams(window.location.search);
}),
renderForm, // Ignore this; it's just for re-rendering the form
);
} else if (collector.type === 'SingleSelectCollector') {
singleValueComponent(formEl, collector, davinciClient.update(collector));
} else if (collector.type === 'MultiSelectCollector') {
multiValueComponent(formEl, collector, davinciClient.update(collector));
}
});

Expand Down
9 changes: 9 additions & 0 deletions e2e/davinci-app/server-configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,13 @@ export const serverConfigs: Record<string, DaVinciConfig> = {
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
},
},
'60de77d5-dd2c-41ef-8c40-f8bb2381a359': {
clientId: '60de77d5-dd2c-41ef-8c40-f8bb2381a359',
redirectUri: window.location.origin + '/',
scope: 'openid profile email name revoke',
serverConfig: {
wellknown:
'https://auth.pingone.ca/02fb4743-189a-4bc7-9d6c-a919edfe6447/as/.well-known/openid-configuration',
},
},
};
8 changes: 8 additions & 0 deletions e2e/davinci-app/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ button:focus-visible {
text-decoration: underline;
}

.checkbox-wrapper input,
.checkbox-wrapper label {
display: inline-block;
width: auto;
vertical-align: middle;
margin-right: 0.5em;
}

@media (prefers-color-scheme: light) {
:root {
color: #213547;
Expand Down
1 change: 1 addition & 0 deletions e2e/davinci-suites/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const config: PlaywrightTestConfig = {
timeout: 30000,
use: {
baseURL,
headless: true,
ignoreHTTPSErrors: true,
geolocation: { latitude: 24.9884, longitude: -87.3459 },
bypassCSP: true,
Expand Down
67 changes: 67 additions & 0 deletions e2e/davinci-suites/src/form-fields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { expect, test } from '@playwright/test';

import { asyncEvents } from './utils/async-events.js';

test('Should render form fields', async ({ page }) => {
const { navigate } = asyncEvents(page);
await navigate('/?clientId=60de77d5-dd2c-41ef-8c40-f8bb2381a359');

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

await expect(page.getByText('Form Fields Test')).toBeVisible();
await page.getByRole('textbox', { name: 'Text Input Label' }).fill('The input');

await page.locator('#checkbox-field-key-1').check();
await page.locator('#checkbox-field-key-2').check();

await page.locator('#dropdown-field-key').selectOption('dropdown-option1-value');
await page.locator('#dropdown-field-key').selectOption('dropdown-option2-value');

await page.locator('#radio-group-key').selectOption('option2 label');

await page.locator('#combobox-field-key-1').check();
await page.locator('#combobox-field-key-2').check();
await page.locator('#combobox-field-key-3').check();
await page.locator('#combobox-field-key-2').uncheck();

await expect(page.getByRole('button', { name: 'Flow Button' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Flow Link' })).toBeVisible();

const requestPromise = page.waitForRequest(
(request) => request.url().includes('customForm') && request.method() === 'POST',
);

await page.getByRole('button', { name: 'Submit' }).click();
const request = await requestPromise;

const parsedData = JSON.parse(request.postData());
const data = parsedData.parameters.data;
expect(data.actionKey).toBe('submit');
expect(data.formData).toStrictEqual({
'text-input-key': 'The input',
'checkbox-field-key': ['option1 value', 'option2 value'],
'dropdown-field-key': 'dropdown-option2-value',
'radio-group-key': 'option2 value',
'combobox-field-key': ['option1 value', 'option3 value'],
});
});

test('should render form validation fields', async ({ page }) => {
await page.goto('http://localhost:5829/?clientId=60de77d5-dd2c-41ef-8c40-f8bb2381a359');

await expect(page.getByText('Select Test Form')).toBeVisible();

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

await expect(page.getByText('Form Fields Validation')).toBeVisible();

await page.getByRole('textbox', { name: 'Username' }).fill('@#$');
await expect(page.getByText('Must be alphanumeric')).toBeVisible();

await page.getByRole('textbox', { name: 'Email Address' }).fill('abc');
await expect(page.getByText('Not a valid email')).toBeVisible();

await page.getByRole('textbox', { name: 'Email Address' }).fill('abc@email.com');
await expect(page.getByText('Not a valid email')).not.toBeVisible();
});
67 changes: 0 additions & 67 deletions e2e/davinci-suites/src/form-fields.test_off.ts

This file was deleted.

Loading
Loading