test(forms): split Forms example app into Reactive and Template-driven ones (#41108)

One of the main goals of the bundling tests is to verify that unused symbols are tree-shaken away in prod bundles.
Currently both Reactive and Template-driven test apps are merged into one. In order to make these tree-shaking
tests even more useful, this commit splits exiting test app into two, so that we can further optimize sets of
symbols that should be retained in both scenarios.

PR Close #41108
This commit is contained in:
Andrew Kushnir 2021-03-07 14:33:10 -08:00
parent 2ebe2bcb2f
commit fa3689f432
12 changed files with 1980 additions and 165 deletions

View File

@ -5,7 +5,7 @@ load("//tools/symbol-extractor:index.bzl", "js_expected_symbol_test")
load("@npm//http-server:index.bzl", "http_server")
ng_module(
name = "forms",
name = "forms_reactive",
srcs = ["index.ts"],
tags = [
"ivy-only",
@ -24,7 +24,7 @@ ng_rollup_bundle(
"ivy-only",
],
deps = [
":forms",
":forms_reactive",
"//packages/core",
"//packages/forms",
"//packages/platform-browser",

View File

@ -227,9 +227,6 @@
{
"name": "FormsExampleModule"
},
{
"name": "FormsModule"
},
{
"name": "INJECTOR"
},
@ -389,21 +386,12 @@
{
"name": "NgForOfContext"
},
{
"name": "NgForm"
},
{
"name": "NgLocaleLocalization"
},
{
"name": "NgLocalization"
},
{
"name": "NgModel"
},
{
"name": "NgModelGroup"
},
{
"name": "NgModuleFactory"
},
@ -473,9 +461,6 @@
{
"name": "RANGE_VALUE_ACCESSOR"
},
{
"name": "REQUIRED_VALIDATOR"
},
{
"name": "RadioControlRegistry"
},
@ -512,9 +497,6 @@
{
"name": "RendererStyleFlags2"
},
{
"name": "RequiredValidator"
},
{
"name": "RootComponent"
},
@ -593,12 +575,6 @@
{
"name": "TRANSITION_ID"
},
{
"name": "TemplateFormsComponent"
},
{
"name": "TemplateFormsComponent_div_14_Template"
},
{
"name": "TemplateRef"
},
@ -941,12 +917,6 @@
{
"name": "formArrayNameProvider"
},
{
"name": "formControlBinding"
},
{
"name": "formDirectiveProvider"
},
{
"name": "formDirectiveProvider"
},
@ -1094,9 +1064,6 @@
{
"name": "getSelectedIndex"
},
{
"name": "getSelectedTNode"
},
{
"name": "getSimpleChangesStore"
},
@ -1271,9 +1238,6 @@
{
"name": "isPromise"
},
{
"name": "isPropertyUpdated"
},
{
"name": "isScheduler"
},
@ -1361,9 +1325,6 @@
{
"name": "mergeValidators"
},
{
"name": "modelGroupProvider"
},
{
"name": "modules"
},
@ -1391,9 +1352,6 @@
{
"name": "nativeParentNode"
},
{
"name": "nextBindingIndex"
},
{
"name": "nextNgElementId"
},
@ -1493,9 +1451,6 @@
{
"name": "renderComponentOrTemplate"
},
{
"name": "renderStringify"
},
{
"name": "renderView"
},
@ -1511,12 +1466,6 @@
{
"name": "resolveProvider"
},
{
"name": "resolvedPromise"
},
{
"name": "resolvedPromise"
},
{
"name": "rxSubscriber"
},
@ -1538,9 +1487,6 @@
{
"name": "selectIndexInternal"
},
{
"name": "selectValueAccessor"
},
{
"name": "setBindingRootForHostBindings"
},
@ -1595,9 +1541,6 @@
{
"name": "setUpControl"
},
{
"name": "setUpFormContainer"
},
{
"name": "setUpValidators"
},
@ -1622,9 +1565,6 @@
{
"name": "subscribeToArray"
},
{
"name": "syncPendingControls"
},
{
"name": "throwProviderNotFoundError"
},
@ -1685,12 +1625,6 @@
{
"name": "ɵɵProvidersFeature"
},
{
"name": "ɵɵadvance"
},
{
"name": "ɵɵattribute"
},
{
"name": "ɵɵclassProp"
},
@ -1730,15 +1664,9 @@
{
"name": "ɵɵlistener"
},
{
"name": "ɵɵnextContext"
},
{
"name": "ɵɵproperty"
},
{
"name": "ɵɵtemplate"
},
{
"name": "ɵɵtext"
}

View File

@ -11,36 +11,16 @@ import {ɵwhenRendered as whenRendered} from '@angular/core';
import {withBody} from '@angular/private/testing';
import * as path from 'path';
const PACKAGE = 'angular/packages/core/test/bundling/forms';
const PACKAGE = 'angular/packages/core/test/bundling/forms_reactive';
const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js'];
describe('functional test for forms', () => {
describe('functional test for reactive forms', () => {
BUNDLES.forEach((bundle) => {
describe(`using ${bundle} bundle`, () => {
it('should render template form', withBody('<app-root></app-root>', async () => {
require(path.join(PACKAGE, bundle));
await (window as any).waitForApp;
// Template forms
const templateFormsComponent = (window as any).templateFormsComponent;
await whenRendered(templateFormsComponent);
const templateForm = document.querySelector('app-template-forms')!;
// Check for inputs
const iputs = templateForm.querySelectorAll('input');
expect(iputs.length).toBe(5);
// Check for button
const templateButtons = templateForm.querySelectorAll('button');
expect(templateButtons.length).toBe(1);
expect(templateButtons[0]).toBeDefined();
// Make sure button click works
const templateFormSpy = spyOn(templateFormsComponent, 'addCity');
templateButtons[0].click();
expect(templateFormSpy).toHaveBeenCalled();
// Reactive forms
const reactiveFormsComponent = (window as any).reactiveFormsComponent;
await whenRendered(reactiveFormsComponent);

View File

@ -2,7 +2,7 @@
<html>
<head>
<title>Angular Forms Example</title>
<title>Angular Reactive Forms Example</title>
</head>
<body>
<!-- The Angular application will be bootstrapped into this element. -->

View File

@ -6,50 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, NgModule, ɵNgModuleFactory as NgModuleFactory} from '@angular/core';
import {FormArray, FormBuilder, FormControl, FormGroup, FormsModule, NgForm, ReactiveFormsModule, Validators} from '@angular/forms';
import {FormArray, FormBuilder, FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
import {BrowserModule, platformBrowser} from '@angular/platform-browser';
@Component({
selector: 'app-template-forms',
template: `
<form novalidate>
<div ngModelGroup="profileForm">
<div>
First Name:
<input name="first" ngModel required />
</div>
<div>
Last Name:
<input name="last" ngModel />
</div>
<div>
Subscribe:
<input name="subscribed" type="checkbox" ngModel />
</div>
<div>Disabled: <input name="foo" ngModel disabled /></div>
<div *ngFor="let city of addresses; let i = index">
City <input [(ngModel)]="addresses[i].city" name="name" />
</div>
<button (click)="addCity()">Add City</button>
</div>
</form>`,
})
class TemplateFormsComponent {
name = {first: 'Nancy', last: 'Drew', subscribed: true};
addresses = [{city: 'Toronto'}];
constructor() {
// We use this reference in our test
(window as any).templateFormsComponent = this;
}
addCity() {
this.addresses.push(({city: ''}));
}
}
@Component({
selector: 'app-reactive-forms',
template: `
@ -75,7 +34,8 @@ class TemplateFormsComponent {
</div>
</div>
<button (click)="addCity()">Add City</button>
</form>`,
</form>
`
})
class ReactiveFormsComponent {
profileForm!: FormGroup;
@ -117,7 +77,6 @@ class ReactiveFormsComponent {
@Component({
selector: 'app-root',
template: `
<app-template-forms></app-template-forms>
<app-reactive-forms></app-reactive-forms>
`
})
@ -125,8 +84,8 @@ class RootComponent {
}
@NgModule({
declarations: [RootComponent, TemplateFormsComponent, ReactiveFormsComponent],
imports: [BrowserModule, FormsModule, ReactiveFormsModule]
declarations: [RootComponent, ReactiveFormsComponent],
imports: [BrowserModule, ReactiveFormsModule]
})
class FormsExampleModule {
ngDoBootstrap(app: any) {

View File

@ -13,7 +13,7 @@ import * as path from 'path';
const UTF8 = {
encoding: 'utf-8'
};
const PACKAGE = 'angular/packages/core/test/bundling/forms';
const PACKAGE = 'angular/packages/core/test/bundling/forms_reactive';
describe('treeshaking with uglify', () => {
let content: string;

View File

@ -0,0 +1,85 @@
package(default_visibility = ["//visibility:public"])
load("//tools:defaults.bzl", "jasmine_node_test", "ng_module", "ng_rollup_bundle", "ts_library")
load("//tools/symbol-extractor:index.bzl", "js_expected_symbol_test")
load("@npm//http-server:index.bzl", "http_server")
ng_module(
name = "forms_template_driven",
srcs = ["index.ts"],
tags = [
"ivy-only",
],
deps = [
"//packages/core",
"//packages/forms",
"//packages/platform-browser",
],
)
ng_rollup_bundle(
name = "bundle",
entry_point = ":index.ts",
tags = [
"ivy-only",
],
deps = [
":forms_template_driven",
"//packages/core",
"//packages/forms",
"//packages/platform-browser",
"@npm//rxjs",
],
)
ts_library(
name = "test_lib",
testonly = True,
srcs = glob(["*_spec.ts"]),
tags = [
"ivy-only",
],
deps = [
"//packages:types",
"//packages/compiler",
"//packages/core",
"//packages/core/testing",
"//packages/private/testing",
],
)
jasmine_node_test(
name = "test",
data = [
":bundle.js",
":bundle.min.js",
":bundle.min.js.br",
":bundle.min_debug.js",
],
tags = [
"ivy-only",
],
deps = [":test_lib"],
)
js_expected_symbol_test(
name = "symbol_test",
src = ":bundle.min_debug.js",
golden = ":bundle.golden_symbols.json",
tags = [
"ivy-aot",
"ivy-only",
],
)
http_server(
name = "prodserver",
data = [
"index.html",
":bundle.min.js",
":bundle.min_debug.js",
],
tags = [
"ivy-only",
],
)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,46 @@
/**
* @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.io/license
*/
import '@angular/compiler';
import {ɵwhenRendered as whenRendered} from '@angular/core';
import {withBody} from '@angular/private/testing';
import * as path from 'path';
const PACKAGE = 'angular/packages/core/test/bundling/forms_template_driven';
const BUNDLES = ['bundle.js', 'bundle.min_debug.js', 'bundle.min.js'];
describe('functional test for forms', () => {
BUNDLES.forEach((bundle) => {
describe(`using ${bundle} bundle`, () => {
it('should render template form', withBody('<app-root></app-root>', async () => {
require(path.join(PACKAGE, bundle));
await (window as any).waitForApp;
// Template forms
const templateFormsComponent = (window as any).templateFormsComponent;
await whenRendered(templateFormsComponent);
const templateForm = document.querySelector('app-template-forms')!;
// Check for inputs
const iputs = templateForm.querySelectorAll('input');
expect(iputs.length).toBe(5);
// Check for button
const templateButtons = templateForm.querySelectorAll('button');
expect(templateButtons.length).toBe(1);
expect(templateButtons[0]).toBeDefined();
// Make sure button click works
const templateFormSpy = spyOn(templateFormsComponent, 'addCity');
templateButtons[0].click();
expect(templateFormSpy).toHaveBeenCalled();
}));
});
});
});

View File

@ -0,0 +1,32 @@
<!doctype html>
<html>
<head>
<title>Angular Template-driven Forms Example</title>
</head>
<body>
<!-- The Angular application will be bootstrapped into this element. -->
<app-root></app-root>
<!--
Script tag which bootstraps the application. Use `?debug` in URL to select
the debug version of the script.
There are two scripts sources: `bundle.min.js` and `bundle.min_debug.js` You can
switch between which bundle the browser loads to experiment with the application.
- `bundle.min.js`: Is what the site would serve to their users. It has gone
through rollup, build-optimizer, and uglify with tree shaking.
- `bundle.min_debug.js`: Is what the developer would like to see when debugging
the application. It has also done through full pipeline of rollup, build-optimizer,
and uglify, however special flags were passed to uglify to prevent inlining and
property renaming.
-->
<script>
document.write('<script src="' +
(document.location.search.endsWith('debug') ? '/bundle.min_debug.js' : '/bundle.min.js') +
'"></' + 'script>');
</script>
</body>
</html>

View File

@ -0,0 +1,74 @@
/**
* @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.io/license
*/
import {Component, NgModule, ɵNgModuleFactory as NgModuleFactory} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {BrowserModule, platformBrowser} from '@angular/platform-browser';
@Component({
selector: 'app-template-forms',
template: `
<form novalidate>
<div ngModelGroup="profileForm">
<div>
First Name:
<input name="first" ngModel required />
</div>
<div>
Last Name:
<input name="last" ngModel />
</div>
<div>
Subscribe:
<input name="subscribed" type="checkbox" ngModel />
</div>
<div>Disabled: <input name="foo" ngModel disabled /></div>
<div *ngFor="let city of addresses; let i = index">
City <input [(ngModel)]="addresses[i].city" name="name" />
</div>
<button (click)="addCity()">Add City</button>
</div>
</form>
`
})
class TemplateFormsComponent {
name = {first: 'Nancy', last: 'Drew', subscribed: true};
addresses = [{city: 'Toronto'}];
constructor() {
// We use this reference in our test
(window as any).templateFormsComponent = this;
}
addCity() {
this.addresses.push({city: ''});
}
}
@Component({
selector: 'app-root',
template: `
<app-template-forms></app-template-forms>
`
})
class RootComponent {
}
@NgModule({
declarations: [RootComponent, TemplateFormsComponent],
imports: [BrowserModule, FormsModule],
})
class FormsExampleModule {
ngDoBootstrap(app: any) {
app.bootstrap(RootComponent);
}
}
(window as any).waitForApp = platformBrowser().bootstrapModuleFactory(
new NgModuleFactory(FormsExampleModule), {ngZone: 'noop'});

View File

@ -0,0 +1,35 @@
/**
* @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.io/license
*/
import '@angular/compiler';
import * as fs from 'fs';
import * as path from 'path';
const UTF8 = {
encoding: 'utf-8'
};
const PACKAGE = 'angular/packages/core/test/bundling/forms_template_driven';
describe('treeshaking with uglify', () => {
let content: string;
// We use the debug version as otherwise symbols/identifiers would be mangled (and the test would
// always pass)
const contentPath = require.resolve(path.join(PACKAGE, 'bundle.min_debug.js'));
beforeAll(() => {
content = fs.readFileSync(contentPath, UTF8);
});
it('should drop unused TypeScript helpers', () => {
expect(content).not.toContain('__asyncGenerator');
});
it('should not contain rxjs from commonjs distro', () => {
expect(content).not.toContain('commonjsGlobal');
expect(content).not.toContain('createCommonjsModule');
});
});