From b61bbd7b34def59994fa1ff0bfaa411f22a0d5f5 Mon Sep 17 00:00:00 2001 From: William Chou Date: Sat, 30 Jun 2018 17:31:03 -0400 Subject: [PATCH 1/5] Make sure amp-list renders via amp-bind happen in mutate context. --- extensions/amp-list/0.1/amp-list.js | 66 ++++++++++++++++------------- src/custom-element.js | 10 ++--- 2 files changed, 40 insertions(+), 36 deletions(-) diff --git a/extensions/amp-list/0.1/amp-list.js b/extensions/amp-list/0.1/amp-list.js index de35c2233177..e5b6221a2ad7 100644 --- a/extensions/amp-list/0.1/amp-list.js +++ b/extensions/amp-list/0.1/amp-list.js @@ -60,7 +60,7 @@ export class AmpList extends AMP.BaseElement { /** * Latest fetched items to render and the promise resolver and rejecter * to be invoked on render success or fail, respectively. - * @private {?{items:!Array, resolver:!Function, rejecter:!Function}} + * @private {?{items:!Array, resolver:!Function, rejecter:!Function, mutate:boolean}} */ this.renderItems_ = null; @@ -125,7 +125,6 @@ export class AmpList extends AMP.BaseElement { /** @override */ layoutCallback() { this.layoutCompleted_ = true; - return this.fetchList_(); } @@ -133,7 +132,6 @@ export class AmpList extends AMP.BaseElement { mutatedAttributesCallback(mutations) { const src = mutations['src']; const state = mutations['state']; - if (src !== undefined) { const typeOfSrc = typeof src; if (typeOfSrc === 'string') { @@ -143,7 +141,7 @@ export class AmpList extends AMP.BaseElement { } } else if (typeOfSrc === 'object') { const items = isArray(src) ? src : [src]; - this.scheduleRender_(items); + this.scheduleRender_(items, /* mutate */ true); // Remove the 'src' now that local data is used to render the list. this.element.setAttribute('src', ''); } else { @@ -151,7 +149,7 @@ export class AmpList extends AMP.BaseElement { } } else if (state !== undefined) { const items = isArray(state) ? state : [state]; - this.scheduleRender_(items); + this.scheduleRender_(items, /* mutate */ true); user().error(TAG, '[state] is deprecated, please use [src] instead.'); } } @@ -246,10 +244,11 @@ export class AmpList extends AMP.BaseElement { /** * Schedules a fetch result to be rendered in the near future. * @param {!Array} items + * @param {boolean} mutate If true, performs DOM changes in a mutate context. * @return {!Promise} * @private */ - scheduleRender_(items) { + scheduleRender_(items, mutate = false) { const deferred = new Deferred(); const {promise, resolve: resolver, reject: rejecter} = deferred; @@ -257,7 +256,7 @@ export class AmpList extends AMP.BaseElement { if (!this.renderItems_) { this.renderPass_.schedule(); } - this.renderItems_ = {items, resolver, rejecter}; + this.renderItems_ = {items, resolver, rejecter, mutate}; return promise; } @@ -277,15 +276,16 @@ export class AmpList extends AMP.BaseElement { this.renderItems_ = null; } }; - this.templates_.findAndRenderTemplateArray(this.element, current.items) + const {items, resolver, rejecter, mutate} = current; + this.templates_.findAndRenderTemplateArray(this.element, items) .then(elements => this.updateBindingsForElements_(elements)) - .then(elements => this.rendered_(elements)) + .then(elements => this.render_(elements, mutate)) .then(/* onFulfilled */ () => { scheduleNextPass(); - current.resolver(); + resolver(); }, /* onRejected */ () => { scheduleNextPass(); - current.rejecter(); + rejecter(); }); } @@ -331,29 +331,35 @@ export class AmpList extends AMP.BaseElement { /** * @param {!Array} elements + * @param {boolean} mutate If true, performs DOM changes in a mutate context. * @private */ - rendered_(elements) { - removeChildren(dev().assertElement(this.container_)); - elements.forEach(element => { - if (!element.hasAttribute('role')) { - element.setAttribute('role', 'listitem'); - } - this.container_.appendChild(element); + render_(elements, mutate) { + const render = () => { + removeChildren(dev().assertElement(this.container_)); + elements.forEach(element => { + if (!element.hasAttribute('role')) { + element.setAttribute('role', 'listitem'); + } + this.container_.appendChild(element); + }); + const event = createCustomEvent(this.win, + AmpEvents.DOM_UPDATE, /* detail */ null, {bubbles: true}); + this.container_.dispatchEvent(event); + // Change height if needed. + this.getVsync().measure(() => { + const scrollHeight = this.container_./*OK*/scrollHeight; + const height = this.element./*OK*/offsetHeight; + if (scrollHeight > height) { + this.attemptChangeHeight(scrollHeight).catch(() => {}); + } }); - const event = createCustomEvent(this.win, - AmpEvents.DOM_UPDATE, /* detail */ null, {bubbles: true}); - this.container_.dispatchEvent(event); - - // Change height if needed. - this.getVsync().measure(() => { - const scrollHeight = this.container_./*OK*/scrollHeight; - const height = this.element./*OK*/offsetHeight; - if (scrollHeight > height) { - this.attemptChangeHeight(scrollHeight).catch(() => {}); - } - }); + if (mutate) { + this.mutateElement(this.container_, render); + } else { + render(); + } } /** diff --git a/src/custom-element.js b/src/custom-element.js index 08c7de60177d..aa5c674ef5d2 100644 --- a/src/custom-element.js +++ b/src/custom-element.js @@ -1294,12 +1294,10 @@ function createBaseCustomElementClass(win) { /** * Called when one or more attributes are mutated. - * Note Must be called inside a mutate context. - * Note Boolean attributes have a value of `true` and `false` when - * present and missing, respectively. - * @param { - * !JsonObject - * } mutations + * Note: Must be called inside a mutate context. + * Note: Boolean attributes have a value of `true` and `false` when + * present and missing, respectively. + * @param {!JsonObject} mutations */ mutatedAttributesCallback(mutations) { this.implementation_.mutatedAttributesCallback(mutations); From cbd2b7a5f4e0cd81fb5284b60facb0567a3b6d97 Mon Sep 17 00:00:00 2001 From: William Chou Date: Mon, 2 Jul 2018 14:26:12 -0400 Subject: [PATCH 2/5] Add unit test. --- extensions/amp-list/0.1/amp-list.js | 21 +++++++++++-------- extensions/amp-list/0.1/test/test-amp-list.js | 21 +++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/extensions/amp-list/0.1/amp-list.js b/extensions/amp-list/0.1/amp-list.js index e5b6221a2ad7..98365a26053d 100644 --- a/extensions/amp-list/0.1/amp-list.js +++ b/extensions/amp-list/0.1/amp-list.js @@ -137,21 +137,22 @@ export class AmpList extends AMP.BaseElement { if (typeOfSrc === 'string') { // Defer to fetch in layoutCallback() before first layout. if (this.layoutCompleted_) { - this.fetchList_(); + return this.fetchList_(/* mutate */ true); } } else if (typeOfSrc === 'object') { - const items = isArray(src) ? src : [src]; - this.scheduleRender_(items, /* mutate */ true); // Remove the 'src' now that local data is used to render the list. this.element.setAttribute('src', ''); + const items = isArray(src) ? src : [src]; + return this.scheduleRender_(items, /* mutate */ true); } else { this.user().error(TAG, 'Unexpected "src" type: ' + src); } } else if (state !== undefined) { - const items = isArray(state) ? state : [state]; - this.scheduleRender_(items, /* mutate */ true); user().error(TAG, '[state] is deprecated, please use [src] instead.'); + const items = isArray(state) ? state : [state]; + return this.scheduleRender_(items, /* mutate */ true); } + return Promise.resolve(); } /** @@ -188,10 +189,11 @@ export class AmpList extends AMP.BaseElement { /** * Request list data from `src` and return a promise that resolves when * the list has been populated with rendered list items. + * @param {boolean} mutate If true, performs DOM changes in a mutate context. * @return {!Promise} * @private */ - fetchList_() { + fetchList_(mutate = false) { if (!this.element.getAttribute('src')) { return Promise.resolve(); } @@ -220,7 +222,7 @@ export class AmpList extends AMP.BaseElement { if (maxLen < items.length) { items = items.slice(0, maxLen); } - return this.scheduleRender_(items); + return this.scheduleRender_(items, mutate); }, error => { throw user().createError('Error fetching amp-list', error); }).then(() => { @@ -353,10 +355,11 @@ export class AmpList extends AMP.BaseElement { if (scrollHeight > height) { this.attemptChangeHeight(scrollHeight).catch(() => {}); } - }); + }); + }; if (mutate) { - this.mutateElement(this.container_, render); + this.mutateElement(render, this.container_); } else { render(); } diff --git a/extensions/amp-list/0.1/test/test-amp-list.js b/extensions/amp-list/0.1/test/test-amp-list.js index d58469c039b8..f849c03420b7 100644 --- a/extensions/amp-list/0.1/test/test-amp-list.js +++ b/extensions/amp-list/0.1/test/test-amp-list.js @@ -14,6 +14,8 @@ * limitations under the License. */ +import * as sinon from 'sinon'; + import {AmpEvents} from '../../../../src/amp-events'; import {AmpList} from '../amp-list'; import {Deferred} from '../../../../src/utils/promise'; @@ -419,6 +421,25 @@ describes.realWin('amp-list component', { }); }); + it('should render in a mutate context', function*() { + listMock.expects('mutateElement') + .withArgs(sinon.match.func, list.container_) + .once(); + + const items = [{title: 'foo'}]; + const foo = doc.createElement('div'); + expectFetchAndRender(items, [foo]); + + // Pretend that layoutCallback() was called already since amp-list will + // normally refuse to refetch until it happens. + list.layoutCompleted_ = true; + + element.setAttribute('src', 'https://new.com/list.json'); + yield list.mutatedAttributesCallback({ + 'src': 'https://new.com/list.json', + }); + }); + describe('`fast-amp-list` experiment', () => { beforeEach(() => { toggleExperiment(win, 'fast-amp-list', true, true); From 6f916a9e8b9ff7053f3adc2b77a24ae3015d41dd Mon Sep 17 00:00:00 2001 From: William Chou Date: Mon, 2 Jul 2018 14:31:04 -0400 Subject: [PATCH 3/5] Remove unnecessary promise chain in tests. --- extensions/amp-list/0.1/test/test-amp-list.js | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/extensions/amp-list/0.1/test/test-amp-list.js b/extensions/amp-list/0.1/test/test-amp-list.js index f849c03420b7..607f1592c3a4 100644 --- a/extensions/amp-list/0.1/test/test-amp-list.js +++ b/extensions/amp-list/0.1/test/test-amp-list.js @@ -79,12 +79,13 @@ describes.realWin('amp-list component', { */ function expectFetchAndRender(fetched, rendered, opts = DEFAULT_LIST_OPTS) { const fetch = Promise.resolve(fetched); + listMock.expects('fetch_') + .withExactArgs(opts.expr).returns(fetch).atLeast(1); + if (opts.resetOnRefresh) { listMock.expects('togglePlaceholder').withExactArgs(true).once(); listMock.expects('toggleLoading').withExactArgs(true, true).once(); } - listMock.expects('fetch_') - .withExactArgs(opts.expr).returns(fetch).atLeast(1); listMock.expects('toggleLoading').withExactArgs(false).once(); listMock.expects('togglePlaceholder').withExactArgs(false).once(); @@ -98,8 +99,6 @@ describes.realWin('amp-list component', { const render = Promise.resolve(rendered); templatesMock.expects('findAndRenderTemplateArray') .withExactArgs(element, itemsToRender).returns(render).atLeast(1); - - return Promise.all([fetch, render]); } describe('without amp-bind', () => { @@ -112,8 +111,8 @@ describes.realWin('amp-list component', { {title: 'Title1'}, ]; const itemElement = doc.createElement('div'); - const rendered = expectFetchAndRender(items, [itemElement]); - return list.layoutCallback().then(() => rendered).then(() => { + expectFetchAndRender(items, [itemElement]); + return list.layoutCallback().then(() => { expect(list.container_.contains(itemElement)).to.be.true; }); }); @@ -125,7 +124,7 @@ describes.realWin('amp-list component', { const itemElement = doc.createElement('div'); itemElement.style.height = '1337px'; - const rendered = expectFetchAndRender(items, [itemElement]); + expectFetchAndRender(items, [itemElement]); let measureFunc; listMock.expects('getVsync').returns({ @@ -138,7 +137,7 @@ describes.realWin('amp-list component', { .withExactArgs(1337) .returns(Promise.resolve()); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(list.container_.contains(itemElement)).to.be.true; expect(measureFunc).to.exist; measureFunc(); @@ -150,10 +149,10 @@ describes.realWin('amp-list component', { const itemElement = doc.createElement('div'); element.setAttribute('single-item', 'true'); - const rendered = expectFetchAndRender( + expectFetchAndRender( items, [itemElement], {expr: 'items', singleItem: true}); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(list.container_.contains(itemElement)).to.be.true; }); }); @@ -167,10 +166,10 @@ describes.realWin('amp-list component', { const itemElement = doc.createElement('div'); element.setAttribute('max-items', '2'); - const rendered = expectFetchAndRender( + expectFetchAndRender( items, [itemElement], {expr: 'items', maxItems: 2}); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(list.container_.contains(itemElement)).to.be.true; }); }); @@ -180,9 +179,9 @@ describes.realWin('amp-list component', { const items = [{title: 'Title1'}]; const itemElement = doc.createElement('div'); - const rendered = expectFetchAndRender(items, [itemElement]); + expectFetchAndRender(items, [itemElement]); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(spy).to.have.been.calledOnce; expect(spy).calledWithMatch({ type: AmpEvents.DOM_UPDATE, @@ -198,7 +197,7 @@ describes.realWin('amp-list component', { const items = [{title: 'foo'}]; const foo = doc.createElement('div'); - const rendered = expectFetchAndRender(items, [foo]); + expectFetchAndRender(items, [foo]); const layout = list.layoutCallback(); // Execute another fetch-triggering action immediately (actually on @@ -212,7 +211,7 @@ describes.realWin('amp-list component', { listMock.expects('togglePlaceholder').withExactArgs(false).once(); - return layout.then(() => rendered).then(() => { + return layout.then(() => { expect(list.container_.contains(foo)).to.be.true; // Only one render pass should be invoked at a time. @@ -226,9 +225,9 @@ describes.realWin('amp-list component', { it('should refetch if refresh action is called', () => { const items = [{title: 'foo'}]; const foo = doc.createElement('div'); - const rendered = expectFetchAndRender(items, [foo]); + expectFetchAndRender(items, [foo]); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(list.container_.contains(foo)).to.be.true; const renderedAgain = expectFetchAndRender(items, [foo]); @@ -247,9 +246,9 @@ describes.realWin('amp-list component', { const items = [{title: 'foo'}]; const foo = doc.createElement('div'); const opts = {expr: 'items', resetOnRefresh: true}; - const rendered = expectFetchAndRender(items, [foo], opts); + expectFetchAndRender(items, [foo], opts); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(list.container_.contains(foo)).to.be.true; const renderedAgain = expectFetchAndRender(items, [foo], opts); @@ -302,9 +301,9 @@ describes.realWin('amp-list component', { it('should set accessibility roles', () => { const items = [{title: 'Title1'}]; const itemElement = doc.createElement('div'); - const rendered = expectFetchAndRender(items, [itemElement]); + expectFetchAndRender(items, [itemElement]); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(list.container_.getAttribute('role')).to.equal('list'); expect(itemElement.getAttribute('role')).to.equal('listitem'); }); @@ -315,9 +314,9 @@ describes.realWin('amp-list component', { element.setAttribute('role', 'list1'); const itemElement = doc.createElement('div'); itemElement.setAttribute('role', 'listitem1'); - const rendered = expectFetchAndRender(items, [itemElement]); + expectFetchAndRender(items, [itemElement]); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(list.element.getAttribute('role')).to.equal('list1'); expect(itemElement.getAttribute('role')).to.equal('listitem1'); }); @@ -392,9 +391,9 @@ describes.realWin('amp-list component', { it('should render and remove `src` if [src] points to local data', () => { const items = [{title: 'foo'}]; const foo = doc.createElement('div'); - const rendered = expectFetchAndRender(items, [foo]); + expectFetchAndRender(items, [foo]); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(list.container_.contains(foo)).to.be.true; listMock.expects('fetchList_').never(); @@ -408,9 +407,9 @@ describes.realWin('amp-list component', { it('should refetch if [src] attribute changes (after layout)', () => { const items = [{title: 'foo'}]; const foo = doc.createElement('div'); - const rendered = expectFetchAndRender(items, [foo]); + expectFetchAndRender(items, [foo]); - return list.layoutCallback().then(() => rendered).then(() => { + return list.layoutCallback().then(() => { expect(list.container_.contains(foo)).to.be.true; // Allowed post-layout. @@ -448,8 +447,8 @@ describes.realWin('amp-list component', { it('should not call scanAndApply() before FIRST_MUTATE', function*() { const items = [{title: 'Title1'}]; const output = [doc.createElement('div')]; - const rendered = expectFetchAndRender(items, output); - yield list.layoutCallback().then(() => rendered); + expectFetchAndRender(items, output); + yield list.layoutCallback(); expect(bind.scanAndApply).to.not.have.been.called; }); @@ -461,8 +460,8 @@ describes.realWin('amp-list component', { const items = [{title: 'Title1'}]; const output = [doc.createElement('div')]; - const rendered = expectFetchAndRender(items, output); - yield list.layoutCallback().then(() => rendered); + expectFetchAndRender(items, output); + yield list.layoutCallback(); expect(bind.scanAndApply).to.have.been.calledOnce; expect(bind.scanAndApply).calledWithExactly(output, [list.container_]); From 748d3406d346dcc0b94694ceb73b49343420ff5a Mon Sep 17 00:00:00 2001 From: William Chou Date: Mon, 2 Jul 2018 18:28:59 -0400 Subject: [PATCH 4/5] Always mutate instead. --- extensions/amp-list/0.1/amp-list.js | 35 +++++++------------ extensions/amp-list/0.1/test/test-amp-list.js | 23 +++--------- 2 files changed, 17 insertions(+), 41 deletions(-) diff --git a/extensions/amp-list/0.1/amp-list.js b/extensions/amp-list/0.1/amp-list.js index 98365a26053d..a0e2765a201d 100644 --- a/extensions/amp-list/0.1/amp-list.js +++ b/extensions/amp-list/0.1/amp-list.js @@ -60,7 +60,7 @@ export class AmpList extends AMP.BaseElement { /** * Latest fetched items to render and the promise resolver and rejecter * to be invoked on render success or fail, respectively. - * @private {?{items:!Array, resolver:!Function, rejecter:!Function, mutate:boolean}} + * @private {?{items:!Array, resolver:!Function, rejecter:!Function}} */ this.renderItems_ = null; @@ -137,20 +137,20 @@ export class AmpList extends AMP.BaseElement { if (typeOfSrc === 'string') { // Defer to fetch in layoutCallback() before first layout. if (this.layoutCompleted_) { - return this.fetchList_(/* mutate */ true); + return this.fetchList_(); } } else if (typeOfSrc === 'object') { // Remove the 'src' now that local data is used to render the list. this.element.setAttribute('src', ''); const items = isArray(src) ? src : [src]; - return this.scheduleRender_(items, /* mutate */ true); + return this.scheduleRender_(items); } else { this.user().error(TAG, 'Unexpected "src" type: ' + src); } } else if (state !== undefined) { user().error(TAG, '[state] is deprecated, please use [src] instead.'); const items = isArray(state) ? state : [state]; - return this.scheduleRender_(items, /* mutate */ true); + return this.scheduleRender_(items); } return Promise.resolve(); } @@ -189,11 +189,10 @@ export class AmpList extends AMP.BaseElement { /** * Request list data from `src` and return a promise that resolves when * the list has been populated with rendered list items. - * @param {boolean} mutate If true, performs DOM changes in a mutate context. * @return {!Promise} * @private */ - fetchList_(mutate = false) { + fetchList_() { if (!this.element.getAttribute('src')) { return Promise.resolve(); } @@ -222,7 +221,7 @@ export class AmpList extends AMP.BaseElement { if (maxLen < items.length) { items = items.slice(0, maxLen); } - return this.scheduleRender_(items, mutate); + return this.scheduleRender_(items); }, error => { throw user().createError('Error fetching amp-list', error); }).then(() => { @@ -246,11 +245,10 @@ export class AmpList extends AMP.BaseElement { /** * Schedules a fetch result to be rendered in the near future. * @param {!Array} items - * @param {boolean} mutate If true, performs DOM changes in a mutate context. * @return {!Promise} * @private */ - scheduleRender_(items, mutate = false) { + scheduleRender_(items) { const deferred = new Deferred(); const {promise, resolve: resolver, reject: rejecter} = deferred; @@ -258,7 +256,7 @@ export class AmpList extends AMP.BaseElement { if (!this.renderItems_) { this.renderPass_.schedule(); } - this.renderItems_ = {items, resolver, rejecter, mutate}; + this.renderItems_ = {items, resolver, rejecter}; return promise; } @@ -278,10 +276,10 @@ export class AmpList extends AMP.BaseElement { this.renderItems_ = null; } }; - const {items, resolver, rejecter, mutate} = current; + const {items, resolver, rejecter} = current; this.templates_.findAndRenderTemplateArray(this.element, items) .then(elements => this.updateBindingsForElements_(elements)) - .then(elements => this.render_(elements, mutate)) + .then(elements => this.render_(elements)) .then(/* onFulfilled */ () => { scheduleNextPass(); resolver(); @@ -333,11 +331,10 @@ export class AmpList extends AMP.BaseElement { /** * @param {!Array} elements - * @param {boolean} mutate If true, performs DOM changes in a mutate context. * @private */ - render_(elements, mutate) { - const render = () => { + render_(elements) { + this.mutateElement(() => { removeChildren(dev().assertElement(this.container_)); elements.forEach(element => { if (!element.hasAttribute('role')) { @@ -356,13 +353,7 @@ export class AmpList extends AMP.BaseElement { this.attemptChangeHeight(scrollHeight).catch(() => {}); } }); - }; - - if (mutate) { - this.mutateElement(render, this.container_); - } else { - render(); - } + }, this.container_); } /** diff --git a/extensions/amp-list/0.1/test/test-amp-list.js b/extensions/amp-list/0.1/test/test-amp-list.js index 607f1592c3a4..80a0b4e56a1d 100644 --- a/extensions/amp-list/0.1/test/test-amp-list.js +++ b/extensions/amp-list/0.1/test/test-amp-list.js @@ -99,6 +99,10 @@ describes.realWin('amp-list component', { const render = Promise.resolve(rendered); templatesMock.expects('findAndRenderTemplateArray') .withExactArgs(element, itemsToRender).returns(render).atLeast(1); + + listMock.expects('mutateElement') + .callsFake(mutator => mutator()) + .atLeast(1); } describe('without amp-bind', () => { @@ -420,25 +424,6 @@ describes.realWin('amp-list component', { }); }); - it('should render in a mutate context', function*() { - listMock.expects('mutateElement') - .withArgs(sinon.match.func, list.container_) - .once(); - - const items = [{title: 'foo'}]; - const foo = doc.createElement('div'); - expectFetchAndRender(items, [foo]); - - // Pretend that layoutCallback() was called already since amp-list will - // normally refuse to refetch until it happens. - list.layoutCompleted_ = true; - - element.setAttribute('src', 'https://new.com/list.json'); - yield list.mutatedAttributesCallback({ - 'src': 'https://new.com/list.json', - }); - }); - describe('`fast-amp-list` experiment', () => { beforeEach(() => { toggleExperiment(win, 'fast-amp-list', true, true); From de2d6f14a60712250a336e7b75ed608068230014 Mon Sep 17 00:00:00 2001 From: William Chou Date: Mon, 2 Jul 2018 19:57:18 -0400 Subject: [PATCH 5/5] Fix lint. --- extensions/amp-list/0.1/test/test-amp-list.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/extensions/amp-list/0.1/test/test-amp-list.js b/extensions/amp-list/0.1/test/test-amp-list.js index 80a0b4e56a1d..900db960fbc3 100644 --- a/extensions/amp-list/0.1/test/test-amp-list.js +++ b/extensions/amp-list/0.1/test/test-amp-list.js @@ -14,8 +14,6 @@ * limitations under the License. */ -import * as sinon from 'sinon'; - import {AmpEvents} from '../../../../src/amp-events'; import {AmpList} from '../amp-list'; import {Deferred} from '../../../../src/utils/promise';