diff --git a/extensions/amp-access/0.1/test/test-login-dialog.js b/extensions/amp-access/0.1/test/test-login-dialog.js index 8842d06d4c28..78990a9cc99e 100644 --- a/extensions/amp-access/0.1/test/test-login-dialog.js +++ b/extensions/amp-access/0.1/test/test-login-dialog.js @@ -1,6 +1,8 @@ import {Services} from '#service'; import {installDocService} from '#service/ampdoc-impl'; +import {FakePerformance} from '#testing/fake-dom'; + import {WebLoginDialog, openLoginDialog} from '../login-dialog'; const RETURN_URL_ESC = encodeURIComponent( @@ -40,6 +42,7 @@ describes.sandboxed('ViewerLoginDialog', {}, (env) => { setInterval: () => { throw new Error('Not allowed'); }, + performance: new FakePerformance(window), }; windowApi.document.defaultView = windowApi; installDocService(windowApi, /* isSingleDoc */ true); @@ -178,6 +181,7 @@ describes.sandboxed('WebLoginDialog', {}, (env) => { setTimeout: (callback, t) => window.setTimeout(callback, t), setInterval: (callback, t) => window.setInterval(callback, t), clearInterval: (intervalId) => window.clearInterval(intervalId), + performance: new FakePerformance(window), }; windowApi = windowObj; windowApi.document.defaultView = windowApi; diff --git a/extensions/amp-experiment/0.1/test/test-variant.js b/extensions/amp-experiment/0.1/test/test-variant.js index e6d507a97d7f..bf6a684ae5eb 100644 --- a/extensions/amp-experiment/0.1/test/test-variant.js +++ b/extensions/amp-experiment/0.1/test/test-variant.js @@ -1,6 +1,8 @@ import {Services} from '#service'; import {AmpDocSingle} from '#service/ampdoc-impl'; +import {FakePerformance} from '#testing/fake-dom'; + import {allocateVariant} from '../variant'; describes.sandboxed('allocateVariant', {}, (env) => { @@ -23,6 +25,7 @@ describes.sandboxed('allocateVariant', {}, (env) => { nodeType: /* DOCUMENT */ 9, body: {}, }, + performance: new FakePerformance(window), }; fakeWin.document.defaultView = fakeWin; ampdoc = new AmpDocSingle(fakeWin); diff --git a/extensions/amp-experiment/1.0/test/test-variant.js b/extensions/amp-experiment/1.0/test/test-variant.js index 2cd86776de2a..b2ad03dd465a 100644 --- a/extensions/amp-experiment/1.0/test/test-variant.js +++ b/extensions/amp-experiment/1.0/test/test-variant.js @@ -1,6 +1,8 @@ import {Services} from '#service'; import {AmpDocSingle} from '#service/ampdoc-impl'; +import {FakePerformance} from '#testing/fake-dom'; + import {allocateVariant} from '../variant'; describes.sandboxed('allocateVariant', {}, (env) => { @@ -24,6 +26,7 @@ describes.sandboxed('allocateVariant', {}, (env) => { nodeType: /* DOCUMENT */ 9, body: {}, }, + performance: new FakePerformance(window), }; fakeWin.document.defaultView = fakeWin; ampdoc = new AmpDocSingle(fakeWin); diff --git a/src/service/ampdoc-impl.js b/src/service/ampdoc-impl.js index 28d0805aa343..8a34f7c29bcc 100644 --- a/src/service/ampdoc-impl.js +++ b/src/service/ampdoc-impl.js @@ -614,14 +614,25 @@ export class AmpDoc { } if (this.visibilityState_ != visibilityState) { - this.visibilityState_ = visibilityState; if (visibilityState == VisibilityState.VISIBLE) { - this.lastVisibleTime_ = Date.now(); - this.signals_.signal(AmpDocSignals.FIRST_VISIBLE); - this.signals_.signal(AmpDocSignals.NEXT_VISIBLE); + let visibleTime; + const {performance} = this.win; + if (this.visibilityState_ == null) { + // If the doc was initialized in the visible state, then prefer the + // timeOrigin of the document over when the JS actually initializes. + visibleTime = Math.floor( + performance.timeOrigin ?? performance.timing.navigationStart + ); + } else { + visibleTime = Math.floor(performance.now()); + } + this.lastVisibleTime_ = visibleTime; + this.signals_.signal(AmpDocSignals.FIRST_VISIBLE, visibleTime); + this.signals_.signal(AmpDocSignals.NEXT_VISIBLE, visibleTime); } else { this.signals_.reset(AmpDocSignals.NEXT_VISIBLE); } + this.visibilityState_ = visibilityState; this.visibilityStateHandlers_.fire(); } } diff --git a/src/service/performance-impl.js b/src/service/performance-impl.js index 8e52a2404add..07ba7ecf7ebe 100644 --- a/src/service/performance-impl.js +++ b/src/service/performance-impl.js @@ -796,13 +796,7 @@ export class Performance { * @param {string} label */ mark(label) { - if ( - this.win.performance && - this.win.performance.mark && - arguments.length == 1 - ) { - this.win.performance.mark(label); - } + this.win.performance.mark?.(label); } /** diff --git a/test/unit/core/window/test-history.js b/test/unit/core/window/test-history.js index be657be4020b..38cdbc984a5e 100644 --- a/test/unit/core/window/test-history.js +++ b/test/unit/core/window/test-history.js @@ -10,6 +10,8 @@ import {installTimerService} from '#service/timer-impl'; import {listenOncePromise} from '#utils/event-helper'; +import {FakePerformance} from '#testing/fake-dom'; + import {parseUrlDeprecated} from '../../../../src/url'; describes.fakeWin( @@ -322,6 +324,7 @@ describes.sandboxed('Window - History', {}, (env) => { 'https://cdn.ampproject.org/c/s/www.example.com/path' ), addEventListener: () => null, + performance: new FakePerformance(window), }; ampdoc = new AmpDocSingle(win); installHistoryServiceForDoc(ampdoc); diff --git a/test/unit/test-action.js b/test/unit/test-action.js index 8abe811942c2..c8e70795ea40 100644 --- a/test/unit/test-action.js +++ b/test/unit/test-action.js @@ -17,6 +17,7 @@ import {AmpDocSingle} from '#service/ampdoc-impl'; import {createCustomEvent} from '#utils/event-helper'; +import {FakePerformance} from '#testing/fake-dom'; import {whenCalled} from '#testing/helpers/service'; /** @@ -35,6 +36,7 @@ function actionService() { __AMP_SERVICES: { vsync: {obj: {}, ctor: Object}, }, + performance: new FakePerformance(window), }; return new ActionService(new AmpDocSingle(win), document); } diff --git a/test/unit/test-activity.js b/test/unit/test-activity.js index f993cd0a11c2..404c98f6973c 100644 --- a/test/unit/test-activity.js +++ b/test/unit/test-activity.js @@ -9,6 +9,8 @@ import {installViewerServiceForDoc} from '#service/viewer-impl'; import {installViewportServiceForDoc} from '#service/viewport/viewport-impl'; import {installVsyncService} from '#service/vsync-impl'; +import {FakePerformance} from '#testing/fake-dom'; + import {installActivityServiceForTesting} from '../../extensions/amp-analytics/0.1/activity-impl'; describes.sandboxed('Activity getTotalEngagedTime', {}, (env) => { @@ -76,6 +78,7 @@ describes.sandboxed('Activity getTotalEngagedTime', {}, (env) => { addEventListener: () => {}, removeEventListener: () => {}, Promise: window.Promise, + performance: new FakePerformance(window), }; fakeDoc.defaultView = fakeWin; fakeDoc.head.defaultView = fakeWin; @@ -301,6 +304,7 @@ describes.sandboxed('Activity getIncrementalEngagedTime', {}, (env) => { addEventListener: () => {}, removeEventListener: () => {}, Promise: window.Promise, + performance: new FakePerformance(window), }; fakeDoc.defaultView = fakeWin; fakeDoc.head.defaultView = fakeWin; diff --git a/test/unit/test-ampdoc.js b/test/unit/test-ampdoc.js index 4ae8484fcb78..dc117347a76d 100644 --- a/test/unit/test-ampdoc.js +++ b/test/unit/test-ampdoc.js @@ -439,7 +439,13 @@ describes.sandboxed('AmpDoc.visibilityState', {}, (env) => { addEventListener: env.sandbox.spy(), removeEventListener: env.sandbox.spy(), }; - win = {document: doc}; + win = { + document: doc, + performance: { + now: Date.now, + timeOrigin: 1, + }, + }; childDoc = { body: null, @@ -447,7 +453,13 @@ describes.sandboxed('AmpDoc.visibilityState', {}, (env) => { addEventListener: env.sandbox.spy(), removeEventListener: env.sandbox.spy(), }; - childWin = {document: childDoc}; + childWin = { + document: childDoc, + performance: { + now: Date.now, + timeOrigin: 2, + }, + }; top = new AmpDocSingle(win); embedSameWindow = new AmpDocFie(win, EMBED_URL, top); @@ -524,6 +536,25 @@ describes.sandboxed('AmpDoc.visibilityState', {}, (env) => { expect(disposableService.dispose).to.be.calledOnce; }); + describe('firstVisibleTime', () => { + it('should prefer timeOrigin doc initialized to visible', () => { + win.performance.timeOrigin = 10; + top = new AmpDocSingle(win, {visibilityState: 'visible'}); + + expect(top.getFirstVisibleTime()).to.equal(10); + }); + + it('should wait for visible', () => { + top = new AmpDocSingle(win, {visibilityState: 'prerender'}); + + expect(top.getFirstVisibleTime()).to.equal(null); + + clock.tick(100); + top.overrideVisibilityState('visible'); + expect(top.getFirstVisibleTime()).to.equal(101); + }); + }); + it('should be visible by default', () => { expect(top.getVisibilityState()).to.equal('visible'); expect(embedSameWindow.getVisibilityState()).to.equal('visible'); @@ -537,13 +568,13 @@ describes.sandboxed('AmpDoc.visibilityState', {}, (env) => { expect(top.getFirstVisibleTime()).to.equal(1); expect(embedSameWindow.getFirstVisibleTime()).to.equal(1); - expect(embedOtherWindow.getFirstVisibleTime()).to.equal(1); - expect(embedChild.getFirstVisibleTime()).to.equal(1); + expect(embedOtherWindow.getFirstVisibleTime()).to.equal(2); + expect(embedChild.getFirstVisibleTime()).to.equal(2); expect(top.getLastVisibleTime()).to.equal(1); expect(embedSameWindow.getLastVisibleTime()).to.equal(1); - expect(embedOtherWindow.getLastVisibleTime()).to.equal(1); - expect(embedChild.getLastVisibleTime()).to.equal(1); + expect(embedOtherWindow.getLastVisibleTime()).to.equal(2); + expect(embedChild.getLastVisibleTime()).to.equal(2); return Promise.all([ top.whenFirstVisible(), @@ -700,10 +731,10 @@ describes.sandboxed('AmpDoc.visibilityState', {}, (env) => { expect(top.getLastVisibleTime()).to.equal(1); expect(embedSameWindow.getFirstVisibleTime()).to.equal(1); expect(embedSameWindow.getLastVisibleTime()).to.equal(1); - expect(embedOtherWindow.getFirstVisibleTime()).to.equal(1); - expect(embedOtherWindow.getLastVisibleTime()).to.equal(1); - expect(embedChild.getFirstVisibleTime()).to.equal(1); - expect(embedChild.getLastVisibleTime()).to.equal(1); + expect(embedOtherWindow.getFirstVisibleTime()).to.equal(2); + expect(embedOtherWindow.getLastVisibleTime()).to.equal(2); + expect(embedChild.getFirstVisibleTime()).to.equal(2); + expect(embedChild.getLastVisibleTime()).to.equal(2); clock.tick(1); embedOtherWindow.overrideVisibilityState('visible'); @@ -715,9 +746,9 @@ describes.sandboxed('AmpDoc.visibilityState', {}, (env) => { expect(top.getLastVisibleTime()).to.equal(1); expect(embedSameWindow.getFirstVisibleTime()).to.equal(1); expect(embedSameWindow.getLastVisibleTime()).to.equal(1); - expect(embedOtherWindow.getFirstVisibleTime()).to.equal(1); + expect(embedOtherWindow.getFirstVisibleTime()).to.equal(2); expect(embedOtherWindow.getLastVisibleTime()).to.equal(3); - expect(embedChild.getFirstVisibleTime()).to.equal(1); + expect(embedChild.getFirstVisibleTime()).to.equal(2); expect(embedChild.getLastVisibleTime()).to.equal(3); }); @@ -852,7 +883,10 @@ describes.realWin('AmpDocSingle', {}, (env) => { addEventListener: function () {}, removeEventListener: function () {}, }; - const win = {document: doc}; + const win = { + document: doc, + performance: env.win.performance, + }; let bodyCallback; env.sandbox.stub(dom, 'waitForBodyOpenPromise').callsFake(() => { diff --git a/test/unit/test-cid.js b/test/unit/test-cid.js index cb8638a6d6d7..edcc8f61ea8f 100644 --- a/test/unit/test-cid.js +++ b/test/unit/test-cid.js @@ -15,6 +15,7 @@ import {installPlatformService} from '#service/platform-impl'; import {installTimerService} from '#service/timer-impl'; import {installViewerServiceForDoc} from '#service/viewer-impl'; +import {FakePerformance} from '#testing/fake-dom'; import {macroTask} from '#testing/helpers'; import {stubServiceForDoc} from '#testing/helpers/service'; @@ -97,6 +98,7 @@ describes.sandboxed('cid', {}, (env) => { clearTimeout: window.clearTimeout, Math: window.Math, Promise: window.Promise, + performance: new FakePerformance(window), }; fakeWin.document.defaultView = fakeWin; installDocService(fakeWin, /* isSingleDoc */ true); diff --git a/test/unit/test-custom-element-registry.js b/test/unit/test-custom-element-registry.js index 6f3941a9f566..519dd4566ae4 100644 --- a/test/unit/test-custom-element-registry.js +++ b/test/unit/test-custom-element-registry.js @@ -12,6 +12,8 @@ import { upgradeOrRegisterElement, } from '#service/custom-element-registry'; +import {FakePerformance} from '#testing/fake-dom'; + import {BaseElement} from '../../src/base-element'; import {getImplSyncForTesting} from '../../src/custom-element'; import {ElementStub} from '../../src/element-stub'; @@ -278,6 +280,7 @@ describes.realWin('CustomElement register', {amp: true}, (env) => { }, HTMLElement, __AMP_EXTENDED_ELEMENTS: {}, + performance: new FakePerformance(window), }; doc.defaultView = win; diff --git a/test/unit/test-fixed-layer.js b/test/unit/test-fixed-layer.js index a496420433a9..83c8454ef95d 100644 --- a/test/unit/test-fixed-layer.js +++ b/test/unit/test-fixed-layer.js @@ -12,7 +12,11 @@ import {installViewerServiceForDoc} from '#service/viewer-impl'; import {Animation} from '#utils/animation'; import {user} from '#utils/log'; -import {FakeMutationObserver, FakeWindow} from '#testing/fake-dom'; +import { + FakeMutationObserver, + FakePerformance, + FakeWindow, +} from '#testing/fake-dom'; describes.sandboxed('FixedLayer', {}, (env) => { let parentApi; @@ -150,6 +154,7 @@ describes.sandboxed('FixedLayer', {}, (env) => { navigator: window.navigator, location: window.location, cookie: '', + performance: new FakePerformance(window), }, createElement: (name) => { return createElement(name); diff --git a/test/unit/test-storage.js b/test/unit/test-storage.js index 90ef382daf50..7a4888afeb05 100644 --- a/test/unit/test-storage.js +++ b/test/unit/test-storage.js @@ -8,6 +8,8 @@ import { import {dev} from '#utils/log'; +import {FakePerformance} from '#testing/fake-dom'; + describes.sandboxed('Storage', {}, (env) => { let storage; let binding; @@ -37,6 +39,7 @@ describes.sandboxed('Storage', {}, (env) => { windowApi = { document: {}, location: 'https://acme.com/document1', + performance: new FakePerformance(window), }; ampdoc = new AmpDocSingle(windowApi); diff --git a/test/unit/test-viewer.js b/test/unit/test-viewer.js index d69d9b0ff0ff..0f2fef3d6aa1 100644 --- a/test/unit/test-viewer.js +++ b/test/unit/test-viewer.js @@ -9,6 +9,8 @@ import {ViewerImpl} from '#service/viewer-impl'; import {dev} from '#utils/log'; +import {FakePerformance} from '#testing/fake-dom'; + import {parseUrlDeprecated, removeFragment} from '../../src/url'; describes.sandboxed('Viewer', {}, (env) => { @@ -66,6 +68,7 @@ describes.sandboxed('Viewer', {}, (env) => { ancestorOrigins: null, search: '', }; + windowApi.performance = new FakePerformance(window); windowApi.addEventListener = (type, listener) => { events[type] = listener; }; @@ -315,11 +318,12 @@ describes.sandboxed('Viewer', {}, (env) => { return promise; }); - it('should initialize firstVisibleTime when doc becomes visible', () => { + it.only('should initialize firstVisibleTime when doc becomes visible', () => { const viewer = new ViewerImpl(ampdoc); expect(ampdoc.isVisible()).to.be.true; - expect(ampdoc.getFirstVisibleTime()).to.equal(0); - expect(ampdoc.getLastVisibleTime()).to.equal(0); + // We don't live during the unix epoch, so time is always positive. + expect(ampdoc.getFirstVisibleTime()).to.equal(1); + expect(ampdoc.getLastVisibleTime()).to.equal(1); // Becomes invisible. clock.tick(1); @@ -327,8 +331,8 @@ describes.sandboxed('Viewer', {}, (env) => { state: 'hidden', }); expect(ampdoc.isVisible()).to.be.false; - expect(ampdoc.getFirstVisibleTime()).to.equal(0); - expect(ampdoc.getLastVisibleTime()).to.equal(0); + expect(ampdoc.getFirstVisibleTime()).to.equal(1); + expect(ampdoc.getLastVisibleTime()).to.equal(1); // Back to visible. clock.tick(1); @@ -336,7 +340,7 @@ describes.sandboxed('Viewer', {}, (env) => { state: 'visible', }); expect(ampdoc.isVisible()).to.be.true; - expect(ampdoc.getFirstVisibleTime()).to.equal(0); + expect(ampdoc.getFirstVisibleTime()).to.equal(1); expect(ampdoc.getLastVisibleTime()).to.equal(2); // Back to invisible again. @@ -345,7 +349,7 @@ describes.sandboxed('Viewer', {}, (env) => { state: 'hidden', }); expect(ampdoc.isVisible()).to.be.false; - expect(ampdoc.getFirstVisibleTime()).to.equal(0); + expect(ampdoc.getFirstVisibleTime()).to.equal(1); expect(ampdoc.getLastVisibleTime()).to.equal(2); }); diff --git a/test/unit/test-viewport.js b/test/unit/test-viewport.js index 82b46a4ee6ff..63e9f8e2ae6b 100644 --- a/test/unit/test-viewport.js +++ b/test/unit/test-viewport.js @@ -23,6 +23,8 @@ import {installVsyncService} from '#service/vsync-impl'; import {loadPromise} from '#utils/event-helper'; import {dev} from '#utils/log'; +import {FakePerformance} from '#testing/fake-dom'; + import {getMode} from '../../src/mode'; import {setParentWindow} from '../../src/service-helpers'; @@ -1648,6 +1650,7 @@ describes.sandboxed('Viewport META', {}, (env) => { clearTimeout: window.clearTimeout, location: {}, Promise: window.Promise, + performance: new FakePerformance(window), }; installTimerService(windowApi); installVsyncService(windowApi); diff --git a/testing/fake-dom.js b/testing/fake-dom.js index b0c85a66bbf3..3e32137ef38c 100644 --- a/testing/fake-dom.js +++ b/testing/fake-dom.js @@ -210,6 +210,9 @@ export class FakeWindow { /** @const */ this.Date = window.Date; + /** @const */ + this.performance = new FakePerformance(this); + /** polyfill setTimeout. */ this.setTimeout = function () { return window.setTimeout.apply(window, arguments); @@ -751,6 +754,21 @@ export class FakeMutationObserver { } } +export class FakePerformance { + constructor(win) { + /** @const */ + this.win_ = win; + } + + get timeOrigin() { + return 1; + } + + now() { + return this.win_.Date.now(); + } +} + /** * @param {!Object} obj * @return {!Object}