Skip to content

Commit edb37b5

Browse files
authored
Add toMatchSpeechInlineSnapshot (#8)
1 parent ec10aeb commit edb37b5

File tree

4 files changed

+511
-411
lines changed

4 files changed

+511
-411
lines changed

examples/jest/index.test.ts

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import * as playwright from "playwright";
22
import {
33
awaitNvdaRecording,
4-
createMatchers,
54
createJestSpeechRecorder,
5+
extendExpect,
66
} from "screen-reader-testing-library";
77

88
const logFilePath = process.env.LOG_FILE_PATH;
9-
expect.extend(createMatchers(logFilePath!));
9+
10+
extendExpect(expect, logFilePath!);
1011

1112
declare global {
1213
namespace jest {
1314
interface Matchers<R> {
1415
toAnnounceNVDA(expectedLines: string[][]): Promise<void>;
1516
toMatchSpeechSnapshot(snapshotName?: string): Promise<void>;
17+
toMatchSpeechInlineSnapshot(expectedLinesSnapshot?: string): void;
1618
}
1719
}
1820
}
@@ -50,48 +52,29 @@ describe("chromium", () => {
5052
await page.bringToFront();
5153
await awaitNvdaRecording();
5254

53-
expect(
54-
await speechRecorder.recordLines(async () => {
55+
await expect(
56+
speechRecorder.record(async () => {
5557
await page.keyboard.press("s");
5658
})
57-
).toMatchInlineSnapshot(`
58-
Array [
59-
Array [
60-
"banner landmark",
61-
],
62-
Array [
63-
"Search",
64-
"combo box",
65-
"expanded",
66-
"has auto complete",
67-
"editable",
68-
"Search…",
69-
"blank",
70-
],
71-
]
72-
`);
73-
74-
expect(
75-
await speechRecorder.recordLines(async () => {
59+
).resolves.toMatchSpeechInlineSnapshot(`
60+
"banner landmark"
61+
"Search, combo box, expanded, has auto complete, editable, Search…, blank"
62+
`);
63+
64+
await expect(
65+
speechRecorder.record(async () => {
7666
await page.keyboard.type("Rating");
7767
})
78-
).toMatchInlineSnapshot(`Array []`);
68+
).resolves.toMatchSpeechInlineSnapshot(``);
7969

80-
expect(
81-
await speechRecorder.recordLines(async () => {
70+
await expect(
71+
speechRecorder.record(async () => {
8272
await page.keyboard.press("ArrowDown");
8373
})
84-
).toMatchInlineSnapshot(`
85-
Array [
86-
Array [
87-
"list",
88-
],
89-
Array [
90-
"Link to the result",
91-
"1 of 5",
92-
],
93-
]
94-
`);
74+
).resolves.toMatchSpeechInlineSnapshot(`
75+
"list"
76+
"Link to the result, 1 of 5"
77+
`);
9578
}, 20000);
9679

9780
it("matches the NVDA speech snapshot when searching the docs", async () => {

src/__tests__/extendExpect.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const { extendExpect } = require("../index");
2+
3+
extendExpect(expect, "unused");
4+
5+
test("custom inline snapshot with no lines", () => {
6+
expect([]).toMatchSpeechInlineSnapshot(``);
7+
});
8+
9+
test("custom inline snapshot with one line", () => {
10+
const actualSpeech = [["banner landmark"]];
11+
expect(actualSpeech).toMatchSpeechInlineSnapshot(`"banner landmark"`);
12+
});
13+
14+
test("custom inline snapshot with two lines", () => {
15+
const actualSpeech = [["banner landmark"], ["Search", "combobox"]];
16+
expect(actualSpeech).toMatchSpeechInlineSnapshot(`
17+
"banner landmark"
18+
"Search, combobox"
19+
`);
20+
});

src/index.js

Lines changed: 83 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
const { create } = require("domain");
12
const { promises: fs } = require("fs");
23
const { default: diff } = require("jest-diff");
34
const { toMatchInlineSnapshot, toMatchSnapshot } = require("jest-snapshot");
45
const { extractSpeechLines } = require("./logParser");
56

7+
const speechSnapshotBrand = Symbol.for(
8+
"screen-reader-testing-library.speechSnapshot"
9+
);
10+
611
/**
712
* @param {number} timeoutMS
813
* @returns {Promise<void>}
@@ -46,7 +51,7 @@ function createSpeechRecorder(logFilePath) {
4651
* @param {() => Promise<void>} fn
4752
* @returns {Promise<string[][]>}
4853
*/
49-
async function recordLines(fn) {
54+
async function record(fn) {
5055
// move to end
5156
await start();
5257
await fn();
@@ -57,31 +62,49 @@ function createSpeechRecorder(logFilePath) {
5762
return fs.access(logFilePath);
5863
}
5964

60-
return { readable, recordLines, start, stop };
65+
return { readable, record, start, stop };
6166
}
6267

6368
/**
69+
* Must return `any` or `expect.extend(createMatchers(logFilePath))` does not typecheck.
70+
* `toMatchInlineSnapshot` will be unassignable for unknown reasons.
6471
* @param {string} logFilePath
72+
* @returns {any}
6573
*/
6674
function createMatchers(logFilePath) {
67-
const recorder = createSpeechRecorder(logFilePath);
75+
const speechRecorder = createSpeechRecorder(logFilePath);
6876

6977
/**
7078
*
71-
* @param {() => Promise<void>} fn
72-
* @param {string[][]} _expectedLines
73-
* @returns {Promise<ReturnType<typeof toMatchInlineSnapshot>>}
79+
* @param {string[][]} recordedSpeech
80+
* @param {string} [expectedSpeechSnapshot]
81+
* @returns {ReturnType<typeof toMatchInlineSnapshot>}
7482
* @this {import('jest-snapshot/build/types').Context}
7583
*/
76-
async function toMatchSpeechInlineSnapshot(fn, _expectedLines) {
77-
// throws with "Jest: Multiple inline snapshots for the same call are not supported."
78-
throw new Error("Not implemented");
79-
// // move to end
80-
// await recorder.start();
81-
// await fn();
82-
// const actualLines = await recorder.stop();
83-
84-
// return toMatchInlineSnapshot.call(this, actualLines);
84+
function toMatchSpeechInlineSnapshot(recordedSpeech, expectedSpeechSnapshot) {
85+
// Abort test on first mismatch.
86+
// Subsequent actions will be based on an incorrect state otherwise and almost always fail as well.
87+
this.dontThrow = () => {};
88+
if (typeof recordedSpeech === "function") {
89+
throw new Error(
90+
"Recording lines is not implemented by the matcher. Use `expect(recordLines(async () => {})).resolves.toMatchInlineSnapshot()` instead"
91+
);
92+
}
93+
94+
const actualSpeechSnapshot = {
95+
[speechSnapshotBrand]: true,
96+
speech: recordedSpeech,
97+
};
98+
99+
// jest's `toMatchInlineSnapshot` relies on arity.
100+
if (expectedSpeechSnapshot === undefined) {
101+
return toMatchInlineSnapshot.call(this, actualSpeechSnapshot);
102+
}
103+
return toMatchInlineSnapshot.call(
104+
this,
105+
actualSpeechSnapshot,
106+
expectedSpeechSnapshot
107+
);
85108
}
86109

87110
/**
@@ -92,51 +115,51 @@ function createMatchers(logFilePath) {
92115
* @this {import('jest-snapshot/build/types').Context}
93116
*/
94117
async function toMatchSpeechSnapshot(fn, snapshotName) {
95-
const actualLines = await recorder.recordLines(fn);
118+
const speech = await speechRecorder.record(fn);
96119

97-
return toMatchSnapshot.call(this, actualLines, snapshotName);
120+
return toMatchSnapshot.call(this, speech, snapshotName);
98121
}
99122

100123
/**
101124
* @param {() => Promise<void>} fn
102-
* @param {string[][]} expectedLines
125+
* @param {string[][]} expectedSpeech
103126
* @returns {Promise<{actual: unknown, message: () => string, pass: boolean}>}
104127
* @this {import('jest-snapshot/build/types').Context}
105128
*/
106-
async function toAnnounceNVDA(fn, expectedLines) {
107-
const actualLines = await recorder.recordLines(fn);
129+
async function toAnnounceNVDA(fn, expectedSpeech) {
130+
const actualSpeech = await speechRecorder.record(fn);
108131

109132
const options = {
110133
comment: "deep equality",
111134
isNot: this.isNot,
112135
promise: this.promise,
113136
};
114137

115-
const pass = this.equals(actualLines, expectedLines);
138+
const pass = this.equals(actualSpeech, expectedSpeech);
116139
const message = pass
117140
? () =>
118141
this.utils.matcherHint("toBe", undefined, undefined, options) +
119142
"\n\n" +
120-
`Expected: not ${this.utils.printExpected(expectedLines)}\n` +
121-
`Received: ${this.utils.printReceived(actualLines)}`
143+
`Expected: not ${this.utils.printExpected(expectedSpeech)}\n` +
144+
`Received: ${this.utils.printReceived(actualSpeech)}`
122145
: () => {
123-
const diffString = diff(expectedLines, actualLines, {
146+
const diffString = diff(expectedSpeech, actualSpeech, {
124147
expand: this.expand,
125148
});
126149
return (
127150
this.utils.matcherHint("toBe", undefined, undefined, options) +
128151
"\n\n" +
129152
(diffString && diffString.includes("- Expect")
130153
? `Difference:\n\n${diffString}`
131-
: `Expected: ${this.utils.printExpected(expectedLines)}\n` +
132-
`Received: ${this.utils.printReceived(actualLines)}`)
154+
: `Expected: ${this.utils.printExpected(expectedSpeech)}\n` +
155+
`Received: ${this.utils.printReceived(actualSpeech)}`)
133156
);
134157
};
135158

136-
return { actual: actualLines, message, pass };
159+
return { actual: actualSpeech, message, pass };
137160
}
138161

139-
return { toAnnounceNVDA, toMatchSpeechInlineSnapshot, toMatchSpeechSnapshot };
162+
return { toAnnounceNVDA, toMatchSpeechSnapshot, toMatchSpeechInlineSnapshot };
140163
}
141164

142165
/**
@@ -156,9 +179,40 @@ function createJestSpeechRecorder(logFilePath) {
156179
return recorder;
157180
}
158181

182+
/**
183+
*
184+
* @param {jest.Expect} expect
185+
* @param {*} logFilePath
186+
*/
187+
function extendExpect(expect, logFilePath) {
188+
expect.extend(createMatchers(logFilePath));
189+
190+
expect.addSnapshotSerializer({
191+
/**
192+
* @param {any} val
193+
*/
194+
print(val) {
195+
/**
196+
* @type {{ speech: string[][] }}
197+
*/
198+
const snapshot = val;
199+
const { speech } = snapshot;
200+
201+
return speech
202+
.map((line) => {
203+
return `"${line.join(", ")}"`;
204+
})
205+
.join("\n");
206+
},
207+
test(value) {
208+
return value != null && value[speechSnapshotBrand] === true;
209+
},
210+
});
211+
}
212+
159213
module.exports = {
160214
awaitNvdaRecording,
161215
createSpeechRecorder,
162-
createMatchers,
163216
createJestSpeechRecorder,
217+
extendExpect,
164218
};

0 commit comments

Comments
 (0)