Skip to content

Commit

Permalink
Add toMatchSpeechInlineSnapshot (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
eps1lon committed Nov 30, 2020
1 parent ec10aeb commit edb37b5
Show file tree
Hide file tree
Showing 4 changed files with 511 additions and 411 deletions.
57 changes: 20 additions & 37 deletions examples/jest/index.test.ts
@@ -1,18 +1,20 @@
import * as playwright from "playwright";
import {
awaitNvdaRecording,
createMatchers,
createJestSpeechRecorder,
extendExpect,
} from "screen-reader-testing-library";

const logFilePath = process.env.LOG_FILE_PATH;
expect.extend(createMatchers(logFilePath!));

extendExpect(expect, logFilePath!);

declare global {
namespace jest {
interface Matchers<R> {
toAnnounceNVDA(expectedLines: string[][]): Promise<void>;
toMatchSpeechSnapshot(snapshotName?: string): Promise<void>;
toMatchSpeechInlineSnapshot(expectedLinesSnapshot?: string): void;
}
}
}
Expand Down Expand Up @@ -50,48 +52,29 @@ describe("chromium", () => {
await page.bringToFront();
await awaitNvdaRecording();

expect(
await speechRecorder.recordLines(async () => {
await expect(
speechRecorder.record(async () => {
await page.keyboard.press("s");
})
).toMatchInlineSnapshot(`
Array [
Array [
"banner landmark",
],
Array [
"Search",
"combo box",
"expanded",
"has auto complete",
"editable",
"Search…",
"blank",
],
]
`);

expect(
await speechRecorder.recordLines(async () => {
).resolves.toMatchSpeechInlineSnapshot(`
"banner landmark"
"Search, combo box, expanded, has auto complete, editable, Search…, blank"
`);

await expect(
speechRecorder.record(async () => {
await page.keyboard.type("Rating");
})
).toMatchInlineSnapshot(`Array []`);
).resolves.toMatchSpeechInlineSnapshot(``);

expect(
await speechRecorder.recordLines(async () => {
await expect(
speechRecorder.record(async () => {
await page.keyboard.press("ArrowDown");
})
).toMatchInlineSnapshot(`
Array [
Array [
"list",
],
Array [
"Link to the result",
"1 of 5",
],
]
`);
).resolves.toMatchSpeechInlineSnapshot(`
"list"
"Link to the result, 1 of 5"
`);
}, 20000);

it("matches the NVDA speech snapshot when searching the docs", async () => {
Expand Down
20 changes: 20 additions & 0 deletions src/__tests__/extendExpect.js
@@ -0,0 +1,20 @@
const { extendExpect } = require("../index");

extendExpect(expect, "unused");

test("custom inline snapshot with no lines", () => {
expect([]).toMatchSpeechInlineSnapshot(``);
});

test("custom inline snapshot with one line", () => {
const actualSpeech = [["banner landmark"]];
expect(actualSpeech).toMatchSpeechInlineSnapshot(`"banner landmark"`);
});

test("custom inline snapshot with two lines", () => {
const actualSpeech = [["banner landmark"], ["Search", "combobox"]];
expect(actualSpeech).toMatchSpeechInlineSnapshot(`
"banner landmark"
"Search, combobox"
`);
});
112 changes: 83 additions & 29 deletions src/index.js
@@ -1,8 +1,13 @@
const { create } = require("domain");
const { promises: fs } = require("fs");
const { default: diff } = require("jest-diff");
const { toMatchInlineSnapshot, toMatchSnapshot } = require("jest-snapshot");
const { extractSpeechLines } = require("./logParser");

const speechSnapshotBrand = Symbol.for(
"screen-reader-testing-library.speechSnapshot"
);

/**
* @param {number} timeoutMS
* @returns {Promise<void>}
Expand Down Expand Up @@ -46,7 +51,7 @@ function createSpeechRecorder(logFilePath) {
* @param {() => Promise<void>} fn
* @returns {Promise<string[][]>}
*/
async function recordLines(fn) {
async function record(fn) {
// move to end
await start();
await fn();
Expand All @@ -57,31 +62,49 @@ function createSpeechRecorder(logFilePath) {
return fs.access(logFilePath);
}

return { readable, recordLines, start, stop };
return { readable, record, start, stop };
}

/**
* Must return `any` or `expect.extend(createMatchers(logFilePath))` does not typecheck.
* `toMatchInlineSnapshot` will be unassignable for unknown reasons.
* @param {string} logFilePath
* @returns {any}
*/
function createMatchers(logFilePath) {
const recorder = createSpeechRecorder(logFilePath);
const speechRecorder = createSpeechRecorder(logFilePath);

/**
*
* @param {() => Promise<void>} fn
* @param {string[][]} _expectedLines
* @returns {Promise<ReturnType<typeof toMatchInlineSnapshot>>}
* @param {string[][]} recordedSpeech
* @param {string} [expectedSpeechSnapshot]
* @returns {ReturnType<typeof toMatchInlineSnapshot>}
* @this {import('jest-snapshot/build/types').Context}
*/
async function toMatchSpeechInlineSnapshot(fn, _expectedLines) {
// throws with "Jest: Multiple inline snapshots for the same call are not supported."
throw new Error("Not implemented");
// // move to end
// await recorder.start();
// await fn();
// const actualLines = await recorder.stop();

// return toMatchInlineSnapshot.call(this, actualLines);
function toMatchSpeechInlineSnapshot(recordedSpeech, expectedSpeechSnapshot) {
// Abort test on first mismatch.
// Subsequent actions will be based on an incorrect state otherwise and almost always fail as well.
this.dontThrow = () => {};
if (typeof recordedSpeech === "function") {
throw new Error(
"Recording lines is not implemented by the matcher. Use `expect(recordLines(async () => {})).resolves.toMatchInlineSnapshot()` instead"
);
}

const actualSpeechSnapshot = {
[speechSnapshotBrand]: true,
speech: recordedSpeech,
};

// jest's `toMatchInlineSnapshot` relies on arity.
if (expectedSpeechSnapshot === undefined) {
return toMatchInlineSnapshot.call(this, actualSpeechSnapshot);
}
return toMatchInlineSnapshot.call(
this,
actualSpeechSnapshot,
expectedSpeechSnapshot
);
}

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

return toMatchSnapshot.call(this, actualLines, snapshotName);
return toMatchSnapshot.call(this, speech, snapshotName);
}

/**
* @param {() => Promise<void>} fn
* @param {string[][]} expectedLines
* @param {string[][]} expectedSpeech
* @returns {Promise<{actual: unknown, message: () => string, pass: boolean}>}
* @this {import('jest-snapshot/build/types').Context}
*/
async function toAnnounceNVDA(fn, expectedLines) {
const actualLines = await recorder.recordLines(fn);
async function toAnnounceNVDA(fn, expectedSpeech) {
const actualSpeech = await speechRecorder.record(fn);

const options = {
comment: "deep equality",
isNot: this.isNot,
promise: this.promise,
};

const pass = this.equals(actualLines, expectedLines);
const pass = this.equals(actualSpeech, expectedSpeech);
const message = pass
? () =>
this.utils.matcherHint("toBe", undefined, undefined, options) +
"\n\n" +
`Expected: not ${this.utils.printExpected(expectedLines)}\n` +
`Received: ${this.utils.printReceived(actualLines)}`
`Expected: not ${this.utils.printExpected(expectedSpeech)}\n` +
`Received: ${this.utils.printReceived(actualSpeech)}`
: () => {
const diffString = diff(expectedLines, actualLines, {
const diffString = diff(expectedSpeech, actualSpeech, {
expand: this.expand,
});
return (
this.utils.matcherHint("toBe", undefined, undefined, options) +
"\n\n" +
(diffString && diffString.includes("- Expect")
? `Difference:\n\n${diffString}`
: `Expected: ${this.utils.printExpected(expectedLines)}\n` +
`Received: ${this.utils.printReceived(actualLines)}`)
: `Expected: ${this.utils.printExpected(expectedSpeech)}\n` +
`Received: ${this.utils.printReceived(actualSpeech)}`)
);
};

return { actual: actualLines, message, pass };
return { actual: actualSpeech, message, pass };
}

return { toAnnounceNVDA, toMatchSpeechInlineSnapshot, toMatchSpeechSnapshot };
return { toAnnounceNVDA, toMatchSpeechSnapshot, toMatchSpeechInlineSnapshot };
}

/**
Expand All @@ -156,9 +179,40 @@ function createJestSpeechRecorder(logFilePath) {
return recorder;
}

/**
*
* @param {jest.Expect} expect
* @param {*} logFilePath
*/
function extendExpect(expect, logFilePath) {
expect.extend(createMatchers(logFilePath));

expect.addSnapshotSerializer({
/**
* @param {any} val
*/
print(val) {
/**
* @type {{ speech: string[][] }}
*/
const snapshot = val;
const { speech } = snapshot;

return speech
.map((line) => {
return `"${line.join(", ")}"`;
})
.join("\n");
},
test(value) {
return value != null && value[speechSnapshotBrand] === true;
},
});
}

module.exports = {
awaitNvdaRecording,
createSpeechRecorder,
createMatchers,
createJestSpeechRecorder,
extendExpect,
};

0 comments on commit edb37b5

Please sign in to comment.