From e658467c8b50ef04c4920a63983424da900de670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Og=C3=B3rek?= Date: Fri, 12 Jul 2019 17:32:58 +0200 Subject: [PATCH] test: Complete rewrite of Browser Integration Tests --- .gitignore | 1 + .travis.yml | 7 +- package.json | 2 + packages/browser/package.json | 14 +- packages/browser/test/integration/.gitignore | 1 - packages/browser/test/integration/.prettierrc | 3 + .../browser/test/integration/.prettierrc.json | 6 - packages/browser/test/integration/browsers.js | 108 + packages/browser/test/integration/common.js | 156 -- .../browser/test/integration/common/init.js | 75 + .../{init.js => common/triggers.js} | 18 +- .../browser/test/integration/common/utils.js | 75 + .../browser/test/integration/console-logs.js | 7 - packages/browser/test/integration/frame.html | 29 - .../browser/test/integration/karma.conf.js | 82 + .../test/integration/loader-lazy-no.html | 29 - .../loader-with-no-global-init-lazy-no.html | 14 - .../loader-with-no-global-init.html | 14 - packages/browser/test/integration/loader.html | 29 - .../test/integration/polyfills/assign.js | 39 + .../test/integration/polyfills/events.js | 102 + .../{whatwg-fetch-3.0.0.js => fetch.js} | 175 +- .../test/integration/polyfills/includes.js | 23 + .../browser/test/integration/polyfills/nan.js | 14 + .../{es6-promise-4.2.5.auto.js => promise.js} | 166 +- .../browser/test/integration/polyfills/raf.js | 34 + packages/browser/test/integration/run.js | 157 ++ .../test/integration/{ => subjects}/123 | 0 .../test/integration/subjects/console-logs.js | 7 + .../integration/{ => subjects}/example.json | 0 .../integration/{ => subjects}/throw-error.js | 2 +- .../{ => subjects}/throw-object.js | 2 +- .../{ => subjects}/throw-string.js | 2 +- .../browser/test/integration/suites/api.js | 182 ++ .../test/integration/suites/breadcrumbs.js | 741 ++++++ .../test/integration/suites/builtins.js | 368 +++ .../browser/test/integration/suites/config.js | 55 + .../test/integration/suites/helpers.js | 88 + .../integration/suites/loader-specific.js | 46 + .../browser/test/integration/suites/loader.js | 59 + .../test/integration/suites/onerror.js | 141 ++ .../browser/test/integration/suites/shell.js | 40 + packages/browser/test/integration/test.js | 2004 ----------------- .../test/integration/variants/frame.html | 25 + .../integration/variants/loader-lazy-no.html | 25 + .../loader-with-no-global-init-lazy-no.html | 11 + .../variants/loader-with-no-global-init.html | 11 + .../test/integration/variants/loader.html | 25 + .../browser/test/karma/integration-files.js | 27 - .../test/karma/karma.integration.config.js | 38 - .../test/karma/karma.saucelabs.config.js | 109 - .../browser/test/{ => unit}/backend.test.ts | 2 +- .../browser/test/{ => unit}/index.test.ts | 2 +- .../{ => unit}/integrations/helpers.test.ts | 2 +- .../integrations/linkederrors.test.ts | 4 +- .../karma.conf.js} | 14 +- .../test/{ => unit}/mocks/simpletransport.ts | 4 +- .../browser/test/{ => unit}/parsers.test.ts | 2 +- .../test/{ => unit}/tracekit/custom.test.ts | 2 +- .../test/{ => unit}/tracekit/original.test.ts | 2 +- .../{ => unit}/tracekit/originalfixtures.ts | 0 .../test/{ => unit}/transports/base.test.ts | 2 +- .../test/{ => unit}/transports/fetch.test.ts | 2 +- .../test/{ => unit}/transports/xhr.test.ts | 2 +- scripts/browser-saucelabs.sh | 8 - yarn.lock | 420 ++-- 66 files changed, 3035 insertions(+), 2821 deletions(-) delete mode 100644 packages/browser/test/integration/.gitignore create mode 100644 packages/browser/test/integration/.prettierrc delete mode 100644 packages/browser/test/integration/.prettierrc.json create mode 100644 packages/browser/test/integration/browsers.js delete mode 100644 packages/browser/test/integration/common.js create mode 100644 packages/browser/test/integration/common/init.js rename packages/browser/test/integration/{init.js => common/triggers.js} (66%) create mode 100644 packages/browser/test/integration/common/utils.js delete mode 100644 packages/browser/test/integration/console-logs.js delete mode 100644 packages/browser/test/integration/frame.html create mode 100644 packages/browser/test/integration/karma.conf.js delete mode 100644 packages/browser/test/integration/loader-lazy-no.html delete mode 100644 packages/browser/test/integration/loader-with-no-global-init-lazy-no.html delete mode 100644 packages/browser/test/integration/loader-with-no-global-init.html delete mode 100644 packages/browser/test/integration/loader.html create mode 100644 packages/browser/test/integration/polyfills/assign.js create mode 100644 packages/browser/test/integration/polyfills/events.js rename packages/browser/test/integration/polyfills/{whatwg-fetch-3.0.0.js => fetch.js} (72%) create mode 100644 packages/browser/test/integration/polyfills/includes.js create mode 100644 packages/browser/test/integration/polyfills/nan.js rename packages/browser/test/integration/polyfills/{es6-promise-4.2.5.auto.js => promise.js} (89%) create mode 100644 packages/browser/test/integration/polyfills/raf.js create mode 100755 packages/browser/test/integration/run.js rename packages/browser/test/integration/{ => subjects}/123 (100%) create mode 100644 packages/browser/test/integration/subjects/console-logs.js rename packages/browser/test/integration/{ => subjects}/example.json (100%) rename packages/browser/test/integration/{ => subjects}/throw-error.js (60%) rename packages/browser/test/integration/{ => subjects}/throw-object.js (77%) rename packages/browser/test/integration/{ => subjects}/throw-string.js (69%) create mode 100644 packages/browser/test/integration/suites/api.js create mode 100644 packages/browser/test/integration/suites/breadcrumbs.js create mode 100644 packages/browser/test/integration/suites/builtins.js create mode 100644 packages/browser/test/integration/suites/config.js create mode 100644 packages/browser/test/integration/suites/helpers.js create mode 100644 packages/browser/test/integration/suites/loader-specific.js create mode 100644 packages/browser/test/integration/suites/loader.js create mode 100644 packages/browser/test/integration/suites/onerror.js create mode 100644 packages/browser/test/integration/suites/shell.js delete mode 100644 packages/browser/test/integration/test.js create mode 100644 packages/browser/test/integration/variants/frame.html create mode 100644 packages/browser/test/integration/variants/loader-lazy-no.html create mode 100644 packages/browser/test/integration/variants/loader-with-no-global-init-lazy-no.html create mode 100644 packages/browser/test/integration/variants/loader-with-no-global-init.html create mode 100644 packages/browser/test/integration/variants/loader.html delete mode 100644 packages/browser/test/karma/integration-files.js delete mode 100644 packages/browser/test/karma/karma.integration.config.js delete mode 100644 packages/browser/test/karma/karma.saucelabs.config.js rename packages/browser/test/{ => unit}/backend.test.ts (86%) rename packages/browser/test/{ => unit}/index.test.ts (99%) rename packages/browser/test/{ => unit}/integrations/helpers.test.ts (99%) rename packages/browser/test/{ => unit}/integrations/linkederrors.test.ts (97%) rename packages/browser/test/{karma/karma.unit.config.js => unit/karma.conf.js} (72%) rename packages/browser/test/{ => unit}/mocks/simpletransport.ts (66%) rename packages/browser/test/{ => unit}/parsers.test.ts (98%) rename packages/browser/test/{ => unit}/tracekit/custom.test.ts (99%) rename packages/browser/test/{ => unit}/tracekit/original.test.ts (99%) rename packages/browser/test/{ => unit}/tracekit/originalfixtures.ts (100%) rename packages/browser/test/{ => unit}/transports/base.test.ts (91%) rename packages/browser/test/{ => unit}/transports/fetch.test.ts (97%) rename packages/browser/test/{ => unit}/transports/xhr.test.ts (97%) delete mode 100755 scripts/browser-saucelabs.sh diff --git a/.gitignore b/.gitignore index 4e390c6617a0..67d04e662998 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ scratch/ yarn-error.log npm-debug.log lerna-debug.log +local.log # ide .idea diff --git a/.travis.yml b/.travis.yml index cbc814b69748..ce045b1f56ca 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,11 +39,8 @@ jobs: - name: '@sentry/packages - build and test [node v10]' node_js: '10' script: scripts/test.sh - - name: '@sentry/browser - integration tests' - node_js: '8' - addons: - chrome: stable - firefox: latest + - name: '@sentry/browser - browserstack integration tests' + node_js: '10' script: scripts/browser-integration.sh - name: 'raven-node [node v4]' if: branch = 4.x diff --git a/package.json b/package.json index a9af55a0648d..f1688f71244a 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ "danger": "^7.1.3", "danger-plugin-tslint": "^2.0.0", "jest": "^24.7.1", + "karma-browserstack-launcher": "^1.5.1", + "karma-firefox-launcher": "^1.1.0", "lerna": "3.13.4", "mocha": "^6.1.4", "npm-run-all": "^4.1.2", diff --git a/packages/browser/package.json b/packages/browser/package.json index 17b1a27a238a..1bb41d4e517a 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -24,17 +24,15 @@ "devDependencies": { "@types/md5": "2.1.33", "chai": "^4.1.2", + "chokidar": "^3.0.2", "jest": "^24.7.1", "jsdom": "^15.0.0", "karma": "^4.1.0", "karma-chai": "^0.1.0", "karma-chrome-launcher": "^2.2.0", - "karma-failed-reporter": "0.0.3", - "karma-firefox-launcher": "^1.1.0", "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.5", "karma-rollup-preprocessor": "^7.0.0", - "karma-sauce-launcher": "^2.0.2", "karma-sinon": "^1.0.5", "karma-typescript": "^4.0.0", "karma-typescript-es6-transform": "^4.0.0", @@ -71,11 +69,11 @@ "fix": "run-s fix:tslint fix:prettier", "fix:prettier": "prettier --write \"{src,test}/**/*.ts\"", "fix:tslint": "tslint --fix -t stylish -p .", - "test": "karma start test/karma/karma.unit.config.js", - "test:watch": "karma start test/karma/karma.unit.config.js --auto-watch --no-single-run", - "test:integration": "karma start test/karma/karma.integration.config.js", - "test:integration:watch": "karma start test/karma/karma.integration.config.js --auto-watch --no-single-run", - "test:saucelabs": "karma start test/karma/karma.saucelabs.config.js", + "test": "run-s test:unit", + "test:unit": "karma start test/unit/karma.conf.js", + "test:unit:watch": "karma start test/unit/karma.conf.js --auto-watch --no-single-run", + "test:integration": "test/integration/run.js", + "test:integration:watch": "test/integration/run.js --watch", "test:manual": "node test/manual/npm-build.js && rm test/manual/tmp.js", "size:check": "run-p size:check:es5 size:check:es6", "size:check:es5": "cat build/bundle.min.js | gzip -9 | wc -c | awk '{$1=$1/1024; print \"ES5: \",$1,\"kB\";}'", diff --git a/packages/browser/test/integration/.gitignore b/packages/browser/test/integration/.gitignore deleted file mode 100644 index 146d0f232423..000000000000 --- a/packages/browser/test/integration/.gitignore +++ /dev/null @@ -1 +0,0 @@ -dedupe.* diff --git a/packages/browser/test/integration/.prettierrc b/packages/browser/test/integration/.prettierrc new file mode 100644 index 000000000000..757fd64caa9e --- /dev/null +++ b/packages/browser/test/integration/.prettierrc @@ -0,0 +1,3 @@ +{ + "trailingComma": "es5" +} diff --git a/packages/browser/test/integration/.prettierrc.json b/packages/browser/test/integration/.prettierrc.json deleted file mode 100644 index 297a29da074a..000000000000 --- a/packages/browser/test/integration/.prettierrc.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "printWidth": 120, - "proseWrap": "always", - "singleQuote": true, - "trailingComma": "es5" -} diff --git a/packages/browser/test/integration/browsers.js b/packages/browser/test/integration/browsers.js new file mode 100644 index 000000000000..4f06770042a1 --- /dev/null +++ b/packages/browser/test/integration/browsers.js @@ -0,0 +1,108 @@ +module.exports = { + bs_chrome: { + base: "BrowserStack", + browser: "Chrome", + browser_version: "latest", + os: "Windows", + os_version: "10", + }, + bs_firefox: { + base: "BrowserStack", + browser: "Firefox", + browser_version: "latest", + os: "Windows", + os_version: "10", + }, + bs_safari: { + base: "BrowserStack", + browser: "Safari", + browser_version: "latest", + os: "OS X", + os_version: "Mojave", + }, + bs_edge: { + base: "BrowserStack", + browser: "Edge", + browser_version: "latest", + os: "Windows", + os_version: "10", + }, + bs_ie11: { + base: "BrowserStack", + browser: "IE", + browser_version: "11.0", + os: "Windows", + os_version: "10", + }, + bs_ie10: { + base: "BrowserStack", + browser: "IE", + browser_version: "10.0", + os: "Windows", + os_version: "8", + }, + bs_ios_12: { + base: "BrowserStack", + browser: "Mobile Safari", + device: "iPhone 8", + os: "ios", + os_version: "12.1", + real_mobile: true, + }, + bs_ios_11: { + base: "BrowserStack", + browser: "Mobile Safari", + device: "iPhone 6", + os: "ios", + os_version: "11.4", + real_mobile: true, + }, + bs_android_9: { + base: "BrowserStack", + browser: "Android", + device: "Samsung Galaxy S10", + os: "android", + os_version: "9.0", + real_mobile: true, + }, + bs_android_8: { + base: "BrowserStack", + browser: "Android", + device: "Samsung Galaxy S9", + os: "android", + os_version: "8.0", + real_mobile: true, + }, + bs_android_7: { + base: "BrowserStack", + browser: "Android", + device: "Samsung Galaxy S8", + os: "android", + os_version: "7.0", + real_mobile: true, + }, + bs_android_6: { + base: "BrowserStack", + browser: "Android", + device: "Samsung Galaxy S7", + os: "android", + os_version: "6.0", + real_mobile: true, + }, + bs_android_5: { + base: "BrowserStack", + browser: "Android", + device: "Google Nexus 9", + os: "android", + os_version: "5.1", + real_mobile: true, + }, + bs_android_4: { + base: "BrowserStack", + browser: "Android", + device: "Google Nexus 5", + os: "android", + os_version: "4.4", + real_mobile: true, + }, +}; diff --git a/packages/browser/test/integration/common.js b/packages/browser/test/integration/common.js deleted file mode 100644 index 719844bb67c0..000000000000 --- a/packages/browser/test/integration/common.js +++ /dev/null @@ -1,156 +0,0 @@ -/** - * requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel - * - * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ - * http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating - * - * MIT license - */ -(function() { - var lastTime = 0; - var vendors = ['ms', 'moz', 'webkit', 'o']; - for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { - window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; - window.cancelAnimationFrame = - window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame']; - } - - if (!window.requestAnimationFrame) - window.requestAnimationFrame = function(callback, element) { - var currTime = new Date().getTime(); - var timeToCall = Math.max(0, 16 - (currTime - lastTime)); - var id = window.setTimeout(function() { - callback(currTime + timeToCall); - }, timeToCall); - lastTime = currTime + timeToCall; - return id; - }; - - if (!window.cancelAnimationFrame) - window.cancelAnimationFrame = function(id) { - clearTimeout(id); - }; -})(); - -/** - * DOM4 MouseEvent and KeyboardEvent Polyfills - * - * References: - * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent - * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent#Polyfill - * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent - */ -(function() { - try { - new MouseEvent('click'); - return false; // No need to polyfill - } catch (e) { - // Need to polyfill - fall through - } - - var MouseEvent = function(eventType) { - var mouseEvent = document.createEvent('MouseEvent'); - mouseEvent.initMouseEvent(eventType, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null); - return mouseEvent; - }; - - MouseEvent.prototype = Event.prototype; - window.MouseEvent = MouseEvent; -})(); - -(function() { - try { - new KeyboardEvent('keypress'); - return false; // No need to polyfill - } catch (e) { - // Need to polyfill - fall through - } - - var KeyboardEvent = function(eventType) { - var keyboardEvent = document.createEvent('KeyboardEvent'); - if (keyboardEvent.initKeyboardEvent) - keyboardEvent.initKeyboardEvent(eventType, true, true, window, false, false, false, false, 'a', 0); - if (keyboardEvent.initKeyEvent) - keyboardEvent.initKeyEvent(eventType, true, true, window, false, false, false, false, 'a'); - return keyboardEvent; - }; - - KeyboardEvent.prototype = Event.prototype; - window.KeyboardEvent = KeyboardEvent; -})(); - -(function() { - // store references to original, unwrapped built-ins in order to: - // - get a clean, unwrapped setTimeout (so stack traces don't include - // frames from mocha) - // - make assertions re: wrapped functions - window.originalBuiltIns = { - setTimeout: setTimeout, - setInterval: setInterval, - requestAnimationFrame: requestAnimationFrame, - xhrProtoOpen: XMLHttpRequest.prototype.open, - headAddEventListener: document.head.addEventListener, // use 'cause body isn't closed yet - headRemoveEventListener: document.head.removeEventListener, - consoleDebug: console.debug, - consoleInfo: console.info, - consoleWarn: console.warn, - consoleError: console.error, - consoleLog: console.log, - }; - - // expose events so we can access them in our tests - window.sentryData = []; - window.sentryBreadcrumbs = []; -})(); - -function initSDK() { - // stub transport so we don't actually transmit any data - function DummyTransport() {} - DummyTransport.prototype.sendEvent = function(event) { - sentryData.push(event); - done(sentryData); - return Promise.resolve({ - status: 'success', - }); - }; - - Sentry.init({ - dsn: 'https://public@example.com/1', - // debug: true, - integrations: [new Sentry.Integrations.Dedupe()], - attachStacktrace: true, - transport: DummyTransport, - ignoreErrors: ['ignoreErrorTest'], - blacklistUrls: ['foo.js'], - // integrations: function(old) { - // return [new Sentry.Integrations.Debug({ stringify: true })].concat(old); - // }, - beforeBreadcrumb: function(breadcrumb) { - // Filter console logs as we use them for debugging *a lot* and they are not *that* important - // But allow then if we explicitly say so (for one of integration tests) - if (breadcrumb.category === 'console' && !window.allowConsoleBreadcrumbs) { - return null; - } - - // overlyComplicatedDebuggingMechanism 'aka' console.log driven debugging - // console.log(JSON.stringify(breadcrumb, null, 2)); - - // Filter internal Karma requests - if ( - breadcrumb.type === 'http' && - (breadcrumb.data.url.indexOf('test.js') !== -1 || breadcrumb.data.url.indexOf('frame.html') !== -1) - ) { - return null; - } - - // Filter "refresh" like navigation which occurs in Mocha when testing on Android 4 - if (breadcrumb.category === 'navigation' && breadcrumb.data.to === breadcrumb.data.from) { - return null; - } - - sentryBreadcrumbs.push(breadcrumb); - - return breadcrumb; - }, - }); -} diff --git a/packages/browser/test/integration/common/init.js b/packages/browser/test/integration/common/init.js new file mode 100644 index 000000000000..342f827dd4d0 --- /dev/null +++ b/packages/browser/test/integration/common/init.js @@ -0,0 +1,75 @@ +// store references to original, unwrapped built-ins in order to: +// - get a clean, unwrapped setTimeout (so stack traces don't include frames from mocha) +// - make assertions re: wrapped functions +var originalBuiltIns = { + setTimeout: setTimeout, + setInterval: setInterval, + requestAnimationFrame: requestAnimationFrame, + xhrProtoOpen: XMLHttpRequest.prototype.open, + headAddEventListener: document.head.addEventListener, // use 'cause body isn't closed yet + headRemoveEventListener: document.head.removeEventListener, + consoleDebug: console.debug, + consoleInfo: console.info, + consoleWarn: console.warn, + consoleError: console.error, + consoleLog: console.log, +}; + +var events = []; +var breadcrumbs = []; + +// Oh dear IE10... +var dsn = + document.location.protocol + + "//public@" + + document.location.hostname + + (document.location.port ? ":" + document.location.port : "") + + "/1"; + +function initSDK() { + Sentry.init({ + dsn: dsn, + integrations: [new Sentry.Integrations.Dedupe()], + attachStacktrace: true, + ignoreErrors: ["ignoreErrorTest"], + blacklistUrls: ["foo.js"], + beforeSend: function(event) { + events.push(event); + return event; + }, + beforeBreadcrumb: function(breadcrumb) { + // Filter console logs as we use them for debugging *a lot* and they are not *that* important + // But allow then if we explicitly say so (for one of integration tests) + if ( + breadcrumb.category === "console" && + !window.allowConsoleBreadcrumbs + ) { + return null; + } + + // One of the tests use manually created breadcrumb without eventId and we want to let it through + if (breadcrumb.category === "sentry" && breadcrumb.event_id) { + return null; + } + + if ( + breadcrumb.type === "http" && + (breadcrumb.data.url.indexOf("test.js") !== -1 || + breadcrumb.data.url.indexOf("frame.html") !== -1) + ) { + return null; + } + + // Filter "refresh" like navigation which occurs in Mocha when testing on Android 4 + if ( + breadcrumb.category === "navigation" && + breadcrumb.data.to === breadcrumb.data.from + ) { + return null; + } + + breadcrumbs.push(breadcrumb); + return breadcrumb; + }, + }); +} diff --git a/packages/browser/test/integration/init.js b/packages/browser/test/integration/common/triggers.js similarity index 66% rename from packages/browser/test/integration/init.js rename to packages/browser/test/integration/common/triggers.js index dccac4fa6387..29cfcb0b9bfc 100644 --- a/packages/browser/test/integration/init.js +++ b/packages/browser/test/integration/common/triggers.js @@ -1,4 +1,4 @@ -initSDK(); // can be found in common.js +// All the functions below can be called within the iframe under the test function bar() { baz(); @@ -16,14 +16,14 @@ function foo2() { function throwNonError() { try { - throw { foo: 'bar' }; + throw { foo: "bar" }; } catch (o_O) { Sentry.captureException(o_O); } } function throwError(message) { - message = message || 'foo'; + message = message || "foo"; try { throw new Error(message); } catch (o_O) { @@ -33,7 +33,7 @@ function throwError(message) { function throwRandomError() { try { - throw new Error('Exception no ' + (Date.now() + Math.random())); + throw new Error("Exception no " + (Date.now() + Math.random())); } catch (o_O) { Sentry.captureException(o_O); } @@ -45,21 +45,15 @@ function throwSameConsecutiveErrors(message) { } function captureMessage(message) { - message = message || 'message'; + message = message || "message"; Sentry.captureMessage(message); } function captureRandomMessage() { - Sentry.captureMessage('Message no ' + (Date.now() + Math.random())); + Sentry.captureMessage("Message no " + (Date.now() + Math.random())); } function captureSameConsecutiveMessages(message) { captureMessage(message); captureMessage(message); } - -function isChrome() { - return ( - /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor) && !/Android/.test(navigator.userAgent) - ); -} diff --git a/packages/browser/test/integration/common/utils.js b/packages/browser/test/integration/common/utils.js new file mode 100644 index 000000000000..d94605becff4 --- /dev/null +++ b/packages/browser/test/integration/common/utils.js @@ -0,0 +1,75 @@ +// All the functions below can be called within the iframe under the test + +function supportsFetch() { + if (!("fetch" in window)) { + return false; + } + + try { + new Headers(); + new Request(""); + new Response(); + return true; + } catch (e) { + return false; + } +} + +function supportsNativeFetch() { + if (!supportsFetch()) { + return false; + } + + function isNativeFunc(func) { + return func.toString().indexOf("native") !== -1; + } + + var result = null; + if (window.document) { + var sandbox = window.document.createElement("iframe"); + sandbox.hidden = true; + try { + window.document.head.appendChild(sandbox); + if (sandbox.contentWindow && sandbox.contentWindow.fetch) { + result = isNativeFunc(sandbox.contentWindow.fetch); + } + window.document.head.removeChild(sandbox); + } catch (o_O) {} + } + + if (result === null) { + result = isNativeFunc(window.fetch); + } + + return result; +} + +function isChrome() { + return ( + /Chrome/.test(navigator.userAgent) && + /Google Inc/.test(navigator.vendor) && + !/Android/.test(navigator.userAgent) + ); +} + +function isBelowIE11() { + return /*@cc_on!@*/ false == !false; +} + +// Thanks for nothing IE! +// (╯°□°)╯︵ ┻━┻ +function canReadFunctionName() { + function foo() {} + if (foo.name === "foo") return true; + return false; +} + +function waitForXHR(xhr, cb) { + if (xhr.readyState === 4) { + return cb(); + } + + setTimeout(function() { + waitForXHR(xhr, cb); + }, 1000 / 60); +} diff --git a/packages/browser/test/integration/console-logs.js b/packages/browser/test/integration/console-logs.js deleted file mode 100644 index 052e6929e7ef..000000000000 --- a/packages/browser/test/integration/console-logs.js +++ /dev/null @@ -1,7 +0,0 @@ -console.log('One'); -console.warn('Two', { a: 1 }); -console.error('Error 2', { b: { c: [] } }); -function a() { - throw new Error('Error thrown 3'); -} -a(); diff --git a/packages/browser/test/integration/frame.html b/packages/browser/test/integration/frame.html deleted file mode 100644 index 3d6459384ade..000000000000 --- a/packages/browser/test/integration/frame.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - -
- -
-
- -
-
-
-
-
- - - diff --git a/packages/browser/test/integration/karma.conf.js b/packages/browser/test/integration/karma.conf.js new file mode 100644 index 000000000000..9f3d016e62a8 --- /dev/null +++ b/packages/browser/test/integration/karma.conf.js @@ -0,0 +1,82 @@ +const path = require("path"); + +const browserstackUsername = process.env.BROWSERSTACK_USERNAME; +const browserstackAccessKey = process.env.BROWSERSTACK_ACCESS_KEY; +const isLocalRun = + browserstackUsername === undefined || browserstackAccessKey === undefined; + +const customLaunchers = isLocalRun ? {} : require("./browsers.js"); +const browsers = isLocalRun ? ["ChromeHeadless"] : Object.keys(customLaunchers); + +const plugins = [ + "karma-mocha", + "karma-chai", + "karma-sinon", + "karma-mocha-reporter", +]; +const reporters = ["mocha"]; + +if (isLocalRun) { + plugins.push("karma-chrome-launcher"); +} else { + plugins.push("karma-browserstack-launcher"); + reporters.push("BrowserStack"); +} + +const files = [ + // Files common across all test-cases (polyfills, setup, loader, sdk), but not tests themselves + { + pattern: path.resolve(__dirname, "artifacts/!(tests).js"), + included: false, + }, + // Files used to trigger errors/provide data + { pattern: path.resolve(__dirname, "subjects/*"), included: false }, + // HTML shells for all test suites + { pattern: path.resolve(__dirname, "variants/*"), included: false }, + // Tests themselves - only this file is included in the index.html generated by Mocha + { pattern: path.resolve(__dirname, "artifacts/tests.js"), included: true }, +]; + +module.exports = config => { + if (isLocalRun) { + console.log(` +╔══════════════════════════════════════════════════════════╗ +║ INFO: Running integration tests in the local environment ║ +╚══════════════════════════════════════════════════════════╝ +`); + } else { + console.log(` +╔═════════════════════════════════════════════════════════════════╗ +║ INFO: Running integration tests in the BrowserStack environment ║ +╚═════════════════════════════════════════════════════════════════╝`); + } + + config.set({ + logLevel: process.env.DEBUG ? config.LOG_DEBUG : config.LOG_INFO, + colors: true, + singleRun: true, + autoWatch: false, + basePath: __dirname, + hostname: isLocalRun ? "localhost" : "bs-local.com", + proxies: { + // Required for non-string fetch url test + "/base/variants/123": "/base/subjects/123", + // Supresses warnings + "/api/1/store/": "/", + }, + frameworks: ["mocha", "chai", "sinon"], + files, + plugins, + reporters, + customLaunchers, + browsers, + client: { + mocha: { + reporter: "html", + ui: "bdd", + }, + }, + build: process.env.TRAVIS_BUILD_NUMBER || Date.now(), + concurrency: isLocalRun ? 1 : 2, + }); +}; diff --git a/packages/browser/test/integration/loader-lazy-no.html b/packages/browser/test/integration/loader-lazy-no.html deleted file mode 100644 index db666fd21413..000000000000 --- a/packages/browser/test/integration/loader-lazy-no.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - -
- -
-
- -
-
-
-
-
- - - - diff --git a/packages/browser/test/integration/loader-with-no-global-init-lazy-no.html b/packages/browser/test/integration/loader-with-no-global-init-lazy-no.html deleted file mode 100644 index 15728250df2b..000000000000 --- a/packages/browser/test/integration/loader-with-no-global-init-lazy-no.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/browser/test/integration/loader-with-no-global-init.html b/packages/browser/test/integration/loader-with-no-global-init.html deleted file mode 100644 index bd3eeaeae4bd..000000000000 --- a/packages/browser/test/integration/loader-with-no-global-init.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/browser/test/integration/loader.html b/packages/browser/test/integration/loader.html deleted file mode 100644 index 2354a91a949f..000000000000 --- a/packages/browser/test/integration/loader.html +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - -
- -
-
- -
-
-
-
-
- - - - diff --git a/packages/browser/test/integration/polyfills/assign.js b/packages/browser/test/integration/polyfills/assign.js new file mode 100644 index 000000000000..a1cb57bb8eab --- /dev/null +++ b/packages/browser/test/integration/polyfills/assign.js @@ -0,0 +1,39 @@ +/** + * Object.assign Polyfill + * + * References: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign + */ + +(function() { + if (typeof Object.assign !== "function") { + // Must be writable: true, enumerable: false, configurable: true + Object.defineProperty(Object, "assign", { + value: function assign(target, varArgs) { + // .length of function is 2 + "use strict"; + if (target === null || target === undefined) { + throw new TypeError("Cannot convert undefined or null to object"); + } + + var to = Object(target); + + for (var index = 1; index < arguments.length; index++) { + var nextSource = arguments[index]; + + if (nextSource !== null && nextSource !== undefined) { + for (var nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; + } + } + } + } + return to; + }, + writable: true, + configurable: true, + }); + } +})(); diff --git a/packages/browser/test/integration/polyfills/events.js b/packages/browser/test/integration/polyfills/events.js new file mode 100644 index 000000000000..bb7d5e7c3a62 --- /dev/null +++ b/packages/browser/test/integration/polyfills/events.js @@ -0,0 +1,102 @@ +/** + * MouseEvent, KeyboardEvent and CustomEvent Polyfills + * + * References: + * https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent + * https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent + * https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent + */ + +(function() { + try { + new MouseEvent("click"); + return false; // No need to polyfill + } catch (e) { + // Need to polyfill - fall through + } + + var MouseEvent = function(eventType) { + var mouseEvent = document.createEvent("MouseEvent"); + mouseEvent.initMouseEvent( + eventType, + true, + true, + window, + 0, + 0, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + null + ); + return mouseEvent; + }; + + MouseEvent.prototype = Event.prototype; + window.MouseEvent = MouseEvent; +})(); + +(function() { + try { + new KeyboardEvent("keypress"); + return false; // No need to polyfill + } catch (e) { + // Need to polyfill - fall through + } + + var KeyboardEvent = function(eventType) { + var keyboardEvent = document.createEvent("KeyboardEvent"); + if (keyboardEvent.initKeyboardEvent) + keyboardEvent.initKeyboardEvent( + eventType, + true, + true, + window, + false, + false, + false, + false, + "a", + 0 + ); + if (keyboardEvent.initKeyEvent) + keyboardEvent.initKeyEvent( + eventType, + true, + true, + window, + false, + false, + false, + false, + "a" + ); + return keyboardEvent; + }; + + KeyboardEvent.prototype = Event.prototype; + window.KeyboardEvent = KeyboardEvent; +})(); + +(function() { + if (typeof window.CustomEvent === "function") return false; + + function CustomEvent(event, params) { + params = params || { bubbles: false, cancelable: false, detail: null }; + var evt = document.createEvent("CustomEvent"); + evt.initCustomEvent( + event, + params.bubbles, + params.cancelable, + params.detail + ); + return evt; + } + + window.CustomEvent = CustomEvent; +})(); diff --git a/packages/browser/test/integration/polyfills/whatwg-fetch-3.0.0.js b/packages/browser/test/integration/polyfills/fetch.js similarity index 72% rename from packages/browser/test/integration/polyfills/whatwg-fetch-3.0.0.js rename to packages/browser/test/integration/polyfills/fetch.js index 549747348e88..370c208dd9e1 100644 --- a/packages/browser/test/integration/polyfills/whatwg-fetch-3.0.0.js +++ b/packages/browser/test/integration/polyfills/fetch.js @@ -1,18 +1,26 @@ +/*! + * @overview whatwg-fetch - an implementation of Fetch API. + * @copyright Copyright (c) 2014-2016 GitHub, Inc. + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/github/fetch/master/LICENSE + * @version v3.0.0+cc84bc28 + */ + (function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) - : typeof define === 'function' && define.amd - ? define(['exports'], factory) - : factory((global.WHATWGFetch = {})); + : typeof define === "function" && define.amd + ? define(["exports"], factory) + : factory((global.WHATWGFetch = {})); })(this, function(exports) { - 'use strict'; + "use strict"; var support = { - searchParams: 'URLSearchParams' in self, - iterable: 'Symbol' in self && 'iterator' in Symbol, + searchParams: "URLSearchParams" in self, + iterable: "Symbol" in self && "iterator" in Symbol, blob: - 'FileReader' in self && - 'Blob' in self && + "FileReader" in self && + "Blob" in self && (function() { try { new Blob(); @@ -21,8 +29,8 @@ return false; } })(), - formData: 'FormData' in self, - arrayBuffer: 'ArrayBuffer' in self, + formData: "FormData" in self, + arrayBuffer: "ArrayBuffer" in self, }; function isDataView(obj) { @@ -31,36 +39,38 @@ if (support.arrayBuffer) { var viewClasses = [ - '[object Int8Array]', - '[object Uint8Array]', - '[object Uint8ClampedArray]', - '[object Int16Array]', - '[object Uint16Array]', - '[object Int32Array]', - '[object Uint32Array]', - '[object Float32Array]', - '[object Float64Array]', + "[object Int8Array]", + "[object Uint8Array]", + "[object Uint8ClampedArray]", + "[object Int16Array]", + "[object Uint16Array]", + "[object Int32Array]", + "[object Uint32Array]", + "[object Float32Array]", + "[object Float64Array]", ]; var isArrayBufferView = ArrayBuffer.isView || function(obj) { - return obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1; + return ( + obj && viewClasses.indexOf(Object.prototype.toString.call(obj)) > -1 + ); }; } function normalizeName(name) { - if (typeof name !== 'string') { + if (typeof name !== "string") { name = String(name); } if (/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(name)) { - throw new TypeError('Invalid character in header field name'); + throw new TypeError("Invalid character in header field name"); } return name.toLowerCase(); } function normalizeValue(value) { - if (typeof value !== 'string') { + if (typeof value !== "string") { value = String(value); } return value; @@ -106,10 +116,10 @@ name = normalizeName(name); value = normalizeValue(value); var oldValue = this.map[name]; - this.map[name] = oldValue ? oldValue + ', ' + value : value; + this.map[name] = oldValue ? oldValue + ", " + value : value; }; - Headers.prototype['delete'] = function(name) { + Headers.prototype["delete"] = function(name) { delete this.map[normalizeName(name)]; }; @@ -164,7 +174,7 @@ function consumed(body) { if (body.bodyUsed) { - return Promise.reject(new TypeError('Already read')); + return Promise.reject(new TypeError("Already read")); } body.bodyUsed = true; } @@ -201,7 +211,7 @@ for (var i = 0; i < view.length; i++) { chars[i] = String.fromCharCode(view[i]); } - return chars.join(''); + return chars.join(""); } function bufferClone(buf) { @@ -220,32 +230,44 @@ this._initBody = function(body) { this._bodyInit = body; if (!body) { - this._bodyText = ''; - } else if (typeof body === 'string') { + this._bodyText = ""; + } else if (typeof body === "string") { this._bodyText = body; } else if (support.blob && Blob.prototype.isPrototypeOf(body)) { this._bodyBlob = body; } else if (support.formData && FormData.prototype.isPrototypeOf(body)) { this._bodyFormData = body; - } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { + } else if ( + support.searchParams && + URLSearchParams.prototype.isPrototypeOf(body) + ) { this._bodyText = body.toString(); } else if (support.arrayBuffer && support.blob && isDataView(body)) { this._bodyArrayBuffer = bufferClone(body.buffer); // IE 10-11 can't handle a DataView body. this._bodyInit = new Blob([this._bodyArrayBuffer]); - } else if (support.arrayBuffer && (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body))) { + } else if ( + support.arrayBuffer && + (ArrayBuffer.prototype.isPrototypeOf(body) || isArrayBufferView(body)) + ) { this._bodyArrayBuffer = bufferClone(body); } else { this._bodyText = body = Object.prototype.toString.call(body); } - if (!this.headers.get('content-type')) { - if (typeof body === 'string') { - this.headers.set('content-type', 'text/plain;charset=UTF-8'); + if (!this.headers.get("content-type")) { + if (typeof body === "string") { + this.headers.set("content-type", "text/plain;charset=UTF-8"); } else if (this._bodyBlob && this._bodyBlob.type) { - this.headers.set('content-type', this._bodyBlob.type); - } else if (support.searchParams && URLSearchParams.prototype.isPrototypeOf(body)) { - this.headers.set('content-type', 'application/x-www-form-urlencoded;charset=UTF-8'); + this.headers.set("content-type", this._bodyBlob.type); + } else if ( + support.searchParams && + URLSearchParams.prototype.isPrototypeOf(body) + ) { + this.headers.set( + "content-type", + "application/x-www-form-urlencoded;charset=UTF-8" + ); } } }; @@ -262,7 +284,7 @@ } else if (this._bodyArrayBuffer) { return Promise.resolve(new Blob([this._bodyArrayBuffer])); } else if (this._bodyFormData) { - throw new Error('could not read FormData body as blob'); + throw new Error("could not read FormData body as blob"); } else { return Promise.resolve(new Blob([this._bodyText])); } @@ -288,7 +310,7 @@ } else if (this._bodyArrayBuffer) { return Promise.resolve(readArrayBufferAsText(this._bodyArrayBuffer)); } else if (this._bodyFormData) { - throw new Error('could not read FormData body as text'); + throw new Error("could not read FormData body as text"); } else { return Promise.resolve(this._bodyText); } @@ -308,7 +330,7 @@ } // HTTP methods whose capitalization should be normalized - var methods = ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']; + var methods = ["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"]; function normalizeMethod(method) { var upcased = method.toUpperCase(); @@ -321,7 +343,7 @@ if (input instanceof Request) { if (input.bodyUsed) { - throw new TypeError('Already read'); + throw new TypeError("Already read"); } this.url = input.url; this.credentials = input.credentials; @@ -339,17 +361,17 @@ this.url = String(input); } - this.credentials = options.credentials || this.credentials || 'same-origin'; + this.credentials = options.credentials || this.credentials || "same-origin"; if (options.headers || !this.headers) { this.headers = new Headers(options.headers); } - this.method = normalizeMethod(options.method || this.method || 'GET'); + this.method = normalizeMethod(options.method || this.method || "GET"); this.mode = options.mode || this.mode || null; this.signal = options.signal || this.signal; this.referrer = null; - if ((this.method === 'GET' || this.method === 'HEAD') && body) { - throw new TypeError('Body not allowed for GET or HEAD requests'); + if ((this.method === "GET" || this.method === "HEAD") && body) { + throw new TypeError("Body not allowed for GET or HEAD requests"); } this._initBody(body); } @@ -362,12 +384,12 @@ var form = new FormData(); body .trim() - .split('&') + .split("&") .forEach(function(bytes) { if (bytes) { - var split = bytes.split('='); - var name = split.shift().replace(/\+/g, ' '); - var value = split.join('=').replace(/\+/g, ' '); + var split = bytes.split("="); + var name = split.shift().replace(/\+/g, " "); + var value = split.join("=").replace(/\+/g, " "); form.append(decodeURIComponent(name), decodeURIComponent(value)); } }); @@ -378,12 +400,12 @@ var headers = new Headers(); // Replace instances of \r\n and \n followed by at least one space or horizontal tab with a space // https://tools.ietf.org/html/rfc7230#section-3.2 - var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, ' '); + var preProcessedHeaders = rawHeaders.replace(/\r?\n[\t ]+/g, " "); preProcessedHeaders.split(/\r?\n/).forEach(function(line) { - var parts = line.split(':'); + var parts = line.split(":"); var key = parts.shift().trim(); if (key) { - var value = parts.join(':').trim(); + var value = parts.join(":").trim(); headers.append(key, value); } }); @@ -397,12 +419,12 @@ options = {}; } - this.type = 'default'; + this.type = "default"; this.status = options.status === undefined ? 200 : options.status; this.ok = this.status >= 200 && this.status < 300; - this.statusText = 'statusText' in options ? options.statusText : 'OK'; + this.statusText = "statusText" in options ? options.statusText : "OK"; this.headers = new Headers(options.headers); - this.url = options.url || ''; + this.url = options.url || ""; this._initBody(bodyInit); } @@ -418,8 +440,8 @@ }; Response.error = function() { - var response = new Response(null, { status: 0, statusText: '' }); - response.type = 'error'; + var response = new Response(null, { status: 0, statusText: "" }); + response.type = "error"; return response; }; @@ -427,7 +449,7 @@ Response.redirect = function(url, status) { if (redirectStatuses.indexOf(status) === -1) { - throw new RangeError('Invalid status code'); + throw new RangeError("Invalid status code"); } return new Response(null, { status: status, headers: { location: url } }); @@ -452,7 +474,7 @@ var request = new Request(input, init); if (request.signal && request.signal.aborted) { - return reject(new exports.DOMException('Aborted', 'AbortError')); + return reject(new exports.DOMException("Aborted", "AbortError")); } var xhr = new XMLHttpRequest(); @@ -465,35 +487,38 @@ var options = { status: xhr.status, statusText: xhr.statusText, - headers: parseHeaders(xhr.getAllResponseHeaders() || ''), + headers: parseHeaders(xhr.getAllResponseHeaders() || ""), }; - options.url = 'responseURL' in xhr ? xhr.responseURL : options.headers.get('X-Request-URL'); - var body = 'response' in xhr ? xhr.response : xhr.responseText; + options.url = + "responseURL" in xhr + ? xhr.responseURL + : options.headers.get("X-Request-URL"); + var body = "response" in xhr ? xhr.response : xhr.responseText; resolve(new Response(body, options)); }; xhr.onerror = function() { - reject(new TypeError('Network request failed')); + reject(new TypeError("Network request failed")); }; xhr.ontimeout = function() { - reject(new TypeError('Network request failed')); + reject(new TypeError("Network request failed")); }; xhr.onabort = function() { - reject(new exports.DOMException('Aborted', 'AbortError')); + reject(new exports.DOMException("Aborted", "AbortError")); }; xhr.open(request.method, request.url, true); - if (request.credentials === 'include') { + if (request.credentials === "include") { xhr.withCredentials = true; - } else if (request.credentials === 'omit') { + } else if (request.credentials === "omit") { xhr.withCredentials = false; } - if ('responseType' in xhr && support.blob) { - xhr.responseType = 'blob'; + if ("responseType" in xhr && support.blob) { + xhr.responseType = "blob"; } request.headers.forEach(function(value, name) { @@ -501,17 +526,19 @@ }); if (request.signal) { - request.signal.addEventListener('abort', abortXhr); + request.signal.addEventListener("abort", abortXhr); xhr.onreadystatechange = function() { // DONE (success or failure) if (xhr.readyState === 4) { - request.signal.removeEventListener('abort', abortXhr); + request.signal.removeEventListener("abort", abortXhr); } }; } - xhr.send(typeof request._bodyInit === 'undefined' ? null : request._bodyInit); + xhr.send( + typeof request._bodyInit === "undefined" ? null : request._bodyInit + ); }); } @@ -529,5 +556,5 @@ exports.Response = Response; exports.fetch = fetch; - Object.defineProperty(exports, '__esModule', { value: true }); + Object.defineProperty(exports, "__esModule", { value: true }); }); diff --git a/packages/browser/test/integration/polyfills/includes.js b/packages/browser/test/integration/polyfills/includes.js new file mode 100644 index 000000000000..1d6c700a60e5 --- /dev/null +++ b/packages/browser/test/integration/polyfills/includes.js @@ -0,0 +1,23 @@ +/** + * String.prototype.includes Polyfill + * + * References: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes + */ + +(function() { + if (typeof String.prototype.includes !== "function") { + String.prototype.includes = function(search, start) { + "use strict"; + if (typeof start !== "number") { + start = 0; + } + + if (start + search.length > this.length) { + return false; + } else { + return this.indexOf(search, start) !== -1; + } + }; + } +})(); diff --git a/packages/browser/test/integration/polyfills/nan.js b/packages/browser/test/integration/polyfills/nan.js new file mode 100644 index 000000000000..a42e6d56cb0c --- /dev/null +++ b/packages/browser/test/integration/polyfills/nan.js @@ -0,0 +1,14 @@ +/** + * Number.isNan Polyfill + * + * References: + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN + */ + +(function() { + if (typeof Number.isNaN !== "function") { + Number.isNaN = function(value) { + return value !== value; + }; + } +})(); diff --git a/packages/browser/test/integration/polyfills/es6-promise-4.2.5.auto.js b/packages/browser/test/integration/polyfills/promise.js similarity index 89% rename from packages/browser/test/integration/polyfills/es6-promise-4.2.5.auto.js rename to packages/browser/test/integration/polyfills/promise.js index 3a4de6d6c979..836b31289f69 100644 --- a/packages/browser/test/integration/polyfills/es6-promise-4.2.5.auto.js +++ b/packages/browser/test/integration/polyfills/promise.js @@ -1,27 +1,27 @@ /*! - * @overview es6-promise - a tiny implementation of Promises/A+. + * @overview es6-promise - an implementation of Promise API. * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) * @license Licensed under MIT license * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE - * @version v4.2.5+7f2b526d + * @version v4.2.8+1e68dce6 */ (function(global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' + typeof exports === "object" && typeof module !== "undefined" ? (module.exports = factory()) - : typeof define === 'function' && define.amd - ? define(factory) - : (global.ES6Promise = factory()); + : typeof define === "function" && define.amd + ? define(factory) + : (global.ES6Promise = factory()); })(this, function() { - 'use strict'; + "use strict"; function objectOrFunction(x) { var type = typeof x; - return x !== null && (type === 'object' || type === 'function'); + return x !== null && (type === "object" || type === "function"); } function isFunction(x) { - return typeof x === 'function'; + return typeof x === "function"; } var _isArray = void 0; @@ -29,7 +29,7 @@ _isArray = Array.isArray; } else { _isArray = function(x) { - return Object.prototype.toString.call(x) === '[object Array]'; + return Object.prototype.toString.call(x) === "[object Array]"; }; } @@ -63,17 +63,20 @@ asap = asapFn; } - var browserWindow = typeof window !== 'undefined' ? window : undefined; + var browserWindow = typeof window !== "undefined" ? window : undefined; var browserGlobal = browserWindow || {}; - var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; + var BrowserMutationObserver = + browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; var isNode = - typeof self === 'undefined' && typeof process !== 'undefined' && {}.toString.call(process) === '[object process]'; + typeof self === "undefined" && + typeof process !== "undefined" && + {}.toString.call(process) === "[object process]"; // test for web worker but not in IE10 var isWorker = - typeof Uint8ClampedArray !== 'undefined' && - typeof importScripts !== 'undefined' && - typeof MessageChannel !== 'undefined'; + typeof Uint8ClampedArray !== "undefined" && + typeof importScripts !== "undefined" && + typeof MessageChannel !== "undefined"; // node function useNextTick() { @@ -86,7 +89,7 @@ // vertx function useVertxTimer() { - if (typeof vertxNext !== 'undefined') { + if (typeof vertxNext !== "undefined") { return function() { vertxNext(flush); }; @@ -98,7 +101,7 @@ function useMutationObserver() { var iterations = 0; var observer = new BrowserMutationObserver(flush); - var node = document.createTextNode(''); + var node = document.createTextNode(""); observer.observe(node, { characterData: true }); return function() { @@ -141,7 +144,7 @@ function attemptVertx() { try { - var vertx = Function('return this')().require('vertx'); + var vertx = Function("return this")().require("vertx"); vertxNext = vertx.runOnLoop || vertx.runOnContext; return useVertxTimer(); } catch (e) { @@ -157,7 +160,7 @@ scheduleFlush = useMutationObserver(); } else if (isWorker) { scheduleFlush = useMessageChannel(); - } else if (browserWindow === undefined && typeof require === 'function') { + } else if (browserWindow === undefined && typeof require === "function") { scheduleFlush = attemptVertx(); } else { scheduleFlush = useSetTimeout(); @@ -221,7 +224,11 @@ /*jshint validthis:true */ var Constructor = this; - if (object && typeof object === 'object' && object.constructor === Constructor) { + if ( + object && + typeof object === "object" && + object.constructor === Constructor + ) { return object; } @@ -240,23 +247,14 @@ var FULFILLED = 1; var REJECTED = 2; - var TRY_CATCH_ERROR = { error: null }; - function selfFulfillment() { - return new TypeError('You cannot resolve a promise with itself'); + return new TypeError("You cannot resolve a promise with itself"); } function cannotReturnOwn() { - return new TypeError('A promises callback cannot return that same promise.'); - } - - function getThen(promise) { - try { - return promise.then; - } catch (error) { - TRY_CATCH_ERROR.error = error; - return TRY_CATCH_ERROR; - } + return new TypeError( + "A promises callback cannot return that same promise." + ); } function tryThen(then$$1, value, fulfillmentHandler, rejectionHandler) { @@ -292,7 +290,7 @@ reject(promise, reason); }, - 'Settle: ' + (promise._label || ' unknown promise') + "Settle: " + (promise._label || " unknown promise") ); if (!sealed && error) { @@ -329,10 +327,7 @@ ) { handleOwnThenable(promise, maybeThenable); } else { - if (then$$1 === TRY_CATCH_ERROR) { - reject(promise, TRY_CATCH_ERROR.error); - TRY_CATCH_ERROR.error = null; - } else if (then$$1 === undefined) { + if (then$$1 === undefined) { fulfill(promise, maybeThenable); } else if (isFunction(then$$1)) { handleForeignThenable(promise, maybeThenable, then$$1); @@ -346,7 +341,14 @@ if (promise === value) { reject(promise, selfFulfillment()); } else if (objectOrFunction(value)) { - handleMaybeThenable(promise, value, getThen(value)); + var then$$1 = void 0; + try { + then$$1 = value.then; + } catch (error) { + reject(promise, error); + return; + } + handleMaybeThenable(promise, value, then$$1); } else { fulfill(promise, value); } @@ -424,31 +426,18 @@ promise._subscribers.length = 0; } - function tryCatch(callback, detail) { - try { - return callback(detail); - } catch (e) { - TRY_CATCH_ERROR.error = e; - return TRY_CATCH_ERROR; - } - } - function invokeCallback(settled, promise, callback, detail) { var hasCallback = isFunction(callback), value = void 0, error = void 0, - succeeded = void 0, - failed = void 0; + succeeded = true; if (hasCallback) { - value = tryCatch(callback, detail); - - if (value === TRY_CATCH_ERROR) { - failed = true; - error = value.error; - value.error = null; - } else { - succeeded = true; + try { + value = callback(detail); + } catch (e) { + succeeded = false; + error = e; } if (promise === value) { @@ -457,14 +446,13 @@ } } else { value = detail; - succeeded = true; } if (promise._state !== PENDING) { // noop } else if (hasCallback && succeeded) { resolve(promise, value); - } else if (failed) { + } else if (succeeded === false) { reject(promise, error); } else if (settled === FULFILLED) { fulfill(promise, value); @@ -501,7 +489,7 @@ } function validationError() { - return new Error('Array Methods must be provided an Array'); + return new Error("Array Methods must be provided an Array"); } var Enumerator = (function() { @@ -544,16 +532,28 @@ var resolve$$1 = c.resolve; if (resolve$$1 === resolve$1) { - var _then = getThen(entry); + var _then = void 0; + var error = void 0; + var didError = false; + try { + _then = entry.then; + } catch (e) { + didError = true; + error = e; + } if (_then === then && entry._state !== PENDING) { this._settledAt(entry._state, i, entry._result); - } else if (typeof _then !== 'function') { + } else if (typeof _then !== "function") { this._remaining--; this._result[i] = entry; } else if (c === Promise$2) { var promise = new c(noop); - handleMaybeThenable(promise, entry, _then); + if (didError) { + reject(promise, error); + } else { + handleMaybeThenable(promise, entry, _then); + } this._willSettleAt(promise, i); } else { this._willSettleAt( @@ -726,7 +726,7 @@ if (!isArray(entries)) { return new Constructor(function(_, reject) { - return reject(new TypeError('You must pass an array to race.')); + return reject(new TypeError("You must pass an array to race.")); }); } else { return new Constructor(function(resolve, reject) { @@ -781,7 +781,9 @@ } function needsResolver() { - throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); + throw new TypeError( + "You must pass a resolver function as the first argument to the promise constructor" + ); } function needsNew() { @@ -901,8 +903,10 @@ this._subscribers = []; if (noop !== resolver) { - typeof resolver !== 'function' && needsResolver(); - this instanceof Promise ? initializePromise(this, resolver) : needsNew(); + typeof resolver !== "function" && needsResolver(); + this instanceof Promise + ? initializePromise(this, resolver) + : needsNew(); } } @@ -1098,9 +1102,9 @@ /** `finally` will be invoked regardless of the promise's fate just as native try/catch/finally behaves - + Synchronous example: - + ```js findAuthor() { if (Math.random() > 0.5) { @@ -1108,7 +1112,7 @@ } return new Author(); } - + try { return findAuthor(); // succeed or fail } catch(error) { @@ -1118,9 +1122,9 @@ // doesn't affect the return value } ``` - + Asynchronous example: - + ```js findAuthor().catch(function(reason){ return findOtherAuther(); @@ -1128,7 +1132,7 @@ // author was either found, or not }); ``` - + @method finally @param {Function} callback @return {Promise} @@ -1172,15 +1176,17 @@ function polyfill() { var local = void 0; - if (typeof global !== 'undefined') { + if (typeof global !== "undefined") { local = global; - } else if (typeof self !== 'undefined') { + } else if (typeof self !== "undefined") { local = self; } else { try { - local = Function('return this')(); + local = Function("return this")(); } catch (e) { - throw new Error('polyfill failed because global object is unavailable in this environment'); + throw new Error( + "polyfill failed because global object is unavailable in this environment" + ); } } @@ -1194,7 +1200,7 @@ // silently ignored } - if (promiseToString === '[object Promise]' && !P.cast) { + if (promiseToString === "[object Promise]" && !P.cast) { return; } } diff --git a/packages/browser/test/integration/polyfills/raf.js b/packages/browser/test/integration/polyfills/raf.js new file mode 100644 index 000000000000..7075b3dbe8ed --- /dev/null +++ b/packages/browser/test/integration/polyfills/raf.js @@ -0,0 +1,34 @@ +/** + * requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel + * + * http://paulirish.com/2011/requestanimationframe-for-smart-animating/ + * http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating + * + * MIT license + */ +(function() { + var lastTime = 0; + var vendors = ["ms", "moz", "webkit", "o"]; + for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { + window.requestAnimationFrame = window[vendors[x] + "RequestAnimationFrame"]; + window.cancelAnimationFrame = + window[vendors[x] + "CancelAnimationFrame"] || + window[vendors[x] + "CancelRequestAnimationFrame"]; + } + + if (!window.requestAnimationFrame) + window.requestAnimationFrame = function(callback, element) { + var currTime = new Date().getTime(); + var timeToCall = Math.max(0, 16 - (currTime - lastTime)); + var id = window.setTimeout(function() { + callback(currTime + timeToCall); + }, timeToCall); + lastTime = currTime + timeToCall; + return id; + }; + + if (!window.cancelAnimationFrame) + window.cancelAnimationFrame = function(id) { + clearTimeout(id); + }; +})(); diff --git a/packages/browser/test/integration/run.js b/packages/browser/test/integration/run.js new file mode 100755 index 000000000000..add8a4aa20ac --- /dev/null +++ b/packages/browser/test/integration/run.js @@ -0,0 +1,157 @@ +#!/usr/bin/env node + +const fs = require("fs"); +const rimraf = require("rimraf"); +const karma = require("karma"); +const path = require("path"); +const chokidar = require("chokidar"); + +const isDebugMode = process.argv.some(x => x === "--debug"); +const isWatchMode = process.argv.some(x => x === "--watch"); +const isManualMode = process.argv.some(x => x === "--manual"); + +function log(...message) { + if (isDebugMode) { + console.log(...message); + } +} + +function readFile(file) { + return fs.readFileSync(path.resolve(__dirname, file), "utf8"); +} + +function writeFile(file, data) { + fs.writeFileSync(path.resolve(__dirname, file), data); +} + +function copyFile(from, to) { + log("Copying file:\n\t=> from:", from, "\n\t=> to:", to); + fs.copyFileSync(path.resolve(__dirname, from), path.resolve(__dirname, to)); +} + +function concatFiles(outputFile, inputFiles) { + log( + "Concatinating:\n\t=> from:", + inputFiles.join(", "), + "\n\t=> to:", + outputFile + ); + writeFile(outputFile, inputFiles.map(file => readFile(file)).join("\n")); +} + +function replacePlaceholders(templateFile) { + log("Replacing placeholders for file:", templateFile); + + return readFile(templateFile).replace( + /\{\{ ?([a-zA-Z-_\.\/]+) ?\}\}/g, + match => { + const matchFile = match.slice(2, -2).trim(); + log("\t=> matched placeholder:", matchFile); + return readFile(matchFile); + } + ); +} + +function mkdir(dirpath) { + fs.mkdirSync(path.resolve(__dirname, dirpath)); +} + +function rmdir(dirpath) { + rimraf.sync(path.resolve(__dirname, dirpath)); +} + +function build() { + log(` +╔════════════════════════════╗ +║ INFO: Building test assets ║ +╚════════════════════════════╝ +`); + + rmdir("artifacts"); + mkdir("artifacts"); + + concatFiles("artifacts/polyfills.js", [ + "polyfills/promise.js", + "polyfills/fetch.js", + "polyfills/raf.js", + "polyfills/events.js", + "polyfills/assign.js", + "polyfills/nan.js", + "polyfills/includes.js", + ]); + + writeFile( + "artifacts/dedupe.js", + readFile("../../../integrations/build/dedupe.js").replace( + "//# sourceMappingURL=dedupe.js.map", + "" + ) + ); + concatFiles("artifacts/setup.js", [ + "artifacts/dedupe.js", + "common/utils.js", + "common/triggers.js", + "common/init.js", + ]); + rmdir("artifacts/dedupe.js"); + + writeFile( + "artifacts/sdk.js", + readFile("../../build/bundle.js").replace( + "//# sourceMappingURL=bundle.js.map", + "" + ) + ); + writeFile( + "artifacts/loader.js", + readFile("../../src/loader.js").replace( + "../../build/bundle.js", + "/base/artifacts/sdk.js" + ) + ); + + writeFile( + "artifacts/tests.js", + [ + readFile("polyfills/promise.js"), + readFile("suites/helpers.js"), + replacePlaceholders("suites/shell.js"), + ].join("\n") + ); +} + +build(); + +let fileWatcher; +if (isWatchMode) { + fileWatcher = chokidar + .watch([ + path.resolve(__dirname, "common"), + path.resolve(__dirname, "subjects"), + path.resolve(__dirname, "suites"), + ]) + .on("change", build); +} + +const karmaConfigOverrides = { + ...(isWatchMode && { + singleRun: false, + autoWatch: true, + }), + ...(isManualMode && { + customLaunchers: {}, + browsers: [], + }), +}; + +new karma.Server( + karma.config.parseConfig( + path.resolve(__dirname, "karma.conf.js"), + karmaConfigOverrides + ), + exitCode => { + rmdir("artifacts"); + if (fileWatcher) fileWatcher.close(); + process.exit(exitCode); + } +).start(); diff --git a/packages/browser/test/integration/123 b/packages/browser/test/integration/subjects/123 similarity index 100% rename from packages/browser/test/integration/123 rename to packages/browser/test/integration/subjects/123 diff --git a/packages/browser/test/integration/subjects/console-logs.js b/packages/browser/test/integration/subjects/console-logs.js new file mode 100644 index 000000000000..20db22fa0041 --- /dev/null +++ b/packages/browser/test/integration/subjects/console-logs.js @@ -0,0 +1,7 @@ +console.log("One"); +console.warn("Two", { a: 1 }); +console.error("Error 2", { b: { c: [] } }); +function a() { + throw new Error("Error thrown 3"); +} +a(); diff --git a/packages/browser/test/integration/example.json b/packages/browser/test/integration/subjects/example.json similarity index 100% rename from packages/browser/test/integration/example.json rename to packages/browser/test/integration/subjects/example.json diff --git a/packages/browser/test/integration/throw-error.js b/packages/browser/test/integration/subjects/throw-error.js similarity index 60% rename from packages/browser/test/integration/throw-error.js rename to packages/browser/test/integration/subjects/throw-error.js index 4ba4f3415da1..32076658cc45 100644 --- a/packages/browser/test/integration/throw-error.js +++ b/packages/browser/test/integration/subjects/throw-error.js @@ -1,4 +1,4 @@ function throwRealError() { - throw new Error('realError'); + throw new Error("realError"); } throwRealError(); diff --git a/packages/browser/test/integration/throw-object.js b/packages/browser/test/integration/subjects/throw-object.js similarity index 77% rename from packages/browser/test/integration/throw-object.js rename to packages/browser/test/integration/subjects/throw-object.js index 8e59dc5f75db..003aa22e48a8 100644 --- a/packages/browser/test/integration/throw-object.js +++ b/packages/browser/test/integration/subjects/throw-object.js @@ -1,7 +1,7 @@ function throwStringError() { // never do this; just making sure Raven.js handles this case // gracefully - throw { error: 'stuff is broken' }; + throw { error: "stuff is broken" }; } throwStringError(); diff --git a/packages/browser/test/integration/throw-string.js b/packages/browser/test/integration/subjects/throw-string.js similarity index 69% rename from packages/browser/test/integration/throw-string.js rename to packages/browser/test/integration/subjects/throw-string.js index 7429303cab1a..9e0dfc04aa75 100644 --- a/packages/browser/test/integration/throw-string.js +++ b/packages/browser/test/integration/subjects/throw-string.js @@ -1,5 +1,5 @@ function throwStringError() { - throw 'stringError'; + throw "stringError"; } throwStringError(); diff --git a/packages/browser/test/integration/suites/api.js b/packages/browser/test/integration/suites/api.js new file mode 100644 index 000000000000..3e2146337f7a --- /dev/null +++ b/packages/browser/test/integration/suites/api.js @@ -0,0 +1,182 @@ +describe("API", function() { + it("should capture Sentry.captureMessage", function() { + return runInSandbox(sandbox, function() { + Sentry.captureMessage("Hello"); + }).then(function(summary) { + assert.equal(summary.events[0].message, "Hello"); + }); + }); + + it("should capture Sentry.captureException", function() { + return runInSandbox(sandbox, function() { + try { + foo(); + } catch (e) { + Sentry.captureException(e); + } + }).then(function(summary) { + assert.isAtLeast( + summary.events[0].exception.values[0].stacktrace.frames.length, + 2 + ); + assert.isAtMost( + summary.events[0].exception.values[0].stacktrace.frames.length, + 4 + ); + }); + }); + + it("should generate a synthetic trace for captureException w/ non-errors", function() { + return runInSandbox(sandbox, function() { + throwNonError(); + }).then(function(summary) { + assert.isAtLeast(summary.events[0].stacktrace.frames.length, 1); + assert.isAtMost(summary.events[0].stacktrace.frames.length, 3); + }); + }); + + it("should have correct stacktrace order", function() { + return runInSandbox(sandbox, function() { + try { + foo(); + } catch (e) { + Sentry.captureException(e); + } + }).then(function(summary) { + assert.equal( + summary.events[0].exception.values[0].stacktrace.frames[ + summary.events[0].exception.values[0].stacktrace.frames.length - 1 + ].function, + "bar" + ); + assert.isAtLeast( + summary.events[0].exception.values[0].stacktrace.frames.length, + 2 + ); + assert.isAtMost( + summary.events[0].exception.values[0].stacktrace.frames.length, + 4 + ); + }); + }); + + it("should have exception with type and value", function() { + return runInSandbox(sandbox, function() { + Sentry.captureException("this is my test exception"); + }).then(function(summary) { + assert.isNotEmpty(summary.events[0].exception.values[0].value); + assert.isNotEmpty(summary.events[0].exception.values[0].type); + }); + }); + + it("should reject duplicate, back-to-back errors from captureException", function() { + return runInSandbox(sandbox, function() { + // Different exceptions, don't dedupe + for (var i = 0; i < 2; i++) { + throwRandomError(); + } + + // Same exceptions and same stacktrace, dedupe + for (var i = 0; i < 2; i++) { + throwError(); + } + + // Same exceptions, different stacktrace (different line number), don't dedupe + throwSameConsecutiveErrors("bar"); + }).then(function(summary) { + assert.match( + summary.events[0].exception.values[0].value, + /Exception no \d+/ + ); + assert.match( + summary.events[1].exception.values[0].value, + /Exception no \d+/ + ); + assert.equal(summary.events[2].exception.values[0].value, "foo"); + assert.equal(summary.events[3].exception.values[0].value, "bar"); + assert.equal(summary.events[4].exception.values[0].value, "bar"); + }); + }); + + it("should not reject back-to-back errors with different stack traces", function() { + return runInSandbox(sandbox, function() { + // same error message, but different stacks means that these are considered + // different errors + + // stack: + // bar + try { + bar(); // declared in frame.html + } catch (e) { + Sentry.captureException(e); + } + + // stack (different # frames): + // bar + // foo + try { + foo(); // declared in frame.html + } catch (e) { + Sentry.captureException(e); + } + + // stack (same # frames, different frames): + // bar + // foo2 + try { + foo2(); // declared in frame.html + } catch (e) { + Sentry.captureException(e); + } + }).then(function(summary) { + // NOTE: regex because exact error message differs per-browser + assert.match(summary.events[0].exception.values[0].value, /baz/); + assert.equal( + summary.events[0].exception.values[0].type, + "ReferenceError" + ); + assert.match(summary.events[1].exception.values[0].value, /baz/); + assert.equal( + summary.events[1].exception.values[0].type, + "ReferenceError" + ); + assert.match(summary.events[2].exception.values[0].value, /baz/); + assert.equal( + summary.events[2].exception.values[0].type, + "ReferenceError" + ); + }); + }); + + it("should reject duplicate, back-to-back messages from captureMessage", function() { + return runInSandbox(sandbox, function() { + // Different messages, don't dedupe + for (var i = 0; i < 2; i++) { + captureRandomMessage(); + } + + // Same messages and same stacktrace, dedupe + for (var i = 0; i < 2; i++) { + captureMessage("same message, same stacktrace"); + } + + // Same messages, different stacktrace (different line number), don't dedupe + captureSameConsecutiveMessages("same message, different stacktrace"); + }).then(function(summary) { + // On the async loader since we replay all messages from the same location, + // so we actually only receive 4 summary.events + assert.match(summary.events[0].message, /Message no \d+/); + assert.match(summary.events[1].message, /Message no \d+/); + assert.equal(summary.events[2].message, "same message, same stacktrace"); + assert.equal( + summary.events[3].message, + "same message, different stacktrace" + ); + !IS_LOADER && + assert.equal( + summary.events[4].message, + "same message, different stacktrace" + ); + }); + }); +}); diff --git a/packages/browser/test/integration/suites/breadcrumbs.js b/packages/browser/test/integration/suites/breadcrumbs.js new file mode 100644 index 000000000000..39629efcc81f --- /dev/null +++ b/packages/browser/test/integration/suites/breadcrumbs.js @@ -0,0 +1,741 @@ +describe("breadcrumbs", function() { + it( + optional("should record an XMLHttpRequest with a handler", IS_LOADER), + function() { + return runInSandbox(sandbox, { manual: true }, function() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "/base/subjects/example.json"); + xhr.onreadystatechange = function() {}; + xhr.send(); + waitForXHR(xhr, function() { + Sentry.captureMessage("test"); + window.finalizeManualTest(); + }); + }).then(function(summary) { + // The async loader doesn't wrap XHR + if (IS_LOADER) { + return; + } + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].type, "http"); + assert.equal(summary.breadcrumbs[0].category, "xhr"); + assert.equal(summary.breadcrumbs[0].data.method, "GET"); + }); + } + ); + + it( + optional( + "should record an XMLHttpRequest without any handlers set", + IS_LOADER + ), + function() { + return runInSandbox(sandbox, { manual: true }, function() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "/base/subjects/example.json"); + xhr.send(); + waitForXHR(xhr, function() { + Sentry.captureMessage("test"); + window.finalizeManualTest(); + }); + }).then(function(summary) { + // The async loader doesn't wrap XHR + if (IS_LOADER) { + return; + } + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].type, "http"); + assert.equal(summary.breadcrumbs[0].category, "xhr"); + assert.equal(summary.breadcrumbs[0].data.method, "GET"); + }); + } + ); + + it( + optional( + "should transform XMLHttpRequests to the Sentry store endpoint as sentry type breadcrumb", + IS_LOADER + ), + function() { + return runInSandbox(sandbox, { manual: true }, function() { + var store = + document.location.protocol + + "//" + + document.location.hostname + + (document.location.port ? ":" + document.location.port : "") + + "/api/1/store/"; + + var xhr = new XMLHttpRequest(); + xhr.open("GET", store); + xhr.send('{"message":"someMessage","level":"warning"}'); + waitForXHR(xhr, function() { + Sentry.captureMessage("test"); + window.finalizeManualTest(); + }); + }).then(function(summary) { + // The async loader doesn't wrap XHR + if (IS_LOADER) { + return; + } + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].category, "sentry"); + assert.equal(summary.breadcrumbs[0].level, "warning"); + assert.equal(summary.breadcrumbs[0].message, "someMessage"); + }); + } + ); + + it("should record a fetch request", function() { + return runInSandbox(sandbox, { manual: true }, function() { + fetch("/base/subjects/example.json") + .then( + function() { + Sentry.captureMessage("test"); + }, + function() { + Sentry.captureMessage("test"); + } + ) + .then(function() { + window.finalizeManualTest(); + }) + .catch(function() { + window.finalizeManualTest(); + }); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap fetch, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + if (summary.window.supportsNativeFetch()) { + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].type, "http"); + assert.equal(summary.breadcrumbs[0].category, "fetch"); + assert.equal(summary.breadcrumbs[0].data.method, "GET"); + assert.equal( + summary.breadcrumbs[0].data.url, + "/base/subjects/example.json" + ); + } else { + // otherwise we use a fetch polyfill based on xhr + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].type, "http"); + assert.equal(summary.breadcrumbs[0].category, "xhr"); + assert.equal(summary.breadcrumbs[0].data.method, "GET"); + assert.equal( + summary.breadcrumbs[0].data.url, + "/base/subjects/example.json" + ); + } + } + }); + }); + + it("should record a fetch request with Request obj instead of URL string", function() { + return runInSandbox(sandbox, { manual: true }, function() { + fetch(new Request("/base/subjects/example.json")) + .then( + function() { + Sentry.captureMessage("test"); + }, + function() { + Sentry.captureMessage("test"); + } + ) + .then(function() { + window.finalizeManualTest(); + }) + .catch(function() { + window.finalizeManualTest(); + }); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap fetch, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + if (summary.window.supportsNativeFetch()) { + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].type, "http"); + assert.equal(summary.breadcrumbs[0].category, "fetch"); + assert.equal(summary.breadcrumbs[0].data.method, "GET"); + // Request constructor normalizes the url + assert.ok( + summary.breadcrumbs[0].data.url.indexOf( + "/base/subjects/example.json" + ) !== -1 + ); + } else { + // otherwise we use a fetch polyfill based on xhr + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].type, "http"); + assert.equal(summary.breadcrumbs[0].category, "xhr"); + assert.equal(summary.breadcrumbs[0].data.method, "GET"); + assert.ok( + summary.breadcrumbs[0].data.url.indexOf( + "/base/subjects/example.json" + ) !== -1 + ); + } + } + }); + }); + + it("should record a fetch request with an arbitrary type argument", function() { + return runInSandbox(sandbox, { manual: true }, function() { + fetch(123) + .then( + function() { + Sentry.captureMessage("test"); + }, + function() { + Sentry.captureMessage("test"); + } + ) + .then(function() { + window.finalizeManualTest(); + }) + .catch(function() { + window.finalizeManualTest(); + }); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap fetch, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + if (summary.window.supportsNativeFetch()) { + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].type, "http"); + assert.equal(summary.breadcrumbs[0].category, "fetch"); + assert.equal(summary.breadcrumbs[0].data.method, "GET"); + assert.ok(summary.breadcrumbs[0].data.url.indexOf("123") !== -1); + } else { + // otherwise we use a fetch polyfill based on xhr + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].type, "http"); + assert.equal(summary.breadcrumbs[0].category, "xhr"); + assert.equal(summary.breadcrumbs[0].data.method, "GET"); + assert.ok(summary.breadcrumbs[0].data.url.indexOf("123") !== -1); + } + } + }); + }); + + it("should not fail with click or keypress handler with no callback", function() { + return runInSandbox(sandbox, function() { + var input = document.getElementsByTagName("input")[0]; + input.addEventListener("click", undefined); + input.addEventListener("keypress", undefined); + + var click = new MouseEvent("click"); + input.dispatchEvent(click); + + var keypress = new KeyboardEvent("keypress"); + input.dispatchEvent(keypress); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 2); + + assert.equal(summary.breadcrumbs[0].category, "ui.click"); + assert.equal( + summary.breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + + assert.equal(summary.breadcrumbs[1].category, "ui.input"); + assert.equal( + summary.breadcrumbs[1].message, + 'body > form#foo-form > input[name="foo"]' + ); + + // There should be no expection, if there is one it means we threw it + assert.isUndefined(summary.events[0].exception); + } + }); + }); + + it("should not fail with custom event", function() { + return runInSandbox(sandbox, function() { + var input = document.getElementsByTagName("input")[0]; + input.addEventListener("build", function(evt) { + evt.stopPropagation(); + }); + + var customEvent = new CustomEvent("build", { detail: 1 }); + input.dispatchEvent(customEvent); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + // There should be no expection, if there is one it means we threw it + assert.isUndefined(summary.events[0].exception); + assert.equal(summary.breadcrumbs.length, 0); + } + }); + }); + + it("should not fail with custom event and handler with no callback", function() { + return runInSandbox(sandbox, function() { + var input = document.getElementsByTagName("input")[0]; + input.addEventListener("build", undefined); + + var customEvent = new CustomEvent("build", { detail: 1 }); + input.dispatchEvent(customEvent); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + // There should be no expection, if there is one it means we threw it + assert.isUndefined(summary.events[0].exception); + assert.equal(summary.breadcrumbs.length, 0); + } + }); + }); + + it("should record a mouse click on element WITH click handler present", function() { + return runInSandbox(sandbox, function() { + // add an event listener to the input. we want to make sure that + // our breadcrumbs still work even if the page has an event listener + // on an element that cancels event bubbling + var input = document.getElementsByTagName("input")[0]; + var clickHandler = function(evt) { + evt.stopPropagation(); // don't bubble + }; + input.addEventListener("click", clickHandler); + + // click + var click = new MouseEvent("click"); + input.dispatchEvent(click); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 1); + + assert.equal(summary.breadcrumbs[0].category, "ui.click"); + assert.equal( + summary.breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + } + }); + }); + + it("should record a mouse click on element WITHOUT click handler present", function() { + return runInSandbox(sandbox, function() { + // click + var click = new MouseEvent("click"); + var input = document.getElementsByTagName("input")[0]; + input.dispatchEvent(click); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 1); + + assert.equal(summary.breadcrumbs[0].category, "ui.click"); + assert.equal( + summary.breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + } + }); + }); + + it("should only record a SINGLE mouse click for a tree of elements with event listeners", function() { + return runInSandbox(sandbox, function() { + var clickHandler = function() {}; + + // mousemove event shouldnt clobber subsequent "breadcrumbed" events (see #724) + document.querySelector(".a").addEventListener("mousemove", clickHandler); + + document.querySelector(".a").addEventListener("click", clickHandler); + document.querySelector(".b").addEventListener("click", clickHandler); + document.querySelector(".c").addEventListener("click", clickHandler); + + // click + var click = new MouseEvent("click"); + var input = document.querySelector(".a"); // leaf node + input.dispatchEvent(click); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 1); + + assert.equal(summary.breadcrumbs[0].category, "ui.click"); + assert.equal( + summary.breadcrumbs[0].message, + "body > div.c > div.b > div.a" + ); + } + }); + }); + + it("should bail out if accessing the `type` and `target` properties of an event throw an exception", function() { + // see: https://github.com/getsentry/sentry-javascript/issues/768 + return runInSandbox(sandbox, function() { + // click + var click = new MouseEvent("click"); + function kaboom() { + throw new Error("lol"); + } + Object.defineProperty(click, "type", { get: kaboom }); + Object.defineProperty(click, "target", { get: kaboom }); + + var input = document.querySelector(".a"); // leaf node + + Sentry.captureMessage("test"); + input.dispatchEvent(click); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].category, "ui.click"); + assert.equal(summary.breadcrumbs[0].message, ""); + } + }); + }); + + it('should record consecutive keypress events into a single "input" breadcrumb', function() { + return runInSandbox(sandbox, function() { + // keypress twice + var keypress1 = new KeyboardEvent("keypress"); + var keypress2 = new KeyboardEvent("keypress"); + + var input = document.getElementsByTagName("input")[0]; + input.dispatchEvent(keypress1); + input.dispatchEvent(keypress2); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 1); + + assert.equal(summary.breadcrumbs[0].category, "ui.input"); + assert.equal( + summary.breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + } + }); + }); + + it( + optional( + "should flush keypress breadcrumbs when an error is thrown", + IS_LOADER + ), + function() { + return runInSandbox(sandbox, function() { + // keypress + var keypress = new KeyboardEvent("keypress"); + var input = document.getElementsByTagName("input")[0]; + input.dispatchEvent(keypress); + foo(); // throw exception + }).then(function(summary) { + if (IS_LOADER) { + return; + } + // TODO: don't really understand what's going on here + // Why do we not catch an error here + + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].category, "ui.input"); + assert.equal( + summary.breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + }); + } + ); + + it("should flush keypress breadcrumb when input event occurs immediately after", function() { + return runInSandbox(sandbox, function() { + // 1st keypress + var keypress1 = new KeyboardEvent("keypress"); + // click + var click = new MouseEvent("click"); + // 2nd keypress + var keypress2 = new KeyboardEvent("keypress"); + + var input = document.getElementsByTagName("input")[0]; + input.dispatchEvent(keypress1); + input.dispatchEvent(click); + input.dispatchEvent(keypress2); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 3); + + assert.equal(summary.breadcrumbs[0].category, "ui.input"); + assert.equal( + summary.breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + + assert.equal(summary.breadcrumbs[1].category, "ui.click"); + assert.equal( + summary.breadcrumbs[1].message, + 'body > form#foo-form > input[name="foo"]' + ); + + assert.equal(summary.breadcrumbs[2].category, "ui.input"); + assert.equal( + summary.breadcrumbs[2].message, + 'body > form#foo-form > input[name="foo"]' + ); + } + }); + }); + + it('should record consecutive keypress events in a contenteditable into a single "input" breadcrumb', function() { + return runInSandbox(sandbox, function() { + // keypress twice + var keypress1 = new KeyboardEvent("keypress"); + var keypress2 = new KeyboardEvent("keypress"); + + var div = document.querySelector("[contenteditable]"); + div.dispatchEvent(keypress1); + div.dispatchEvent(keypress2); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 1); + + assert.equal(summary.breadcrumbs[0].category, "ui.input"); + assert.equal( + summary.breadcrumbs[0].message, + "body > form#foo-form > div.contenteditable" + ); + } + }); + }); + + it("should record click events that were handled using an object with handleEvent property and call original callback", function() { + return runInSandbox(sandbox, function() { + window.handleEventCalled = false; + + var input = document.getElementsByTagName("input")[0]; + input.addEventListener("click", { + handleEvent: function() { + window.handleEventCalled = true; + }, + }); + input.dispatchEvent(new MouseEvent("click")); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].category, "ui.click"); + assert.equal( + summary.breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + + assert.equal(summary.window.handleEventCalled, true); + } + }); + }); + + it("should record keypress events that were handled using an object with handleEvent property and call original callback", function() { + return runInSandbox(sandbox, function() { + window.handleEventCalled = false; + + var input = document.getElementsByTagName("input")[0]; + input.addEventListener("keypress", { + handleEvent: function() { + window.handleEventCalled = true; + }, + }); + input.dispatchEvent(new KeyboardEvent("keypress")); + + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs + assert.lengthOf(summary.events, 1); + } else { + assert.equal(summary.breadcrumbs.length, 1); + assert.equal(summary.breadcrumbs[0].category, "ui.input"); + assert.equal( + summary.breadcrumbs[0].message, + 'body > form#foo-form > input[name="foo"]' + ); + + assert.equal(summary.window.handleEventCalled, true); + } + }); + }); + + it( + optional( + "should record history.[pushState|replaceState] changes as navigation breadcrumbs", + IS_LOADER + ), + function() { + return runInSandbox(sandbox, function() { + history.pushState({}, "", "/foo"); + history.pushState({}, "", "/bar?a=1#fragment"); + history.pushState({}, "", {}); // pushState calls toString on non-string args + history.pushState({}, "", null); // does nothing / no-op + // can't call history.back() because it will change url of parent document + // (e.g. document running mocha) ... instead just "emulate" a back button + // press by calling replaceState + history.replaceState({}, "", "/bar?a=1#fragment"); + Sentry.captureMessage("test"); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap history + return; + } + assert.equal(summary.breadcrumbs.length, 4); + assert.equal(summary.breadcrumbs[0].category, "navigation"); // (start) => foo + assert.equal(summary.breadcrumbs[1].category, "navigation"); // foo => bar?a=1#fragment + assert.equal(summary.breadcrumbs[2].category, "navigation"); // bar?a=1#fragment => [object%20Object] + assert.equal(summary.breadcrumbs[3].category, "navigation"); // [object%20Object] => bar?a=1#fragment (back button) + + assert.ok( + /\/base\/variants\/.*\.html$/.test(summary.breadcrumbs[0].data.from), + "'from' url is incorrect" + ); + assert.ok( + /\/foo$/.test(summary.breadcrumbs[0].data.to), + "'to' url is incorrect" + ); + + assert.ok( + /\/foo$/.test(summary.breadcrumbs[1].data.from), + "'from' url is incorrect" + ); + assert.ok( + /\/bar\?a=1#fragment$/.test(summary.breadcrumbs[1].data.to), + "'to' url is incorrect" + ); + + assert.ok( + /\/bar\?a=1#fragment$/.test(summary.breadcrumbs[2].data.from), + "'from' url is incorrect" + ); + assert.ok( + /\[object Object\]$/.test(summary.breadcrumbs[2].data.to), + "'to' url is incorrect" + ); + + assert.ok( + /\[object Object\]$/.test(summary.breadcrumbs[3].data.from), + "'from' url is incorrect" + ); + assert.ok( + /\/bar\?a=1#fragment/.test(summary.breadcrumbs[3].data.to), + "'to' url is incorrect" + ); + }); + } + ); + + it( + optional("should preserve native code detection compatibility", IS_LOADER), + function() { + return runInSandbox(sandbox, { manual: true }, function() { + window.resolveTest(); + }).then(function() { + if (IS_LOADER) { + // The async loader doesn't wrap anything + return; + } + assert.include( + Function.prototype.toString.call(window.setTimeout), + "[native code]" + ); + assert.include( + Function.prototype.toString.call(window.setInterval), + "[native code]" + ); + assert.include( + Function.prototype.toString.call(window.addEventListener), + "[native code]" + ); + assert.include( + Function.prototype.toString.call(window.removeEventListener), + "[native code]" + ); + assert.include( + Function.prototype.toString.call(window.requestAnimationFrame), + "[native code]" + ); + if ("fetch" in window) { + assert.include( + Function.prototype.toString.call(window.fetch), + "[native code]" + ); + } + }); + } + ); + + it("should add breadcrumbs on thrown errors", function() { + return runInSandbox(sandbox, { manual: true }, function() { + window.allowConsoleBreadcrumbs = true; + var logs = document.createElement("script"); + logs.src = "/base/subjects/console-logs.js"; + logs.onload = function() { + window.finalizeManualTest(); + }; + document.head.appendChild(logs); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't capture breadcrumbs, but we should receive the event without them + assert.lengthOf(summary.events, 1); + } else { + assert.ok(summary.breadcrumbs); + assert.lengthOf(summary.breadcrumbs, 3); + assert.deepEqual(summary.breadcrumbs[0].data.extra.arguments, ["One"]); + assert.deepEqual(summary.breadcrumbs[1].data.extra.arguments, [ + "Two", + { a: 1 }, + ]); + assert.deepEqual(summary.breadcrumbs[2].data.extra.arguments, [ + "Error 2", + { b: { c: "[Array]" } }, + ]); + } + }); + }); +}); diff --git a/packages/browser/test/integration/suites/builtins.js b/packages/browser/test/integration/suites/builtins.js new file mode 100644 index 000000000000..d2e4fef26770 --- /dev/null +++ b/packages/browser/test/integration/suites/builtins.js @@ -0,0 +1,368 @@ +describe("wrapped built-ins", function() { + it("should capture exceptions from event listeners", function() { + return runInSandbox(sandbox, function() { + var div = document.createElement("div"); + document.body.appendChild(div); + div.addEventListener( + "click", + function() { + window.element = div; + window.context = this; + foo(); + }, + false + ); + var click = new MouseEvent("click"); + div.dispatchEvent(click); + }).then(function(summary) { + // Make sure we preserve the correct context + assert.equal(summary.window.element, summary.window.context); + delete summary.window.element; + delete summary.window.context; + assert.match(summary.events[0].exception.values[0].value, /baz/); + }); + }); + + it("should transparently remove event listeners from wrapped functions", function() { + return runInSandbox(sandbox, function() { + var div = document.createElement("div"); + document.body.appendChild(div); + var click = new MouseEvent("click"); + var fooFn = function() { + foo(); + }; + var barFn = function() { + bar(); + }; + div.addEventListener("click", fooFn, false); + div.addEventListener("click", barFn); + div.removeEventListener("click", barFn); + div.dispatchEvent(new MouseEvent("click")); + }).then(function(summary) { + assert.lengthOf(summary.events, 1); + }); + }); + + it("should capture unhandledrejection with error", function() { + return runInSandbox(sandbox, function() { + if (isChrome()) { + Promise.reject(new Error("test2")); + } else { + window.resolveTest({ window: window }); + } + }).then(function(summary) { + if (summary.window.isChrome()) { + assert.equal(summary.events[0].exception.values[0].value, "test2"); + assert.equal(summary.events[0].exception.values[0].type, "Error"); + assert.isAtLeast( + summary.events[0].exception.values[0].stacktrace.frames.length, + 1 + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.handled, + false + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.type, + "onunhandledrejection" + ); + } + }); + }); + + it("should capture unhandledrejection with a string", function() { + return runInSandbox(sandbox, function() { + if (isChrome()) { + Promise.reject("test"); + } else { + window.resolveTest({ window: window }); + } + }).then(function(summary) { + if (summary.window.isChrome()) { + // non-error rejections doesnt provide stacktraces so we can skip the assertion + assert.equal(summary.events[0].exception.values[0].value, '"test"'); + assert.equal( + summary.events[0].exception.values[0].type, + "UnhandledRejection" + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.handled, + false + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.type, + "onunhandledrejection" + ); + } + }); + }); + + it("should capture unhandledrejection with a monster string", function() { + return runInSandbox(sandbox, function() { + if (isChrome()) { + Promise.reject("test".repeat(100)); + } else { + window.resolveTest({ window: window }); + } + }).then(function(summary) { + if (summary.window.isChrome()) { + // non-error rejections doesnt provide stacktraces so we can skip the assertion + assert.equal(summary.events[0].exception.values[0].value.length, 253); + assert.equal( + summary.events[0].exception.values[0].type, + "UnhandledRejection" + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.handled, + false + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.type, + "onunhandledrejection" + ); + } + }); + }); + + it("should capture unhandledrejection with an object", function() { + return runInSandbox(sandbox, function() { + if (isChrome()) { + Promise.reject({ a: "b" }); + } else { + window.resolveTest({ window: window }); + } + }).then(function(summary) { + if (summary.window.isChrome()) { + // non-error rejections doesnt provide stacktraces so we can skip the assertion + assert.equal(summary.events[0].exception.values[0].value, '{"a":"b"}'); + assert.equal( + summary.events[0].exception.values[0].type, + "UnhandledRejection" + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.handled, + false + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.type, + "onunhandledrejection" + ); + } + }); + }); + + it("should capture unhandledrejection with an monster object", function() { + return runInSandbox(sandbox, function() { + if (isChrome()) { + var a = { + a: "1".repeat("100"), + b: "2".repeat("100"), + c: "3".repeat("100"), + }; + a.d = a.a; + a.e = a; + Promise.reject(a); + } else { + window.resolveTest({ window: window }); + } + }).then(function(summary) { + if (summary.window.isChrome()) { + // non-error rejections doesnt provide stacktraces so we can skip the assertion + assert.equal(summary.events[0].exception.values[0].value.length, 253); + assert.equal( + summary.events[0].exception.values[0].type, + "UnhandledRejection" + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.handled, + false + ); + assert.equal( + summary.events[0].exception.values[0].mechanism.type, + "onunhandledrejection" + ); + } + }); + }); + + it("should capture exceptions inside setTimeout", function() { + return runInSandbox(sandbox, function() { + setTimeout(function() { + foo(); + }); + }).then(function(summary) { + assert.match(summary.events[0].exception.values[0].value, /baz/); + }); + }); + + it("should capture exceptions inside setInterval", function() { + return runInSandbox(sandbox, function() { + var exceptionInterval = setInterval(function() { + clearInterval(exceptionInterval); + foo(); + }, 0); + }).then(function(summary) { + assert.match(summary.events[0].exception.values[0].value, /baz/); + }); + }); + + it("should capture exceptions inside requestAnimationFrame", function() { + // needs to be visible or requestAnimationFrame won't ever fire + sandbox.style.display = "block"; + + return runInSandbox(sandbox, { manual: true }, function() { + requestAnimationFrame(function() { + window.finalizeManualTest(); + foo(); + }); + }).then(function(summary) { + assert.match(summary.events[0].exception.values[0].value, /baz/); + }); + }); + + it("should capture exceptions from XMLHttpRequest event handlers (e.g. onreadystatechange)", function() { + return runInSandbox(sandbox, { manual: true }, function() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "/base/subjects/example.json"); + // intentionally assign event handlers *after* open, since this is what jQuery does + xhr.onreadystatechange = function() { + window.finalizeManualTest(); + // replace onreadystatechange with no-op so exception doesn't + // fire more than once as XHR changes loading state + xhr.onreadystatechange = function() {}; + foo(); + }; + xhr.send(); + }).then(function(summary) { + assert.match(summary.events[0].exception.values[0].value, /baz/); + }); + }); + + it( + optional( + "should capture built-in's mechanism type as instrument", + IS_LOADER + ), + function() { + return runInSandbox(sandbox, function() { + setTimeout(function() { + foo(); + }); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap setTimeout + // so we don't receive the full mechanism + assert.ok(summary.events[0].exception.values[0].mechanism); + } else { + var fn = + summary.events[0].exception.values[0].mechanism.data.function; + delete summary.events[0].exception.values[0].mechanism.data; + + if (summary.window.canReadFunctionName()) { + assert.equal(fn, "setTimeout"); + } else { + assert.equal(fn, ""); + } + + assert.deepEqual(summary.events[0].exception.values[0].mechanism, { + type: "instrument", + handled: true, + }); + } + }); + } + ); + + it( + optional( + "should capture built-in's handlers fn name in mechanism data", + IS_LOADER + ), + function() { + return runInSandbox(sandbox, function() { + var div = document.createElement("div"); + document.body.appendChild(div); + div.addEventListener( + "click", + function namedFunction() { + foo(); + }, + false + ); + var click = new MouseEvent("click"); + div.dispatchEvent(click); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap addEventListener + // so we don't receive the full mechanism + assert.ok(summary.events[0].exception.values[0].mechanism); + } else { + var handler = + summary.events[0].exception.values[0].mechanism.data.handler; + delete summary.events[0].exception.values[0].mechanism.data.handler; + var target = + summary.events[0].exception.values[0].mechanism.data.target; + delete summary.events[0].exception.values[0].mechanism.data.target; + + if (summary.window.canReadFunctionName()) { + assert.equal(handler, "namedFunction"); + } else { + assert.equal(handler, ""); + } + + // IE vs. Rest of the world + assert.oneOf(target, ["Node", "EventTarget"]); + assert.deepEqual(summary.events[0].exception.values[0].mechanism, { + type: "instrument", + handled: true, + data: { + function: "addEventListener", + }, + }); + } + }); + } + ); + + it( + optional( + "should fallback to fn name in mechanism data if one is unavailable", + IS_LOADER + ), + function() { + return runInSandbox(sandbox, function() { + var div = document.createElement("div"); + document.body.appendChild(div); + div.addEventListener( + "click", + function() { + foo(); + }, + false + ); + var click = new MouseEvent("click"); + div.dispatchEvent(click); + }).then(function(summary) { + if (IS_LOADER) { + // The async loader doesn't wrap + assert.ok(summary.events[0].exception.values[0].mechanism); + } else { + var target = + summary.events[0].exception.values[0].mechanism.data.target; + delete summary.events[0].exception.values[0].mechanism.data.target; + + // IE vs. Rest of the world + assert.oneOf(target, ["Node", "EventTarget"]); + assert.deepEqual(summary.events[0].exception.values[0].mechanism, { + type: "instrument", + handled: true, + data: { + function: "addEventListener", + handler: "", + }, + }); + } + }); + } + ); +}); diff --git a/packages/browser/test/integration/suites/config.js b/packages/browser/test/integration/suites/config.js new file mode 100644 index 000000000000..3ba82a06c3fc --- /dev/null +++ b/packages/browser/test/integration/suites/config.js @@ -0,0 +1,55 @@ +describe("config", function() { + it("should allow to ignore specific errors", function() { + return runInSandbox(sandbox, function() { + Sentry.captureException(new Error("foo")); + Sentry.captureException(new Error("ignoreErrorTest")); + Sentry.captureException(new Error("bar")); + }).then(function(summary) { + assert.equal(summary.events[0].exception.values[0].type, "Error"); + assert.equal(summary.events[0].exception.values[0].value, "foo"); + assert.equal(summary.events[1].exception.values[0].type, "Error"); + assert.equal(summary.events[1].exception.values[0].value, "bar"); + }); + }); + + it("should allow to ignore specific urls", function() { + return runInSandbox(sandbox, function() { + /** + * We always filter on the caller, not the cause of the error + * + * > foo.js file called a function in bar.js + * > bar.js file called a function in baz.js + * > baz.js threw an error + * + * foo.js is blacklisted in the `init` call (init.js), thus we filter it + * */ + var urlWithBlacklistedUrl = new Error("filter"); + urlWithBlacklistedUrl.stack = + "Error: bar\n" + + " at http://localhost:5000/foo.js:7:19\n" + + " at bar(http://localhost:5000/bar.js:2:3)\n" + + " at baz(http://localhost:5000/baz.js:2:9)\n"; + + /** + * > foo-pass.js file called a function in bar-pass.js + * > bar-pass.js file called a function in baz-pass.js + * > baz-pass.js threw an error + * + * foo-pass.js is *not* blacklisted in the `init` call (init.js), thus we don't filter it + * */ + var urlWithoutBlacklistedUrl = new Error("pass"); + urlWithoutBlacklistedUrl.stack = + "Error: bar\n" + + " at http://localhost:5000/foo-pass.js:7:19\n" + + " at bar(http://localhost:5000/bar-pass.js:2:3)\n" + + " at baz(http://localhost:5000/baz-pass.js:2:9)\n"; + + Sentry.captureException(urlWithBlacklistedUrl); + Sentry.captureException(urlWithoutBlacklistedUrl); + }).then(function(summary) { + assert.lengthOf(summary.events, 1); + assert.equal(summary.events[0].exception.values[0].type, "Error"); + assert.equal(summary.events[0].exception.values[0].value, "pass"); + }); + }); +}); diff --git a/packages/browser/test/integration/suites/helpers.js b/packages/browser/test/integration/suites/helpers.js new file mode 100644 index 000000000000..e3ab31bf281f --- /dev/null +++ b/packages/browser/test/integration/suites/helpers.js @@ -0,0 +1,88 @@ +function evaluateInSandbox(sandbox, code) { + // use setTimeout so stack trace doesn't go all the way back to mocha test runner + sandbox && + sandbox.contentWindow && + sandbox.contentWindow.eval( + "window.originalBuiltIns.setTimeout.call(window, " + + code.toString() + + ");" + ); +} + +function runInSandbox(sandbox, options, code) { + if (typeof options === "function") { + code = options; + options = {}; + } + + var resolveTest; + var donePromise = new Promise(function(resolve) { + resolveTest = resolve; + }); + sandbox.contentWindow.resolveTest = function(summary) { + clearTimeout(lastResort); + resolveTest(summary); + }; + + // If by some unexplainable way we reach the timeout limit, try to finalize the test and pray for the best + // NOTE: 5000 so it's easier to grep for all timeout instances (shell.js, loader-specific.js and here) + var lastResort = setTimeout(function() { + var force = function() { + window.resolveTest({ + events: events, + breadcrumbs: breadcrumbs, + window: window, + }); + }; + if (sandbox) { + evaluateInSandbox(sandbox, force.toString()); + } + }, 5000 - 500); + + var finalize = function() { + var summary = { + events: events, + breadcrumbs: breadcrumbs, + window: window, + }; + + Sentry.onLoad(function() { + setTimeout(function() { + Sentry.flush() + .then(function() { + window.resolveTest(summary); + }) + .catch(function() { + window.resolveTest(summary); + }); + }); + }); + }; + + sandbox.contentWindow.finalizeManualTest = function() { + evaluateInSandbox(sandbox, finalize.toString()); + }; + + evaluateInSandbox(sandbox, code.toString()); + + if (!options.manual) { + evaluateInSandbox(sandbox, finalize.toString()); + } + + return donePromise; +} + +function createSandbox(done, file) { + var sandbox = document.createElement("iframe"); + sandbox.style.display = "none"; + sandbox.src = "/base/variants/" + file + ".html"; + sandbox.onload = function() { + done(); + }; + document.body.appendChild(sandbox); + return sandbox; +} + +function optional(title, condition) { + return condition ? "⚠ SKIPPED: " + title : title; +} diff --git a/packages/browser/test/integration/suites/loader-specific.js b/packages/browser/test/integration/suites/loader-specific.js new file mode 100644 index 000000000000..b6382151aaba --- /dev/null +++ b/packages/browser/test/integration/suites/loader-specific.js @@ -0,0 +1,46 @@ +var loaderVariants = [ + "loader-with-no-global-init", + "loader-with-no-global-init-lazy-no", +]; + +for (var idx in loaderVariants) { + (function() { + describe(loaderVariants[idx], function() { + this.timeout(5000); + this.retries(3); + + var sandbox; + + beforeEach(function(done) { + sandbox = createSandbox(done, loaderVariants[idx]); + }); + + afterEach(function() { + document.body.removeChild(sandbox); + }); + + describe("Loader Specific Tests - With no Global init() call", function() { + it("should add breadcrumb from onLoad callback from undefined error", function() { + return runInSandbox(sandbox, function() { + Sentry.onLoad(function() { + initSDK(); + Sentry.addBreadcrumb({ + category: "auth", + message: "testing loader", + level: "error", + }); + }); + setTimeout(function() { + Sentry.captureMessage("test"); + }); + undefinedMethod(); + }).then(function(summary) { + assert.ok(summary.breadcrumbs); + assert.lengthOf(summary.breadcrumbs, 1); + assert.equal(summary.breadcrumbs[0].message, "testing loader"); + }); + }); + }); + }); + })(); +} diff --git a/packages/browser/test/integration/suites/loader.js b/packages/browser/test/integration/suites/loader.js new file mode 100644 index 000000000000..0bac4e9d9590 --- /dev/null +++ b/packages/browser/test/integration/suites/loader.js @@ -0,0 +1,59 @@ +if (IS_LOADER) { + describe("Loader Specific Tests", function() { + it("should add breadcrumb from onLoad callback from undefined error", function() { + return runInSandbox(sandbox, function() { + Sentry.onLoad(function() { + Sentry.addBreadcrumb({ + category: "auth", + message: "testing loader", + level: "error", + }); + }); + setTimeout(function() { + Sentry.captureMessage("test"); + }); + undefinedMethod(); + }).then(function(summary) { + if (IS_ASYNC_LOADER) { + assert.notOk(summary.events[0].breadcrumbs); + } else { + if (summary.events[0].breadcrumbs) { + assert.ok(summary.events[0].breadcrumbs); + assert.lengthOf(summary.events[0].breadcrumbs, 1); + assert.equal( + summary.events[0].breadcrumbs[0].message, + "testing loader" + ); + } else { + // This seems to be happening only in chrome + assert.notOk(summary.events[0].breadcrumbs); + } + } + }); + }); + + it("should add breadcrumb from onLoad callback from undefined error with custom init()", function() { + return runInSandbox(sandbox, function() { + Sentry.onLoad(function() { + Sentry.init({ debug: true }); + Sentry.addBreadcrumb({ + category: "auth", + message: "testing loader", + level: "error", + }); + }); + setTimeout(function() { + Sentry.captureMessage("test"); + }); + undefinedMethod(); //trigger error + }).then(function(summary) { + assert.ok(summary.events[0].breadcrumbs); + assert.lengthOf(summary.events[0].breadcrumbs, 1); + assert.equal( + summary.events[0].breadcrumbs[0].message, + "testing loader" + ); + }); + }); + }); +} diff --git a/packages/browser/test/integration/suites/onerror.js b/packages/browser/test/integration/suites/onerror.js new file mode 100644 index 000000000000..f4817267b4d4 --- /dev/null +++ b/packages/browser/test/integration/suites/onerror.js @@ -0,0 +1,141 @@ +describe("window.onerror", function() { + it("should catch syntax errors", function() { + return runInSandbox(sandbox, function() { + eval("foo{};"); + }).then(function(summary) { + // ¯\_(ツ)_/¯ + if (summary.window.isBelowIE11()) { + assert.equal(summary.events[0].exception.values[0].type, "Error"); + } else { + assert.match(summary.events[0].exception.values[0].type, /SyntaxError/); + } + assert.equal( + summary.events[0].exception.values[0].stacktrace.frames.length, + 1 + ); // just one frame + }); + }); + + it("should catch thrown strings", function() { + return runInSandbox(sandbox, { manual: true }, function() { + // intentionally loading this error via a script file to make + // sure it is 1) not caught by instrumentation 2) doesn't trigger + // "Script error" + var script = document.createElement("script"); + script.src = "/base/subjects/throw-string.js"; + script.onload = function() { + window.finalizeManualTest(); + }; + document.head.appendChild(script); + }).then(function(summary) { + assert.match(summary.events[0].exception.values[0].value, /stringError$/); + assert.equal( + summary.events[0].exception.values[0].stacktrace.frames.length, + 1 + ); // always 1 because thrown strings can't provide > 1 frame + + // some browsers extract proper url, line, and column for thrown strings + // but not all - falls back to frame url + assert.match( + summary.events[0].exception.values[0].stacktrace.frames[0].filename, + /(\/subjects\/throw-string.js|\/base\/variants\/)/ + ); + assert.match( + summary.events[0].exception.values[0].stacktrace.frames[0]["function"], + /throwStringError|\?|global code/i + ); + }); + }); + + it("should catch thrown objects", function() { + return runInSandbox(sandbox, { manual: true }, function() { + // intentionally loading this error via a script file to make + // sure it is 1) not caught by instrumentation 2) doesn't trigger + // "Script error" + var script = document.createElement("script"); + script.src = "/base/subjects/throw-object.js"; + script.onload = function() { + window.finalizeManualTest(); + }; + document.head.appendChild(script); + }).then(function(summary) { + assert.equal(summary.events[0].exception.values[0].type, "Error"); + + // # is covering default Android 4.4 and 5.1 browser + assert.match( + summary.events[0].exception.values[0].value, + /^(\[object Object\]|#)$/ + ); + assert.equal( + summary.events[0].exception.values[0].stacktrace.frames.length, + 1 + ); // always 1 because thrown objects can't provide > 1 frame + + // some browsers extract proper url, line, and column for thrown objects + // but not all - falls back to frame url + assert.match( + summary.events[0].exception.values[0].stacktrace.frames[0].filename, + /(\/subjects\/throw-object.js|\/base\/variants\/)/ + ); + assert.match( + summary.events[0].exception.values[0].stacktrace.frames[0]["function"], + /throwStringError|\?|global code/i + ); + }); + }); + + it("should catch thrown errors", function() { + return runInSandbox(sandbox, { manual: true }, function() { + // intentionally loading this error via a script file to make + // sure it is 1) not caught by instrumentation 2) doesn't trigger + // "Script error" + var script = document.createElement("script"); + script.src = "/base/subjects/throw-error.js"; + script.onload = function() { + window.finalizeManualTest(); + }; + document.head.appendChild(script); + }).then(function(summary) { + // ¯\_(ツ)_/¯ + if (summary.window.isBelowIE11()) { + assert.equal(summary.events[0].exception.values[0].type, "Error"); + } else { + assert.match(summary.events[0].exception.values[0].type, /^Error/); + } + assert.match(summary.events[0].exception.values[0].value, /realError$/); + // 1 or 2 depending on platform + assert.isAtLeast( + summary.events[0].exception.values[0].stacktrace.frames.length, + 1 + ); + assert.isAtMost( + summary.events[0].exception.values[0].stacktrace.frames.length, + 2 + ); + assert.match( + summary.events[0].exception.values[0].stacktrace.frames[0].filename, + /\/subjects\/throw-error\.js/ + ); + assert.match( + summary.events[0].exception.values[0].stacktrace.frames[0]["function"], + /\?|global code|throwRealError/i + ); + }); + }); + + it("should NOT catch an exception already caught [but rethrown] via Sentry.captureException", function() { + return runInSandbox(sandbox, function() { + try { + foo(); + } catch (e) { + Sentry.captureException(e); + throw e; // intentionally re-throw + } + }).then(function(summary) { + // IE10 uses different type (Error instead of ReferenceError) for rethrown errors... + if (!summary.window.isBelowIE11()) { + assert.equal(summary.events.length, 1); + } + }); + }); +}); diff --git a/packages/browser/test/integration/suites/shell.js b/packages/browser/test/integration/suites/shell.js new file mode 100644 index 000000000000..206f068ea0b8 --- /dev/null +++ b/packages/browser/test/integration/suites/shell.js @@ -0,0 +1,40 @@ +var variants = ["frame", "loader", "loader-lazy-no"]; + +function runVariant(variant) { + var IS_LOADER = !!variant.match(/^loader/); + var IS_ASYNC_LOADER = !!variant.match(/^loader$/); + var IS_SYNC_LOADER = !!variant.match(/^loader-lazy-no$/); + + describe(variant, function() { + this.timeout(5000); + this.retries(3); + + var sandbox; + + beforeEach(function(done) { + sandbox = createSandbox(done, variant); + }); + + afterEach(function() { + document.body.removeChild(sandbox); + }); + + /** + * This part will be replaced by the test runner + */ + {{ suites/config.js }} // prettier-ignore + {{ suites/api.js }} // prettier-ignore + {{ suites/onerror.js }} // prettier-ignore + {{ suites/builtins.js }} // prettier-ignore + {{ suites/breadcrumbs.js }} // prettier-ignore + {{ suites/loader.js }} // prettier-ignore + }); +} + +for (var idx in variants) { + (function() { + runVariant(variants[idx]); + })(); +} + +{{ suites/loader-specific.js }} // prettier-ignore diff --git a/packages/browser/test/integration/test.js b/packages/browser/test/integration/test.js deleted file mode 100644 index 9de74c31beb5..000000000000 --- a/packages/browser/test/integration/test.js +++ /dev/null @@ -1,2004 +0,0 @@ -/*global assert*/ -function iframeExecute(iframe, done, execute, assertCallback) { - iframe.contentWindow.done = function(sentryData) { - try { - assertCallback(sentryData); - } catch (e) { - done(e); - } - }; - // use setTimeout so stack trace doesn't go all the way back to mocha test runner - iframe.contentWindow.eval('window.originalBuiltIns.setTimeout.call(window, ' + execute.toString() + ');'); -} - -function createIframe(done, file) { - var iframe = document.createElement('iframe'); - iframe.style.display = 'none'; - iframe.src = './base/test/integration/' + file + '.html'; - iframe.onload = function() { - done(); - }; - document.body.appendChild(iframe); - return iframe; -} - -var anchor = document.createElement('a'); -function parseUrl(url) { - var out = { pathname: '', origin: '', protocol: '' }; - if (!url) anchor.href = url; - for (var key in out) { - out[key] = anchor[key]; - } - return out; -} - -function isBelowIE11() { - return /*@cc_on!@*/ false == !false; -} - -// Thanks for nothing IE! -// (╯°□°)╯︵ ┻━┻ -function canReadFunctionName() { - function foo() {} - if (foo.name === 'foo') return true; - return false; -} - -var assertTimeout = undefined; -function debounceAssertEventCount(sentryData, count, done) { - if (sentryData === undefined) { - return false; - } - clearTimeout(assertTimeout); - assertTimeout = setTimeout(function() { - done(new Error('Did not receive ' + count + ' events')); - }, 1000); - if (sentryData.length != count) { - return false; - } - clearTimeout(assertTimeout); - return true; -} - -function _alt(title, condition) { - return condition ? '⚠️ Skipped: ' + title : title; -} - -var frames = ['frame', 'loader', 'loader-lazy-no']; - -for (var idx in frames) { - (function() { - var filename = frames[idx]; - var IS_LOADER = !!filename.match(/^loader/); - var IS_ASYNC_LOADER = !!filename.match(/^loader$/); - var IS_SYNC_LOADER = !!filename.match(/^loader-lazy-no$/); - - describe(filename + '.html', function() { - this.timeout(30000); - - beforeEach(function(done) { - this.iframe = createIframe(done, filename); - }); - - afterEach(function() { - document.body.removeChild(this.iframe); - }); - - describe('config', function() { - it('should allow to ignore specific errors', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - Sentry.captureException(new Error('foo')); - Sentry.captureException(new Error('ignoreErrorTest')); - Sentry.captureException(new Error('bar')); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 2, done)) { - assert.equal(sentryData[0].exception.values[0].type, 'Error'); - assert.equal(sentryData[0].exception.values[0].value, 'foo'); - assert.equal(sentryData[1].exception.values[0].type, 'Error'); - assert.equal(sentryData[1].exception.values[0].value, 'bar'); - done(); - } - } - ); - }); - - it('should allow to ignore specific urls', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - /** - * We always filter on the caller, not the cause of the error - * - * > foo.js file called a function in bar.js - * > bar.js file called a function in baz.js - * > baz.js threw an error - * - * foo.js is blacklisted in the `init` call (init.js), thus we filter it - * */ - var urlWithBlacklistedUrl = new Error('filter'); - urlWithBlacklistedUrl.stack = - 'Error: bar\n' + - ' at http://localhost:5000/foo.js:7:19\n' + - ' at bar(http://localhost:5000/bar.js:2:3)\n' + - ' at baz(http://localhost:5000/baz.js:2:9)\n'; - - /** - * > foo-pass.js file called a function in bar-pass.js - * > bar-pass.js file called a function in baz-pass.js - * > baz-pass.js threw an error - * - * foo-pass.js is *not* blacklisted in the `init` call (init.js), thus we don't filter it - * */ - var urlWithoutBlacklistedUrl = new Error('pass'); - urlWithoutBlacklistedUrl.stack = - 'Error: bar\n' + - ' at http://localhost:5000/foo-pass.js:7:19\n' + - ' at bar(http://localhost:5000/bar-pass.js:2:3)\n' + - ' at baz(http://localhost:5000/baz-pass.js:2:9)\n'; - - Sentry.captureException(urlWithBlacklistedUrl); - Sentry.captureException(urlWithoutBlacklistedUrl); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - assert.equal(sentryData[0].exception.values[0].type, 'Error'); - assert.equal(sentryData[0].exception.values[0].value, 'pass'); - done(); - } - } - ); - }); - }); - - describe('API', function() { - it('should capture Sentry.captureMessage', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - Sentry.captureMessage('Hello'); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - assert.equal(sentryData.message, 'Hello'); - done(); - } - } - ); - }); - - it('should capture Sentry.captureException', function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - try { - foo(); - } catch (e) { - Sentry.captureException(e); - } - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - assert.isAtLeast(sentryData.exception.values[0].stacktrace.frames.length, 2); - assert.isAtMost(sentryData.exception.values[0].stacktrace.frames.length, 4); - done(); - } - } - ); - }); - - it('should generate a synthetic trace for captureException w/ non-errors', function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - throwNonError(); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - assert.isAtLeast(sentryData.stacktrace.frames.length, 1); - assert.isAtMost(sentryData.stacktrace.frames.length, 3); - done(); - } - } - ); - }); - - it('should have correct stacktrace order', function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - try { - foo(); - } catch (e) { - Sentry.captureException(e); - } - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - assert.equal( - sentryData.exception.values[0].stacktrace.frames[ - sentryData.exception.values[0].stacktrace.frames.length - 1 - ].function, - 'bar' - ); - assert.isAtLeast(sentryData.exception.values[0].stacktrace.frames.length, 2); - assert.isAtMost(sentryData.exception.values[0].stacktrace.frames.length, 4); - done(); - } - } - ); - }); - - it('should have exception with type and value', function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - Sentry.captureException('this is my test exception'); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - assert.isNotEmpty(sentryData.exception.values[0].value); - assert.isNotEmpty(sentryData.exception.values[0].type); - done(); - } - } - ); - }); - - it('should reject duplicate, back-to-back errors from captureException', function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - // Different exceptions, don't dedupe - for (var i = 0; i < 2; i++) { - throwRandomError(); - } - - // Same exceptions and same stacktrace, dedupe - for (var i = 0; i < 2; i++) { - throwError(); - } - - // Same exceptions, different stacktrace (different line number), don't dedupe - throwSameConsecutiveErrors('bar'); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 5, done)) { - assert.match(sentryData[0].exception.values[0].value, /Exception no \d+/); - assert.match(sentryData[1].exception.values[0].value, /Exception no \d+/); - assert.equal(sentryData[2].exception.values[0].value, 'foo'); - assert.equal(sentryData[3].exception.values[0].value, 'bar'); - assert.equal(sentryData[4].exception.values[0].value, 'bar'); - done(); - } - } - ); - }); - - it('should not reject back-to-back errors with different stack traces', function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - // same error message, but different stacks means that these are considered - // different errors - - // stack: - // bar - try { - bar(); // declared in frame.html - } catch (e) { - Sentry.captureException(e); - } - - // stack (different # frames): - // bar - // foo - try { - foo(); // declared in frame.html - } catch (e) { - Sentry.captureException(e); - } - - // stack (same # frames, different frames): - // bar - // foo2 - try { - foo2(); // declared in frame.html - } catch (e) { - Sentry.captureException(e); - } - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 3, done)) { - // NOTE: regex because exact error message differs per-browser - assert.match(sentryData[0].exception.values[0].value, /baz/); - assert.equal(sentryData[0].exception.values[0].type, 'ReferenceError'); - assert.match(sentryData[1].exception.values[0].value, /baz/); - assert.equal(sentryData[1].exception.values[0].type, 'ReferenceError'); - assert.match(sentryData[2].exception.values[0].value, /baz/); - assert.equal(sentryData[2].exception.values[0].type, 'ReferenceError'); - done(); - } - } - ); - }); - - it('should reject duplicate, back-to-back messages from captureMessage', function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - // Different messages, don't dedupe - for (var i = 0; i < 2; i++) { - captureRandomMessage(); - } - - // Same messages and same stacktrace, dedupe - for (var i = 0; i < 2; i++) { - captureMessage('same message, same stacktrace'); - } - - // Same messages, different stacktrace (different line number), don't dedupe - captureSameConsecutiveMessages('same message, different stacktrace'); - }, - function(sentryData) { - var eventCount = 5; - if (IS_LOADER) { - // On the async loader since we replay all messages from the same location - // we actually only receive 4 events - eventCount = 4; - } - if (debounceAssertEventCount(sentryData, eventCount, done)) { - assert.match(sentryData[0].message, /Message no \d+/); - assert.match(sentryData[1].message, /Message no \d+/); - assert.equal(sentryData[2].message, 'same message, same stacktrace'); - assert.equal(sentryData[3].message, 'same message, different stacktrace'); - !IS_LOADER && assert.equal(sentryData[4].message, 'same message, different stacktrace'); - done(); - } - } - ); - }); - }); - - describe('window.onerror', function() { - it('should catch syntax errors', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - eval('foo{};'); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - // ¯\_(ツ)_/¯ - if (isBelowIE11()) { - assert.equal(sentryData.exception.values[0].type, 'Error'); - } else { - assert.match(sentryData.exception.values[0].type, /SyntaxError/); - } - assert.equal(sentryData.exception.values[0].stacktrace.frames.length, 1); // just one frame - done(); - } - } - ); - }); - - it('should catch thrown strings', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - // intentionally loading this error via a script file to make - // sure it is 1) not caught by instrumentation 2) doesn't trigger - // "Script error" - var script = document.createElement('script'); - script.src = 'throw-string.js'; - script.onload = function() { - done(); - }; - document.head.appendChild(script); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - assert.match(sentryData.exception.values[0].value, /stringError$/); - assert.equal(sentryData.exception.values[0].stacktrace.frames.length, 1); // always 1 because thrown strings can't provide > 1 frame - - // some browsers extract proper url, line, and column for thrown strings - // but not all - falls back to frame url - assert.match(sentryData.exception.values[0].stacktrace.frames[0].filename, /\/test\/integration\//); - assert.match( - sentryData.exception.values[0].stacktrace.frames[0]['function'], - /throwStringError|\?|global code/i - ); - done(); - } - } - ); - }); - - it('should catch thrown objects', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - // intentionally loading this error via a script file to make - // sure it is 1) not caught by instrumentation 2) doesn't trigger - // "Script error" - var script = document.createElement('script'); - script.src = 'throw-object.js'; - script.onload = function() { - done(); - }; - document.head.appendChild(script); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - assert.equal(sentryData.exception.values[0].type, 'Error'); - - // # is covering default Android 4.4 and 5.1 browser - assert.match(sentryData.exception.values[0].value, /^(\[object Object\]|#)$/); - assert.equal(sentryData.exception.values[0].stacktrace.frames.length, 1); // always 1 because thrown objects can't provide > 1 frame - - // some browsers extract proper url, line, and column for thrown objects - // but not all - falls back to frame url - assert.match(sentryData.exception.values[0].stacktrace.frames[0].filename, /\/test\/integration\//); - assert.match( - sentryData.exception.values[0].stacktrace.frames[0]['function'], - /throwStringError|\?|global code/i - ); - done(); - } - } - ); - }); - - it('should catch thrown errors', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - // intentionally loading this error via a script file to make - // sure it is 1) not caught by instrumentation 2) doesn't trigger - // "Script error" - var script = document.createElement('script'); - script.src = 'throw-error.js'; - script.onload = function() { - done(); - }; - document.head.appendChild(script); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = iframe.contentWindow.sentryData[0]; - // ¯\_(ツ)_/¯ - if (isBelowIE11()) { - assert.equal(sentryData.exception.values[0].type, 'Error'); - } else { - assert.match(sentryData.exception.values[0].type, /^Error/); - } - assert.match(sentryData.exception.values[0].value, /realError$/); - // 1 or 2 depending on platform - assert.isAtLeast(sentryData.exception.values[0].stacktrace.frames.length, 1); - assert.isAtMost(sentryData.exception.values[0].stacktrace.frames.length, 2); - assert.match( - sentryData.exception.values[0].stacktrace.frames[0].filename, - /\/test\/integration\/throw-error\.js/ - ); - assert.match( - sentryData.exception.values[0].stacktrace.frames[0]['function'], - /\?|global code|throwRealError/i - ); - done(); - } - } - ); - }); - - it('should NOT catch an exception already caught [but rethrown] via Sentry.captureException', function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - setTimeout(done, 1000); - try { - foo(); - } catch (e) { - Sentry.captureException(e); - throw e; // intentionally re-throw - } - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - assert.equal(sentryData.length, 1); - done(); - } - } - ); - }); - }); - - describe('wrapped built-ins', function() { - it('should capture exceptions from event listeners', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - var div = document.createElement('div'); - document.body.appendChild(div); - div.addEventListener( - 'click', - function() { - window.element = div; - window.context = this; - foo(); - }, - false - ); - - var click = new MouseEvent('click'); - div.dispatchEvent(click); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - // Make sure we preserve the correct context - assert.equal(iframe.contentWindow.element, iframe.contentWindow.context); - delete iframe.contentWindow.element; - delete iframe.contentWindow.context; - assert.match(sentryData[0].exception.values[0].value, /baz/); - done(); - } - } - ); - }); - - it('should transparently remove event listeners from wrapped functions', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(done, 1000); - - var div = document.createElement('div'); - document.body.appendChild(div); - var fooFn = function() { - foo(); - }; - div.addEventListener('click', fooFn, false); - div.removeEventListener('click', fooFn); - - var click = new MouseEvent('click'); - div.dispatchEvent(click); - }, - function() { - var sentryData = iframe.contentWindow.sentryData[0]; - assert.equal(sentryData, null); // should never trigger error - done(); - } - ); - }); - - it('should capture unhandledrejection with error', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - if (isChrome()) { - Promise.reject(new Error('test2')); - } else { - done(); - } - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - assert.equal(sentryData[0].exception.values[0].value, 'test2'); - assert.equal(sentryData[0].exception.values[0].type, 'Error'); - assert.isAtLeast(sentryData[0].exception.values[0].stacktrace.frames.length, 1); - assert.equal(sentryData[0].exception.values[0].mechanism.handled, false); - assert.equal(sentryData[0].exception.values[0].mechanism.type, 'onunhandledrejection'); - done(); - } else { - // This test will be skipped if it's not Chrome Desktop - done(); - } - } - ); - }); - - it('should capture unhandledrejection with a string', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - if (isChrome()) { - Promise.reject('test'); - } else { - done(); - } - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - // non-error rejections doesnt provide stacktraces so we can skip the assertion - assert.equal(sentryData[0].exception.values[0].value, '"test"'); - assert.equal(sentryData[0].exception.values[0].type, 'UnhandledRejection'); - assert.equal(sentryData[0].exception.values[0].mechanism.handled, false); - assert.equal(sentryData[0].exception.values[0].mechanism.type, 'onunhandledrejection'); - done(); - } else { - // This test will be skipped if it's not Chrome Desktop - done(); - } - } - ); - }); - - it('should capture unhandledrejection with a monster string', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - if (isChrome()) { - Promise.reject('test'.repeat(100)); - } else { - done(); - } - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - // non-error rejections doesnt provide stacktraces so we can skip the assertion - assert.equal(sentryData[0].exception.values[0].value.length, 253); - assert.equal(sentryData[0].exception.values[0].type, 'UnhandledRejection'); - assert.equal(sentryData[0].exception.values[0].mechanism.handled, false); - assert.equal(sentryData[0].exception.values[0].mechanism.type, 'onunhandledrejection'); - done(); - } else { - // This test will be skipped if it's not Chrome Desktop - done(); - } - } - ); - }); - - it('should capture unhandledrejection with an object', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - if (isChrome()) { - Promise.reject({ a: 'b' }); - } else { - done(); - } - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - // non-error rejections doesnt provide stacktraces so we can skip the assertion - assert.equal(sentryData[0].exception.values[0].value, '{"a":"b"}'); - assert.equal(sentryData[0].exception.values[0].type, 'UnhandledRejection'); - assert.equal(sentryData[0].exception.values[0].mechanism.handled, false); - assert.equal(sentryData[0].exception.values[0].mechanism.type, 'onunhandledrejection'); - done(); - } else { - // This test will be skipped if it's not Chrome Desktop - done(); - } - } - ); - }); - - it('should capture unhandledrejection with an monster object', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - if (isChrome()) { - var a = { - a: '1'.repeat('100'), - b: '2'.repeat('100'), - c: '3'.repeat('100'), - }; - a.d = a.a; - a.e = a; - Promise.reject(a); - } else { - done(); - } - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - // non-error rejections doesnt provide stacktraces so we can skip the assertion - assert.equal(sentryData[0].exception.values[0].value.length, 253); - assert.equal(sentryData[0].exception.values[0].type, 'UnhandledRejection'); - assert.equal(sentryData[0].exception.values[0].mechanism.handled, false); - assert.equal(sentryData[0].exception.values[0].mechanism.type, 'onunhandledrejection'); - done(); - } else { - // This test will be skipped if it's not Chrome Desktop - done(); - } - } - ); - }); - - it('should capture exceptions inside setTimeout', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - foo(); - }); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - assert.match(sentryData[0].exception.values[0].value, /baz/); - done(); - } - } - ); - }); - - it('should capture exceptions inside setInterval', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - var exceptionInterval = setInterval(function() { - clearInterval(exceptionInterval); - foo(); - }, 10); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - assert.match(sentryData[0].exception.values[0].value, /baz/); - done(); - } - } - ); - }); - - it('should capture exceptions inside requestAnimationFrame', function(done) { - var iframe = this.iframe; - // needs to be visible or requestAnimationFrame won't ever fire - iframe.style.display = 'block'; - - iframeExecute( - iframe, - done, - function() { - requestAnimationFrame(function() { - foo(); - }); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - assert.match(sentryData[0].exception.values[0].value, /baz/); - done(); - } - } - ); - }); - - it('should capture exceptions from XMLHttpRequest event handlers (e.g. onreadystatechange)', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - var xhr = new XMLHttpRequest(); - - // intentionally assign event handlers *after* XMLHttpRequest.prototype.open, - // since this is what jQuery does - // https://github.com/jquery/jquery/blob/master/src/ajax/xhr.js#L37 - - xhr.open('GET', 'example.json'); - xhr.onreadystatechange = function() { - setTimeout(done, 1000); - // replace onreadystatechange with no-op so exception doesn't - // fire more than once as XHR changes loading state - xhr.onreadystatechange = function() {}; - foo(); - }; - xhr.send(); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - assert.match(sentryData[0].exception.values[0].value, /baz/); - done(); - } - } - ); - }); - - it(_alt("should capture built-in's mechanism type as instrument", IS_LOADER), function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - foo(); - }); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - - if (IS_LOADER) { - // The async loader doesn't wrap setTimeout - // so we don't receive the full mechanism - assert.ok(sentryData.exception.values[0].mechanism); - return done(); - } - - var fn = sentryData.exception.values[0].mechanism.data.function; - delete sentryData.exception.values[0].mechanism.data; - - if (canReadFunctionName()) { - assert.equal(fn, 'setTimeout'); - } else { - assert.equal(fn, ''); - } - - assert.deepEqual(sentryData.exception.values[0].mechanism, { - type: 'instrument', - handled: true, - }); - done(); - } - } - ); - }); - - it("should capture built-in's handlers fn name in mechanism data", function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - var div = document.createElement('div'); - document.body.appendChild(div); - div.addEventListener( - 'click', - function namedFunction() { - foo(); - }, - false - ); - - var click = new MouseEvent('click'); - div.dispatchEvent(click); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - - if (IS_LOADER) { - // The async loader doesn't wrap addEventListener - // so we don't receive the full mechanism - assert.ok(sentryData.exception.values[0].mechanism); - return done(); - } - - var handler = sentryData.exception.values[0].mechanism.data.handler; - delete sentryData.exception.values[0].mechanism.data.handler; - var target = sentryData.exception.values[0].mechanism.data.target; - delete sentryData.exception.values[0].mechanism.data.target; - - if (canReadFunctionName()) { - assert.equal(handler, 'namedFunction'); - } else { - assert.equal(handler, ''); - } - - // IE vs. Rest of the world - assert.oneOf(target, ['Node', 'EventTarget']); - assert.deepEqual(sentryData.exception.values[0].mechanism, { - type: 'instrument', - handled: true, - data: { - function: 'addEventListener', - }, - }); - done(); - } - } - ); - }); - - it('should fallback to fn name in mechanism data if one is unavailable', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - var div = document.createElement('div'); - document.body.appendChild(div); - div.addEventListener( - 'click', - function() { - foo(); - }, - false - ); - - var click = new MouseEvent('click'); - div.dispatchEvent(click); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = sentryData[0]; - - if (IS_LOADER) { - // The async loader doesn't wrap - assert.ok(sentryData.exception.values[0].mechanism); - return done(); - } - - var target = sentryData.exception.values[0].mechanism.data.target; - delete sentryData.exception.values[0].mechanism.data.target; - - // IE vs. Rest of the world - assert.oneOf(target, ['Node', 'EventTarget']); - assert.deepEqual(sentryData.exception.values[0].mechanism, { - type: 'instrument', - handled: true, - data: { - function: 'addEventListener', - handler: '', - }, - }); - done(); - } - } - ); - }); - }); - - describe('breadcrumbs', function() { - it(_alt('should record an XMLHttpRequest with a handler', IS_LOADER), function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - var xhr = new XMLHttpRequest(); - xhr.open('GET', 'example.json'); - xhr.setRequestHeader('Content-type', 'application/json'); - xhr.onreadystatechange = function() { - if (xhr.readyState === 4) { - done(); - } - }; - xhr.send(); - }, - function() { - if (IS_LOADER) { - // The async loader doesn't wrap XHR - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].type, 'http'); - assert.equal(breadcrumbs[0].category, 'xhr'); - assert.equal(breadcrumbs[0].data.method, 'GET'); - - done(); - } - ); - }); - - it(_alt('should record an XMLHttpRequest without any handlers set', IS_LOADER), function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - // I hate to do a time-based "done" trigger, but unfortunately we can't - // set an onload/onreadystatechange handler on XHR to verify that it finished - // - that's the whole point of this test! :( - setTimeout(done, 1000); - var xhr = new XMLHttpRequest(); - xhr.open('GET', 'example.json'); - xhr.setRequestHeader('Content-type', 'application/json'); - xhr.send(); - }, - function() { - if (IS_LOADER) { - // Since we do a xhr as soon as the page loads - // the async loader is not able to pick this - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].type, 'http'); - assert.equal(breadcrumbs[0].category, 'xhr'); - assert.equal(breadcrumbs[0].data.method, 'GET'); - - done(); - } - ); - }); - - it( - _alt('should transform XMLHttpRequests to the Sentry store endpoint as sentry type breadcrumb', IS_LOADER), - function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - setTimeout(done, 1000); - var xhr = new XMLHttpRequest(); - xhr.open('GET', 'https://example.com/api/1/store/'); - xhr.send('{"message":"someMessage","level":"warning"}'); - }, - function() { - if (IS_LOADER) { - // Since we do a xhr as soon as the page loads - // the async loader is not able to pick this - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].category, 'sentry'); - assert.equal(breadcrumbs[0].level, 'warning'); - assert.equal(breadcrumbs[0].message, 'someMessage'); - done(); - } - ); - } - ); - - it('should record a fetch request', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - fetch('example.json').then( - function() { - Sentry.captureMessage('test'); - }, - function() { - Sentry.captureMessage('test'); - } - ); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap fetch, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - var breadcrumbUrl = 'example.json'; - - if ('fetch' in window) { - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].type, 'http'); - assert.equal(breadcrumbs[0].category, 'fetch'); - assert.equal(breadcrumbs[0].data.method, 'GET'); - assert.equal(breadcrumbs[0].data.url, breadcrumbUrl); - } else { - // otherwise we use a fetch polyfill based on xhr - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].type, 'http'); - assert.equal(breadcrumbs[0].category, 'xhr'); - assert.equal(breadcrumbs[0].data.method, 'GET'); - assert.equal(breadcrumbs[0].data.url, breadcrumbUrl); - } - done(); - } - ); - }); - - it('should record a fetch request with Request obj instead of URL string', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - fetch(new Request('example.json')).then( - function() { - Sentry.captureMessage('test'); - }, - function() { - Sentry.captureMessage('test'); - } - ); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap fetch, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - var breadcrumbUrl = 'example.json'; - - if ('fetch' in window) { - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].type, 'http'); - assert.equal(breadcrumbs[0].category, 'fetch'); - assert.equal(breadcrumbs[0].data.method, 'GET'); - // Request constructor normalizes the url - assert.ok(breadcrumbs[0].data.url.indexOf(breadcrumbUrl) !== -1); - } else { - // otherwise we use a fetch polyfill based on xhr - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].type, 'http'); - assert.equal(breadcrumbs[0].category, 'xhr'); - assert.equal(breadcrumbs[0].data.method, 'GET'); - assert.ok(breadcrumbs[0].data.url.indexOf(breadcrumbUrl) !== -1); - } - done(); - } - ); - }); - - it('should record a fetch request with an arbitrary type argument', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - fetch(123).then( - function() { - Sentry.captureMessage('test'); - }, - function() { - Sentry.captureMessage('test'); - } - ); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap fetch, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - var breadcrumbUrl = '123'; - - if ('fetch' in window) { - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].type, 'http'); - assert.equal(breadcrumbs[0].category, 'fetch'); - assert.equal(breadcrumbs[0].data.method, 'GET'); - // Request constructor normalizes the url - assert.ok(breadcrumbs[0].data.url.indexOf(breadcrumbUrl) !== -1); - } else { - // otherwise we use a fetch polyfill based on xhr - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].type, 'http'); - assert.equal(breadcrumbs[0].category, 'xhr'); - assert.equal(breadcrumbs[0].data.method, 'GET'); - assert.ok(breadcrumbs[0].data.url.indexOf(breadcrumbUrl) !== -1); - } - done(); - } - ); - }); - - it('should not fail with click or keypress handler with no callback', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - Sentry.captureMessage('test'); - }, 1000); - - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('click', undefined); - input.addEventListener('keypress', undefined); - - var click = new MouseEvent('click'); - input.dispatchEvent(click); - - var keypress = new KeyboardEvent('keypress'); - input.dispatchEvent(keypress); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 2); - - assert.equal(breadcrumbs[0].category, 'ui.click'); - assert.equal(breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(breadcrumbs[1].category, 'ui.input'); - assert.equal(breadcrumbs[1].message, 'body > form#foo-form > input[name="foo"]'); - - // There should be no expection, if there is one it means we threw it - assert.isUndefined(sentryData[0].exception); - - done(); - } - ); - }); - - it('should not fail with custom event', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - Sentry.captureMessage('test'); - }, 1000); - - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('build', function(evt) { - evt.stopPropagation(); - }); - - var customEvent = new CustomEvent('build', { detail: 1 }); - input.dispatchEvent(customEvent); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - // There should be no expection, if there is one it means we threw it - assert.isUndefined(sentryData[0].exception); - assert.equal(breadcrumbs.length, 0); - - done(); - } - ); - }); - - it('should not fail with custom event and handler with no callback', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - Sentry.captureMessage('test'); - }, 1000); - - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('build', undefined); - - var customEvent = new CustomEvent('build', { detail: 1 }); - input.dispatchEvent(customEvent); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - // There should be no expection, if there is one it means we threw it - assert.isUndefined(sentryData[0].exception); - assert.equal(breadcrumbs.length, 0); - - done(); - } - ); - }); - - it('should record a mouse click on element WITH click handler present', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - Sentry.captureMessage('test'); - }, 1000); - - // add an event listener to the input. we want to make sure that - // our breadcrumbs still work even if the page has an event listener - // on an element that cancels event bubbling - var input = document.getElementsByTagName('input')[0]; - var clickHandler = function(evt) { - evt.stopPropagation(); // don't bubble - }; - input.addEventListener('click', clickHandler); - - // click - var click = new MouseEvent('click'); - input.dispatchEvent(click); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - - assert.equal(breadcrumbs[0].category, 'ui.click'); - assert.equal(breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - done(); - } - ); - }); - - it('should record a mouse click on element WITHOUT click handler present', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - Sentry.captureMessage('test'); - }, 1000); - - // click - var click = new MouseEvent('click'); - var input = document.getElementsByTagName('input')[0]; - input.dispatchEvent(click); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - - assert.equal(breadcrumbs[0].category, 'ui.click'); - assert.equal(breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - done(); - } - ); - }); - - it('should only record a SINGLE mouse click for a tree of elements with event listeners', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - Sentry.captureMessage('test'); - }, 1000); - - var clickHandler = function() {}; - - // mousemove event shouldnt clobber subsequent "breadcrumbed" events (see #724) - document.querySelector('.a').addEventListener('mousemove', clickHandler); - - document.querySelector('.a').addEventListener('click', clickHandler); - document.querySelector('.b').addEventListener('click', clickHandler); - document.querySelector('.c').addEventListener('click', clickHandler); - - // click - var click = new MouseEvent('click'); - var input = document.querySelector('.a'); // leaf node - input.dispatchEvent(click); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - - assert.equal(breadcrumbs[0].category, 'ui.click'); - assert.equal(breadcrumbs[0].message, 'body > div.c > div.b > div.a'); - done(); - } - ); - }); - - it('should bail out if accessing the `type` and `target` properties of an event throw an exception', function(done) { - // see: https://github.com/getsentry/sentry-javascript/issues/768 - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - Sentry.captureMessage('test'); - }, 1000); - - // click - var click = new MouseEvent('click'); - function kaboom() { - throw new Error('lol'); - } - Object.defineProperty(click, 'type', { get: kaboom }); - Object.defineProperty(click, 'target', { get: kaboom }); - - var input = document.querySelector('.a'); // leaf node - input.dispatchEvent(click); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].category, 'ui.click'); - assert.equal(breadcrumbs[0].message, ''); - done(); - } - ); - }); - - it('should record consecutive keypress events into a single "input" breadcrumb', function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - Sentry.captureMessage('test'); - }, 1000); - - // keypress twice - var keypress1 = new KeyboardEvent('keypress'); - var keypress2 = new KeyboardEvent('keypress'); - - var input = document.getElementsByTagName('input')[0]; - input.dispatchEvent(keypress1); - input.dispatchEvent(keypress2); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - - assert.equal(breadcrumbs[0].category, 'ui.input'); - assert.equal(breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - done(); - } - ); - }); - - it(_alt('should flush keypress breadcrumbs when an error is thrown', IS_LOADER), function(done) { - var iframe = this.iframe; - iframeExecute( - iframe, - done, - function() { - setTimeout(done, 1000); - // some browsers trigger onpopstate for load / reset breadcrumb state - - // keypress - var keypress = new KeyboardEvent('keypress'); - - var input = document.getElementsByTagName('input')[0]; - input.dispatchEvent(keypress); - - foo(); // throw exception - }, - function(sentryData) { - if (IS_LOADER) { - return done(); - } - // TODO: don't really understand what's going on here - // Why do we not catch an error here - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].category, 'ui.input'); - assert.equal(breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - done(); - } - ); - }); - - it('should flush keypress breadcrumb when input event occurs immediately after', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - Sentry.captureMessage('test'); - }, 1000); - - // 1st keypress - var keypress1 = new KeyboardEvent('keypress'); - // click - var click = new MouseEvent('click'); - // 2nd keypress - var keypress2 = new KeyboardEvent('keypress'); - - var input = document.getElementsByTagName('input')[0]; - input.dispatchEvent(keypress1); - input.dispatchEvent(click); - input.dispatchEvent(keypress2); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 3); - - assert.equal(breadcrumbs[0].category, 'ui.input'); - assert.equal(breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(breadcrumbs[1].category, 'ui.click'); - assert.equal(breadcrumbs[1].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(breadcrumbs[2].category, 'ui.input'); - assert.equal(breadcrumbs[2].message, 'body > form#foo-form > input[name="foo"]'); - done(); - } - ); - }); - - it('should record consecutive keypress events in a contenteditable into a single "input" breadcrumb', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(function() { - setTimeout(done, 1000); - Sentry.captureMessage('test'); - }, 1000); - - // keypress twice - var keypress1 = new KeyboardEvent('keypress'); - var keypress2 = new KeyboardEvent('keypress'); - - var div = document.querySelector('[contenteditable]'); - div.dispatchEvent(keypress1); - div.dispatchEvent(keypress2); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - - assert.equal(breadcrumbs[0].category, 'ui.input'); - assert.equal(breadcrumbs[0].message, 'body > form#foo-form > div.contenteditable'); - done(); - } - ); - }); - - it('should record click events that were handled using an object with handleEvent property and call original callback', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - var frame = this; - frame.handleEventCalled = false; - - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('click', { - handleEvent() { - frame.handleEventCalled = true; - }, - }); - input.dispatchEvent(new MouseEvent('click')); - - Sentry.captureMessage('test'); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].category, 'ui.click'); - assert.equal(breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(iframe.contentWindow.handleEventCalled, true); - - done(); - } - ); - }); - - it('should record keypress events that were handled using an object with handleEvent property and call original callback', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - var frame = this; - frame.handleEventCalled = false; - - var input = document.getElementsByTagName('input')[0]; - input.addEventListener('keypress', { - handleEvent() { - frame.handleEventCalled = true; - }, - }); - input.dispatchEvent(new KeyboardEvent('keypress')); - - Sentry.captureMessage('test'); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap event listeners, but we should receive the event without breadcrumbs - assert.lengthOf(sentryData, 1); - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 1); - assert.equal(breadcrumbs[0].category, 'ui.input'); - assert.equal(breadcrumbs[0].message, 'body > form#foo-form > input[name="foo"]'); - - assert.equal(iframe.contentWindow.handleEventCalled, true); - - done(); - } - ); - }); - - it( - _alt('should record history.[pushState|replaceState] changes as navigation breadcrumbs', IS_LOADER), - function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - setTimeout(done, 1000); - - history.pushState({}, '', '/foo'); - history.pushState({}, '', '/bar?a=1#fragment'); - history.pushState({}, '', {}); // pushState calls toString on non-string args - history.pushState({}, '', null); // does nothing / no-op - // can't call history.back() because it will change url of parent document - // (e.g. document running mocha) ... instead just "emulate" a back button - // press by calling replaceState - history.replaceState({}, '', '/bar?a=1#fragment'); - }, - function(sentryData) { - if (IS_LOADER) { - // The async loader doesn't wrap history - return done(); - } - var breadcrumbs = iframe.contentWindow.sentryBreadcrumbs; - - assert.equal(breadcrumbs.length, 4); - assert.equal(breadcrumbs[0].category, 'navigation'); // (start) => foo - assert.equal(breadcrumbs[1].category, 'navigation'); // foo => bar?a=1#fragment - assert.equal(breadcrumbs[2].category, 'navigation'); // bar?a=1#fragment => [object%20Object] - assert.equal(breadcrumbs[3].category, 'navigation'); // [object%20Object] => bar?a=1#fragment (back button) - - assert.ok(/\/test\/integration\/.*\.html$/.test(breadcrumbs[0].data.from), "'from' url is incorrect"); - assert.ok(/\/foo$/.test(breadcrumbs[0].data.to), "'to' url is incorrect"); - - assert.ok(/\/foo$/.test(breadcrumbs[1].data.from), "'from' url is incorrect"); - assert.ok(/\/bar\?a=1#fragment$/.test(breadcrumbs[1].data.to), "'to' url is incorrect"); - - assert.ok(/\/bar\?a=1#fragment$/.test(breadcrumbs[2].data.from), "'from' url is incorrect"); - assert.ok(/\[object Object\]$/.test(breadcrumbs[2].data.to), "'to' url is incorrect"); - - assert.ok(/\[object Object\]$/.test(breadcrumbs[3].data.from), "'from' url is incorrect"); - assert.ok(/\/bar\?a=1#fragment/.test(breadcrumbs[3].data.to), "'to' url is incorrect"); - - done(); - } - ); - } - ); - - it(_alt('should preserve native code detection compatibility', IS_LOADER), function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - done(); - }, - function() { - if (IS_LOADER) { - // The async loader doesn't wrap anything - return done(); - } - assert.include(Function.prototype.toString.call(window.setTimeout), '[native code]'); - assert.include(Function.prototype.toString.call(window.setInterval), '[native code]'); - assert.include(Function.prototype.toString.call(window.addEventListener), '[native code]'); - assert.include(Function.prototype.toString.call(window.removeEventListener), '[native code]'); - assert.include(Function.prototype.toString.call(window.requestAnimationFrame), '[native code]'); - if ('fetch' in window) { - assert.include(Function.prototype.toString.call(window.fetch), '[native code]'); - } - done(); - } - ); - }); - - it('should add breadcrumbs on thrown errors', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - window.allowConsoleBreadcrumbs = true; - var logs = document.createElement('script'); - logs.src = 'console-logs.js'; - logs.onload = function() { - done(); - }; - document.head.appendChild(logs); - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - if (IS_LOADER) { - // The async loader doesn't capture breadcrumbs, but we should receive the event without them - assert.lengthOf(sentryData, 1); - return done(); - } - - var sentryData = iframe.contentWindow.sentryData[0]; - assert.ok(sentryData.breadcrumbs); - assert.lengthOf(sentryData.breadcrumbs, 3); - assert.deepEqual(sentryData.breadcrumbs[0].data.extra.arguments, ['One']); - assert.deepEqual(sentryData.breadcrumbs[1].data.extra.arguments, ['Two', { a: 1 }]); - assert.deepEqual(sentryData.breadcrumbs[2].data.extra.arguments, ['Error 2', { b: { c: '[Array]' } }]); - done(); - } - } - ); - }); - }); - - // LOADER SPECIFIC TESTS ----------------------------------------------------------------------------------------- - if (IS_LOADER) { - describe('Loader Specific Tests', function() { - it('should add breadcrumb from onLoad callback from undefined error', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - Sentry.onLoad(function() { - Sentry.addBreadcrumb({ - category: 'auth', - message: 'testing loader', - level: 'error', - }); - }); - setTimeout(function() { - setTimeout(done, 1000); - Sentry.captureMessage('test'); - }, 1000); - undefinedMethod(); //trigger error - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = iframe.contentWindow.sentryData[0]; - if (IS_ASYNC_LOADER) { - assert.notOk(sentryData.breadcrumbs); - } else { - if (sentryData.breadcrumbs) { - assert.ok(sentryData.breadcrumbs); - assert.lengthOf(sentryData.breadcrumbs, 1); - assert.equal(sentryData.breadcrumbs[0].message, 'testing loader'); - } else { - // This seems to be happening only in chrome - assert.notOk(sentryData.breadcrumbs); - } - } - done(); - } - } - ); - }); - - it('should add breadcrumb from onLoad callback from undefined error with custom init()', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - Sentry.onLoad(function() { - Sentry.init({ debug: true }); - Sentry.addBreadcrumb({ - category: 'auth', - message: 'testing loader', - level: 'error', - }); - }); - setTimeout(function() { - setTimeout(done, 1000); - Sentry.captureMessage('test'); - }, 1000); - undefinedMethod(); //trigger error - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = iframe.contentWindow.sentryData[0]; - assert.ok(sentryData.breadcrumbs); - assert.lengthOf(sentryData.breadcrumbs, 1); - assert.equal(sentryData.breadcrumbs[0].message, 'testing loader'); - done(); - } - } - ); - }); - }); - } - // ------------------------------------------------------------------------------------------------------------- - }); - })(); -} - -var loaderSpecific = ['loader-with-no-global-init', 'loader-with-no-global-init-lazy-no']; - -for (var idx in loaderSpecific) { - (function() { - var filename = loaderSpecific[idx]; - - describe(filename + '.html', function() { - this.timeout(30000); - - beforeEach(function(done) { - this.iframe = createIframe(done, filename); - }); - - afterEach(function() { - document.body.removeChild(this.iframe); - }); - - describe('Loader Specific Tests - With no Global init() call', function() { - it('should add breadcrumb from onLoad callback from undefined error', function(done) { - var iframe = this.iframe; - - iframeExecute( - iframe, - done, - function() { - Sentry.onLoad(function() { - initSDK(); - Sentry.addBreadcrumb({ - category: 'auth', - message: 'testing loader', - level: 'error', - }); - }); - setTimeout(function() { - setTimeout(done, 1000); - Sentry.captureMessage('test'); - }, 1000); - undefinedMethod(); //trigger error - }, - function(sentryData) { - if (debounceAssertEventCount(sentryData, 1, done)) { - var sentryData = iframe.contentWindow.sentryData[0]; - assert.ok(sentryData.breadcrumbs); - assert.lengthOf(sentryData.breadcrumbs, 1); - assert.equal(sentryData.breadcrumbs[0].message, 'testing loader'); - done(); - } - } - ); - }); - }); - }); - })(); -} diff --git a/packages/browser/test/integration/variants/frame.html b/packages/browser/test/integration/variants/frame.html new file mode 100644 index 000000000000..9027cf53b753 --- /dev/null +++ b/packages/browser/test/integration/variants/frame.html @@ -0,0 +1,25 @@ + + + + + + + + + + + +
+ +
+
+ +
+
+
+
+
+ + diff --git a/packages/browser/test/integration/variants/loader-lazy-no.html b/packages/browser/test/integration/variants/loader-lazy-no.html new file mode 100644 index 000000000000..43a0e77172eb --- /dev/null +++ b/packages/browser/test/integration/variants/loader-lazy-no.html @@ -0,0 +1,25 @@ + + + + + + + + + + + +
+ +
+
+ +
+
+
+
+
+ + diff --git a/packages/browser/test/integration/variants/loader-with-no-global-init-lazy-no.html b/packages/browser/test/integration/variants/loader-with-no-global-init-lazy-no.html new file mode 100644 index 000000000000..5470af88c458 --- /dev/null +++ b/packages/browser/test/integration/variants/loader-with-no-global-init-lazy-no.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/browser/test/integration/variants/loader-with-no-global-init.html b/packages/browser/test/integration/variants/loader-with-no-global-init.html new file mode 100644 index 000000000000..aaadc3fb229a --- /dev/null +++ b/packages/browser/test/integration/variants/loader-with-no-global-init.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/browser/test/integration/variants/loader.html b/packages/browser/test/integration/variants/loader.html new file mode 100644 index 000000000000..47e2c4e31e50 --- /dev/null +++ b/packages/browser/test/integration/variants/loader.html @@ -0,0 +1,25 @@ + + + + + + + + + + + +
+ +
+
+ +
+
+
+
+
+ + diff --git a/packages/browser/test/karma/integration-files.js b/packages/browser/test/karma/integration-files.js deleted file mode 100644 index 0b3d4cf1412b..000000000000 --- a/packages/browser/test/karma/integration-files.js +++ /dev/null @@ -1,27 +0,0 @@ -const fs = require('fs'); -fs.copyFileSync('../integrations/build/dedupe.js', './test/integration/dedupe.js'); -fs.copyFileSync('../integrations/build/dedupe.js.map', './test/integration/dedupe.js.map'); - -module.exports = [ - { pattern: 'test/integration/polyfills/es6-promise-4.2.5.auto.js', included: false }, - { pattern: 'test/integration/polyfills/whatwg-fetch-3.0.0.js', included: false }, - { pattern: 'test/integration/123', included: false }, - { pattern: 'test/integration/console-logs.js', included: false }, - { pattern: 'test/integration/throw-string.js', included: false }, - { pattern: 'test/integration/throw-error.js', included: false }, - { pattern: 'test/integration/throw-object.js', included: false }, - { pattern: 'test/integration/example.json', included: false }, - { pattern: 'test/integration/frame.html', included: false }, - { pattern: 'test/integration/loader.html', included: false }, - { pattern: 'test/integration/loader-lazy-no.html', included: false }, - { pattern: 'test/integration/loader-with-no-global-init.html', included: false }, - { pattern: 'test/integration/loader-with-no-global-init-lazy-no.html', included: false }, - { pattern: 'test/integration/common.js', included: false }, - { pattern: 'src/loader.js', included: false }, - { pattern: 'test/integration/init.js', included: false }, - { pattern: 'build/bundle.js', included: false }, - { pattern: 'build/bundle.js.map', included: false }, - { pattern: 'test/integration/dedupe.js', included: false }, - { pattern: 'test/integration/dedupe.js.map', included: false }, - 'test/integration/test.js', -]; diff --git a/packages/browser/test/karma/karma.integration.config.js b/packages/browser/test/karma/karma.integration.config.js deleted file mode 100644 index ee50aa7d5ec5..000000000000 --- a/packages/browser/test/karma/karma.integration.config.js +++ /dev/null @@ -1,38 +0,0 @@ -module.exports = config => { - config.set({ - colors: true, - singleRun: true, - autoWatch: false, - basePath: process.cwd(), - files: require('./integration-files'), - frameworks: ['mocha', 'chai', 'sinon'], - plugins: [ - 'karma-mocha', - 'karma-mocha-reporter', - 'karma-chai', - 'karma-sinon', - 'karma-chrome-launcher', - 'karma-firefox-launcher', - ], - reporters: ['mocha'], - browsers: ['ChromeHeadlessNoSandbox'], - customLaunchers: { - ChromeHeadlessNoSandbox: { - base: 'ChromeHeadless', - flags: ['--no-sandbox', '--disable-setuid-sandbox'], - }, - // FirefoxHeadless: { - // base: 'Firefox', - // flags: ['-headless'], - // }, - }, - browserNoActivityTimeout: 30000, - concurrency: 1, - client: { - mocha: { - reporter: 'html', - ui: 'bdd', - }, - }, - }); -}; diff --git a/packages/browser/test/karma/karma.saucelabs.config.js b/packages/browser/test/karma/karma.saucelabs.config.js deleted file mode 100644 index 50b4f0b12ef5..000000000000 --- a/packages/browser/test/karma/karma.saucelabs.config.js +++ /dev/null @@ -1,109 +0,0 @@ -var customLaunchers = { - sl_chrome: { - base: 'SauceLabs', - browserName: 'chrome', - platform: 'Windows 10', - version: 'latest', - }, - sl_firefox: { - base: 'SauceLabs', - browserName: 'firefox', - platform: 'Windows 10', - version: 'latest', - }, - sl_edge: { - base: 'SauceLabs', - browserName: 'microsoftedge', - version: 'latest', - platform: 'Windows 10', - }, - sl_ie_11: { - base: 'SauceLabs', - browserName: 'internet explorer', - platform: 'Windows 7', - version: '11', - }, - sl_ie_10: { - base: 'SauceLabs', - browserName: 'internet explorer', - platform: 'Windows 7', - version: '10', - }, - sl_safari: { - base: 'SauceLabs', - browserName: 'safari', - platform: 'OS X 10.13', - version: '11.1', - }, - sl_ios: { - base: 'SauceLabs', - browserName: 'iphone', - platform: 'OS X 10.13', - version: '11.1', - }, - sl_android_7: { - base: 'SauceLabs', - browserName: 'Chrome', - platform: 'Android', - version: '7.1', - device: 'Android GoogleAPI Emulator', - }, - sl_android_6: { - base: 'SauceLabs', - browserName: 'Chrome', - platform: 'Android', - version: '6.0', - device: 'Android Emulator', - }, - sl_android_5: { - base: 'SauceLabs', - browserName: 'android', - platform: 'Linux', - version: '5.1', - }, - sl_android_4: { - base: 'SauceLabs', - browserName: 'android', - platform: 'Linux', - version: '4.4', - }, -}; - -module.exports = function(config) { - config.set({ - logLevel: config.LOG_INFO, - basePath: process.cwd(), - files: require('./integration-files'), - frameworks: ['mocha', 'chai', 'sinon'], - plugins: ['karma-mocha', 'karma-chai', 'karma-sinon', 'karma-failed-reporter', 'karma-sauce-launcher'], - concurrency: 2, - client: { - mocha: { - reporter: 'html', - ui: 'bdd', - }, - }, - customLaunchers: customLaunchers, - browsers: Object.keys(customLaunchers), - reporters: ['failed', 'saucelabs'], - singleRun: true, - build: process.env.TRAVIS_BUILD_NUMBER, - // SauceLabs allows for 2 tunnels only, therefore some browsers will have to wait - // rather long time. Plus mobile emulators tend to require a lot of time to start up. - // 10 minutes should be more than enough to run all of them. - browserNoActivityTimeout: 600000, - captureTimeout: 600000, - sauceLabs: { - // NOTE: To run tests locally, change `startConnect` to `true` and use command: - // $ SAUCE_USERNAME=sentryio SAUCE_ACCESS_KEY= yarn test:saucelabs - startConnect: false, - // Just something "random" so we don't have to provide additional ENV var when running locally - tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER || Math.ceil(Math.random() * 1337), - recordScreenshots: false, - recordVideo: false, - testName: '@sentry/browser' + (process.env.TRAVIS_JOB_NUMBER ? ' #' + process.env.TRAVIS_JOB_NUMBER : ''), - public: 'public', - extendedDebugging: true, - }, - }); -}; diff --git a/packages/browser/test/backend.test.ts b/packages/browser/test/unit/backend.test.ts similarity index 86% rename from packages/browser/test/backend.test.ts rename to packages/browser/test/unit/backend.test.ts index 0b70689eb3e6..5477dc27a567 100644 --- a/packages/browser/test/backend.test.ts +++ b/packages/browser/test/unit/backend.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { BrowserBackend } from '../src/backend'; +import { BrowserBackend } from '../../src/backend'; let backend: BrowserBackend; diff --git a/packages/browser/test/index.test.ts b/packages/browser/test/unit/index.test.ts similarity index 99% rename from packages/browser/test/index.test.ts rename to packages/browser/test/unit/index.test.ts index c2d060582aaf..411eff34e3fd 100644 --- a/packages/browser/test/index.test.ts +++ b/packages/browser/test/unit/index.test.ts @@ -14,7 +14,7 @@ import { init, Integrations, Scope, -} from '../src'; +} from '../../src'; import { SimpleTransport } from './mocks/simpletransport'; diff --git a/packages/browser/test/integrations/helpers.test.ts b/packages/browser/test/unit/integrations/helpers.test.ts similarity index 99% rename from packages/browser/test/integrations/helpers.test.ts rename to packages/browser/test/unit/integrations/helpers.test.ts index 822ea15769e3..eb2eb37e3b1b 100644 --- a/packages/browser/test/integrations/helpers.test.ts +++ b/packages/browser/test/unit/integrations/helpers.test.ts @@ -2,7 +2,7 @@ import { WrappedFunction } from '@sentry/types'; import { expect } from 'chai'; import { SinonSpy, spy } from 'sinon'; -import { wrap } from '../../src/helpers'; +import { wrap } from '../../../src/helpers'; describe('wrap()', () => { it('should wrap only functions', () => { diff --git a/packages/browser/test/integrations/linkederrors.test.ts b/packages/browser/test/unit/integrations/linkederrors.test.ts similarity index 97% rename from packages/browser/test/integrations/linkederrors.test.ts rename to packages/browser/test/unit/integrations/linkederrors.test.ts index 1f4d2891059d..dbae42d45535 100644 --- a/packages/browser/test/integrations/linkederrors.test.ts +++ b/packages/browser/test/unit/integrations/linkederrors.test.ts @@ -2,8 +2,8 @@ import { ExtendedError } from '@sentry/types'; import { expect } from 'chai'; import { stub } from 'sinon'; -import { BrowserBackend } from '../../src/backend'; -import { LinkedErrors } from '../../src/integrations/linkederrors'; +import { BrowserBackend } from '../../../src/backend'; +import { LinkedErrors } from '../../../src/integrations/linkederrors'; let linkedErrors: any; diff --git a/packages/browser/test/karma/karma.unit.config.js b/packages/browser/test/unit/karma.conf.js similarity index 72% rename from packages/browser/test/karma/karma.unit.config.js rename to packages/browser/test/unit/karma.conf.js index 82fa187e0786..50223c63a8cc 100644 --- a/packages/browser/test/karma/karma.unit.config.js +++ b/packages/browser/test/unit/karma.conf.js @@ -4,7 +4,7 @@ module.exports = config => { singleRun: true, autoWatch: false, basePath: process.cwd(), - files: ['test/**/*.ts', 'src/**/*.+(js|ts)'], + files: ['test/unit/**/*.ts', 'src/**/*.+(js|ts)'], frameworks: ['mocha', 'chai', 'sinon', 'karma-typescript'], browsers: ['ChromeHeadless'], reporters: ['mocha', 'karma-typescript'], @@ -18,18 +18,18 @@ module.exports = config => { declaration: false, declarationMap: false, paths: { - '@sentry/utils/*': ['../../utils/src/*'], - '@sentry/core': ['../../core/src'], - '@sentry/hub': ['../../hub/src'], - '@sentry/types': ['../../types/src'], - '@sentry/minimal': ['../../minimal/src'], + '@sentry/utils/*': ['../../../utils/src/*'], + '@sentry/core': ['../../../core/src'], + '@sentry/hub': ['../../../hub/src'], + '@sentry/types': ['../../../types/src'], + '@sentry/minimal': ['../../../minimal/src'], }, }, bundlerOptions: { sourceMap: true, transforms: [require('karma-typescript-es6-transform')()], }, - include: ['test/**/*.ts'], + include: ['test/unit/**/*.ts'], reports: { html: 'coverage', 'text-summary': '', diff --git a/packages/browser/test/mocks/simpletransport.ts b/packages/browser/test/unit/mocks/simpletransport.ts similarity index 66% rename from packages/browser/test/mocks/simpletransport.ts rename to packages/browser/test/unit/mocks/simpletransport.ts index a8f8dcb357ee..a270c074e567 100644 --- a/packages/browser/test/mocks/simpletransport.ts +++ b/packages/browser/test/unit/mocks/simpletransport.ts @@ -1,5 +1,5 @@ -import { Event, Response, Status } from '../../src'; -import { BaseTransport } from '../../src/transports'; +import { Event, Response, Status } from '../../../src'; +import { BaseTransport } from '../../../src/transports'; export class SimpleTransport extends BaseTransport { public sendEvent(_: Event): Promise { diff --git a/packages/browser/test/parsers.test.ts b/packages/browser/test/unit/parsers.test.ts similarity index 98% rename from packages/browser/test/parsers.test.ts rename to packages/browser/test/unit/parsers.test.ts index 6ce4c35341aa..a8c4ae63e327 100644 --- a/packages/browser/test/parsers.test.ts +++ b/packages/browser/test/unit/parsers.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { prepareFramesForEvent } from '../src/parsers'; +import { prepareFramesForEvent } from '../../src/parsers'; describe('Parsers', () => { describe('prepareFramesForEvent()', () => { diff --git a/packages/browser/test/tracekit/custom.test.ts b/packages/browser/test/unit/tracekit/custom.test.ts similarity index 99% rename from packages/browser/test/tracekit/custom.test.ts rename to packages/browser/test/unit/tracekit/custom.test.ts index 06c63724d5b6..852d655317ac 100644 --- a/packages/browser/test/tracekit/custom.test.ts +++ b/packages/browser/test/unit/tracekit/custom.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { _computeStackTrace } from '../../src/tracekit'; +import { _computeStackTrace } from '../../../src/tracekit'; describe('Tracekit - Custom Tests', () => { describe('should parse exceptions with native code frames', () => { diff --git a/packages/browser/test/tracekit/original.test.ts b/packages/browser/test/unit/tracekit/original.test.ts similarity index 99% rename from packages/browser/test/tracekit/original.test.ts rename to packages/browser/test/unit/tracekit/original.test.ts index b2babd4e874e..9dba7c8c91b5 100644 --- a/packages/browser/test/tracekit/original.test.ts +++ b/packages/browser/test/unit/tracekit/original.test.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; -import { _computeStackTrace } from '../../src/tracekit'; +import { _computeStackTrace } from '../../../src/tracekit'; import { ANDROID_REACT_NATIVE, diff --git a/packages/browser/test/tracekit/originalfixtures.ts b/packages/browser/test/unit/tracekit/originalfixtures.ts similarity index 100% rename from packages/browser/test/tracekit/originalfixtures.ts rename to packages/browser/test/unit/tracekit/originalfixtures.ts diff --git a/packages/browser/test/transports/base.test.ts b/packages/browser/test/unit/transports/base.test.ts similarity index 91% rename from packages/browser/test/transports/base.test.ts rename to packages/browser/test/unit/transports/base.test.ts index 25ecfca83503..708fba4f9251 100644 --- a/packages/browser/test/transports/base.test.ts +++ b/packages/browser/test/unit/transports/base.test.ts @@ -1,6 +1,6 @@ import { expect } from 'chai'; -import { BaseTransport } from '../../src/transports/base'; +import { BaseTransport } from '../../../src/transports/base'; const testDsn = 'https://123@sentry.io/42'; diff --git a/packages/browser/test/transports/fetch.test.ts b/packages/browser/test/unit/transports/fetch.test.ts similarity index 97% rename from packages/browser/test/transports/fetch.test.ts rename to packages/browser/test/unit/transports/fetch.test.ts index ecb548781e20..253e2dfb9cd2 100644 --- a/packages/browser/test/transports/fetch.test.ts +++ b/packages/browser/test/unit/transports/fetch.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { SinonStub, stub } from 'sinon'; -import { Status, Transports } from '../../src'; +import { Status, Transports } from '../../../src'; const testDsn = 'https://123@sentry.io/42'; const transportUrl = 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7'; diff --git a/packages/browser/test/transports/xhr.test.ts b/packages/browser/test/unit/transports/xhr.test.ts similarity index 97% rename from packages/browser/test/transports/xhr.test.ts rename to packages/browser/test/unit/transports/xhr.test.ts index b6783d2a0ffd..d35554d55fae 100644 --- a/packages/browser/test/transports/xhr.test.ts +++ b/packages/browser/test/unit/transports/xhr.test.ts @@ -1,7 +1,7 @@ import { expect } from 'chai'; import { fakeServer, SinonFakeServer } from 'sinon'; -import { Status, Transports } from '../../src'; +import { Status, Transports } from '../../../src'; const testDsn = 'https://123@sentry.io/42'; const transportUrl = 'https://sentry.io/api/42/store/?sentry_key=123&sentry_version=7'; diff --git a/scripts/browser-saucelabs.sh b/scripts/browser-saucelabs.sh deleted file mode 100755 index 056dd24e52ce..000000000000 --- a/scripts/browser-saucelabs.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -e - -yarn -# We have to build other packages first, as we use absolute packages import in TypeScript -yarn build -cd packages/browser -yarn test:saucelabs diff --git a/yarn.lock b/yarn.lock index 9801ebe698bc..2044db0bac4b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1593,10 +1593,6 @@ address@^1.0.1: resolved "https://registry.yarnpkg.com/address/-/address-1.0.3.tgz#b5f50631f8d6cec8bd20c963963afb55e06cbce9" integrity sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg== -adm-zip@~0.4.3: - version "0.4.13" - resolved "https://registry.yarnpkg.com/adm-zip/-/adm-zip-0.4.13.tgz#597e2f8cc3672151e1307d3e95cddbc75672314a" - after@0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" @@ -1723,6 +1719,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.0.3.tgz#2fb624fe0e84bccab00afee3d0006ed310f22f09" + integrity sha512-c6IvoeBECQlMVuYUjSwimnhmztImpErfxJzWZhIQinIvQWoGOnB0dLIgifbPHQt5heS6mNlaZG16f06H3C8t1g== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -1933,7 +1937,7 @@ async@1.x, async@^1.4.0: resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= -async@^2.0.1, async@^2.1.2: +async@^2.0.1: version "2.6.1" resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" dependencies: @@ -2539,6 +2543,11 @@ binary-extensions@^1.0.0: version "1.12.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + blob@0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" @@ -2627,6 +2636,13 @@ braces@^2.3.0, braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +braces@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -2708,6 +2724,24 @@ browserslist@^3.2.6: caniuse-lite "^1.0.30000844" electron-to-chromium "^1.3.47" +browserstack-local@^1.3.7: + version "1.4.0" + resolved "https://registry.yarnpkg.com/browserstack-local/-/browserstack-local-1.4.0.tgz#d979cac056f57b9af159b3bcd7fdc09b4354537c" + integrity sha512-BUJWxIsJkJxqfTPJIvGWTsf+IYSqSFUeFNW9tnuyTG7va/0LkXLhIi/ErFGDle1urQkol48HlQUXj4QrliXFpg== + dependencies: + https-proxy-agent "^2.2.1" + is-running "^2.0.0" + ps-tree "=1.1.1" + sinon "^1.17.6" + temp-fs "^0.9.9" + +browserstack@~1.5.1: + version "1.5.2" + resolved "https://registry.yarnpkg.com/browserstack/-/browserstack-1.5.2.tgz#17d8bb76127a1cc0ea416424df80d218f803673f" + integrity sha512-+6AFt9HzhKykcPF79W6yjEUJcdvZOV0lIXdkORXMJftGrDl0OKWqRF4GHqpDNkxiceDT/uB7Fb/aDwktvXX7dg== + dependencies: + https-proxy-agent "^2.2.1" + bs-logger@0.x: version "0.2.6" resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" @@ -3045,6 +3079,21 @@ chokidar@^2.1.0: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.0.2.tgz#0d1cd6d04eb2df0327446188cd13736a3367d681" + integrity sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA== + dependencies: + anymatch "^3.0.1" + braces "^3.0.2" + glob-parent "^5.0.0" + is-binary-path "^2.1.0" + is-glob "^4.0.1" + normalize-path "^3.0.0" + readdirp "^3.1.1" + optionalDependencies: + fsevents "^2.0.6" + chownr@^1.0.1, chownr@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" @@ -3190,10 +3239,6 @@ colors@^1.1.0: version "1.3.2" resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.2.tgz#2df8ff573dfbf255af562f8ce7181d6b971a359b" -colors@~0.6.0: - version "0.6.2" - resolved "http://registry.npmjs.org/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc" - columnify@^1.5.4: version "1.5.4" resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" @@ -3499,11 +3544,6 @@ core-js@^2.5.7: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== -core-js@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65" - integrity sha1-+rg/uwstjchfpjbEudNMdUIMbWU= - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -3833,7 +3873,7 @@ defaults@^1.0.3: dependencies: clone "^1.0.2" -define-properties@^1.1.2: +define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== @@ -4002,7 +4042,7 @@ duplexer3@^0.1.4: resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= -duplexer@^0.1.1: +duplexer@^0.1.1, duplexer@~0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= @@ -4157,17 +4197,7 @@ error-ex@^1.2.0, error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" -es-abstract@^1.4.3: - version "1.12.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" - dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" - -es-abstract@^1.5.1: +es-abstract@^1.12.0, es-abstract@^1.5.1: version "1.13.0" resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9" integrity sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg== @@ -4179,6 +4209,16 @@ es-abstract@^1.5.1: is-regex "^1.0.4" object-keys "^1.0.12" +es-abstract@^1.4.3: + version "1.12.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" + dependencies: + es-to-primitive "^1.1.1" + function-bind "^1.1.1" + has "^1.0.1" + is-callable "^1.1.3" + is-regex "^1.0.4" + es-to-primitive@^1.1.1, es-to-primitive@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" @@ -4193,11 +4233,6 @@ es6-promise@^4.0.3: resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q== -es6-promise@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" - integrity sha1-AQ1YWEI6XxGJeWZfRkhqlcbuK7Y= - es6-promisify@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" @@ -4286,6 +4321,19 @@ etag@~1.8.0, etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-stream@=3.3.4: + version "3.3.4" + resolved "https://registry.yarnpkg.com/event-stream/-/event-stream-3.3.4.tgz#4ab4c9a0f5a54db9338b4c34d86bfce8f4b35571" + integrity sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE= + dependencies: + duplexer "~0.1.1" + from "~0" + map-stream "~0.1.0" + pause-stream "0.0.11" + split "0.3" + stream-combiner "~0.0.4" + through "~2.3.1" + event-target-shim@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.0.tgz#8fabe06975219af64c36ceed4771cf1ad1c3455c" @@ -4576,6 +4624,13 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + finalhandler@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" @@ -4674,6 +4729,13 @@ form-data@~2.3.2: combined-stream "^1.0.6" mime-types "^2.1.12" +formatio@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.1.1.tgz#5ed3ccd636551097383465d996199100e86161e9" + integrity sha1-XtPM1jZVEJc4NGXZlhmRAOhhYek= + dependencies: + samsam "~1.1" + forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -4702,6 +4764,11 @@ from2@^2.1.0: inherits "^2.0.1" readable-stream "^2.0.0" +from@~0: + version "0.1.7" + resolved "https://registry.yarnpkg.com/from/-/from-0.1.7.tgz#83c60afc58b9c56997007ed1a768b3ab303a44fe" + integrity sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4= + fs-access@^1.0.0: version "1.0.1" resolved "http://registry.npmjs.org/fs-access/-/fs-access-1.0.1.tgz#d6a87f262271cefebec30c553407fb995da8777a" @@ -4767,6 +4834,11 @@ fsevents@^1.2.7: nan "^2.9.2" node-pre-gyp "^0.10.0" +fsevents@^2.0.6: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a" + integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ== + fstream@^1.0.0, fstream@^1.0.2: version "1.0.11" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.11.tgz#5c1fb1f117477114f0632a0eb4b71b3cb0fd3171" @@ -4988,6 +5060,13 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-parent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.0.0.tgz#1dc99f0f39b006d3e92c2c284068382f0c20e954" + integrity sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg== + dependencies: + is-glob "^4.0.1" + glob-to-regexp@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" @@ -5411,11 +5490,6 @@ ignore@^3.3.5: resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" integrity sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug== -immediate@~3.0.5: - version "3.0.6" - resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" - integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= - import-fresh@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-2.0.0.tgz#d81355c15612d386c61f9ddd3922d4304822a546" @@ -5575,6 +5649,11 @@ is-accessor-descriptor@^1.0.0: dependencies: kind-of "^6.0.0" +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -5591,6 +5670,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -5703,6 +5789,11 @@ is-generator-fn@^2.0.0: resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.0.0.tgz#038c31b774709641bda678b1f06a4e3227c10b3e" integrity sha512-elzyIdM7iKoFHzcrndIqjYomImhxrFRnGP3galODoII4TB9gI7mZ+FnlLQmmjf27SxHS2gKEeyhX5/+YRS6H9g== +is-generator-function@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" + integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== + is-glob@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" @@ -5717,6 +5808,13 @@ is-glob@^4.0.0: dependencies: is-extglob "^2.1.1" +is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -5734,6 +5832,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" @@ -5773,6 +5876,11 @@ is-retry-allowed@^1.0.0: resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ= +is-running@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-running/-/is-running-2.1.0.tgz#30a73ff5cc3854e4fc25490809e9f5abf8de09e0" + integrity sha1-MKc/9cw4VOT8JUkICen1q/jeCeA= + is-ssh@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.3.1.tgz#f349a8cadd24e65298037a522cf7520f2e81a0f3" @@ -6530,17 +6638,6 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jszip@^3.1.3: - version "3.1.5" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.1.5.tgz#e3c2a6c6d706ac6e603314036d43cd40beefdf37" - integrity sha512-5W8NUaFRFRqTOL7ZDDrx5qWHJyBXy6velVudIzQUSoqAAYqzSh2Z7/m0Rf1QbmQJccegD0r+YZxBjzqoBiEeJQ== - dependencies: - core-js "~2.3.0" - es6-promise "~3.0.2" - lie "~3.1.0" - pako "~1.0.2" - readable-stream "~2.0.6" - just-extend@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc" @@ -6563,6 +6660,15 @@ jws@^3.1.5: jwa "^1.2.0" safe-buffer "^5.0.1" +karma-browserstack-launcher@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/karma-browserstack-launcher/-/karma-browserstack-launcher-1.5.1.tgz#4caf4cd476a76d3c88205818d8994fc170a68fb4" + integrity sha512-zt9Ukow5A9WZHZXCFVO/h5kRsAdaZYeMNJK9Uan8v42amQXt3B/DZVxl24NCcAIxufKjW13UWd9iJ9knG9OCYw== + dependencies: + browserstack "~1.5.1" + browserstack-local "^1.3.7" + q "~1.5.0" + karma-chai@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/karma-chai/-/karma-chai-0.1.0.tgz#bee5ad40400517811ae34bb945f762909108b79a" @@ -6584,15 +6690,10 @@ karma-coverage@^1.1.1: minimatch "^3.0.0" source-map "^0.5.1" -karma-failed-reporter@0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/karma-failed-reporter/-/karma-failed-reporter-0.0.3.tgz#4532ec9652c9fe297d0b72d08d9cade9725ef733" - dependencies: - colors "~0.6.0" - karma-firefox-launcher@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz#2c47030452f04531eb7d13d4fc7669630bb93339" + integrity sha512-LbZ5/XlIXLeQ3cqnCbYLn+rOVhuMIK9aZwlP6eOLGzWdo1UVp7t6CN3DP4SafiRLjexKwHeKHDm0c38Mtd3VxA== karma-mocha-reporter@^2.2.5: version "2.2.5" @@ -6616,15 +6717,6 @@ karma-rollup-preprocessor@^7.0.0: chokidar "^2.1.0" debounce "^1.2.0" -karma-sauce-launcher@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/karma-sauce-launcher/-/karma-sauce-launcher-2.0.2.tgz#dbf98e70d86bf287b03a537cf637eb7aefa975c3" - integrity sha512-jLUFaJhHMcKpxFWUesyWYihzM5FvQiJsDwGcCtKeOy2lsWhkVw0V0Byqb1d+wU6myU1mribBtsIcub23HS4kWA== - dependencies: - sauce-connect-launcher "^1.2.4" - saucelabs "^1.5.0" - selenium-webdriver "^4.0.0-alpha.1" - karma-sinon@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/karma-sinon/-/karma-sinon-1.0.5.tgz#4e3443f2830fdecff624d3747163f1217daa2a9a" @@ -6848,13 +6940,6 @@ libnpmpublish@^1.1.1: semver "^5.5.1" ssri "^6.0.1" -lie@~3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" - integrity sha1-mkNrLMd0bKWd56QfpGmz77dr2H4= - dependencies: - immediate "~3.0.5" - load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -7017,7 +7102,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.11, lodash@^4.13.1, lodash@^4.16.6, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: +lodash@4.17.11, lodash@^4.13.1, lodash@^4.17.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -7038,6 +7123,11 @@ log4js@^4.0.0, log4js@^4.0.1: rfdc "^1.1.2" streamroller "^1.0.1" +lolex@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.3.2.tgz#7c3da62ffcb30f0f5a80a2566ca24e45d8a01f31" + integrity sha1-fD2mL/yzDw9agKJWbKJORdigHzE= + lolex@^2.3.2: version "2.7.5" resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.5.tgz#113001d56bfc7e02d56e36291cc5c413d1aa0733" @@ -7178,6 +7268,11 @@ map-obj@^2.0.0: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= +map-stream@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.1.0.tgz#e56aa94c4c8055a16404a0674b78f215f7c8e194" + integrity sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ= + map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" @@ -7956,6 +8051,16 @@ object.assign@4.1.0: has-symbols "^1.0.0" object-keys "^1.0.11" +object.entries@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.0.tgz#2024fc6d6ba246aee38bdb0ffd5cfbcf371b7519" + integrity sha512-l+H6EQ8qzGRxbkHOd5I/aHRhHDKoQXQ8g0BYt4uSweQU1/J6dZUOyWh9a2Vky35YCKjzmgxOzta2hH6kf9HuXA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + object.getownpropertydescriptors@^2.0.3: version "2.0.3" resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" @@ -8069,7 +8174,7 @@ os-name@^3.0.0: macos-release "^2.0.0" windows-release "^3.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" @@ -8225,11 +8330,6 @@ pad@^2.2.2: dependencies: wcwidth "^1.0.1" -pako@~1.0.2: - version "1.0.8" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.8.tgz#6844890aab9c635af868ad5fecc62e8acbba3ea4" - integrity sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA== - pako@~1.0.5: version "1.0.6" resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.6.tgz#0101211baa70c4bca4a0f63f2206e97b7dfaf258" @@ -8435,6 +8535,13 @@ pathval@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" +pause-stream@0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/pause-stream/-/pause-stream-0.0.11.tgz#fe5a34b0cbce12b5aa6a2b403ee2e73b602f1445" + integrity sha1-/lo0sMvOErWqaitAPuLnO2AvFEU= + dependencies: + through "~2.3" + pbkdf2@^3.0.3: version "3.0.17" resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.17.tgz#976c206530617b14ebb32114239f7b09336e93a6" @@ -8450,6 +8557,11 @@ performance-now@^2.1.0: resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= +picomatch@^2.0.4: + version "2.0.7" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" + integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== + pidtree@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.3.0.tgz#f6fada10fccc9f99bf50e90d0b23d72c9ebc2e6b" @@ -8566,10 +8678,6 @@ private@^0.1.6, private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" - process-nextick-args@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" @@ -8640,6 +8748,13 @@ prr@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" +ps-tree@=1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ps-tree/-/ps-tree-1.1.1.tgz#5f1ba35455b8c25eeb718d04c37de1555d96d3db" + integrity sha512-kef7fYYSKVqQffmzTMsVcUD1ObNJMp8sNSmHGlGKsZQyL/ht9MZKk86u0Rd1NhpTOAuhqwKCLLpktwkqz+MF8A== + dependencies: + event-stream "=3.3.4" + pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" @@ -8699,7 +8814,7 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -q@^1.5.1: +q@^1.5.1, q@~1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= @@ -8883,18 +8998,6 @@ readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.1.1: string_decoder "^1.1.1" util-deprecate "^1.0.1" -readable-stream@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" - integrity sha1-j5A0HmilPMySh4jaz80Rs265t44= - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" - util-deprecate "~1.0.1" - readdir-scoped-modules@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747" @@ -8913,6 +9016,13 @@ readdirp@^2.0.0, readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.1.1.tgz#b158123ac343c8b0f31d65680269cc0fc1025db1" + integrity sha512-XXdSXZrQuvqoETj50+JAitxz1UPdt5dupjT6T5nVB+WvjMv2XKYj+s7hPeAVCXvmJrL36O4YYyWlIC3an2ePiQ== + dependencies: + picomatch "^2.0.4" + readline-sync@^1.4.9: version "1.4.9" resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.9.tgz#3eda8e65f23cd2a17e61301b1f0003396af5ecda" @@ -9214,6 +9324,13 @@ rimraf@^2.6.0, rimraf@^2.6.2: dependencies: glob "^7.0.5" +rimraf@~2.5.2: + version "2.5.4" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" + integrity sha1-loAAk8vxoMhr2VtGJUZ1NcKd+gQ= + dependencies: + glob "^7.0.5" + ripemd160@^2.0.0, ripemd160@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c" @@ -9339,6 +9456,16 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +samsam@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.2.tgz#bec11fdc83a9fda063401210e40176c3024d1567" + integrity sha1-vsEf3IOp/aBjQBIQ5AF2wwJNFWc= + +samsam@~1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.1.3.tgz#9f5087419b4d091f232571e7fa52e90b0f552621" + integrity sha1-n1CHQZtNCR8jJXHn+lLpCw9VJiE= + sane@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/sane/-/sane-4.0.3.tgz#e878c3f19e25cc57fbb734602f48f8a97818b181" @@ -9354,25 +9481,7 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sauce-connect-launcher@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sauce-connect-launcher/-/sauce-connect-launcher-1.2.4.tgz#8d38f85242a9fbede1b2303b559f7e20c5609a1c" - integrity sha512-X2vfwulR6brUGiicXKxPm1GJ7dBEeP1II450Uv4bHGrcGOapZNgzJvn9aioea5IC5BPp/7qjKdE3xbbTBIVXMA== - dependencies: - adm-zip "~0.4.3" - async "^2.1.2" - https-proxy-agent "^2.2.1" - lodash "^4.16.6" - rimraf "^2.5.4" - -saucelabs@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/saucelabs/-/saucelabs-1.5.0.tgz#9405a73c360d449b232839919a86c396d379fd9d" - integrity sha512-jlX3FGdWvYf4Q3LFfFWS1QvPg3IGCGWxIc8QBFdPTbpTJnt/v17FHXYVAn7C8sHf1yUXo2c7yIM0isDryfYtHQ== - dependencies: - https-proxy-agent "^2.2.1" - -sax@>=0.6.0, sax@^1.2.4: +sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" @@ -9391,16 +9500,6 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -selenium-webdriver@^4.0.0-alpha.1: - version "4.0.0-alpha.1" - resolved "https://registry.yarnpkg.com/selenium-webdriver/-/selenium-webdriver-4.0.0-alpha.1.tgz#cc93415e21d2dc1dfd85dfc5f6b55f3ac53933b1" - integrity sha512-z88rdjHAv3jmTZ7KSGUkTvo4rGzcDGMq0oXWHNIDK96Gs31JKVdu9+FMtT4KBrVoibg8dUicJDok6GnqqttO5Q== - dependencies: - jszip "^3.1.3" - rimraf "^2.5.4" - tmp "0.0.30" - xml2js "^0.4.17" - semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -9583,6 +9682,16 @@ signal-exit@^3.0.0, signal-exit@^3.0.2: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= +sinon@^1.17.6: + version "1.17.7" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-1.17.7.tgz#4542a4f49ba0c45c05eb2e9dd9d203e2b8efe0bf" + integrity sha1-RUKk9JugxFwF6y6d2dID4rjv4L8= + dependencies: + formatio "1.1.1" + lolex "1.3.2" + samsam "1.1.2" + util ">=0.10.3 <1" + sinon@^7.3.2: version "7.3.2" resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.3.2.tgz#82dba3a6d85f6d2181e1eca2c10d8657c2161f28" @@ -9841,6 +9950,13 @@ split2@^2.0.0: dependencies: through2 "^2.0.2" +split@0.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/split/-/split-0.3.3.tgz#cd0eea5e63a211dfff7eb0f091c4133e2d0dd28f" + integrity sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8= + dependencies: + through "2" + split@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" @@ -9920,6 +10036,13 @@ stream-browserify@^2.0.2: inherits "~2.0.1" readable-stream "^2.0.2" +stream-combiner@~0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/stream-combiner/-/stream-combiner-0.0.4.tgz#4d5e433c185261dde623ca3f44c586bcf5c4ad14" + integrity sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ= + dependencies: + duplexer "~0.1.1" + stream-each@^1.1.0: version "1.2.3" resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae" @@ -10025,10 +10148,6 @@ string_decoder@^1.0.0, string_decoder@^1.1.1, string_decoder@^1.2.0: dependencies: safe-buffer "~5.1.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "http://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -10195,6 +10314,13 @@ temp-dir@^1.0.0: resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= +temp-fs@^0.9.9: + version "0.9.9" + resolved "https://registry.yarnpkg.com/temp-fs/-/temp-fs-0.9.9.tgz#8071730437870720e9431532fe2814364f8803d7" + integrity sha1-gHFzBDeHByDpQxUy/igUNk+IA9c= + dependencies: + rimraf "~2.5.2" + temp-write@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492" @@ -10286,7 +10412,7 @@ through2@^2.0.0, through2@^2.0.2: readable-stream "~2.3.6" xtend "~4.0.1" -through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@~2.3, through@~2.3.1: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= @@ -10302,13 +10428,6 @@ timers-browserify@^2.0.10, timers-browserify@^2.0.4: dependencies: setimmediate "^1.0.4" -tmp@0.0.30: - version "0.0.30" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.30.tgz#72419d4a8be7d6ce75148fd8b324e593a711c2ed" - integrity sha1-ckGdSovn1s51FI/YsyTlk6cRwu0= - dependencies: - os-tmpdir "~1.0.1" - tmp@0.0.33, tmp@0.0.x, tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -10352,6 +10471,13 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" @@ -10753,6 +10879,17 @@ util@0.10.3: dependencies: inherits "2.0.1" +"util@>=0.10.3 <1": + version "0.12.1" + resolved "https://registry.yarnpkg.com/util/-/util-0.12.1.tgz#f908e7b633e7396c764e694dd14e716256ce8ade" + integrity sha512-MREAtYOp+GTt9/+kwf00IYoHZyjM8VU4aVrkzUlejyqaIjd2GztVl5V9hGXKlvBKE3gENn/FMfHE5v6hElXGcQ== + dependencies: + inherits "^2.0.3" + is-arguments "^1.0.4" + is-generator-function "^1.0.7" + object.entries "^1.1.0" + safe-buffer "^5.1.2" + util@^0.10.3: version "0.10.4" resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" @@ -11107,19 +11244,6 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml2js@^0.4.17: - version "0.4.19" - resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" - integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== - dependencies: - sax ">=0.6.0" - xmlbuilder "~9.0.1" - -xmlbuilder@~9.0.1: - version "9.0.7" - resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" - integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= - xmlchars@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-1.3.1.tgz#1dda035f833dbb4f86a0c28eaa6ca769214793cf"