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:
parent
2ebe2bcb2f
commit
fa3689f432
|
@ -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",
|
|
@ -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"
|
||||
}
|
|
@ -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);
|
|
@ -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. -->
|
|
@ -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) {
|
|
@ -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;
|
|
@ -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
|
@ -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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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>
|
|
@ -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'});
|
|
@ -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');
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue