Files
Andrew Scott 470cf430ce test(language-service): optimize language service test environment and flatten test setup
Optimization Goals:
The primary goals of this optimization are to dramatically reduce the execution time of the language-service test suite and stabilize the mock file system infrastructure. Previously, the suite suffered from significant overhead due to recreating the `MockFileSystem`, `MockServerHost`, and TypeScript `ProjectService` for every single test block, leading to redundant parsing operations, slow test initialization, and reduced spec performance.

How the Goals Were Achieved:
1. **Mock File System Optimizations (Shared State)**:
   - Evaluated that standard test files (e.g., TS/Angular lib definitions) are immutable across tests.
   - Introduced and utilized `lockMockFileSystem()` to initialize the mock `FileSystem` with `loadStandardTestFiles()` only once per test suite run rather than repeatedly per test.
   - Refactored `LanguageServiceTestEnv.setup()` to reuse the singleton file system, completely skipping redundant module loading by eagerly flagging `fsInitialized = true`.

2. **Language Service Test Environment Enhancements (TypeScript Project Reuse)**:
   - Implemented partial configuration reloads in the `Project` class via the `update()` method, removing the need to tear down and rebuild the entire `MockServerHost` and TypeScript `ProjectService` from scratch when minimal file changes (like HTML templates or local TS edits) are made dynamically by a test.
   - Applied `projectService.reloadProjects()` and `scriptInfo.reloadFromFile()` to synchronously push mock file tree invalidations to the active TS program, skipping expensive environment initialization and saving considerable latency across tests.
   - Added `projectName` identifiers inside complex isolate tests (e.g. module alias aliasing) so custom environment injections can sandbox safely without invalidating the global default environment cache.

3. **Test Suite Unification**:
   - Flattened fragmented test groups (`grp1`, `grp3`, `grp4`) into a cohesive single directory at `packages/language-service/test/`. This simplifies execution config, improves test runner concurrency, and unifies local development targeting.
   - Cleaned out broken inline debug logging and unneeded config reloading loops.

4. **Maintaining Test Isolation**:
   - **Explicit TypeScript Configuration**: While the underlying `MockFileSystem` ("disk") is aggressively reused across tests, the TypeScript `ProjectService` and its execution environment are entirely recreated for every test run to ensure isolated ASTs and module resolution caches.
   - **Strict tsconfig.json Files Array**: When a project is initialized, it explicitly defines its boundary using the strict `files: [ ... ]` array in `tsconfig.json`. This ensures that any leftover files physically on the mock disk from an older test run are completely invisible to the TS Compiler.
   - **Namespace Sandboxing**: For tests doing custom modifications (e.g., overriding module resolution paths), they utilize localized `projectName` arguments (like `"test_alias_completions"`) to configure sandboxed working directories.
2026-03-16 14:49:34 -06:00

198 lines
5.8 KiB
TypeScript

/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {createModuleAndProjectWithDeclarations, LanguageServiceTestEnv} from '../testing';
describe('get typecheck block', () => {
it('should find the typecheck block for an inline template', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
template: '<div>{{ myProp }}</div>',
standalone: false,
})
export class AppCmp {
myProp!: string;
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
appFile.moveCursorToText('{{ my¦Prop }}');
const result = appFile.getTcb();
if (result === undefined) {
fail('Expected a valid TCB response');
return;
}
const {content, selections} = result;
expect(selections.length).toBe(1);
const {start, length} = selections[0];
expect(content.substring(start, start + length)).toContain('myProp');
});
it('should find the typecheck block for an external template', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
templateUrl: './app.html',
standalone: false,
})
export class AppCmp {
myProp!: string;
}`,
'app.html': '<div>{{ myProp }}</div>',
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const htmlFile = project.openFile('app.html');
htmlFile.moveCursorToText('{{ my¦Prop }}');
const result = htmlFile.getTcb();
if (result === undefined) {
fail('Expected a valid TCB response');
return;
}
const {content, selections} = result;
expect(selections.length).toBe(1);
const {start, length} = selections[0];
expect(content.substring(start, start + length)).toContain('myProp');
});
it('should find type check block for a host binding of a component', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
template: '',
standalone: false,
host: {'[id]': 'getId()'}
})
export class AppCmp {
getId() {
return 'test';
}
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
appFile.moveCursorToText(`'get¦Id()'`);
const result = appFile.getTcb();
if (result === undefined) {
fail('Expected a valid TCB response');
return;
}
const {content, selections} = result;
expect(selections.length).toBe(1);
const {start, length} = selections[0];
expect(content.substring(start, start + length)).toContain('getId');
});
it('should find type check block for a host listener of a component', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
template: '',
standalone: false,
host: {
'(click)': 'handleClick()'
}
})
export class AppCmp {
handleClick() {}
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
appFile.moveCursorToText(`'handl¦eClick()'`);
const result = appFile.getTcb();
if (result === undefined) {
fail('Expected a valid TCB response');
return;
}
const {content, selections} = result;
expect(selections.length).toBe(1);
const {start, length} = selections[0];
expect(content.substring(start, start + length)).toContain('handleClick');
});
it('should find type check block for a host binding of a directive', () => {
const files = {
'app.ts': `
import {Directive} from '@angular/core';
@Directive({
standalone: false,
selector: '[my-dir]',
host: {'[id]': 'getId()'}
})
export class MyDir {
getId() {
return 'test';
}
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
appFile.moveCursorToText(`'get¦Id()'`);
const result = appFile.getTcb();
if (result === undefined) {
fail('Expected a valid TCB response');
return;
}
const {content, selections} = result;
expect(selections.length).toBe(1);
const {start, length} = selections[0];
expect(content.substring(start, start + length)).toContain('getId');
});
it('should not find typecheck blocks outside a template', () => {
const files = {
'app.ts': `
import {Component} from '@angular/core';
@Component({
template: '<div>{{ myProp }}</div>',
standalone: false,
})
export class AppCmp {
myProp!: string;
}`,
};
const env = LanguageServiceTestEnv.setup();
const project = createModuleAndProjectWithDeclarations(env, 'test', files);
project.expectNoSourceDiagnostics();
const appFile = project.openFile('app.ts');
appFile.moveCursorToText('my¦Prop!: string;');
const result = appFile.getTcb();
expect(result).toBeUndefined();
});
});