Skip to content

Commit 615dc2c

Browse files
committed
test: add test for Project
1 parent 4b9f1bc commit 615dc2c

File tree

2 files changed

+354
-1
lines changed

2 files changed

+354
-1
lines changed

packages/codegen/src/project.test.ts

Lines changed: 352 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { access, chmod } from 'node:fs/promises';
1+
import { access, chmod, rm, writeFile } from 'node:fs/promises';
22
import { TsConfigFileNotFoundError } from '@css-modules-kit/core';
3+
import dedent from 'dedent';
34
import { describe, expect, test } from 'vitest';
45
import { ReadCSSModuleFileError } from './error.js';
56
import { createProject } from './project.js';
@@ -31,6 +32,356 @@ describe('createProject', () => {
3132
);
3233
});
3334

35+
test('isWildcardMatchedFile', async () => {
36+
const iff = await createIFF({
37+
'tsconfig.json': dedent`
38+
{
39+
"include": ["src"],
40+
"exclude": ["src/excluded"]
41+
}
42+
`,
43+
});
44+
const project = createProject({ project: iff.rootDir });
45+
expect(project.isWildcardMatchedFile(iff.join('src/a.module.css'))).toBe(true);
46+
expect(project.isWildcardMatchedFile(iff.join('src/excluded/b.module.css'))).toBe(false);
47+
expect(project.isWildcardMatchedFile(iff.join('c.module.css'))).toBe(false);
48+
});
49+
50+
describe('addFile', () => {
51+
test('The diagnostics of the added file are reported, and .d.ts file is emitted', async () => {
52+
const iff = await createIFF({
53+
'tsconfig.json': '{}',
54+
'src': {},
55+
});
56+
const project = createProject({ project: iff.rootDir });
57+
58+
// Even if the file is added, diagnostics will not change until notified by `addFile`.
59+
await writeFile(iff.join('src/a.module.css'), '.a_1 {');
60+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
61+
[
62+
{
63+
"category": "error",
64+
"text": "The file specified in tsconfig.json not found.",
65+
},
66+
]
67+
`);
68+
69+
project.addFile(iff.join('src/a.module.css'));
70+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
71+
[
72+
{
73+
"category": "error",
74+
"fileName": "<rootDir>/src/a.module.css",
75+
"length": 1,
76+
"start": {
77+
"column": 1,
78+
"line": 1,
79+
},
80+
"text": "Unclosed block",
81+
},
82+
]
83+
`);
84+
85+
await project.emitDtsFiles();
86+
await expect(access(iff.join('generated/src/a.module.css.d.ts'))).resolves.not.toThrow();
87+
});
88+
test('changes diagnostics in files that import it directly or indirectly', async () => {
89+
// This test case suggests the following facts:
90+
// - The check stage cache for files that directly import the added file should be invalidated.
91+
// - The check stage cache for files that indirectly import the added file should also be invalidated.
92+
const iff = await createIFF({
93+
'tsconfig.json': '{}',
94+
'src/b.module.css': '@import "./a.module.css";', // directly
95+
'src/c.module.css': '@value a_1 from "./b.module.css";', // indirectly
96+
});
97+
const project = createProject({ project: iff.rootDir });
98+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
99+
[
100+
{
101+
"category": "error",
102+
"fileName": "<rootDir>/src/b.module.css",
103+
"length": 14,
104+
"start": {
105+
"column": 10,
106+
"line": 1,
107+
},
108+
"text": "Cannot import module './a.module.css'",
109+
},
110+
{
111+
"category": "error",
112+
"fileName": "<rootDir>/src/c.module.css",
113+
"length": 3,
114+
"start": {
115+
"column": 8,
116+
"line": 1,
117+
},
118+
"text": "Module './b.module.css' has no exported token 'a_1'.",
119+
},
120+
]
121+
`);
122+
await writeFile(iff.join('src/a.module.css'), '@value a_1: red;');
123+
project.addFile(iff.join('src/a.module.css'));
124+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`[]`);
125+
});
126+
test('changes the resolution results of import specifiers in other files', async () => {
127+
// This test case suggests the following facts:
128+
// - The check stage cache for files that import other files should be invalidated.
129+
// - Only independent files that do not import any other files can keep the check stage cache.
130+
const iff = await createIFF({
131+
'tsconfig.json': dedent`
132+
{
133+
"compilerOptions": {
134+
"paths": {
135+
"@/a.module.css": ["src/a-1.module.css", "src/a-2.module.css"]
136+
}
137+
}
138+
}
139+
`,
140+
'src/a-2.module.css': '@value a_2: red;',
141+
'src/b.module.css': '@import "@/a.module.css";',
142+
'src/c.module.css': '@value a_2 from "./b.module.css";',
143+
});
144+
const project = createProject({ project: iff.rootDir });
145+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`[]`);
146+
await writeFile(iff.join('src/a-1.module.css'), '@value a_1: red;');
147+
project.addFile(iff.join('src/a-1.module.css'));
148+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
149+
[
150+
{
151+
"category": "error",
152+
"fileName": "<rootDir>/src/c.module.css",
153+
"length": 3,
154+
"start": {
155+
"column": 8,
156+
"line": 1,
157+
},
158+
"text": "Module './b.module.css' has no exported token 'a_2'.",
159+
},
160+
]
161+
`);
162+
});
163+
});
164+
165+
describe('updateFile', () => {
166+
test('The new diagnostics of the changed file are reported, and new .d.ts file is emitted', async () => {
167+
const iff = await createIFF({
168+
'tsconfig.json': '{}',
169+
'src/a.module.css': '',
170+
});
171+
const project = createProject({ project: iff.rootDir });
172+
173+
// Even if the file is updated, diagnostics will not change until notified by `updateFile`.
174+
await writeFile(iff.join('src/a.module.css'), '.a_1 {');
175+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`[]`);
176+
177+
// New syntactic diagnostics are reported
178+
project.updateFile(iff.join('src/a.module.css'));
179+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
180+
[
181+
{
182+
"category": "error",
183+
"fileName": "<rootDir>/src/a.module.css",
184+
"length": 1,
185+
"start": {
186+
"column": 1,
187+
"line": 1,
188+
},
189+
"text": "Unclosed block",
190+
},
191+
]
192+
`);
193+
194+
// New .d.ts file is emitted
195+
await project.emitDtsFiles();
196+
expect(await iff.readFile('generated/src/a.module.css.d.ts')).toMatchInlineSnapshot(`
197+
"// @ts-nocheck
198+
declare const styles = {
199+
a_1: '' as readonly string,
200+
};
201+
export default styles;
202+
"
203+
`);
204+
205+
// New semantic diagnostics are reported
206+
await writeFile(iff.join('src/a.module.css'), `@import './non-existent.module.css';`);
207+
project.updateFile(iff.join('src/a.module.css'));
208+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
209+
[
210+
{
211+
"category": "error",
212+
"fileName": "<rootDir>/src/a.module.css",
213+
"length": 25,
214+
"start": {
215+
"column": 10,
216+
"line": 1,
217+
},
218+
"text": "Cannot import module './non-existent.module.css'",
219+
},
220+
]
221+
`);
222+
});
223+
test('changes diagnostics in files that import it directly or indirectly', async () => {
224+
// This test case suggests the following facts:
225+
// - The resolution cache should be invalidated.
226+
// - The check stage cache for files that directly import the changed file should be invalidated.
227+
// - The check stage cache for files that indirectly import the changed file should also be invalidated.
228+
const iff = await createIFF({
229+
'tsconfig.json': '{}',
230+
'src/a.module.css': '',
231+
'src/b.module.css': dedent`
232+
@value a_1 from "./a.module.css";
233+
@import "./a.module.css";
234+
`,
235+
'src/c.module.css': '@value a_2 from "./b.module.css";',
236+
});
237+
const project = createProject({ project: iff.rootDir });
238+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
239+
[
240+
{
241+
"category": "error",
242+
"fileName": "<rootDir>/src/b.module.css",
243+
"length": 3,
244+
"start": {
245+
"column": 8,
246+
"line": 1,
247+
},
248+
"text": "Module './a.module.css' has no exported token 'a_1'.",
249+
},
250+
{
251+
"category": "error",
252+
"fileName": "<rootDir>/src/c.module.css",
253+
"length": 3,
254+
"start": {
255+
"column": 8,
256+
"line": 1,
257+
},
258+
"text": "Module './b.module.css' has no exported token 'a_2'.",
259+
},
260+
]
261+
`);
262+
await writeFile(iff.join('src/a.module.css'), '@value a_1: red; @value a_2: blue;');
263+
project.updateFile(iff.join('src/a.module.css'));
264+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`[]`);
265+
});
266+
});
267+
268+
describe('removeFile', () => {
269+
test('The diagnostics of the removed file are not reported, and .d.ts file is not emitted', async () => {
270+
const iff = await createIFF({
271+
'tsconfig.json': '{}',
272+
'src/a.module.css': '.a_1 {',
273+
});
274+
const project = createProject({ project: iff.rootDir });
275+
276+
// Even if the file is deleted, diagnostics will not change until notified by `removeFile`.
277+
await rm(iff.join('src/a.module.css'));
278+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
279+
[
280+
{
281+
"category": "error",
282+
"fileName": "<rootDir>/src/a.module.css",
283+
"length": 1,
284+
"start": {
285+
"column": 1,
286+
"line": 1,
287+
},
288+
"text": "Unclosed block",
289+
},
290+
]
291+
`);
292+
293+
project.removeFile(iff.join('src/a.module.css'));
294+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
295+
[
296+
{
297+
"category": "error",
298+
"text": "The file specified in tsconfig.json not found.",
299+
},
300+
]
301+
`);
302+
303+
await project.emitDtsFiles();
304+
await expect(access(iff.join('generated/src/a.module.css.d.ts'))).rejects.toThrow();
305+
});
306+
test('changes diagnostics in files that import it directly or indirectly', async () => {
307+
// This test case suggests the following facts:
308+
// - The check stage cache for files that directly import the changed file should be invalidated.
309+
// - The check stage cache for files that indirectly import the changed file should also be invalidated.
310+
const iff = await createIFF({
311+
'tsconfig.json': '{}',
312+
'src/a.module.css': '@value a_1: red;',
313+
'src/b.module.css': '@import "./a.module.css";', // directly
314+
'src/c.module.css': '@value a_1 from "./b.module.css";', // indirectly
315+
});
316+
const project = createProject({ project: iff.rootDir });
317+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`[]`);
318+
await rm(iff.join('src/a.module.css'));
319+
project.removeFile(iff.join('src/a.module.css'));
320+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
321+
[
322+
{
323+
"category": "error",
324+
"fileName": "<rootDir>/src/b.module.css",
325+
"length": 14,
326+
"start": {
327+
"column": 10,
328+
"line": 1,
329+
},
330+
"text": "Cannot import module './a.module.css'",
331+
},
332+
{
333+
"category": "error",
334+
"fileName": "<rootDir>/src/c.module.css",
335+
"length": 3,
336+
"start": {
337+
"column": 8,
338+
"line": 1,
339+
},
340+
"text": "Module './b.module.css' has no exported token 'a_1'.",
341+
},
342+
]
343+
`);
344+
});
345+
test('changes the resolution results of import specifiers in other files', async () => {
346+
// This test case suggests the following facts:
347+
// - The resolution cache should be invalidated.
348+
// - The check stage cache for files that import the removed file should be invalidated.
349+
const iff = await createIFF({
350+
'tsconfig.json': dedent`
351+
{
352+
"compilerOptions": {
353+
"paths": {
354+
"@/a.module.css": ["src/a-1.module.css", "src/a-2.module.css"]
355+
}
356+
}
357+
}
358+
`,
359+
'src/a-1.module.css': '@value a_1: red;',
360+
'src/a-2.module.css': '@value a_2: red;',
361+
'src/b.module.css': '@import "@/a.module.css";',
362+
'src/c.module.css': '@value a_2 from "./b.module.css";',
363+
});
364+
const project = createProject({ project: iff.rootDir });
365+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`
366+
[
367+
{
368+
"category": "error",
369+
"fileName": "<rootDir>/src/c.module.css",
370+
"length": 3,
371+
"start": {
372+
"column": 8,
373+
"line": 1,
374+
},
375+
"text": "Module './b.module.css' has no exported token 'a_2'.",
376+
},
377+
]
378+
`);
379+
await rm(iff.join('src/a-1.module.css'));
380+
project.removeFile(iff.join('src/a-1.module.css'));
381+
expect(formatDiagnostics(project.getDiagnostics(), iff.rootDir)).toMatchInlineSnapshot(`[]`);
382+
});
383+
});
384+
34385
describe('getDiagnostics', () => {
35386
test('returns empty array when no diagnostics', async () => {
36387
const iff = await createIFF({

packages/codegen/src/runner.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,5 @@ describe('runCMK', () => {
9494
await expect(access(iff.join('generated/src/old.module.css.d.ts'))).rejects.toThrow();
9595
});
9696
});
97+
98+
describe.todo('runCMKInWatchMode', () => {});

0 commit comments

Comments
 (0)