diff --git a/src/service/resources-impl.js b/src/service/resources-impl.js index 5cd95054cdc0..e9f99fd3d487 100644 --- a/src/service/resources-impl.js +++ b/src/service/resources-impl.js @@ -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'; @@ -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(); @@ -215,7 +219,11 @@ 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_(); }); @@ -223,6 +231,51 @@ export class Resources { 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} diff --git a/test/functional/test-resources.js b/test/functional/test-resources.js index 2683bd8cb97f..42c0aecf48db 100644 --- a/test/functional/test-resources.js +++ b/test/functional/test-resources.js @@ -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'; @@ -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); + }); +});