diff --git a/packages/config-cli/README.md b/packages/config-cli/README.md new file mode 100644 index 000000000..7450c7ed2 --- /dev/null +++ b/packages/config-cli/README.md @@ -0,0 +1,18 @@ +# Basemaps Config CLI + +This package is to control the configuration in the LINZ basemaps product. + +## Usage -- Screenshots + +Dump the screenshots from basemaps production + +```bash +./bin/bmc.js screenshot +``` + +Dump the screenshots from different host and tag + +```bash +./bin/bmc.js screenshot --host HOST --tag PR-TAG + +``` diff --git a/packages/config-cli/bin/bmc.js b/packages/config-cli/bin/bmc.js new file mode 100755 index 000000000..14a485f0c --- /dev/null +++ b/packages/config-cli/bin/bmc.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +Error.stackTraceLimit = 100; +import { BasemapsConfig } from '../build/cli/screenshot/index.js'; + +new BasemapsConfig().run(); diff --git a/packages/config-cli/package.json b/packages/config-cli/package.json new file mode 100644 index 000000000..8dfb3acc5 --- /dev/null +++ b/packages/config-cli/package.json @@ -0,0 +1,42 @@ +{ + "name": "@basemaps/config-cli", + "version": "6.27.0", + "repository": { + "type": "git", + "url": "https://github.com/linz/basemaps.git", + "directory": "packages/config" + }, + "author": { + "name": "Land Information New Zealand", + "url": "https://linz.govt.nz", + "organization": true + }, + "type": "module", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "license": "MIT", + "main": "./build/index.js", + "types": "./build/index.d.ts", + "bin": { + "bmc": "./bin/bmc.js" + }, + "scripts": { + "test": "ospec --globs 'build/**/*.test.js'" + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "build/", + "bin/" + ], + "dependencies": { + "@basemaps/config-cli": "^6.27.0", + "@basemaps/geo": "^6.26.0", + "@basemaps/shared": "^6.27.0", + "@rushstack/ts-command-line": "^4.3.13", + "playwright": "^1.22.0", + "zod": "^3.17.3" + } +} diff --git a/packages/config-cli/src/cli/screenshot/index.ts b/packages/config-cli/src/cli/screenshot/index.ts new file mode 100644 index 000000000..fc685cda6 --- /dev/null +++ b/packages/config-cli/src/cli/screenshot/index.ts @@ -0,0 +1,13 @@ +import { BaseCommandLine } from '@basemaps/shared/build/cli/base.js'; +import 'source-map-support/register.js'; +import { CommandScreenShot } from './screenshot.js'; + +export class BasemapsConfig extends BaseCommandLine { + constructor() { + super({ + toolFilename: 'bmc', + toolDescription: 'Basemaps config command tools', + }); + this.addAction(new CommandScreenShot()); + } +} diff --git a/packages/config-cli/src/cli/screenshot/screenshot.ts b/packages/config-cli/src/cli/screenshot/screenshot.ts new file mode 100644 index 000000000..ebc698661 --- /dev/null +++ b/packages/config-cli/src/cli/screenshot/screenshot.ts @@ -0,0 +1,142 @@ +import { Config, fsa, LogConfig, LogType } from '@basemaps/shared'; +import { mkdir } from 'fs/promises'; +import { Browser, chromium } from 'playwright'; +import { CommandLineAction, CommandLineStringParameter } from '@rushstack/ts-command-line'; +import { z } from 'zod'; + +enum TileMatrixIdentifier { + Nztm2000Quad = 'NZTM2000Quad', + Google = 'WebMercatorQuad', +} + +const zLocation = z.object({ + lat: z.number().gte(-90).lte(90), + lng: z.number().gte(-180).lte(180), + z: z.number().gte(0).lte(32), +}); + +const zTileTest = z.object({ + name: z.string(), + tileMatrix: z.nativeEnum(TileMatrixIdentifier), + location: zLocation, + tileSet: z.string(), + style: z.string().optional(), +}); + +export type TileTestSchema = z.infer; + +export class CommandScreenShot extends CommandLineAction { + private host: CommandLineStringParameter; + private tag: CommandLineStringParameter; + private tiles: CommandLineStringParameter; + + public constructor() { + super({ + actionName: 'screenshot', + summary: 'dump screenshots of from LINZ Basemaps', + documentation: 'Dump screenshots with selected tile sets', + }); + } + + protected onDefineParameters(): void { + 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', + }); + + this.tiles = this.defineStringParameter({ + argumentName: 'TILES', + parameterLongName: '--tiles', + description: 'JSON file path for the test tiles', + defaultValue: './test-tiles/default.test.tiles.json', + }); + } + + async onExecute(): Promise { + const logger = LogConfig.get(); + logger.info('Page:Launch'); + const chrome = await chromium.launch(); + + try { + await this.takeScreenshots(chrome, logger); + } finally { + await chrome.close(); + } + } + + async takeScreenshots(chrome: Browser, logger: LogType): Promise { + 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(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); + + const searchParam = new URLSearchParams(); + searchParam.set('p', test.tileMatrix); + searchParam.set('i', tileSetId); + if (styleId) searchParam.set('s', styleId); + + 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 }); + + const url = `https://${host}/?${searchParam.toString()}&debug=true&debug.screenshot=true#${loc}`; + + logger.info({ url, expected: fileName }, 'Page:Load'); + + 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(); + } + } + + async getTileSetId(tileSetId: string, tag: string): Promise { + 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 { + 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; + } +} diff --git a/packages/config-cli/test-tiles/default.test.tiles.json b/packages/config-cli/test-tiles/default.test.tiles.json new file mode 100644 index 000000000..926c7b13f --- /dev/null +++ b/packages/config-cli/test-tiles/default.test.tiles.json @@ -0,0 +1,42 @@ +[ + { + "name": "health-3857-z5", + "tileMatrix": "WebMercatorQuad", + "location": { "lat": -41.8899962, "lng": 174.0492437, "z": 5 }, + "tileSet": "health" + }, + { + "name": "health-2193-z5", + "tileMatrix": "NZTM2000Quad", + "location": { "lat": -41.8899962, "lng": 174.0492437, "z": 1 }, + "tileSet": "aerial" + }, + { + "name": "topographic-3857-z5", + "tileMatrix": "WebMercatorQuad", + "location": { "lat": -41.8899962, "lng": 174.0492437, "z": 5 }, + "tileSet": "topographic", + "style": "topographic" + }, + { + "name": "topolite-3857-z5", + "tileMatrix": "WebMercatorQuad", + "location": { "lat": -41.8899962, "lng": 174.0492437, "z": 5 }, + "tileSet": "topographic", + "style": "topolite" + }, + { + "name": "topographic-3857-z14", + "tileMatrix": "WebMercatorQuad", + "location": { "lat": -41.8899962, "lng": 174.0492437, "z": 14 }, + "tileSet": "topographic", + "style": "topographic" + }, + { + "name": "topolite-3857-z17", + "tileMatrix": "WebMercatorQuad", + "location": { "lat": -43.8063936, "lng": 172.9679876, "z": 17 }, + "tileSet": "topographic", + "style": "topolite" + } +] diff --git a/packages/config-cli/tsconfig.json b/packages/config-cli/tsconfig.json new file mode 100644 index 000000000..5de502c96 --- /dev/null +++ b/packages/config-cli/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "module": "ES2020", + "moduleResolution": "node", + "rootDir": "./src", + "outDir": "./build" + }, + "include": ["src/**/*"], + "references": [{ "path": "../__tests__" }] +} diff --git a/tsconfig.json b/tsconfig.json index 2d6679d35..5bd260586 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ { "path": "./packages/bathymetry" }, { "path": "./packages/_infra" }, { "path": "./packages/config" }, + { "path": "./packages/config-cli" }, { "path": "./packages/shared" }, { "path": "./packages/lambda-cog" }, { "path": "./packages/lambda-tiler" }, diff --git a/yarn.lock b/yarn.lock index 297b9916b..6065bed55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6006,6 +6006,18 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +playwright-core@1.22.2: + version "1.22.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.22.2.tgz#ed2963d79d71c2a18d5a6fd25b60b9f0a344661a" + integrity sha512-w/hc/Ld0RM4pmsNeE6aL/fPNWw8BWit2tg+TfqJ3+p59c6s3B6C8mXvXrIPmfQEobkcFDc+4KirNzOQ+uBSP1Q== + +playwright@^1.22.0: + version "1.22.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.22.2.tgz#353a7c29f89ca9600edc7a9a30aed790823c797d" + integrity sha512-hUTpg7LytIl3/O4t0AQJS1V6hWsaSY5uZ7w1oCC8r3a1AQN5d6otIdCkiB3cbzgQkcMaRxisinjMFMVqZkybdQ== + dependencies: + playwright-core "1.22.2" + pngjs@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-4.0.1.tgz#f803869bb2fc1bfe1bf99aa4ec21c108117cfdbe" @@ -7995,6 +8007,11 @@ yocto-queue@^1.0.0: resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== +zod@^3.17.3: + version "3.17.3" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.17.3.tgz#86abbc670ff0063a4588d85a4dcc917d6e4af2ba" + integrity sha512-4oKP5zvG6GGbMlqBkI5FESOAweldEhSOZ6LI6cG+JzUT7ofj1ZOC0PJudpQOpT1iqOFpYYtX5Pw0+o403y4bcg== + zod@^3.2.0: version "3.5.1" resolved "https://registry.yarnpkg.com/zod/-/zod-3.5.1.tgz#e93ce58e182bb76f7d29ccd24feee72611f9a129"