diff --git a/package-lock.json b/package-lock.json
index 6f9d31f2650..2105b7f2907 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6438,9 +6438,9 @@
       "dev": true
     },
     "jasmine-core": {
-      "version": "2.99.1",
-      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-2.99.1.tgz",
-      "integrity": "sha1-5kAN8ea1bhMLYcS80JPap/boyhU=",
+      "version": "3.4.0",
+      "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.4.0.tgz",
+      "integrity": "sha512-HU/YxV4i6GcmiH4duATwAbJQMlE0MsDIR5XmSVxURxKHn3aGAdbY1/ZJFmVRbKtnLwIxxMJD7gYaPsypcbYimg==",
       "dev": true
     },
     "js-base64": {
@@ -6735,12 +6735,6 @@
         "which": "^1.2.1"
       }
     },
-    "karma-fail-fast-reporter": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/karma-fail-fast-reporter/-/karma-fail-fast-reporter-1.0.5.tgz",
-      "integrity": "sha1-9ScyP5jcXx6oEEfwCkuD7etgv3U=",
-      "dev": true
-    },
     "karma-firefox-launcher": {
       "version": "1.1.0",
       "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-1.1.0.tgz",
@@ -6757,15 +6751,18 @@
       }
     },
     "karma-jasmine": {
-      "version": "1.1.2",
-      "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-1.1.2.tgz",
-      "integrity": "sha1-OU8rJf+0pkS5rabyLUQ+L9CIhsM=",
-      "dev": true
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz",
+      "integrity": "sha512-iuC0hmr9b+SNn1DaUD2QEYtUxkS1J+bSJSn7ejdEexs7P8EYvA1CWkEdrDQ+8jVH3AgWlCNwjYsT1chjcNW9lA==",
+      "dev": true,
+      "requires": {
+        "jasmine-core": "^3.3"
+      }
     },
     "karma-jasmine-spec-tags": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/karma-jasmine-spec-tags/-/karma-jasmine-spec-tags-1.0.1.tgz",
-      "integrity": "sha1-Mz7WJZKSMG81Dez3f5uNxcOVQhI=",
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/karma-jasmine-spec-tags/-/karma-jasmine-spec-tags-1.1.0.tgz",
+      "integrity": "sha512-uhGYcGV1jmUSe2QZ6D/pmVehnggQaB8LCaG17EWetYJnGGu6C7LUcPkVNeMXMiewwa5V6g8RxOG8LP8Jh1ZBsQ==",
       "dev": true
     },
     "karma-spec-reporter": {
diff --git a/package.json b/package.json
index 4aa75b39b64..045d7583924 100644
--- a/package.json
+++ b/package.json
@@ -132,16 +132,15 @@
     "gzip-size": "^5.1.1",
     "image-size": "^0.7.4",
     "into-stream": "^4.0.0",
-    "jasmine-core": "^2.99.1",
+    "jasmine-core": "^3.4.0",
     "jsdom": "^11.12.0",
     "karma": "^4.1.0",
     "karma-browserify": "^6.0.0",
     "karma-chrome-launcher": "^2.0.0",
-    "karma-fail-fast-reporter": "^1.0.5",
     "karma-firefox-launcher": "^1.0.1",
     "karma-ie-launcher": "^1.0.0",
-    "karma-jasmine": "^1.1.2",
-    "karma-jasmine-spec-tags": "^1.0.1",
+    "karma-jasmine": "^2.0.1",
+    "karma-jasmine-spec-tags": "^1.1.0",
     "karma-spec-reporter": "0.0.32",
     "karma-verbose-reporter": "0.0.6",
     "karma-viewport": "^1.0.4",
diff --git a/test/jasmine/assets/custom_assertions.js b/test/jasmine/assets/custom_assertions.js
index 30828bf7605..067a1f09b88 100644
--- a/test/jasmine/assets/custom_assertions.js
+++ b/test/jasmine/assets/custom_assertions.js
@@ -1,6 +1,7 @@
 'use strict';
 
 var d3 = require('d3');
+var negateIf = require('./negate_if');
 
 exports.assertDims = function(dims) {
     var traces = d3.selectAll('.trace');
@@ -122,8 +123,7 @@ exports.assertHoverLabelContent = function(expectation, msg) {
         assertLabelContent(nameSel, expectation.name, ptMsg + ' (name)');
 
         if('isRotated' in expectation) {
-            expect(g.attr('transform').match(reRotate))
-                .negateIf(expectation.isRotated)
+            negateIf(expectation.isRotated, expect(g.attr('transform').match(reRotate)))
                 .toBe(null, ptMsg + ' should be rotated');
         }
     } else if(ptCnt > 1) {
@@ -162,8 +162,7 @@ exports.assertHoverLabelContent = function(expectation, msg) {
             });
 
             if('isRotated' in expectation) {
-                expect(g.attr('transform').match(reRotate))
-                    .negateIf(expectation.isRotated)
+                negateIf(expectation.isRotated, expect(g.attr('transform').match(reRotate)))
                     .toBe(null, ptMsg + ' ' + i + ' should be rotated');
             }
         });
diff --git a/test/jasmine/assets/custom_matchers.js b/test/jasmine/assets/custom_matchers.js
index f1d877d8d51..515e77b741d 100644
--- a/test/jasmine/assets/custom_matchers.js
+++ b/test/jasmine/assets/custom_matchers.js
@@ -1,15 +1,6 @@
 /*
  * custom_matchers - to be included in karma.conf.js, so it can
  * add these matchers to jasmine globally and all suites have access.
- *
- * Also adds `.negateIf` which is not a matcher but a conditional `.not`:
- *
- *     expect(x).negateIf(condition).toBe(0);
- *
- * is equivalent to:
- *
- *     if(condition) expect(x).toBe(0);
- *     else expect(x).not.toBe(0);
  */
 
 'use strict';
@@ -241,9 +232,4 @@ function arrayToStr(array) {
 
 beforeAll(function() {
     jasmine.addMatchers(matchers);
-
-    jasmine.Expectation.prototype.negateIf = function(negate) {
-        if(negate) return this.not;
-        return this;
-    };
 });
diff --git a/test/jasmine/assets/negate_if.js b/test/jasmine/assets/negate_if.js
new file mode 100644
index 00000000000..dab8aadb898
--- /dev/null
+++ b/test/jasmine/assets/negate_if.js
@@ -0,0 +1,21 @@
+'use strict';
+
+/**
+ * Helpers that can negate an expectation given a condition
+ *
+ * @param {boolean OR function} condition
+ * @param {jasmine expect return} expectation
+ * @returns {jasmine expect return}
+ *
+ * Example:
+ *
+ * negateIf(myCondition, expect(actual)).toBe(expected);
+ *
+ */
+function negateIf(condition, expectation) {
+    return (typeof condition === 'function' ? condition() : condition) ?
+        expectation.not :
+        expectation;
+}
+
+module.exports = negateIf;
diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js
index 1f7da995b80..8a045467271 100644
--- a/test/jasmine/karma.conf.js
+++ b/test/jasmine/karma.conf.js
@@ -4,10 +4,15 @@ var path = require('path');
 var minimist = require('minimist');
 var constants = require('../../tasks/util/constants');
 
-var isCI = !!process.env.CI;
+var isCI = Boolean(process.env.CI);
+
 var argv = minimist(process.argv.slice(4), {
     string: ['bundleTest', 'width', 'height'],
-    'boolean': ['info', 'nowatch', 'failFast', 'verbose', 'Chrome', 'Firefox', 'IE11'],
+    'boolean': [
+        'info',
+        'nowatch', 'failFast', 'verbose', 'randomize',
+        'Chrome', 'Firefox', 'IE11'
+    ],
     alias: {
         'Chrome': 'chrome',
         'Firefox': ['firefox', 'FF'],
@@ -21,6 +26,7 @@ var argv = minimist(process.argv.slice(4), {
         nowatch: isCI,
         failFast: false,
         verbose: false,
+        randomize: false,
         width: '1035',
         height: '617'
     }
@@ -60,6 +66,7 @@ if(argv.info) {
         '  - `--failFast` (dflt: `false`): exit karma upon first test failure',
         '  - `--verbose` (dflt: `false`): show test result using verbose reporter',
         '  - `--showSkipped` (dflt: `false`): show tests that are skipped',
+        '  - `--randomize` (dflt: `false`): randomize test ordering (useful to detect bad test teardown)',
         '  - `--tags`: run only test with given tags (using the `jasmine-spec-tags` framework)',
         '  - `--width`(dflt: 1035): set width of the browser window',
         '  - `--height` (dflt: 617): set height of the browser window',
@@ -113,7 +120,6 @@ var pathToUnpolyfill = path.join(__dirname, 'assets', 'unpolyfill.js');
 var pathToMathJax = path.join(constants.pathToDist, 'extras', 'mathjax');
 
 var reporters = ((isFullSuite && !argv.tags) || argv.showSkipped) ? ['dots', 'spec'] : ['progress'];
-if(argv.failFast) reporters.push('fail-fast');
 if(argv.verbose) reporters.push('verbose');
 
 function func(config) {
@@ -224,27 +230,33 @@ func.defaultConfig = {
         debug: true
     },
 
-    // Options for `karma-jasmine-spec-tags`
-    // see https://www.npmjs.com/package/karma-jasmine-spec-tags
-    //
-    // A few tests don't behave well on CI
-    // add @noCI to the spec description to skip a spec on CI
-    //
-    // Although not recommended, some tests "depend" on other
-    // tests to pass (e.g. the Plotly.react tests check that
-    // all available traces and transforms are tested). Tag these
-    // with @noCIdep, so that
-    // - $ npm run test-jasmine -- tags=noCI,noCIdep
-    // can pass.
-    //
-    // Label tests that require a WebGL-context by @gl so that
-    // they can be skipped using:
-    // - $ npm run test-jasmine -- --skip-tags=gl
-    // or run is isolation easily using:
-    // - $ npm run test-jasmine -- --tags=gl
     client: {
+        // Options for `karma-jasmine-spec-tags`
+        // see https://www.npmjs.com/package/karma-jasmine-spec-tags
+        //
+        // A few tests don't behave well on CI
+        // add @noCI to the spec description to skip a spec on CI
+        //
+        // Although not recommended, some tests "depend" on other
+        // tests to pass (e.g. the Plotly.react tests check that
+        // all available traces and transforms are tested). Tag these
+        // with @noCIdep, so that
+        // - $ npm run test-jasmine -- tags=noCI,noCIdep
+        // can pass.
+        //
+        // Label tests that require a WebGL-context by @gl so that
+        // they can be skipped using:
+        // - $ npm run test-jasmine -- --skip-tags=gl
+        // or run is isolation easily using:
+        // - $ npm run test-jasmine -- --tags=gl
         tagPrefix: '@',
-        skipTags: isCI ? 'noCI' : null
+        skipTags: isCI ? 'noCI' : null,
+
+        // See https://jasmine.github.io/api/3.4/Configuration.html
+        jasmine: {
+            random: argv.randomize,
+            failFast: argv.failFast
+        }
     },
 
     // use 'karma-spec-reporter' to log info about skipped specs
@@ -253,14 +265,8 @@ func.defaultConfig = {
         suppressFailed: true,
         suppressPassed: true,
         suppressSkipped: false,
-        showSpecTiming: false,
-        // use 'karma-fail-fast-reporter' to fail fast w/o conflicting
-        // with other karma plugins
-        failFast: false
-    },
-
-    // e.g. when a test file does not container a given spec tags
-    failOnEmptyTestSuite: false
+        showSpecTiming: false
+    }
 };
 
 func.defaultConfig.preprocessors[pathToCustomMatchers] = ['browserify'];
diff --git a/test/jasmine/tests/bar_test.js b/test/jasmine/tests/bar_test.js
index b81ce4cc08c..9a108e5644a 100644
--- a/test/jasmine/tests/bar_test.js
+++ b/test/jasmine/tests/bar_test.js
@@ -12,6 +12,7 @@ var DBLCLICKDELAY = require('../../../src/constants/interactions').DBLCLICKDELAY
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
 var failTest = require('../assets/fail_test');
+var negateIf = require('../assets/negate_if');
 var checkTicks = require('../assets/custom_assertions').checkTicks;
 var supplyAllDefaults = require('../assets/supply_defaults');
 var color = require('../../../src/components/color');
@@ -1759,7 +1760,7 @@ describe('A bar plot', function() {
                 if(!i) return;
                 var bbox = this.getBoundingClientRect();
                 ['left', 'right', 'top', 'bottom', 'width', 'height'].forEach(function(dim) {
-                    expect(bbox[dim]).negateIf(dims.indexOf(dim) === -1)
+                    negateIf(dims.indexOf(dim) === -1, expect(bbox[dim]))
                         .toBeWithin(bbox1[dim], 0.1, msg + ' (' + i + '): ' + dim);
                 });
             });
diff --git a/test/jasmine/tests/colorbar_test.js b/test/jasmine/tests/colorbar_test.js
index 33091d779ee..ac11638f43c 100644
--- a/test/jasmine/tests/colorbar_test.js
+++ b/test/jasmine/tests/colorbar_test.js
@@ -8,6 +8,7 @@ var subroutines = require('@src/plot_api/subroutines');
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
 var failTest = require('../assets/fail_test');
+var negateIf = require('../assets/negate_if');
 var supplyAllDefaults = require('../assets/supply_defaults');
 var assertPlotSize = require('../assets/custom_assertions').assertPlotSize;
 var drag = require('../assets/drag');
@@ -119,7 +120,7 @@ describe('Test colorbar:', function() {
                 var cbbg = colorbars.selectAll('.cbbg');
                 var cbfills = colorbars.selectAll('.cbfill');
 
-                expect(cbfills.size()).negateIf(multiFill).toBe(1);
+                negateIf(multiFill, expect(cbfills.size())).toBe(1);
 
                 if(!cbHeight) cbHeight = 400;
                 var bgHeight = +cbbg.attr('height');
diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js
index 5253c43b6b7..0ecc00250ef 100644
--- a/test/jasmine/tests/geo_test.js
+++ b/test/jasmine/tests/geo_test.js
@@ -11,6 +11,7 @@ var d3 = require('d3');
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
 var failTest = require('../assets/fail_test');
+var negateIf = require('../assets/negate_if');
 var getClientPosition = require('../assets/get_client_position');
 var mouseEvent = require('../assets/mouse_event');
 var click = require('../assets/click');
@@ -1571,8 +1572,8 @@ describe('Test geo base layers', function() {
             var cd0 = gd.calcdata[0];
             var subplot = gd._fullLayout.geo._subplot;
 
-            expect(cd0[0].geojson).negateIf(geojson[0]).toBe(null);
-            expect(cd0[1].geojson).negateIf(geojson[1]).toBe(null);
+            negateIf(geojson[0], expect(cd0[0].geojson)).toBe(null);
+            negateIf(geojson[1], expect(cd0[1].geojson)).toBe(null);
 
             expect(Object.keys(subplot.layers).length).toEqual(layers.length, '# of layers');
 
diff --git a/test/jasmine/tests/plot_api_test.js b/test/jasmine/tests/plot_api_test.js
index 3df45e4af6c..885fbba5cc2 100644
--- a/test/jasmine/tests/plot_api_test.js
+++ b/test/jasmine/tests/plot_api_test.js
@@ -17,6 +17,7 @@ var d3 = require('d3');
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
 var failTest = require('../assets/fail_test');
+var negateIf = require('../assets/negate_if');
 var checkTicks = require('../assets/custom_assertions').checkTicks;
 var supplyAllDefaults = require('../assets/supply_defaults');
 
@@ -1110,9 +1111,9 @@ describe('Test plot api', function() {
             var zmax1 = 10;
 
             function check(auto, msg) {
-                expect(gd._fullData[0].zmin).negateIf(auto).toBe(zmin0, msg);
+                negateIf(auto, expect(gd._fullData[0].zmin)).toBe(zmin0, msg);
                 expect(gd._fullData[0].zauto).toBe(auto, msg);
-                expect(gd._fullData[1].zmax).negateIf(auto).toBe(zmax1, msg);
+                negateIf(auto, expect(gd._fullData[1].zmax)).toBe(zmax1, msg);
                 expect(gd._fullData[1].zauto).toBe(auto, msg);
             }
 
@@ -1153,12 +1154,12 @@ describe('Test plot api', function() {
 
             function check(auto, autocolorscale, msg) {
                 expect(gd._fullData[0].marker.cauto).toBe(auto, msg);
-                expect(gd._fullData[0].marker.cmin).negateIf(auto).toBe(mcmin0);
+                negateIf(auto, expect(gd._fullData[0].marker.cmin)).toBe(mcmin0);
                 expect(gd._fullData[0].marker.autocolorscale).toBe(autocolorscale, msg);
                 expect(gd._fullData[0].marker.colorscale).toEqual(auto ? autocscale : scales[mcscl0]);
 
                 expect(gd._fullData[1].marker.line.cauto).toBe(auto, msg);
-                expect(gd._fullData[1].marker.line.cmax).negateIf(auto).toBe(mlcmax1);
+                negateIf(auto, expect(gd._fullData[1].marker.line.cmax)).toBe(mlcmax1);
                 expect(gd._fullData[1].marker.line.autocolorscale).toBe(autocolorscale, msg);
                 expect(gd._fullData[1].marker.line.colorscale).toEqual(auto ? autocscale : scales[mlcscl1]);
             }
@@ -1323,8 +1324,8 @@ describe('Test plot api', function() {
             function check(auto, msg) {
                 expect(gd.data[0].autocontour).toBe(auto, msg);
                 expect(gd.data[1].autocontour).toBe(auto, msg);
-                expect(gd.data[0].contours.start).negateIf(auto).toBe(start0, msg);
-                expect(gd.data[1].contours.size).negateIf(auto).toBe(size1, msg);
+                negateIf(auto, expect(gd.data[0].contours.start)).toBe(start0, msg);
+                negateIf(auto, expect(gd.data[1].contours.size)).toBe(size1, msg);
             }
 
             Plotly.plot(gd, [
@@ -1411,9 +1412,9 @@ describe('Test plot api', function() {
             var dtick1 = 0.8;
 
             function check(auto, msg) {
-                expect(gd._fullData[0].colorbar.tick0).negateIf(auto).toBe(tick00, msg);
+                negateIf(auto, expect(gd._fullData[0].colorbar.tick0)).toBe(tick00, msg);
                 expect(gd._fullData[0].colorbar.tickmode).toBe(auto ? 'auto' : 'linear', msg);
-                expect(gd._fullData[1].colorbar.dtick).negateIf(auto).toBe(dtick1, msg);
+                negateIf(auto, expect(gd._fullData[1].colorbar.dtick)).toBe(dtick1, msg);
                 expect(gd._fullData[1].colorbar.tickmode).toBe(auto ? 'auto' : 'linear', msg);
             }
 
diff --git a/test/jasmine/tests/polar_test.js b/test/jasmine/tests/polar_test.js
index e9ada238e78..e087f2495e4 100644
--- a/test/jasmine/tests/polar_test.js
+++ b/test/jasmine/tests/polar_test.js
@@ -7,6 +7,7 @@ var d3 = require('d3');
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
 var failTest = require('../assets/fail_test');
+var negateIf = require('../assets/negate_if');
 var mouseEvent = require('../assets/mouse_event');
 var click = require('../assets/click');
 var doubleClick = require('../assets/double_click');
@@ -460,7 +461,7 @@ describe('Test relayout on polar subplots:', function() {
                 expect(txt.text()).toBe(content, 'radial axis title');
             }
 
-            expect(newBBox).negateIf(didBBoxChanged).toEqual(lastBBox, 'did bbox change');
+            negateIf(didBBoxChanged, expect(newBBox)).toEqual(lastBBox, 'did bbox change');
             lastBBox = newBBox;
         }
 
diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js
index 7643125f063..a20945e303d 100644
--- a/test/jasmine/tests/scatter_test.js
+++ b/test/jasmine/tests/scatter_test.js
@@ -8,6 +8,7 @@ var Plotly = require('@lib/index');
 var createGraphDiv = require('../assets/create_graph_div');
 var destroyGraphDiv = require('../assets/destroy_graph_div');
 var customAssertions = require('../assets/custom_assertions');
+var negateIf = require('../assets/negate_if');
 var failTest = require('../assets/fail_test');
 var transitions = require('../assets/transitions');
 
@@ -952,7 +953,7 @@ describe('end-to-end scatter tests', function() {
         function checkFill(visible, msg) {
             var fillSelection = d3.select(gd).selectAll('.scatterlayer .js-fill');
             expect(fillSelection.size()).toBe(1, msg);
-            expect(fillSelection.attr('d')).negateIf(visible).toBe('M0,0Z', msg);
+            negateIf(visible, expect(fillSelection.attr('d'))).toBe('M0,0Z', msg);
         }
 
         Plotly.newPlot(gd, [trace0, trace1, trace2], {}, {scrollZoom: true})