Skip to content

Commit 60e2bb6

Browse files
committed
test: fix the ci runs
- code: checks wait with vi.waitFor and longer timeouts under coverage - e2e: the tutorial copy gets its own nf, check-build watches tutorial - knip: mkfifo used by the fs tests - command-palette: cover the keyboard shortcuts rows
1 parent 1de614e commit 60e2bb6

6 files changed

Lines changed: 140 additions & 37 deletions

File tree

‎.github/workflows/tests.yml‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ jobs:
6363
- name: Install browsers
6464
uses: ./.github/actions/playwright-install
6565
with:
66-
browsers: chromium firefox webkit
66+
browsers: chromium firefox
6767

6868
# Coverage on Chromium (v8 coverage reads Chromium only), failing under each
6969
# workspace's floor (coverage-floors.json). The kit's component tests use the runner's
@@ -73,10 +73,10 @@ jobs:
7373
env:
7474
CHROME: /usr/bin/google-chrome
7575

76-
- name: Run the browser cache tests on Firefox and WebKit
76+
- name: Run the browser cache tests on Firefox
7777
run: pnpm --filter @nanoforge-dev/editor-project exec vitest run --config vitest.config.ts --project browser
7878
env:
79-
BROWSERS: firefox,webkit
79+
BROWSERS: firefox
8080

8181
- name: Upload coverage
8282
if: always()

‎apps/editor/e2e/check-build.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import { join } from 'node:path';
33

44
const ROOT = join(import.meta.dirname, '../../..');
55
const BUILD = join(import.meta.dirname, '../dist/index.js');
6-
const SOURCES = ['apps/editor/src', 'apps/editor/server', 'packages', 'plugins', 'tooling'];
6+
const SOURCES = [
7+
'apps/editor/src',
8+
'apps/editor/server',
9+
'apps/editor/tutorial',
10+
'packages',
11+
'plugins',
12+
'tooling',
13+
];
714
const SKIPPED = new Set([
815
'node_modules',
916
'dist',

‎apps/editor/e2e/prepare-workspace.mjs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,10 @@ if (existsSync(join(pong, 'node_modules/.bin/nf')) && bridge) {
253253
linkNodeModules(join(pong, 'apps/client/node_modules'), join(tutorial, 'node_modules'));
254254
rmSync(join(tutorial, 'node_modules/@nanoforge-dev/cli'), { recursive: true, force: true });
255255
symlinkSync(cli, join(tutorial, 'node_modules/@nanoforge-dev/cli'));
256+
/** Builds run the project's own `nf`: without it they need one on the PATH, which CI has not. */
257+
mkdirSync(join(tutorial, 'node_modules/.bin'), { recursive: true });
258+
rmSync(join(tutorial, 'node_modules/.bin/nf'), { force: true });
259+
symlinkSync(join(cli, 'dist/nf.js'), join(tutorial, 'node_modules/.bin/nf'));
256260
symlinkSync(
257261
realpathSync(join(pong, 'node_modules/@nanoforge-dev/config')),
258262
join(tutorial, 'node_modules/@nanoforge-dev/config'),

‎knip.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
".engine/**"
1111
],
1212
"ignoreDependencies": ["eslint-formatter-pretty", "@nanoforge-dev/actions"],
13+
"ignoreBinaries": ["mkfifo"],
1314
"workspaces": {
1415
".": {
1516
"entry": ["scripts/*.mjs", "vitest.*.ts", "commitlint.config.cjs"]

‎packages/code/test/service/code-service.spec.ts‎

Lines changed: 28 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ import { serveEngine } from '../../src/worker-rpc/serve-engine';
3434

3535
const MAIN = 'apps/client/src/main.ts';
3636

37+
/**
38+
* Waits for a check's result. A checker runs TypeScript over the project: with coverage on a slow
39+
* runner it takes several times longer than locally, so the wait is long and the tests' timeouts
40+
* leave room for it.
41+
*/
42+
const settled = (done: () => unknown, timeout = 25_000) =>
43+
vi.waitFor(
44+
() => {
45+
if (!done()) throw new Error('not yet');
46+
},
47+
{ timeout, interval: 25 },
48+
);
49+
3750
/** Worker plugin: renames the first `const` declared in a file. */
3851
const renamePlugin: WorkerPluginModule = {
3952
activate: (sdk) => {
@@ -229,22 +242,18 @@ describe('CodeService', () => {
229242
await code.engine();
230243
await (await code.engine()).setFiles([{ path: uri, text: await documents.getText(uri) }]);
231244
const watch = code.watchDiagnostics(uri);
232-
const start = Date.now();
233-
while (!diagnostics.forUri(uri).length && Date.now() - start < 8000)
234-
await new Promise((r) => setTimeout(r, 25));
245+
await settled(() => diagnostics.forUri(uri).length);
235246
expect(diagnostics.forUri(uri).map((d) => d.code)).toEqual([2322]);
236247
watch.dispose();
237248
expect(diagnostics.forUri(uri)).toEqual([]);
238249
code.dispose();
239-
});
250+
}, 60_000);
240251

241252
it('checks every file of the project while asked to, read-only packages aside', async () => {
242253
const { code, diagnostics } = setup();
243254
const uri = 'checks/broken.ts';
244255
const check = code.checkProject();
245-
const start = Date.now();
246-
while (!diagnostics.forUri(uri).length && Date.now() - start < 15_000)
247-
await new Promise((r) => setTimeout(r, 25));
256+
await settled(() => diagnostics.forUri(uri).length);
248257
expect(diagnostics.forUri(uri)).toMatchObject([
249258
{ code: 2322, source: 'typescript', line: 1, column: 14 },
250259
]);
@@ -254,7 +263,7 @@ describe('CodeService', () => {
254263
check.dispose();
255264
expect(diagnostics.forUri(uri)).toEqual([]);
256265
code.dispose();
257-
});
266+
}, 60_000);
258267

259268
it('still checks the whole project when an edit comes before the first check ends', async () => {
260269
const { code, diagnostics } = setup();
@@ -265,42 +274,34 @@ describe('CodeService', () => {
265274
return [];
266275
});
267276
const check = code.checkProject();
268-
const start = Date.now();
269-
while (!diagnostics.forUri('checks/broken.ts').length && Date.now() - start < 15_000)
270-
await new Promise((r) => setTimeout(r, 25));
277+
await settled(() => diagnostics.forUri('checks/broken.ts').length);
271278
expect(diagnostics.forUri('checks/broken.ts').map((d) => d.code)).toEqual([2322]);
272279
await project.fs.write('checks/speed.ts', 'export const speed = 1;\n');
273280
check.dispose();
274281
code.dispose();
275-
}, 20_000);
282+
}, 60_000);
276283

277284
it('checks everything again after an edit to global declarations', async () => {
278285
const { code, diagnostics } = setup();
279286
const check = code.checkProject();
280-
const start = Date.now();
281-
while (!diagnostics.forUri('checks/broken.ts').length && Date.now() - start < 15_000)
282-
await new Promise((r) => setTimeout(r, 25));
287+
await settled(() => diagnostics.forUri('checks/broken.ts').length);
283288
const refresh = vi.spyOn(code, 'refreshDiagnostics');
284289
await project.fs.write(
285290
'checks/speed.ts',
286291
'export const speed = 1;\ndeclare global {\n var debugSpeed: number;\n}\n',
287292
);
288-
const edited = Date.now();
289293
const checked = () => new Set(refresh.mock.calls.flatMap(([paths]) => paths));
290-
while (!checked().has('checks/broken.ts') && Date.now() - edited < 10_000)
291-
await new Promise((r) => setTimeout(r, 25));
294+
await settled(() => checked().has('checks/broken.ts'));
292295
expect(checked().has('checks/broken.ts')).toBe(true);
293296
await project.fs.write('checks/speed.ts', 'export const speed = 1;\n');
294297
check.dispose();
295298
code.dispose();
296-
}, 30_000);
299+
}, 60_000);
297300

298301
it('keeps an edit made while a check reads what changed', async () => {
299302
const { code, diagnostics } = setup();
300303
const check = code.checkProject();
301-
const start = Date.now();
302-
while (!diagnostics.forUri('checks/broken.ts').length && Date.now() - start < 15_000)
303-
await new Promise((r) => setTimeout(r, 25));
304+
await settled(() => diagnostics.forUri('checks/broken.ts').length);
304305
const refresh = vi.spyOn(code, 'refreshDiagnostics');
305306
/** While the check of `broken.ts` reads it, `speed.ts` changes: what imports it is checked. */
306307
const service = code as unknown as {
@@ -313,35 +314,29 @@ describe('CodeService', () => {
313314
});
314315
await project.fs.write('checks/broken.ts', 'export const n: number =\n "other";\n');
315316
const user = 'checks/uses-speed.ts';
316-
const edited = Date.now();
317317
const checked = () => new Set(refresh.mock.calls.flatMap(([paths]) => paths));
318-
while (!checked().has(user) && Date.now() - edited < 10_000)
319-
await new Promise((r) => setTimeout(r, 25));
318+
await settled(() => checked().has(user));
320319
expect(checked().has('checks/broken.ts')).toBe(true);
321320
expect(checked().has(user)).toBe(true);
322321
await project.fs.write('checks/broken.ts', 'export const n: number =\n "text";\n');
323322
await project.fs.write('checks/speed.ts', 'export const speed = 1;\n');
324323
check.dispose();
325324
code.dispose();
326-
}, 30_000);
325+
}, 60_000);
327326

328327
it('after an edit, checks again only the file and what imports it', async () => {
329328
const { code, diagnostics } = setup();
330329
const check = code.checkProject();
331-
const start = Date.now();
332-
while (!diagnostics.forUri('checks/broken.ts').length && Date.now() - start < 15_000)
333-
await new Promise((r) => setTimeout(r, 25));
330+
await settled(() => diagnostics.forUri('checks/broken.ts').length);
334331
const refresh = vi.spyOn(code, 'refreshDiagnostics');
335332
await project.fs.write('checks/speed.ts', 'export const speed = "fast";\n');
336333
const user = 'checks/uses-speed.ts';
337-
const edited = Date.now();
338-
while (!diagnostics.forUri(user).length && Date.now() - edited < 10_000)
339-
await new Promise((r) => setTimeout(r, 25));
334+
await settled(() => diagnostics.forUri(user).length);
340335
expect(diagnostics.forUri(user).map((d) => d.code)).toEqual([2322]);
341336
const checked = new Set(refresh.mock.calls.flatMap(([paths]) => paths));
342337
expect([...checked].sort()).toEqual(['checks/speed.ts', user]);
343338
await project.fs.write('checks/speed.ts', 'export const speed = 1;\n');
344339
check.dispose();
345340
code.dispose();
346-
});
341+
}, 60_000);
347342
});
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { describe, expect, it, vi } from 'vitest';
2+
3+
import type { EditorAction, ResolvedKeybinding } from '@nanoforge-dev/editor-sdk/ui';
4+
5+
import {
6+
conflictsOf,
7+
filterRows,
8+
keymapRows,
9+
strokesOf,
10+
userConflicts,
11+
} from '../../src/keymap/keymap-rows';
12+
13+
vi.mock('@nanoforge-dev/editor-sdk/ui', () => ({
14+
actionId: (command: string, args: readonly unknown[]) =>
15+
args.length ? `${command}:${JSON.stringify(args)}` : command,
16+
actionLabel: (action: { title: string; category?: string }) =>
17+
action.category ? `${action.category}: ${action.title}` : action.title,
18+
bindingsConflict: (a: ResolvedKeybinding, b: ResolvedKeybinding) =>
19+
a.key === b.key && a.command !== b.command,
20+
findConflicts: (bindings: readonly ResolvedKeybinding[]) =>
21+
[...Map.groupBy(bindings, (binding) => binding.key).values()]
22+
.filter((group) => group.length > 1)
23+
.map((group) => ({ bindings: group })),
24+
formatKeybinding: (key: string) => key.replace('Mod', 'Ctrl'),
25+
isActionChanged: (overrides: readonly { command: string }[], command: string) =>
26+
overrides.some((override) => override.command === command),
27+
parseKeybinding: (key: string) => {
28+
if (!key.trim()) throw new Error('empty');
29+
return key.split(' ');
30+
},
31+
}));
32+
33+
const action = (command: string, title: string, category?: string): EditorAction => ({
34+
id: command,
35+
command,
36+
args: [],
37+
title,
38+
...(category && { category }),
39+
when: [],
40+
});
41+
42+
const binding = (
43+
command: string,
44+
key: string,
45+
source: ResolvedKeybinding['source'] = 'default',
46+
): ResolvedKeybinding => ({ command, args: [], key, strokes: key.split(' '), source });
47+
48+
const SAVE = action('file.save', 'Save', 'File');
49+
const PLAY = action('run.play', 'Play', 'Run');
50+
const BINDINGS = [binding('file.save', 'Mod+S'), binding('editor.hidden', 'Mod+H')];
51+
const LAYERS = { before: [], own: [{ command: 'run.play' }], after: [] } as never;
52+
53+
describe('keymap rows', () => {
54+
it('lists every action, and the untitled commands that have shortcuts', () => {
55+
const rows = keymapRows([SAVE, PLAY], LAYERS, BINDINGS, new Set());
56+
expect(rows.map((row) => [row.label, row.bindings.length, row.changed])).toEqual([
57+
['File: Save', 1, false],
58+
['Run: Play', 0, true],
59+
['editor.hidden', 1, false],
60+
]);
61+
});
62+
63+
it('marks the actions whose shortcuts conflict with a change of the user', () => {
64+
const mine = binding('run.play', 'Mod+S', 'user');
65+
const bindings = [...BINDINGS, mine];
66+
const conflicting = userConflicts(bindings);
67+
expect(conflicting).toEqual(new Set([BINDINGS[0], mine]));
68+
expect(userConflicts(BINDINGS)).toEqual(new Set());
69+
const rows = keymapRows([SAVE, PLAY], LAYERS, bindings, conflicting);
70+
expect(rows.filter((row) => row.conflict).map((row) => row.action.id)).toEqual([
71+
'file.save',
72+
'run.play',
73+
]);
74+
});
75+
76+
it('filters by label, command, shortcut, changes and conflicts', () => {
77+
const rows = keymapRows([SAVE, PLAY], LAYERS, BINDINGS, new Set([BINDINGS[0]!]));
78+
const ids = (query: string, onlyChanged = false, onlyConflicts = false) =>
79+
filterRows(rows, { query, onlyChanged, onlyConflicts }).map((row) => row.action.id);
80+
expect(ids(' SAVE ')).toEqual(['file.save']);
81+
expect(ids('run.')).toEqual(['run.play']);
82+
expect(ids('ctrl+h')).toEqual(['editor.hidden']);
83+
expect(ids('mod+s')).toEqual(['file.save']);
84+
expect(ids('', true)).toEqual(['run.play']);
85+
expect(ids('', false, true)).toEqual(['file.save']);
86+
expect(ids('nothing')).toEqual([]);
87+
});
88+
89+
it('parses strokes, and finds what a candidate shortcut would get in the way of', () => {
90+
expect(strokesOf('Mod+K S')).toEqual(['Mod+K', 'S']);
91+
expect(strokesOf(' ')).toBeUndefined();
92+
expect(conflictsOf(BINDINGS, PLAY, 'Mod+S', undefined)).toEqual([BINDINGS[0]]);
93+
expect(conflictsOf(BINDINGS, PLAY, 'Mod+S', 'editorFocus', BINDINGS[0])).toEqual([]);
94+
expect(conflictsOf(BINDINGS, PLAY, ' ', undefined)).toEqual([]);
95+
});
96+
});

0 commit comments

Comments
 (0)