Skip to content

Commit 36e0c79

Browse files
feat: zoneless ATL (#570)
1 parent b8261f8 commit 36e0c79

File tree

9 files changed

+657
-0
lines changed

9 files changed

+657
-0
lines changed

projects/testing-library/schematics/collection.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
"factory": "./ng-add",
66
"schema": "./ng-add/schema.json",
77
"description": "Add @testing-library/angular to your application"
8+
},
9+
"migrate-to-zoneless": {
10+
"factory": "./migrate-to-zoneless",
11+
"schema": "./migrate-to-zoneless/schema.json",
12+
"description": "Migrate imports from @testing-library/angular to @testing-library/angular/zoneless"
813
}
914
}
1015
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
2+
import * as path from 'path';
3+
import { EmptyTree } from '@angular-devkit/schematics';
4+
import { test, expect } from 'vitest';
5+
6+
test('migrates imports from @testing-library/angular to @testing-library/angular/zoneless', async () => {
7+
const before = `
8+
import { render, screen } from '@testing-library/angular';
9+
import { AppComponent } from './app.component';
10+
11+
describe('AppComponent', () => {
12+
it('should render', async () => {
13+
await render(AppComponent);
14+
expect(screen.getByText('Hello')).toBeInTheDocument();
15+
});
16+
});
17+
`;
18+
19+
const after = `
20+
import { render, screen } from '@testing-library/angular/zoneless';
21+
import { AppComponent } from './app.component';
22+
23+
describe('AppComponent', () => {
24+
it('should render', async () => {
25+
await render(AppComponent);
26+
expect(screen.getByText('Hello')).toBeInTheDocument();
27+
});
28+
});
29+
`;
30+
31+
const tree = await setup({
32+
'src/app.spec.ts': before,
33+
});
34+
35+
expect(tree.readContent('src/app.spec.ts')).toBe(after);
36+
});
37+
38+
test('migrates imports with double quotes', async () => {
39+
const before = `import { render } from "@testing-library/angular";`;
40+
const after = `import { render } from "@testing-library/angular/zoneless";`;
41+
42+
const tree = await setup({
43+
'src/test.spec.ts': before,
44+
});
45+
46+
expect(tree.readContent('src/test.spec.ts')).toBe(after);
47+
});
48+
49+
test('migrates multiple imports in the same file', async () => {
50+
const before = `
51+
import { render, screen } from '@testing-library/angular';
52+
import { fireEvent } from '@testing-library/angular';
53+
`;
54+
55+
const after = `
56+
import { render, screen } from '@testing-library/angular/zoneless';
57+
import { fireEvent } from '@testing-library/angular/zoneless';
58+
`;
59+
60+
const tree = await setup({
61+
'src/multi.spec.ts': before,
62+
});
63+
64+
expect(tree.readContent('src/multi.spec.ts')).toBe(after);
65+
});
66+
67+
test('does not modify imports from other packages', async () => {
68+
const before = `
69+
import { render } from '@testing-library/angular';
70+
import { screen } from '@testing-library/dom';
71+
import { Component } from '@angular/core';
72+
`;
73+
74+
const after = `
75+
import { render } from '@testing-library/angular/zoneless';
76+
import { screen } from '@testing-library/dom';
77+
import { Component } from '@angular/core';
78+
`;
79+
80+
const tree = await setup({
81+
'src/other.spec.ts': before,
82+
});
83+
84+
expect(tree.readContent('src/other.spec.ts')).toBe(after);
85+
});
86+
87+
test('handles files without @testing-library/angular imports', async () => {
88+
const content = `
89+
import { Component } from '@angular/core';
90+
91+
@Component({
92+
selector: 'app-root',
93+
template: '<h1>Hello</h1>'
94+
})
95+
export class AppComponent {}
96+
`;
97+
98+
const tree = await setup({
99+
'src/regular.ts': content,
100+
});
101+
102+
expect(tree.readContent('src/regular.ts')).toBe(content);
103+
});
104+
105+
test('migrates multiple files', async () => {
106+
const tree = await setup({
107+
'src/file1.spec.ts': `import { render } from '@testing-library/angular';`,
108+
'src/file2.spec.ts': `import { screen } from '@testing-library/angular';`,
109+
'src/file3.spec.ts': `import { fireEvent } from '@testing-library/angular';`,
110+
});
111+
112+
expect(tree.readContent('src/file1.spec.ts')).toBe(`import { render } from '@testing-library/angular/zoneless';`);
113+
expect(tree.readContent('src/file2.spec.ts')).toBe(`import { screen } from '@testing-library/angular/zoneless';`);
114+
expect(tree.readContent('src/file3.spec.ts')).toBe(`import { fireEvent } from '@testing-library/angular/zoneless';`);
115+
});
116+
117+
async function setup(files: Record<string, string>) {
118+
const collectionPath = path.join(__dirname, '../../../../dist/@testing-library/angular/schematics/collection.json');
119+
const schematicRunner = new SchematicTestRunner('schematics', collectionPath);
120+
121+
const tree = new UnitTestTree(new EmptyTree());
122+
123+
for (const [filePath, content] of Object.entries(files)) {
124+
tree.create(filePath, content);
125+
}
126+
127+
await schematicRunner.runSchematic('migrate-to-zoneless', {}, tree);
128+
129+
return tree;
130+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics';
2+
import * as ts from 'typescript';
3+
4+
export default function (): Rule {
5+
return async (tree: Tree, context: SchematicContext) => {
6+
context.logger.info('Migrating imports from @testing-library/angular to @testing-library/angular/zoneless...');
7+
8+
let filesUpdated = 0;
9+
10+
tree.visit((path) => {
11+
if (!path.endsWith('.ts') || path.includes('node_modules')) {
12+
return;
13+
}
14+
15+
const content = tree.read(path);
16+
if (!content) {
17+
return;
18+
}
19+
20+
const text = content.toString('utf-8');
21+
22+
if (!text.includes('@testing-library/angular')) {
23+
return;
24+
}
25+
26+
const sourceFile = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true);
27+
28+
const changes: { start: number; end: number; newText: string }[] = [];
29+
30+
function visit(node: ts.Node) {
31+
if (ts.isImportDeclaration(node)) {
32+
const moduleSpecifier = node.moduleSpecifier;
33+
34+
if (ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text === '@testing-library/angular') {
35+
const fullText = moduleSpecifier.getFullText(sourceFile);
36+
const quoteChar = fullText.trim()[0]; // ' or "
37+
38+
changes.push({
39+
start: moduleSpecifier.getStart(sourceFile),
40+
end: moduleSpecifier.getEnd(),
41+
newText: `${quoteChar}@testing-library/angular/zoneless${quoteChar}`,
42+
});
43+
}
44+
}
45+
46+
ts.forEachChild(node, visit);
47+
}
48+
49+
visit(sourceFile);
50+
51+
if (changes.length > 0) {
52+
changes.sort((a, b) => b.start - a.start);
53+
54+
let updatedText = text;
55+
for (const change of changes) {
56+
updatedText = updatedText.slice(0, change.start) + change.newText + updatedText.slice(change.end);
57+
}
58+
59+
tree.overwrite(path, updatedText);
60+
filesUpdated++;
61+
context.logger.info(`Updated: ${path}`);
62+
}
63+
});
64+
65+
if (filesUpdated > 0) {
66+
context.logger.info(`✓ Successfully migrated ${filesUpdated} file(s) to use @testing-library/angular/zoneless`);
67+
} else {
68+
context.logger.warn('No files found with @testing-library/angular imports.');
69+
}
70+
71+
return tree;
72+
};
73+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "http://json-schema.org/schema",
3+
"$id": "MigrateToZonelessSchema",
4+
"title": "Migrate to Zoneless Schema",
5+
"type": "object",
6+
"description": "Migrate imports from @testing-library/angular to @testing-library/angular/zoneless",
7+
"properties": {},
8+
"required": []
9+
}

projects/testing-library/schematics/migrations/dtl-as-dev-dependency/index.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SchematicTestRunner, UnitTestTree } from '@angular-devkit/schematics/testing';
22
import * as path from 'path';
33
import { EmptyTree } from '@angular-devkit/schematics';
4+
import { test, expect } from 'vitest';
45

56
test('adds DTL to devDependencies', async () => {
67
const tree = await setup({});

0 commit comments

Comments
 (0)