Diff
Modified: trunk/Websites/perf.webkit.org/ChangeLog (232587 => 232588)
--- trunk/Websites/perf.webkit.org/ChangeLog 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/ChangeLog 2018-06-07 18:25:35 UTC (rev 232588)
@@ -1,3 +1,149 @@
+2018-06-07 Ryosuke Niwa <[email protected]>
+
+ Add the basic support for writing components in node.js
+ https://bugs.webkit.org/show_bug.cgi?id=186299
+
+ Reviewed by Antti Koivisto.
+
+ Add the basic support for writing components in node.js for generating rich email notifications.
+
+ To do this, this patch introduces MarkupComponentBase and MarkupPage which implement similar API
+ to ComponentBase and Page classes of v3 UI code. This enables us to share code between frontend
+ and the backend in the future. Because there is no support for declarative custom elements or
+ shadow root in HTML, MarkupComponentBase uses a similar but distinct concept of "content" tree
+ to represent the "DOM" tree for a component. When generating the HTML, MarkupComponentBase and
+ MarkupPage collectively transforms stylesheets and flattens the tree into a single HTML. In order
+ to keep this flatteneing logic simple, MarkupComponentBase only supports a very small subset of
+ CSS selectors to select elements by their local names and class names.
+
+ Specifically, each class name and element name based selectors are replaced by a globally unique
+ class name based selector, and each element which matches the selector is applied of the same
+ globally unique class name. The transformation is applied when constructing the "content" tree
+ as well as calls to renderReplace.
+
+ Because much of v3 frontend code relies on DOM API, this patch also implements the simplest form
+ of a fake DOM API as MarkupNode, MarkupParentNode, MarkupElement, and MarkupText. In order to avoid
+ reimplementing HTML & CSS parsers, this patch introduces the concept of content and style templates
+ to ComponentBase which are JSON alternatives to HTML & CSS template strings which can be used in
+ both frontend & backend.
+
+ * browser-tests/close-button-tests.js: Include CommonComponentBase.
+ * browser-tests/commit-log-viewer-tests.js: Ditto.
+ * browser-tests/component-base-tests.js: Ditto. Added a test cases for content & style templates.
+ (async.importComponentBase): Added.
+ * browser-tests/editable-text-tests.js: Include CommonComponentBase.
+ * browser-tests/index.html:
+ * browser-tests/markup-page-tests.js: Added.
+ * browser-tests/page-router-tests.js: Include CommonComponentBase.
+ * browser-tests/page-tests.js: Ditto.
+ * browser-tests/test-group-form-tests.js: Ditto.
+ * public/shared/common-component-base.js: Added.
+ (CommonComponentBase): Extracted out of ComponentBase.
+ (CommonComponentBase.prototype.renderReplace): Added.
+ (CommonComponentBase.renderReplace): Moved from ComponentBase.
+ (CommonComponentBase.prototype._recursivelyUpgradeUnknownElements): Moved and renamed from
+ ComponentBase's _recursivelyReplaceUnknownElementsByComponents.
+ (CommonComponentBase.prototype._upgradeUnknownElement): Extracted out of the same function.
+ (CommonComponentBase._constructStylesheetFromTemplate): Added.
+ (CommonComponentBase._constructNodeTreeFromTemplate): Added.
+ (CommonComponentBase.prototype.createElement): Added.
+ (CommonComponentBase.createElement): Moved from ComponentBase.
+ (CommonComponentBase._addContentToElement): Moved from ComponentBase.
+ (CommonComponentBase.prototype.createLink): Added.
+ (CommonComponentBase.createLink): Moved from ComponentBase.
+ (CommonComponentBase._context): Added. Set to document in a browser and MarkupDocument in node.js.
+ (CommonComponentBase._isNode): Added. Set to a function which does instanceof Node/MarkupNode check.
+ (CommonComponentBase._baseClass): Added. Set to ComponentBase or MarkupComponentBase.
+ * public/v3/components/base.js:
+ (ComponentBase):
+ (ComponentBase.prototype._ensureShadowTree): Added the support for the content and style templates.
+ Also avoid parsing the html template each time a component is instantiated by caching the result.
+ * public/v3/index.html:
+ * tools/js/markup-component.js: Added.
+ (MarkupDocument): Added. A fake Document.
+ (MarkupDocument.prototype.createContentRoot): A substitude for attachShadow.
+ (MarkupDocument.prototype.createElement):
+ (MarkupDocument.prototype.createTextNode):
+ (MarkupDocument.prototype._idForClone):
+ (MarkupDocument.prototype.reset):
+ (MarkupDocument.prototype.markup):
+ (MarkupDocument.prototype.escapeAttributeValue):
+ (MarkupDocument.prototype.escapeNodeData):
+ (MarkupNode): Added. A fake Node. Each node gets an unique ID.
+ (MarkupNode.prototype._markup):
+ (MarkupNode.prototype.clone): Implemented by the leave class.
+ (MarkupNode.prototype._cloneNodeData):
+ (MarkupNode.prototype.remove):
+ (MarkupParentNode): Added. An equivalent of ContainerNode in WebCore.
+ (MarkupParentNode.prototype.get childNodes):
+ (MarkupParentNode.prototype._cloneNodeData):
+ (MarkupParentNode.prototype.appendChild):
+ (MarkupParentNode.prototype.removeChild):
+ (MarkupParentNode.prototype.removeAllChildren):
+ (MarkupParentNode.prototype.replaceChild):
+ (MarkupContentRoot): Added. Used like a shadow tree.
+ (MarkupContentRoot.prototype._markup): Added.
+ (MarkupElement): Added. A fake Element. It also implements a subset of IDL attributes implemented by
+ subclasses such as HTMLInputElement for simplicity.
+ (MarkupElement.prototype.get id): Added.
+ (MarkupElement.prototype.get localName): Added.
+ (MarkupElement.prototype.clone): Added.
+ (MarkupElement.prototype.appendChild): Added.
+ (MarkupElement.prototype.addEventListener): Added.
+ (MarkupElement.prototype.setAttribute): Added.
+ (MarkupElement.prototype.getAttribute): Added.
+ (MarkupElement.prototype.get attributes): Added.
+ (MarkupElement.prototype.get textContent): Added.
+ (MarkupElement.prototype.set textContent): Added.
+ (MarkupElement.prototype._serializeStyle): Added.
+ (MarkupElement.prototype._markup): Added. Flattens the tree with content tree like copy & paste so
+ this can't be used to implement innerHTML.
+ (MarkupElement.prototype.get value): Added.
+ (MarkupElement.prototype.set value): Added.
+ (MarkupElement.prototype.get style): Added. Returns a fake writeonly CSSStyleDeclaration.
+ (MarkupElement.prototype.set style): Added.
+ (MarkupElement.get selfClosingNames): Added. A small list of self-closing tags for the HTML generation.
+ (MarkupText): Added.
+ (MarkupText.prototype.clone): Added.
+ (MarkupText.prototype._markup): Added.
+ (MarkupText.prototype.get data): Added.
+ (MarkupText.prototype.set data): Added.
+ (MarkupComponentBase): Added.
+ (MarkupComponentBase.prototype.element): Added. Like ComponentBase's element.
+ (MarkupComponentBase.prototype.content): Added. Like ComponentBase's content.
+ (MarkupComponentBase.prototype._findElementRecursivelyById): Added. A fake getElementById.
+ (MarkupComponentBase.prototype.render): Added. Like ComponentBase's render.
+ (MarkupComponentBase.prototype.runRenderLoop): Added. In ComponentBase, we use requestAnimationFrame.
+ In MarkupComponentBase, we keep rendering until the queue drains empty.
+ (MarkupComponentBase.prototype.renderReplace): Added. Like ComponentBase's renderReplace but applies
+ the transformation of classes to workaround the lack of shadow tree support in scriptless HTML.
+ (MarkupComponentBase.prototype._applyStyleOverrides): Added. Recursively applies the transformation.
+ (MarkupComponentBase.prototype._ensureContentTree): Added. Like ComponentBase's _ensureShadowTree.
+ (MarkupComponentBase.reset): Added.
+ (MarkupComponentBase._parseTemplates): Added. Parses the content & style templates, and generates the
+ transformed fake DOM tree and stylesheet text whereby selectors in each component is modified to be
+ unique across all components. The function to apply the necessary changes to an element is saved in
+ the global map of components, and later used in renderReplace via _applyStyleOverrides.
+ (MarkupComponentBase.defineElement): Added. Like ComponentBase's defineElement.
+ (MarkupComponentBase.prototype.createEventHandler): Added.
+ (MarkupComponentBase.createEventHandler): Added.
+ (MarkupPage): Added. The top-level component responsible for generating a DOCTYPE, head, and body.
+ (MarkupPage.prototype.pageTitle): Added.
+ (MarkupPage.prototype.content): Added. Overrides the one in MarkupComponentBase to return what would
+ be the content of the body element as opposed to the html element for the connivance of subclasses,
+ and to match the behavior of the frontend Page class.
+ (MarkupPage.prototype.render): Added.
+ (MarkupPage.prototype._updateComponentsStylesheet): Added. Concatenates the transformed stylesheet of
+ all components used.
+ (MarkupPage.get contentTemplate): Added.
+ (MarkupPage.prototype.generateMarkup): Added. Enqueues the page to render, spin the render loop, and
+ generates the HTML. We enqueue the page twice in order to invoke _updateComponentsStylesheet after
+ all subcomponent had finished rendering.
+ * unit-tests/markup-component-base-tests.js: Added.
+ * unit-tests/markup-element-tests.js: Added.
+ (.createElement): Added.
+ * unit-tests/markup-page-tests.js: Added.
+
2018-05-23 Dewei Zhu <[email protected]>
OSBuildFetcher should respect maxRevision while finding OS builds to report.
Modified: trunk/Websites/perf.webkit.org/browser-tests/close-button-tests.js (232587 => 232588)
--- trunk/Websites/perf.webkit.org/browser-tests/close-button-tests.js 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/browser-tests/close-button-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -1,6 +1,6 @@
describe('CloseButton', () => {
- const scripts = ['instrumentation.js', 'components/base.js', 'components/button-base.js', 'components/close-button.js'];
+ const scripts = ['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'components/button-base.js', 'components/close-button.js'];
it('must dispatch "activate" action when the anchor is clicked', () => {
const context = new BrowsingContext();
Modified: trunk/Websites/perf.webkit.org/browser-tests/commit-log-viewer-tests.js (232587 => 232588)
--- trunk/Websites/perf.webkit.org/browser-tests/commit-log-viewer-tests.js 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/browser-tests/commit-log-viewer-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -10,10 +10,11 @@
'models/repository.js',
'models/commit-set.js',
'models/commit-log.js',
+ '../shared/common-component-base.js',
'components/base.js',
'components/spinner-icon.js',
'components/commit-log-viewer.js'];
- return context.importScripts(scripts, 'ComponentBase', 'CommitLogViewer', 'Repository', 'CommitLog', 'RemoteAPI').then(() => {
+ return context.importScripts(scripts, 'CommonComponentBase', 'ComponentBase', 'CommitLogViewer', 'Repository', 'CommitLog', 'RemoteAPI').then(() => {
return context.symbols.CommitLogViewer;
});
}
Modified: trunk/Websites/perf.webkit.org/browser-tests/component-base-tests.js (232587 => 232588)
--- trunk/Websites/perf.webkit.org/browser-tests/component-base-tests.js 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/browser-tests/component-base-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -1,10 +1,18 @@
describe('ComponentBase', function() {
+ async function importComponentBase(context)
+ {
+ const [Instrumentation, CommonComponentBase, ComponentBase] = await context.importScripts(
+ ['instrumentation.js', '../shared/common-component-base.js', 'components/base.js'],
+ 'Instrumentation', 'CommonComponentBase', 'ComponentBase');
+ return ComponentBase;
+ }
+
function createTestToCheckExistenceOfShadowTree(callback, options = {htmlTemplate: false, cssTemplate: true})
{
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
class SomeComponent extends ComponentBase { }
if (options.htmlTemplate)
SomeComponent.htmlTemplate = () => { return '<div id="div" style="height: 10px;"></div>'; };
@@ -20,7 +28,7 @@
it('must enqueue a connected component to render', () => {
const context = new BrowsingContext();
- return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
let renderCall = 0;
class SomeComponent extends ComponentBase {
render() { renderCall++; }
@@ -46,7 +54,7 @@
it('must enqueue a connected component to render upon a resize event if enqueueToRenderOnResize is true', () => {
const context = new BrowsingContext();
- return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
class SomeComponent extends ComponentBase {
static get enqueueToRenderOnResize() { return true; }
}
@@ -70,7 +78,7 @@
it('must not enqueue a disconnected component to render upon a resize event if enqueueToRenderOnResize is true', () => {
const context = new BrowsingContext();
- return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
class SomeComponent extends ComponentBase {
static get enqueueToRenderOnResize() { return true; }
}
@@ -92,13 +100,13 @@
describe('constructor', () => {
it('is a function', () => {
- return new BrowsingContext().importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(new BrowsingContext()).then((ComponentBase) => {
expect(ComponentBase).to.be.a('function');
});
});
it('can be instantiated', () => {
- return new BrowsingContext().importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(new BrowsingContext()).then((ComponentBase) => {
let callCount = 0;
class SomeComponent extends ComponentBase {
constructor() {
@@ -123,7 +131,7 @@
describe('element()', () => {
it('must return an element', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
class SomeComponent extends ComponentBase { }
let instance = new SomeComponent('some-component');
expect(instance.element()).to.be.a(context.global.HTMLElement);
@@ -131,7 +139,8 @@
});
it('must return an element whose component() matches the component', () => {
- return new BrowsingContext().importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ const context = new BrowsingContext();
+ return importComponentBase(context).then((ComponentBase) => {
class SomeComponent extends ComponentBase { }
let instance = new SomeComponent('some-component');
expect(instance.element().component()).to.be(instance);
@@ -161,7 +170,7 @@
});
it('must return the element matching the id if an id is specified', () => {
- return new BrowsingContext().importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(new BrowsingContext()).then((ComponentBase) => {
class SomeComponent extends ComponentBase {
static htmlTemplate() { return '<div id="part1" title="foo"></div><div id="part1"></div>'; }
}
@@ -174,6 +183,28 @@
expect(instance.content('part2')).to.be(null);
});
});
+
+ it('it must create DOM tree from contentTemplate', async () => {
+ const context = new BrowsingContext();
+ const ComponentBase = await importComponentBase(context);
+ class SomeComponent extends ComponentBase { };
+ SomeComponent.contentTemplate = ['div', {id: 'container'}, 'hello, world'];
+ const instance = new SomeComponent('some-component');
+ const container = instance.content('container');
+ expect(container).to.be.a(context.global.HTMLDivElement);
+ expect(container.textContent).to.be('hello, world');
+ });
+
+ it('it must create stylsheet from styleTemplate', async () => {
+ const context = new BrowsingContext();
+ const ComponentBase = await importComponentBase(context);
+ class SomeComponent extends ComponentBase { };
+ SomeComponent.contentTemplate = ['span', 'hello, world'];
+ SomeComponent.styleTemplate = {':host': {'font-weight': 'bold'}};
+ const instance = new SomeComponent('some-component');
+ context.document.body.append(instance.element());
+ expect(context.global.getComputedStyle(instance.content().firstChild).fontWeight).to.be('bold');
+ });
});
describe('part()', () => {
@@ -185,7 +216,7 @@
});
it('must return the component matching the id if an id is specified', () => {
- return new BrowsingContext().importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(new BrowsingContext()).then((ComponentBase) => {
class SomeComponent extends ComponentBase { }
ComponentBase.defineElement('some-component', SomeComponent);
@@ -206,7 +237,7 @@
describe('dispatchAction()', () => {
it('must invoke a callback specified in listenToAction', () => {
- return new BrowsingContext().importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(new BrowsingContext()).then((ComponentBase) => {
class SomeComponent extends ComponentBase { }
ComponentBase.defineElement('some-component', SomeComponent);
@@ -227,7 +258,7 @@
});
it('must not do anything when there are no callbacks', () => {
- return new BrowsingContext().importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(new BrowsingContext()).then((ComponentBase) => {
class SomeComponent extends ComponentBase { }
ComponentBase.defineElement('some-component', SomeComponent);
@@ -240,7 +271,7 @@
describe('enqueueToRender()', () => {
it('must not immediately call render()', () => {
const context = new BrowsingContext();
- return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
context.global.requestAnimationFrame = () => {}
let renderCallCount = 0;
@@ -259,7 +290,7 @@
it('must request an animation frame exactly once', () => {
const context = new BrowsingContext();
- return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
let requestAnimationFrameCount = 0;
context.global.requestAnimationFrame = () => { requestAnimationFrameCount++; }
@@ -286,7 +317,7 @@
it('must invoke render() when the callback to requestAnimationFrame is called', () => {
const context = new BrowsingContext();
- return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
let callback = null;
context.global.requestAnimationFrame = (newCallback) => {
expect(callback).to.be(null);
@@ -321,7 +352,7 @@
it('must immediately invoke render() on a component enqueued inside another render() call', () => {
const context = new BrowsingContext();
- return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
let callback = null;
context.global.requestAnimationFrame = (newCallback) => {
expect(callback).to.be(null);
@@ -366,7 +397,7 @@
it('must request a new animation frame once it exited the callback from requestAnimationFrame', () => {
const context = new BrowsingContext();
- return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
let requestAnimationFrameCount = 0;
let callback = null;
context.global.requestAnimationFrame = (newCallback) => {
@@ -449,7 +480,7 @@
it('must invoke didConstructShadowTree after creating the shadow tree', () => {
const context = new BrowsingContext();
- return context.importScripts(['instrumentation.js', 'components/base.js'], 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
let didConstructShadowTreeCount = 0;
let htmlTemplateCount = 0;
@@ -482,7 +513,7 @@
it('should create an element of the specified name', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
const div = ComponentBase.createElement('div');
expect(div).to.be.a(context.global.HTMLDivElement);
});
@@ -490,7 +521,7 @@
it('should create an element with the specified attributes', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
const input = ComponentBase.createElement('input', {'title': 'hi', 'id': 'foo', 'required': false, 'checked': true});
expect(input).to.be.a(context.global.HTMLInputElement);
expect(input.attributes.length).to.be(3);
@@ -499,13 +530,13 @@
expect(input.attributes[1].localName).to.be('id');
expect(input.attributes[1].value).to.be('foo');
expect(input.attributes[2].localName).to.be('checked');
- expect(input.attributes[2].value).to.be('checked');
+ expect(input.attributes[2].value).to.be('');
});
});
it('should create an element with the specified event handlers and attributes', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
let clickCount = 0;
const div = ComponentBase.createElement('div', {'title': 'hi', 'onclick': () => clickCount++});
expect(div).to.be.a(context.global.HTMLDivElement);
@@ -520,7 +551,7 @@
it('should create an element with the specified children when there is no attribute specified', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
const element = ComponentBase.createElement;
const span = element('span');
const div = element('div', [span, 'hi']);
@@ -535,7 +566,7 @@
it('should create an element with the specified children when the second argument is a span', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
const element = ComponentBase.createElement;
const span = element('span');
const div = element('div', span);
@@ -548,7 +579,7 @@
it('should create an element with the specified children when the second argument is a Text node', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
const element = ComponentBase.createElement;
const text = context.document.createTextNode('hi');
const div = element('div', text);
@@ -561,7 +592,7 @@
it('should create an element with the specified children when the second argument is a component', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
class SomeComponent extends ComponentBase { };
ComponentBase.defineElement('some-component', SomeComponent);
const element = ComponentBase.createElement;
@@ -576,7 +607,7 @@
it('should create an element with the specified attributes and children', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
const element = ComponentBase.createElement;
const span = element('span');
const div = element('div', {'lang': 'en'}, [span, 'hi']);
@@ -597,7 +628,7 @@
it('must define a custom element with a class of an appropriate name', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
class SomeComponent extends ComponentBase { }
ComponentBase.defineElement('some-component', SomeComponent);
@@ -609,7 +640,7 @@
it('must define a custom element that can be instantiated via document.createElement', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
let instances = [];
class SomeComponent extends ComponentBase {
constructor() {
@@ -632,7 +663,7 @@
it('must define a custom element that can be instantiated via new', () => {
const context = new BrowsingContext();
- return context.importScript('components/base.js', 'ComponentBase').then((ComponentBase) => {
+ return importComponentBase(context).then((ComponentBase) => {
let instances = [];
class SomeComponent extends ComponentBase {
constructor() {
Modified: trunk/Websites/perf.webkit.org/browser-tests/editable-text-tests.js (232587 => 232588)
--- trunk/Websites/perf.webkit.org/browser-tests/editable-text-tests.js 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/browser-tests/editable-text-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -1,6 +1,6 @@
describe('EditableText', () => {
- const scripts = ['instrumentation.js', 'components/base.js', 'components/editable-text.js'];
+ const scripts = ['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'components/editable-text.js'];
it('show the set text', () => {
const context = new BrowsingContext();
Modified: trunk/Websites/perf.webkit.org/browser-tests/index.html (232587 => 232588)
--- trunk/Websites/perf.webkit.org/browser-tests/index.html 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/browser-tests/index.html 2018-06-07 18:25:35 UTC (rev 232588)
@@ -27,6 +27,7 @@
<script src=""
<script src=""
<script src=""
+<script src=""
<script>
afterEach(() => {
@@ -224,10 +225,11 @@
'models/metric.js',
'models/commit-set.js',
'models/commit-log.js',
+ '../shared/common-component-base.js',
'components/base.js',
'components/time-series-chart.js',
'components/interactive-time-series-chart.js'],
- 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart',
+ 'CommonComponentBase', 'ComponentBase', 'TimeSeriesChart', 'InteractiveTimeSeriesChart',
'Platform', 'Metric', 'Test', 'Repository', 'MeasurementSet', 'MockRemoteAPI', 'AsyncTask').then(() => {
return context.symbols.TimeSeriesChart;
})
Added: trunk/Websites/perf.webkit.org/browser-tests/markup-page-tests.js (0 => 232588)
--- trunk/Websites/perf.webkit.org/browser-tests/markup-page-tests.js (rev 0)
+++ trunk/Websites/perf.webkit.org/browser-tests/markup-page-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -0,0 +1,274 @@
+
+describe('MarkupPage', function () {
+
+ async function importMarkupComponent(context)
+ {
+ return await context.importScripts(['lazily-evaluated-function.js', '../shared/common-component-base.js', '../../tools/js/markup-component.js'],
+ 'MarkupComponentBase', 'MarkupPage');
+ }
+
+ describe('pageContent', function () {
+ it('should define the content of the generated page', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ class SomePage extends MarkupPage { };
+ SomePage.pageContent = ['div', 'hello world'];
+ MarkupComponentBase.defineElement('some-page', SomePage);
+
+ const page = new SomePage;
+ expect(context.document.title).not.to.be('Some Page');
+ expect(page.generateMarkup()).to.contain('<div>hello world</div>');
+ });
+ });
+
+ describe('content', function () {
+ it('should return the page body', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ class SomePage extends MarkupPage {
+ render()
+ {
+ super.render();
+ this.renderReplace(this.content(), MarkupComponentBase.createElement('span'));
+ }
+ }
+ SomePage.pageContent = ['div', ['some-component']];
+ SomePage.styleTemplate = {'span': {'font-weight': 'bold'}};
+ MarkupComponentBase.defineElement('some-page', SomePage);
+
+ const page = new SomePage;
+ context.document.open();
+ context.document.write(page.generateMarkup());
+ context.document.close();
+ expect(context.document.head.querySelector('style')).to.be.a(context.global.HTMLStyleElement);
+ expect(context.document.head.querySelector('title')).to.be.a(context.global.HTMLTitleElement);
+ });
+ });
+
+ describe('generateMarkup', function () {
+ it('must enqueue itself to render and run the render loop', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+ let renderCall = 0;
+ class SomePage extends MarkupPage {
+ render() {
+ super.render();
+ renderCall++;
+ }
+ }
+ MarkupComponentBase.defineElement('some-page', SomePage);
+ const page = new SomePage;
+ page.generateMarkup();
+ expect(renderCall).to.be.greaterThan(0);
+ });
+
+ it('must generate DOCTYPE, html, head, and body elements', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ let renderCall = 0;
+ class SomePage extends MarkupPage {
+ render() {
+ super.render();
+ renderCall++;
+ }
+ }
+ MarkupComponentBase.defineElement('some-page', SomePage);
+ const page = new SomePage;
+ expect(page.generateMarkup()).to.contain('<!DOCTYPE html><html><head');
+ expect(page.generateMarkup()).to.contain('</head><body');
+ });
+
+ it('must generate the title element', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ let renderCall = 0;
+ class SomePage extends MarkupPage {
+ constructor() { super('Some Page'); }
+ render() {
+ super.render();
+ renderCall++;
+ }
+ }
+ MarkupComponentBase.defineElement('some-page', SomePage);
+ const page = new SomePage;
+ expect(context.document.title).not.to.be('Some Page');
+ context.document.open();
+ context.document.write(page.generateMarkup());
+ context.document.close();
+ expect(context.document.title).to.be('Some Page');
+ });
+
+ it('must generate the content for components in the page', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ class SomePage extends MarkupPage { }
+ SomePage.pageContent = ['div', ['some-component']];
+ MarkupComponentBase.defineElement('some-page', SomePage);
+
+ class SomeComponent extends MarkupComponentBase { };
+ SomeComponent.contentTemplate = ['div', 'hello world'];
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+
+ const page = new SomePage;
+ expect(page.generateMarkup()).to.contain('<div>hello world</div>');
+ });
+
+ it('must generate the style for components in the page', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ class SomePage extends MarkupPage { }
+ SomePage.pageContent = ['div', ['some-component']];
+ MarkupComponentBase.defineElement('some-page', SomePage);
+
+ class SomeComponent extends MarkupComponentBase {};
+ SomeComponent.contentTemplate = [['span', 'hello world'], ['p']];
+ SomeComponent.styleTemplate = {'span': {'font-weight': 'bold'}};
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+
+ const page = new SomePage;
+ context.document.open();
+ context.document.write(page.generateMarkup());
+ context.document.close();
+ expect(context.global.getComputedStyle(context.document.querySelector('p')).fontWeight).to.be('normal');
+ expect(context.global.getComputedStyle(context.document.querySelector('span')).fontWeight).to.be('bold');
+ });
+
+ it('must not apply the styles from a sibling component', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ class SomePage extends MarkupPage { }
+ SomePage.pageContent = ['div', [['some-component'], ['other-component']]];
+ MarkupComponentBase.defineElement('some-page', SomePage);
+
+ class SomeComponent extends MarkupComponentBase {};
+ SomeComponent.contentTemplate = [['section', 'hello'], ['p', 'world']];
+ SomeComponent.styleTemplate = {'section': {'font-weight': 'bold'}};
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+
+ class OtherComponent extends MarkupComponentBase {};
+ OtherComponent.contentTemplate = [['section', 'hello'], ['p', 'world']];
+ OtherComponent.styleTemplate = {'p': {'color': 'blue'}};
+ MarkupComponentBase.defineElement('other-component', OtherComponent);
+
+ const page = new SomePage;
+ context.document.open();
+ context.document.write(page.generateMarkup());
+ context.document.close();
+ const getComputedStyle = (element) => context.global.getComputedStyle(element);
+ const querySelector = (selector) => context.document.querySelector(selector);
+ expect(getComputedStyle(querySelector('some-component section')).fontWeight).to.be('bold');
+ expect(getComputedStyle(querySelector('other-component section')).fontWeight).to.be('normal');
+ expect(getComputedStyle(querySelector('some-component p')).color).to.be('rgb(0, 0, 0)');
+ expect(getComputedStyle(querySelector('other-component p')).color).to.be('rgb(0, 0, 255)');
+ });
+
+ it('must not apply the styles from a ancestor component', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ class SomePage extends MarkupPage { }
+ SomePage.pageContent = ['div', ['some-component']];
+ SomePage.styleTemplate = {'div': {'width': '300px'}};
+ MarkupComponentBase.defineElement('some-page', SomePage);
+
+ class SomeComponent extends MarkupComponentBase {};
+ SomeComponent.contentTemplate = ['div', ['other-component']];
+ SomeComponent.styleTemplate = {'div': {'width': '200px'}};
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+
+ class OtherComponent extends MarkupComponentBase {};
+ OtherComponent.contentTemplate = ['div'];
+ OtherComponent.styleTemplate = {'div': {'width': '100px'}};
+ MarkupComponentBase.defineElement('other-component', OtherComponent);
+
+ const page = new SomePage;
+ context.document.open();
+ context.document.write(page.generateMarkup());
+ context.document.close();
+ const getComputedStyle = (element) => context.global.getComputedStyle(element);
+ const divs = Array.from(context.document.querySelectorAll('div'));
+ expect(getComputedStyle(divs[0]).width).to.be('300px');
+ expect(getComputedStyle(divs[1]).width).to.be('200px');
+ expect(getComputedStyle(divs[2]).width).to.be('100px');
+ });
+
+ it('must apply the styles to elements generated in renderReplace', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ class SomePage extends MarkupPage { }
+ SomePage.pageContent = ['div', ['some-component']];
+ MarkupComponentBase.defineElement('some-page', SomePage);
+
+ class SomeComponent extends MarkupComponentBase {
+ render()
+ {
+ super.render();
+ this.renderReplace(this.content(), MarkupComponentBase.createElement('span'));
+ }
+ };
+ SomeComponent.contentTemplate = [];
+ SomeComponent.styleTemplate = {'span': {'font-weight': 'bold'}};
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+
+ const page = new SomePage;
+ context.document.open();
+ context.document.write(page.generateMarkup());
+ context.document.close();
+ expect(context.global.getComputedStyle(context.document.querySelector('span')).fontWeight).to.be('bold');
+ });
+
+ it('must apply the styles to elements based on class names', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ class SomePage extends MarkupPage { }
+ SomePage.pageContent = ['div', {class: 'target'}, ['some-component']];
+ SomePage.styleTemplate = {'.target': {'border': 'solid 1px black'}};
+ MarkupComponentBase.defineElement('some-page', SomePage);
+
+ const page = new SomePage;
+ context.document.open();
+ context.document.write(page.generateMarkup());
+ context.document.close();
+ const target = context.document.querySelector('.target');
+ expect(context.global.getComputedStyle(target).borderWidth).to.be('1px');
+ expect(target.classList.length).to.be(2);
+ });
+
+ it('must not add the same class name multiple times to an element', async () => {
+ const context = new BrowsingContext();
+ const [MarkupComponentBase, MarkupPage] = await importMarkupComponent(context);
+
+ class SomePage extends MarkupPage {
+ render()
+ {
+ super.render();
+ const container = this.createElement('div');
+ this.renderReplace(container, this.createElement('div', {class: 'target'}));
+ this.renderReplace(this.content(), container);
+ }
+ }
+ SomePage.pageContent = [];
+ SomePage.styleTemplate = {'.target': {'border': 'solid 1px black'}};
+ MarkupComponentBase.defineElement('some-page', SomePage);
+
+ const page = new SomePage;
+ context.document.open();
+ context.document.write(page.generateMarkup());
+ context.document.close();
+ const target = context.document.querySelector('.target');
+ expect(context.global.getComputedStyle(target).borderWidth).to.be('1px');
+ expect(target.classList.length).to.be(2);
+ });
+
+ });
+});
+
Modified: trunk/Websites/perf.webkit.org/browser-tests/page-router-tests.js (232587 => 232588)
--- trunk/Websites/perf.webkit.org/browser-tests/page-router-tests.js 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/browser-tests/page-router-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -4,7 +4,7 @@
it('should choose the longest match', async () => {
const context = new BrowsingContext();
const [Page, PageRouter, ComponentBase] = await context.importScripts(
- ['instrumentation.js', 'components/base.js', 'pages/page.js', 'pages/page-router.js'],
+ ['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'pages/page.js', 'pages/page-router.js'],
'Page', 'PageRouter', 'ComponentBase');
let someRenderCount = 0;
Modified: trunk/Websites/perf.webkit.org/browser-tests/page-tests.js (232587 => 232588)
--- trunk/Websites/perf.webkit.org/browser-tests/page-tests.js 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/browser-tests/page-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -4,7 +4,7 @@
describe('open', () => {
it('must replace the content of document.body', async () => {
const context = new BrowsingContext();
- const Page = await context.importScripts(['instrumentation.js', 'components/base.js', 'pages/page.js'], 'Page');
+ const Page = await context.importScripts(['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'pages/page.js'], 'Page');
class SomePage extends Page {
constructor() { super('some page'); }
@@ -25,7 +25,7 @@
it('must update the document title', async () => {
const context = new BrowsingContext();
- const Page = await context.importScripts(['instrumentation.js', 'components/base.js', 'pages/page.js'], 'Page');
+ const Page = await context.importScripts(['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'pages/page.js'], 'Page');
class SomePage extends Page {
constructor() { super('some page'); }
@@ -41,7 +41,7 @@
it('must enqueue itself to render', async () => {
const context = new BrowsingContext();
- const [Page, ComponentBase] = await context.importScripts(['instrumentation.js', 'components/base.js', 'pages/page.js'], 'Page', 'ComponentBase');
+ const [Page, ComponentBase] = await context.importScripts(['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'pages/page.js'], 'Page', 'ComponentBase');
let renderCount = 0;
class SomePage extends Page {
@@ -62,7 +62,7 @@
it('must update the current page of the router', async () => {
const context = new BrowsingContext();
const [Page, PageRouter, ComponentBase] = await context.importScripts(
- ['instrumentation.js', 'components/base.js', 'pages/page.js', 'pages/page-router.js'],
+ ['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'pages/page.js', 'pages/page-router.js'],
'Page', 'PageRouter', 'ComponentBase');
class SomePage extends Page {
@@ -83,7 +83,7 @@
it('must not enqueue itself to render if the router is set and the current page is not itself', async () => {
const context = new BrowsingContext();
const [Page, PageRouter, ComponentBase] = await context.importScripts(
- ['instrumentation.js', 'components/base.js', 'pages/page.js', 'pages/page-router.js'],
+ ['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'pages/page.js', 'pages/page-router.js'],
'Page', 'PageRouter', 'ComponentBase');
let someRenderCount = 0;
Modified: trunk/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js (232587 => 232588)
--- trunk/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -1,6 +1,6 @@
describe('TestGroupFormTests', () => {
- const scripts = ['instrumentation.js', 'components/base.js', 'components/test-group-form.js'];
+ const scripts = ['instrumentation.js', '../shared/common-component-base.js', 'components/base.js', 'components/test-group-form.js'];
function createTestGroupFormWithContext(context)
{
Added: trunk/Websites/perf.webkit.org/public/shared/common-component-base.js (0 => 232588)
--- trunk/Websites/perf.webkit.org/public/shared/common-component-base.js (rev 0)
+++ trunk/Websites/perf.webkit.org/public/shared/common-component-base.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -0,0 +1,172 @@
+
+class CommonComponentBase {
+
+ renderReplace(element, content) { CommonComponentBase.renderReplace(element, content); }
+
+ // FIXME: Deprecate these static functions.
+ static renderReplace(element, content)
+ {
+ element.textContent = '';
+ if (content)
+ ComponentBase._addContentToElement(element, content);
+ }
+
+ _recursivelyUpgradeUnknownElements(parent, findUpgrade, didConstructComponent = () => { })
+ {
+ let nextSibling;
+ for (let child of parent.childNodes) {
+ const componentClass = findUpgrade(child);
+ if (componentClass) {
+ const intance = this._upgradeUnknownElement(parent, child, componentClass);
+ didConstructComponent(intance);
+ }
+ if (child.childNodes)
+ this._recursivelyUpgradeUnknownElements(child, findUpgrade, didConstructComponent);
+ }
+ }
+
+ _upgradeUnknownElement(parent, unknownElement, componentClass)
+ {
+ const instance = new componentClass;
+ const newElement = instance.element();
+
+ for (let i = 0; i < unknownElement.attributes.length; i++) {
+ const attr = unknownElement.attributes[i];
+ newElement.setAttribute(attr.name, attr.value);
+ }
+ parent.replaceChild(newElement, unknownElement);
+
+ for (const child of Array.from(unknownElement.childNodes))
+ newElement.appendChild(child);
+
+ return instance;
+ }
+
+ static _constructStylesheetFromTemplate(styleTemplate, didCreateRule = (selector, rule) => selector)
+ {
+ let stylesheet = '';
+ for (const selector in styleTemplate) {
+ const rules = styleTemplate[selector];
+
+ let ruleText = '';
+ for (const property in rules) {
+ const value = rules[property];
+ ruleText += ` ${property}: ${value};\n`;
+ }
+
+ const modifiedSelector = didCreateRule(selector, ruleText);
+
+ stylesheet += modifiedSelector + ' {\n' + ruleText + '}\n\n';
+ }
+ return stylesheet;
+ }
+
+ static _constructNodeTreeFromTemplate(template, didCreateElement = (element) => { })
+ {
+ if (typeof(template) == 'string')
+ return [CommonComponentBase._context.createTextNode(template)];
+ console.assert(Array.isArray(template));
+ if (typeof(template[0]) == 'string') {
+ const tagName = template[0];
+ let attributes = {};
+ let content = null;
+ if (Array.isArray(template[1])) {
+ content = template[1];
+ } else {
+ attributes = template[1];
+ content = template[2];
+ }
+ const element = this.createElement(tagName, attributes);
+ didCreateElement(element);
+ const children = content && content.length ? this._constructNodeTreeFromTemplate(content, didCreateElement) : [];
+ for (const child of children)
+ element.appendChild(child);
+ return [element];
+ } else {
+ let result = [];
+ for (const item of template) {
+ if (typeof(item) == 'string')
+ result.push(CommonComponentBase._context.createTextNode(item));
+ else
+ result = result.concat(this._constructNodeTreeFromTemplate(item, didCreateElement));
+ }
+ return result;
+ }
+ }
+
+ createElement(name, attributes, content) { return CommonComponentBase.createElement(name, attributes, content); }
+
+ static createElement(name, attributes, content)
+ {
+ const element = CommonComponentBase._context.createElement(name);
+ if (!content && (Array.isArray(attributes) || CommonComponentBase._isNode(attributes)
+ || attributes instanceof CommonComponentBase._baseClass || typeof(attributes) != 'object')) {
+ content = attributes;
+ attributes = {};
+ }
+
+ if (attributes) {
+ for (const name in attributes) {
+ if (name.startsWith('on'))
+ element.addEventListener(name.substring(2), attributes[name]);
+ else if (attributes[name] === true)
+ element.setAttribute(name, '');
+ else if (attributes[name] !== false)
+ element.setAttribute(name, attributes[name].toString());
+ }
+ }
+
+ if (content)
+ CommonComponentBase._addContentToElement(element, content);
+
+ return element;
+ }
+
+ static _addContentToElement(element, content)
+ {
+ if (Array.isArray(content)) {
+ for (var nestedChild of content)
+ this._addContentToElement(element, nestedChild);
+ } else if (CommonComponentBase._isNode(content))
+ element.appendChild(content);
+ else if (content instanceof CommonComponentBase._baseClass)
+ element.appendChild(content.element());
+ else
+ element.appendChild(CommonComponentBase._context.createTextNode(content));
+ }
+
+ createLink(content, titleOrCallback, callback, isExternal)
+ {
+ return CommonComponentBase.createLink(content, titleOrCallback, callback, isExternal);
+ }
+
+ static createLink(content, titleOrCallback, callback, isExternal)
+ {
+ var title = titleOrCallback;
+ if (callback === undefined) {
+ title = content;
+ callback = titleOrCallback;
+ }
+
+ var attributes = {
+ href: '#',
+ title: title,
+ };
+
+ if (typeof(callback) === 'string')
+ attributes['href'] = callback;
+ else
+ attributes['onclick'] = CommonComponentBase._baseClass.createEventHandler(callback);
+
+ if (isExternal)
+ attributes['target'] = '_blank';
+ return CommonComponentBase.createElement('a', attributes, content);
+ }
+};
+
+CommonComponentBase._context = null;
+CommonComponentBase._isNode = null;
+CommonComponentBase._baseClass = null;
+
+if (typeof module != 'undefined')
+ module.exports.CommonComponentBase = CommonComponentBase;
Modified: trunk/Websites/perf.webkit.org/public/v3/components/base.js (232587 => 232588)
--- trunk/Websites/perf.webkit.org/public/v3/components/base.js 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/public/v3/components/base.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -1,7 +1,8 @@
-class ComponentBase {
+class ComponentBase extends CommonComponentBase {
constructor(name)
{
+ super();
this._componentName = name || ComponentBase._componentByClass.get(new.target);
const currentlyConstructed = ComponentBase._currentlyConstructedByInterface;
@@ -144,39 +145,56 @@
ComponentBase._componentsToRenderOnResize.delete(component);
}
- renderReplace(element, content) { ComponentBase.renderReplace(element, content); }
-
- static renderReplace(element, content)
- {
- element.innerHTML = '';
- if (content)
- ComponentBase._addContentToElement(element, content);
- }
-
_ensureShadowTree()
{
if (this._shadow)
return;
- const newTarget = this.__proto__.constructor;
- const htmlTemplate = newTarget['htmlTemplate'];
- const cssTemplate = newTarget['cssTemplate'];
+ const thisClass = this.__proto__.constructor;
- if (!htmlTemplate && !cssTemplate)
+ let content;
+ let stylesheet;
+ if (!thisClass._parsed) {
+ thisClass._parsed = true;
+
+ const contentTemplate = thisClass['contentTemplate'];
+ if (contentTemplate)
+ content = ComponentBase._constructNodeTreeFromTemplate(contentTemplate);
+ else if (thisClass.htmlTemplate) {
+ const templateElement = document.createElement('template');
+ templateElement.innerHTML = thisClass.htmlTemplate();
+ content = [templateElement.content];
+ }
+
+ const styleTemplate = thisClass['styleTemplate'];
+ if (styleTemplate)
+ stylesheet = ComponentBase._constructStylesheetFromTemplate(styleTemplate);
+ else if (thisClass.cssTemplate)
+ stylesheet = thisClass.cssTemplate();
+
+ thisClass._parsedContent = content;
+ thisClass._parsedStylesheet = stylesheet;
+ } else {
+ content = thisClass._parsedContent;
+ stylesheet = thisClass._parsedStylesheet;
+ }
+
+ if (!content && !stylesheet)
return;
const shadow = this._element.attachShadow({mode: 'closed'});
- if (htmlTemplate) {
- const template = document.createElement('template');
- template.innerHTML = newTarget.htmlTemplate();
- shadow.appendChild(document.importNode(template.content, true));
- this._recursivelyReplaceUnknownElementsByComponents(shadow);
+ if (content) {
+ for (const node of content)
+ shadow.appendChild(document.importNode(node, true));
+ this._recursivelyUpgradeUnknownElements(shadow, (node) => {
+ return node instanceof Element ? ComponentBase._componentByName.get(node.localName) : null;
+ });
}
- if (cssTemplate) {
+ if (stylesheet) {
const style = document.createElement('style');
- style.textContent = newTarget.cssTemplate();
+ style.textContent = stylesheet;
shadow.appendChild(style);
}
this._shadow = shadow;
@@ -185,29 +203,6 @@
didConstructShadowTree() { }
- _recursivelyReplaceUnknownElementsByComponents(parent)
- {
- let nextSibling;
- for (let child = parent.firstChild; child; child = child.nextSibling) {
- if (child instanceof HTMLElement && !child.component) {
- const elementInterface = ComponentBase._componentByName.get(child.localName);
- if (elementInterface) {
- const component = new elementInterface();
- const newChild = component.element();
-
- for (let i = 0; i < child.attributes.length; i++) {
- const attr = child.attributes[i];
- newChild.setAttribute(attr.name, attr.value);
- }
-
- parent.replaceChild(newChild, child);
- child = newChild;
- }
- }
- this._recursivelyReplaceUnknownElementsByComponents(child);
- }
- }
-
static defineElement(name, elementInterface)
{
ComponentBase._componentByName.set(name, elementInterface);
@@ -254,68 +249,6 @@
customElements.define(name, elementClass);
}
- static createElement(name, attributes, content)
- {
- var element = document.createElement(name);
- if (!content && (Array.isArray(attributes) || attributes instanceof Node
- || attributes instanceof ComponentBase || typeof(attributes) != 'object')) {
- content = attributes;
- attributes = {};
- }
-
- if (attributes) {
- for (let name in attributes) {
- if (name.startsWith('on'))
- element.addEventListener(name.substring(2), attributes[name]);
- else if (attributes[name] === true)
- element.setAttribute(name, name);
- else if (attributes[name] !== false)
- element.setAttribute(name, attributes[name]);
- }
- }
-
- if (content)
- ComponentBase._addContentToElement(element, content);
-
- return element;
- }
-
- static _addContentToElement(element, content)
- {
- if (Array.isArray(content)) {
- for (var nestedChild of content)
- this._addContentToElement(element, nestedChild);
- } else if (content instanceof Node)
- element.appendChild(content);
- else if (content instanceof ComponentBase)
- element.appendChild(content.element());
- else
- element.appendChild(document.createTextNode(content));
- }
-
- static createLink(content, titleOrCallback, callback, isExternal)
- {
- var title = titleOrCallback;
- if (callback === undefined) {
- title = content;
- callback = titleOrCallback;
- }
-
- var attributes = {
- href: '#',
- title: title,
- };
-
- if (typeof(callback) === 'string')
- attributes['href'] = callback;
- else
- attributes['onclick'] = ComponentBase.createEventHandler(callback);
-
- if (isExternal)
- attributes['target'] = '_blank';
- return ComponentBase.createElement('a', attributes, content);
- }
-
createEventHandler(callback) { return ComponentBase.createEventHandler(callback); }
static createEventHandler(callback)
{
@@ -327,6 +260,10 @@
}
}
+CommonComponentBase._context = document;
+CommonComponentBase._isNode = (node) => node instanceof Node;
+CommonComponentBase._baseClass = ComponentBase;
+
ComponentBase.useNativeCustomElements = !!window.customElements;
ComponentBase._componentByName = new Map;
ComponentBase._componentByClass = new Map;
Modified: trunk/Websites/perf.webkit.org/public/v3/index.html (232587 => 232588)
--- trunk/Websites/perf.webkit.org/public/v3/index.html 2018-06-07 17:43:47 UTC (rev 232587)
+++ trunk/Websites/perf.webkit.org/public/v3/index.html 2018-06-07 18:25:35 UTC (rev 232588)
@@ -39,6 +39,7 @@
<template id="unbundled-scripts">
<script src=""
<script src=""
+ <script src=""
<script src=""
<script src=""
Added: trunk/Websites/perf.webkit.org/tools/js/markup-component.js (0 => 232588)
--- trunk/Websites/perf.webkit.org/tools/js/markup-component.js (rev 0)
+++ trunk/Websites/perf.webkit.org/tools/js/markup-component.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -0,0 +1,653 @@
+
+const MarkupDocument = new class MarkupDocument {
+ constructor()
+ {
+ this._nodeId = 1;
+ }
+
+ createContentRoot(host)
+ {
+ const id = this._nodeId++;
+ return new MarkupContentRoot(id, host);
+ }
+
+ createElement(name)
+ {
+ const id = this._nodeId++;
+ return new MarkupElement(id, name);
+ }
+
+ createTextNode(data)
+ {
+ const id = this._nodeId++;
+ const text = new MarkupText(id);
+ text.data = ""
+ return text;
+ }
+
+ _idForClone(original)
+ {
+ console.assert(original instanceof MarkupNode);
+ return this._nodeId++;
+ }
+
+ reset()
+ {
+ this._nodeId = 1;
+ }
+
+ markup(node)
+ {
+ console.assert(node instanceof MarkupNode);
+ return node._markup();
+ }
+
+ escapeAttributeValue(string)
+ {
+ return this.escapeNodeData(string).replace(/\"/g, '&quod8;').replace(/\'/g, ''');
+ }
+
+ escapeNodeData(string)
+ {
+ return string.replace(/&/g, '&').replace(/\</g, '<').replace(/\>/g, '>');
+ }
+}
+
+class MarkupNode {
+ constructor(id)
+ {
+ console.assert(typeof(id) == 'number');
+ this._id = id;
+ this._parentNode = null;
+ }
+
+ _markup()
+ {
+ throw 'NotImplemented';
+ }
+
+ clone()
+ {
+ throw 'NotImplemented';
+ }
+
+ _cloneNodeData(clonedNode)
+ {
+ console.assert(typeof(clonedNode._id) == 'number');
+ console.assert(this._id != clonedNode._id);
+ console.assert(clonedNode._parentNode == null);
+ }
+
+ remove()
+ {
+ const parentNode = this._parentNode;
+ if (parentNode)
+ parentNode.removeChild(this);
+ }
+}
+
+class MarkupParentNode extends MarkupNode {
+ constructor(id)
+ {
+ super(id);
+ this._childNodes = [];
+ }
+
+ get childNodes() { return this._childNodes.slice(0); }
+
+ _cloneNodeData(clonedNode)
+ {
+ super._cloneNodeData(clonedNode);
+ clonedNode._childNodes = this._childNodes.map((child) => {
+ const clonedChild = child.clone();
+ clonedChild._parentNode = clonedNode;
+ return clonedChild;
+ });
+ }
+
+ appendChild(child)
+ {
+ if (child._parentNode == this)
+ return;
+
+ if (child._parentNode)
+ child.remove();
+
+ console.assert(child._parentNode == null);
+ this._childNodes.push(child);
+ child._parentNode = this;
+ }
+
+ removeChild(child)
+ {
+ if (child._parentNode != this)
+ return;
+ const index = this._childNodes.indexOf(child);
+ console.assert(index >= 0);
+ this._childNodes.splice(index, 1);
+ child._parentNode = null;
+ }
+
+ removeAllChildren()
+ {
+ for (const child of this._childNodes)
+ child._parentNode = null;
+ this._childNodes = [];
+ }
+
+ replaceChild(newChild, oldChild)
+ {
+ if (oldChild._parentNode != this)
+ throw 'Invalid operation';
+
+ if (newChild._parentNode)
+ newChild.remove();
+ console.assert(newChild._parentNode == null);
+
+ const index = this._childNodes.indexOf(oldChild);
+ console.assert(index >= 0);
+ this._childNodes.splice(index, 1, newChild);
+ oldChild._parentNode = null;
+ newChild._parentNode = this;
+ }
+}
+
+class MarkupContentRoot extends MarkupParentNode {
+ constructor(id, host)
+ {
+ console.assert(host instanceof MarkupElement);
+ console.assert(host._contentRoot == null);
+ super(id);
+ this._hostElement = null;
+ host._contentRoot = this;
+ }
+
+ _markup()
+ {
+ let result = '';
+ for (const child of this._childNodes)
+ result += child._markup();
+ return result;
+ }
+}
+
+class MarkupElement extends MarkupParentNode {
+ constructor(id, name)
+ {
+ super(id);
+ console.assert(typeof(name) == 'string');
+ this._name = name;
+ this._attributes = new Map;
+ this._value = null;
+ this._styleProxy = null;
+ this._inlineStyleProperties = new Map;
+ this._contentRoot = null;
+ }
+
+ get id() { return this.getAttribute('id'); }
+ get localName() { return this._name; }
+
+ clone()
+ {
+ const clonedNode = new MarkupElement(MarkupDocument._idForClone(this), this._name);
+ super._cloneNodeData(clonedNode);
+ for (const [name, value] of this._attributes)
+ clonedNode._attributes.set(name, value);
+ clonedNode._value = this._value;
+ for (const [name, value] of this._inlineStyleProperties)
+ clonedNode._inlineStyleProperties.set(name, value);
+ if (this._contentRoot) {
+ const clonedContentRoot = new MarkupContentRoot(MarkupDocument._idForClone(this._contentRoot), clonedNode);
+ this._contentRoot._cloneNodeData(clonedContentRoot);
+ }
+ return clonedNode;
+ }
+
+ appendChild(child)
+ {
+ if (MarkupElement.selfClosingNames.includes(this._name))
+ throw 'The operation is not supported';
+ super.appendChild(child);
+ }
+
+ addEventListener(name, callback)
+ {
+ throw 'The operation is not supported';
+ }
+
+ setAttribute(name, value = null)
+ {
+ if (name == 'style')
+ this._inlineStyleProperties.clear();
+ this._attributes.set(name.toString(), '' + value);
+ }
+
+ getAttribute(name, value)
+ {
+ if (name == 'style' && this._inlineStyleProperties.size)
+ return this._serializeStyle();
+ return this._attributes.get(name);
+ }
+
+ get attributes()
+ {
+ // FIXME: Add the support for named property.
+ const result = [];
+ for (const [name, value] of this._attributes)
+ result.push({localName: name, name, value});
+ return result;
+ }
+
+ get textContent()
+ {
+ let result = '';
+ for (const node of this._childNodes) {
+ if (node instanceof MarkupText)
+ result += node.data;
+ }
+ return result;
+ }
+
+ set textContent(newContent)
+ {
+ this.removeAllChildren();
+ if (newContent)
+ this.appendChild(MarkupDocument.createTextNode(newContent));
+ }
+
+ _serializeStyle()
+ {
+ let styleValue = '';
+ for (const [name, value] of this._inlineStyleProperties)
+ styleValue += (styleValue ? '; ' : '') + name + ': ' + value;
+ return styleValue;
+ }
+
+ _markup()
+ {
+ let markup = '<' + this._name;
+ if (this._styleProxy && this._inlineStyleProperties.size)
+ markup += ` style="${MarkupDocument.escapeAttributeValue(this._serializeStyle())}"`;
+ for (const [name, value] of this._attributes)
+ markup += ` ${name}="${MarkupDocument.escapeAttributeValue(value)}"`;
+ markup += '>';
+ if (this._contentRoot)
+ markup += this._contentRoot._markup();
+ else {
+ for (const child of this._childNodes)
+ markup += child._markup();
+ }
+ if (!MarkupElement.selfClosingNames.includes(this._name))
+ markup += '</' + this._name + '>';
+ return markup;
+ }
+
+ get value()
+ {
+ if (this._name != 'input')
+ throw 'The operation is not supported';
+ return this._value;
+ }
+ set value(value)
+ {
+ if (this._name != 'input')
+ throw 'The operation is not supported';
+ this._value = value.toString();
+ }
+
+ get style()
+ {
+ if (!this._styleProxy) {
+ const proxyTarget = {};
+ const cssPropertyFromJSProperty = (jsPropertyName) => {
+ let cssPropertyName = '';
+ for (let i = 0; i < jsPropertyName.length; i++) {
+ const currentChar = jsPropertyName.charAt(i);
+ if ('A' <= currentChar && currentChar <= 'Z')
+ cssPropertyName += '-' + currentChar.toLowerCase();
+ else
+ cssPropertyName += currentChar;
+ }
+ return cssPropertyName;
+ };
+ this._styleProxy = new Proxy(proxyTarget, {
+ get: (target, property) => {
+ throw 'The operation is not supported';
+ },
+ set: (target, property, value) => {
+ this._inlineStyleProperties.set(cssPropertyFromJSProperty(property), value);
+ return true;
+ },
+ });
+ }
+ return this._styleProxy;
+ }
+
+ set style(value)
+ {
+ throw 'This operation is not supported';
+ }
+
+ static get selfClosingNames() { return ['img', 'br', 'meta', 'link']; }
+}
+
+class MarkupText extends MarkupNode {
+ constructor(id)
+ {
+ super(id);
+ this._data = null;
+ }
+
+ clone()
+ {
+ const clonedNode = new MarkupText(MarkupDocument._idForClone(this));
+ clonedNode._data = this._data;
+ return clonedNode;
+ }
+
+ _markup()
+ {
+ return MarkupDocument.escapeNodeData(this._data);
+ }
+
+ get data() { return this._data; }
+ set data(newData) { this._data = newData.toString(); }
+}
+
+const componentsMap = new Map;
+const componentsByClass = new Map;
+const componentsToRender = new Set;
+let currentlyRenderingComponent = null;
+class MarkupComponentBase extends CommonComponentBase {
+ constructor(name)
+ {
+ super();
+ this._name = componentsByClass.get(new.target);
+ const component = componentsMap.get(this._name);
+ console.assert(component, `Component "${this._name}" has not been defined`);
+ this._componentId = component.id;
+ this._element = null;
+ this._contentRoot = null;
+ }
+
+ element()
+ {
+ if (!this._element) {
+ this._element = MarkupDocument.createElement(this._name);
+ this._element.component = () => this;
+ }
+ return this._element;
+ }
+
+ content(id = null)
+ {
+ this._ensureContentTree();
+ if (id) {
+ // FIXME: Make this more efficient.
+ return this._contentRoot ? this._findElementRecursivelyById(this._contentRoot, id) : null;
+ }
+ return this._contentRoot;
+ }
+
+ _findElementRecursivelyById(parent, id)
+ {
+ for (const child of parent.childNodes) {
+ if (child.id == id)
+ return child;
+ if (child instanceof MarkupParentNode) {
+ const result = this._findElementRecursivelyById(child, id);
+ if (result)
+ return result;
+ }
+ }
+ return null;
+ }
+
+ render() { this._ensureContentTree(); }
+
+ enqueueToRender()
+ {
+ componentsToRender.add(this);
+ }
+
+ static runRenderLoop()
+ {
+ console.assert(!currentlyRenderingComponent);
+ do {
+ const currentSet = [...componentsToRender];
+ componentsToRender.clear();
+ for (let component of currentSet) {
+ const enqueuedAgain = componentsToRender.has(component);
+ if (enqueuedAgain)
+ continue;
+ currentlyRenderingComponent = component;
+ component.render();
+ }
+ currentlyRenderingComponent = null;
+ } while (componentsToRender.size);
+ }
+
+ renderReplace(parentNode, content)
+ {
+ console.assert(currentlyRenderingComponent == this);
+ console.assert(parentNode instanceof MarkupParentNode);
+ parentNode.removeAllChildren();
+ if (content) {
+ MarkupComponentBase._addContentToElement(parentNode, content);
+
+ const component = componentsMap.get(this._name);
+ console.assert(component);
+ this._applyStyleOverrides(parentNode, component.styleOverride);
+ }
+ }
+
+ _applyStyleOverrides(node, styleOverride)
+ {
+ if (node instanceof MarkupElement)
+ styleOverride(node);
+ if (node.childNodes) {
+ for (const child of node.childNodes)
+ this._applyStyleOverrides(child, styleOverride);
+ }
+ }
+
+ _ensureContentTree()
+ {
+ if (this._contentRoot)
+ return;
+
+ const thisClass = this.__proto__.constructor;
+ const component = componentsMap.get(this._name);
+ if (!component.parsed) {
+ component.parsed = true;
+
+ const htmlTemplate = thisClass['htmlTemplate'];
+ const cssTemplate = thisClass['cssTemplate'];
+ if (htmlTemplate || cssTemplate)
+ throw 'The operation is not supported';
+
+ const contentTemplate = thisClass['contentTemplate'];
+ const styleTemplate = thisClass['styleTemplate'];
+ if (!contentTemplate && !styleTemplate)
+ return;
+
+ const result = MarkupComponentBase._parseTemplates(this._name, this._componentId, contentTemplate, styleTemplate);
+ component.content = result.content;
+ component.stylesheet = result.stylesheet;
+ component.styleOverride = result.styleOverride;
+ }
+
+ this._contentRoot = MarkupDocument.createContentRoot(this.element());
+ if (component.content) {
+ for (const node of component.content)
+ this._contentRoot.appendChild(node.clone());
+ this._recursivelyUpgradeUnknownElements(this._contentRoot,
+ (node) => {
+ const component = node instanceof MarkupElement ? componentsMap.get(node.localName) : null;
+ return component ? component.class : null;
+ },
+ (component) => component.enqueueToRender());
+ }
+ // FIXME: Add a call to didConstructShadowTree.
+ }
+
+ static reset()
+ {
+ console.assert(!currentlyRenderingComponent);
+ MarkupDocument.reset();
+ componentsMap.clear();
+ componentsByClass.clear();
+ componentsToRender.clear();
+ }
+
+ static _parseTemplates(componentName, componentId, contentTemplate, styleTemplate)
+ {
+ const styledClasses = new Map;
+ const styledElements = new Map;
+ let stylesheet = null;
+ let selectorId = 0;
+ let content = null;
+ let styleOverride = () => { }
+ if (styleTemplate) {
+ stylesheet = this._constructStylesheetFromTemplate(styleTemplate, (selector, rule) => {
+ if (selector == ':host')
+ return componentName;
+
+ const match = selector.match(/^(\.?[a-zA-Z0-9\-]+)(\[[a-zA-Z0-9\-]+\]|\:[a-z\-]+)*$/);
+ if (!match)
+ throw 'Unsupported selector: ' + selector;
+
+ const selectorSuffix = match[2] || '';
+ let globalClassName;
+ // FIXME: Preserve the specificity of selectors.
+ selectorId++;
+ if (match[1].startsWith('.')) {
+ const className = match[1].substring(1);
+ globalClassName = `content-${componentId}-class-${className}`;
+ styledClasses.set(className, globalClassName);
+ return '.' + globalClassName + selectorSuffix;
+ }
+
+ const elementName = match[1].toLowerCase();
+ globalClassName = `content-${componentId}-element-${elementName}`;
+ styledElements.set(elementName, globalClassName);
+ return '.' + globalClassName + selectorSuffix;
+ });
+
+ if (styledClasses.size || styledElements.size) {
+ styleOverride = (element) => {
+ const classNamesToAdd = new Set;
+ const globalClassNameForName = styledElements.get(element.localName);
+ if (globalClassNameForName)
+ classNamesToAdd.add(globalClassNameForName);
+
+ const currentClass = element.getAttribute('class');
+ if (currentClass) {
+ const classList = currentClass.split(/\s+/);
+ for (const className of classList) {
+ const globalClass = styledClasses.get(className);
+ if (globalClass)
+ classNamesToAdd.add(globalClass);
+ classNamesToAdd.add(className);
+ }
+ if (classList.length == classNamesToAdd.size)
+ return;
+ } else if (!classNamesToAdd.size)
+ return;
+ element.setAttribute('class', Array.from(classNamesToAdd).join(' '));
+ }
+ }
+ }
+
+ if (contentTemplate)
+ content = MarkupComponentBase._constructNodeTreeFromTemplate(contentTemplate, styleOverride);
+
+ return {stylesheet, content, styleOverride};
+ }
+
+ static defineElement(name, componentClass)
+ {
+ console.assert(!componentsMap.get(name), `The component "${name}" has already been defined`);
+ const existingComponentForClass = componentsByClass.get(componentClass);
+ console.assert(!existingComponentForClass, existingComponentForClass
+ ? `The component class "${existingComponentForClass}" has already been used to define another component "${existingComponentForClass.name}"` : '');
+ componentsMap.set(name, {
+ class: componentClass,
+ id: componentsMap.size + 1,
+ parsed: false,
+ content: null,
+ stylesheet: null,
+ });
+ componentsByClass.set(componentClass, name);
+ }
+
+ createEventHandler(callback) { return MarkupComponentBase.createEventHandler(callback); }
+ static createEventHandler(callback)
+ {
+ throw 'The operation is not supported';
+ }
+}
+CommonComponentBase._context = MarkupDocument;
+CommonComponentBase._isNode = (node) => node instanceof MarkupNode;
+CommonComponentBase._baseClass = MarkupComponentBase;
+
+class MarkupPage extends MarkupComponentBase {
+ constructor(title)
+ {
+ super('page-component');
+ this._title = title;
+ this._updateComponentsStylesheetLazily = new LazilyEvaluatedFunction(this._updateComponentsStylesheet.bind(this));
+ }
+
+ pageTitle() { return this._title; }
+
+ content(id)
+ {
+ if (id)
+ return super.content(id);
+ return super.content('page-body');
+ }
+
+ render()
+ {
+ super.render();
+ this.content('page-title').textContent = this.pageTitle();
+ this._updateComponentsStylesheetLazily.evaluate([...componentsMap.values()].filter((component) => component.parsed && component.stylesheet));
+ }
+
+ _updateComponentsStylesheet(componentsWithStylesheets)
+ {
+ let mergedStylesheetText = '';
+ for (const component of componentsWithStylesheets)
+ mergedStylesheetText += component.stylesheet;
+ this.content('component-style-rules').textContent = mergedStylesheetText;
+ }
+
+ static get contentTemplate()
+ {
+ return ['html', [
+ ['head', [
+ ['title', {id: 'page-title'}],
+ ['style', {id: 'component-style-rules'}]
+ ]],
+ ['body', {id: 'page-body'}, this.pageContent]
+ ]];
+ }
+
+ generateMarkup()
+ {
+ this.enqueueToRender(this);
+ MarkupComponentBase.runRenderLoop();
+ this.enqueueToRender(this);
+ MarkupComponentBase.runRenderLoop();
+ return '<!DOCTYPE html>' + MarkupDocument.markup(super.content());
+ }
+
+}
+MarkupComponentBase.defineElement('page-component', MarkupPage);
+
+if (typeof module != 'undefined') {
+ module.exports.MarkupDocument = MarkupDocument;
+ module.exports.MarkupComponentBase = MarkupComponentBase;
+ module.exports.MarkupPage = MarkupPage;
+}
Added: trunk/Websites/perf.webkit.org/unit-tests/markup-component-base-tests.js (0 => 232588)
--- trunk/Websites/perf.webkit.org/unit-tests/markup-component-base-tests.js (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/markup-component-base-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -0,0 +1,514 @@
+'use strict';
+
+const assert = require('assert');
+global.LazilyEvaluatedFunction = require('../public/v3/lazily-evaluated-function.js').LazilyEvaluatedFunction;
+global.CommonComponentBase = require('../public/shared/common-component-base.js').CommonComponentBase;
+const MarkupComponentBase = require('../tools/js/markup-component.js').MarkupComponentBase;
+
+describe('MarkupComponentBase', function () {
+ beforeEach(() => {
+ MarkupComponentBase.reset();
+ });
+
+ describe('constructor', function () {
+ it('should construct a component', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ assert.ok(component instanceof SomeComponent);
+ });
+
+ it('should throw if the component had not been defined', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ assert.throws(() => new SomeComponent);
+ });
+
+ it('should throw if the component was defined with a different class (legacy named-based lookup should not be supported)', () => {
+ class SomeComponent extends MarkupComponentBase {
+ constructor() { super('some-component'); }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ class OtherComponent extends MarkupComponentBase {
+ constructor() { super('some-component'); }
+ };
+ assert.throws(() => new OtherComponent);
+ });
+ });
+
+ describe('defineElement', function () {
+ it('should throw if the component had already been defined', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ assert.throws(() => MarkupComponentBase.defineElement('some-component', SomeComponent));
+ });
+
+ it('should throw if the same class has already been used to define another component', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ assert.throws(() => MarkupComponentBase.defineElement('other-component', SomeComponent));
+ });
+ });
+
+ describe('element', function () {
+ it('should return a MarkupElement', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ const element = component.element();
+ assert.ok(element);
+ assert.equal(element.__proto__.constructor.name, 'MarkupElement');
+ });
+
+ it('should return the same element each time called', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ const element = component.element();
+ assert.equal(component.element(), element);
+ });
+
+ it('should return a different element for each instance', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component1 = new SomeComponent;
+ const component2 = new SomeComponent;
+ assert.notEqual(component1.element(), component2.element());
+ });
+ });
+
+ describe('content', function () {
+ it('should parse the content template once', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ SomeComponent.contentTemplate = ['span', {'id': 'some'}, 'original'];
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const instance1 = new SomeComponent;
+ assert.equal(instance1.content('some').textContent, 'original');
+ SomeComponent.contentTemplate = ['span', {'id': 'some'}, 'modified'];
+ const instance2 = new SomeComponent;
+ assert.equal(instance2.content('some').textContent, 'original');
+ });
+
+ it('should upgrade components in the content tree', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ SomeComponent.contentTemplate = ['span', ['other-component', {'id': 'other'}, 'hello']];
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ class OtherComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('other-component', OtherComponent);
+ const someComponent = new SomeComponent;
+ const otherComponent = someComponent.content('other');
+ assert.equal(otherComponent.localName, 'other-component');
+ assert.equal(otherComponent.textContent, 'hello');
+ assert.ok(otherComponent.component() instanceof OtherComponent);
+ });
+
+ it('should upgrade components in the content tree in each instance', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ SomeComponent.contentTemplate = ['span', ['other-component', {'id': 'other'}, 'hello']];
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ let constructorCount = 0;
+ class OtherComponent extends MarkupComponentBase {
+ constructor(...args)
+ {
+ super(...args);
+ constructorCount++;
+ }
+ };
+ MarkupComponentBase.defineElement('other-component', OtherComponent);
+ assert.equal(constructorCount, 0);
+ const someComponent1 = new SomeComponent;
+ assert.ok(someComponent1.content('other').component() instanceof OtherComponent);
+ assert.equal(constructorCount, 1);
+ assert.ok(someComponent1.content('other').component() instanceof OtherComponent);
+ const someComponent2 = new SomeComponent;
+ assert.equal(constructorCount, 1);
+ assert.ok(someComponent2.content('other').component() instanceof OtherComponent);
+ assert.equal(constructorCount, 2);
+ });
+
+ it('should throw when the style template contains an unsupported selector', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ SomeComponent.styleTemplate = {'div.target': {'font-weight': 'bold'}};
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ assert.throws(() => component.content());
+ });
+
+ describe('without arguments', function () {
+ it('should return null when there are no templates', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ assert.equal(component.content(), null);
+ });
+
+ it('should return a MarkupContentRoot when there is a content template', () => {
+ class SomeComponent extends MarkupComponentBase {
+ static get contentTemplate() { return []; }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ const contentRoot = component.content();
+ assert.ok(contentRoot);
+ assert.equal(contentRoot.__proto__.constructor.name, 'MarkupContentRoot');
+ assert.deepEqual(contentRoot.childNodes, []);
+ });
+ });
+
+ describe('with an ID', () => {
+ it('should return null when there are no templates', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ assert.equal(component.content('some'), null);
+ });
+
+ it('should return null when there is a content template but no matching element', () => {
+ class SomeComponent extends MarkupComponentBase {
+ static get contentTemplate() { return ['span', {'id': 'other'}, 'hello world']; }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ const contentRoot = component.content();
+ assert.ok(contentRoot);
+ assert.equal(component.content('some'), null);
+ });
+
+ it('should return the matching element when there is one', () => {
+ class SomeComponent extends MarkupComponentBase {
+ static get contentTemplate() { return ['span', {'id': 'some'}, 'hello world']; }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ const contentRoot = component.content();
+ assert.ok(contentRoot);
+ const element = component.content('some');
+ assert.ok(element);
+ assert.equal(element.__proto__.constructor.name, 'MarkupElement');
+ assert.equal(element.id, 'some');
+ assert.equal(element.localName, 'span');
+ assert.equal(element.textContent, 'hello world');
+ });
+
+ it('should return the first matching element in the tree order', () => {
+ class SomeComponent extends MarkupComponentBase {
+ static get contentTemplate() { return [
+ ['div', ['b', {'id': 'some'}, 'hello']],
+ ['span', {'id': 'some'}, 'world'],
+ ]; }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ const contentRoot = component.content();
+ assert.ok(contentRoot);
+ const element = component.content('some');
+ assert.ok(element);
+ assert.equal(element.__proto__.constructor.name, 'MarkupElement');
+ assert.equal(element.id, 'some');
+ assert.equal(element.localName, 'b');
+ assert.equal(element.textContent, 'hello');
+ });
+ });
+ });
+
+ describe('enqueueRender', function () {
+ it('should enqueue the component to render', () => {
+ let renderCalls = 0;
+ class SomeComponent extends MarkupComponentBase {
+ render() { renderCalls++; }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ component.enqueueToRender();
+ assert.equal(renderCalls, 0);
+ MarkupComponentBase.runRenderLoop();
+ assert.equal(renderCalls, 1);
+ });
+
+ it('should not enqueue the same component multiple times', () => {
+ let renderCalls = 0;
+ class SomeComponent extends MarkupComponentBase {
+ render() { renderCalls++; }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component = new SomeComponent;
+ component.enqueueToRender();
+ component.enqueueToRender();
+ assert.equal(renderCalls, 0);
+ MarkupComponentBase.runRenderLoop();
+ assert.equal(renderCalls, 1);
+ });
+ });
+
+ describe('runRenderLoop', function () {
+ it('should invoke render() on enqueued components in the oreder', () => {
+ let renderCalls = [];
+ class SomeComponent extends MarkupComponentBase {
+ render() { renderCalls.push(this); }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const component1 = new SomeComponent;
+ const component2 = new SomeComponent;
+ component1.enqueueToRender();
+ component2.enqueueToRender();
+ assert.deepEqual(renderCalls, []);
+ MarkupComponentBase.runRenderLoop();
+ assert.deepEqual(renderCalls, [component1, component2]);
+ });
+
+ it('should process cascading calls to enqueueRender()', () => {
+ let renderCalls = [];
+ class SomeComponent extends MarkupComponentBase {
+ render() {
+ renderCalls.push(this);
+ if (this == instance1)
+ instance2.enqueueToRender();
+ }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const instance1 = new SomeComponent;
+ const instance2 = new SomeComponent;
+ instance1.enqueueToRender();
+ assert.deepEqual(renderCalls, []);
+ MarkupComponentBase.runRenderLoop();
+ assert.deepEqual(renderCalls, [instance1, instance2]);
+ });
+
+ it('should delay render() call upon a cascading enqueuing', () => {
+ let renderCalls = [];
+ class SomeComponent extends MarkupComponentBase {
+ render() {
+ renderCalls.push(this);
+ if (this == instance1)
+ instance2.enqueueToRender();
+ }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const instance1 = new SomeComponent;
+ const instance2 = new SomeComponent;
+ instance1.enqueueToRender();
+ instance2.enqueueToRender();
+ assert.deepEqual(renderCalls, []);
+ MarkupComponentBase.runRenderLoop();
+ assert.deepEqual(renderCalls, [instance1, instance2]);
+ });
+
+ it('should call render() again when a cascading enqueueing occurs after the initial call', () => {
+ let renderCalls = [];
+ class SomeComponent extends MarkupComponentBase {
+ render() {
+ renderCalls.push(this);
+ if (this == instance1)
+ instance2.enqueueToRender();
+ }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const instance1 = new SomeComponent;
+ const instance2 = new SomeComponent;
+ instance2.enqueueToRender();
+ instance1.enqueueToRender();
+ assert.deepEqual(renderCalls, []);
+ MarkupComponentBase.runRenderLoop();
+ assert.deepEqual(renderCalls, [instance1, instance2, instance1]);
+ });
+ });
+
+ describe('renderReplace', function () {
+ it('should remove old children', () => {
+ class SomeComponent extends MarkupComponentBase {
+ render() {
+ const element = MarkupComponentBase.createElement;
+ this.renderReplace(this.content(), element('b', 'world'));
+ }
+
+ static get contentTemplate() {
+ return ['span', 'hello'];
+ }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const instance = new SomeComponent;
+
+ let content = instance.content();
+ assert.equal(content.childNodes.length, 1);
+ assert.equal(content.childNodes[0].localName, 'span');
+ assert.equal(content.childNodes[0].textContent, 'hello');
+
+ instance.enqueueToRender();
+ MarkupComponentBase.runRenderLoop();
+
+ content = instance.content();
+ assert.equal(content.childNodes.length, 1);
+ assert.equal(content.childNodes[0].localName, 'b');
+ assert.equal(content.childNodes[0].textContent, 'world');
+ });
+
+ it('should insert the element of a component in the content tree', () => {
+ class SomeComponent extends MarkupComponentBase {
+ render() {
+ this.renderReplace(this.content(), new OtherComponent);
+ }
+ static get contentTemplate() {
+ return [];
+ }
+ };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+
+ class OtherComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('other-component', OtherComponent);
+
+ const someComponent = new SomeComponent;
+ const content = someComponent.content();
+ assert.equal(content.childNodes.length, 0);
+
+ someComponent.enqueueToRender();
+ MarkupComponentBase.runRenderLoop();
+
+ assert.equal(content.childNodes.length, 1);
+ assert.equal(content.childNodes[0].localName, 'other-component');
+ assert.equal(content.childNodes[0].textContent, '');
+ const otherComponent = content.childNodes[0].component();
+ assert.ok(otherComponent instanceof OtherComponent);
+ });
+
+ it('should add classes to the generated elements if there are matching styles', () => {
+ class SomeComponent extends MarkupComponentBase {
+ render() {
+ this.renderReplace(this.content(), [
+ this.createElement('div'),
+ this.createElement('section', {class: 'target'}),
+ ]);
+ }
+ };
+ SomeComponent.styleTemplate = {
+ 'div': {'font-weight': 'bold'},
+ '.target': {'border': 'solid 1px blue'},
+ }
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const instance = new SomeComponent;
+ instance.enqueueToRender();
+ MarkupComponentBase.runRenderLoop();
+
+ const content = instance.content();
+ const div = content.childNodes[0];
+ const section = content.childNodes[1];
+ assert.equal(div.localName, 'div');
+ assert.ok(div.getAttribute('class'));
+ assert.equal(section.localName, 'section');
+ assert.ok(section.getAttribute('class').split(/\s+/).length, 2);
+ });
+ });
+
+ describe('createElement', function () {
+
+ it('should create an element of the specified name', () => {
+ const div = MarkupComponentBase.createElement('div');
+ assert.equal(div.localName, 'div');
+ assert.equal(div.__proto__.constructor.name, 'MarkupElement');
+ });
+
+ it('should create an element with the specified attributes', () => {
+ const input = MarkupComponentBase.createElement('input', {'title': 'hi', 'id': 'foo', 'required': false, 'checked': true});
+ assert.equal(input.localName, 'input');
+ assert.equal(input.attributes.length, 3);
+ assert.equal(input.attributes[0].localName, 'title');
+ assert.equal(input.attributes[0].value, 'hi');
+ assert.equal(input.attributes[1].localName, 'id');
+ assert.equal(input.attributes[1].value, 'foo');
+ assert.equal(input.attributes[2].localName, 'checked');
+ assert.equal(input.attributes[2].value, '');
+ });
+
+ it('should throw when an event handler is set', () => {
+ assert.throws(() => MarkupComponentBase.createElement('a', {'onclick': () => {}}));
+ });
+
+ it('should create an element with the specified children when the second argument is a span', () => {
+ const element = MarkupComponentBase.createElement;
+ const span = element('span');
+ const div = element('div', span);
+ assert.equal(div.attributes.length, 0);
+ assert.equal(div.childNodes.length, 1);
+ assert.equal(div.childNodes[0], span);
+ });
+
+ it('should create an element with the specified children when the second argument is a string', () => {
+ const element = MarkupComponentBase.createElement;
+ const div = element('div', 'hello');
+ assert.equal(div.attributes.length, 0);
+ assert.equal(div.childNodes.length, 1);
+ assert.equal(div.childNodes[0].__proto__.constructor.name, 'MarkupText');
+ assert.equal(div.childNodes[0].data, 'hello');
+ });
+
+ it('should create an element with the specified children when the second argument is a component', () => {
+ class SomeComponent extends MarkupComponentBase { };
+ MarkupComponentBase.defineElement('some-component', SomeComponent);
+ const element = MarkupComponentBase.createElement;
+ const component = new SomeComponent;
+ const div = element('div', component);
+ assert.equal(div.attributes.length, 0);
+ assert.equal(div.childNodes.length, 1);
+ assert.equal(div.childNodes[0], component.element());
+ });
+
+ it('should create an element with the specified attributes and children', () => {
+ const element = MarkupComponentBase.createElement;
+ const span = element('span');
+ const div = element('div', {'lang': 'en'}, [span, 'hi']);
+ assert.equal(div.localName, 'div');
+ assert.equal(div.attributes.length, 1);
+ assert.equal(div.attributes[0].localName, 'lang');
+ assert.equal(div.attributes[0].value, 'en');
+ assert.equal(div.childNodes.length, 2);
+ assert.equal(div.childNodes[0], span);
+ assert.equal(div.childNodes[1].data, 'hi');
+ });
+ });
+
+ describe('createLink', function () {
+ it('should create an anchor element', () => {
+ const anchor = MarkupComponentBase.createLink('hello', '#some-url');
+ assert.equal(anchor.localName, 'a');
+ assert.equal(anchor.__proto__.constructor.name, 'MarkupElement');
+ });
+
+ it('should create an anchor element with href and title when the second argument is a string and the third argument is ommitted', () => {
+ const anchor = MarkupComponentBase.createLink('hello', '#some-url');
+ assert.equal(anchor.localName, 'a');
+ assert.equal(anchor.__proto__.constructor.name, 'MarkupElement');
+ assert.equal(anchor.attributes.length, 2);
+ assert.equal(anchor.getAttribute('href'), '#some-url');
+ assert.equal(anchor.getAttribute('title'), 'hello');
+ assert.equal(anchor.textContent, 'hello');
+ });
+
+ it('should create an anchor element with href and title when the second and third arguments are string', () => {
+ const anchor = MarkupComponentBase.createLink('hello', 'some link', '#some-url');
+ assert.equal(anchor.localName, 'a');
+ assert.equal(anchor.__proto__.constructor.name, 'MarkupElement');
+ assert.equal(anchor.attributes.length, 2);
+ assert.equal(anchor.getAttribute('href'), '#some-url');
+ assert.equal(anchor.getAttribute('title'), 'some link');
+ assert.equal(anchor.textContent, 'hello');
+ });
+
+ it('should throw when the second argument is a function', () => {
+ assert.throws(() => MarkupComponentBase.createLink('hello', () => { }));
+ });
+
+ it('should throw when the third argument is a function', () => {
+ assert.throws(() => MarkupComponentBase.createLink('hello', 'some link', () => { }));
+ });
+
+ it('should create an anchor element with target=_blank when isExternal is true', () => {
+ const anchor = MarkupComponentBase.createLink('hello', 'some link', '#some-url', true);
+ assert.equal(anchor.localName, 'a');
+ assert.equal(anchor.__proto__.constructor.name, 'MarkupElement');
+ assert.equal(anchor.attributes.length, 3);
+ assert.equal(anchor.getAttribute('href'), '#some-url');
+ assert.equal(anchor.getAttribute('title'), 'some link');
+ assert.equal(anchor.getAttribute('target'), '_blank');
+ assert.equal(anchor.textContent, 'hello');
+ });
+ });
+
+});
Added: trunk/Websites/perf.webkit.org/unit-tests/markup-element-tests.js (0 => 232588)
--- trunk/Websites/perf.webkit.org/unit-tests/markup-element-tests.js (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/markup-element-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -0,0 +1,66 @@
+'use strict';
+
+const assert = require('assert');
+global.LazilyEvaluatedFunction = require('../public/v3/lazily-evaluated-function.js').LazilyEvaluatedFunction;
+global.CommonComponentBase = require('../public/shared/common-component-base.js').CommonComponentBase;
+const MarkupComponentBase = require('../tools/js/markup-component.js').MarkupComponentBase;
+
+describe('MarkupElement', function () {
+ beforeEach(() => {
+ MarkupComponentBase.reset();
+ });
+
+ function createElement(name)
+ {
+ class DummyComponent extends MarkupComponentBase { }
+ DummyComponent.contentTemplate = [name];
+ MarkupComponentBase.defineElement('dummy-component', DummyComponent);
+ const component = new DummyComponent;
+ return component.content().childNodes[0];
+ }
+
+ describe('style', function () {
+ it('should set the style content attribute', () => {
+ const div = createElement('div');
+ assert.equal(div.getAttribute('style'), null);
+ div.style.color = 'blue';
+ assert.equal(div.getAttribute('style'), 'color: blue');
+ });
+
+ it('should convert camelCased property names', () => {
+ const div = createElement('div');
+ assert.equal(div.getAttribute('style'), null);
+ div.style.fontWeight = 'bold';
+ assert.equal(div.getAttribute('style'), 'font-weight: bold');
+ });
+
+ it('should be able to serialize multiple properties', () => {
+ const div = createElement('div');
+ assert.equal(div.getAttribute('style'), null);
+ div.style.color = 'blue';
+ div.style.fontWeight = 'bold';
+ assert.equal(div.getAttribute('style'), 'color: blue; font-weight: bold');
+ });
+
+ it('should override properties after the conversion from camelCase', () => {
+ const div = createElement('div');
+ assert.equal(div.getAttribute('style'), null);
+ div.style['font-weight'] = 'bold';
+ assert.equal(div.getAttribute('style'), 'font-weight: bold');
+ div.style.fontWeight = 'normal';
+ assert.equal(div.getAttribute('style'), 'font-weight: normal');
+ });
+ });
+
+ describe('setAttribute', function () {
+ it('should override the inline style', () => {
+ const div = createElement('div');
+ assert.equal(div.getAttribute('style'), null);
+ div.style.color = 'blue';
+ assert.equal(div.getAttribute('style'), 'color: blue');
+ div.setAttribute('style', 'font-weight: bold');
+ assert.equal(div.getAttribute('style'), 'font-weight: bold');
+ });
+ });
+
+});
Added: trunk/Websites/perf.webkit.org/unit-tests/markup-page-tests.js (0 => 232588)
--- trunk/Websites/perf.webkit.org/unit-tests/markup-page-tests.js (rev 0)
+++ trunk/Websites/perf.webkit.org/unit-tests/markup-page-tests.js 2018-06-07 18:25:35 UTC (rev 232588)
@@ -0,0 +1,39 @@
+'use strict';
+
+const assert = require('assert');
+global.LazilyEvaluatedFunction = require('../public/v3/lazily-evaluated-function.js').LazilyEvaluatedFunction;
+global.CommonComponentBase = require('../public/shared/common-component-base.js').CommonComponentBase;
+const {MarkupComponentBase, MarkupPage} = require('../tools/js/markup-component.js');
+
+describe('MarkupPage', function () {
+ beforeEach(() => {
+ MarkupComponentBase.reset();
+ });
+
+ describe('generateMarkup', function () {
+ it('should render page contents', () => {
+ class SomePage extends MarkupPage { }
+ SomePage.pageContent = ['div', 'hello, world'];
+ MarkupComponentBase.defineElement('some-page', SomePage);
+ const page = new SomePage;
+ assert.ok(page instanceof SomePage);
+ const markup = page.generateMarkup();
+ assert.ok(markup.startsWith('<!DOCTYPE html><html><head'));
+ assert.ok(markup.includes('</head><body'));
+ assert.ok(markup.endsWith('</body></html>'));
+ assert.ok(markup.includes('<div>hello, world</div>'));
+ });
+
+ it('should render page contents with stylesheet when a style template is available', () => {
+ class SomePage extends MarkupPage { }
+ SomePage.pageContent = ['div', {class: 'container'}, 'hello, world'];
+ SomePage.styleTemplate = {'.container': {'font-weight': 'bold'}};
+ MarkupComponentBase.defineElement('some-page', SomePage);
+ const page = new SomePage;
+ assert.ok(page instanceof SomePage);
+ const markup = page.generateMarkup();
+ assert.ok(markup.search(/font-weight\:\s*bold;\s*}\s*<\/style>/));
+ });
+ });
+
+});