aboutsummaryrefslogtreecommitdiff
path: root/node_modules/ava/lib
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/ava/lib')
-rw-r--r--node_modules/ava/lib/assert.js32
-rw-r--r--node_modules/ava/lib/ava-files.js5
-rw-r--r--node_modules/ava/lib/babel-config.js8
-rw-r--r--node_modules/ava/lib/beautify-stack.js51
-rw-r--r--node_modules/ava/lib/caching-precompiler.js5
-rw-r--r--node_modules/ava/lib/cli.js14
-rw-r--r--node_modules/ava/lib/colors.js1
-rw-r--r--node_modules/ava/lib/enhance-assert.js22
-rw-r--r--node_modules/ava/lib/extract-stack.js10
-rw-r--r--node_modules/ava/lib/fork.js4
-rw-r--r--node_modules/ava/lib/logger.js11
-rw-r--r--node_modules/ava/lib/main.js3
-rw-r--r--node_modules/ava/lib/reporters/improper-usage-messages.js4
-rw-r--r--node_modules/ava/lib/reporters/mini.js53
-rw-r--r--node_modules/ava/lib/reporters/tap.js47
-rw-r--r--node_modules/ava/lib/reporters/verbose.js76
-rw-r--r--node_modules/ava/lib/run-status.js9
-rw-r--r--node_modules/ava/lib/runner.js7
-rw-r--r--node_modules/ava/lib/serialize-error.js9
-rw-r--r--node_modules/ava/lib/snapshot-manager.js30
-rw-r--r--node_modules/ava/lib/test-collection.js32
-rw-r--r--node_modules/ava/lib/test-worker.js13
-rw-r--r--node_modules/ava/lib/test.js10
-rw-r--r--node_modules/ava/lib/watcher.js20
24 files changed, 353 insertions, 123 deletions
diff --git a/node_modules/ava/lib/assert.js b/node_modules/ava/lib/assert.js
index a0e9fe82c..18009b97d 100644
--- a/node_modules/ava/lib/assert.js
+++ b/node_modules/ava/lib/assert.js
@@ -64,6 +64,7 @@ function wrapAssertions(callbacks) {
const pass = callbacks.pass;
const pending = callbacks.pending;
const fail = callbacks.fail;
+ const log = callbacks.log;
const noop = () => {};
const makeRethrow = reason => () => {
@@ -86,14 +87,25 @@ function wrapAssertions(callbacks) {
if (Object.is(actual, expected)) {
pass(this);
} else {
- const actualDescriptor = concordance.describe(actual, concordanceOptions);
- const expectedDescriptor = concordance.describe(expected, concordanceOptions);
- fail(this, new AssertionError({
- assertion: 'is',
- message,
- raw: {actual, expected},
- values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
- }));
+ const result = concordance.compare(actual, expected, concordanceOptions);
+ const actualDescriptor = result.actual || concordance.describe(actual, concordanceOptions);
+ const expectedDescriptor = result.expected || concordance.describe(expected, concordanceOptions);
+
+ if (result.pass) {
+ fail(this, new AssertionError({
+ assertion: 'is',
+ message,
+ raw: {actual, expected},
+ values: [formatDescriptorWithLabel('Values are deeply equal to each other, but they are not the same:', actualDescriptor)]
+ }));
+ } else {
+ fail(this, new AssertionError({
+ assertion: 'is',
+ message,
+ raw: {actual, expected},
+ values: [formatDescriptorDiff(actualDescriptor, expectedDescriptor)]
+ }));
+ }
}
},
@@ -110,6 +122,10 @@ function wrapAssertions(callbacks) {
}
},
+ log(text) {
+ log(this, text);
+ },
+
deepEqual(actual, expected, message) {
const result = concordance.compare(actual, expected, concordanceOptions);
if (result.pass) {
diff --git a/node_modules/ava/lib/ava-files.js b/node_modules/ava/lib/ava-files.js
index cfdc9f202..b6520da37 100644
--- a/node_modules/ava/lib/ava-files.js
+++ b/node_modules/ava/lib/ava-files.js
@@ -125,6 +125,7 @@ class AvaFiles {
autoBind(this);
}
+
findTestFiles() {
return handlePaths(this.files, this.excludePatterns, {
cwd: this.cwd,
@@ -134,6 +135,7 @@ class AvaFiles {
symlinks: Object.create(null)
});
}
+
findTestHelpers() {
return handlePaths(defaultHelperPatterns(), ['!**/node_modules/**'], {
cwd: this.cwd,
@@ -144,6 +146,7 @@ class AvaFiles {
symlinks: Object.create(null)
});
}
+
isSource(filePath) {
let mixedPatterns = [];
const defaultIgnorePatterns = getDefaultIgnorePatterns();
@@ -195,6 +198,7 @@ class AvaFiles {
return false;
}
+
isTest(filePath) {
const excludePatterns = this.excludePatterns;
const initialPatterns = this.files.concat(excludePatterns);
@@ -241,6 +245,7 @@ class AvaFiles {
// excludePatterns into account. This mimicks the behavior in api.js
return multimatch(matchable(filePath), recursivePatterns.concat(excludePatterns)).length === 1;
}
+
getChokidarPatterns() {
let paths = [];
let ignored = [];
diff --git a/node_modules/ava/lib/babel-config.js b/node_modules/ava/lib/babel-config.js
index 62e841f05..d58b700b8 100644
--- a/node_modules/ava/lib/babel-config.js
+++ b/node_modules/ava/lib/babel-config.js
@@ -6,6 +6,7 @@ const figures = require('figures');
const configManager = require('hullabaloo-config-manager');
const md5Hex = require('md5-hex');
const makeDir = require('make-dir');
+const semver = require('semver');
const colors = require('./colors');
function validate(conf) {
@@ -99,6 +100,7 @@ function build(projectDir, cacheDir, userOptions, powerAssert) {
const baseOptions = {
babelrc: false,
+ plugins: [],
presets: [
['@ava/transform-test-files', {powerAssert}]
]
@@ -107,6 +109,12 @@ function build(projectDir, cacheDir, userOptions, powerAssert) {
baseOptions.presets.unshift('@ava/stage-4');
}
+ // Include object rest spread support for node versions that support it
+ // natively.
+ if (userOptions === 'default' && semver.satisfies(process.versions.node, '>= 8.3.0')) {
+ baseOptions.plugins.push('babel-plugin-syntax-object-rest-spread');
+ }
+
const baseConfig = configManager.createConfig({
dir: AVA_DIR, // Presets are resolved relative to this directory
hash: md5Hex(JSON.stringify(baseOptions)),
diff --git a/node_modules/ava/lib/beautify-stack.js b/node_modules/ava/lib/beautify-stack.js
index 189ed0714..4ae8c04be 100644
--- a/node_modules/ava/lib/beautify-stack.js
+++ b/node_modules/ava/lib/beautify-stack.js
@@ -8,6 +8,7 @@ let ignoreStackLines = [];
const avaInternals = /\/ava\/(?:lib\/)?[\w-]+\.js:\d+:\d+\)?$/;
const avaDependencies = /\/node_modules\/(?:bluebird|empower-core|(?:ava\/node_modules\/)?(?:babel-runtime|core-js))\//;
+const stackFrameLine = /^.+( \(.+:\d+:\d+\)|:\d+:\d+)$/;
if (!debug.enabled) {
ignoreStackLines = StackUtils.nodeInternals();
@@ -17,21 +18,55 @@ if (!debug.enabled) {
const stackUtils = new StackUtils({internals: ignoreStackLines});
+function extractFrames(stack) {
+ return stack
+ .split('\n')
+ .map(line => line.trim())
+ .filter(line => stackFrameLine.test(line))
+ .join('\n');
+}
+
+/**
+ * Given a string value of the format generated for the `stack` property of a
+ * V8 error object, return a string that contains only stack frame information
+ * for frames that have relevance to the consumer.
+ *
+ * For example, given the following string value:
+ *
+ * ```
+ * Error
+ * at inner (/home/ava/ex.js:7:12)
+ * at /home/ava/ex.js:12:5
+ * at outer (/home/ava/ex.js:13:4)
+ * at Object.<anonymous> (/home/ava/ex.js:14:3)
+ * at Module._compile (module.js:570:32)
+ * at Object.Module._extensions..js (module.js:579:10)
+ * at Module.load (module.js:487:32)
+ * at tryModuleLoad (module.js:446:12)
+ * at Function.Module._load (module.js:438:3)
+ * at Module.runMain (module.js:604:10)
+ * ```
+ *
+ * ...this function returns the following string value:
+ *
+ * ```
+ * inner (/home/ava/ex.js:7:12)
+ * /home/ava/ex.js:12:5
+ * outer (/home/ava/ex.js:13:4)
+ * Object.<anonymous> (/home/ava/ex.js:14:3)
+ * ```
+ */
module.exports = stack => {
if (!stack) {
return '';
}
+ stack = extractFrames(stack);
// Workaround for https://github.com/tapjs/stack-utils/issues/14
// TODO: fix it in `stack-utils`
stack = cleanStack(stack);
- const title = stack.split('\n')[0];
- const lines = stackUtils
- .clean(stack)
- .split('\n')
- .map(x => ` ${x}`)
- .join('\n');
-
- return `${title}\n${lines}`;
+ return stackUtils.clean(stack)
+ // Remove the trailing newline inserted by the `stack-utils` module
+ .trim();
};
diff --git a/node_modules/ava/lib/caching-precompiler.js b/node_modules/ava/lib/caching-precompiler.js
index 937309bf0..f6e5e47c3 100644
--- a/node_modules/ava/lib/caching-precompiler.js
+++ b/node_modules/ava/lib/caching-precompiler.js
@@ -33,6 +33,7 @@ class CachingPrecompiler {
this.fileHashes = {};
this.transform = this._createTransform();
}
+
precompileFile(filePath) {
if (!this.fileHashes[filePath]) {
const source = stripBomBuf(fs.readFileSync(filePath));
@@ -41,11 +42,13 @@ class CachingPrecompiler {
return this.fileHashes[filePath];
}
+
// Conditionally called by caching-transform when precompiling is required
_init() {
this.babel = require('babel-core');
return this._transform;
}
+
_transform(code, filePath, hash) {
code = code.toString();
@@ -79,6 +82,7 @@ class CachingPrecompiler {
return `${result.code}\n${comment}`;
}
+
_createTransform() {
const salt = packageHash.sync([
require.resolve('../package.json'),
@@ -93,6 +97,7 @@ class CachingPrecompiler {
ext: '.js'
});
}
+
_generateHash(code, filePath, salt) {
const hash = md5Hex([code, filePath, salt]);
this.fileHashes[filePath] = hash;
diff --git a/node_modules/ava/lib/cli.js b/node_modules/ava/lib/cli.js
index 5649a8190..0c2c2f82f 100644
--- a/node_modules/ava/lib/cli.js
+++ b/node_modules/ava/lib/cli.js
@@ -43,7 +43,7 @@ exports.run = () => {
--match, -m Only run tests with matching title (Can be repeated)
--watch, -w Re-run tests when tests and source files change
--timeout, -T Set global timeout
- --concurrency, -c Maximum number of test files running at the same time (EXPERIMENTAL)
+ --concurrency, -c Max number of test files running at the same time (Default: CPU cores)
--update-snapshots, -u Update snapshots
Examples
@@ -75,7 +75,7 @@ exports.run = () => {
],
default: {
cache: conf.cache,
- color: 'color' in conf ? conf.color : require('supports-color') !== false,
+ color: 'color' in conf ? conf.color : require('supports-color').stdout !== false,
concurrency: conf.concurrency,
failFast: conf.failFast,
init: conf.init,
@@ -119,7 +119,12 @@ exports.run = () => {
}
if (cli.flags.concurrency === '') {
- throw new Error(colors.error(figures.cross) + ' The --concurrency and -c flags must be provided the maximum number of test files to run at once.');
+ throw new Error(colors.error(figures.cross) + ' The --concurrency and -c flags must be provided.');
+ }
+
+ if (cli.flags.concurrency &&
+ (!Number.isInteger(Number.parseFloat(cli.flags.concurrency)) || parseInt(cli.flags.concurrency, 10) < 0)) {
+ throw new Error(colors.error(figures.cross) + ' The --concurrency and -c flags must be a nonnegative integer.');
}
if (hasFlag('--require') || hasFlag('-r')) {
@@ -144,6 +149,7 @@ exports.run = () => {
timeout: conf.timeout,
concurrency: conf.concurrency ? parseInt(conf.concurrency, 10) : 0,
updateSnapshots: conf.updateSnapshots,
+ snapshotDir: conf.snapshotDir ? path.resolve(projectDir, conf.snapshotDir) : null,
color: conf.color
});
@@ -152,7 +158,7 @@ exports.run = () => {
if (conf.tap && !conf.watch) {
reporter = new TapReporter();
} else if (conf.verbose || isCi) {
- reporter = new VerboseReporter({color: conf.color});
+ reporter = new VerboseReporter({color: conf.color, watching: conf.watch});
} else {
reporter = new MiniReporter({color: conf.color, watching: conf.watch});
}
diff --git a/node_modules/ava/lib/colors.js b/node_modules/ava/lib/colors.js
index 74be14bb1..75fb4d8aa 100644
--- a/node_modules/ava/lib/colors.js
+++ b/node_modules/ava/lib/colors.js
@@ -2,6 +2,7 @@
const chalk = require('chalk');
module.exports = {
+ log: chalk.gray,
title: chalk.bold.white,
error: chalk.red,
skip: chalk.yellow,
diff --git a/node_modules/ava/lib/enhance-assert.js b/node_modules/ava/lib/enhance-assert.js
index 6e127b3d6..6991caf40 100644
--- a/node_modules/ava/lib/enhance-assert.js
+++ b/node_modules/ava/lib/enhance-assert.js
@@ -1,6 +1,7 @@
'use strict';
const concordance = require('concordance');
const dotProp = require('dot-prop');
+const generate = require('babel-generator').default;
const concordanceOptions = require('./concordance-options').default;
// When adding patterns, don't forget to add to
@@ -15,31 +16,16 @@ const PATTERNS = [
't.notRegex(contents, regex, [message])'
];
-const isRangeMatch = (a, b) => {
- return (a[0] === b[0] && a[1] === b[1]) ||
- (a[0] > b[0] && a[0] < b[1]) ||
- (a[1] > b[0] && a[1] < b[1]);
-};
-
-const computeStatement = (tokens, range) => {
- return tokens
- .filter(token => isRangeMatch(token.range, range))
- .map(token => token.value === undefined ? token.type.label : token.value)
- .join('');
-};
-
+const computeStatement = node => generate(node, {quotes: 'single'}).code;
const getNode = (ast, path) => dotProp.get(ast, path.replace(/\//g, '.'));
const formatter = context => {
const ast = JSON.parse(context.source.ast);
- const tokens = JSON.parse(context.source.tokens);
const args = context.args[0].events;
-
return args
.map(arg => {
- const range = getNode(ast, arg.espath).range;
- const statement = computeStatement(tokens, range);
-
+ const node = getNode(ast, arg.espath);
+ const statement = computeStatement(node);
const formatted = concordance.format(arg.value, concordanceOptions);
return [statement, formatted];
})
diff --git a/node_modules/ava/lib/extract-stack.js b/node_modules/ava/lib/extract-stack.js
deleted file mode 100644
index 64f63db1c..000000000
--- a/node_modules/ava/lib/extract-stack.js
+++ /dev/null
@@ -1,10 +0,0 @@
-'use strict';
-const stackLineRegex = /^.+ \(.+:[0-9]+:[0-9]+\)$/;
-
-module.exports = stack => {
- return stack
- .split('\n')
- .filter(line => stackLineRegex.test(line))
- .map(line => line.trim())
- .join('\n');
-};
diff --git a/node_modules/ava/lib/fork.js b/node_modules/ava/lib/fork.js
index bf918d391..0ca0f45a4 100644
--- a/node_modules/ava/lib/fork.js
+++ b/node_modules/ava/lib/fork.js
@@ -10,12 +10,10 @@ if (fs.realpathSync(__filename) !== __filename) {
console.warn('WARNING: `npm link ava` and the `--preserve-symlink` flag are incompatible. We have detected that AVA is linked via `npm link`, and that you are using either an early version of Node 6, or the `--preserve-symlink` flag. This breaks AVA. You should upgrade to Node 6.2.0+, avoid the `--preserve-symlink` flag, or avoid using `npm link ava`.');
}
-let env = process.env;
+const env = Object.assign({NODE_ENV: 'test'}, process.env);
// Ensure NODE_PATH paths are absolute
if (env.NODE_PATH) {
- env = Object.assign({}, env);
-
env.NODE_PATH = env.NODE_PATH
.split(path.delimiter)
.map(x => path.resolve(x))
diff --git a/node_modules/ava/lib/logger.js b/node_modules/ava/lib/logger.js
index 54bd23c94..e8edb120f 100644
--- a/node_modules/ava/lib/logger.js
+++ b/node_modules/ava/lib/logger.js
@@ -6,6 +6,7 @@ class Logger {
this.reporter = reporter;
autoBind(this);
}
+
start(runStatus) {
if (!this.reporter.start) {
return;
@@ -13,6 +14,7 @@ class Logger {
this.write(this.reporter.start(runStatus), runStatus);
}
+
reset(runStatus) {
if (!this.reporter.reset) {
return;
@@ -20,9 +22,11 @@ class Logger {
this.write(this.reporter.reset(runStatus), runStatus);
}
+
test(test, runStatus) {
this.write(this.reporter.test(test, runStatus), runStatus);
}
+
unhandledError(err, runStatus) {
if (!this.reporter.unhandledError) {
return;
@@ -30,6 +34,7 @@ class Logger {
this.write(this.reporter.unhandledError(err, runStatus), runStatus);
}
+
finish(runStatus) {
if (!this.reporter.finish) {
return;
@@ -37,6 +42,7 @@ class Logger {
this.write(this.reporter.finish(runStatus), runStatus);
}
+
section() {
if (!this.reporter.section) {
return;
@@ -44,6 +50,7 @@ class Logger {
this.write(this.reporter.section());
}
+
clear() {
if (!this.reporter.clear) {
return false;
@@ -52,6 +59,7 @@ class Logger {
this.write(this.reporter.clear());
return true;
}
+
write(str, runStatus) {
if (typeof str === 'undefined') {
return;
@@ -59,6 +67,7 @@ class Logger {
this.reporter.write(str, runStatus);
}
+
stdout(data, runStatus) {
if (!this.reporter.stdout) {
return;
@@ -66,6 +75,7 @@ class Logger {
this.reporter.stdout(data, runStatus);
}
+
stderr(data, runStatus) {
if (!this.reporter.stderr) {
return;
@@ -73,6 +83,7 @@ class Logger {
this.reporter.stderr(data, runStatus);
}
+
exit(code) {
process.exit(code); // eslint-disable-line unicorn/no-process-exit
}
diff --git a/node_modules/ava/lib/main.js b/node_modules/ava/lib/main.js
index 1b03cc854..4c5fdc373 100644
--- a/node_modules/ava/lib/main.js
+++ b/node_modules/ava/lib/main.js
@@ -13,7 +13,8 @@ const runner = new Runner({
match: opts.match,
projectDir: opts.projectDir,
serial: opts.serial,
- updateSnapshots: opts.updateSnapshots
+ updateSnapshots: opts.updateSnapshots,
+ snapshotDir: opts.snapshotDir
});
worker.setRunner(runner);
diff --git a/node_modules/ava/lib/reporters/improper-usage-messages.js b/node_modules/ava/lib/reporters/improper-usage-messages.js
index 298ef79a5..014a4bf0d 100644
--- a/node_modules/ava/lib/reporters/improper-usage-messages.js
+++ b/node_modules/ava/lib/reporters/improper-usage-messages.js
@@ -15,7 +15,9 @@ exports.forError = error => {
Visit the following URL for more details:
${chalk.blue.underline('https://github.com/avajs/ava#throwsfunctionpromise-error-message')}`;
- } else if (assertion === 'snapshot') {
+ }
+
+ if (assertion === 'snapshot') {
const name = error.improperUsage.name;
const snapPath = error.improperUsage.snapPath;
diff --git a/node_modules/ava/lib/reporters/mini.js b/node_modules/ava/lib/reporters/mini.js
index 8acfab8e7..a21d02a6b 100644
--- a/node_modules/ava/lib/reporters/mini.js
+++ b/node_modules/ava/lib/reporters/mini.js
@@ -5,12 +5,12 @@ const lastLineTracker = require('last-line-stream/tracker');
const plur = require('plur');
const spinners = require('cli-spinners');
const chalk = require('chalk');
+const figures = require('figures');
const cliTruncate = require('cli-truncate');
const cross = require('figures').cross;
const indentString = require('indent-string');
const ansiEscapes = require('ansi-escapes');
const trimOffNewlines = require('trim-off-newlines');
-const extractStack = require('../extract-stack');
const codeExcerpt = require('../code-excerpt');
const colors = require('../colors');
const formatSerializedError = require('./format-serialized-error');
@@ -33,6 +33,7 @@ class MiniReporter {
this.stream = process.stderr;
this.stringDecoder = new StringDecoder();
}
+
start() {
this.interval = setInterval(() => {
this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
@@ -41,6 +42,7 @@ class MiniReporter {
return this.prefix('');
}
+
reset() {
this.clearInterval();
this.passCount = 0;
@@ -56,13 +58,16 @@ class MiniReporter {
this.spinnerIndex = 0;
this.lastLineTracker = lastLineTracker();
}
+
spinnerChar() {
return this.spinnerFrames[this.spinnerIndex];
}
+
clearInterval() {
clearInterval(this.interval);
this.interval = null;
}
+
test(test) {
if (test.todo) {
this.todoCount++;
@@ -83,6 +88,7 @@ class MiniReporter {
return this.prefix(this._test(test));
}
+
prefix(str) {
str = str || this.currentTest;
this.currentTest = str;
@@ -91,6 +97,7 @@ class MiniReporter {
// TODO(jamestalmage): Figure out why it's needed and document it here
return ` \n ${this.spinnerChar()} ${str}`;
}
+
_test(test) {
const SPINNER_WIDTH = 3;
const PADDING = 1;
@@ -102,6 +109,7 @@ class MiniReporter {
return title + '\n' + this.reportCounts();
}
+
unhandledError(err) {
if (err.type === 'exception') {
this.exceptionCount++;
@@ -109,6 +117,7 @@ class MiniReporter {
this.rejectionCount++;
}
}
+
reportCounts(time) {
const lines = [
this.passCount > 0 ? '\n ' + colors.pass(this.passCount, 'passed') : '',
@@ -124,6 +133,7 @@ class MiniReporter {
return lines.join('');
}
+
finish(runStatus) {
this.clearInterval();
let time;
@@ -163,6 +173,21 @@ class MiniReporter {
}
status += ' ' + colors.title(test.title) + '\n';
+
+ if (test.logs) {
+ test.logs.forEach(log => {
+ const logLines = indentString(colors.log(log), 6);
+ const logLinesWithLeadingFigure = logLines.replace(
+ /^ {6}/,
+ ` ${colors.information(figures.info)} `
+ );
+
+ status += logLinesWithLeadingFigure + '\n';
+ });
+
+ status += '\n';
+ }
+
if (test.error.source) {
status += ' ' + colors.errorSource(test.error.source.file + ':' + test.error.source.line) + '\n';
@@ -191,9 +216,9 @@ class MiniReporter {
}
if (test.error.stack) {
- const extracted = extractStack(test.error.stack);
- if (extracted.includes('\n')) {
- status += '\n' + indentString(colors.errorStack(extracted), 2) + '\n';
+ const stack = test.error.stack;
+ if (stack.includes('\n')) {
+ status += '\n' + indentString(colors.errorStack(stack), 2) + '\n';
}
}
@@ -212,14 +237,14 @@ class MiniReporter {
status += ' ' + colors.error(cross + ' ' + err.message) + '\n\n';
} else {
const title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
- let description = err.stack ? err.stack.trimRight() : JSON.stringify(err);
- description = description.split('\n');
- const errorTitle = err.name ? description[0] : 'Threw non-error: ' + description[0];
- const errorStack = description.slice(1).join('\n');
-
status += ' ' + colors.title(title) + '\n';
- status += ' ' + colors.stack(errorTitle) + '\n';
- status += colors.errorStack(errorStack) + '\n\n';
+
+ if (err.name) {
+ status += ' ' + colors.stack(err.summary) + '\n';
+ status += colors.errorStack(err.stack) + '\n\n';
+ } else {
+ status += ' Threw non-error: ' + err.summary + '\n';
+ }
}
});
}
@@ -235,24 +260,30 @@ class MiniReporter {
return '\n' + trimOffNewlines(status) + '\n';
}
+
section() {
return '\n' + chalk.gray.dim('\u2500'.repeat(process.stdout.columns || 80));
}
+
clear() {
return '';
}
+
write(str) {
cliCursor.hide();
this.currentStatus = str;
this._update();
this.statusLineCount = this.currentStatus.split('\n').length;
}
+
stdout(data) {
this._update(data);
}
+
stderr(data) {
this._update(data);
}
+
_update(data) {
let str = '';
let ct = this.statusLineCount;
diff --git a/node_modules/ava/lib/reporters/tap.js b/node_modules/ava/lib/reporters/tap.js
index 37c2cfd95..5ef8a23e3 100644
--- a/node_modules/ava/lib/reporters/tap.js
+++ b/node_modules/ava/lib/reporters/tap.js
@@ -3,12 +3,6 @@ const format = require('util').format;
const indentString = require('indent-string');
const stripAnsi = require('strip-ansi');
const yaml = require('js-yaml');
-const extractStack = require('../extract-stack');
-
-// Parses stack trace and extracts original function name, file name and line
-function getSourceFromStack(stack) {
- return extractStack(stack).split('\n')[0];
-}
function dumpError(error, includeMessage) {
const obj = Object.assign({}, error.object);
@@ -35,7 +29,7 @@ function dumpError(error, includeMessage) {
}
if (error.stack) {
- obj.at = getSourceFromStack(error.stack);
+ obj.at = error.stack.split('\n')[0];
}
return ` ---\n${indentString(yaml.safeDump(obj).trim(), 4)}\n ...`;
@@ -45,11 +39,13 @@ class TapReporter {
constructor() {
this.i = 0;
}
+
start() {
return 'TAP version 13';
}
+
test(test) {
- let output;
+ const output = [];
let directive = '';
const passed = test.todo ? 'not ok' : 'ok';
@@ -62,21 +58,34 @@ class TapReporter {
const title = stripAnsi(test.title);
+ const appendLogs = () => {
+ if (test.logs) {
+ test.logs.forEach(log => {
+ const logLines = indentString(log, 4);
+ const logLinesWithLeadingFigure = logLines.replace(
+ /^ {4}/,
+ ' * '
+ );
+
+ output.push(logLinesWithLeadingFigure);
+ });
+ }
+ };
+
+ output.push(`# ${title}`);
+
if (test.error) {
- output = [
- '# ' + title,
- format('not ok %d - %s', ++this.i, title),
- dumpError(test.error, true)
- ];
+ output.push(format('not ok %d - %s', ++this.i, title));
+ appendLogs();
+ output.push(dumpError(test.error, true));
} else {
- output = [
- `# ${title}`,
- format('%s %d - %s %s', passed, ++this.i, title, directive).trim()
- ];
+ output.push(format('%s %d - %s %s', passed, ++this.i, title, directive).trim());
+ appendLogs();
}
return output.join('\n');
}
+
unhandledError(err) {
const output = [
`# ${err.message}`,
@@ -89,6 +98,7 @@ class TapReporter {
return output.join('\n');
}
+
finish(runStatus) {
const output = [
'',
@@ -105,12 +115,15 @@ class TapReporter {
return output.join('\n');
}
+
write(str) {
console.log(str);
}
+
stdout(data) {
process.stderr.write(data);
}
+
stderr(data) {
this.stdout(data);
}
diff --git a/node_modules/ava/lib/reporters/verbose.js b/node_modules/ava/lib/reporters/verbose.js
index cd47683e8..c58d8db3b 100644
--- a/node_modules/ava/lib/reporters/verbose.js
+++ b/node_modules/ava/lib/reporters/verbose.js
@@ -5,7 +5,6 @@ const figures = require('figures');
const chalk = require('chalk');
const plur = require('plur');
const trimOffNewlines = require('trim-off-newlines');
-const extractStack = require('../extract-stack');
const codeExcerpt = require('../code-excerpt');
const colors = require('../colors');
const formatSerializedError = require('./format-serialized-error');
@@ -20,34 +19,46 @@ class VerboseReporter {
colors[key].enabled = this.options.color;
}
}
+
start() {
return '';
}
+
test(test, runStatus) {
+ const lines = [];
if (test.error) {
- return ' ' + colors.error(figures.cross) + ' ' + test.title + ' ' + colors.error(test.error.message);
- }
-
- if (test.todo) {
- return ' ' + colors.todo('- ' + test.title);
+ lines.push(' ' + colors.error(figures.cross) + ' ' + test.title + ' ' + colors.error(test.error.message));
+ } else if (test.todo) {
+ lines.push(' ' + colors.todo('- ' + test.title));
} else if (test.skip) {
- return ' ' + colors.skip('- ' + test.title);
- }
+ lines.push(' ' + colors.skip('- ' + test.title));
+ } else if (test.failing) {
+ lines.push(' ' + colors.error(figures.tick) + ' ' + colors.error(test.title));
+ } else if (runStatus.fileCount === 1 && runStatus.testCount === 1 && test.title === '[anonymous]') {
+ // No output
+ } else {
+ // Display duration only over a threshold
+ const threshold = 100;
+ const duration = test.duration > threshold ? colors.duration(' (' + prettyMs(test.duration) + ')') : '';
- if (test.failing) {
- return ' ' + colors.error(figures.tick) + ' ' + colors.error(test.title);
+ lines.push(' ' + colors.pass(figures.tick) + ' ' + test.title + duration);
}
- if (runStatus.fileCount === 1 && runStatus.testCount === 1 && test.title === '[anonymous]') {
- return undefined;
- }
+ if (test.logs) {
+ test.logs.forEach(log => {
+ const logLines = indentString(colors.log(log), 6);
+ const logLinesWithLeadingFigure = logLines.replace(
+ /^ {6}/,
+ ` ${colors.information(figures.info)} `
+ );
- // Display duration only over a threshold
- const threshold = 100;
- const duration = test.duration > threshold ? colors.duration(' (' + prettyMs(test.duration) + ')') : '';
+ lines.push(logLinesWithLeadingFigure);
+ });
+ }
- return ' ' + colors.pass(figures.tick) + ' ' + test.title + duration;
+ return lines.length > 0 ? lines.join('\n') : undefined;
}
+
unhandledError(err) {
if (err.type === 'exception' && err.name === 'AvaError') {
return colors.error(' ' + figures.cross + ' ' + err.message);
@@ -61,6 +72,7 @@ class VerboseReporter {
let output = colors.error(types[err.type] + ':', err.file) + '\n';
if (err.stack) {
+ output += ' ' + colors.stack(err.title || err.summary) + '\n';
output += ' ' + colors.stack(err.stack) + '\n';
} else {
output += ' ' + colors.stack(JSON.stringify(err)) + '\n';
@@ -70,6 +82,7 @@ class VerboseReporter {
return output;
}
+
finish(runStatus) {
let output = '';
@@ -86,7 +99,9 @@ class VerboseReporter {
].filter(Boolean);
if (lines.length > 0) {
- lines[0] += ' ' + chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
+ if (this.options.watching) {
+ lines[0] += ' ' + chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
+ }
output += lines.join('\n') + '\n';
}
@@ -104,6 +119,21 @@ class VerboseReporter {
}
output += ' ' + colors.title(test.title) + '\n';
+
+ if (test.logs) {
+ test.logs.forEach(log => {
+ const logLines = indentString(colors.log(log), 6);
+ const logLinesWithLeadingFigure = logLines.replace(
+ /^ {6}/,
+ ` ${colors.information(figures.info)} `
+ );
+
+ output += logLinesWithLeadingFigure + '\n';
+ });
+
+ output += '\n';
+ }
+
if (test.error.source) {
output += ' ' + colors.errorSource(test.error.source.file + ':' + test.error.source.line) + '\n';
@@ -132,9 +162,9 @@ class VerboseReporter {
}
if (test.error.stack) {
- const extracted = extractStack(test.error.stack);
- if (extracted.includes('\n')) {
- output += '\n' + indentString(colors.errorStack(extracted), 2) + '\n';
+ const stack = test.error.stack;
+ if (stack.includes('\n')) {
+ output += '\n' + indentString(colors.errorStack(stack), 2) + '\n';
}
}
@@ -153,15 +183,19 @@ class VerboseReporter {
return '\n' + trimOffNewlines(output) + '\n';
}
+
section() {
return chalk.gray.dim('\u2500'.repeat(process.stdout.columns || 80));
}
+
write(str) {
console.error(str);
}
+
stdout(data) {
process.stderr.write(data);
}
+
stderr(data) {
process.stderr.write(data);
}
diff --git a/node_modules/ava/lib/run-status.js b/node_modules/ava/lib/run-status.js
index 8e095655a..461ab8f90 100644
--- a/node_modules/ava/lib/run-status.js
+++ b/node_modules/ava/lib/run-status.js
@@ -44,6 +44,7 @@ class RunStatus extends EventEmitter {
autoBind(this);
}
+
observeFork(emitter) {
emitter
.on('teardown', this.handleTeardown)
@@ -54,6 +55,7 @@ class RunStatus extends EventEmitter {
.on('stdout', this.handleOutput.bind(this, 'stdout'))
.on('stderr', this.handleOutput.bind(this, 'stderr'));
}
+
handleRejections(data) {
this.rejectionCount += data.rejections.length;
@@ -64,6 +66,7 @@ class RunStatus extends EventEmitter {
this.errors.push(err);
});
}
+
handleExceptions(data) {
this.exceptionCount++;
const err = data.exception;
@@ -72,10 +75,12 @@ class RunStatus extends EventEmitter {
this.emit('error', err, this);
this.errors.push(err);
}
+
handleTeardown(data) {
this.emit('dependencies', data.file, data.dependencies, this);
this.emit('touchedFiles', data.touchedFiles);
}
+
handleStats(stats) {
this.emit('stats', stats, this);
@@ -85,6 +90,7 @@ class RunStatus extends EventEmitter {
this.testCount += stats.testCount;
}
+
handleTest(test) {
test.title = this.prefixTitle(test.file) + test.title;
@@ -98,6 +104,7 @@ class RunStatus extends EventEmitter {
this.emit('test', test, this);
}
+
prefixTitle(file) {
if (!this.prefixTitles) {
return '';
@@ -107,9 +114,11 @@ class RunStatus extends EventEmitter {
return prefixTitle(file, this.base, separator);
}
+
handleOutput(channel, data) {
this.emit(channel, data, this);
}
+
processResults(results) {
// Assemble stats from all tests
this.stats = results.map(result => result.stats);
diff --git a/node_modules/ava/lib/runner.js b/node_modules/ava/lib/runner.js
index bda2132fd..eb02dde45 100644
--- a/node_modules/ava/lib/runner.js
+++ b/node_modules/ava/lib/runner.js
@@ -52,6 +52,7 @@ class Runner extends EventEmitter {
this.projectDir = options.projectDir;
this.serial = options.serial;
this.updateSnapshots = options.updateSnapshots;
+ this.snapshotDir = options.snapshotDir;
this.hasStarted = false;
this.results = [];
@@ -112,7 +113,7 @@ class Runner extends EventEmitter {
}
if (metadata.type === 'test' && this.match.length > 0) {
- metadata.exclusive = title !== null && matcher([title], this.match).length === 1;
+ metadata.exclusive = matcher([title || ''], this.match).length === 1;
}
const validationError = validateTest(title, fn, metadata);
@@ -130,6 +131,7 @@ class Runner extends EventEmitter {
addTestResult(result) {
const test = result.result;
const props = {
+ logs: test.logs,
duration: test.duration,
title: test.title,
error: result.reason,
@@ -183,6 +185,8 @@ class Runner extends EventEmitter {
compareTestSnapshot(options) {
if (!this.snapshots) {
this.snapshots = snapshotManager.load({
+ file: this.file,
+ fixedLocation: this.snapshotDir,
name: path.basename(this.file),
projectDir: this.projectDir,
relFile: path.relative(this.projectDir, this.file),
@@ -219,6 +223,7 @@ class Runner extends EventEmitter {
});
return Bluebird.try(() => this.tests.build().run());
}
+
attributeLeakedError(err) {
return this.tests.attributeLeakedError(err);
}
diff --git a/node_modules/ava/lib/serialize-error.js b/node_modules/ava/lib/serialize-error.js
index 55717e161..13146ff42 100644
--- a/node_modules/ava/lib/serialize-error.js
+++ b/node_modules/ava/lib/serialize-error.js
@@ -4,7 +4,6 @@ const cleanYamlObject = require('clean-yaml-object');
const StackUtils = require('stack-utils');
const assert = require('./assert');
const beautifyStack = require('./beautify-stack');
-const extractStack = require('./extract-stack');
function isAvaAssertionError(source) {
return source instanceof assert.AssertionError;
@@ -20,7 +19,7 @@ function extractSource(stack) {
return null;
}
- const firstStackLine = extractStack(stack).split('\n')[0];
+ const firstStackLine = stack.split('\n')[0];
return stackUtils.parseLine(firstStackLine);
}
function buildSource(source) {
@@ -90,5 +89,11 @@ module.exports = error => {
}
}
+ if (typeof error.stack === 'string') {
+ retval.summary = error.stack.split('\n')[0];
+ } else {
+ retval.summary = JSON.stringify(error);
+ }
+
return retval;
};
diff --git a/node_modules/ava/lib/snapshot-manager.js b/node_modules/ava/lib/snapshot-manager.js
index ea1246585..fcc24922e 100644
--- a/node_modules/ava/lib/snapshot-manager.js
+++ b/node_modules/ava/lib/snapshot-manager.js
@@ -11,6 +11,7 @@ const indentString = require('indent-string');
const makeDir = require('make-dir');
const md5Hex = require('md5-hex');
const Buffer = require('safe-buffer').Buffer;
+const convertSourceMap = require('convert-source-map');
const concordanceOptions = require('./concordance-options').snapshotManager;
@@ -355,18 +356,39 @@ class Manager {
}
}
-function determineSnapshotDir(projectDir, testDir) {
- const parts = new Set(path.relative(projectDir, testDir).split(path.sep));
+function determineSnapshotDir(options) {
+ const testDir = determineSourceMappedDir(options);
+ if (options.fixedLocation) {
+ const relativeTestLocation = path.relative(options.projectDir, testDir);
+ return path.join(options.fixedLocation, relativeTestLocation);
+ }
+
+ const parts = new Set(path.relative(options.projectDir, testDir).split(path.sep));
if (parts.has('__tests__')) {
return path.join(testDir, '__snapshots__');
- } else if (parts.has('test') || parts.has('tests')) { // Accept tests, even though it's not in the default test patterns
+ }
+ if (parts.has('test') || parts.has('tests')) { // Accept tests, even though it's not in the default test patterns
return path.join(testDir, 'snapshots');
}
+
return testDir;
}
+function determineSourceMappedDir(options) {
+ const source = tryRead(options.file).toString();
+ const converter = convertSourceMap.fromSource(source) || convertSourceMap.fromMapFileSource(source, options.testDir);
+ if (converter) {
+ const map = converter.toObject();
+ const firstSource = `${map.sourceRoot || ''}${map.sources[0]}`;
+ const sourceFile = path.resolve(options.testDir, firstSource);
+ return path.dirname(sourceFile);
+ }
+
+ return options.testDir;
+}
+
function load(options) {
- const dir = determineSnapshotDir(options.projectDir, options.testDir);
+ const dir = determineSnapshotDir(options);
const reportFile = `${options.name}.md`;
const snapFile = `${options.name}.snap`;
const snapPath = path.join(dir, snapFile);
diff --git a/node_modules/ava/lib/test-collection.js b/node_modules/ava/lib/test-collection.js
index 91c604e06..fd34bc489 100644
--- a/node_modules/ava/lib/test-collection.js
+++ b/node_modules/ava/lib/test-collection.js
@@ -33,6 +33,7 @@ class TestCollection extends EventEmitter {
this._emitTestResult = this._emitTestResult.bind(this);
}
+
add(test) {
const metadata = test.metadata;
const type = metadata.type;
@@ -91,6 +92,7 @@ class TestCollection extends EventEmitter {
this.tests.concurrent.push(test);
}
}
+
_skippedTest(test) {
return {
run: () => {
@@ -103,10 +105,12 @@ class TestCollection extends EventEmitter {
}
};
}
+
_emitTestResult(result) {
this.pendingTestInstances.delete(result.result);
this.emit('test', result);
}
+
_buildHooks(hooks, testTitle, context) {
return hooks.map(hook => {
const test = this._buildHook(hook, testTitle, context);
@@ -118,6 +122,7 @@ class TestCollection extends EventEmitter {
return test;
});
}
+
_buildHook(hook, testTitle, contextRef) {
let title = hook.title;
@@ -141,6 +146,7 @@ class TestCollection extends EventEmitter {
this.pendingTestInstances.add(test);
return test;
}
+
_buildTest(test, contextRef) {
if (!contextRef) {
contextRef = null;
@@ -158,6 +164,7 @@ class TestCollection extends EventEmitter {
this.pendingTestInstances.add(test);
return test;
}
+
_buildTestWithHooks(test) {
if (test.metadata.skipped || test.metadata.todo) {
return new Sequence([this._skippedTest(this._buildTest(test))], true);
@@ -175,24 +182,41 @@ class TestCollection extends EventEmitter {
}
return sequence;
}
+
_buildTests(tests) {
return tests.map(test => this._buildTestWithHooks(test));
}
- build() {
- const beforeHooks = new Sequence(this._buildHooks(this.hooks.before));
- const afterHooks = new Sequence(this._buildHooks(this.hooks.after));
+ _hasUnskippedTests() {
+ return this.tests.serial.concat(this.tests.concurrent)
+ .some(test => {
+ return !(test.metadata && test.metadata.skipped === true);
+ });
+ }
+
+ build() {
const serialTests = new Sequence(this._buildTests(this.tests.serial), this.bail);
const concurrentTests = new Concurrent(this._buildTests(this.tests.concurrent), this.bail);
const allTests = new Sequence([serialTests, concurrentTests]);
- let finalTests = new Sequence([beforeHooks, allTests, afterHooks], true);
+ let finalTests;
+ // Only run before and after hooks when there are unskipped tests
+ if (this._hasUnskippedTests()) {
+ const beforeHooks = new Sequence(this._buildHooks(this.hooks.before));
+ const afterHooks = new Sequence(this._buildHooks(this.hooks.after));
+ finalTests = new Sequence([beforeHooks, allTests, afterHooks], true);
+ } else {
+ finalTests = new Sequence([allTests], true);
+ }
+
if (this.hooks.afterAlways.length > 0) {
const afterAlwaysHooks = new Sequence(this._buildHooks(this.hooks.afterAlways));
finalTests = new Sequence([finalTests, afterAlwaysHooks], false);
}
+
return finalTests;
}
+
attributeLeakedError(err) {
for (const test of this.pendingTestInstances) {
if (test.attributeLeakedError(err)) {
diff --git a/node_modules/ava/lib/test-worker.js b/node_modules/ava/lib/test-worker.js
index 0061775f0..84fba364e 100644
--- a/node_modules/ava/lib/test-worker.js
+++ b/node_modules/ava/lib/test-worker.js
@@ -2,7 +2,6 @@
// Check if the test is being run without AVA cli
{
- /* eslint-disable import/order */
const path = require('path');
const chalk = require('chalk');
@@ -17,22 +16,18 @@
}
}
+const currentlyUnhandled = require('currently-unhandled')();
+const isObj = require('is-obj');
+
const adapter = require('./process-adapter');
const globals = require('./globals');
const opts = adapter.opts;
globals.options = opts;
-/* eslint-enable import/order */
-const Bluebird = require('bluebird');
-const currentlyUnhandled = require('currently-unhandled')();
-const isObj = require('is-obj');
const serializeError = require('./serialize-error');
-// Bluebird specific
-Bluebird.longStackTraces();
-
-(opts.require || []).forEach(require);
+(opts.require || []).forEach(x => require(x));
adapter.installSourceMapSupport();
adapter.installPrecompilerHook();
diff --git a/node_modules/ava/lib/test.js b/node_modules/ava/lib/test.js
index 58be54d32..839101b40 100644
--- a/node_modules/ava/lib/test.js
+++ b/node_modules/ava/lib/test.js
@@ -71,6 +71,7 @@ class ExecutionContext {
_throwsArgStart(assertion, file, line) {
this._test.trackThrows({assertion, file, line});
}
+
_throwsArgEnd() {
this._test.trackThrows(null);
}
@@ -78,6 +79,10 @@ class ExecutionContext {
{
const assertions = assert.wrapAssertions({
+ log(executionContext, text) {
+ executionContext._test.addLog(text);
+ },
+
pass(executionContext) {
executionContext._test.countPassedAssertion();
},
@@ -108,6 +113,7 @@ class Test {
this.metadata = options.metadata;
this.onResult = options.onResult;
this.title = options.title;
+ this.logs = [];
this.snapshotInvocationCount = 0;
this.compareWithSnapshot = assertionOptions => {
@@ -175,6 +181,10 @@ class Test {
this.assertCount++;
}
+ addLog(text) {
+ this.logs.push(text);
+ }
+
addPendingAssertion(promise) {
if (this.finishing) {
this.saveFirstError(new Error('Assertion passed, but test has already finished'));
diff --git a/node_modules/ava/lib/watcher.js b/node_modules/ava/lib/watcher.js
index c90c810f0..3f5ed3ee7 100644
--- a/node_modules/ava/lib/watcher.js
+++ b/node_modules/ava/lib/watcher.js
@@ -25,6 +25,7 @@ class Debouncer {
this.timer = null;
this.repeat = false;
}
+
debounce(delay) {
if (this.timer) {
this.again = true;
@@ -55,6 +56,7 @@ class Debouncer {
this.timer = timer;
}
+
cancel() {
if (this.timer) {
clearTimeout(this.timer);
@@ -69,6 +71,7 @@ class TestDependency {
this.file = file;
this.sources = sources;
}
+
contains(source) {
return this.sources.indexOf(source) !== -1;
}
@@ -146,6 +149,7 @@ class Watcher {
this.watchFiles();
this.rerunAll();
}
+
watchFiles() {
const patterns = this.avaFiles.getChokidarPatterns();
@@ -160,16 +164,18 @@ class Watcher {
}
});
}
+
trackTestDependencies(api) {
const relative = absPath => nodePath.relative(process.cwd(), absPath);
api.on('test-run', runStatus => {
runStatus.on('dependencies', (file, dependencies) => {
- const sourceDeps = dependencies.map(relative).filter(this.avaFiles.isSource);
+ const sourceDeps = dependencies.map(x => relative(x)).filter(this.avaFiles.isSource);
this.updateTestDependencies(file, sourceDeps);
});
});
}
+
updateTestDependencies(file, sources) {
if (sources.length === 0) {
this.testDependencies = this.testDependencies.filter(dep => dep.file !== file);
@@ -190,6 +196,7 @@ class Watcher {
this.testDependencies.push(new TestDependency(file, sources));
}
}
+
trackTouchedFiles(api) {
api.on('test-run', runStatus => {
runStatus.on('touchedFiles', files => {
@@ -199,11 +206,13 @@ class Watcher {
});
});
}
+
trackExclusivity(api) {
api.on('stats', stats => {
this.updateExclusivity(stats.file, stats.hasExclusive);
});
}
+
updateExclusivity(file, hasExclusiveTests) {
const index = this.filesWithExclusiveTests.indexOf(file);
@@ -213,6 +222,7 @@ class Watcher {
this.filesWithExclusiveTests.splice(index, 1);
}
}
+
trackFailures(api) {
api.on('test-run', (runStatus, files) => {
files.forEach(file => {
@@ -230,9 +240,11 @@ class Watcher {
});
});
}
+
pruneFailures(file) {
this.filesWithFailures = this.filesWithFailures.filter(state => state.file !== file);
}
+
countFailure(file, vector) {
const isUpdate = this.filesWithFailures.some(state => {
if (state.file !== file) {
@@ -251,6 +263,7 @@ class Watcher {
});
}
}
+
sumPreviousFailures(beforeVector) {
let total = 0;
@@ -262,6 +275,7 @@ class Watcher {
return total;
}
+
cleanUnlinkedTests(unlinkedTests) {
unlinkedTests.forEach(testFile => {
this.updateTestDependencies(testFile, []);
@@ -269,6 +283,7 @@ class Watcher {
this.pruneFailures(testFile);
});
}
+
observeStdin(stdin) {
stdin.resume();
stdin.setEncoding('utf8');
@@ -295,14 +310,17 @@ class Watcher {
});
});
}
+
rerunAll() {
this.dirtyStates = {};
this.run();
}
+
updatePreviousSnapshots() {
this.dirtyStates = {};
this.run(this.previousFiles, true);
}
+
runAfterChanges() {
const dirtyStates = this.dirtyStates;
this.dirtyStates = {};