diff --git a/src/api/test-controller/index.js b/src/api/test-controller/index.js index 472e6710362..0b94dcf0832 100644 --- a/src/api/test-controller/index.js +++ b/src/api/test-controller/index.js @@ -24,6 +24,7 @@ import { SwitchToMainWindowCommand, SetNativeDialogHandlerCommand, GetNativeDialogHistoryCommand, + GetBrowserConsoleMessagesCommand, SetTestSpeedCommand, SetPageLoadTimeoutCommand, UseRoleCommand @@ -239,6 +240,12 @@ export default class TestController { return this.testRun.executeCommand(new GetNativeDialogHistoryCommand(), callsite); } + _getBrowserConsoleMessages$ () { + var callsite = getCallsiteForMethod('getBrowserConsoleMessages'); + + return this.testRun.executeCommand(new GetBrowserConsoleMessagesCommand(), callsite); + } + _expect$ (actual) { return new Assertion(actual, this); } diff --git a/src/client/driver/driver.js b/src/client/driver/driver.js index 026adc337c1..a088b175038 100644 --- a/src/client/driver/driver.js +++ b/src/client/driver/driver.js @@ -27,6 +27,8 @@ import { CurrentIframeNotFoundError, CurrentIframeIsInvisibleError } from '../../errors/test-run'; + +import BrowserConsoleMessages from '../../test-run/browser-console-messages'; import NativeDialogTracker from './native-dialog-tracker'; import { SetNativeDialogHandlerMessage, TYPE as MESSAGE_TYPE } from './driver-link/messages'; @@ -59,6 +61,7 @@ const ACTIVE_IFRAME_SELECTOR = 'testcafe|driver|active-iframe-sele const TEST_SPEED = 'testcafe|driver|test-speed'; const ASSERTION_RETRIES_TIMEOUT = 'testcafe|driver|assertion-retries-timeout'; const ASSERTION_RETRIES_START_TIME = 'testcafe|driver|assertion-retries-start-time'; +const CONSOLE_MESSAGES = 'testcafe|driver|console-messages'; const CHECK_IFRAME_DRIVER_LINK_DELAY = 500; const ACTION_IFRAME_ERROR_CTORS = { @@ -114,6 +117,7 @@ export default class Driver { hammerhead.on(hammerhead.EVENTS.uncaughtJsError, err => this._onJsError(err)); hammerhead.on(hammerhead.EVENTS.unhandledRejection, err => this._onJsError(err)); + hammerhead.on(hammerhead.EVENTS.consoleMethCalled, e => this._onConsoleMessage(e)); } set speed (val) { @@ -124,6 +128,14 @@ export default class Driver { return this.contextStorage.getItem(TEST_SPEED); } + get consoleMessages () { + return new BrowserConsoleMessages(this.contextStorage.getItem(CONSOLE_MESSAGES)); + } + + set consoleMessages (messages) { + return this.contextStorage.setItem(CONSOLE_MESSAGES, messages ? messages.getCopy() : null); + } + // Error handling _onJsError (err) { // NOTE: we should not send any message to the server if we've @@ -156,6 +168,26 @@ export default class Driver { return false; } + // Console messages + _onConsoleMessage (e) { + const meth = e.meth; + + const args = e.args.map(arg => { + if (arg === null) + return 'null'; + + if (arg === void 0) + return 'undefined'; + + return arg.toString(); + }); + + const messages = this.consoleMessages; + + messages.addMessage(meth, Array.prototype.slice.call(args).join(' ')); + + this.consoleMessages = messages; + } // Status _addPendingErrorToStatus (status) { @@ -173,12 +205,18 @@ export default class Driver { status.pageError = status.pageError || dialogError; } + _addConsoleMessagesToStatus (status) { + status.consoleMessages = this.consoleMessages; + this.consoleMessages = null; + } + _sendStatus (status) { // NOTE: We should not modify the status if it is resent after // the page load because the server has cached the response if (!status.resent) { this._addPendingErrorToStatus(status); this._addUnexpectedDialogErrorToStatus(status); + this._addConsoleMessagesToStatus(status); } this.contextStorage.setItem(PENDING_STATUS, status); @@ -327,6 +365,10 @@ export default class Driver { })); } + _onGetBrowserConsoleMessagesCommand () { + this._onReady(new DriverStatus({ isCommandResult: true })); + } + _onNavigateToCommand (command) { this.contextStorage.setItem(this.COMMAND_EXECUTING_FLAG, true); @@ -489,6 +531,9 @@ export default class Driver { else if (command.type === COMMAND_TYPE.getNativeDialogHistory) this._onGetNativeDialogHistoryCommand(command); + else if (command.type === COMMAND_TYPE.getBrowserConsoleMessages) + this._onGetBrowserConsoleMessagesCommand(command); + else if (command.type === COMMAND_TYPE.setTestSpeed) this._onSetTestSpeedCommand(command); diff --git a/src/client/driver/status.js b/src/client/driver/status.js index 613e6de516b..b805a2180af 100644 --- a/src/client/driver/status.js +++ b/src/client/driver/status.js @@ -12,6 +12,7 @@ export default class DriverStatus extends Assignable { this.pageError = null; this.resent = false; this.result = null; + this.consoleMessages = null; this._assignFrom(obj, true); } @@ -21,7 +22,8 @@ export default class DriverStatus extends Assignable { { name: 'isCommandResult' }, { name: 'executionError' }, { name: 'pageError' }, - { name: 'result' } + { name: 'result' }, + { name: 'consoleMessages' } ]; } } diff --git a/src/test-run/bookmark.js b/src/test-run/bookmark.js index ac990f49b51..9e3db23508c 100644 --- a/src/test-run/bookmark.js +++ b/src/test-run/bookmark.js @@ -28,6 +28,7 @@ export default class TestRunBookmark { this.pageLoadTimeout = testRun.pageLoadTimeout; this.ctx = testRun.ctx; this.fixtureCtx = testRun.fixtureCtx; + this.consoleMessages = testRun.consoleMessages; } async init () { @@ -94,8 +95,9 @@ export default class TestRunBookmark { this.testRun.phase = TEST_RUN_PHASE.inBookmarkRestore; - this.testRun.ctx = this.ctx; - this.testRun.fixtureCtx = this.fixtureCtx; + this.testRun.ctx = this.ctx; + this.testRun.fixtureCtx = this.fixtureCtx; + this.testRun.consoleMessages = this.consoleMessages; try { await this._restoreSpeed(); diff --git a/src/test-run/browser-console-messages.js b/src/test-run/browser-console-messages.js new file mode 100644 index 00000000000..5c7db976990 --- /dev/null +++ b/src/test-run/browser-console-messages.js @@ -0,0 +1,46 @@ +// ------------------------------------------------------------- +// WARNING: this file is used by both the client and the server. +// Do not use any browser or node-specific API! +// ------------------------------------------------------------- +import { assignIn } from 'lodash'; +import Assignable from '../utils/assignable'; + + +export default class BrowserConsoleMessages extends Assignable { + constructor (obj) { + super(); + + this.log = []; + this.info = []; + this.warn = []; + this.error = []; + + this._assignFrom(obj); + } + + _getAssignableProperties () { + return [ + { name: 'log' }, + { name: 'info' }, + { name: 'warn' }, + { name: 'error' } + ]; + } + + concat (consoleMessages) { + this.log = this.log.concat(consoleMessages.log); + this.info = this.info.concat(consoleMessages.info); + this.warn = this.warn.concat(consoleMessages.warn); + this.error = this.error.concat(consoleMessages.error); + } + + addMessage (type, msg) { + this[type].push(msg); + } + + getCopy () { + const { log, info, warn, error } = this; + + return assignIn({}, { log, info, warn, error }); + } +} diff --git a/src/test-run/commands/actions.js b/src/test-run/commands/actions.js index 26f19a70ae2..7b00a5bd80f 100644 --- a/src/test-run/commands/actions.js +++ b/src/test-run/commands/actions.js @@ -418,6 +418,12 @@ export class GetNativeDialogHistoryCommand { } } +export class GetBrowserConsoleMessagesCommand { + constructor () { + this.type = TYPE.getBrowserConsoleMessages; + } +} + export class SetTestSpeedCommand extends Assignable { constructor (obj) { super(obj); diff --git a/src/test-run/commands/type.js b/src/test-run/commands/type.js index 5e1be877b48..bebd3807c04 100644 --- a/src/test-run/commands/type.js +++ b/src/test-run/commands/type.js @@ -34,6 +34,7 @@ export default { switchToMainWindow: 'switch-to-main-window', setNativeDialogHandler: 'set-native-dialog-handler', getNativeDialogHistory: 'get-native-dialog-history', + getBrowserConsoleMessages: 'get-browser-console-messages', setTestSpeed: 'set-test-speed', setPageLoadTimeout: 'set-page-load-timeout', debug: 'debug', diff --git a/src/test-run/index.js b/src/test-run/index.js index 02029124939..ce8a560b4e8 100644 --- a/src/test-run/index.js +++ b/src/test-run/index.js @@ -22,7 +22,7 @@ import ROLE_PHASE from '../role/phase'; import TestRunBookmark from './bookmark'; import ClientFunctionBuilder from '../client-functions/client-function-builder'; import ReporterPluginHost from '../reporter/plugin-host'; - +import BrowserConsoleMessages from './browser-console-messages'; import { TakeScreenshotOnFailCommand } from './commands/browser-manipulation'; import { SetNativeDialogHandlerCommand, SetTestSpeedCommand, SetPageLoadTimeoutCommand } from './commands/actions'; @@ -73,6 +73,8 @@ export default class TestRun extends Session { this.speed = this.opts.speed; this.pageLoadTimeout = this.opts.pageLoadTimeout; + this.consoleMessages = new BrowserConsoleMessages(); + this.pendingRequest = null; this.pendingPageError = null; @@ -269,6 +271,12 @@ export default class TestRun extends Session { return this.executeCommand(new PrepareBrowserManipulationCommand(command.type), callsite); } + async _enqueueBrowserConsoleMessagesCommand (command, callsite) { + await this._enqueueCommand(command, callsite); + + return this.consoleMessages.getCopy(); + } + async _enqueueSetBreakpointCommand (callsite, error) { debugLogger.showBreakpoint(this.id, this.browserConnection.userAgent, callsite, error); @@ -344,6 +352,8 @@ export default class TestRun extends Session { var currentTaskRejectedByError = pageError && this._handlePageErrorStatus(pageError); + this.consoleMessages.concat(driverStatus.consoleMessages); + if (!currentTaskRejectedByError && driverStatus.isCommandResult) { if (this.currentDriverTask.command.type === COMMAND_TYPE.testDone) { this._resolveCurrentDriverTask(); @@ -426,6 +436,9 @@ export default class TestRun extends Session { if (command.type === COMMAND_TYPE.assertion) return this._executeAssertion(command, callsite); + if (command.type === COMMAND_TYPE.getBrowserConsoleMessages) + return await this._enqueueBrowserConsoleMessagesCommand(command, callsite); + return this._enqueueCommand(command, callsite); } @@ -448,8 +461,9 @@ export default class TestRun extends Session { } async switchToCleanRun () { - this.ctx = Object.create(null); - this.fixtureCtx = Object.create(null); + this.ctx = Object.create(null); + this.fixtureCtx = Object.create(null); + this.consoleMessages = new BrowserConsoleMessages(); this.useStateSnapshot(null); diff --git a/test/functional/fixtures/api/es-next/console/pages/empty.html b/test/functional/fixtures/api/es-next/console/pages/empty.html new file mode 100644 index 00000000000..bf22ae584aa --- /dev/null +++ b/test/functional/fixtures/api/es-next/console/pages/empty.html @@ -0,0 +1,8 @@ + + + + Console messages (empty page) + + + + diff --git a/test/functional/fixtures/api/es-next/console/pages/index.html b/test/functional/fixtures/api/es-next/console/pages/index.html new file mode 100644 index 00000000000..b56df5d8467 --- /dev/null +++ b/test/functional/fixtures/api/es-next/console/pages/index.html @@ -0,0 +1,27 @@ + + + + Console messages + + + + +Reload + + + + diff --git a/test/functional/fixtures/api/es-next/console/test.js b/test/functional/fixtures/api/es-next/console/test.js new file mode 100644 index 00000000000..c3433a1a67e --- /dev/null +++ b/test/functional/fixtures/api/es-next/console/test.js @@ -0,0 +1,9 @@ +describe('[API] t.getBrowserConsoleMessages()', function () { + it('Should return messages from the console', function () { + return runTests('./testcafe-fixtures/console-test.js', 't.getBrowserConsoleMessages'); + }); + + it('Should format messages if several args were passed', function () { + return runTests('./testcafe-fixtures/console-test.js', 'messages formatting'); + }); +}); diff --git a/test/functional/fixtures/api/es-next/console/testcafe-fixtures/console-test.js b/test/functional/fixtures/api/es-next/console/testcafe-fixtures/console-test.js new file mode 100644 index 00000000000..40b034b0a8e --- /dev/null +++ b/test/functional/fixtures/api/es-next/console/testcafe-fixtures/console-test.js @@ -0,0 +1,43 @@ +fixture `getBrowserConsoleMessages`; + + +test + .page `http://localhost:3000/fixtures/api/es-next/console/pages/index.html` +('t.getBrowserConsoleMessages', async t => { + let messages = await t.getBrowserConsoleMessages(); + + await t + .expect(messages.error).eql(['error1']) + .expect(messages.warn).eql(['warn1']) + .expect(messages.log).eql(['log1']) + .expect(messages.info).eql(['info1']) + + .click('#trigger-messages') + + // Check the driver keeps the messages between page reloads + .click('#reload'); + + // Changes in the getBrowserConsoleMessages result object should + // not affect the console messages state in the test run. + messages.log.push('unexpected'); + + messages = await t.getBrowserConsoleMessages(); + + await t + .expect(messages.error).eql(['error1', 'error2']) + .expect(messages.warn).eql(['warn1', 'warn2']) + .expect(messages.log).eql(['log1', 'log2']) + .expect(messages.info).eql(['info1', 'info2']); +}); + +test + .page `http://localhost:3000/fixtures/api/es-next/console/pages/empty.html` +('messages formatting', async t => { + /* eslint-disable no-console */ + await t.eval(() => console.log('a', 1, null, void 0, ['b', 2], { c: 3 })); + /* eslint-enable no-console */ + + const { log } = await t.getBrowserConsoleMessages(); + + await t.expect(log[0]).eql('a 1 null undefined b,2 [object Object]'); +}); diff --git a/test/functional/fixtures/api/es-next/roles/test.js b/test/functional/fixtures/api/es-next/roles/test.js index 2c6c5a9753f..f85d1f4f389 100644 --- a/test/functional/fixtures/api/es-next/roles/test.js +++ b/test/functional/fixtures/api/es-next/roles/test.js @@ -23,7 +23,7 @@ describe('[API] t.useRole()', function () { return runTests('./testcafe-fixtures/configuration-test.js', 'Clear configuration', TEST_WITH_IFRAME_FAILED_RUN_OPTIONS) .catch(function (errs) { expect(errs[0]).contains('- Error in Role initializer - A native alert dialog was invoked'); - expect(errs[0]).contains('> 32 | await t.click(showAlertBtn);'); + expect(errs[0]).contains('> 34 | await t.click(showAlertBtn);'); }); }); diff --git a/test/functional/fixtures/api/es-next/roles/testcafe-fixtures/configuration-test.js b/test/functional/fixtures/api/es-next/roles/testcafe-fixtures/configuration-test.js index d8c484a86b1..bbca39db327 100644 --- a/test/functional/fixtures/api/es-next/roles/testcafe-fixtures/configuration-test.js +++ b/test/functional/fixtures/api/es-next/roles/testcafe-fixtures/configuration-test.js @@ -1,5 +1,4 @@ import { Selector, Role, t } from 'testcafe'; -import { noop } from 'lodash'; const iframeElement = Selector('#element-in-iframe'); const pageElement = Selector('#element-on-page'); @@ -12,12 +11,15 @@ async function initConfiguration () { const history = await t.getNativeDialogHistory(); + /* eslint-disable no-console */ await t .expect(history[0].text).eql('Hey!') .switchToIframe('#iframe') .expect(iframeElement.exists).ok() .setTestSpeed(0.95) - .setPageLoadTimeout(95); + .setPageLoadTimeout(95) + .eval(() => console.log('init-configuration')); + /* eslint-enable no-console */ t.ctx.someVal = 'ctxVal'; t.fixtureCtx.someVal = 'fixtureCtxVal'; @@ -32,7 +34,15 @@ const role1 = Role('http://localhost:3000/fixtures/api/es-next/roles/pages/index await t.click(showAlertBtn); }); -const role2 = Role('http://localhost:3000/fixtures/api/es-next/roles/pages/index.html', noop); +const role2 = Role('http://localhost:3000/fixtures/api/es-next/roles/pages/index.html', async () => { + /* eslint-disable no-console */ + await t.eval(() => console.log('init-role')); + /* eslint-enable no-console */ + + const { log } = await t.getBrowserConsoleMessages(); + + await t.expect(log).eql(['init-role']); +}); fixture `Configuration management` .page `http://localhost:3000/fixtures/api/es-next/roles/pages/index.html`; @@ -46,7 +56,10 @@ test('Clear configuration', async () => { test('Restore configuration', async () => { await initConfiguration(); + let { log } = await t.getBrowserConsoleMessages(); + await t + .expect(log).eql(['init-configuration']) .useRole(role2) .expect(iframeElement.exists).ok() .expect(t.ctx.someVal).eql('ctxVal') @@ -59,4 +72,8 @@ test('Restore configuration', async () => { const history = await t.getNativeDialogHistory(); await t.expect(history[0].text).eql('Hey!'); + + log = (await t.getBrowserConsoleMessages()).log; + + await t.expect(log).eql(['init-configuration']); }); diff --git a/test/server/data/test-suites/typescript-defs/test-controller.ts b/test/server/data/test-suites/typescript-defs/test-controller.ts index a9f1ed6a681..30d45975ef9 100644 --- a/test/server/data/test-suites/typescript-defs/test-controller.ts +++ b/test/server/data/test-suites/typescript-defs/test-controller.ts @@ -704,7 +704,7 @@ test('Take a screenshot in quarantine mode', async t => { test('Type text in input', async t => { - await t.typeText('#input', 'a', { replace: true }); + await t.typeText('#input', 'a', {replace: true}); }); @@ -746,3 +746,39 @@ test('Chaining callsites', async t => { .click('#error') .click('#btn3'); }); + +test('t.getBrowserConsoleMessages', async t => { + let messages = await t.getBrowserConsoleMessages(); + + await t + .expect(messages.error).eql(['error1']) + .expect(messages.warn).eql(['warn1']) + .expect(messages.log).eql(['log1']) + .expect(messages.info).eql(['info1']) + + .click('#trigger-messages') + + // Check the driver keeps the messages between page reloads + .click('#reload'); + + messages = await t.getBrowserConsoleMessages(); + + await t + .expect(messages.error).eql(['error1', 'error2']) + .expect(messages.warn).eql(['warn1', 'warn2']) + .expect(messages.log).eql(['log1', 'log2']) + .expect(messages.info).eql(['info1', 'info2']); +}); + +test('messages formatting', async t => { + // Several arguments + await t.eval(() => console.log('a', 1, null, void 0, ['b', 2], {c: 3})); + + const messages = await t.getBrowserConsoleMessages(); + + await t + .expect(messages.log[0]).eql('a 1 null undefined b,2 [object Object]') + .expect(messages.info.length).eql(0) + .expect(messages.warn.length).eql(0) + .expect(messages.error.length).eql(0); +}); \ No newline at end of file diff --git a/ts-defs/index.d.ts b/ts-defs/index.d.ts index 2020675e839..77b28c7fb39 100644 --- a/ts-defs/index.d.ts +++ b/ts-defs/index.d.ts @@ -799,6 +799,25 @@ interface NativeDialogHistoryItem { url: string; } +interface BrowserConsoleMessages { + /** + * Messages output to the browser console by the console.log() method. + */ + log: string[], + /** + * Warning messages output to the browser console by the console.warn() method. + */ + warn: string[], + /** + * Error messages output to the browser console by the console.error() method. + */ + error: string[], + /** + * Information messages output to the browser console by the console.info() method. + */ + info: string[] +} + interface TestController { /** * Dictionary that is shared between test hook functions and test code. @@ -1004,6 +1023,10 @@ interface TestController { * corresponds to a certain native dialog that appears in the main window or in an `