spincycle
Version:
A reactive message router and object manager that lets clients subscribe to object property changes on the server
327 lines (256 loc) • 9.19 kB
HTML
<!--
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<!--
The `<test-fixture>` element can simplify the exercise of consistently
resetting a test suite's DOM. To use it, wrap the test suite's DOM as a template:
```html
<test-fixture id="SomeElementFixture">
<template>
<some-element id="SomeElementForTesting"></some-element>
</template>
</test-fixture>
```
Now, the `<test-fixture>` element can be used to generate a copy of its
template:
```html
<script>
describe('<some-element>', function () {
var someElement;
beforeEach(function () {
document.getElementById('SomeElementFixture').create();
someElement = document.getElementById('SomeElementForTesting');
});
});
</script>
```
Fixtured elements can be cleaned up by calling `restore` on the `<test-fixture>`:
```javascript
afterEach(function () {
document.getElementById('SomeElementFixture').restore();
// <some-element id='SomeElementForTesting'> has been removed
});
```
`<test-fixture>` will create fixtures from all of its immediate `<template>`
children. The DOM structure of fixture templates can be as simple or as complex
as the situation calls for.
## Even simpler usage in Mocha
In Mocha, usage can be simplified even further. Include `test-fixture-mocha.js`
after Mocha in the `<head>` of your document and then fixture elements like so:
```html
<script>
describe('<some-element>', function () {
var someElement;
beforeEach(function () {
someElement = fixture('SomeElementFixture');
});
});
</script>
```
Fixtured elements will be automatically restored in the `afterEach` phase of the
current Mocha `Suite`.
## Data-bound templates
Data-binding systems are also supported, as long as your (custom) template
elements define a `stamp(model)` method that returns a document fragment. This
allows you to stamp out templates w/ custom models for your fixtures.
For example, using Polymer 0.8's `dom-template`:
```html
<test-fixture id="bound">
<template is="dom-template">
<span>{{greeting}}</span>
</template>
</test-fixture>
```
You can pass an optional context argument to `create()` or `fixture()` to pass
the model:
```js
var bound = fixture('bound', {greeting: 'ohai thurr'});
```
## The problem being addressed
Consider the following `web-component-tester` test suite:
```html
<!doctype html>
<html>
<head>
<title>some-element test suite</title>
<link rel="import" href="../some-element.html">
</head>
<body>
<some-element id="SomeElementForTesting"></some-element>
<script>
describe('<some-element>', function () {
var someElement;
beforeEach(function () {
someElement = document.getElementById('SomeElementForTesting');
});
it('can receive property `foo`', function () {
someElement.foo = 'bar';
expect(someElement.foo).to.be.equal('bar');
});
it('has a default `foo` value of `undefined`', function () {
expect(someElement.foo).to.be.equal(undefined);
});
});
</script>
</body>
</html>
```
In this contrived example, the suite will pass or fail depending on which order
the tests are run in. It is non-deterministic because `someElement` has
internal state that is not properly reset at the end of each test.
It would be trivial in the above example to simply reset `someElement.foo` to
the expected default value of `undefined` in an `afterEach` hook. However, for
non-contrived test suites that target complex elements, this can result in a
large quantity of ever-growing set-up and tear-down boilerplate.
@pseudoElement test-fixture
-->
<script>
(function () {
var TestFixturePrototype = Object.create(HTMLElement.prototype);
var TestFixtureExtension = {
_fixtureTemplates: null,
_elementsFixtured: false,
get elementsFixtured () {
return this._elementsFixtured;
},
get fixtureTemplates () {
if (!this._fixtureTemplates) {
// Copy fixtures to a true Array for Safari 7. This prevents their
// `content` property from being improperly garbage collected.
this._fixtureTemplates = Array.prototype.slice.apply(this.querySelectorAll('template'));
}
return this._fixtureTemplates;
},
create: function (model) {
var generatedDoms = [];
this.restore();
this.removeElements(this.fixtureTemplates);
this.forElements(this.fixtureTemplates, function (fixtureTemplate) {
generatedDoms.push(
this.createFrom(fixtureTemplate, model)
);
}, this);
this.forcePolyfillAttachedStateSynchrony();
if (generatedDoms.length < 2) {
return generatedDoms[0];
}
return generatedDoms;
},
createFrom: function (fixtureTemplate, model) {
var fixturedFragment;
var fixturedElements;
var fixturedElement;
if (!(fixtureTemplate &&
fixtureTemplate.tagName === 'TEMPLATE')) {
return;
}
try {
fixturedFragment = this.stamp(fixtureTemplate, model);
} catch (error) {
console.error('Error stamping', fixtureTemplate, error);
throw error;
}
fixturedElements = this.collectElementChildren(fixturedFragment);
this.appendChild(fixturedFragment);
this._elementsFixtured = true;
if (fixturedElements.length < 2) {
return fixturedElements[0];
}
return fixturedElements;
},
restore: function () {
if (!this._elementsFixtured) {
return;
}
this.removeElements(this.children);
this.forElements(this.fixtureTemplates, function (fixtureTemplate) {
this.appendChild(fixtureTemplate);
}, this);
this.generatedDomStack = [];
this._elementsFixtured = false;
this.forcePolyfillAttachedStateSynchrony();
},
forcePolyfillAttachedStateSynchrony: function () {
// Force synchrony in attachedCallback and detachedCallback where
// implemented, in the event that we are dealing with the async Web
// Components Polyfill.
if (window.CustomElements && window.CustomElements.takeRecords) {
window.CustomElements.takeRecords();
}
},
collectElementChildren: function (parent) {
// Note: Safari 7.1 does not support `firstElementChild` or
// `nextElementSibling`, so we do things the old-fashioned way:
var elements = [];
var child = parent.firstChild;
while (child) {
if (child.nodeType === Node.ELEMENT_NODE) {
elements.push(child);
}
child = child.nextSibling;
}
return elements;
},
removeElements: function (elements) {
this.forElements(elements, function (element) {
this.removeChild(element);
}, this);
},
forElements: function (elements, iterator, context) {
Array.prototype.slice.call(elements)
.forEach(iterator, context);
},
stamp: function (fixtureTemplate, model) {
var stamped;
// Check if we are dealing with a "stampable" `<template>`. This is a
// vaguely defined special case of a `<template>` that is a custom
// element with a public `stamp` method that implements some manner of
// data binding.
if (fixtureTemplate.stamp) {
stamped = fixtureTemplate.stamp(model);
// We leak Polymer specifics a little; if there is an element `root`, we
// want that to be returned.
stamped = stamped.root || stamped;
// Otherwise, we fall back to standard HTML templates, which do not have
// any sort of binding support.
} else {
if (model) {
console.warn(this, 'was given a model to stamp, but the template is not of a bindable type');
}
stamped = document.importNode(fixtureTemplate.content, true);
// Immediately upgrade the subtree if we are dealing with async
// Web Components polyfill.
// https://github.com/Polymer/polymer/blob/0.8-preview/src/features/mini/template.html#L52
if (window.CustomElements && CustomElements.upgradeSubtree) {
CustomElements.upgradeSubtree(stamped);
}
}
return stamped;
}
};
Object.getOwnPropertyNames(TestFixtureExtension)
.forEach(function (property) {
Object.defineProperty(
TestFixturePrototype,
property,
Object.getOwnPropertyDescriptor(TestFixtureExtension, property)
);
});
try {
document.registerElement('test-fixture', {
prototype: TestFixturePrototype
});
} catch (e) {
if (window.WCT) {
console.warn('if you are using WCT, you do not need to manually import test-fixture.html');
} else {
console.warn('test-fixture has already been registered!');
}
}
})();
</script>