From d9aa318aa40b96256d9601431b90aa2a33f8bcd8 Mon Sep 17 00:00:00 2001 From: Josh Thomas Date: Wed, 20 Feb 2019 15:35:20 -0600 Subject: [PATCH] tests(react): create tests for the react bindings to core (#17551) * Add tests. * updates after API meeting. * test(): add tests for create controller components. * correct testing for controller component * Ensure tests properly cleanup after each run. * create common test utils for overlay and controllers. * initial tests for ion router outlet * simple update. * add mocks for jest tests. --- react/__mocks__/@ionic/core/loader/index.js | 1 + .../ionicons/icons/index.js} | 0 react/__mocks__/ionicons/index.js | 2 + react/jest.setup.js | 1 + react/package.json | 25 +++- react/src/components/IonLoading.tsx | 4 +- react/src/components/IonPage.tsx | 29 +++++ .../components/__tests__/IonRouterOutlet.tsx | 40 ++++++ .../components/__tests__/createComponent.tsx | 49 +++++++ .../__tests__/createControllerComponent.tsx | 120 ++++++++++++++++++ .../__tests__/createOverlayComponent.tsx | 110 ++++++++++++++++ react/src/components/__tests__/utils.ts | 88 +++++++++++++ react/src/components/createComponent.tsx | 6 +- .../components/createControllerComponent.tsx | 34 +++-- .../src/components/createOverlayComponent.tsx | 28 ++-- react/src/components/index.ts | 23 +++- .../components/navigation/IonRouterOutlet.tsx | 11 +- .../components/utils/controller-test-utils.ts | 53 ++++++++ .../components/{utils.ts => utils/index.ts} | 18 ++- react/src/index.ts | 2 +- react/src/register.ts | 14 -- react/tsconfig.json | 4 + 22 files changed, 595 insertions(+), 67 deletions(-) create mode 100644 react/__mocks__/@ionic/core/loader/index.js rename react/{test/IonAlert.spec.tsx => __mocks__/ionicons/icons/index.js} (100%) create mode 100644 react/__mocks__/ionicons/index.js create mode 100644 react/jest.setup.js create mode 100644 react/src/components/IonPage.tsx create mode 100644 react/src/components/__tests__/IonRouterOutlet.tsx create mode 100644 react/src/components/__tests__/createComponent.tsx create mode 100644 react/src/components/__tests__/createControllerComponent.tsx create mode 100644 react/src/components/__tests__/createOverlayComponent.tsx create mode 100644 react/src/components/__tests__/utils.ts create mode 100644 react/src/components/utils/controller-test-utils.ts rename react/src/components/{utils.ts => utils/index.ts} (79%) delete mode 100644 react/src/register.ts diff --git a/react/__mocks__/@ionic/core/loader/index.js b/react/__mocks__/@ionic/core/loader/index.js new file mode 100644 index 0000000000..97c4089e08 --- /dev/null +++ b/react/__mocks__/@ionic/core/loader/index.js @@ -0,0 +1 @@ +exports.defineCustomElements = () => {}; diff --git a/react/test/IonAlert.spec.tsx b/react/__mocks__/ionicons/icons/index.js similarity index 100% rename from react/test/IonAlert.spec.tsx rename to react/__mocks__/ionicons/icons/index.js diff --git a/react/__mocks__/ionicons/index.js b/react/__mocks__/ionicons/index.js new file mode 100644 index 0000000000..6da7decf39 --- /dev/null +++ b/react/__mocks__/ionicons/index.js @@ -0,0 +1,2 @@ + +exports.addIcons = () => {}; diff --git a/react/jest.setup.js b/react/jest.setup.js new file mode 100644 index 0000000000..23b59855ee --- /dev/null +++ b/react/jest.setup.js @@ -0,0 +1 @@ +global.crypto = require('@trust/webcrypto'); diff --git a/react/package.json b/react/package.json index bfabb5b310..29118a807d 100644 --- a/react/package.json +++ b/react/package.json @@ -24,7 +24,8 @@ "clean": "rm -rf dist", "compile": "npm run tsc", "deploy": "np --any-branch", - "tsc": "tsc -p ." + "tsc": "tsc -p .", + "test": "jest" }, "main": "./dist/index.js", "module": "./dist/index.js", @@ -33,25 +34,39 @@ "dist/" ], "devDependencies": { + "@trust/webcrypto": "^0.9.2", "@types/jest": "23.3.9", "@types/node": "10.12.9", "@types/react": "16.7.6", "@types/react-dom": "16.0.9", - "@types/react-router": "^4.4.3", + "@types/react-router": "^4.4.4", + "@types/react-router-dom": "^4.3.1", + "jest": "^23.0.0", + "jest-dom": "^3.0.2", + "np": "^3.1.0", "react": "^16.7.0", "react-dom": "^16.7.0", "react-router": "^4.3.1", "react-router-dom": "^4.3.1", - "typescript": "^3.2.2", - "np": "^3.1.0" + "react-testing-library": "^5.5.3", + "ts-jest": "^23.10.5", + "typescript": "^3.2.2" }, "dependencies": { - "@ionic/core": "4.0.1" + "@ionic/core": "^4.0.2" }, "peerDependencies": { "react": "^16.7.0", "react-dom": "^16.7.0", "react-router": "^4.3.1", "react-router-dom": "^4.3.1" + }, + "jest": { + "preset": "ts-jest", + "setupTestFrameworkScriptFile": "/jest.setup.js", + "testPathIgnorePatterns": [ + "node_modules", + "dist" + ] } } diff --git a/react/src/components/IonLoading.tsx b/react/src/components/IonLoading.tsx index ffd20f0b21..66c4d2379a 100644 --- a/react/src/components/IonLoading.tsx +++ b/react/src/components/IonLoading.tsx @@ -3,5 +3,5 @@ import { createControllerComponent } from './createControllerComponent'; export type LoadingOptions = Components.IonLoadingAttributes; -const IonActionSheet = createControllerComponent('ion-loading', 'ion-loading-controller') -export default IonActionSheet; +const IonLoading = createControllerComponent('ion-loading', 'ion-loading-controller') +export default IonLoading; diff --git a/react/src/components/IonPage.tsx b/react/src/components/IonPage.tsx new file mode 100644 index 0000000000..38b27b41a9 --- /dev/null +++ b/react/src/components/IonPage.tsx @@ -0,0 +1,29 @@ +import React from 'react'; + + +type Props = React.DetailedHTMLProps, HTMLDivElement>; + +type InternalProps = Props & { + forwardedRef?: React.RefObject +}; + +type ExternalProps = Props & { + ref?: React.RefObject +}; + +const IonPage: React.SFC = ({ children, forwardedRef, className, ...props }) => ( +
+ {children} +
+); + +function forwardRef(props: InternalProps, ref: React.RefObject) { + return ; +} +forwardRef.displayName = 'IonPage'; + +export default React.forwardRef(forwardRef); diff --git a/react/src/components/__tests__/IonRouterOutlet.tsx b/react/src/components/__tests__/IonRouterOutlet.tsx new file mode 100644 index 0000000000..dfa5bc9c62 --- /dev/null +++ b/react/src/components/__tests__/IonRouterOutlet.tsx @@ -0,0 +1,40 @@ +import React, { SFC, ReactElement } from 'react'; +import { Route, Router } from 'react-router-dom'; +import { createMemoryHistory } from 'history'; +import { render, cleanup } from 'react-testing-library'; +import { IonRouterOutlet } from '../navigation/IonRouterOutlet'; + +afterEach(cleanup) + +function createReactPage(text: string) { + const ReactPage: SFC = () => {text}; + return ReactPage; +} + +function renderWithRouter( + ui: ReactElement, + { + route = '/', + history = createMemoryHistory({ initialEntries: [route] }), + } = {} +) { + return { + ...render({ui}), + history + } +} + +test('landing on a bad page', () => { + const { container } = renderWithRouter( + + + + + + + , { + route: '/schedule', + }); + + expect(container.innerHTML).toContain('
Schedule Home
'); +}) diff --git a/react/src/components/__tests__/createComponent.tsx b/react/src/components/__tests__/createComponent.tsx new file mode 100644 index 0000000000..9b80383074 --- /dev/null +++ b/react/src/components/__tests__/createComponent.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Components } from '@ionic/core' +import { createReactComponent } from '../createComponent'; +import { render, fireEvent, cleanup } from 'react-testing-library'; + +afterEach(cleanup); + +describe('createComponent - events', () => { + test('should set events on handler', () => { + const FakeOnClick = jest.fn((e) => e); + const IonButton = createReactComponent('ion-button'); + + const { getByText } = render( + + ButtonNameA + + ); + fireEvent.click(getByText('ButtonNameA')); + expect(FakeOnClick).toBeCalledTimes(1); + }); + + test('should add custom events', () => { + const FakeIonFocus = jest.fn((e) => e); + const IonInput = createReactComponent('ion-input'); + + const { getByText } = render( + + ButtonNameA + + ); + const ionInputItem = getByText('ButtonNameA'); + expect(Object.keys((ionInputItem as any).__events)).toEqual(['ionFocus']); + }); +}); + +describe('createComponent - ref', () => { + test('should pass ref on to web component instance', () => { + const ionButtonRef: React.RefObject = React.createRef(); + const IonButton = createReactComponent('ion-button'); + + const { getByText } = render( + + ButtonNameA + + ) + const ionButtonItem = getByText('ButtonNameA'); + expect(ionButtonRef.current).toEqual(ionButtonItem); + }); +}); diff --git a/react/src/components/__tests__/createControllerComponent.tsx b/react/src/components/__tests__/createControllerComponent.tsx new file mode 100644 index 0000000000..2d30934d8f --- /dev/null +++ b/react/src/components/__tests__/createControllerComponent.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import { Components } from '@ionic/core'; +import { createControllerComponent } from '../createControllerComponent'; +import { render, waitForElement, wait } from 'react-testing-library'; +import * as utils from '../utils'; +import { createControllerUtils } from '../utils/controller-test-utils'; +import 'jest-dom/extend-expect'; + +describe('createControllerComponent - events', () => { + const { cleanupAfterController, createControllerElement, augmentController} = createControllerUtils('ion-loading'); + type LoadingOptions = Components.IonLoadingAttributes; + const IonLoading = createControllerComponent('ion-loading', 'ion-loading-controller') + + afterEach(cleanupAfterController); + + test('should create controller component outside of the react component', async () => { + const { container, baseElement } = render( + <> + + + ButtonNameA + + ); + expect(container).toContainHTML('
ButtonNameA
'); + expect(baseElement.querySelector('ion-loading-controller')).toBeInTheDocument(); + }); + + test('should create component and attach props on opening', async () => { + const onDidDismiss = jest.fn(); + const { baseElement, container, rerender } = render( + + ButtonNameA + + ); + + const [element, presentFunction] = createControllerElement(); + const loadingController = augmentController(baseElement, container, element); + + const attachEventPropsSpy = jest.spyOn(utils, "attachEventProps"); + + rerender( + + ButtonNameA + + ); + + await waitForElement(() => container.querySelector('ion-loading')); + + expect((loadingController as any).create).toHaveBeenCalledWith({ + duration: 2000, + children: 'ButtonNameA', + onIonLoadingDidDismiss: onDidDismiss + }); + expect(presentFunction).toHaveBeenCalled(); + expect(attachEventPropsSpy).toHaveBeenCalledWith(element, { + duration: 2000, + children: 'ButtonNameA', + onIonLoadingDidDismiss: onDidDismiss + }); + }); + + test('should dismiss component on hiding', async () => { + const { container, baseElement, rerender } = render( + + ButtonNameA + + ); + + const [element, , dismissFunction] = createControllerElement(); + augmentController(baseElement, container, element); + + rerender( + + ButtonNameA + + ); + + await waitForElement(() => container.querySelector('ion-loading')); + + rerender( + + ButtonNameA + + ); + + await wait(() => { + const item = container.querySelector('ion-loading'); + if (item) { + throw new Error(); + } + }); + + expect(dismissFunction).toHaveBeenCalled(); + }); + +}); diff --git a/react/src/components/__tests__/createOverlayComponent.tsx b/react/src/components/__tests__/createOverlayComponent.tsx new file mode 100644 index 0000000000..11247e6c63 --- /dev/null +++ b/react/src/components/__tests__/createOverlayComponent.tsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { Components } from '@ionic/core' +import { createOverlayComponent } from '../createOverlayComponent'; +import { render, waitForElement } from 'react-testing-library'; +import * as utils from '../utils'; +import { createControllerUtils } from '../utils/controller-test-utils'; +import 'jest-dom/extend-expect'; + +describe('createOverlayComponent - events', () => { + const { cleanupAfterController, createControllerElement, augmentController} = createControllerUtils('ion-action-sheet'); + type ActionSheetOptions = Components.IonActionSheetAttributes; + const IonActionSheet = createOverlayComponent('ion-action-sheet', 'ion-action-sheet-controller'); + + afterEach(cleanupAfterController); + + test('should set events on handler', async () => { + const onDismiss = jest.fn(); + const { baseElement, container } = render( + <> + + ButtonNameA + + ButtonNameA + + ); + expect(container).toContainHTML('
ButtonNameA
'); + expect(baseElement.querySelector('ion-action-sheet-controller')).toBeInTheDocument(); + }); + + test('should create component and attach props on opening', async () => { + const onDidDismiss = jest.fn(); + const { baseElement, rerender, container } = render( + + ButtonNameA + + ); + + const [element, presentFunction] = createControllerElement(); + const actionSheetController = augmentController(baseElement, container, element); + + const attachEventPropsSpy = jest.spyOn(utils, "attachEventProps"); + + rerender( + + ButtonNameA + + ); + + await waitForElement(() => container.querySelector('ion-action-sheet')); + + expect((actionSheetController as any).create).toHaveBeenCalled(); + expect(presentFunction).toHaveBeenCalled(); + expect(attachEventPropsSpy).toHaveBeenCalledWith(element, { + buttons: [], + onIonActionSheetDidDismiss: onDidDismiss + }); + }); + + test('should dismiss component on hiding', async () => { + const [element, , dismissFunction] = createControllerElement(); + + const { baseElement, rerender, container } = render( + + ButtonNameA + + ); + + augmentController(baseElement, container, element); + + rerender( + + ButtonNameA + + ); + + await waitForElement(() => container.querySelector('ion-action-sheet')); + + rerender( + + ButtonNameA + + ); + + expect(dismissFunction).toHaveBeenCalled(); + }); +}); diff --git a/react/src/components/__tests__/utils.ts b/react/src/components/__tests__/utils.ts new file mode 100644 index 0000000000..cb3f3e88de --- /dev/null +++ b/react/src/components/__tests__/utils.ts @@ -0,0 +1,88 @@ +import * as utils from '../utils'; +import 'jest-dom/extend-expect' + +describe('isCoveredByReact', () => { + it('should identify standard events as covered by React', () => { + expect(utils.isCoveredByReact('click', document)).toEqual(true); + }); + it('should identify custom events as not covered by React', () => { + expect(utils.isCoveredByReact('change', document)).toEqual(true); + expect(utils.isCoveredByReact('ionchange', document)).toEqual(false); + }); +}); + +describe('syncEvent', () => { + it('should add event on sync and readd on additional syncs', () => { + var div = document.createElement("div"); + const addEventListener = jest.spyOn(div, "addEventListener"); + const removeEventListener = jest.spyOn(div, "removeEventListener"); + const ionClickCallback = jest.fn(); + + utils.syncEvent(div, 'ionClick', ionClickCallback); + expect(removeEventListener).not.toHaveBeenCalled(); + expect(addEventListener).toHaveBeenCalledWith('ionClick', expect.any(Function)); + + utils.syncEvent(div, 'ionClick', ionClickCallback); + expect(removeEventListener).toHaveBeenCalledWith('ionClick', expect.any(Function)); + expect(addEventListener).toHaveBeenCalledWith('ionClick', expect.any(Function)); + + const event = new CustomEvent('ionClick', { detail: 'test'}); + div.dispatchEvent(event); + expect(ionClickCallback).toHaveBeenCalled(); + }) +}); + +describe('ensureElementInBody', () => { + it('should return if exists', () => { + const element = document.createElement("some-random-thing"); + document.body.innerHTML = ''; + document.body.appendChild(element); + + const returnedElement = utils.ensureElementInBody('some-random-thing'); + expect(returnedElement).toEqual(element); + expect(document.body.children.length).toEqual(1); + }); + + it('should create if it does not exist', () => { + document.body.innerHTML = ''; + + const returnedElement = utils.ensureElementInBody('some-random-thing'); + expect(returnedElement).toBeDefined(); + expect(returnedElement.tagName).toEqual('SOME-RANDOM-THING'); + expect(document.body.children.length).toEqual(1); + }); +}); + +describe('attachEventProps', () => { + it('should pass props to a dom node', () => { + const onIonClickCallback = () => {}; + + var div = document.createElement("div"); + utils.attachEventProps(div, { + 'children': [], + 'style': 'color: red', + 'ref': () => {}, + 'onClick': () => {}, + 'onIonClick': onIonClickCallback, + 'testprop': ['red'] + }); + + expect((div as any).testprop).toEqual(['red']); + expect(div).toHaveStyle(''); + expect(Object.keys((div as any).__events)).toEqual(['ionClick']); + }); + +}); + +describe('generateUniqueId', () => { + const uniqueRegexMatch = /^(\w){8}-(\w){4}-(\w){4}-(\w){4}-(\w){12}$/; + + it('should generate a global unique id', () => { + const first = utils.generateUniqueId(); + const second = utils.generateUniqueId(); + + expect(first).toMatch(uniqueRegexMatch); + expect(second).not.toEqual(first); + expect(second).toMatch(uniqueRegexMatch); + }); +}); diff --git a/react/src/components/createComponent.tsx b/react/src/components/createComponent.tsx index 578482a26c..ee863daf8f 100644 --- a/react/src/components/createComponent.tsx +++ b/react/src/components/createComponent.tsx @@ -33,11 +33,7 @@ export function createReactComponent(tagName: string) { } componentWillReceiveProps(props: InternalProps) { - const node = ReactDOM.findDOMNode(this); - - if (!(node instanceof HTMLElement)) { - return; - } + const node = ReactDOM.findDOMNode(this) as HTMLElement; attachEventProps(node, props); } diff --git a/react/src/components/createControllerComponent.tsx b/react/src/components/createControllerComponent.tsx index e5bf03c37e..b2a46834d6 100644 --- a/react/src/components/createControllerComponent.tsx +++ b/react/src/components/createControllerComponent.tsx @@ -1,43 +1,55 @@ import React from 'react'; -import { attachEventProps } from './utils' -import { ensureElementInBody, dashToPascalCase } from './utils'; +import { OverlayEventDetail } from '@ionic/core'; +import { attachEventProps, ensureElementInBody, dashToPascalCase, generateUniqueId } from './utils' import { OverlayComponentElement, OverlayControllerComponentElement } from '../types'; export function createControllerComponent>(tagName: string, controllerTagName: string) { const displayName = dashToPascalCase(tagName); + const dismissEventName = `on${displayName}DidDismiss`; type ReactProps = { - show: boolean; + isOpen: boolean; + onDidDismiss: (event: CustomEvent) => void; } type Props = T & ReactProps; return class ReactControllerComponent extends React.Component { element: E; controllerElement: C; + id: string; constructor(props: Props) { super(props); + + this.id = generateUniqueId(); } static get displayName() { return displayName; } - async componentDidMount() { + componentDidMount() { this.controllerElement = ensureElementInBody(controllerTagName); - await this.controllerElement.componentOnReady(); } async componentDidUpdate(prevProps: Props) { - if (prevProps.show !== this.props.show && this.props.show === true) { - const { show, ...cProps} = this.props; + if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) { + const { isOpen, onDidDismiss, ...cProps} = this.props; + const elementProps = { + ...cProps, + [dismissEventName]: onDidDismiss + }; + + if (this.controllerElement.componentOnReady) { + await this.controllerElement.componentOnReady(); + } + + this.element = await this.controllerElement.create(elementProps); + attachEventProps(this.element, elementProps); - this.element = await this.controllerElement.create(cProps); await this.element.present(); - - attachEventProps(this.element, cProps); } - if (prevProps.show !== this.props.show && this.props.show === false) { + if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) { await this.element.dismiss(); } } diff --git a/react/src/components/createOverlayComponent.tsx b/react/src/components/createOverlayComponent.tsx index b25e154c07..e3a3177722 100644 --- a/react/src/components/createOverlayComponent.tsx +++ b/react/src/components/createOverlayComponent.tsx @@ -1,15 +1,18 @@ import React from 'react'; import ReactDOM from 'react-dom'; +import { OverlayEventDetail } from '@ionic/core'; import { attachEventProps } from './utils' import { ensureElementInBody, dashToPascalCase } from './utils'; import { OverlayComponentElement, OverlayControllerComponentElement } from '../types'; export function createOverlayComponent>(tagName: string, controllerTagName: string) { const displayName = dashToPascalCase(tagName); + const dismissEventName = `on${displayName}DidDismiss`; type ReactProps = { children: React.ReactNode; - show: boolean; + isOpen: boolean; + onDidDismiss: (event: CustomEvent) => void; } type Props = T & ReactProps; @@ -28,25 +31,32 @@ export function createOverlayComponent(controllerTagName); - await this.controllerElement.componentOnReady(); } async componentDidUpdate(prevProps: Props) { - if (prevProps.show !== this.props.show && this.props.show === true) { - const { children, show, ...cProps} = this.props; + if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) { + const { children, isOpen, onDidDismiss, ...cProps} = this.props; + const elementProps = { + ...cProps, + [dismissEventName]: onDidDismiss + } + + if (this.controllerElement.componentOnReady) { + await this.controllerElement.componentOnReady(); + } this.element = await this.controllerElement.create({ - ...cProps, + ...elementProps, component: this.el, componentProps: {} }); - await this.element.present(); + attachEventProps(this.element, elementProps); - attachEventProps(this.element, cProps); + await this.element.present(); } - if (prevProps.show !== this.props.show && this.props.show === false) { + if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) { await this.element.dismiss(); } } diff --git a/react/src/components/index.ts b/react/src/components/index.ts index 3674ad0d3c..a07ad68767 100644 --- a/react/src/components/index.ts +++ b/react/src/components/index.ts @@ -1,25 +1,39 @@ +import { addIcons } from 'ionicons'; +import { ICON_PATHS } from 'ionicons/icons'; +import { defineCustomElements } from '@ionic/core/loader'; import { Components as IoniconsComponents } from 'ionicons'; import { Components } from '@ionic/core'; import { createReactComponent } from './createComponent'; - export { AlertButton, AlertInput } from '@ionic/core'; -export { default as IonActionSheet } from './IonActionSheet'; +// createControllerComponent export { default as IonAlert } from './IonAlert'; export { default as IonLoading } from './IonLoading'; +export { default as IonToast } from './IonToast'; + +// createOverlayComponent +export { default as IonActionSheet } from './IonActionSheet'; export { default as IonModal } from './IonModal'; export { default as IonPopover } from './IonPopover'; -export { default as IonToast } from './IonToast'; + +// Custom Components +export { default as IonPage } from './IonPage'; export { default as IonTabs } from './navigation/IonTabs'; export { default as IonTabBar } from './navigation/IonTabBar'; export { IonRouterOutlet, IonBackButton } from './navigation/IonRouterOutlet'; +addIcons(ICON_PATHS); +defineCustomElements(window); + +// ionicons +export const IonIcon = createReactComponent('ion-icon'); + +// createReactComponent export const IonTabBarInner = createReactComponent('ion-tab-bar'); export const IonRouterOutletInner = createReactComponent('ion-router-outlet'); export const IonBackButtonInner = createReactComponent('ion-back-button'); export const IonTab = createReactComponent('ion-tab'); export const IonTabButton = createReactComponent('ion-tab-button'); - export const IonAnchor = createReactComponent('ion-anchor'); export const IonApp = createReactComponent('ion-app'); export const IonAvatar = createReactComponent('ion-avatar'); @@ -43,7 +57,6 @@ export const IonFabList = createReactComponent('ion-footer'); export const IonGrid = createReactComponent('ion-grid'); export const IonHeader = createReactComponent('ion-header'); -export const IonIcon = createReactComponent('ion-icon'); export const IonImg = createReactComponent('ion-img'); export const IonInfiniteScroll = createReactComponent('ion-infinite-scroll'); export const IonInput = createReactComponent('ion-input'); diff --git a/react/src/components/navigation/IonRouterOutlet.tsx b/react/src/components/navigation/IonRouterOutlet.tsx index 4dd0b4b660..c51823b7c4 100644 --- a/react/src/components/navigation/IonRouterOutlet.tsx +++ b/react/src/components/navigation/IonRouterOutlet.tsx @@ -4,6 +4,7 @@ import { Components } from '@ionic/core'; import { generateUniqueId } from '../utils'; import { Location } from 'history'; import { IonBackButtonInner, IonRouterOutletInner } from '../index'; +import IonPage from '../IonPage'; type ChildProps = RouteProps & { computedMatch: match @@ -190,27 +191,27 @@ class RouterOutlet extends Component { props = { 'ref': this.leavingEl, 'hidden': this.state.direction == null, - 'className': 'ion-page' + (this.state.direction == null ? ' ion-page-hidden' : '') + 'className': (this.state.direction == null ? ' ion-page-hidden' : '') }; } else if (item.id === this.state.activeId) { props = { 'ref': this.enteringEl, - 'className': 'ion-page' + (this.state.direction != null ? ' ion-page-invisible' : '') + 'className': (this.state.direction != null ? ' ion-page-invisible' : '') }; } else { props = { 'aria-hidden': true, - 'className': 'ion-page ion-page-hidden' + 'className': 'ion-page-hidden' }; } return ( -
{ this.renderChild(item) } -
+
); })} diff --git a/react/src/components/utils/controller-test-utils.ts b/react/src/components/utils/controller-test-utils.ts new file mode 100644 index 0000000000..6fb03e2de6 --- /dev/null +++ b/react/src/components/utils/controller-test-utils.ts @@ -0,0 +1,53 @@ +import { cleanup } from 'react-testing-library'; + +export function createControllerUtils(tagName: string) { + const elementTag = tagName; + const controllerTag = `${tagName}-controller`; + + function cleanupAfterController() { + const controller = document.querySelector(controllerTag); + if (controller) { + controller.remove(); + } + const element = document.querySelector(elementTag); + if (element) { + element.remove(); + } + cleanup(); + } + + function createControllerElement(): [HTMLElement, jest.Mock, jest.Mock] { + const element = document.createElement(elementTag); + const presentFunction = jest.fn(() => { + element.setAttribute('active', 'true'); + return Promise.resolve(true) + }); + const dismissFunction = jest.fn(() => { + element.remove(); + Promise.resolve(true) + }); + (element as any).present = presentFunction; + (element as any).dismiss = dismissFunction; + + return [element, presentFunction, dismissFunction]; + } + + function augmentController(baseElement: HTMLElement, container: HTMLElement, childElement: HTMLElement): HTMLElement { + const controller: HTMLElement = baseElement.querySelector(controllerTag); + (controller as any).componentOnReady = jest.fn(async () => { + return true; + }); + (controller as any).create = jest.fn(async () => { + container.append(childElement); + return childElement; + }); + + return controller; + } + + return { + cleanupAfterController, + createControllerElement, + augmentController + }; +} diff --git a/react/src/components/utils.ts b/react/src/components/utils/index.ts similarity index 79% rename from react/src/components/utils.ts rename to react/src/components/utils/index.ts index 066428948c..40078c644c 100644 --- a/react/src/components/utils.ts +++ b/react/src/components/utils/index.ts @@ -2,12 +2,12 @@ * Checks if an event is supported in the current execution environment. * @license Modernizr 3.0.0pre (Custom Build) | MIT */ -function isCoveredByReact(eventNameSuffix: string) { +export function isCoveredByReact(eventNameSuffix: string, doc: Document = document) { const eventName = 'on' + eventNameSuffix; - let isSupported = eventName in document; + let isSupported = eventName in doc; if (!isSupported) { - const element = document.createElement('div'); + const element = doc.createElement('div'); element.setAttribute(eventName, 'return;'); isSupported = typeof (element)[eventName] === 'function'; } @@ -15,7 +15,7 @@ function isCoveredByReact(eventNameSuffix: string) { return isSupported; } -function syncEvent(node: Element, eventName: string, newEventHandler: (e: Event) => any) { +export function syncEvent(node: Element, eventName: string, newEventHandler: (e: Event) => any) { const eventStore = (node as any).__events || ((node as any).__events = {}); const oldEventHandler = eventStore[eventName]; @@ -25,11 +25,9 @@ function syncEvent(node: Element, eventName: string, newEventHandler: (e: Event) } // Bind new listener. - if (newEventHandler) { - node.addEventListener(eventName, eventStore[eventName] = function handler(e: Event) { - newEventHandler.call(this, e); - }); - } + node.addEventListener(eventName, eventStore[eventName] = function handler(e: Event) { + newEventHandler.call(this, e); + }); } export const dashToPascalCase = (str: string) => str.toLowerCase().split('-').map(segment => segment.charAt(0).toUpperCase() + segment.slice(1)).join(''); @@ -65,7 +63,7 @@ export function attachEventProps(node: E, props: any) { export function generateUniqueId() { return ([1e7].toString() + -1e3.toString() + -4e3.toString() + -8e3.toString() + -1e11.toString()).replace(/[018]/g, function(c: any) { - const random = window.crypto.getRandomValues(new Uint8Array(1)) as Uint8Array; + const random = crypto.getRandomValues(new Uint8Array(1)) as Uint8Array; return (c ^ random[0] & 15 >> c / 4).toString(16); }); } diff --git a/react/src/index.ts b/react/src/index.ts index 0b01f9489d..35974dcfd9 100644 --- a/react/src/index.ts +++ b/react/src/index.ts @@ -2,4 +2,4 @@ export * from './components'; export * from './types'; -export * from './register'; +export { setupConfig } from '@ionic/core'; diff --git a/react/src/register.ts b/react/src/register.ts deleted file mode 100644 index ac5aaec443..0000000000 --- a/react/src/register.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { addIcons } from 'ionicons'; -import { ICON_PATHS } from 'ionicons/icons'; -import { IonicConfig } from '@ionic/core'; -import { defineCustomElements } from '@ionic/core/loader'; -import { IonicWindow } from './types'; - -export function registerIonic(config: IonicConfig = {}) { - const win: IonicWindow = window as any; - const Ionic = (win.Ionic = win.Ionic || {}); - addIcons(ICON_PATHS); - - Ionic.config = config; - defineCustomElements(window); -} diff --git a/react/tsconfig.json b/react/tsconfig.json index 2e4c6de552..b2bc88b558 100644 --- a/react/tsconfig.json +++ b/react/tsconfig.json @@ -5,6 +5,7 @@ "declaration": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, + "esModuleInterop": true, "lib": ["dom", "es2015"], "module": "es2015", "moduleResolution": "node", @@ -22,6 +23,9 @@ "src/**/*.ts", "src/**/*.tsx" ], + "exclude": [ + "**/__tests__/**" + ], "compileOnSave": false, "buildOnSave": false }