Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add analytics metadata to Autosuggest component #2667

Merged
merged 4 commits into from
Sep 9, 2024
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
231 changes: 231 additions & 0 deletions src/autosuggest/__tests__/analytics-metadata.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// 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 {
activateAnalyticsMetadata,
GeneratedAnalyticsMetadataFragment,
} from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
import { getGeneratedAnalyticsMetadata } from '@cloudscape-design/component-toolkit/internal/analytics-metadata/utils';

import Autosuggest, { AutosuggestProps } from '../../../lib/components/autosuggest';
import InternalAutosuggest from '../../../lib/components/autosuggest/internal';
import FormField from '../../../lib/components/form-field';
import createWrapper from '../../../lib/components/test-utils/dom';
import { validateComponentNameAndLabels } from '../../internal/__tests__/analytics-metadata-test-utils';

import optionLabels from '../../../lib/components/internal/components/option/analytics-metadata/styles.css.js';
import selectableItemsLabels from '../../../lib/components/internal/components/selectable-item/analytics-metadata/styles.css.js';

const labels = { ...selectableItemsLabels, ...optionLabels };

function renderAutosuggest(props: Partial<AutosuggestProps>) {
const renderResult = render(
<Autosuggest
options={options}
value=""
ariaLabel="autosuggest with metadatada"
enteredTextLabel={value => `Use "${value}"`}
onChange={() => {}}
clearAriaLabel="clear content"
{...props}
/>
);
return createWrapper(renderResult.container).findAutosuggest()!;
}

const getMetadataContexts = (label = 'autosuggest with metadatada', disabled?: boolean) => {
const metadata: GeneratedAnalyticsMetadataFragment = {
contexts: [
{
type: 'component',
detail: {
name: 'awsui.Autosuggest',
label,
properties: {
disabled: disabled ? 'true' : 'false',
},
},
},
],
};
return metadata;
};

const options: AutosuggestProps['options'] = [
{ value: 'value1' },
{ value: 'value2', label: 'label2' },
{ value: 'value4', label: 'label4', disabled: true },
];
const optionsWithGroups: AutosuggestProps['options'] = [
{
label: 'group1',
options: [{ value: 'value1', label: 'label1' }, { value: 'value1repeated' }],
},
{
label: 'group2',
disabled: true,
options: [{ value: 'value2', label: 'label2' }],
},
{
label: 'group3',
options: [{ value: 'value3', label: 'label3' }],
},
{
label: 'group4',
options: [
{ value: 'value4', label: 'label4', disabled: true },
{ value: 'value5', label: 'label5', disabled: true },
],
},
{
label: 'group5',
options: [
{ value: 'value6', label: 'label6' },
{ value: 'value7', label: 'label7' },
],
},
];

beforeAll(() => {
activateAnalyticsMetadata(true);
});
describe('Autosuggest renders correct analytics metadata', () => {
test('on the clear input button', () => {
const wrapper = renderAutosuggest({ value: 'something' });
const clearButton = wrapper.findClearButton()!.getElement();
validateComponentNameAndLabels(clearButton, labels);
expect(getGeneratedAnalyticsMetadata(clearButton)).toEqual({
action: 'clearInput',
detail: { label: 'clear content' },
...getMetadataContexts(),
});
});
test('when disabled', () => {
const wrapper = renderAutosuggest({ disabled: true });
expect(getGeneratedAnalyticsMetadata(wrapper.getElement())).toEqual({
...getMetadataContexts(undefined, true),
});
});
test('with simple items', () => {
const wrapper = renderAutosuggest({ value: 'something' });
wrapper.findNativeInput().focus();

const simpleEnabledItemWithoutLabel = wrapper.findDropdown().findOptionByValue('value1')!.getElement();
validateComponentNameAndLabels(simpleEnabledItemWithoutLabel, labels);
expect(getGeneratedAnalyticsMetadata(simpleEnabledItemWithoutLabel)).toEqual({
action: 'select',
detail: {
label: 'value1',
position: '1',
value: 'value1',
},
...getMetadataContexts(),
});

const simpleEnabledItemWithLabel = wrapper.findDropdown().findOptionByValue('value2')!.getElement();
validateComponentNameAndLabels(simpleEnabledItemWithLabel, labels);
expect(getGeneratedAnalyticsMetadata(simpleEnabledItemWithLabel)).toEqual({
action: 'select',
detail: {
label: 'label2',
position: '2',
value: 'value2',
},
...getMetadataContexts(),
});

const disabledItem = wrapper.findDropdown().findOptionByValue('value4')!.getElement();
validateComponentNameAndLabels(disabledItem, labels);
expect(getGeneratedAnalyticsMetadata(disabledItem)).toEqual({
...getMetadataContexts(),
});
});
test.each([false, true])('with groups and expandToViewport=%s', expandToViewport => {
const wrapper = renderAutosuggest({ options: optionsWithGroups, expandToViewport });
wrapper.findNativeInput().focus();

const enabledItemWithoutLabel = wrapper
.findDropdown({ expandToViewport })
.findOptionByValue('value1repeated')!
.getElement();
validateComponentNameAndLabels(enabledItemWithoutLabel, labels);
expect(getGeneratedAnalyticsMetadata(enabledItemWithoutLabel)).toEqual({
action: 'select',
detail: {
label: 'value1repeated',
position: '1,2',
value: 'value1repeated',
groupLabel: 'group1',
},
...getMetadataContexts(),
});

const enabledItemWithLabel = wrapper.findDropdown({ expandToViewport }).findOptionByValue('value3')!.getElement();
validateComponentNameAndLabels(enabledItemWithLabel, labels);
expect(getGeneratedAnalyticsMetadata(enabledItemWithLabel)).toEqual({
action: 'select',
detail: {
label: 'label3',
position: '3,1',
value: 'value3',
groupLabel: 'group3',
},
...getMetadataContexts(),
});

const inDisabledGroup = wrapper.findDropdown({ expandToViewport }).findOptionByValue('value2')!.getElement();
validateComponentNameAndLabels(inDisabledGroup, labels);
expect(getGeneratedAnalyticsMetadata(inDisabledGroup)).toEqual({
...getMetadataContexts(),
});

const disabledItem = wrapper.findDropdown({ expandToViewport }).findOptionByValue('value4')!.getElement();
validateComponentNameAndLabels(disabledItem, labels);
expect(getGeneratedAnalyticsMetadata(disabledItem)).toEqual({
...getMetadataContexts(),
});
});
test('within a formfield', () => {
const renderResult = render(
<FormField label="formfield label">
<Autosuggest
options={options}
value=""
enteredTextLabel={value => `Use "${value}"`}
onChange={() => {}}
clearAriaLabel="clear content"
/>
</FormField>
);
const element = createWrapper(renderResult.container).findAutosuggest()!.getElement();
expect(getGeneratedAnalyticsMetadata(element)).toEqual({
contexts: [
...getMetadataContexts('formfield label').contexts!,
{
type: 'component',
detail: {
name: 'awsui.FormField',
label: 'formfield label',
},
},
],
});
});
});

test('Internal Autosuggest does not render "component" metadata', () => {
const renderResult = render(
<InternalAutosuggest
options={options}
value=""
ariaLabel="autosuggest with metadatada"
enteredTextLabel={value => `Use "${value}"`}
onChange={() => {}}
clearAriaLabel="clear content"
/>
);
const wrapper = createWrapper(renderResult.container).findAutosuggest()!;
expect(getGeneratedAnalyticsMetadata(wrapper.findNativeInput()!.getElement())).toEqual({});
});
17 changes: 17 additions & 0 deletions src/autosuggest/analytics-metadata/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import { GeneratedAnalyticsMetadataInputClearInput } from '../../input/analytics-metadata/interfaces';
import { GeneratedAnalyticsMetadataSelectableItemSelect } from '../../internal/components/selectable-item/analytics-metadata/interfaces';

export type GeneratedAnalyticsMetadataAutosuggestSelect = GeneratedAnalyticsMetadataSelectableItemSelect;

export type GeneratedAnalyticsMetadataAutosuggestClearInput = GeneratedAnalyticsMetadataInputClearInput;

export interface GeneratedAnalyticsMetadataAutosuggestComponent {
name: 'awsui.Autosuggest';
label: string;
properties: {
disabled: string;
};
}
13 changes: 13 additions & 0 deletions src/autosuggest/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
import React from 'react';

import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';

import useBaseComponent from '../internal/hooks/use-base-component';
import { applyDisplayName } from '../internal/utils/apply-display-name';
import { getExternalProps } from '../internal/utils/external-props';
import { GeneratedAnalyticsMetadataAutosuggestComponent } from './analytics-metadata/interfaces';
import { AutosuggestProps } from './interfaces';
import InternalAutosuggest from './internal';

Expand All @@ -25,6 +28,15 @@ const Autosuggest = React.forwardRef(
virtualScroll: props.virtualScroll,
},
});

const componentAnalyticsMetadata: GeneratedAnalyticsMetadataAutosuggestComponent = {
name: 'awsui.Autosuggest',
label: 'input',
properties: {
disabled: `${!!props.disabled}`,
Copy link
Member

Choose a reason for hiding this comment

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

Should this also include readonly?

Copy link
Member Author

Choose a reason for hiding this comment

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

I can include it at a later stage if needed

},
};

const externalProps = getExternalProps(props);
return (
<InternalAutosuggest
Expand All @@ -34,6 +46,7 @@ const Autosuggest = React.forwardRef(
{...externalProps}
{...baseComponentProps}
ref={ref}
{...getAnalyticsMetadataAttribute({ component: componentAnalyticsMetadata })}
/>
);
}
Expand Down
36 changes: 36 additions & 0 deletions src/input/__tests__/analytics-metadata.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// 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 { activateAnalyticsMetadata } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';
import { getGeneratedAnalyticsMetadata } from '@cloudscape-design/component-toolkit/internal/analytics-metadata/utils';

import Input from '../../../lib/components/input';
import InternalInput from '../../../lib/components/input/internal';
import createWrapper from '../../../lib/components/test-utils/dom';

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

beforeAll(() => {
activateAnalyticsMetadata(true);
});
describe('Input renders correct analytics metadata', () => {
test('on the clear button', () => {
const renderResult = render(<Input value="value" onChange={() => {}} type="search" clearAriaLabel="clear" />);
const clearInputButton = createWrapper(renderResult.container).findInput()!.findClearButton()!.getElement();
expect(getGeneratedAnalyticsMetadata(clearInputButton)).toEqual({
action: 'clearInput',
detail: {
label: 'clear',
},
});
});
test('without the clear button', () => {
const renderResult = render(<InternalInput value="value" onChange={() => {}} __rightIcon="settings" />);
const rightIconButton = createWrapper(renderResult.container)
.findByClassName(styles['input-icon-right'])!
.getElement();
expect(getGeneratedAnalyticsMetadata(rightIconButton)).toEqual({});
});
});
9 changes: 9 additions & 0 deletions src/input/analytics-metadata/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

export interface GeneratedAnalyticsMetadataInputClearInput {
action: 'clearInput';
detail: {
label: string;
};
}
12 changes: 11 additions & 1 deletion src/input/internal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import React, { Ref, useRef } from 'react';
import clsx from 'clsx';

import { getAnalyticsMetadataAttribute } from '@cloudscape-design/component-toolkit/internal/analytics-metadata';

import InternalButton from '../button/internal';
import { useInternalI18n } from '../i18n/context';
import { IconProps } from '../icon/interfaces';
Expand All @@ -13,6 +15,7 @@ import { fireKeyboardEvent, fireNonCancelableEvent, NonCancelableEventHandler }
import { InternalBaseComponentProps } from '../internal/hooks/use-base-component';
import { useDebounceCallback } from '../internal/hooks/use-debounce-callback';
import { useMergeRefs } from '../internal/hooks/use-merge-refs';
import { GeneratedAnalyticsMetadataInputClearInput } from './analytics-metadata/interfaces';
import { BaseChangeDetail, BaseInputProps, InputAutoCorrect, InputProps } from './interfaces';
import { convertAutoComplete, useSearchProps } from './utils';

Expand Down Expand Up @@ -188,7 +191,14 @@ function InternalInput(
)}
<input ref={mergedRef} {...attributes} />
{__rightIcon && (
<span className={styles['input-icon-right']}>
<span
className={styles['input-icon-right']}
{...(__rightIcon === 'close'
? getAnalyticsMetadataAttribute({
action: 'clearInput',
} as Partial<GeneratedAnalyticsMetadataInputClearInput>)
: {})}
>
<InternalButton
// Used for test utils
// eslint-disable-next-line react/forbid-component-props
Expand Down
Loading