From 7996b47aa6b78bab5c960f711f4e32a3935392ea Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 22:25:44 +0900 Subject: [PATCH 01/10] test(ts-plugin): split diagnostics and file-operation e2e tests into per-behavior cases Co-Authored-By: Claude Opus 4.7 (1M context) --- .../feature/semantic-diagnostics.test.ts | 162 +++++------ .../feature/syntactic-diagnostics.test.ts | 126 ++++----- .../ts-plugin/e2e-test/file-operation.test.ts | 263 ++++++++---------- 3 files changed, 250 insertions(+), 301 deletions(-) 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..90e78f9f 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,79 @@ 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(` - [ - { - "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; }'.", - }, - ] - `); +const tsserver = launchTsserver(); - const res2 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['a.module.css'], - }); - expect(res2.body).toMatchInlineSnapshot(` - [ - { - "category": "error", - "code": 0, - "end": { - "line": 2, - "offset": 32, - }, - "source": "css-modules-kit", - "start": { - "line": 2, - "offset": 29, +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + describe('from a TS file', () => { + 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, + ...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), }, - "text": "Module './c.module.css' has no exported token 'c_3'.", - }, - { - "category": "error", - "code": 0, - "end": { - "line": 3, - "offset": 35, + ]); + }); + }); + + describe('from a CSS file', () => { + test('reports a missing exported token in @value ... from', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value b_1, b_2 from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, + }); + 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, + source: 'css-modules-kit', + text: "Module './b.module.css' has no exported token 'b_2'.", + ...getRange('a.module.css', 'b_2'), }, - "source": "css-modules-kit", - "start": { - "line": 3, - "offset": 10, + ]); + }); + + test('reports an unresolvable @import specifier', 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, + source: 'css-modules-kit', + text: "Cannot import module './unresolvable.module.css'", + ...getRange('a.module.css', './unresolvable.module.css'), }, - "text": "Cannot import module './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..a59fe660 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,68 @@ -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(` - [ +const tsserver = launchTsserver(); + +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + test('reports `@value` with no name as invalid syntax', 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": 1, - "offset": 8, - }, - "source": "css-modules-kit", - "start": { - "line": 1, - "offset": 1, - }, - "text": "\`@value\` is a invalid syntax.", + category: 'error', + code: 0, + source: 'css-modules-kit', + text: '`@value` is a invalid syntax.', + ...getRange('a.module.css', '@value;'), }, + ]); + }); + + test('reports `:global(...)` inside `:local(...)` as not allowed', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `:local(:global(.a_1)) { color: red; }`, + }); + 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": 2, - "offset": 21, - }, - "source": "css-modules-kit", - "start": { - "line": 2, - "offset": 8, - }, - "text": "A \`:global(...)\` is not allowed inside of \`:local(...)\`.", + category: 'error', + code: 0, + source: 'css-modules-kit', + text: 'A `:global(...)` is not allowed inside of `:local(...)`.', + ...getRange('a.module.css', ':global(.a_1)'), }, + ]); + }); + + test('reports `:local` without parens as unsupported', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `:local .a_1 { color: red; }`, + }); + 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: 'css-modules-kit does not support `:local`. Use `:local(...)` instead.', + ...getRange('a.module.css', ':local'), }, - ] - `); + ]); + }); }); diff --git a/packages/ts-plugin/e2e-test/file-operation.test.ts b/packages/ts-plugin/e2e-test/file-operation.test.ts index bc9cdbe1..008b606f 100644 --- a/packages/ts-plugin/e2e-test/file-operation.test.ts +++ b/packages/ts-plugin/e2e-test/file-operation.test.ts @@ -1,164 +1,141 @@ 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, - }, - "text": "Cannot find module './a.module.css' or its corresponding type declarations.", - }, - ] - `); +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + describe('when adding a CSS module', () => { + test('reports a missing module diagnostic before the CSS module exists', 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'] }] }); - // Add a.module.css - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.join('a.module.css'), fileContent: '.a_1 { color: red; }' }], - }); + const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); - // 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, + expect(res.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'] }], - }); + // NOTE: After `sendUpdateOpen` adds the CSS module, the diagnostic should ideally clear, + // but a tsserver caching bug keeps the original `Cannot find module` diagnostic in place. + test('retains the missing module diagnostic after the 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'] }] }); + await tsserver.sendUpdateOpen({ + openFiles: [{ file: iff.join('a.module.css'), fileContent: '.a_1 { color: red; }' }], + }); + + const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); - const res1 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['index.ts'], + expect(res.body).toStrictEqual([ + { + category: 'error', + code: 2307, + text: "Cannot find module './a.module.css' or its corresponding type declarations.", + ...getRange('index.ts', "'./a.module.css'"), + }, + ]); + }); }); - expect(res1.body).toMatchInlineSnapshot(` - [ - { - "category": "error", - "code": 2339, - "end": { - "line": 2, - "offset": 11, + + describe('when updating a CSS module', () => { + test('propagates new CSS-side diagnostics when the CSS module is modified', async () => { + const { iff } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + await tsserver.sendUpdateOpen({ + changedFiles: [ + { + fileName: iff.paths['a.module.css'], + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: `@import './unresolvable.module.css';`, + }, + ], + }, + ], + }); + + const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['a.module.css'] }); + + expect(res.body).toStrictEqual([ + { + category: 'error', + code: 0, + source: 'css-modules-kit', + text: "Cannot import module './unresolvable.module.css'", + start: { line: 1, offset: 10 }, + end: { line: 1, offset: 35 }, }, - "start": { - "line": 2, - "offset": 8, + ]); + }); + + test('clears the unknown property diagnostic on the importer when the missing token is added', 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('reports a missing module diagnostic on the importer after the CSS module is removed'); }); - expect(res3.body).toMatchInlineSnapshot(`[]`); }); - -test.todo('removing file'); From c3f8e95701ec0a50548fc8db10ba5c31314bc1a1 Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 23:18:19 +0900 Subject: [PATCH 02/10] test(ts-plugin): add hover e2e test for the styles binding type Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ts-plugin/e2e-test/feature/hover.test.ts | 31 +++++++++++++++++++ .../ts-plugin/e2e-test/test-util/tsserver.ts | 2 ++ 2 files changed, 33 insertions(+) create mode 100644 packages/ts-plugin/e2e-test/feature/hover.test.ts diff --git a/packages/ts-plugin/e2e-test/feature/hover.test.ts b/packages/ts-plugin/e2e-test/feature/hover.test.ts new file mode 100644 index 00000000..c40e2951 --- /dev/null +++ b/packages/ts-plugin/e2e-test/feature/hover.test.ts @@ -0,0 +1,31 @@ +import dedent from 'dedent'; +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'; + +const tsserver = launchTsserver(); + +describe.each([ + { namedExports: false, expectedStylesA1Hover: `(property) 'a_1': string` }, + { namedExports: true, expectedStylesA1Hover: `(alias) var 'a_1': string\nexport 'a_1'` }, +])('namedExports: $namedExports', ({ namedExports, expectedStylesA1Hover }) => { + test('reports the type of a local class accessed via styles.', async () => { + const { iff, getLoc } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendQuickInfo({ + file: iff.paths['index.ts'], + ...getLoc('index.ts', 'a_1'), + }); + + expect(res.body?.displayString).toBe(expectedStylesA1Hover); + }); +}); diff --git a/packages/ts-plugin/e2e-test/test-util/tsserver.ts b/packages/ts-plugin/e2e-test/test-util/tsserver.ts index 01a37fce..c2d78ffc 100644 --- a/packages/ts-plugin/e2e-test/test-util/tsserver.ts +++ b/packages/ts-plugin/e2e-test/test-util/tsserver.ts @@ -32,6 +32,7 @@ interface Tsserver { args: server.protocol.CompletionDetailsRequest['arguments'], ): Promise; sendGetCodeFixes(args: server.protocol.CodeFixRequest['arguments']): Promise; + sendQuickInfo(args: server.protocol.QuickInfoRequest['arguments']): Promise; } export function launchTsserver(): Tsserver { @@ -82,6 +83,7 @@ export function launchTsserver(): Tsserver { sendCompletionInfo: async (args) => sendRequest(ts.server.protocol.CommandTypes.CompletionInfo, args), sendCompletionDetails: async (args) => sendRequest(ts.server.protocol.CommandTypes.CompletionDetails, args), sendGetCodeFixes: async (args) => sendRequest(ts.server.protocol.CommandTypes.GetCodeFixes, args), + sendQuickInfo: async (args) => sendRequest(ts.server.protocol.CommandTypes.Quickinfo, args), }; } From 682728d4a93232229faf59e6f3fff5bb8650f714 Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 23:36:57 +0900 Subject: [PATCH 03/10] test(ts-plugin): flatten describe groups in semantic-diagnostics e2e test Co-Authored-By: Claude Opus 4.7 (1M context) --- .../feature/semantic-diagnostics.test.ts | 114 +++++++++--------- 1 file changed, 55 insertions(+), 59 deletions(-) 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 90e78f9f..bd1fb214 100644 --- a/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts @@ -7,73 +7,69 @@ import { launchTsserver } from '../test-util/tsserver.js'; const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { - describe('from a TS file', () => { - 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, - ...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), - }, - ]); + 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'] }] }); - describe('from a CSS file', () => { - test('reports a missing exported token in @value ... from', async () => { - const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), - 'a.module.css': `@value b_1, b_2 from './b.module.css';`, - 'b.module.css': `@value b_1: red;`, - }); - await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); - const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['a.module.css'] }); + expect(res.body).toStrictEqual([ + { + 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), + }, + ]); + }); - expect(res.body).toStrictEqual([ - { - category: 'error', - code: 0, - source: 'css-modules-kit', - text: "Module './b.module.css' has no exported token 'b_2'.", - ...getRange('a.module.css', 'b_2'), - }, - ]); + test('reports a missing exported token in @value ... from', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value b_1, b_2 from './b.module.css';`, + 'b.module.css': `@value b_1: red;`, }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - test('reports an unresolvable @import specifier', 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'] }); - const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['a.module.css'] }); + expect(res.body).toStrictEqual([ + { + category: 'error', + code: 0, + source: 'css-modules-kit', + text: "Module './b.module.css' has no exported token 'b_2'.", + ...getRange('a.module.css', 'b_2'), + }, + ]); + }); - expect(res.body).toStrictEqual([ - { - category: 'error', - code: 0, - source: 'css-modules-kit', - text: "Cannot import module './unresolvable.module.css'", - ...getRange('a.module.css', './unresolvable.module.css'), - }, - ]); + test('reports an unresolvable @import specifier', 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, + source: 'css-modules-kit', + text: "Cannot import module './unresolvable.module.css'", + ...getRange('a.module.css', './unresolvable.module.css'), + }, + ]); }); }); From 39abf2051a6c9676ae6dfa45d1638acb2cf12ba5 Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 23:40:11 +0900 Subject: [PATCH 04/10] test(ts-plugin): consolidate semantic-diagnostics e2e test to a single representative case Exhaustive coverage of core checker diagnostic kinds is owned by packages/core/src/checker.test.ts. The e2e test now keeps only one representative case to verify that a CSS-side diagnostic surfaces in tsserver via ts-plugin. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../feature/semantic-diagnostics.test.ts | 27 ++++--------------- 1 file changed, 5 insertions(+), 22 deletions(-) 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 bd1fb214..cf34c10e 100644 --- a/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts @@ -32,28 +32,11 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: ]); }); - test('reports a missing exported token in @value ... from', async () => { - const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), - 'a.module.css': `@value b_1, b_2 from './b.module.css';`, - 'b.module.css': `@value b_1: red;`, - }); - 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, - source: 'css-modules-kit', - text: "Module './b.module.css' has no exported token 'b_2'.", - ...getRange('a.module.css', 'b_2'), - }, - ]); - }); - - test('reports an unresolvable @import specifier', async () => { + // This test only verifies that a CSS-side diagnostic from the core checker reaches + // tsserver via ts-plugin. Exhaustive coverage of every diagnostic kind lives in + // `packages/core/src/checker.test.ts`; an unresolvable `@import` is used here as a + // representative example. + test('surfaces a representative core checker diagnostic on a CSS module', async () => { const { iff, getRange } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.module.css': `@import './unresolvable.module.css';`, From 8428f5fe21da5283464d4309e8112c7875db95c9 Mon Sep 17 00:00:00 2001 From: mizdra Date: Tue, 5 May 2026 00:08:04 +0900 Subject: [PATCH 05/10] test(ts-plugin): rename the consolidated semantic-diagnostics e2e test to focus on the behavior Co-Authored-By: Claude Opus 4.7 (1M context) --- .../e2e-test/feature/semantic-diagnostics.test.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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 cf34c10e..a8fdf500 100644 --- a/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts @@ -32,11 +32,10 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: ]); }); - // This test only verifies that a CSS-side diagnostic from the core checker reaches - // tsserver via ts-plugin. Exhaustive coverage of every diagnostic kind lives in - // `packages/core/src/checker.test.ts`; an unresolvable `@import` is used here as a - // representative example. - test('surfaces a representative core checker diagnostic on a CSS module', async () => { + // This test only verifies that a CSS-side diagnostic reaches tsserver via ts-plugin. + // Exhaustive coverage of every diagnostic kind lives in `packages/core/src/checker.test.ts`; + // an unresolvable `@import` is used here as a representative example. + 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';`, From daa7d97f51e7963f780ba55d731401b3bc4e72c4 Mon Sep 17 00:00:00 2001 From: mizdra Date: Tue, 5 May 2026 00:10:11 +0900 Subject: [PATCH 06/10] test(ts-plugin): consolidate syntactic-diagnostics e2e test to a single representative case Exhaustive coverage of parser-side diagnostic kinds is owned by the parser tests under packages/core/src/parser/. The e2e test now keeps only one representative case to verify that a CSS-side syntactic diagnostic surfaces in tsserver via ts-plugin. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../feature/syntactic-diagnostics.test.ts | 45 ++----------------- 1 file changed, 4 insertions(+), 41 deletions(-) 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 a59fe660..53cd0c1d 100644 --- a/packages/ts-plugin/e2e-test/feature/syntactic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/feature/syntactic-diagnostics.test.ts @@ -6,7 +6,10 @@ import { launchTsserver } from '../test-util/tsserver.js'; const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { - test('reports `@value` with no name as invalid syntax', async () => { + // This test only verifies that a CSS-side syntactic diagnostic reaches tsserver via ts-plugin. + // Exhaustive coverage of every diagnostic kind lives in the parser tests under + // `packages/core/src/parser/`; `@value;` is used here as a representative example. + 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;`, @@ -25,44 +28,4 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }, ]); }); - - test('reports `:global(...)` inside `:local(...)` as not allowed', async () => { - const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), - 'a.module.css': `:local(:global(.a_1)) { color: red; }`, - }); - 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, - source: 'css-modules-kit', - text: 'A `:global(...)` is not allowed inside of `:local(...)`.', - ...getRange('a.module.css', ':global(.a_1)'), - }, - ]); - }); - - test('reports `:local` without parens as unsupported', async () => { - const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), - 'a.module.css': `:local .a_1 { color: red; }`, - }); - 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, - source: 'css-modules-kit', - text: 'css-modules-kit does not support `:local`. Use `:local(...)` instead.', - ...getRange('a.module.css', ':local'), - }, - ]); - }); }); From 527f8e2b03b7cb6ebb7986d6a08bb2aed495b70f Mon Sep 17 00:00:00 2001 From: mizdra Date: Tue, 5 May 2026 00:11:57 +0900 Subject: [PATCH 07/10] test(ts-plugin): drop redundant comments from diagnostics e2e tests Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts | 3 --- .../ts-plugin/e2e-test/feature/syntactic-diagnostics.test.ts | 3 --- 2 files changed, 6 deletions(-) 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 a8fdf500..6d86e5c4 100644 --- a/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts @@ -32,9 +32,6 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: ]); }); - // This test only verifies that a CSS-side diagnostic reaches tsserver via ts-plugin. - // Exhaustive coverage of every diagnostic kind lives in `packages/core/src/checker.test.ts`; - // an unresolvable `@import` is used here as a representative example. test('reports a semantic diagnostic on a CSS module file', async () => { const { iff, getRange } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 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 53cd0c1d..49839399 100644 --- a/packages/ts-plugin/e2e-test/feature/syntactic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/feature/syntactic-diagnostics.test.ts @@ -6,9 +6,6 @@ import { launchTsserver } from '../test-util/tsserver.js'; const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { - // This test only verifies that a CSS-side syntactic diagnostic reaches tsserver via ts-plugin. - // Exhaustive coverage of every diagnostic kind lives in the parser tests under - // `packages/core/src/parser/`; `@value;` is used here as a representative example. test('reports a syntactic diagnostic on a CSS module file', async () => { const { iff, getRange } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), From f6885cf5d40ab6b42fd54fa127f3759e75babda1 Mon Sep 17 00:00:00 2001 From: mizdra Date: Tue, 5 May 2026 00:21:00 +0900 Subject: [PATCH 08/10] test(ts-plugin): consolidate file-operation e2e tests around importer diagnostic updates Restore the original 1-test-per-file-operation structure and rename each test to focus on the behavior being verified: that adding, modifying, or removing a CSS module updates the importer's diagnostics. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../ts-plugin/e2e-test/file-operation.test.ts | 65 +++---------------- 1 file changed, 9 insertions(+), 56 deletions(-) diff --git a/packages/ts-plugin/e2e-test/file-operation.test.ts b/packages/ts-plugin/e2e-test/file-operation.test.ts index 008b606f..fcad565a 100644 --- a/packages/ts-plugin/e2e-test/file-operation.test.ts +++ b/packages/ts-plugin/e2e-test/file-operation.test.ts @@ -8,7 +8,7 @@ const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { describe('when adding a CSS module', () => { - test('reports a missing module diagnostic before the CSS module exists', async () => { + 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` @@ -18,9 +18,8 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); - - expect(res.body).toStrictEqual([ + const before = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); + expect(before.body).toStrictEqual([ { category: 'error', code: 2307, @@ -28,26 +27,15 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: ...getRange('index.ts', "'./a.module.css'"), }, ]); - }); - // NOTE: After `sendUpdateOpen` adds the CSS module, the diagnostic should ideally clear, - // but a tsserver caching bug keeps the original `Cannot find module` diagnostic in place. - test('retains the missing module diagnostic after the 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'] }] }); await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.join('a.module.css'), fileContent: '.a_1 { color: red; }' }], }); - const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['index.ts'] }); - - expect(res.body).toStrictEqual([ + 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, @@ -59,42 +47,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); describe('when updating a CSS module', () => { - test('propagates new CSS-side diagnostics when the CSS module is modified', async () => { - const { iff } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), - 'a.module.css': '', - }); - await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); - await tsserver.sendUpdateOpen({ - changedFiles: [ - { - fileName: iff.paths['a.module.css'], - textChanges: [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: `@import './unresolvable.module.css';`, - }, - ], - }, - ], - }); - - const res = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['a.module.css'] }); - - expect(res.body).toStrictEqual([ - { - category: 'error', - code: 0, - source: 'css-modules-kit', - text: "Cannot import module './unresolvable.module.css'", - start: { line: 1, offset: 10 }, - end: { line: 1, offset: 35 }, - }, - ]); - }); - - test('clears the unknown property diagnostic on the importer when the missing token is added', async () => { + 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` @@ -136,6 +89,6 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); describe('when removing a CSS module', () => { - test.todo('reports a missing module diagnostic on the importer after the CSS module is removed'); + test.todo("updates the importer's diagnostic when a CSS module is removed"); }); }); From fcad611eb8e5b9a2e1f20f20e61bab59a535f800 Mon Sep 17 00:00:00 2001 From: mizdra Date: Tue, 5 May 2026 11:52:11 +0900 Subject: [PATCH 09/10] Revert "test(ts-plugin): add hover e2e test for the styles binding type" This reverts commit c3f8e95701ec0a50548fc8db10ba5c31314bc1a1. --- .../ts-plugin/e2e-test/feature/hover.test.ts | 31 ------------------- .../ts-plugin/e2e-test/test-util/tsserver.ts | 2 -- 2 files changed, 33 deletions(-) delete mode 100644 packages/ts-plugin/e2e-test/feature/hover.test.ts diff --git a/packages/ts-plugin/e2e-test/feature/hover.test.ts b/packages/ts-plugin/e2e-test/feature/hover.test.ts deleted file mode 100644 index c40e2951..00000000 --- a/packages/ts-plugin/e2e-test/feature/hover.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -import dedent from 'dedent'; -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'; - -const tsserver = launchTsserver(); - -describe.each([ - { namedExports: false, expectedStylesA1Hover: `(property) 'a_1': string` }, - { namedExports: true, expectedStylesA1Hover: `(alias) var 'a_1': string\nexport 'a_1'` }, -])('namedExports: $namedExports', ({ namedExports, expectedStylesA1Hover }) => { - test('reports the type of a local class accessed via styles.', async () => { - const { iff, getLoc } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), - 'index.ts': dedent` - ${buildStylesImport('./a.module.css', { namedExports })} - styles.a_1; - `, - 'a.module.css': `.a_1 { color: red; }`, - }); - await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - - const res = await tsserver.sendQuickInfo({ - file: iff.paths['index.ts'], - ...getLoc('index.ts', 'a_1'), - }); - - expect(res.body?.displayString).toBe(expectedStylesA1Hover); - }); -}); diff --git a/packages/ts-plugin/e2e-test/test-util/tsserver.ts b/packages/ts-plugin/e2e-test/test-util/tsserver.ts index c2d78ffc..01a37fce 100644 --- a/packages/ts-plugin/e2e-test/test-util/tsserver.ts +++ b/packages/ts-plugin/e2e-test/test-util/tsserver.ts @@ -32,7 +32,6 @@ interface Tsserver { args: server.protocol.CompletionDetailsRequest['arguments'], ): Promise; sendGetCodeFixes(args: server.protocol.CodeFixRequest['arguments']): Promise; - sendQuickInfo(args: server.protocol.QuickInfoRequest['arguments']): Promise; } export function launchTsserver(): Tsserver { @@ -83,7 +82,6 @@ export function launchTsserver(): Tsserver { sendCompletionInfo: async (args) => sendRequest(ts.server.protocol.CommandTypes.CompletionInfo, args), sendCompletionDetails: async (args) => sendRequest(ts.server.protocol.CommandTypes.CompletionDetails, args), sendGetCodeFixes: async (args) => sendRequest(ts.server.protocol.CommandTypes.GetCodeFixes, args), - sendQuickInfo: async (args) => sendRequest(ts.server.protocol.CommandTypes.Quickinfo, args), }; } From ec693dc8c510c41b47ad2992af7963476c076915 Mon Sep 17 00:00:00 2001 From: mizdra Date: Tue, 5 May 2026 11:54:12 +0900 Subject: [PATCH 10/10] test(ts-plugin): verify the styles binding type via assignment in semantic-diagnostics e2e Replaces the reverted hover-based check with a structural type assignment (`type Expected = { a_1: string }; const _t: Expected = styles;`) inside semantic-diagnostics.test.ts. This avoids hard-coding the TypeScript compiler's hover wording while still exercising the .d.ts-generated type on the styles binding. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../feature/semantic-diagnostics.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) 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 6d86e5c4..a6bfeb55 100644 --- a/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts +++ b/packages/ts-plugin/e2e-test/feature/semantic-diagnostics.test.ts @@ -32,6 +32,23 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: ]); }); + 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([]); + }); + test('reports a semantic diagnostic on a CSS module file', async () => { const { iff, getRange } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }),