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 @@ -10,8 +10,8 @@ import React from 'react';
import { render } from '@testing-library/react';
import CheckBoxGroup from '../../src/components/CheckBoxGroup';
import { createForm, Provider, renderComponent } from '../utils';
import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom/extend-expect"
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';

const field = {
name: 'checkbox',
Expand Down Expand Up @@ -135,7 +135,7 @@ describe('Checkbox Group', () => {
test('enum names should render correctly under a non-default locale', async () => {
const f = {
...field,
enumNames: ['case à cocher 1', 'case à cocher 2', 'case à cocher 3'],
enumNames: ['case à cocher 1', 'case à cocher 2', 'case à cocher 3']
};
const form = createForm(f);
const component = <CheckBoxGroup {...form.items[0].getState()} />;
Expand All @@ -146,4 +146,23 @@ describe('Checkbox Group', () => {
expect(getByText('case à cocher 2')).not.toBeNull();
expect(getByText('case à cocher 3')).not.toBeNull();
});

test('should render as anchor tag with checkbox hidden for toggleablelink', async () => {

const field = {
id: 'toggleablelink-abcd',
name: 'link1234',
visible: true,
fieldType: 'checkbox-group',
':type': 'core/fd/components/form/toggleablelink/v1/toggleablelink',
enum: ['https://www.adobe.com'],
enumNames: ['label for the link']
};
const { renderResponse } = await helper(field);
const anchor = renderResponse.container.querySelector('a.cmp-adaptiveform-checkboxgroup__links');
const checkboxInput = renderResponse.container.querySelector(`[name=${field.name}]`);

expect(anchor).toHaveAttribute('href', field.enum[0]);
expect(checkboxInput).toHaveStyle('display: none');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong license header. This file has the closed-source Adobe beta license. All other test files in this repo use Apache 2.0. Replace with the standard Apache 2.0 header matching the rest of the codebase (e.g. see CheckBoxGroup.test.tsx).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This text is same as other files, could be false positive as i updated year to 2026

* Copyright 2026 Adobe, Inc.
*
* Your access and use of this software is governed by the Adobe Customer Feedback Program Terms and Conditions or other Beta License Agreement signed by your employer and Adobe, Inc.. This software is NOT open source and may not be used without one of the foregoing licenses. Even with a foregoing license, your access and use of this file is limited to the earlier of (a) 180 days, (b) general availability of the product(s) which utilize this software (i.e. AEM Forms), (c) January 1, 2023, (d) Adobe providing notice to you that you may no longer use the software or that your beta trial has otherwise ended.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL ADOBE NOR ITS THIRD PARTY PROVIDERS AND PARTNERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import { act } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom/extend-expect';
import TermsAndConditions from '../../src/components/TermsAndConditions';
import mappings from '../../src/utils/mappings';
import { renderComponent } from '../utils';

// jsdom does not implement IntersectionObserver; capture the callback so
// tests can simulate the intersetion element entering the viewport.
let intersectionCallback: (entries: Array<{ isIntersecting: boolean }>) => void = () => undefined;
const mockUnobserve = jest.fn();

beforeEach(() => {
mockUnobserve.mockClear();
(global as any).IntersectionObserver = jest.fn((cb: any) => {
intersectionCallback = cb;
return {
observe: jest.fn(),
unobserve: mockUnobserve,
disconnect: jest.fn()
};
});
});

const textItem = {
id: 'text-def',
fieldType: 'plain-text',
name: 'consenttext',
value: 'Text related to the terms and conditions come here'
};

const checkboxItem = {
id: 'checkbox-abc',
fieldType: 'checkbox',
name: 'approvalcheckbox',
type: 'string',
required: true,
enabled: false,
label: { value: 'I agree to the terms & conditions' },
enum: ['true']
};

const textOnlyField = {
id: 'termsandconditions-abc',
fieldType: 'panel',
name: 'termsandconditions1234',
visible: true,
enabled: true,
type: 'object',
label: { value: 'Terms And Conditions', visible: true },
properties: {
'fd:tnc': true
},
items: [textItem, checkboxItem]
};

const modalModeField = {
...textOnlyField,
id: 'termsandconditions-modal',
properties: {
'fd:tnc': true,
'fd:showAsPopup': true
}
};

const linkItem = {
id: 'toggleablelink-1234',
fieldType: 'checkbox-group',
name: 'link1234',
visible: true,
enum: ['https://www.adobe.com'],
enumNames: ['label for the link'],
':type': 'core/fd/components/form/toggleablelink/v1/toggleablelink',
events: {
'custom:setProperty': [
'$event.payload'
],
'change': [
"if(length($field.$value) == length($field.$enum), dispatchEvent($parent.approvalcheckbox, 'custom:setProperty', {enabled : true()}), {})"
]
}
};

const linkModeField = {
id: 'termsandconditions-abc',
fieldType: 'panel',
name: 'termsandconditions1234',
visible: true,
enabled: true,
type: 'object',
label: { value: 'Terms And Conditions', visible: true },
properties: {
'fd:tnc': true
},
items: [
{
id: 'checkbox-abc',
fieldType: 'checkbox',
name: 'approvalcheckbox',
type: 'string',
required: true,
enabled: false,
enforceEnum: true,
label: { value: 'I agree to the terms & conditions' },
enum: ['true']
},
linkItem
]
};


describe('Terms And Conditions', () => {

test('should render plain text and approval checkbox with no link', () => {
const helper = renderComponent(TermsAndConditions);
const { renderResponse } = helper(textOnlyField, null, mappings);

expect(renderResponse.getByText(textItem.value)).toBeVisible();
expect(renderResponse.getByText(checkboxItem.label.value)).toBeVisible();

const checkboxWidget = renderResponse.container.getElementsByClassName('cmp-adaptiveform-checkbox__widget')[0];

expect(checkboxWidget).toBeVisible();
expect(checkboxWidget).toHaveAttribute('name', checkboxItem.name);
expect(renderResponse.container.getElementsByClassName('cmp-adaptiveform-termsandcondition__link').length).toEqual(0);
});


test('link mode renders the checkbox-group and approval checkbox, with no text ', () => {
const helper = renderComponent(TermsAndConditions);
const { renderResponse } = helper(linkModeField, null, mappings);

expect(renderResponse.container.getElementsByClassName('cmp-adaptiveform-termsandcondition__text').length).toEqual(0);
expect(renderResponse.container.getElementsByClassName('cmp-adaptiveform-termsandcondition__link').length).toEqual(1);
const linkTag = renderResponse.container.getElementsByClassName('cmp-adaptiveform-checkboxgroup__links')[0];
expect(linkTag).toHaveAttribute('href', linkItem.enum[0]);
expect(linkTag).toHaveAttribute('title', linkItem.enumNames[0]);

const checkboxWidget = renderResponse.container.getElementsByClassName('cmp-adaptiveform-checkbox__widget')[0];
expect(checkboxWidget).toHaveAttribute('name', 'approvalcheckbox');
});

test('clicking the link enables the approval checkbox', () => {
const helper = renderComponent(TermsAndConditions);
const { renderResponse } = helper(linkModeField, null, mappings);
const checkboxWidget = renderResponse.container.getElementsByClassName('cmp-adaptiveform-checkbox__widget')[0];
expect(checkboxWidget).toHaveAttribute('name', 'approvalcheckbox');
expect(checkboxWidget).toBeDisabled();
const linkTag = renderResponse.getByText(linkItem.enumNames[0]);
userEvent.click(linkTag);
expect(checkboxWidget).toBeEnabled();
});

test('modal can be toggled using approval checkbox', () => {
const helper = renderComponent(TermsAndConditions);
const { renderResponse } = helper(modalModeField, null, mappings);

const contentContainer = renderResponse.container.getElementsByClassName('cmp-adaptiveform-termsandcondition__content-container--modal')[0];
expect(contentContainer).toHaveStyle('display: none');

const checkboxWrapper = renderResponse.container.getElementsByClassName('cmp-adaptiveform-termsandcondition__approvalcheckbox')[0];
userEvent.click(checkboxWrapper);
expect(contentContainer).toHaveStyle('display: block');

act(() => {
intersectionCallback([{ isIntersecting: true }]);
});

const closeButton = renderResponse.getByLabelText('Close terms and conditions document');
userEvent.click(closeButton);
expect(contentContainer).toHaveStyle('display: none');
const checkboxWidget = renderResponse.container.getElementsByClassName('cmp-adaptiveform-checkbox__widget')[0];
expect(checkboxWidget).toHaveAttribute('name', 'approvalcheckbox');
expect(checkboxWidget).toBeEnabled();
});


test('checkbox becomes enabled once the text-intersect div is scrolled into view', () => {
const helper = renderComponent(TermsAndConditions);
const largeTextItem = {...textItem, value: 'Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here.Text related to the terms and conditions come here'}
const field = {...textOnlyField, items: [checkboxItem, largeTextItem]};
const { renderResponse } = helper(field, null, mappings);

const checkboxElement = renderResponse.container.querySelector(`#${checkboxItem.id}-widget`);
expect(checkboxElement).toBeDisabled();

act(() => {
intersectionCallback([{ isIntersecting: true }]);
});

expect(checkboxElement).toBeEnabled();
expect(mockUnobserve).toHaveBeenCalled();
});

test('checkbox stays disabled while intersection has not fired', () => {
const helper = renderComponent(TermsAndConditions);
const largeTextItem = {...textItem, value: 'Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here. Text related to the terms and conditions come here.Text related to the terms and conditions come here'}
const field = {...textOnlyField, items: [checkboxItem, largeTextItem]};
const { renderResponse } = helper(field, null, mappings);

const checkboxElement = renderResponse.container.querySelector(`#${checkboxItem.id}-widget`);
expect(checkboxElement).toBeDisabled();

act(() => {
intersectionCallback([{ isIntersecting: false }]);
});

expect(checkboxElement).toBeDisabled();
expect(mockUnobserve).not.toHaveBeenCalled();
});
});
4 changes: 2 additions & 2 deletions packages/react-vanilla-components/__tests__/utils/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,15 @@ export const Provider =
};

export const renderComponent = function <T>(Component: JSXElementConstructor<any>) {
const test = (field: any, operation?: any) => {
const test = (field: any, operation?: any, mappings = {}) => {
const form = createForm(field);
if (operation) {
operation(form, form.items[0]);
}
const e = form.items[0].getState();
//@ts-ignore
let component = <Component {...e} />;
const wrapper = Provider(form);
const wrapper = Provider(form,mappings);
const renderResponse = render(component, { wrapper });
return {
renderResponse,
Expand Down
7 changes: 4 additions & 3 deletions packages/react-vanilla-components/src/components/CheckBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ const CheckBox = (props: PROPS) => {
const unSelectedValue = (enums?.length || 0) < 2 ? null : enums?.[1];

const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
if(readOnly) { return; }
const val = e.target.checked ? selectedValue : unSelectedValue;
props.dispatchChange(val);
}, [props.dispatchChange]);
}, [props.dispatchChange, readOnly]);

return (
<div
Expand Down Expand Up @@ -56,12 +57,12 @@ const CheckBox = (props: PROPS) => {
className={'cmp-adaptiveform-checkbox__widget'}
title={props.tooltipText || ''}
onChange={handleChange}
value={value}
value={value || ''}
checked={selectedValue === value ? true : false}
name={name}
required={required}
readOnly={readOnly}
disabled={!enabled}
disabled={!enabled || readOnly}
aria-checked={selectedValue === value ? 'true' : 'false'} aria-invalid={!valid}
aria-describedby={syncAriaDescribedBy(id, props.tooltip, props.description, props.errorMessage)}
/>
Expand Down
30 changes: 24 additions & 6 deletions packages/react-vanilla-components/src/components/CheckBoxGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const CheckBoxGroup = (props: PROPS) => {
const { id, label, required, enumNames, enum: enums, value, name, readOnly, visible, enabled, appliedCssClassNames, valid } = props;
const options = enumNames && enumNames.length ? enumNames : enums || [];
const orientation = props.layout?.orientation.toUpperCase();
const isToggleableLink = props[':type'].includes('toggleablelink');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (crash risk): props[':type'] is optional — .includes() will throw if it's undefined (e.g. plain checkbox-group without :type set).

const isToggleableLink = props[':type']?.includes('toggleablelink') ?? false;


const getValue = useCallback((value: any) => {
if (value) {
Expand All @@ -40,9 +41,8 @@ const CheckBoxGroup = (props: PROPS) => {

const newVal = getValue(value);

const changeHandler = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
const val = event.target.value;
const checked = event.target.checked;
const toggleValue = useCallback((val: string, checked: boolean) => {
if(readOnly) { return; }
let valAdded = [...newVal];
if (checked) {
valAdded.push(val);
Expand All @@ -51,7 +51,16 @@ const CheckBoxGroup = (props: PROPS) => {
valAdded = valAdded.filter((item) => item != val);
}
props.dispatchChange(valAdded);
}, [props.dispatchChange, newVal]);
}, [props.dispatchChange, newVal, readOnly]);

const changeHandler = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
toggleValue(event.target.value, event.target.checked);
}, [toggleValue]);

const linkClickHandler = useCallback((index: number) => {
let valAdded = [...newVal];
toggleValue(enums![index],!valAdded.includes(enums![index]));
},[toggleValue, enums]);

return (
<div
Expand Down Expand Up @@ -88,10 +97,19 @@ const CheckBoxGroup = (props: PROPS) => {
value={enums![index]}
onChange={changeHandler}
readOnly={readOnly}
checked={value?.includes(enums?.[index])}
checked={!!value?.includes(enums?.[index])}
aria-invalid={!valid}
disabled={!enabled || readOnly}
style={isToggleableLink ? {display: 'none'}: undefined}
/>
{richTextString(item)}
{isToggleableLink ?
<a className='cmp-adaptiveform-checkboxgroup__links'

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Security: target='_blank' without rel opens reverse tabnapping. Add rel="noopener noreferrer".

<a
  className='cmp-adaptiveform-checkboxgroup__links'
  target='_blank'
  rel='noopener noreferrer'
  href={enums![index]}
  title={item || ''}
  onClick={() => linkClickHandler(index)}
>

target='_blank'
href={enums![index]}
title={item || ''}
onClick={()=>linkClickHandler(index)}
>{richTextString(item)}</a> : richTextString(item)
}
</label>
</div>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ const RadioButtonGroup = (props: PROPS) => {
const orientation = props.layout?.orientation.toUpperCase();

const changeHandler = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
if(readOnly) { return; }
const val = event.target.value;
props.dispatchChange(val);
}, [props.dispatchChange]);
}, [props.dispatchChange, readOnly]);

return (
<div
Expand Down Expand Up @@ -72,6 +73,7 @@ const RadioButtonGroup = (props: PROPS) => {
aria-checked={value === enums![index] ? 'true' : 'false'}
checked={value?.length ? value?.includes(enums?.[index]) : null}
aria-invalid={!valid}
disabled={!enabled || readOnly}
/>
{richTextString(item)}
</label>
Expand Down
Loading
Loading