diff --git a/core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts b/core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts index 26fb3c402cd..ffab135f4c6 100644 --- a/core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts +++ b/core-libs/setup/ssr/optimized-engine/optimized-ssr-engine.spec.ts @@ -36,14 +36,14 @@ class MockExpressServerLogger implements Partial { * Usage: * 1. Instantiate the class with desired options * 2. Call request() to run request through engine - * 3. Examine renders property for the renders + * 3. Examine responses property */ class TestEngineRunner { /** Accumulates html output for engine runs */ - renders: (string | Error)[] = []; + responses: (string | Error)[] = []; - /** Accumulates response parameters for engine runs */ - responseParams: object[] = []; + /** Accumulates responses' headers for engine runs */ + responsesHeaders: object[] = []; renderCount = 0; optimizedSsrEngine: OptimizedSsrEngine; @@ -86,7 +86,7 @@ class TestEngineRunner { return new TestEngineRunner(options, renderTime, { withError: true }); } - /** Run request against the engine. The result will be stored in rendering property. */ + /** Run request against the engine. The result will be stored in `responses` property. */ request( url: string, params?: { @@ -94,8 +94,8 @@ class TestEngineRunner { httpHeaders?: IncomingHttpHeaders; } ): TestEngineRunner { - const response: { [key: string]: string } = {}; - const headers = params?.httpHeaders ?? { host }; + const responseHeaders: { [key: string]: string } = {}; + const requestHeaders = params?.httpHeaders ?? { host }; /** used when resolving getRequestUrl() and getRequestOrigin() */ const app = >{ get: @@ -108,22 +108,22 @@ class TestEngineRunner { req: >{ protocol: 'https', originalUrl: url, - headers, + headers: requestHeaders, get: (header: string): string | string[] | null | undefined => { - return headers[header]; + return requestHeaders[header]; }, app, connection: >{}, res: >{ - set: (key: string, value: any) => (response[key] = value), + set: (key: string, value: any) => (responseHeaders[key] = value), locals: {}, }, }, }; this.engineInstance(url, optionsMock, (error, html): void => { - this.renders.push(html ?? error ?? ''); - this.responseParams.push(response); + this.responses.push(html ?? error ?? ''); + this.responsesHeaders.push(responseHeaders); }); return this; @@ -208,12 +208,12 @@ describe('OptimizedSsrEngine', () => { }); describe('rendering', () => { - it('should return rendered output if no errors', fakeAsync(() => { + it('should return rendered HTML if no errors', fakeAsync(() => { const originalUrl = 'a'; const engineRunner = new TestEngineRunner({}).request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0']); + expect(engineRunner.responses).toEqual(['a-0']); expect(consoleLogSpy).toHaveBeenCalledWith( expect.stringContaining( `Request is resolved with the SSR rendering result (${originalUrl})` @@ -226,7 +226,7 @@ describe('OptimizedSsrEngine', () => { const engineRunner = TestEngineRunner.withError({}).request('a'); tick(200); - expect(engineRunner.renders).toEqual([new Error('a-0')]); + expect(engineRunner.responses).toEqual([new Error('a-0')]); expect(consoleErrorSpy).toHaveBeenCalledWith( expect.stringContaining( `Request is resolved with the SSR rendering error (${originalUrl})` @@ -260,39 +260,39 @@ describe('OptimizedSsrEngine', () => { }); describe('timeout option', () => { - it('should fallback to CSR if rendering exceeds timeout', fakeAsync(() => { + it('should fallback to CSR if rendering exceeds a request timeout', fakeAsync(() => { const engineRunner = new TestEngineRunner({ timeout: 50 }).request('a'); tick(200); - expect(engineRunner.renders).toEqual(['']); + expect(engineRunner.responses).toEqual(['']); })); - it('should return timed out render in the followup request', fakeAsync(() => { + it('should reuse HTML meant for a previous timeouted request, if the new request is for the same url', fakeAsync(() => { const engineRunner = new TestEngineRunner({ timeout: 50 }).request('a'); tick(200); - expect(engineRunner.renders).toEqual(['']); + expect(engineRunner.responses).toEqual(['']); engineRunner.request('a'); - expect(engineRunner.renders[1]).toEqual('a-0'); + expect(engineRunner.responses[1]).toEqual('a-0'); })); - it('should return render if rendering meets timeout', fakeAsync(() => { + it('should return HTML if rendering meets timeout', fakeAsync(() => { const engineRunner = new TestEngineRunner({ timeout: 150 }).request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0']); + expect(engineRunner.responses).toEqual(['a-0']); })); it('should fallback instantly if is set to 0', () => { const engineRunner = new TestEngineRunner({ timeout: 0 }).request('a'); - expect(engineRunner.renders).toEqual(['']); + expect(engineRunner.responses).toEqual(['']); }); - it('should return timed out render in the followup request, also when timeout is set to 0', fakeAsync(() => { + it('should return HTML meant for a previous timeouted request if the new request is for the same url, even if `timeout` is configured to 0', fakeAsync(() => { const engineRunner = new TestEngineRunner({ timeout: 0 }).request('a'); - expect(engineRunner.renders).toEqual(['']); + expect(engineRunner.responses).toEqual(['']); expect(getCurrentConcurrency(engineRunner)).toEqual({ currentConcurrency: 1, }); @@ -303,37 +303,38 @@ describe('OptimizedSsrEngine', () => { }); engineRunner.request('a'); - expect(engineRunner.renders[1]).toEqual('a-0'); + expect(engineRunner.responses[1]).toEqual('a-0'); expect(getCurrentConcurrency(engineRunner)).toEqual({ currentConcurrency: 0, }); })); }); - describe('no-store cache control header', () => { - it('should be applied for a fallback', () => { + describe('no-store cache control header in response', () => { + it('should be applied if a request times out ', () => { const engineRunner = new TestEngineRunner({ timeout: 0 }).request('a'); - expect(engineRunner.renders).toEqual(['']); - expect(engineRunner.responseParams).toEqual([ + expect(engineRunner.responses).toEqual(['']); + expect(engineRunner.responsesHeaders).toEqual([ { 'Cache-Control': 'no-store' }, ]); }); - it('should not be applied for a render within time limit', fakeAsync(() => { + it('should not be applied if rendering finishes before request times out', fakeAsync(() => { const engineRunner = new TestEngineRunner({ timeout: 200 }).request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0']); - expect(engineRunner.responseParams).toEqual([{}]); + expect(engineRunner.responses).toEqual(['a-0']); + expect(engineRunner.responsesHeaders).toEqual([{}]); })); - it('should not be applied for a render served with next response', fakeAsync(() => { + + it('should not be applied for subsequent requests to the same url, when reusing a HTML meant for a previous timed out request', fakeAsync(() => { const engineRunner = new TestEngineRunner({ timeout: 50 }).request('a'); tick(200); engineRunner.request('a'); - expect(engineRunner.renders).toEqual(['', 'a-0']); - expect(engineRunner.responseParams).toEqual([ + expect(engineRunner.responses).toEqual(['', 'a-0']); + expect(engineRunner.responsesHeaders).toEqual([ { 'Cache-Control': 'no-store' }, {}, ]); @@ -341,7 +342,7 @@ describe('OptimizedSsrEngine', () => { }); describe('cache option', () => { - it('should not cache requests if disabled', fakeAsync(() => { + it('should not cache HTML if `cache` is disabled', fakeAsync(() => { const engineRunner = new TestEngineRunner({ cache: false, timeout: 200, @@ -352,9 +353,10 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0', 'a-1', 'a-2']); + expect(engineRunner.responses).toEqual(['a-0', 'a-1', 'a-2']); })); - it('should cache requests if enabled', fakeAsync(() => { + + it('should cache HTML if `cache` is enabled', fakeAsync(() => { const engineRunner = new TestEngineRunner({ cache: true, timeout: 200, @@ -379,7 +381,7 @@ describe('OptimizedSsrEngine', () => { tick(200); - expect(engineRunner.renders).toEqual(['a-0', 'a-0', 'a-0']); + expect(engineRunner.responses).toEqual(['a-0', 'a-0', 'a-0']); })); }); @@ -398,7 +400,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ new Error('a-0'), new Error('a-1'), new Error('a-2'), @@ -418,7 +420,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ new Error('a-0'), new Error('a-0'), new Error('a-0'), @@ -438,7 +440,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0', 'a-0', 'a-0']); + expect(engineRunner.responses).toEqual(['a-0', 'a-0', 'a-0']); })); it('should cache HTML if `avoidCachingErrors` is set to false', fakeAsync(() => { @@ -454,7 +456,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0', 'a-0', 'a-0']); + expect(engineRunner.responses).toEqual(['a-0', 'a-0', 'a-0']); })); }); }); @@ -471,7 +473,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ new Error('a-0'), new Error('a-1'), new Error('a-2'), @@ -489,7 +491,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ new Error('a-0'), new Error('a-0'), new Error('a-0'), @@ -507,7 +509,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0', 'a-1', 'a-2']); + expect(engineRunner.responses).toEqual(['a-0', 'a-1', 'a-2']); })); it('should cache HTML if `shouldCacheRenderingResult` returns true', fakeAsync(() => { @@ -521,7 +523,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0', 'a-0', 'a-0']); + expect(engineRunner.responses).toEqual(['a-0', 'a-0', 'a-0']); })); }); @@ -538,7 +540,7 @@ describe('OptimizedSsrEngine', () => { .request('e'); tick(200); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ '', // CSR fallback for 'd' '', // CSR fallback for 'e' 'a-0', @@ -561,7 +563,7 @@ describe('OptimizedSsrEngine', () => { engineRunner.request('f').request('g'); tick(200); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ '', // CSR fallback for 'c' 'a-0', '', // CSR fallback for 'e' @@ -574,7 +576,7 @@ describe('OptimizedSsrEngine', () => { }); describe('ttl option', () => { - it('should invalidate expired renders', fakeAsync(() => { + it('should invalidate expired cache entries', fakeAsync(() => { let currentDate = 100; jest.spyOn(Date, 'now').mockImplementation(() => currentDate); @@ -593,10 +595,10 @@ describe('OptimizedSsrEngine', () => { engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0', 'a-0', 'a-1']); + expect(engineRunner.responses).toEqual(['a-0', 'a-0', 'a-1']); })); - it('should not invalidate renders if ttl is not defined', fakeAsync(() => { + it('should not invalidate cache entries if `ttl` is not defined', fakeAsync(() => { let currentDate = 100; jest.spyOn(Date, 'now').mockImplementation(() => currentDate); @@ -614,7 +616,7 @@ describe('OptimizedSsrEngine', () => { engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0', 'a-0', 'a-0']); + expect(engineRunner.responses).toEqual(['a-0', 'a-0', 'a-0']); })); }); @@ -679,7 +681,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('elu'); tick(200); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ 'ala-0', 'ala-0', 'ela-1', @@ -699,7 +701,7 @@ describe('OptimizedSsrEngine', () => { }).request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0']); + expect(engineRunner.responses).toEqual(['a-0']); })); it('should ignore timeout also when it is set to 0', fakeAsync(() => { @@ -712,10 +714,10 @@ describe('OptimizedSsrEngine', () => { engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual(['a-0']); + expect(engineRunner.responses).toEqual(['a-0']); })); - it('when reuseCurrentRendering is false, it should render each request separately, even if there is already a pending render for the same rendering key', fakeAsync(() => { + it('when reuseCurrentRendering is false, it should render for each request separately, even if there is already a pending render for the same rendering key', fakeAsync(() => { const engineRunner = new TestEngineRunner({ renderingStrategyResolver: () => RenderingStrategy.ALWAYS_SSR, timeout: 200, @@ -736,10 +738,10 @@ describe('OptimizedSsrEngine', () => { expect(getCurrentConcurrency(engineRunner)).toEqual({ currentConcurrency: 2, }); - expect(engineRunner.renders).toEqual([]); + expect(engineRunner.responses).toEqual([]); tick(100); - expect(engineRunner.renders).toEqual(['a-0', 'a-1']); + expect(engineRunner.responses).toEqual(['a-0', 'a-1']); expect( engineRunner.optimizedSsrEngine['expressEngine'] ).toHaveBeenCalledTimes(2); @@ -762,7 +764,7 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); tick(200); - expect(engineRunner.renders).toEqual(['', '']); + expect(engineRunner.responses).toEqual(['', '']); })); it('should not start the actual render in the background', fakeAsync(() => { @@ -777,7 +779,7 @@ describe('OptimizedSsrEngine', () => { ); engineRunner.request('a'); - expect(engineRunner.renders).toEqual(['']); + expect(engineRunner.responses).toEqual(['']); expect( engineRunner.optimizedSsrEngine['expressEngine'] @@ -796,10 +798,10 @@ describe('OptimizedSsrEngine', () => { tick(200); engineRunner.request('a'); - expect(engineRunner.renders).toEqual(['', 'a-0']); + expect(engineRunner.responses).toEqual(['', 'a-0']); })); - it('when reuseCurrentRendering is false, it should fallback to CSR when there is already pending a render for the same rendering key', fakeAsync(() => { + it('when reuseCurrentRendering is false, it should fallback to CSR when there is already a pending render for the same rendering key', fakeAsync(() => { const engineRunner = new TestEngineRunner({ renderingStrategyResolver: () => RenderingStrategy.DEFAULT, timeout: 200, @@ -815,10 +817,10 @@ describe('OptimizedSsrEngine', () => { currentConcurrency: 1, }); - expect(engineRunner.renders).toEqual(['']); // immediate fallback to CSR for the 2nd request for the same key + expect(engineRunner.responses).toEqual(['']); // immediate fallback to CSR for the 2nd request for the same key tick(100); - expect(engineRunner.renders).toEqual(['', 'a-0']); + expect(engineRunner.responses).toEqual(['', 'a-0']); expect(getCurrentConcurrency(engineRunner)).toEqual({ currentConcurrency: 0, }); @@ -839,13 +841,13 @@ describe('OptimizedSsrEngine', () => { engineRunner.request('a', { httpHeaders: { 'User-Agent': 'bot' } }); tick(200); - expect(engineRunner.renders).toEqual(['', 'a-1']); + expect(engineRunner.responses).toEqual(['', 'a-1']); })); }); }); describe('forcedSsrTimeout option', () => { - it('should fallback to CSR when forcedSsrTimeout timeout is exceeded for ALWAYS_SSR rendering strategy, and return the timed out render in the followup request', fakeAsync(() => { + it('should fallback to CSR when forcedSsrTimeout timeout is exceeded for ALWAYS_SSR rendering strategy, and after that rendering ends, its HTML should be reused in the next request for the same url', fakeAsync(() => { const engineRunner = new TestEngineRunner({ renderingStrategyResolver: () => RenderingStrategy.ALWAYS_SSR, timeout: 50, @@ -858,15 +860,15 @@ describe('OptimizedSsrEngine', () => { }); tick(60); - expect(engineRunner.renders).toEqual([]); + expect(engineRunner.responses).toEqual([]); tick(50); - expect(engineRunner.renders).toEqual(['']); + expect(engineRunner.responses).toEqual(['']); expect(getCurrentConcurrency(engineRunner)).toEqual({ currentConcurrency: 0, }); engineRunner.request('a'); - expect(engineRunner.renders).toEqual(['', 'a-0']); + expect(engineRunner.responses).toEqual(['', 'a-0']); expect(getCurrentConcurrency(engineRunner)).toEqual({ currentConcurrency: 0, }); @@ -881,11 +883,11 @@ describe('OptimizedSsrEngine', () => { engineRunner.request('a'); tick(60); - expect(engineRunner.renders).toEqual(['']); + expect(engineRunner.responses).toEqual(['']); tick(50); engineRunner.request('a'); - expect(engineRunner.renders).toEqual(['', 'a-0']); + expect(engineRunner.responses).toEqual(['', 'a-0']); })); }); describe('maxRenderTime option', () => { @@ -1017,7 +1019,7 @@ describe('OptimizedSsrEngine', () => { renderTime ).request(requestUrl); jest.spyOn(engineRunner.optimizedSsrEngine as any, 'log'); - expect(engineRunner.renders).toEqual([]); + expect(engineRunner.responses).toEqual([]); tick(fiveMinutes + 101); expect(engineRunner.optimizedSsrEngine['log']).toHaveBeenCalledWith( @@ -1025,10 +1027,10 @@ describe('OptimizedSsrEngine', () => { false, { request: expect.objectContaining({ originalUrl: requestUrl }) } ); - expect(engineRunner.renders).toEqual(['']); + expect(engineRunner.responses).toEqual(['']); engineRunner.request(requestUrl); - expect(engineRunner.renders).toEqual(['']); // if the result was cached, the 2nd request would get immediately 'a-0' + expect(engineRunner.responses).toEqual(['']); // if the result was cached, the 2nd request would get immediately 'a-0' flush(); })); }); @@ -1083,14 +1085,14 @@ describe('OptimizedSsrEngine', () => { false, { request: expect.objectContaining({ originalUrl: requestUrl }) } ); - expect(engineRunner.renders).toEqual(['', '']); + expect(engineRunner.responses).toEqual(['', '']); flush(); })); }); describe('when enabled', () => { - describe('multiple subsequent requests for the same rendering key should reuse the same render', () => { + describe('multiple subsequent requests for the same rendering key should await for the result of the same pending rendering, and all get the same HTML response', () => { it('and the first request should timeout', fakeAsync(() => { const timeout = 300; const engineRunner = new TestEngineRunner( @@ -1113,7 +1115,7 @@ describe('OptimizedSsrEngine', () => { tick(100); expect(engineRunner.renderCount).toEqual(1); - expect(engineRunner.renders).toEqual(['', `${requestUrl}-0`]); + expect(engineRunner.responses).toEqual(['', `${requestUrl}-0`]); flush(); })); @@ -1151,7 +1153,7 @@ describe('OptimizedSsrEngine', () => { expect(renderExceedMessageCount).toBe(2); expect(engineRunner.renderCount).toEqual(0); - expect(engineRunner.renders).toEqual(['', '']); + expect(engineRunner.responses).toEqual(['', '']); flush(); })); @@ -1187,7 +1189,7 @@ describe('OptimizedSsrEngine', () => { tick(200); expect(engineRunner.renderCount).toEqual(1); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ `${requestUrl}-0`, `${requestUrl}-0`, ]); @@ -1237,7 +1239,7 @@ describe('OptimizedSsrEngine', () => { false, { request: expect.objectContaining({ originalUrl: requestUrl }) } ); - expect(engineRunner.renders).toEqual(['']); // the first request fallback to CSR due to timeout + expect(engineRunner.responses).toEqual(['']); // the first request fallback to CSR due to timeout expect(getCurrentConcurrency(engineRunner)).toEqual({ currentConcurrency: 1, }); // the render still continues in the background @@ -1245,7 +1247,7 @@ describe('OptimizedSsrEngine', () => { // eventually the render succeeds and 2 remaining requests get the same response: tick(100); expect(engineRunner.renderCount).toEqual(1); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ '', // CSR fallback of the 1st request due to it timed out `${requestUrl}-0`, `${requestUrl}-0`, @@ -1260,7 +1262,7 @@ describe('OptimizedSsrEngine', () => { flush(); })); - it('and concurrency limit should NOT fallback to CSR, when the request is for a pending render', fakeAsync(() => { + it('and concurrency limit should NOT fallback to CSR, when there is a pending rendering for the same rendering key', fakeAsync(() => { const engineRunner = new TestEngineRunner({ reuseCurrentRendering: true, timeout: 200, @@ -1277,7 +1279,7 @@ describe('OptimizedSsrEngine', () => { ).not.toHaveBeenCalledWith( `CSR fallback: Concurrency limit exceeded (1)` ); - expect(engineRunner.renders).toEqual(['a-0', 'a-0']); + expect(engineRunner.responses).toEqual(['a-0', 'a-0']); })); it('combined with a different request should take up two concurrency slots', fakeAsync(() => { @@ -1312,7 +1314,7 @@ describe('OptimizedSsrEngine', () => { }); tick(250); - expect(engineRunner.renders).toEqual([ + expect(engineRunner.responses).toEqual([ 'a-0', 'a-0', 'a-0', @@ -1429,7 +1431,7 @@ describe('OptimizedSsrEngine', () => { ); expect(engineRunner.renderCount).toEqual(1); - expect(engineRunner.renders).toEqual(['', '']); + expect(engineRunner.responses).toEqual(['', '']); flush(); }));