ui5-test-runner 4.0.0 → 4.1.1

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/README.md CHANGED
@@ -17,7 +17,6 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
17
17
 
18
18
  ## šŸ“š [Documentation](https://arnaudbuchholz.github.io/ui5-test-runner/)
19
19
 
20
-
21
20
  ## šŸ’æ How to install
22
21
 
23
22
  * Works with [Node.js](https://nodejs.org/en/download/) >= 18
@@ -30,20 +29,6 @@ A self-sufficient test runner for UI5 applications enabling parallel execution o
30
29
 
31
30
  **NOTE** : additional packages might be needed during the execution (`puppeteer`, `selenium-webdriver`, `nyc`...) . If they are found installed **locally** in the tested project, they are used. Otherwise, they are installed **globally**.
32
31
 
33
- ## šŸ–„ļø How to demo
34
-
35
- * Clone the project [training-ui5con18-opa](https://github.com/ArnaudBuchholz/training-ui5con18-opa) and run `npm install`
36
- * Use `npm run karma` to test with the karma runner
37
- * *Serving the application (a.k.a. legacy mode)*
38
- * `npx ui5-test-runner --port 8081 --ui5 https://ui5.sap.com/1.109.0/ --cache .ui5 --keep-alive`
39
- * Follow the progress of the test executions using http://localhost:8081/_/progress.html
40
- * When the tests are completed, check the code coverage with http://localhost:8081/_/coverage/lcov-report/index.html
41
- * *Serving the application with `@ui5/cli`*
42
- * Use `npm start` to serve the application with `@ui5/cli`
43
- * `npx ui5-test-runner --port 8081 --url http://localhost:8080/test/testsuite.qunit.html --keep-alive`
44
- * Follow the progress of the test executions using http://localhost:8081/_/progress.html
45
-
46
-
47
32
  ## āš–ļø License
48
33
  [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2FArnaudBuchholz%2Fui5-test-runner?ref=badge_large)
49
34
 
package/package.json CHANGED
@@ -1,13 +1,8 @@
1
1
  {
2
2
  "name": "ui5-test-runner",
3
- "version": "4.0.0",
3
+ "version": "4.1.1",
4
4
  "description": "Standalone test runner for UI5",
5
5
  "main": "index.js",
6
- "files": [
7
- "defaults/*",
8
- "src/**/*",
9
- "*.js"
10
- ],
11
6
  "bin": {
12
7
  "ui5-test-runner": "./index.js"
13
8
  },
@@ -74,16 +69,16 @@
74
69
  "reserve": "^1.15.6"
75
70
  },
76
71
  "devDependencies": {
77
- "@openui5/types": "^1.120.6",
78
- "@ui5/cli": "^3.9.0",
72
+ "@openui5/types": "^1.121.0",
73
+ "@ui5/cli": "^3.9.1",
79
74
  "@ui5/middleware-code-coverage": "^1.1.1",
80
75
  "jest": "^29.7.0",
81
- "nock": "^13.5.1",
76
+ "nock": "^13.5.3",
82
77
  "nyc": "^15.1.0",
83
78
  "standard": "^17.1.0",
84
79
  "start-server-and-test": "^2.0.3",
85
80
  "typescript": "^5.3.3",
86
- "ui5-tooling-transpile": "^3.3.3"
81
+ "ui5-tooling-transpile": "^3.3.4"
87
82
  },
88
83
  "optionalDependencies": {
89
84
  "fsevents": "^2.3.3"
package/src/coverage.js CHANGED
@@ -86,8 +86,31 @@ async function instrument (job) {
86
86
  await nyc(job, 'instrument', job.webapp, join(job.coverageTempDir, 'instrumented'), '--nycrc-path', job[$nycSettingsPath])
87
87
  }
88
88
 
89
+ async function getReadableSource (job, pathOrUrl) {
90
+ if (isAbsolute(pathOrUrl)) {
91
+ try {
92
+ await access(pathOrUrl, constants.R_OK)
93
+ return pathOrUrl
94
+ } catch (e) {}
95
+ }
96
+ try {
97
+ const filePath = join(job.webapp, pathOrUrl)
98
+ await access(filePath, constants.R_OK)
99
+ return filePath
100
+ } catch (e) {}
101
+ try {
102
+ // Assuming all files are coming from the same server
103
+ const { origin } = new URL(job.testPageUrls[0])
104
+ const filePath = join(job.coverageTempDir, 'sources', pathOrUrl)
105
+ await download(origin + pathOrUrl, filePath)
106
+ return filePath
107
+ } catch (e) {}
108
+ }
109
+
89
110
  async function generateCoverageReport (job) {
90
111
  job.status = 'Generating coverage report'
112
+ const output = getOutput(job)
113
+ output.debug('coverage', 'Generating coverage report...')
91
114
  await cleanDir(job.coverageReportDir)
92
115
  const coverageMergedDir = join(job.coverageTempDir, 'merged')
93
116
  await createDir(coverageMergedDir)
@@ -95,25 +118,17 @@ async function generateCoverageReport (job) {
95
118
  await nyc(job, 'merge', job.coverageTempDir, coverageFilename)
96
119
  if (job[$coverageRemote] && !job.coverageProxy) {
97
120
  job.status = 'Checking remote source files'
98
- // Assuming all files are coming from the same server
99
- const { origin } = new URL(job.testPageUrls[0])
100
- const sourcesBasePath = join(job.coverageTempDir, 'sources')
121
+ output.debug('coverage', 'Checking remote source files...')
101
122
  const coverageData = require(coverageFilename)
102
123
  const filenames = Object.keys(coverageData)
103
124
  let changes = 0
104
125
  for (const filename of filenames) {
105
126
  const fileData = coverageData[filename]
106
- const { path } = fileData
107
- if (isAbsolute(path)) {
108
- try {
109
- await access(path, constants.R_OK)
110
- continue
111
- } catch (e) {}
127
+ const filePath = await getReadableSource(job, fileData.path)
128
+ if (filePath && filePath !== fileData.path) {
129
+ fileData.path = filePath
130
+ ++changes
112
131
  }
113
- const filePath = join(sourcesBasePath, path)
114
- fileData.path = filePath
115
- await download(origin + path, filePath)
116
- ++changes
117
132
  }
118
133
  if (changes > 0) {
119
134
  await writeFile(coverageFilename, JSON.stringify(coverageData))
@@ -151,9 +166,7 @@ module.exports = {
151
166
  async collect (job, url, coverageData) {
152
167
  job[$coverageFileIndex] = (job[$coverageFileIndex] || 0) + 1
153
168
  const coverageFileName = join(job.coverageTempDir, `${filename(url)}_${job[$coverageFileIndex]}.json`)
154
- if (job.debugCoverage) {
155
- getOutput(job).wrap(() => console.log('coverage', coverageFileName))
156
- }
169
+ getOutput(job).debug('coverage', `saved coverage in '${coverageFileName}'`)
157
170
  await writeFile(coverageFileName, JSON.stringify(coverageData))
158
171
  },
159
172
  generateCoverageReport: job => job.coverage ? generateCoverageReport(job) : Promise.resolve(),
@@ -175,8 +188,8 @@ module.exports = {
175
188
  }
176
189
  if (job.mode === 'url' && job.coverageProxy) {
177
190
  await setupNyc(job)
191
+ // Assuming all files are coming from the same server
178
192
  const { origin } = new URL(job.url[0])
179
- const sourcesBasePath = join(job.coverageTempDir, 'sources')
180
193
  const { createInstrumenter } = require(join(await nycInstallationPath, 'node_modules/istanbul-lib-instrument'))
181
194
  const instrumenter = createInstrumenter({
182
195
  produceSourceMap: true,
@@ -188,34 +201,30 @@ module.exports = {
188
201
  return [{
189
202
  match: /(.*\.js)(\?.*)?$/,
190
203
  custom: async (request, response, url) => {
191
- if (!url.match(job.coverageProxyInclude) || url.match(job.coverageProxyExclude)) {
192
- if (job.debugCoverage) {
193
- getOutput(job).wrap(() => console.log('coverage_proxy ignore', url))
194
- }
195
- return // Ignore
204
+ if (!url.match(job.coverageProxyInclude) || url.match(/\bresources\b/) || url.match(job.coverageProxyExclude)) {
205
+ getOutput(job).debug('coverage', 'coverage_proxy ignore', url)
206
+ return
196
207
  }
197
- const sourcePath = join(sourcesBasePath, url)
208
+ const instrumentedSourcePath = join(instrumentedBasePath, url)
198
209
  try {
199
- await access(sourcePath, constants.R_OK)
200
- } catch (e) {
201
- try {
202
- if (sources[url]) {
203
- await sources[url]
204
- } else {
205
- if (job.debugCoverage) {
206
- getOutput(job).wrap(() => console.log('coverage_proxy instrument', url))
207
- }
208
- sources[url] = await download(origin + url, sourcePath)
209
- }
210
- } catch (statusCode) {
211
- return statusCode
212
- }
210
+ await access(instrumentedSourcePath, constants.R_OK)
211
+ return
212
+ } catch (e) {}
213
+ const instrumenting = sources[url]
214
+ if (instrumenting) {
215
+ await instrumenting
216
+ return // ok
213
217
  }
214
- const source = (await readFile(sourcePath)).toString()
215
- const instrumentedSource = await instrument(source, sourcePath)
216
- const instrumentedSourcePath = join(instrumentedBasePath, url)
217
- await createDir(dirname(instrumentedSourcePath))
218
- await writeFile(instrumentedSourcePath, instrumentedSource)
218
+ sources[url] = (async () => {
219
+ const sourcePath = await getReadableSource(job, url)
220
+ getOutput(job).debug('coverage', 'coverage_proxy instrument', url, sourcePath)
221
+ if (sourcePath) {
222
+ const source = (await readFile(sourcePath)).toString()
223
+ const instrumentedSource = await instrument(source, sourcePath)
224
+ await createDir(dirname(instrumentedSourcePath))
225
+ await writeFile(instrumentedSourcePath, instrumentedSource)
226
+ }
227
+ })()
219
228
  }
220
229
  },
221
230
  instrumentedMapping,
@@ -4,6 +4,16 @@
4
4
  report.ready.then(update => {
5
5
  let lastState = {}
6
6
 
7
+ async function retry () {
8
+ try {
9
+ await fetch('/_/progress', { method: 'INFO' })
10
+ location.hash = ''
11
+ refresh()
12
+ } catch (e) {
13
+ setTimeout(retry, 250)
14
+ }
15
+ }
16
+
7
17
  async function refresh () {
8
18
  const [, page, test] = location.hash.match(/#?([^-]*)(?:-(.*))?/)
9
19
  let url = '/_/progress'
@@ -22,6 +32,7 @@
22
32
  ...lastState,
23
33
  disconnected: true
24
34
  })
35
+ retry()
25
36
  return
26
37
  }
27
38
  if (test) {
package/src/endpoints.js CHANGED
@@ -141,6 +141,14 @@ module.exports = job => {
141
141
  match: '^/_/punyexpr.js',
142
142
  file: punyexprBinPath
143
143
  }, {
144
+ // Endpoint to retry on progress
145
+ method: 'INFO',
146
+ match: '^/_/progress',
147
+ custom: (request, response) => {
148
+ response.writeHead(204)
149
+ response.end()
150
+ }
151
+ }, {
144
152
  // Endpoint to follow progress
145
153
  match: '^/_/progress(?:\\?page=([^&]*)(?:&test=([^&]*))?)?',
146
154
  custom: (request, response, pageId, testId) => getJobProgress(job, request, response, pageId, testId)
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { $proxifiedUrls } = require('./symbols')
4
+
3
5
  const send = (response, obj) => {
4
6
  let json
5
7
  if (typeof obj !== 'string') {
@@ -60,6 +62,13 @@ module.exports = {
60
62
  ...job,
61
63
  status: job.status
62
64
  }, (key, value) => {
65
+ if (key === 'qunitPages' && job[$proxifiedUrls]) {
66
+ const unproxifiedQunitPages = {}
67
+ for (const url of Object.keys(job.qunitPages)) {
68
+ unproxifiedQunitPages[job[$proxifiedUrls][url] || url] = job.qunitPages[url]
69
+ }
70
+ return unproxifiedQunitPages
71
+ }
63
72
  if (key === 'modules') {
64
73
  return undefined
65
74
  }
@@ -2,7 +2,6 @@
2
2
  'use strict'
3
3
 
4
4
  const MODULE = 'ui5-test-runner/opa-iframe-coverage'
5
-
6
5
  if (window[MODULE]) {
7
6
  return // already installed
8
7
  }
@@ -1,9 +1,9 @@
1
1
  (function () {
2
2
  'use strict'
3
3
 
4
- const POST = 'ui5-test-runner/post'
5
- if (window[POST]) {
6
- return
4
+ const MODULE = 'ui5-test-runner/post'
5
+ if (window[MODULE]) {
6
+ return // already installed
7
7
  }
8
8
 
9
9
  const base = window['ui5-test-runner/base-host'] || ''
@@ -19,7 +19,7 @@
19
19
  }
20
20
  const ui5Summary = obj => {
21
21
  const id = obj.getId && obj.getId()
22
- const className = obj.getMetadata().getName()
22
+ const className = obj.getMetadata && obj.getMetadata() && obj.getMetadata().getName()
23
23
  return {
24
24
  'ui5:class': className,
25
25
  'ui5:id': id
@@ -71,7 +71,7 @@
71
71
 
72
72
  window['ui5-test-runner/stringify'] = stringify
73
73
 
74
- window[POST] = function post (url, data) {
74
+ window[MODULE] = function post (url, data) {
75
75
  function request () {
76
76
  return new Promise(function (resolve, reject) {
77
77
  const xhr = new XMLHttpRequest()
@@ -87,11 +87,10 @@
87
87
  })
88
88
  }
89
89
  lastPost = lastPost
90
+ .then(request)
90
91
  .then(undefined, function (reason) {
91
92
  console.error('Failed to POST to ' + url + '\nreason: ' + reason.toString())
92
- throw new Error('failed')
93
93
  })
94
- .then(request)
95
94
  return lastPost
96
95
  }
97
96
  }())
@@ -18,18 +18,34 @@
18
18
  }
19
19
  }
20
20
 
21
+ function getModules () {
22
+ if (QUnit.config && QUnit.config.modules) {
23
+ return QUnit.config.modules.map(({ name, tests }) => ({
24
+ name,
25
+ tests: tests.map(({ name, testId, skip }) => ({ name, testId, skip }))
26
+ }))
27
+ }
28
+ return []
29
+ }
30
+
31
+ function extend (details) {
32
+ details.isOpa = isOpa()
33
+ details.modules = getModules()
34
+ return details
35
+ }
36
+
21
37
  QUnit.begin(function (details) {
22
38
  details.isOpa = isOpa()
23
39
  return post('QUnit/begin', details)
24
40
  })
25
41
 
26
42
  QUnit.testStart(function (details) {
27
- return post('QUnit/testStart', details)
43
+ return post('QUnit/testStart', extend(details))
28
44
  })
29
45
 
30
46
  QUnit.log(function (log) {
31
47
  let ready = false
32
- post('QUnit/log', log)
48
+ post('QUnit/log', extend(log))
33
49
  .then(undefined, function () {
34
50
  console.error('Failed to POST to QUnit/log (no timestamp)', log)
35
51
  })
@@ -2,6 +2,12 @@
2
2
  (function () {
3
3
  'use strict'
4
4
 
5
+ const MODULE = 'ui5-test-runner/qunit-intercept'
6
+ if (window[MODULE]) {
7
+ return // already installed
8
+ }
9
+ window[MODULE] = true
10
+
5
11
  const callbacks = {}
6
12
  const mock = new Proxy({}, {
7
13
  get: function (instance, property) {
@@ -1,6 +1,12 @@
1
1
  (function () {
2
2
  'use strict'
3
3
 
4
+ const MODULE = 'ui5-test-runner/ui5-coverage'
5
+ if (window[MODULE]) {
6
+ return // already installed
7
+ }
8
+ window[MODULE] = true
9
+
4
10
  // inspired from ui5/resources/sap/ui/qunit/qunit-coverage-istanbul-dbg.js
5
11
 
6
12
  function appendUrlParameter (url) {
package/src/job-mode.js CHANGED
@@ -39,7 +39,6 @@ function buildAndCheckMode (job) {
39
39
  'libs',
40
40
  'mappings',
41
41
  'cache',
42
- 'webapp',
43
42
  'watch',
44
43
  'testsuite'
45
44
  ])
package/src/job.js CHANGED
@@ -105,9 +105,10 @@ function getCommand (cwd) {
105
105
  .option('--no-npm-install', '[šŸ’»šŸ”—šŸ§Ŗ] Prevent any NPM install (execution may fail if a dependency is missing)')
106
106
  .option('-bt, --browser-close-timeout <timeout>', '[šŸ’»šŸ”—šŸ§Ŗ] Maximum waiting time for browser close', timeout, 2000)
107
107
  .option('-br, --browser-retry <count>', '[šŸ’»šŸ”—šŸ§Ŗ] Browser instantiation retries : if the command fails unexpectedly, it is re-executed (0 means no retry)', 1)
108
+ .option('-oi, --output-interval <interval>', '[šŸ’»šŸ”—šŸ§Ŗ] Interval for reporting progress on non interactive output (CI/CD) (0 means no output)', timeout, 30000)
108
109
 
109
110
  // Common to legacy and url
110
- .option('-qs, --qunit-strict', '[šŸ’»šŸ”—] Strict mode on qunit execution (fails if no modules declared)', boolean, false)
111
+ .option('--webapp <path>', '[šŸ’»šŸ”—] Base folder of the web application (relative to cwd)', 'webapp')
111
112
  .option('-pf, --page-filter <regexp>', '[šŸ’»šŸ”—] Filter out pages not matching the regexp')
112
113
  .option('-pp, --page-params <params>', '[šŸ’»šŸ”—] Add parameters to page URL')
113
114
  .option('-t, --global-timeout <timeout>', '[šŸ’»šŸ”—] Limit the pages execution time, fail the page if it takes longer than the timeout (0 means no timeout)', timeout, 0)
@@ -135,7 +136,6 @@ function getCommand (cwd) {
135
136
  .option('--libs <lib...>', '[šŸ’»] Library mapping (<relative>=<path> or <path>)', arrayOf(lib))
136
137
  .option('--mappings <mapping...>', '[šŸ’»] Custom mapping (<match>=<file|url>(<config>))', arrayOf(mapping))
137
138
  .option('--cache <path>', '[šŸ’»] Cache UI5 resources locally in the given folder (empty to disable)')
138
- .option('--webapp <path>', '[šŸ’»] Base folder of the web application (relative to cwd)', 'webapp')
139
139
  .option('--testsuite <path>', '[šŸ’»] Path of the testsuite file (relative to webapp, URL parameters are supported)', 'test/testsuite.qunit.html')
140
140
  .option('-w, --watch [flag]', '[šŸ’»] Monitor the webapp folder and re-execute tests on change', boolean, false)
141
141
 
@@ -150,7 +150,6 @@ function getCommand (cwd) {
150
150
  .addOption(new Option('--debug-keep-report', DEBUG_OPTION, boolean).hideHelp())
151
151
  .addOption(new Option('--debug-capabilities-test <name>', DEBUG_OPTION).hideHelp())
152
152
  .addOption(new Option('--debug-capabilities-no-timeout', DEBUG_OPTION, boolean).hideHelp())
153
- .addOption(new Option('--debug-coverage', DEBUG_OPTION, boolean).hideHelp())
154
153
  .addOption(new Option('--debug-coverage-no-custom-fs', DEBUG_OPTION, boolean).hideHelp())
155
154
  .addOption(new Option('--debug-verbose <module...>', DEBUG_OPTION, arrayOf(string), []).hideHelp())
156
155
 
@@ -264,9 +263,7 @@ function finalize (job) {
264
263
  if (job.coverage) {
265
264
  function overrideIfNotSet (option, valueFromSettings) {
266
265
  if (valueFromSettings && job[$valueSources][option] !== 'cli') {
267
- if (job.debugCoverage) {
268
- output.wrap(() => console.log(`${option} extracted from nyc settings : ${valueFromSettings}`))
269
- }
266
+ output.debug('coverage', `${option} extracted from nyc settings : ${valueFromSettings}`)
270
267
  job[option] = valueFromSettings
271
268
  }
272
269
  }
package/src/output.js CHANGED
@@ -14,6 +14,7 @@ const { filename, noop, pad } = require('./tools')
14
14
  const inJest = typeof jest !== 'undefined'
15
15
  const interactive = process.stdout.columns !== undefined && !inJest
16
16
  const $output = Symbol('output')
17
+ const $outputStart = Symbol('output-start')
17
18
 
18
19
  if (!interactive) {
19
20
  const UTF8_BOM_CODE = '\ufeff'
@@ -31,6 +32,15 @@ if (inJest) {
31
32
  cons = console
32
33
  }
33
34
 
35
+ const formatTime = duration => {
36
+ duration = Math.ceil(duration / 1000)
37
+ const seconds = duration % 60
38
+ const minutes = (duration - seconds) / 60
39
+ return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0')
40
+ }
41
+
42
+ const getElapsed = job => formatTime(Date.now() - job[$outputStart])
43
+
34
44
  const write = (...parts) => parts.forEach(part => process.stdout.write(part))
35
45
 
36
46
  function clean (job) {
@@ -84,8 +94,16 @@ function bar (ratio, msg) {
84
94
  const TICKS = ['\u280b', '\u2819', '\u2839', '\u2838', '\u283c', '\u2834', '\u2826', '\u2827', '\u2807', '\u280f']
85
95
 
86
96
  function progress (job, cleanFirst = true) {
87
- if (cleanFirst) {
88
- clean(job)
97
+ if (interactive) {
98
+ if (cleanFirst) {
99
+ clean(job)
100
+ }
101
+ } else {
102
+ if (job[$browsers]) {
103
+ write(`${getElapsed(job)} │ Progress\n──────┓──────────\n`)
104
+ } else {
105
+ return
106
+ }
89
107
  }
90
108
  const output = job[$output]
91
109
  output.lines = 1
@@ -131,11 +149,26 @@ function progress (job, cleanFirst = true) {
131
149
  }
132
150
  }
133
151
 
134
- function output (job, ...texts) {
135
- writeFileSync(join(job.reportDir, 'output.txt'), texts.map(t => t.toString()).join(' ') + '\n', {
136
- encoding: 'utf-8',
137
- flag: 'a'
138
- })
152
+ function output (job, ...args) {
153
+ writeFileSync(
154
+ join(job.reportDir, 'output.txt'),
155
+ args.map(arg => {
156
+ if (typeof arg === 'object') {
157
+ return JSON.stringify(arg, undefined, 2)
158
+ }
159
+ if (arg === undefined) {
160
+ return 'undefined'
161
+ }
162
+ if (arg === null) {
163
+ return 'null'
164
+ }
165
+ return arg.toString()
166
+ }).join(' ') + '\n',
167
+ {
168
+ encoding: 'utf-8',
169
+ flag: 'a'
170
+ }
171
+ )
139
172
  }
140
173
 
141
174
  function log (job, ...texts) {
@@ -185,13 +218,6 @@ function browserIssue (job, { type, url, code, dir }) {
185
218
  log(job, p`└──────────${pad.x('─')}ā”˜`)
186
219
  }
187
220
 
188
- const formatTime = duration => {
189
- duration = Math.ceil(duration / 1000)
190
- const seconds = duration % 60
191
- const minutes = (duration - seconds) / 60
192
- return minutes.toString().padStart(2, '0') + ':' + seconds.toString().padStart(2, '0')
193
- }
194
-
195
221
  function build (job) {
196
222
  let wrap
197
223
  if (interactive) {
@@ -206,8 +232,7 @@ function build (job) {
206
232
  } else {
207
233
  wrap = method => method
208
234
  }
209
- const outputStart = Date.now()
210
- const getElapsed = () => formatTime(Date.now() - outputStart)
235
+ job[$outputStart] = Date.now()
211
236
 
212
237
  return {
213
238
  lastTick: 0,
@@ -228,6 +253,7 @@ function build (job) {
228
253
  debug: wrap((module, ...args) => {
229
254
  if (job.debugVerbose && job.debugVerbose.includes(module)) {
230
255
  console.log(`šŸž${module}`, ...args)
256
+ output(job, `šŸž${module}`, ...args)
231
257
  }
232
258
  }),
233
259
 
@@ -251,7 +277,7 @@ function build (job) {
251
277
  } else {
252
278
  method = log
253
279
  }
254
- const text = `${getElapsed()} │ ${status}`
280
+ const text = `${getElapsed(job)} │ ${status}`
255
281
  method(job, '')
256
282
  method(job, text)
257
283
  method(job, '──────┓'.padEnd(text.length, '─'))
@@ -268,6 +294,8 @@ function build (job) {
268
294
  reportOnJobProgress () {
269
295
  if (interactive) {
270
296
  this.reportIntervalId = setInterval(progress.bind(null, job), 250)
297
+ } else if (job.outputInterval) {
298
+ this.reportIntervalId = setInterval(progress.bind(null, job), job.outputInterval)
271
299
  }
272
300
  },
273
301
 
@@ -311,7 +339,7 @@ function build (job) {
311
339
  },
312
340
 
313
341
  browserStart (url) {
314
- const text = p80()`${getElapsed()} >> ${pad.lt(url)} [${filename(url)}]`
342
+ const text = p80()`${getElapsed(job)} >> ${pad.lt(url)} [${filename(url)}]`
315
343
  if (interactive) {
316
344
  output(job, text)
317
345
  } else {
@@ -325,7 +353,7 @@ function build (job) {
325
353
  if (page) {
326
354
  duration = ' (' + formatTime(page.end - page.start) + ')'
327
355
  }
328
- const text = p80()`${getElapsed()} << ${pad.lt(url)} ${duration} [${filename(url)}]`
356
+ const text = p80()`${getElapsed(job)} << ${pad.lt(url)} ${duration} [${filename(url)}]`
329
357
  if (interactive) {
330
358
  output(job, text)
331
359
  } else {
@@ -418,10 +446,6 @@ function build (job) {
418
446
  log(job, p80()`Skipping nyc instrumentation (--url)`)
419
447
  }),
420
448
 
421
- qunitEarlyStart: wrap(url => {
422
- log(job, p80()`QUnit start without tests in ${pad.lt(url)}`)
423
- }),
424
-
425
449
  endpointError: wrap(({ api, url, data, error }) => {
426
450
  const p = p80()
427
451
  log(job, p`ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€${pad.x('─')}┐`)
@@ -514,7 +538,9 @@ function build (job) {
514
538
  stop () {
515
539
  if (this.reportIntervalId) {
516
540
  clearInterval(this.reportIntervalId)
517
- clean(job)
541
+ if (interactive) {
542
+ clean(job)
543
+ }
518
544
  }
519
545
  }
520
546
  }