@testim/testim-cli 3.241.0 → 3.244.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testim/testim-cli",
3
- "version": "3.241.0",
3
+ "version": "3.244.0",
4
4
  "description": "Command line interface for running Testing on your CI",
5
5
  "author": "Oren Rubin",
6
6
  "contributors": [{
@@ -47,8 +47,8 @@
47
47
  "multer": "1.4.4"
48
48
  },
49
49
  "dependencies": {
50
- "@applitools/eyes-sdk-core": "13.2.0",
51
- "@applitools/visual-grid-client": "15.11.0",
50
+ "@applitools/eyes-sdk-core": "13.6.23",
51
+ "@applitools/visual-grid-client": "15.12.34",
52
52
  "@testim/coralogix-logger": "1.1.27-beta.1",
53
53
  "@testim/webdriverio": "0.0.5",
54
54
  "abort-controller": "3.0.0",
@@ -116,7 +116,7 @@
116
116
  "test": "IS_UNIT_TEST=1 ../../node_modules/mocha/bin/_mocha --timeout 2000 --reporter spec --exit --recursive \"./src/**/*.test.js\" --exclude ./src/codim/template.js/tests/examples/**/*.test.js",
117
117
  "test:watch": "IS_UNIT_TEST=1 ../../node_modules/mocha/bin/_mocha --timeout 2000 --exit --recursive \"./src/**/*.test.js\" --exclude ./src/codim/template.js/tests/examples/**/*.test.js --watch",
118
118
  "test:cov": "nyc --reporter=lcov --reporter=text yarn test",
119
- "upload-bundle-s3": "scripts/upload.js",
119
+ "upload-bundle-s3": "ts-node-transpile-only scripts/upload.js",
120
120
  "prepare-version": "rm -rf ./deploy && mkdir -p deploy && gulp prepare-version-on-prem",
121
121
  "make-onprem-deps": "cp ../../yarn.lock deploy && cd deploy && yarn install --production && bundle-deps",
122
122
  "pack-onprem": "yarn prepare-version && yarn make-onprem-deps && cd deploy && npm pack && zip -r testim-cli.zip testim-cli-*.tgz",
@@ -1,12 +1,11 @@
1
- "use strict";
2
- var COMMUNICATION_BUFFER_TIME = 1000;
1
+ 'use strict';
2
+
3
3
  const _ = require('lodash');
4
- const logger = require('../../commons/logger').getLogger('playback-timeout-calculator');
5
4
 
6
- let warnedAboutDebugger = false;
5
+ const COMMUNICATION_BUFFER_TIME = 1000;
6
+ const UI_VERIFICATION_STEPS = ['simple-ui-verification', 'wait-for-simple-ui-verification'];
7
7
 
8
8
  class PlaybackTimeoutCalculator {
9
-
10
9
  constructor(isDebuggerConnected) {
11
10
  this.resetStepVariables();
12
11
  this.resetRetryVariables();
@@ -18,13 +17,13 @@ class PlaybackTimeoutCalculator {
18
17
  this.totalStepTime = totalStepTime || 0;
19
18
  this.totalStepTimesReport = [];
20
19
  this.currentRetryTimesReport = {};
21
- var now = Date.now();
20
+ const now = Date.now();
22
21
  this.currentRetryStart = now;
23
22
  this.lastUpdateTime = now;
24
23
  }
25
24
 
26
25
  resetRetryVariables() {
27
- var now = Date.now();
26
+ const now = Date.now();
28
27
  this.currentRetryStart = now;
29
28
  this.lastUpdateTime = now;
30
29
  this.totalStepTimesReport.push(this.currentRetryTimesReport);
@@ -32,7 +31,7 @@ class PlaybackTimeoutCalculator {
32
31
  }
33
32
 
34
33
  initStepRun(stepPlayback) {
35
- var getRetryTimeoutSuggestions = (totalStepTime) => {
34
+ const getRetryTimeoutSuggestions = (totalStepTime) => {
36
35
  const timeToPlayStep = this.getTotalStepTimeLeftToPlay(stepPlayback, totalStepTime);
37
36
  const MINIMAL_RETRY_TIME = 5000;
38
37
  if (timeToPlayStep <= MINIMAL_RETRY_TIME) {
@@ -42,7 +41,7 @@ class PlaybackTimeoutCalculator {
42
41
  };
43
42
  stepPlayback.setStartTimestamp();
44
43
  const totalStepTime = this.getTotalStepRunTime(stepPlayback);
45
- var currentRetryTimes = ["simple-ui-verification", "wait-for-simple-ui-verification", "custom-validation"].includes(stepPlayback.stepType) ? [totalStepTime] : getRetryTimeoutSuggestions(totalStepTime);
44
+ const currentRetryTimes = [...UI_VERIFICATION_STEPS, 'custom-validation'].includes(stepPlayback.stepType) ? [totalStepTime] : getRetryTimeoutSuggestions(totalStepTime);
46
45
  this.resetStepVariables(totalStepTime, currentRetryTimes);
47
46
  stepPlayback.context.data.maxTotalStepTime = totalStepTime;
48
47
  }
@@ -57,9 +56,12 @@ class PlaybackTimeoutCalculator {
57
56
  }
58
57
 
59
58
  getTotalStepRunTime(stepPlayback) {
60
- return (stepPlayback.step.useStepTimeout && stepPlayback.step.stepTimeout) ?
61
- stepPlayback.step.stepTimeout :
62
- stepPlayback.context.config.stepTimeout;
59
+ const HALF_HOUR_IN_MS = 30 * 60 * 1000;
60
+ let fallbackTimeout = stepPlayback.context.config.stepTimeout;
61
+ if (UI_VERIFICATION_STEPS.includes(stepPlayback.stepType)) {
62
+ fallbackTimeout = stepPlayback.context.config.applitoolsStepTimeout || HALF_HOUR_IN_MS;
63
+ }
64
+ return (stepPlayback.step.useStepTimeout && stepPlayback.step.stepTimeout) ? stepPlayback.step.stepTimeout : fallbackTimeout;
63
65
  }
64
66
  getTotalStepTimeLeftToPlay(stepPlayback, totalStepTime = this.totalStepTime) {
65
67
  const playTimeSoFar = Date.now() - stepPlayback.startTimestamp;
@@ -71,7 +73,7 @@ class PlaybackTimeoutCalculator {
71
73
  this.getTotalStepTimeLeftToPlay(stepPlayback);
72
74
  }
73
75
  getTotalCurrentRetryTimeLeft(stepPlayback) {
74
- var totalRetryTime = Date.now() - this.currentRetryStart;
76
+ const totalRetryTime = Date.now() - this.currentRetryStart;
75
77
  return this.getCurrentRetryTime(stepPlayback) - totalRetryTime + COMMUNICATION_BUFFER_TIME;
76
78
  }
77
79
  getTabTimeout(stepPlayback) {
@@ -97,7 +99,6 @@ class PlaybackTimeoutCalculator {
97
99
  }
98
100
 
99
101
  return (stepPlayback.step.events.length * timePerEvent) + buffer;
100
-
101
102
  }
102
103
 
103
104
  getActionTimeout(stepPlayback) {
@@ -108,10 +109,10 @@ class PlaybackTimeoutCalculator {
108
109
  const actionType = stepPlayback.step.type;
109
110
  const MIN_ACTION_PLAYBACK_TIME = 30000;
110
111
 
111
- var actionTime;
112
- if (actionType === "sleep") {
112
+ let actionTime;
113
+ if (actionType === 'sleep') {
113
114
  actionTime = stepPlayback.step.durationMS + SLEEP_ERROR_MARGIN_MS;
114
- } else if (actionType === "android-scroll") {
115
+ } else if (actionType === 'android-scroll') {
115
116
  actionTime = Math.max(this.calcAndroidScrollTimeout(stepPlayback), MIN_ACTION_PLAYBACK_TIME);
116
117
  } else {
117
118
  actionTime = Math.max(this.getTotalStepTimeLeftToPlay(stepPlayback), MIN_ACTION_PLAYBACK_TIME);
@@ -120,34 +121,34 @@ class PlaybackTimeoutCalculator {
120
121
  }
121
122
 
122
123
  setStepPhaseTime(phase) {
123
- var now = Date.now();
124
- var totalTime = now - this.lastUpdateTime;
124
+ const now = Date.now();
125
+ const totalTime = now - this.lastUpdateTime;
125
126
  this.lastUpdateTime = now;
126
127
  this.currentRetryTimesReport[phase] = totalTime;
127
128
  }
128
129
 
129
130
  reportGetTabTime() {
130
- this.setStepPhaseTime("tab");
131
+ this.setStepPhaseTime('tab');
131
132
  }
132
133
 
133
134
  reportGetFrameTime() {
134
- this.setStepPhaseTime("frame");
135
+ this.setStepPhaseTime('frame');
135
136
  }
136
137
 
137
138
  reportCalcConditionTime() {
138
- this.setStepPhaseTime("condition");
139
+ this.setStepPhaseTime('condition');
139
140
  }
140
141
 
141
142
  reportPreLocateActionsTime() {
142
- this.setStepPhaseTime("pre-locate");
143
+ this.setStepPhaseTime('pre-locate');
143
144
  }
144
145
 
145
146
  reportFindElementsTime() {
146
- this.setStepPhaseTime("locate");
147
+ this.setStepPhaseTime('locate');
147
148
  }
148
149
 
149
150
  reportStepActionTime() {
150
- this.setStepPhaseTime("action");
151
+ this.setStepPhaseTime('action');
151
152
  }
152
153
  }
153
154
 
@@ -1,14 +1,15 @@
1
- "use strict";
1
+ 'use strict';
2
+
2
3
  const Promise = require('bluebird');
3
4
  const sessionPlayer = require('../../commons/getSessionPlayerRequire');
4
5
  const StepAction = require('./stepAction');
6
+
5
7
  const constants = sessionPlayer.commonConstants.stepResult;
6
8
  const { stepParamBuilder } = sessionPlayer;
7
9
  const logger = require('../../commons/logger').getLogger('evaluate-expression-step-action');
8
10
  const _ = require('lodash');
9
11
 
10
12
  class EvaluateExpressionStepAction extends StepAction {
11
-
12
13
  execute() {
13
14
  const step = this.step;
14
15
  const context = this.context;
@@ -25,8 +26,8 @@ class EvaluateExpressionStepAction extends StepAction {
25
26
 
26
27
  const params = ['context', ...incomingParams.as.functionParameters];
27
28
  const args = [context, ...incomingParams.as.functionArguments];
28
- const expressionToEvaluate = step.subType === "text" ? "'" + step.expression.replace(/'/g, "\\\'") + "'" : step.expression;
29
- const code = ("return " + expressionToEvaluate).replace(/\n/g, "\\n");
29
+ const expressionToEvaluate = step.subType === 'text' ? `'${step.expression.replace(/'/g, "\\\'")}'` : step.expression;
30
+ const code = (`return ${expressionToEvaluate}`).replace(/\n/g, '\\n');
30
31
  const textEvaluateFunction = Function.apply(Function, params.concat([code]));
31
32
  const evaluatedText = textEvaluateFunction.apply(null, args);
32
33
 
@@ -38,8 +39,8 @@ class EvaluateExpressionStepAction extends StepAction {
38
39
 
39
40
  const result = {
40
41
  success: true,
41
- evaluatedText: evaluatedText,
42
- data: context.data
42
+ evaluatedText,
43
+ data: context.data,
43
44
  };
44
45
 
45
46
  resolve(result);
@@ -48,7 +49,6 @@ class EvaluateExpressionStepAction extends StepAction {
48
49
  }
49
50
  });
50
51
  }
51
-
52
52
  }
53
53
 
54
54
  module.exports = EvaluateExpressionStepAction;
@@ -1,4 +1,6 @@
1
- "use strict";
1
+ /* globals getLocatedElement, dispatchFocus */
2
+
3
+ 'use strict';
2
4
 
3
5
  var doClick = function (eventData, done) {
4
6
  var eventConstructorSupported = typeof Event === 'function';
@@ -1,4 +1,6 @@
1
- "use strict";
1
+ /* global getLocatedElement */
2
+
3
+ 'use strict';
2
4
 
3
5
  var html5dndAction = function(eventData) {
4
6
  var data = {};
@@ -1,4 +1,6 @@
1
- "use strict";
1
+ /* global getLocatedElement */
2
+
3
+ 'use strict';
2
4
 
3
5
  var html5dndAction = function (eventData, done) {
4
6
  var mouseEventConstructorSupported = typeof MouseEvent === 'function';
@@ -3,23 +3,40 @@
3
3
  'use strict';
4
4
 
5
5
  const runCode = function (eventData, preCompiledCode) {
6
+ typeof Object.tstassign !== 'function' && (Object.tstassign = function (n, t) {
7
+ 'use strict';
6
8
 
7
- "function"!=typeof Object.tstassign&&(Object.tstassign=function(n,t){"use strict";if(null==n)throw new TypeError("Cannot convert undefined or null to object");for(var r=Object(n),e=1;e<arguments.length;e++){var o=arguments[e];if(null!=o)for(var a in o)Object.prototype.hasOwnProperty.call(o,a)&&(r[a]=o[a])}return r});
8
- Object.assign = typeof Object.assign !== "function" ? Object.tstassign : Object.assign;
9
+ if (n == null) throw new TypeError('Cannot convert undefined or null to object'); for (var r = Object(n), e = 1; e < arguments.length; e++) { var o = arguments[e]; if (o != null) for (var a in o)Object.prototype.hasOwnProperty.call(o, a) && (r[a] = o[a]); } return r;
10
+ });
11
+ Object.assign = typeof Object.assign !== 'function' ? Object.tstassign : Object.assign;
9
12
 
10
13
  function appendToStorage(name, data) {
11
14
  const sessionItem = 'data-testim-' + name;
15
+
16
+ const nativeFuncErrMsg = 'Native sessionStorage is not available';
17
+ function isNativeFunction(fn) {
18
+ if (!fn || !fn.toString) {
19
+ return false;
20
+ }
21
+ return fn.toString().indexOf('[native code]') > -1;
22
+ }
12
23
  try {
24
+ if (![window.sessionStorage.setItem, window.sessionStorage.getItem].every(isNativeFunction)) {
25
+ throw new Error(nativeFuncErrMsg);
26
+ }
13
27
  const oldData = JSON.parse(window.sessionStorage.getItem(sessionItem) || '{}');
14
28
  const newData = Object.tstassign({}, oldData, data);
15
29
  window.sessionStorage.setItem(sessionItem, JSON.stringify(newData));
16
30
  } catch (err) {
17
- // any vernation QuotaExceededError from browsers
31
+ // any variation QuotaExceededError from browsers
18
32
  const isQuotaExceededError = err.message.toLowerCase().indexOf('quota') > -1;
33
+ const isNativeFunctionError = err.message === nativeFuncErrMsg;
34
+
19
35
  if (err.message.indexOf('sessionStorage') > -1 || // Chrome + Firefox
20
36
  err.message.indexOf('The operation is insecure') > -1 || // Safari
21
37
  err.message.indexOf('SecurityError') > -1 || // Edge
22
- isQuotaExceededError
38
+ isQuotaExceededError ||
39
+ isNativeFunctionError
23
40
  ) {
24
41
  var storage = document.head.querySelector('#testim-storage-backup');
25
42
  if (!storage) {
@@ -30,13 +47,13 @@ const runCode = function (eventData, preCompiledCode) {
30
47
  const oldData = JSON.parse(storage.getAttribute(sessionItem) || '{}');
31
48
  const newData = Object.tstassign({}, oldData, data);
32
49
  storage.setAttribute(sessionItem, JSON.stringify(newData));
33
- if (isQuotaExceededError) {
50
+ if (isQuotaExceededError || isNativeFunctionError) {
34
51
  try {
35
52
  window.sessionStorage.removeItem(sessionItem);
36
53
  } catch (e) {
37
54
  // Prevents future retries from looking in sessionStorage with old data
38
55
  }
39
- window.TSTA.useFallbackStorage = true;
56
+ (window.TSTA = window.TSTA || {}).useFallbackStorage = true;
40
57
  }
41
58
  return;
42
59
  }
@@ -1,9 +1,4 @@
1
- // Disabling the eslint to keep this in a format which works on IE 11.
2
- /* eslint-disable no-var */
3
- /* eslint-disable no-redeclare */
4
- /* eslint-disable object-shorthand */
5
- /* eslint-disable one-var */
6
- /* eslint-disable operator-linebreak */
1
+ /* globals getLocatedElement, elementScrollTo */
7
2
 
8
3
  'use strict';
9
4
 
@@ -1,4 +1,7 @@
1
- "use strict";
1
+ /* globals getLocatedElement, dispatchFocus */
2
+
3
+ 'use strict';
4
+
2
5
  /*global KeyboardEvent */
3
6
  var setText = function (eventData, done) {
4
7
  var eventConstructorSupported = typeof Event === 'function';
package/processHandler.js CHANGED
@@ -2,18 +2,18 @@
2
2
 
3
3
  'use strict';
4
4
 
5
+ const Promise = require('bluebird');
5
6
  const logger = require('./commons/logger').getLogger('process-handler');
6
7
 
7
8
  const exitHooks = [];
8
- const Promise = require('bluebird');
9
9
 
10
- module.exports = function (onExit) {
10
+ module.exports = function (onExit, _process = process) {
11
11
  async function cleanup(err) {
12
12
  // give cleanup and socket reports a chance to run
13
13
  await Promise.all(exitHooks.map(x => x())).timeout(10000).catch(() => {});
14
14
  onExit(err);
15
15
  }
16
- process.on('uncaughtException', async (err) => {
16
+ _process.on('uncaughtException', async (err) => {
17
17
  logger.error('Caught exception', { err });
18
18
  console.log('Uncaught exception');
19
19
  if (err.message) {
@@ -25,7 +25,7 @@ module.exports = function (onExit) {
25
25
  await cleanup(err);
26
26
  });
27
27
 
28
- process.on('unhandledRejection', (reason) => {
28
+ _process.on('unhandledRejection', (reason) => {
29
29
  // rollout manages promises incorrectly and generates unhandled rejections from within their code
30
30
  logger.fatal('Caught unhandled promise rejection', reason);
31
31
  //TODO(benji) - this is a pretty shitty way to detect this error since rollout can change their API endpoint
@@ -37,11 +37,11 @@ module.exports = function (onExit) {
37
37
  throw reason;
38
38
  });
39
39
 
40
- process.on('rejectionHandled', () => {
40
+ _process.on('rejectionHandled', () => {
41
41
  logger.error('Caught rejection handled');
42
42
  });
43
43
 
44
- process.once('SIGTERM', () => {
44
+ _process.once('SIGTERM', () => {
45
45
  const msg = 'Runner aborted - SIGTERM event';
46
46
  const err = new Error(msg);
47
47
  logger.error(msg);
@@ -49,7 +49,7 @@ module.exports = function (onExit) {
49
49
  throw err;
50
50
  });
51
51
 
52
- process.once('SIGINT', () => {
52
+ _process.once('SIGINT', () => {
53
53
  const msg = 'Runner aborted - SIGINT event';
54
54
  const err = new Error(msg);
55
55
  logger.error(msg);
@@ -58,12 +58,15 @@ module.exports = function (onExit) {
58
58
  });
59
59
 
60
60
  // One time self-call is expected :(
61
- process.once('exit', (e) => {
61
+ _process.once('exit', (e) => {
62
62
  onExit(e);
63
63
  });
64
64
  };
65
65
 
66
-
67
66
  module.exports.registerExitHook = function (hook) {
68
67
  exitHooks.push(hook);
69
68
  };
69
+
70
+ module.exports.reset = function () {
71
+ exitHooks.splice(0, exitHooks.length);
72
+ };
@@ -0,0 +1,56 @@
1
+
2
+ const { delay } = require('bluebird');
3
+ const EventEmitter = require('events');
4
+ const { expect, sinon } = require('../test/utils/testUtils');
5
+ const processHandler = require('./processHandler');
6
+
7
+
8
+ class Process extends EventEmitter {
9
+ constructor() {
10
+ super();
11
+ this.stdout = new EventEmitter();
12
+ this.stderr = new EventEmitter();
13
+ }
14
+ }
15
+
16
+ describe('testimTunnel', () => {
17
+ let process;
18
+ let onExitMock;
19
+
20
+ beforeEach(() => {
21
+ process = new Process();
22
+ onExitMock = sinon.spy();
23
+ processHandler(onExitMock, process);
24
+ });
25
+
26
+ afterEach(async () => {
27
+ await delay(10);
28
+ expect(onExitMock).to.have.been.calledOnce;
29
+ processHandler.reset();
30
+ });
31
+
32
+
33
+ it('should register a SIGTERM handler', (done) => {
34
+ processHandler.registerExitHook(done);
35
+ expect(() => process.emit('SIGTERM')).to.throw('Runner aborted - SIGTERM event');
36
+ });
37
+ it('should register a SIGINT handler', (done) => {
38
+ processHandler.registerExitHook(done);
39
+ expect(() => process.emit('SIGINT')).to.throw('Runner aborted - SIGINT event');
40
+ });
41
+ it('should register a unhandledRejection handler', () => {
42
+ expect(() => process.emit('unhandledRejection', new Error('reason'))).to.throw('reason');
43
+ onExitMock();
44
+ });
45
+ it('should register a uncaughtException handler', (done) => {
46
+ processHandler.registerExitHook(done);
47
+ expect(() => process.emit('uncaughtException', new Error())).not.to.throw();
48
+ });
49
+ it('should do nothing on rejectionHandled', () => {
50
+ expect(() => process.emit('rejectionHandled')).not.to.throw();
51
+ onExitMock();
52
+ });
53
+ it('should register a exit handler', () => {
54
+ expect(() => process.emit('exit')).not.to.throw();
55
+ });
56
+ });
package/testRunStatus.js CHANGED
@@ -17,7 +17,6 @@ const { calculateCoverage } = require('./coverage/jsCoverage');
17
17
  const featureAvailabilityService = require('./commons/featureAvailabilityService');
18
18
  const featureFlags = require('./commons/featureFlags');
19
19
  const { mapFilesToLocalDrive } = require('./services/localRCASaver');
20
- const { removeHiddenParamsInTestData } = require('./commons/testimServicesApi');
21
20
 
22
21
  const gitBranch = utils.getEnvironmentGitBranch();
23
22
  const gitCommit = process.env.GIT_COMMIT || process.env.CIRCLE_SHA1 || process.env.TRAVIS_COMMIT;
@@ -125,13 +124,6 @@ RunStatus.prototype.addRetryTestResult = async function ({
125
124
 
126
125
  this.testRunStatus[newResultId] = newTestResult;
127
126
 
128
- const { projectData } = this.options;
129
-
130
-
131
- if (newTestResult.config.testData) {
132
- newTestResult.config.testData = removeHiddenParamsInTestData(newTestResult.config.testData, projectData.defaults);
133
- }
134
-
135
127
  return servicesApi.addTestRetry({
136
128
  projectId,
137
129
  runId: executionId,
package/utils.js CHANGED
@@ -71,20 +71,16 @@ function getRunConfigByBrowserName(browser, saucelabs, browserstack) {
71
71
  return _.merge(selectedBrowser, selectedOS);
72
72
  }
73
73
 
74
- function handleAngularURIComponent(part) {
75
- return part.replace(/(~|\/)/g, m => ({ '~': '~~', '/': '~2F' }[m]));
76
- }
77
-
78
74
  function getTestUrl(editorUrl, projectId, testId, resultId, branch) {
79
75
  let testUrl = '';
80
- branch = branch ? handleAngularURIComponent(branch) : 'master';
76
+ branch = branch ? encodeURIComponent(branch) : 'master';
81
77
  if (projectId && testId) {
82
78
  testUrl = `${editorUrl}/#/project/${projectId}/branch/${branch}/test/${testId}`;
83
79
  if (resultId) {
84
80
  testUrl += `?result-id=${resultId}`;
85
81
  }
86
82
  }
87
- return encodeURI(testUrl);
83
+ return testUrl;
88
84
  }
89
85
 
90
86
  function isPromise(obj) {
package/utils.test.js CHANGED
@@ -32,7 +32,7 @@ describe('utils', () => {
32
32
  expect(utils.getTestUrl('http://localhost:8080', 'project', 'test', 'result', 'normal-branch-name'))
33
33
  .to.equal('http://localhost:8080/#/project/project/branch/normal-branch-name/test/test?result-id=result');
34
34
  expect(utils.getTestUrl('http://localhost:8080', 'project', 'test', 'result', 'branch/with/slashes'))
35
- .to.equal('http://localhost:8080/#/project/project/branch/branch~2Fwith~2Fslashes/test/test?result-id=result');
35
+ .to.equal('http://localhost:8080/#/project/project/branch/branch%2Fwith%2Fslashes/test/test?result-id=result');
36
36
  expect(utils.getTestUrl('http://localhost:8080', 'project', 'test', 'result', 'branch with spaces'))
37
37
  .to.equal('http://localhost:8080/#/project/project/branch/branch%20with%20spaces/test/test?result-id=result');
38
38
  expect(utils.getTestUrl('http://localhost:8080', 'project', 'test', 'result', 'encoded%20branch'))