feat(testing-helpers): fixture can handle strings and TemplateResults

This commit is contained in:
Thomas Allmer
2019-01-02 00:27:28 +01:00
parent 09fab1de02
commit 0649ea03bd
25 changed files with 199 additions and 155 deletions

View File

@@ -53,7 +53,7 @@ it('has the following shadow dom', async () => {
```
## Literal matching
By default dom is diffed 'semantically'. Differences in whitespace, newlines, attributes/class order are ignored and style, script and commend nodes are removed.
By default dom is diffed 'semantically'. Differences in whitespace, newlines, attributes/class order are ignored and style, script and comment nodes are removed.
If you want to match literally instead you can use some of the provided utilities to handle diffing on browsers with the shadow dom polyfill:

View File

@@ -68,9 +68,11 @@ export const chaiDomEquals = (chai, utils) => {
try {
new chai.Assertion(actualHTML).to.equal(expectedHTML);
} catch (error) {
/* eslint-disable no-console */
console.log('Snapshot changed, want to accept the change:');
console.log('');
console.log(actualHTML);
/* eslint-enable no-console */
throw error;
}
@@ -82,9 +84,11 @@ export const chaiDomEquals = (chai, utils) => {
try {
new chai.Assertion(actualHTML).to.equal(expectedHTML);
} catch (error) {
/* eslint-disable no-console */
console.log('Snapshot changed, want to accept the change:');
console.log('');
console.log(actualHTML);
/* eslint-enable no-console */
throw error;
}

View File

@@ -1,6 +1,6 @@
// do manual setup and not use testing to not have circle dependencies
import { chai } from '@bundled-es-modules/chai';
import { cachedWrappers } from '@open-wc/testing-helpers/fixture.js';
import { cachedWrappers } from '@open-wc/testing-helpers/fixtureWrapper.js';
import { chaiDomEquals } from '../chai-dom-equals.js';
// register-cleanup

View File

@@ -26,10 +26,10 @@ it('can instantiate an element', async () => {
## Test a custom element with properties
```js
import { html, litFixture } from '@open-wc/testing-helpers';
import { html, fixture } from '@open-wc/testing-helpers';
it('can instantiate an element with properties', async () => {
const el = await litFixture(html`<my-el .foo=${'bar'}></my-el>`);
const el = await fixture(html`<my-el .foo=${'bar'}></my-el>`);
expect(el.foo).to.equal('bar');
}
```
@@ -57,7 +57,7 @@ This is using a "workaround" which is not performant for rerenders.
As this is usually not a problem for tests it's ok here but do NOT use it in production code.
```js
import { html, litFixture, defineCE, unsafeStatic } from '@open-wc/testing-helpers';
import { html, fixture, defineCE, unsafeStatic } from '@open-wc/testing-helpers';
const tagName = defineCE(class extends MyMixin(HTMLElement) {
constructor() {
@@ -66,7 +66,7 @@ const tagName = defineCE(class extends MyMixin(HTMLElement) {
}
});
const tag = unsafeStatic(tagName);
const el = litFixture(html`<${tag} .bar=${'baz'}></${tag}>`);
const el = fixture(html`<${tag} .bar=${'baz'}></${tag}>`);
expect(el.bar).to.equal('baz');
```
@@ -74,9 +74,9 @@ expect(el.bar).to.equal('baz');
If you need to wait for multiple elements to update you can use `nextFrame`.
```js
import { nextFrame, aTimeout, html, litFixture } from '@open-wc/testing-helpers';
import { nextFrame, aTimeout, html, fixture } from '@open-wc/testing-helpers';
const el = await litFixture(html`<my-el .foo=${'bar'}></my-el>`);
const el = await fixture(html`<my-el .foo=${'bar'}></my-el>`);
expect(el.foo).to.equal('bar');
el.foo = 'baz';
await nextFrame();

View File

@@ -1,5 +1,4 @@
type PropsFunction = (element: HTMLElement) => Object;
type FixtureProps = Object | PropsFunction;
export class FixtureWrapper extends HTMLElement { };
export function fixtureSync(template: string, props?: FixtureProps): FixtureWrapper;
export async function fixture(template: string, setup?: FixtureProps): Promise<FixtureWrapper>;
import { TemplateResult } from 'lit-html';
export function fixtureSync(template: string | TemplateResult): Element;
export function fixture(template: string | TemplateResult): Promise<Element>;

View File

@@ -1,54 +1,34 @@
import { TemplateResult } from 'lit-html';
import { nextFrame } from './helpers.js';
export const cachedWrappers = [];
/**
* Creates a wrapper as a direct child of `<body>` to put the tested element into.
* Needed to run a `connectedCallback()` on a tested element.
*
* @returns {HTMLElement}
* @private
*/
export class FixtureWrapper {
constructor() {
const wrapper = document.createElement('div');
document.body.appendChild(wrapper);
cachedWrappers.push(wrapper);
return wrapper;
}
}
import { stringFixtureSync } from './stringFixture.js';
import { litFixtureSync } from './litFixture.js';
/**
* Setups an element synchronously from the provided string template and puts it in the DOM.
* Allows to specify properties via an object or a function taking the element as an argument.
*
* @param {string} template
* @param {Object|function(element: HTMLElement)} props
* @returns {HTMLElement}
* @param {string | TemplateResult} template
* @returns {Element}
*/
export function fixtureSync(template, props = {}) {
const parent = document.createElement('div'); // we need a real dom node for getters/setters
parent.innerHTML = template;
const element = parent.children[0];
const properties = typeof props === 'function' ? props(element) : props;
Object.keys(properties).forEach(prop => {
element[prop] = properties[prop];
});
const wrapper = new FixtureWrapper();
wrapper.appendChild(element);
return wrapper.children[0];
export function fixtureSync(template) {
if (typeof template === 'string') {
return stringFixtureSync(template);
}
if (template instanceof TemplateResult) {
return litFixtureSync(template);
}
throw new Error('Invalid template provided - string or lit-html TemplateResult is supported');
}
/**
* Setups an element asynchronously from the provided string template and puts it in the DOM.
* Allows to specify properties via an object or a function taking the element as an argument.
*
* @param {string} template
* @param {Object|function(element: HTMLElement)} props
* @returns {Promise<HTMLElement>}
* @param {string | TemplateResult} template
* @returns {Promise<Element>}
*/
export async function fixture(template, setup = {}) {
const result = fixtureSync(template, setup);
export async function fixture(template) {
const result = fixtureSync(template);
await nextFrame();
return result;
}

View File

@@ -0,0 +1,2 @@
export const cachedWrappers: Array<Element>;
export function fixtureWrapper(): Element;

View File

@@ -0,0 +1,16 @@
/** @type Array<Element> */
export const cachedWrappers = [];
/**
* Creates a wrapper as a direct child of `<body>` to put the tested element into.
* Needed to run a `connectedCallback()` on a tested element.
*
* @returns {Element}
* @private
*/
export function fixtureWrapper() {
const wrapper = document.createElement('div');
document.body.appendChild(wrapper);
cachedWrappers.push(wrapper);
return wrapper;
}

View File

@@ -2,9 +2,9 @@ type Constructor<T = {}> = new (...args: any[]) => T;
export function defineCE<TBase extends Constructor>(klass: TBase): string;
export function isIE(): boolean
export async function aTimeout(ms: int): void
export async function triggerBlurFor(element: HTMLElement): void
export async function triggerFocusFor(element: HTMLElement): void
export async function oneEvent(element: HTMLElement, eventName: string): Event
export async function nextFrame(): void
export async function flush(): void
export function aTimeout(ms: number): Promise<void>
export function triggerBlurFor(element: HTMLElement): Promise<void>
export function triggerFocusFor(element: HTMLElement): Promise<void>
export function oneEvent(element: HTMLElement, eventName: string): Promise<Event>
export function nextFrame(): Promise<void>
export function flush(): Promise<void>

View File

@@ -11,7 +11,7 @@ let defineCECounter = 0;
* const el = fixture(`<${tag}></${tag}>`);
* // test el
*
* @param {function()} klass
* @param {function} klass
* @returns {string}
*/
export function defineCE(klass) {

View File

@@ -1,12 +1,13 @@
export { html, unsafeStatic } from './lit-html';
export { html, unsafeStatic } from './lit-html.js';
export {
aTimeout,
defineCE,
isIE,
nextFrame,
oneEvent,
triggerBlurFor,
triggerFocusFor,
} from './helpers';
export { litFixture, litFixtureSync } from './litFixture';
export { fixture, fixtureSync } from './fixture';
oneEvent,
isIE,
defineCE,
aTimeout,
nextFrame,
} from './helpers.js';
export { litFixture, litFixtureSync } from './litFixture.js';
export { stringFixture, stringFixtureSync } from './stringFixture.js';
export { fixture, fixtureSync } from './fixture.js';

View File

@@ -9,4 +9,5 @@ export {
nextFrame,
} from './helpers.js';
export { litFixture, litFixtureSync } from './litFixture.js';
export { stringFixture, stringFixtureSync } from './stringFixture.js';
export { fixture, fixtureSync } from './fixture.js';

View File

@@ -1,4 +1,4 @@
import { FixtureWrapper } from './fixture';
import { TemplateResult } from 'lit-html';
export function litFixtureSync(template: TemplateResult): FixtureWrapper;
export async function litFixture(template: TemplateResult): Promise<FixtureWrapper>;
export function litFixtureSync(template: TemplateResult): Element;
export function litFixture(template: TemplateResult): Promise<Element>;

View File

@@ -1,15 +1,15 @@
import { FixtureWrapper } from './fixture.js';
import { fixtureWrapper } from './fixtureWrapper.js';
import { render } from './lit-html.js';
import { nextFrame } from './helpers.js';
/**
* Setups an element synchronously from the provided lit-html template and puts it in the DOM.
*
* @param {TemplateResult} template
* @returns {HTMLElement}
* @param {import('lit-html').TemplateResult} template
* @returns {Element}
*/
export function litFixtureSync(template) {
const wrapper = new FixtureWrapper();
const wrapper = fixtureWrapper();
render(template, wrapper);
return wrapper.children[0];
}
@@ -17,8 +17,8 @@ export function litFixtureSync(template) {
/**
* Setups an element asynchronously from the provided lit-html template and puts it in the DOM.
*
* @param {TemplateResult} template
* @returns {Promise<HTMLElement>}
* @param {import('lit-html').TemplateResult} template
* @returns {Promise<Element>}
*/
export async function litFixture(template) {
const fixture = litFixtureSync(template);

View File

@@ -0,0 +1,28 @@
import { nextFrame } from './helpers.js';
import { fixtureWrapper } from './fixtureWrapper.js';
/**
* Setups an element synchronously from the provided string template and puts it in the DOM.
* Allows to specify properties via an object or a function taking the element as an argument.
*
* @param {string} template
* @returns {Element}
*/
export function stringFixtureSync(template) {
const wrapper = fixtureWrapper();
wrapper.innerHTML = template;
return wrapper.children[0];
}
/**
* Setups an element asynchronously from the provided string template and puts it in the DOM.
* Allows to specify properties via an object or a function taking the element as an argument.
*
* @param {string} template
* @returns {Promise<Element>}
*/
export async function stringFixture(template) {
const result = stringFixtureSync(template);
await nextFrame();
return result;
}

View File

@@ -1,5 +1,5 @@
// do manual setup and not use testing to not have circle dependencies
import { cachedWrappers } from '../fixture.js';
import { cachedWrappers } from '../fixtureWrapper.js';
// register-cleanup
if (afterEach) {

View File

@@ -0,0 +1,48 @@
import { expect } from '@bundled-es-modules/chai';
import { html, fixture, fixtureSync } from '../index.js';
class TestComponent2 extends HTMLElement {}
customElements.define('test-component2', TestComponent2);
describe('fixtureSync & fixture', () => {
it('supports strings', async () => {
/**
* @param {Element} element
*/
function testElement(element) {
expect(element.getAttribute('foo')).to.equal('bar');
}
const elementSync = fixtureSync(html`
<test-component2 foo="${'bar'}"></test-component2>
`);
testElement(elementSync);
const elementAsync = await fixture(html`
<test-component2 foo="${'bar'}"></test-component2>
`);
testElement(elementAsync);
});
it('supports lit-html TemplateResult with properties', async () => {
const myFunction = () => {};
/**
* @param {Element} element
*/
function testElement(element) {
expect(element.propNumber).to.equal(10);
expect(element.propFunction).to.equal(myFunction);
}
const elementSync = fixtureSync(html`
<test-component2 .propNumber=${10} .propFunction=${myFunction}></test-component2>
`);
testElement(elementSync);
const elementAsync = await fixture(html`
<test-component2 .propNumber=${10} .propFunction=${myFunction}></test-component2>
`);
testElement(elementAsync);
});
});

View File

@@ -13,7 +13,12 @@
</script>
<script type="module">
import './index.js';
import './bdd-setup.js';
import './fixture.test.js';
import './helpers.test.js';
import './lit-html.test.js';
import './stringLitFixture.test.js';
mocha.checkLeaks();
mocha.run();

View File

@@ -1,6 +0,0 @@
// do manual setup and not use testing to not have circle dependencies
import './bdd-setup.js';
import './lit-html.test.js';
import './helpers.test.js';
import './litFixture.test.js';

View File

@@ -1 +0,0 @@
import './index.js';

View File

@@ -1,17 +1,20 @@
import { expect } from '@bundled-es-modules/chai';
import { html, fixture, fixtureSync, litFixture, litFixtureSync } from '../index.js';
import { html, stringFixture, stringFixtureSync, litFixture, litFixtureSync } from '../index.js';
class TestComponent extends HTMLElement {}
customElements.define('test-component', TestComponent);
describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
describe('stringFixtureSync & litFixtureSync & fixture & litFixture', () => {
it('asynchronously returns an element node', async () => {
/**
* @param {Element} element
*/
function testElement(element) {
expect(element).to.be.an.instanceof(TestComponent);
expect(element.textContent).to.equal('Text content');
}
[
fixtureSync('<test-component>Text content</test-component>'),
stringFixtureSync('<test-component>Text content</test-component>'),
litFixtureSync(
html`
<test-component>Text content</test-component>
@@ -19,7 +22,7 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
),
].forEach(testElement);
(await Promise.all([
fixture('<test-component>Text content</test-component>'),
stringFixture('<test-component>Text content</test-component>'),
litFixture(
html`
<test-component>Text content</test-component>
@@ -29,12 +32,15 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
});
it('wraps element into a div attached to the body', async () => {
/**
* @param {Element} element
*/
function testElement(element) {
expect(element.parentNode).to.be.an.instanceof(HTMLDivElement);
expect(element.parentNode.parentNode).to.equal(document.body);
}
[
fixtureSync('<test-component></test-component>'),
stringFixtureSync('<test-component></test-component>'),
litFixtureSync(
html`
<test-component></test-component>
@@ -42,7 +48,7 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
),
].forEach(testElement);
(await Promise.all([
fixture('<test-component></test-component>'),
stringFixture('<test-component></test-component>'),
litFixture(
html`
<test-component></test-component>
@@ -52,14 +58,17 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
});
it('allows to create several fixtures in one test', async () => {
/**
* @param {Element} element
*/
function testElement(element) {
expect(element).to.be.an.instanceof(TestComponent);
expect(element.parentNode).to.be.an.instanceof(HTMLDivElement);
expect(element.parentNode.parentNode).to.equal(document.body);
}
[
fixtureSync('<test-component></test-component>'),
fixtureSync('<test-component></test-component>'),
stringFixtureSync('<test-component></test-component>'),
stringFixtureSync('<test-component></test-component>'),
litFixtureSync(
html`
<test-component></test-component>
@@ -72,8 +81,8 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
),
].forEach(testElement);
(await Promise.all([
fixture('<test-component></test-component>'),
fixture('<test-component></test-component>'),
stringFixture('<test-component></test-component>'),
stringFixture('<test-component></test-component>'),
litFixture(
html`
<test-component></test-component>
@@ -88,11 +97,14 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
});
it('handles self closing tags', async () => {
/**
* @param {Element} element
*/
function testElement(element) {
expect(element).to.be.an.instanceof(TestComponent);
}
[
fixtureSync('<test-component/>'),
stringFixtureSync('<test-component/>'),
litFixtureSync(
html`
<test-component />
@@ -100,7 +112,7 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
),
].forEach(testElement);
(await Promise.all([
fixture('<test-component/>'),
stringFixture('<test-component/>'),
litFixture(
html`
<test-component />
@@ -110,11 +122,14 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
});
it('always returns first child element ignoring whitespace and other elements', async () => {
/**
* @param {Element} element
*/
function testElement(element) {
expect(element).to.be.an.instanceof(TestComponent);
}
[
fixtureSync(`
stringFixtureSync(`
<test-component></test-component>
<div></div>
`),
@@ -124,7 +139,7 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
`),
].forEach(testElement);
(await Promise.all([
fixture(`
stringFixture(`
<test-component></test-component>
<div></div>
`),
@@ -135,50 +150,3 @@ describe('fixtureSync & litFixtureSync & fixture & litFixture', () => {
])).forEach(testElement);
});
});
describe('fixtureSync & fixture', () => {
it('accepts root element properties via second argument as an object', async () => {
const elementSync = fixtureSync('<div></div>', {
myProp: 'value',
});
expect(elementSync.myProp).to.equal('value');
const elementAsync = await fixture('<div></div>', {
myProp: 'value',
});
expect(elementAsync.myProp).to.equal('value');
});
it('accepts root element properties via second argument as a function with "element" argument', async () => {
const elementSync = fixtureSync('<div></div>', element => ({
myProp: `access-to-${element.tagName}-instance`,
}));
expect(elementSync.myProp).to.equal('access-to-DIV-instance');
const elementAsync = await fixture('<div></div>', element => ({
myProp: `access-to-${element.tagName}-instance`,
}));
expect(elementAsync.myProp).to.equal('access-to-DIV-instance');
});
});
describe('litFixtureSync & litFixture', () => {
it('supports lit-html', async () => {
const myFunction = () => {};
function testElement(element) {
expect(element.propNumber).to.equal(10);
expect(element.propFunction).to.equal(myFunction);
}
const elementSync = litFixtureSync(html`
<test-component .propNumber=${10} .propFunction=${myFunction}></test-component>
`);
testElement(elementSync);
const elementAsync = await litFixture(html`
<test-component .propNumber=${10} .propFunction=${myFunction}></test-component>
`);
testElement(elementAsync);
});
});

View File

@@ -48,7 +48,7 @@ import { expect } '@open-wc/testing';
This will have the following side effect:
- use the plugin [chai-dom-equals](https://www.npmjs.com/package/@open-wc/chai-dom-equals)
- enables cleanup after each test for `fixture` and `litFixture`
- enables cleanup after each test for all `fixture`s
## Automating Tests
Normally, you'll want some way of automatically running all of your tests, for that we recommend karma via `@open-wc/testing-karma` and browserstack via `@open-wc/testing-karma-bs`.
@@ -104,7 +104,6 @@ A typical webcomponent test will look something like this:
import {
html,
fixture,
litFixture,
expect,
} from '@open-wc/testing';
@@ -118,14 +117,14 @@ describe('True Checking', () => {
it('false values will have a light-dom of <p>NOPE</p>', async () => {
const el = await fixture('<get-result></get-result>');
expect(el).dom.to.semantically.equal('<get-result><p>NOPE</p></get-result>');
expect(el).dom.to.equal('<get-result><p>NOPE</p></get-result>');
});
it('true values will have a light-dom of <p>YEAH</p>', async () => {
const foo = 1;
const el = await litFixture(html`<get-result .success=${foo === 1}></get-result>`);
const el = await fixture(html`<get-result .success=${foo === 1}></get-result>`);
expect(el.success).to.be.true;
expect(el).dom.to.semantically.equal('<get-result><p>YEAH</p></get-result>');
expect(el).dom.to.equal('<get-result><p>YEAH</p></get-result>');
});
});
```

View File

@@ -11,7 +11,7 @@ describe('True Checking', () => {
it('false values will have a light-dom of <p>NOPE</p>', async () => {
const el = await fixture('<get-result></get-result>');
expect(el).dom.to.semantically.equal('<get-result><p>NOPE</p></get-result>');
expect(el).dom.to.equal('<get-result><p>NOPE</p></get-result>');
});
it('true values will have a light-dom of <p>YEAH</p>', async () => {
@@ -22,6 +22,6 @@ describe('True Checking', () => {
`,
);
expect(el.success).to.be.true;
expect(el).dom.to.semantically.equal('<get-result><p>YEAH</p></get-result>');
expect(el).dom.to.equal('<get-result><p>YEAH</p></get-result>');
});
});

View File

@@ -1,4 +1,4 @@
import { cachedWrappers } from '@open-wc/testing-helpers/fixture.js';
import { cachedWrappers } from '@open-wc/testing-helpers/fixtureWrapper.js';
if (afterEach) {
afterEach(() => {

View File

@@ -1,6 +1,6 @@
/* eslint-disable no-unused-expressions */
import { cachedWrappers } from '@open-wc/testing-helpers/fixture.js';
import { cachedWrappers } from '@open-wc/testing-helpers/fixtureWrapper.js';
import { fixture, expect } from '../index.js';
describe('BDD', () => {
@@ -17,6 +17,6 @@ describe('BDD', () => {
it('uses chai dom equals plugin', async () => {
const el = await fixture(`<div><!-- comment --><h1>${'Hey'} </h1> </div>`);
expect(el).dom.to.semantically.equal('<div><h1>Hey</h1></div>');
expect(el).dom.to.equal('<div><h1>Hey</h1></div>');
});
});