Alan Agius 481ccdbc5a fix(@angular/ssr): enable serving of prerendered pages in the App Engine
This commit implements the capability for the App Engine to serve prerendered pages directly. Previously, we relied on frameworks like Express for this functionality, which resulted in inconsistent redirects for directories where in some cases a trailing slash was added to the route.

**Note:** This change applies only when using the new SSR APIs. When using the `CommonEngine`, a 3rd party static serve middleware is still required.
2024-11-01 10:45:05 +01:00

199 lines
7.3 KiB
TypeScript

/**
* @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.dev/license
*/
// The compiler is needed as tests are in JIT.
/* eslint-disable import/no-unassigned-import */
import '@angular/compiler';
/* eslint-enable import/no-unassigned-import */
import { Component } from '@angular/core';
import { AngularServerApp, destroyAngularServerApp } from '../src/app';
import { RenderMode } from '../src/routes/route-config';
import { setAngularAppTestingManifest } from './testing-utils';
describe('AngularServerApp', () => {
let app: AngularServerApp;
beforeAll(() => {
destroyAngularServerApp();
@Component({
standalone: true,
selector: 'app-home',
template: `Home works`,
})
class HomeComponent {}
setAngularAppTestingManifest(
[
{ path: 'home', component: HomeComponent },
{ path: 'home-csr', component: HomeComponent },
{ path: 'home-ssg', component: HomeComponent },
{ path: 'page-with-headers', component: HomeComponent },
{ path: 'page-with-status', component: HomeComponent },
{ path: 'redirect', redirectTo: 'home' },
{ path: 'redirect/relative', redirectTo: 'home' },
{ path: 'redirect/absolute', redirectTo: '/home' },
],
[
{
path: 'home-csr',
renderMode: RenderMode.Client,
},
{
path: 'home-ssg',
renderMode: RenderMode.Prerender,
headers: {
'X-Some-Header': 'value',
},
},
{
path: 'page-with-status',
renderMode: RenderMode.Server,
status: 201,
},
{
path: 'page-with-headers',
renderMode: RenderMode.Server,
headers: {
'Cache-Control': 'no-cache',
'X-Some-Header': 'value',
},
},
{
path: '**',
renderMode: RenderMode.Server,
},
],
undefined,
{
'home-ssg/index.html': async () =>
`<html>
<head>
<title>SSG home page</title>
<base href="/" />
</head>
<body>
<app-root>Home SSG works</app-root>
</body>
</html>
`,
},
);
app = new AngularServerApp();
});
describe('handle', () => {
describe('CSR and SSG pages', () => {
it('should correctly render the content for the requested page', async () => {
const response = await app.handle(new Request('http://localhost/home'));
expect(await response?.text()).toContain('Home works');
});
it(`should correctly render the content when the URL ends with 'index.html'`, async () => {
const response = await app.handle(new Request('http://localhost/home/index.html'));
expect(await response?.text()).toContain('Home works');
});
it('should correctly handle top level redirects', async () => {
const response = await app.handle(new Request('http://localhost/redirect'));
expect(response?.headers.get('location')).toContain('http://localhost/home');
expect(response?.status).toBe(302);
});
it('should correctly handle relative nested redirects', async () => {
const response = await app.handle(new Request('http://localhost/redirect/relative'));
expect(response?.headers.get('location')).toContain('http://localhost/redirect/home');
expect(response?.status).toBe(302);
});
it('should correctly handle absolute nested redirects', async () => {
const response = await app.handle(new Request('http://localhost/redirect/absolute'));
expect(response?.headers.get('location')).toContain('http://localhost/home');
expect(response?.status).toBe(302);
});
it('should handle request abortion gracefully', async () => {
const controller = new AbortController();
const request = new Request('http://localhost/home', { signal: controller.signal });
// Schedule the abortion of the request in the next microtask
queueMicrotask(() => {
controller.abort();
});
await expectAsync(app.handle(request)).toBeRejectedWithError(/Request for: .+ was aborted/);
});
it('should return configured headers for pages with specific header settings', async () => {
const response = await app.handle(new Request('http://localhost/page-with-headers'));
const headers = response?.headers.entries() ?? [];
expect(Object.fromEntries(headers)).toEqual({
'cache-control': 'no-cache',
'x-some-header': 'value',
'content-type': 'text/html;charset=UTF-8',
});
});
it('should return only default headers for pages without specific header configurations', async () => {
const response = await app.handle(new Request('http://localhost/home'));
const headers = response?.headers.entries() ?? [];
expect(Object.fromEntries(headers)).toEqual({
'content-type': 'text/html;charset=UTF-8', // default header
});
});
it('should return the configured status for pages with specific status settings', async () => {
const response = await app.handle(new Request('http://localhost/page-with-status'));
expect(response?.status).toBe(201);
});
it('should return static `index.csr.html` for routes with CSR rendering mode', async () => {
const response = await app.handle(new Request('http://localhost/home-csr'));
const content = await response?.text();
expect(content).toContain('<title>CSR page</title>');
expect(content).not.toContain('ng-server-context');
});
it('should include `ng-server-context="ssr"` for SSR rendering mode', async () => {
const response = await app.handle(new Request('http://localhost/home'));
expect(await response?.text()).toContain('ng-server-context="ssr"');
});
});
describe('SSG pages', () => {
it('should correctly serve the content for the requested prerendered page', async () => {
const response = await app.handle(new Request('http://localhost/home-ssg'));
expect(await response?.text()).toContain('Home SSG works');
});
it(`should correctly serve the content for the requested prerendered page when the URL ends with 'index.html'`, async () => {
const response = await app.handle(new Request('http://localhost/home-ssg/index.html'));
expect(await response?.text()).toContain('Home SSG works');
});
it('should return configured headers for pages with specific header settings', async () => {
const response = await app.handle(new Request('http://localhost/home-ssg'));
const headers = response?.headers.entries() ?? [];
expect(Object.fromEntries(headers)).toEqual({
'cache-control': 'max-age=31536000',
'x-some-header': 'value',
'content-type': 'text/html;charset=UTF-8',
});
});
it('should return null for a non-prerendered page', async () => {
const response = await app.handle(new Request('http://localhost/unknown'));
expect(response).toBeNull();
});
});
});
});