/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/language-service/test/linked_editing_range_spec.ts
500 строк
16 KB
Andrew Scott
test(language-service): optimize language service test environment and flatten test setup
16 мар 2026, 23:49
16 мар 2026, 23:49
470cf43
Код
Авторство
О чём код?
/** * @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 {LanguageServiceTestEnv} from '../testing'; import {createModuleAndProjectWithDeclarations} from '../testing/src/util'; describe('linked editing ranges', () => { let env: LanguageServiceTestEnv; beforeEach(() => { env = LanguageServiceTestEnv.setup(); }); describe('external templates', () => { it('should return linked ranges for element tag in external template', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', templateUrl: './app.html', }) export class AppComponent {} `, 'app.html': '<div>content</div>', }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); // Position on the opening "div" tag const linkedRanges = project.getLinkedEditingRangeAtPosition('app.html', 2); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); // Opening tag span: <div> - "div" starts at position 1 expect(linkedRanges!.ranges[0].start).toBe(1); expect(linkedRanges!.ranges[0].length).toBe(3); // Closing tag span: </div> - "div" starts at position 14 (<div>content</div> = pos 14 for closing div) expect(linkedRanges!.ranges[1].start).toBe(14); expect(linkedRanges!.ranges[1].length).toBe(3); }); it('should return linked ranges when on closing tag', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', templateUrl: './app.html', }) export class AppComponent {} `, 'app.html': '<span>text</span>', }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); // Position on the closing "span" tag (</span> starts at position 10, "span" at 13) const linkedRanges = project.getLinkedEditingRangeAtPosition('app.html', 13); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); it('should return null for void elements', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', templateUrl: './app.html', }) export class AppComponent {} `, 'app.html': '<br>', }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); // Position on "br" const linkedRanges = project.getLinkedEditingRangeAtPosition('app.html', 2); // Void elements have no closing tag, so no linked ranges expect(linkedRanges).toBeNull(); }); it('should return null when not on a tag name', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', templateUrl: './app.html', }) export class AppComponent {} `, 'app.html': '<div class="test">content</div>', }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); // Position on "class" attribute const linkedRanges = project.getLinkedEditingRangeAtPosition('app.html', 8); expect(linkedRanges).toBeNull(); }); }); describe('inline templates', () => { it('should return linked ranges for element tag in inline template', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<div>content</div>', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Find position of opening "div" in the inline template const templateStart = appFile.contents.indexOf("template: '") + "template: '".length; const divPosition = templateStart + 1; // Skip '<' const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', divPosition); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); expect(linkedRanges!.wordPattern).toBe('[-\\w]+'); }); it('should return linked ranges when on closing tag in inline template', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<span>text</span>', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Find position of closing "span" in the inline template // '</span>' - we want to be on the 's' of span const closingSpanIndex = appFile.contents.indexOf('</span>') + 2; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', closingSpanIndex); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); it('should return linked ranges for nested elements in inline template', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<div><span>nested</span></div>', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Find position of "span" opening tag const templateStart = appFile.contents.indexOf("template: '") + "template: '".length; const spanPosition = templateStart + '<div><'.length; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', spanPosition); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); it('should return linked ranges for template literals', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: \` <section> <article>content</article> </section> \`, }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Find position of "section" opening tag const sectionStart = appFile.contents.indexOf('<section>') + 1; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', sectionStart); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); it('should return null for void elements in inline template', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<img src="test.png">', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Find position of "img" const imgStart = appFile.contents.indexOf('<img') + 1; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', imgStart); // Void elements have no closing tag, so no linked ranges expect(linkedRanges).toBeNull(); }); it('should handle inline templates with @if control flow', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: \` @if (show) { <button>Click me</button> } \`, }) export class AppComponent { show = true; } `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Find position of "button" opening tag const buttonStart = appFile.contents.indexOf('<button>') + 1; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', buttonStart); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); it('should return null when position is on attribute name', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<div class="test">content</div>', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Find position of "class" attribute const classStart = appFile.contents.indexOf('class='); const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', classStart); expect(linkedRanges).toBeNull(); }); it('should return null when position is on content text', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<div>some content here</div>', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Find position in the middle of the text content const contentStart = appFile.contents.indexOf('some content'); const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', contentStart); expect(linkedRanges).toBeNull(); }); it('should handle hyphenated tag names', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<my-custom-element>content</my-custom-element>', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Find position on the opening tag name const tagStart = appFile.contents.indexOf('<my-custom-element>') + 1; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', tagStart); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); // Verify the tag name length includes hyphens expect(linkedRanges!.ranges[0].length).toBe('my-custom-element'.length); expect(linkedRanges!.ranges[1].length).toBe('my-custom-element'.length); }); it('should handle @for control flow blocks', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: \` @for (item of items; track item) { <li>{{ item }}</li> } \`, }) export class AppComponent { items = ['a', 'b', 'c']; } `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); const liStart = appFile.contents.indexOf('<li>') + 1; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', liStart); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); it('should handle elements inside ng-template', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: \` <ng-template> <span>template content</span> </ng-template> \`, }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); const spanStart = appFile.contents.indexOf('<span>') + 1; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', spanStart); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); it('should handle position at exact start of tag name', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<p>paragraph</p>', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Position right after '<' const pStart = appFile.contents.indexOf('<p>') + 1; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', pStart); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); it('should handle position at exact end of tag name', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<div>text</div>', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); // Position right at the end of 'div' (just before '>') const divEnd = appFile.contents.indexOf('<div>') + 4; // 1 for '<' + 3 for 'div' const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', divEnd); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); it('should return null for self-closing elements', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: '<input type="text" />', }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); const inputStart = appFile.contents.indexOf('<input') + 1; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', inputStart); // Self-closing elements don't have linked ranges expect(linkedRanges).toBeNull(); }); it('should work with SVG elements', () => { const files = { 'app.ts': ` import {Component} from '@angular/core'; @Component({ selector: 'my-app', template: \` <svg> <rect width="100" height="100"></rect> </svg> \`, }) export class AppComponent {} `, }; const project = createModuleAndProjectWithDeclarations(env, 'test', files); const appFile = project.openFile('app.ts'); const rectStart = appFile.contents.indexOf('<rect') + 1; const linkedRanges = project.getLinkedEditingRangeAtPosition('app.ts', rectStart); expect(linkedRanges).not.toBeNull(); expect(linkedRanges!.ranges.length).toBe(2); }); }); });