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
@@ -0,0 +1,7 @@
{
"type": "none",
"comment": "chore: adds e2e tests",
"packageName": "@fluentui/react-tag-picker-preview",
"email": "bernardo.sunderhus@gmail.com",
"dependentChangeType": "none"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { baseConfig } from '@fluentui/scripts-cypress';
Comment thread
bsunderhus marked this conversation as resolved.

export default baseConfig;
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"storybook": "start-storybook",
"test": "jest --passWithNoTests",
"test-ssr": "test-ssr \"./stories/**/*.stories.tsx\"",
"type-check": "tsc -b tsconfig.json"
"type-check": "tsc -b tsconfig.json",
"e2e": "cypress run --component",
"e2e:local": "cypress open --component"
},
"devDependencies": {
"@fluentui/eslint-plugin": "*",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
/// <reference types="cypress" />
/// <reference types="cypress-real-events" />

import * as React from 'react';
import { mount as mountBase } from '@cypress/react';
import { FluentProvider } from '@fluentui/react-provider';
import { teamsLightTheme } from '@fluentui/react-theme';
import { TagPickerProps } from './TagPicker.types';
import { TagPicker } from './TagPicker';
import { TagPickerControl } from '../TagPickerControl/TagPickerControl';
import { TagPickerGroup } from '../TagPickerGroup/TagPickerGroup';
import { Tag } from '@fluentui/react-tags';
import { TagPickerInput } from '../TagPickerInput/TagPickerInput';
import { TagPickerList } from '../TagPickerList/TagPickerList';
import { TagPickerOption } from '../TagPickerOption/TagPickerOption';
import { Avatar } from '@fluentui/react-avatar';
import { Button } from '@fluentui/react-button';

const mount = (element: JSX.Element) => {
mountBase(<FluentProvider theme={teamsLightTheme}>{element}</FluentProvider>);
};

const options = [
'John Doe',
'Jane Doe',
'Max Mustermann',
'Erika Mustermann',
'Pierre Dupont',
'Amelie Dupont',
'Mario Rossi',
'Maria Rossi',
];

type TagPickerControlledProps = Pick<TagPickerProps, 'open' | 'defaultOpen' | 'defaultSelectedOptions'>;

const TagPickerControlled = ({ open, defaultOpen, defaultSelectedOptions = [] }: TagPickerControlledProps) => {
const [selectedOptions, setSelectedOptions] = React.useState<string[]>(defaultSelectedOptions);
const onOptionSelect: TagPickerProps['onOptionSelect'] = (e, data) => {
setSelectedOptions(data.selectedOptions);
};
const handleAllClear: React.MouseEventHandler = event => {
setSelectedOptions([]);
};

return (
<div style={{ maxWidth: 400 }}>
<TagPicker
onOptionSelect={onOptionSelect}
selectedOptions={selectedOptions}
open={open}
defaultOpen={defaultOpen}
>
<TagPickerControl
expandIcon={{ 'data-testid': 'tag-picker-control__expandIcon' } as {}}
data-testid="tag-picker-control"
secondaryAction={
<Button
data-testid="tag-picker-control__secondaryAction"
appearance="transparent"
size="small"
shape="rounded"
onClick={handleAllClear}
>
All Clear
</Button>
}
>
<TagPickerGroup>
{selectedOptions.map(option => (
<Tag
data-testid={`tag--${option}`}
key={option}
shape="rounded"
media={<Avatar name={option} color="colorful" />}
value={option}
>
{option}
</Tag>
))}
</TagPickerGroup>
<TagPickerInput data-testid="tag-picker-input" />
</TagPickerControl>
<TagPickerList data-testid="tag-picker-list">
{options
.filter(option => !selectedOptions.includes(option))
.map((option, index) => (
<TagPickerOption
id={`tag-picker-option--${index}`}
data-testid={`tag-picker-option--${option}`}
secondaryContent="Microsoft FTE"
media={<Avatar name={option} color="colorful" />}
value={option}
key={option}
>
{option}
</TagPickerOption>
))}
</TagPickerList>
</TagPicker>
</div>
);
};

describe('TagPicker', () => {
it('should render a closed listbox', () => {
mount(<TagPickerControlled />);
cy.get('[data-testid="tag-picker-control"]').should('exist');
cy.get('[data-testid="tag-picker-list"]').should('not.exist');
});
it('should render an opened listbox', () => {
mount(<TagPickerControlled open />);
cy.get('[data-testid="tag-picker-control"]').should('exist');
cy.get('[data-testid="tag-picker-list"]').should('be.visible');
});
describe('Mouse navigation', () => {
it('should open/close a listbox once trigger is clicked', () => {
mount(<TagPickerControlled />);
cy.get('[data-testid="tag-picker-list"]').should('not.exist');
cy.get('[data-testid="tag-picker-input"]').realClick();
cy.get('[data-testid="tag-picker-list"]').should('be.visible');
cy.get('[data-testid="tag-picker-input"]').should('be.focused');
cy.get('[data-testid="tag-picker-input"]').realClick();
cy.get('[data-testid="tag-picker-list"]').should('not.be.visible');
});
it('should open/close a listbox once expandIcon is clicked', () => {
mount(<TagPickerControlled />);
cy.get('[data-testid="tag-picker-list"]').should('not.exist');
cy.get('[data-testid="tag-picker-control__expandIcon"]').realClick();
cy.get('[data-testid="tag-picker-list"]').should('be.visible');
cy.get('[data-testid="tag-picker-input"]').should('be.focused');
cy.get('[data-testid="tag-picker-control__expandIcon"]').realClick();
cy.get('[data-testid="tag-picker-list"]').should('not.be.visible');
});
it('should select a tag on option click', () => {
mount(<TagPickerControlled defaultOpen />);
cy.get('[data-testid="tag-picker-list"]').should('be.visible');
cy.get(`[data-testid="tag-picker-option--${options[0]}"]`).should('exist').realClick();
cy.get('[data-testid="tag-picker-list"]').should('not.be.visible');
cy.get(`[data-testid="tag--${options[0]}"]`).should('exist');
cy.get('[data-testid="tag-picker-input"]').should('be.focused');
});
it('should dismiss a tag on tag click', () => {
mount(<TagPickerControlled defaultOpen />);
cy.get(`[data-testid="tag-picker-option--${options[0]}"]`).realClick();
cy.get(`[data-testid="tag--${options[0]}"]`).should('exist').realClick().should('not.exist');
cy.get('[data-testid="tag-picker-input"]').should('be.focused');
});
it('should dismiss a tag on clear all click', () => {
mount(<TagPickerControlled defaultOpen />);
cy.get(`[data-testid="tag-picker-option--${options[0]}"]`).realClick();
cy.get(`[data-testid="tag--${options[0]}"]`).should('exist').realClick();
cy.get('[data-testid="tag-picker-input"]').should('be.focused');
cy.get('[data-testid="tag-picker-control__secondaryAction"]').should('exist').realClick();
cy.get(`[data-testid="tag--${options[0]}"]`).should('not.exist');
});
});
describe('Keyboard navigation', () => {
it('should not open listbox on input focus', () => {
mount(<TagPickerControlled />);
cy.get('[data-testid="tag-picker-list"]').should('not.exist');
cy.get('[data-testid="tag-picker-input"]').focus();
cy.get('[data-testid="tag-picker-list"]').should('exist').should('not.be.visible');
});
(['Enter', 'ArrowDown', 'ArrowUp'] as const).forEach(keypress => {
it(`should open listbox on input ${keypress} key press`, () => {
mount(<TagPickerControlled />);
cy.get('[data-testid="tag-picker-list"]').should('not.exist');
cy.get('[data-testid="tag-picker-input"]').focus().realPress(keypress);
cy.get('[data-testid="tag-picker-list"]').should('exist').should('be.visible');
cy.get(`[data-testid="tag-picker-input"]`).should('have.attr', 'aria-activedescendant', `tag-picker-option--0`);
});
});
it('should not open listbox on input Space key press', () => {
mount(<TagPickerControlled />);
cy.get('[data-testid="tag-picker-list"]').should('not.exist');
cy.get('[data-testid="tag-picker-input"]').focus().realPress('Space');
cy.get('[data-testid="tag-picker-list"]').should('exist').should('not.be.visible');
});
it('should close listbox on input Escape key press', () => {
mount(<TagPickerControlled defaultOpen />);
cy.get('[data-testid="tag-picker-list"]').should('be.visible');
cy.get(`[data-testid="tag-picker-input"]`).should('have.attr', 'aria-activedescendant', `tag-picker-option--0`);
cy.get('[data-testid="tag-picker-input"]').focus().realPress('Escape');
cy.get('[data-testid="tag-picker-list"]').should('exist').should('not.be.visible');
cy.get(`[data-testid="tag-picker-input"]`).should('not.have.attr', 'aria-activedescendant');
});
(['ArrowDown', 'ArrowUp'] as const).forEach(keypress =>
it(`should move aria-activedescendant on ${keypress}`, () => {
mount(<TagPickerControlled defaultOpen />);
cy.get('[data-testid="tag-picker-list"]').should('be.visible');
cy.get(`[data-testid="tag-picker-input"]`).should('have.attr', 'aria-activedescendant', `tag-picker-option--0`);
cy.get('[data-testid="tag-picker-input"]').focus().realPress('ArrowDown');
cy.get(`[data-testid="tag-picker-input"]`).should('have.attr', 'aria-activedescendant', `tag-picker-option--1`);
cy.get('[data-testid="tag-picker-input"]').focus().realPress(keypress);
cy.get(`[data-testid="tag-picker-input"]`).should(
'have.attr',
'aria-activedescendant',
`tag-picker-option--${keypress === 'ArrowDown' ? 2 : 0}`,
);
}),
);
it('should close listbox and select activedescendant on Enter key press', () => {
mount(<TagPickerControlled />);
cy.get('[data-testid="tag-picker-list"]').should('not.exist');
cy.get(`[data-testid="tag--${options[0]}]"`).should('not.exist');
cy.get('[data-testid="tag-picker-input"]').focus().realPress('Enter');
cy.get('[data-testid="tag-picker-list"]').should('exist').should('be.visible');
cy.get(`[data-testid="tag-picker-input"]`).should('have.attr', 'aria-activedescendant', `tag-picker-option--0`);
cy.get('[data-testid="tag-picker-input"]').focus().realPress('Enter');
cy.get('[data-testid="tag-picker-list"]').should('exist').should('not.be.visible');
cy.get(`[data-testid="tag-picker-input"]`).should('not.have.attr', 'aria-activedescendant');
cy.get(`[data-testid="tag-picker-option--0"]`).should('not.exist');
cy.get(`[data-testid="tag--${options[0]}"]`).should('exist');
});
it('should select activedescendant on Space key press', () => {
mount(<TagPickerControlled />);
cy.get('[data-testid="tag-picker-list"]').should('not.exist');
cy.get(`[data-testid="tag--${options[0]}]"`).should('not.exist');
cy.get('[data-testid="tag-picker-input"]').focus().realPress('Enter');
cy.get('[data-testid="tag-picker-list"]').should('exist').should('be.visible');
cy.get(`[data-testid="tag-picker-input"]`).should('have.attr', 'aria-activedescendant', `tag-picker-option--0`);
cy.get('[data-testid="tag-picker-input"]').realPress('Space');
cy.get('[data-testid="tag-picker-list"]').should('be.visible');
cy.get(`[data-testid="tag-picker-option--0"]`).should('not.exist');
cy.get(`[data-testid="tag--${options[0]}"]`).should('exist');
});
describe('Tags', () => {
it('should focus on last tag on Shift + Tab', () => {
mount(<TagPickerControlled defaultSelectedOptions={options} />);
cy.get(`[data-testid="tag--${options[0]}"]`).should('exist');
cy.get(`[data-testid="tag--${options[options.length - 1]}"]`).should('exist');
cy.get('[data-testid="tag-picker-input"]').focus().realPress(['Shift', 'Tab']);
cy.get(`[data-testid="tag--${options[options.length - 1]}"]`).should('be.focused');
});
it('should navigate circularly between tags with Arrow key press', () => {
mount(<TagPickerControlled defaultSelectedOptions={options} />);
cy.get(`[data-testid="tag--${options[0]}"]`).focus().realPress('ArrowRight');
cy.get(`[data-testid="tag--${options[1]}"]`).should('be.focused').realPress('ArrowDown');
cy.get(`[data-testid="tag--${options[2]}"]`).should('be.focused').realPress('ArrowLeft');
cy.get(`[data-testid="tag--${options[1]}"]`).should('be.focused').realPress('ArrowUp');
cy.get(`[data-testid="tag--${options[0]}"]`).should('be.focused').realPress('ArrowUp');
cy.get(`[data-testid="tag--${options[options.length - 1]}"]`).should('be.focused');
});
it('should memorize last focused tag while switching focus between tags and input', () => {
mount(<TagPickerControlled defaultSelectedOptions={options} />);
cy.get(`[data-testid="tag--${options[0]}"]`).focus().realPress('ArrowRight');
cy.get(`[data-testid="tag--${options[1]}"]`).should('be.focused').realPress('Tab');
cy.get('[data-testid="tag-picker-input"]').should('be.focused').realPress(['Shift', 'Tab']);
cy.get(`[data-testid="tag--${options[1]}"]`).should('be.focused');
});
it('should remove tag on Backspace key press and focus on next one', () => {
mount(<TagPickerControlled defaultSelectedOptions={options} />);
cy.get(`[data-testid="tag--${options[0]}"]`).focus().realPress('Backspace').should('not.exist');
cy.get(`[data-testid="tag--${options[1]}"]`).should('be.focused');
});
it('should remove last tag on Backspace key press on input', () => {
mount(<TagPickerControlled defaultSelectedOptions={options} />);
cy.get('[data-testid="tag-picker-input"]').focus().realPress('Backspace');
cy.get(`[data-testid="tag--${options[options.length - 1]}"]`).should('not.exist');
});
it('should focus on input once all tags have been removed', () => {
mount(<TagPickerControlled defaultSelectedOptions={[options[0]]} />);
cy.get(`[data-testid="tag--${options[0]}"]`).focus().realPress('Backspace').should('not.exist');
cy.get('[data-testid="tag-picker-input"]').should('be.focused');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
TagPickerOption,
TagPickerGroup,
} from '@fluentui/react-tag-picker-preview';
import { Tag, Avatar, Field } from '@fluentui/react-components';
import { Tag, Avatar } from '@fluentui/react-components';

const options = [
'John Doe',
Expand All @@ -29,34 +29,32 @@ export const Default = () => {

return (
<div style={{ maxWidth: 400 }}>
<Field label={'Select User'} validationState={'error'} validationMessage={'User must be selected'}>
<TagPicker onOptionSelect={onOptionSelect} selectedOptions={selectedOptions}>
<TagPickerControl>
<TagPickerGroup>
{selectedOptions.map(option => (
<Tag key={option} shape="rounded" media={<Avatar name={option} color="colorful" />} value={option}>
{option}
</Tag>
))}
</TagPickerGroup>
<TagPickerInput />
</TagPickerControl>
<TagPickerList>
{options
.filter(option => !selectedOptions.includes(option))
.map(option => (
<TagPickerOption
secondaryContent="Microsoft FTE"
media={<Avatar name={option} color="colorful" />}
value={option}
key={option}
>
{option}
</TagPickerOption>
))}
</TagPickerList>
</TagPicker>
</Field>
<TagPicker onOptionSelect={onOptionSelect} selectedOptions={selectedOptions}>
<TagPickerControl>
<TagPickerGroup>
{selectedOptions.map(option => (
<Tag key={option} shape="rounded" media={<Avatar name={option} color="colorful" />} value={option}>
{option}
</Tag>
))}
</TagPickerGroup>
<TagPickerInput />
</TagPickerControl>
<TagPickerList>
{options
.filter(option => !selectedOptions.includes(option))
.map(option => (
<TagPickerOption
secondaryContent="Microsoft FTE"
media={<Avatar name={option} color="colorful" />}
value={option}
key={option}
>
{option}
</TagPickerOption>
))}
</TagPickerList>
</TagPicker>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"isolatedModules": false,
"types": ["node", "cypress", "cypress-storybook/cypress", "cypress-real-events"],
"lib": ["ES2019", "dom"]
},
"include": ["**/*.cy.ts", "**/*.cy.tsx"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
},
{
"path": "./.storybook/tsconfig.json"
},
{
"path": "./tsconfig.cy.json"
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
"**/*.test.ts",
"**/*.test.tsx",
"**/*.stories.ts",
"**/*.stories.tsx"
"**/*.stories.tsx",
"**/*.cy.ts",
"**/*.cy.tsx"
],
"include": ["./src/**/*.ts", "./src/**/*.tsx"]
}