Skip to content

Commit b94fbbd

Browse files
committed
Fail on obsolete snapshots; Use file-specific unique snapshot names
1 parent 4be2387 commit b94fbbd

7 files changed

Lines changed: 99 additions & 57 deletions

File tree

x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export default function apiTest({ getService }: FtrProviderContext) {
2929

3030
expect(body.statusCode).to.be(403);
3131
expect(body.error).to.be('Forbidden');
32+
3233
expectSnapshot(body.message).toMatchInline(
3334
`"To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning."`
3435
);

x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap

Lines changed: 1 addition & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x-pack/test/apm_api_integration/common/match_snapshot.ts

Lines changed: 92 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,62 +9,111 @@ import path from 'path';
99
import expect from '@kbn/expect';
1010
// @ts-expect-error
1111
import prettier from 'prettier';
12-
import { once } from 'lodash';
1312
// @ts-expect-error
1413
import babelTraverse from '@babel/traverse';
14+
import { Suite, Test } from 'mocha';
1515

1616
type ISnapshotState = InstanceType<typeof SnapshotState>;
1717

18-
let testContext: {
19-
file: string;
20-
testTitle: string;
21-
getSnapshotContext: () => SnapshotContext;
22-
} | null = null;
2318
interface SnapshotContext {
2419
snapshotState: ISnapshotState;
20+
currentTestName: string;
2521
}
2622

23+
let testContext: {
24+
file: string;
25+
snapshotTitle: string;
26+
snapshotContext: SnapshotContext;
27+
} | null = null;
28+
2729
let registered: boolean = false;
2830

31+
function getSnapshotMeta(currentTest: Test) {
32+
// Make sure snapshot title is unique per-file, rather than entire
33+
// suite. This allows reuse of tests, for instance to compare
34+
// results for different configurations.
35+
36+
const titles = [currentTest.title];
37+
const file = currentTest.file;
38+
39+
let test: Suite | undefined = currentTest?.parent;
40+
41+
while (test && test.file === file) {
42+
titles.push(test.title);
43+
test = test.parent;
44+
}
45+
46+
const snapshotTitle = titles.reverse().join(' ');
47+
48+
if (!file || !snapshotTitle) {
49+
throw new Error(`file or snapshotTitle not available in Mocha test context`);
50+
}
51+
52+
return {
53+
file,
54+
snapshotTitle,
55+
};
56+
}
57+
2958
export function registerMochaHooksForSnapshots() {
30-
const snapshotsToSave: ISnapshotState[] = [];
59+
let snapshotStatesByFilePath: Record<string, ISnapshotState> = {};
3160

3261
registered = true;
3362

3463
beforeEach(function () {
35-
const mochaContext = this;
36-
const file = mochaContext.currentTest?.file;
37-
const testTitle = mochaContext.currentTest?.fullTitle();
64+
const { file, snapshotTitle } = getSnapshotMeta(this.currentTest!);
3865

39-
if (!file || !testTitle) {
40-
throw new Error(`file or fullTitle not found in Mocha test context`);
66+
if (!snapshotStatesByFilePath[file]) {
67+
snapshotStatesByFilePath[file] = getSnapshotState(file);
4168
}
4269

4370
testContext = {
4471
file,
45-
testTitle,
46-
getSnapshotContext: once(() => {
47-
const ctx = getSnapshotContextOrThrow({ file, testTitle });
48-
snapshotsToSave.push(ctx.snapshotState);
49-
return ctx;
50-
}),
72+
snapshotTitle,
73+
snapshotContext: {
74+
snapshotState: snapshotStatesByFilePath[file],
75+
currentTestName: snapshotTitle,
76+
},
5177
};
5278
});
5379

54-
afterEach(() => {
80+
afterEach(function () {
81+
if (!this.currentTest?.isPassed()) {
82+
const { file, snapshotTitle } = getSnapshotMeta(this.currentTest!);
83+
snapshotStatesByFilePath[file].markSnapshotsAsCheckedForTest(snapshotTitle);
84+
}
85+
5586
testContext = null;
5687
});
5788

58-
after(() => {
59-
// save snapshot after tests complete, in reverse order (bottom to top)
60-
// to not change line/column number of successive inline snapshot tests
61-
snapshotsToSave
62-
.concat()
63-
.reverse()
64-
.forEach((snapshot) => {
65-
snapshot.save();
66-
});
67-
snapshotsToSave.length = 0;
89+
after(function () {
90+
// save snapshot after tests complete
91+
92+
const unused: string[] = [];
93+
94+
const isUpdatingSnapshots = process.env.UPDATE_SNAPSHOTS;
95+
96+
Object.keys(snapshotStatesByFilePath).forEach((file) => {
97+
const snapshot = snapshotStatesByFilePath[file];
98+
99+
if (!isUpdatingSnapshots) {
100+
unused.push(...snapshot.getUncheckedKeys());
101+
} else {
102+
snapshot.removeUncheckedKeys();
103+
}
104+
105+
snapshot.save();
106+
});
107+
108+
if (unused.length) {
109+
throw new Error(
110+
`${unused.length} obsolete snapshot(s) found:\n${unused.join(
111+
'\n\t'
112+
)}.\n\nRun tests again with \`UPDATE_SNAPSHOTS=1\` to remove them.`
113+
);
114+
}
115+
116+
snapshotStatesByFilePath = {};
68117

69118
registered = false;
70119
});
@@ -85,23 +134,20 @@ Error.prepareStackTrace = (error, structuredStackTrace) => {
85134
}
86135
};
87136

88-
function getSnapshotContextOrThrow({ file, testTitle }: { file: string; testTitle: string }) {
137+
function getSnapshotState(file: string) {
89138
const dirname = path.dirname(file);
90139
const filename = path.basename(file);
91140

92141
const snapshotState = new SnapshotState(
93142
path.join(dirname + `/__snapshots__/` + filename.replace(path.extname(filename), '.snap')),
94143
{
95-
updateSnapshot: process.env.UPDATE_APM_SNAPSHOTS ? 'all' : 'new',
144+
updateSnapshot: process.env.UPDATE_SNAPSHOTS ? 'all' : 'new',
96145
getPrettier: () => prettier,
97146
getBabelTraverse: () => babelTraverse,
98147
}
99148
);
100149

101-
return {
102-
snapshotState,
103-
currentTestName: testTitle,
104-
} as SnapshotContext;
150+
return snapshotState;
105151
}
106152

107153
export function expectSnapshot(received: any) {
@@ -115,23 +161,26 @@ export function expectSnapshot(received: any) {
115161
throw new Error('A current Mocha context is needed to match snapshots');
116162
}
117163

118-
const snapshotContext = testContext.getSnapshotContext();
119-
120164
return {
121-
toMatch: expectToMatchSnapshot.bind(snapshotContext, received),
122-
toMatchInline: expectToMatchInlineSnapshot.bind(snapshotContext, received),
165+
toMatch: expectToMatchSnapshot.bind(null, testContext.snapshotContext, received),
166+
// use bind to support optional 3rd argument (actual)
167+
toMatchInline: expectToMatchInlineSnapshot.bind(null, testContext.snapshotContext, received),
123168
};
124169
}
125170

126-
function expectToMatchSnapshot(this: SnapshotContext, received: any) {
127-
const matcher = toMatchSnapshot.bind(this as any);
171+
function expectToMatchSnapshot(snapshotContext: SnapshotContext, received: any) {
172+
const matcher = toMatchSnapshot.bind(snapshotContext as any);
128173
const result = matcher(received);
129174

130175
expect(result.pass).to.eql(true, result.message());
131176
}
132177

133-
function expectToMatchInlineSnapshot(this: SnapshotContext, received: any, _actual?: any) {
134-
const matcher = toMatchInlineSnapshot.bind(this as any);
178+
function expectToMatchInlineSnapshot(
179+
snapshotContext: SnapshotContext,
180+
received: any,
181+
_actual?: any
182+
) {
183+
const matcher = toMatchInlineSnapshot.bind(snapshotContext as any);
135184

136185
const result = arguments.length === 1 ? matcher(received) : matcher(received, _actual);
137186

0 commit comments

Comments
 (0)