Skip to content

Commit

Permalink
refactor(test): only start servers when needed
Browse files Browse the repository at this point in the history
Note that this change is also a workaround for:
microsoft/playwright#18209
  • Loading branch information
divdavem committed Mar 29, 2024
1 parent bb81899 commit 07cf987
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 81 deletions.
36 changes: 23 additions & 13 deletions e2e/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ export {expect} from '@playwright/test';

export type FixtureOptions = {
// with option: true
project?: string;
framework?: string;
sampleKey?: string;

// with option: false (default)
coverage: void;
sampleInfo?: Pick<SampleInfo, 'componentName' | 'sampleName' | 'style'> & {sampleURL: string};
sampleInfo?: Pick<SampleInfo, 'componentName' | 'sampleName' | 'style'>;
};

let cachedSamplesList: undefined | ReturnType<typeof getSamplesList>;
Expand All @@ -23,25 +24,34 @@ export const samplesList = () => {
return cachedSamplesList;
};

const serversManagerURL = process.env.SERVERS_MANAGER_URL!;

export const test = base.extend<FixtureOptions>({
project: [undefined, {option: true}],
framework: [undefined, {option: true}],
sampleKey: [undefined, {option: true}],
sampleInfo: async ({sampleKey, baseURL}, use) => {
sampleInfo: async ({sampleKey}, use) => {
let sampleInfo: FixtureOptions['sampleInfo'];
if (sampleKey && baseURL) {
const sampleItem = samplesList()[sampleKey];
if (sampleItem) {
const sampleURL = new URL(
sampleItem.style === 'bootstrap'
? `#/${sampleItem.componentName}/${sampleItem.sampleName}`
: `../${sampleItem.style}/#/${sampleItem.componentName}/${sampleItem.sampleName}`,
baseURL,
).href;
sampleInfo = {...sampleItem, sampleURL};
}
if (sampleKey) {
sampleInfo = samplesList()[sampleKey];
}
await use(sampleInfo);
},
baseURL: [
async ({project, framework, sampleKey, sampleInfo}, use) => {
const req = await fetch(serversManagerURL, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({project, framework, sampleKey, sampleInfo}),
});
const answer = await req.json();
if (!req.ok) {
throw new Error(answer.error);
}
await use(answer.url);
},
{timeout: 60000, scope: 'test'},
],
coverage: [
async ({page, browserName}, use) => {
await use();
Expand Down
8 changes: 7 additions & 1 deletion e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import setupCoverage from '@agnos-ui/code-coverage/setup';
import serversManager from './serversManager';

async function globalSetup() {
return await setupCoverage(import.meta.dirname);
const coverageTeardown = await setupCoverage(import.meta.dirname);
const serversManagerTeardown = await serversManager();
return async () => {
await serversManagerTeardown();
await coverageTeardown();
};
}

export default globalSetup;
4 changes: 2 additions & 2 deletions e2e/samplesMarkup.singlebrowser-e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ test.describe(`Samples markup consistency check`, async () => {

test.fixme(sampleKey === 'daisyui/rating/default', 'This sample does not currently have a consistent markup!');

test(`should have a consistent markup`, async ({page, sampleInfo}) => {
await page.goto(`${sampleInfo?.sampleURL}${samplesExtraHash[sampleKey] ?? ''}`, {waitUntil: 'networkidle'});
test(`should have a consistent markup`, async ({page, baseURL}) => {
await page.goto(`${baseURL}${samplesExtraHash[sampleKey] ?? ''}`, {waitUntil: 'networkidle'});
await expect.poll(async () => (await page.locator('#root').innerHTML()).trim().length).toBeGreaterThan(0);
await samplesExtraAction[sampleKey]?.(page);
expect(await htmlSnapshot(page.locator('body'))).toMatchSnapshot(`${sampleKey}.html`);
Expand Down
171 changes: 171 additions & 0 deletions e2e/serversManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import {spawn} from 'child_process';
import type {Server} from 'http';
import type {AddressInfo} from 'net';
import type {SampleInfo} from '../demo/src/lib/layout/sample';

interface RequestBody {
project?: string;
framework?: string;
sampleKey?: string;
sampleInfo?: Pick<SampleInfo, 'componentName' | 'sampleName' | 'style'>;
}

const isPreview = process.env.PREVIEW === 'true';
const includeCoverage = process.env.COVERAGE === 'true';
const coverageSuffix = includeCoverage ? ':coverage' : '';
const previewOrDev = isPreview ? 'preview' : 'dev';
const allServers = {
angularDemoDevBootstrap: {
command: `npm run -w angular/demo dev:bootstrap${coverageSuffix}`,
url: 'http://localhost:4200',
urlReadyPath: '/angular/samples/bootstrap/',
},
angularDemoDevDaisyui: {
command: `npm run -w angular/demo dev:daisyui${coverageSuffix}`,
url: 'http://localhost:4201',
urlReadyPath: '/angular/samples/daisyui/',
},
reactDemoDev: {
command: `npm run -w react/demo dev${coverageSuffix}`,
url: 'http://localhost:3000',
urlReadyPath: '/react/samples/bootstrap/',
},
svelteDemoDev: {
command: `npm run -w svelte/demo dev${coverageSuffix}`,
url: 'http://localhost:3001',
urlReadyPath: '/svelte/samples/bootstrap/',
},
demoSite: {
command: `npm run -w demo ${previewOrDev}${coverageSuffix}`,
url: 'http://localhost:4000',
urlReadyPath: '/',
},
};
type ServerKey = keyof typeof allServers;

const getNeededServersAndURL = ({project, framework, sampleInfo}: RequestBody) => {
let url: string | undefined;
const servers: ServerKey[] = [];
switch (project) {
case 'singlebrowser':
case 'main': {
let urlPath = '/';
if (framework) {
if (sampleInfo) {
urlPath = `/${framework}/samples/${sampleInfo.style}/#/${sampleInfo.componentName}/${sampleInfo.sampleName}`;
} else {
urlPath = `/${framework}/samples/bootstrap/`;
}
}
if (isPreview) {
servers.push('demoSite');
} else {
switch (framework) {
case 'angular':
if (sampleInfo?.style === 'daisyui') {
servers.push('angularDemoDevDaisyui');
} else {
servers.push('angularDemoDevBootstrap');
}
break;
case 'react':
servers.push('reactDemoDev');
break;
case 'svelte':
servers.push('svelteDemoDev');
break;
}
}
url = `${allServers[servers[0]].url}${urlPath}`;
break;
}
case 'demo':
servers.push('demoSite');
if (!isPreview) {
servers.push('angularDemoDevBootstrap', 'angularDemoDevDaisyui', 'reactDemoDev', 'svelteDemoDev');
}
url = allServers.demoSite.url;
break;
}
return {servers, url};
};

const isURLReady = async (url: string) => {
try {
const res = await fetch(url);
return res.ok;
} catch (error) {
return false;
}
};

const startServer = async (serverKey: ServerKey, abortSignal: AbortSignal) => {
const serverInfo = allServers[serverKey];
const url = `${serverInfo.url}${serverInfo.urlReadyPath}`;
if (await isURLReady(url)) {
// server is already running
return;
}
const proc = spawn(allServers[serverKey].command, {
shell: true,
signal: abortSignal,
stdio: 'inherit',
});

proc.on('error', (error) => {
// avoid displaying an error when the abort signal is triggered
if (!abortSignal.aborted) {
console.error(`Failed to start server ${serverKey}: ${error}`);
}
});
let urlReady = false;
while (!urlReady && !abortSignal.aborted) {
urlReady = await isURLReady(url);
await new Promise((resolve) => setTimeout(resolve, 100));
}
};

export default async () => {
const serversStatus = new Map<ServerKey, Promise<void>>();
const abortController = new AbortController();

const ensureServerRuns = async (serverKey: ServerKey) => {
let status = serversStatus.get(serverKey);
if (!status) {
console.log(`Starting server ${serverKey}`);
status = startServer(serverKey, abortController.signal);
serversStatus.set(serverKey, status);
}
return await status;
};

const express = await import('express');
const app = express.default();
app.use(express.json());

app.post('/', async (req, res) => {
try {
const {servers, url} = getNeededServersAndURL(req.body);
await Promise.all(servers.map(ensureServerRuns));
res.json({url});
} catch (error) {
res.status(500).json({error: `${error}`});
}
});

const server = await new Promise<Server>((resolve, reject) => {
const server = app.listen(0, '127.0.0.1', () => resolve(server)).on('error', reject);
});
const port = (server.address() as AddressInfo).port;
const serverManagerURL = `http://127.0.0.1:${port}`;
process.env.SERVERS_MANAGER_URL = serverManagerURL;
console.log(`Server manager was started on ${serverManagerURL}`);
return async () => {
await new Promise((resolve) => {
abortController.abort();
console.log('Closing server manager');
server.close(resolve);
server.closeAllConnections();
});
};
};
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-jsdoc": "^48.2.1",
"express": "^4.19.2",
"glob": "10.3.10",
"husky": "^9.0.11",
"lint-staged": "^15.2.2",
Expand Down Expand Up @@ -128,7 +129,8 @@
"command": "node scripts/e2e.js --ui",
"dependencies": [
"./code-coverage:build"
]
],
"service": true
},
"prepare": {
"dependencies": [
Expand Down
Loading

0 comments on commit 07cf987

Please sign in to comment.