diff --git a/change/@fluentui-react-tag-picker-preview-058f9a6b-51de-431d-8457-d32a44469a38.json b/change/@fluentui-react-tag-picker-preview-058f9a6b-51de-431d-8457-d32a44469a38.json new file mode 100644 index 00000000000000..f8957e267841ba --- /dev/null +++ b/change/@fluentui-react-tag-picker-preview-058f9a6b-51de-431d-8457-d32a44469a38.json @@ -0,0 +1,7 @@ +{ + "type": "none", + "comment": "chore: adds e2e tests", + "packageName": "@fluentui/react-tag-picker-preview", + "email": "bernardo.sunderhus@gmail.com", + "dependentChangeType": "none" +} diff --git a/packages/react-components/react-tag-picker-preview/cypress.config.ts b/packages/react-components/react-tag-picker-preview/cypress.config.ts new file mode 100644 index 00000000000000..ca52cf041bbf2c --- /dev/null +++ b/packages/react-components/react-tag-picker-preview/cypress.config.ts @@ -0,0 +1,3 @@ +import { baseConfig } from '@fluentui/scripts-cypress'; + +export default baseConfig; diff --git a/packages/react-components/react-tag-picker-preview/package.json b/packages/react-components/react-tag-picker-preview/package.json index 1fa0f59290d5e1..785aaaf43ee88a 100644 --- a/packages/react-components/react-tag-picker-preview/package.json +++ b/packages/react-components/react-tag-picker-preview/package.json @@ -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": "*", diff --git a/packages/react-components/react-tag-picker-preview/src/components/TagPicker/TagPicker.cy.tsx b/packages/react-components/react-tag-picker-preview/src/components/TagPicker/TagPicker.cy.tsx new file mode 100644 index 00000000000000..5ae35607e2e9bf --- /dev/null +++ b/packages/react-components/react-tag-picker-preview/src/components/TagPicker/TagPicker.cy.tsx @@ -0,0 +1,268 @@ +/// +/// + +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({element}); +}; + +const options = [ + 'John Doe', + 'Jane Doe', + 'Max Mustermann', + 'Erika Mustermann', + 'Pierre Dupont', + 'Amelie Dupont', + 'Mario Rossi', + 'Maria Rossi', +]; + +type TagPickerControlledProps = Pick; + +const TagPickerControlled = ({ open, defaultOpen, defaultSelectedOptions = [] }: TagPickerControlledProps) => { + const [selectedOptions, setSelectedOptions] = React.useState(defaultSelectedOptions); + const onOptionSelect: TagPickerProps['onOptionSelect'] = (e, data) => { + setSelectedOptions(data.selectedOptions); + }; + const handleAllClear: React.MouseEventHandler = event => { + setSelectedOptions([]); + }; + + return ( +
+ + + All Clear + + } + > + + {selectedOptions.map(option => ( + } + value={option} + > + {option} + + ))} + + + + + {options + .filter(option => !selectedOptions.includes(option)) + .map((option, index) => ( + } + value={option} + key={option} + > + {option} + + ))} + + +
+ ); +}; + +describe('TagPicker', () => { + it('should render a closed listbox', () => { + mount(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + 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(); + cy.get(`[data-testid="tag--${options[0]}"]`).focus().realPress('Backspace').should('not.exist'); + cy.get('[data-testid="tag-picker-input"]').should('be.focused'); + }); + }); + }); +}); diff --git a/packages/react-components/react-tag-picker-preview/stories/TagPicker/TagPickerDefault.stories.tsx b/packages/react-components/react-tag-picker-preview/stories/TagPicker/TagPickerDefault.stories.tsx index fec45f7f3c969e..c8fe44e82c9fd7 100644 --- a/packages/react-components/react-tag-picker-preview/stories/TagPicker/TagPickerDefault.stories.tsx +++ b/packages/react-components/react-tag-picker-preview/stories/TagPicker/TagPickerDefault.stories.tsx @@ -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', @@ -29,34 +29,32 @@ export const Default = () => { return (
- - - - - {selectedOptions.map(option => ( - } value={option}> - {option} - - ))} - - - - - {options - .filter(option => !selectedOptions.includes(option)) - .map(option => ( - } - value={option} - key={option} - > - {option} - - ))} - - - + + + + {selectedOptions.map(option => ( + } value={option}> + {option} + + ))} + + + + + {options + .filter(option => !selectedOptions.includes(option)) + .map(option => ( + } + value={option} + key={option} + > + {option} + + ))} + +
); }; diff --git a/packages/react-components/react-tag-picker-preview/tsconfig.cy.json b/packages/react-components/react-tag-picker-preview/tsconfig.cy.json new file mode 100644 index 00000000000000..93a140885851da --- /dev/null +++ b/packages/react-components/react-tag-picker-preview/tsconfig.cy.json @@ -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"] +} diff --git a/packages/react-components/react-tag-picker-preview/tsconfig.json b/packages/react-components/react-tag-picker-preview/tsconfig.json index 1941a041d46c19..1317f81620ca5e 100644 --- a/packages/react-components/react-tag-picker-preview/tsconfig.json +++ b/packages/react-components/react-tag-picker-preview/tsconfig.json @@ -20,6 +20,9 @@ }, { "path": "./.storybook/tsconfig.json" + }, + { + "path": "./tsconfig.cy.json" } ] } diff --git a/packages/react-components/react-tag-picker-preview/tsconfig.lib.json b/packages/react-components/react-tag-picker-preview/tsconfig.lib.json index 6f90cf95c005bd..e17f808c039339 100644 --- a/packages/react-components/react-tag-picker-preview/tsconfig.lib.json +++ b/packages/react-components/react-tag-picker-preview/tsconfig.lib.json @@ -16,7 +16,9 @@ "**/*.test.ts", "**/*.test.tsx", "**/*.stories.ts", - "**/*.stories.tsx" + "**/*.stories.tsx", + "**/*.cy.ts", + "**/*.cy.tsx" ], "include": ["./src/**/*.ts", "./src/**/*.tsx"] }