Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(config-cli): Provide a cli for creating temporary server and dump screenshots. #2236

Merged
merged 13 commits into from
Jun 13, 2022
Merged
11 changes: 9 additions & 2 deletions packages/config-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,16 @@ Dump the screenshots from basemaps production
./bin/bmc.js screenshot
```

Dump the screenshots from different host and tag
Dump the screenshots from different host

```bash
./bin/bmc.js screenshot --host HOST --tag PR-TAG
./bin/bmc.js screenshot --host HOST

```

Dump the screenshots with config file

```bash
./bin/bmc.js screenshot --config s3://..../config.json.gz

```
6 changes: 4 additions & 2 deletions packages/config-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"repository": {
"type": "git",
"url": "https://github.com/linz/basemaps.git",
"directory": "packages/config"
"directory": "packages/config-cli"
},
"author": {
"name": "Land Information New Zealand",
Expand Down Expand Up @@ -32,10 +32,12 @@
"bin/"
],
"dependencies": {
"@basemaps/config-cli": "^6.28.1",
"@basemaps/config": "^6.28.1",
"@basemaps/server": "^6.28.1",
"@basemaps/geo": "^6.28.1",
"@basemaps/shared": "^6.28.1",
"@rushstack/ts-command-line": "^4.3.13",
"get-port": "^6.1.2",
"playwright": "^1.22.0",
"zod": "^3.17.3"
}
Expand Down
139 changes: 71 additions & 68 deletions packages/config-cli/src/cli/screenshot/screenshot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@ import { mkdir } from 'fs/promises';
import { Browser, chromium } from 'playwright';
import { CommandLineAction, CommandLineStringParameter } from '@rushstack/ts-command-line';
import { z } from 'zod';
import getPort, { portNumbers } from 'get-port';
import { createServer } from '@basemaps/server';
import { FastifyInstance } from 'fastify/types/instance';
import { ConfigBundled, ConfigProviderMemory } from '@basemaps/config';

export const DefaultTestTiles = './test-tiles/default.test.tiles.json';
export const DefaultHost = 'basemaps.linz.govt.nz';

enum TileMatrixIdentifier {
Nztm2000Quad = 'NZTM2000Quad',
Expand All @@ -26,8 +33,8 @@ const zTileTest = z.object({
export type TileTestSchema = z.infer<typeof zTileTest>;

export class CommandScreenShot extends CommandLineAction {
private config: CommandLineStringParameter;
private host: CommandLineStringParameter;
private tag: CommandLineStringParameter;
private tiles: CommandLineStringParameter;

public constructor() {
Expand All @@ -39,104 +46,100 @@ export class CommandScreenShot extends CommandLineAction {
}

protected onDefineParameters(): void {
this.config = this.defineStringParameter({
argumentName: 'CONFIG',
parameterLongName: '--config',
description: 'Path of config bundle file, could be both local file or s3.',
});

this.host = this.defineStringParameter({
argumentName: 'HOST',
parameterLongName: '--host',
description: 'Host to use',
defaultValue: 'basemaps.linz.govt.nz',
});

this.tag = this.defineStringParameter({
argumentName: 'TAG',
parameterShortName: '-t',
parameterLongName: '--tag',
description: 'PR tag(PR-number) or "production"',
defaultValue: 'production',
defaultValue: DefaultHost,
});

this.tiles = this.defineStringParameter({
argumentName: 'TILES',
parameterLongName: '--tiles',
description: 'JSON file path for the test tiles',
defaultValue: './test-tiles/default.test.tiles.json',
defaultValue: DefaultTestTiles,
});
}

async onExecute(): Promise<void> {
const logger = LogConfig.get();
const config = this.config.value;
let host = this.host.value ?? DefaultHost;
const tiles = this.tiles.value ?? DefaultTestTiles;

let BasemapsServer: FastifyInstance | undefined = undefined;
if (config != null) {
const port = await getPort({ port: portNumbers(10000, 11000) });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just use any port?

host = `http://localhost:${port}`;
BasemapsServer = await startServer(host, port, config, logger);
}

logger.info('Page:Launch');
const chrome = await chromium.launch();

try {
await this.takeScreenshots(chrome, logger);
await takeScreenshots(host, tiles, chrome, logger);
} finally {
await chrome.close();
if (BasemapsServer != null) await BasemapsServer.close();
}
}
}

async takeScreenshots(chrome: Browser, logger: LogType): Promise<void> {
const host = this.host.value ?? this.host.defaultValue;
const tag = this.tag.value ?? this.tag.defaultValue;
const tiles = this.tiles.value ?? this.tiles.defaultValue;
if (host == null || tag == null || tiles == null)
throw new Error('Missing essential parameter to run the process.');

const TestTiles = await fsa.readJson<TileTestSchema[]>(tiles);
for (const test of TestTiles) {
const page = await chrome.newPage();

const tileSetId = await this.getTileSetId(test.tileSet, tag);
const styleId = await this.getStyleId(test.style, tag);
export async function takeScreenshots(host: string, tiles: string, chrome: Browser, logger: LogType): Promise<void> {
const TestTiles = await fsa.readJson<TileTestSchema[]>(tiles);
for (const test of TestTiles) {
const page = await chrome.newPage();

const searchParam = new URLSearchParams();
searchParam.set('p', test.tileMatrix);
searchParam.set('i', tileSetId);
if (styleId) searchParam.set('s', styleId);
const searchParam = new URLSearchParams();
searchParam.set('p', test.tileMatrix);
searchParam.set('i', test.tileSet);
if (test.style) searchParam.set('s', test.style);

const loc = `@${test.location.lat},${test.location.lng},z${test.location.z}`;
const fileName = '.artifacts/visual-snapshots/' + host + '_' + test.name + '.png';
const loc = `@${test.location.lat},${test.location.lng},z${test.location.z}`;
const fileName = '.artifacts/visual-snapshots/' + host + '_' + test.name + '.png';

await mkdir(`.artifacts/visual-snapshots/`, { recursive: true });
await mkdir(`.artifacts/visual-snapshots/`, { recursive: true });

const url = `https://${host}/?${searchParam.toString()}&debug=true&debug.screenshot=true#${loc}`;
let url = `${host}/?${searchParam.toString()}&debug=true&debug.screenshot=true#${loc}`;
if (!url.startsWith('http')) url = `https"//${url}`;

logger.info({ url, expected: fileName }, 'Page:Load');
logger.info({ url, expected: fileName }, 'Page:Load');

await page.goto(url);
await page.goto(url);

try {
await page.waitForSelector('div#map-loaded', { state: 'attached' });
await page.waitForTimeout(1000);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: fileName });
} catch (e) {
await page.screenshot({ path: fileName });
throw e;
}
logger.info({ url, expected: fileName }, 'Page:Load:Done');
await page.close();
try {
await page.waitForSelector('div#map-loaded', { state: 'attached' });
await page.waitForTimeout(1000);
await page.waitForLoadState('networkidle');
await page.screenshot({ path: fileName });
} catch (e) {
await page.screenshot({ path: fileName });
throw e;
}
logger.info({ url, expected: fileName }, 'Page:Load:Done');
await page.close();
}
}

async getTileSetId(tileSetId: string, tag: string): Promise<string> {
if (tag === 'production') return tileSetId;

const tileSetTagId = `${tileSetId}@${tag}`;
const dbId = Config.TileSet.id(tileSetTagId);
const tileSet = await Config.TileSet.get(dbId);

if (tileSet) return tileSetTagId;
return tileSetId;
}

async getStyleId(styleId: string | undefined, tag: string): Promise<string> {
if (styleId == null) return '';
if (tag === 'production') return styleId ?? '';

const styleIdTagId = `${styleId}@${tag}`;
const dbId = Config.Style.id(styleIdTagId);
const style = await Config.Style.get(dbId);
if (style) return styleIdTagId;
return styleId;
}
async function startServer(host: string, port: number, config: string, logger: LogType): Promise<FastifyInstance> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is host passed in just to log it?

could generate host inside the function or log outside the function

// Bundle Config
const configJson = await fsa.readJson<ConfigBundled>(config);
const mem = ConfigProviderMemory.fromJson(configJson);
Config.setConfigProvider(mem);

// Start server
const server = createServer(logger);
await new Promise<void>((resolve) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return new Promise<FastifyInstance>(r => {
r(server);
});

server.listen(port, '0.0.0.0', () => {
logger.info({ url: host }, 'ServerStarted');
resolve();
}),
);
return server;
}
2 changes: 1 addition & 1 deletion packages/config-cli/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
"outDir": "./build"
},
"include": ["src/**/*"],
"references": [{ "path": "../__tests__" }]
"references": [{ "path": "../config" }, { "path": "../geo" }, { "path": "../server" }, { "path": "../shared" }]
}
2 changes: 1 addition & 1 deletion packages/config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export {
export { ConfigVectorStyle, Sources, StyleJson } from './config/vector.style.js';
export { ConfigProviderDynamo } from './dynamo/dynamo.config.js';
export { ConfigDynamoBase } from './dynamo/dynamo.config.base.js';
export { ConfigProviderMemory } from './memory/memory.config.js';
export { ConfigProviderMemory, ConfigBundled } from './memory/memory.config.js';
export { TileSetNameComponents, TileSetNameParser } from './tile.set.name.js';
export { parseHex, parseRgba } from './color.js';
export { ConfigJson } from './json/json.config.js';
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3702,6 +3702,11 @@ get-port@^5.1.1:
resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193"
integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==

get-port@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/get-port/-/get-port-6.1.2.tgz#c1228abb67ba0e17fb346da33b15187833b9c08a"
integrity sha512-BrGGraKm2uPqurfGVj/z97/zv8dPleC6x9JBNRTrDNtCkkRF4rPwrQXFgL7+I+q8QSdU4ntLQX2D7KIxSy8nGw==

get-stdin@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
Expand Down