/** * @license * Copyright (C) 2016 The Android Open Source Project * * 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 '../../test/common-test-setup-karma.js'; import {GrReporting, DEFAULT_STARTUP_TIMERS, initErrorReporter} from './gr-reporting_impl.js'; import {appContext} from '../app-context.js'; suite('gr-reporting tests', () => { let service; let clock; let fakePerformance; const NOW_TIME = 100; setup(() => { clock = sinon.useFakeTimers(NOW_TIME); service = new GrReporting(appContext.flagsService); service._baselines = {...DEFAULT_STARTUP_TIMERS}; sinon.stub(service, 'reporter'); }); teardown(() => { clock.restore(); }); test('appStarted', () => { fakePerformance = { navigationStart: 1, loadEventEnd: 2, }; fakePerformance.toJSON = () => fakePerformance; sinon.stub(service, 'performanceTiming').get(() => fakePerformance); sinon.stub(window.performance, 'now').returns(42); service.appStarted(); assert.isTrue( service.reporter.calledWithMatch( 'timing-report', 'UI Latency', 'App Started', 42 )); assert.isTrue( service.reporter.calledWithExactly( 'timing-report', 'UI Latency', 'NavResTime - loadEventEnd', fakePerformance.loadEventEnd - fakePerformance.navigationStart, undefined, true) ); }); test('WebComponentsReady', () => { sinon.stub(window.performance, 'now').returns(42); service.timeEnd('WebComponentsReady'); assert.isTrue(service.reporter.calledWithMatch( 'timing-report', 'UI Latency', 'WebComponentsReady', 42 )); }); test('beforeLocationChanged', () => { service._baselines['garbage'] = 'monster'; sinon.stub(service, 'time'); service.beforeLocationChanged(); assert.isTrue(service.time.calledWithExactly('DashboardDisplayed')); assert.isTrue(service.time.calledWithExactly('ChangeDisplayed')); assert.isTrue(service.time.calledWithExactly('ChangeFullyLoaded')); assert.isTrue(service.time.calledWithExactly('DiffViewDisplayed')); assert.isTrue(service.time.calledWithExactly('FileListDisplayed')); assert.isFalse(service._baselines.hasOwnProperty('garbage')); }); test('changeDisplayed', () => { sinon.spy(service, 'timeEnd'); service.changeDisplayed(); assert.isFalse(service.timeEnd.calledWith('ChangeDisplayed')); assert.isTrue(service.timeEnd.calledWith('StartupChangeDisplayed')); service.changeDisplayed(); assert.isTrue(service.timeEnd.calledWith('ChangeDisplayed')); }); test('changeFullyLoaded', () => { sinon.spy(service, 'timeEnd'); service.changeFullyLoaded(); assert.isFalse( service.timeEnd.calledWithExactly('ChangeFullyLoaded')); assert.isTrue( service.timeEnd.calledWithExactly('StartupChangeFullyLoaded')); service.changeFullyLoaded(); assert.isTrue(service.timeEnd.calledWithExactly('ChangeFullyLoaded')); }); test('diffViewDisplayed', () => { sinon.spy(service, 'timeEnd'); service.diffViewDisplayed(); assert.isFalse(service.timeEnd.calledWith('DiffViewDisplayed')); assert.isTrue(service.timeEnd.calledWith('StartupDiffViewDisplayed')); service.diffViewDisplayed(); assert.isTrue(service.timeEnd.calledWith('DiffViewDisplayed')); }); test('fileListDisplayed', () => { sinon.spy(service, 'timeEnd'); service.fileListDisplayed(); assert.isFalse( service.timeEnd.calledWithExactly('FileListDisplayed')); assert.isTrue( service.timeEnd.calledWithExactly('StartupFileListDisplayed')); service.fileListDisplayed(); assert.isTrue(service.timeEnd.calledWithExactly('FileListDisplayed')); }); test('dashboardDisplayed', () => { sinon.spy(service, 'timeEnd'); service.dashboardDisplayed(); assert.isFalse(service.timeEnd.calledWith('DashboardDisplayed')); assert.isTrue(service.timeEnd.calledWith('StartupDashboardDisplayed')); service.dashboardDisplayed(); assert.isTrue(service.timeEnd.calledWith('DashboardDisplayed')); }); test('dashboardDisplayed details', () => { sinon.spy(service, 'timeEnd'); sinon.stub(window, 'performance').value( { memory: { usedJSHeapSize: 1024 * 1024, }, measure: () => {}, now: () => { 42; }, }); service.reportRpcTiming('/changes/*~*/comments', 500); service.dashboardDisplayed(); assert.isTrue( service.timeEnd.calledWithExactly('StartupDashboardDisplayed', {rpcList: [ { anonymizedUrl: '/changes/*~*/comments', elapsed: 500, }, ], screenSize: { width: window.screen.width, height: window.screen.height, }, viewport: { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight, }, usedJSHeapSizeMb: 1, hiddenDurationMs: 0, } )); }); suite('hidden duration', () => { let nowStub; let visibilityStateStub; const assertHiddenDurationsMs = hiddenDurationMs => { service.dashboardDisplayed(); assert.isTrue( service.timeEnd.calledWithMatch('StartupDashboardDisplayed', {hiddenDurationMs} )); }; setup(() => { sinon.spy(service, 'timeEnd'); nowStub = sinon.stub(window.performance, 'now'); visibilityStateStub = { value: value => { Object.defineProperty(document, 'visibilityState', {value, configurable: true}); }, }; }); test('starts in hidden', () => { nowStub.returns(10); visibilityStateStub.value('hidden'); service.onVisibilityChange(); nowStub.returns(15); visibilityStateStub.value('visible'); service.onVisibilityChange(); assertHiddenDurationsMs(5); }); test('full in hidden', () => { nowStub.returns(10); visibilityStateStub.value('hidden'); assertHiddenDurationsMs(10); }); test('full in visible', () => { nowStub.returns(10); visibilityStateStub.value('visible'); assertHiddenDurationsMs(0); }); test('accumulated', () => { nowStub.returns(10); visibilityStateStub.value('hidden'); service.onVisibilityChange(); nowStub.returns(15); visibilityStateStub.value('visible'); service.onVisibilityChange(); nowStub.returns(20); visibilityStateStub.value('hidden'); service.onVisibilityChange(); nowStub.returns(25); assertHiddenDurationsMs(10); }); test('reset after location change', () => { nowStub.returns(10); visibilityStateStub.value('hidden'); assertHiddenDurationsMs(10); visibilityStateStub.value('visible'); nowStub.returns(15); service.beforeLocationChanged(); service.timeEnd.resetHistory(); service.dashboardDisplayed(); assert.isTrue( service.timeEnd.calledWithMatch('DashboardDisplayed', {hiddenDurationMs: 0} )); }); }); test('time and timeEnd', () => { const nowStub = sinon.stub(window.performance, 'now').returns(0); service.time('foo'); nowStub.returns(1); service.time('bar'); nowStub.returns(2); service.timeEnd('bar'); nowStub.returns(3); service.timeEnd('foo'); assert.isTrue(service.reporter.calledWithMatch( 'timing-report', 'UI Latency', 'foo', 3 )); assert.isTrue(service.reporter.calledWithMatch( 'timing-report', 'UI Latency', 'bar', 1 )); }); test('timer object', () => { const nowStub = sinon.stub(window.performance, 'now').returns(100); const timer = service.getTimer('foo-bar'); nowStub.returns(150); timer.end(); assert.isTrue(service.reporter.calledWithMatch( 'timing-report', 'UI Latency', 'foo-bar', 50)); }); test('timer object double call', () => { const timer = service.getTimer('foo-bar'); timer.end(); assert.isTrue(service.reporter.calledOnce); assert.throws(() => { timer.end(); }, 'Timer for "foo-bar" already ended.'); }); test('timer object maximum', () => { const nowStub = sinon.stub(window.performance, 'now').returns(100); const timer = service.getTimer('foo-bar').withMaximum(100); nowStub.returns(150); timer.end(); assert.isTrue(service.reporter.calledOnce); timer.reset(); nowStub.returns(260); timer.end(); assert.isTrue(service.reporter.calledOnce); }); test('recordDraftInteraction', () => { const key = 'TimeBetweenDraftActions'; const nowStub = sinon.stub(window.performance, 'now').returns(100); const timingStub = sinon.stub(service, '_reportTiming'); service.recordDraftInteraction(); assert.isFalse(timingStub.called); nowStub.returns(200); service.recordDraftInteraction(); assert.isTrue(timingStub.calledOnce); assert.equal(timingStub.lastCall.args[0], key); assert.equal(timingStub.lastCall.args[1], 100); nowStub.returns(350); service.recordDraftInteraction(); assert.isTrue(timingStub.calledTwice); assert.equal(timingStub.lastCall.args[0], key); assert.equal(timingStub.lastCall.args[1], 150); nowStub.returns(370 + 2 * 60 * 1000); service.recordDraftInteraction(); assert.isFalse(timingStub.calledThrice); }); test('timeEndWithAverage', () => { const nowStub = sinon.stub(window.performance, 'now').returns(0); nowStub.returns(1000); service.time('foo'); nowStub.returns(1100); service.timeEndWithAverage('foo', 'bar', 10); assert.isTrue(service.reporter.calledTwice); assert.isTrue(service.reporter.calledWithMatch( 'timing-report', 'UI Latency', 'foo', 100)); assert.isTrue(service.reporter.calledWithMatch( 'timing-report', 'UI Latency', 'bar', 10)); }); test('reportExtension', () => { service.reportExtension('foo'); assert.isTrue(service.reporter.calledWithExactly( 'lifecycle', 'Extension detected', 'foo' )); }); test('reportInteraction', () => { service.reporter.restore(); sinon.spy(service, '_reportEvent'); service.pluginsLoaded(); // so we don't cache service.reportInteraction('button-click', {name: 'sendReply'}); assert.isTrue(service._reportEvent.getCall(2).calledWithMatch( { type: 'interaction', name: 'button-click', eventDetails: JSON.stringify({name: 'sendReply'}), } )); }); test('report start time', () => { service.reporter.restore(); sinon.stub(window.performance, 'now').returns(42); sinon.spy(service, '_reportEvent'); const dispatchStub = sinon.spy(document, 'dispatchEvent'); service.pluginsLoaded(); service.time('timeAction'); service.timeEnd('timeAction'); assert.isTrue(service._reportEvent.getCall(2).calledWithMatch( { type: 'timing-report', category: 'UI Latency', name: 'timeAction', value: 0, eventStart: 42, } )); assert.equal(dispatchStub.getCall(2).args[0].detail.eventStart, 42); }); suite('plugins', () => { setup(() => { service.reporter.restore(); sinon.stub(service, '_reportEvent'); }); test('pluginsLoaded reports time', () => { sinon.stub(window.performance, 'now').returns(42); service.pluginsLoaded(); assert.isTrue(service._reportEvent.calledWithMatch( { type: 'timing-report', category: 'UI Latency', name: 'PluginsLoaded', value: 42, } )); }); test('pluginsLoaded reports plugins', () => { service.pluginsLoaded(['foo', 'bar']); assert.isTrue(service._reportEvent.calledWithMatch( { type: 'lifecycle', category: 'Plugins installed', eventDetails: JSON.stringify({pluginsList: ['foo', 'bar']}), } )); }); test('caches reports if plugins are not loaded', () => { service.timeEnd('foo'); assert.isFalse(service._reportEvent.called); }); test('reports if plugins are loaded', () => { service.pluginsLoaded(); assert.isTrue(service._reportEvent.called); }); test('reports if metrics plugin xyz is loaded', () => { service.pluginLoaded('metrics-xyz'); assert.isTrue(service._reportEvent.called); }); test('reports cached events preserving order', () => { service.time('foo'); service.time('bar'); service.timeEnd('foo'); service.pluginsLoaded(); service.timeEnd('bar'); assert.isTrue(service._reportEvent.getCall(0).calledWithMatch( {type: 'timing-report', category: 'UI Latency', name: 'foo'} )); assert.isTrue(service._reportEvent.getCall(1).calledWithMatch( {type: 'timing-report', category: 'UI Latency', name: 'PluginsLoaded'} )); assert.isTrue(service._reportEvent.getCall(2).calledWithMatch( {type: 'lifecycle', category: 'Plugins installed'} )); assert.isTrue(service._reportEvent.getCall(3).calledWithMatch( {type: 'timing-report', category: 'UI Latency', name: 'bar'} )); }); }); test('search', () => { service.locationChanged('_handleSomeRoute'); assert.isTrue(service.reporter.calledWithExactly( 'nav-report', 'Location Changed', 'Page', '_handleSomeRoute')); }); suite('exception logging', () => { let fakeWindow; let reporter; const emulateThrow = function(msg, url, line, column, error) { return fakeWindow.onerror(msg, url, line, column, error); }; setup(() => { reporter = service.reporter; fakeWindow = { handlers: {}, addEventListener(type, handler) { this.handlers[type] = handler; }, }; sinon.stub(console, 'error'); Object.defineProperty(appContext, 'reportingService', { get() { return service; }, }); const errorReporter = initErrorReporter(appContext); errorReporter.catchErrors(fakeWindow); }); test('is reported', () => { const error = new Error('bar'); error.stack = undefined; emulateThrow('bar', 'http://url', 4, 2, error); assert.isTrue(reporter.calledWith('error', 'exception', 'onError: bar')); }); test('is reported with stack', () => { const error = new Error('bar'); emulateThrow('bar', 'http://url', 4, 2, error); const eventDetails = reporter.lastCall.args[4]; assert.equal(error.stack, eventDetails.stack); }); test('prevent default event handler', () => { assert.isTrue(emulateThrow()); }); test('unhandled rejection', () => { const newError = new Error('bar'); fakeWindow.handlers['unhandledrejection']({reason: newError}); assert.isTrue(reporter.calledWith('error', 'exception', 'unhandledrejection: bar')); }); }); });