ava 4.0.0 → 4.2.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.
@@ -90,7 +90,7 @@ function load(projectDir, overrides) {
90
90
 
91
91
  const helper = Object.freeze({
92
92
  classifyFile: classifyForESLint,
93
- classifyImport: importPath => {
93
+ classifyImport(importPath) {
94
94
  if (hasExtension(globs.extensions, importPath)) {
95
95
  // The importPath has one of the test file extensions: we can classify
96
96
  // it directly.
package/lib/api.js CHANGED
@@ -177,13 +177,19 @@ export default class Api extends Emittery {
177
177
  const fileCount = selectedFiles.length;
178
178
 
179
179
  // The files must be in the same order across all runs, so sort them.
180
- selectedFiles = selectedFiles.sort((a, b) => a.localeCompare(b, [], {numeric: true}));
180
+ const defaultComparator = (a, b) => a.localeCompare(b, [], {numeric: true});
181
+ selectedFiles = selectedFiles.sort(this.options.sortTestFiles || defaultComparator);
181
182
  selectedFiles = chunkd(selectedFiles, currentIndex, totalRuns);
182
183
 
183
184
  const currentFileCount = selectedFiles.length;
184
185
 
185
186
  runStatus = new RunStatus(fileCount, {currentFileCount, currentIndex, totalRuns}, selectionInsights);
186
187
  } else {
188
+ // If a custom sorter was configured, use it.
189
+ if (this.options.sortTestFiles) {
190
+ selectedFiles = selectedFiles.sort(this.options.sortTestFiles);
191
+ }
192
+
187
193
  runStatus = new RunStatus(selectedFiles.length, null, selectionInsights);
188
194
  }
189
195
 
@@ -261,8 +267,8 @@ export default class Api extends Emittery {
261
267
  }
262
268
 
263
269
  const lineNumbers = getApplicableLineNumbers(globs.normalizeFileForMatching(apiOptions.projectDir, file), filter);
264
- // Removing `providers` field because they cannot be transfered to the worker threads.
265
- const {providers, ...forkOptions} = apiOptions;
270
+ // Removing `providers` and `sortTestFiles` fields because they cannot be transferred to the worker threads.
271
+ const {providers, sortTestFiles, ...forkOptions} = apiOptions;
266
272
  const options = {
267
273
  ...forkOptions,
268
274
  providerStates,
package/lib/cli.js CHANGED
@@ -21,6 +21,7 @@ import {splitPatternAndLineNumbers} from './line-numbers.js';
21
21
  import {loadConfig} from './load-config.js';
22
22
  import normalizeModuleTypes from './module-types.js';
23
23
  import normalizeNodeArguments from './node-arguments.js';
24
+ import pkg from './pkg.cjs';
24
25
  import providerManager from './provider-manager.js';
25
26
  import DefaultReporter from './reporters/default.js';
26
27
  import TapReporter from './reporters/tap.js';
@@ -102,8 +103,15 @@ export default async function loadCli() { // eslint-disable-line complexity
102
103
  let conf;
103
104
  let confError;
104
105
  try {
105
- const {argv: {config: configFile}} = yargs(hideBin(process.argv)).help(false);
106
- conf = await loadConfig({configFile});
106
+ const {argv: {config: configFile}} = yargs(hideBin(process.argv)).help(false).version(false);
107
+ const loaded = await loadConfig({configFile});
108
+ if (loaded.unsupportedFiles.length > 0) {
109
+ console.log(chalk.magenta(
110
+ ` ${figures.warning} AVA does not support JSON config, ignoring:\n\n ${loaded.unsupportedFiles.join('\n ')}`,
111
+ ));
112
+ }
113
+
114
+ conf = loaded.config;
107
115
  if (conf.configFile && path.basename(conf.configFile) !== path.relative(conf.projectDir, conf.configFile)) {
108
116
  console.log(chalk.magenta(` ${figures.warning} Using configuration from ${conf.configFile}`));
109
117
  }
@@ -132,6 +140,7 @@ export default async function loadCli() { // eslint-disable-line complexity
132
140
 
133
141
  let resetCache = false;
134
142
  const {argv} = yargs(hideBin(process.argv))
143
+ .version(pkg.version)
135
144
  .parserConfiguration({
136
145
  'boolean-negation': true,
137
146
  'camel-case-expansion': false,
@@ -161,7 +170,7 @@ export default async function loadCli() { // eslint-disable-line complexity
161
170
  })
162
171
  .command('* [<pattern>...]', 'Run tests', yargs => yargs.options(FLAGS).positional('pattern', {
163
172
  array: true,
164
- describe: 'Select which test files to run. Leave empty if you want AVA to run all test files as per your configuration. Accepts glob patterns, directories that (recursively) contain test files, and file paths. Add a colon and specify line numbers of specific tests to run',
173
+ describe: 'Select which test files to run. Leave empty if you want AVA to run all test files as per your configuration. Accepts glob patterns, directories that (recursively) contain test files, and file paths optionally suffixed with a colon and comma-separated numbers and/or ranges identifying the 1-based line(s) of specific tests to run',
165
174
  type: 'string',
166
175
  }), argv => {
167
176
  if (activeInspector) {
@@ -188,7 +197,7 @@ export default async function loadCli() { // eslint-disable-line complexity
188
197
  },
189
198
  }).positional('pattern', {
190
199
  demand: true,
191
- describe: 'Glob patterns to select a single test file to debug. Add a colon and specify line numbers of specific tests to run',
200
+ describe: 'Glob pattern to select a single test file to debug, optionally suffixed with a colon and comma-separated numbers and/or ranges identifying the 1-based line(s) of specific tests to run',
192
201
  type: 'string',
193
202
  }),
194
203
  argv => {
@@ -319,16 +328,20 @@ export default async function loadCli() { // eslint-disable-line complexity
319
328
  exit('’sources’ has been removed. Use ’ignoredByWatcher’ to provide glob patterns of files that the watcher should ignore.');
320
329
  }
321
330
 
322
- let pkg;
331
+ if (Reflect.has(conf, 'sortTestFiles') && typeof conf.sortTestFiles !== 'function') {
332
+ exit('’sortTestFiles’ must be a comparator function.');
333
+ }
334
+
335
+ let projectPackageObject;
323
336
  try {
324
- pkg = JSON.parse(fs.readFileSync(path.resolve(projectDir, 'package.json')));
337
+ projectPackageObject = JSON.parse(fs.readFileSync(path.resolve(projectDir, 'package.json')));
325
338
  } catch (error) {
326
339
  if (error.code !== 'ENOENT') {
327
340
  throw error;
328
341
  }
329
342
  }
330
343
 
331
- const {type: defaultModuleType = 'commonjs'} = pkg || {};
344
+ const {type: defaultModuleType = 'commonjs'} = projectPackageObject || {};
332
345
 
333
346
  const providers = [];
334
347
  if (Reflect.has(conf, 'typescript')) {
@@ -380,7 +393,7 @@ export default async function loadCli() { // eslint-disable-line complexity
380
393
  }
381
394
 
382
395
  let parallelRuns = null;
383
- if (isCi && ciParallelVars) {
396
+ if (isCi && ciParallelVars && combined.utilizeParallelBuilds !== false) {
384
397
  const {index: currentIndex, total: totalRuns} = ciParallelVars;
385
398
  parallelRuns = {currentIndex, totalRuns};
386
399
  }
@@ -411,6 +424,7 @@ export default async function loadCli() { // eslint-disable-line complexity
411
424
  moduleTypes,
412
425
  nodeArguments,
413
426
  parallelRuns,
427
+ sortTestFiles: conf.sortTestFiles,
414
428
  projectDir,
415
429
  providers,
416
430
  ranFromCli: true,
@@ -41,7 +41,7 @@ const buildGlobs = ({conf, providers, projectDir, overrideExtensions, overrideFi
41
41
 
42
42
  const resolveGlobs = async (projectDir, overrideExtensions, overrideFiles) => {
43
43
  if (!configCache.has(projectDir)) {
44
- configCache.set(projectDir, loadConfig({resolveFrom: projectDir}).then(async conf => {
44
+ configCache.set(projectDir, loadConfig({resolveFrom: projectDir}).then(async ({config: conf}) => {
45
45
  const providers = await collectProviders({conf, projectDir});
46
46
  return {conf, providers};
47
47
  }));
@@ -20,14 +20,15 @@ const importConfig = async ({configFile, fileForErrorMessage}) => {
20
20
  };
21
21
 
22
22
  const loadConfigFile = async ({projectDir, configFile}) => {
23
- if (!fs.existsSync(configFile)) {
24
- return null;
25
- }
26
-
27
23
  const fileForErrorMessage = path.relative(projectDir, configFile);
28
24
  try {
25
+ await fs.promises.access(configFile);
29
26
  return {config: await importConfig({configFile, fileForErrorMessage}), configFile, fileForErrorMessage};
30
27
  } catch (error) {
28
+ if (error.code === 'ENOENT') {
29
+ return null;
30
+ }
31
+
31
32
  throw Object.assign(new Error(`Error loading ${fileForErrorMessage}: ${error.message}`), {parent: error});
32
33
  }
33
34
  };
@@ -63,6 +64,20 @@ async function findRepoRoot(fromDir) {
63
64
  return root;
64
65
  }
65
66
 
67
+ async function checkJsonFile(searchDir) {
68
+ const file = path.join(searchDir, 'ava.config.json');
69
+ try {
70
+ await fs.promises.access(file);
71
+ return file;
72
+ } catch (error) {
73
+ if (error.code === 'ENOENT') {
74
+ return null;
75
+ }
76
+
77
+ throw error;
78
+ }
79
+ }
80
+
66
81
  export async function loadConfig({configFile, resolveFrom = process.cwd(), defaults = {}} = {}) {
67
82
  let packageConf = await packageConfig('ava', {cwd: resolveFrom});
68
83
  const filepath = packageJsonPath(packageConf);
@@ -74,6 +89,7 @@ export async function loadConfig({configFile, resolveFrom = process.cwd(), defau
74
89
  const allowConflictWithPackageJson = Boolean(configFile);
75
90
  configFile = resolveConfigFile(configFile);
76
91
 
92
+ const unsupportedFiles = [];
77
93
  let fileConf = NO_SUCH_FILE;
78
94
  let fileForErrorMessage;
79
95
  let conflicting = [];
@@ -86,12 +102,17 @@ export async function loadConfig({configFile, resolveFrom = process.cwd(), defau
86
102
  let searchDir = projectDir;
87
103
  const stopAt = path.dirname(repoRoot);
88
104
  do {
89
- const results = await Promise.all([ // eslint-disable-line no-await-in-loop
105
+ const [jsonFile, ...results] = await Promise.all([ // eslint-disable-line no-await-in-loop
106
+ checkJsonFile(searchDir),
90
107
  loadConfigFile({projectDir, configFile: path.join(searchDir, 'ava.config.js')}),
91
108
  loadConfigFile({projectDir, configFile: path.join(searchDir, 'ava.config.cjs')}),
92
109
  loadConfigFile({projectDir, configFile: path.join(searchDir, 'ava.config.mjs')}),
93
110
  ]);
94
111
 
112
+ if (jsonFile !== null) {
113
+ unsupportedFiles.push(jsonFile);
114
+ }
115
+
95
116
  [{config: fileConf, fileForErrorMessage, configFile} = {config: NO_SUCH_FILE, fileForErrorMessage: undefined}, ...conflicting] = results.filter(result => result !== null);
96
117
 
97
118
  searchDir = path.dirname(searchDir);
@@ -139,5 +160,5 @@ export async function loadConfig({configFile, resolveFrom = process.cwd(), defau
139
160
  }
140
161
  }
141
162
 
142
- return config;
163
+ return {config, unsupportedFiles};
143
164
  }
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import indentString from 'indent-string';
5
5
  import plur from 'plur';
6
6
  import stripAnsi from 'strip-ansi';
7
- import supertap from 'supertap';
7
+ import * as supertap from 'supertap';
8
8
 
9
9
  import beautifyStack from './beautify-stack.js';
10
10
  import prefixTitle from './prefix-title.js';
package/lib/runner.js CHANGED
@@ -224,8 +224,8 @@ export default class Runner extends Emittery {
224
224
  return this.snapshots.skipSnapshot(options);
225
225
  }
226
226
 
227
- saveSnapshotState() {
228
- return {touchedFiles: this.snapshots.save()};
227
+ async saveSnapshotState() {
228
+ return {touchedFiles: await this.snapshots.save()};
229
229
  }
230
230
 
231
231
  onRun(runnable) {
@@ -182,8 +182,8 @@ function sortBlocks(blocksByTitle, blockIndices) {
182
182
  );
183
183
  }
184
184
 
185
- function encodeSnapshots(snapshotData) {
186
- const encoded = cbor.encodeOne(snapshotData, {
185
+ async function encodeSnapshots(snapshotData) {
186
+ const encoded = await cbor.encodeAsync(snapshotData, {
187
187
  omitUndefinedProperties: true,
188
188
  canonical: true,
189
189
  });
@@ -351,7 +351,7 @@ class Manager {
351
351
  this.recordSerialized({belongsTo, index, ...snapshot});
352
352
  }
353
353
 
354
- save() {
354
+ async save() {
355
355
  const {dir, relFile, snapFile, snapPath, reportPath} = this;
356
356
 
357
357
  if (this.updating && this.newBlocksByTitle.size === 0) {
@@ -371,15 +371,17 @@ class Manager {
371
371
  ),
372
372
  };
373
373
 
374
- const buffer = encodeSnapshots(snapshots);
374
+ const buffer = await encodeSnapshots(snapshots);
375
375
  const reportBuffer = generateReport(relFile, snapFile, snapshots);
376
376
 
377
- fs.mkdirSync(dir, {recursive: true});
377
+ await fs.promises.mkdir(dir, {recursive: true});
378
378
 
379
379
  const temporaryFiles = [];
380
380
  const tmpfileCreated = file => temporaryFiles.push(file);
381
- writeFileAtomic.sync(snapPath, buffer, {tmpfileCreated});
382
- writeFileAtomic.sync(reportPath, reportBuffer, {tmpfileCreated});
381
+ await Promise.all([
382
+ writeFileAtomic(snapPath, buffer, {tmpfileCreated}),
383
+ writeFileAtomic(reportPath, reportBuffer, {tmpfileCreated}),
384
+ ]);
383
385
  return {
384
386
  changedFiles: [snapPath, reportPath],
385
387
  temporaryFiles,
package/lib/test.js CHANGED
@@ -24,16 +24,16 @@ const testMap = new WeakMap();
24
24
  class ExecutionContext extends Assertions {
25
25
  constructor(test) {
26
26
  super({
27
- pass: () => {
27
+ pass() {
28
28
  test.countPassedAssertion();
29
29
  },
30
- pending: promise => {
30
+ pending(promise) {
31
31
  test.addPendingAssertion(promise);
32
32
  },
33
- fail: error => {
33
+ fail(error) {
34
34
  test.addFailedAssertion(error);
35
35
  },
36
- skip: () => {
36
+ skip() {
37
37
  test.countPassedAssertion();
38
38
  },
39
39
  compareWithSnapshot: options => test.compareWithSnapshot(options),
@@ -109,7 +109,7 @@ class ExecutionContext extends Assertions {
109
109
  logs: [...logs], // Don't allow modification of logs.
110
110
  passed,
111
111
  title: attemptTitle,
112
- commit: ({retainLogs = true} = {}) => {
112
+ commit({retainLogs = true} = {}) {
113
113
  if (committed) {
114
114
  return;
115
115
  }
@@ -132,7 +132,7 @@ class ExecutionContext extends Assertions {
132
132
  startingSnapshotCount,
133
133
  });
134
134
  },
135
- discard: ({retainLogs = false} = {}) => {
135
+ discard({retainLogs = false} = {}) {
136
136
  if (committed) {
137
137
  test.saveFirstError(new Error('Can’t discard a result that was previously committed'));
138
138
  return;
@@ -81,7 +81,7 @@ const run = async options => {
81
81
 
82
82
  runner.on('finish', async () => {
83
83
  try {
84
- const {touchedFiles} = runner.saveSnapshotState();
84
+ const {touchedFiles} = await runner.saveSnapshotState();
85
85
  if (touchedFiles) {
86
86
  channel.send({type: 'touched-files', files: touchedFiles});
87
87
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ava",
3
- "version": "4.0.0",
3
+ "version": "4.2.0",
4
4
  "description": "Node.js test runner that lets you develop with confidence.",
5
5
  "license": "MIT",
6
6
  "repository": "avajs/ava",
@@ -78,32 +78,32 @@
78
78
  "callsites": "^4.0.0",
79
79
  "cbor": "^8.1.0",
80
80
  "chalk": "^5.0.0",
81
- "chokidar": "^3.5.2",
81
+ "chokidar": "^3.5.3",
82
82
  "chunkd": "^2.0.1",
83
83
  "ci-info": "^3.3.0",
84
84
  "ci-parallel-vars": "^1.0.1",
85
85
  "clean-yaml-object": "^0.1.0",
86
86
  "cli-truncate": "^3.1.0",
87
- "code-excerpt": "^3.0.0",
87
+ "code-excerpt": "^4.0.0",
88
88
  "common-path-prefix": "^3.0.0",
89
89
  "concordance": "^5.0.4",
90
90
  "currently-unhandled": "^0.4.1",
91
91
  "debug": "^4.3.3",
92
92
  "del": "^6.0.0",
93
- "emittery": "^0.10.0",
93
+ "emittery": "^0.10.1",
94
94
  "figures": "^4.0.0",
95
- "globby": "^12.0.2",
95
+ "globby": "^13.1.1",
96
96
  "ignore-by-default": "^2.0.0",
97
97
  "indent-string": "^5.0.0",
98
98
  "is-error": "^2.2.2",
99
99
  "is-plain-object": "^5.0.0",
100
100
  "is-promise": "^4.0.0",
101
101
  "matcher": "^5.0.0",
102
- "mem": "^9.0.1",
102
+ "mem": "^9.0.2",
103
103
  "ms": "^2.1.3",
104
104
  "p-event": "^5.0.1",
105
105
  "p-map": "^5.3.0",
106
- "picomatch": "^2.3.0",
106
+ "picomatch": "^2.3.1",
107
107
  "pkg-conf": "^4.0.0",
108
108
  "plur": "^5.1.0",
109
109
  "pretty-ms": "^7.0.1",
@@ -111,30 +111,30 @@
111
111
  "slash": "^3.0.0",
112
112
  "stack-utils": "^2.0.5",
113
113
  "strip-ansi": "^7.0.1",
114
- "supertap": "^2.0.0",
114
+ "supertap": "^3.0.1",
115
115
  "temp-dir": "^2.0.0",
116
- "write-file-atomic": "^3.0.3",
116
+ "write-file-atomic": "^4.0.1",
117
117
  "yargs": "^17.3.1"
118
118
  },
119
119
  "devDependencies": {
120
120
  "@ava/test": "github:avajs/test",
121
121
  "@ava/typescript": "^3.0.1",
122
- "@sinonjs/fake-timers": "^8.1.0",
122
+ "@sinonjs/fake-timers": "^9.1.1",
123
123
  "ansi-escapes": "^5.0.0",
124
124
  "c8": "^7.11.0",
125
125
  "delay": "^5.0.0",
126
- "execa": "^6.0.0",
127
- "fs-extra": "^10.0.0",
126
+ "execa": "^6.1.0",
127
+ "fs-extra": "^10.0.1",
128
128
  "get-stream": "^6.0.1",
129
129
  "replace-string": "^4.0.0",
130
- "sinon": "^12.0.1",
131
- "tap": "^15.1.5",
130
+ "sinon": "^13.0.1",
131
+ "tap": "^16.0.0",
132
132
  "temp-write": "^5.0.0",
133
133
  "tempy": "^2.0.0",
134
134
  "touch": "^3.1.0",
135
135
  "tsd": "^0.19.1",
136
- "typescript": "^4.4.4",
137
- "xo": "^0.47.0",
136
+ "typescript": "^4.6.2",
137
+ "xo": "^0.48.0",
138
138
  "zen-observable": "^0.8.15"
139
139
  },
140
140
  "peerDependencies": {
package/plugin.d.ts CHANGED
@@ -41,7 +41,7 @@ export namespace SharedWorker {
41
41
  readonly file: string;
42
42
  publish: (data: Data) => PublishedMessage<Data>;
43
43
  subscribe: () => AsyncIterableIterator<ReceivedMessage<Data>>;
44
- teardown: <TeardownFn extends () => void> (fn: TeardownFn) => TeardownFn;
44
+ teardown: (fn: (() => Promise<void>) | (() => void)) => () => Promise<void>;
45
45
  };
46
46
 
47
47
  export namespace Plugin {
package/readme.md CHANGED
@@ -1,3 +1,5 @@
1
+ [![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://vshymanskyy.github.io/StandWithUkraine/)
2
+
1
3
  # <img src="media/header.png" title="AVA" alt="AVA logo" width="530">
2
4
 
3
5
  AVA is a test runner for Node.js with a concise API, detailed error output, embrace of new language features and process isolation that lets you develop with confidence 🚀
@@ -71,7 +73,7 @@ Don't forget to configure the `test` script in your `package.json` as per above.
71
73
  Create a file named `test.js` in the project root directory:
72
74
 
73
75
  ```js
74
- const test = require('ava');
76
+ import test from 'ava';
75
77
 
76
78
  test('foo', t => {
77
79
  t.pass();
@@ -139,15 +141,16 @@ We have a growing list of [common pitfalls](docs/08-common-pitfalls.md) you may
139
141
 
140
142
  ### Recipes
141
143
 
142
- - [Shared workers](docs/recipes/shared-workers.md)
143
144
  - [Test setup](docs/recipes/test-setup.md)
144
- - [Code coverage](docs/recipes/code-coverage.md)
145
+ - [TypeScript](docs/recipes/typescript.md)
146
+ - [Shared workers](docs/recipes/shared-workers.md)
145
147
  - [Watch mode](docs/recipes/watch-mode.md)
146
- - [Endpoint testing](docs/recipes/endpoint-testing.md)
147
148
  - [When to use `t.plan()`](docs/recipes/when-to-use-plan.md)
148
- - [Browser testing](docs/recipes/browser-testing.md)
149
- - [TypeScript](docs/recipes/typescript.md)
150
149
  - [Passing arguments to your test files](docs/recipes/passing-arguments-to-your-test-files.md)
150
+ - [Splitting tests in CI](docs/recipes/splitting-tests-ci.md)
151
+ - [Code coverage](docs/recipes/code-coverage.md)
152
+ - [Endpoint testing](docs/recipes/endpoint-testing.md)
153
+ - [Browser testing](docs/recipes/browser-testing.md)
151
154
  - [Testing Vue.js components](docs/recipes/vue.md)
152
155
  - [Debugging tests with Chrome DevTools](docs/recipes/debugging-with-chrome-devtools.md)
153
156
  - [Debugging tests with VSCode](docs/recipes/debugging-with-vscode.md)
@@ -138,6 +138,18 @@ export interface DeepEqualAssertion {
138
138
  */
139
139
  <Actual, Expected extends Actual>(actual: Actual, expected: Expected, message?: string): actual is Expected;
140
140
 
141
+ /**
142
+ * Assert that `actual` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to
143
+ * `expected`, returning a boolean indicating whether the assertion passed.
144
+ */
145
+ <Actual extends Expected, Expected>(actual: Actual, expected: Expected, message?: string): expected is Actual;
146
+
147
+ /**
148
+ * Assert that `actual` is [deeply equal](https://github.com/concordancejs/concordance#comparison-details) to
149
+ * `expected`, returning a boolean indicating whether the assertion passed.
150
+ */
151
+ <Actual, Expected>(actual: Actual, expected: Expected, message?: string): boolean;
152
+
141
153
  /** Skip this assertion. */
142
154
  skip(actual: any, expected: any, message?: string): void;
143
155
  }
@@ -46,7 +46,7 @@ export interface PlanFn {
46
46
  export type TimeoutFn = (ms: number, message?: string) => void;
47
47
 
48
48
  /** Declare a function to be run after the test has ended. */
49
- export type TeardownFn = (fn: () => void) => void;
49
+ export type TeardownFn = (fn: (() => Promise<void>) | (() => void)) => void;
50
50
 
51
51
  export type ImplementationFn<Args extends unknown[], Context = unknown> =
52
52
  ((t: ExecutionContext<Context>, ...args: Args) => PromiseLike<void>) |