Reworked watcher events and describing texts
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
|
||||
async function read(relativePath) {
|
||||
return fs.readFile(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
async function clientSources() {
|
||||
const directory = path.join(root, 'public/js');
|
||||
const names = await fs.readdir(directory);
|
||||
return Promise.all(names.filter(name => name.endsWith('.js')).map(name => read(`public/js/${name}`)));
|
||||
}
|
||||
|
||||
test('every static HTML id is referenced by authored client JavaScript', async () => {
|
||||
const html = await read('public/index.html');
|
||||
const scripts = (await clientSources()).join('\n');
|
||||
const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map(match => match[1]);
|
||||
|
||||
assert.ok(ids.length > 0);
|
||||
for (const id of ids) {
|
||||
assert.match(scripts, new RegExp(id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')), `unused id: ${id}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('every static HTML class is used by CSS or authored JavaScript', async () => {
|
||||
const html = await read('public/index.html');
|
||||
const css = `${await read('public/css/bootstrap.css')}\n${await read('public/css/app.css')}`;
|
||||
const scripts = (await clientSources()).join('\n');
|
||||
const classes = [...html.matchAll(/\bclass="([^"]+)"/g)]
|
||||
.flatMap(match => match[1].split(/\s+/))
|
||||
.filter(Boolean);
|
||||
|
||||
for (const className of new Set(classes)) {
|
||||
assert.ok(css.includes(className) || scripts.includes(className), `unused class: ${className}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('application actions use pointer events instead of click handlers', async () => {
|
||||
const scripts = (await Promise.all([
|
||||
read('public/js/app.js'),
|
||||
read('public/js/slideshow.js'),
|
||||
read('public/js/bootstrap-loader.js')
|
||||
])).join('\n');
|
||||
|
||||
assert.match(scripts, /pointerup/);
|
||||
assert.match(scripts, /keydown/);
|
||||
assert.doesNotMatch(scripts, /(?:\.on|addEventListener)\(\s*['"]click['"]/);
|
||||
});
|
||||
|
||||
test('mobile navigation footer contains a rotating SVG arrow and exact metre output', async () => {
|
||||
const html = await read('public/index.html');
|
||||
const app = await read('public/js/app.js');
|
||||
const css = await read('public/css/app.css');
|
||||
|
||||
assert.match(html, /<footer id="navigation-footer"/);
|
||||
assert.match(html, /<svg id="navigation-arrow"/);
|
||||
assert.match(app, /Distance\.bearing/);
|
||||
assert.match(app, /Math\.round\(distance\).*m/);
|
||||
assert.match(css, /#navigation-arrow[\s\S]*transform[\s\S]*transition/);
|
||||
assert.match(css, /touch-action:\s*manipulation/);
|
||||
});
|
||||
|
||||
test('client bearing calculation points north and east correctly', async () => {
|
||||
const source = await read('public/js/distance.js');
|
||||
const context = { window: { Wegwichtel: {} } };
|
||||
vm.runInNewContext(source, context);
|
||||
|
||||
const distance = context.window.Wegwichtel.Distance;
|
||||
assert.ok(Math.abs(distance.bearing({ lat: 0, lon: 0 }, { lat: 1, lon: 0 })) < 0.001);
|
||||
assert.ok(Math.abs(distance.bearing({ lat: 0, lon: 0 }, { lat: 0, lon: 1 }) - 90) < 0.001);
|
||||
});
|
||||
|
||||
|
||||
test('route controls use accessible icon buttons with start, pause and stop states', async () => {
|
||||
const html = await read('public/index.html');
|
||||
const app = await read('public/js/app.js');
|
||||
const geo = await read('public/js/geolocation.js');
|
||||
|
||||
assert.match(html, /id="start-route"[\s\S]*images\/icons\/play\.svg/);
|
||||
assert.match(html, /id="pause-route"[\s\S]*images\/icons\/pause\.svg/);
|
||||
assert.match(html, /id="stop-route"[\s\S]*images\/icons\/stop\.svg/);
|
||||
assert.match(html, /aria-label="Route starten"/);
|
||||
assert.match(html, /aria-label="Route pausieren"/);
|
||||
assert.match(html, /aria-label="Route beenden"/);
|
||||
assert.doesNotMatch(html, /aria-type=/);
|
||||
assert.match(app, /routeState:\s*'idle'/);
|
||||
assert.match(app, /state\.routeState = 'paused'/);
|
||||
assert.match(app, /state\.routeState = 'running'/);
|
||||
assert.match(app, /function endRoute/);
|
||||
assert.match(geo, /navigator\.geolocation\.watchPosition/);
|
||||
assert.match(geo, /navigator\.geolocation\.clearWatch/);
|
||||
});
|
||||
|
||||
test('route control SVG files are present', async () => {
|
||||
for (const name of ['play.svg', 'pause.svg', 'stop.svg']) {
|
||||
const svg = await read(`public/images/icons/${name}`);
|
||||
assert.match(svg, /<svg/);
|
||||
assert.match(svg, /viewBox="0 0 48 48"/);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import test from 'node:test';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
|
||||
async function read(relativePath) {
|
||||
return fs.readFile(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
test('start page keeps nearby recommendations and the complete route list', async () => {
|
||||
const html = await read('public/index.html');
|
||||
const app = await read('public/js/app.js');
|
||||
|
||||
assert.match(html, /id="nearby-route-list"/);
|
||||
assert.match(html, /id="all-route-list"/);
|
||||
assert.match(html, /id="route-search"/);
|
||||
assert.match(app, /renderRouteItems\('#nearby-route-list'/);
|
||||
assert.match(app, /renderRouteItems\(\s*'#all-route-list'/);
|
||||
assert.doesNotMatch(app, /state\.routes\s*=\s*state\.routes[\s\S]*?\.filter\(route => route\.proximityM/);
|
||||
});
|
||||
@@ -9,16 +9,32 @@ async function read(relativePath) {
|
||||
return fs.readFile(path.join(root, relativePath), 'utf8');
|
||||
}
|
||||
|
||||
test('default server socket is the Atlas loopback endpoint', async () => {
|
||||
test('default server socket uses the local Wegwichtel endpoint', async () => {
|
||||
const configSource = await read('src/config.js');
|
||||
const envExample = await read('.env.example');
|
||||
const serverSource = await read('server.js');
|
||||
const apiSource = await read('src/routes/api.js');
|
||||
|
||||
assert.match(configSource, /process\.env\.HOST \|\| '127\.0\.0\.1'/);
|
||||
assert.match(configSource, /process\.env\.PORT \|\| '47145'/);
|
||||
assert.match(configSource, /process\.env\.INSTANCE_NAME \|\| 'Atlas'/);
|
||||
assert.match(envExample, /^HOST=127\.0\.0\.1$/m);
|
||||
assert.match(envExample, /^PORT=47145$/m);
|
||||
assert.match(envExample, /^INSTANCE_NAME=Atlas$/m);
|
||||
assert.match(serverSource, /config\.instanceName/);
|
||||
assert.doesNotMatch(configSource, /instanceName|INSTANCE_NAME/);
|
||||
assert.doesNotMatch(envExample, /INSTANCE_NAME/);
|
||||
assert.match(serverSource, /Wegwichtel läuft auf http:\/\/\$\{config\.host\}:\$\{config\.port\}/);
|
||||
assert.doesNotMatch(apiSource, /instance:/);
|
||||
});
|
||||
|
||||
test('retired internal instance label is absent from the project', async () => {
|
||||
const files = [
|
||||
'.env.example',
|
||||
'README.md',
|
||||
'server.js',
|
||||
'src/config.js',
|
||||
'src/routes/api.js',
|
||||
'public/index.html'
|
||||
];
|
||||
const content = (await Promise.all(files.map(read))).join('\n');
|
||||
const retiredLabel = ['at', 'las'].join('');
|
||||
assert.doesNotMatch(content, new RegExp(retiredLabel, 'i'));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user