|
| 1 | +import nodeModule from 'node:module'; |
| 2 | +import { pathToFileURL } from 'node:url'; |
| 3 | + |
1 | 4 | import type { Pattern } from 'cspell-lib'; |
2 | 5 | import * as cspell from 'cspell-lib'; |
3 | 6 | import { getDefaultSettings } from 'cspell-lib'; |
@@ -27,6 +30,14 @@ const oc = (...params: Parameters<typeof expect.objectContaining>) => expect.obj |
27 | 30 |
|
28 | 31 | vi.mock('vscode-languageserver/node'); |
29 | 32 | vi.mock('./vscode.config.mjs'); |
| 33 | +vi.mock('node:module', () => ({ |
| 34 | + default: { |
| 35 | + findPackageJSON: vi.fn(), |
| 36 | + }, |
| 37 | +})); |
| 38 | + |
| 39 | +// eslint-disable-next-line n/no-unsupported-features/node-builtins |
| 40 | +const mockFindPackageJSON = vi.mocked(nodeModule.findPackageJSON); |
30 | 41 |
|
31 | 42 | const mockGetWorkspaceFolders = vi.mocked(getWorkspaceFolders); |
32 | 43 | const mockGetConfiguration = vi.mocked(getConfiguration); |
@@ -84,6 +95,8 @@ describe('Validate DocumentSettings', () => { |
84 | 95 | beforeEach(() => { |
85 | 96 | // Clear all mock instances and calls to constructor and all methods: |
86 | 97 | mockGetWorkspaceFolders.mockClear(); |
| 98 | + // Default to not finding any bundled dict packages (simulates "not installed") |
| 99 | + mockFindPackageJSON.mockReset(); |
87 | 100 | }); |
88 | 101 |
|
89 | 102 | test('version', async () => { |
@@ -508,6 +521,144 @@ describe('Validate RegExp corrections', () => { |
508 | 521 | }); |
509 | 522 | }); |
510 | 523 |
|
| 524 | +describe('findPackageJSON', () => { |
| 525 | + const { findPackageJSON } = __testing__; |
| 526 | + |
| 527 | + beforeEach(() => { |
| 528 | + mockFindPackageJSON.mockReset(); |
| 529 | + }); |
| 530 | + |
| 531 | + test('returns a URL when the package is found', () => { |
| 532 | + const pkgJsonPath = '/home/project/node_modules/some-pkg/package.json'; |
| 533 | + mockFindPackageJSON.mockReturnValue(pkgJsonPath); |
| 534 | + const url = new URL('file:///home/project/src/test.ts'); |
| 535 | + const result = findPackageJSON('some-pkg', url); |
| 536 | + expect(result).toBeInstanceOf(URL); |
| 537 | + expect(result?.href).toBe(pathToFileURL(pkgJsonPath).href); |
| 538 | + }); |
| 539 | + |
| 540 | + test('returns undefined when the package is not found (throws)', () => { |
| 541 | + mockFindPackageJSON.mockImplementation(() => { |
| 542 | + const err = Object.assign(new Error('Package not found'), { code: 'ERR_MODULE_NOT_FOUND' }); |
| 543 | + throw err; |
| 544 | + }); |
| 545 | + const url = new URL('file:///home/project/src/test.ts'); |
| 546 | + const result = findPackageJSON('nonexistent-pkg', url); |
| 547 | + expect(result).toBeUndefined(); |
| 548 | + }); |
| 549 | + |
| 550 | + test('returns undefined when nodeModule.findPackageJSON returns a falsy value', () => { |
| 551 | + mockFindPackageJSON.mockReturnValue(undefined); |
| 552 | + const url = new URL('file:///home/project/src/test.ts'); |
| 553 | + const result = findPackageJSON('some-pkg', url); |
| 554 | + expect(result).toBeUndefined(); |
| 555 | + }); |
| 556 | +}); |
| 557 | + |
| 558 | +describe('useLocallyInstalledCSpellDictionaries resolution', () => { |
| 559 | + // Pathname strings used to simulate the different installation scenarios |
| 560 | + const bundledDictsPackageJsonPath = pathToFileURL( |
| 561 | + Path.resolve(pathWorkspaceRoot, 'node_modules/@cspell/cspell-bundled-dicts/package.json'), |
| 562 | + ).pathname; |
| 563 | + const cspellPackageJsonPath = pathToFileURL(Path.resolve(pathWorkspaceRoot, 'node_modules/cspell/package.json')).pathname; |
| 564 | + |
| 565 | + beforeEach(() => { |
| 566 | + mockGetWorkspaceFolders.mockReset(); |
| 567 | + mockFindPackageJSON.mockReset(); |
| 568 | + }); |
| 569 | + |
| 570 | + function implLocalGetConfiguration(section?: string | ConfigurationItem | ConfigurationItem[]): Promise<any> { |
| 571 | + let sec: string | undefined; |
| 572 | + if (typeof section === 'string') { |
| 573 | + sec = section; |
| 574 | + } else if (Array.isArray(section)) { |
| 575 | + sec = section[0]?.section; |
| 576 | + } else { |
| 577 | + sec = section?.section; |
| 578 | + } |
| 579 | + if (sec === 'cSpell.trustedWorkspace') { |
| 580 | + return Promise.resolve(true); |
| 581 | + } |
| 582 | + return Promise.resolve(undefined); |
| 583 | + } |
| 584 | + |
| 585 | + function newDocumentSettingsLocal(defaultSettings: CSpellUserAndExtensionSettings = {}) { |
| 586 | + const connection = createMockConnection(); |
| 587 | + const mockWorkspaceGetConfiguration = vi.mocked(connection.workspace.getConfiguration); |
| 588 | + mockWorkspaceGetConfiguration.mockImplementation(implLocalGetConfiguration); |
| 589 | + return new DocumentSettings(connection, createMockServerSideApi(), defaultSettings); |
| 590 | + } |
| 591 | + |
| 592 | + async function getSettingsWithLocalDicts(useLocallyInstalledCSpellDictionaries: boolean | undefined) { |
| 593 | + const mockFolders: WorkspaceFolder[] = [workspaceFolderRoot, workspaceFolderServer]; |
| 594 | + mockGetWorkspaceFolders.mockReturnValue(Promise.resolve(mockFolders)); |
| 595 | + mockGetConfiguration.mockReturnValue(Promise.resolve([{ mergeCSpellSettings: true, useLocallyInstalledCSpellDictionaries }, {}])); |
| 596 | + const docSettings = newDocumentSettingsLocal(); |
| 597 | + return docSettings.getSettings({ uri: Uri.file(Path.join(pathWorkspaceServer, 'src/test.ts')).toString() }); |
| 598 | + } |
| 599 | + |
| 600 | + test('(a) loads bundled dicts when @cspell/cspell-bundled-dicts is present in workspace', async () => { |
| 601 | + // Simulate: @cspell/cspell-bundled-dicts found directly in node_modules |
| 602 | + mockFindPackageJSON.mockReturnValue(bundledDictsPackageJsonPath); |
| 603 | + |
| 604 | + const settings = await getSettingsWithLocalDicts(true); |
| 605 | + |
| 606 | + // findPackageJSON should have been called for @cspell/cspell-bundled-dicts |
| 607 | + expect(mockFindPackageJSON).toHaveBeenCalledWith('@cspell/cspell-bundled-dicts', expect.any(URL)); |
| 608 | + // Bundled dictionaries should be loaded (there are many of them, e.g. en_us, bash, etc.) |
| 609 | + const dictNames = settings.dictionaryDefinitions?.map((d) => d.name) ?? []; |
| 610 | + expect(dictNames).toContain('en_us'); |
| 611 | + }); |
| 612 | + |
| 613 | + test('(b) loads bundled dicts when only available via cspell dependencies', async () => { |
| 614 | + // Simulate: @cspell/cspell-bundled-dicts not directly in workspace, but cspell is |
| 615 | + mockFindPackageJSON |
| 616 | + .mockImplementationOnce(() => { |
| 617 | + throw Object.assign(new Error('not found'), { code: 'ERR_MODULE_NOT_FOUND' }); |
| 618 | + }) |
| 619 | + .mockReturnValue(cspellPackageJsonPath); |
| 620 | + |
| 621 | + const settings = await getSettingsWithLocalDicts(true); |
| 622 | + |
| 623 | + // Both packages should have been searched |
| 624 | + expect(mockFindPackageJSON).toHaveBeenCalledWith('@cspell/cspell-bundled-dicts', expect.any(URL)); |
| 625 | + expect(mockFindPackageJSON).toHaveBeenCalledWith('cspell', expect.any(URL)); |
| 626 | + // Bundled dictionaries should still be loaded via cspell's location |
| 627 | + const dictNames = settings.dictionaryDefinitions?.map((d) => d.name) ?? []; |
| 628 | + expect(dictNames).toContain('en_us'); |
| 629 | + }); |
| 630 | + |
| 631 | + test('(c) does not add import when neither package is installed', async () => { |
| 632 | + // Simulate: neither package is found |
| 633 | + mockFindPackageJSON.mockImplementation(() => { |
| 634 | + throw Object.assign(new Error('not found'), { code: 'ERR_MODULE_NOT_FOUND' }); |
| 635 | + }); |
| 636 | + |
| 637 | + const settings = await getSettingsWithLocalDicts(true); |
| 638 | + |
| 639 | + expect(mockFindPackageJSON).toHaveBeenCalledWith('@cspell/cspell-bundled-dicts', expect.any(URL)); |
| 640 | + expect(mockFindPackageJSON).toHaveBeenCalledWith('cspell', expect.any(URL)); |
| 641 | + // Without bundled dicts, en_us should not be in dictionary definitions |
| 642 | + const dictNames = settings.dictionaryDefinitions?.map((d) => d.name) ?? []; |
| 643 | + expect(dictNames).not.toContain('en_us'); |
| 644 | + }); |
| 645 | + |
| 646 | + test('does not search for packages when useLocallyInstalledCSpellDictionaries is false', async () => { |
| 647 | + const settings = await getSettingsWithLocalDicts(false); |
| 648 | + |
| 649 | + expect(mockFindPackageJSON).not.toHaveBeenCalled(); |
| 650 | + // Settings should still be valid |
| 651 | + expect(settings).toBeDefined(); |
| 652 | + }); |
| 653 | + |
| 654 | + test('does not search for packages when useLocallyInstalledCSpellDictionaries is undefined', async () => { |
| 655 | + const settings = await getSettingsWithLocalDicts(undefined); |
| 656 | + |
| 657 | + expect(mockFindPackageJSON).not.toHaveBeenCalled(); |
| 658 | + expect(settings).toBeDefined(); |
| 659 | + }); |
| 660 | +}); |
| 661 | + |
511 | 662 | function filePathToUri(file: string | Uri): Uri { |
512 | 663 | return typeof file == 'string' ? Uri.file(file) : file; |
513 | 664 | } |
|
0 commit comments