Skip to content

Commit 4f093ab

Browse files
Remove snapshot files when a test file stops using snapshots
Fixes #1424. Co-authored-by: Mark Wubben <[email protected]>
1 parent 98595da commit 4f093ab

File tree

36 files changed

+710
-5
lines changed

36 files changed

+710
-5
lines changed

lib/runner.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class Runner extends Emittery {
2929

3030
this.activeRunnables = new Set();
3131
this.boundCompareTestSnapshot = this.compareTestSnapshot.bind(this);
32+
this.skippedSnapshots = false;
33+
this.boundSkipSnapshot = this.skipSnapshot.bind(this);
3234
this.interrupted = false;
3335
this.snapshots = null;
3436
this.nextTaskIndex = 0;
@@ -199,8 +201,19 @@ class Runner extends Emittery {
199201
return this.snapshots.compare(options);
200202
}
201203

204+
skipSnapshot() {
205+
this.skippedSnapshots = true;
206+
}
207+
202208
saveSnapshotState() {
203-
if (this.updateSnapshots && (this.runOnlyExclusive || this.skippingTests)) {
209+
if (
210+
this.updateSnapshots &&
211+
(
212+
this.runOnlyExclusive ||
213+
this.skippingTests ||
214+
this.skippedSnapshots
215+
)
216+
) {
204217
return {cannotSave: true};
205218
}
206219

@@ -209,9 +222,11 @@ class Runner extends Emittery {
209222
}
210223

211224
if (this.updateSnapshots) {
212-
// TODO: There may be unused snapshot files if no test caused the
213-
// snapshots to be loaded. Prune them. But not if tests (including hooks!)
214-
// were skipped. Perhaps emit a warning if this occurs?
225+
return {touchedFiles: snapshotManager.cleanSnapshots({
226+
file: this.file,
227+
fixedLocation: this.snapshotDir,
228+
projectDir: this.projectDir
229+
})};
215230
}
216231

217232
return {};
@@ -297,6 +312,7 @@ class Runner extends Emittery {
297312
task.implementation :
298313
t => task.implementation.apply(null, [t].concat(task.args)),
299314
compareTestSnapshot: this.boundCompareTestSnapshot,
315+
skipSnapshot: this.boundSkipSnapshot,
300316
updateSnapshots: this.updateSnapshots,
301317
metadata: {...task.metadata, associatedTaskIndex},
302318
powerAssert: this.powerAssert,
@@ -349,6 +365,7 @@ class Runner extends Emittery {
349365
task.implementation :
350366
t => task.implementation.apply(null, [t].concat(task.args)),
351367
compareTestSnapshot: this.boundCompareTestSnapshot,
368+
skipSnapshot: this.boundSkipSnapshot,
352369
updateSnapshots: this.updateSnapshots,
353370
metadata: task.metadata,
354371
powerAssert: this.powerAssert,

lib/snapshot-manager.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,12 +449,49 @@ const determineSnapshotDir = mem(({file, fixedLocation, projectDir}) => {
449449

450450
exports.determineSnapshotDir = determineSnapshotDir;
451451

452-
function load({file, fixedLocation, projectDir, recordNewSnapshots, updating}) {
452+
function determineSnapshotPaths({file, fixedLocation, projectDir}) {
453453
const dir = determineSnapshotDir({file, fixedLocation, projectDir});
454454
const relFile = path.relative(projectDir, resolveSourceFile(file));
455455
const name = path.basename(relFile);
456456
const reportFile = `${name}.md`;
457457
const snapFile = `${name}.snap`;
458+
459+
return {
460+
dir,
461+
relFile,
462+
snapFile,
463+
reportFile
464+
};
465+
}
466+
467+
function cleanFile(file) {
468+
try {
469+
fs.unlinkSync(file);
470+
return [file];
471+
} catch (error) {
472+
if (error.code === 'ENOENT') {
473+
return [];
474+
}
475+
476+
throw error;
477+
}
478+
}
479+
480+
// Remove snapshot and report if they exist. Returns an array containing the
481+
// paths of the touched files.
482+
function cleanSnapshots({file, fixedLocation, projectDir}) {
483+
const {dir, snapFile, reportFile} = determineSnapshotPaths({file, fixedLocation, projectDir});
484+
485+
return [
486+
...cleanFile(path.join(dir, snapFile)),
487+
...cleanFile(path.join(dir, reportFile))
488+
];
489+
}
490+
491+
exports.cleanSnapshots = cleanSnapshots;
492+
493+
function load({file, fixedLocation, projectDir, recordNewSnapshots, updating}) {
494+
const {dir, relFile, snapFile, reportFile} = determineSnapshotPaths({file, fixedLocation, projectDir});
458495
const snapPath = path.join(dir, snapFile);
459496

460497
let appendOnly = !updating;

lib/test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,10 @@ class Test {
249249
};
250250

251251
this.skipSnapshot = () => {
252+
if (typeof options.skipSnapshot === 'function') {
253+
options.skipSnapshot();
254+
}
255+
252256
if (options.updateSnapshots) {
253257
this.addFailedAssertion(new Error('Snapshot assertions cannot be skipped when updating snapshots'));
254258
} else {

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
"delay": "^4.4.0",
124124
"esm": "^3.2.25",
125125
"execa": "^5.0.0",
126+
"fs-extra": "^9.0.1",
126127
"get-stream": "^6.0.0",
127128
"it-first": "^1.0.4",
128129
"proxyquire": "^2.1.3",
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const test = require('../../../..');
2+
3+
if (process.env.TEMPLATE) {
4+
test('test title', t => {
5+
t.snapshot({foo: 'bar'});
6+
t.snapshot({answer: 42});
7+
t.pass();
8+
});
9+
10+
test('another test', t => {
11+
t.snapshot(new Map());
12+
});
13+
} else {
14+
test('test title', t => {
15+
t.pass();
16+
});
17+
18+
test('another test', t => {
19+
t.pass();
20+
});
21+
}

test-tap/integration/watcher.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,52 @@ test('watcher does not rerun test files when they write snapshot files', t => {
8282
});
8383
});
8484

85+
test('watcher does not rerun test files when they unlink snapshot files', t => {
86+
// Run fixture as template to generate snapshots
87+
execCli(
88+
['--update-snapshots'],
89+
{
90+
dirname: 'fixture/snapshots/watcher-rerun-unlink',
91+
env: {AVA_FORCE_CI: 'not-ci', TEMPLATE: 'true'}
92+
},
93+
err => {
94+
t.ifError(err);
95+
96+
// Run fixture in watch mode; snapshots should be removed, and watcher should not rerun
97+
let killed = false;
98+
99+
const child = execCli(
100+
['--verbose', '--watch', '--update-snapshots', 'test.js'],
101+
{
102+
dirname: 'fixture/snapshots/watcher-rerun-unlink',
103+
env: {AVA_FORCE_CI: 'not-ci'}
104+
},
105+
err => {
106+
t.ok(killed);
107+
t.ifError(err);
108+
t.end();
109+
}
110+
);
111+
112+
let buffer = '';
113+
let passedFirst = false;
114+
child.stdout.on('data', string => {
115+
buffer += string;
116+
if (buffer.includes('2 tests passed') && !passedFirst) {
117+
buffer = '';
118+
passedFirst = true;
119+
setTimeout(() => {
120+
child.kill();
121+
killed = true;
122+
}, 500);
123+
} else if (passedFirst && !killed) {
124+
t.is(buffer.replace(/\s/g, '').replace(END_MESSAGE.replace(/\s/g, ''), ''), '');
125+
}
126+
});
127+
}
128+
);
129+
});
130+
85131
test('watcher does not rerun test files when ignored files change', t => {
86132
let killed = false;
87133

test-tap/test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,36 @@ test('snapshot assertion can be skipped', t => {
696696
});
697697
});
698698

699+
// Snapshots reused from test/assert.js
700+
test('snapshot assertions call options.skipSnapshot when skipped', async t => {
701+
const projectDir = path.join(__dirname, 'fixture');
702+
const manager = snapshotManager.load({
703+
file: path.join(projectDir, 'assert.js'),
704+
projectDir,
705+
fixedLocation: null,
706+
updating: false
707+
});
708+
709+
const skipSnapshot = sinon.spy();
710+
711+
const test = new Test({
712+
compareTestSnapshot: options => manager.compare(options),
713+
skipSnapshot,
714+
updateSnapshots: false,
715+
metadata: {},
716+
title: 'passes',
717+
fn(t) {
718+
t.snapshot.skip({not: {a: 'match'}});
719+
t.snapshot.skip({not: {b: 'match'}});
720+
t.snapshot(React.createElement(HelloMessage, {name: 'Sindre'}));
721+
}
722+
});
723+
724+
await test.run();
725+
726+
t.true(skipSnapshot.calledTwice);
727+
});
728+
699729
test('snapshot assertion cannot be skipped when updating snapshots', t => {
700730
return new Test({
701731
updateSnapshots: true,
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"ava": {
3+
"snapshotDir": "fixedSnapshotDir"
4+
}
5+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const test = require(process.env.AVA_PATH); // This fixture is copied to a temporary directory, so require AVA through its configured path.
2+
3+
if (process.env.TEMPLATE) {
4+
test('some snapshots', t => {
5+
t.snapshot('foo');
6+
t.snapshot('bar');
7+
t.pass();
8+
});
9+
10+
test('another snapshot', t => {
11+
t.snapshot('baz');
12+
t.pass();
13+
});
14+
} else {
15+
test('some snapshots', t => {
16+
t.pass();
17+
});
18+
19+
test('another snapshot', t => {
20+
t.pass();
21+
});
22+
}

0 commit comments

Comments
 (0)