From a2a95af57a2acafba8bfe46199c02e37ac5ee14a Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Fri, 3 Apr 2020 11:33:31 +0100 Subject: [PATCH 01/15] Dojo Test Renderer --- docs/en/testing/introduction.md | 58 +- docs/en/testing/supplemental.md | 725 ++++++++++-------- src/core/vdom.ts | 4 + src/testing/assertRender.ts | 138 ++++ src/testing/assertionTemplate.ts | 250 ++++-- src/testing/decorate.ts | 142 ++++ src/testing/harness/assertionTemplate.ts | 207 +++++ src/testing/{ => harness}/harness.ts | 16 +- .../{ => harness}/support/assertRender.ts | 12 +- src/testing/{ => harness}/support/selector.ts | 6 +- src/testing/interfaces.d.ts | 22 + src/testing/renderer.ts | 295 +++++++ tests/core/unit/vdom.tsx | 1 - tests/routing/unit/ActiveLink.ts | 64 +- tests/routing/unit/Link.ts | 75 +- tests/routing/unit/Route.ts | 19 +- tests/testing/unit/all.ts | 8 +- tests/testing/unit/assertRender.tsx | 158 ++++ tests/testing/unit/assertionTemplate.tsx | 282 +++---- tests/testing/unit/harness/all.ts | 4 + .../unit/harness/assertionTemplate.tsx | 363 +++++++++ tests/testing/unit/{ => harness}/harness.tsx | 14 +- .../unit/{ => harness}/harnessWithTsx.tsx | 6 +- .../testing/unit/{ => harness}/support/all.ts | 0 .../{ => harness}/support/assertRender.ts | 10 +- .../unit/{ => harness}/support/selector.ts | 8 +- .../unit/mocks/middleware/breakpoint.tsx | 55 +- tests/testing/unit/mocks/middleware/focus.tsx | 9 +- .../testing/unit/mocks/middleware/icache.tsx | 9 +- .../unit/mocks/middleware/intersection.tsx | 11 +- tests/testing/unit/mocks/middleware/node.tsx | 21 +- .../testing/unit/mocks/middleware/resize.tsx | 55 +- tests/testing/unit/mocks/middleware/store.tsx | 45 +- .../unit/mocks/middleware/validity.tsx | 15 +- tests/testing/unit/renderer.tsx | 545 +++++++++++++ 35 files changed, 2859 insertions(+), 793 deletions(-) create mode 100644 src/testing/assertRender.ts create mode 100644 src/testing/decorate.ts create mode 100644 src/testing/harness/assertionTemplate.ts rename src/testing/{ => harness}/harness.ts (96%) rename src/testing/{ => harness}/support/assertRender.ts (95%) rename src/testing/{ => harness}/support/selector.ts (96%) create mode 100644 src/testing/interfaces.d.ts create mode 100644 src/testing/renderer.ts create mode 100644 tests/testing/unit/assertRender.tsx create mode 100644 tests/testing/unit/harness/all.ts create mode 100644 tests/testing/unit/harness/assertionTemplate.tsx rename tests/testing/unit/{ => harness}/harness.tsx (98%) rename tests/testing/unit/{ => harness}/harnessWithTsx.tsx (97%) rename tests/testing/unit/{ => harness}/support/all.ts (100%) rename tests/testing/unit/{ => harness}/support/assertRender.ts (93%) rename tests/testing/unit/{ => harness}/support/selector.ts (94%) create mode 100644 tests/testing/unit/renderer.tsx diff --git a/docs/en/testing/introduction.md b/docs/en/testing/introduction.md index efba82418..41e75f34d 100644 --- a/docs/en/testing/introduction.md +++ b/docs/en/testing/introduction.md @@ -41,7 +41,7 @@ dojo test --functional --config local ## Writing unit tests -- Using Dojo's [`harness` API](/learn/testing/dojo-test-harness#harness-api) for unit testing widgets. +- Using Dojo's [test `renderer` API](/learn/testing/test-renderer) for unit testing widgets. > src/widgets/Home.tsx @@ -63,8 +63,7 @@ export default Home; ```ts const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import harness from '@dojo/framework/testing/harness'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import renderer, { assertionTemplate } from '@dojo/framework/testing/renderer'; import Home from '../../../src/widgets/Home'; import * as css from '../../../src/widgets/Home.m.css'; @@ -73,13 +72,13 @@ const baseTemplate = assertionTemplate(() =>

Home Page< describe('Home', () => { it('default renders correctly', () => { - const h = harness(() => ); - h.expect(baseTemplate); + const r = renderer(() => ); + r.expect(baseTemplate); }); }); ``` -The `harness` API allows you to verify that the output of a rendered widget is what you expect. +The `renderer` API allows you to verify that the output of a rendered widget is what you expect. - Does it render as expected? - Do event handlers work as expected? @@ -149,53 +148,66 @@ export default Profile; > tests/unit/widgets/Profile.tsx -```ts +```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; -import harness from '@dojo/framework/testing/harness'; +import renderer from '@dojo/framework/testing/renderer'; import Profile from '../../../src/widgets/Profile'; import * as css from '../../../src/widgets/Profile.m.css'; // Create an assertion -const profileAssertion = assertionTemplate(() => ( -

- Welcome Stranger! -

-)); +const profileAssertion = assertionTemplate(() =>

Welcome Stranger!

); describe('Profile', () => { it('default renders correctly', () => { - const h = harness(() => ); + const r = renderer(() => ); // Test against my base assertion - h.expect(profileAssertion); + r.expect(profileAssertion); }); }); ``` -A value can be provided to any virtual DOM node under test using `assertion-key` properties defined in the assertion template. Note: when `v()` and `w()` from `@dojo/framework/core/vdom` are used, the `~key` property serves the same purpose. +To work with assertion templates, wrapped nodes can get created using `@dojo/framework/testing/renderer#wrap` in order to use the assertion template API. Note: when using wrapped `VNode`s with `v()`, the `.tag` property needs to get used, for example `v(WrappedDiv.tag, {} [])`. > tests/unit/widgets/Profile.tsx -```ts +```tsx +const { describe, it } = intern.getInterface('bdd'); +import { tsx } from '@dojo/framework/core/vdom'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import renderer { wrap } from '@dojo/framework/testing/renderer'; + +import Profile from '../../../src/widgets/Profile'; +import * as css from '../../../src/widgets/Profile.m.css'; + +// Create a wrapped test node +const WrappedHeader = wrap('h1'); + +// Create an assertion +const profileAssertion = assertionTemplate(() => ( + // Use the wrapped node in place of the normal node + Welcome Stranger! +)); + describe('Profile', () => { it('default renders correctly', () => { - const h = harness(() => ); + const r = renderer(() => ); // Test against my base assertion - h.expect(profileAssertion); + r.expect(profileAssertion); }); it('renders given username correctly', () => { // update the expected result with a given username - const namedAssertion = profileAssertion.setChildren('~welcome', () => ['Welcome Kel Varnsen!']); - const h = harness(() => ); - h.expect(namedAssertion); + const namedAssertion = profileAssertion.setChildren(WrappedHeader, () => ['Welcome Kel Varnsen!']); + const r = renderer(() => ); + r.expect(namedAssertion); }); }); ``` -Using the `setChildren` method of an assertion template with the assigned `assertion-key` value, ~welcome in this case, will return an assertion template with the updated virtual DOM structure. This resulting assertion template can then be used to test widget output. +Using the `setChildren` method of an assertion template with a wrapped testing node, `WrappedHeader` in this case, will return an assertion template with the updated virtual DOM structure. This resulting assertion template can then be used to test widget output. [dojo cli]: https://github.com/dojo/cli [intern]: https://theintern.io/ diff --git a/docs/en/testing/supplemental.md b/docs/en/testing/supplemental.md index f78e4676a..7b0f73c65 100644 --- a/docs/en/testing/supplemental.md +++ b/docs/en/testing/supplemental.md @@ -1,378 +1,452 @@ -# Dojo test harness +# Test Renderer -`harness()` is the primary API when working with `@dojo/framework/testing`, essentially setting up each test and providing a context to perform virtual DOM assertions and interactions. The harness is designed to mirror the core behavior for widgets when updating `properties` or `children` and widget invalidation, with no special or custom logic required. +Dojo provides a simple and type safe test renderer for shallowly asserting the expected output and behavior from a widget. The test renderer's API has been designed to encourage unit testing best practices from the outset to ensure high confidence in you Dojo application. -## Harness API +Working with [assertion templates](/missing/link) and the test renderer is done using [wrapped test nodes](/missing/link) that are defined in the assertion templates structure, ensuring type safety throughout the testing life-cycle. -```ts -interface HarnessOptions { - customComparators?: CustomComparator[]; - middleware?: [MiddlewareResultFactory, MiddlewareResultFactory][]; -} +The expected structure of a widget is defined using an assertionTemplate and passed to the test renderer's [`.expect()`](/missing/link) function which executes the assertion. + +> src/MyWidget.spec.tsx + +```tsx +import { tsx } from '@dojo/framework/core/vdom'; +import renderer, { assertionTemplate } from '@dojo/framework/testing/renderer'; + +import MyWidget from './MyWidget'; + +const baseTemplate = assertionTemplate(() => ( +
+

Heading

+

Sub Heading

+
Content
+
+)); -harness(renderFunction: () => WNode, customComparators?: CustomComparator[]): Harness; -harness(renderFunction: () => WNode, options?: HarnessOptions): Harness; +const r = renderer(() => ); + +r.expect(baseTemplate); ``` -- `renderFunction`: A function that returns a `WNode` for the widget under test -- [`customComparators`](/learn/testing/dojo-test-harness#custom-comparators): Array of custom comparator descriptors. Each provides a comparator function to be used during the comparison for `properties` located using a `selector` and `property` name -- `options`: Expanded options for the harness which includes `customComparators` and an array of middleware/mocks tuples. +## The test renderer uses the location of a wrapped test node in the expected tree to attempt to perform the requested action (either `r.property()` or `r.child()`) on the actual output of the widget under test. If the wrapped test node does not match the corresponding node in the actual output tree then no action will be performed and the assertion will report a failure. + +In order for the test renderer and assertion templates to be able to identify nodes within the expected and actual node structure a special wrapping node must be used. The wrapped nodes can get used in place of the real node in the expected assertion template structure, maintaining all the correct property and children typings. + +To create a wrapped test node use the `wrap` function from `@dojo/framework/testing/renderer`: -The harness returns a `Harness` object that provides a small API for interacting with the widget under test: +> src/MyWidget.spec.tsx -`Harness` +```tsx +import { wrap } from '@dojo/framework/testing/renderer'; -- [`expect`](/learn/testing/dojo-test-harness#harnessexpect): Performs an assertion against the full render output from the widget under test. -- [`expectPartial`](/learn/testing/dojo-test-harness#harnessexpectpartial): Performs an assertion against a section of the render output from the widget under test. -- [`trigger`](/learn/testing/dojo-test-harness#harnesstrigger): Used to trigger a function from a node on the widget under test's API -- [`getRender`](/learn/testing/dojo-test-harness#harnessgetRender): Returns a render from the harness based on the index provided +import MyWidget from './MyWidget'; -Setting up a widget for testing is simple and familiar using the `w()` function from `@dojo/framework/core` or by returning TSX from the render function: +// Create a wrapped node for a widget +const WrappedMyWidget = wrap(MyWidget); -> tests/unit/widgets/MyWidget.tsx +// Create a wrapped node for a vnode +const WrappedDiv = wrap('div'); +``` -```ts -const { describe, it } = intern.getInterface('bdd'); +The test renderer uses the location of a wrapped test node in the expected tree to attempt to perform the requested action (either `r.property()` or `r.child()`) on the actual output of the widget under test. If the wrapped test node does not match the corresponding node in the actual output tree then no action will be performed and the assertion will report a failure. + +## Assertion Templates + +Assertion templates get used to build the expected widget output structure to use with `renderer.expect()`. The templates expose a wide range of APIs that enable the expected output to vary between tests. + +Given a widget that renders output differently based on property values: + +> src/Profile.tsx + +```tsx import { create, tsx } from '@dojo/framework/core/vdom'; -import harness from '@dojo/framework/testing/harness'; -const factory = create().properties<{ foo: string }>(); +import * as css from './Profile.m.css'; + +export interface ProfileProperties { + username?: string; +} + +const factory = create().properties(); -const MyWidget = factory(function MyWidget({ properties, children }) { - const { foo } = properties(); - return
{children}
; +const Profile = factory(function Profile({ properties }) { + const { username = 'Stranger' } = properties(); + return

{`Welcome ${username}!`}

; }); -const h = harness(() => child); +export default Profile; ``` -The `renderFunction` is lazily executed so it can include additional logic to manipulate the widget's `properties` and `children` between assertions. +Create an assertion template using `@dojo/framework/testing/renderer#assertionTemplate`: -```ts -describe('MyWidget', () => { - it('renders with foo correctly', () => { - let foo = 'bar'; +> src/Profile.spec.tsx + +```tsx +const { describe, it } = intern.getInterface('bdd'); +import { tsx } from '@dojo/framework/core/vdom'; +import renderer, { assertionTemplate, wrap } from '@dojo/framework/testing/renderer'; + +import Profile from '../../../src/widgets/Profile'; +import * as css from '../../../src/widgets/Profile.m.css'; - const h = harness(() => child); +// Create a wrapped node +const WrappedHeader = wrap('h1'); - h.expect(/** assertion that includes bar **/); - // update the property that is passed to the widget - foo = 'foo'; - h.expect(/** assertion that includes foo **/); +// Create an assertion template using the `WrappedHeader` in place of the `h1` +const baseAssertion = assertionTemplate(() => Welcome Stranger!); + +describe('Profile', () => { + it('Should render using the default username', () => { + const r = renderer(() => ); + + // Test against the base assertion + h.expect(baseAssertion); }); }); ``` -## Mocking middleware +To test when a `username` property gets passed to the `Profile` widget, we could create a new assertion template with the updated expected username. However, as a widget increases its functionality, recreating the entire assertion template for each scenario becomes verbose and unmaintainable, as any changes to the common widget structure would require updating every assertion template. -When initializing the harness, mock middleware can be specified as part of the `HarnessOptions`. The mock middleware is defined as a tuple of the original middleware and the mock middleware implementation. Mock middleware is created in the same way as any other middleware. +To help avoid the maintenance overhead and reduce duplication, assertion templates offer a comprehensive API for creating variations from a base template. The assertion template API uses wrapped test nodes to identify the node in the expected structure to update. -```ts -import myMiddleware from './myMiddleware'; -import myMockMiddleware from './myMockMiddleware'; -import harness from '@dojo/framework/testing/harness'; +> src/Profile.spec.tsx -import MyWidget from './MyWidget'; +```tsx +const { describe, it } = intern.getInterface('bdd'); +import { tsx } from '@dojo/framework/core/vdom'; +import renderer, { assertionTemplate, wrap } from '@dojo/framework/testing/renderer'; -describe('MyWidget', () => { - it('renders', () => { - const h = harness(() => , { middleware: [[myMiddleware, myMockMiddleware]] }); - h.expect(/** assertion that executes the mock middleware instead of the normal middleware **/); +import Profile from '../../../src/widgets/Profile'; +import * as css from '../../../src/widgets/Profile.m.css'; + +// Create a wrapped node +const WrappedHeader = wrap('h1'); + +// Create an assertion template using the `WrappedHeader` in place of the `h1` +const baseAssertion = assertionTemplate(() => Welcome Stranger!); + +describe('Profile', () => { + it('Should render using the default username', () => { + const r = renderer(() => ); + + // Test against the base assertion + h.expect(baseAssertion); + }); + + it('Should render using the passed username', () => { + const r = renderer(() => ); + + // Create a variation of the base template + const usernameTemplate = baseAssertion.setChildren(WrappedHeader, () => ['Dojo']); + + // Test against the username template + h.expect(usernameTemplate); }); }); ``` -The harness automatically mocks a number of core middlewares that will be injected into any middleware that requires them: +Creating templates from a base template means that if there is a change to the default widget output, only a change to the baseTemplate is required to update all the widget's tests. -- `invalidator` -- `setProperty` -- `destroy` +### Assertion Template API -Additionally, there are a number of mock middleware available to support widgets that use the corresponding provided Dojo middleware. See the [mocking](/learn/testing/mocking#provided-middleware-mocks) section for more information on provided mock middleware. +#### `assertionTemplate.setChildren()` + +Returns a new assertion template with the new children are either pre-pended, appended or replaced depending on the `type` passed. -## Custom comparators +```tsx +.setChildren( + wrapped: Wrapped, + children: () => RenderResult, + type: 'prepend' | 'replace' | 'append' = 'replace' +);` +``` -There are circumstances where the exact value of a property is unknown during testing, so will require the use of a custom compare descriptor. +Convience functions exists for all 3 types, [`prepend()`](/missing/link), [`append()`](/missing/link) and [`replaceChildren()`](/missing/link). -The descriptors have a [`selector`](/learn/testing/dojo-test-harness#selectors) to locate the virtual nodes to check, a property name for the custom compare and a comparator function that receives the actual value and returns a boolean result for the assertion. +#### `assertionTemplate.append()` -```ts -const compareId = { - selector: '*', // all nodes - property: 'id', - comparator: (value: any) => typeof value === 'string' // checks the property value is a string -}; +Returns a new assertion template with the new children appended to the nodes existing children. -const h = harness(() => w(MyWidget, {}), [compareId]); +```tsx +.append(wrapped: Wrapped, children: () => RenderResult); ``` -For all assertions, using the returned `harness` API will now only test identified `id` properties using the `comparator` instead of the standard equality. +#### `assertionTemplate.prepend()` -## Selectors +Returns a new assertion template with the new children pre-pended to the nodes existing children. + +```tsx +.append(wrapped: Wrapped, children: () => RenderResult); +``` -The `harness` APIs commonly support a concept of CSS style selectors to target nodes within the virtual DOM for assertions and operations. Review the [full list of supported selectors](https://github.com/fb55/css-select#supported-selectors) for more information. +#### `assertionTemplate.replaceChildren()` -In addition to the standard API: +Returns a new assertion template with the new children replacing the nodes existing children. -- The `@` symbol is supported as shorthand for targeting a node's `key` property -- The `classes` property is used instead of `class` when using the standard shorthand `.` for targeting classes +```tsx +.append(wrapped: Wrapped, children: () => RenderResult); +``` -## `harness.expect` +#### `assertionTemplate.insertSiblings()` -The most common requirement for testing is to assert the structural output from a widget's `render` function. `expect` accepts a render function that returns the expected render output from the widget under test. +Returns a new assertion template with the passed children either inserted either `before` or `after` depending on the `type` passed. -```ts -expect(expectedRenderFunction: () => DNode | DNode[], actualRenderFunction?: () => DNode | DNode[]); +```tsx +.insertSiblings( + wrapped: Wrapped, + children: () => RenderResult, + type: 'before' | 'after' = 'before' +);` ``` -- `expectedRenderFunction`: A function that returns the expected `DNode` structure of the queried node -- `actualRenderFunction`: An optional function that returns the actual `DNode` structure to be asserted +#### `assertionTemplate.insertBefore()` -```ts -h.expect(() => -
- - text node - -
-); +Returns a new assertion template with the passed children inserted before the existing nodes children. + +```tsx +.insertBefore(wrapped: Wrapped, children: () => RenderResult); ``` -Optionally `expect` can accept a second parameter of a function that returns a render result to assert against. +#### `assertionTemplate.insertAfter()` -```ts -h.expect(() =>
, () =>
); +Returns a new assertion template with the passed children inserted after the existing nodes children. + +```tsx +.insertAfter(wrapped: Wrapped, children: () => RenderResult); ``` -If the actual render output and expected render output are different, an exception is thrown with a structured visualization indicating all differences with `(A)` (the actual value) and `(E)` (the expected value). +#### `assertionTemplate.replace()` -Example assertion failure output: +Returns a new assertion template replacing the existing node with the node that is passed. Note that if you need to interact with the new node in either assertion templates or the test renderer, it should be a wrapped test node. -```ts -v('div', { - 'classes': [ - 'root', -(A) 'other' -(E) 'another' - ], - 'onclick': 'function' -}, [ - v('span', { - 'classes': 'span', - 'id': 'random-id', - 'key': 'label', - 'onclick': 'function', - 'style': 'width: 100px' - }, [ - 'hello 0' - ]) - w(ChildWidget, { - 'id': 'random-id', - 'key': 'widget' - }) - w('registry-item', { - 'id': true, - 'key': 'registry' - }) -]) +```tsx +.replace(wrapped: Wrapped, node: DNode); ``` -## `harness.trigger` - -`harness.trigger()` calls a function with the `name` on the node targeted by the `selector`. +#### `assertionTemplate.remove()` -```ts -interface FunctionalSelector { - (node: VNode | WNode): undefined | Function; -} +Returns a new assertion template removing the target wrapped node completely. -trigger(selector: string, functionSelector: string | FunctionalSelector, ...args: any[]): any; +```tsx +.remove(wrapped: Wrapped); ``` -- `selector`: The selector query to find the node to target -- `functionSelector`: Either the name of the function to call from found node's properties or a functional selector that returns a function from a nodes properties. -- `args`: The arguments to call the located function with - -Returns the result of the function triggered if one is returned. +#### `assertionTemplate.setProperty()` -Example Usage(s): +Returns a new assertion template with the updated property for the target wrapped node. -```ts -// calls the `onclick` function on the first node with a key of `foo` -h.trigger('@foo', 'onclick'); +```tsx +.setProperty( + wrapped: Wrapped, + property: K, + value: T['properties'][K] ``` -```ts -// calls the `customFunction` function on the first node with a key of `bar` with an argument of `100` -// and receives the result of the triggered function -const result = h.trigger('@bar', 'customFunction', 100); +#### `assertionTemplate.setProperties()` + +Returns a new assertion template with the updated properties for the target wrapped node. + +```tsx +.setProperties( + wrapped: Wrapped, + value: T['properties'] | PropertiesComparatorFunction +): AssertionTemplateResult; ``` -A `functionalSelector` can be used return a function that is nested in a widget's properties. The function will be triggered, in the same way that using a plain string `functionSelector`. +A function can be set in place of the properties object to return the expected properties based of the actual properties. -### Trigger example +## Triggering Properties -Given the following VDOM structure: +In addition to asserting the output from a widget, widget behavior can be tested by using the `renderer.property()` function. The `property()` function takes a [wrapped test node]() and the key of a property to call before the next call to `expect()`. -```ts -v(Toolbar, { - key: 'toolbar', - buttons: [ - { - icon: 'save', - onClick: () => this._onSave() - }, - { - icon: 'cancel', - onClick: () => this._onCancel() - } - ] -}); -``` +> src/MyWidget.tsx -The save toolbar button's `onClick` function can be triggered by: +```tsx +import { create, tsx } from '@dojo/framework/core/vdom'; +import icache from '@dojo/framework/core/middleware/icache'; +import { RenderResult } from '@dojo/framework/core/interfaces'; -```typescript -h.trigger('@buttons', (renderResult: DNode) => { - return renderResult.properties.buttons[0].onClick; +import MyWidgetWithChildren from './MyWidgetWithChildren'; + +const factory = create({ icache }).properties<{ onClick: () => void }>(); + +export const MyWidget = factory(function MyWidget({ properties, middleware: { icache } }) { + const count = icache.getOrSet('count', 0); + return ( +
+

Header

+ {`${count}`} + +
+ ); }); ``` -**Note:** If the specified selector cannot be found, `trigger` will throw an error. +> src/MyWidget.spec.tsx -## `harness.getRender` +```tsx +const { describe, it } = intern.getInterface('bdd'); +import { tsx } from '@dojo/framework/core/vdom'; +import renderer, { assertionTemplate, wrap } from '@dojo/framework/testing/renderer'; +import * as sinon from 'sinon'; -`harness.getRender()` returns the render with the index provided, when no index is provided it returns the last render. +import MyWidget from './MyWidget'; -```ts -getRender(index?: number); -``` +// Create a wrapped node for the button +const WrappedButton = wrap('button'); -- `index`: The index of the render result to return +const WrappedSpan = wrap('span'); -Example Usage(s): +const baseAssertion = assertionTemplate(() => ( +
+

Header

+ 0 + { + icache.set('count', icache.getOrSet('count', 0) + 1); + properties().onClick(); + }}>Increase Counter! + +)); -```ts -// Returns the result of the last render -const render = h.getRender(); -``` +describe('MyWidget', () => { + it('render', () => { + const onClickStub = sinon.stub(); + const r = renderer(() => ); -```ts -// Returns the result of the render for the index provided -h.getRender(1); -``` + // assert against the base assertion + h.expect(baseAssertion); -# Assertion templates + // register a call to the button's onclick property + h.property(WrappedButton, 'onclick'); -Assertion templates provide a reusable base to assert against a widget's entire render output, but allow portions to be modified as needed between several test executions. This means common elements that do not change across multiple tests can be abstracted and defined once and reused in multiple locations. + // create a new template with the updated count + const counterTemplate = baseAssertion.setChildren(WrappedSpan, () => ['1']); -To use assertion templates first import the module: + // expect against the new template, the property will be called before the test render + h.expect(counterTemplate); -```ts -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; + // once the assertion is complete, check that the stub property was called + assert.isTrue(onClickStub.calledOnce); + }); +}); ``` -A base assertion should be created which defines the widget's default render state. Given the following widget: +## Asserting Functional Children -> src/widgets/Profile.tsx +To assert the output from functional children the test renderer needs to understand how to resolve the child render functions. This includes passing in any expected injected values. -```ts -import { create, tsx } from '@dojo/framework/core/vdom'; +The test renderer `renderer.child()` function enables children to get resolved in order to include them in the assertion. Using the `.child()` function requires the widget with functional children to be wrapped when included in the assertion template, and the wrapped node gets passed to the `.child` function. -import * as css from './styles/Profile.m.css'; +> src/MyWidget.tsx -export interface ProfileProperties { - username?: string; -} +```tsx +import { create, tsx } from '@dojo/framework/core/vdom'; +import { RenderResult } from '@dojo/framework/core/interfaces'; -const factory = create().properties(); +import MyWidgetWithChildren from './MyWidgetWithChildren'; -const Profile = factory(function Profile({ properties }) { - const { username } = properties(); - return

{`Welcome ${username || 'Stranger'}!`}

; -}); +const factory = create().children<(value: string) => RenderResult>(); -export default Profile; +export const MyWidget = factory(function MyWidget() { + return ( +
+

Header

+ {(value) =>
{value}
}
+
+ ); +}); ``` -The base assertion might look like: - -> tests/unit/widgets/Profile.tsx +> src/MyWidget.spec.tsx -```ts +```tsx const { describe, it } = intern.getInterface('bdd'); -import harness from '@dojo/framework/testing/harness'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; import { tsx } from '@dojo/framework/core/vdom'; +import renderer, { assertionTemplate, wrap } from '@dojo/framework/testing/renderer'; -import Profile from '../../../src/widgets/Profile'; -import * as css from '../../../src/widgets/Profile.m.css'; +import MyWidgetWithChildren from './MyWidgetWithChildren'; +import MyWidget from './MyWidget'; + +// Create a wrapped node for the widget with functional children +const WrappedMyWidgetWithChildren = wrap(MyWidgetWithChildren); -const profileAssertion = assertionTemplate(() => ( -

- Welcome Stranger! -

+const baseAssertion = assertionTemplate(() => ( +
+

Header

+ {() =>
Hello!
} +
)); + +describe('MyWidget', () => { + it('render', () => { + const r = renderer(() => ); + + // instruct the test renderer to resolve the children + // with the provided params + r.child(WrappedMyWidgetWithChildren, ['Hello!']); + + h.expect(baseAssertion); + }); +}); ``` -and in a test would look like: +## Custom Property Comparators -> tests/unit/widgets/Profile.tsx +There are circumstances where the exact value of a property is unknown during testing, so will require the use of a custom comparator. Custom comparators get used for any wrapped widget along with the `@dojo/framework/testing/renderer#compare` function in place of the usual widget or node property. -```ts -const profileAssertion = assertionTemplate(() => ( -

- Welcome Stranger! -

-)); +```tsx +compare(comparator: (actual) => boolean) +``` -describe('Profile', () => { - it('default renders correctly', () => { - const h = harness(() => ); - h.expect(profileAssertion); - }); -}); +```tsx +import { assertionTemplate, wrap, compare } from '@dojo/framework/testing/renderer'; + +// create a wrapped node the `h1` +const WrappedHeader = wrap('h1'); + +const baseTemplate = assertionTemplate(() => ( +
+ typeof actual === 'string')}>Header! +
+)); ``` -To test the scenario of a `username` property being passed to the `Profile`, the assertion template can be parameterized such as: +## Mocking Middleware -> tests/unit/widgets/Profile.tsx +When initializing the test renderer, mock middleware can get specified as part of the `RendererOptions`. The mock middleware gets defined as a tuple of the original middleware and the mock middleware implementation. Mock middleware gets created in the same way as any other middleware. -```ts -describe('Profile', () => { - ... - - it('renders given username correctly', () => { - // update the expected result with a given username - const namedAssertion = profileAssertion.setChildren('~welcome', () => [ - 'Welcome Kel Varnsen!' - ]); - const h = harness(() => ); - h.expect(namedAssertion); +```tsx +import myMiddleware from './myMiddleware'; +import myMockMiddleware from './myMockMiddleware'; +import renderer from '@dojo/framework/testing/renderer'; + +import MyWidget from './MyWidget'; + +describe('MyWidget', () => { + it('renders', () => { + const r = renderer(() => , { middleware: [[myMiddleware, myMockMiddleware]] }); + + h + .expect + /** assertion that executes the mock middleware instead of the normal middleware **/ + (); }); }); ``` -Here the `setChildren()` api is used on the baseAssertion, and the special `~` selector allows finding a node with a key of `~welcome`. The `assertion-key` property (or when using `w()` or `v()`functions, `~key`) is a special property on assertion templates that will be erased at assertion time so it doesn't show up when matching the renders. This allows the assertion templates to easily select nodes without having to augment the actual widget render function. Once the `welcome` node is found, its children are overridden to a new value of `['Welcome Kel Varnsen!']`, and the resulting template is then used in `h.expect`. It's important to note that assertion templates always return a new assertion template when setting a value. This ensures that an existing template is not accidentally mutated, which would cause other tests to potentially fail, and allows construction of layered templates that incrementally build on each other. +The test renderer automatically mocks a number of core middlewares that will get injected into any middleware that requires them: -Assertion template has the following API: +- `invalidator` +- `setProperty` +- `destroy` -```ts -insertBefore(selector: string, children: () => DNode[]): AssertionTemplateResult; -insertAfter(selector: string, children: () => DNode[]): AssertionTemplateResult; -insertSiblings(selector: string, children: () => DNode[], type?: 'before' | 'after'): AssertionTemplateResult; -append(selector: string, children: () => DNode[]): AssertionTemplateResult; -prepend(selector: string, children: () => DNode[]): AssertionTemplateResult; -replaceChildren(selector: string, children: () => DNode[]): AssertionTemplateResult; -setChildren(selector: string, children: () => DNode[], type?: 'prepend' | 'replace' | 'append'): AssertionTemplateResult; -setProperty(selector: string, property: string, value: any): AssertionTemplateResult; -setProperties(selector: string, value: any | PropertiesComparatorFunction): AssertionTemplateResult; -getChildren(selector: string): DNode[]; -getProperty(selector: string, property: string): any; -getProperties(selector: string): any; -replace(selector: string, node: DNode): AssertionTemplateResult; -remove(selector: string): AssertionTemplateResult; -``` +Additionally, there are a number of mock middleware available to support widgets that use the corresponding provided Dojo middleware. See the [mocking](/learn/testing/mocking#provided-middleware-mocks) section for more information on provided mock middleware. # Mocking @@ -409,7 +483,7 @@ To test that the `properties().fetchItems` method is called when the button is c const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; -import harness from '@dojo/framework/testing/harness'; +import renderer, { wrap } from '@dojo/framework/testing/renderer'; import Action from '../../../src/widgets/Action'; import * as css from '../../../src/widgets/Action.m.css'; @@ -422,15 +496,18 @@ import { assert } from 'chai'; describe('Action', () => { const fetchItems = stub(); it('can fetch data on button click', () => { - const h = harness(() => ); - h.expect(() => ( + const WrappedButton = wrap(Button); + const template = assertionTemplate(() => (
- +
)); - h.trigger('@button', 'onClick'); + const r = renderer(() => ); + r.expect(template); + r.property(WrappedButton, 'onClick'); + r.expect(template); assert.isTrue(fetchItems.calledOnce); }); }); @@ -479,34 +556,30 @@ By using the `mockBreakpoint(key: string, contentRect: Partial) ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import harness from '@dojo/framework/testing/harness'; +import renderer, { wrap } from '@dojo/framework/testing/renderer'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; import breakpoint from '@dojo/framework/core/middleware/breakpoint'; import createBreakpointMock from '@dojo/framework/testing/mocks/middleware/breakpoint'; import Breakpoint from '../../src/Breakpoint'; describe('Breakpoint', () => { it('resizes correctly', () => { + const WrappedHeader = wrap('h1'); const mockBreakpoint = createBreakpointMock(); - - const h = harness(() => , { - middleware: [[breakpoint, mockBreakpoint]] - }); - h.expect(() => ( + const template = assertionTemplate(() => (
-

Header

+ Header
Longer description
)); + const r = renderer(() => , { + middleware: [[breakpoint, mockBreakpoint]] + }); + h.expect(template); mockBreakpoint('root', { breakpoint: 'LG', contentRect: { width: 800 } }); - h.expect(() => ( -
-

Header

-

Subtitle

-
Longer description
-
- )); + h.expect(template.insertAfter(WrappedHeader, () => [

Subtitle

]); }); }); ``` @@ -544,7 +617,8 @@ By calling `focusMock(key: string | number, value: boolean)` the result of the ` ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import harness from '@dojo/framework/testing/harness'; +import renderer, { wrap } from '@dojo/framework/testing/renderer'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; import focus from '@dojo/framework/core/middleware/focus'; import createFocusMock from '@dojo/framework/testing/mocks/middleware/focus'; import * as css from './FormWidget.m.css'; @@ -552,24 +626,21 @@ import * as css from './FormWidget.m.css'; describe('Focus', () => { it('adds a "focused" class to the wrapper when the input is focused', () => { const focusMock = createFocusMock(); - - const h = harness(() => , { + const WrappedRoot = wrap('div'); + const template = assertionTemplate(() => ( + + + + )); + const r = renderer(() => , { middleware: [[focus, focusMock]] }); - h.expect(() => ( -
- -
- )); + h.expect(template); focusMock('text', true); - h.expect(() => ( -
- -
- )); + h.expect(template.setProperty(WrappedRoot, 'classes', [css.root, css.focused])); }); }); ``` @@ -605,7 +676,8 @@ Testing the asynchronous result using the mock `icache` middleware is simple: ```tsx const { describe, it, afterEach } = intern.getInterface('bdd'); -import harness from '@dojo/framework/testing/harness'; +import renderer, { wrap } from '@dojo/framework/testing/renderer'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; import { tsx } from '@dojo/framework/core/vdom'; import * as sinon from 'sinon'; import global from '@dojo/framework/shim/global'; @@ -622,13 +694,15 @@ describe('MyWidget', () => { // stub the fetch call to return a known value global.fetch = sinon.stub().returns(Promise.resolve({ json: () => Promise.resolve('api data') })); + const WrappedRoot = wrap('div'); + const template = assertionTemplate(() => Loading); const mockICache = createICacheMock(); - const h = harness(() => , { middleware: [[icache, mockICache]] }); - h.expect(() =>
Loading
); + const r = renderer(() => , { middleware: [[icache, mockICache]] }); + h.expect(template); // await the async method passed to the mock cache await mockICache('users'); - h.expect(() =>
api data
); + h.expect(template.setChildren(WrappedRoot, () => ['api data'])); }); }); ``` @@ -657,7 +731,8 @@ Using the mock `intersection` middleware: import { tsx } from '@dojo/framework/core/vdom'; import createIntersectionMock from '@dojo/framework/testing/mocks/middleware/intersection'; import intersection from '@dojo/framework/core/middleware/intersection'; -import harness from '@dojo/framework/testing/harness'; +import renderer, { wrap } from '@dojo/framework/testing/renderer'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; import MyWidget from './MyWidget'; @@ -665,19 +740,22 @@ describe('MyWidget', () => { it('test', () => { // create the intersection mock const intersectionMock = createIntersectionMock(); - // pass the intersection mock to the harness so it knows to + // pass the intersection mock to the renderer so it knows to // replace the original middleware - const h = harness(() => , { middleware: [[intersection, intersectionMock]] }); - - // call harness.expect as usual, asserting the default response - h.expect(() =>
{`{"intersectionRatio":0,"isIntersecting":false}`}
); + const r = renderer(() => , { middleware: [[intersection, intersectionMock]] }); + const WrappedRoot = wrap('div'); + const assertionTemplate = assertionTemplate(() => ( + {`{"intersectionRatio":0,"isIntersecting":false}`} + )); + // call renderer.expect as usual, asserting the default response + r.expect(assertionTemplate); // use the intersection mock to set the expected return // of the intersection middleware by key intersectionMock('root', { isIntersecting: true }); // assert again with the updated expectation - h.expect(() =>
{`{"isIntersecting": true }`}
); + h.expect(assertionTemplate.setChildren(WrappedRoot, () => [`{"isIntersecting": true }`])); }); }); ``` @@ -730,7 +808,8 @@ Using the mock `resize` middleware: import { tsx } from '@dojo/framework/core/vdom'; import createResizeMock from '@dojo/framework/testing/mocks/middleware/resize'; import resize from '@dojo/framework/core/middleware/resize'; -import harness from '@dojo/framework/testing/harness'; +import renderer, { wrap } from '@dojo/framework/testing/renderer'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; import MyWidget from './MyWidget'; @@ -738,19 +817,22 @@ describe('MyWidget', () => { it('test', () => { // create the resize mock const resizeMock = createResizeMock(); - // pass the resize mock to the harness so it knows to replace the original + // pass the resize mock to the test renderer so it knows to replace the original // middleware - const h = harness(() => , { middleware: [[resize, resizeMock]] }); + const r = renderer(() => , { middleware: [[resize, resizeMock]] }); + + const WrappedRoot = wrap('div'); + const template = assertionTemplate(() =>
null
); - // call harness.expect as usual - h.expect(() =>
null
); + // call renderer.expect as usual + r.expect(template); // use the resize mock to set the expected return of the resize middleware // by key resizeMock('root', { width: 100 }); // assert again with the updated expectation - h.expect(() =>
{`{"width":100}`}
); + r.expect(template.setChildren(WrappedRoot, () [`{"width":100}`]);) }); }); ``` @@ -805,7 +887,7 @@ Using the mock `store` middleware: ```tsx import { tsx } from '@dojo/framework/core/vdom' import createMockStoreMiddleware from '@dojo/framework/testing/mocks/middleware/store'; -import harness from '@dojo/framework/testing/harness'; +import renderer from '@dojo/framework/testing/renderer'; import { myProcess } from './processes'; import MyWidget from './MyWidget'; @@ -825,30 +907,30 @@ describe('MyWidget', () => { // pass through an array of tuples `[originalProcess, stub]` for mocked processes // calls to processes not stubbed/mocked get ignored const mockStore = createMockStoreMiddleware([[myProcess, myProcessStub]]); - const h = harness(() => , { + const r = renderer(() => , { middleware: [[store, mockStore]] }); - h.expect(/* assertion template for `Loading`*/); + r.expect(/* assertion template for `Loading`*/); // assert again the stubbed process expect(myProcessStub.calledWith({ id: 'id' })).toBeTruthy(); mockStore((path) => [replace(path('isLoading', true)]); - h.expect(/* assertion template for `Loading`*/); + r.expect(/* assertion template for `Loading`*/); expect(myProcessStub.calledOnce()).toBeTruthy(); // use the mock store to apply operations to the store mockStore((path) => [replace(path('details', { id: 'id' })]); mockStore((path) => [replace(path('isLoading', true)]); - h.expect(/* assertion template for `ShowDetails`*/); + r.expect(/* assertion template for `ShowDetails`*/); properties.id = 'other'; - h.expect(/* assertion template for `Loading`*/); + r.expect(/* assertion template for `Loading`*/); expect(myProcessStub.calledTwice()).toBeTruthy(); expect(myProcessStub.secondCall.calledWith({ id: 'other' })).toBeTruthy(); mockStore((path) => [replace(path('details', { id: 'other' })]); - h.expect(/* assertion template for `ShowDetails`*/); + r.expect(/* assertion template for `ShowDetails`*/); }); }); ``` @@ -889,7 +971,8 @@ Using `validityMock(key: string, value: { valid?: boolean, message?: string; })` ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import harness from '@dojo/framework/testing/harness'; +import renderer from '@dojo/framework/testing/renderer'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; import validity from '@dojo/framework/core/middleware/validity'; import createValidityMock from '@dojo/framework/testing/mocks/middleware/validity'; import * as css from './FormWidget.m.css'; @@ -898,24 +981,26 @@ describe('Validity', () => { it('adds the "invalid" class to the wrapper when the input is invalid and displays a message', () => { const validityMock = createValidityMock(); - const h = harness(() => , { + const r = renderer(() => , { middleware: [[validity, validityMock]] }); - h.expect(() => ( -
+ const WrappedRoot = wrap('div'); + const template = assertionTemplate(() => ( + {}} /> -
+ )); + h.expect(template); + validityMock('input', { valid: false, message: 'invalid message' }); - h.expect(() => ( -
- {}} /> -

invalid message

-
- )); + const invalidTemplate = template + .append(WrappedRoot, () => [

invalid message

]) + .setProperty(WrappedRoot, 'classes', [css.root, css.invalid]); + + h.expect(invalidTemplate); }); }); ``` diff --git a/src/core/vdom.ts b/src/core/vdom.ts index ebe2936d2..614414039 100644 --- a/src/core/vdom.ts +++ b/src/core/vdom.ts @@ -414,6 +414,10 @@ export function v( let properties: VNodeProperties | DeferredVirtualProperties = propertiesOrChildren; let deferredPropertiesCallback; + if (typeof (tag as any).tag === 'function') { + return (tag as any).tag(properties, children); + } + if (Array.isArray(propertiesOrChildren)) { children = propertiesOrChildren; properties = {}; diff --git a/src/testing/assertRender.ts b/src/testing/assertRender.ts new file mode 100644 index 000000000..fb3fdbbe5 --- /dev/null +++ b/src/testing/assertRender.ts @@ -0,0 +1,138 @@ +import { DNode, WNode, VNode } from '../core/interfaces'; +import * as diff from 'diff'; +import WeakMap from '../shim/WeakMap'; +import Set from '../shim/Set'; +import Map from '../shim/Map'; +import { from as arrayFrom } from '../shim/array'; +import { isWNode, isVNode } from '../core/vdom'; + +const LINE_BREAK = '\n'; +const TAB = '\t'; + +let widgetClassCounter = 0; +const widgetMap = new WeakMap(); + +function getTabs(depth = 0): string { + return new Array(depth + 1).join(TAB); +} + +function isNode(value: any): value is DNode { + return isWNode(value) || isVNode(value); +} + +function getTagName(node: VNode | WNode): string { + if (isVNode(node)) { + return node.tag; + } + const { widgetConstructor } = node; + let name: string; + if (typeof widgetConstructor === 'string' || typeof widgetConstructor === 'symbol') { + name = widgetConstructor.toString(); + } else { + name = (widgetConstructor as any).name; + if (name === undefined) { + let id = widgetMap.get(widgetConstructor); + if (id === undefined) { + id = ++widgetClassCounter; + widgetMap.set(widgetConstructor, id); + } + name = `Widget-${id}`; + } + } + return name; +} + +function format(nodes: DNode | DNode[], depth = 0): string { + nodes = Array.isArray(nodes) ? nodes : [nodes]; + const nodeString = nodes.reduce((str: string, node, index) => { + if (node == null || node === true || node === false) { + return str; + } + + if (index > 0) { + str = `${str}\n${getTabs(depth)}`; + } + + if (typeof node === 'string') { + return `${str}${node}`; + } + + const tag = getTagName(node); + str = `${str}<${tag}`; + + const propertyKeys = Object.keys(node.properties).sort(); + if (propertyKeys.length) { + const properties = propertyKeys.reduce((props, propKey) => { + return `${props} ${propKey}=${typeof node.properties[propKey] === 'string' ? '"' : '{'}${ + typeof node.properties[propKey] === 'function' + ? `"function"` + : node.properties[propKey] instanceof Set || node.properties[propKey] instanceof Map + ? arrayFrom(node.properties[propKey]) + : node.properties[propKey] + }${typeof node.properties[propKey] === 'string' ? '"' : '}'}`; + }, ''); + + str = `${str}${properties}`; + } + str = `${str}>`; + + if (node.children && node.children.length) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + if (!child) { + continue; + } + if (isNode(child) || typeof child === 'string') { + str = `${str}${LINE_BREAK}${format(child, depth + 1)}`; + } else if (typeof child === 'function') { + str = `${str}${LINE_BREAK}${getTabs(depth + 1)}{`; + str = `${str}${LINE_BREAK}${getTabs(depth + 2)}"child function"`; + str = `${str}${LINE_BREAK}${getTabs(depth + 1)}}`; + } else if (typeof child === 'object') { + str = `${str}${LINE_BREAK}${getTabs(depth + 1)}{`; + const childrenKeys = Object.keys(child); + for (let j = 0; j < childrenKeys.length; j++) { + str = `${str}${LINE_BREAK}${getTabs(depth + 2)}${childrenKeys[j]}: (`; + if (typeof child[childrenKeys[j]] !== 'function') { + str = `${str}${LINE_BREAK}${format(child[childrenKeys[j]], depth + 3)}`; + } else { + str = `${str}${LINE_BREAK}${getTabs(depth + 3)}"child function"`; + } + str = `${str}${LINE_BREAK}${getTabs(depth + 2)})`; + } + str = `${str}${LINE_BREAK}${getTabs(depth + 1)}}`; + } + } + } + if (tag) { + str = `${str}${LINE_BREAK}${getTabs(depth)}`; + } + return str; + }, getTabs(depth)); + return nodeString; +} + +export function assertRender(actual: DNode | DNode[], expected: DNode | DNode[]): void { + const parsedActual = format(actual); + const parsedExpected = format(expected); + const diffResult = diff.diffLines(parsedActual, parsedExpected); + let diffFound = false; + const parsedDiff = diffResult.reduce((result: string, part) => { + if (part.added) { + diffFound = true; + result = `${result}(E)${part.value.replace(/\n\t/g, '\n(E)\t')}`; + } else if (part.removed) { + diffFound = true; + result = `${result}(A)${part.value.replace(/\n\t/g, '\n(A)\t')}`; + } else { + result = `${result}${part.value}`; + } + return result; + }, '\n'); + + if (diffFound) { + throw new Error(parsedDiff); + } +} + +export default assertRender; diff --git a/src/testing/assertionTemplate.ts b/src/testing/assertionTemplate.ts index 3eeab547b..734098f78 100644 --- a/src/testing/assertionTemplate.ts +++ b/src/testing/assertionTemplate.ts @@ -1,62 +1,161 @@ -import select from './support/selector'; -import { VNode, WNode, DNode } from '../core/interfaces'; +import { VNode, WNode, DNode, RenderResult, WidgetBaseInterface, Constructor } from '../core/interfaces'; import { isWNode, isVNode } from '../core/vdom'; import { decorate } from '../core/util'; +import { Wrapped, WidgetFactory, NonComparable, CompareFunc } from './interfaces'; import WidgetBase from '../core/WidgetBase'; -export type PropertiesComparatorFunction = (actualProperties: any) => any; +export type PropertiesComparatorFunction = (actualProperties: T) => T; -export type TemplateChildren = DNode[] | (() => DNode[]); +export type TemplateChildren = () => T; + +export interface DecoratorResult { + hasDeferredProperties: boolean; + nodes: T; +} + +export function decorateNodes(dNode: DNode[]): DecoratorResult; +export function decorateNodes(dNode: DNode): DecoratorResult; +export function decorateNodes(dNode: DNode | DNode[]): DecoratorResult; +export function decorateNodes(dNode: any): DecoratorResult { + let hasDeferredProperties = false; + function addParent(parent: WNode | VNode): void { + (parent.children || []).forEach((child: any) => { + if (isVNode(child) || isWNode(child)) { + (child as any).parent = parent; + } + }); + if (isVNode(parent) && typeof parent.deferredPropertiesCallback === 'function') { + hasDeferredProperties = true; + parent.properties = { ...parent.properties, ...parent.deferredPropertiesCallback(false) }; + } + } + const nodes = decorate(dNode, addParent, (node: DNode): node is WNode | VNode => isWNode(node) || isVNode(node)); + return { hasDeferredProperties, nodes }; +} + +function isWrappedNode(value: any): value is (WNode & { id: string }) | (WNode & { id: string }) { + return Boolean(value && value.id && (isWNode(value) || isVNode(value))); +} + +function findNode>(renderResult: RenderResult, wrapped: T): VNode | WNode { + renderResult = decorateNodes(renderResult).nodes; + let nodes = Array.isArray(renderResult) ? [...renderResult] : [renderResult]; + while (nodes.length) { + let node = nodes.pop(); + if (isWrappedNode(node)) { + if (node.id === wrapped.id) { + return node; + } + } + if (isVNode(node) || isWNode(node)) { + const children = node.children || []; + nodes = [...children, ...nodes]; + } + } + throw new Error('Unable to find node'); +} export interface AssertionTemplateResult { (): DNode | DNode[]; - append(selector: string, children: TemplateChildren): AssertionTemplateResult; - prepend(selector: string, children: TemplateChildren): AssertionTemplateResult; - replaceChildren(selector: string, children: TemplateChildren): AssertionTemplateResult; - insertBefore(selector: string, children: TemplateChildren): AssertionTemplateResult; - insertAfter(selector: string, children: TemplateChildren): AssertionTemplateResult; - insertSiblings(selector: string, children: TemplateChildren, type?: 'before' | 'after'): AssertionTemplateResult; - setChildren( - selector: string, + append( + target: Wrapped>, + children: TemplateChildren + ): AssertionTemplateResult; + append( + target: Wrapped, + children: TemplateChildren + ): AssertionTemplateResult; + prepend( + target: Wrapped>, + children: TemplateChildren + ): AssertionTemplateResult; + prepend( + target: Wrapped, + children: TemplateChildren + ): AssertionTemplateResult; + replaceChildren( + target: Wrapped>, + children: TemplateChildren + ): AssertionTemplateResult; + replaceChildren( + target: Wrapped, + children: TemplateChildren + ): AssertionTemplateResult; + insertBefore( + target: Wrapped>, + children: TemplateChildren + ): AssertionTemplateResult; + insertBefore( + target: Wrapped, + children: TemplateChildren + ): AssertionTemplateResult; + insertAfter( + target: Wrapped>, + children: TemplateChildren + ): AssertionTemplateResult; + insertAfter( + target: Wrapped, + children: TemplateChildren + ): AssertionTemplateResult; + insertSiblings( + target: T, children: TemplateChildren, + type?: 'before' | 'after' + ): AssertionTemplateResult; + setChildren( + target: Wrapped, + children: TemplateChildren, type?: 'prepend' | 'replace' | 'append' ): AssertionTemplateResult; - setProperty(selector: string, property: string, value: any): AssertionTemplateResult; - setProperties(selector: string, value: any | PropertiesComparatorFunction): AssertionTemplateResult; - getChildren(selector: string): DNode[]; - getProperty(selector: string, property: string): any; - getProperties(selector: string): any; - replace(selector: string, node: DNode): AssertionTemplateResult; - remove(selector: string): AssertionTemplateResult; + setChildren( + target: Wrapped>, + children: TemplateChildren, + type?: 'prepend' | 'replace' | 'append' + ): AssertionTemplateResult; + setProperty( + wrapped: Wrapped>, + property: K, + value: Exclude> + ): AssertionTemplateResult; + setProperty( + wrapped: Wrapped, + property: K, + value: Exclude> + ): AssertionTemplateResult; + setProperties( + wrapped: Wrapped>, + value: NonComparable | PropertiesComparatorFunction> + ): AssertionTemplateResult; + setProperties( + wrapped: Wrapped, + value: NonComparable | PropertiesComparatorFunction> + ): AssertionTemplateResult; + getChildren(target: Wrapped>): T['children']; + getChildren(target: Wrapped): T['children']; + getProperty( + target: Wrapped>, + property: K + ): Exclude>; + getProperty( + target: Wrapped, + property: K + ): Exclude>; + getProperties(target: Wrapped>): NonComparable; + getProperties(target: Wrapped): NonComparable; + replace(target: Wrapped>, node: DNode): AssertionTemplateResult; + replace(target: Wrapped, node: DNode): AssertionTemplateResult; + remove(target: Wrapped>): AssertionTemplateResult; + remove(target: Wrapped): AssertionTemplateResult; } type NodeWithProperties = (VNode | WNode) & { properties: { [index: string]: any } }; -const findOne = (nodes: DNode | DNode[], selector: string): NodeWithProperties => { - let finalSelector = selector; - if (selector.indexOf('~') === 0) { - finalSelector = `[\\~key='${selector.substr(1)}']`; - } - let [node] = select(finalSelector, nodes); - if (!node) { - finalSelector = `[assertion-key='${selector.substr(1)}']`; - [node] = select(finalSelector, nodes); - } - if (!node) { - throw Error(`Node not found for selector "${selector}"`); - } - if (!isWNode(node) && !isVNode(node)) { - throw Error('Cannot set or get on unknown node'); - } - return node; -}; - const replaceChildren = ( - selector: string, + wrapped: Wrapped, render: DNode | DNode[], modifyChildrenFn: (index: number, children: DNode[]) => DNode[] ): DNode | DNode[] => { - const node = findOne(render, selector); + const node = findNode(render, wrapped); const parent: (VNode | WNode) & { children: DNode[] } | undefined = (node as any).parent; const siblings = parent ? parent.children : Array.isArray(render) ? render : [render]; const newChildren = modifyChildrenFn(siblings.indexOf(node), [...siblings]); @@ -82,49 +181,41 @@ export function assertionTemplate(renderFunc: () => DNode | DNode[]) { }); return render; }; - assertionTemplateResult.setProperty = (selector: string, property: string, value: any) => { + assertionTemplateResult.setProperty = (wrapped: Wrapped, property: string, value: any) => { return assertionTemplate(() => { const render = renderFunc(); - const node = findOne(render, selector); + const node = findNode(render, wrapped); node.properties[property] = value; return render; }); }; - assertionTemplateResult.setProperties = (selector: string, value: any | PropertiesComparatorFunction) => { + assertionTemplateResult.setProperties = (wrapped: Wrapped, value: any | PropertiesComparatorFunction) => { return assertionTemplate(() => { const render = renderFunc(); - const node = findOne(render, selector); + const node = findNode(render, wrapped); node.properties = value; return render; }); }; - assertionTemplateResult.append = (selector: string, children: TemplateChildren) => { - return assertionTemplateResult.setChildren(selector, children, 'append'); + assertionTemplateResult.append = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionTemplateResult.setChildren(wrapped, children, 'append'); }; - assertionTemplateResult.prepend = (selector: string, children: TemplateChildren) => { - return assertionTemplateResult.setChildren(selector, children, 'prepend'); + assertionTemplateResult.prepend = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionTemplateResult.setChildren(wrapped, children, 'prepend'); }; - assertionTemplateResult.replaceChildren = (selector: string, children: TemplateChildren) => { - return assertionTemplateResult.setChildren(selector, children, 'replace'); + assertionTemplateResult.replaceChildren = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionTemplateResult.setChildren(wrapped, children, 'replace'); }; assertionTemplateResult.setChildren = ( - selector: string, + wrapped: Wrapped, children: TemplateChildren, type: 'prepend' | 'replace' | 'append' = 'replace' ) => { - if (Array.isArray(children)) { - console.warn( - 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' - ); - } return assertionTemplate(() => { const render = renderFunc(); - const node = findOne(render, selector); + const node = findNode(render, wrapped); node.children = node.children || []; - let childrenResult = children; - if (typeof childrenResult === 'function') { - childrenResult = childrenResult(); - } + let childrenResult = children(); switch (type) { case 'prepend': node.children = [...childrenResult, ...node.children]; @@ -139,26 +230,21 @@ export function assertionTemplate(renderFunc: () => DNode | DNode[]) { return render; }); }; - assertionTemplateResult.insertBefore = (selector: string, children: TemplateChildren) => { - return assertionTemplateResult.insertSiblings(selector, children, 'before'); + assertionTemplateResult.insertBefore = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionTemplateResult.insertSiblings(wrapped, children, 'before'); }; - assertionTemplateResult.insertAfter = (selector: string, children: TemplateChildren) => { - return assertionTemplateResult.insertSiblings(selector, children, 'after'); + assertionTemplateResult.insertAfter = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionTemplateResult.insertSiblings(wrapped, children, 'after'); }; assertionTemplateResult.insertSiblings = ( - selector: string, + wrapped: Wrapped, children: TemplateChildren, type: 'before' | 'after' = 'after' ) => { - if (Array.isArray(children)) { - console.warn( - 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' - ); - } return assertionTemplate(() => { const render = renderFunc(); const insertedChildren = typeof children === 'function' ? children() : children; - return replaceChildren(selector, render, (index, children) => { + return replaceChildren(wrapped, render, (index, children) => { if (type === 'after') { children.splice(index + 1, 0, ...insertedChildren); } else { @@ -168,34 +254,34 @@ export function assertionTemplate(renderFunc: () => DNode | DNode[]) { }); }); }; - assertionTemplateResult.getProperty = (selector: string, property: string) => { + assertionTemplateResult.getProperty = (wrapped: Wrapped, property: string) => { const render = renderFunc(); - const node = findOne(render, selector); + const node = findNode(render, wrapped); return node.properties[property]; }; - assertionTemplateResult.getProperties = (selector: string, property: string) => { + assertionTemplateResult.getProperties = (wrapped: Wrapped) => { const render = renderFunc(); - const node = findOne(render, selector); + const node = findNode(render, wrapped); return node.properties; }; - assertionTemplateResult.getChildren = (selector: string) => { + assertionTemplateResult.getChildren = (wrapped: Wrapped) => { const render = renderFunc(); - const node = findOne(render, selector); + const node = findNode(render, wrapped); return node.children || []; }; - assertionTemplateResult.replace = (selector: string, newNode: DNode) => { + assertionTemplateResult.replace = (wrapped: Wrapped, newNode: DNode) => { return assertionTemplate(() => { const render = renderFunc(); - return replaceChildren(selector, render, (index, children) => { + return replaceChildren(wrapped, render, (index, children) => { children.splice(index, 1, newNode); return children; }); }); }; - assertionTemplateResult.remove = (selector: string) => { + assertionTemplateResult.remove = (wrapped: Wrapped) => { return assertionTemplate(() => { const render = renderFunc(); - return replaceChildren(selector, render, (index, children) => { + return replaceChildren(wrapped, render, (index, children) => { children.splice(index, 1); return children; }); diff --git a/src/testing/decorate.ts b/src/testing/decorate.ts new file mode 100644 index 000000000..512637082 --- /dev/null +++ b/src/testing/decorate.ts @@ -0,0 +1,142 @@ +import { isVNode, isWNode } from '../core/vdom'; +import { RenderResult, DNode, VNode, WNode } from '../core/interfaces'; +import { Instruction } from './renderer'; +import Map from '../shim/Map'; +import { Ignore } from './assertionTemplate'; +import { findIndex } from '../shim/array'; +import { decorate as coreDecorate } from '../core/util'; + +type DecorateTuple = [DNode[], DNode[]]; + +export interface DecoratorResult { + hasDeferredProperties: boolean; + nodes: T; +} + +function isNode(node: any): node is VNode | WNode { + return isVNode(node) || isWNode(node); +} + +export function decorateNodes(dNode: DNode[]): DecoratorResult; +export function decorateNodes(dNode: DNode): DecoratorResult; +export function decorateNodes(dNode: DNode | DNode[]): DecoratorResult; +export function decorateNodes(dNode: any): DecoratorResult { + let hasDeferredProperties = false; + function addParent(parent: WNode | VNode): void { + (parent.children || []).forEach((child: any) => { + if (isVNode(child) || isWNode(child)) { + (child as any).parent = parent; + } + }); + if (isVNode(parent) && typeof parent.deferredPropertiesCallback === 'function') { + hasDeferredProperties = true; + parent.properties = { ...parent.properties, ...parent.deferredPropertiesCallback(false) }; + } + } + const nodes = coreDecorate( + dNode, + addParent, + (node: DNode): node is WNode | VNode => isWNode(node) || isVNode(node) + ); + return { hasDeferredProperties, nodes }; +} + +export function decorate(actual: RenderResult, expected: RenderResult, instructions: Map) { + let nodes: DecorateTuple[] = [ + [Array.isArray(actual) ? [...actual] : [actual], Array.isArray(expected) ? [...expected] : [expected]] + ]; + + let node = nodes.shift(); + while (node) { + const [actualNodes, expectedNodes] = node; + let childNodes: DecorateTuple[] = []; + while (expectedNodes.length > 0) { + let actualNode: DNode | { [index: string]: any } = actualNodes.shift(); + let expectedNode: DNode | { [index: string]: any } = expectedNodes.shift(); + if (isNode(expectedNode)) { + const instruction = instructions.get((expectedNode as any).id); + if (instruction) { + if (instruction.type === 'child') { + const expectedChild: any = expectedNode.children && expectedNode.children[0]; + const actualChild: any = isNode(actualNode) && actualNode.children && actualNode.children[0]; + + if (typeof expectedChild === 'object') { + const keys = Object.keys(expectedChild); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (typeof expectedChild[key] === 'function') { + const newExpectedChildren = expectedChild[key](); + expectedChild[key] = newExpectedChildren; + } + if (typeof actualChild === 'object' && typeof actualChild[key] === 'function') { + const newActualChildren = actualChild[key](...(instruction.params[key] || [])); + actualChild[key] = newActualChildren; + } + } + } else if (typeof expectedChild === 'function') { + const newExpectedChildren = expectedChild(); + (expectedNode as any).children[0] = newExpectedChildren; + if (typeof actualChild === 'function') { + const newActualChildren = actualChild(...instruction.params); + (actualNode as any).children[0] = newActualChildren; + } + } + } else if ( + instruction.type === 'property' && + isNode(actualNode) && + typeof actualNode.properties[instruction.key] === 'function' + ) { + actualNode.properties[instruction.key](...instruction.params); + } + } + } + + if (isNode(expectedNode) && isNode(actualNode)) { + const propertyKeys = Object.keys(expectedNode.properties); + for (let i = 0; i < propertyKeys.length; i++) { + const key = propertyKeys[i]; + if (expectedNode.properties[key].type === 'compare') { + const result = expectedNode.properties[key](actualNode.properties[key]); + if (result) { + expectedNode.properties[key] = actualNode.properties[key]; + } else { + expectedNode.properties[key] = 'Compare FAILED'; + } + } + } + } + + if (expectedNode && (expectedNode as any).widgetConstructor === Ignore) { + const index = findIndex((expectedNode as any).parent.children, (child) => child === expectedNode); + if (index !== -1) { + (expectedNode as any).parent.children[index] = actualNode || expectedNode; + } + } + + if (isNode(expectedNode)) { + if (typeof expectedNode.properties === 'function') { + const actualProperties = isNode(actualNode) ? actualNode.properties : {}; + expectedNode.properties = (expectedNode as any).properties(actualProperties); + } + } + + if (isNode(expectedNode)) { + const expectedChildren = expectedNode.children ? [...expectedNode.children] : []; + const actualChildren = isNode(actualNode) && actualNode.children ? [...actualNode.children] : []; + childNodes.push([actualChildren, expectedChildren]); + } else if (expectedNode && typeof expectedNode === 'object') { + const expectedChildren = Object.keys(expectedNode).map((key) => (expectedNode as any)[key]); + const actualChildren = + actualNode && typeof actualNode === 'object' + ? Object.keys(actualNode).map((key) => (actualNode as any)[key]) + : []; + childNodes.push([actualChildren, expectedChildren]); + } + } + childNodes.reverse(); + nodes = [...childNodes, ...nodes]; + node = nodes.shift(); + } +} + +export default decorate; diff --git a/src/testing/harness/assertionTemplate.ts b/src/testing/harness/assertionTemplate.ts new file mode 100644 index 000000000..0519edc5b --- /dev/null +++ b/src/testing/harness/assertionTemplate.ts @@ -0,0 +1,207 @@ +import select from './support/selector'; +import { VNode, WNode, DNode } from '../../core/interfaces'; +import { isWNode, isVNode } from '../../core/vdom'; +import { decorate } from '../../core/util'; +import WidgetBase from '../../core/WidgetBase'; + +export type PropertiesComparatorFunction = (actualProperties: any) => any; + +export type TemplateChildren = DNode[] | (() => DNode[]); + +export interface AssertionTemplateResult { + (): DNode | DNode[]; + append(selector: string, children: TemplateChildren): AssertionTemplateResult; + prepend(selector: string, children: TemplateChildren): AssertionTemplateResult; + replaceChildren(selector: string, children: TemplateChildren): AssertionTemplateResult; + insertBefore(selector: string, children: TemplateChildren): AssertionTemplateResult; + insertAfter(selector: string, children: TemplateChildren): AssertionTemplateResult; + insertSiblings(selector: string, children: TemplateChildren, type?: 'before' | 'after'): AssertionTemplateResult; + setChildren( + selector: string, + children: TemplateChildren, + type?: 'prepend' | 'replace' | 'append' + ): AssertionTemplateResult; + setProperty(selector: string, property: string, value: any): AssertionTemplateResult; + setProperties(selector: string, value: any | PropertiesComparatorFunction): AssertionTemplateResult; + getChildren(selector: string): DNode[]; + getProperty(selector: string, property: string): any; + getProperties(selector: string): any; + replace(selector: string, node: DNode): AssertionTemplateResult; + remove(selector: string): AssertionTemplateResult; +} + +type NodeWithProperties = (VNode | WNode) & { properties: { [index: string]: any } }; + +const findOne = (nodes: DNode | DNode[], selector: string): NodeWithProperties => { + let finalSelector = selector; + if (selector.indexOf('~') === 0) { + finalSelector = `[\\~key='${selector.substr(1)}']`; + } + let [node] = select(finalSelector, nodes); + if (!node) { + finalSelector = `[assertion-key='${selector.substr(1)}']`; + [node] = select(finalSelector, nodes); + } + if (!node) { + throw Error(`Node not found for selector "${selector}"`); + } + if (!isWNode(node) && !isVNode(node)) { + throw Error('Cannot set or get on unknown node'); + } + return node; +}; + +const replaceChildren = ( + selector: string, + render: DNode | DNode[], + modifyChildrenFn: (index: number, children: DNode[]) => DNode[] +): DNode | DNode[] => { + const node = findOne(render, selector); + const parent: (VNode | WNode) & { children: DNode[] } | undefined = (node as any).parent; + const siblings = parent ? parent.children : Array.isArray(render) ? render : [render]; + const newChildren = modifyChildrenFn(siblings.indexOf(node), [...siblings]); + + if (!parent) { + return newChildren; + } + + parent.children = newChildren; + return render; +}; + +export class Ignore extends WidgetBase {} + +export function assertionTemplate(renderFunc: () => DNode | DNode[]) { + const assertionTemplateResult: any = () => { + const render = renderFunc(); + decorate(render, (node) => { + if (isWNode(node) || isVNode(node)) { + delete (node as NodeWithProperties).properties['~key']; + delete (node as NodeWithProperties).properties['assertion-key']; + } + }); + return render; + }; + assertionTemplateResult.setProperty = (selector: string, property: string, value: any) => { + return assertionTemplate(() => { + const render = renderFunc(); + const node = findOne(render, selector); + node.properties[property] = value; + return render; + }); + }; + assertionTemplateResult.setProperties = (selector: string, value: any | PropertiesComparatorFunction) => { + return assertionTemplate(() => { + const render = renderFunc(); + const node = findOne(render, selector); + node.properties = value; + return render; + }); + }; + assertionTemplateResult.append = (selector: string, children: TemplateChildren) => { + return assertionTemplateResult.setChildren(selector, children, 'append'); + }; + assertionTemplateResult.prepend = (selector: string, children: TemplateChildren) => { + return assertionTemplateResult.setChildren(selector, children, 'prepend'); + }; + assertionTemplateResult.replaceChildren = (selector: string, children: TemplateChildren) => { + return assertionTemplateResult.setChildren(selector, children, 'replace'); + }; + assertionTemplateResult.setChildren = ( + selector: string, + children: TemplateChildren, + type: 'prepend' | 'replace' | 'append' = 'replace' + ) => { + if (Array.isArray(children)) { + console.warn( + 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' + ); + } + return assertionTemplate(() => { + const render = renderFunc(); + const node = findOne(render, selector); + node.children = node.children || []; + let childrenResult = children; + if (typeof childrenResult === 'function') { + childrenResult = childrenResult(); + } + switch (type) { + case 'prepend': + node.children = [...childrenResult, ...node.children]; + break; + case 'append': + node.children = [...node.children, ...childrenResult]; + break; + case 'replace': + node.children = [...childrenResult]; + break; + } + return render; + }); + }; + assertionTemplateResult.insertBefore = (selector: string, children: TemplateChildren) => { + return assertionTemplateResult.insertSiblings(selector, children, 'before'); + }; + assertionTemplateResult.insertAfter = (selector: string, children: TemplateChildren) => { + return assertionTemplateResult.insertSiblings(selector, children, 'after'); + }; + assertionTemplateResult.insertSiblings = ( + selector: string, + children: TemplateChildren, + type: 'before' | 'after' = 'after' + ) => { + if (Array.isArray(children)) { + console.warn( + 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' + ); + } + return assertionTemplate(() => { + const render = renderFunc(); + const insertedChildren = typeof children === 'function' ? children() : children; + return replaceChildren(selector, render, (index, children) => { + if (type === 'after') { + children.splice(index + 1, 0, ...insertedChildren); + } else { + children.splice(index, 0, ...insertedChildren); + } + return children; + }); + }); + }; + assertionTemplateResult.getProperty = (selector: string, property: string) => { + const render = renderFunc(); + const node = findOne(render, selector); + return node.properties[property]; + }; + assertionTemplateResult.getProperties = (selector: string, property: string) => { + const render = renderFunc(); + const node = findOne(render, selector); + return node.properties; + }; + assertionTemplateResult.getChildren = (selector: string) => { + const render = renderFunc(); + const node = findOne(render, selector); + return node.children || []; + }; + assertionTemplateResult.replace = (selector: string, newNode: DNode) => { + return assertionTemplate(() => { + const render = renderFunc(); + return replaceChildren(selector, render, (index, children) => { + children.splice(index, 1, newNode); + return children; + }); + }); + }; + assertionTemplateResult.remove = (selector: string) => { + return assertionTemplate(() => { + const render = renderFunc(); + return replaceChildren(selector, render, (index, children) => { + children.splice(index, 1); + return children; + }); + }); + }; + return assertionTemplateResult as AssertionTemplateResult; +} + +export default assertionTemplate; diff --git a/src/testing/harness.ts b/src/testing/harness/harness.ts similarity index 96% rename from src/testing/harness.ts rename to src/testing/harness/harness.ts index 234a6793d..d362298bc 100644 --- a/src/testing/harness.ts +++ b/src/testing/harness/harness.ts @@ -1,9 +1,17 @@ import assertRender from './support/assertRender'; import { decorateNodes, select } from './support/selector'; -import { WNode, DNode, Constructor, VNode, Callback, RenderResult, MiddlewareResultFactory } from '../core/interfaces'; -import { WidgetBase } from '../core/WidgetBase'; -import { isWidgetFunction } from '../core/Registry'; -import { invalidator, diffProperty, destroy, create, propertiesDiff } from '../core/vdom'; +import { + WNode, + DNode, + Constructor, + VNode, + Callback, + RenderResult, + MiddlewareResultFactory +} from '../../core/interfaces'; +import { WidgetBase } from '../../core/WidgetBase'; +import { isWidgetFunction } from '../../core/Registry'; +import { invalidator, diffProperty, destroy, create, propertiesDiff } from '../../core/vdom'; export interface CustomComparator { selector: string; diff --git a/src/testing/support/assertRender.ts b/src/testing/harness/support/assertRender.ts similarity index 95% rename from src/testing/support/assertRender.ts rename to src/testing/harness/support/assertRender.ts index f390baca7..68f60ca4b 100644 --- a/src/testing/support/assertRender.ts +++ b/src/testing/harness/support/assertRender.ts @@ -1,10 +1,10 @@ -import { DNode, WNode, VNode, DefaultWidgetBaseInterface, Constructor } from '../../core/interfaces'; +import { DNode, WNode, VNode, DefaultWidgetBaseInterface, Constructor } from '../../../core/interfaces'; import * as diff from 'diff'; -import WeakMap from '../../shim/WeakMap'; -import Set from '../../shim/Set'; -import Map from '../../shim/Map'; -import { from as arrayFrom } from '../../shim/array'; -import { isVNode, isWNode } from '../../core/vdom'; +import WeakMap from '../../../shim/WeakMap'; +import Set from '../../../shim/Set'; +import Map from '../../../shim/Map'; +import { from as arrayFrom } from '../../../shim/array'; +import { isVNode, isWNode } from '../../../core/vdom'; import { Ignore } from '../assertionTemplate'; let widgetClassCounter = 0; diff --git a/src/testing/support/selector.ts b/src/testing/harness/support/selector.ts similarity index 96% rename from src/testing/support/selector.ts rename to src/testing/harness/support/selector.ts index ca5556ec1..a708dd816 100644 --- a/src/testing/support/selector.ts +++ b/src/testing/harness/support/selector.ts @@ -1,7 +1,7 @@ -import { DNode, DefaultWidgetBaseInterface, WNode, VNode } from '../../core/interfaces'; +import { DNode, DefaultWidgetBaseInterface, WNode, VNode } from '../../../core/interfaces'; import * as cssSelect from 'css-select-umd'; -import { isVNode, isWNode } from '../../core/vdom'; -import { decorate } from '../../core/util'; +import { isVNode, isWNode } from '../../../core/vdom'; +import { decorate } from '../../../core/util'; export type TestFunction = (elem: DNode) => boolean; diff --git a/src/testing/interfaces.d.ts b/src/testing/interfaces.d.ts new file mode 100644 index 000000000..5842dfa25 --- /dev/null +++ b/src/testing/interfaces.d.ts @@ -0,0 +1,22 @@ +import { + WNodeFactory, + WidgetBaseInterface, + Constructor, + OptionalWNodeFactory, + DefaultChildrenWNodeFactory +} from '../core/interfaces'; + +export type WidgetFactory = WNodeFactory | OptionalWNodeFactory | DefaultChildrenWNodeFactory; + +export type Wrapped | WidgetFactory> = T & { + id: string; +}; + +export interface CompareFunc { + (actual: T): boolean; + type: 'compare'; +} + +export type Comparable = { [P in keyof T]: T[P] | CompareFunc }; + +export type NonComparable = { [P in keyof T]: Exclude> }; diff --git a/src/testing/renderer.ts b/src/testing/renderer.ts new file mode 100644 index 000000000..3af13dc90 --- /dev/null +++ b/src/testing/renderer.ts @@ -0,0 +1,295 @@ +import assertRender from './assertRender'; +import { + WNode, + DNode, + Constructor, + Callback, + RenderResult, + MiddlewareResultFactory, + WNodeFactory, + VNodeProperties, + OptionalWNodeFactory, + WidgetBaseInterface, + DefaultChildrenWNodeFactory +} from '../core/interfaces'; +import { WidgetBase } from '../core/WidgetBase'; +import { isWidgetFunction } from '../core/Registry'; +import { invalidator, diffProperty, destroy, create, propertiesDiff, w, v } from '../core/vdom'; +import { uuid } from '../core/util'; +import decorate, { decorateNodes } from './decorate'; +import { AssertionTemplateResult } from './assertionTemplate'; +import { Wrapped, WidgetFactory, CompareFunc, Comparable } from './interfaces'; + +export interface Expect { + (expectedRenderFunc: AssertionTemplateResult): void; +} + +export interface ChildInstruction { + type: 'child'; + wrapped: Wrapped; + params: any; +} + +export interface PropertyInstruction { + type: 'property'; + id: string; + key: string; + wrapped: Wrapped; + params: any; +} + +export type Instruction = ChildInstruction | PropertyInstruction; + +export interface Child { + >( + wrapped: Wrapped, + params: T['children'] extends { [index: string]: (...args: any[]) => RenderResult } + ? { [P in keyof T['children']]: Parameters } + : T['children'] extends (...args: any[]) => RenderResult ? Parameters : never + ): void; +} + +export type KnownKeys = { [K in keyof T]: string extends K ? never : number extends K ? never : K } extends { + [_ in keyof T]: infer U +} + ? U + : never; +export type FunctionPropertyNames = { + [K in keyof T]: T[K] extends ((...args: any[]) => any | undefined) ? K : never +}[keyof T]; +export type RequiredVNodeProperties = Required>>; + +export interface Property { + , K extends FunctionPropertyNames>>( + wrapped: Constructor, + key: K, + ...params: Parameters>> + ): void; + >>( + wrapped: T, + key: K, + ...params: Parameters>> + ): void; + < + T extends OptionalWNodeFactory<{ properties: Comparable; children: any }>, + K extends FunctionPropertyNames + >( + wrapped: T, + key: K, + ...params: any[] + ): void; +} + +export interface RendererAPI { + expect: Expect; + child: Child; + property: Property; +} + +let middlewareId = 0; + +interface RendererOptions { + middleware?: [MiddlewareResultFactory, MiddlewareResultFactory][]; +} + +export function wrap( + node: string +): Wrapped; children: DNode | (DNode | DNode[])[] }>> & { + tag: string; +}; +export function wrap( + node: Constructor +): Wrapped>>>; +export function wrap>( + node: T +): Wrapped; children: T['children'] }>>; +export function wrap>( + node: T +): Wrapped; children: T['children'] }>>; +export function wrap>( + node: T +): Wrapped; children: T['children'] }>>; +export function wrap(node: any): any { + const id = uuid(); + const nodeFactory: any = (properties: any, children: any[]) => { + const dNode: any = + typeof node === 'string' ? v(node, properties, children) : w(node as any, properties, children); + dNode.id = id; + return dNode; + }; + nodeFactory.id = id; + nodeFactory.isFactory = true; + if (typeof node === 'string') { + nodeFactory.tag = nodeFactory; + } + return nodeFactory; +} + +export function compare(compareFunc: (actual: unknown, expected: unknown) => boolean): CompareFunc { + (compareFunc as any).type = 'compare'; + return compareFunc as any; +} + +const factory = create(); + +export function renderer(renderFunc: () => WNode, options: RendererOptions = {}): RendererAPI { + let invalidated = true; + let wNode = renderFunc(); + let expectedRenderResult: RenderResult; + let renderResult: RenderResult; + let widget: WidgetBase | Callback; + let middleware: any = {}; + let properties: any = {}; + let children: any = []; + let customDiffs: any[] = []; + let customDiffNames: string[] = []; + let childInstructions = new Map(); + let propertyInstructions: PropertyInstruction[] = []; + let mockMiddleware = options.middleware || []; + + if (isWidgetFunction(wNode.widgetConstructor)) { + widget = wNode.widgetConstructor; + + const resolveMiddleware = (middlewares: any, mocks: any[]) => { + const keys = Object.keys(middlewares); + const results: any = {}; + const uniqueId = `${middlewareId++}`; + const mockMiddlewareMap = new Map(mocks); + + for (let i = 0; i < keys.length; i++) { + let isMock = false; + let middleware = middlewares[keys[i]](); + if (mockMiddlewareMap.has(middlewares[keys[i]])) { + middleware = mockMiddlewareMap.get(middlewares[keys[i]]); + isMock = true; + } + const payload: any = { + id: uniqueId, + properties: () => { + return { ...properties }; + }, + children: () => { + return children; + } + }; + if (middleware.middlewares) { + const resolvedMiddleware = resolveMiddleware(middleware.middlewares, mocks); + payload.middleware = resolvedMiddleware; + results[keys[i]] = middleware.callback(payload); + } else { + if (isMock) { + let result = middleware(); + const resolvedMiddleware = resolveMiddleware(result.middlewares, mocks); + payload.middleware = resolvedMiddleware; + results[keys[i]] = result.callback(payload); + } else { + results[keys[i]] = middleware.callback(payload); + } + } + } + return results; + }; + mockMiddleware.push([ + invalidator, + factory(() => () => { + invalidated = true; + }) + ]); + mockMiddleware.push([destroy, factory(() => () => {})]); + mockMiddleware.push([ + diffProperty, + factory(() => (propName: string, func: any) => { + if (customDiffNames.indexOf(propName) === -1) { + customDiffNames.push(propName); + customDiffs.push(func); + } + }) + ]); + middleware = resolveMiddleware((wNode.widgetConstructor as any).middlewares, mockMiddleware); + } else { + const widgetConstructor = wNode.widgetConstructor as Constructor; + if (typeof widgetConstructor === 'function') { + widget = new class extends widgetConstructor { + invalidate() { + invalidated = true; + super.invalidate(); + } + }(); + _tryRender(); + } else { + throw new Error('Renderer does not support registry items'); + } + } + + function _tryRender() { + let render: RenderResult; + const wNode = renderFunc(); + if (isWidgetFunction(widget)) { + customDiffs.forEach((diff) => diff(properties, wNode.properties)); + propertiesDiff( + properties, + wNode.properties, + () => { + invalidated = true; + }, + [...customDiffNames] + ); + if (children.length || wNode.children.length) { + invalidated = true; + } + properties = { ...wNode.properties }; + children = wNode.children; + if (invalidated) { + render = widget({ id: 'test', middleware, properties: () => properties, children: () => children }); + } + } else { + widget.__setProperties__(wNode.properties); + widget.__setChildren__(wNode.children); + if (invalidated) { + render = widget.__render__(); + } + } + if (invalidated) { + let { hasDeferredProperties, nodes } = decorateNodes(render); + if (hasDeferredProperties) { + nodes = decorateNodes(render).nodes; + } + renderResult = nodes; + invalidated = false; + } + } + + function _expect(expectedRenderFunc: AssertionTemplateResult) { + if (expectedRenderResult && propertyInstructions.length > 0) { + propertyInstructions.forEach((instruction) => { + decorate(renderResult, expectedRenderResult, new Map([[instruction.id, instruction]])); + }); + + propertyInstructions = []; + } + + _tryRender(); + expectedRenderResult = expectedRenderFunc(); + expectedRenderResult = decorateNodes(expectedRenderResult).nodes; + decorate(renderResult, expectedRenderResult, childInstructions); + assertRender(renderResult, expectedRenderResult); + } + + return { + child(wrapped: any, params: any) { + childInstructions.set(wrapped.id, { wrapped, params, type: 'child' }); + invalidated = true; + }, + property(wrapped: any, key: any, params: any = []) { + if (!expectedRenderResult) { + throw new Error('To use `.property` please perform an initial expect'); + } + propertyInstructions.push({ id: wrapped.id, wrapped, params, type: 'property', key }); + }, + expect(expectedRenderFunc: AssertionTemplateResult) { + return _expect(expectedRenderFunc); + } + }; +} + +export default renderer; diff --git a/tests/core/unit/vdom.tsx b/tests/core/unit/vdom.tsx index 21c5110ab..02151e889 100644 --- a/tests/core/unit/vdom.tsx +++ b/tests/core/unit/vdom.tsx @@ -3867,7 +3867,6 @@ jsdomDescribe('vdom', () => { } const currentStart = icache.getOrSet('currentStart', 1); - console.log(currentStart, start); const nodes = [
p
]; for (let i = currentStart; i < 10; i++) { diff --git a/tests/routing/unit/ActiveLink.ts b/tests/routing/unit/ActiveLink.ts index 266e228d0..11b7f5272 100644 --- a/tests/routing/unit/ActiveLink.ts +++ b/tests/routing/unit/ActiveLink.ts @@ -7,7 +7,8 @@ import Link from '../../../src/routing/Link'; import ActiveLink from '../../../src/routing/ActiveLink'; import { w, create, getRegistry } from '../../../src/core/vdom'; -import harness from '../../../src/testing/harness'; +import renderer, { wrap } from '../../../src/testing/renderer'; +import assertionTemplate from '../../../src/testing/assertionTemplate'; const registry = new Registry(); @@ -64,25 +65,28 @@ const mockGetRegistry = factory(() => { describe('ActiveLink', () => { it('Should add and remove active class as the outlet match status changes', () => { router.setPath('/other'); - const h = harness(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo', undefined, null] }), { + const r = renderer(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo', undefined, null] }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => w(Link, { classes: [], to: 'foo' })); + const WrappedLink = wrap(Link); + const template = assertionTemplate(() => w(WrappedLink, { classes: [], to: 'foo' })); + r.expect(template); router.setPath('/foo'); - h.expect(() => w(Link, { classes: ['foo', undefined, null], to: 'foo' })); + r.expect(template.setProperty(WrappedLink, 'classes', ['foo', undefined, null])); }); it('Should render the ActiveLink children', () => { router.setPath('/foo'); - const h = harness(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo'] }, ['hello']), { + const r = renderer(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo'] }, ['hello']), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => w(Link, { classes: ['foo'], to: 'foo' }, ['hello'])); + const template = assertionTemplate(() => w(Link, { classes: ['foo'], to: 'foo' }, ['hello'])); + r.expect(template); }); it('Should render the ActiveLink children when matching query params', () => { router.setPath('/query/path?query=query'); - const h = harness( + const r = renderer( () => w(ActiveLink, { to: 'query', params: { path: 'path', query: 'query' }, activeClasses: ['foo'] }, [ 'hello' @@ -91,48 +95,54 @@ describe('ActiveLink', () => { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(() => w(Link, { classes: ['foo'], to: 'query', params: { path: 'path', query: 'query' } }, ['hello'])); + const template = assertionTemplate(() => + w(Link, { classes: ['foo'], to: 'query', params: { path: 'path', query: 'query' } }, ['hello']) + ); + r.expect(template); }); it('Should mix the active class onto existing string class when the outlet is active', () => { router.setPath('/foo'); - const h = harness(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo'], classes: 'bar' }), { + const r = renderer(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo'], classes: 'bar' }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => w(Link, { classes: ['bar', 'foo'], to: 'foo' })); + const template = assertionTemplate(() => w(Link, { classes: ['bar', 'foo'], to: 'foo' })); + r.expect(template); }); it('Should mix the active class onto existing array of classes when the outlet is active', () => { router.setPath('/foo'); - const h = harness(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo', 'qux'], classes: ['bar', 'baz'] }), { + const r = renderer(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo', 'qux'], classes: ['bar', 'baz'] }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => w(Link, { classes: ['bar', 'baz', 'foo', 'qux'], to: 'foo' })); + const template = assertionTemplate(() => w(Link, { classes: ['bar', 'baz', 'foo', 'qux'], to: 'foo' })); + r.expect(template); }); it('Should support changing the target outlet', () => { router.setPath('/foo'); let properties: any = { to: 'foo', activeClasses: ['foo'] }; - - const h = harness(() => w(ActiveLink, properties), { + const WrappedLink = wrap(Link); + const r = renderer(() => w(ActiveLink, properties), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => w(Link, { to: 'foo', classes: ['foo'] })); + const template = assertionTemplate(() => w(WrappedLink, { to: 'foo', classes: ['foo'] })); + r.expect(template); properties = { to: 'other', activeClasses: ['foo'] }; - h.expect(() => w(Link, { to: 'other', classes: [] })); + r.expect(template.setProperties(WrappedLink, { to: 'other', classes: [] })); router.setPath('/foo/bar'); - h.expect(() => w(Link, { to: 'other', classes: [] })); + r.expect(template.setProperties(WrappedLink, { to: 'other', classes: [] })); router.setPath('/other'); - h.expect(() => w(Link, { to: 'other', classes: ['foo'] })); + r.expect(template.setProperties(WrappedLink, { to: 'other', classes: ['foo'] })); }); it('Should look at route params when determining active', () => { router.setPath('/param/one'); - const h1 = harness( + const r1 = renderer( () => w(ActiveLink, { to: 'suffixed-param', @@ -145,9 +155,11 @@ describe('ActiveLink', () => { middleware: [[getRegistry, mockGetRegistry]] } ); - h1.expect(() => w(Link, { to: 'suffixed-param', classes: ['foo'], params: { suffix: 'one' } })); + r1.expect( + assertionTemplate(() => w(Link, { to: 'suffixed-param', classes: ['foo'], params: { suffix: 'one' } })) + ); - const h2 = harness( + const r2 = renderer( () => w(ActiveLink, { to: 'suffixed-param', @@ -160,19 +172,19 @@ describe('ActiveLink', () => { middleware: [[getRegistry, mockGetRegistry]] } ); - h2.expect(() => w(Link, { to: 'suffixed-param', classes: [], params: { suffix: 'two' } })); + r2.expect(assertionTemplate(() => w(Link, { to: 'suffixed-param', classes: [], params: { suffix: 'two' } }))); }); it('Should be able to check for an exact match', () => { router.setPath('/param/suffix'); - let h = harness(() => w(ActiveLink, { to: 'param', activeClasses: ['foo'] }), { + let r = renderer(() => w(ActiveLink, { to: 'param', activeClasses: ['foo'] }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => w(Link, { classes: ['foo'], to: 'param' })); + r.expect(assertionTemplate(() => w(Link, { classes: ['foo'], to: 'param' }))); - h = harness(() => w(ActiveLink, { to: 'param', activeClasses: ['foo'], isExact: true }), { + r = renderer(() => w(ActiveLink, { to: 'param', activeClasses: ['foo'], isExact: true }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => w(Link, { classes: [], to: 'param' })); + r.expect(assertionTemplate(() => w(Link, { classes: [], to: 'param' }))); }); }); diff --git a/tests/routing/unit/Link.ts b/tests/routing/unit/Link.ts index 45f479fa4..6782987d2 100644 --- a/tests/routing/unit/Link.ts +++ b/tests/routing/unit/Link.ts @@ -7,7 +7,8 @@ import { Registry } from '../../../src/core/Registry'; import { Link } from '../../../src/routing/Link'; import { Router } from '../../../src/routing/Router'; import { MemoryHistory } from '../../../src/routing/history/MemoryHistory'; -import harness from '../../../src/testing/harness'; +import renderer, { wrap } from '../../../src/testing/renderer'; +import assertionTemplate from '../../../src/testing/assertionTemplate'; const registry = new Registry(); @@ -37,7 +38,7 @@ function createMockEvent( metaKey: false, ctrlKey: false } -) { +): MouseEvent { const { ctrlKey = false, metaKey = false, isRightClick = false } = options; return { @@ -48,7 +49,7 @@ function createMockEvent( button: isRightClick ? undefined : 0, metaKey, ctrlKey - }; + } as any; } const noop: any = () => {}; @@ -71,35 +72,39 @@ describe('Link', () => { }); it('Generate link component for basic outlet', () => { - const h = harness(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => v('a', { href: 'foo', onclick: noop })); + const r = renderer(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); + r.expect(assertionTemplate(() => v('a', { href: 'foo', onclick: noop }))); }); it('Generate link component for outlet with specified params', () => { - const h = harness(() => w(Link, { to: 'foo2', params: { foo: 'foo' } }), { + const r = renderer(() => w(Link, { to: 'foo2', params: { foo: 'foo' } }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => v('a', { href: 'foo/foo', onclick: noop })); + r.expect(assertionTemplate(() => v('a', { href: 'foo/foo', onclick: noop }))); }); it('Generate link component for fixed href', () => { - const h = harness(() => w(Link, { to: '#foo/static', isOutlet: false }), { + const r = renderer(() => w(Link, { to: '#foo/static', isOutlet: false }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => v('a', { href: '#foo/static', onclick: noop })); + r.expect(assertionTemplate(() => v('a', { href: '#foo/static', onclick: noop }))); }); it('Set router path on click', () => { - const h = harness(() => w(Link, { to: '#foo/static', isOutlet: false }), { + const WrappedAnchor = wrap('a'); + const r = renderer(() => w(Link, { to: '#foo/static', isOutlet: false }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => v('a', { href: '#foo/static', onclick: noop })); - h.trigger('a', 'onclick', createMockEvent()); + const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: '#foo/static', onclick: noop })); + r.expect(template); + r.property(WrappedAnchor, 'onclick', createMockEvent()); + r.expect(template); assert.isTrue(routerSetPathSpy.calledWith('#foo/static')); }); it('Custom onClick handler can prevent default', () => { - const h = harness( + const WrappedAnchor = wrap('a'); + const r = renderer( () => w(Link, { to: 'foo', @@ -110,44 +115,58 @@ describe('Link', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(() => v('a', { href: 'foo', registry, onclick: noop })); - h.trigger('a', 'onclick', createMockEvent()); + const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', registry, onclick: noop })); + r.expect(template); + r.property(WrappedAnchor, 'onclick', createMockEvent()); + r.expect(template); assert.isTrue(routerSetPathSpy.notCalled); }); it('Does not set router path when target attribute is set', () => { - const h = harness(() => w(Link, { to: 'foo', target: '_blank' }), { + const WrappedAnchor = wrap('a'); + const r = renderer(() => w(Link, { to: 'foo', target: '_blank' }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => v('a', { href: 'foo', onclick: noop })); - h.trigger('a', 'onclick', createMockEvent()); + const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); + r.expect(template); + r.property(WrappedAnchor, 'onclick', createMockEvent()); + r.expect(template); assert.isTrue(routerSetPathSpy.notCalled); }); it('Does not set router path on right click', () => { - const h = harness(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => v('a', { href: 'foo', onclick: noop })); - h.trigger('a', 'onclick', createMockEvent({ isRightClick: true })); + const WrappedAnchor = wrap('a'); + const r = renderer(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); + const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); + r.expect(template); + r.property(WrappedAnchor, 'onclick', createMockEvent({ isRightClick: true })); + r.expect(template); assert.isTrue(routerSetPathSpy.notCalled); }); it('Does not set router path on ctrl click', () => { - const h = harness(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => v('a', { href: 'foo', onclick: noop })); - h.trigger('a', 'onclick', createMockEvent({ ctrlKey: true })); + const WrappedAnchor = wrap('a'); + const r = renderer(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); + const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); + r.expect(template); + r.property(WrappedAnchor, 'onclick', createMockEvent({ isRightClick: true })); + r.expect(template); assert.isTrue(routerSetPathSpy.notCalled); }); it('Does not set router path on meta click', () => { - const h = harness(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(() => v('a', { href: 'foo', onclick: noop })); - h.trigger('a', 'onclick', createMockEvent({ metaKey: true })); + const WrappedAnchor = wrap('a'); + const r = renderer(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); + const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); + r.expect(template); + r.property(WrappedAnchor, 'onclick', createMockEvent({ isRightClick: true })); + r.expect(template); assert.isTrue(routerSetPathSpy.notCalled); }); it('throw error if the injected router cannot be found with the router key', () => { try { - harness(() => w(Link, { to: '#foo/static', isOutlet: false, routerKey: 'fake-key' }), { + renderer(() => w(Link, { to: '#foo/static', isOutlet: false, routerKey: 'fake-key' }), { middleware: [[getRegistry, mockGetRegistry]] }); assert.fail('Should throw an error when the injected router cannot be found with the routerKey'); diff --git a/tests/routing/unit/Route.ts b/tests/routing/unit/Route.ts index 8dd092d2b..f9eda0863 100644 --- a/tests/routing/unit/Route.ts +++ b/tests/routing/unit/Route.ts @@ -7,7 +7,8 @@ import { Route } from '../../../src/routing/Route'; import { Registry } from '../../../src/core/Registry'; import { registerRouterInjector } from '../../../src/routing/RouterInjector'; import { w, create, getRegistry } from '../../../src/core/vdom'; -import harness from '../../../src/testing/harness'; +import renderer from '../../../src/testing/renderer'; +import assertionTemplate from '../../../src/testing/assertionTemplate'; class Widget extends WidgetBase { render() { @@ -54,7 +55,7 @@ describe('Route', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); router.setPath('/foo'); - const h = harness( + const r = renderer( () => w(Route, { id: 'foo', @@ -64,14 +65,14 @@ describe('Route', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(() => w(Widget, {}, [])); + r.expect(assertionTemplate(() => w(Widget, {}, []))); }); it('Should set the type as index for exact matches', () => { let matchType: string | undefined; const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); router.setPath('/foo'); - const h = harness( + const r = renderer( () => w(Route, { id: 'foo', @@ -82,7 +83,7 @@ describe('Route', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(() => null); + r.expect(assertionTemplate(() => null)); assert.strictEqual(matchType, 'index'); }); @@ -90,7 +91,7 @@ describe('Route', () => { let matchType: string | undefined; const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); router.setPath('/foo/other'); - const h = harness( + const r = renderer( () => w(Route, { id: 'foo', @@ -101,7 +102,7 @@ describe('Route', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(() => null); + r.expect(assertionTemplate(() => null)); assert.strictEqual(matchType, 'error'); }); @@ -116,7 +117,7 @@ describe('Route', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); router.setPath('/other'); - const h = harness( + const r = renderer( () => w(Route, { id: 'foo', @@ -128,6 +129,6 @@ describe('Route', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(() => null); + r.expect(assertionTemplate(() => null)); }); }); diff --git a/tests/testing/unit/all.ts b/tests/testing/unit/all.ts index 0c7a21069..9214a4e7a 100644 --- a/tests/testing/unit/all.ts +++ b/tests/testing/unit/all.ts @@ -1,5 +1,5 @@ -import './harness'; -import './harnessWithTsx'; -import './mocks/middleware/all'; +import './harness/all'; import './assertionTemplate'; -import './support/all'; +import './assertRender'; +import './renderer'; +import './mocks/middleware/all'; diff --git a/tests/testing/unit/assertRender.tsx b/tests/testing/unit/assertRender.tsx new file mode 100644 index 000000000..9c1c01f8d --- /dev/null +++ b/tests/testing/unit/assertRender.tsx @@ -0,0 +1,158 @@ +const { describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import Set from '../../../src/shim/Set'; +import Map from '../../../src/shim/Map'; +import assertRender from '../../../src/testing/assertRender'; +import { v, w } from '../../../src/core/vdom'; +import WidgetBase from '../../../src/core/WidgetBase'; + +class MockWidget extends WidgetBase { + render() { + return v('div'); + } +} + +class OtherWidget extends WidgetBase { + render() { + return v('div', { key: 'one', classes: 'class' }, ['text node', undefined, '', null, w(MockWidget, {})]); + } +} + +class FalsyChildren extends WidgetBase { + render() { + return v('div', { key: 'one', classes: 'class' }, [undefined, '', null]); + } +} + +class ChildWidget extends WidgetBase {} + +class WidgetWithMap extends WidgetBase { + render() { + const bar = new Set(); + bar.add('foo'); + const foo = new Map(); + foo.set('a', 'a'); + return w(ChildWidget, { foo, bar }); + } +} + +function getExpectedError() { + const widgetName = (MockWidget as any).name || 'Widget-5'; + return ` +(A)
+(E)
+(E) +(E) text node +(E) other +(E) +(E) +(E) + text node +(A) <${widgetName}> +(A) +(E) +(E) +
`; +} + +describe('new/assertRender', () => { + it('should create an informative error message', () => { + const widget = new OtherWidget(); + const renderResult = widget.__render__(); + try { + assertRender( + renderResult, + v('div', { extras: 'foo', key: 'two', classes: 'other' }, [ + v('span', ['text node', 'other']), + v('span'), + 'text node', + v('span') + ]) + ); + assert.fail(); + } catch (e) { + assert.strictEqual(e.message, getExpectedError()); + } + }); + + it('Should not throw when actual and expected match', () => { + const widget = new OtherWidget(); + const renderResult = widget.__render__(); + assert.doesNotThrow(() => { + assertRender(renderResult, renderResult); + }); + }); + + it('Should not throw when actual and expected but properties are ordered differently', () => { + const widget = new OtherWidget(); + const renderResult = widget.__render__(); + assert.doesNotThrow(() => { + assertRender( + renderResult, + v('div', { classes: 'class', key: 'one' }, ['text node', undefined, '', null, w(MockWidget, {})]) + ); + }); + }); + + it('Should not throw when all the children are falsy', () => { + const widget = new FalsyChildren(); + const renderResult = widget.__render__(); + assert.doesNotThrow(() => { + assertRender(renderResult, v('div', { classes: 'class', key: 'one' }, [undefined, '', null])); + }); + }); + + it('Should throw when actual and expected do not match', () => { + const widget = new OtherWidget(); + const renderResult = widget.__render__(); + assert.throws(() => { + assertRender(renderResult, v('div', { key: 'one', classes: 'class' }, ['text node', v('span')])); + }); + }); + + it('Should not throw when map and set properties are equal', () => { + const bar = new Set(); + bar.add('foo'); + const foo = new Map(); + foo.set('a', 'a'); + const widget = new WidgetWithMap(); + const renderResult = widget.__render__(); + assert.doesNotThrow(() => { + assertRender(renderResult, w(ChildWidget, { bar, foo })); + }); + }); + + it('Should throw when a map property is not equal', () => { + const bar = new Set(); + bar.add('foo'); + const foo = new Map(); + foo.set('a', 'b'); + const widget = new WidgetWithMap(); + const renderResult = widget.__render__(); + assert.throws(() => { + assertRender(renderResult, w(ChildWidget, { bar, foo })); + }); + }); + + it('Should throw when a set property is not equal', () => { + const bar = new Set(); + bar.add('bar'); + const foo = new Map(); + foo.set('a', 'a'); + const widget = new WidgetWithMap(); + const renderResult = widget.__render__(); + assert.throws(() => { + assertRender(renderResult, w(ChildWidget, { bar, foo })); + }); + }); + + it('Should ignore non-nodes', () => { + assert.doesNotThrow(() => { + assertRender( + v('div', { key: 'one' }, [{ something: 'else' } as any, v('div', ['hello'])]), + v('div', { key: 'one' }, [{ something: 'else' } as any, v('div', ['hello'])]) + ); + }); + }); +}); diff --git a/tests/testing/unit/assertionTemplate.tsx b/tests/testing/unit/assertionTemplate.tsx index 31782b933..965f0e75f 100644 --- a/tests/testing/unit/assertionTemplate.tsx +++ b/tests/testing/unit/assertionTemplate.tsx @@ -1,11 +1,11 @@ -const { describe, it, after, afterEach } = intern.getInterface('bdd'); +const { describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -import { stub } from 'sinon'; -import { harness } from '../../../src/testing/harness'; +import { renderer, wrap } from '../../../src/testing/renderer'; import { WidgetBase } from '../../../src/core/WidgetBase'; import { v, w, tsx, create } from '../../../src/core/vdom'; import assertionTemplate, { Ignore } from '../../../src/testing/assertionTemplate'; +import { DNode } from '../../../src/core/interfaces'; class MyWidget extends WidgetBase<{ toggleProperty?: boolean; @@ -45,11 +45,16 @@ class MyWidget extends WidgetBase<{ } } +const WrappedRoot = wrap('div'); +const WrappedHeader = wrap('h2'); +const WrappedList = wrap('ul'); +const WrappedListItem = wrap('li'); + const baseAssertion = assertionTemplate(() => - v('div', { '~key': 'root', classes: ['root'] }, [ - v('h2', { '~key': 'header' }, ['hello']), + v(WrappedRoot.tag, { classes: ['root'] }, [ + v(WrappedHeader.tag, {}, ['hello']), undefined, - v('ul', [v('li', { '~key': 'li-one', foo: 'a' }, ['one']), v('li', ['two']), v('li', ['three'])]), + v(WrappedList.tag, [v(WrappedListItem.tag, { foo: 'a' }, ['one']), v('li', ['two']), v('li', ['three'])]), undefined ]) ); @@ -64,7 +69,7 @@ class ListWidget extends WidgetBase { } } -const baseListAssertion = assertionTemplate(() => v('div', { classes: ['root'] }, [v('ul', [])])); +const baseListAssertion = assertionTemplate(() => v('div', { classes: ['root'] }, [v(WrappedList.tag, [])])); class MultiRootWidget extends WidgetBase<{ after?: boolean; last?: boolean }> { render() { @@ -80,114 +85,122 @@ class MultiRootWidget extends WidgetBase<{ after?: boolean; last?: boolean }> { } } +const WrappedFirst = wrap('div'); +const WrappedSecond = wrap('div'); + const baseMultiRootAssertion = assertionTemplate(() => [ - v('div', { '~key': 'first' }, ['first']), - v('div', { '~key': 'last' }, ['last']) + v(WrappedFirst.tag, ['first']), + v(WrappedSecond.tag, ['last']) ]); const tsxAssertion = assertionTemplate(() => (

hello

    -
  • - one -
  • + one
  • two
  • three
)); -let consoleStub = stub(console, 'warn'); - -describe('assertionTemplate', () => { - afterEach(() => { - consoleStub.resetHistory(); - }); - - after(() => { - consoleStub.restore(); - }); - +describe('new/assertionTemplate', () => { it('can get a property', () => { - const classes = baseAssertion.getProperty('~root', 'classes'); + const classes = baseAssertion.getProperty(WrappedRoot, 'classes'); assert.deepEqual(classes, ['root']); }); it('can get properties', () => { - const properties = baseAssertion.getProperties('~root'); - assert.deepEqual(properties, { '~key': 'root', classes: ['root'] }); + const properties = baseAssertion.getProperties(WrappedRoot); + assert.deepEqual(properties, { classes: ['root'] }); }); it('can get a child', () => { - const children = baseAssertion.getChildren('~header'); - assert.equal(children[0], 'hello'); + const children = baseAssertion.getChildren(WrappedHeader); + // TODO this is pony + assert.equal(Array.isArray(children) ? children[0] : children, 'hello'); }); it('can assert a base assertion', () => { - const h = harness(() => w(MyWidget, {})); - h.expect(baseAssertion); + const r = renderer(() => w(MyWidget, {})); + r.expect(baseAssertion); }); it('can set a property', () => { - const h = harness(() => w(MyWidget, { toggleProperty: true })); - const propertyAssertion = baseAssertion.setProperty('~li-one', 'foo', 'b'); - h.expect(propertyAssertion); + const r = renderer(() => w(MyWidget, { toggleProperty: true })); + r.expect(baseAssertion.setProperty(WrappedListItem, 'foo', 'b')); }); it('can set properties', () => { - const h = harness(() => w(MyWidget, { toggleProperty: true })); - const propertyAssertion = baseAssertion.setProperties('~li-one', { foo: 'b' }); - h.expect(propertyAssertion); + const r = renderer(() => w(MyWidget, { toggleProperty: true })); + r.expect(baseAssertion.setProperties(WrappedListItem, { foo: 'b' })); + }); + + it('can set properties on a widget', () => { + class MyClassWidget extends WidgetBase<{ foo: string; bar?: number }> {} + const MyFunctionWidget = create().properties<{ foo: string; bar?: number }>()(function MyFunctionWidget() { + return 'test'; + }); + const MyWidget = create()(function MyWidget() { + return ( +
+ + +
+ ); + }); + const WrappedClassWidget = wrap(MyClassWidget); + const WrappedFunctionWidget = wrap(MyFunctionWidget); + const template = assertionTemplate(() => ( +
+ + +
+ )); + + const r = renderer(() => ); + const updatedTemplate = template + .setProperty(WrappedClassWidget, 'foo', 'foo') + // type error as property type is not correct + // .setProperty(WrappedClassWidget, 'foo', 1) + .setProperty(WrappedClassWidget, 'bar', 1) + .setProperty(WrappedFunctionWidget, 'foo', 'foo') + .setProperty(WrappedFunctionWidget, 'bar', 1); + + r.expect(updatedTemplate); }); it('can set properties and use the actual properties', () => { - const h = harness(() => w(MyWidget, { toggleProperty: true })); - const propertyAssertion = baseAssertion.setProperties('~li-one', (actualProps: any) => { + const propertyAssertion = baseAssertion.setProperties(WrappedListItem, (actualProps: any) => { return actualProps; }); - h.expect(propertyAssertion); + const r = renderer(() => w(MyWidget, { toggleProperty: true })); + r.expect(propertyAssertion); }); it('can remove a node', () => { - const h = harness(() => w(MyWidget, { removeHeader: true })); - const childAssertion = baseAssertion.remove('~header'); - h.expect(childAssertion); + const r = renderer(() => w(MyWidget, { removeHeader: true })); + r.expect(baseAssertion.remove(WrappedHeader)); }); it('can remove a node in the root', () => { - const h = harness(() => w(MultiRootWidget, { last: false })); - const insertionAssertion = baseMultiRootAssertion.remove('~last'); - h.expect(insertionAssertion); + const r = renderer(() => w(MultiRootWidget, { last: false })); + r.expect(baseMultiRootAssertion.remove(WrappedSecond)); }); it('can replace a node', () => { - const h = harness(() => w(MyWidget, { replaceChild: true })); - const childAssertion = baseAssertion.replace('~header', v('h2', ['replace'])); - h.expect(childAssertion); + const r = renderer(() => w(MyWidget, { replaceChild: true })); + r.expect(baseAssertion.replace(WrappedHeader, v('h2', ['replace']))); }); it('can replace a node in the root', () => { - const h = harness(() => w(MyWidget, { removeHeader: true, before: true })); - const childAssertion = baseAssertion.replace('~header', v('span', ['before'])); - h.expect(childAssertion); + const r = renderer(() => w(MyWidget, { removeHeader: true, before: true })); + r.expect(baseAssertion.replace(WrappedHeader, v('span', ['before']))); }); it('can set a child', () => { - const h = harness(() => w(MyWidget, { replaceChild: true })); - const childAssertion = baseAssertion.setChildren('~header', ['replace']); - assert.isTrue(consoleStub.calledOnce); - assert.strictEqual( - consoleStub.firstCall.args[0], - 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' - ); - h.expect(childAssertion); - }); - - it('can set a child with a factory function', () => { - const h = harness(() => w(MyWidget, { replaceChild: true })); - const childAssertion = baseAssertion.setChildren('~header', () => ['replace']); - h.expect(childAssertion); + const r = renderer(() => w(MyWidget, { replaceChild: true })); + r.expect(baseAssertion.setChildren(WrappedHeader, () => ['replace'])); }); it('children set should be immutable', () => { @@ -210,146 +223,87 @@ describe('assertionTemplate', () => { ); }); - const baseAssertion = assertionTemplate(() => v('div', { key: 'parent', classes: ['root'] }, [])); + const WrappedParent = wrap('div'); + const WrappedChild = wrap('div'); - const childAssertion = baseAssertion.setChildren('@parent', () => [
hello
]); + const baseAssertion = assertionTemplate(() => v(WrappedParent.tag, { key: 'parent', classes: ['root'] }, [])); - const h = harness(() => w(Widget, {})); - const h1 = harness(() => w(WidgetWithProps, {})); - h.expect(childAssertion); - const propertyAssertion = childAssertion.setProperty('@child', 'disabled', true); - h1.expect(propertyAssertion); - h.expect(childAssertion); - }); + const childAssertion = baseAssertion.setChildren(WrappedParent, () => [ + hello + ]); - it('can set a child with replace', () => { - const h = harness(() => w(MyWidget, { replaceChild: true })); - const childAssertion = baseAssertion.replaceChildren('~header', ['replace']); - assert.isTrue(consoleStub.calledOnce); - assert.strictEqual( - consoleStub.firstCall.args[0], - 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' - ); - h.expect(childAssertion); + const r = renderer(() => w(Widget, {})); + r.expect(childAssertion); + const h1 = renderer(() => w(WidgetWithProps, {})); + h1.expect(childAssertion.setProperty(WrappedChild, 'disabled', true)); + r.expect(childAssertion); }); - it('can set a child with replace with a factory function', () => { - const h = harness(() => w(MyWidget, { replaceChild: true })); - const childAssertion = baseAssertion.replaceChildren('~header', () => ['replace']); - h.expect(childAssertion); + it('can set a child with replace', () => { + const r = renderer(() => w(MyWidget, { replaceChild: true })); + r.expect(baseAssertion.replaceChildren(WrappedHeader, () => ['replace'])); }); it('can set a child with prepend', () => { - const h = harness(() => w(MyWidget, { prependChild: true })); - const childAssertion = baseAssertion.prepend('~header', ['prepend']); - assert.isTrue(consoleStub.calledOnce); - assert.strictEqual( - consoleStub.firstCall.args[0], - 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' - ); - h.expect(childAssertion); - }); - - it('can set a child with prepend with a factory function', () => { - const h = harness(() => w(MyWidget, { prependChild: true })); - const childAssertion = baseAssertion.prepend('~header', () => ['prepend']); - h.expect(childAssertion); + const r = renderer(() => w(MyWidget, { prependChild: true })); + r.expect(baseAssertion.prepend(WrappedHeader, () => ['prepend'])); }); it('can set a child with append', () => { - const h = harness(() => w(MyWidget, { appendChild: true })); - const childAssertion = baseAssertion.append('~header', ['append']); - assert.isTrue(consoleStub.calledOnce); - assert.strictEqual( - consoleStub.firstCall.args[0], - 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' - ); - h.expect(childAssertion); - }); - - it('can set a child with append with a factory function', () => { - const h = harness(() => w(MyWidget, { appendChild: true })); - const childAssertion = baseAssertion.append('~header', () => ['append']); - h.expect(childAssertion); + const r = renderer(() => w(MyWidget, { appendChild: true })); + r.expect(baseAssertion.append(WrappedHeader, () => ['append'])); }); it('can set children after with insert', () => { - const h = harness(() => w(MyWidget, { after: true })); - const childAssertion = baseAssertion.insertAfter('ul', [v('span', ['after'])]); - assert.isTrue(consoleStub.calledOnce); - assert.strictEqual( - consoleStub.firstCall.args[0], - 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' - ); - h.expect(childAssertion); - }); - - it('can set children after with insert with a factory function', () => { - const h = harness(() => w(MyWidget, { after: true })); - const childAssertion = baseAssertion.insertAfter('ul', () => [v('span', ['after'])]); - h.expect(childAssertion); + const r = renderer(() => w(MyWidget, { after: true })); + r.expect(baseAssertion.insertAfter(WrappedList, () => [v('span', ['after'])])); }); it('can insert after a node in the root', () => { - const h = harness(() => w(MultiRootWidget, { after: true })); - const insertionAssertion = baseMultiRootAssertion.insertAfter('~first', () => [v('div', {}, ['after'])]); - h.expect(insertionAssertion); + const insertionAssertion = baseMultiRootAssertion.insertAfter(WrappedFirst, () => [v('div', {}, ['after'])]); + const r = renderer(() => w(MultiRootWidget, { after: true })); + r.expect(insertionAssertion); }); it('can set children before with insert', () => { - const h = harness(() => w(MyWidget, { before: true })); - const childAssertion = baseAssertion.insertBefore('ul', [v('span', ['before'])]); - assert.isTrue(consoleStub.calledOnce); - assert.strictEqual( - consoleStub.firstCall.args[0], - 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' - ); - h.expect(childAssertion); - }); - - it('can set children before with insert with a factory function', () => { - const h = harness(() => w(MyWidget, { before: true })); - const childAssertion = baseAssertion.insertBefore('ul', () => [v('span', ['before'])]); - h.expect(childAssertion); + const r = renderer(() => w(MyWidget, { before: true })); + r.expect(baseAssertion.insertBefore(WrappedList, () => [v('span', ['before'])])); }); it('can be used with tsx', () => { - const h = harness(() => ); - const propertyAssertion = tsxAssertion.setProperty('~li-one', 'foo', 'b'); - h.expect(propertyAssertion); + const r = renderer(() => ); + r.expect(tsxAssertion.setProperty(WrappedListItem, 'foo', 'b')); }); it('should throw an error when selector is not found', () => { - const h = harness(() => w(MyWidget, {})); - assert.throws( - () => h.expect(baseAssertion.setProperty('~cant-spell', 'foo', 'b')), - 'Node not found for selector "~cant-spell"' - ); + const UnknownWrapped = wrap('unknown'); + const r = renderer(() => w(MyWidget, {})); + assert.throws(() => r.expect(baseAssertion.setProperty(UnknownWrapped, 'foo', 'b')), 'Unable to find node'); }); it('can use ignore', () => { - const h = harness(() => w(ListWidget, {})); - const nodes = []; + const nodes: DNode[] = []; for (let i = 0; i < 28; i++) { nodes.push(w(Ignore, {})); } - const childListAssertion = baseListAssertion.replaceChildren('ul', [ + const childListAssertion = baseListAssertion.replaceChildren(WrappedList, () => [ v('li', ['item: 0']), ...nodes, v('li', ['item: 29']) ]); - h.expect(childListAssertion); + const r = renderer(() => w(ListWidget, {})); + r.expect(childListAssertion); }); it('should be immutable', () => { const fooAssertion = baseAssertion - .setChildren(':root', ['foo']) - .setProperty(':root', 'foo', true) - .setProperty(':root', 'bar', false); + .setChildren(WrappedRoot, () => ['foo']) + .setProperty(WrappedRoot, 'foo', true) + .setProperty(WrappedRoot, 'bar', false); const barAssertion = fooAssertion - .setChildren(':root', ['bar']) - .setProperty(':root', 'foo', false) - .setProperty(':root', 'bar', true); + .setChildren(WrappedRoot, () => ['bar']) + .setProperty(WrappedRoot, 'foo', false) + .setProperty(WrappedRoot, 'bar', true); const foo = fooAssertion() as any; const bar = barAssertion() as any; diff --git a/tests/testing/unit/harness/all.ts b/tests/testing/unit/harness/all.ts new file mode 100644 index 000000000..4418f11a3 --- /dev/null +++ b/tests/testing/unit/harness/all.ts @@ -0,0 +1,4 @@ +import './support/all'; +import './assertionTemplate'; +import './harness'; +import './harnessWithTsx'; diff --git a/tests/testing/unit/harness/assertionTemplate.tsx b/tests/testing/unit/harness/assertionTemplate.tsx new file mode 100644 index 000000000..a6d94e0a7 --- /dev/null +++ b/tests/testing/unit/harness/assertionTemplate.tsx @@ -0,0 +1,363 @@ +const { describe, it, after, afterEach } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); +import { stub } from 'sinon'; + +import { harness } from '../../../../src/testing/harness/harness'; +import { WidgetBase } from '../../../../src/core/WidgetBase'; +import { v, w, tsx, create } from '../../../../src/core/vdom'; +import assertionTemplate, { Ignore } from '../../../../src/testing/harness/assertionTemplate'; + +class MyWidget extends WidgetBase<{ + toggleProperty?: boolean; + prependChild?: boolean; + appendChild?: boolean; + replaceChild?: boolean; + removeHeader?: boolean; + before?: boolean; + after?: boolean; +}> { + render() { + const { + toggleProperty, + prependChild, + appendChild, + replaceChild, + removeHeader, + before, + after + } = this.properties; + let children = ['hello']; + if (prependChild) { + children = ['prepend', ...children]; + } + if (appendChild) { + children = [...children, 'append']; + } + if (replaceChild) { + children = ['replace']; + } + return v('div', { classes: ['root'] }, [ + removeHeader ? undefined : v('h2', children), + before ? v('span', ['before']) : undefined, + v('ul', [v('li', { foo: toggleProperty ? 'b' : 'a' }, ['one']), v('li', ['two']), v('li', ['three'])]), + after ? v('span', ['after']) : undefined + ]); + } +} + +const baseAssertion = assertionTemplate(() => + v('div', { '~key': 'root', classes: ['root'] }, [ + v('h2', { '~key': 'header' }, ['hello']), + undefined, + v('ul', [v('li', { '~key': 'li-one', foo: 'a' }, ['one']), v('li', ['two']), v('li', ['three'])]), + undefined + ]) +); + +class ListWidget extends WidgetBase { + render() { + let children = []; + for (let i = 0; i < 30; i++) { + children.push(v('li', [`item: ${i}`])); + } + return v('div', { classes: ['root'] }, [v('ul', children)]); + } +} + +const baseListAssertion = assertionTemplate(() => v('div', { classes: ['root'] }, [v('ul', [])])); + +class MultiRootWidget extends WidgetBase<{ after?: boolean; last?: boolean }> { + render() { + const { after, last = true } = this.properties; + const result = [v('div', ['first'])]; + if (after) { + result.push(v('div', ['after'])); + } + if (last) { + result.push(v('div', ['last'])); + } + return result; + } +} + +const baseMultiRootAssertion = assertionTemplate(() => [ + v('div', { '~key': 'first' }, ['first']), + v('div', { '~key': 'last' }, ['last']) +]); + +const tsxAssertion = assertionTemplate(() => ( +
+

hello

+
    +
  • + one +
  • +
  • two
  • +
  • three
  • +
+
+)); + +let consoleStub = stub(console, 'warn'); + +describe('assertionTemplate', () => { + afterEach(() => { + consoleStub.resetHistory(); + }); + + after(() => { + consoleStub.restore(); + }); + + it('can get a property', () => { + const classes = baseAssertion.getProperty('~root', 'classes'); + assert.deepEqual(classes, ['root']); + }); + + it('can get properties', () => { + const properties = baseAssertion.getProperties('~root'); + assert.deepEqual(properties, { '~key': 'root', classes: ['root'] }); + }); + + it('can get a child', () => { + const children = baseAssertion.getChildren('~header'); + assert.equal(children[0], 'hello'); + }); + + it('can assert a base assertion', () => { + const h = harness(() => w(MyWidget, {})); + h.expect(baseAssertion); + }); + + it('can set a property', () => { + const h = harness(() => w(MyWidget, { toggleProperty: true })); + const propertyAssertion = baseAssertion.setProperty('~li-one', 'foo', 'b'); + h.expect(propertyAssertion); + }); + + it('can set properties', () => { + const h = harness(() => w(MyWidget, { toggleProperty: true })); + const propertyAssertion = baseAssertion.setProperties('~li-one', { foo: 'b' }); + h.expect(propertyAssertion); + }); + + it('can set properties and use the actual properties', () => { + const h = harness(() => w(MyWidget, { toggleProperty: true })); + const propertyAssertion = baseAssertion.setProperties('~li-one', (actualProps: any) => { + return actualProps; + }); + h.expect(propertyAssertion); + }); + + it('can remove a node', () => { + const h = harness(() => w(MyWidget, { removeHeader: true })); + const childAssertion = baseAssertion.remove('~header'); + h.expect(childAssertion); + }); + + it('can remove a node in the root', () => { + const h = harness(() => w(MultiRootWidget, { last: false })); + const insertionAssertion = baseMultiRootAssertion.remove('~last'); + h.expect(insertionAssertion); + }); + + it('can replace a node', () => { + const h = harness(() => w(MyWidget, { replaceChild: true })); + const childAssertion = baseAssertion.replace('~header', v('h2', ['replace'])); + h.expect(childAssertion); + }); + + it('can replace a node in the root', () => { + const h = harness(() => w(MyWidget, { removeHeader: true, before: true })); + const childAssertion = baseAssertion.replace('~header', v('span', ['before'])); + h.expect(childAssertion); + }); + + it('can set a child', () => { + const h = harness(() => w(MyWidget, { replaceChild: true })); + const childAssertion = baseAssertion.setChildren('~header', ['replace']); + assert.isTrue(consoleStub.calledOnce); + assert.strictEqual( + consoleStub.firstCall.args[0], + 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' + ); + h.expect(childAssertion); + }); + + it('can set a child with a factory function', () => { + const h = harness(() => w(MyWidget, { replaceChild: true })); + const childAssertion = baseAssertion.setChildren('~header', () => ['replace']); + h.expect(childAssertion); + }); + + it('children set should be immutable', () => { + const factory = create(); + const Widget = factory(function Widget() { + return ( +
+
hello
+
+ ); + }); + + const WidgetWithProps = factory(function Widget() { + return ( +
+
+ hello +
+
+ ); + }); + + const baseAssertion = assertionTemplate(() => v('div', { key: 'parent', classes: ['root'] }, [])); + + const childAssertion = baseAssertion.setChildren('@parent', () => [
hello
]); + + const h = harness(() => w(Widget, {})); + const h1 = harness(() => w(WidgetWithProps, {})); + h.expect(childAssertion); + const propertyAssertion = childAssertion.setProperty('@child', 'disabled', true); + h1.expect(propertyAssertion); + h.expect(childAssertion); + }); + + it('can set a child with replace', () => { + const h = harness(() => w(MyWidget, { replaceChild: true })); + const childAssertion = baseAssertion.replaceChildren('~header', ['replace']); + assert.isTrue(consoleStub.calledOnce); + assert.strictEqual( + consoleStub.firstCall.args[0], + 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' + ); + h.expect(childAssertion); + }); + + it('can set a child with replace with a factory function', () => { + const h = harness(() => w(MyWidget, { replaceChild: true })); + const childAssertion = baseAssertion.replaceChildren('~header', () => ['replace']); + h.expect(childAssertion); + }); + + it('can set a child with prepend', () => { + const h = harness(() => w(MyWidget, { prependChild: true })); + const childAssertion = baseAssertion.prepend('~header', ['prepend']); + assert.isTrue(consoleStub.calledOnce); + assert.strictEqual( + consoleStub.firstCall.args[0], + 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' + ); + h.expect(childAssertion); + }); + + it('can set a child with prepend with a factory function', () => { + const h = harness(() => w(MyWidget, { prependChild: true })); + const childAssertion = baseAssertion.prepend('~header', () => ['prepend']); + h.expect(childAssertion); + }); + + it('can set a child with append', () => { + const h = harness(() => w(MyWidget, { appendChild: true })); + const childAssertion = baseAssertion.append('~header', ['append']); + assert.isTrue(consoleStub.calledOnce); + assert.strictEqual( + consoleStub.firstCall.args[0], + 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' + ); + h.expect(childAssertion); + }); + + it('can set a child with append with a factory function', () => { + const h = harness(() => w(MyWidget, { appendChild: true })); + const childAssertion = baseAssertion.append('~header', () => ['append']); + h.expect(childAssertion); + }); + + it('can set children after with insert', () => { + const h = harness(() => w(MyWidget, { after: true })); + const childAssertion = baseAssertion.insertAfter('ul', [v('span', ['after'])]); + assert.isTrue(consoleStub.calledOnce); + assert.strictEqual( + consoleStub.firstCall.args[0], + 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' + ); + h.expect(childAssertion); + }); + + it('can set children after with insert with a factory function', () => { + const h = harness(() => w(MyWidget, { after: true })); + const childAssertion = baseAssertion.insertAfter('ul', () => [v('span', ['after'])]); + h.expect(childAssertion); + }); + + it('can insert after a node in the root', () => { + const h = harness(() => w(MultiRootWidget, { after: true })); + const insertionAssertion = baseMultiRootAssertion.insertAfter('~first', () => [v('div', {}, ['after'])]); + h.expect(insertionAssertion); + }); + + it('can set children before with insert', () => { + const h = harness(() => w(MyWidget, { before: true })); + const childAssertion = baseAssertion.insertBefore('ul', [v('span', ['before'])]); + assert.isTrue(consoleStub.calledOnce); + assert.strictEqual( + consoleStub.firstCall.args[0], + 'The array API (`children: DNode[]`) has been deprecated. Working with children should use a factory to avoid issues with mutation.' + ); + h.expect(childAssertion); + }); + + it('can set children before with insert with a factory function', () => { + const h = harness(() => w(MyWidget, { before: true })); + const childAssertion = baseAssertion.insertBefore('ul', () => [v('span', ['before'])]); + h.expect(childAssertion); + }); + + it('can be used with tsx', () => { + const h = harness(() => ); + const propertyAssertion = tsxAssertion.setProperty('~li-one', 'foo', 'b'); + h.expect(propertyAssertion); + }); + + it('should throw an error when selector is not found', () => { + const h = harness(() => w(MyWidget, {})); + assert.throws( + () => h.expect(baseAssertion.setProperty('~cant-spell', 'foo', 'b')), + 'Node not found for selector "~cant-spell"' + ); + }); + + it('can use ignore', () => { + const h = harness(() => w(ListWidget, {})); + const nodes = []; + for (let i = 0; i < 28; i++) { + nodes.push(w(Ignore, {})); + } + const childListAssertion = baseListAssertion.replaceChildren('ul', [ + v('li', ['item: 0']), + ...nodes, + v('li', ['item: 29']) + ]); + h.expect(childListAssertion); + }); + + it('should be immutable', () => { + const fooAssertion = baseAssertion + .setChildren(':root', ['foo']) + .setProperty(':root', 'foo', true) + .setProperty(':root', 'bar', false); + const barAssertion = fooAssertion + .setChildren(':root', ['bar']) + .setProperty(':root', 'foo', false) + .setProperty(':root', 'bar', true); + + const foo = fooAssertion() as any; + const bar = barAssertion() as any; + assert.equal(foo!.children[0], 'foo'); + assert.equal(bar!.children[0], 'bar'); + assert.equal(foo!.properties.foo, true); + assert.equal(foo!.properties.bar, false); + assert.equal(bar!.properties.foo, false); + assert.equal(bar!.properties.bar, true); + }); +}); diff --git a/tests/testing/unit/harness.tsx b/tests/testing/unit/harness/harness.tsx similarity index 98% rename from tests/testing/unit/harness.tsx rename to tests/testing/unit/harness/harness.tsx index 09b37d7ba..7ea0d32f4 100644 --- a/tests/testing/unit/harness.tsx +++ b/tests/testing/unit/harness/harness.tsx @@ -1,13 +1,13 @@ const { describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -import { harness } from '../../../src/testing/harness'; -import { WidgetBase } from '../../../src/core/WidgetBase'; -import { v, w, isVNode, tsx, create, diffProperty, invalidator } from '../../../src/core/vdom'; -import Set from '../../../src/shim/Set'; -import Map from '../../../src/shim/Map'; -import { VNode, WNode, WidgetProperties } from '../../../src/core/interfaces'; -import icache from '../../../src/core/middleware/icache'; +import { harness } from '../../../../src/testing/harness/harness'; +import { WidgetBase } from '../../../../src/core/WidgetBase'; +import { v, w, isVNode, tsx, create, diffProperty, invalidator } from '../../../../src/core/vdom'; +import Set from '../../../../src/shim/Set'; +import Map from '../../../../src/shim/Map'; +import { VNode, WNode, WidgetProperties } from '../../../../src/core/interfaces'; +import icache from '../../../../src/core/middleware/icache'; const noop: any = () => {}; diff --git a/tests/testing/unit/harnessWithTsx.tsx b/tests/testing/unit/harness/harnessWithTsx.tsx similarity index 97% rename from tests/testing/unit/harnessWithTsx.tsx rename to tests/testing/unit/harness/harnessWithTsx.tsx index 4fe32293d..ec9e774e6 100644 --- a/tests/testing/unit/harnessWithTsx.tsx +++ b/tests/testing/unit/harness/harnessWithTsx.tsx @@ -1,9 +1,9 @@ const { describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -import { harness } from '../../../src/testing/harness'; -import { WidgetBase } from '../../../src/core/WidgetBase'; -import { tsx, fromRegistry } from '../../../src/core/vdom'; +import { harness } from '../../../../src/testing/harness/harness'; +import { WidgetBase } from '../../../../src/core/WidgetBase'; +import { tsx, fromRegistry } from '../../../../src/core/vdom'; class ChildWidget extends WidgetBase {} const RegistryWidget = fromRegistry('registry-item'); diff --git a/tests/testing/unit/support/all.ts b/tests/testing/unit/harness/support/all.ts similarity index 100% rename from tests/testing/unit/support/all.ts rename to tests/testing/unit/harness/support/all.ts diff --git a/tests/testing/unit/support/assertRender.ts b/tests/testing/unit/harness/support/assertRender.ts similarity index 93% rename from tests/testing/unit/support/assertRender.ts rename to tests/testing/unit/harness/support/assertRender.ts index 30dbe7a22..de6bc22cc 100644 --- a/tests/testing/unit/support/assertRender.ts +++ b/tests/testing/unit/harness/support/assertRender.ts @@ -1,11 +1,11 @@ const { describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -import Set from '../../../../src/shim/Set'; -import Map from '../../../../src/shim/Map'; -import assertRender from '../../../../src/testing/support/assertRender'; -import { v, w } from '../../../../src/core/vdom'; -import WidgetBase from '../../../../src/core/WidgetBase'; +import Set from '../../../../../src/shim/Set'; +import Map from '../../../../../src/shim/Map'; +import assertRender from '../../../../../src/testing/harness/support/assertRender'; +import { v, w } from '../../../../../src/core/vdom'; +import WidgetBase from '../../../../../src/core/WidgetBase'; class MockWidget extends WidgetBase { render() { diff --git a/tests/testing/unit/support/selector.ts b/tests/testing/unit/harness/support/selector.ts similarity index 94% rename from tests/testing/unit/support/selector.ts rename to tests/testing/unit/harness/support/selector.ts index 8b40afda5..33195a5b8 100644 --- a/tests/testing/unit/support/selector.ts +++ b/tests/testing/unit/harness/support/selector.ts @@ -1,10 +1,10 @@ const { describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -import { adapter, parseSelector } from '../../../../src/testing/support/selector'; -import { v, w } from '../../../../src/core/vdom'; -import { WidgetBase } from '../../../../src/core/WidgetBase'; -import { DNode, WNode } from '../../../../src/core/interfaces'; +import { adapter, parseSelector } from '../../../../../src/testing/harness/support/selector'; +import { v, w } from '../../../../../src/core/vdom'; +import { WidgetBase } from '../../../../../src/core/WidgetBase'; +import { DNode, WNode } from '../../../../../src/core/interfaces'; describe('selector', () => { describe('adapter', () => { diff --git a/tests/testing/unit/mocks/middleware/breakpoint.tsx b/tests/testing/unit/mocks/middleware/breakpoint.tsx index 9b2cc0b89..f83a645e7 100644 --- a/tests/testing/unit/mocks/middleware/breakpoint.tsx +++ b/tests/testing/unit/mocks/middleware/breakpoint.tsx @@ -3,7 +3,8 @@ const { describe } = intern.getPlugin('jsdom'); import createBreakpointMock from '../../../../../src/testing/mocks/middleware/breakpoint'; import breakpoint from '../../../../../src/core/middleware/breakpoint'; import { tsx, create } from '../../../../../src/core/vdom'; -import harness from '../../../../../src/testing/harness'; +import renderer from '../../../../../src/testing/renderer'; +import assertionTemplate from '../../../../../src/testing/assertionTemplate'; describe('breakpoint mock', () => { it('should mock breakpoint middleware calls', () => { @@ -13,12 +14,12 @@ describe('breakpoint mock', () => { const breakpointResult = breakpoint.get('root'); return
{JSON.stringify(breakpointResult)}
; }); - const h = harness(() => , { middleware: [[breakpoint, breakpointMock]] }); - h.expect(() =>
null
); + const r = renderer(() => , { middleware: [[breakpoint, breakpointMock]] }); + r.expect(assertionTemplate(() =>
null
)); breakpointMock('root', { breakpoint: 'SM', contentRect: { width: 20 } }); - h.expect(() =>
{'{"breakpoint":"SM","contentRect":{"width":20}}'}
); + r.expect(assertionTemplate(() =>
{'{"breakpoint":"SM","contentRect":{"width":20}}'}
)); breakpointMock('root', { breakpoint: 'XL', contentRect: { width: 1020 } }); - h.expect(() =>
{'{"breakpoint":"XL","contentRect":{"width":1020}}'}
); + r.expect(assertionTemplate(() =>
{'{"breakpoint":"XL","contentRect":{"width":1020}}'}
)); }); it('should deal with multiple mocked keys', () => { @@ -34,27 +35,33 @@ describe('breakpoint mock', () => {
); }); - const h = harness(() => , { middleware: [[breakpoint, breakpointMock]] }); - h.expect(() => ( -
-
null
-
null
-
- )); + const r = renderer(() => , { middleware: [[breakpoint, breakpointMock]] }); + r.expect( + assertionTemplate(() => ( +
+
null
+
null
+
+ )) + ); breakpointMock('root', { breakpoint: 'SM', contentRect: { width: 50 } }); - h.expect(() => ( -
-
{'{"breakpoint":"SM","contentRect":{"width":50}}'}
-
null
-
- )); + r.expect( + assertionTemplate(() => ( +
+
{'{"breakpoint":"SM","contentRect":{"width":50}}'}
+
null
+
+ )) + ); breakpointMock('root', { breakpoint: 'XL', contentRect: { width: 1020 } }); breakpointMock('other', { breakpoint: 'MD', contentRect: { width: 620 } }); - h.expect(() => ( -
-
{'{"breakpoint":"XL","contentRect":{"width":1020}}'}
-
{'{"breakpoint":"MD","contentRect":{"width":620}}'}
-
- )); + r.expect( + assertionTemplate(() => ( +
+
{'{"breakpoint":"XL","contentRect":{"width":1020}}'}
+
{'{"breakpoint":"MD","contentRect":{"width":620}}'}
+
+ )) + ); }); }); diff --git a/tests/testing/unit/mocks/middleware/focus.tsx b/tests/testing/unit/mocks/middleware/focus.tsx index b4354cc7f..22029b2df 100644 --- a/tests/testing/unit/mocks/middleware/focus.tsx +++ b/tests/testing/unit/mocks/middleware/focus.tsx @@ -3,7 +3,8 @@ const { describe } = intern.getPlugin('jsdom'); import createFocusMock from '../../../../../src/testing/mocks/middleware/focus'; import focus from '../../../../../src/core/middleware/focus'; import { tsx, create } from '../../../../../src/core/vdom'; -import harness from '../../../../../src/testing/harness'; +import renderer from '../../../../../src/testing/renderer'; +import assertionTemplate from '../../../../../src/testing/assertionTemplate'; describe('focus mock', () => { it('should mock focus of a node', () => { @@ -12,12 +13,12 @@ describe('focus mock', () => { const App = factory(({ middleware: { focus } }) => { return
{focus.isFocused('root') ? 'focus' : 'no focus'}
; }); - const h = harness(() => , { middleware: [[focus, focusMock]] }); + const r = renderer(() => , { middleware: [[focus, focusMock]] }); focusMock('root', false); - h.expect(() =>
no focus
); + r.expect(assertionTemplate(() =>
no focus
)); focusMock('root', true); - h.expect(() =>
focus
); + r.expect(assertionTemplate(() =>
focus
)); }); }); diff --git a/tests/testing/unit/mocks/middleware/icache.tsx b/tests/testing/unit/mocks/middleware/icache.tsx index b40247513..587c501f1 100644 --- a/tests/testing/unit/mocks/middleware/icache.tsx +++ b/tests/testing/unit/mocks/middleware/icache.tsx @@ -2,7 +2,8 @@ const { it } = intern.getInterface('bdd'); const { describe } = intern.getPlugin('jsdom'); import * as sinon from 'sinon'; import { tsx, create } from '../../../../../src/core/vdom'; -import harness from '../../../../../src/testing/harness'; +import renderer from '../../../../../src/testing/renderer'; +import assertionTemplate from '../../../../../src/testing/assertionTemplate'; import createICacheMock from '../../../../../src/testing/mocks/middleware/icache'; import icache from '../../../../../src/core/middleware/icache'; import global from '../../../../../src/shim/global'; @@ -22,9 +23,9 @@ describe('icache mock', () => { global.fetch = sinon.stub().returns(Promise.resolve({ json: () => Promise.resolve('api data') })); - const h = harness(() => , { middleware: [[icache, iCacheMock]] }); - h.expect(() =>
Loading
); + const r = renderer(() => , { middleware: [[icache, iCacheMock]] }); + r.expect(assertionTemplate(() =>
Loading
)); await iCacheMock('users'); - h.expect(() =>
api data
); + r.expect(assertionTemplate(() =>
api data
)); }); }); diff --git a/tests/testing/unit/mocks/middleware/intersection.tsx b/tests/testing/unit/mocks/middleware/intersection.tsx index 041d06cbf..3aac4d612 100644 --- a/tests/testing/unit/mocks/middleware/intersection.tsx +++ b/tests/testing/unit/mocks/middleware/intersection.tsx @@ -1,7 +1,8 @@ const { it } = intern.getInterface('bdd'); const { describe } = intern.getPlugin('jsdom'); import { tsx, create } from '../../../../../src/core/vdom'; -import harness from '../../../../../src/testing/harness'; +import renderer from '../../../../../src/testing/renderer'; +import assertionTemplate from '../../../../../src/testing/assertionTemplate'; import createIntersectionMock from '../../../../../src/testing/mocks/middleware/intersection'; import intersection from '../../../../../src/core/middleware/intersection'; @@ -13,11 +14,11 @@ describe('intersection mock', () => { const details = intersection.get('root'); return
{JSON.stringify(details)}
; }); - const h = harness(() => , { middleware: [[intersection, intersectionMock]] }); - h.expect(() =>
{`{"intersectionRatio":0,"isIntersecting":false}`}
); + const r = renderer(() => , { middleware: [[intersection, intersectionMock]] }); + r.expect(assertionTemplate(() =>
{`{"intersectionRatio":0,"isIntersecting":false}`}
)); intersectionMock('root', { isIntersecting: true }); - h.expect(() =>
{`{"isIntersecting":true}`}
); + r.expect(assertionTemplate(() =>
{`{"isIntersecting":true}`}
)); intersectionMock('root', { isIntersecting: false }); - h.expect(() =>
{`{"isIntersecting":false}`}
); + r.expect(assertionTemplate(() =>
{`{"isIntersecting":false}`}
)); }); }); diff --git a/tests/testing/unit/mocks/middleware/node.tsx b/tests/testing/unit/mocks/middleware/node.tsx index ba314e5e1..f4a996ed4 100644 --- a/tests/testing/unit/mocks/middleware/node.tsx +++ b/tests/testing/unit/mocks/middleware/node.tsx @@ -3,7 +3,8 @@ const { describe } = intern.getPlugin('jsdom'); import createNodeMock from '../../../../../src/testing/mocks/middleware/node'; import dimensions from '../../../../../src/core/middleware/dimensions'; import { tsx, create, node } from '../../../../../src/core/vdom'; -import harness from '../../../../../src/testing/harness'; +import renderer from '../../../../../src/testing/renderer'; +import assertionTemplate from '../../../../../src/testing/assertionTemplate'; describe('node mock', () => { it('should mock nodes', () => { @@ -13,10 +14,12 @@ describe('node mock', () => { const rects = dimensions.get('root'); return
{JSON.stringify(rects)}
; }); - const h = harness(() => , { middleware: [[node, nodeMock]] }); - h.expect(() => ( -
{`{"client":{"height":0,"left":0,"top":0,"width":0},"offset":{"height":0,"left":0,"top":0,"width":0},"position":{"bottom":0,"left":0,"right":0,"top":0},"scroll":{"height":0,"left":0,"top":0,"width":0},"size":{"width":0,"height":0}}`}
- )); + const r = renderer(() => , { middleware: [[node, nodeMock]] }); + r.expect( + assertionTemplate(() => ( +
{`{"client":{"height":0,"left":0,"top":0,"width":0},"offset":{"height":0,"left":0,"top":0,"width":0},"position":{"bottom":0,"left":0,"right":0,"top":0},"scroll":{"height":0,"left":0,"top":0,"width":0},"size":{"width":0,"height":0}}`}
+ )) + ); const client = { clientLeft: 1, clientTop: 2, clientWidth: 3, clientHeight: 4 }; const offset = { offsetHeight: 10, offsetLeft: 10, offsetTop: 10, offsetWidth: 10 }; const scroll = { scrollHeight: 10, scrollLeft: 10, scrollTop: 10, scrollWidth: 10 }; @@ -33,8 +36,10 @@ describe('node mock', () => { }) }; nodeMock('root', domNode); - h.expect(() => ( -
{`{"client":{"height":4,"left":1,"top":2,"width":3},"offset":{"height":10,"left":10,"top":10,"width":10},"position":{"bottom":10,"left":10,"right":10,"top":10},"scroll":{"height":10,"left":10,"top":10,"width":10},"size":{"width":10,"height":10}}`}
- )); + r.expect( + assertionTemplate(() => ( +
{`{"client":{"height":4,"left":1,"top":2,"width":3},"offset":{"height":10,"left":10,"top":10,"width":10},"position":{"bottom":10,"left":10,"right":10,"top":10},"scroll":{"height":10,"left":10,"top":10,"width":10},"size":{"width":10,"height":10}}`}
+ )) + ); }); }); diff --git a/tests/testing/unit/mocks/middleware/resize.tsx b/tests/testing/unit/mocks/middleware/resize.tsx index 743904124..abba134b2 100644 --- a/tests/testing/unit/mocks/middleware/resize.tsx +++ b/tests/testing/unit/mocks/middleware/resize.tsx @@ -3,7 +3,8 @@ const { describe } = intern.getPlugin('jsdom'); import createResizeMock from '../../../../../src/testing/mocks/middleware/resize'; import resize from '../../../../../src/core/middleware/resize'; import { tsx, create } from '../../../../../src/core/vdom'; -import harness from '../../../../../src/testing/harness'; +import renderer from '../../../../../src/testing/renderer'; +import assertionTemplate from '../../../../../src/testing/assertionTemplate'; describe('resize mock', () => { it('should mock resize middleware calls', () => { @@ -13,12 +14,12 @@ describe('resize mock', () => { const rects = resize.get('root'); return
{JSON.stringify(rects)}
; }); - const h = harness(() => , { middleware: [[resize, resizeMock]] }); - h.expect(() =>
null
); + const r = renderer(() => , { middleware: [[resize, resizeMock]] }); + r.expect(assertionTemplate(() =>
null
)); resizeMock('root', { width: 100 }); - h.expect(() =>
{`{"width":100}`}
); + r.expect(assertionTemplate(() =>
{`{"width":100}`}
)); resizeMock('root', { width: 101 }); - h.expect(() =>
{`{"width":101}`}
); + r.expect(assertionTemplate(() =>
{`{"width":101}`}
)); }); it('should deal with multiple mocked keys', () => { @@ -34,27 +35,33 @@ describe('resize mock', () => {
); }); - const h = harness(() => , { middleware: [[resize, resizeMock]] }); - h.expect(() => ( -
-
null
-
null
-
- )); + const r = renderer(() => , { middleware: [[resize, resizeMock]] }); + r.expect( + assertionTemplate(() => ( +
+
null
+
null
+
+ )) + ); resizeMock('root', { width: 100 }); - h.expect(() => ( -
-
{`{"width":100}`}
-
null
-
- )); + r.expect( + assertionTemplate(() => ( +
+
{`{"width":100}`}
+
null
+
+ )) + ); resizeMock('root', { width: 101 }); resizeMock('other', { width: 100 }); - h.expect(() => ( -
-
{`{"width":101}`}
-
{`{"width":100}`}
-
- )); + r.expect( + assertionTemplate(() => ( +
+
{`{"width":101}`}
+
{`{"width":100}`}
+
+ )) + ); }); }); diff --git a/tests/testing/unit/mocks/middleware/store.tsx b/tests/testing/unit/mocks/middleware/store.tsx index 7ad0ebd94..25cb632aa 100644 --- a/tests/testing/unit/mocks/middleware/store.tsx +++ b/tests/testing/unit/mocks/middleware/store.tsx @@ -3,7 +3,8 @@ const { assert } = intern.getPlugin('chai'); import createStoreMock from '../../../../../src/testing/mocks/middleware/store'; import { createStoreMiddleware } from '../../../../../src/core/middleware/store'; import { tsx, create } from '../../../../../src/core/vdom'; -import harness from '../../../../../src/testing/harness'; +import renderer, { wrap } from '../../../../../src/testing/renderer'; +import assertionTemplate from '../../../../../src/testing/assertionTemplate'; import { createProcess } from '../../../../../src/stores/process'; import { stub } from 'sinon'; import { replace } from '../../../../../src/stores/state/operations'; @@ -48,40 +49,28 @@ describe('store mock', () => {
); }); - const h = harness(() => , { middleware: [[store, storeMock]] }); - h.expect(() => { + const r = renderer(() => , { middleware: [[store, storeMock]] }); + const WrappedButton = wrap('button'); + const WrappedOtherButton = wrap('button'); + const WrappedSpan = wrap('span'); + const template = assertionTemplate(() => { return (
-
); }); + r.expect(template); assert.isTrue(processStub.notCalled); - h.trigger('@button', 'onclick'); - h.trigger('@other', 'onclick'); - assert.isTrue(processStub.calledOnce); + r.property(WrappedButton, 'onclick'); + r.property(WrappedOtherButton, 'onclick'); storeMock((path) => [replace(path('foo'), 'foo')]); - h.expect(() => { - return ( -
-
- ); - }); + const fooTemplate = template.setChildren(WrappedSpan, () => ['foo']); + r.expect(fooTemplate); + assert.isTrue(processStub.calledOnce); storeMock((path) => [replace(path('bar'), { qux: 1 })]); - h.expect(() => { - return ( -
-
- ); - }); + r.expect(fooTemplate.insertAfter(WrappedSpan, () => [1])); }); }); diff --git a/tests/testing/unit/mocks/middleware/validity.tsx b/tests/testing/unit/mocks/middleware/validity.tsx index 0369daea2..c3f46c52a 100644 --- a/tests/testing/unit/mocks/middleware/validity.tsx +++ b/tests/testing/unit/mocks/middleware/validity.tsx @@ -5,7 +5,8 @@ const { assert } = intern.getPlugin('chai'); import { tsx, create } from '../../../../../src/core/vdom'; import validity from '../../../../../src/core/middleware/validity'; import createValidityMock from '../../../../../src/testing/mocks/middleware/validity'; -import harness from '../../../../../src/testing/harness'; +import renderer from '../../../../../src/testing/renderer'; +import assertionTemplate from '../../../../../src/testing/assertionTemplate'; describe('validity mock', () => { it('should mock validity', () => { @@ -17,12 +18,12 @@ describe('validity mock', () => { }); validityMock('test', { valid: false, message: 'test message' }); - let h = harness(() => , { middleware: [[validity, validityMock]] }); - h.expect(() =>
test message
); + let r = renderer(() => , { middleware: [[validity, validityMock]] }); + r.expect(assertionTemplate(() =>
test message
)); validityMock('test', { valid: true, message: '' }); - h = harness(() => , { middleware: [[validity, validityMock]] }); - h.expect(() =>
valid
); + r = renderer(() => , { middleware: [[validity, validityMock]] }); + r.expect(assertionTemplate(() =>
valid
)); }); it('defaults to a default return value', () => { @@ -35,7 +36,7 @@ describe('validity mock', () => { return !valid ?
{message}
:
valid
; }); - let h = harness(() => , { middleware: [[validity, validityMock]] }); - h.expect(() =>
); + const r = renderer(() => , { middleware: [[validity, validityMock]] }); + r.expect(assertionTemplate(() =>
)); }); }); diff --git a/tests/testing/unit/renderer.tsx b/tests/testing/unit/renderer.tsx new file mode 100644 index 000000000..d6d3d4427 --- /dev/null +++ b/tests/testing/unit/renderer.tsx @@ -0,0 +1,545 @@ +const { describe, it } = intern.getInterface('bdd'); +const { assert } = intern.getPlugin('chai'); + +import { renderer, wrap, compare } from '../../../src/testing/renderer'; +import assertionTemplate from '../../../src/testing/assertionTemplate'; +import { WidgetBase } from '../../../src/core/WidgetBase'; +import { v, w, create, tsx, diffProperty, invalidator } from '../../../src/core/vdom'; +import Set from '../../../src/shim/Set'; +import Map from '../../../src/shim/Map'; +import { WNode, WidgetProperties, RenderResult } from '../../../src/core/interfaces'; +import icache from '../../../src/core/middleware/icache'; +import { uuid } from '../../../src/core/util'; + +const noop: any = () => {}; + +class ChildWidget extends WidgetBase<{ id: string; func?: () => void }> {} + +class MyWidget extends WidgetBase { + _count = 0; + _result = 'result'; + _onclick() { + this._count++; + this.invalidate(); + } + + _otherOnClick(count: any = 50) { + this._count = count; + this.invalidate(); + } + + _widgetFunction() { + this._result = `${this._result}-result`; + this.invalidate(); + } + + // prettier-ignore + protected render() { + return v('div', { classes: ['root', 'other'], onclick: this._otherOnClick }, [ + v('span', { + key: 'span', + classes: 'span', + style: 'width: 100px', + id: 'random-id', + onclick: this._onclick + }, [ + `hello ${this._count}` + ]), + this._result, + w(ChildWidget, { key: 'widget', id: 'random-id', func: this._widgetFunction }), + w('registry-item', { key: 'registry', id: 'random-id' }) + ]); + } +} + +class MyDeferredWidget extends WidgetBase { + // prettier-ignore + protected render() { + return v('div', (inserted: boolean) => { + return { classes: ['root', 'other'], styles: { marginTop: '100px' } }; + }); + } +} + +describe('test renderer', () => { + describe('widget with a single top level DNode', () => { + it('expect', () => { + const baseAssertion = assertionTemplate(() => + v('div', { classes: ['root', 'other'], onclick: () => {} }, [ + v( + 'span', + { key: 'span', classes: 'span', style: 'width: 100px', id: 'random-id', onclick: () => {} }, + ['hello 0'] + ), + 'result', + w(ChildWidget, { key: 'widget', id: 'random-id', func: noop }), + w('registry-item', { key: 'registry', id: 'random-id' }) + ]) + ); + + const r = renderer(() => w(MyWidget, {})); + r.expect(baseAssertion); + }); + + it('Should support deferred properties', () => { + const r = renderer(() => w(MyDeferredWidget, {})); + r.expect(assertionTemplate(() => v('div', { classes: ['root', 'other'], styles: { marginTop: '100px' } }))); + }); + + it('Should support widgets that have typed children', () => { + class WidgetWithTypedChildren extends WidgetBase> {} + const r = renderer(() => w(WidgetWithTypedChildren, {}, [w(MyDeferredWidget, {})])); + r.expect(assertionTemplate(() => v('div', [w(MyDeferredWidget, {})]))); + }); + + it('trigger property of wrapped node', () => { + const WrappedDiv = wrap('div'); + const baseTemplate = assertionTemplate(() => + v(WrappedDiv.tag, { classes: ['root', 'other'], onclick: () => {} }, [ + v( + 'span', + { key: 'span', classes: 'span', style: 'width: 100px', id: 'random-id', onclick: () => {} }, + ['hello 0'] + ), + 'result', + w(ChildWidget, { key: 'widget', id: 'random-id', func: noop }), + w('registry-item', { key: 'registry', id: 'random-id' }) + ]) + ); + const r = renderer(() => w(MyWidget, {})); + r.expect(baseTemplate); + + r.property(WrappedDiv, 'onclick'); + r.expect( + assertionTemplate(() => + v(WrappedDiv.tag, { classes: ['root', 'other'], onclick: () => {} }, [ + v( + 'span', + { key: 'span', classes: 'span', style: 'width: 100px', id: 'random-id', onclick: () => {} }, + ['hello 50'] + ), + 'result', + w(ChildWidget, { key: 'widget', id: 'random-id', func: noop }), + w('registry-item', { key: 'registry', id: 'random-id' }) + ]) + ) + ); + r.property(WrappedDiv, 'onclick', [100]); + r.expect( + assertionTemplate(() => + v('div', { classes: ['root', 'other'], onclick: () => {} }, [ + v( + 'span', + { key: 'span', classes: 'span', style: 'width: 100px', id: 'random-id', onclick: () => {} }, + ['hello 100'] + ), + 'result', + w(ChildWidget, { key: 'widget', id: 'random-id', func: noop }), + w('registry-item', { key: 'registry', id: 'random-id' }) + ]) + ) + ); + }); + + it('trigger property of wrapped widget', () => { + const WrappedChild = wrap(ChildWidget); + const baseTemplate = assertionTemplate(() => + v('div', { classes: ['root', 'other'], onclick: () => {} }, [ + v( + 'span', + { key: 'span', classes: 'span', style: 'width: 100px', id: 'random-id', onclick: () => {} }, + ['hello 0'] + ), + 'result', + w(WrappedChild, { key: 'widget', id: compare(() => true), func: noop }), + w('registry-item', { key: 'registry', id: 'random-id' }) + ]) + ); + const r = renderer(() => w(MyWidget, {})); + r.expect(baseTemplate); + r.property(WrappedChild, 'func'); + r.expect( + assertionTemplate(() => + v('div', { classes: ['root', 'other'], onclick: () => {} }, [ + v( + 'span', + { key: 'span', classes: 'span', style: 'width: 100px', id: 'random-id', onclick: () => {} }, + ['hello 0'] + ), + 'result-result', + w(WrappedChild, { key: 'widget', id: 'random-id', func: noop }), + w('registry-item', { key: 'registry', id: 'random-id' }) + ]) + ) + ); + r.property(WrappedChild, 'func'); + r.expect( + assertionTemplate(() => + v('div', { classes: ['root', 'other'], onclick: () => {} }, [ + v( + 'span', + { key: 'span', classes: 'span', style: 'width: 100px', id: 'random-id', onclick: () => {} }, + ['hello 0'] + ), + 'result-result-result', + w(WrappedChild, { key: 'widget', id: 'random-id', func: noop }), + w('registry-item', { key: 'registry', id: 'random-id' }) + ]) + ) + ); + }); + + it('should trigger properties in the correct order', () => { + const factory = create({ icache }); + + const MyWidget = factory(function MyWidget({ middleware: { icache } }) { + const name = icache.getOrSet('name', 'Dojo'); + const inputValue = icache.getOrSet('input', ''); + return ( +
+ + { + const value = (event.target as HTMLInputElement).value; + icache.set('input', value); + }} + /> + hello, + {name} +
+ ); + }); + + const r = renderer(() => ); + + const WrappedInput = wrap('input'); + const WrappedButton = wrap('button'); + const WrappedSpan = wrap('span'); + + const template = assertionTemplate(() => ( +
+ {}}> + Update Name + + {}} /> + hello, + Dojo +
+ )); + r.expect(template); + + const nameTemplate = template.replaceChildren(WrappedSpan, () => ['Dojo.io']); + + r.property(WrappedInput, 'oninput', { target: { value: 'Dojo.io' } }); + r.property(WrappedButton, 'onclick'); + r.expect(nameTemplate); + }); + + it('should throw error when property used before first expect', () => { + const WrappedDiv = wrap('div'); + const r = renderer(() => w(MyWidget, {})); + assert.throws(() => { + r.property(WrappedDiv, 'onclick'); + }, 'To use `.property` please perform an initial expect'); + }); + + it('resolve functional children', () => { + const childFunctionFactory = create().children<(value: string) => RenderResult>(); + + const ChildFunctionWidget = childFunctionFactory(function ChildFunctionWidget() { + return ''; + }); + + const childObjectFactory = create().children<{ + top: (value: string) => RenderResult; + bottom: (value: string) => RenderResult; + }>(); + + const ChildObjectFactory = childObjectFactory(function ChildObjectFactory() { + return ''; + }); + + const factory = create(); + + const MyWidget = factory(function MyWidget() { + return ( +
+ {{ top: (value) => value, bottom: (value) => value }} + {(value) => value} + + {{ + top: (parentTop) => parentTop, + bottom: (parentBottom) => ( + + {(childValue) => ( + + {{ + top: (value) => `${value}-${childValue}`, + bottom: (value) => `${value}-${parentBottom}` + }} + + )} + + ) + }} + +
+ ); + }); + + const WrappedChildObjectFactory = wrap(ChildObjectFactory); + const WrappedChildFunctionWidget = wrap(ChildFunctionWidget); + + const WrappedParentChildObjectFactory = wrap(ChildObjectFactory); + const WrappedNestedChildFunctionWidget = wrap(ChildFunctionWidget); + const WrappedNestedChildObjectFactory = wrap(ChildObjectFactory); + const r = renderer(() => ); + + r.child(WrappedChildObjectFactory, { top: ['top'], bottom: ['bottom'] }); + r.child(WrappedChildFunctionWidget, ['func']); + r.child(WrappedParentChildObjectFactory, { top: ['parent-top'], bottom: ['parent-bottom'] }); + r.child(WrappedNestedChildObjectFactory, { top: ['nested-top'], bottom: ['nested-bottom'] }); + r.child(WrappedNestedChildFunctionWidget, ['nested-function']); + + r.expect( + assertionTemplate(() => ( +
+ + {{ top: () => 'top', bottom: () => 'bottom' }} + + {() => 'func'} + + {{ + top: () => 'parent-top', + bottom: () => ( + + {() => ( + + {{ + top: () => 'nested-top-nested-function', + bottom: () => 'nested-bottom-parent-bottom' + }} + + )} + + ) + }} + +
+ )) + ); + + assert.throws(() => { + r.expect( + assertionTemplate(() => ( +
+ + {{ top: () => 'top', bottom: () => 'bottom' }} + + {() => 'func'} + + {{ + top: () => 'parent-top', + bottom: () => ( + + {() => ( + + {{ + top: () => 'nested-topper-nested-function', + bottom: () => 'nested-bottom-parent-bottomer' + }} + + )} + + ) + }} + +
+ )) + ); + }, '\n
\n\t\n\t\t{\n\t\t\ttop: (\n\t\t\t\ttop\n\t\t\t)\n\t\t\tbottom: (\n\t\t\t\tbottom\n\t\t\t)\n\t\t}\n\t\n\t\n\t\tfunc\n\t\n\t\n\t\t{\n\t\t\ttop: (\n\t\t\t\tparent-top\n\t\t\t)\n\t\t\tbottom: (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttop: (\n(A)\t\t\t\t\t\t\t\tnested-top-nested-function\n(E)\t\t\t\t\t\t\t\tnested-topper-nested-function\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\tbottom: (\n(A)\t\t\t\t\t\t\t\tnested-bottom-parent-bottom\n(E)\t\t\t\t\t\t\t\tnested-bottom-parent-bottomer\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t)\n\t\t}\n\t\n
'); + }); + + it('Should use custom comparator for the template assertion', () => { + const factory = create(); + const WrappedSpan = wrap('span'); + const MyWidget = factory(function MyWidget() { + return ( +
+ +
+ ); + }); + const r = renderer(() => ); + r.expect( + assertionTemplate(() => ( +
+ typeof actual === 'string')} /> +
+ )) + ); + }); + + it('Should fail with an unsuccessful custom compare', () => { + const factory = create(); + const WrappedSpan = wrap('span'); + const MyWidget = factory(function MyWidget() { + return ( +
+ +
+ ); + }); + const r = renderer(() => ); + assert.throws(() => { + r.expect( + assertionTemplate(() => ( +
+ typeof actual !== 'string')} /> +
+ )) + ); + }); + }); + + it('Support Maps and Sets in properties', () => { + class Bar extends WidgetBase<{ foo: Map; bar: Set }> {} + class Foo extends WidgetBase<{ foo: Map; bar: Set }> { + render() { + const { foo, bar } = this.properties; + return w(Bar, { foo, bar }); + } + } + const bar = new Set(); + bar.add('foo'); + const foo = new Map(); + foo.set('a', 'a'); + const r = renderer(() => w(Foo, { foo, bar })); + r.expect(assertionTemplate(() => w(Bar, { foo, bar }))); + }); + }); + + describe('functional widgets', () => { + it('should inject invalidator mock', () => { + const factory = create({ icache }); + + const App = factory(({ middleware: { icache } }) => { + const counter = icache.get('counter') || 0; + return ( +
+ +
+ ); + }); + const WrappedButton = wrap('button'); + const r = renderer(() => ); + r.expect( + assertionTemplate(() => ( +
+ {}}> + Click Me 0 + +
+ )) + ); + r.property(WrappedButton, 'onclick'); + r.expect( + assertionTemplate(() => ( +
+ +
+ )) + ); + }); + + it('should run diffProperty middleware', () => { + const factory = create({ diffProperty, invalidator }); + let id = 0; + const App = factory(({ middleware: { diffProperty, invalidator } }) => { + diffProperty('key', () => { + id++; + invalidator(); + }); + return ( +
+ +
+ ); + }); + const r = renderer(() => ); + + r.expect( + assertionTemplate(() => ( +
+ +
+ )) + ); + + r.expect( + assertionTemplate(() => ( +
+ +
+ )) + ); + r.expect( + assertionTemplate(() => ( +
+ +
+ )) + ); + }); + + it('should support conditional logic in diffProperty middleware', () => { + const factory = create({ diffProperty, invalidator }); + let id = 0; + const App = factory(({ middleware: { diffProperty, invalidator }, properties }) => { + diffProperty('key', (prev: any, current: any) => { + if (prev.key === 'app' && current.key === 'app') { + id++; + invalidator(); + } + }); + return ( +
+ +
+ ); + }); + const r = renderer(() => ); + r.expect( + assertionTemplate(() => ( +
+ +
+ )) + ); + r.expect( + assertionTemplate(() => ( +
+ +
+ )) + ); + }); + }); +}); From 3553a0ae6b072fe60d499a8aeb2be08233e10aa1 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Fri, 3 Apr 2020 11:35:34 +0100 Subject: [PATCH 02/15] Fix title --- docs/en/testing/supplemental.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/testing/supplemental.md b/docs/en/testing/supplemental.md index 7b0f73c65..bbfa07aac 100644 --- a/docs/en/testing/supplemental.md +++ b/docs/en/testing/supplemental.md @@ -27,7 +27,7 @@ const r = renderer(() => ); r.expect(baseTemplate); ``` -## The test renderer uses the location of a wrapped test node in the expected tree to attempt to perform the requested action (either `r.property()` or `r.child()`) on the actual output of the widget under test. If the wrapped test node does not match the corresponding node in the actual output tree then no action will be performed and the assertion will report a failure. +## Wrapped Test Nodes In order for the test renderer and assertion templates to be able to identify nodes within the expected and actual node structure a special wrapping node must be used. The wrapped nodes can get used in place of the real node in the expected assertion template structure, maintaining all the correct property and children typings. From e303e912d525cd4b4e2747552fe56f316cb254ec Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Fri, 3 Apr 2020 12:26:11 +0100 Subject: [PATCH 03/15] fix tests for ie --- tests/testing/unit/assertRender.tsx | 2 +- tests/testing/unit/harness/support/assertRender.ts | 2 +- tests/testing/unit/renderer.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/testing/unit/assertRender.tsx b/tests/testing/unit/assertRender.tsx index 9c1c01f8d..357147436 100644 --- a/tests/testing/unit/assertRender.tsx +++ b/tests/testing/unit/assertRender.tsx @@ -38,7 +38,7 @@ class WidgetWithMap extends WidgetBase { } function getExpectedError() { - const widgetName = (MockWidget as any).name || 'Widget-5'; + const widgetName = (MockWidget as any).name || 'Widget-3'; return ` (A)
(E)
diff --git a/tests/testing/unit/harness/support/assertRender.ts b/tests/testing/unit/harness/support/assertRender.ts index de6bc22cc..47e605cc9 100644 --- a/tests/testing/unit/harness/support/assertRender.ts +++ b/tests/testing/unit/harness/support/assertRender.ts @@ -38,7 +38,7 @@ class WidgetWithMap extends WidgetBase { } function getExpectedError() { - const widgetName = (MockWidget as any).name || 'Widget-5'; + const widgetName = (MockWidget as any).name || 'Widget-1'; return ` v("div", { (A) "classes": "class", diff --git a/tests/testing/unit/renderer.tsx b/tests/testing/unit/renderer.tsx index d6d3d4427..cfc28f0e8 100644 --- a/tests/testing/unit/renderer.tsx +++ b/tests/testing/unit/renderer.tsx @@ -366,7 +366,7 @@ describe('test renderer', () => {
)) ); - }, '\n
\n\t\n\t\t{\n\t\t\ttop: (\n\t\t\t\ttop\n\t\t\t)\n\t\t\tbottom: (\n\t\t\t\tbottom\n\t\t\t)\n\t\t}\n\t\n\t\n\t\tfunc\n\t\n\t\n\t\t{\n\t\t\ttop: (\n\t\t\t\tparent-top\n\t\t\t)\n\t\t\tbottom: (\n\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttop: (\n(A)\t\t\t\t\t\t\t\tnested-top-nested-function\n(E)\t\t\t\t\t\t\t\tnested-topper-nested-function\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\tbottom: (\n(A)\t\t\t\t\t\t\t\tnested-bottom-parent-bottom\n(E)\t\t\t\t\t\t\t\tnested-bottom-parent-bottomer\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t}\n\t\t\t\t\t\n\t\t\t\t\n\t\t\t)\n\t\t}\n\t\n
'); + }); }); it('Should use custom comparator for the template assertion', () => { From 6a8d91c5f8521dc888ff9843d2fa78cfe0fa5388 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Wed, 8 Apr 2020 10:21:32 +0100 Subject: [PATCH 04/15] convert outlet tests to use test renderer --- tests/routing/unit/Outlet.tsx | 43 ++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/tests/routing/unit/Outlet.tsx b/tests/routing/unit/Outlet.tsx index e01484eea..0b3267dbe 100644 --- a/tests/routing/unit/Outlet.tsx +++ b/tests/routing/unit/Outlet.tsx @@ -4,7 +4,7 @@ import { MemoryHistory as HistoryManager } from '../../../src/routing/history/Me import { Registry } from '../../../src/core/Registry'; import { registerRouterInjector } from '../../../src/routing/RouterInjector'; import { create, getRegistry, tsx } from '../../../src/core/vdom'; -import harness from '../../../src/testing/harness'; +import renderer, { wrap } from '../../../src/testing/renderer'; import assertionTemplate from '../../../src/testing/assertionTemplate'; import Outlet from '../../../src/routing/Outlet'; @@ -70,7 +70,7 @@ describe('Outlet', () => { )); router.setPath('/widget/widget/overview/type'); - const h = harness( + const r = renderer( () => ( {{ @@ -81,7 +81,7 @@ describe('Outlet', () => { ), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(template); + r.expect(template); }); it('should restrict matches using matcher property based on match details', () => { @@ -92,7 +92,7 @@ describe('Outlet', () => { )); router.setPath('/widget/widget/overview/type'); - const h = harness( + const r = renderer( () => ( { ), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(template); + r.expect(template); router.setPath('/widget/widget/overview'); - h.expect( + r.expect( assertionTemplate(() => (
overview
@@ -131,7 +131,7 @@ describe('Outlet', () => {
)); router.setPath('/widget/widget/overview/type'); - const h = harness( + const r = renderer( () => ( { ), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(template); + r.expect(template); }); it('should render function child if there is any route matches for the outlet', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); const template = assertionTemplate(() =>
function
); router.setPath('/widget/widget/overview/type'); - const h = harness(() => {() =>
function
}
, { + const r = renderer(() => {() =>
function
}
, { middleware: [[getRegistry, mockGetRegistry]] }); - h.expect(template); + r.expect(template); }); it('should be able to access match details in children functions', () => { @@ -169,7 +169,7 @@ describe('Outlet', () => { )); router.setPath('/widget/widget/overview/type'); - const h = harness( + const r = renderer( () => ( {{ @@ -180,14 +180,14 @@ describe('Outlet', () => { ), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(template); + r.expect(template); }); it('should return null if no router has been registered', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); const template = assertionTemplate(() => null); router.setPath('/widget/widget/overview/type'); - const h = harness(() => ( + const r = renderer(() => ( {{ overview: ({ params: { widget } }) =>
{widget}
, @@ -195,14 +195,14 @@ describe('Outlet', () => { }}
)); - h.expect(template); + r.expect(template); }); it('should return null if no routes match for the outlet', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); const template = assertionTemplate(() => null); router.setPath('/other/widget/overview/type'); - const h = harness(() => ( + const r = renderer(() => ( {{ overview: ({ params: { widget } }) =>
{widget}
, @@ -210,7 +210,7 @@ describe('Outlet', () => { }}
)); - h.expect(template); + r.expect(template); }); it('should be able to use a custom router key', () => { @@ -219,14 +219,15 @@ describe('Outlet', () => { const properties: any = { id: 'main' }; + const WrappedType = wrap('div'); const template = assertionTemplate(() => (
overview
-
type
+ type
)); router.setPath('/widget/widget/overview/type'); - const h = harness( + const r = renderer( () => ( {{ @@ -237,13 +238,13 @@ describe('Outlet', () => { ), { middleware: [[getRegistry, mockGetRegistry]] } ); - h.expect(template); + r.expect(template); const customRouter = registerRouterInjector(routeConfig, registry, { HistoryManager, key: 'custom' }); properties.routerKey = 'custom'; customRouter.setPath('/widget/widget/overview'); - h.expect(template.remove('@type')); + r.expect(template.remove(WrappedType)); properties.routerKey = undefined; router.setPath('/widget/widget/overview/type'); - h.expect(template); + r.expect(template); }); }); From 82976658be8101b72095c68df30ac4d0f060f6d3 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Wed, 8 Apr 2020 10:21:58 +0100 Subject: [PATCH 05/15] remove console log --- tests/core/unit/vdom.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/core/unit/vdom.tsx b/tests/core/unit/vdom.tsx index 02151e889..b67dea32b 100644 --- a/tests/core/unit/vdom.tsx +++ b/tests/core/unit/vdom.tsx @@ -3508,7 +3508,6 @@ jsdomDescribe('vdom', () => { const root = document.createElement('div'); const r = renderer(() => App({})); r.mount({ domNode: root }); - console.log(root.innerHTML); assert.strictEqual(root.innerHTML, '
0
'); properties.count = 1; (root.children[0].children[0] as any).click(); From 0d04b5f16b8d05415e494e01893434bf5b209131 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Wed, 8 Apr 2020 10:25:13 +0100 Subject: [PATCH 06/15] error if the same wrapped test node is used more than once --- src/testing/decorate.ts | 15 ++++++++++++--- tests/testing/unit/renderer.tsx | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/testing/decorate.ts b/src/testing/decorate.ts index 512637082..3aafd6b88 100644 --- a/src/testing/decorate.ts +++ b/src/testing/decorate.ts @@ -13,10 +13,14 @@ export interface DecoratorResult { nodes: T; } -function isNode(node: any): node is VNode | WNode { +function isNode(node: any): node is VNode & { id?: string } | WNode & { id?: string } { return isVNode(node) || isWNode(node); } +function isWrappedNode(node: any): node is VNode & { id: string } | WNode & { id: string } { + return Boolean(isNode(node) && node.id); +} + export function decorateNodes(dNode: DNode[]): DecoratorResult; export function decorateNodes(dNode: DNode): DecoratorResult; export function decorateNodes(dNode: DNode | DNode[]): DecoratorResult; @@ -45,6 +49,7 @@ export function decorate(actual: RenderResult, expected: RenderResult, instructi let nodes: DecorateTuple[] = [ [Array.isArray(actual) ? [...actual] : [actual], Array.isArray(expected) ? [...expected] : [expected]] ]; + const wrappedNodeIds = []; let node = nodes.shift(); while (node) { @@ -53,8 +58,11 @@ export function decorate(actual: RenderResult, expected: RenderResult, instructi while (expectedNodes.length > 0) { let actualNode: DNode | { [index: string]: any } = actualNodes.shift(); let expectedNode: DNode | { [index: string]: any } = expectedNodes.shift(); - if (isNode(expectedNode)) { - const instruction = instructions.get((expectedNode as any).id); + if (isWrappedNode(expectedNode)) { + if (wrappedNodeIds.indexOf(expectedNode.id) !== -1) { + throw new Error('Cannot use a wrapped test node more than once within an assertion template.'); + } + const instruction = instructions.get(expectedNode.id); if (instruction) { if (instruction.type === 'child') { const expectedChild: any = expectedNode.children && expectedNode.children[0]; @@ -89,6 +97,7 @@ export function decorate(actual: RenderResult, expected: RenderResult, instructi actualNode.properties[instruction.key](...instruction.params); } } + wrappedNodeIds.push(expectedNode.id); } if (isNode(expectedNode) && isNode(actualNode)) { diff --git a/tests/testing/unit/renderer.tsx b/tests/testing/unit/renderer.tsx index cfc28f0e8..3a07e2717 100644 --- a/tests/testing/unit/renderer.tsx +++ b/tests/testing/unit/renderer.tsx @@ -426,6 +426,30 @@ describe('test renderer', () => { const r = renderer(() => w(Foo, { foo, bar })); r.expect(assertionTemplate(() => w(Bar, { foo, bar }))); }); + + it('should throw error if wrapped test node is used more than once', () => { + const factory = create(); + const WrappedSpan = wrap('span'); + const MyWidget = factory(function MyWidget() { + return ( +
+ hello + world +
+ ); + }); + const r = renderer(() => ); + assert.throws(() => { + r.expect( + assertionTemplate(() => ( +
+ hello + world +
+ )) + ); + }, 'Cannot use a wrapped test node more than once within an assertion template.'); + }); }); describe('functional widgets', () => { From 5d3767ffbe14ed5dc1e52175f28fbc29c94b207c Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Wed, 8 Apr 2020 10:26:26 +0100 Subject: [PATCH 07/15] docs --- docs/en/testing/supplemental.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/en/testing/supplemental.md b/docs/en/testing/supplemental.md index bbfa07aac..499e10c32 100644 --- a/docs/en/testing/supplemental.md +++ b/docs/en/testing/supplemental.md @@ -49,6 +49,8 @@ const WrappedDiv = wrap('div'); The test renderer uses the location of a wrapped test node in the expected tree to attempt to perform the requested action (either `r.property()` or `r.child()`) on the actual output of the widget under test. If the wrapped test node does not match the corresponding node in the actual output tree then no action will be performed and the assertion will report a failure. +**Note:** Wrapped test nodes should only be used once within an assertion template, if the same test node is detected more than once during an assertion an error will be thrown and the test fail. + ## Assertion Templates Assertion templates get used to build the expected widget output structure to use with `renderer.expect()`. The templates expose a wide range of APIs that enable the expected output to vary between tests. From 1099f47ef17f11d8de53d92b2c4fc51a96b2844a Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Thu, 9 Apr 2020 19:38:20 +0100 Subject: [PATCH 08/15] Apply suggestions from code review Co-Authored-By: Bradley Maier --- docs/en/testing/supplemental.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/en/testing/supplemental.md b/docs/en/testing/supplemental.md index 499e10c32..f28875777 100644 --- a/docs/en/testing/supplemental.md +++ b/docs/en/testing/supplemental.md @@ -152,7 +152,7 @@ Creating templates from a base template means that if there is a change to the d #### `assertionTemplate.setChildren()` -Returns a new assertion template with the new children are either pre-pended, appended or replaced depending on the `type` passed. +Returns a new assertion template with the new children either pre-pended, appended or replaced depending on the `type` passed. ```tsx .setChildren( @@ -162,11 +162,11 @@ Returns a new assertion template with the new children are either pre-pended, ap );` ``` -Convience functions exists for all 3 types, [`prepend()`](/missing/link), [`append()`](/missing/link) and [`replaceChildren()`](/missing/link). +Convenience functions exists for all 3 types, [`prepend()`](/missing/link), [`append()`](/missing/link) and [`replaceChildren()`](/missing/link). #### `assertionTemplate.append()` -Returns a new assertion template with the new children appended to the nodes existing children. +Returns a new assertion template with the new children appended to the node's existing children. ```tsx .append(wrapped: Wrapped, children: () => RenderResult); @@ -174,7 +174,7 @@ Returns a new assertion template with the new children appended to the nodes exi #### `assertionTemplate.prepend()` -Returns a new assertion template with the new children pre-pended to the nodes existing children. +Returns a new assertion template with the new children pre-pended to the node's existing children. ```tsx .append(wrapped: Wrapped, children: () => RenderResult); @@ -182,7 +182,7 @@ Returns a new assertion template with the new children pre-pended to the nodes e #### `assertionTemplate.replaceChildren()` -Returns a new assertion template with the new children replacing the nodes existing children. +Returns a new assertion template with the new children replacing the node's existing children. ```tsx .append(wrapped: Wrapped, children: () => RenderResult); @@ -190,7 +190,7 @@ Returns a new assertion template with the new children replacing the nodes exist #### `assertionTemplate.insertSiblings()` -Returns a new assertion template with the passed children either inserted either `before` or `after` depending on the `type` passed. +Returns a new assertion template with the passed children inserted either `before` or `after` depending on the `type` passed. ```tsx .insertSiblings( @@ -202,7 +202,7 @@ Returns a new assertion template with the passed children either inserted either #### `assertionTemplate.insertBefore()` -Returns a new assertion template with the passed children inserted before the existing nodes children. +Returns a new assertion template with the passed children inserted before the existing node's children. ```tsx .insertBefore(wrapped: Wrapped, children: () => RenderResult); @@ -210,7 +210,7 @@ Returns a new assertion template with the passed children inserted before the ex #### `assertionTemplate.insertAfter()` -Returns a new assertion template with the passed children inserted after the existing nodes children. +Returns a new assertion template with the passed children inserted after the existing node's children. ```tsx .insertAfter(wrapped: Wrapped, children: () => RenderResult); @@ -254,7 +254,7 @@ Returns a new assertion template with the updated properties for the target wrap ): AssertionTemplateResult; ``` -A function can be set in place of the properties object to return the expected properties based of the actual properties. +A function can be set in place of the properties object to return the expected properties based off the actual properties. ## Triggering Properties From b3fe64740f947c87940160e9a32d4d1f02205006 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Thu, 9 Apr 2020 19:46:11 +0100 Subject: [PATCH 09/15] address feedback --- docs/en/testing/supplemental.md | 38 +++++++++++++++++---------------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/docs/en/testing/supplemental.md b/docs/en/testing/supplemental.md index f28875777..958417e10 100644 --- a/docs/en/testing/supplemental.md +++ b/docs/en/testing/supplemental.md @@ -2,9 +2,9 @@ Dojo provides a simple and type safe test renderer for shallowly asserting the expected output and behavior from a widget. The test renderer's API has been designed to encourage unit testing best practices from the outset to ensure high confidence in you Dojo application. -Working with [assertion templates](/missing/link) and the test renderer is done using [wrapped test nodes](/missing/link) that are defined in the assertion templates structure, ensuring type safety throughout the testing life-cycle. +Working with [assertion templates](/learn/testing/test-renderer#assertion-templates) and the test renderer is done using [wrapped test nodes](/learn/testing/test-renderer#wrapped-test-nodes) that are defined in the assertion templates structure, ensuring type safety throughout the testing life-cycle. -The expected structure of a widget is defined using an assertionTemplate and passed to the test renderer's [`.expect()`](/missing/link) function which executes the assertion. +The expected structure of a widget is defined using an assertionTemplate and passed to the test renderer's `.expect()` function which executes the assertion. > src/MyWidget.spec.tsx @@ -101,7 +101,7 @@ describe('Profile', () => { const r = renderer(() => ); // Test against the base assertion - h.expect(baseAssertion); + r.expect(baseAssertion); }); }); ``` @@ -131,7 +131,7 @@ describe('Profile', () => { const r = renderer(() => ); // Test against the base assertion - h.expect(baseAssertion); + r.expect(baseAssertion); }); it('Should render using the passed username', () => { @@ -141,7 +141,7 @@ describe('Profile', () => { const usernameTemplate = baseAssertion.setChildren(WrappedHeader, () => ['Dojo']); // Test against the username template - h.expect(usernameTemplate); + r.expect(usernameTemplate); }); }); ``` @@ -322,16 +322,16 @@ describe('MyWidget', () => { const r = renderer(() => ); // assert against the base assertion - h.expect(baseAssertion); + r.expect(baseAssertion); // register a call to the button's onclick property - h.property(WrappedButton, 'onclick'); + r.property(WrappedButton, 'onclick'); // create a new template with the updated count const counterTemplate = baseAssertion.setChildren(WrappedSpan, () => ['1']); // expect against the new template, the property will be called before the test render - h.expect(counterTemplate); + r.expect(counterTemplate); // once the assertion is complete, check that the stub property was called assert.isTrue(onClickStub.calledOnce); @@ -339,6 +339,8 @@ describe('MyWidget', () => { }); ``` +Arguments for the function can be passed after the function name, for example `r.property(WrappedButton, 'onclick', { target: { value: 'value' }})`. When there are multiple parameters for the function they are passed one after the other `r.property(WrappedButton, 'onclick', 'first-arg', 'second-arg', 'third-arg')` + ## Asserting Functional Children To assert the output from functional children the test renderer needs to understand how to resolve the child render functions. This includes passing in any expected injected values. @@ -393,7 +395,7 @@ describe('MyWidget', () => { // with the provided params r.child(WrappedMyWidgetWithChildren, ['Hello!']); - h.expect(baseAssertion); + r.expect(baseAssertion); }); }); ``` @@ -577,11 +579,11 @@ describe('Breakpoint', () => { const r = renderer(() => , { middleware: [[breakpoint, mockBreakpoint]] }); - h.expect(template); + r.expect(template); mockBreakpoint('root', { breakpoint: 'LG', contentRect: { width: 800 } }); - h.expect(template.insertAfter(WrappedHeader, () => [

Subtitle

]); + r.expect(template.insertAfter(WrappedHeader, () => [

Subtitle

]); }); }); ``` @@ -638,11 +640,11 @@ describe('Focus', () => { middleware: [[focus, focusMock]] }); - h.expect(template); + r.expect(template); focusMock('text', true); - h.expect(template.setProperty(WrappedRoot, 'classes', [css.root, css.focused])); + r.expect(template.setProperty(WrappedRoot, 'classes', [css.root, css.focused])); }); }); ``` @@ -700,11 +702,11 @@ describe('MyWidget', () => { const template = assertionTemplate(() => Loading); const mockICache = createICacheMock(); const r = renderer(() => , { middleware: [[icache, mockICache]] }); - h.expect(template); + r.expect(template); // await the async method passed to the mock cache await mockICache('users'); - h.expect(template.setChildren(WrappedRoot, () => ['api data'])); + r.expect(template.setChildren(WrappedRoot, () => ['api data'])); }); }); ``` @@ -757,7 +759,7 @@ describe('MyWidget', () => { intersectionMock('root', { isIntersecting: true }); // assert again with the updated expectation - h.expect(assertionTemplate.setChildren(WrappedRoot, () => [`{"isIntersecting": true }`])); + r.expect(assertionTemplate.setChildren(WrappedRoot, () => [`{"isIntersecting": true }`])); }); }); ``` @@ -994,7 +996,7 @@ describe('Validity', () => { )); - h.expect(template); + r.expect(template); validityMock('input', { valid: false, message: 'invalid message' }); @@ -1002,7 +1004,7 @@ describe('Validity', () => { .append(WrappedRoot, () => [

invalid message

]) .setProperty(WrappedRoot, 'classes', [css.root, css.invalid]); - h.expect(invalidTemplate); + r.expect(invalidTemplate); }); }); ``` From 0c4fa0015ddfedf24443c9d63c56f08914d42d53 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Thu, 9 Apr 2020 20:05:31 +0100 Subject: [PATCH 10/15] address feedback --- docs/en/testing/supplemental.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/testing/supplemental.md b/docs/en/testing/supplemental.md index 958417e10..72c77ed8a 100644 --- a/docs/en/testing/supplemental.md +++ b/docs/en/testing/supplemental.md @@ -47,7 +47,7 @@ const WrappedMyWidget = wrap(MyWidget); const WrappedDiv = wrap('div'); ``` -The test renderer uses the location of a wrapped test node in the expected tree to attempt to perform the requested action (either `r.property()` or `r.child()`) on the actual output of the widget under test. If the wrapped test node does not match the corresponding node in the actual output tree then no action will be performed and the assertion will report a failure. +The test renderer uses the location of a wrapped test node in the expected tree to attempt to perform any requested actions (either `r.property()` or `r.child()`) on the actual output of the widget under test. If the wrapped test node does not match the corresponding node in the actual output tree then no action will be performed and the assertion will report a failure. **Note:** Wrapped test nodes should only be used once within an assertion template, if the same test node is detected more than once during an assertion an error will be thrown and the test fail. From dbf6dab54bd29323fa1233a7798b71708c1c3de4 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Tue, 14 Apr 2020 16:11:56 +0100 Subject: [PATCH 11/15] add extra test and fix child typings --- src/testing/renderer.ts | 22 +++++++----- tests/testing/unit/renderer.tsx | 60 +++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 9 deletions(-) diff --git a/src/testing/renderer.ts b/src/testing/renderer.ts index 3af13dc90..65b8869f4 100644 --- a/src/testing/renderer.ts +++ b/src/testing/renderer.ts @@ -43,8 +43,12 @@ export type Instruction = ChildInstruction | PropertyInstruction; export interface Child { >( wrapped: Wrapped, - params: T['children'] extends { [index: string]: (...args: any[]) => RenderResult } - ? { [P in keyof T['children']]: Parameters } + params: T['children'] extends { [index: string]: any } + ? { + [P in keyof T['children']]?: Parameters extends never + ? [] + : Parameters + } : T['children'] extends (...args: any[]) => RenderResult ? Parameters : never ): void; } @@ -61,12 +65,7 @@ export type RequiredVNodeProperties = Required, K extends FunctionPropertyNames>>( - wrapped: Constructor, - key: K, - ...params: Parameters>> - ): void; - >>( - wrapped: T, + wrapped: Wrapped>, key: K, ...params: Parameters>> ): void; @@ -74,10 +73,15 @@ export interface Property { T extends OptionalWNodeFactory<{ properties: Comparable; children: any }>, K extends FunctionPropertyNames >( - wrapped: T, + wrapped: Wrapped, key: K, ...params: any[] ): void; + >>( + wrapped: Wrapped, + key: K, + ...params: Parameters>> + ): void; } export interface RendererAPI { diff --git a/tests/testing/unit/renderer.tsx b/tests/testing/unit/renderer.tsx index 3a07e2717..f16d131f9 100644 --- a/tests/testing/unit/renderer.tsx +++ b/tests/testing/unit/renderer.tsx @@ -369,6 +369,66 @@ describe('test renderer', () => { }); }); + it('should selectively named children functions but resolve assert all children', () => { + const factory = create({ icache }); + + const App = factory(function App({ middleware: { icache } }) { + const strings = icache.getOrSet('strings', []); + + return ( +
+ {{ leading: strings, trailing: () => strings }} + +
+ ); + }); + + interface WidgetChildren { + leading: string[]; + trailing: () => RenderResult; + } + + const widgetFactory = create().children(); + + const Widget = widgetFactory(function Widget({ children }) { + const [{ leading }] = children(); + + return
The strings are {leading.join(', ')}
; + }); + + const r = renderer(() => ); + + const WrappedWidget = wrap(Widget); + const WrappedButton = wrap('button'); + const baseAssertion = assertionTemplate(() => ( +
+ {{ leading: [], trailing: () => [] }} + undefined}> + Add String + +
+ )); + + r.child(WrappedWidget, { trailing: [] }); + r.expect(baseAssertion); + r.property(WrappedButton, 'onclick'); + r.expect( + baseAssertion.setChildren(WrappedWidget, () => ({ leading: ['string'], trailing: () => ['string'] })) + ); + r.property(WrappedButton, 'onclick'); + r.expect( + baseAssertion.setChildren(WrappedWidget, () => ({ + leading: ['string', 'string'], + trailing: () => ['string', 'string'] + })) + ); + }); + it('Should use custom comparator for the template assertion', () => { const factory = create(); const WrappedSpan = wrap('span'); From 017304cf02352fbb001810080ea6e7a9bfbda766 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Wed, 15 Apr 2020 12:06:07 +0100 Subject: [PATCH 12/15] Ensure unnamed widgets are not considered equal --- src/testing/assertRender.ts | 2 +- tests/testing/unit/renderer.tsx | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/testing/assertRender.ts b/src/testing/assertRender.ts index fb3fdbbe5..5bfccb934 100644 --- a/src/testing/assertRender.ts +++ b/src/testing/assertRender.ts @@ -30,7 +30,7 @@ function getTagName(node: VNode | WNode): string { name = widgetConstructor.toString(); } else { name = (widgetConstructor as any).name; - if (name === undefined) { + if (!name) { let id = widgetMap.get(widgetConstructor); if (id === undefined) { id = ++widgetClassCounter; diff --git a/tests/testing/unit/renderer.tsx b/tests/testing/unit/renderer.tsx index f16d131f9..a44b838ac 100644 --- a/tests/testing/unit/renderer.tsx +++ b/tests/testing/unit/renderer.tsx @@ -553,6 +553,26 @@ describe('test renderer', () => { ); }); + it('should differentiate between two unnamed widgets', () => { + const factory = create(); + const Foo = factory(() => 'foo'); + const Bar = factory(() => 'bar'); + const WidgetUnderTest = factory(() => ( +
+ +
+ )); + const template = assertionTemplate(() => ( +
+ +
+ )); + const r = renderer(() => ); + assert.throws(() => { + r.expect(template); + }); + }); + it('should run diffProperty middleware', () => { const factory = create({ diffProperty, invalidator }); let id = 0; From fdec205c919e035543f8bb5b012e55b22ddc3644 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Wed, 15 Apr 2020 13:59:18 +0100 Subject: [PATCH 13/15] Add ignore wrapper instead of the existing ignore widget --- docs/en/testing/supplemental.md | 38 +++++++++++++++++++++++- src/testing/assertionTemplate.ts | 3 -- src/testing/decorate.ts | 33 ++++++++++++++------ src/testing/interfaces.d.ts | 4 +++ src/testing/renderer.ts | 32 ++++++++++++++++++++ tests/testing/unit/assertionTemplate.tsx | 22 ++++++++++++-- tests/testing/unit/renderer.tsx | 2 +- 7 files changed, 117 insertions(+), 17 deletions(-) diff --git a/docs/en/testing/supplemental.md b/docs/en/testing/supplemental.md index 72c77ed8a..c0bbf5561 100644 --- a/docs/en/testing/supplemental.md +++ b/docs/en/testing/supplemental.md @@ -409,7 +409,8 @@ compare(comparator: (actual) => boolean) ``` ```tsx -import { assertionTemplate, wrap, compare } from '@dojo/framework/testing/renderer'; +import { wrap, compare } from '@dojo/framework/testing/renderer'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; // create a wrapped node the `h1` const WrappedHeader = wrap('h1'); @@ -421,6 +422,41 @@ const baseTemplate = assertionTemplate(() => ( )); ``` +## Ignoring Nodes during Assertion + +When dealing with widgets that render multiple items, for example a list it can be desirable to be able to instruct the test renderer to ignore sections of the output. For example asserting that the first and last items are valid and then ignoring the detail of the items in-between, simply asserting that they are the expected type. To do this with the test renderer the `ignore` function can be used that instructs the test renderer to ignore the node, as long as it is the same type, i.e. matching tag name or matching widget factory/constructor. + +```tsx +import { create, tsx } from '@dojo/framework/core/vdom'; +import renderer, { ignore } from '@dojo/framework/testing/renderer'; +import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; + +const factory = create().properties<{ items: string[] }>(); + +const ListWidget = create(function ListWidget({ properties }) { + const { items } = properties(); + return ( +
+
    {items.map((item) =>
  • {item}
  • )}
+
+ ); +}); + +const r = renderer(() => ); +const IgnoredItem = ignore('li'); +const template = assertionTemplate(() => ( +
+
    +
  • a
  • + + +
  • d
  • +
+
+)); +r.expect(template); +``` + ## Mocking Middleware When initializing the test renderer, mock middleware can get specified as part of the `RendererOptions`. The mock middleware gets defined as a tuple of the original middleware and the mock middleware implementation. Mock middleware gets created in the same way as any other middleware. diff --git a/src/testing/assertionTemplate.ts b/src/testing/assertionTemplate.ts index 734098f78..4a684134c 100644 --- a/src/testing/assertionTemplate.ts +++ b/src/testing/assertionTemplate.ts @@ -2,7 +2,6 @@ import { VNode, WNode, DNode, RenderResult, WidgetBaseInterface, Constructor } f import { isWNode, isVNode } from '../core/vdom'; import { decorate } from '../core/util'; import { Wrapped, WidgetFactory, NonComparable, CompareFunc } from './interfaces'; -import WidgetBase from '../core/WidgetBase'; export type PropertiesComparatorFunction = (actualProperties: T) => T; @@ -168,8 +167,6 @@ const replaceChildren = ( return render; }; -export class Ignore extends WidgetBase {} - export function assertionTemplate(renderFunc: () => DNode | DNode[]) { const assertionTemplateResult: any = () => { const render = renderFunc(); diff --git a/src/testing/decorate.ts b/src/testing/decorate.ts index 3aafd6b88..3aa5a114b 100644 --- a/src/testing/decorate.ts +++ b/src/testing/decorate.ts @@ -2,7 +2,6 @@ import { isVNode, isWNode } from '../core/vdom'; import { RenderResult, DNode, VNode, WNode } from '../core/interfaces'; import { Instruction } from './renderer'; import Map from '../shim/Map'; -import { Ignore } from './assertionTemplate'; import { findIndex } from '../shim/array'; import { decorate as coreDecorate } from '../core/util'; @@ -21,6 +20,20 @@ function isWrappedNode(node: any): node is VNode & { id: string } | WNode & { id return Boolean(isNode(node) && node.id); } +function isIgnoredNode(node: any): node is VNode | WNode { + return Boolean(node && node.isIgnore && isNode(node)); +} + +function isSameType(expected: DNode | { [index: string]: any }, actual: DNode | { [index: string]: any }): boolean { + if (isNode(expected) && isNode(actual)) { + if (isVNode(expected) && isVNode(actual) && expected.tag === actual.tag) { + return true; + } + return isWNode(expected) && isWNode(actual) && expected.widgetConstructor === actual.widgetConstructor; + } + return false; +} + export function decorateNodes(dNode: DNode[]): DecoratorResult; export function decorateNodes(dNode: DNode): DecoratorResult; export function decorateNodes(dNode: DNode | DNode[]): DecoratorResult; @@ -63,7 +76,7 @@ export function decorate(actual: RenderResult, expected: RenderResult, instructi throw new Error('Cannot use a wrapped test node more than once within an assertion template.'); } const instruction = instructions.get(expectedNode.id); - if (instruction) { + if (instruction && isSameType(actualNode, expectedNode)) { if (instruction.type === 'child') { const expectedChild: any = expectedNode.children && expectedNode.children[0]; const actualChild: any = isNode(actualNode) && actualNode.children && actualNode.children[0]; @@ -100,6 +113,15 @@ export function decorate(actual: RenderResult, expected: RenderResult, instructi wrappedNodeIds.push(expectedNode.id); } + if (isIgnoredNode(expectedNode)) { + const index = findIndex((expectedNode as any).parent.children, (child) => child === expectedNode); + if (index !== -1) { + if (isSameType(expectedNode, actualNode)) { + (expectedNode as any).parent.children[index] = actualNode || expectedNode; + } + } + } + if (isNode(expectedNode) && isNode(actualNode)) { const propertyKeys = Object.keys(expectedNode.properties); for (let i = 0; i < propertyKeys.length; i++) { @@ -115,13 +137,6 @@ export function decorate(actual: RenderResult, expected: RenderResult, instructi } } - if (expectedNode && (expectedNode as any).widgetConstructor === Ignore) { - const index = findIndex((expectedNode as any).parent.children, (child) => child === expectedNode); - if (index !== -1) { - (expectedNode as any).parent.children[index] = actualNode || expectedNode; - } - } - if (isNode(expectedNode)) { if (typeof expectedNode.properties === 'function') { const actualProperties = isNode(actualNode) ? actualNode.properties : {}; diff --git a/src/testing/interfaces.d.ts b/src/testing/interfaces.d.ts index 5842dfa25..6ec1a174a 100644 --- a/src/testing/interfaces.d.ts +++ b/src/testing/interfaces.d.ts @@ -12,6 +12,10 @@ export type Wrapped | WidgetFactory> id: string; }; +export type Ignore | WidgetFactory> = T & { + isIgnore: boolean; +}; + export interface CompareFunc { (actual: T): boolean; type: 'compare'; diff --git a/src/testing/renderer.ts b/src/testing/renderer.ts index 65b8869f4..b71c8d4b9 100644 --- a/src/testing/renderer.ts +++ b/src/testing/renderer.ts @@ -129,6 +129,38 @@ export function wrap(node: any): any { return nodeFactory; } +export function ignore( + node: string +): OptionalWNodeFactory<{ properties: Comparable; children: DNode | (DNode | DNode[])[] }> & { + tag: string; +}; +export function ignore( + node: Constructor +): Constructor>>; +export function ignore>( + node: T +): OptionalWNodeFactory<{ properties: Comparable; children: T['children'] }>; +export function ignore>( + node: T +): DefaultChildrenWNodeFactory<{ properties: Comparable; children: T['children'] }>; +export function ignore>( + node: T +): WNodeFactory<{ properties: Comparable; children: T['children'] }>; +export function ignore(node: any): any { + const nodeFactory: any = (properties: any, children: any[]) => { + const dNode: any = + typeof node === 'string' ? v(node, properties, children) : w(node as any, properties, children); + dNode.isIgnore = true; + return dNode; + }; + + nodeFactory.isFactory = true; + if (typeof node === 'string') { + nodeFactory.tag = nodeFactory; + } + return nodeFactory; +} + export function compare(compareFunc: (actual: unknown, expected: unknown) => boolean): CompareFunc { (compareFunc as any).type = 'compare'; return compareFunc as any; diff --git a/tests/testing/unit/assertionTemplate.tsx b/tests/testing/unit/assertionTemplate.tsx index 965f0e75f..5e4444037 100644 --- a/tests/testing/unit/assertionTemplate.tsx +++ b/tests/testing/unit/assertionTemplate.tsx @@ -1,10 +1,10 @@ const { describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -import { renderer, wrap } from '../../../src/testing/renderer'; +import { renderer, wrap, ignore } from '../../../src/testing/renderer'; import { WidgetBase } from '../../../src/core/WidgetBase'; import { v, w, tsx, create } from '../../../src/core/vdom'; -import assertionTemplate, { Ignore } from '../../../src/testing/assertionTemplate'; +import assertionTemplate from '../../../src/testing/assertionTemplate'; import { DNode } from '../../../src/core/interfaces'; class MyWidget extends WidgetBase<{ @@ -283,8 +283,9 @@ describe('new/assertionTemplate', () => { it('can use ignore', () => { const nodes: DNode[] = []; + const IgnoredListItem = ignore('li'); for (let i = 0; i < 28; i++) { - nodes.push(w(Ignore, {})); + nodes.push(w(IgnoredListItem, {})); } const childListAssertion = baseListAssertion.replaceChildren(WrappedList, () => [ v('li', ['item: 0']), @@ -295,6 +296,21 @@ describe('new/assertionTemplate', () => { r.expect(childListAssertion); }); + it('will not ignore when the node type does not match', () => { + const nodes: DNode[] = []; + const IgnoredListItem = ignore('div'); + for (let i = 0; i < 28; i++) { + nodes.push(w(IgnoredListItem, {})); + } + const childListAssertion = baseListAssertion.replaceChildren(WrappedList, () => [ + v('li', ['item: 0']), + ...nodes, + v('li', ['item: 29']) + ]); + const r = renderer(() => w(ListWidget, {})); + assert.throws(() => r.expect(childListAssertion)); + }); + it('should be immutable', () => { const fooAssertion = baseAssertion .setChildren(WrappedRoot, () => ['foo']) diff --git a/tests/testing/unit/renderer.tsx b/tests/testing/unit/renderer.tsx index a44b838ac..84c990746 100644 --- a/tests/testing/unit/renderer.tsx +++ b/tests/testing/unit/renderer.tsx @@ -189,7 +189,7 @@ describe('test renderer', () => { ); }); - it('should trigger properties in the correct order', () => { + it('should call properties in the correct order', () => { const factory = create({ icache }); const MyWidget = factory(function MyWidget({ middleware: { icache } }) { From 8de7c63efe2837669265ebc02c7bb035fee21dfb Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Wed, 15 Apr 2020 14:38:39 +0100 Subject: [PATCH 14/15] rename assertion template to assertion --- docs/en/testing/introduction.md | 18 +- docs/en/testing/supplemental.md | 220 +++++------ src/testing/README.md | 370 +----------------- src/testing/assertionTemplate.ts | 290 -------------- src/testing/renderer.ts | 291 +++++++++++++- tests/routing/unit/ActiveLink.ts | 25 +- tests/routing/unit/Link.ts | 21 +- tests/routing/unit/Outlet.tsx | 21 +- tests/routing/unit/Route.ts | 11 +- tests/testing/unit/all.ts | 2 +- .../{assertionTemplate.tsx => assertion.tsx} | 20 +- .../unit/mocks/middleware/breakpoint.tsx | 15 +- tests/testing/unit/mocks/middleware/focus.tsx | 7 +- .../testing/unit/mocks/middleware/icache.tsx | 7 +- .../unit/mocks/middleware/intersection.tsx | 9 +- tests/testing/unit/mocks/middleware/node.tsx | 7 +- .../testing/unit/mocks/middleware/resize.tsx | 15 +- tests/testing/unit/mocks/middleware/store.tsx | 5 +- .../unit/mocks/middleware/validity.tsx | 9 +- tests/testing/unit/renderer.tsx | 53 ++- 20 files changed, 499 insertions(+), 917 deletions(-) delete mode 100644 src/testing/assertionTemplate.ts rename tests/testing/unit/{assertionTemplate.tsx => assertion.tsx} (93%) diff --git a/docs/en/testing/introduction.md b/docs/en/testing/introduction.md index 41e75f34d..382755878 100644 --- a/docs/en/testing/introduction.md +++ b/docs/en/testing/introduction.md @@ -63,17 +63,17 @@ export default Home; ```ts const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import renderer, { assertionTemplate } from '@dojo/framework/testing/renderer'; +import renderer, { assertion } from '@dojo/framework/testing/renderer'; import Home from '../../../src/widgets/Home'; import * as css from '../../../src/widgets/Home.m.css'; -const baseTemplate = assertionTemplate(() =>

Home Page

); +const baseAssertion = assertion(() =>

Home Page

); describe('Home', () => { it('default renders correctly', () => { const r = renderer(() => ); - r.expect(baseTemplate); + r.expect(baseAssertion); }); }); ``` @@ -144,21 +144,20 @@ const Profile = factory(function Profile({ properties }) { export default Profile; ``` -- Create an assertion template using `@dojo/framework/testing/assertionTemplate` +- Create an assertion template using `@dojo/framework/testing/assertion` > tests/unit/widgets/Profile.tsx ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; -import renderer from '@dojo/framework/testing/renderer'; +import renderer, { assertion } from '@dojo/framework/testing/renderer'; import Profile from '../../../src/widgets/Profile'; import * as css from '../../../src/widgets/Profile.m.css'; // Create an assertion -const profileAssertion = assertionTemplate(() =>

Welcome Stranger!

); +const profileAssertion = assertion(() =>

Welcome Stranger!

); describe('Profile', () => { it('default renders correctly', () => { @@ -176,8 +175,7 @@ To work with assertion templates, wrapped nodes can get created using `@dojo/fra ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; -import renderer { wrap } from '@dojo/framework/testing/renderer'; +import renderer { wrap, assertion } from '@dojo/framework/testing/renderer'; import Profile from '../../../src/widgets/Profile'; import * as css from '../../../src/widgets/Profile.m.css'; @@ -186,7 +184,7 @@ import * as css from '../../../src/widgets/Profile.m.css'; const WrappedHeader = wrap('h1'); // Create an assertion -const profileAssertion = assertionTemplate(() => ( +const profileAssertion = assertion(() => ( // Use the wrapped node in place of the normal node Welcome Stranger! )); diff --git a/docs/en/testing/supplemental.md b/docs/en/testing/supplemental.md index c0bbf5561..aea199c0e 100644 --- a/docs/en/testing/supplemental.md +++ b/docs/en/testing/supplemental.md @@ -2,19 +2,19 @@ Dojo provides a simple and type safe test renderer for shallowly asserting the expected output and behavior from a widget. The test renderer's API has been designed to encourage unit testing best practices from the outset to ensure high confidence in you Dojo application. -Working with [assertion templates](/learn/testing/test-renderer#assertion-templates) and the test renderer is done using [wrapped test nodes](/learn/testing/test-renderer#wrapped-test-nodes) that are defined in the assertion templates structure, ensuring type safety throughout the testing life-cycle. +Working with [assertions](/learn/testing/test-renderer#assertion) and the test renderer is done using [wrapped test nodes](/learn/testing/test-renderer#wrapped-test-nodes) that are defined in the assertion structure, ensuring type safety throughout the testing life-cycle. -The expected structure of a widget is defined using an assertionTemplate and passed to the test renderer's `.expect()` function which executes the assertion. +The expected structure of a widget is defined using an assertion and passed to the test renderer's `.expect()` function which executes the assertion. > src/MyWidget.spec.tsx ```tsx import { tsx } from '@dojo/framework/core/vdom'; -import renderer, { assertionTemplate } from '@dojo/framework/testing/renderer'; +import renderer, { assertion } from '@dojo/framework/testing/renderer'; import MyWidget from './MyWidget'; -const baseTemplate = assertionTemplate(() => ( +const baseAssertion = assertion(() => (

Heading

Sub Heading

@@ -24,12 +24,12 @@ const baseTemplate = assertionTemplate(() => ( const r = renderer(() => ); -r.expect(baseTemplate); +r.expect(baseAssertion); ``` ## Wrapped Test Nodes -In order for the test renderer and assertion templates to be able to identify nodes within the expected and actual node structure a special wrapping node must be used. The wrapped nodes can get used in place of the real node in the expected assertion template structure, maintaining all the correct property and children typings. +In order for the test renderer and assertions to be able to identify nodes within the expected and actual node structure a special wrapping node must be used. The wrapped nodes can get used in place of the real node in the expected assertion structure, maintaining all the correct property and children typings. To create a wrapped test node use the `wrap` function from `@dojo/framework/testing/renderer`: @@ -49,11 +49,11 @@ const WrappedDiv = wrap('div'); The test renderer uses the location of a wrapped test node in the expected tree to attempt to perform any requested actions (either `r.property()` or `r.child()`) on the actual output of the widget under test. If the wrapped test node does not match the corresponding node in the actual output tree then no action will be performed and the assertion will report a failure. -**Note:** Wrapped test nodes should only be used once within an assertion template, if the same test node is detected more than once during an assertion an error will be thrown and the test fail. +**Note:** Wrapped test nodes should only be used once within an assertion, if the same test node is detected more than once during an assertion an error will be thrown and the test fail. -## Assertion Templates +## Assertion -Assertion templates get used to build the expected widget output structure to use with `renderer.expect()`. The templates expose a wide range of APIs that enable the expected output to vary between tests. +Assertions get used to build the expected widget output structure to use with `renderer.expect()`. The assertion expose a wide range of APIs that enable the expected output to vary between tests. Given a widget that renders output differently based on property values: @@ -78,14 +78,14 @@ const Profile = factory(function Profile({ properties }) { export default Profile; ``` -Create an assertion template using `@dojo/framework/testing/renderer#assertionTemplate`: +Create an assertion using `@dojo/framework/testing/renderer#assertion`: > src/Profile.spec.tsx ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import renderer, { assertionTemplate, wrap } from '@dojo/framework/testing/renderer'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import Profile from '../../../src/widgets/Profile'; import * as css from '../../../src/widgets/Profile.m.css'; @@ -93,8 +93,8 @@ import * as css from '../../../src/widgets/Profile.m.css'; // Create a wrapped node const WrappedHeader = wrap('h1'); -// Create an assertion template using the `WrappedHeader` in place of the `h1` -const baseAssertion = assertionTemplate(() => Welcome Stranger!); +// Create an assertion using the `WrappedHeader` in place of the `h1` +const baseAssertion = assertion(() => Welcome Stranger!); describe('Profile', () => { it('Should render using the default username', () => { @@ -106,16 +106,16 @@ describe('Profile', () => { }); ``` -To test when a `username` property gets passed to the `Profile` widget, we could create a new assertion template with the updated expected username. However, as a widget increases its functionality, recreating the entire assertion template for each scenario becomes verbose and unmaintainable, as any changes to the common widget structure would require updating every assertion template. +To test when a `username` property gets passed to the `Profile` widget, we could create a new assertion with the updated expected username. However, as a widget increases its functionality, recreating the entire assertion for each scenario becomes verbose and unmaintainable, as any changes to the common widget structure would require updating every assertion. -To help avoid the maintenance overhead and reduce duplication, assertion templates offer a comprehensive API for creating variations from a base template. The assertion template API uses wrapped test nodes to identify the node in the expected structure to update. +To help avoid the maintenance overhead and reduce duplication, assertions offer a comprehensive API for creating variations from a base assertion. The assertion API uses wrapped test nodes to identify the node in the expected structure to update. > src/Profile.spec.tsx ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import renderer, { assertionTemplate, wrap } from '@dojo/framework/testing/renderer'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import Profile from '../../../src/widgets/Profile'; import * as css from '../../../src/widgets/Profile.m.css'; @@ -123,8 +123,8 @@ import * as css from '../../../src/widgets/Profile.m.css'; // Create a wrapped node const WrappedHeader = wrap('h1'); -// Create an assertion template using the `WrappedHeader` in place of the `h1` -const baseAssertion = assertionTemplate(() => Welcome Stranger!); +// Create an assertion using the `WrappedHeader` in place of the `h1` +const baseAssertion = assertion(() => Welcome Stranger!); describe('Profile', () => { it('Should render using the default username', () => { @@ -137,121 +137,120 @@ describe('Profile', () => { it('Should render using the passed username', () => { const r = renderer(() => ); - // Create a variation of the base template - const usernameTemplate = baseAssertion.setChildren(WrappedHeader, () => ['Dojo']); + // Create a variation of the base assertion + const usernameAssertion = baseAssertion.setChildren(WrappedHeader, () => ['Dojo']); - // Test against the username template - r.expect(usernameTemplate); + // Test against the username assertion + r.expect(usernameAssertion); }); }); ``` -Creating templates from a base template means that if there is a change to the default widget output, only a change to the baseTemplate is required to update all the widget's tests. +Creating assertions from a base assertion means that if there is a change to the default widget output, only a change to the baseAssertion is required to update all the widget's tests. -### Assertion Template API +### Assertion API -#### `assertionTemplate.setChildren()` +#### `assertion.setChildren()` -Returns a new assertion template with the new children either pre-pended, appended or replaced depending on the `type` passed. +Returns a new assertion with the new children either pre-pended, appended or replaced depending on the `type` passed. ```tsx .setChildren( wrapped: Wrapped, children: () => RenderResult, type: 'prepend' | 'replace' | 'append' = 'replace' -);` +): AssertionResult; ``` -Convenience functions exists for all 3 types, [`prepend()`](/missing/link), [`append()`](/missing/link) and [`replaceChildren()`](/missing/link). +#### `assertion.append()` -#### `assertionTemplate.append()` - -Returns a new assertion template with the new children appended to the node's existing children. +Returns a new assertion with the new children appended to the node's existing children. ```tsx -.append(wrapped: Wrapped, children: () => RenderResult); +.append(wrapped: Wrapped, children: () => RenderResult): AssertionResult; ``` -#### `assertionTemplate.prepend()` +#### `assertion.prepend()` -Returns a new assertion template with the new children pre-pended to the node's existing children. +Returns a new assertion with the new children pre-pended to the node's existing children. ```tsx -.append(wrapped: Wrapped, children: () => RenderResult); +.append(wrapped: Wrapped, children: () => RenderResult): AssertionResult; ``` -#### `assertionTemplate.replaceChildren()` +#### `assertion.replaceChildren()` -Returns a new assertion template with the new children replacing the node's existing children. +Returns a new assertion with the new children replacing the node's existing children. ```tsx -.append(wrapped: Wrapped, children: () => RenderResult); +.append(wrapped: Wrapped, children: () => RenderResult): AssertionResult; ``` -#### `assertionTemplate.insertSiblings()` +#### `assertion.insertSiblings()` -Returns a new assertion template with the passed children inserted either `before` or `after` depending on the `type` passed. +Returns a new assertion with the passed children inserted either `before` or `after` depending on the `type` passed. ```tsx .insertSiblings( wrapped: Wrapped, children: () => RenderResult, type: 'before' | 'after' = 'before' -);` +): AssertionResult; ``` -#### `assertionTemplate.insertBefore()` +#### `assertion.insertBefore()` -Returns a new assertion template with the passed children inserted before the existing node's children. +Returns a new assertion with the passed children inserted before the existing node's children. ```tsx -.insertBefore(wrapped: Wrapped, children: () => RenderResult); +.insertBefore(wrapped: Wrapped, children: () => RenderResult): AssertionResult; ``` -#### `assertionTemplate.insertAfter()` +#### `assertion.insertAfter()` -Returns a new assertion template with the passed children inserted after the existing node's children. +Returns a new assertion with the passed children inserted after the existing node's children. ```tsx -.insertAfter(wrapped: Wrapped, children: () => RenderResult); +.insertAfter(wrapped: Wrapped, children: () => RenderResult): AssertionResult; ``` -#### `assertionTemplate.replace()` +#### `assertion.replace()` -Returns a new assertion template replacing the existing node with the node that is passed. Note that if you need to interact with the new node in either assertion templates or the test renderer, it should be a wrapped test node. +Returns a new assertion replacing the existing node with the node that is passed. Note that if you need to interact with the new node in either assertions or the test renderer, it should be a wrapped test node. ```tsx -.replace(wrapped: Wrapped, node: DNode); +.replace(wrapped: Wrapped, node: DNode): AssertionResult; ``` -#### `assertionTemplate.remove()` +#### `assertion.remove()` -Returns a new assertion template removing the target wrapped node completely. +Returns a new assertion removing the target wrapped node completely. ```tsx -.remove(wrapped: Wrapped); +.remove(wrapped: Wrapped): AssertionResult; ``` -#### `assertionTemplate.setProperty()` +#### `assertion.setProperty()` -Returns a new assertion template with the updated property for the target wrapped node. +Returns a new assertion with the updated property for the target wrapped node. ```tsx .setProperty( wrapped: Wrapped, property: K, value: T['properties'][K] +): AssertionResult; ``` -#### `assertionTemplate.setProperties()` +#### `assertion.setProperties()` -Returns a new assertion template with the updated properties for the target wrapped node. +Returns a new assertion with the updated properties for the target wrapped node. ```tsx .setProperties( wrapped: Wrapped, value: T['properties'] | PropertiesComparatorFunction -): AssertionTemplateResult; +): AssertionResult; ``` A function can be set in place of the properties object to return the expected properties based off the actual properties. @@ -295,7 +294,7 @@ export const MyWidget = factory(function MyWidget({ properties, middleware: { ic ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import renderer, { assertionTemplate, wrap } from '@dojo/framework/testing/renderer'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import * as sinon from 'sinon'; import MyWidget from './MyWidget'; @@ -305,7 +304,7 @@ const WrappedButton = wrap('button'); const WrappedSpan = wrap('span'); -const baseAssertion = assertionTemplate(() => ( +const baseAssertion = assertion(() => (

Header

0 @@ -327,11 +326,11 @@ describe('MyWidget', () => { // register a call to the button's onclick property r.property(WrappedButton, 'onclick'); - // create a new template with the updated count - const counterTemplate = baseAssertion.setChildren(WrappedSpan, () => ['1']); + // create a new assertion with the updated count + const counterAssertion = baseAssertion.setChildren(WrappedSpan, () => ['1']); - // expect against the new template, the property will be called before the test render - r.expect(counterTemplate); + // expect against the new assertion, the property will be called before the test render + r.expect(counterAssertion); // once the assertion is complete, check that the stub property was called assert.isTrue(onClickStub.calledOnce); @@ -345,7 +344,7 @@ Arguments for the function can be passed after the function name, for example `r To assert the output from functional children the test renderer needs to understand how to resolve the child render functions. This includes passing in any expected injected values. -The test renderer `renderer.child()` function enables children to get resolved in order to include them in the assertion. Using the `.child()` function requires the widget with functional children to be wrapped when included in the assertion template, and the wrapped node gets passed to the `.child` function. +The test renderer `renderer.child()` function enables children to get resolved in order to include them in the assertion. Using the `.child()` function requires the widget with functional children to be wrapped when included in the assertion, and the wrapped node gets passed to the `.child` function. > src/MyWidget.tsx @@ -372,7 +371,7 @@ export const MyWidget = factory(function MyWidget() { ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import renderer, { assertionTemplate, wrap } from '@dojo/framework/testing/renderer'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import MyWidgetWithChildren from './MyWidgetWithChildren'; import MyWidget from './MyWidget'; @@ -380,7 +379,7 @@ import MyWidget from './MyWidget'; // Create a wrapped node for the widget with functional children const WrappedMyWidgetWithChildren = wrap(MyWidgetWithChildren); -const baseAssertion = assertionTemplate(() => ( +const baseAssertion = assertion(() => (

Header

{() =>
Hello!
} @@ -409,13 +408,12 @@ compare(comparator: (actual) => boolean) ``` ```tsx -import { wrap, compare } from '@dojo/framework/testing/renderer'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import { assertion, wrap, compare } from '@dojo/framework/testing/renderer'; // create a wrapped node the `h1` const WrappedHeader = wrap('h1'); -const baseTemplate = assertionTemplate(() => ( +const baseAssertion = assertion(() => (
typeof actual === 'string')}>Header!
@@ -428,8 +426,7 @@ When dealing with widgets that render multiple items, for example a list it can ```tsx import { create, tsx } from '@dojo/framework/core/vdom'; -import renderer, { ignore } from '@dojo/framework/testing/renderer'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import renderer, { assertion, ignore } from '@dojo/framework/testing/renderer'; const factory = create().properties<{ items: string[] }>(); @@ -444,7 +441,7 @@ const ListWidget = create(function ListWidget({ properties }) { const r = renderer(() => ); const IgnoredItem = ignore('li'); -const template = assertionTemplate(() => ( +const listAssertion = assertion(() => (
  • a
  • @@ -454,7 +451,7 @@ const template = assertionTemplate(() => (
)); -r.expect(template); +r.expect(listAssertion); ``` ## Mocking Middleware @@ -522,8 +519,7 @@ To test that the `properties().fetchItems` method is called when the button is c ```ts const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; -import renderer, { wrap } from '@dojo/framework/testing/renderer'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import Action from '../../../src/widgets/Action'; import * as css from '../../../src/widgets/Action.m.css'; @@ -537,7 +533,7 @@ describe('Action', () => { const fetchItems = stub(); it('can fetch data on button click', () => { const WrappedButton = wrap(Button); - const template = assertionTemplate(() => ( + const baseAssertion = assertion(() => (
{}}> Fetch @@ -545,9 +541,9 @@ describe('Action', () => {
)); const r = renderer(() => ); - r.expect(template); + r.expect(baseAssertion); r.property(WrappedButton, 'onClick'); - r.expect(template); + r.expect(baseAssertion); assert.isTrue(fetchItems.calledOnce); }); }); @@ -596,8 +592,7 @@ By using the `mockBreakpoint(key: string, contentRect: Partial) ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import renderer, { wrap } from '@dojo/framework/testing/renderer'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import breakpoint from '@dojo/framework/core/middleware/breakpoint'; import createBreakpointMock from '@dojo/framework/testing/mocks/middleware/breakpoint'; import Breakpoint from '../../src/Breakpoint'; @@ -606,7 +601,7 @@ describe('Breakpoint', () => { it('resizes correctly', () => { const WrappedHeader = wrap('h1'); const mockBreakpoint = createBreakpointMock(); - const template = assertionTemplate(() => ( + const baseAssertion = assertion(() => (
Header
Longer description
@@ -615,11 +610,11 @@ describe('Breakpoint', () => { const r = renderer(() => , { middleware: [[breakpoint, mockBreakpoint]] }); - r.expect(template); + r.expect(baseAssertion); mockBreakpoint('root', { breakpoint: 'LG', contentRect: { width: 800 } }); - r.expect(template.insertAfter(WrappedHeader, () => [

Subtitle

]); + r.expect(baseAssertion.insertAfter(WrappedHeader, () => [

Subtitle

]); }); }); ``` @@ -657,8 +652,7 @@ By calling `focusMock(key: string | number, value: boolean)` the result of the ` ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import renderer, { wrap } from '@dojo/framework/testing/renderer'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import focus from '@dojo/framework/core/middleware/focus'; import createFocusMock from '@dojo/framework/testing/mocks/middleware/focus'; import * as css from './FormWidget.m.css'; @@ -667,7 +661,7 @@ describe('Focus', () => { it('adds a "focused" class to the wrapper when the input is focused', () => { const focusMock = createFocusMock(); const WrappedRoot = wrap('div'); - const template = assertionTemplate(() => ( + const baseAssertion = assertion(() => ( @@ -676,11 +670,11 @@ describe('Focus', () => { middleware: [[focus, focusMock]] }); - r.expect(template); + r.expect(baseAssertion); focusMock('text', true); - r.expect(template.setProperty(WrappedRoot, 'classes', [css.root, css.focused])); + r.expect(baseAssertion.setProperty(WrappedRoot, 'classes', [css.root, css.focused])); }); }); ``` @@ -716,8 +710,7 @@ Testing the asynchronous result using the mock `icache` middleware is simple: ```tsx const { describe, it, afterEach } = intern.getInterface('bdd'); -import renderer, { wrap } from '@dojo/framework/testing/renderer'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import { tsx } from '@dojo/framework/core/vdom'; import * as sinon from 'sinon'; import global from '@dojo/framework/shim/global'; @@ -735,14 +728,14 @@ describe('MyWidget', () => { global.fetch = sinon.stub().returns(Promise.resolve({ json: () => Promise.resolve('api data') })); const WrappedRoot = wrap('div'); - const template = assertionTemplate(() => Loading); + const baseAssertion = assertion(() => Loading); const mockICache = createICacheMock(); const r = renderer(() => , { middleware: [[icache, mockICache]] }); - r.expect(template); + r.expect(baseAssertion); // await the async method passed to the mock cache await mockICache('users'); - r.expect(template.setChildren(WrappedRoot, () => ['api data'])); + r.expect(baseAssertion.setChildren(WrappedRoot, () => ['api data'])); }); }); ``` @@ -771,8 +764,7 @@ Using the mock `intersection` middleware: import { tsx } from '@dojo/framework/core/vdom'; import createIntersectionMock from '@dojo/framework/testing/mocks/middleware/intersection'; import intersection from '@dojo/framework/core/middleware/intersection'; -import renderer, { wrap } from '@dojo/framework/testing/renderer'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import MyWidget from './MyWidget'; @@ -784,18 +776,18 @@ describe('MyWidget', () => { // replace the original middleware const r = renderer(() => , { middleware: [[intersection, intersectionMock]] }); const WrappedRoot = wrap('div'); - const assertionTemplate = assertionTemplate(() => ( + const assertion = assertion(() => ( {`{"intersectionRatio":0,"isIntersecting":false}`} )); // call renderer.expect as usual, asserting the default response - r.expect(assertionTemplate); + r.expect(assertion); // use the intersection mock to set the expected return // of the intersection middleware by key intersectionMock('root', { isIntersecting: true }); // assert again with the updated expectation - r.expect(assertionTemplate.setChildren(WrappedRoot, () => [`{"isIntersecting": true }`])); + r.expect(assertion.setChildren(WrappedRoot, () => [`{"isIntersecting": true }`])); }); }); ``` @@ -848,8 +840,7 @@ Using the mock `resize` middleware: import { tsx } from '@dojo/framework/core/vdom'; import createResizeMock from '@dojo/framework/testing/mocks/middleware/resize'; import resize from '@dojo/framework/core/middleware/resize'; -import renderer, { wrap } from '@dojo/framework/testing/renderer'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import renderer, { assertion, wrap } from '@dojo/framework/testing/renderer'; import MyWidget from './MyWidget'; @@ -862,17 +853,17 @@ describe('MyWidget', () => { const r = renderer(() => , { middleware: [[resize, resizeMock]] }); const WrappedRoot = wrap('div'); - const template = assertionTemplate(() =>
null
); + const baseAssertion = assertion(() =>
null
); // call renderer.expect as usual - r.expect(template); + r.expect(baseAssertion); // use the resize mock to set the expected return of the resize middleware // by key resizeMock('root', { width: 100 }); // assert again with the updated expectation - r.expect(template.setChildren(WrappedRoot, () [`{"width":100}`]);) + r.expect(baseAssertion.setChildren(WrappedRoot, () [`{"width":100}`]);) }); }); ``` @@ -950,27 +941,27 @@ describe('MyWidget', () => { const r = renderer(() => , { middleware: [[store, mockStore]] }); - r.expect(/* assertion template for `Loading`*/); + r.expect(/* assertion for `Loading`*/); // assert again the stubbed process expect(myProcessStub.calledWith({ id: 'id' })).toBeTruthy(); mockStore((path) => [replace(path('isLoading', true)]); - r.expect(/* assertion template for `Loading`*/); + r.expect(/* assertion for `Loading`*/); expect(myProcessStub.calledOnce()).toBeTruthy(); // use the mock store to apply operations to the store mockStore((path) => [replace(path('details', { id: 'id' })]); mockStore((path) => [replace(path('isLoading', true)]); - r.expect(/* assertion template for `ShowDetails`*/); + r.expect(/* assertion for `ShowDetails`*/); properties.id = 'other'; - r.expect(/* assertion template for `Loading`*/); + r.expect(/* assertion for `Loading`*/); expect(myProcessStub.calledTwice()).toBeTruthy(); expect(myProcessStub.secondCall.calledWith({ id: 'other' })).toBeTruthy(); mockStore((path) => [replace(path('details', { id: 'other' })]); - r.expect(/* assertion template for `ShowDetails`*/); + r.expect(/* assertion for `ShowDetails`*/); }); }); ``` @@ -1011,8 +1002,7 @@ Using `validityMock(key: string, value: { valid?: boolean, message?: string; })` ```tsx const { describe, it } = intern.getInterface('bdd'); import { tsx } from '@dojo/framework/core/vdom'; -import renderer from '@dojo/framework/testing/renderer'; -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; +import renderer, { assertion } from '@dojo/framework/testing/renderer'; import validity from '@dojo/framework/core/middleware/validity'; import createValidityMock from '@dojo/framework/testing/mocks/middleware/validity'; import * as css from './FormWidget.m.css'; @@ -1026,21 +1016,21 @@ describe('Validity', () => { }); const WrappedRoot = wrap('div'); - const template = assertionTemplate(() => ( + const baseAssertion = assertion(() => ( {}} /> )); - r.expect(template); + r.expect(baseAssertion); validityMock('input', { valid: false, message: 'invalid message' }); - const invalidTemplate = template + const invalidAssertion = baseAssertion .append(WrappedRoot, () => [

invalid message

]) .setProperty(WrappedRoot, 'classes', [css.root, css.invalid]); - r.expect(invalidTemplate); + r.expect(invalidAssertion); }); }); ``` diff --git a/src/testing/README.md b/src/testing/README.md index 23c0d8e8b..850fbbb7d 100644 --- a/src/testing/README.md +++ b/src/testing/README.md @@ -2,373 +2,17 @@ Simple API for testing and asserting Dojo widget's expected virtual DOM and behavior. -- [Features](#features) -- [`harness`](#harness) - - [Custom Comparators](#custom-comparators) -- [selectors](#selectors) -- [`harness.expect`](#harnessexpect) -- [`harness.expectPartial`](#harnessexpectpartial) -- [`harness.trigger`](#harnesstrigger) -- [Assertion Templates](#assertion-templates) - -## Features - - Simple, familiar and minimal API - Focused on testing Dojo virtual DOM structures - No DOM requirement by default - Full functional and tsx support -## harness - -`harness()` is the primary API when working with `@dojo/framework/testing`, essentially setting up each test and providing a context to perform virtual DOM assertions and interactions. Designed to mirror the core behavior for widgets when updating `properties` or `children` and widget invalidation, with no special or custom logic required. - -### API - -```ts -harness(renderFunction: () => WNode, customComparatorsOrOptions?: CustomComparator[] | HarnessOptions): Harness; -``` - -- `renderFunction`: A function that returns a WNode for the widget under test -- [`customComparators`](custom-comparators): Array of custom comparator descriptors. Each provides a comparator function to be used during the comparison for `properties` located using a `selector` and `property` name -- ['options']: options object that can be used to define custom comparators or mock middleware. - -The harness returns a `Harness` object that provides a small API for interacting with the widget under test: - -`Harness` - -- [`expect`](#harnessexpect): Performs an assertion against the full render output from the widget under test. -- [`expectPartial`](#harnessexpectpartial): Performs an assertion against a section of the render output from the widget under test. -- [`trigger`](#harnesstrigger): Used to trigger a function from a node on the widget under test's API -- [`getRender`](#harnessgetRender): Returns a render from the harness based on the index provided - -Setting up a widget for testing is simple and familiar using the `w()` function from `@dojo/framework/core`: - -```ts -class MyWidget extends WidgetBase<{ foo: string }> { - protected render() { - const { foo } = this.properties; - return v('div', { foo }, this.children); - } -} - -const h = harness(() => w(MyWidget, { foo: 'bar' }, ['child'])); -``` - -The harness also supports `tsx` usage as show below. For the rest of the README the examples will be using the programmatic `w()` API, there are more examples of `tsx` in the [unit tests](./blob/master/tests/unit/harnessWithTsx.tsx). - -```ts -const h = harness(() => child); -``` - -The `renderFunction` is lazily executed so it can include additional logic to manipulate the widget's `properties` and `children` between assertions. - -```ts -let foo = 'bar'; - -const h = harness(() => { - return w(MyWidget, { foo }, [ 'child' ])); -}; - -h.expect(/** assertion that includes bar **/); -// update the property that is passed to the widget -foo = 'foo'; -h.expect(/** assertion that includes foo **/) -``` - -### Custom Comparators - -There are circumstances where the exact value of a property is unknown during testing, so will require the use of a custom compare descriptor. - -The descriptors have a [`selector`](./path/to/selector) to locate the virtual nodes to check, a property name for the custom compare and a comparator function that receives the actual value and returns a boolean result for the assertion. - -```ts -const compareId = { - selector: '*', // all nodes - property: 'id', - comparator: (value: any) => typeof value === 'string' // checks the property value is a string -}; - -const h = harness(() => w(MyWidget, {}), [compareId]); -``` - -For all assertions, using the returned `harness` API will now only test identified `id` properties using the `comparator` instead of the standard equality. - -### Middlewares - -The middlewares harness option, is for functional widgets that utilize middlewares. The option accepts a tuple the original middleware and a mock middleware that will be injected into the any widgets or middleware used by the widget under test. - -```tsx -import realMiddleware from './realMiddleware'; -import mockMiddleware from './mockMiddleware'; - -const h = harness(() => , { - middleware: [ [ realMiddleware: mockMiddleware ] ] -}); -``` - -Core middleware provided by `@dojo/framework/core/vdom` such as `destroy`, `diffProperty` and `invalidator` are automatically mocked for all middlewares. All other middleware will need to be manually mocked, Dojo provides a selection of mocks: - -- node -- resize -- intersection - -Example: - -```tsx -const resizeMock = createResizeMock(); -const factory = create({ resize }); -const App = factory(({ middleware: { resize } }) => { - const rects = resize.get('root'); - return
{JSON.stringify(rects)}
; -}); -const h = harness(() => , { middleware: [[resize, resizeMock]] }); -h.expect(() =>
null
); -resizeMock('root', { width: 100 }); -h.expect(() =>
{`{"width":100}`}
); -resizeMock('root', { width: 101 }); -h.expect(() =>
{`{"width":101}`}
); -``` - -## selectors - -The `harness` APIs commonly support a concept of CSS style selectors to target nodes within the virtual DOM for assertions and operations. Review the [full list of supported selectors](https://github.com/fb55/css-select#supported-selectors) for more information. - -In addition to the standard API: - -- The `@` symbol is supported as shorthand for targeting a node's `key` property -- The `classes` property is used instead of `class` when using the standard shorthand `.` for targeting classes - -## `harness.expect` - -The most common requirement for testing is to assert the structural output from a widget's `render` function. `expect` accepts a render function that returns the expected render output from the widget under test. - -API - -```ts -expect(expectedRenderFunction: () => DNode | DNode[], actualRenderFunction?: () => DNode | DNode[]); -``` - -- `expectedRenderFunction`: A function that returns the expected `DNode` structure of the queried node -- `actualRenderFunction`: An optional function that returns the actual `DNode` structure to be asserted - -```ts -h.expect(() => - v('div', { key: 'foo' }, [w(Widget, { key: 'child-widget' }), 'text node', v('span', { classes: ['class'] })]) -); -``` - -Optionally `expect` can accepts a second parameter of function that returns a render result to assert against. - -```ts -h.expect(() => v('div', { key: 'foo' }), () => v('div', { key: 'foo' })); -``` - -If the actual render output and expected render output are different, an exception is thrown with a structured visualization indicating all differences with `(A)` (the actual value) and `(E)` (the expected value). - -Example assertion failure output: - -```ts -v("div", { - "classes": [ - "root", -(A) "other" -(E) "another" - ], - "onclick": "function" -}, [ - v("span", { - "classes": "span", - "id": "random-id", - "key": "label", - "onclick": "function", - "style": "width: 100px" - }, [ - "hello 0" - ]) - w(ChildWidget, { - "id": "random-id", - "key": "widget" - }) - w("registry-item", { - "id": true, - "key": "registry" - }) -]) -``` - -### `harness.expectPartial` - -`expectPartial` asserts against a section of the widget's render output based on a [`selector`](#selectors). - -API - -```ts -expectPartial(selector: string, expectedRenderFunction: () => DNode | DNode[]); -``` - -- `selector`: The selector query to find the node to target -- `expectedRenderFunction`: A function that returns the expected `DNode` structure of the queried node -- `actualRenderFunction`: An optional function that returns the actual `DNode` structure to be asserted - -Example usage: - -```ts -h.expectPartial('@child-widget', () => w(Widget, { key: 'child-widget' })); -``` - -#### `harness.trigger` - -`harness.trigger()` calls a function with the `name` on the node targeted by the `selector`. - -```ts -interface FunctionalSelector { - (node: VNode | WNode): undefined | Function; -} - -trigger(selector: string, functionSelector: string | FunctionalSelector, ...args: any[]): any; -``` - -- `selector`: The selector query to find the node to target -- `functionSelector`: Either the name of the function to call from found node's properties or a functional selector that returns a function from a nodes properties. -- `args`: The arguments to call the located function with - -Returns the result of the function triggered if one is returned. - -Example Usage(s): - -```ts -// calls the `onclick` function on the first node with a key of `foo` -h.trigger('@foo', 'onclick'); -``` - -```ts -// calls the `customFunction` function on the first node with a key of `bar` with an argument of `100` -// and receives the result of the triggered function -const result = h.trigger('@bar', 'customFunction', 100); -``` - -A `functionalSelector` can be used return a function that is nested in a widget's properties. The function will be triggered, in the same way that using a plain string `functionSelector`. - -Example Usage: - -Given a VDOM tree like, - -```typescript -v(Toolbar, { - key: 'toolbar', - buttons: [ - { - icon: 'save', - onClick: () => this._onSave() - }, - { - icon: 'cancel', - onClick: () => this._onCancel() - } - ] -}); -``` - -And you want to trigger the save toolbar button's `onClick` function. - -```typescript -h.trigger('@buttons', (renderResult: DNode) => { - return renderResult.properties.buttons[0].onClick; -}); -``` - -#### `harness.getRender` - -`harness.getRender()` returns the render with the index provided, when no index is provided it returns the last render. - -```ts -getRender(index?: number); -``` - -- `index`: The index of the render result to return - -Example Usage(s): - -```ts -// Returns the result of the last render -const render = h.getRender(); -``` - -```ts -// Returns the result of the render for the index provided -h.getRender(1); -``` - -## Assertion Templates - -Assertion Templates allow you to build expected render functions to pass to `h.expect()`. The idea behind Assertion Templates is to always assert against the entire render output, and modify portions of the assertion itslef has needed. - -To use Assertion Templates first import the module: - -```ts -import assertionTemplate from '@dojo/framework/testing/assertionTemplate'; -``` - -In your tests you can then write a base assertion which would be the default render state of your widget: - -Given the following widget: - -```ts -class NumberWidget extends WidgetBase<{ num?: number }> { - protected render() { - const { num } = this.properties; - const message = num === undefined ? 'no number passed' : `the number ${num}`; - return v('div', [v('span', [message])]); - } -} -``` - -The base assertion might look like: - -```ts -const baseAssertion = assertionTemplate(() => { - return v('div', [ - v('span', { '~key': 'message' }, [ 'no number passed' ]); - ]); -}); -``` - -and in a test would look like: - -```ts -it('should render no number passed when no number is passed as a property', () => { - const h = harness(() => w(NumberWidget, {})); - h.expect(baseAssertion); -}); -``` - -now lets see how we'd test the output when the `num` property is passed to the `NumberWidget`: - -```ts -it('should render the number when a number is passed as a property', () => { - const numberAssertion = baseAssertion.setChildren('~message', ['the number 5']); - const h = harness(() => w(NumberWidget, { num: 5 })); - h.expect(numberAssertion); -}); -``` - -Here we're using the `setChildren()` api on the baseAssertion, and we're using the special `~` selector to find a node with a key of `~message`. The `~key` property (or when using tsx in a template, `assertion-key`) is a special property on Assertion Templates that will be erased at assertion time so it doesn't show up when matching the renders. This allows you to decorate the AssertionTemplates to easily select nodes, without having to augment the actual widgets render function. Once we have the `message` node we then set the children to the expected `the number 5`, and use the resulting template in `h.expect`. It's important to note that Assertion Templates always return a new Assertion Template when setting a value, this ensures that you do not accidentally mutate an existing template (causing other tests to potentially fail), and allows you to build layered Templates that incrementally build on each other. +## Features -Assertion Template has the following api's: +- `renderer` a renderer for rendering the thing in a test enviroment +- `assertion` an assertion builder which can be expected against the renderer +- `wrap` a type safe component/element selector +- `ignore` a utility function to exclude components/elements from an assertion +- `compare` a custom comparator for component/element properties -``` -insertBefore(selector: string, children: DNode[] | (() => DNode[])): AssertionTemplateResult; -insertAfter(selector: string, children: DNode[] | (() => DNode[])): AssertionTemplateResult; -insertSiblings(selector: string, children: DNode[] | (() => DNode[]), type?: 'before' | 'after'): AssertionTemplateResult; -append(selector: string, children: DNode[] | (() => DNode[])): AssertionTemplateResult; -prepend(selector: string, children: DNode[] | (() => DNode[])): AssertionTemplateResult; -replaceChildren(selector: string, children: DNode[] | (() => DNode[])): AssertionTemplateResult; -setChildren(selector: string, children: DNode[] | (() => DNode[]), type?: 'prepend' | 'replace' | 'append'): AssertionTemplateResult; -setProperty(selector: string, property: string, value: any): AssertionTemplateResult; -setProperties(selector: string, value: any | PropertiesComparatorFunction): AssertionTemplateResult; -getChildren(selector: string): DNode[]; -getProperty(selector: string, property: string): any; -getProperties(selector: string): any; -replace(selector: string, node: DNode): AssertionTemplateResult; -remove(selector: string): AssertionTemplateResult; -``` +Please see the [reference guide](https://dojo.io/learn/testing) for more information. diff --git a/src/testing/assertionTemplate.ts b/src/testing/assertionTemplate.ts deleted file mode 100644 index 4a684134c..000000000 --- a/src/testing/assertionTemplate.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { VNode, WNode, DNode, RenderResult, WidgetBaseInterface, Constructor } from '../core/interfaces'; -import { isWNode, isVNode } from '../core/vdom'; -import { decorate } from '../core/util'; -import { Wrapped, WidgetFactory, NonComparable, CompareFunc } from './interfaces'; - -export type PropertiesComparatorFunction = (actualProperties: T) => T; - -export type TemplateChildren = () => T; - -export interface DecoratorResult { - hasDeferredProperties: boolean; - nodes: T; -} - -export function decorateNodes(dNode: DNode[]): DecoratorResult; -export function decorateNodes(dNode: DNode): DecoratorResult; -export function decorateNodes(dNode: DNode | DNode[]): DecoratorResult; -export function decorateNodes(dNode: any): DecoratorResult { - let hasDeferredProperties = false; - function addParent(parent: WNode | VNode): void { - (parent.children || []).forEach((child: any) => { - if (isVNode(child) || isWNode(child)) { - (child as any).parent = parent; - } - }); - if (isVNode(parent) && typeof parent.deferredPropertiesCallback === 'function') { - hasDeferredProperties = true; - parent.properties = { ...parent.properties, ...parent.deferredPropertiesCallback(false) }; - } - } - const nodes = decorate(dNode, addParent, (node: DNode): node is WNode | VNode => isWNode(node) || isVNode(node)); - return { hasDeferredProperties, nodes }; -} - -function isWrappedNode(value: any): value is (WNode & { id: string }) | (WNode & { id: string }) { - return Boolean(value && value.id && (isWNode(value) || isVNode(value))); -} - -function findNode>(renderResult: RenderResult, wrapped: T): VNode | WNode { - renderResult = decorateNodes(renderResult).nodes; - let nodes = Array.isArray(renderResult) ? [...renderResult] : [renderResult]; - while (nodes.length) { - let node = nodes.pop(); - if (isWrappedNode(node)) { - if (node.id === wrapped.id) { - return node; - } - } - if (isVNode(node) || isWNode(node)) { - const children = node.children || []; - nodes = [...children, ...nodes]; - } - } - throw new Error('Unable to find node'); -} - -export interface AssertionTemplateResult { - (): DNode | DNode[]; - append( - target: Wrapped>, - children: TemplateChildren - ): AssertionTemplateResult; - append( - target: Wrapped, - children: TemplateChildren - ): AssertionTemplateResult; - prepend( - target: Wrapped>, - children: TemplateChildren - ): AssertionTemplateResult; - prepend( - target: Wrapped, - children: TemplateChildren - ): AssertionTemplateResult; - replaceChildren( - target: Wrapped>, - children: TemplateChildren - ): AssertionTemplateResult; - replaceChildren( - target: Wrapped, - children: TemplateChildren - ): AssertionTemplateResult; - insertBefore( - target: Wrapped>, - children: TemplateChildren - ): AssertionTemplateResult; - insertBefore( - target: Wrapped, - children: TemplateChildren - ): AssertionTemplateResult; - insertAfter( - target: Wrapped>, - children: TemplateChildren - ): AssertionTemplateResult; - insertAfter( - target: Wrapped, - children: TemplateChildren - ): AssertionTemplateResult; - insertSiblings( - target: T, - children: TemplateChildren, - type?: 'before' | 'after' - ): AssertionTemplateResult; - setChildren( - target: Wrapped, - children: TemplateChildren, - type?: 'prepend' | 'replace' | 'append' - ): AssertionTemplateResult; - setChildren( - target: Wrapped>, - children: TemplateChildren, - type?: 'prepend' | 'replace' | 'append' - ): AssertionTemplateResult; - setProperty( - wrapped: Wrapped>, - property: K, - value: Exclude> - ): AssertionTemplateResult; - setProperty( - wrapped: Wrapped, - property: K, - value: Exclude> - ): AssertionTemplateResult; - setProperties( - wrapped: Wrapped>, - value: NonComparable | PropertiesComparatorFunction> - ): AssertionTemplateResult; - setProperties( - wrapped: Wrapped, - value: NonComparable | PropertiesComparatorFunction> - ): AssertionTemplateResult; - getChildren(target: Wrapped>): T['children']; - getChildren(target: Wrapped): T['children']; - getProperty( - target: Wrapped>, - property: K - ): Exclude>; - getProperty( - target: Wrapped, - property: K - ): Exclude>; - getProperties(target: Wrapped>): NonComparable; - getProperties(target: Wrapped): NonComparable; - replace(target: Wrapped>, node: DNode): AssertionTemplateResult; - replace(target: Wrapped, node: DNode): AssertionTemplateResult; - remove(target: Wrapped>): AssertionTemplateResult; - remove(target: Wrapped): AssertionTemplateResult; -} - -type NodeWithProperties = (VNode | WNode) & { properties: { [index: string]: any } }; - -const replaceChildren = ( - wrapped: Wrapped, - render: DNode | DNode[], - modifyChildrenFn: (index: number, children: DNode[]) => DNode[] -): DNode | DNode[] => { - const node = findNode(render, wrapped); - const parent: (VNode | WNode) & { children: DNode[] } | undefined = (node as any).parent; - const siblings = parent ? parent.children : Array.isArray(render) ? render : [render]; - const newChildren = modifyChildrenFn(siblings.indexOf(node), [...siblings]); - - if (!parent) { - return newChildren; - } - - parent.children = newChildren; - return render; -}; - -export function assertionTemplate(renderFunc: () => DNode | DNode[]) { - const assertionTemplateResult: any = () => { - const render = renderFunc(); - decorate(render, (node) => { - if (isWNode(node) || isVNode(node)) { - delete (node as NodeWithProperties).properties['~key']; - delete (node as NodeWithProperties).properties['assertion-key']; - } - }); - return render; - }; - assertionTemplateResult.setProperty = (wrapped: Wrapped, property: string, value: any) => { - return assertionTemplate(() => { - const render = renderFunc(); - const node = findNode(render, wrapped); - node.properties[property] = value; - return render; - }); - }; - assertionTemplateResult.setProperties = (wrapped: Wrapped, value: any | PropertiesComparatorFunction) => { - return assertionTemplate(() => { - const render = renderFunc(); - const node = findNode(render, wrapped); - node.properties = value; - return render; - }); - }; - assertionTemplateResult.append = (wrapped: Wrapped, children: TemplateChildren) => { - return assertionTemplateResult.setChildren(wrapped, children, 'append'); - }; - assertionTemplateResult.prepend = (wrapped: Wrapped, children: TemplateChildren) => { - return assertionTemplateResult.setChildren(wrapped, children, 'prepend'); - }; - assertionTemplateResult.replaceChildren = (wrapped: Wrapped, children: TemplateChildren) => { - return assertionTemplateResult.setChildren(wrapped, children, 'replace'); - }; - assertionTemplateResult.setChildren = ( - wrapped: Wrapped, - children: TemplateChildren, - type: 'prepend' | 'replace' | 'append' = 'replace' - ) => { - return assertionTemplate(() => { - const render = renderFunc(); - const node = findNode(render, wrapped); - node.children = node.children || []; - let childrenResult = children(); - switch (type) { - case 'prepend': - node.children = [...childrenResult, ...node.children]; - break; - case 'append': - node.children = [...node.children, ...childrenResult]; - break; - case 'replace': - node.children = [...childrenResult]; - break; - } - return render; - }); - }; - assertionTemplateResult.insertBefore = (wrapped: Wrapped, children: TemplateChildren) => { - return assertionTemplateResult.insertSiblings(wrapped, children, 'before'); - }; - assertionTemplateResult.insertAfter = (wrapped: Wrapped, children: TemplateChildren) => { - return assertionTemplateResult.insertSiblings(wrapped, children, 'after'); - }; - assertionTemplateResult.insertSiblings = ( - wrapped: Wrapped, - children: TemplateChildren, - type: 'before' | 'after' = 'after' - ) => { - return assertionTemplate(() => { - const render = renderFunc(); - const insertedChildren = typeof children === 'function' ? children() : children; - return replaceChildren(wrapped, render, (index, children) => { - if (type === 'after') { - children.splice(index + 1, 0, ...insertedChildren); - } else { - children.splice(index, 0, ...insertedChildren); - } - return children; - }); - }); - }; - assertionTemplateResult.getProperty = (wrapped: Wrapped, property: string) => { - const render = renderFunc(); - const node = findNode(render, wrapped); - return node.properties[property]; - }; - assertionTemplateResult.getProperties = (wrapped: Wrapped) => { - const render = renderFunc(); - const node = findNode(render, wrapped); - return node.properties; - }; - assertionTemplateResult.getChildren = (wrapped: Wrapped) => { - const render = renderFunc(); - const node = findNode(render, wrapped); - return node.children || []; - }; - assertionTemplateResult.replace = (wrapped: Wrapped, newNode: DNode) => { - return assertionTemplate(() => { - const render = renderFunc(); - return replaceChildren(wrapped, render, (index, children) => { - children.splice(index, 1, newNode); - return children; - }); - }); - }; - assertionTemplateResult.remove = (wrapped: Wrapped) => { - return assertionTemplate(() => { - const render = renderFunc(); - return replaceChildren(wrapped, render, (index, children) => { - children.splice(index, 1); - return children; - }); - }); - }; - return assertionTemplateResult as AssertionTemplateResult; -} - -export default assertionTemplate; diff --git a/src/testing/renderer.ts b/src/testing/renderer.ts index b71c8d4b9..27d19e16c 100644 --- a/src/testing/renderer.ts +++ b/src/testing/renderer.ts @@ -10,19 +10,16 @@ import { VNodeProperties, OptionalWNodeFactory, WidgetBaseInterface, - DefaultChildrenWNodeFactory + DefaultChildrenWNodeFactory, + VNode } from '../core/interfaces'; import { WidgetBase } from '../core/WidgetBase'; import { isWidgetFunction } from '../core/Registry'; -import { invalidator, diffProperty, destroy, create, propertiesDiff, w, v } from '../core/vdom'; +import { invalidator, diffProperty, destroy, create, propertiesDiff, w, v, isVNode, isWNode } from '../core/vdom'; import { uuid } from '../core/util'; import decorate, { decorateNodes } from './decorate'; -import { AssertionTemplateResult } from './assertionTemplate'; -import { Wrapped, WidgetFactory, CompareFunc, Comparable } from './interfaces'; - -export interface Expect { - (expectedRenderFunc: AssertionTemplateResult): void; -} +import { decorate as coreDecorate } from '../core/util'; +import { Wrapped, WidgetFactory, CompareFunc, Comparable, NonComparable } from './interfaces'; export interface ChildInstruction { type: 'child'; @@ -84,18 +81,270 @@ export interface Property { ): void; } -export interface RendererAPI { - expect: Expect; - child: Child; - property: Property; -} - let middlewareId = 0; interface RendererOptions { middleware?: [MiddlewareResultFactory, MiddlewareResultFactory][]; } +export type PropertiesComparatorFunction = (actualProperties: T) => T; + +export type TemplateChildren = () => T; + +export interface DecoratorResult { + hasDeferredProperties: boolean; + nodes: T; +} + +function isWrappedNode(value: any): value is (WNode & { id: string }) | (WNode & { id: string }) { + return Boolean(value && value.id && (isWNode(value) || isVNode(value))); +} + +function findNode>(renderResult: RenderResult, wrapped: T): VNode | WNode { + renderResult = decorateNodes(renderResult).nodes; + let nodes = Array.isArray(renderResult) ? [...renderResult] : [renderResult]; + while (nodes.length) { + let node = nodes.pop(); + if (isWrappedNode(node)) { + if (node.id === wrapped.id) { + return node; + } + } + if (isVNode(node) || isWNode(node)) { + const children = node.children || []; + nodes = [...children, ...nodes]; + } + } + throw new Error('Unable to find node'); +} + +export interface AssertionResult { + (): DNode | DNode[]; + append( + target: Wrapped>, + children: TemplateChildren + ): AssertionResult; + append(target: Wrapped, children: TemplateChildren): AssertionResult; + prepend( + target: Wrapped>, + children: TemplateChildren + ): AssertionResult; + prepend(target: Wrapped, children: TemplateChildren): AssertionResult; + replaceChildren( + target: Wrapped>, + children: TemplateChildren + ): AssertionResult; + replaceChildren( + target: Wrapped, + children: TemplateChildren + ): AssertionResult; + insertBefore( + target: Wrapped>, + children: TemplateChildren + ): AssertionResult; + insertBefore( + target: Wrapped, + children: TemplateChildren + ): AssertionResult; + insertAfter( + target: Wrapped>, + children: TemplateChildren + ): AssertionResult; + insertAfter( + target: Wrapped, + children: TemplateChildren + ): AssertionResult; + insertSiblings( + target: T, + children: TemplateChildren, + type?: 'before' | 'after' + ): AssertionResult; + setChildren( + target: Wrapped, + children: TemplateChildren, + type?: 'prepend' | 'replace' | 'append' + ): AssertionResult; + setChildren( + target: Wrapped>, + children: TemplateChildren, + type?: 'prepend' | 'replace' | 'append' + ): AssertionResult; + setProperty( + wrapped: Wrapped>, + property: K, + value: Exclude> + ): AssertionResult; + setProperty( + wrapped: Wrapped, + property: K, + value: Exclude> + ): AssertionResult; + setProperties( + wrapped: Wrapped>, + value: NonComparable | PropertiesComparatorFunction> + ): AssertionResult; + setProperties( + wrapped: Wrapped, + value: NonComparable | PropertiesComparatorFunction> + ): AssertionResult; + getChildren(target: Wrapped>): T['children']; + getChildren(target: Wrapped): T['children']; + getProperty( + target: Wrapped>, + property: K + ): Exclude>; + getProperty( + target: Wrapped, + property: K + ): Exclude>; + getProperties(target: Wrapped>): NonComparable; + getProperties(target: Wrapped): NonComparable; + replace(target: Wrapped>, node: DNode): AssertionResult; + replace(target: Wrapped, node: DNode): AssertionResult; + remove(target: Wrapped>): AssertionResult; + remove(target: Wrapped): AssertionResult; +} + +type NodeWithProperties = (VNode | WNode) & { properties: { [index: string]: any } }; + +const replaceChildren = ( + wrapped: Wrapped, + render: DNode | DNode[], + modifyChildrenFn: (index: number, children: DNode[]) => DNode[] +): DNode | DNode[] => { + const node = findNode(render, wrapped); + const parent: (VNode | WNode) & { children: DNode[] } | undefined = (node as any).parent; + const siblings = parent ? parent.children : Array.isArray(render) ? render : [render]; + const newChildren = modifyChildrenFn(siblings.indexOf(node), [...siblings]); + + if (!parent) { + return newChildren; + } + + parent.children = newChildren; + return render; +}; + +export function assertion(renderFunc: () => DNode | DNode[]) { + const assertionResult: any = () => { + const render = renderFunc(); + coreDecorate(render, (node) => { + if (isWNode(node) || isVNode(node)) { + delete (node as NodeWithProperties).properties['~key']; + delete (node as NodeWithProperties).properties['assertion-key']; + } + }); + return render; + }; + assertionResult.setProperty = (wrapped: Wrapped, property: string, value: any) => { + return assertion(() => { + const render = renderFunc(); + const node = findNode(render, wrapped); + node.properties[property] = value; + return render; + }); + }; + assertionResult.setProperties = (wrapped: Wrapped, value: any | PropertiesComparatorFunction) => { + return assertion(() => { + const render = renderFunc(); + const node = findNode(render, wrapped); + node.properties = value; + return render; + }); + }; + assertionResult.append = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionResult.setChildren(wrapped, children, 'append'); + }; + assertionResult.prepend = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionResult.setChildren(wrapped, children, 'prepend'); + }; + assertionResult.replaceChildren = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionResult.setChildren(wrapped, children, 'replace'); + }; + assertionResult.setChildren = ( + wrapped: Wrapped, + children: TemplateChildren, + type: 'prepend' | 'replace' | 'append' = 'replace' + ) => { + return assertion(() => { + const render = renderFunc(); + const node = findNode(render, wrapped); + node.children = node.children || []; + let childrenResult = children(); + switch (type) { + case 'prepend': + node.children = [...childrenResult, ...node.children]; + break; + case 'append': + node.children = [...node.children, ...childrenResult]; + break; + case 'replace': + node.children = [...childrenResult]; + break; + } + return render; + }); + }; + assertionResult.insertBefore = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionResult.insertSiblings(wrapped, children, 'before'); + }; + assertionResult.insertAfter = (wrapped: Wrapped, children: TemplateChildren) => { + return assertionResult.insertSiblings(wrapped, children, 'after'); + }; + assertionResult.insertSiblings = ( + wrapped: Wrapped, + children: TemplateChildren, + type: 'before' | 'after' = 'after' + ) => { + return assertion(() => { + const render = renderFunc(); + const insertedChildren = typeof children === 'function' ? children() : children; + return replaceChildren(wrapped, render, (index, children) => { + if (type === 'after') { + children.splice(index + 1, 0, ...insertedChildren); + } else { + children.splice(index, 0, ...insertedChildren); + } + return children; + }); + }); + }; + assertionResult.getProperty = (wrapped: Wrapped, property: string) => { + const render = renderFunc(); + const node = findNode(render, wrapped); + return node.properties[property]; + }; + assertionResult.getProperties = (wrapped: Wrapped) => { + const render = renderFunc(); + const node = findNode(render, wrapped); + return node.properties; + }; + assertionResult.getChildren = (wrapped: Wrapped) => { + const render = renderFunc(); + const node = findNode(render, wrapped); + return node.children || []; + }; + assertionResult.replace = (wrapped: Wrapped, newNode: DNode) => { + return assertion(() => { + const render = renderFunc(); + return replaceChildren(wrapped, render, (index, children) => { + children.splice(index, 1, newNode); + return children; + }); + }); + }; + assertionResult.remove = (wrapped: Wrapped) => { + return assertion(() => { + const render = renderFunc(); + return replaceChildren(wrapped, render, (index, children) => { + children.splice(index, 1); + return children; + }); + }); + }; + return assertionResult as AssertionResult; +} + export function wrap( node: string ): Wrapped; children: DNode | (DNode | DNode[])[] }>> & { @@ -166,6 +415,16 @@ export function compare(compareFunc: (actual: unknown, expected: unknown) => boo return compareFunc as any; } +export interface Expect { + (expectedRenderFunc: AssertionResult): void; +} + +export interface RendererAPI { + expect: Expect; + child: Child; + property: Property; +} + const factory = create(); export function renderer(renderFunc: () => WNode, options: RendererOptions = {}): RendererAPI { @@ -295,7 +554,7 @@ export function renderer(renderFunc: () => WNode, options: RendererOptions = {}) } } - function _expect(expectedRenderFunc: AssertionTemplateResult) { + function _expect(expectedRenderFunc: AssertionResult) { if (expectedRenderResult && propertyInstructions.length > 0) { propertyInstructions.forEach((instruction) => { decorate(renderResult, expectedRenderResult, new Map([[instruction.id, instruction]])); @@ -322,7 +581,7 @@ export function renderer(renderFunc: () => WNode, options: RendererOptions = {}) } propertyInstructions.push({ id: wrapped.id, wrapped, params, type: 'property', key }); }, - expect(expectedRenderFunc: AssertionTemplateResult) { + expect(expectedRenderFunc: AssertionResult) { return _expect(expectedRenderFunc); } }; diff --git a/tests/routing/unit/ActiveLink.ts b/tests/routing/unit/ActiveLink.ts index 11b7f5272..74e1289da 100644 --- a/tests/routing/unit/ActiveLink.ts +++ b/tests/routing/unit/ActiveLink.ts @@ -7,8 +7,7 @@ import Link from '../../../src/routing/Link'; import ActiveLink from '../../../src/routing/ActiveLink'; import { w, create, getRegistry } from '../../../src/core/vdom'; -import renderer, { wrap } from '../../../src/testing/renderer'; -import assertionTemplate from '../../../src/testing/assertionTemplate'; +import renderer, { assertion, wrap } from '../../../src/testing/renderer'; const registry = new Registry(); @@ -69,7 +68,7 @@ describe('ActiveLink', () => { middleware: [[getRegistry, mockGetRegistry]] }); const WrappedLink = wrap(Link); - const template = assertionTemplate(() => w(WrappedLink, { classes: [], to: 'foo' })); + const template = assertion(() => w(WrappedLink, { classes: [], to: 'foo' })); r.expect(template); router.setPath('/foo'); r.expect(template.setProperty(WrappedLink, 'classes', ['foo', undefined, null])); @@ -80,7 +79,7 @@ describe('ActiveLink', () => { const r = renderer(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo'] }, ['hello']), { middleware: [[getRegistry, mockGetRegistry]] }); - const template = assertionTemplate(() => w(Link, { classes: ['foo'], to: 'foo' }, ['hello'])); + const template = assertion(() => w(Link, { classes: ['foo'], to: 'foo' }, ['hello'])); r.expect(template); }); @@ -95,7 +94,7 @@ describe('ActiveLink', () => { middleware: [[getRegistry, mockGetRegistry]] } ); - const template = assertionTemplate(() => + const template = assertion(() => w(Link, { classes: ['foo'], to: 'query', params: { path: 'path', query: 'query' } }, ['hello']) ); r.expect(template); @@ -106,7 +105,7 @@ describe('ActiveLink', () => { const r = renderer(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo'], classes: 'bar' }), { middleware: [[getRegistry, mockGetRegistry]] }); - const template = assertionTemplate(() => w(Link, { classes: ['bar', 'foo'], to: 'foo' })); + const template = assertion(() => w(Link, { classes: ['bar', 'foo'], to: 'foo' })); r.expect(template); }); @@ -115,7 +114,7 @@ describe('ActiveLink', () => { const r = renderer(() => w(ActiveLink, { to: 'foo', activeClasses: ['foo', 'qux'], classes: ['bar', 'baz'] }), { middleware: [[getRegistry, mockGetRegistry]] }); - const template = assertionTemplate(() => w(Link, { classes: ['bar', 'baz', 'foo', 'qux'], to: 'foo' })); + const template = assertion(() => w(Link, { classes: ['bar', 'baz', 'foo', 'qux'], to: 'foo' })); r.expect(template); }); @@ -127,7 +126,7 @@ describe('ActiveLink', () => { const r = renderer(() => w(ActiveLink, properties), { middleware: [[getRegistry, mockGetRegistry]] }); - const template = assertionTemplate(() => w(WrappedLink, { to: 'foo', classes: ['foo'] })); + const template = assertion(() => w(WrappedLink, { to: 'foo', classes: ['foo'] })); r.expect(template); properties = { to: 'other', activeClasses: ['foo'] }; @@ -155,9 +154,7 @@ describe('ActiveLink', () => { middleware: [[getRegistry, mockGetRegistry]] } ); - r1.expect( - assertionTemplate(() => w(Link, { to: 'suffixed-param', classes: ['foo'], params: { suffix: 'one' } })) - ); + r1.expect(assertion(() => w(Link, { to: 'suffixed-param', classes: ['foo'], params: { suffix: 'one' } }))); const r2 = renderer( () => @@ -172,7 +169,7 @@ describe('ActiveLink', () => { middleware: [[getRegistry, mockGetRegistry]] } ); - r2.expect(assertionTemplate(() => w(Link, { to: 'suffixed-param', classes: [], params: { suffix: 'two' } }))); + r2.expect(assertion(() => w(Link, { to: 'suffixed-param', classes: [], params: { suffix: 'two' } }))); }); it('Should be able to check for an exact match', () => { @@ -180,11 +177,11 @@ describe('ActiveLink', () => { let r = renderer(() => w(ActiveLink, { to: 'param', activeClasses: ['foo'] }), { middleware: [[getRegistry, mockGetRegistry]] }); - r.expect(assertionTemplate(() => w(Link, { classes: ['foo'], to: 'param' }))); + r.expect(assertion(() => w(Link, { classes: ['foo'], to: 'param' }))); r = renderer(() => w(ActiveLink, { to: 'param', activeClasses: ['foo'], isExact: true }), { middleware: [[getRegistry, mockGetRegistry]] }); - r.expect(assertionTemplate(() => w(Link, { classes: [], to: 'param' }))); + r.expect(assertion(() => w(Link, { classes: [], to: 'param' }))); }); }); diff --git a/tests/routing/unit/Link.ts b/tests/routing/unit/Link.ts index 6782987d2..a7bab6e30 100644 --- a/tests/routing/unit/Link.ts +++ b/tests/routing/unit/Link.ts @@ -7,8 +7,7 @@ import { Registry } from '../../../src/core/Registry'; import { Link } from '../../../src/routing/Link'; import { Router } from '../../../src/routing/Router'; import { MemoryHistory } from '../../../src/routing/history/MemoryHistory'; -import renderer, { wrap } from '../../../src/testing/renderer'; -import assertionTemplate from '../../../src/testing/assertionTemplate'; +import renderer, { assertion, wrap } from '../../../src/testing/renderer'; const registry = new Registry(); @@ -73,21 +72,21 @@ describe('Link', () => { it('Generate link component for basic outlet', () => { const r = renderer(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); - r.expect(assertionTemplate(() => v('a', { href: 'foo', onclick: noop }))); + r.expect(assertion(() => v('a', { href: 'foo', onclick: noop }))); }); it('Generate link component for outlet with specified params', () => { const r = renderer(() => w(Link, { to: 'foo2', params: { foo: 'foo' } }), { middleware: [[getRegistry, mockGetRegistry]] }); - r.expect(assertionTemplate(() => v('a', { href: 'foo/foo', onclick: noop }))); + r.expect(assertion(() => v('a', { href: 'foo/foo', onclick: noop }))); }); it('Generate link component for fixed href', () => { const r = renderer(() => w(Link, { to: '#foo/static', isOutlet: false }), { middleware: [[getRegistry, mockGetRegistry]] }); - r.expect(assertionTemplate(() => v('a', { href: '#foo/static', onclick: noop }))); + r.expect(assertion(() => v('a', { href: '#foo/static', onclick: noop }))); }); it('Set router path on click', () => { @@ -95,7 +94,7 @@ describe('Link', () => { const r = renderer(() => w(Link, { to: '#foo/static', isOutlet: false }), { middleware: [[getRegistry, mockGetRegistry]] }); - const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: '#foo/static', onclick: noop })); + const template = assertion(() => v(WrappedAnchor.tag, { href: '#foo/static', onclick: noop })); r.expect(template); r.property(WrappedAnchor, 'onclick', createMockEvent()); r.expect(template); @@ -115,7 +114,7 @@ describe('Link', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', registry, onclick: noop })); + const template = assertion(() => v(WrappedAnchor.tag, { href: 'foo', registry, onclick: noop })); r.expect(template); r.property(WrappedAnchor, 'onclick', createMockEvent()); r.expect(template); @@ -127,7 +126,7 @@ describe('Link', () => { const r = renderer(() => w(Link, { to: 'foo', target: '_blank' }), { middleware: [[getRegistry, mockGetRegistry]] }); - const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); + const template = assertion(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); r.expect(template); r.property(WrappedAnchor, 'onclick', createMockEvent()); r.expect(template); @@ -137,7 +136,7 @@ describe('Link', () => { it('Does not set router path on right click', () => { const WrappedAnchor = wrap('a'); const r = renderer(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); - const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); + const template = assertion(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); r.expect(template); r.property(WrappedAnchor, 'onclick', createMockEvent({ isRightClick: true })); r.expect(template); @@ -147,7 +146,7 @@ describe('Link', () => { it('Does not set router path on ctrl click', () => { const WrappedAnchor = wrap('a'); const r = renderer(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); - const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); + const template = assertion(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); r.expect(template); r.property(WrappedAnchor, 'onclick', createMockEvent({ isRightClick: true })); r.expect(template); @@ -157,7 +156,7 @@ describe('Link', () => { it('Does not set router path on meta click', () => { const WrappedAnchor = wrap('a'); const r = renderer(() => w(Link, { to: 'foo' }), { middleware: [[getRegistry, mockGetRegistry]] }); - const template = assertionTemplate(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); + const template = assertion(() => v(WrappedAnchor.tag, { href: 'foo', onclick: noop })); r.expect(template); r.property(WrappedAnchor, 'onclick', createMockEvent({ isRightClick: true })); r.expect(template); diff --git a/tests/routing/unit/Outlet.tsx b/tests/routing/unit/Outlet.tsx index 0b3267dbe..c7e702984 100644 --- a/tests/routing/unit/Outlet.tsx +++ b/tests/routing/unit/Outlet.tsx @@ -4,8 +4,7 @@ import { MemoryHistory as HistoryManager } from '../../../src/routing/history/Me import { Registry } from '../../../src/core/Registry'; import { registerRouterInjector } from '../../../src/routing/RouterInjector'; import { create, getRegistry, tsx } from '../../../src/core/vdom'; -import renderer, { wrap } from '../../../src/testing/renderer'; -import assertionTemplate from '../../../src/testing/assertionTemplate'; +import renderer, { assertion, wrap } from '../../../src/testing/renderer'; import Outlet from '../../../src/routing/Outlet'; let registry: Registry; @@ -63,7 +62,7 @@ describe('Outlet', () => { it('should match all routes for an outlet by default', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); - const template = assertionTemplate(() => ( + const template = assertion(() => (
overview
type
@@ -86,7 +85,7 @@ describe('Outlet', () => { it('should restrict matches using matcher property based on match details', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); - const template = assertionTemplate(() => ( + const template = assertion(() => (
type
@@ -115,7 +114,7 @@ describe('Outlet', () => { r.expect(template); router.setPath('/widget/widget/overview'); r.expect( - assertionTemplate(() => ( + assertion(() => (
overview
@@ -125,7 +124,7 @@ describe('Outlet', () => { it('should be able to use custom keys with a matcher property', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); - const template = assertionTemplate(() => ( + const template = assertion(() => (
custom
@@ -152,7 +151,7 @@ describe('Outlet', () => { it('should render function child if there is any route matches for the outlet', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); - const template = assertionTemplate(() =>
function
); + const template = assertion(() =>
function
); router.setPath('/widget/widget/overview/type'); const r = renderer(() => {() =>
function
}
, { middleware: [[getRegistry, mockGetRegistry]] @@ -162,7 +161,7 @@ describe('Outlet', () => { it('should be able to access match details in children functions', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); - const template = assertionTemplate(() => ( + const template = assertion(() => (
widget
widget
@@ -185,7 +184,7 @@ describe('Outlet', () => { it('should return null if no router has been registered', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); - const template = assertionTemplate(() => null); + const template = assertion(() => null); router.setPath('/widget/widget/overview/type'); const r = renderer(() => ( @@ -200,7 +199,7 @@ describe('Outlet', () => { it('should return null if no routes match for the outlet', () => { const router = registerRouterInjector(routeConfig, registry, { HistoryManager }); - const template = assertionTemplate(() => null); + const template = assertion(() => null); router.setPath('/other/widget/overview/type'); const r = renderer(() => ( @@ -220,7 +219,7 @@ describe('Outlet', () => { id: 'main' }; const WrappedType = wrap('div'); - const template = assertionTemplate(() => ( + const template = assertion(() => (
overview
type diff --git a/tests/routing/unit/Route.ts b/tests/routing/unit/Route.ts index f9eda0863..931f5ec87 100644 --- a/tests/routing/unit/Route.ts +++ b/tests/routing/unit/Route.ts @@ -7,8 +7,7 @@ import { Route } from '../../../src/routing/Route'; import { Registry } from '../../../src/core/Registry'; import { registerRouterInjector } from '../../../src/routing/RouterInjector'; import { w, create, getRegistry } from '../../../src/core/vdom'; -import renderer from '../../../src/testing/renderer'; -import assertionTemplate from '../../../src/testing/assertionTemplate'; +import renderer, { assertion } from '../../../src/testing/renderer'; class Widget extends WidgetBase { render() { @@ -65,7 +64,7 @@ describe('Route', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - r.expect(assertionTemplate(() => w(Widget, {}, []))); + r.expect(assertion(() => w(Widget, {}, []))); }); it('Should set the type as index for exact matches', () => { @@ -83,7 +82,7 @@ describe('Route', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - r.expect(assertionTemplate(() => null)); + r.expect(assertion(() => null)); assert.strictEqual(matchType, 'index'); }); @@ -102,7 +101,7 @@ describe('Route', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - r.expect(assertionTemplate(() => null)); + r.expect(assertion(() => null)); assert.strictEqual(matchType, 'error'); }); @@ -129,6 +128,6 @@ describe('Route', () => { }), { middleware: [[getRegistry, mockGetRegistry]] } ); - r.expect(assertionTemplate(() => null)); + r.expect(assertion(() => null)); }); }); diff --git a/tests/testing/unit/all.ts b/tests/testing/unit/all.ts index 9214a4e7a..c7829ed23 100644 --- a/tests/testing/unit/all.ts +++ b/tests/testing/unit/all.ts @@ -1,5 +1,5 @@ import './harness/all'; -import './assertionTemplate'; +import './assertion'; import './assertRender'; import './renderer'; import './mocks/middleware/all'; diff --git a/tests/testing/unit/assertionTemplate.tsx b/tests/testing/unit/assertion.tsx similarity index 93% rename from tests/testing/unit/assertionTemplate.tsx rename to tests/testing/unit/assertion.tsx index 5e4444037..88f160cb2 100644 --- a/tests/testing/unit/assertionTemplate.tsx +++ b/tests/testing/unit/assertion.tsx @@ -1,10 +1,9 @@ const { describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -import { renderer, wrap, ignore } from '../../../src/testing/renderer'; +import { renderer, wrap, ignore, assertion } from '../../../src/testing/renderer'; import { WidgetBase } from '../../../src/core/WidgetBase'; import { v, w, tsx, create } from '../../../src/core/vdom'; -import assertionTemplate from '../../../src/testing/assertionTemplate'; import { DNode } from '../../../src/core/interfaces'; class MyWidget extends WidgetBase<{ @@ -50,7 +49,7 @@ const WrappedHeader = wrap('h2'); const WrappedList = wrap('ul'); const WrappedListItem = wrap('li'); -const baseAssertion = assertionTemplate(() => +const baseAssertion = assertion(() => v(WrappedRoot.tag, { classes: ['root'] }, [ v(WrappedHeader.tag, {}, ['hello']), undefined, @@ -69,7 +68,7 @@ class ListWidget extends WidgetBase { } } -const baseListAssertion = assertionTemplate(() => v('div', { classes: ['root'] }, [v(WrappedList.tag, [])])); +const baseListAssertion = assertion(() => v('div', { classes: ['root'] }, [v(WrappedList.tag, [])])); class MultiRootWidget extends WidgetBase<{ after?: boolean; last?: boolean }> { render() { @@ -88,12 +87,9 @@ class MultiRootWidget extends WidgetBase<{ after?: boolean; last?: boolean }> { const WrappedFirst = wrap('div'); const WrappedSecond = wrap('div'); -const baseMultiRootAssertion = assertionTemplate(() => [ - v(WrappedFirst.tag, ['first']), - v(WrappedSecond.tag, ['last']) -]); +const baseMultiRootAssertion = assertion(() => [v(WrappedFirst.tag, ['first']), v(WrappedSecond.tag, ['last'])]); -const tsxAssertion = assertionTemplate(() => ( +const tsxAssertion = assertion(() => (

hello

    @@ -104,7 +100,7 @@ const tsxAssertion = assertionTemplate(() => (
)); -describe('new/assertionTemplate', () => { +describe('new/assertion', () => { it('can get a property', () => { const classes = baseAssertion.getProperty(WrappedRoot, 'classes'); assert.deepEqual(classes, ['root']); @@ -151,7 +147,7 @@ describe('new/assertionTemplate', () => { }); const WrappedClassWidget = wrap(MyClassWidget); const WrappedFunctionWidget = wrap(MyFunctionWidget); - const template = assertionTemplate(() => ( + const template = assertion(() => (
@@ -226,7 +222,7 @@ describe('new/assertionTemplate', () => { const WrappedParent = wrap('div'); const WrappedChild = wrap('div'); - const baseAssertion = assertionTemplate(() => v(WrappedParent.tag, { key: 'parent', classes: ['root'] }, [])); + const baseAssertion = assertion(() => v(WrappedParent.tag, { key: 'parent', classes: ['root'] }, [])); const childAssertion = baseAssertion.setChildren(WrappedParent, () => [ hello diff --git a/tests/testing/unit/mocks/middleware/breakpoint.tsx b/tests/testing/unit/mocks/middleware/breakpoint.tsx index f83a645e7..99a5ce075 100644 --- a/tests/testing/unit/mocks/middleware/breakpoint.tsx +++ b/tests/testing/unit/mocks/middleware/breakpoint.tsx @@ -3,8 +3,7 @@ const { describe } = intern.getPlugin('jsdom'); import createBreakpointMock from '../../../../../src/testing/mocks/middleware/breakpoint'; import breakpoint from '../../../../../src/core/middleware/breakpoint'; import { tsx, create } from '../../../../../src/core/vdom'; -import renderer from '../../../../../src/testing/renderer'; -import assertionTemplate from '../../../../../src/testing/assertionTemplate'; +import renderer, { assertion } from '../../../../../src/testing/renderer'; describe('breakpoint mock', () => { it('should mock breakpoint middleware calls', () => { @@ -15,11 +14,11 @@ describe('breakpoint mock', () => { return
{JSON.stringify(breakpointResult)}
; }); const r = renderer(() => , { middleware: [[breakpoint, breakpointMock]] }); - r.expect(assertionTemplate(() =>
null
)); + r.expect(assertion(() =>
null
)); breakpointMock('root', { breakpoint: 'SM', contentRect: { width: 20 } }); - r.expect(assertionTemplate(() =>
{'{"breakpoint":"SM","contentRect":{"width":20}}'}
)); + r.expect(assertion(() =>
{'{"breakpoint":"SM","contentRect":{"width":20}}'}
)); breakpointMock('root', { breakpoint: 'XL', contentRect: { width: 1020 } }); - r.expect(assertionTemplate(() =>
{'{"breakpoint":"XL","contentRect":{"width":1020}}'}
)); + r.expect(assertion(() =>
{'{"breakpoint":"XL","contentRect":{"width":1020}}'}
)); }); it('should deal with multiple mocked keys', () => { @@ -37,7 +36,7 @@ describe('breakpoint mock', () => { }); const r = renderer(() => , { middleware: [[breakpoint, breakpointMock]] }); r.expect( - assertionTemplate(() => ( + assertion(() => (
null
null
@@ -46,7 +45,7 @@ describe('breakpoint mock', () => { ); breakpointMock('root', { breakpoint: 'SM', contentRect: { width: 50 } }); r.expect( - assertionTemplate(() => ( + assertion(() => (
{'{"breakpoint":"SM","contentRect":{"width":50}}'}
null
@@ -56,7 +55,7 @@ describe('breakpoint mock', () => { breakpointMock('root', { breakpoint: 'XL', contentRect: { width: 1020 } }); breakpointMock('other', { breakpoint: 'MD', contentRect: { width: 620 } }); r.expect( - assertionTemplate(() => ( + assertion(() => (
{'{"breakpoint":"XL","contentRect":{"width":1020}}'}
{'{"breakpoint":"MD","contentRect":{"width":620}}'}
diff --git a/tests/testing/unit/mocks/middleware/focus.tsx b/tests/testing/unit/mocks/middleware/focus.tsx index 22029b2df..aad77458c 100644 --- a/tests/testing/unit/mocks/middleware/focus.tsx +++ b/tests/testing/unit/mocks/middleware/focus.tsx @@ -3,8 +3,7 @@ const { describe } = intern.getPlugin('jsdom'); import createFocusMock from '../../../../../src/testing/mocks/middleware/focus'; import focus from '../../../../../src/core/middleware/focus'; import { tsx, create } from '../../../../../src/core/vdom'; -import renderer from '../../../../../src/testing/renderer'; -import assertionTemplate from '../../../../../src/testing/assertionTemplate'; +import renderer, { assertion } from '../../../../../src/testing/renderer'; describe('focus mock', () => { it('should mock focus of a node', () => { @@ -16,9 +15,9 @@ describe('focus mock', () => { const r = renderer(() => , { middleware: [[focus, focusMock]] }); focusMock('root', false); - r.expect(assertionTemplate(() =>
no focus
)); + r.expect(assertion(() =>
no focus
)); focusMock('root', true); - r.expect(assertionTemplate(() =>
focus
)); + r.expect(assertion(() =>
focus
)); }); }); diff --git a/tests/testing/unit/mocks/middleware/icache.tsx b/tests/testing/unit/mocks/middleware/icache.tsx index 587c501f1..316b3cf25 100644 --- a/tests/testing/unit/mocks/middleware/icache.tsx +++ b/tests/testing/unit/mocks/middleware/icache.tsx @@ -2,8 +2,7 @@ const { it } = intern.getInterface('bdd'); const { describe } = intern.getPlugin('jsdom'); import * as sinon from 'sinon'; import { tsx, create } from '../../../../../src/core/vdom'; -import renderer from '../../../../../src/testing/renderer'; -import assertionTemplate from '../../../../../src/testing/assertionTemplate'; +import renderer, { assertion } from '../../../../../src/testing/renderer'; import createICacheMock from '../../../../../src/testing/mocks/middleware/icache'; import icache from '../../../../../src/core/middleware/icache'; import global from '../../../../../src/shim/global'; @@ -24,8 +23,8 @@ describe('icache mock', () => { global.fetch = sinon.stub().returns(Promise.resolve({ json: () => Promise.resolve('api data') })); const r = renderer(() => , { middleware: [[icache, iCacheMock]] }); - r.expect(assertionTemplate(() =>
Loading
)); + r.expect(assertion(() =>
Loading
)); await iCacheMock('users'); - r.expect(assertionTemplate(() =>
api data
)); + r.expect(assertion(() =>
api data
)); }); }); diff --git a/tests/testing/unit/mocks/middleware/intersection.tsx b/tests/testing/unit/mocks/middleware/intersection.tsx index 3aac4d612..47b224785 100644 --- a/tests/testing/unit/mocks/middleware/intersection.tsx +++ b/tests/testing/unit/mocks/middleware/intersection.tsx @@ -1,8 +1,7 @@ const { it } = intern.getInterface('bdd'); const { describe } = intern.getPlugin('jsdom'); import { tsx, create } from '../../../../../src/core/vdom'; -import renderer from '../../../../../src/testing/renderer'; -import assertionTemplate from '../../../../../src/testing/assertionTemplate'; +import renderer, { assertion } from '../../../../../src/testing/renderer'; import createIntersectionMock from '../../../../../src/testing/mocks/middleware/intersection'; import intersection from '../../../../../src/core/middleware/intersection'; @@ -15,10 +14,10 @@ describe('intersection mock', () => { return
{JSON.stringify(details)}
; }); const r = renderer(() => , { middleware: [[intersection, intersectionMock]] }); - r.expect(assertionTemplate(() =>
{`{"intersectionRatio":0,"isIntersecting":false}`}
)); + r.expect(assertion(() =>
{`{"intersectionRatio":0,"isIntersecting":false}`}
)); intersectionMock('root', { isIntersecting: true }); - r.expect(assertionTemplate(() =>
{`{"isIntersecting":true}`}
)); + r.expect(assertion(() =>
{`{"isIntersecting":true}`}
)); intersectionMock('root', { isIntersecting: false }); - r.expect(assertionTemplate(() =>
{`{"isIntersecting":false}`}
)); + r.expect(assertion(() =>
{`{"isIntersecting":false}`}
)); }); }); diff --git a/tests/testing/unit/mocks/middleware/node.tsx b/tests/testing/unit/mocks/middleware/node.tsx index f4a996ed4..4561a99ff 100644 --- a/tests/testing/unit/mocks/middleware/node.tsx +++ b/tests/testing/unit/mocks/middleware/node.tsx @@ -3,8 +3,7 @@ const { describe } = intern.getPlugin('jsdom'); import createNodeMock from '../../../../../src/testing/mocks/middleware/node'; import dimensions from '../../../../../src/core/middleware/dimensions'; import { tsx, create, node } from '../../../../../src/core/vdom'; -import renderer from '../../../../../src/testing/renderer'; -import assertionTemplate from '../../../../../src/testing/assertionTemplate'; +import renderer, { assertion } from '../../../../../src/testing/renderer'; describe('node mock', () => { it('should mock nodes', () => { @@ -16,7 +15,7 @@ describe('node mock', () => { }); const r = renderer(() => , { middleware: [[node, nodeMock]] }); r.expect( - assertionTemplate(() => ( + assertion(() => (
{`{"client":{"height":0,"left":0,"top":0,"width":0},"offset":{"height":0,"left":0,"top":0,"width":0},"position":{"bottom":0,"left":0,"right":0,"top":0},"scroll":{"height":0,"left":0,"top":0,"width":0},"size":{"width":0,"height":0}}`}
)) ); @@ -37,7 +36,7 @@ describe('node mock', () => { }; nodeMock('root', domNode); r.expect( - assertionTemplate(() => ( + assertion(() => (
{`{"client":{"height":4,"left":1,"top":2,"width":3},"offset":{"height":10,"left":10,"top":10,"width":10},"position":{"bottom":10,"left":10,"right":10,"top":10},"scroll":{"height":10,"left":10,"top":10,"width":10},"size":{"width":10,"height":10}}`}
)) ); diff --git a/tests/testing/unit/mocks/middleware/resize.tsx b/tests/testing/unit/mocks/middleware/resize.tsx index abba134b2..9ae37f936 100644 --- a/tests/testing/unit/mocks/middleware/resize.tsx +++ b/tests/testing/unit/mocks/middleware/resize.tsx @@ -3,8 +3,7 @@ const { describe } = intern.getPlugin('jsdom'); import createResizeMock from '../../../../../src/testing/mocks/middleware/resize'; import resize from '../../../../../src/core/middleware/resize'; import { tsx, create } from '../../../../../src/core/vdom'; -import renderer from '../../../../../src/testing/renderer'; -import assertionTemplate from '../../../../../src/testing/assertionTemplate'; +import renderer, { assertion } from '../../../../../src/testing/renderer'; describe('resize mock', () => { it('should mock resize middleware calls', () => { @@ -15,11 +14,11 @@ describe('resize mock', () => { return
{JSON.stringify(rects)}
; }); const r = renderer(() => , { middleware: [[resize, resizeMock]] }); - r.expect(assertionTemplate(() =>
null
)); + r.expect(assertion(() =>
null
)); resizeMock('root', { width: 100 }); - r.expect(assertionTemplate(() =>
{`{"width":100}`}
)); + r.expect(assertion(() =>
{`{"width":100}`}
)); resizeMock('root', { width: 101 }); - r.expect(assertionTemplate(() =>
{`{"width":101}`}
)); + r.expect(assertion(() =>
{`{"width":101}`}
)); }); it('should deal with multiple mocked keys', () => { @@ -37,7 +36,7 @@ describe('resize mock', () => { }); const r = renderer(() => , { middleware: [[resize, resizeMock]] }); r.expect( - assertionTemplate(() => ( + assertion(() => (
null
null
@@ -46,7 +45,7 @@ describe('resize mock', () => { ); resizeMock('root', { width: 100 }); r.expect( - assertionTemplate(() => ( + assertion(() => (
{`{"width":100}`}
null
@@ -56,7 +55,7 @@ describe('resize mock', () => { resizeMock('root', { width: 101 }); resizeMock('other', { width: 100 }); r.expect( - assertionTemplate(() => ( + assertion(() => (
{`{"width":101}`}
{`{"width":100}`}
diff --git a/tests/testing/unit/mocks/middleware/store.tsx b/tests/testing/unit/mocks/middleware/store.tsx index 25cb632aa..e8ed87518 100644 --- a/tests/testing/unit/mocks/middleware/store.tsx +++ b/tests/testing/unit/mocks/middleware/store.tsx @@ -3,8 +3,7 @@ const { assert } = intern.getPlugin('chai'); import createStoreMock from '../../../../../src/testing/mocks/middleware/store'; import { createStoreMiddleware } from '../../../../../src/core/middleware/store'; import { tsx, create } from '../../../../../src/core/vdom'; -import renderer, { wrap } from '../../../../../src/testing/renderer'; -import assertionTemplate from '../../../../../src/testing/assertionTemplate'; +import renderer, { wrap, assertion } from '../../../../../src/testing/renderer'; import { createProcess } from '../../../../../src/stores/process'; import { stub } from 'sinon'; import { replace } from '../../../../../src/stores/state/operations'; @@ -53,7 +52,7 @@ describe('store mock', () => { const WrappedButton = wrap('button'); const WrappedOtherButton = wrap('button'); const WrappedSpan = wrap('span'); - const template = assertionTemplate(() => { + const template = assertion(() => { return (
{}} /> diff --git a/tests/testing/unit/mocks/middleware/validity.tsx b/tests/testing/unit/mocks/middleware/validity.tsx index c3f46c52a..87db42c29 100644 --- a/tests/testing/unit/mocks/middleware/validity.tsx +++ b/tests/testing/unit/mocks/middleware/validity.tsx @@ -5,8 +5,7 @@ const { assert } = intern.getPlugin('chai'); import { tsx, create } from '../../../../../src/core/vdom'; import validity from '../../../../../src/core/middleware/validity'; import createValidityMock from '../../../../../src/testing/mocks/middleware/validity'; -import renderer from '../../../../../src/testing/renderer'; -import assertionTemplate from '../../../../../src/testing/assertionTemplate'; +import renderer, { assertion } from '../../../../../src/testing/renderer'; describe('validity mock', () => { it('should mock validity', () => { @@ -19,11 +18,11 @@ describe('validity mock', () => { validityMock('test', { valid: false, message: 'test message' }); let r = renderer(() => , { middleware: [[validity, validityMock]] }); - r.expect(assertionTemplate(() =>
test message
)); + r.expect(assertion(() =>
test message
)); validityMock('test', { valid: true, message: '' }); r = renderer(() => , { middleware: [[validity, validityMock]] }); - r.expect(assertionTemplate(() =>
valid
)); + r.expect(assertion(() =>
valid
)); }); it('defaults to a default return value', () => { @@ -37,6 +36,6 @@ describe('validity mock', () => { }); const r = renderer(() => , { middleware: [[validity, validityMock]] }); - r.expect(assertionTemplate(() =>
)); + r.expect(assertion(() =>
)); }); }); diff --git a/tests/testing/unit/renderer.tsx b/tests/testing/unit/renderer.tsx index 84c990746..175c2567d 100644 --- a/tests/testing/unit/renderer.tsx +++ b/tests/testing/unit/renderer.tsx @@ -1,8 +1,7 @@ const { describe, it } = intern.getInterface('bdd'); const { assert } = intern.getPlugin('chai'); -import { renderer, wrap, compare } from '../../../src/testing/renderer'; -import assertionTemplate from '../../../src/testing/assertionTemplate'; +import { renderer, wrap, compare, assertion } from '../../../src/testing/renderer'; import { WidgetBase } from '../../../src/core/WidgetBase'; import { v, w, create, tsx, diffProperty, invalidator } from '../../../src/core/vdom'; import Set from '../../../src/shim/Set'; @@ -64,7 +63,7 @@ class MyDeferredWidget extends WidgetBase { describe('test renderer', () => { describe('widget with a single top level DNode', () => { it('expect', () => { - const baseAssertion = assertionTemplate(() => + const baseAssertion = assertion(() => v('div', { classes: ['root', 'other'], onclick: () => {} }, [ v( 'span', @@ -83,18 +82,18 @@ describe('test renderer', () => { it('Should support deferred properties', () => { const r = renderer(() => w(MyDeferredWidget, {})); - r.expect(assertionTemplate(() => v('div', { classes: ['root', 'other'], styles: { marginTop: '100px' } }))); + r.expect(assertion(() => v('div', { classes: ['root', 'other'], styles: { marginTop: '100px' } }))); }); it('Should support widgets that have typed children', () => { class WidgetWithTypedChildren extends WidgetBase> {} const r = renderer(() => w(WidgetWithTypedChildren, {}, [w(MyDeferredWidget, {})])); - r.expect(assertionTemplate(() => v('div', [w(MyDeferredWidget, {})]))); + r.expect(assertion(() => v('div', [w(MyDeferredWidget, {})]))); }); it('trigger property of wrapped node', () => { const WrappedDiv = wrap('div'); - const baseTemplate = assertionTemplate(() => + const baseTemplate = assertion(() => v(WrappedDiv.tag, { classes: ['root', 'other'], onclick: () => {} }, [ v( 'span', @@ -111,7 +110,7 @@ describe('test renderer', () => { r.property(WrappedDiv, 'onclick'); r.expect( - assertionTemplate(() => + assertion(() => v(WrappedDiv.tag, { classes: ['root', 'other'], onclick: () => {} }, [ v( 'span', @@ -126,7 +125,7 @@ describe('test renderer', () => { ); r.property(WrappedDiv, 'onclick', [100]); r.expect( - assertionTemplate(() => + assertion(() => v('div', { classes: ['root', 'other'], onclick: () => {} }, [ v( 'span', @@ -143,7 +142,7 @@ describe('test renderer', () => { it('trigger property of wrapped widget', () => { const WrappedChild = wrap(ChildWidget); - const baseTemplate = assertionTemplate(() => + const baseTemplate = assertion(() => v('div', { classes: ['root', 'other'], onclick: () => {} }, [ v( 'span', @@ -159,7 +158,7 @@ describe('test renderer', () => { r.expect(baseTemplate); r.property(WrappedChild, 'func'); r.expect( - assertionTemplate(() => + assertion(() => v('div', { classes: ['root', 'other'], onclick: () => {} }, [ v( 'span', @@ -174,7 +173,7 @@ describe('test renderer', () => { ); r.property(WrappedChild, 'func'); r.expect( - assertionTemplate(() => + assertion(() => v('div', { classes: ['root', 'other'], onclick: () => {} }, [ v( 'span', @@ -225,7 +224,7 @@ describe('test renderer', () => { const WrappedButton = wrap('button'); const WrappedSpan = wrap('span'); - const template = assertionTemplate(() => ( + const template = assertion(() => (
{}}> Update Name @@ -311,7 +310,7 @@ describe('test renderer', () => { r.child(WrappedNestedChildFunctionWidget, ['nested-function']); r.expect( - assertionTemplate(() => ( + assertion(() => (
{{ top: () => 'top', bottom: () => 'bottom' }} @@ -340,7 +339,7 @@ describe('test renderer', () => { assert.throws(() => { r.expect( - assertionTemplate(() => ( + assertion(() => (
{{ top: () => 'top', bottom: () => 'bottom' }} @@ -405,7 +404,7 @@ describe('test renderer', () => { const WrappedWidget = wrap(Widget); const WrappedButton = wrap('button'); - const baseAssertion = assertionTemplate(() => ( + const baseAssertion = assertion(() => (
{{ leading: [], trailing: () => [] }} undefined}> @@ -441,7 +440,7 @@ describe('test renderer', () => { }); const r = renderer(() => ); r.expect( - assertionTemplate(() => ( + assertion(() => (
typeof actual === 'string')} />
@@ -462,7 +461,7 @@ describe('test renderer', () => { const r = renderer(() => ); assert.throws(() => { r.expect( - assertionTemplate(() => ( + assertion(() => (
typeof actual !== 'string')} />
@@ -484,7 +483,7 @@ describe('test renderer', () => { const foo = new Map(); foo.set('a', 'a'); const r = renderer(() => w(Foo, { foo, bar })); - r.expect(assertionTemplate(() => w(Bar, { foo, bar }))); + r.expect(assertion(() => w(Bar, { foo, bar }))); }); it('should throw error if wrapped test node is used more than once', () => { @@ -501,7 +500,7 @@ describe('test renderer', () => { const r = renderer(() => ); assert.throws(() => { r.expect( - assertionTemplate(() => ( + assertion(() => (
hello world @@ -533,7 +532,7 @@ describe('test renderer', () => { const WrappedButton = wrap('button'); const r = renderer(() => ); r.expect( - assertionTemplate(() => ( + assertion(() => (
{}}> Click Me 0 @@ -543,7 +542,7 @@ describe('test renderer', () => { ); r.property(WrappedButton, 'onclick'); r.expect( - assertionTemplate(() => ( + assertion(() => (
)); - const template = assertionTemplate(() => ( + const template = assertion(() => (
@@ -590,7 +589,7 @@ describe('test renderer', () => { const r = renderer(() => ); r.expect( - assertionTemplate(() => ( + assertion(() => (
@@ -598,14 +597,14 @@ describe('test renderer', () => { ); r.expect( - assertionTemplate(() => ( + assertion(() => (
)) ); r.expect( - assertionTemplate(() => ( + assertion(() => (
@@ -631,14 +630,14 @@ describe('test renderer', () => { }); const r = renderer(() => ); r.expect( - assertionTemplate(() => ( + assertion(() => (
)) ); r.expect( - assertionTemplate(() => ( + assertion(() => (
From ae31cbd0d089fe48106f296722360c823fc19d29 Mon Sep 17 00:00:00 2001 From: Anthony Gubler Date: Wed, 15 Apr 2020 15:09:48 +0100 Subject: [PATCH 15/15] update docs --- docs/en/testing/introduction.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/en/testing/introduction.md b/docs/en/testing/introduction.md index 382755878..fb470d14e 100644 --- a/docs/en/testing/introduction.md +++ b/docs/en/testing/introduction.md @@ -2,12 +2,12 @@ Dojo provides a robust testing framework using `@dojo/cli-test-intern`. It allows you to efficiently test the output of your widgets and validate your expectations. -| Feature | Description | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | -| **Minimal API** | Simple API for testing and asserting Dojo widget's expected virtual DOM and behavior. | -| **Unit tests** | Unit tests are tests run via node and browser to test isolated blocks of code. | -| **Functional tests** | Functional tests are run using Selenium in the browser and test the overall functionality of the software as a user would interact with it. | -| **Assertion templates** | Assertion Templates allow you to build expected render functions to validate the output of your widgets. | +| Feature | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | +| **Minimal API** | Simple API for testing and asserting Dojo widget's expected virtual DOM and behavior. | +| **Unit tests** | Unit tests are tests run via node and browser to test isolated blocks of code. | +| **Functional tests** | Functional tests are run using Selenium in the browser and test the overall functionality of the software as a user would interact with it. | +| **Assertions** | Assertions allow you to build expected render functions to validate the output of your widgets. | # Basic usage @@ -117,9 +117,9 @@ describe('routing', () => { }); ``` -## Using assertion templates +## Using assertions -Assertion templates provide a way to create a base assertion that allow parts of the expected output to vary between tests. +Assertions provide a way to create a base assertion that allow parts of the expected output to vary between tests. - Given a widget that renders output differently based on property values: @@ -144,7 +144,7 @@ const Profile = factory(function Profile({ properties }) { export default Profile; ``` -- Create an assertion template using `@dojo/framework/testing/assertion` +- Create an assertion using `@dojo/framework/testing/renderer#assertion` > tests/unit/widgets/Profile.tsx @@ -168,7 +168,7 @@ describe('Profile', () => { }); ``` -To work with assertion templates, wrapped nodes can get created using `@dojo/framework/testing/renderer#wrap` in order to use the assertion template API. Note: when using wrapped `VNode`s with `v()`, the `.tag` property needs to get used, for example `v(WrappedDiv.tag, {} [])`. +To work with assertion, wrapped nodes can get created using `@dojo/framework/testing/renderer#wrap` in order to use the assertion API. Note: when using wrapped `VNode`s with `v()`, the `.tag` property needs to get used, for example `v(WrappedDiv.tag, {} [])`. > tests/unit/widgets/Profile.tsx @@ -205,7 +205,7 @@ describe('Profile', () => { }); ``` -Using the `setChildren` method of an assertion template with a wrapped testing node, `WrappedHeader` in this case, will return an assertion template with the updated virtual DOM structure. This resulting assertion template can then be used to test widget output. +Using the `setChildren` method of an assertion with a wrapped testing node, `WrappedHeader` in this case, will return an assertion with the updated virtual DOM structure. This resulting assertion can then be used to test widget output. [dojo cli]: https://github.com/dojo/cli [intern]: https://theintern.io/