Skip to content

Commit

Permalink
IE: resolve the matchMedia problem (#3112)
Browse files Browse the repository at this point in the history
* IE: resolve the matchMedia problem

* linter

* Protect from both min-width and max-width errors
  • Loading branch information
dvoytenko authored and erwinmombay committed May 6, 2016
1 parent 54cdbdc commit e5a611b
Show file tree
Hide file tree
Showing 2 changed files with 232 additions and 2 deletions.
57 changes: 55 additions & 2 deletions src/service/resources-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {installFramerateService} from './framerate-impl';
import {installViewerService, VisibilityState} from './viewer-impl';
import {installViewportService} from './viewport-impl';
import {installVsyncService} from './vsync-impl';
import {platformFor} from '../platform';
import {FiniteStateMachine} from '../finite-state-machine';
import {isArray} from '../types';

Expand Down Expand Up @@ -74,9 +75,12 @@ export class Resources {
/** @const {!Window} */
this.win = window;

/** @const {!Viewer} */
/** @const @private {!Viewer} */
this.viewer_ = installViewerService(window);

/** @const @private {!Platform} */
this.platform_ = platformFor(window);

/** @private {boolean} */
this.isRuntimeOn_ = this.viewer_.isRuntimeOn();

Expand Down Expand Up @@ -215,14 +219,63 @@ export class Resources {
onDocumentReady(this.win.document, () => {
this.documentReady_ = true;
this.forceBuild_ = true;
this.relayoutAll_ = true;
if (this.platform_.isIe()) {
this.fixMediaIe_(this.win);
} else {
this.relayoutAll_ = true;
}
this.schedulePass();
this.monitorInput_();
});

this.schedulePass();
}

/**
* An ugly fix for IE's problem with `matchMedia` API, where media queries
* are evaluated incorrectly. See #2577 for more details.
* @param {!Window} win
* @private
*/
fixMediaIe_(win) {
if (!this.platform_.isIe() || this.matchMediaIeQuite_(win)) {
this.relayoutAll_ = true;
return;
}

// Poll until the expression resolves correctly, but only up to a point.
const endTime = timer.now() + 2000;
const interval = win.setInterval(() => {
const now = timer.now();
const matches = this.matchMediaIeQuite_(win);
if (matches || now > endTime) {
win.clearInterval(interval);
this.relayoutAll_ = true;
this.schedulePass();
if (!matches) {
dev.error(TAG_, 'IE media never resolved');
}
}
}, 10);
}

/**
* @param {!Window} win
* @return {boolean}
* @private
*/
matchMediaIeQuite_(win) {
const q = `(min-width: ${win./*OK*/innerWidth}px)` +
` AND (max-width: ${win./*OK*/innerWidth}px)`;
try {
return win.matchMedia(q).matches;
} catch (e) {
dev.error(TAG_, 'IE matchMedia failed: ', e);
// Return `true` to avoid polling on a broken API.
return true;
}
}

/**
* Returns a list of resources.
* @return {!Array<!Resource>}
Expand Down
177 changes: 177 additions & 0 deletions test/functional/test-resources.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
TaskQueue_,
} from '../../src/service/resources-impl';
import {VisibilityState} from '../../src/service/viewer-impl';
import {dev} from '../../src/log';
import {layoutRectLtwh} from '../../src/layout-rect';
import * as sinon from 'sinon';

Expand Down Expand Up @@ -2190,3 +2191,179 @@ describe('Resource renderOutsideViewport', () => {
});
});
});

describe('Resources fix IE matchMedia', () => {
let sandbox;
let clock;
let windowApi, windowMock;
let platformMock;
let resources;
let devErrorStub;
let schedulePassStub;

beforeEach(() => {
sandbox = sinon.sandbox.create();
clock = sandbox.useFakeTimers();
resources = new Resources(window);
resources.relayoutAll_ = false;
resources.doPass_ = () => {};
platformMock = sandbox.mock(resources.platform_);
devErrorStub = sandbox.stub(dev, 'error');
schedulePassStub = sandbox.stub(resources, 'schedulePass');

windowApi = {
innerWidth: 320,
setInterval: () => {},
clearInterval: () => {},
matchMedia: () => {},
};
windowMock = sandbox.mock(windowApi);
});

afterEach(() => {
platformMock.verify();
windowMock.verify();
sandbox.restore();
});

it('should bypass polling for non-IE browsers', () => {
platformMock.expects('isIe').returns(false);
windowMock.expects('matchMedia').never();
windowMock.expects('setInterval').never();
resources.fixMediaIe_(windowApi);
expect(resources.relayoutAll_).to.be.true;
expect(devErrorStub.callCount).to.equal(0);
expect(schedulePassStub.callCount).to.equal(0);
});

it('should bypass polling when matchMedia is not broken', () => {
platformMock.expects('isIe').returns(true);
windowMock.expects('matchMedia')
.withExactArgs('(min-width: 320px) AND (max-width: 320px)')
.returns({matches: true})
.once();
windowMock.expects('setInterval').never();
resources.fixMediaIe_(windowApi);
expect(resources.relayoutAll_).to.be.true;
expect(devErrorStub.callCount).to.equal(0);
expect(schedulePassStub.callCount).to.equal(0);
});

it('should poll when matchMedia is wrong, but eventually succeeds', () => {
platformMock.expects('isIe').returns(true);

// Scheduling pass.
windowMock.expects('matchMedia')
.withExactArgs('(min-width: 320px) AND (max-width: 320px)')
.returns({matches: false})
.once();
const intervalId = 111;
let intervalCallback;
windowMock.expects('setInterval')
.withExactArgs(
sinon.match(arg => {
intervalCallback = arg;
return true;
}),
10
)
.returns(intervalId)
.once();

resources.fixMediaIe_(windowApi);
expect(resources.relayoutAll_).to.be.false;
expect(devErrorStub.callCount).to.equal(0);
expect(schedulePassStub.callCount).to.equal(0);
expect(intervalCallback).to.exist;
windowMock.verify();
windowMock./*OK*/restore();

// Second pass.
clock.tick(10);
windowMock = sandbox.mock(windowApi);
windowMock.expects('matchMedia')
.withExactArgs('(min-width: 320px) AND (max-width: 320px)')
.returns({matches: false})
.once();
windowMock.expects('clearInterval').never();
intervalCallback();
expect(resources.relayoutAll_).to.be.false;
expect(devErrorStub.callCount).to.equal(0);
expect(schedulePassStub.callCount).to.equal(0);
windowMock.verify();
windowMock./*OK*/restore();

// Third pass - succeed.
clock.tick(10);
windowMock = sandbox.mock(windowApi);
windowMock.expects('matchMedia')
.withExactArgs('(min-width: 320px) AND (max-width: 320px)')
.returns({matches: true})
.once();
windowMock.expects('clearInterval').withExactArgs(intervalId).once();
intervalCallback();
expect(resources.relayoutAll_).to.be.true;
expect(schedulePassStub.callCount).to.equal(1);
expect(devErrorStub.callCount).to.equal(0);
windowMock.verify();
windowMock./*OK*/restore();
});

it('should poll until times out', () => {
platformMock.expects('isIe').returns(true);

// Scheduling pass.
windowMock.expects('matchMedia')
.withExactArgs('(min-width: 320px) AND (max-width: 320px)')
.returns({matches: false})
.atLeast(2);
const intervalId = 111;
let intervalCallback;
windowMock.expects('setInterval')
.withExactArgs(
sinon.match(arg => {
intervalCallback = arg;
return true;
}),
10
)
.returns(intervalId)
.once();
windowMock.expects('clearInterval').withExactArgs(intervalId).once();

resources.fixMediaIe_(windowApi);
expect(resources.relayoutAll_).to.be.false;
expect(devErrorStub.callCount).to.equal(0);
expect(schedulePassStub.callCount).to.equal(0);
expect(intervalCallback).to.exist;

// Second pass.
clock.tick(10);
intervalCallback();
expect(resources.relayoutAll_).to.be.false;
expect(devErrorStub.callCount).to.equal(0);
expect(schedulePassStub.callCount).to.equal(0);

// Third pass - timeout.
clock.tick(2000);
intervalCallback();
expect(resources.relayoutAll_).to.be.true;
expect(schedulePassStub.callCount).to.equal(1);
expect(devErrorStub.callCount).to.equal(1);
});

it('should tolerate matchMedia exceptions', () => {
platformMock.expects('isIe').returns(true);

windowMock.expects('matchMedia')
.withExactArgs('(min-width: 320px) AND (max-width: 320px)')
.throws(new Error('intentional'))
.once();
windowMock.expects('setInterval').never();

resources.fixMediaIe_(windowApi);
expect(resources.relayoutAll_).to.be.true;
expect(devErrorStub.callCount).to.equal(1);
expect(schedulePassStub.callCount).to.equal(0);
});
});

0 comments on commit e5a611b

Please sign in to comment.