Skip to content

Commit

Permalink
Ensure that aborting layout via unload cancels the layout promise
Browse files Browse the repository at this point in the history
  • Loading branch information
Dima Voytenko committed Jan 21, 2021
1 parent 1f0911d commit 9a3e80a
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 17 deletions.
2 changes: 2 additions & 0 deletions src/friendly-iframe-embed.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
} from './service';
import {escapeHtml} from './dom';
import {getExperimentBranch} from './experiments';
import {install as installAbortController} from './polyfills/abort-controller';
import {installAmpdocServicesForEmbed} from './service/core-services';
import {install as installCustomElements} from './polyfills/custom-elements';
import {install as installDOMTokenList} from './polyfills/domtokenlist';
Expand Down Expand Up @@ -715,6 +716,7 @@ function installPolyfillsInChildWindow(parentWin, childWin) {
installCustomElements(childWin, class {});
installIntersectionObserver(parentWin, childWin);
installResizeObserver(parentWin, childWin);
installAbortController(childWin);
}
}

Expand Down
75 changes: 59 additions & 16 deletions src/polyfills/abort-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,60 @@
* limitations under the License.
*/

class AbortController {
/** */
constructor() {
/** @const {!AbortSignal} */
this.signal_ = new AbortSignal();
}
/**
* @param {!Window} win
* @return {typeof AbortController}
*/
function createAbortController(win) {
class AbortController {
/** */
constructor() {
/** @const {!AbortSignal} */
this.signal_ = new AbortSignal();
}

/** */
abort() {
this.signal_.isAborted_ = true;
}
/** */
abort() {
this.signal_.isAborted_ = true;
if (this.signal_.onabort_) {
const event = win.document.createEvent('CustomEvent');
event.initCustomEvent(
'abort',
/* bubbles */ false,
/* cancelable */ false,
/* detail */ null
);
// Notice that in IE the target/currentTarget are not overridable.
try {
Object.defineProperties(event, {
'target': {value: this.signal_},
'currentTarget': {value: this.signal_},
});
} catch (e) {
// Ignore.
}
this.signal_.onabort_(event);
}
}

/**
* @return {!AbortSignal}
*/
get signal() {
return this.signal_;
/**
* @return {!AbortSignal}
*/
get signal() {
return this.signal_;
}
}

return AbortController;
}

class AbortSignal {
/** */
constructor() {
/** @private {boolean} */
this.isAborted_ = false;
/** @private {?function(!Event)} */
this.onabort_ = null;
}

/**
Expand All @@ -47,6 +76,20 @@ class AbortSignal {
get aborted() {
return this.isAborted_;
}

/**
* @return {?function(!Event)}
*/
get onabort() {
return this.onabort_;
}

/**
* @param {?function(!Event)} value
*/
set onabort(value) {
this.onabort_ = value;
}
}

/**
Expand All @@ -60,7 +103,7 @@ export function install(win) {
configurable: true,
enumerable: false,
writable: true,
value: AbortController,
value: createAbortController(win),
});
Object.defineProperty(win, 'AbortSignal', {
configurable: true,
Expand Down
5 changes: 4 additions & 1 deletion src/service/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -953,12 +953,15 @@ export class Resource {

const promise = new Promise((resolve, reject) => {
Services.vsyncFor(this.hostWin).mutate(() => {
let callbackResult;
try {
resolve(this.element.layoutCallback(signal));
callbackResult = this.element.layoutCallback(signal);
} catch (e) {
reject(e);
}
Promise.resolve(callbackResult).then(resolve, reject);
});
signal.onabort = () => reject(cancellation());
}).then(
() => this.layoutComplete_(true, signal),
(reason) => this.layoutComplete_(false, signal, reason)
Expand Down
63 changes: 63 additions & 0 deletions test/unit/polyfills/test-abort-controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/**
* Copyright 2020 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {install} from '../../../src/polyfills/abort-controller';

describes.realWin('AbortController', {}, (env) => {
let win;
let controller, signal;

beforeEach(() => {
win = env.win;

delete win.AbortController;
delete win.AbortSignal;
install(win);

controller = new win.AbortController();
signal = controller.signal;
});

it('should create AbortController and signal', () => {
expect(signal).to.exist;
expect(signal.aborted).to.be.false;
expect(signal.onabort).to.be.null;
});

it('should abort signal without listener', () => {
controller.abort();
expect(signal.aborted).to.be.true;
});

it('should abort signal with listener', () => {
const spy = env.sandbox.spy();
signal.onabort = spy;
expect(signal.onabort).to.equal(spy);

controller.abort();
expect(signal.aborted).to.be.true;
expect(spy).to.be.calledOnce;
const event = spy.firstCall.firstArg;
expect(event).to.contain({
type: 'abort',
bubbles: false,
cancelable: false,
});
// In IE, target/currentTarget are not overridable.
expect(event.target === null || event.target === signal).to.be.true;
expect(event.currentTarget === null || event.currentTarget === signal).to.be.true;
});
});
22 changes: 22 additions & 0 deletions test/unit/test-resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {OwnersImpl} from '../../src/service/owners-impl';
import {Resource, ResourceState} from '../../src/service/resource';
import {ResourcesImpl} from '../../src/service/resources-impl';
import {Services} from '../../src/services';
import {isCancellation} from '../../src/error';
import {layoutRectLtwh} from '../../src/layout-rect';

describes.realWin('Resource', {amp: true}, (env) => {
Expand Down Expand Up @@ -621,6 +622,27 @@ describes.realWin('Resource', {amp: true}, (env) => {
expect(resource.getState()).to.equal(ResourceState.LAYOUT_SCHEDULED);
});

it('should abort startLayout with unload', async () => {
const neverEndingPromise = new Promise(() => {});
elementMock.expects('layoutCallback').returns(neverEndingPromise).once();

resource.state_ = ResourceState.LAYOUT_SCHEDULED;
resource.layoutBox_ = {left: 11, top: 12, width: 10, height: 10};
const layoutPromise = resource.startLayout();
expect(resource.getState()).to.equal(ResourceState.LAYOUT_SCHEDULED);

resource.unload();

let error;
try {
await layoutPromise;
} catch (e) {
error = e;
}
expect(error).to.exist;
expect(isCancellation(error)).to.be.true;
});

it('should ignore startLayout for re-layout when not opt-in', () => {
elementMock.expects('layoutCallback').never();

Expand Down

0 comments on commit 9a3e80a

Please sign in to comment.