Skip to content

Commit

Permalink
FocusTrapZone: port most tests to cypress (#21741)
Browse files Browse the repository at this point in the history
  • Loading branch information
ecraig12345 authored Mar 2, 2022
1 parent d4c3346 commit f52f47c
Show file tree
Hide file tree
Showing 14 changed files with 787 additions and 1,110 deletions.
8 changes: 6 additions & 2 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,12 @@ jobs:

# only run e2e tests when the appropriate storybook is published by scoping to relevant packages
- script: |
yarn e2e $(sinceArg) --scope @fluentui/react-components --scope @fluentui/react
displayName: Cypress E2E tests
yarn e2e $(sinceArg) --scope @fluentui/react-components
displayName: v9 Cypress E2E tests
- script: |
yarn e2e $(sinceArg) --scope @fluentui/react
displayName: v8 Cypress E2E tests
- template: .devops/templates/cleanup.yml

Expand Down
33 changes: 33 additions & 0 deletions packages/react-examples/src/e2e/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import * as React from 'react';

/**
* Define a global function on `window` and clean it up after the test finishes.
* NOTE: This only runs once (updates to the function are not respected).
*/
export function useGlobal<Globals>(name: keyof Globals, func: Required<Globals>[typeof name]) {
React.useEffect(() => {
((window as unknown) as Globals)[name] = func;
return () => {
// Clean up the global to avoid timing issues where a test tries to call the version of a
// global defined by a previous test (this can happen in headless mode)
delete ((window as unknown) as Globals)[name];
};
// eslint-disable-next-line react-hooks/exhaustive-deps -- should only run on mount
}, []);
}

export type UsePropsGlobals<TProps> = {
/** Callback allowing a test to update the value returned by `useProps` inside a story. */
setProps?: (props: TProps) => void;
};

/**
* Define a global `window.setProps` to set props for the story, and clean it up
* after the test finishes.
* @returns the latest props
*/
export function useProps<TProps>() {
const [props, setProps] = React.useState<TProps | undefined>();
useGlobal<UsePropsGlobals<TProps>>('setProps' as const, setProps);
return props;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import * as React from 'react';
import { FocusTrapZone } from '@fluentui/react';
import { useGlobal } from '../../../e2e/utils';
import { FTZTestGlobals } from './types';
import { rootClass } from './shared';

/**
* It maintains a proper stack of FocusTrapZones as more are mounted/unmounted
*/
export const FocusStack = () => {
// Whether to render each FocusTrapZone
const [shouldRender, setShouldRender] = React.useState([true, false, false, false, false]);

const updateFTZ = (num: 1 | 2 | 3 | 4, newValue: boolean) => {
setShouldRender(prevValues => {
const newValues = [...prevValues];
newValues[num] = newValue;
return newValues;
});
};

useGlobal<FTZTestGlobals>('getFocusStack', () => FocusTrapZone.focusStack);

return (
<div className={rootClass}>
<FocusTrapZone id="ftz0">
ftz0
<button onClick={() => updateFTZ(1, true)}>add ftz1</button>
<button onClick={() => updateFTZ(3, true)}>add ftz3</button>
<button onClick={() => updateFTZ(4, true)}>add ftz4</button>
</FocusTrapZone>

{shouldRender[1] && (
<FocusTrapZone id="ftz1">
ftz1
<button onClick={() => updateFTZ(2, true)}>add ftz2</button>
</FocusTrapZone>
)}
{shouldRender[2] && (
<FocusTrapZone id="ftz2">
ftz2
<button onClick={() => updateFTZ(1, false)}>remove ftz1</button>
<button onClick={() => updateFTZ(2, false)}>remove ftz2</button>
</FocusTrapZone>
)}
{shouldRender[3] && (
<FocusTrapZone id="ftz3" forceFocusInsideTrap={false}>
ftz3
<button onClick={() => updateFTZ(3, false)}>remove ftz3</button>
</FocusTrapZone>
)}
{shouldRender[4] && (
<FocusTrapZone id="ftz4" disabled>
ftz4
</FocusTrapZone>
)}
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as React from 'react';
import { FocusZone, FocusTrapZone } from '@fluentui/react';
import type { IFocusTrapZone, IFocusTrapZoneProps } from '@fluentui/react';
import { useGlobal, useProps } from '../../../e2e/utils';
import type { FTZTestGlobals } from './types';
import { rootClass } from './shared';

/** Imperatively focusing the FTZ */
export const ImperativeFocus = () => {
const props = useProps<IFocusTrapZoneProps>();
const focusTrapZoneRef = React.useRef<IFocusTrapZone>(null);

useGlobal<FTZTestGlobals>('imperativeFocus', () => focusTrapZoneRef.current?.focus());

return (
// don't render until props have been set
props && (
<div className={rootClass}>
<FocusTrapZone disableFirstFocus componentRef={focusTrapZoneRef} {...props}>
<button>first</button>
<FocusZone>
<button>mid</button>
<button>last</button>
</FocusZone>
</FocusTrapZone>
<button>after</button>
</div>
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from 'react';
import { FocusTrapZone } from '@fluentui/react';
import type { IFocusTrapZoneProps } from '@fluentui/react';
import { useProps } from '../../../e2e/utils';
import { rootClass } from './shared';

/**
* Tab and shift-tab when the FTZ contains 0 tabbable items
*/
export const NoTabbableItems = () => {
const props = useProps<IFocusTrapZoneProps>();

return (
// don't render until props have been set
props && (
<div className={rootClass}>
<button>before</button>
<FocusTrapZone forceFocusInsideTrap {...props}>
<button tabIndex={-1}>first</button>
<button tabIndex={-1}>mid</button>
<button tabIndex={-1}>last</button>
</FocusTrapZone>
<button>after</button>
</div>
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { FocusTrapZone } from '@fluentui/react';
import type { IFocusTrapZoneProps } from '@fluentui/react';
import { useProps } from '../../../e2e/utils';
import { rootClass } from './shared';

/** Respects default and explicit prop values */
export const PropValues = () => {
const [buttonClicked, setButtonClicked] = React.useState('');
const props = useProps<IFocusTrapZoneProps>();

return (
// don't render until props have been set
props && (
<div className={rootClass} onClick={ev => setButtonClicked((ev.target as HTMLButtonElement).textContent || '')}>
<span id="buttonClicked" style={{ display: 'block' /* avoid inherited div styling */ }}>
clicked {buttonClicked}
</span>
<button>before</button>
<FocusTrapZone {...props}>
<button>first</button>
<button>mid</button>
<button className="last-class" id="last">
last
</button>
</FocusTrapZone>
<button>after</button>
</div>
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import * as React from 'react';
import { FocusZone, FocusTrapZone, FocusZoneDirection } from '@fluentui/react';
import { rootClass } from './shared';

/**
* Tab and shift-tab wrap at extreme ends of the FTZ:
*
* can tab between a button and a FocusZone
*/
export const TabWrappingButtonFocusZone = () => {
return (
<div className={rootClass}>
<FocusTrapZone forceFocusInsideTrap={false}>
<div>
<button>first</button>
</div>
<FocusZone direction={FocusZoneDirection.horizontal}>
<div>
<button>fzFirst</button>
</div>
<div>
<div>
<button>fzMid1</button>
<button>fzMid2</button>
<button>fzLast</button>
</div>
</div>
</FocusZone>
</FocusTrapZone>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import * as React from 'react';
import { FocusZone, FocusTrapZone, FocusZoneDirection } from '@fluentui/react';
import { rootClass } from './shared';

/**
* Tab and shift-tab wrap at extreme ends of the FTZ:
*
* can trap focus when FTZ bookmark elements are FocusZones,
* and those elements have inner elements focused that are not the first inner element
*/
export const TabWrappingFocusZoneBumpers = () => {
return (
<div className={rootClass}>
<button>before</button>
<FocusTrapZone forceFocusInsideTrap={false}>
<FocusZone direction={FocusZoneDirection.horizontal}>
<button>fz1First</button>
<button>fz1Mid</button>
<button>fz1Last</button>
</FocusZone>
<button>mid</button>
<FocusZone direction={FocusZoneDirection.horizontal}>
<button>fz2First</button>
<button>fz2Mid</button>
<button>fz2Last</button>
</FocusZone>
</FocusTrapZone>
<button>after</button>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,43 +1,33 @@
import * as React from 'react';
import { FocusZone, FocusTrapZone, FocusZoneDirection, mergeStyles } from '@fluentui/react';

const rootClass = mergeStyles({
position: 'relative',
button: { position: 'absolute', height: 30, width: 30 },
'#a': { top: 0, left: 0 },
'#b': { top: 0, left: 30 },
'#c': { top: 0, left: 60 },
'#d': { top: 30, left: 0 },
'#e': { top: 30, left: 30 },
'#f': { top: 30, left: 60 },
});
import { FocusZone, FocusTrapZone, FocusZoneDirection } from '@fluentui/react';
import { rootClass } from './shared';

/**
* Tab and shift-tab wrap at extreme ends of the FTZ:
*
* can tab across FocusZones with different button structures
* can tab between multiple FocusZones with different button structures
*/
export const TabWrappingMultiFocusZone = () => {
return (
<div className={rootClass}>
<FocusTrapZone forceFocusInsideTrap={false}>
<FocusZone direction={FocusZoneDirection.horizontal}>
<div>
<button id="a">a</button>
<button>fz1First</button>
</div>
<div>
<button id="b">b</button>
<button>fz1Mid</button>
</div>
<div>
<button id="c">c</button>
<button>fz1Last</button>
</div>
</FocusZone>
<FocusZone direction={FocusZoneDirection.horizontal}>
<div>
<div>
<button id="d">d</button>
<button id="e">e</button>
<button id="f">f</button>
<button>fz2First</button>
<button>fz2Mid</button>
<button>fz2Last</button>
</div>
</div>
</FocusZone>
Expand Down
Loading

0 comments on commit f52f47c

Please sign in to comment.