diff --git a/packages/polyfills/__tests__/consoleTimers-itest.js b/packages/polyfills/__tests__/consoleTimers-itest.js new file mode 100644 index 000000000000..ae762adca0b8 --- /dev/null +++ b/packages/polyfills/__tests__/consoleTimers-itest.js @@ -0,0 +1,163 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + * @fantom_mode * + */ + +const LOG_LEVELS = { + trace: 0, + info: 1, + warn: 2, + error: 3, +}; + +describe('console.time / console.timeEnd / console.timeLog', () => { + let originalNativeLoggingHook; + let logFn; + + beforeEach(() => { + originalNativeLoggingHook = global.nativeLoggingHook; + logFn = global.nativeLoggingHook = jest.fn(); + }); + + afterEach(() => { + global.nativeLoggingHook = originalNativeLoggingHook; + }); + + it('should log elapsed time on timeEnd', () => { + console.time('test'); + console.timeEnd('test'); + + expect(logFn).toHaveBeenCalledTimes(1); + const message = logFn.mock.calls[0][0]; + expect(message).toMatch(/^test: \d+(\.\d+)?ms$/); + expect(logFn.mock.calls[0][1]).toBe(LOG_LEVELS.info); + }); + + it('should use "default" label when none is provided', () => { + console.time(); + console.timeEnd(); + + const message = logFn.mock.calls[0][0]; + expect(message).toMatch(/^default: \d+(\.\d+)?ms$/); + }); + + it('should warn when starting a timer that already exists', () => { + console.time('dup'); + console.time('dup'); + + expect(logFn).toHaveBeenCalledWith( + 'Timer "dup" already exists', + LOG_LEVELS.warn, + ); + + // Clean up + console.timeEnd('dup'); + }); + + it('should warn when ending a timer that does not exist', () => { + console.timeEnd('nonexistent'); + + expect(logFn).toHaveBeenCalledWith( + 'Timer "nonexistent" does not exist', + LOG_LEVELS.warn, + ); + }); + + it('should log elapsed time with timeLog without stopping the timer', () => { + console.time('ongoing'); + console.timeLog('ongoing'); + console.timeLog('ongoing'); + console.timeEnd('ongoing'); + + // timeLog called twice + timeEnd called once = 3 info logs + expect(logFn).toHaveBeenCalledTimes(3); + for (let i = 0; i < 3; i++) { + expect(logFn.mock.calls[i][0]).toMatch(/^ongoing: \d+(\.\d+)?ms/); + } + }); + + it('should warn when calling timeLog on a nonexistent timer', () => { + console.timeLog('ghost'); + + expect(logFn).toHaveBeenCalledWith( + 'Timer "ghost" does not exist', + LOG_LEVELS.warn, + ); + }); + + it('should support multiple concurrent timers', () => { + console.time('a'); + console.time('b'); + console.timeEnd('a'); + console.timeEnd('b'); + + expect(logFn).toHaveBeenCalledTimes(2); + expect(logFn.mock.calls[0][0]).toMatch(/^a: /); + expect(logFn.mock.calls[1][0]).toMatch(/^b: /); + }); +}); + +describe('console.count / console.countReset', () => { + let originalNativeLoggingHook; + let logFn; + + beforeEach(() => { + originalNativeLoggingHook = global.nativeLoggingHook; + logFn = global.nativeLoggingHook = jest.fn(); + }); + + afterEach(() => { + global.nativeLoggingHook = originalNativeLoggingHook; + }); + + it('should increment and log the count', () => { + console.count('clicks'); + console.count('clicks'); + console.count('clicks'); + + expect(logFn).toHaveBeenCalledTimes(3); + expect(logFn.mock.calls[0][0]).toBe('clicks: 1'); + expect(logFn.mock.calls[1][0]).toBe('clicks: 2'); + expect(logFn.mock.calls[2][0]).toBe('clicks: 3'); + }); + + it('should use "default" label when none is provided', () => { + console.count(); + + expect(logFn.mock.calls[0][0]).toMatch(/^default: \d+$/); + }); + + it('should reset the count', () => { + console.count('resets'); + console.count('resets'); + console.countReset('resets'); + console.count('resets'); + + expect(logFn.mock.calls[2][0]).toBe('resets: 1'); + }); + + it('should warn when resetting a nonexistent counter', () => { + console.countReset('nope'); + + expect(logFn).toHaveBeenCalledWith( + 'Count for "nope" does not exist', + LOG_LEVELS.warn, + ); + }); + + it('should track separate labels independently', () => { + console.count('a'); + console.count('b'); + console.count('a'); + + expect(logFn.mock.calls[0][0]).toBe('a: 1'); + expect(logFn.mock.calls[1][0]).toBe('b: 1'); + expect(logFn.mock.calls[2][0]).toBe('a: 2'); + }); +}); diff --git a/packages/polyfills/console.js b/packages/polyfills/console.js index 4968c3ed9466..d8cc601581f1 100644 --- a/packages/polyfills/console.js +++ b/packages/polyfills/console.js @@ -571,6 +571,90 @@ function consoleAssertPolyfill(expression, label) { function stub() {} +// Use high-resolution timer if available, fall back to Date.now +var now = global.nativePerformanceNow || Date.now; + +// console.time / console.timeLog / console.timeEnd +// https://console.spec.whatwg.org/#timing +var timerTable = {}; + +function consoleTimePolyfill(label) { + var name = label === undefined ? 'default' : '' + label; + if (timerTable[name] !== undefined) { + global.nativeLoggingHook( + 'Timer "' + name + '" already exists', + LOG_LEVELS.warn, + ); + return; + } + timerTable[name] = now(); +} + +function consoleTimeEndPolyfill(label) { + var name = label === undefined ? 'default' : '' + label; + var startTime = timerTable[name]; + if (startTime === undefined) { + global.nativeLoggingHook( + 'Timer "' + name + '" does not exist', + LOG_LEVELS.warn, + ); + return; + } + delete timerTable[name]; + var elapsed = now() - startTime; + global.nativeLoggingHook(name + ': ' + elapsed + 'ms', LOG_LEVELS.info); +} + +function consoleTimeLogPolyfill(label) { + var name = label === undefined ? 'default' : '' + label; + var startTime = timerTable[name]; + if (startTime === undefined) { + global.nativeLoggingHook( + 'Timer "' + name + '" does not exist', + LOG_LEVELS.warn, + ); + return; + } + var elapsed = now() - startTime; + var extra = + arguments.length > 1 + ? ' ' + + Array.prototype.slice + .call(arguments, 1) + .map(function (arg) { + return inspect(arg, {depth: 10}); + }) + .join(' ') + : ''; + global.nativeLoggingHook( + name + ': ' + elapsed + 'ms' + extra, + LOG_LEVELS.info, + ); +} + +// console.count / console.countReset +// https://console.spec.whatwg.org/#counting +var countTable = {}; + +function consoleCountPolyfill(label) { + var name = label === undefined ? 'default' : '' + label; + var count = (countTable[name] || 0) + 1; + countTable[name] = count; + global.nativeLoggingHook(name + ': ' + count, LOG_LEVELS.info); +} + +function consoleCountResetPolyfill(label) { + var name = label === undefined ? 'default' : '' + label; + if (countTable[name] === undefined) { + global.nativeLoggingHook( + 'Count for "' + name + '" does not exist', + LOG_LEVELS.warn, + ); + return; + } + countTable[name] = 0; +} + // https://developer.chrome.com/docs/devtools/console/api#createtask function consoleCreateTaskStub() { return {run: cb => cb()}; @@ -587,11 +671,12 @@ if (global.nativeLoggingHook) { } global.console = { - time: stub, - timeEnd: stub, + time: consoleTimePolyfill, + timeEnd: consoleTimeEndPolyfill, + timeLog: consoleTimeLogPolyfill, timeStamp: stub, - count: stub, - countReset: stub, + count: consoleCountPolyfill, + countReset: consoleCountResetPolyfill, createTask: consoleCreateTaskStub, ...(originalConsole ?? {}), error: getNativeLogFunction(LOG_LEVELS.error),