test: add Integration tests for hydration and event reply (#55708)

This commit introduces integration tests for hydration and event reply functionalities. Additionally, it implements a payload size check for the `event-dispatch-contract.min.js`.

PR Close #55708
This commit is contained in:
Alan Agius
2024-05-07 09:40:00 +00:00
committed by Andrew Kushnir
parent ca6cdcd269
commit 8630dc8149
28 changed files with 7144 additions and 3 deletions
+1
View File
@@ -17,6 +17,7 @@ integration/cli-hello-world-ivy-i18n/node_modules
integration/cli-hello-world-lazy/node_modules
integration/cli-hello-world-mocha/node_modules
integration/cli-signal-inputs/node_modules
integration/platform-server-hydration/node_modules
integration/defer/node_modules
integration/dynamic-compiler/node_modules
integration/forms/node_modules
@@ -51,5 +51,12 @@
"polyfills": 33807,
"defer.component": 371
}
},
"platform-server-hydration/browser": {
"uncompressed": {
"main": 200584,
"polyfills": 33807,
"event-dispatch-contract.min": 11293
}
}
}
@@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
# System files
.DS_Store
Thumbs.db
# event contract
public/event-dispatch-contract.min.js
@@ -0,0 +1,10 @@
load("//integration:index.bzl", "ng_integration_test")
ng_integration_test(
name = "test",
setup_chromium = True,
track_payload_paths = [
"/browser",
],
track_payload_size = "platform-server-hydration",
)
@@ -0,0 +1,99 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"cli": {
"analytics": false,
"cache": {
"enabled": false
}
},
"projects": {
"hydration": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular-devkit/build-angular:application",
"options": {
"progress": false,
"outputPath": "dist",
"index": "src/index.html",
"browser": "src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "tsconfig.app.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": ["src/styles.css"],
"scripts": [],
"server": "src/main.server.ts",
"prerender": false,
"ssr": true
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "2kb",
"maximumError": "4kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"port": 0
},
"configurations": {
"production": {
"buildTarget": "hydration:build:production"
},
"development": {
"buildTarget": "hydration:build:development"
}
},
"defaultConfiguration": "development"
},
"e2e": {
"builder": "@angular-devkit/build-angular:protractor",
"options": {
"port": 0,
"protractorConfig": "e2e/protractor.conf.js",
"webdriverUpdate": false
},
"configurations": {
"production": {
"devServerTarget": "hydration:serve:production"
},
"development": {
"devServerTarget": "hydration:serve:development"
}
},
"defaultConfiguration": "development"
}
}
}
}
}
@@ -0,0 +1,11 @@
/**
* This script copies '@angular/core/event-dispatch-contract.min.js' to the 'public' directory, ensuring it's available as an asset.
* This is necessary to perform size checks on the distributed script.
*/
import {copyFileSync} from 'node:fs';
copyFileSync(
'./node_modules/@angular/core/event-dispatch-contract.min.js',
'public/event-dispatch-contract.min.js',
);
@@ -0,0 +1,42 @@
// @ts-check
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const {SpecReporter} = require('jasmine-spec-reporter');
/**
* @type { import("protractor").Config }
*/
exports.config = {
allScriptsTimeout: 11000,
specs: ['./src/**/*.e2e-spec.ts'],
chromeDriver: process.env.CHROMEDRIVER_BIN,
capabilities: {
browserName: 'chrome',
chromeOptions: {
binary: process.env.CHROME_BIN,
// See /integration/README.md#browser-tests for more info on these args
args: [
'--no-sandbox',
'--headless',
'--disable-gpu',
'--disable-dev-shm-usage',
'--hide-scrollbars',
'--mute-audio',
],
},
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function () {},
},
onPrepare() {
require('ts-node').register({
project: require('path').join(__dirname, './tsconfig.json'),
});
jasmine.getEnv().addReporter(new SpecReporter({spec: {displayStacktrace: true}}));
},
};
@@ -0,0 +1,30 @@
import {browser, by, element} from 'protractor';
import {bootstrapClientApp, navigateTo, verifyNoBrowserErrors} from './util';
describe('App E2E Tests', () => {
beforeEach(async () => {
// Don't wait for Angular since it is not bootstrapped automatically.
await browser.waitForAngularEnabled(false);
// Load the page without waiting for Angular since it is not bootstrapped automatically.
await navigateTo('');
});
afterEach(async () => {
// Make sure there were no client side errors.
await verifyNoBrowserErrors();
});
it('should reply click event', async () => {
const divElement = element(by.css('#divElement'));
expect(await divElement.getText()).toContain('click not triggered');
// Trigger click
await divElement.click();
// Bootstrap client application
await bootstrapClientApp();
expect(await divElement.getText()).toContain('click triggered');
});
});
@@ -0,0 +1,31 @@
/**
* @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 * as webdriver from 'selenium-webdriver';
import {browser} from 'protractor';
export async function verifyNoBrowserErrors(): Promise<void> {
const browserLog = await browser.manage().logs().get('browser');
const errors: string[] = [];
for (const {message, level} of browserLog) {
console.log('>> ' + message);
if (level.value >= webdriver.logging.Level.INFO.value) {
errors.push(message);
}
}
expect(errors).toEqual([]);
}
export async function navigateTo(url: string): Promise<void> {
await browser.driver.get(browser.baseUrl + url);
}
export async function bootstrapClientApp(): Promise<void> {
await browser.executeScript('doBootstrap()');
}
@@ -0,0 +1,13 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es2019",
"types": [
"jasmine",
"node"
]
}
}
@@ -0,0 +1,40 @@
{
"name": "hydration",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build --configuration production",
"postinstall": "node ./copy-event-dispatch-contract.mjs",
"pretest": "ng version",
"test": "yarn e2e",
"lint": "ng lint",
"e2e": "ng e2e --configuration=production"
},
"private": true,
"dependencies": {
"@angular/animations": "file:../../dist/packages-dist/animations",
"@angular/common": "file:../../dist/packages-dist/common",
"@angular/compiler": "file:../../dist/packages-dist/compiler",
"@angular/core": "file:../../dist/packages-dist/core",
"@angular/platform-browser": "file:../../dist/packages-dist/platform-browser",
"@angular/platform-browser-dynamic": "file:../../dist/packages-dist/platform-browser-dynamic",
"@angular/platform-server": "file:../../dist/packages-dist/platform-server",
"@angular/router": "file:../../dist/packages-dist/router",
"rxjs": "file:../../node_modules/rxjs",
"tslib": "file:../../node_modules/tslib",
"zone.js": "file:../../dist/zone.js-dist/archive/zone.js.tgz"
},
"devDependencies": {
"@angular-devkit/build-angular": "file:../../node_modules/@angular-devkit/build-angular",
"@angular/cli": "file:../../node_modules/@angular/cli",
"@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli",
"@types/jasmine": "file:../../node_modules/@types/jasmine",
"@types/node": "file:../../node_modules/@types/node",
"jasmine-core": "3.5.0",
"jasmine-spec-reporter": "4.2.1",
"protractor": "file:../../node_modules/protractor",
"ts-node": "8.3.0",
"typescript": "file:../../node_modules/typescript"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,3 @@
<div (click)="onClick()" id="divElement">click not triggered.</div>
<router-outlet />
@@ -0,0 +1,29 @@
import {TestBed} from '@angular/core/testing';
import {AppComponent} from './app.component';
describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [AppComponent],
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have the 'hydration' title`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('hydration');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, hydration');
});
});
@@ -0,0 +1,16 @@
import {Component} from '@angular/core';
import {RouterOutlet} from '@angular/router';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet],
templateUrl: './app.component.html',
styleUrl: './app.component.css',
})
export class AppComponent {
title = 'hydration';
onClick = () => {
document.querySelector('#divElement')!.textContent = 'click triggered.';
};
}
@@ -0,0 +1,9 @@
import {mergeApplicationConfig, ApplicationConfig} from '@angular/core';
import {provideServerRendering} from '@angular/platform-server';
import {appConfig} from './app.config';
const serverConfig: ApplicationConfig = {
providers: [provideServerRendering()],
};
export const config = mergeApplicationConfig(appConfig, serverConfig);
@@ -0,0 +1,18 @@
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
import {provideRouter} from '@angular/router';
import {
provideClientHydration,
provideProtractorTestingSupport,
withEventReplay,
} from '@angular/platform-browser';
import {routes} from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({eventCoalescing: true}),
provideRouter(routes),
provideClientHydration(withEventReplay()),
provideProtractorTestingSupport(),
],
};
@@ -0,0 +1,3 @@
import {Routes} from '@angular/router';
export const routes: Routes = [];
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Hydration</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>
@@ -0,0 +1,7 @@
import {bootstrapApplication} from '@angular/platform-browser';
import {AppComponent} from './app/app.component';
import {config} from './app/app.config.server';
const bootstrap = () => bootstrapApplication(AppComponent, config);
export default bootstrap;
@@ -0,0 +1,7 @@
import {bootstrapApplication} from '@angular/platform-browser';
import {appConfig} from './app/app.config';
import {AppComponent} from './app/app.component';
(globalThis as any)['doBootstrap'] = () => {
bootstrapApplication(AppComponent, appConfig).catch((err) => console.error(err));
};
@@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */
@@ -0,0 +1,17 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./out-tsc/app",
"types": [
"node"
]
},
"files": [
"src/main.ts",
"src/main.server.ts"
],
"include": [
"src/**/*.d.ts"
]
}
@@ -0,0 +1,32 @@
/* To learn more about this file see: https://angular.io/config/tsconfig. */
{
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"skipLibCheck": true,
"esModuleInterop": true,
"sourceMap": true,
"declaration": false,
"experimentalDecorators": true,
"moduleResolution": "bundler",
"importHelpers": true,
"target": "ES2022",
"module": "ES2022",
"useDefineForClassFields": false,
"lib": [
"ES2022",
"dom"
]
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}
File diff suppressed because it is too large Load Diff
@@ -10,8 +10,11 @@ import {DOCUMENT} from '@angular/common';
import {Component, destroyPlatform, getPlatform, Type} from '@angular/core';
import {EventContract} from '@angular/core/primitives/event-dispatch';
import {TestBed} from '@angular/core/testing';
import {bootstrapApplication, provideClientHydration} from '@angular/platform-browser';
import {withEventReplay} from '@angular/platform-browser/src/hydration';
import {
withEventReplay,
bootstrapApplication,
provideClientHydration,
} from '@angular/platform-browser';
import {provideServerRendering} from '../public_api';
import {EVENT_DISPATCH_SCRIPT_ID, renderApplication} from '../src/utils';