Skip to content

Commit 4c39cc6

Browse files
authored
chore: add tests to runtime (#75)
1 parent db233c7 commit 4c39cc6

19 files changed

+890
-17
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ dist-ssr
2020
.pnpm-store
2121
/tutorialkit/template
2222
tsconfig.tsbuildinfo
23+
tsconfig.build.tsbuildinfo
2324
.tmp

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pnpm-lock.yaml
2+
.astro
23
**/*.md
34
**/*.mdx

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"template:build": "pnpm run build && pnpm run --filter=tutorialkit-starter build",
1010
"docs": "pnpm run --filter=tutorialkit.dev dev",
1111
"docs:build": "pnpm run --filter=tutorialkit.dev build",
12-
"test": "pnpm run --filter=tutorialkit test"
12+
"test": "pnpm run --filter=@tutorialkit/* --filter=tutorialkit test"
1313
},
1414
"license": "MIT",
1515
"packageManager": "[email protected]",

packages/astro/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"@tutorialkit/runtime": "workspace:*",
3030
"@tutorialkit/types": "workspace:*",
3131
"@types/react": "^18.2.75",
32-
"@webcontainer/api": "beta",
32+
"@webcontainer/api": "1.2.0",
3333
"astro-expressive-code": "^0.35.3",
3434
"chokidar": "3.6.0",
3535
"fast-glob": "^3.3.2",

packages/components/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"@nanostores/react": "0.7.2",
3434
"@radix-ui/react-accordion": "^1.1.2",
3535
"@tutorialkit/runtime": "workspace:*",
36-
"@webcontainer/api": "beta",
36+
"@webcontainer/api": "1.2.0",
3737
"@xterm/addon-fit": "^0.10.0",
3838
"@xterm/addon-web-links": "^0.11.0",
3939
"@xterm/xterm": "^5.5.0",

packages/runtime/package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222
}
2323
},
2424
"scripts": {
25-
"build": "tsc -b"
25+
"build": "tsc -b tsconfig.build.json",
26+
"test": "vitest"
2627
},
2728
"dependencies": {
28-
"@webcontainer/api": "beta",
29+
"@webcontainer/api": "1.2.0",
2930
"nanostores": "^0.10.3",
3031
"@tutorialkit/types": "workspace:*"
3132
},
3233
"devDependencies": {
34+
"test-utils": "workspace:*",
3335
"typescript": "^5.4.5",
34-
"vite": "^5.2.11"
36+
"vite": "^5.2.11",
37+
"vitest": "^1.6.0"
3538
}
3639
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { describe, test, expect, beforeAll, vi, afterAll, beforeEach } from 'vitest';
2+
import { LessonFilesFetcher } from './lesson-files.js';
3+
4+
const originalFetch = global.fetch;
5+
6+
let fetchBody: any;
7+
8+
const fetchSpy = vi.fn(() =>
9+
Promise.resolve({
10+
ok: true,
11+
json: () => Promise.resolve(fetchBody),
12+
}),
13+
);
14+
15+
beforeAll(() => {
16+
global.fetch = fetchSpy as any;
17+
});
18+
19+
beforeEach(() => {
20+
fetchSpy.mockClear();
21+
});
22+
23+
afterAll(() => {
24+
global.fetch = originalFetch;
25+
});
26+
27+
describe('LessonFilesFetcher', () => {
28+
test('getLessonTemplate should fetch template', async () => {
29+
fetchBody = { 'a.txt': 'content' };
30+
31+
const fetcher = new LessonFilesFetcher();
32+
const files = await fetcher.getLessonTemplate({ data: { template: 'default' } } as any);
33+
34+
expect(files).toEqual({ 'a.txt': 'content' });
35+
expect(fetchSpy).toHaveBeenCalledWith('/template-default.json', expect.anything());
36+
});
37+
38+
test('getLessonFiles should fetch files', async () => {
39+
fetchBody = { 'a.txt': 'content' };
40+
41+
const fetcher = new LessonFilesFetcher();
42+
const files = await fetcher.getLessonFiles({
43+
files: ['1-welcome-1-intro-1-welcome-files.json', ['a.txt']],
44+
} as any);
45+
46+
expect(files).toEqual({ 'a.txt': 'content' });
47+
expect(fetchSpy).toHaveBeenCalledWith('/1-welcome-1-intro-1-welcome-files.json');
48+
});
49+
50+
test('getLessonSolution should fetch solution', async () => {
51+
fetchBody = { 'a.txt': 'content' };
52+
53+
const fetcher = new LessonFilesFetcher();
54+
const files = await fetcher.getLessonSolution({
55+
solution: ['1-welcome-1-intro-1-welcome-solution.json', ['a.txt']],
56+
} as any);
57+
58+
expect(files).toEqual({ 'a.txt': 'content' });
59+
expect(fetchSpy).toHaveBeenCalledWith('/1-welcome-1-intro-1-welcome-solution.json');
60+
});
61+
62+
test('invalidate should return none if files are not in map', async () => {
63+
const fetcher = new LessonFilesFetcher();
64+
const result = await fetcher.invalidate('1-welcome-1-intro-1-welcome-files.json');
65+
66+
expect(result).toEqual({ type: 'none' });
67+
});
68+
69+
test('invalidate should fetch files', async () => {
70+
fetchBody = { 'a.txt': 'content' };
71+
72+
const fetcher = new LessonFilesFetcher();
73+
74+
const initialData = await fetcher.getLessonFiles({
75+
files: ['1-welcome-1-intro-1-welcome-files.json', ['a.txt']],
76+
} as any);
77+
78+
expect(initialData).toEqual({ 'a.txt': 'content' });
79+
80+
fetchBody = { 'a.txt': 'foobar' };
81+
82+
const result = await fetcher.invalidate('1-welcome-1-intro-1-welcome-files.json');
83+
84+
expect(result).toEqual({ type: 'files', files: { 'a.txt': 'foobar' } });
85+
expect(fetchSpy).toHaveBeenCalledWith('/1-welcome-1-intro-1-welcome-files.json');
86+
});
87+
});
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { resetProcessFactory, setProcessFactory } from 'test-utils';
2+
import { WebContainer } from '@webcontainer/api';
3+
import { describe, test, expect, beforeEach, vi } from 'vitest';
4+
import type { MockedWebContainer } from 'test-utils';
5+
import { TutorialRunner } from './tutorial-runner.js';
6+
import { StepsController } from './webcontainer/steps.js';
7+
import { TerminalStore } from './store/terminal.js';
8+
import { withResolvers } from './utils/promises.js';
9+
10+
beforeEach(() => {
11+
resetProcessFactory();
12+
});
13+
14+
describe('TutorialRunner', () => {
15+
test('prepareFiles should mount files to WebContainer', async () => {
16+
const webcontainer = WebContainer.boot();
17+
const mock = (await webcontainer) as MockedWebContainer;
18+
const runner = new TutorialRunner(webcontainer, new TerminalStore(webcontainer, false), new StepsController());
19+
20+
await runner.prepareFiles({
21+
files: {
22+
'/src/index.js': 'console.log("Hello, world!")',
23+
'/src/index.html': '<h1>Hello, world!</h1>',
24+
},
25+
template: {
26+
'/package.json': '{ "name": "my-project" }',
27+
},
28+
});
29+
30+
expect(mock._fakeFs).toMatchInlineSnapshot(`
31+
{
32+
"package.json": {
33+
"file": {
34+
"contents": "{ "name": "my-project" }",
35+
},
36+
},
37+
"src": {
38+
"directory": {
39+
"index.html": {
40+
"file": {
41+
"contents": "<h1>Hello, world!</h1>",
42+
},
43+
},
44+
"index.js": {
45+
"file": {
46+
"contents": "console.log("Hello, world!")",
47+
},
48+
},
49+
},
50+
},
51+
}
52+
`);
53+
});
54+
55+
test('runCommands should execute commands only after load has completed', async () => {
56+
const webcontainer = WebContainer.boot();
57+
const mock = (await webcontainer) as MockedWebContainer;
58+
59+
const { promise: runCommandPromise, resolve } = withResolvers();
60+
61+
const processFactory = vi.fn(() => {
62+
resolve(JSON.stringify(mock._fakeFs, undefined, 2));
63+
64+
return {
65+
exit: new Promise<number>(() => {}),
66+
};
67+
});
68+
69+
setProcessFactory(processFactory);
70+
71+
const runner = new TutorialRunner(webcontainer, new TerminalStore(webcontainer, false), new StepsController());
72+
73+
runner.setCommands({
74+
mainCommand: 'some command',
75+
});
76+
77+
runner.prepareFiles({
78+
files: {
79+
'/src/index.js': 'console.log("Hello, world!")',
80+
'/src/index.html': '<h1>Hello, world!</h1>',
81+
},
82+
template: {
83+
'/package.json': '{ "name": "my-project" }',
84+
},
85+
});
86+
87+
runner.runCommands();
88+
89+
const fs = await runCommandPromise;
90+
91+
expect(processFactory).toHaveBeenCalledTimes(1);
92+
expect(fs).toMatchInlineSnapshot(`
93+
"{
94+
"package.json": {
95+
"file": {
96+
"contents": "{ \\"name\\": \\"my-project\\" }"
97+
}
98+
},
99+
"src": {
100+
"directory": {
101+
"index.js": {
102+
"file": {
103+
"contents": "console.log(\\"Hello, world!\\")"
104+
}
105+
},
106+
"index.html": {
107+
"file": {
108+
"contents": "<h1>Hello, world!</h1>"
109+
}
110+
}
111+
}
112+
}
113+
}"
114+
`);
115+
116+
expect(mock._fakeProcesses).toMatchInlineSnapshot(`
117+
[
118+
{
119+
"args": [
120+
"command",
121+
],
122+
"command": "some",
123+
"options": {
124+
"terminal": undefined,
125+
},
126+
},
127+
]
128+
`);
129+
});
130+
});
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { Commands, Command } from './command.js';
3+
4+
describe('Commands', () => {
5+
it('should accept shell like commands', () => {
6+
const commands = new Commands({
7+
mainCommand: 'npm run start',
8+
prepareCommands: ['npm install'],
9+
});
10+
11+
expect(commands.mainCommand?.shellCommand).toBe('npm run start');
12+
expect(commands.prepareCommands?.[0].shellCommand).toBe('npm install');
13+
});
14+
15+
it('should accept a tuple of [shell, title]', () => {
16+
const commands = new Commands({
17+
mainCommand: ['npm run start', 'Start dev server'],
18+
prepareCommands: [['npm install', 'Installing dependencies']],
19+
});
20+
21+
expect(commands.mainCommand?.shellCommand).toBe('npm run start');
22+
expect(commands.mainCommand?.title).toBe('Start dev server');
23+
expect(commands.prepareCommands?.[0].shellCommand).toBe('npm install');
24+
expect(commands.prepareCommands?.[0].title).toBe('Installing dependencies');
25+
});
26+
27+
it('should accept objects with { title, command }', () => {
28+
const commands = new Commands({
29+
mainCommand: { command: 'npm run start', title: 'Start dev server' },
30+
prepareCommands: [{ command: 'npm install', title: 'Installing dependencies' }],
31+
});
32+
33+
expect(commands.mainCommand?.shellCommand).toBe('npm run start');
34+
expect(commands.mainCommand?.title).toBe('Start dev server');
35+
expect(commands.prepareCommands?.[0].shellCommand).toBe('npm install');
36+
expect(commands.prepareCommands?.[0].title).toBe('Installing dependencies');
37+
});
38+
39+
it('should be iterable', () => {
40+
const commands = new Commands({
41+
mainCommand: 'npm run start',
42+
prepareCommands: ['npm install', 'npm run build'],
43+
});
44+
45+
const iterator: Iterator<Command> = commands[Symbol.iterator]();
46+
expect(iterator.next().value.shellCommand).toBe('npm install');
47+
expect(iterator.next().value.shellCommand).toBe('npm run build');
48+
expect(iterator.next().value.shellCommand).toBe('npm run start');
49+
expect(iterator.next().done).toBe(true);
50+
});
51+
});
52+
53+
describe('Command', () => {
54+
it('should be runnable if shell command is not empty', () => {
55+
const command = new Command('npm install');
56+
expect(command.isRunnable()).toBe(true);
57+
});
58+
59+
it('should not be runnable if shell command is empty', () => {
60+
const command = new Command('');
61+
expect(command.isRunnable()).toBe(false);
62+
});
63+
64+
it('should compare commands by shell command', () => {
65+
const a = new Command('npm install');
66+
const b = new Command('npm install');
67+
const c = new Command('npm run start');
68+
69+
expect(Command.equals(a, b)).toBe(true);
70+
expect(Command.equals(a, c)).toBe(false);
71+
});
72+
73+
it('should extract title from command', () => {
74+
const command = new Command({ command: 'npm install', title: 'Installing dependencies' });
75+
expect(command.title).toBe('Installing dependencies');
76+
});
77+
78+
it('should convert command to shell command', () => {
79+
const command = new Command({ command: 'npm install', title: 'Installing dependencies' });
80+
expect(command.shellCommand).toBe('npm install');
81+
});
82+
});

0 commit comments

Comments
 (0)