Skip to content

Commit 6b94d25

Browse files
authored
feat: support standalone components (#297)
BREAKING CHANGE: This version requires Angular v14+
1 parent 6389bf2 commit 6b94d25

File tree

7 files changed

+95
-47
lines changed

7 files changed

+95
-47
lines changed

apps/example-app/src/app/examples/03-forms.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component } from '@angular/core';
2-
import { FormBuilder, Validators } from '@angular/forms';
2+
import { UntypedFormBuilder, Validators } from '@angular/forms';
33

44
@Component({
55
selector: 'app-fixture',
@@ -41,7 +41,7 @@ export class FormsComponent {
4141
color: ['', Validators.required],
4242
});
4343

44-
constructor(private formBuilder: FormBuilder) {}
44+
constructor(private formBuilder: UntypedFormBuilder) {}
4545

4646
get formErrors() {
4747
return Object.keys(this.form.controls)

apps/example-app/src/app/examples/04-forms-with-material.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Component } from '@angular/core';
2-
import { FormBuilder, Validators } from '@angular/forms';
2+
import { UntypedFormBuilder, Validators } from '@angular/forms';
33

44
@Component({
55
selector: 'app-fixture',
@@ -66,7 +66,7 @@ export class MaterialFormsComponent {
6666
color: [null, Validators.required],
6767
});
6868

69-
constructor(private formBuilder: FormBuilder) {}
69+
constructor(private formBuilder: UntypedFormBuilder) {}
7070

7171
get colorControlDisplayValue(): string | undefined {
7272
const selectedId = this.form.get('color')?.value;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { render, screen } from '@testing-library/angular';
2+
import { StandaloneComponent, StandaloneWithChildComponent } from './19-standalone-component';
3+
4+
test('is possible to render a standalone component', async () => {
5+
await render(StandaloneComponent);
6+
7+
const content = screen.getByTestId('standalone');
8+
9+
expect(content).toHaveTextContent('Standalone Component');
10+
});
11+
12+
test('is possibl to render a standalone component with a child', async () => {
13+
await render(StandaloneWithChildComponent, {
14+
componentProperties: { name: 'Bob' },
15+
});
16+
17+
const childContent = screen.getByTestId('standalone');
18+
expect(childContent).toHaveTextContent('Standalone Component');
19+
20+
expect(screen.getByText('Hi Bob')).toBeInTheDocument();
21+
expect(screen.getByText('This has a child')).toBeInTheDocument();
22+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Component, Input } from '@angular/core';
2+
3+
@Component({
4+
selector: 'app-standalone',
5+
template: `<div data-testid="standalone">Standalone Component</div>`,
6+
standalone: true,
7+
})
8+
export class StandaloneComponent { }
9+
10+
@Component({
11+
selector: 'app-standalone-with-child',
12+
template: `<h1>Hi {{ name }}</h1>
13+
<p>This has a child</p>
14+
<app-standalone></app-standalone> `,
15+
standalone: true,
16+
imports: [StandaloneComponent],
17+
})
18+
export class StandaloneWithChildComponent {
19+
@Input()
20+
name?: string;
21+
}

package.json

+21-20
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@
2828
"prepare": "git config core.hookspath .githooks"
2929
},
3030
"dependencies": {
31-
"@angular/animations": "13.1.1",
32-
"@angular/cdk": "13.1.1",
33-
"@angular/common": "13.1.1",
34-
"@angular/compiler": "13.1.1",
35-
"@angular/core": "13.1.1",
36-
"@angular/material": "13.1.1",
37-
"@angular/platform-browser": "13.1.1",
38-
"@angular/platform-browser-dynamic": "13.1.1",
39-
"@angular/router": "13.1.1",
40-
"@ngrx/store": "13.0.2",
31+
"@angular/animations": "14.0.0",
32+
"@angular/cdk": "14.0.0",
33+
"@angular/common": "14.0.0",
34+
"@angular/compiler": "14.0.0",
35+
"@angular/core": "14.0.0",
36+
"@angular/material": "14.0.0",
37+
"@angular/platform-browser": "14.0.0",
38+
"@angular/platform-browser-dynamic": "14.0.0",
39+
"@angular/router": "14.0.0",
40+
"@ngrx/store": "14.0.0-beta.0",
4141
"@nrwl/angular": "13.4.3",
4242
"@nrwl/nx-cloud": "13.0.2",
4343
"@testing-library/dom": "^8.11.1",
@@ -46,14 +46,14 @@
4646
"zone.js": "~0.11.4"
4747
},
4848
"devDependencies": {
49-
"@angular-devkit/build-angular": "13.1.2",
49+
"@angular-devkit/build-angular": "14.0.0",
5050
"@angular-eslint/eslint-plugin": "13.0.1",
5151
"@angular-eslint/eslint-plugin-template": "13.0.1",
5252
"@angular-eslint/template-parser": "13.0.1",
53-
"@angular/cli": "13.1.2",
54-
"@angular/compiler-cli": "13.1.1",
55-
"@angular/forms": "13.1.1",
56-
"@angular/language-service": "13.1.1",
53+
"@angular/cli": "14.0.0",
54+
"@angular/compiler-cli": "14.0.0",
55+
"@angular/forms": "14.0.0",
56+
"@angular/language-service": "14.0.0",
5757
"@nrwl/cli": "13.4.3",
5858
"@nrwl/eslint-plugin-nx": "13.4.3",
5959
"@nrwl/jest": "13.4.3",
@@ -79,23 +79,24 @@
7979
"eslint-plugin-testing-library": "~5.0.1",
8080
"jasmine-core": "^3.10.1",
8181
"jasmine-spec-reporter": "^7.0.0",
82-
"jest": "27.4.7",
83-
"jest-preset-angular": "11.0.1",
82+
"jest": "28.1.1",
83+
"jest-environment-jsdom": "^28.1.1",
84+
"jest-preset-angular": "12.1.0",
8485
"karma": "^6.3.9",
8586
"karma-chrome-launcher": "^3.1.0",
8687
"karma-jasmine": "^4.0.1",
8788
"karma-jasmine-html-reporter": "^1.7.0",
8889
"lint-staged": "^12.1.6",
89-
"ng-packagr": "13.1.2",
90+
"ng-packagr": "14.0.0",
9091
"postcss": "^8.4.5",
9192
"postcss-import": "^14.0.2",
9293
"postcss-preset-env": "^7.2.0",
9394
"postcss-url": "^10.1.3",
9495
"prettier": "^2.4.1",
9596
"rimraf": "^3.0.2",
9697
"semantic-release": "^18.0.0",
97-
"ts-jest": "27.1.2",
98+
"ts-jest": "28.0.4",
9899
"ts-node": "~10.4.0",
99-
"typescript": "4.5.4"
100+
"typescript": "4.7.2"
100101
}
101102
}

projects/testing-library/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@
2929
"migrations": "./schematics/migrations/migration.json"
3030
},
3131
"peerDependencies": {
32-
"@angular/common": ">= 13.0.0",
33-
"@angular/platform-browser": ">= 13.0.0",
34-
"@angular/router": ">= 13.0.0",
35-
"@angular/core": ">= 13.0.0",
32+
"@angular/common": ">= 14.0.0",
33+
"@angular/platform-browser": ">= 14.0.0",
34+
"@angular/router": ">= 14.0.0",
35+
"@angular/core": ">= 14.0.0",
3636
"rxjs": ">= 7.4.0"
3737
},
3838
"dependencies": {

projects/testing-library/src/lib/testing-library.ts

+23-19
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
OnChanges,
88
SimpleChanges,
99
ApplicationInitStatus,
10+
ɵisStandalone,
1011
} from '@angular/core';
1112
import { ComponentFixture, TestBed, tick } from '@angular/core/testing';
1213
import { By } from '@angular/platform-browser';
@@ -76,7 +77,7 @@ export async function render<SutType, WrapperType = SutType>(
7677
excludeComponentDeclaration,
7778
wrapper,
7879
}),
79-
imports: addAutoImports({
80+
imports: addAutoImports(sut,{
8081
imports: imports.concat(defaultImports),
8182
routes,
8283
}),
@@ -128,23 +129,23 @@ export async function render<SutType, WrapperType = SutType>(
128129
const [path, params] = (basePath + href).split('?');
129130
const queryParams = params
130131
? params.split('&').reduce((qp, q) => {
131-
const [key, value] = q.split('=');
132-
const currentValue = qp[key];
133-
if (typeof currentValue === 'undefined') {
134-
qp[key] = value;
135-
} else if (Array.isArray(currentValue)) {
136-
qp[key] = [...currentValue, value];
137-
} else {
138-
qp[key] = [currentValue, value];
139-
}
140-
return qp;
141-
}, {} as Record<string, string | string[]>)
132+
const [key, value] = q.split('=');
133+
const currentValue = qp[key];
134+
if (typeof currentValue === 'undefined') {
135+
qp[key] = value;
136+
} else if (Array.isArray(currentValue)) {
137+
qp[key] = [...currentValue, value];
138+
} else {
139+
qp[key] = [currentValue, value];
140+
}
141+
return qp;
142+
}, {} as Record<string, string | string[]>)
142143
: undefined;
143144

144145
const navigateOptions: NavigationExtras | undefined = queryParams
145146
? {
146-
queryParams,
147-
}
147+
queryParams,
148+
}
148149
: undefined;
149150

150151
const doNavigate = () => {
@@ -296,20 +297,23 @@ function addAutoDeclarations<SutType>(
296297
return [...declarations, wrapper];
297298
}
298299

299-
const components = () => (excludeComponentDeclaration ? [] : [sut]);
300+
const components = () => (excludeComponentDeclaration || ɵisStandalone(sut) ? [] : [sut]);
300301
return [...declarations, ...components()];
301302
}
302303

303-
function addAutoImports({ imports = [], routes }: Pick<RenderComponentOptions<any>, 'imports' | 'routes'>) {
304+
function addAutoImports<SutType>(
305+
sut: Type<SutType> | string,
306+
{ imports = [], routes }: Pick<RenderComponentOptions<any>, 'imports' | 'routes'>,
307+
) {
304308
const animations = () => {
305309
const animationIsDefined =
306310
imports.indexOf(NoopAnimationsModule) > -1 || imports.indexOf(BrowserAnimationsModule) > -1;
307311
return animationIsDefined ? [] : [NoopAnimationsModule];
308312
};
309313

310314
const routing = () => (routes ? [RouterTestingModule.withRoutes(routes)] : []);
311-
312-
return [...imports, ...animations(), ...routing()];
315+
const components = () => (typeof sut !== 'string' && ɵisStandalone(sut) ? [sut] : []);
316+
return [...imports, ...components(), ...animations(), ...routing()];
313317
}
314318

315319
/**
@@ -394,7 +398,7 @@ if (typeof process === 'undefined' || !process.env?.ATL_SKIP_AUTO_CLEANUP) {
394398
}
395399

396400
@Component({ selector: 'atl-wrapper-component', template: '' })
397-
class WrapperComponent {}
401+
class WrapperComponent { }
398402

399403
/**
400404
* Wrap findBy queries to poke the Angular change detection cycle

0 commit comments

Comments
 (0)