/** * @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 */ import { APP_ID, Component, makeStateKey, NgModule, TransferState, ɵgetTransferState as getTransferState, Injector, inject, DOCUMENT, } from '@angular/core'; import {BrowserModule, withEventReplay, withIncrementalHydration} from '@angular/platform-browser'; import {renderModule, ServerModule} from '../index'; import {getHydrationInfoFromTransferState, ssr} from './hydration_utils'; import domino from '../third_party/domino/bundled-domino'; describe('transfer_state', () => { const defaultExpectedOutput = 'Works!'; it('adds transfer script tag when using renderModule', async () => { const STATE_KEY = makeStateKey('test'); @Component({ selector: 'app', template: 'Works!', standalone: false, }) class TransferComponent { constructor(private transferStore: TransferState) { this.transferStore.set(STATE_KEY, 10); } } @NgModule({ bootstrap: [TransferComponent], declarations: [TransferComponent], imports: [BrowserModule, ServerModule], }) class TransferStoreModule {} const output = await renderModule(TransferStoreModule, {document: ''}); expect(output).toBe(defaultExpectedOutput); }); it('cannot break out of ', ); }); it('adds transfer script tag when setting state during onSerialize', async () => { const STATE_KEY = makeStateKey('test'); @Component({ selector: 'app', template: 'Works!', standalone: false, }) class TransferComponent { constructor(private transferStore: TransferState) { this.transferStore.onSerialize(STATE_KEY, () => 10); } } @NgModule({ bootstrap: [TransferComponent], declarations: [TransferComponent], imports: [BrowserModule, ServerModule], }) class TransferStoreModule {} const output = await renderModule(TransferStoreModule, {document: ''}); expect(output).toBe(defaultExpectedOutput); }); describe('getTransferState', () => { it('ensures it only returns public info of the Transfer State', async () => { @Component({ selector: 'dep', template: ``, }) class Dep {} @Component({ selector: 'app', imports: [Dep], template: ` @defer (hydrate on interaction) { } `, }) class SimpleComponent { constructor() { // This is adds a data to the transfer state. inject(TransferState).set(makeStateKey('test'), 'testitest'); } } const hydrationFeatures = () => [withIncrementalHydration(), withEventReplay()]; const appId = 'custom-app-id'; const html = await ssr(SimpleComponent, { envProviders: [{provide: APP_ID, useValue: appId}], hydrationFeatures, }); const transferCacheJson = getHydrationInfoFromTransferState(html)!; // getTransferState reaches into the DOM to retrieve the transfer state. // So we need to set the document with the generated HTML. const {document} = domino.createWindow(html); const transferState = getTransferState( Injector.create({ providers: [ {provide: DOCUMENT, useValue: document}, {provide: APP_ID, useValue: appId}, ], }), ); // The transfer state also contains internal hydration keys, expect(Object.keys(transferState).length).not.toEqual(JSON.parse(transferCacheJson).length); // We only retrieve the public data from the transfer state. expect(Object.keys(transferState)).toEqual(['test']); }); }); });