WebKit Bugzilla
Attachment 342093 Details for
Bug 186299
: Add the basic support for writing components in node.js
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Requests
|
Help
|
New Account
|
Log In
Remember
[x]
|
Forgot Password
Login:
[x]
[patch]
Fixed a bug
fix186299b.patch (text/plain), 114.66 KB, created by
Ryosuke Niwa
on 2018-06-06 16:32:22 PDT
(
hide
)
Description:
Fixed a bug
Filename:
MIME Type:
Creator:
Ryosuke Niwa
Created:
2018-06-06 16:32:22 PDT
Size:
114.66 KB
patch
obsolete
>commit 2f27b709208b9ffbfea6070b9dac4b5c7eb80619 >Author: Ryosuke Niwa <rniwa@webkit.org> >Date: Sun Jun 3 20:42:50 2018 -0700 > > Basic support for MarkupComponentBase > > Add in-browser tests for MarkupPage and fix a bug in renderReplace > > Share code in CommonComponentBase.js > > Share more code > >diff --git a/Websites/perf.webkit.org/ChangeLog b/Websites/perf.webkit.org/ChangeLog >index c9543ff08dc..be73d85f5b3 100644 >--- a/Websites/perf.webkit.org/ChangeLog >+++ b/Websites/perf.webkit.org/ChangeLog >@@ -1,3 +1,149 @@ >+2018-06-05 Ryosuke Niwa <rniwa@webkit.org> >+ >+ Add the basic support for writing components in node.js >+ https://bugs.webkit.org/show_bug.cgi?id=186299 >+ >+ Reviewed by NOBODY (OOPS!). >+ >+ 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 <dewei_zhu@apple.com> > > OSBuildFetcher should respect maxRevision while finding OS builds to report. >diff --git a/Websites/perf.webkit.org/browser-tests/close-button-tests.js b/Websites/perf.webkit.org/browser-tests/close-button-tests.js >index fda3ce90a1d..ed47cdc2386 100644 >--- a/Websites/perf.webkit.org/browser-tests/close-button-tests.js >+++ b/Websites/perf.webkit.org/browser-tests/close-button-tests.js >@@ -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(); >diff --git a/Websites/perf.webkit.org/browser-tests/commit-log-viewer-tests.js b/Websites/perf.webkit.org/browser-tests/commit-log-viewer-tests.js >index ea95688c8a2..dbc4cfcd9bc 100644 >--- a/Websites/perf.webkit.org/browser-tests/commit-log-viewer-tests.js >+++ b/Websites/perf.webkit.org/browser-tests/commit-log-viewer-tests.js >@@ -10,10 +10,11 @@ describe('CommitLogViewer', () => { > '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; > }); > } >diff --git a/Websites/perf.webkit.org/browser-tests/component-base-tests.js b/Websites/perf.webkit.org/browser-tests/component-base-tests.js >index 00c7a28105c..897c24f6504 100644 >--- a/Websites/perf.webkit.org/browser-tests/component-base-tests.js >+++ b/Websites/perf.webkit.org/browser-tests/component-base-tests.js >@@ -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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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('ComponentBase', function() { > > 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('ComponentBase', function() { > 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 @@ describe('ComponentBase', function() { > }); > > 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 @@ describe('ComponentBase', function() { > }); > > 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 @@ describe('ComponentBase', function() { > 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 @@ describe('ComponentBase', function() { > }); > > 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('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > }); > > 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('ComponentBase', function() { > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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 @@ describe('ComponentBase', function() { > > 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() { >diff --git a/Websites/perf.webkit.org/browser-tests/editable-text-tests.js b/Websites/perf.webkit.org/browser-tests/editable-text-tests.js >index cfed690d618..7f3618c4ca6 100644 >--- a/Websites/perf.webkit.org/browser-tests/editable-text-tests.js >+++ b/Websites/perf.webkit.org/browser-tests/editable-text-tests.js >@@ -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(); >diff --git a/Websites/perf.webkit.org/browser-tests/index.html b/Websites/perf.webkit.org/browser-tests/index.html >index dded1790503..d26bdce9f7a 100644 >--- a/Websites/perf.webkit.org/browser-tests/index.html >+++ b/Websites/perf.webkit.org/browser-tests/index.html >@@ -27,6 +27,7 @@ mocha.setup('bdd'); > <script src="chart-revision-range-tests.js"></script> > <script src="commit-log-viewer-tests.js"></script> > <script src="test-group-form-tests.js"></script> >+<script src="markup-page-tests.js"></script> > <script> > > afterEach(() => { >@@ -224,10 +225,11 @@ const ChartTest = { > '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; > }) >diff --git a/Websites/perf.webkit.org/browser-tests/markup-page-tests.js b/Websites/perf.webkit.org/browser-tests/markup-page-tests.js >new file mode 100644 >index 00000000000..1d045c1ad09 >--- /dev/null >+++ b/Websites/perf.webkit.org/browser-tests/markup-page-tests.js >@@ -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); >+ }); >+ >+ }); >+}); >+ >diff --git a/Websites/perf.webkit.org/browser-tests/page-router-tests.js b/Websites/perf.webkit.org/browser-tests/page-router-tests.js >index 9f42d45bc03..9367b6daeb4 100644 >--- a/Websites/perf.webkit.org/browser-tests/page-router-tests.js >+++ b/Websites/perf.webkit.org/browser-tests/page-router-tests.js >@@ -4,7 +4,7 @@ describe('PageRouter', () => { > 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; >diff --git a/Websites/perf.webkit.org/browser-tests/page-tests.js b/Websites/perf.webkit.org/browser-tests/page-tests.js >index 30916d0cdb8..e1e4f1459eb 100644 >--- a/Websites/perf.webkit.org/browser-tests/page-tests.js >+++ b/Websites/perf.webkit.org/browser-tests/page-tests.js >@@ -4,7 +4,7 @@ describe('Page', function() { > 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 @@ describe('Page', function() { > > 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 @@ describe('Page', function() { > > 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 @@ describe('Page', function() { > 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 @@ describe('Page', function() { > 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; >diff --git a/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js b/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js >index 26535672849..7e20933949b 100644 >--- a/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js >+++ b/Websites/perf.webkit.org/browser-tests/test-group-form-tests.js >@@ -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) > { >diff --git a/Websites/perf.webkit.org/public/shared/common-component-base.js b/Websites/perf.webkit.org/public/shared/common-component-base.js >new file mode 100644 >index 00000000000..aafc9082939 >--- /dev/null >+++ b/Websites/perf.webkit.org/public/shared/common-component-base.js >@@ -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; >diff --git a/Websites/perf.webkit.org/public/v3/components/base.js b/Websites/perf.webkit.org/public/v3/components/base.js >index c288fef6093..42ed8fe4c26 100644 >--- a/Websites/perf.webkit.org/public/v3/components/base.js >+++ b/Websites/perf.webkit.org/public/v3/components/base.js >@@ -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 @@ class ComponentBase { > 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; >+ >+ let content; >+ let stylesheet; >+ if (!thisClass._parsed) { >+ thisClass._parsed = true; > >- if (!htmlTemplate && !cssTemplate) >+ 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 @@ class ComponentBase { > > 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 @@ class ComponentBase { > 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 @@ class ComponentBase { > } > } > >+CommonComponentBase._context = document; >+CommonComponentBase._isNode = (node) => node instanceof Node; >+CommonComponentBase._baseClass = ComponentBase; >+ > ComponentBase.useNativeCustomElements = !!window.customElements; > ComponentBase._componentByName = new Map; > ComponentBase._componentByClass = new Map; >diff --git a/Websites/perf.webkit.org/public/v3/index.html b/Websites/perf.webkit.org/public/v3/index.html >index 9c0876a6189..e731077f6eb 100644 >--- a/Websites/perf.webkit.org/public/v3/index.html >+++ b/Websites/perf.webkit.org/public/v3/index.html >@@ -39,6 +39,7 @@ Run tools/bundle-v3-scripts to speed up the load time for production.`); > <template id="unbundled-scripts"> > <script src="../shared/statistics.js"></script> > <script src="../shared/common-remote.js"></script> >+ <script src="../shared/common-component-base.js"></script> > > <script src="instrumentation.js"></script> > <script src="remote.js"></script> >diff --git a/Websites/perf.webkit.org/tools/js/markup-component.js b/Websites/perf.webkit.org/tools/js/markup-component.js >new file mode 100644 >index 00000000000..acf049d388f >--- /dev/null >+++ b/Websites/perf.webkit.org/tools/js/markup-component.js >@@ -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 = 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; >+} >diff --git a/Websites/perf.webkit.org/unit-tests/markup-component-base-tests.js b/Websites/perf.webkit.org/unit-tests/markup-component-base-tests.js >new file mode 100644 >index 00000000000..e87b639df07 >--- /dev/null >+++ b/Websites/perf.webkit.org/unit-tests/markup-component-base-tests.js >@@ -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'); >+ }); >+ }); >+ >+}); >diff --git a/Websites/perf.webkit.org/unit-tests/markup-element-tests.js b/Websites/perf.webkit.org/unit-tests/markup-element-tests.js >new file mode 100644 >index 00000000000..459f1ba92ad >--- /dev/null >+++ b/Websites/perf.webkit.org/unit-tests/markup-element-tests.js >@@ -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'); >+ }); >+ }); >+ >+}); >diff --git a/Websites/perf.webkit.org/unit-tests/markup-page-tests.js b/Websites/perf.webkit.org/unit-tests/markup-page-tests.js >new file mode 100644 >index 00000000000..03f71e14eb5 >--- /dev/null >+++ b/Websites/perf.webkit.org/unit-tests/markup-page-tests.js >@@ -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>/)); >+ }); >+ }); >+ >+});
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Flags:
koivisto
:
review+
Actions:
View
|
Formatted Diff
|
Diff
Attachments on
bug 186299
:
342030
|
342040
| 342093