Skip to content

Commit

Permalink
feat(server-renderer): move and adapt old SSR code (#135)
Browse files Browse the repository at this point in the history
Co-authored-by: Clemens Akens <clebert@me.com>
Co-authored-by: Mathis Wiehl <mail@mathiswiehl.de>
  • Loading branch information
3 people authored Jan 8, 2019
1 parent f93e904 commit b33f744
Show file tree
Hide file tree
Showing 9 changed files with 503 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,16 @@ describe('HistoryService#create (on Node.js)', () => {
consoleWarnSpy.mockImplementation(jest.fn());

createHistoryServiceBinder = (serverRequest: ServerRequest | undefined) => {
const mockServerRenderer: ServerRendererV1 = {serverRequest};

const mockFeatureServices = {
's2:server-renderer': mockServerRenderer
};
const mockServerRenderer: Partial<ServerRendererV1> = {serverRequest};

const mockEnv: FeatureServiceEnvironment<
undefined,
{'s2:server-renderer': ServerRendererV1}
> = {
config: undefined,
featureServices: mockFeatureServices
featureServices: {
's2:server-renderer': mockServerRenderer as ServerRendererV1
}
};

const sharedHistoryService = defineHistoryService(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,9 @@ import {
FeatureServiceBinding,
FeatureServiceEnvironment
} from '@feature-hub/core';
import {ServerRendererV1, ServerRequest} from '@feature-hub/server-renderer';
import {ServerRendererV1} from '@feature-hub/server-renderer';
import {History} from 'history';
import {
HistoryServiceV1,
RootLocationTransformer,
SharedHistoryService,
defineHistoryService
} from '..';
import {HistoryServiceV1, SharedHistoryService, defineHistoryService} from '..';
import {testRootLocationTransformer} from '../internal/test-root-location-transformer';

const simulateOnPopState = (state: unknown, url: string) => {
Expand All @@ -37,10 +32,9 @@ describe('defineHistoryService', () => {
});

describe('#create', () => {
let createHistoryServiceBinder: (
serverRequest?: ServerRequest,
rootLocationTransformer?: RootLocationTransformer
) => FeatureServiceBinder<HistoryServiceV1>;
let createHistoryServiceBinder: () => FeatureServiceBinder<
HistoryServiceV1
>;

let pushStateSpy: jest.SpyInstance;
let replaceStateSpy: jest.SpyInstance;
Expand All @@ -55,19 +49,21 @@ describe('defineHistoryService', () => {
consoleWarnSpy = jest.spyOn(console, 'warn');
consoleWarnSpy.mockImplementation(jest.fn());

const mockServerRenderer: Partial<ServerRendererV1> = {
serverRequest: {
path: '/example',
cookies: {},
headers: {}
}
};

const mockEnv: FeatureServiceEnvironment<
undefined,
{'s2:server-renderer': ServerRendererV1}
> = {
config: undefined,
featureServices: {
's2:server-renderer': {
serverRequest: {
path: '/example',
cookies: {},
headers: {}
}
}
's2:server-renderer': mockServerRenderer as ServerRendererV1
}
};

Expand Down
275 changes: 258 additions & 17 deletions packages/server-renderer/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,20 @@ import {
FeatureServiceBinder,
FeatureServiceProviderDefinition
} from '@feature-hub/core';
import {ServerRendererV1, defineServerRenderer} from '..';
import {ServerRendererV1, ServerRequest, defineServerRenderer} from '..';
import {useFakeTimers} from './use-fake-timers';

describe('defineServerRenderer', () => {
let mockEnv: FeatureAppEnvironment<undefined, {}>;
let serverRendererDefinition: FeatureServiceProviderDefinition;
let serverRequest: ServerRequest;

beforeEach(() => {
jest.useFakeTimers();

mockEnv = {config: undefined, featureServices: {}, idSpecifier: undefined};

const serverRequest = {
serverRequest = {
path: '/app',
cookies: {},
headers: {}
Expand All @@ -21,6 +25,10 @@ describe('defineServerRenderer', () => {
serverRendererDefinition = defineServerRenderer(serverRequest);
});

afterEach(() => {
jest.useRealTimers();
});

it('creates a server renderer definition', () => {
expect(serverRendererDefinition.id).toBe('s2:server-renderer');
expect(serverRendererDefinition.dependencies).toBeUndefined();
Expand All @@ -32,29 +40,262 @@ describe('defineServerRenderer', () => {

expect(sharedServerRenderer['1.0']).toBeDefined();
});

for (const invalidConfig of [null, {rerenderWait: false}]) {
describe(`with an invalid config ${JSON.stringify(
invalidConfig
)}`, () => {
it('throws an error', () => {
expect(() =>
serverRendererDefinition.create({
featureServices: {},
config: invalidConfig
})
).toThrowError(new Error('The ServerRenderer config is invalid.'));
});
});
}
});

describe('ServerRendererV1', () => {
let serverRendererBinder: FeatureServiceBinder<ServerRendererV1>;

beforeEach(() => {
serverRendererBinder = serverRendererDefinition.create(mockEnv)[
'1.0'
] as FeatureServiceBinder<ServerRendererV1>;
});

it('exposes a serverRequest', () => {
const serverRequest = {
path: '/app',
cookies: {
hallo: 'world'
},
headers: {
'content-type': 'application/json'
}
const serverRenderer = serverRendererBinder('test:1').featureService;

expect(serverRenderer.serverRequest).toEqual(serverRequest);
});

describe('rendering', () => {
const createServerRendererConsumer = (
consumerUid: string,
rerenderWait = 0
) => {
const serverRenderer = serverRendererBinder(consumerUid).featureService;

let firstRender = true;
let completed = false;

const render = () => {
if (firstRender) {
firstRender = false;
serverRenderer.register(() => completed);

setTimeout(async () => {
completed = true;

await serverRenderer.rerender();
}, rerenderWait);
}
};

return {render};
};

serverRendererDefinition = defineServerRenderer(serverRequest);
describe('with an integrator as the only consumer', () => {
it('resolves with the result of the given render function after the first render pass', async () => {
const serverRenderer = serverRendererBinder('test').featureService;
const mockRender = jest.fn(() => 'testHtml');
const html = await serverRenderer.renderUntilCompleted(mockRender);

const serverRendererBinder = serverRendererDefinition.create(mockEnv)[
'1.0'
] as FeatureServiceBinder<ServerRendererV1>;
expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(1);
});
});

describe('with an integrator, and a consumer that is completed in the first render pass', () => {
it('resolves with an html string after the first render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
).featureService;

const serverRendererConsumer = serverRendererBinder('test:consumer')
.featureService;

const mockRender = jest.fn(() => {
serverRendererConsumer.register(() => true);

return 'testHtml';
});

const html = await useFakeTimers(async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender)
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(1);
});
});

describe('with an integrator, and a consumer that is completed after triggering a rerender', () => {
it('resolves with an html string after the second render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
).featureService;

const serverRendererConsumer = createServerRendererConsumer(
'test:consumer'
);

const mockRender = jest.fn(() => {
serverRendererConsumer.render();

return 'testHtml';
});

const html = await useFakeTimers(async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender)
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(2);
});
});

describe('with an integrator, and two consumers that are completed after both triggered a rerender within "rerenderWait" milliseconds', () => {
it('resolves with an html string after the second render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
).featureService;

const serverRendererConsumer1 = createServerRendererConsumer(
'test:consumer:1',
50
);

const serverRendererConsumer2 = createServerRendererConsumer(
'test:consumer:2',
100
);

const mockRender = jest.fn(() => {
serverRendererConsumer1.render();
serverRendererConsumer2.render();

return 'testHtml';
});

const html = await useFakeTimers(
async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender),
150
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(2);
});
});

describe('with an integrator, and two consumers that are completed after triggering a rerender more than "rerenderWait" milliseconds apart from one another', () => {
describe('and the default "rerenderWait"', () => {
it('resolves with an html string after the third render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
).featureService;

const serverRendererConsumer1 = createServerRendererConsumer(
'test:consumer:1',
50
);

const serverRendererConsumer2 = createServerRendererConsumer(
'test:consumer:2',
101
);

const mockRender = jest.fn(() => {
serverRendererConsumer1.render();
serverRendererConsumer2.render();

return 'testHtml';
});

const html = await useFakeTimers(
async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender),
151
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(3);
});
});

describe('and a custom, higher rerenderWait', () => {
beforeEach(() => {
serverRendererBinder = serverRendererDefinition.create({
config: {rerenderWait: 51},
featureServices: {}
})['1.0'] as FeatureServiceBinder<ServerRendererV1>;
});

it('resolves with an html string after the second render pass', async () => {
const serverRendererIntegrator = serverRendererBinder(
'test:integrator'
).featureService;

const serverRendererConsumer1 = createServerRendererConsumer(
'test:consumer:1',
50
);

const serverRendererConsumer2 = createServerRendererConsumer(
'test:consumer:2',
101
);

const mockRender = jest.fn(() => {
serverRendererConsumer1.render();
serverRendererConsumer2.render();

return 'testHtml';
});

const html = await useFakeTimers(
async () =>
serverRendererIntegrator.renderUntilCompleted(mockRender),
152
);

expect(html).toEqual('testHtml');
expect(mockRender).toHaveBeenCalledTimes(2);
});
});
});

describe('when the given render function throws an error', () => {
it('rejects with the error', async () => {
const serverRenderer = serverRendererBinder('test').featureService;
const mockError = new Error('Failed to render.');

const mockRender = jest.fn(() => {
throw mockError;
});

return expect(
serverRenderer.renderUntilCompleted(mockRender)
).rejects.toEqual(mockError);
});
});

describe('when renderUntilCompleted is called multiple times', () => {
it('rejects with an error', async () => {
const serverRenderer = serverRendererBinder('test').featureService;
const mockRender = jest.fn(() => 'testHtml');

await serverRenderer.renderUntilCompleted(mockRender);

expect(
serverRendererBinder('test:1').featureService.serverRequest
).toEqual(serverRequest);
return expect(
serverRenderer.renderUntilCompleted(mockRender)
).rejects.toEqual(new Error('Rendering has already been started.'));
});
});
});
});
});
Loading

0 comments on commit b33f744

Please sign in to comment.