diff --git a/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts b/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts index 08ed47b4..a6bfeb55 100644 --- a/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts @@ -1,99 +1,71 @@ import dedent from 'dedent'; -import { expect, test } from 'vite-plus/test'; -import { createIFF } from '../test-util/fixture.js'; +import { describe, expect, test } from 'vite-plus/test'; +import { buildStylesImport, buildTSConfigJSON } from '../../src/test/builder.js'; +import { setupFixture } from '../test-util/fixture.js'; import { launchTsserver } from '../test-util/tsserver.js'; -test('Semantic Diagnostics', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - import styles from './a.module.css'; - type Expected = { a_1: string, a_2: string, b_1: string, c_1: string, c_alias: string, c_3: string }; - const t1: Expected = styles; - const t2: typeof styles = t1; - styles.unknown; - `, - 'a.module.css': dedent` - @import './b.module.css'; - @value c_1, c_2 as c_alias, c_3 from './c.module.css'; - @import './unresolvable.module.css'; - .a_1 { color: red; } - @value a_2: red; - `, - 'b.module.css': dedent` - .b_1 { color: red; } - `, - 'c.module.css': dedent` - @value c_1: red; - @value c_2: red; - `, - 'tsconfig.json': dedent` - { - "compilerOptions": {}, - "cmkOptions": { - "enabled": true, - "dtsOutDir": "generated" - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['index.ts'] }], - }); - const res1 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['index.ts'], - }); - expect(res1.body).toMatchInlineSnapshot(` - [ +const tsserver = launchTsserver(); + +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + test('reports an unknown property access on a styles binding', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.unknown; + `, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + + expect(res.body).toStrictEqual([ { - "category": "error", - "code": 2339, - "end": { - "line": 5, - "offset": 15, - }, - "start": { - "line": 5, - "offset": 8, - }, - "text": "Property 'unknown' does not exist on type '{ c_1: string; c_alias: string; c_3: any; b_1: string; a_1: string; a_2: string; }'.", + category: 'error', + code: 2339, + ...getRange('index.ts', 'unknown'), + // The `text` is not asserted because the message contains the type shape that + // varies with `namedExports` and is owned by the TypeScript compiler, not ts-plugin. + text: expect.any(String), }, - ] - `); + ]); + }); - const res2 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['a.module.css'], + test('provides the .d.ts-generated type on the styles binding', async () => { + const { iff } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + type Expected = { a_1: string }; + const _t: Expected = styles; + `, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + + expect(res.body).toStrictEqual([]); }); - expect(res2.body).toMatchInlineSnapshot(` - [ - { - "category": "error", - "code": 0, - "end": { - "line": 2, - "offset": 32, - }, - "source": "css-modules-kit", - "start": { - "line": 2, - "offset": 29, - }, - "text": "Module './c.module.css' has no exported token 'c_3'.", - }, + + test('reports a semantic diagnostic on a CSS module file', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@import './unresolvable.module.css';`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['a.module.css'] }); + + expect(res.body).toStrictEqual([ { - "category": "error", - "code": 0, - "end": { - "line": 3, - "offset": 35, - }, - "source": "css-modules-kit", - "start": { - "line": 3, - "offset": 10, - }, - "text": "Cannot import module './unresolvable.module.css'", + category: 'error', + code: 0, + source: 'css-modules-kit', + text: "Cannot import module './unresolvable.module.css'", + ...getRange('a.module.css', './unresolvable.module.css'), }, - ] - `); + ]); + }); }); diff --git a/packages/ts-plugin/e2e-test/feature/syntactic-diagnostics.test.ts b/packages/ts-plugin/e2e-test/feature/syntactic-diagnostics.test.ts index 11e3ed79..49839399 100644 --- a/packages/ts-plugin/e2e-test/feature/syntactic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/feature/syntactic-diagnostics.test.ts @@ -1,76 +1,28 @@ -import dedent from 'dedent'; -import { expect, test } from 'vite-plus/test'; -import { createIFF } from '../test-util/fixture.js'; +import { describe, expect, test } from 'vite-plus/test'; +import { buildTSConfigJSON } from '../../src/test/builder.js'; +import { setupFixture } from '../test-util/fixture.js'; import { launchTsserver } from '../test-util/tsserver.js'; -test('Syntactic Diagnostics', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'a.module.css': dedent` - @value; - :local(:global(.a_1)) { color: red; } - :local .a_2 { color: red; } - `, - 'tsconfig.json': dedent` - { - "compilerOptions": {}, - "cmkOptions": { - "enabled": true, - "dtsOutDir": "generated" - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['a.module.css'] }], - }); - const res1 = await tsserver.sendSyntacticDiagnosticsSync({ - file: iff.paths['a.module.css'], - }); - expect(res1.body).toMatchInlineSnapshot(` - [ - { - "category": "error", - "code": 0, - "end": { - "line": 1, - "offset": 8, - }, - "source": "css-modules-kit", - "start": { - "line": 1, - "offset": 1, - }, - "text": "\`@value\` is a invalid syntax.", - }, - { - "category": "error", - "code": 0, - "end": { - "line": 2, - "offset": 21, - }, - "source": "css-modules-kit", - "start": { - "line": 2, - "offset": 8, - }, - "text": "A \`:global(...)\` is not allowed inside of \`:local(...)\`.", - }, +const tsserver = launchTsserver(); + +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + test('reports a syntactic diagnostic on a CSS module file', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value;`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendSyntacticDiagnosticsSync({ file: iff.paths['a.module.css'] }); + + expect(res.body).toStrictEqual([ { - "category": "error", - "code": 0, - "end": { - "line": 3, - "offset": 7, - }, - "source": "css-modules-kit", - "start": { - "line": 3, - "offset": 1, - }, - "text": "css-modules-kit does not support \`:local\`. Use \`:local(...)\` instead.", + category: 'error', + code: 0, + source: 'css-modules-kit', + text: '`@value` is a invalid syntax.', + ...getRange('a.module.css', '@value;'), }, - ] - `); + ]); + }); }); diff --git a/packages/ts-plugin/e2e-test/file-operation.test.ts b/packages/ts-plugin/e2e-test/file-operation.test.ts index bc9cdbe1..fcad565a 100644 --- a/packages/ts-plugin/e2e-test/file-operation.test.ts +++ b/packages/ts-plugin/e2e-test/file-operation.test.ts @@ -1,164 +1,94 @@ import dedent from 'dedent'; -import { expect, test } from 'vite-plus/test'; -import { createIFF } from './test-util/fixture.js'; +import { describe, expect, test } from 'vite-plus/test'; +import { buildStylesImport, buildTSConfigJSON } from '../src/test/builder.js'; +import { setupFixture } from './test-util/fixture.js'; import { launchTsserver } from './test-util/tsserver.js'; -test('adding file', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - import styles from './a.module.css'; - styles.a_1; - `, - 'tsconfig.json': dedent` - { - "compilerOptions": {}, - "cmkOptions": { "enabled": true } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['index.ts'] }], - }); +const tsserver = launchTsserver(); - // If a.module.css does not exist, a diagnostic should be reported in index.ts - const res1 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['index.ts'], - }); - expect(res1.body).toMatchInlineSnapshot(` - [ - { - "category": "error", - "code": 2307, - "end": { - "line": 1, - "offset": 36, - }, - "start": { - "line": 1, - "offset": 20, +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + describe('when adding a CSS module', () => { + test("updates the importer's diagnostic when a CSS module is added", async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const before = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + expect(before.body).toStrictEqual([ + { + category: 'error', + code: 2307, + text: "Cannot find module './a.module.css' or its corresponding type declarations.", + ...getRange('index.ts', "'./a.module.css'"), }, - "text": "Cannot find module './a.module.css' or its corresponding type declarations.", - }, - ] - `); + ]); - // Add a.module.css - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.join('a.module.css'), fileContent: '.a_1 { color: red; }' }], - }); + await tsserver.sendUpdateOpen({ + openFiles: [{ file: iff.join('a.module.css'), fileContent: '.a_1 { color: red; }' }], + }); - // If a.module.css exists, the diagnostic should disappear - const res2 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['index.ts'], - }); - // TODO: This should become `[]`, but due to a bug in tsserver, it does not. - expect(res2.body).toMatchInlineSnapshot(` - [ - { - "category": "error", - "code": 2307, - "end": { - "line": 1, - "offset": 36, + const after = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + // NOTE: Ideally `after` should be `[]`, but a tsserver caching bug keeps the + // original `Cannot find module` diagnostic in place. + expect(after.body).toStrictEqual([ + { + category: 'error', + code: 2307, + text: "Cannot find module './a.module.css' or its corresponding type declarations.", + ...getRange('index.ts', "'./a.module.css'"), }, - "start": { - "line": 1, - "offset": 20, - }, - "text": "Cannot find module './a.module.css' or its corresponding type declarations.", - }, - ] - `); -}); - -test('updating file', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - import styles from './a.module.css'; - styles.a_1; - `, - 'a.module.css': '', - 'tsconfig.json': dedent` - { - "compilerOptions": {}, - "cmkOptions": { "enabled": true } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['index.ts'] }], + ]); + }); }); - const res1 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['index.ts'], - }); - expect(res1.body).toMatchInlineSnapshot(` - [ - { - "category": "error", - "code": 2339, - "end": { - "line": 2, - "offset": 11, - }, - "start": { - "line": 2, - "offset": 8, + describe('when updating a CSS module', () => { + test("updates the importer's diagnostic when a CSS module is modified", async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const before = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + expect(before.body).toStrictEqual([ + { + category: 'error', + code: 2339, + ...getRange('index.ts', 'a_1'), + text: expect.any(String), }, - "text": "Property 'a_1' does not exist on type '{}'.", - }, - ] - `); + ]); - // Update a.module.css to have a semantic error - await tsserver.sendUpdateOpen({ - changedFiles: [ - { - fileName: iff.paths['a.module.css'], - textChanges: [ + await tsserver.sendUpdateOpen({ + changedFiles: [ { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: dedent` - @import './unresolvable.module.css'; - .a_1 {} - `, + fileName: iff.paths['a.module.css'], + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: `.a_1 {}`, + }, + ], }, ], - }, - ], - }); + }); - // The diagnostics in a.module.css are updated - const res2 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['a.module.css'], + const after = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + expect(after.body).toStrictEqual([]); + }); }); - expect(res2.body).toMatchInlineSnapshot(` - [ - { - "category": "error", - "code": 0, - "end": { - "line": 1, - "offset": 35, - }, - "source": "css-modules-kit", - "start": { - "line": 1, - "offset": 10, - }, - "text": "Cannot import module './unresolvable.module.css'", - }, - ] - `); - // The diagnostics of files importing a.module.css are updated. - const res3 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['index.ts'], + describe('when removing a CSS module', () => { + test.todo("updates the importer's diagnostic when a CSS module is removed"); }); - expect(res3.body).toMatchInlineSnapshot(`[]`); }); - -test.todo('removing file');