Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions test/audio-device-switch.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,13 @@
* by permissions - it leaves the interview with no microphone at all, mid-answer, having been
* asked only to change one.
*/
import { readFileSync } from 'node:fs';

import { codeOnly, createChecker, methodBody } from './helpers.mjs';
import { codeOnly, createChecker, methodBody, readSource } from './helpers.mjs';

export async function run() {
const { check, failures } = createChecker('audio-device-switch');

const source = readFileSync(
new URL('../src/renderer/services/live-transcription.service.ts', import.meta.url),
'utf8'
const source = readSource(
new URL('../src/renderer/services/live-transcription.service.ts', import.meta.url)
);

const body = methodBody(source, 'async setAudioInputDevice(');
Expand Down Expand Up @@ -61,7 +58,10 @@ export async function run() {
// off - with the config naming the other one. The UI disables the picker while a swap is in
// flight, but that is a guard in a different layer from the bug.
const bodyCode = codeOnly(body);
check('a generation is taken before the awaits', /const seq = \+\+this\.micSwitchSeq;/.test(bodyCode));
check(
'a generation is taken before the awaits',
/const seq = \+\+this\.micSwitchSeq;/.test(bodyCode)
);
check(
'the generation is taken before getUserMedia',
bodyCode.indexOf('this.micSwitchSeq') < bodyCode.indexOf('getUserMedia')
Expand Down
16 changes: 16 additions & 0 deletions test/helpers.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,22 @@ export function createChecker(name) {
};
}

/**
* Read a source file for the source-level checks, with line endings normalised.
*
* `core.autocrlf` is true and there is no `.gitattributes`, so a checkout on Windows - which is
* where this project is developed - materialises these files with CRLF while CI materialises
* them with LF. Any anchor containing a literal newline therefore matches on the runner and
* fails on the developer's machine, which is the worst direction for it to fail in: green CI,
* and a suite that looks broken to whoever just pulled.
*
* Normalised on read rather than making each matcher line-ending aware, because the matchers are
* written against the source as it reads on screen and that is the useful thing about them.
*/
export function readSource(url) {
return fs.readFileSync(url, 'utf8').replace(/\r\n?/g, '\n');
}

/**
* Strip comments from TypeScript source before matching against it.
*
Expand Down
25 changes: 11 additions & 14 deletions test/language-switch.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,21 @@
* but the hook only sets that after awaiting the config write, so a second pick lands in
* between. That is a guard in a different layer from the bug, which is why it is not the fix.
*/
import { readFileSync } from 'node:fs';

import { codeOnly, createChecker, methodBody } from './helpers.mjs';
import { codeOnly, createChecker, methodBody, readSource } from './helpers.mjs';

export async function run() {
const { check, failures } = createChecker('language-switch');

const source = readFileSync(
new URL('../src/renderer/services/live-transcription.service.ts', import.meta.url),
'utf8'
const source = readSource(
new URL('../src/renderer/services/live-transcription.service.ts', import.meta.url)
);

// The anchors below contain literal newlines, which is only safe because `readSource`
// normalises: `core.autocrlf` is on with no `.gitattributes`, so this source arrives CRLF on a
// Windows checkout and LF in CI. Read raw, every one of them matches on the runner and fails
// on the machine the project is developed on.
check('the source is read with its line endings normalised', !source.includes('\r'));

// Both classes declare `async setLanguage(language: Language)`. The channel's is the first in
// the file and the service's wraps it, so they are picked apart by what follows the brace.
const body = methodBody(source, 'async setLanguage(language: Language): Promise<void> {\n if');
Expand Down Expand Up @@ -143,10 +146,7 @@ export async function run() {
// switch the user has already moved off clears the warning raised by the one that replaced it
// and re-enables the trigger while that one is still reconnecting.
const hook = codeOnly(
readFileSync(
new URL('../src/renderer/hooks/use-interview-language.ts', import.meta.url),
'utf8'
)
readSource(new URL('../src/renderer/hooks/use-interview-language.ts', import.meta.url))
);
check('the hook takes a generation per switch', /const seq = \+\+switchSeq\.current;/.test(hook));
check(
Expand All @@ -165,10 +165,7 @@ export async function run() {
// Its sibling has the identical shape and the identical race - `micSwitchSeq` abandons a
// superseded swap in the service, and the hook has to stop reporting on it here.
const deviceHook = codeOnly(
readFileSync(
new URL('../src/renderer/hooks/use-audio-input-device.ts', import.meta.url),
'utf8'
)
readSource(new URL('../src/renderer/hooks/use-audio-input-device.ts', import.meta.url))
);
check(
'the microphone hook takes the same guard',
Expand Down
9 changes: 2 additions & 7 deletions test/language.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
* that a stored language actually reaches the request bodies, and that an unknown one resolves to
* English here rather than travelling to the backend and the ASR URL.
*/
import { readFileSync } from 'node:fs';

import { createChecker, loadMain } from './helpers.mjs';
import { createChecker, loadMain, readSource } from './helpers.mjs';

export async function run() {
const { check, failures } = createChecker('language');
Expand Down Expand Up @@ -37,10 +35,7 @@ export async function run() {
// no renderer entry renders a blank trigger, and a renderer entry with no enum member is an
// option that resolves straight back to English when picked. The renderer source is read as
// text because it is never built into electron-dist, which is all `loadMain` can reach.
const rendererSource = readFileSync(
new URL('../src/renderer/types/language.ts', import.meta.url),
'utf8'
);
const rendererSource = readSource(new URL('../src/renderer/types/language.ts', import.meta.url));
const rendererCodes = [...rendererSource.matchAll(/code: Language\.\w+, name: '/g)].length;
const rendererEnum = [...rendererSource.matchAll(/^ \w+ = '([a-z]{2})',$/gm)].map((m) => m[1]);

Expand Down
6 changes: 2 additions & 4 deletions test/rtl-rendering.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,9 @@
* Source-level checks, like `audio-device-switch.test.mjs`: this is renderer code and the
* renderer has no runtime harness in this directory.
*/
import { readFileSync } from 'node:fs';
import { codeOnly, createChecker, readSource } from './helpers.mjs';

import { codeOnly, createChecker } from './helpers.mjs';

const read = (path) => codeOnly(readFileSync(new URL(path, import.meta.url), 'utf8'));
const read = (path) => codeOnly(readSource(new URL(path, import.meta.url)));

export async function run() {
const { check, failures } = createChecker('rtl-rendering');
Expand Down