Skip to content

Commit

Permalink
chore: add SSR tests (test command) (#23457)
Browse files Browse the repository at this point in the history
* chore: add SSR tests (test command)

* use console.warn & console.error

* address review comments

* use waitForSelector

* inline the script

* use ts-node directly

* avoid mocks in buildAssets.test.ts

* Update apps/ssr-tests-v9/src/test.ts

Co-authored-by: Martin Hochel <[email protected]>

* Update apps/ssr-tests-v9/src/test.ts

Co-authored-by: Martin Hochel <[email protected]>

* Update apps/ssr-tests-v9/src/test.ts

Co-authored-by: Martin Hochel <[email protected]>

* close browser always

* add name to the error

* fix versions

* fix versions

Co-authored-by: Martin Hochel <[email protected]>
  • Loading branch information
layershifter and Hotell authored Jun 23, 2022
1 parent c76e61e commit 4405e16
Show file tree
Hide file tree
Showing 13 changed files with 176 additions and 8 deletions.
23 changes: 23 additions & 0 deletions apps/ssr-tests-v9/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,26 @@ flowchart TB
b22-->b3
end
```

### `test`

Uses assets from the `build` step and runs them in a real browser to ensure that there are no errors in console related to SSR.

```shell
# yarn test
```

```mermaid
flowchart TB
subgraph Test
t1(Open a browser)
t2(Open a page and run JS)
t3(Ensure that console is empty)
t1-->t2
t2-->t3
end
```

#### Debugging

All assets are available in `./dist` folder that is available once `build` have been run. You can open `./dist/index.html` in any browser and debug relevant issues.
4 changes: 2 additions & 2 deletions apps/ssr-tests-v9/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
},
"license": "MIT",
"scripts": {
"build": "node -r @fluentui/scripts/ts-node-register ./src/build.ts",
"build": "ts-node --transpile-only ./src/build.ts",
"clean": "just-scripts clean",
"code-style": "just-scripts code-style",
"lint": "just-scripts lint",
"test": "jest --passWithNoTests",
"test": "jest && ts-node --transpile-only ./src/test.ts",
"type-check": "tsc -b tsconfig.json"
},
"dependencies": {
Expand Down
4 changes: 4 additions & 0 deletions apps/ssr-tests-v9/src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { buildAssets } from './utils/buildAssets';
import { generateEntryPoints } from './utils/generateEntryPoints';
import { hrToSeconds } from './utils/helpers';
import { renderToHTML } from './utils/renderToHTML';
import { getChromeVersion } from './utils/getChromeVersion';

async function build() {
const distDirectory = path.resolve(__dirname, '..', 'dist');
Expand Down Expand Up @@ -41,9 +42,12 @@ async function build() {

// ---

const chromeVersion = await getChromeVersion();
const buildStartTime = process.hrtime();

await buildAssets({
chromeVersion,

esmEntryPoint,
cjsEntryPoint,

Expand Down
101 changes: 101 additions & 0 deletions apps/ssr-tests-v9/src/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import * as chalk from 'chalk';
import * as fs from 'fs';
import * as path from 'path';
import type { Browser } from 'puppeteer';

import { PROVIDER_ID } from './utils/constants';
import { hrToSeconds } from './utils/helpers';
import { launchBrowser } from './utils/launchBrowser';

class RenderError extends Error {
public name = 'RangeError';
}

export async function runTest(browser: Browser, url: string): Promise<void> {
const page = await browser.newPage();
await page.setRequestInterception(true);

let error: Error | undefined;

page.on('console', message => {
if (message.type() === 'error') {
// Ignoring network errors as we have an interceptor that prevents loading everything except our JS bundle
if (!message.text().includes('net::ERR_FAILED')) {
error = new RenderError(message.text());
}
}
});

page.on('request', request => {
// Our interceptor allows only our HTML and JS output
if (request.url() === url || request.url().endsWith('/out-esm.js')) {
return request.continue();
}

return request.abort();
});

page.on('pageerror', err => {
error = err;
});

await page.goto(url);
await page.waitForSelector(`#${PROVIDER_ID}`);

await page.close();

if (error) {
throw error;
}
}

async function test(): Promise<void> {
const startTime = process.hrtime();
console.log('Starting a browser...');

let browser: Browser | undefined;

try {
browser = await launchBrowser();
console.log('Using', await browser.version());

const htmlPath = path.resolve(__dirname, '..', 'dist', 'index.html');

if (!fs.existsSync(htmlPath)) {
throw new Error('"dist/index.html" does not exist, please run "yarn build" first');
}

const url = `file://${htmlPath}`;
console.log(`Using "${url}"`);

await runTest(browser, url);
console.log(`Test finished successfully in ${hrToSeconds(process.hrtime(startTime))}`);
} finally {
if (browser) {
await browser.close();
}
}
}

test().catch((err: Error) => {
console.error('');
console.error(chalk.bgRed.whiteBright(' @fluentui/ssr-tests-v9 '));

if (err instanceof RenderError) {
console.error(
[
' The test failed.',
'Please use `$ npx serve dist` or `$ open dist/index.html` to open a HTML page that is used in tests.',
].join(' '),
);
console.error(' The reference error is below, you will see it in Devtools on the opened page.');
console.error('');
} else {
console.error(' The test failed, the error below contains relevant information.');
console.error('');
}

console.error(err);

process.exit(1);
});
2 changes: 1 addition & 1 deletion apps/ssr-tests-v9/src/utils/buildAssets.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe('buildAssets', () => {
await fs.promises.writeFile(cjsEntryPoint, template);
await fs.promises.writeFile(esmEntryPoint, template);

await buildAssets({ cjsEntryPoint, cjsOutfile, esmEntryPoint, esmOutfile });
await buildAssets({ chromeVersion: 100, cjsEntryPoint, cjsOutfile, esmEntryPoint, esmOutfile });

const cjsContent = stripComments(await fs.promises.readFile(cjsOutfile, { encoding: 'utf8' }));
const esmContent = stripComments(await fs.promises.readFile(esmOutfile, { encoding: 'utf8' }));
Expand Down
6 changes: 4 additions & 2 deletions apps/ssr-tests-v9/src/utils/buildAssets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ type BuildConfig = {
cjsOutfile: string;
esmEntryPoint: string;
esmOutfile: string;

chromeVersion: number;
};

export async function buildAssets(config: BuildConfig): Promise<void> {
const { cjsEntryPoint, cjsOutfile, esmEntryPoint, esmOutfile } = config;
const { chromeVersion, cjsEntryPoint, cjsOutfile, esmEntryPoint, esmOutfile } = config;

try {
// Used for SSR rendering, see renderToHTML.js
Expand Down Expand Up @@ -47,7 +49,7 @@ export async function buildAssets(config: BuildConfig): Promise<void> {
require.resolve('../shims/module'),
],
format: 'iife',
target: 'chrome101',
target: `chrome${chromeVersion}`,
});
} catch (e) {
throw new Error(
Expand Down
1 change: 1 addition & 0 deletions apps/ssr-tests-v9/src/utils/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const PROVIDER_ID = 'root-provider';
2 changes: 1 addition & 1 deletion apps/ssr-tests-v9/src/utils/generateEntryPoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('generateEntryPoints', () => {
export const App = () => (
<SSRProvider>
<FluentProvider theme={teamsLightTheme}>
<FluentProvider id=\\"root-provider\\" theme={teamsLightTheme}>
<ModuleDefault />
<ModuleFoo />
</FluentProvider>
Expand Down
3 changes: 2 additions & 1 deletion apps/ssr-tests-v9/src/utils/generateEntryPoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as prettier from 'prettier';

import { PROVIDER_ID } from './constants';
import { getImportsFromIndexFile } from './getImportsFromIndexFile';

type GenerateEntryPointsConfig = {
Expand Down Expand Up @@ -50,7 +51,7 @@ export async function generateEntryPoints(config: GenerateEntryPointsConfig): Pr
export const App = () => (
<SSRProvider>
<FluentProvider theme={teamsLightTheme}>
<FluentProvider id="${PROVIDER_ID}" theme={teamsLightTheme}>
${imports.map(entry => `<${entry.imported} />`).join('\n')}
</FluentProvider>
</SSRProvider>
Expand Down
13 changes: 13 additions & 0 deletions apps/ssr-tests-v9/src/utils/getChromeVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { launchBrowser } from './launchBrowser';

export async function getChromeVersion(): Promise<number> {
const browser = await launchBrowser();

// includes browser name, example: HeadlessChrome/103.0.5058.0
const rawVersion = await browser.version();
const version = rawVersion.split('/')[1].split('.')[0];

await browser.close();

return parseInt(version, 10);
}
23 changes: 23 additions & 0 deletions apps/ssr-tests-v9/src/utils/launchBrowser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Browser, launch } from 'puppeteer';

export async function launchBrowser(): Promise<Browser> {
let browser;
let attempt = 1;

while (!browser) {
try {
browser = await launch();
} catch (err) {
if (attempt === 5) {
console.error(`Failed to launch a browser after 5 attempts...`);
throw err;
}

console.warn('A browser failed to start, retrying...');
console.warn(err);
attempt++;
}
}

return browser;
}
1 change: 1 addition & 0 deletions apps/ssr-tests-v9/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"target": "ES2019",
"noEmit": true,
"isolatedModules": true,
Expand Down
1 change: 0 additions & 1 deletion apps/ssr-tests-v9/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "dist",
"types": ["jest", "node"]
},
Expand Down

0 comments on commit 4405e16

Please sign in to comment.