diff --git a/packages/devtools/src/__tests__/analog-load-inherit.test.ts b/packages/devtools/src/__tests__/analog-load-inherit.test.ts new file mode 100644 index 00000000..4ef2f292 --- /dev/null +++ b/packages/devtools/src/__tests__/analog-load-inherit.test.ts @@ -0,0 +1,109 @@ +// @vitest-environment jsdom +import '@angular/compiler'; +import { Component } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; +import { Router, RouterOutlet, provideRouter, type Route, type Routes } from '@angular/router'; +import { describe, expect, it } from 'vitest'; +import { ANALOG_META_DESCRIPTION, chainOf, collectAnalog } from '../analog-runtime.ts'; +import { scanAnalog } from '../rpc/analog-scan.ts'; +import { resolveAnalogReport } from '../rpc/analog-tools.ts'; +import { BASE_FILES, makeProject } from './analog-fixture.ts'; + +TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting()); + +const META = Symbol(ANALOG_META_DESCRIPTION); + +function analogRoute(endpoint: string, endpointKey: string, route: Route): Route { + return Object.assign(route, { [META]: { endpoint, endpointKey } }); +} + +class ProductsLayout {} +Component({ imports: [RouterOutlet], template: '' })(ProductsLayout); + +class ProductList {} +Component({ template: '

list

' })(ProductList); + +describe('Analog load() of a layout', () => { + it('stays with the layout when an empty-path child page inherits it', async () => { + const routes: Routes = [ + { + path: 'products', + children: [ + analogRoute('/pages/products', '/src/app/pages/products.server.ts', { + path: '', + component: ProductsLayout, + resolve: { load: () => ({ items: [1, 2] }) }, + children: [ + analogRoute('/pages/products/index', '/src/app/pages/products/index.server.ts', { + path: '', + component: ProductList, + }), + ], + }), + ], + }, + ]; + TestBed.configureTestingModule({ providers: [provideRouter(routes)] }); + const router = TestBed.inject(Router); + await router.navigateByUrl('/products'); + + const { chain, data, loadFrom } = chainOf( + router.routerState.snapshot.root as unknown as Record, + ); + expect(chain.map((c) => c.serverFile)).toEqual([ + '/src/app/pages/products.server.ts', + '/src/app/pages/products/index.server.ts', + ]); + expect(data).toEqual({ items: [1, 2] }); + // The list page has no .server.ts: its `data.load` is the layout's, + // inherited by Angular because its path is empty. + + document.body.innerHTML = ''; + const project = scanAnalog( + makeProject(BASE_FILES, { + 'src/app/pages/products.server.ts': 'export const load = async () => ({ items: [] });\n', + }), + ); + const report = collectAnalog( + { ɵgetRouterInstance: () => router, getInjector: () => null }, + 'p1', + [], + () => ({ hydrated: 0 }), + )!; + expect(report.load?.keys).toEqual(['items']); + expect(resolveAnalogReport(project, report).load?.keys).toEqual(['items']); + expect(loadFrom).toBe(0); + }); + + it('credits the child when it resolves its own load, even to the same object', async () => { + const cached = { items: [1, 2] }; + const routes: Routes = [ + { + path: 'products', + children: [ + analogRoute('/pages/products', '/src/app/pages/products.server.ts', { + path: '', + component: ProductsLayout, + resolve: { load: () => cached }, + children: [ + analogRoute('/pages/products/index', '/src/app/pages/products/index.server.ts', { + path: '', + component: ProductList, + resolve: { load: () => cached }, + }), + ], + }), + ], + }, + ]; + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ providers: [provideRouter(routes)] }); + const router = TestBed.inject(Router); + await router.navigateByUrl('/products'); + const { loadFrom } = chainOf( + router.routerState.snapshot.root as unknown as Record, + ); + expect(loadFrom).toBe(1); + }); +}); diff --git a/packages/devtools/src/analog-runtime.ts b/packages/devtools/src/analog-runtime.ts index b3c44094..545a10a5 100644 --- a/packages/devtools/src/analog-runtime.ts +++ b/packages/devtools/src/analog-runtime.ts @@ -108,7 +108,11 @@ export function chainOf(root: AnyRecord | null): { if (meta) { chain.push({ path: `/${segments.join('/')}`, ...fileOfEndpoint(meta.endpointKey) }); const load = read(() => node!['data']?.['load'], undefined); - if (load !== undefined && (loadFrom === undefined || !isEmptyObject(load))) { + // An empty-path child inherits its parent's resolved data, so the same + // object again is the parent's load(), unless this route resolves its own. + const own = read(() => !!config?.['resolve']?.['load'], false); + const inherited = load === data && !own; + if (load !== undefined && !inherited && (loadFrom === undefined || !isEmptyObject(load))) { data = load; loadFrom = chain.length - 1; }