From 89b47d3b03ed23fd4898111589b2faa6bed652fc Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 01:18:29 +0900 Subject: [PATCH 1/6] test(ts-plugin): split completion and code-fix e2e tests into per-behavior cases Refactor both test files so each `test` block owns a minimal fixture and asserts a single behavior, organized by the .d.ts/mapping pattern under test. Follows the structure established in #378 and #379. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../e2e-test/feature/code-fix.test.ts | 425 ++++++++---------- .../e2e-test/feature/completion.test.ts | 356 +++++++-------- 2 files changed, 346 insertions(+), 435 deletions(-) diff --git a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts index 8f31b64c..eb81d8f6 100644 --- a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts +++ b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts @@ -5,249 +5,212 @@ import { CANNOT_FIND_NAME_ERROR_CODE, PROPERTY_DOES_NOT_EXIST_ERROR_CODES, } from '../../src/language-service/feature/code-fix.js'; -import { createIFF } from '../test-util/fixture.js'; +import { buildStylesImport, buildTSConfigJSON } from '../../src/test/builder.js'; +import { setupFixture } from '../test-util/fixture.js'; import { formatPath, launchTsserver, normalizeCodeFixActions } from '../test-util/tsserver.js'; -test.each([ - { - namedExports: false, - importStatement: "import styles from './a.module.css';", - }, - { - namedExports: true, - importStatement: "import * as styles from './a.module.css';", - }, -])( - 'fixMissingCSSRule inserts a new CSS rule for a missing class property (namedExports: $namedExports)', - async ({ namedExports, importStatement }) => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'a.tsx': dedent` - ${importStatement} - import bStyles from './b.module.css'; - styles.a_1; - bStyles.b_2; - `, - 'a.module.css': '', - 'b.module.css': dedent` - .b_1 { - color: red; - } - `, - 'tsconfig.json': dedent` - { - "cmkOptions": { - "enabled": true, - "namedExports": ${namedExports} - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['tsconfig.json'] }], - }); +const tsserver = launchTsserver(); - const res1 = await tsserver.sendGetCodeFixes({ - errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]], - file: iff.paths['a.tsx'], - startLine: 3, - startOffset: 11, - endLine: 3, - endOffset: 11, - }); - expect(normalizeCodeFixActions(res1.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'fixMissingCSSRule', - changes: [ - { - fileName: formatPath(iff.paths['a.module.css']), - textChanges: [{ start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: '\n.a_1 {\n \n}' }], - }, - ], - }, - ]), - ); +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + describe('fixMissingCSSRule', () => { + test('inserts a new CSS rule into an empty CSS module', 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': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - const res2 = await tsserver.sendGetCodeFixes({ - errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[1]], - file: iff.paths['a.tsx'], - startLine: 3, - startOffset: 11, - endLine: 3, - endOffset: 11, - }); - expect(normalizeCodeFixActions(res2.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'fixMissingCSSRule', - changes: [ - { - fileName: formatPath(iff.paths['a.module.css']), - textChanges: [{ start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: '\n.a_1 {\n \n}' }], - }, - ], - }, - ]), - ); + const loc = getLoc('index.ts', 'a_1'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]], + file: iff.paths['index.ts'], + startLine: loc.line, + startOffset: loc.offset, + endLine: loc.line, + endOffset: loc.offset, + }); - const res3 = await tsserver.sendGetCodeFixes({ - errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]], - file: iff.paths['a.tsx'], - startLine: 4, - startOffset: 12, - endLine: 4, - endOffset: 12, + expect(normalizeCodeFixActions(res.body!)).toStrictEqual( + normalizeCodeFixActions([ + { + fixName: 'fixMissingCSSRule', + changes: [ + { + fileName: formatPath(iff.paths['a.module.css']), + textChanges: [ + { start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: '\n.a_1 {\n \n}' }, + ], + }, + ], + }, + ]), + ); }); - expect(normalizeCodeFixActions(res3.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'fixMissingCSSRule', - changes: [ - { - fileName: formatPath(iff.paths['b.module.css']), - textChanges: [{ start: { line: 3, offset: 2 }, end: { line: 3, offset: 2 }, newText: '\n.b_2 {\n \n}' }], - }, - ], - }, - ]), - ); - }, -); -test.each([ - { - name: 'auto-import inserts default import statement if namedExports is false', - namedExports: false, - importStatement: `import styles from "./a.module.css";`, - }, - { - name: 'auto-import inserts namespace import statement if namedExports is true', - namedExports: true, - importStatement: `import * as styles from "./a.module.css";`, - }, -])('$name', async ({ namedExports, importStatement }) => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - styles; - `, - 'a.module.css': '', - 'tsconfig.json': dedent` - { - "cmkOptions": { - "enabled": true, - "namedExports": ${namedExports} - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['tsconfig.json'] }], - }); - const res = await tsserver.sendGetCodeFixes({ - errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], - file: iff.paths['index.ts'], - startLine: 1, - startOffset: 1, - endLine: 1, - endOffset: 7, + test('appends a new CSS rule to a non-empty CSS module', async () => { + const { iff, getLoc } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_2; + `, + 'a.module.css': dedent` + .a_1 { + color: red; + } + `, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const loc = getLoc('index.ts', 'a_2'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]], + file: iff.paths['index.ts'], + startLine: loc.line, + startOffset: loc.offset, + endLine: loc.line, + endOffset: loc.offset, + }); + + expect(normalizeCodeFixActions(res.body!)).toStrictEqual( + normalizeCodeFixActions([ + { + fixName: 'fixMissingCSSRule', + changes: [ + { + fileName: formatPath(iff.paths['a.module.css']), + textChanges: [ + { start: { line: 3, offset: 2 }, end: { line: 3, offset: 2 }, newText: '\n.a_2 {\n \n}' }, + ], + }, + ], + }, + ]), + ); + }); }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'import', - changes: [ + + describe('auto-import', () => { + test('inserts the import statement when accepted', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': `styles;`, + 'a.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const range = getRange('index.ts', 'styles'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], + file: iff.paths['index.ts'], + startLine: range.start.line, + startOffset: range.start.offset, + endLine: range.end.line, + endOffset: range.end.offset, + }); + + const importStatement = namedExports + ? `import * as styles from "./a.module.css";` + : `import styles from "./a.module.css";`; + expect(normalizeCodeFixActions(res.body!)).toStrictEqual( + normalizeCodeFixActions([ { - fileName: formatPath(iff.paths['index.ts']), - textChanges: [ + fixName: 'import', + changes: [ { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: `${importStatement}${ts.sys.newLine}${ts.sys.newLine}`, + fileName: formatPath(iff.paths['index.ts']), + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: `${importStatement}${ts.sys.newLine}${ts.sys.newLine}`, + }, + ], }, ], }, - ], - }, - ]), - ); -}); + ]), + ); + }); -test('auto-import excludes generated files from suggestions', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - styles; - `, - 'generated/a.module.css.d.ts': dedent` - const styles: {}; - export default styles; - `, - 'tsconfig.json': dedent` - { - "cmkOptions": { - "enabled": true, - "dtsOutDir": "generated" - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['tsconfig.json'] }], - }); - const res = await tsserver.sendGetCodeFixes({ - errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], - file: iff.paths['index.ts'], - startLine: 1, - startOffset: 1, - endLine: 1, - endOffset: 7, + test('excludes generated files from suggestions', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports, dtsOutDir: 'generated' }, + }), + 'index.ts': `styles;`, + 'generated/a.module.css.d.ts': dedent` + const styles: {}; + export default styles; + `, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const range = getRange('index.ts', 'styles'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], + file: iff.paths['index.ts'], + startLine: range.start.line, + startOffset: range.start.offset, + endLine: range.end.line, + endOffset: range.end.offset, + }); + + expect(normalizeCodeFixActions(res.body!)).toStrictEqual([]); + }); }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual([]); }); -describe('auto-import suggests named exports instead of namespace import when prioritizeNamedImports is true', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - styles; - a_1; - `, - 'a.module.css': dedent` - .a_1 { color: red; } - `, - 'tsconfig.json': dedent` - { - "cmkOptions": { - "enabled": true, - "namedExports": true, - "prioritizeNamedImports": true - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['tsconfig.json'] }], - }); - test.each([ - { - name: 'styles', +describe('prioritizeNamedImports (namedExports: true)', () => { + test('omits the default styles binding code fix', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: true }, + }), + 'index.ts': `styles;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const range = getRange('index.ts', 'styles'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], file: iff.paths['index.ts'], - startLine: 1, - startOffset: 1, - endLine: 1, - endOffset: 7, - expected: [], - }, - { - name: 'a_1', + startLine: range.start.line, + startOffset: range.start.offset, + endLine: range.end.line, + endOffset: range.end.offset, + }); + + expect(normalizeCodeFixActions(res.body!)).toStrictEqual([]); + }); + + test('suggests a named import code fix', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: true }, + }), + 'index.ts': `a_1;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const range = getRange('index.ts', 'a_1'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], file: iff.paths['index.ts'], - startLine: 2, - startOffset: 1, - endLine: 2, - endOffset: 4, - expected: [ + startLine: range.start.line, + startOffset: range.start.offset, + endLine: range.end.line, + endOffset: range.end.offset, + }); + + expect(normalizeCodeFixActions(res.body!)).toStrictEqual( + normalizeCodeFixActions([ { fixName: 'import', changes: [ @@ -263,17 +226,7 @@ describe('auto-import suggests named exports instead of namespace import when pr }, ], }, - ], - }, - ])('$name', async ({ file, startLine, startOffset, endLine, endOffset, expected }) => { - const res = await tsserver.sendGetCodeFixes({ - errorCodes: [2304], - file, - startLine, - startOffset, - endLine, - endOffset, - }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual(normalizeCodeFixActions(expected)); + ]), + ); }); }); diff --git a/packages/ts-plugin/e2e-test/feature/completion.test.ts b/packages/ts-plugin/e2e-test/feature/completion.test.ts index e9941a9b..f73a0470 100644 --- a/packages/ts-plugin/e2e-test/feature/completion.test.ts +++ b/packages/ts-plugin/e2e-test/feature/completion.test.ts @@ -2,225 +2,183 @@ import { join } from '@css-modules-kit/core'; import dedent from 'dedent'; import ts from 'typescript'; import { describe, expect, test } from 'vite-plus/test'; -import { createIFF } from '../test-util/fixture.js'; +import { buildStylesImport, buildTSConfigJSON } from '../../src/test/builder.js'; +import { setupFixture } from '../test-util/fixture.js'; import { launchTsserver, normalizeCompletionDetails, normalizeCompletionEntry } from '../test-util/tsserver.js'; const reactDtsPath = join(require.resolve('@types/react/package.json'), '../index.d.ts'); +const tsserver = launchTsserver(); -describe('Completion', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'a.tsx': dedent` - styles; - const jsx =
; - `, - 'b.tsx': dedent` - import styles from './b.module.css'; - const jsx =
; - `, - 'a.module.css': '', - 'b.module.css': '', - // Generated files should be excluded from import statement suggestions - 'generated/generated.module.css.d.ts': dedent` - const styles: {}; - export default styles; - `, - 'tsconfig.json': dedent` - { - "compilerOptions": { - "jsx": "react-jsx", - "types": ["${reactDtsPath}"] +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + describe('styles binding suggestion', () => { + test('prioritizes the CSS module corresponding to the current component file', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + compilerOptions: { jsx: 'react-jsx', types: [reactDtsPath] }, + cmkOptions: { namedExports }, + }), + 'a.tsx': `styles;`, + 'a.module.css': '', + 'b.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.tsx'] }] }); + await tsserver.sendConfigure({ + preferences: { + includeCompletionsForModuleExports: true, + quotePreference: 'single', }, - "cmkOptions": { - "enabled": true, - "dtsOutDir": "generated" - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['tsconfig.json'] }], - }); - await tsserver.sendConfigure({ - preferences: { - includeCompletionsForModuleExports: true, - includeCompletionsWithSnippetText: true, - includeCompletionsWithInsertText: true, - jsxAttributeCompletionStyle: 'auto', - quotePreference: 'single', - }, - }); - test.each([ - { - name: 'styles', - entryName: 'styles', - file: iff.paths['a.tsx'], - line: 1, - offset: 7, - expected: [ - { name: 'styles', sortText: '0', source: './a.module.css' }, - { name: 'styles', sortText: '16', source: './b.module.css' }, - ], - }, - { - name: "className with `quotePreference: 'double'`", - entryName: 'className', - quotePreference: 'double' as const, - file: iff.paths['a.tsx'], - line: 2, - offset: 27, - expected: [{ name: 'className', insertText: 'className={$1}', sortText: expect.anything() }], - }, - { - name: "className with `quotePreference: 'single'`", - entryName: 'className', - quotePreference: 'single' as const, - file: iff.paths['b.tsx'], - line: 2, - offset: 27, - expected: [{ name: 'className', insertText: 'className={$1}', sortText: expect.anything() }], - }, - ])('Completions for $name', async ({ entryName, quotePreference, file, line, offset, expected }) => { - await tsserver.sendConfigure({ - preferences: { - quotePreference: quotePreference ?? 'auto', - }, - }); - const res = await tsserver.sendCompletionInfo({ - file, - line, - offset, + }); + + const res = await tsserver.sendCompletionInfo({ + file: iff.paths['a.tsx'], + ...getRange('a.tsx', 'styles').end, + }); + + expect( + normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'styles') ?? []), + ).toStrictEqual( + normalizeCompletionEntry([ + { name: 'styles', sortText: '0', source: './a.module.css' }, + { name: 'styles', sortText: '16', source: './b.module.css' }, + ]), + ); }); - expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === entryName) ?? [])).toStrictEqual( - normalizeCompletionEntry(expected), - ); - }); -}); -test.each([ - { - name: 'auto-import inserts default import statement if namedExports is false', - namedExports: false, - importStatement: `import styles from './a.module.css';`, - }, - { - name: 'auto-import inserts namespace import statement if namedExports is true', - namedExports: true, - importStatement: `import * as styles from './a.module.css';`, - }, -])('$name', async ({ namedExports, importStatement }) => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - styles; - `, - 'a.module.css': '', - 'tsconfig.json': dedent` - { - "cmkOptions": { - "enabled": true, - "namedExports": ${namedExports} - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['tsconfig.json'] }], - }); - await tsserver.sendConfigure({ - preferences: { quotePreference: 'single' }, - }); - const res = await tsserver.sendCompletionDetails({ - file: iff.paths['index.ts'], - line: 1, - offset: 7, - entryNames: [ - { - name: 'styles', - source: './a.module.css', - data: { - exportName: ts.InternalSymbolName.Default, - fileName: iff.paths['a.module.css'], - moduleSpecifier: './a.module.css', - }, - }, - ], - }); - expect(normalizeCompletionDetails(res.body!)).toStrictEqual([ - { - codeActions: [ + test('inserts the import statement when accepted', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': `styles;`, + 'a.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + await tsserver.sendConfigure({ + preferences: { quotePreference: 'single' }, + }); + + const res = await tsserver.sendCompletionDetails({ + file: iff.paths['index.ts'], + ...getRange('index.ts', 'styles').end, + entryNames: [ + { + name: 'styles', + source: './a.module.css', + data: { + exportName: ts.InternalSymbolName.Default, + fileName: iff.paths['a.module.css'], + moduleSpecifier: './a.module.css', + }, + }, + ], + }); + + const importStatement = buildStylesImport('./a.module.css', { namedExports }); + expect(normalizeCompletionDetails(res.body!)).toStrictEqual([ { - changes: [ + codeActions: [ { - fileName: iff.paths['index.ts'], - textChanges: [ + changes: [ { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: `${importStatement}${ts.sys.newLine}${ts.sys.newLine}`, + fileName: iff.paths['index.ts'], + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: `${importStatement}${ts.sys.newLine}${ts.sys.newLine}`, + }, + ], }, ], }, ], }, - ], - }, - ]); -}); - -describe('auto-import suggests named exports instead of namespace import when prioritizeNamedImports is true', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - styles; - a_1; - `, - 'a.module.css': dedent` - .a_1 { color: red; } - `, - 'tsconfig.json': dedent` - { - "cmkOptions": { - "enabled": true, - "namedExports": true, - "prioritizeNamedImports": true - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['tsconfig.json'] }], + ]); + }); }); - await tsserver.sendConfigure({ - preferences: { - includeCompletionsForModuleExports: true, - }, + + describe('className attribute snippet', () => { + test.each([{ quotePreference: 'single' as const }, { quotePreference: 'double' as const }])( + 'completes as className={$$1} with quotePreference: $quotePreference', + async ({ quotePreference }) => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + compilerOptions: { jsx: 'react-jsx', types: [reactDtsPath] }, + cmkOptions: { namedExports }, + }), + 'a.tsx': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + const jsx =
; + `, + 'a.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.tsx'] }] }); + await tsserver.sendConfigure({ + preferences: { + includeCompletionsWithSnippetText: true, + includeCompletionsWithInsertText: true, + jsxAttributeCompletionStyle: 'auto', + quotePreference, + }, + }); + + const res = await tsserver.sendCompletionInfo({ + file: iff.paths['a.tsx'], + ...getRange('a.tsx', 'className').end, + }); + + expect( + normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'className') ?? []), + ).toStrictEqual( + normalizeCompletionEntry([{ name: 'className', insertText: 'className={$1}', sortText: expect.anything() }]), + ); + }, + ); }); - test.each([ - { - name: 'styles', - entryName: 'styles', - file: iff.paths['index.ts'], - line: 1, - offset: 7, - expected: [], - }, - { - name: 'a_1', - entryName: 'a_1', +}); + +describe('prioritizeNamedImports (namedExports: true)', () => { + test('omits the default styles binding from suggestions', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: true }, + }), + 'index.ts': `styles;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + await tsserver.sendConfigure({ + preferences: { includeCompletionsForModuleExports: true }, + }); + + const res = await tsserver.sendCompletionInfo({ file: iff.paths['index.ts'], - line: 2, - offset: 4, - expected: [{ name: 'a_1', sortText: '16', source: './a.module.css' }], - }, - ])('Completions for $name', async ({ entryName, file, line, offset, expected }) => { + ...getRange('index.ts', 'styles').end, + }); + + expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'styles') ?? [])).toStrictEqual( + [], + ); + }); + + test('suggests named token bindings', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: true }, + }), + 'index.ts': `a_1;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + await tsserver.sendConfigure({ + preferences: { includeCompletionsForModuleExports: true }, + }); + const res = await tsserver.sendCompletionInfo({ - file, - line, - offset, + file: iff.paths['index.ts'], + ...getRange('index.ts', 'a_1').end, }); - expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === entryName) ?? [])).toStrictEqual( - normalizeCompletionEntry(expected), + + expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'a_1') ?? [])).toStrictEqual( + normalizeCompletionEntry([{ name: 'a_1', sortText: '16', source: './a.module.css' }]), ); }); }); From b2dcb2609d9a00dd01cacfe3ea132ef0c4ca6ff1 Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 01:39:54 +0900 Subject: [PATCH 2/6] test(ts-plugin): drop unused JSX compilerOptions from styles priority test The fixture only contains `styles;` and never renders any JSX, so the `jsx` and `types` compilerOptions are not needed for that test. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/ts-plugin/e2e-test/feature/completion.test.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/ts-plugin/e2e-test/feature/completion.test.ts b/packages/ts-plugin/e2e-test/feature/completion.test.ts index f73a0470..9529c3d3 100644 --- a/packages/ts-plugin/e2e-test/feature/completion.test.ts +++ b/packages/ts-plugin/e2e-test/feature/completion.test.ts @@ -13,10 +13,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: describe('styles binding suggestion', () => { test('prioritizes the CSS module corresponding to the current component file', async () => { const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ - compilerOptions: { jsx: 'react-jsx', types: [reactDtsPath] }, - cmkOptions: { namedExports }, - }), + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'a.tsx': `styles;`, 'a.module.css': '', 'b.module.css': '', From 414fa058c74f41ac645dd0c5c8212b41b2782986 Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 19:05:07 +0900 Subject: [PATCH 3/6] test(ts-plugin): cover named token suppression when prioritizeNamedImports is false Add the negative-side cases for the `namedExports: true && !prioritizeNamedImports` branches so that both the positive (current `prioritizeNamedImports: true`) and negative behaviors of the suppression logic are pinned: - completion: named tokens are filtered out of suggestions - code-fix: named import code fixes are excluded Reorganize the existing `prioritizeNamedImports (namedExports: true)` describe into two parallel `prioritizeNamedImports: false / true` blocks under a single parent so the on/off pair is visually adjacent. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../e2e-test/feature/code-fix.test.ts | 141 +++++++++++------- .../e2e-test/feature/completion.test.ts | 101 ++++++++----- 2 files changed, 146 insertions(+), 96 deletions(-) diff --git a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts index eb81d8f6..93b67fcc 100644 --- a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts +++ b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts @@ -165,68 +165,93 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); }); -describe('prioritizeNamedImports (namedExports: true)', () => { - test('omits the default styles binding code fix', async () => { - const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ - cmkOptions: { namedExports: true, prioritizeNamedImports: true }, - }), - 'index.ts': `styles;`, - 'a.module.css': `.a_1 { color: red; }`, - }); - await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - - const range = getRange('index.ts', 'styles'); - const res = await tsserver.sendGetCodeFixes({ - errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], - file: iff.paths['index.ts'], - startLine: range.start.line, - startOffset: range.start.offset, - endLine: range.end.line, - endOffset: range.end.offset, - }); +describe('named import code fix (namedExports: true)', () => { + describe('prioritizeNamedImports: false (default)', () => { + test('omits the named import code fix', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true } }), + 'index.ts': `a_1;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual([]); - }); + const range = getRange('index.ts', 'a_1'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], + file: iff.paths['index.ts'], + startLine: range.start.line, + startOffset: range.start.offset, + endLine: range.end.line, + endOffset: range.end.offset, + }); - test('suggests a named import code fix', async () => { - const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ - cmkOptions: { namedExports: true, prioritizeNamedImports: true }, - }), - 'index.ts': `a_1;`, - 'a.module.css': `.a_1 { color: red; }`, + expect(normalizeCodeFixActions(res.body!)).toStrictEqual([]); }); - await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - - const range = getRange('index.ts', 'a_1'); - const res = await tsserver.sendGetCodeFixes({ - errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], - file: iff.paths['index.ts'], - startLine: range.start.line, - startOffset: range.start.offset, - endLine: range.end.line, - endOffset: range.end.offset, + }); + + describe('prioritizeNamedImports: true', () => { + test('omits the default styles binding code fix', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: true }, + }), + 'index.ts': `styles;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const range = getRange('index.ts', 'styles'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], + file: iff.paths['index.ts'], + startLine: range.start.line, + startOffset: range.start.offset, + endLine: range.end.line, + endOffset: range.end.offset, + }); + + expect(normalizeCodeFixActions(res.body!)).toStrictEqual([]); }); - expect(normalizeCodeFixActions(res.body!)).toStrictEqual( - normalizeCodeFixActions([ - { - fixName: 'import', - changes: [ - { - fileName: formatPath(iff.paths['index.ts']), - textChanges: [ - { - start: { line: 1, offset: 1 }, - end: { line: 1, offset: 1 }, - newText: `import { a_1 } from "./a.module.css";${ts.sys.newLine}${ts.sys.newLine}`, - }, - ], - }, - ], - }, - ]), - ); + test('suggests a named import code fix', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: true }, + }), + 'index.ts': `a_1;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const range = getRange('index.ts', 'a_1'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [CANNOT_FIND_NAME_ERROR_CODE], + file: iff.paths['index.ts'], + startLine: range.start.line, + startOffset: range.start.offset, + endLine: range.end.line, + endOffset: range.end.offset, + }); + + expect(normalizeCodeFixActions(res.body!)).toStrictEqual( + normalizeCodeFixActions([ + { + fixName: 'import', + changes: [ + { + fileName: formatPath(iff.paths['index.ts']), + textChanges: [ + { + start: { line: 1, offset: 1 }, + end: { line: 1, offset: 1 }, + newText: `import { a_1 } from "./a.module.css";${ts.sys.newLine}${ts.sys.newLine}`, + }, + ], + }, + ], + }, + ]), + ); + }); }); }); diff --git a/packages/ts-plugin/e2e-test/feature/completion.test.ts b/packages/ts-plugin/e2e-test/feature/completion.test.ts index 9529c3d3..dc46e41b 100644 --- a/packages/ts-plugin/e2e-test/feature/completion.test.ts +++ b/packages/ts-plugin/e2e-test/feature/completion.test.ts @@ -132,50 +132,75 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); }); -describe('prioritizeNamedImports (namedExports: true)', () => { - test('omits the default styles binding from suggestions', async () => { - const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ - cmkOptions: { namedExports: true, prioritizeNamedImports: true }, - }), - 'index.ts': `styles;`, - 'a.module.css': `.a_1 { color: red; }`, - }); - await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - await tsserver.sendConfigure({ - preferences: { includeCompletionsForModuleExports: true }, - }); +describe('named token completion (namedExports: true)', () => { + describe('prioritizeNamedImports: false (default)', () => { + test('omits named tokens from suggestions', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true } }), + 'index.ts': `a_1;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + await tsserver.sendConfigure({ + preferences: { includeCompletionsForModuleExports: true }, + }); - const res = await tsserver.sendCompletionInfo({ - file: iff.paths['index.ts'], - ...getRange('index.ts', 'styles').end, - }); + const res = await tsserver.sendCompletionInfo({ + file: iff.paths['index.ts'], + ...getRange('index.ts', 'a_1').end, + }); - expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'styles') ?? [])).toStrictEqual( - [], - ); + expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'a_1') ?? [])).toStrictEqual( + [], + ); + }); }); - test('suggests named token bindings', async () => { - const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ - cmkOptions: { namedExports: true, prioritizeNamedImports: true }, - }), - 'index.ts': `a_1;`, - 'a.module.css': `.a_1 { color: red; }`, - }); - await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); - await tsserver.sendConfigure({ - preferences: { includeCompletionsForModuleExports: true }, - }); + describe('prioritizeNamedImports: true', () => { + test('omits the default styles binding from suggestions', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: true }, + }), + 'index.ts': `styles;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + await tsserver.sendConfigure({ + preferences: { includeCompletionsForModuleExports: true }, + }); + + const res = await tsserver.sendCompletionInfo({ + file: iff.paths['index.ts'], + ...getRange('index.ts', 'styles').end, + }); - const res = await tsserver.sendCompletionInfo({ - file: iff.paths['index.ts'], - ...getRange('index.ts', 'a_1').end, + expect( + normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'styles') ?? []), + ).toStrictEqual([]); }); - expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'a_1') ?? [])).toStrictEqual( - normalizeCompletionEntry([{ name: 'a_1', sortText: '16', source: './a.module.css' }]), - ); + test('suggests named token bindings', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: true }, + }), + 'index.ts': `a_1;`, + 'a.module.css': `.a_1 { color: red; }`, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + await tsserver.sendConfigure({ + preferences: { includeCompletionsForModuleExports: true }, + }); + + const res = await tsserver.sendCompletionInfo({ + file: iff.paths['index.ts'], + ...getRange('index.ts', 'a_1').end, + }); + + expect(normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'a_1') ?? [])).toStrictEqual( + normalizeCompletionEntry([{ name: 'a_1', sortText: '16', source: './a.module.css' }]), + ); + }); }); }); From e78c94f64566612d964ad4956e8ef5b9dfdbcae6 Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 19:10:35 +0900 Subject: [PATCH 4/6] test(ts-plugin): drop redundant (default) suffix from describe names `prioritizeNamedImports: false` already conveys the configured value; the `(default)` annotation is redundant and risks going stale if the default changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/ts-plugin/e2e-test/feature/code-fix.test.ts | 6 ++++-- packages/ts-plugin/e2e-test/feature/completion.test.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts index 93b67fcc..b65527bd 100644 --- a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts +++ b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts @@ -166,10 +166,12 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); describe('named import code fix (namedExports: true)', () => { - describe('prioritizeNamedImports: false (default)', () => { + describe('prioritizeNamedImports: false', () => { test('omits the named import code fix', async () => { const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true } }), + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: false }, + }), 'index.ts': `a_1;`, 'a.module.css': `.a_1 { color: red; }`, }); diff --git a/packages/ts-plugin/e2e-test/feature/completion.test.ts b/packages/ts-plugin/e2e-test/feature/completion.test.ts index dc46e41b..3d12f333 100644 --- a/packages/ts-plugin/e2e-test/feature/completion.test.ts +++ b/packages/ts-plugin/e2e-test/feature/completion.test.ts @@ -133,10 +133,12 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: }); describe('named token completion (namedExports: true)', () => { - describe('prioritizeNamedImports: false (default)', () => { + describe('prioritizeNamedImports: false', () => { test('omits named tokens from suggestions', async () => { const { iff, getRange } = await setupFixture({ - 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports: true } }), + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports: true, prioritizeNamedImports: false }, + }), 'index.ts': `a_1;`, 'a.module.css': `.a_1 { color: red; }`, }); From 67d399c3a1f5289a9ea331ee5bb953042e69805b Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 20:03:00 +0900 Subject: [PATCH 5/6] test(ts-plugin): cover generated-file exclusion and multi-import fixMissingCSSRule Co-Authored-By: Claude Opus 4.7 (1M context) --- .../e2e-test/feature/code-fix.test.ts | 44 +++++++++++++++++-- .../e2e-test/feature/completion.test.ts | 30 +++++++++++++ packages/ts-plugin/src/test/builder.ts | 12 ++++- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts index b65527bd..098b87aa 100644 --- a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts +++ b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts @@ -92,6 +92,46 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: ]), ); }); + + test('inserts the rule into the CSS module bound to the accessed identifier', async () => { + const { iff, getLoc } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + ${buildStylesImport('./b.module.css', { namedExports, name: 'bStyles' })} + bStyles.b_1; + `, + 'a.module.css': '', + 'b.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const loc = getLoc('index.ts', 'b_1'); + const res = await tsserver.sendGetCodeFixes({ + errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]], + file: iff.paths['index.ts'], + startLine: loc.line, + startOffset: loc.offset, + endLine: loc.line, + endOffset: loc.offset, + }); + + expect(normalizeCodeFixActions(res.body!)).toStrictEqual( + normalizeCodeFixActions([ + { + fixName: 'fixMissingCSSRule', + changes: [ + { + fileName: formatPath(iff.paths['b.module.css']), + textChanges: [ + { start: { line: 1, offset: 1 }, end: { line: 1, offset: 1 }, newText: '\n.b_1 {\n \n}' }, + ], + }, + ], + }, + ]), + ); + }); }); describe('auto-import', () => { @@ -113,9 +153,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: endOffset: range.end.offset, }); - const importStatement = namedExports - ? `import * as styles from "./a.module.css";` - : `import styles from "./a.module.css";`; + const importStatement = buildStylesImport('./a.module.css', { namedExports, quote: 'double' }); expect(normalizeCodeFixActions(res.body!)).toStrictEqual( normalizeCodeFixActions([ { diff --git a/packages/ts-plugin/e2e-test/feature/completion.test.ts b/packages/ts-plugin/e2e-test/feature/completion.test.ts index 3d12f333..c0ed3e3a 100644 --- a/packages/ts-plugin/e2e-test/feature/completion.test.ts +++ b/packages/ts-plugin/e2e-test/feature/completion.test.ts @@ -41,6 +41,36 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: ); }); + test('excludes generated files from suggestions', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ + cmkOptions: { namedExports, dtsOutDir: 'generated' }, + }), + 'a.tsx': `styles;`, + 'a.module.css': '', + 'generated/b.module.css.d.ts': dedent` + const styles: {}; + export default styles; + `, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.tsx'] }] }); + await tsserver.sendConfigure({ + preferences: { + includeCompletionsForModuleExports: true, + quotePreference: 'single', + }, + }); + + const res = await tsserver.sendCompletionInfo({ + file: iff.paths['a.tsx'], + ...getRange('a.tsx', 'styles').end, + }); + + expect( + normalizeCompletionEntry(res.body?.entries.filter((entry) => entry.name === 'styles') ?? []), + ).toStrictEqual(normalizeCompletionEntry([{ name: 'styles', sortText: '0', source: './a.module.css' }])); + }); + test('inserts the import statement when accepted', async () => { const { iff, getRange } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), diff --git a/packages/ts-plugin/src/test/builder.ts b/packages/ts-plugin/src/test/builder.ts index 1368b6bb..556074b5 100644 --- a/packages/ts-plugin/src/test/builder.ts +++ b/packages/ts-plugin/src/test/builder.ts @@ -13,6 +13,14 @@ export function buildTSConfigJSON(args?: TSConfig): string { }); } -export function buildStylesImport(specifier: string, { namedExports }: { namedExports: boolean }): string { - return namedExports ? `import * as styles from '${specifier}';` : `import styles from '${specifier}';`; +interface BuildStylesImportOptions { + namedExports: boolean; + quote?: 'single' | 'double'; + name?: string; +} + +export function buildStylesImport(specifier: string, options: BuildStylesImportOptions): string { + const { namedExports, quote = 'single', name = 'styles' } = options; + const q = quote === 'single' ? "'" : '"'; + return namedExports ? `import * as ${name} from ${q}${specifier}${q};` : `import ${name} from ${q}${specifier}${q};`; } From f55f4bbecd0c584922f11c06891610fb996888f0 Mon Sep 17 00:00:00 2001 From: mizdra Date: Mon, 4 May 2026 20:11:34 +0900 Subject: [PATCH 6/6] test(ts-plugin): cover both PROPERTY_DOES_NOT_EXIST diagnostics in fixMissingCSSRule Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/ts-plugin/e2e-test/feature/code-fix.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts index 098b87aa..709dbbb2 100644 --- a/packages/ts-plugin/e2e-test/feature/code-fix.test.ts +++ b/packages/ts-plugin/e2e-test/feature/code-fix.test.ts @@ -13,7 +13,10 @@ const tsserver = launchTsserver(); describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { describe('fixMissingCSSRule', () => { - test('inserts a new CSS rule into an empty CSS module', async () => { + test.each([ + { errorCode: PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0] }, + { errorCode: PROPERTY_DOES_NOT_EXIST_ERROR_CODES[1] }, + ])('inserts a new CSS rule into an empty CSS module for diagnostic $errorCode', async ({ errorCode }) => { const { iff, getLoc } = await setupFixture({ 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), 'index.ts': dedent` @@ -26,7 +29,7 @@ describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: const loc = getLoc('index.ts', 'a_1'); const res = await tsserver.sendGetCodeFixes({ - errorCodes: [PROPERTY_DOES_NOT_EXIST_ERROR_CODES[0]], + errorCodes: [errorCode], file: iff.paths['index.ts'], startLine: loc.line, startOffset: loc.offset,