Downgrade css to prevent older browsers displaying faulty views
Some checks failed
Sonarqube Scanner / Build and analyze (push) Failing after 1m36s
Some checks failed
Sonarqube Scanner / Build and analyze (push) Failing after 1m36s
This commit is contained in:
parent
7760ae4f49
commit
fa9ab1aa14
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.12.11",
|
"version": "0.12.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.12.11",
|
"version": "0.12.12",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@file-type/av": "0.2.0",
|
"@file-type/av": "0.2.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "wegwichtel-next",
|
"name": "wegwichtel-next",
|
||||||
"version": "0.12.11",
|
"version": "0.12.12",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.",
|
"description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@ -68,12 +68,12 @@ button,
|
|||||||
box-shadow: 0 .15rem .5rem rgba(0, 0, 0, .2);
|
box-shadow: 0 .15rem .5rem rgba(0, 0, 0, .2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-page > header:has(.back-button) {
|
.app-page > header.has-back-button {
|
||||||
grid-template-columns: var(--icon-button-size) minmax(0, 1fr) var(--icon-button-size);
|
grid-template-columns: var(--icon-button-size) minmax(0, 1fr) var(--icon-button-size);
|
||||||
gap: .5rem;
|
gap: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-page > header:has(.back-button)::after {
|
.app-page > header.has-back-button::after {
|
||||||
content: "";
|
content: "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,9 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="theme-color" content="#315d3a">
|
<meta name="theme-color" content="#315d3a">
|
||||||
<title>Wegwichtel</title>
|
<title>Wegwichtel</title>
|
||||||
<link rel="stylesheet" href="css/bootstrap.css?v=0.12.11">
|
<link rel="stylesheet" href="css/bootstrap.css?v=0.12.12">
|
||||||
|
<link rel="stylesheet" href="vendor/jquery-ui/jquery-ui-1.14.2.min.css?v=0.12.12">
|
||||||
|
<link rel="stylesheet" href="css/app.css?v=0.12.12">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="bootstrap-screen" aria-live="polite" aria-busy="true">
|
<div id="bootstrap-screen" aria-live="polite" aria-busy="true">
|
||||||
@ -41,7 +43,9 @@
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Routen in deiner Nähe</h2>
|
<h2>Routen in deiner Nähe</h2>
|
||||||
<ul id="nearby-route-list" class="route-list" aria-live="polite"></ul>
|
<ul id="nearby-route-list" class="route-list" aria-live="polite">
|
||||||
|
<li><p>Routen werden geladen …</p></li>
|
||||||
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@ -58,14 +62,16 @@
|
|||||||
<button id="clear-route-search" type="button">Suche leeren</button>
|
<button id="clear-route-search" type="button">Suche leeren</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p id="all-routes-summary" aria-live="polite"></p>
|
<p id="all-routes-summary" aria-live="polite">Routen werden geladen …</p>
|
||||||
<ul id="all-route-list" class="route-list" aria-live="polite"></ul>
|
<ul id="all-route-list" class="route-list" aria-live="polite">
|
||||||
|
<li><p>Routen werden geladen …</p></li>
|
||||||
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="route-page" class="app-page" hidden>
|
<section id="route-page" class="app-page" hidden>
|
||||||
<header>
|
<header class="has-back-button">
|
||||||
<button class="back-button" type="button" data-page-target="#routes-page" aria-label="Zurück zur Routenauswahl" title="Zurück zur Routenauswahl">
|
<button class="back-button" type="button" data-page-target="#routes-page" aria-label="Zurück zur Routenauswahl" title="Zurück zur Routenauswahl">
|
||||||
<img src="images/icons/back.svg" alt="">
|
<img src="images/icons/back.svg" alt="">
|
||||||
</button>
|
</button>
|
||||||
@ -101,7 +107,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="poi-page" class="app-page" hidden>
|
<section id="poi-page" class="app-page" hidden>
|
||||||
<header>
|
<header class="has-back-button">
|
||||||
<button class="back-button" type="button" data-page-target="#route-page" aria-label="Zurück zur Route" title="Zurück zur Route">
|
<button class="back-button" type="button" data-page-target="#route-page" aria-label="Zurück zur Route" title="Zurück zur Route">
|
||||||
<img src="images/icons/back.svg" alt="">
|
<img src="images/icons/back.svg" alt="">
|
||||||
</button>
|
</button>
|
||||||
@ -145,6 +151,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="js/bootstrap-loader.js?v=0.12.11"></script>
|
<script src="js/bootstrap-loader.js?v=0.12.12"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -66,12 +66,13 @@
|
|||||||
|
|
||||||
function renderRouteItems(listSelector, routes, emptyText) {
|
function renderRouteItems(listSelector, routes, emptyText) {
|
||||||
const list = $(listSelector).empty();
|
const list = $(listSelector).empty();
|
||||||
if (!routes.length) {
|
const items = Array.isArray(routes) ? routes.filter(Boolean) : [];
|
||||||
|
if (!items.length) {
|
||||||
list.append(`<li><p>${escapeHtml(emptyText)}</p></li>`);
|
list.append(`<li><p>${escapeHtml(emptyText)}</p></li>`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
routes.forEach(route => {
|
items.forEach(route => {
|
||||||
const proximity = route.proximityM == null
|
const proximity = route.proximityM == null
|
||||||
? ''
|
? ''
|
||||||
: `<small>${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</small>`;
|
: `<small>${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</small>`;
|
||||||
@ -89,18 +90,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderRoutes() {
|
function renderRoutes() {
|
||||||
|
const routes = Array.isArray(state.routes) ? state.routes.filter(Boolean) : [];
|
||||||
const radiusM = ns.Config.routeRadiusKm * 1000;
|
const radiusM = ns.Config.routeRadiusKm * 1000;
|
||||||
const nearbyRoutes = state.position
|
const nearbyRoutes = state.position
|
||||||
? state.routes
|
? routes
|
||||||
.filter(route => route.proximityM != null && route.proximityM <= radiusM)
|
.filter(route => route.proximityM != null && route.proximityM <= radiusM)
|
||||||
|
.slice()
|
||||||
.sort((a, b) => a.proximityM - b.proximityM)
|
.sort((a, b) => a.proximityM - b.proximityM)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
const searchTerm = normalizeSearch($('#route-search').val());
|
const searchTerm = normalizeSearch($('#route-search').val());
|
||||||
const allRoutes = state.routes
|
const allRoutes = routes
|
||||||
.filter(route => routeMatchesSearch(route, searchTerm))
|
.filter(route => routeMatchesSearch(route, searchTerm))
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.name.localeCompare(b.name, 'de'));
|
.sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
|
||||||
|
|
||||||
const nearbyEmptyText = state.position
|
const nearbyEmptyText = state.position
|
||||||
? `Keine Route im Umkreis von ${ns.Config.routeRadiusKm} km gefunden. Wähle unten eine beliebige Route aus.`
|
? `Keine Route im Umkreis von ${ns.Config.routeRadiusKm} km gefunden. Wähle unten eine beliebige Route aus.`
|
||||||
@ -115,13 +118,15 @@
|
|||||||
|
|
||||||
$('#all-routes-summary').text(
|
$('#all-routes-summary').text(
|
||||||
searchTerm
|
searchTerm
|
||||||
? `${allRoutes.length} von ${state.routes.length} Routen gefunden.`
|
? `${allRoutes.length} von ${routes.length} Routen gefunden.`
|
||||||
: `${state.routes.length} Routen verfügbar.`
|
: `${routes.length} Routen verfügbar.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeRouteSearch() {
|
function initializeRouteSearch() {
|
||||||
const input = $('#route-search');
|
const input = $('#route-search');
|
||||||
|
if (!input.length || typeof input.autocomplete !== 'function') return;
|
||||||
|
|
||||||
input.autocomplete({
|
input.autocomplete({
|
||||||
minLength: 0,
|
minLength: 0,
|
||||||
source(request, response) {
|
source(request, response) {
|
||||||
@ -498,6 +503,43 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function routesFromResponse(response) {
|
||||||
|
if (!Array.isArray(response?.routes)) {
|
||||||
|
throw new Error('Die Routen-API hat keine gültige Routenliste geliefert.');
|
||||||
|
}
|
||||||
|
return response.routes.filter(route => route && route.id != null && route.name != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRouteProximity(position) {
|
||||||
|
state.routes = state.routes.map(route => ({
|
||||||
|
...route,
|
||||||
|
proximityM: route.start ? ns.Distance.meters(position, route.start) : null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeLocation(context) {
|
||||||
|
try {
|
||||||
|
const position = await ns.Geo.current();
|
||||||
|
state.position = position;
|
||||||
|
updateRouteProximity(position);
|
||||||
|
$('#location-hint').text(
|
||||||
|
`Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m). `
|
||||||
|
+ `Nahe Routen werden bis ${ns.Config.routeRadiusKm} km empfohlen.`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
$('#location-hint').text(
|
||||||
|
`Standort nicht verfügbar: ${error.message}. Alle Routen können weiterhin ausgewählt werden.`
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
context.markStep('location', 'done');
|
||||||
|
renderRoutes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreRouteLists() {
|
||||||
|
if (state.routes.length) renderRoutes();
|
||||||
|
}
|
||||||
|
|
||||||
function bindEvents() {
|
function bindEvents() {
|
||||||
$(document).on('input', '#route-search', renderRoutes);
|
$(document).on('input', '#route-search', renderRoutes);
|
||||||
|
|
||||||
@ -546,6 +588,11 @@
|
|||||||
const page = event.state?.page || '#routes-page';
|
const page = event.state?.page || '#routes-page';
|
||||||
navigate(page, { updateHistory: false, instant: true });
|
navigate(page, { updateHistory: false, instant: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
globalThis.addEventListener('pageshow', restoreRouteLists);
|
||||||
|
document.addEventListener('visibilitychange', () => {
|
||||||
|
if (document.visibilityState === 'visible') restoreRouteLists();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ns.App = {
|
ns.App = {
|
||||||
@ -557,31 +604,15 @@
|
|||||||
context.markStep('server', 'done');
|
context.markStep('server', 'done');
|
||||||
|
|
||||||
const response = await ns.Api.routes(null);
|
const response = await ns.Api.routes(null);
|
||||||
state.routes = response.routes;
|
state.routes = routesFromResponse(response);
|
||||||
context.markStep('routes', 'done');
|
context.markStep('routes', 'done');
|
||||||
|
|
||||||
try {
|
|
||||||
const position = await ns.Geo.current();
|
|
||||||
state.position = position;
|
|
||||||
$('#location-hint').text(
|
|
||||||
`Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m). `
|
|
||||||
+ `Nahe Routen werden bis ${ns.Config.routeRadiusKm} km empfohlen.`
|
|
||||||
);
|
|
||||||
state.routes = state.routes.map(route => ({
|
|
||||||
...route,
|
|
||||||
proximityM: route.start ? ns.Distance.meters(position, route.start) : null
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
|
||||||
$('#location-hint').text(
|
|
||||||
`Standort nicht verfügbar: ${error.message}. Alle Routen können weiterhin ausgewählt werden.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.markStep('location', 'done');
|
|
||||||
initializeRouteSearch();
|
|
||||||
renderRoutes();
|
renderRoutes();
|
||||||
|
initializeRouteSearch();
|
||||||
updateRouteControls();
|
updateRouteControls();
|
||||||
hideNavigation();
|
hideNavigation();
|
||||||
|
|
||||||
|
void initializeLocation(context);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}(globalThis.Wegwichtel, globalThis.jQuery));
|
}(globalThis.Wegwichtel, globalThis.jQuery));
|
||||||
|
|||||||
15
public/js/bootstrap-loader.js
vendored
15
public/js/bootstrap-loader.js
vendored
@ -1,7 +1,7 @@
|
|||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const versions = Object.freeze({ app: '0.12.11', jquery: '4.0.0', jqueryUi: '1.14.2' });
|
const versions = Object.freeze({ app: '0.12.12', jquery: '4.0.0', jqueryUi: '1.14.2' });
|
||||||
const steps = ['jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location'];
|
const steps = ['jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location'];
|
||||||
const progress = document.getElementById('bootstrap-progress');
|
const progress = document.getElementById('bootstrap-progress');
|
||||||
const errorBox = document.getElementById('bootstrap-error');
|
const errorBox = document.getElementById('bootstrap-error');
|
||||||
@ -31,17 +31,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadStyle(href) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const link = document.createElement('link');
|
|
||||||
link.rel = 'stylesheet';
|
|
||||||
link.href = versioned(href);
|
|
||||||
link.onload = resolve;
|
|
||||||
link.onerror = () => reject(new Error(`Stylesheet konnte nicht geladen werden: ${href}`));
|
|
||||||
document.head.appendChild(link);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeJqueryUi() {
|
function initializeJqueryUi() {
|
||||||
const $ = globalThis.jQuery;
|
const $ = globalThis.jQuery;
|
||||||
$('#bootstrap-retry').button({ icon: 'ui-icon-refresh' });
|
$('#bootstrap-retry').button({ icon: 'ui-icon-refresh' });
|
||||||
@ -62,8 +51,6 @@
|
|||||||
);
|
);
|
||||||
mark('jquery', 'done');
|
mark('jquery', 'done');
|
||||||
|
|
||||||
await loadStyle(`vendor/jquery-ui/jquery-ui-${versions.jqueryUi}.min.css`);
|
|
||||||
await loadStyle('css/app.css');
|
|
||||||
await loadScript(
|
await loadScript(
|
||||||
`vendor/jquery-ui/jquery-ui-${versions.jqueryUi}.min.js`,
|
`vendor/jquery-ui/jquery-ui-${versions.jqueryUi}.min.js`,
|
||||||
() => globalThis.jQuery?.ui?.version === versions.jqueryUi
|
() => globalThis.jQuery?.ui?.version === versions.jqueryUi
|
||||||
|
|||||||
16
server.js
16
server.js
@ -10,6 +10,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||||||
ensureRuntimeDirectories();
|
ensureRuntimeDirectories();
|
||||||
const db = openDatabase();
|
const db = openDatabase();
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const publicDirectory = path.join(__dirname, 'public');
|
||||||
|
|
||||||
app.disable('x-powered-by');
|
app.disable('x-powered-by');
|
||||||
app.use(express.json({
|
app.use(express.json({
|
||||||
@ -18,17 +19,22 @@ app.use(express.json({
|
|||||||
}));
|
}));
|
||||||
app.use(express.urlencoded({ extended: true, limit: '2mb' }));
|
app.use(express.urlencoded({ extended: true, limit: '2mb' }));
|
||||||
app.use('/api', createApiRouter(db));
|
app.use('/api', createApiRouter(db));
|
||||||
app.use(express.static(path.join(__dirname, 'public'), {
|
app.use(express.static(publicDirectory, {
|
||||||
extensions: ['html'],
|
extensions: ['html'],
|
||||||
etag: true,
|
etag: true,
|
||||||
lastModified: true,
|
lastModified: true,
|
||||||
maxAge: 0,
|
maxAge: 0,
|
||||||
setHeaders(res, filePath) {
|
setHeaders(res, filePath) {
|
||||||
if (filePath.endsWith('.html')) {
|
const extension = path.extname(filePath).toLowerCase();
|
||||||
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
const authoredAsset = ['.html', '.css', '.js'].includes(extension);
|
||||||
return;
|
res.setHeader(
|
||||||
|
'Cache-Control',
|
||||||
|
authoredAsset ? 'no-cache, no-store, must-revalidate' : 'public, max-age=0, must-revalidate'
|
||||||
|
);
|
||||||
|
if (authoredAsset) {
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
}
|
}
|
||||||
res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate');
|
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
app.use(notFoundHandler);
|
app.use(notFoundHandler);
|
||||||
|
|||||||
@ -32,6 +32,13 @@ function sendFileResource(res, file, contentType = null) {
|
|||||||
export function createApiRouter(db) {
|
export function createApiRouter(db) {
|
||||||
const api = Router();
|
const api = Router();
|
||||||
|
|
||||||
|
api.use((req, res, next) => {
|
||||||
|
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
|
||||||
|
res.setHeader('Pragma', 'no-cache');
|
||||||
|
res.setHeader('Expires', '0');
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
api.get('/health', (req, res) => {
|
api.get('/health', (req, res) => {
|
||||||
const sqliteVersion = db.prepare('SELECT sqlite_version() AS version').get().version;
|
const sqliteVersion = db.prepare('SELECT sqlite_version() AS version').get().version;
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@ -138,6 +138,18 @@ test('the npm-started local API is exercised through a write-auth proxy on loopb
|
|||||||
const healthResponse = await fetch(`${baseUrl}/api/health`);
|
const healthResponse = await fetch(`${baseUrl}/api/health`);
|
||||||
assert.equal(healthResponse.status, 200);
|
assert.equal(healthResponse.status, 200);
|
||||||
assert.equal(healthResponse.headers.get('www-authenticate'), null);
|
assert.equal(healthResponse.headers.get('www-authenticate'), null);
|
||||||
|
assert.match(healthResponse.headers.get('cache-control') || '', /no-store/);
|
||||||
|
|
||||||
|
const indexResponse = await fetch(`${baseUrl}/`);
|
||||||
|
const indexHtml = await indexResponse.text();
|
||||||
|
assert.equal(indexResponse.status, 200);
|
||||||
|
assert.match(indexResponse.headers.get('cache-control') || '', /no-store/);
|
||||||
|
assert.match(indexHtml, /css\/app\.css\?v=0\.12\.12/);
|
||||||
|
|
||||||
|
const appStyleResponse = await fetch(`${baseUrl}/css/app.css?v=0.12.12`);
|
||||||
|
assert.equal(appStyleResponse.status, 200);
|
||||||
|
assert.match(appStyleResponse.headers.get('cache-control') || '', /no-store/);
|
||||||
|
await appStyleResponse.arrayBuffer();
|
||||||
|
|
||||||
const pythonHealthCheck = await runProcess('python3', ['-c', `
|
const pythonHealthCheck = await runProcess('python3', ['-c', `
|
||||||
from common import ApiClient
|
from common import ApiClient
|
||||||
|
|||||||
@ -20,3 +20,28 @@ test('start page keeps nearby recommendations and the complete route list', asyn
|
|||||||
assert.match(app, /renderRouteItems\(\s*'#all-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/);
|
assert.doesNotMatch(app, /state\.routes\s*=\s*state\.routes[\s\S]*?\.filter\(route => route\.proximityM/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('route selection renders independently from geolocation and restores after mobile page resume', async () => {
|
||||||
|
const html = await read('public/index.html');
|
||||||
|
const app = await read('public/js/app.js');
|
||||||
|
const loader = await read('public/js/bootstrap-loader.js');
|
||||||
|
const css = await read('public/css/app.css');
|
||||||
|
const server = await read('server.js');
|
||||||
|
const api = await read('src/routes/api.js');
|
||||||
|
|
||||||
|
assert.match(html, /vendor\/jquery-ui\/jquery-ui-1\.14\.2\.min\.css\?v=0\.12\.12/);
|
||||||
|
assert.match(html, /css\/app\.css\?v=0\.12\.12/);
|
||||||
|
assert.match(html, /id="all-routes-summary"[^>]*>Routen werden geladen/);
|
||||||
|
assert.doesNotMatch(loader, /loadStyle\(/);
|
||||||
|
assert.doesNotMatch(css, /:has\(/);
|
||||||
|
assert.match(html, /<header class="has-back-button">/);
|
||||||
|
|
||||||
|
const firstRender = app.indexOf('renderRoutes();', app.indexOf('async initialize(context)'));
|
||||||
|
const locationStart = app.indexOf('void initializeLocation(context);', app.indexOf('async initialize(context)'));
|
||||||
|
assert.ok(firstRender >= 0 && locationStart > firstRender, 'Routen müssen vor der Standortabfrage gerendert werden.');
|
||||||
|
assert.match(app, /globalThis\.addEventListener\('pageshow', restoreRouteLists\)/);
|
||||||
|
assert.match(app, /document\.visibilityState === 'visible'/);
|
||||||
|
assert.match(app, /Array\.isArray\(response\?\.routes\)/);
|
||||||
|
assert.match(server, /no-cache, no-store, must-revalidate/);
|
||||||
|
assert.match(api, /api\.use\(\(req, res, next\)/);
|
||||||
|
});
|
||||||
|
|||||||
@ -44,3 +44,13 @@ test('retired internal instance label is absent from authored project files', as
|
|||||||
const retiredLabel = ['at', 'las'].join('');
|
const retiredLabel = ['at', 'las'].join('');
|
||||||
assert.doesNotMatch(content, new RegExp(retiredLabel, 'i'));
|
assert.doesNotMatch(content, new RegExp(retiredLabel, 'i'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('runtime route directories keep their repository placeholders', async () => {
|
||||||
|
for (const relativePath of [
|
||||||
|
'storage/active/routes/.gitkeep',
|
||||||
|
'storage/trash/routes/.gitkeep'
|
||||||
|
]) {
|
||||||
|
const stat = await fs.stat(path.join(root, relativePath));
|
||||||
|
assert.ok(stat.isFile(), `${relativePath} fehlt`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user