Skip to content

Commit 84bcb4f

Browse files
committed
chore: add tests
1 parent f038dc0 commit 84bcb4f

13 files changed

Lines changed: 504 additions & 7 deletions

File tree

package-lock.json

Lines changed: 38 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
"lint": "eslint .",
2323
"lint-fix": "eslint --fix .",
2424
"check-types": "tsc",
25-
"test": "playwright test --project=unit --max-failures=1 && playwright test --project=api --max-failures=1",
25+
"test": "playwright test",
2626
"quality": "npm run lint && npm run check-types && npm run test",
2727
"prepare": "husky || true"
2828
},
@@ -32,6 +32,7 @@
3232
"express": "^5.0.0",
3333
"helmet": "^8.1.0",
3434
"http-terminator": "^3.2.0",
35+
"prom-client": "^15.1.3",
3536
"tileserver-gl-light": "^4.12.1",
3637
"zod": "^3.23.8"
3738
},

playwright.config.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ export default defineConfig({
99
retries: process.env.CI ? 1 : 0,
1010
reporter: 'list',
1111

12+
globalSetup: './tests/support/global-setup.ts',
13+
globalTeardown: './tests/support/global-teardown.ts',
14+
1215
use: {
13-
baseURL: 'http://localhost:' + process.env.DEV_PORT,
16+
baseURL: process.env.TEST_TILESERVER_URL,
1417
trace: 'on-first-retry'
1518
},
1619

src/app.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
1+
import { type Server } from 'node:http'
12
import express, { type Express, type Request, type Response, type NextFunction } from 'express'
23
import helmet from 'helmet'
34
// @ts-expect-error no types shipped with tileserver-gl-light
4-
import tileserverGl from 'tileserver-gl-light'
5+
import { server as tileserverGl } from 'tileserver-gl-light/src/server.js'
56
import config from '#config'
67
import log from '#log'
78
import { buildTileserverConfig } from './build-config.ts'
89

10+
interface TileserverRunning {
11+
app: Express
12+
server: Server
13+
startupPromise: Promise<unknown>
14+
}
15+
916
export const createApp = async (): Promise<Express> => {
1017
const tileserverConfig = await buildTileserverConfig()
1118

12-
const tsApp: Express = await tileserverGl({
19+
// tileserver-gl-light's `server()` both builds the express app AND calls `.listen()` on
20+
// an internal http server — there is no "just give me the app" entry point. We bind it to
21+
// an ephemeral port, wait for its async init, then close the inner socket and reuse the
22+
// still-fully-wired express app as middleware under our own outer http server.
23+
const running = tileserverGl({
1324
config: tileserverConfig,
1425
publicUrl: config.publicUrl,
15-
port: config.port
26+
port: 0,
27+
silent: true
28+
}) as TileserverRunning
29+
await running.startupPromise
30+
await new Promise<void>((resolve, reject) => {
31+
running.server.close((err) => err ? reject(err) : resolve())
1632
})
33+
const tsApp = running.app
1734

1835
const app = express()
1936

src/config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const EnvSchema = z.object({
99
PORT: z.coerce.number().int().positive().default(8080),
1010
LOG_LEVEL: z.enum(['trace', 'debug', 'info', 'warn', 'error']).default('info'),
1111
ARTEFACTS_PAGE_SIZE: z.coerce.number().int().positive().default(100),
12-
OBSERVER_ACTIVE: z.coerce.boolean().default(true),
12+
OBSERVER_ACTIVE: z.enum(['true', 'false']).default('true').transform(v => v === 'true'),
1313
OBSERVER_PORT: z.coerce.number().int().positive().default(9090)
1414
})
1515

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "basic",
3+
"version": "1.0.0",
4+
"description": "Minimal maplibre style used as a test fixture.",
5+
"registry": {
6+
"category": "maplibre-style"
7+
}
8+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"version": 8,
3+
"name": "basic",
4+
"sources": {
5+
"openmaptiles": {
6+
"type": "vector",
7+
"url": "mbtiles://{tileset-world}"
8+
}
9+
},
10+
"glyphs": "https://example.invalid/{fontstack}/{range}.pbf",
11+
"sprite": "https://example.invalid/sprite",
12+
"layers": [
13+
{
14+
"id": "background",
15+
"type": "background",
16+
"paint": { "background-color": "#ffffff" }
17+
}
18+
]
19+
}

tests/support/axios.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { axiosBuilder } from '@data-fair/lib-node/axios.js'
22

3-
export const baseURL = `http://localhost:${process.env.DEV_PORT}`
3+
export const baseURL = process.env.TEST_TILESERVER_URL ?? ''
44

55
export const anonymousAx = axiosBuilder({ baseURL })

tests/support/fixtures.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { createWriteStream } from 'node:fs'
2+
import { readFile, readdir, rm, stat } from 'node:fs/promises'
3+
import { join, relative } from 'node:path'
4+
import { pipeline } from 'node:stream/promises'
5+
import { createGzip } from 'node:zlib'
6+
import sqlite3 from 'sqlite3'
7+
// @ts-expect-error no types shipped with tar-stream
8+
import tarStream from 'tar-stream'
9+
10+
interface RunnableDb {
11+
run: (sql: string, params?: unknown[]) => Promise<void>
12+
close: () => Promise<void>
13+
}
14+
15+
const runDb = (db: sqlite3.Database, sql: string, params: unknown[] = []): Promise<void> =>
16+
new Promise((resolve, reject) => {
17+
db.run(sql, params, (err) => err ? reject(err) : resolve())
18+
})
19+
20+
const closeDb = (db: sqlite3.Database): Promise<void> =>
21+
new Promise((resolve, reject) => {
22+
db.close((err) => err ? reject(err) : resolve())
23+
})
24+
25+
const openDb = (path: string): Promise<RunnableDb> => new Promise((resolve, reject) => {
26+
const db = new sqlite3.Database(path, (err) => {
27+
if (err) return reject(err)
28+
resolve({
29+
run: (sql, params) => runDb(db, sql, params),
30+
close: () => closeDb(db)
31+
})
32+
})
33+
})
34+
35+
export const buildMbtiles = async (destPath: string, tilesetId: string): Promise<void> => {
36+
await rm(destPath, { force: true })
37+
const db = await openDb(destPath)
38+
try {
39+
await db.run('CREATE TABLE metadata (name TEXT, value TEXT)')
40+
await db.run('CREATE TABLE tiles (zoom_level INTEGER, tile_column INTEGER, tile_row INTEGER, tile_data BLOB, PRIMARY KEY (zoom_level, tile_column, tile_row))')
41+
42+
const metadata: Record<string, string> = {
43+
name: tilesetId,
44+
type: 'overlay',
45+
version: '1.0.0',
46+
description: `Test fixture ${tilesetId}`,
47+
format: 'pbf',
48+
minzoom: '0',
49+
maxzoom: '2',
50+
bounds: '-180.0,-85.0511,180.0,85.0511',
51+
center: '0,0,0',
52+
json: JSON.stringify({
53+
vector_layers: [
54+
{ id: 'placeholder', description: '', minzoom: 0, maxzoom: 2, fields: {} }
55+
]
56+
})
57+
}
58+
for (const [k, v] of Object.entries(metadata)) {
59+
await db.run('INSERT INTO metadata (name, value) VALUES (?, ?)', [k, v])
60+
}
61+
} finally {
62+
await db.close()
63+
}
64+
}
65+
66+
export const packStyleTarball = async (sourceDir: string, destPath: string): Promise<void> => {
67+
const pack = tarStream.pack()
68+
69+
const walk = async (dir: string): Promise<string[]> => {
70+
const entries = await readdir(dir, { withFileTypes: true })
71+
const files: string[] = []
72+
for (const entry of entries) {
73+
const full = join(dir, entry.name)
74+
if (entry.isDirectory()) files.push(...await walk(full))
75+
else if (entry.isFile()) files.push(full)
76+
}
77+
return files
78+
}
79+
80+
const files = await walk(sourceDir)
81+
// Kick off the write in parallel with pack entries so backpressure works.
82+
const writePromise = pipeline(pack, createGzip(), createWriteStream(destPath))
83+
84+
for (const absPath of files) {
85+
const rel = relative(sourceDir, absPath).split('\\').join('/')
86+
const name = `package/${rel}`
87+
const content = await readFile(absPath)
88+
const st = await stat(absPath)
89+
await new Promise<void>((resolve, reject) => {
90+
pack.entry({ name, size: content.length, mode: st.mode }, content, (err: Error | null) => {
91+
if (err) reject(err)
92+
else resolve()
93+
})
94+
})
95+
}
96+
pack.finalize()
97+
await writePromise
98+
}

0 commit comments

Comments
 (0)