Modernize editable-text component and add tests
authorrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 26 Jan 2017 04:00:41 +0000 (04:00 +0000)
committerrniwa@webkit.org <rniwa@webkit.org@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Thu, 26 Jan 2017 04:00:41 +0000 (04:00 +0000)
https://bugs.webkit.org/show_bug.cgi?id=167398

Reviewed by Yusuke Suzuki.

Modernized EditableText component to use the action feature added in r210938.

* browser-tests/editable-text-tests.js: Added. Added tests for EditableText component.
(.waitToRender):
* browser-tests/index.html:
* public/v3/components/base.js:
(ComponentBase.prototype.dispatchAction): Return the result from the callback.
* public/v3/components/editable-text.js:
(EditableText): Removed a bunch of instance variables that are no longer needed.
(EditableText.prototype.didConstructShadowTree): Added. Add event listeners on the Edit/Save button and the host.
(EditableText.prototype.editedText): Return the text field's value directly.
(EditableText.prototype.text): Added.
(EditableText.prototype.setText): Call enqueueToRender automatically instead of relying on the parent component
to do so in _startedEditingCallback, which has been removed.
(EditableText.prototype.render): Modernized the code.
(EditableText.prototype._didClick): No longer prevents the default action manually since that's automatically done
in createEventHandler. Handle the case where the update action is not handled.
(EditableText.prototype._endEditingMode): Renamed from _didUpdate.
(EditableText.htmlTemplate): Added ids on various elements in the shadow tree.
(EditableText.cssTemplate): Updated the CSS selectors per above change.
* public/v3/main.js:
(main): Fixed a typo.
* public/v3/pages/analysis-task-page.js:
(AnalysisTaskPage): Use the action listener instead of manually setting callbacks.
(AnalysisTaskPage.prototype._createTestGroupListItem): Ditto.
(AnalysisTaskPage.prototype._didStartEditingTaskName): Deleted.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@211196 268f45cc-cd09-0410-ab3c-d52691b4dbfc

Websites/perf.webkit.org/ChangeLog
Websites/perf.webkit.org/browser-tests/editable-text-tests.js [new file with mode: 0644]
Websites/perf.webkit.org/browser-tests/index.html
Websites/perf.webkit.org/public/v3/components/base.js
Websites/perf.webkit.org/public/v3/components/editable-text.js
Websites/perf.webkit.org/public/v3/main.js
Websites/perf.webkit.org/public/v3/pages/analysis-task-page.js

index 8001ea1..da7a1ca 100644 (file)
@@ -1,3 +1,37 @@
+2017-01-24  Ryosuke Niwa  <rniwa@webkit.org>
+
+        Modernize editable-text component and add tests
+        https://bugs.webkit.org/show_bug.cgi?id=167398
+
+        Reviewed by Yusuke Suzuki.
+
+        Modernized EditableText component to use the action feature added in r210938.
+
+        * browser-tests/editable-text-tests.js: Added. Added tests for EditableText component.
+        (.waitToRender):
+        * browser-tests/index.html:
+        * public/v3/components/base.js:
+        (ComponentBase.prototype.dispatchAction): Return the result from the callback.
+        * public/v3/components/editable-text.js:
+        (EditableText): Removed a bunch of instance variables that are no longer needed.
+        (EditableText.prototype.didConstructShadowTree): Added. Add event listeners on the Edit/Save button and the host.
+        (EditableText.prototype.editedText): Return the text field's value directly.
+        (EditableText.prototype.text): Added.
+        (EditableText.prototype.setText): Call enqueueToRender automatically instead of relying on the parent component
+        to do so in _startedEditingCallback, which has been removed.
+        (EditableText.prototype.render): Modernized the code.
+        (EditableText.prototype._didClick): No longer prevents the default action manually since that's automatically done
+        in createEventHandler. Handle the case where the update action is not handled.
+        (EditableText.prototype._endEditingMode): Renamed from _didUpdate.
+        (EditableText.htmlTemplate): Added ids on various elements in the shadow tree.
+        (EditableText.cssTemplate): Updated the CSS selectors per above change.
+        * public/v3/main.js:
+        (main): Fixed a typo.
+        * public/v3/pages/analysis-task-page.js:
+        (AnalysisTaskPage): Use the action listener instead of manually setting callbacks.
+        (AnalysisTaskPage.prototype._createTestGroupListItem): Ditto.
+        (AnalysisTaskPage.prototype._didStartEditingTaskName): Deleted.
+
 2017-01-20  Ryosuke Niwa  <rniwa@webkit.org>
 
         Build fix after r210783. Didn't mean to require custom elements API.
diff --git a/Websites/perf.webkit.org/browser-tests/editable-text-tests.js b/Websites/perf.webkit.org/browser-tests/editable-text-tests.js
new file mode 100644 (file)
index 0000000..0cdf8f8
--- /dev/null
@@ -0,0 +1,200 @@
+
+describe('EditableText', () => {
+    const scripts = ['instrumentation.js', 'components/base.js', 'components/editable-text.js'];
+
+    function waitToRender(context)
+    {
+        if (!context._dummyComponent) {
+            const ComponentBase = context.symbols.ComponentBase;
+            context._dummyComponent = class SomeComponent extends ComponentBase {
+                constructor(resolve)
+                {
+                    super();
+                    this._resolve = resolve;
+                }
+                render() { setTimeout(this._resolve, 0); }
+            }
+            ComponentBase.defineElement('dummy-component', context._dummyComponent);
+        }
+        return new Promise((resolve) => {
+            const instance = new context._dummyComponent(resolve);
+            context.document.body.appendChild(instance.element());
+            instance.enqueueToRender();
+        });
+    }
+
+    it('show the set text', () => {
+        const context = new BrowsingContext();
+        let editableText;
+        return context.importScripts(scripts, 'EditableText', 'ComponentBase').then((symbols) => {
+            const [EditableText] = symbols;
+            editableText = new EditableText;
+            context.document.body.appendChild(editableText.element());
+            editableText.enqueueToRender();
+            return waitToRender(context);
+        }).then(() => {
+            expect(editableText.content().textContent).toNotInclude('hello');
+            editableText.setText('hello');
+            editableText.enqueueToRender();
+            return waitToRender(context);
+        }).then(() => {
+            expect(editableText.content().textContent).toInclude('hello');
+        });
+    });
+
+    it('go to the editing mode', () => {
+        const context = new BrowsingContext();
+        let editableText;
+        return context.importScripts(scripts, 'EditableText', 'ComponentBase').then((symbols) => {
+            const [EditableText] = symbols;
+            editableText = new EditableText;
+            context.document.body.appendChild(editableText.element());
+            editableText.setText('hello');
+            editableText.enqueueToRender();
+            return waitToRender(context);
+        }).then(() => {
+            expect(editableText.content().querySelector('input').offsetHeight).toBe(0);
+            expect(editableText.content().textContent).toInclude('hello');
+            expect(editableText.content().querySelector('a').textContent).toInclude('Edit');
+            expect(editableText.content().querySelector('a').textContent).toNotInclude('Save');
+            editableText.content().querySelector('a').click();
+            return waitToRender(context);
+        }).then(() => {
+            expect(editableText.content().querySelector('input').offsetHeight).toNotBe(0);
+            expect(editableText.content().querySelector('a').textContent).toNotInclude('Edit');
+            expect(editableText.content().querySelector('a').textContent).toInclude('Save');
+        });
+    });
+
+    it('should dispatch "update" action', () => {
+        const context = new BrowsingContext();
+        let editableText;
+        let updateCount = 0;
+        return context.importScripts(scripts, 'EditableText', 'ComponentBase').then((symbols) => {
+            const [EditableText] = symbols;
+            editableText = new EditableText;
+            context.document.body.appendChild(editableText.element());
+            editableText.setText('hello');
+            editableText.enqueueToRender();
+            editableText.listenToAction('update', () => updateCount++);
+            return waitToRender(context);
+        }).then(() => {
+            editableText.content().querySelector('a').click();
+            return waitToRender(context);
+        }).then(() => {
+            const input = editableText.content().querySelector('input');
+            expect(input.offsetHeight).toNotBe(0);
+            expect(editableText.editedText()).toBe('hello');
+            input.value = 'world';
+            expect(editableText.editedText()).toBe('world');
+            expect(updateCount).toBe(0);
+            editableText.content().querySelector('a').click();
+            expect(updateCount).toBe(1);
+            expect(editableText.editedText()).toBe('world');
+            expect(editableText.text()).toBe('hello');
+        });
+    });
+
+    it('should end the editing mode when it loses the focus', () => {
+        const context = new BrowsingContext();
+        let editableText;
+        let updateCount = 0;
+        return context.importScripts(scripts, 'EditableText', 'ComponentBase').then((symbols) => {
+            const [EditableText] = symbols;
+            editableText = new EditableText;
+            context.document.body.appendChild(editableText.element());
+            editableText.setText('hello');
+            editableText.enqueueToRender();
+            editableText.listenToAction('update', () => updateCount++);
+            return waitToRender(context);
+        }).then(() => {
+            editableText.content().querySelector('a').click();
+            return waitToRender(context);
+        }).then(() => {
+            const input = editableText.content().querySelector('input');
+            expect(input.offsetHeight).toNotBe(0);
+            expect(editableText.editedText()).toBe('hello');
+            input.value = 'world';
+            expect(updateCount).toBe(0);
+
+            const focusableElement = document.createElement('div');
+            focusableElement.setAttribute('tabindex', 0);
+            document.body.appendChild(focusableElement);
+            focusableElement.focus();
+
+            return waitToRender(context);
+        }).then(() => {
+            expect(editableText.content().querySelector('input').offsetHeight).toBe(0);
+            expect(editableText.text()).toBe('hello');
+            expect(updateCount).toBe(0);
+        });
+    });
+
+    it('should not end the editing mode when the "Save" button is focused', () => {
+        const context = new BrowsingContext();
+        let editableText;
+        let updateCount = 0;
+        return context.importScripts(scripts, 'EditableText', 'ComponentBase').then((symbols) => {
+            const [EditableText] = symbols;
+            editableText = new EditableText;
+            context.document.body.appendChild(editableText.element());
+            editableText.setText('hello');
+            editableText.enqueueToRender();
+            editableText.listenToAction('update', () => updateCount++);
+            return waitToRender(context);
+        }).then(() => {
+            editableText.content().querySelector('a').click();
+            return waitToRender(context);
+        }).then(() => {
+            const input = editableText.content().querySelector('input');
+            expect(input.offsetHeight).toNotBe(0);
+            expect(editableText.editedText()).toBe('hello');
+            input.value = 'world';
+            expect(updateCount).toBe(0);
+            editableText.content().querySelector('a').focus();
+            return waitToRender(context);
+        }).then(() => {
+            expect(editableText.content().querySelector('input').offsetHeight).toNotBe(0);
+            editableText.content().querySelector('a').click();
+            expect(editableText.editedText()).toBe('world');
+            expect(updateCount).toBe(1);
+        });
+    });
+
+    it('should dipsatch "update" action when the "Save" button is clicked in Safari', () => {
+        const context = new BrowsingContext();
+        let editableText;
+        let updateCount = 0;
+        return context.importScripts(scripts, 'EditableText', 'ComponentBase').then((symbols) => {
+            const [EditableText] = symbols;
+            editableText = new EditableText;
+            context.document.body.appendChild(editableText.element());
+            editableText.setText('hello');
+            editableText.enqueueToRender();
+            editableText.listenToAction('update', () => updateCount++);
+            return waitToRender(context);
+        }).then(() => {
+            editableText.content('action-button').click();
+            return waitToRender(context);
+        }).then(() => {
+            const input = editableText.content('text-field');
+            expect(input.offsetHeight).toNotBe(0);
+            expect(editableText.editedText()).toBe('hello');
+            input.value = 'world';
+            expect(updateCount).toBe(0);
+            return waitToRender(context);
+        }).then(() => {
+            editableText.content('action-button').dispatchEvent(new MouseEvent('mousedown'));
+            return new Promise((resolve) => setTimeout(resolve, 0));
+        }).then(() => {
+            editableText.content('text-field').blur();
+            editableText.content('action-button').dispatchEvent(new MouseEvent('mouseup'));
+            return waitToRender(context);
+        }).then(() => {
+            expect(editableText.content('text-field').offsetHeight).toBe(0);
+            expect(updateCount).toBe(1);
+            expect(editableText.editedText()).toBe('world');
+        });
+    });
+
+});
index 9b3a381..c808d46 100644 (file)
@@ -15,6 +15,7 @@ mocha.setup('bdd');
 <div id="mocha"></div>
 <script src="component-base-tests.js"></script>
 <script src="close-button-tests.js"></script>
+<script src="editable-text-tests.js"></script>
 <script>
 
 afterEach(() => {
index a6931b5..9f75cd0 100644 (file)
@@ -45,7 +45,7 @@ class ComponentBase {
     {
         const callback = this._actionCallbacks.get(actionName);
         if (callback)
-            callback.apply(this, args);
+            return callback.apply(this, args);
     }
 
     listenToAction(actionName, callback)
index 6431352..0ddb87a 100644 (file)
@@ -6,70 +6,70 @@ class EditableText extends ComponentBase {
         super('editable-text');
         this._text = text;
         this._inEditingMode = false;
-        this._startedEditingCallback = null;
-        this._updateCallback = null;
         this._updatingPromise = null;
         this._actionLink = this.content().querySelector('.editable-text-action a');
-        this._actionLink.onclick = this._didClick.bind(this);
-        this._actionLink.onmousedown = this._didClick.bind(this);
-        this._textField = this.content().querySelector('.editable-text-field');
-        this._textField.onblur = this._didBlur.bind(this);
         this._label = this.content().querySelector('.editable-text-label');
     }
 
-    editedText() { return this._textField.value; }
-    setText(text) { this._text = text; }
+    didConstructShadowTree()
+    {
+        const button = this.content('action-button');
+        button.addEventListener('mousedown', this.createEventHandler(() => this._didClick()));
+        button.addEventListener('click', this.createEventHandler(() => this._didClick()));
+        this.element().addEventListener('blur',() => {
+            if (!this.content().activeElement)
+                this._endEditingMode();
+        });
+    }
 
-    setStartedEditingCallback(callback) { this._startedEditingCallback = callback; }
-    setUpdateCallback(callback) { this._updateCallback = callback; }
+    editedText() { return this.content('text-field').value; }
+    text() { return this._text; }
+    setText(text)
+    {
+        this._text = text;
+        this.enqueueToRender();
+    }
 
     render()
     {
-        this._label.textContent = this._text;
-        this._actionLink.textContent = this._inEditingMode ? (this._updatingPromise ? '...' : 'Save') : 'Edit';
-        this._actionLink.parentNode.style.display = this._text ? null : 'none';
+        const label = this.content('label');
+        label.textContent = this._text;
+        label.style.display = this._inEditingMode ? 'none' : null;
+
+        const textField = this.content('text-field');
+        textField.style.display = this._inEditingMode ? null : 'none';
+        textField.readOnly = !!this._updatingPromise;
+
+        this.content('action-button').textContent = this._inEditingMode ? (this._updatingPromise ? '...' : 'Save') : 'Edit';
+        this.content('action-button-container').style.display = this._text ? null : 'none';
 
         if (this._inEditingMode) {
-            this._textField.readOnly = !!this._updatingPromise;
-            this._textField.style.display = null;
-            this._label.style.display = 'none';
             if (!this._updatingPromise)
-                this._textField.focus();
-        } else {
-            this._textField.style.display = 'none';
-            this._label.style.display = null;
+                textField.focus();
         }
-
-        super.render();
     }
 
-    _didClick(event)
+    _didClick()
     {
-        event.preventDefault();
-        event.stopPropagation();
-
-        if (!this._updateCallback || this._updatingPromise)
+        if (this._updatingPromise)
             return;
 
-        if (this._inEditingMode)
-            this._updatingPromise = this._updateCallback().then(this._didUpdate.bind(this));
-        else {
+        if (this._inEditingMode) {
+            const result = this.dispatchAction('update');
+            if (result instanceof Promise)
+                this._updatingPromise = result.then(() => this._endEditingMode());
+            else
+                this._endEditingMode();
+        } else {
             this._inEditingMode = true;
-            this._textField.value = this._text;
-            this._textField.style.width = (this._text.length / 1.5) + 'rem';
-            if (this._startedEditingCallback)
-                this._startedEditingCallback();
+            const textField = this.content('text-field');
+            textField.value = this._text;
+            textField.style.width = (this._text.length / 1.5) + 'rem';
+            this.enqueueToRender();
         }
     }
 
-    _didBlur(event)
-    {
-        var self = this;
-        if (self._inEditingMode && !self._updatingPromise && !self.hasFocus())
-            self._didUpdate();
-    }
-
-    _didUpdate()
+    _endEditingMode()
     {
         this._inEditingMode = false;
         this._updatingPromise = null;
@@ -79,32 +79,32 @@ class EditableText extends ComponentBase {
     static htmlTemplate()
     {
         return `
-            <span class="editable-text-container">
-                <input type="text" class="editable-text-field">
-                <span class="editable-text-label"></span>
-                <span class="editable-text-action">(<a href="#">Edit</a>)</span>
+            <span id="container">
+                <input id="text-field" type="text">
+                <span id="label"></span>
+                <span id="action-button-container">(<a id="action-button" href="#">Edit</a>)</span>
             </span>`;
     }
 
     static cssTemplate()
     {
         return `
-            .editable-text-container {
+            #container {
                 position: relative;
                 padding-right: 2.5rem;
             }
-            .editable-text-field {
+            #text-field {
                 background: transparent;
                 margin: 0;
                 padding: 0;
                 color: inherit;
                 font-weight: inherit;
                 font-size: inherit;
-                width: 8rem;
                 border: none;
             }
-            .editable-text-action {
+            #action-button-container {
                 position: absolute;
+                right: 0;
                 padding-left: 0.2rem;
                 color: #999;
                 font-size: 0.8rem;
@@ -112,7 +112,7 @@ class EditableText extends ComponentBase {
                 margin-top: -0.4rem;
                 vertical-align: middle;
             }
-            .editable-text-action a {
+            #action-button {
                 color: inherit;
                 text-decoration: none;
             }
index 954bb8a..4075c9c 100644 (file)
@@ -7,12 +7,12 @@ class SpinningPage extends Page {
 }
 
 function main() {
-    const requriedFeatures = {
+    const requiredFeatures = {
         'Shadow DOM API': () => { return !!Element.prototype.attachShadow; },
     };
 
-    for (let name in requriedFeatures) {
-        if (!requriedFeatures[name]())
+    for (let name in requiredFeatures) {
+        if (!requiredFeatures[name]())
             return alert(`Your browser does not support ${name}. Try using the latest Safari or Chrome.`);
     }
 
index a186f7b..a76b7f1 100644 (file)
@@ -62,9 +62,9 @@ class AnalysisTaskPage extends PageWithHeading {
         this._analysisResultsViewer.setRangeSelectorLabels(['A', 'B']);
         this._analysisResultsViewer.setRangeSelectorCallback(this._selectedRowInAnalysisResultsViewer.bind(this));
         this._testGroupResultsTable = this.content().querySelector('test-group-results-table').component();
+
         this._taskNameLabel = this.content().querySelector('.analysis-task-name editable-text').component();
-        this._taskNameLabel.setStartedEditingCallback(this._didStartEditingTaskName.bind(this));
-        this._taskNameLabel.setUpdateCallback(this._updateTaskName.bind(this));
+        this._taskNameLabel.listenToAction('update', () => this._updateTaskName());
 
         this.content().querySelector('.change-type-form').onsubmit = this._updateChangeType.bind(this);
         this._taskStatusControl = this.content().querySelector('.change-type-form select');
@@ -364,8 +364,7 @@ class AnalysisTaskPage extends PageWithHeading {
     _createTestGroupListItem(group)
     {
         var text = new EditableText(group.label());
-        text.setStartedEditingCallback(() => { return text.enqueueToRender(); });
-        text.setUpdateCallback(this._updateTestGroupName.bind(this, group));
+        text.listenToAction('update', () => this._updateTestGroupName(group));
 
         this._testGroupLabelMap.set(group, text);
         return ComponentBase.createElement('li', {class: 'test-group-list-' + group.id()},
@@ -418,11 +417,6 @@ class AnalysisTaskPage extends PageWithHeading {
         this.enqueueToRender();
     }
 
-    _didStartEditingTaskName()
-    {
-        this._taskNameLabel.enqueueToRender();
-    }
-
     _updateTaskName()
     {
         console.assert(this._task);