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
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ import DatePickerWrapper from '../../../../lib/components/test-utils/selectors/d
import DropdownWrapper from '../../../../lib/components/test-utils/selectors/internal/dropdown';

import styles from '../../../../lib/components/date-picker/styles.selectors.js';
import inputStyles from '../../../../lib/components/input/styles.selectors.js';

export default class DatePickerPage extends BasePageObject {
public datePickerWrapper: DatePickerWrapper;
public datePickerInput: string;
// The field box that paints the border; the native input sits inside it and is border-width smaller on each side.
public datePickerField: string;
public sourceInput: string;
private readonly expandToViewport: boolean;

Expand All @@ -22,6 +25,7 @@ export default class DatePickerPage extends BasePageObject {
super(browser);
this.datePickerWrapper = new DatePickerWrapper(datepickerId);
this.datePickerInput = this.datePickerWrapper.findNativeInput().toSelector();
this.datePickerField = this.datePickerWrapper.find(`.${inputStyles['input-field']}`).toSelector();
this.sourceInput = createWrapper().findInput().findNativeInput().toSelector();
this.expandToViewport = expandToViewport;
}
Expand All @@ -42,7 +46,7 @@ export default class DatePickerPage extends BasePageObject {
}

async getInputBoundingBox() {
const boundingBox = await this.getBoundingBox(this.datePickerInput);
const boundingBox = await this.getBoundingBox(this.datePickerField);
return boundingBox;
}

Expand Down
5 changes: 4 additions & 1 deletion src/dropdown/__integ__/alignment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import useBrowser from '@cloudscape-design/browser-test-tools/use-browser';

import createWrapper from '../../../lib/components/test-utils/selectors';

import inputStyles from '../../../lib/components/input/styles.selectors.js';

describe('Dropdown and trigger element alignment', () => {
describe.each(['expandable', 'expandable-iframe'])('%s', pageName => {
describe.each([true, false])('expandToViewport=%s', expandToViewport => {
Expand All @@ -26,7 +28,8 @@ describe('Dropdown and trigger element alignment', () => {
const wrapper = createWrapper().findAutosuggest(`#${alignment}`);
await page.click(wrapper.findNativeInput().toSelector());
const dropdownBox = await page.getBoundingBox(wrapper.findDropdown({ expandToViewport }).toSelector());
const triggerBox = await page.getBoundingBox(wrapper.findNativeInput().toSelector());
// The dropdown aligns with the field box that paints the border, not with the borderless native input inside it.
const triggerBox = await page.getBoundingBox(wrapper.find(`.${inputStyles['input-field']}`).toSelector());
if (alignment.endsWith('-left')) {
expect(dropdownBox.left).toEqual(triggerBox.left);
} else {
Expand Down
206 changes: 115 additions & 91 deletions src/input/__tests__/adornments.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { render } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';

import '../../__a11y__/to-validate-a11y';
import Input, { InputProps } from '../../../lib/components/input';
import InternalInput from '../../../lib/components/input/internal';
import customCssProps from '../../../lib/components/internal/generated/custom-css-properties';
import createWrapper from '../../../lib/components/test-utils/dom';
import createWrapper, { InputWrapper } from '../../../lib/components/test-utils/dom';

import styles from '../../../lib/components/input/styles.css.js';

Expand All @@ -17,28 +17,130 @@ function renderInput(props: Partial<InputProps> = {}) {
return { wrapper, rerender };
}

describe('prefix and suffix adornments', () => {
test('does not render adornment container, prefix, or suffix by default', () => {
const findField = (wrapper: ReturnType<typeof renderInput>['wrapper']) =>
wrapper.findByClassName(styles['input-field'])!.getElement();

describe('field box', () => {
test('wraps the native input and renders no prefix, suffix or divider by default', () => {
const { wrapper } = renderInput();
expect(wrapper.findByClassName(styles['input-adorned-container'])).toBeNull();
const field = findField(wrapper);
expect(field).toContainElement(wrapper.findNativeInput().getElement());
expect(field.children).toHaveLength(1);
expect(wrapper.findPrefix()).toBeNull();
expect(wrapper.findSuffix()).toBeNull();
expect(wrapper.findNativeInput().getElement()).not.toHaveClass(styles['input-adorned']);
expect(wrapper.findAllByClassName(styles['input-adornment-divider'])).toHaveLength(0);
});

test('renders the clear button outside the field box so its focus does not show the field focus ring', () => {
const { wrapper } = renderInput({ type: 'search', value: 'query', clearAriaLabel: 'Clear' });
const field = findField(wrapper);
const clearButton = wrapper.findClearButton()!.getElement();
expect(field).not.toContainElement(clearButton);
expect(wrapper.getElement()).toContainElement(clearButton);
});

test('splits custom Input styles between the field box and the native input', () => {
const style: InputProps['style'] = {
root: {
backgroundColor: { default: '#ffffff', hover: '#f2f3f3' },
borderColor: { default: '#000000', hover: '#111111' },
borderRadius: '6px',
borderWidth: '2px',
color: { default: '#222222', disabled: '#999999' },
paddingBlock: '12px',
paddingInline: '16px',
},
};
const { wrapper } = renderInput({ style });
const field = findField(wrapper);
const nativeInput = wrapper.findNativeInput().getElement();

expect(field).toHaveStyle({ borderRadius: '6px', borderWidth: '2px' });
expect(field.style.paddingBlock).toBe('');
expect(field.style.paddingInline).toBe('');
expect(field.style.getPropertyValue(customCssProps.styleBackgroundHover)).toBe('#f2f3f3');
expect(field.style.getPropertyValue(customCssProps.styleBorderColorHover)).toBe('#111111');

expect(nativeInput.style.paddingBlock).toBe('12px');
expect(nativeInput.style.paddingInline).toBe('16px');
expect(nativeInput.style.borderRadius).toBe('');
expect(nativeInput.style.borderWidth).toBe('');
expect(nativeInput.style.getPropertyValue(customCssProps.styleColorDisabled)).toBe('#999999');
});

describe('reflects validation and interaction state', () => {
const getField = (props: Partial<InputProps>) => findField(renderInput(props).wrapper);

test('adds the invalid modifier when invalid', () => {
expect(getField({ invalid: true })).toHaveClass(styles['input-field-invalid']);
});

test('prefers the invalid modifier over the warning modifier', () => {
const field = getField({ invalid: true, warning: true });
expect(field).toHaveClass(styles['input-field-invalid']);
expect(field).not.toHaveClass(styles['input-field-warning']);
});

test('adds the warning modifier when warning and not invalid', () => {
expect(getField({ warning: true })).toHaveClass(styles['input-field-warning']);
});

test('adds the disabled modifier when disabled', () => {
const field = getField({ disabled: true });
expect(field).toHaveClass(styles['input-field-disabled']);
expect(field).toHaveAttribute('aria-disabled', 'true');
});

test('keeps the invalid modifier when disabled', () => {
const field = getField({ disabled: true, invalid: true });
expect(field).toHaveClass(styles['input-field-disabled']);
expect(field).toHaveClass(styles['input-field-invalid']);
});

test('keeps the warning modifier when disabled', () => {
const field = getField({ disabled: true, warning: true });
expect(field).toHaveClass(styles['input-field-disabled']);
expect(field).toHaveClass(styles['input-field-warning']);
});

test('adds the readonly modifier when readOnly and not disabled', () => {
expect(getField({ readOnly: true })).toHaveClass(styles['input-field-readonly']);
});

test('prefers the disabled modifier over the readonly modifier', () => {
const field = getField({ disabled: true, readOnly: true });
expect(field).toHaveClass(styles['input-field-disabled']);
expect(field).not.toHaveClass(styles['input-field-readonly']);
});
});

test('moves the no-border-radius modifier to the field box', () => {
const { container } = render(<InternalInput value="" onChange={() => {}} __noBorderRadius={true} />);
const wrapper = createWrapper(container).findComponent(`.${styles['input-container']}`, InputWrapper)!;
expect(findField(wrapper)).toHaveClass(styles['input-field-no-border-radius']);
});

test('does not treat the field box or the adornments as click targets', () => {
const { wrapper } = renderInput({ prefix: '$' });
fireEvent.mouseDown(findField(wrapper));
fireEvent.mouseDown(wrapper.findPrefix()!.getElement());
expect(wrapper.findNativeInput().getElement()).not.toHaveFocus();
});
});

describe('prefix and suffix adornments', () => {
test('renders a prefix', () => {
const { wrapper } = renderInput({ prefix: '$' });
expect(wrapper.findPrefix()!.getElement()).toHaveTextContent('$');
expect(wrapper.findSuffix()).toBeNull();
expect(wrapper.findByClassName(styles['input-adorned-container'])).not.toBeNull();
expect(wrapper.findNativeInput().getElement()).toHaveClass(styles['input-adorned']);
expect(findField(wrapper)).toContainElement(wrapper.findPrefix()!.getElement());
});

test('renders a suffix', () => {
const { wrapper } = renderInput({ suffix: '%' });
expect(wrapper.findSuffix()!.getElement()).toHaveTextContent('%');
expect(wrapper.findPrefix()).toBeNull();
expect(wrapper.findNativeInput().getElement()).toHaveClass(styles['input-adorned']);
expect(findField(wrapper)).toContainElement(wrapper.findSuffix()!.getElement());
});

test('renders both a prefix and a suffix', () => {
Expand Down Expand Up @@ -66,40 +168,10 @@ describe('prefix and suffix adornments', () => {
expect(wrapper.findSuffix()!.getElement()).toHaveTextContent('1');
});

test('applies custom Input styles to the adorned container', () => {
const style: InputProps['style'] = {
root: {
backgroundColor: { default: '#ffffff', hover: '#f2f3f3' },
borderColor: { default: '#000000', hover: '#111111' },
borderRadius: '6px',
borderWidth: '2px',
color: { default: '#222222', disabled: '#999999' },
paddingBlock: '12px',
paddingInline: '16px',
},
};
const { wrapper } = renderInput({ prefix: '$', style });
const container = wrapper.findByClassName(styles['input-adorned-container'])!.getElement();

expect(container).toHaveStyle({ borderRadius: '6px', borderWidth: '2px' });
expect(container.style.getPropertyValue(customCssProps.styleBackgroundHover)).toBe('#f2f3f3');
expect(container.style.getPropertyValue(customCssProps.styleBorderColorHover)).toBe('#111111');
expect(container.style.getPropertyValue(customCssProps.styleColorDisabled)).toBe('#999999');

// paddingBlock and paddingInline go to the native input, NOT the container
const nativeInput = wrapper.findNativeInput()!.getElement();
expect(nativeInput.style.paddingBlock).toBe('12px');
expect(nativeInput.style.paddingInline).toBe('16px');
});

test('contains the end icon within the adorned focus container', () => {
const { container } = render(<InternalInput value="" onChange={() => {}} prefix="$" __endIcon="settings" />);
const adornedContainer = container.querySelector(`.${styles['input-adorned-container']}`)!;
const endIcon = container.querySelector(`.${styles['input-icon-end']}`)!;

expect(adornedContainer).toContainElement(endIcon as HTMLElement);
endIcon.querySelector('button')!.focus();
expect(adornedContainer.contains(document.activeElement)).toBe(true);
test('keeps the default text color on the value and applies the status color to the adornments only', () => {
const { wrapper } = renderInput({ prefix: '$', invalid: true });
expect(findField(wrapper)).toHaveClass(styles['input-field-invalid']);
expect(wrapper.findNativeInput().getElement()).not.toHaveClass(styles['input-invalid']);
});

test.each([null, false, 0, undefined, ''] as const)(
Expand All @@ -108,58 +180,10 @@ describe('prefix and suffix adornments', () => {
const { wrapper } = renderInput({ prefix: absentContent, suffix: absentContent });
expect(wrapper.findPrefix()).toBeNull();
expect(wrapper.findSuffix()).toBeNull();
expect(wrapper.findByClassName(styles['input-adorned-container'])).toBeNull();
expect(wrapper.findAllByClassName(styles['input-adornment-divider'])).toHaveLength(0);
}
);

describe('adornment container reflects validation and interaction state', () => {
const getContainer = (props: Partial<InputProps>) =>
renderInput({ prefix: '$', ...props })
.wrapper.findByClassName(styles['input-adorned-container'])!
.getElement();

test('adds the invalid modifier when invalid', () => {
expect(getContainer({ invalid: true })).toHaveClass(styles['input-adorned-container-invalid']);
});

test('prefers the invalid modifier over the warning modifier', () => {
const container = getContainer({ invalid: true, warning: true });
expect(container).toHaveClass(styles['input-adorned-container-invalid']);
expect(container).not.toHaveClass(styles['input-adorned-container-warning']);
});

test('adds the warning modifier when warning and not invalid', () => {
expect(getContainer({ warning: true })).toHaveClass(styles['input-adorned-container-warning']);
});

test('adds the disabled modifier when disabled', () => {
expect(getContainer({ disabled: true })).toHaveClass(styles['input-adorned-container-disabled']);
});

test('keeps the invalid modifier when disabled', () => {
const container = getContainer({ disabled: true, invalid: true });
expect(container).toHaveClass(styles['input-adorned-container-disabled']);
expect(container).toHaveClass(styles['input-adorned-container-invalid']);
});

test('keeps the warning modifier when disabled', () => {
const container = getContainer({ disabled: true, warning: true });
expect(container).toHaveClass(styles['input-adorned-container-disabled']);
expect(container).toHaveClass(styles['input-adorned-container-warning']);
});
test('adds the readonly modifier when readOnly and not disabled', () => {
const container = getContainer({ readOnly: true });
expect(container).toHaveClass(styles['input-adorned-container-readonly']);
});

test('prefers the disabled modifier over the readonly modifier', () => {
const container = getContainer({ disabled: true, readOnly: true });
expect(container).toHaveClass(styles['input-adorned-container-disabled']);
expect(container).not.toHaveClass(styles['input-adorned-container-readonly']);
});
});

describe('accessibility', () => {
test('marks adornments as decorative with aria-hidden', () => {
const { wrapper } = renderInput({ prefix: '$', suffix: '%' });
Expand Down
Loading
Loading