test: Add a browser based test harness (#2641)
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { RELEASE_VERSION_TOKEN } from './release-version.js';
|
||||
|
||||
// What the card substitutes to mean "the version in package.json". The
|
||||
// development substitution is not used here because it appends a git hash that
|
||||
// only a build step knows.
|
||||
const PACKAGE_VERSION = 'pkg';
|
||||
|
||||
/**
|
||||
* Substitutes the release version the way the build does.
|
||||
*
|
||||
* The card reads it out of a string literal that Rollup rewrites, so without
|
||||
* this it renders the placeholder itself: the loading screen shows the raw
|
||||
* token, which then appears in every failure screenshot.
|
||||
*/
|
||||
export const releaseVersion = () => ({
|
||||
name: 'release-version',
|
||||
|
||||
transform(code, id) {
|
||||
if (!id.includes('src/utils/diagnostics.ts')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return { code: code.replace(RELEASE_VERSION_TOKEN, PACKAGE_VERSION), map: null };
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
/**
|
||||
* The literal the card carries in place of its version, for the build to
|
||||
* rewrite.
|
||||
*
|
||||
* `getReleaseVersion` cannot import this: it has to sit in that source as a
|
||||
* plain string for the build to have something to replace. It lives in its own
|
||||
* module because the build, the browser tests and a unit test all have to agree
|
||||
* on it, and none of them should have to depend on either of the others.
|
||||
*
|
||||
* It must be in a JS file as Node's loader cannot import TypeScript.
|
||||
*/
|
||||
export const RELEASE_VERSION_TOKEN = '__ADVANCED_CAMERA_CARD_RELEASE_VERSION__';
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Vite plugin: `import style from './foo.scss'` gives the compiled CSS as a
|
||||
* string, which is what the source tree passes to Lit's `unsafeCSS`. Left
|
||||
* alone, Vite adds those styles to the page and the import returns nothing
|
||||
* useful. Appending Vite's own `?inline` asks for the text instead, compiled by
|
||||
* the same sass the build uses.
|
||||
*
|
||||
* The build's `rollup-plugin-styler` cannot be used here: Vite always handles
|
||||
* `.scss` itself, so it would take that plugin's JavaScript output and hand it
|
||||
* to sass, which rejects it.
|
||||
*
|
||||
* @type {() => import('vite').Plugin}
|
||||
*/
|
||||
export const scssString = () => ({
|
||||
name: 'scss-string',
|
||||
|
||||
// Vite: run before its builtin CSS handling.
|
||||
enforce: 'pre',
|
||||
|
||||
async resolveId(source, importer, options) {
|
||||
if (!source.endsWith('.scss')) {
|
||||
return null;
|
||||
}
|
||||
return await this.resolve(`${source}?inline`, importer, options);
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const FIXTURE_DIRECTORY = 'tests/browser/fixtures';
|
||||
|
||||
const CONTENT_TYPES = {
|
||||
'.png': 'image/png',
|
||||
};
|
||||
|
||||
const OK = 200;
|
||||
|
||||
// Requests answered per token, so `responses` can be read as a sequence.
|
||||
const requestCounts = new Map();
|
||||
|
||||
/**
|
||||
* Serves a fixture at `/test-media/<file>`, behaving as the query asks:
|
||||
*
|
||||
* token Which counter the request belongs to. Required, because one
|
||||
* server serves every test in a run and a shared counter would
|
||||
* make a test's behaviour depend on what ran before it.
|
||||
* responses The status to answer each request with, in order: `200` serves
|
||||
* the file and anything else is sent as an empty error. Once the
|
||||
* list runs out, requests are never answered at all, which is how
|
||||
* a camera goes quiet.
|
||||
*
|
||||
* Nothing here waits for a set time. Tests run on a fake clock while requests
|
||||
* are served in real time, so a response that is merely slow is a race: run the
|
||||
* suite on a loaded machine and it arrives in the middle of a test that assumed
|
||||
* it would not.
|
||||
*/
|
||||
export const testMediaServer = () => ({
|
||||
name: 'test-media-server',
|
||||
|
||||
configureServer(server) {
|
||||
server.middlewares.use('/test-media', (req, res) => {
|
||||
const url = new URL(req.url ?? '/', 'http://localhost');
|
||||
|
||||
// A name rather than a path: nothing outside the fixtures is servable.
|
||||
const file = path.basename(url.pathname);
|
||||
const contentType = CONTENT_TYPES[path.extname(file)];
|
||||
const location = path.resolve(FIXTURE_DIRECTORY, file);
|
||||
const token = url.searchParams.get('token');
|
||||
|
||||
if (!contentType || !token || !fs.existsSync(location)) {
|
||||
res.statusCode = 404;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const responses = (url.searchParams.get('responses') ?? '')
|
||||
.split(',')
|
||||
.filter((status) => status !== '')
|
||||
.map(Number);
|
||||
|
||||
const answered = requestCounts.get(token) ?? 0;
|
||||
requestCounts.set(token, answered + 1);
|
||||
|
||||
// Held open deliberately. The socket is released when the test ends and
|
||||
// the card that asked for it is torn down.
|
||||
if (answered >= responses.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = responses[answered];
|
||||
if (status !== OK) {
|
||||
res.statusCode = status;
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', contentType);
|
||||
res.end(fs.readFileSync(location));
|
||||
});
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user