diff --git a/packages/ts-plugin/e2e-test/disabled.test.ts b/packages/ts-plugin/e2e-test/disabled.test.ts index 2aabc3c0..d4555a5d 100644 --- a/packages/ts-plugin/e2e-test/disabled.test.ts +++ b/packages/ts-plugin/e2e-test/disabled.test.ts @@ -1,29 +1,25 @@ import dedent from 'dedent'; import { expect, test } from 'vite-plus/test'; -import { createIFF } from './test-util/fixture.js'; +import { setupFixture } from './test-util/fixture.js'; import { launchTsserver, normalizeDefinitions } from './test-util/tsserver.js'; -test('does not provide language features when cmkOptions.enabled is false', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ +const tsserver = launchTsserver(); + +test('returns no Go to Definition results when cmkOptions.enabled is false', async () => { + const { iff, getLoc } = await setupFixture({ + 'tsconfig.json': `{ "cmkOptions": { "enabled": false } }`, 'index.ts': dedent` import styles from './a.module.css'; styles.a_1; `, - 'a.module.css': dedent` - .a_1 { color: red; } - `, - 'tsconfig.json': dedent` - { "cmkOptions": { "enabled": false } } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['index.ts'] }], + 'a.module.css': `.a_1 { color: red; }`, }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + const res = await tsserver.sendDefinitionAndBoundSpan({ file: iff.paths['index.ts'], - line: 2, - offset: 8, + ...getLoc('index.ts', 'a_1'), }); + expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual([]); }); diff --git a/packages/ts-plugin/e2e-test/feature/refactor.test.ts b/packages/ts-plugin/e2e-test/feature/refactor.test.ts index 53ab7fc5..79a9f514 100644 --- a/packages/ts-plugin/e2e-test/feature/refactor.test.ts +++ b/packages/ts-plugin/e2e-test/feature/refactor.test.ts @@ -1,71 +1,83 @@ -import dedent from 'dedent'; import { describe, expect, test } from 'vite-plus/test'; import { createCssModuleFileRefactor } from '../../src/language-service/feature/refactor.js'; -import { createIFF } from '../test-util/fixture.js'; +import { buildTSConfigJSON } from '../../src/test/builder.js'; +import { setupFixture } from '../test-util/fixture.js'; import { launchTsserver } from '../test-util/tsserver.js'; -describe('Refactor', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'a.tsx': '', - 'b.ts': '', - 'tsconfig.json': dedent` - { - "compilerOptions": { "jsx": "react-jsx" }, - "cmkOptions": { - "enabled": true, - "dtsOutDir": "generated" - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['a.tsx'] }], - }); - test.each([ - { - name: 'a.tsx', +const tsserver = launchTsserver(); + +describe('Get Applicable Refactors', () => { + test('offers Create CSS Module file for a component file when no paired CSS module exists', async () => { + const { iff } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ compilerOptions: { jsx: 'react-jsx' } }), + 'a.tsx': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.tsx'] }] }); + + const res = await tsserver.sendGetApplicableRefactors({ file: iff.paths['a.tsx'], line: 1, offset: 1, - expected: [createCssModuleFileRefactor], - }, - { - name: 'b.ts', - file: iff.paths['b.ts'], + }); + + expect(res.body).toStrictEqual([createCssModuleFileRefactor]); + }); + + test('omits Create CSS Module file for a non-component file', async () => { + const { iff } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON(), + 'a.ts': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.ts'] }] }); + + const res = await tsserver.sendGetApplicableRefactors({ + file: iff.paths['a.ts'], line: 1, offset: 1, - expected: [], - }, - ])('Get Applicable Refactors for $name', async ({ file, line, offset, expected }) => { - const res = await tsserver.sendGetApplicableRefactors({ - file, - line, - offset, }); - expect(res.body).toStrictEqual(expected); + + expect(res.body).toStrictEqual([]); }); - test.each([ - { - name: 'a.tsx', + + test('omits Create CSS Module file when the paired CSS module already exists', async () => { + const { iff } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ compilerOptions: { jsx: 'react-jsx' } }), + 'a.tsx': '', + 'a.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.tsx'] }] }); + + const res = await tsserver.sendGetApplicableRefactors({ file: iff.paths['a.tsx'], line: 1, offset: 1, - expected: [ - { - fileName: iff.join('a.module.css'), - textChanges: [{ start: { line: 0, offset: 0 }, end: { line: 0, offset: 0 }, newText: '' }], - }, - ], - }, - ])('Get Edits For Refactor for $name', async ({ file, line, offset, expected }) => { + }); + + expect(res.body).toStrictEqual([]); + }); +}); + +describe('Get Edits For Refactor', () => { + test('emits an edit that creates a new empty CSS module file paired with the component file', async () => { + const { iff } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ compilerOptions: { jsx: 'react-jsx' } }), + 'a.tsx': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.tsx'] }] }); + const res = await tsserver.sendGetEditsForRefactor({ refactor: createCssModuleFileRefactor.name, action: createCssModuleFileRefactor.actions[0].name, - file, - line, - offset, + file: iff.paths['a.tsx'], + line: 1, + offset: 1, }); - expect(res.body?.edits).toStrictEqual(expected); + + expect(res.body?.edits).toStrictEqual([ + { + fileName: iff.join('a.module.css'), + textChanges: [{ start: { line: 0, offset: 0 }, end: { line: 0, offset: 0 }, newText: '' }], + }, + ]); }); }); diff --git a/packages/ts-plugin/e2e-test/feature/rename-file.test.ts b/packages/ts-plugin/e2e-test/feature/rename-file.test.ts index a8dc9576..468c1133 100644 --- a/packages/ts-plugin/e2e-test/feature/rename-file.test.ts +++ b/packages/ts-plugin/e2e-test/feature/rename-file.test.ts @@ -1,75 +1,73 @@ -import dedent from 'dedent'; 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 { formatPath, launchTsserver } from '../test-util/tsserver.js'; -describe('Rename File', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - import styles from './a.module.css'; - `, - 'a.module.css': dedent` - @import './b.module.css'; - @value c_1 from './c.module.css'; - `, - 'b.module.css': dedent` - .b_1 { color: red; } - `, - 'c.module.css': dedent` - @value c_1: red; - `, - 'tsconfig.json': dedent` - { - "compilerOptions": { - "paths": { "@/*": ["./*"] } - }, - "cmkOptions": { - "enabled": true, - "dtsOutDir": "generated" - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['index.ts'] }], - }); - test.each([ - { - name: 'a.module.css', - oldFilePath: iff.paths['a.module.css'], - newFilePath: iff.join('aa.module.css'), - expected: [ +const tsserver = launchTsserver(); + +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + describe('rewrites the import specifier when a CSS module is renamed', () => { + test('from `import ... from` in TS', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': buildStylesImport('./a.module.css', { namedExports }), + 'a.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + + const res = await tsserver.sendGetEditsForFileRename({ + oldFilePath: iff.paths['a.module.css'], + newFilePath: iff.join('aa.module.css'), + }); + + expect(res.body).toStrictEqual([ { fileName: formatPath(iff.paths['index.ts']), - textChanges: [{ start: { line: 1, offset: 21 }, end: { line: 1, offset: 35 }, newText: './aa.module.css' }], + textChanges: [{ ...getRange('index.ts', './a.module.css'), newText: './aa.module.css' }], }, - ], - }, - { - name: 'b.module.css', - oldFilePath: iff.paths['b.module.css'], - newFilePath: iff.join('bb.module.css'), - expected: [ + ]); + }); + + test('from `@import` in CSS', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@import './b.module.css';`, + 'b.module.css': '', + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + + const res = await tsserver.sendGetEditsForFileRename({ + oldFilePath: iff.paths['b.module.css'], + newFilePath: iff.join('bb.module.css'), + }); + + expect(res.body).toStrictEqual([ { fileName: formatPath(iff.paths['a.module.css']), - textChanges: [{ start: { line: 1, offset: 10 }, end: { line: 1, offset: 24 }, newText: './bb.module.css' }], + textChanges: [{ ...getRange('a.module.css', './b.module.css'), newText: './bb.module.css' }], }, - ], - }, - { - name: 'c.module.css', - oldFilePath: iff.paths['c.module.css'], - newFilePath: iff.join('cc.module.css'), - expected: [ + ]); + }); + + test('from `@value ... from` in CSS', async () => { + const { iff, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': `@value b_1 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.sendGetEditsForFileRename({ + oldFilePath: iff.paths['b.module.css'], + newFilePath: iff.join('bb.module.css'), + }); + + expect(res.body).toStrictEqual([ { fileName: formatPath(iff.paths['a.module.css']), - textChanges: [{ start: { line: 2, offset: 18 }, end: { line: 2, offset: 32 }, newText: './cc.module.css' }], + textChanges: [{ ...getRange('a.module.css', './b.module.css'), newText: './bb.module.css' }], }, - ], - }, - ])('for $name', async ({ oldFilePath, newFilePath, expected }) => { - const res = await tsserver.sendGetEditsForFileRename({ oldFilePath, newFilePath }); - expect(res.body).toStrictEqual(expected); + ]); + }); }); }); diff --git a/packages/ts-plugin/e2e-test/ignore-generated-files.test.ts b/packages/ts-plugin/e2e-test/ignore-generated-files.test.ts index 97f10138..93fb56fd 100644 --- a/packages/ts-plugin/e2e-test/ignore-generated-files.test.ts +++ b/packages/ts-plugin/e2e-test/ignore-generated-files.test.ts @@ -1,52 +1,36 @@ import dedent from 'dedent'; import { expect, test } from 'vite-plus/test'; -import { createIFF } from './test-util/fixture.js'; +import { setupFixture } from './test-util/fixture.js'; import { launchTsserver } from './test-util/tsserver.js'; -test('report the import of files where .d.ts exists but .module.css does not exist', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - // `a.module.css` is not exist, and the .d.ts file is exist. - // But `'./a.module.css'` should report an error. - 'index.ts': dedent` - import styles from './a.module.css'; - `, - 'generated/a.module.css.d.ts': dedent` - const styles: {}; - export default styles; - `, +const tsserver = launchTsserver(); + +test('excludes generated .d.ts files from module resolution even when listed in rootDirs', async () => { + const { iff, getRange } = await setupFixture({ 'tsconfig.json': dedent` { "compilerOptions": { - "rootDirs": [".", "generated"], + "rootDirs": [".", "generated"] }, - "cmkOptions": { - "enabled": true - } + "cmkOptions": { "enabled": true } } `, + 'index.ts': `import styles from './a.module.css';`, + 'generated/a.module.css.d.ts': dedent` + const styles: {}; + export default styles; + `, }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['index.ts'] }], - }); - const res = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['index.ts'], - }); - expect(res.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.", - }, - ] - `); + 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: 2307, + ...getRange('index.ts', `'./a.module.css'`), + text: `Cannot find module './a.module.css' or its corresponding type declarations.`, + }, + ]); }); diff --git a/packages/ts-plugin/e2e-test/invalid-syntax.test.ts b/packages/ts-plugin/e2e-test/invalid-syntax.test.ts index 950bd24c..6ac55e34 100644 --- a/packages/ts-plugin/e2e-test/invalid-syntax.test.ts +++ b/packages/ts-plugin/e2e-test/invalid-syntax.test.ts @@ -1,53 +1,58 @@ import dedent from 'dedent'; 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 { formatPath, launchTsserver, normalizeDefinitions } from './test-util/tsserver.js'; -describe('handle invalid syntax CSS without crashing', async () => { - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'index.ts': dedent` - import styles from './a.module.css'; - styles.a_1; - `, - 'a.module.css': dedent` - .a_1 { color: red; } - .a_2 { - `, - 'tsconfig.json': dedent` - { - "compilerOptions": {}, - "cmkOptions": { - "enabled": true, - "dtsOutDir": "generated" - } - } - `, - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['index.ts'] }], - }); - test('can get definition and bound span', async () => { +const tsserver = launchTsserver(); + +describe.each([{ namedExports: false }, { namedExports: true }])('namedExports: $namedExports', ({ namedExports }) => { + test('resolves Go to Definition on a valid token even when later rules contain invalid syntax', async () => { + const { iff, getLoc, getRange } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'index.ts': dedent` + ${buildStylesImport('./a.module.css', { namedExports })} + styles.a_1; + `, + 'a.module.css': dedent` + .a_1 { color: red; } + .a_2 { + `, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['index.ts'] }] }); + const res = await tsserver.sendDefinitionAndBoundSpan({ file: iff.paths['index.ts'], - line: 2, - offset: 8, + ...getLoc('index.ts', 'a_1'), }); - const expected = [ - { - file: formatPath(iff.paths['a.module.css']), - start: { line: 1, offset: 2 }, - end: { line: 1, offset: 5 }, - contextStart: { line: 1, offset: 1 }, - contextEnd: { line: 1, offset: 21 }, - }, - ]; - expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual(normalizeDefinitions(expected)); + + const { start: contextStart, end: contextEnd } = getRange('a.module.css', '.a_1 { color: red; }'); + expect(normalizeDefinitions(res.body?.definitions ?? [])).toStrictEqual( + normalizeDefinitions([ + { + file: formatPath(iff.paths['a.module.css']), + ...getRange('a.module.css', 'a_1'), + contextStart, + contextEnd, + }, + ]), + ); }); - test('does not report syntactic diagnostics', async () => { + + test('reports no syntactic diagnostics for a CSS module with parse errors', async () => { + const { iff } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON({ cmkOptions: { namedExports } }), + 'a.module.css': dedent` + .a_1 { color: red; } + .a_2 { + `, + }); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['a.module.css'] }] }); + const res = await tsserver.sendSyntacticDiagnosticsSync({ file: iff.paths['a.module.css'], }); - expect(res.body).toMatchInlineSnapshot(`[]`); + + expect(res.body).toStrictEqual([]); }); }); diff --git a/packages/ts-plugin/e2e-test/regression/pure-css-file.test.ts b/packages/ts-plugin/e2e-test/regression/pure-css-file.test.ts index 53a22188..63b88ee6 100644 --- a/packages/ts-plugin/e2e-test/regression/pure-css-file.test.ts +++ b/packages/ts-plugin/e2e-test/regression/pure-css-file.test.ts @@ -1,26 +1,21 @@ -import dedent from 'dedent'; import { expect, test } from 'vite-plus/test'; -import { createIFF } from '../test-util/fixture.js'; +import { buildTSConfigJSON } from '../../src/test/builder.js'; +import { setupFixture } from '../test-util/fixture.js'; import { launchTsserver } from '../test-util/tsserver.js'; -test('ts-plugin does not process pure css files', async () => { - // ref: https://github.com/mizdra/css-modules-kit/issues/170 - const tsserver = launchTsserver(); - const iff = await createIFF({ - 'global.css': dedent` - * { margin: 0; } - `, - 'tsconfig.json': '{ "cmkOptions": { "enabled": true } }', - }); - await tsserver.sendUpdateOpen({ - openFiles: [{ file: iff.paths['tsconfig.json'] }], - }); - const res1 = await tsserver.sendSyntacticDiagnosticsSync({ - file: iff.paths['global.css'], - }); - expect(res1.body?.length).toBe(0); - const res2 = await tsserver.sendSemanticDiagnosticsSync({ - file: iff.paths['global.css'], +const tsserver = launchTsserver(); + +// ref: https://github.com/mizdra/css-modules-kit/issues/170 +test('reports no diagnostics for a non-module .css file', async () => { + const { iff } = await setupFixture({ + 'tsconfig.json': buildTSConfigJSON(), + 'global.css': `* { margin: 0; }`, }); - expect(res2.body?.length).toBe(0); + await tsserver.sendUpdateOpen({ openFiles: [{ file: iff.paths['tsconfig.json'] }] }); + + const syntactic = await tsserver.sendSyntacticDiagnosticsSync({ file: iff.paths['global.css'] }); + expect(syntactic.body).toStrictEqual([]); + + const semantic = await tsserver.sendSemanticDiagnosticsSync({ file: iff.paths['global.css'] }); + expect(semantic.body).toStrictEqual([]); });