UNPKG

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
<!-- 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>