diff --git a/packages/patternfly-4/react-core/src/components/Switch/Switch.test.js b/packages/patternfly-4/react-core/src/components/Switch/Switch.test.js index 312a648641d..d1289718bfb 100644 --- a/packages/patternfly-4/react-core/src/components/Switch/Switch.test.js +++ b/packages/patternfly-4/react-core/src/components/Switch/Switch.test.js @@ -1,5 +1,5 @@ import React from 'react'; -import { shallow } from 'enzyme'; +import { mount } from 'enzyme'; import { Switch } from './Switch'; const props = { @@ -8,52 +8,48 @@ const props = { }; test('switch label for attribute equals input id attribute', () => { - const view = shallow(); + const view = mount(); expect(view.find('input').prop('id')).toBe('foo'); expect(view.find('label').prop('htmlFor')).toBe('foo'); }); test('switch label id is auto generated', () => { - const view = shallow(); + const view = mount(); expect(view.find('input').prop('id')).toBeDefined(); }); test('switch is checked', () => { - const view = shallow(); + const view = mount(); expect(view).toMatchSnapshot(); }); test('switch is not checked', () => { - const view = shallow(); + const view = mount(); expect(view).toMatchSnapshot(); }); test('no label switch is checked', () => { - const view = shallow(); + const view = mount(); expect(view).toMatchSnapshot(); }); test('no label switch is not checked', () => { - const view = shallow(); + const view = mount(); expect(view).toMatchSnapshot(); }); test('switch is checked and disabled', () => { - const view = shallow(); + const view = mount(); expect(view).toMatchSnapshot(); }); test('switch is not checked and disabled', () => { - const view = shallow(); + const view = mount(); expect(view).toMatchSnapshot(); }); test('switch passes value and event to onChange handler', () => { - const newValue = true; - const event = { - currentTarget: { checked: newValue } - }; - const view = shallow(); - view.find('input').simulate('change', event); - expect(props.onChange).toBeCalledWith(newValue, event); + const view = mount(); + view.find('input').simulate('change', { target: { checked: true } }); + expect(view.find('input').prop('checked')).toBe(true); }); diff --git a/packages/patternfly-4/react-core/src/components/Switch/Switch.tsx b/packages/patternfly-4/react-core/src/components/Switch/Switch.tsx index 4346a8d90bb..0e936503231 100644 --- a/packages/patternfly-4/react-core/src/components/Switch/Switch.tsx +++ b/packages/patternfly-4/react-core/src/components/Switch/Switch.tsx @@ -4,7 +4,7 @@ import { css } from '@patternfly/react-styles'; import { CheckIcon } from '@patternfly/react-icons'; import { getUniqueId } from '../../helpers/util'; import { Omit } from '../../helpers/typeUtils'; -import { isOUIAEnvironment, getUniqueId as getOUIAUniqueId } from '../../helpers/ouia'; +import { InjectedOuiaProps, withOuiaContext } from '../withOuia'; export interface SwitchProps extends Omit, 'type' | 'onChange' | 'disabled' | 'label'> { /** id for the label. */ @@ -23,9 +23,8 @@ export interface SwitchProps extends Omit, 'ty 'aria-label'?: string }; -export class Switch extends React.Component { +class Switch extends React.Component { id = ''; - ouiaId = getOUIAUniqueId(); static defaultProps = { id: '', @@ -37,7 +36,7 @@ export class Switch extends React.Component { onChange: () => undefined as any }; - constructor(props: SwitchProps) { + constructor(props: SwitchProps & InjectedOuiaProps) { super(props); if (!props.id && !props['aria-label']) { // tslint:disable-next-line:no-console @@ -47,14 +46,14 @@ export class Switch extends React.Component { } render() { - const { className, label, isChecked, isDisabled, onChange, ...props } = this.props; + const { className, label, isChecked, isDisabled, onChange, ouiaContext, ouiaId, ...props } = this.props; return ( + + `; exports[`no label switch is not checked 1`] = ` - + + + + `; exports[`switch is checked 1`] = ` - + + + + + `; exports[`switch is checked and disabled 1`] = ` - + + + + `; exports[`switch is not checked 1`] = ` - + + + + + `; exports[`switch is not checked and disabled 1`] = ` - + + + + `; diff --git a/packages/patternfly-4/react-core/src/components/index.ts b/packages/patternfly-4/react-core/src/components/index.ts index 127cea1ff1d..7ac7e36be80 100644 --- a/packages/patternfly-4/react-core/src/components/index.ts +++ b/packages/patternfly-4/react-core/src/components/index.ts @@ -44,3 +44,4 @@ export * from './TextInput'; export * from './Title'; export * from './Tooltip'; export * from './Wizard'; +export * from './withOuia'; \ No newline at end of file diff --git a/packages/patternfly-4/react-core/src/components/withOuia/index.ts b/packages/patternfly-4/react-core/src/components/withOuia/index.ts new file mode 100644 index 00000000000..c046e2f7de8 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/withOuia/index.ts @@ -0,0 +1 @@ +export * from './withOuia'; diff --git a/packages/patternfly-4/react-core/src/components/withOuia/ouia.ts b/packages/patternfly-4/react-core/src/components/withOuia/ouia.ts new file mode 100644 index 00000000000..ad6e909d5b3 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/withOuia/ouia.ts @@ -0,0 +1,5 @@ +export const isOUIAEnvironment = (): boolean => typeof window !== 'undefined' && window.localStorage.ouia && window.localStorage.ouia.toLowerCase() === 'true' || false; +export const generateOUIAId = (): boolean => typeof window !== 'undefined' && window.localStorage['ouia-generate-id'] && window.localStorage['ouia-generate-id'].toLowerCase() === 'true' || false; + +let id = 0; +export const getUniqueId = (): number => id++; diff --git a/packages/patternfly-4/react-core/src/components/withOuia/withOuia.md b/packages/patternfly-4/react-core/src/components/withOuia/withOuia.md new file mode 100644 index 00000000000..4ea42f9c9db --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/withOuia/withOuia.md @@ -0,0 +1,56 @@ +### Adding OUIA capabilities to library components + + 1. Import: `import { InjectedOuiaProps, withOuiaContext } from '../withOuia';` + 2. For TS combine the props with the InjectedOuiaProps `class Switch extends React.Component` + 3. Wrap the component in the withOuiaContext higher-order-component +``` +const SwitchWithOuiaContext = withOuiaContext(Switch); +export { SwitchWithOuiaContext as Switch }; +``` + 4. OUIA props are in `this.props.ouiaContext` +``` +const { ouiaContext, ouiaId } = this.props; + +``` + +### Consumer usage +#### Case 1: non-ouia users +``` + +``` +> No re-render, does not render ouia attributes +#### Case 2: enable ouia through local storage +##### in local storage _ouia: true_ +``` + +``` +> render's ouia attribute **data-ouia-component-type="Switch"** +#### Case 3: enable ouia through local storage and generate id +##### in local storage _ouia: true_ +##### in local storage _ouia-generate-id: true_ +``` + +``` +> render's ouia attributes **data-ouia-component-type="Switch" data-ouia-component-id="0"** +#### Case 4: enable ouia through local storage and provide id +##### in local storage _ouia: true_ +``` + +``` +> render's ouia attributes **data-ouia-component-type="Switch" data-ouia-component-id="my_switch_id"** +#### Case 5: enable ouia through context and provide id +##### Note: If context provided _isOuia_ is true and local storage provided _isOuia_ is false, context will win out. Context will also win if its _isOuia_ is false and local storage's is true. Context > local storage +``` +import { OuiaContext } from '@patternfly/react-core'; + + + +``` +> render's ouia attributes **data-ouia-component-type="Switch" data-ouia-component-id="my_switch_id"** diff --git a/packages/patternfly-4/react-core/src/components/withOuia/withOuia.tsx b/packages/patternfly-4/react-core/src/components/withOuia/withOuia.tsx new file mode 100644 index 00000000000..aaaf4416131 --- /dev/null +++ b/packages/patternfly-4/react-core/src/components/withOuia/withOuia.tsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { isOUIAEnvironment, getUniqueId, generateOUIAId } from './ouia'; +import { Omit } from '../../helpers/typeUtils'; + +export const OuiaContext = React.createContext(null); + +export interface InjectedOuiaProps { + ouiaContext?: OuiaContextProps; + ouiaId?: number | string; +} + +export interface OuiaContextProps { + isOuia?: boolean; + ouiaId?: number | string; +} + +export function withOuiaContext

>( + WrappedComponent: React.ComponentClass

| React.FunctionComponent

+): React.FunctionComponent { + return (props: R) => ( + + {(value: OuiaContextProps) => } + + ); +} + +interface OuiaProps { + component: any; + componentProps: any; + consumerContext?: OuiaContextProps; +} + +interface OuiaState { + isOuia?: boolean; + ouiaId?: number | string; +} + +class ComponentWithOuia extends React.Component { + + constructor(props: OuiaProps) { + super(props); + + this.state = { + isOuia: false, + ouiaId: null + }; + } + + /** + * if either consumer set isOuia through context or local storage + * then force a re-render + */ + componentDidMount() { + const { isOuia, ouiaId } = this.state; + const { consumerContext } = this.props; + const isOuiaEnv = isOUIAEnvironment(); + if ((consumerContext && consumerContext.isOuia !== undefined && consumerContext.isOuia !== isOuia) || isOuiaEnv !== isOuia ) { + this.setState({ + isOuia: consumerContext && consumerContext.isOuia !== undefined ? consumerContext.isOuia : isOuiaEnv, + ouiaId: consumerContext && consumerContext.ouiaId !== undefined ? consumerContext.ouiaId : (generateOUIAId() ? getUniqueId() : ouiaId) + }); + } + } + + render() { + const { isOuia, ouiaId } = this.state; + const { component: WrappedComponent, componentProps, consumerContext } = this.props; + return ( + + + {(value: OuiaContextProps) => } + + + ) + } +} diff --git a/packages/patternfly-4/react-core/src/helpers/ouia.ts b/packages/patternfly-4/react-core/src/helpers/ouia.ts deleted file mode 100644 index 4d52610207f..00000000000 --- a/packages/patternfly-4/react-core/src/helpers/ouia.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const isOUIAEnvironment = (): boolean => typeof window !== 'undefined' && window.localStorage.ouia; - -let id = 0; -export const getUniqueId = (): number => id++; diff --git a/packages/patternfly-4/react-integration/cypress/integration/ouia.spec.ts b/packages/patternfly-4/react-integration/cypress/integration/ouia.spec.ts new file mode 100644 index 00000000000..64021b11bd1 --- /dev/null +++ b/packages/patternfly-4/react-integration/cypress/integration/ouia.spec.ts @@ -0,0 +1,19 @@ +describe('Switch Demo Test', () => { + it('Navigate to demo section', () => { + cy.visit('http://localhost:3000/'); + cy.get('#ouia-demo-nav-item-link').click(); + cy.url().should('eq', 'http://localhost:3000/ouia-demo-nav-link') + }); + + it('Verify Switches exist', () => { + cy.get('.pf-c-switch[for="simple-switch"]').should('exist'); + cy.get('.pf-c-switch[for="disabled-switch-off"]').should('exist'); + }); + + it('Verify OUIA attributes exist', () => { + cy.get('.pf-c-switch[for="simple-switch"]').should('have.attr', 'data-ouia-component-type', 'Switch'); + cy.get('.pf-c-switch[for="simple-switch"]').should('have.attr', 'data-ouia-component-id', 'first_switch'); + cy.get('.pf-c-switch[for="disabled-switch-off"]').should('have.attr', 'data-ouia-component-type', 'Switch'); + cy.get('.pf-c-switch[for="disabled-switch-off"]').should('not.have.attr', 'data-ouia-component-id'); + }); +}); \ No newline at end of file diff --git a/packages/patternfly-4/react-integration/demo-app-ts/src/Demos.ts b/packages/patternfly-4/react-integration/demo-app-ts/src/Demos.ts index 42373caae04..923e12977e9 100644 --- a/packages/patternfly-4/react-integration/demo-app-ts/src/Demos.ts +++ b/packages/patternfly-4/react-integration/demo-app-ts/src/Demos.ts @@ -321,6 +321,11 @@ export const Demos: DemoInterface[] = [ name: 'Options Menu Demo', componentType: Examples.OptionsMenuDemo }, + { + id: 'ouia-demo', + name: 'Ouia Demo', + componentType: Examples.OuiaDemo + }, { id: 'page-demo', name: 'Page Demo', diff --git a/packages/patternfly-4/react-integration/demo-app-ts/src/components/demos/OuiaDemo/OuiaDemo.tsx b/packages/patternfly-4/react-integration/demo-app-ts/src/components/demos/OuiaDemo/OuiaDemo.tsx new file mode 100644 index 00000000000..f4f5a2fa436 --- /dev/null +++ b/packages/patternfly-4/react-integration/demo-app-ts/src/components/demos/OuiaDemo/OuiaDemo.tsx @@ -0,0 +1,39 @@ +import React, { Fragment } from 'react'; +import { Switch, SwitchProps, OuiaContext } from '@patternfly/react-core'; + +interface SwitchState { + isChecked: boolean +}; +export class OuiaDemo extends React.Component { + constructor(props: SwitchProps) { + super(props); + this.state = { + isChecked: true + }; + } + handleChange = (isChecked: boolean) => { + this.setState({ + isChecked + }); + }; + + render() { + const { isChecked } = this.state; + return ( + + + +
+ +
+
+ ); + } +} \ No newline at end of file diff --git a/packages/patternfly-4/react-integration/demo-app-ts/src/components/demos/index.ts b/packages/patternfly-4/react-integration/demo-app-ts/src/components/demos/index.ts index a3e107713fc..5ef7192cfd6 100644 --- a/packages/patternfly-4/react-integration/demo-app-ts/src/components/demos/index.ts +++ b/packages/patternfly-4/react-integration/demo-app-ts/src/components/demos/index.ts @@ -62,6 +62,7 @@ export * from './ModalDemo/ModalDemo'; export * from './NavDemo/NavDemo'; export * from './NotificationBadgeDemo/NotificationBadgeDemo'; export * from './OptionsMenuDemo/OptionsMenuDemo'; +export * from './OuiaDemo/OuiaDemo'; export * from './PieChartDemo/PieBlueDemo'; export * from './PieChartDemo/PieColorDemo'; export * from './PieChartDemo/PieOrangeDemo';