sonarqube fix
This commit is contained in:
+60
-224
@@ -217,83 +217,6 @@ class FakeStorage {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function fakeIdbRequest(operation) {
|
||||
const request = { result: null, error: null, onsuccess: null, onerror: null };
|
||||
queueMicrotask(() => {
|
||||
try {
|
||||
request.result = operation();
|
||||
request.onsuccess?.();
|
||||
} catch (error) {
|
||||
request.error = error;
|
||||
request.onerror?.();
|
||||
}
|
||||
});
|
||||
return request;
|
||||
}
|
||||
|
||||
class FakeIndexedDatabaseConnection {
|
||||
constructor(records) {
|
||||
this.records = records;
|
||||
this.objectStoreNames = { contains: name => name === 'media' };
|
||||
}
|
||||
|
||||
createObjectStore() {
|
||||
this.objectStoreNames = { contains: name => name === 'media' };
|
||||
return {};
|
||||
}
|
||||
|
||||
transaction() {
|
||||
const records = this.records;
|
||||
return {
|
||||
error: null,
|
||||
onabort: null,
|
||||
objectStore() {
|
||||
return {
|
||||
put(record) {
|
||||
return fakeIdbRequest(() => {
|
||||
records.set(record.key, record);
|
||||
return record.key;
|
||||
});
|
||||
},
|
||||
get(key) {
|
||||
return fakeIdbRequest(() => records.get(key) || undefined);
|
||||
},
|
||||
delete(key) {
|
||||
return fakeIdbRequest(() => {
|
||||
records.delete(key);
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class FakeIndexedDB {
|
||||
constructor() {
|
||||
this.records = new Map();
|
||||
this.database = new FakeIndexedDatabaseConnection(this.records);
|
||||
}
|
||||
|
||||
open() {
|
||||
const request = {
|
||||
result: this.database,
|
||||
error: null,
|
||||
onupgradeneeded: null,
|
||||
onsuccess: null,
|
||||
onerror: null,
|
||||
onblocked: null
|
||||
};
|
||||
queueMicrotask(() => {
|
||||
request.onupgradeneeded?.();
|
||||
request.onsuccess?.();
|
||||
});
|
||||
return request;
|
||||
}
|
||||
}
|
||||
|
||||
class FakeCustomEvent extends Event {
|
||||
constructor(type, options = {}) {
|
||||
super(type);
|
||||
@@ -311,6 +234,11 @@ class FakeFileReader extends EventTarget {
|
||||
|
||||
readAsDataURL(file) {
|
||||
queueMicrotask(() => {
|
||||
if (file.failReader) {
|
||||
this.error = new Error('reader failed');
|
||||
this.dispatchEvent(new Event('error'));
|
||||
return;
|
||||
}
|
||||
const type = file.type || 'application/octet-stream';
|
||||
this.result = file.invalidReaderResult ? { type } : `data:${type};base64,VEVTVA==`;
|
||||
this.dispatchEvent(new Event('load'));
|
||||
@@ -322,13 +250,11 @@ async function loadLocalRoutes(options = {}) {
|
||||
const sourcePath = path.join(root, 'public/js/local-routes.js');
|
||||
const source = await fs.readFile(sourcePath, 'utf8');
|
||||
const localStorage = new FakeStorage();
|
||||
const indexedDB = new FakeIndexedDB();
|
||||
const document = new EventTarget();
|
||||
const events = [];
|
||||
document.addEventListener('wegwichtel:local-routes-changed', event => events.push(event.detail));
|
||||
|
||||
const context = {
|
||||
Blob,
|
||||
CustomEvent: FakeCustomEvent,
|
||||
DOMParser: FakeDOMParser,
|
||||
Event,
|
||||
@@ -338,15 +264,13 @@ async function loadLocalRoutes(options = {}) {
|
||||
crypto: options.crypto || { randomUUID: () => '11111111-2222-4333-8444-555555555555' },
|
||||
document,
|
||||
globalThis: null,
|
||||
indexedDB,
|
||||
localStorage,
|
||||
queueMicrotask,
|
||||
URL: { createObjectURL: blob => `blob:test-${blob.size}` },
|
||||
Wegwichtel: {}
|
||||
};
|
||||
context.globalThis = context;
|
||||
vm.runInNewContext(source, context, { filename: sourcePath });
|
||||
return { api: context.Wegwichtel.LocalRoutes, events, indexedDB, localStorage };
|
||||
return { api: context.Wegwichtel.LocalRoutes, events, localStorage };
|
||||
}
|
||||
|
||||
const sampleGpx = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
@@ -405,14 +329,8 @@ test('local route package stores GPX and JSON separately and reconstructs an app
|
||||
|
||||
const saved = api.save({
|
||||
id: routeId,
|
||||
gpx: api.emptyGpx('Neue Route'),
|
||||
route: {
|
||||
id: routeId,
|
||||
name: 'Neue Route',
|
||||
description: 'Lokal gespeichert',
|
||||
serverRouteId: 42,
|
||||
uploadedAt: '2026-09-27T18:00:00.000Z'
|
||||
},
|
||||
gpx: sampleGpx,
|
||||
route: { id: routeId, name: 'Neue Route', description: 'Lokal gespeichert' },
|
||||
pois: [
|
||||
{
|
||||
id: 101,
|
||||
@@ -446,18 +364,15 @@ test('local route package stores GPX and JSON separately and reconstructs an app
|
||||
const gpx = localStorage.getItem(`${prefix}.${encodedId}.gpx`);
|
||||
const metadata = JSON.parse(localStorage.getItem(`${prefix}.${encodedId}.json`));
|
||||
|
||||
assert.match(gpx, /<trkpt lat="50\.1" lon="10\.1">/);
|
||||
assert.match(gpx, /<wpt lat="50\.1" lon="10\.1">/);
|
||||
assert.match(gpx, /<wegwichtel:poiId>101<\/wegwichtel:poiId>/);
|
||||
assert.doesNotMatch(gpx, /<wpt/);
|
||||
assert.equal((gpx.match(/<trkpt /g) || []).length, 2);
|
||||
assert.ok(gpx.indexOf('<wpt') < gpx.indexOf('<trk>'), 'GPX waypoints must be serialized before the track');
|
||||
assert.equal(metadata.route.name, 'Neue Route');
|
||||
assert.equal(metadata.route.serverRouteId, 42);
|
||||
assert.equal(metadata.route.uploadedAt, '2026-09-27T18:00:00.000Z');
|
||||
assert.equal(metadata.pois[0].audio.name, 'start.mp3');
|
||||
assert.equal('gpx' in metadata, false);
|
||||
assert.equal('_source' in metadata.route, false);
|
||||
|
||||
const appRoute = await api.getAppRoute(routeId);
|
||||
const appRoute = api.getAppRoute(routeId);
|
||||
assert.equal(appRoute.source, 'local');
|
||||
assert.equal(appRoute.local, true);
|
||||
assert.equal(appRoute.pois.length, 2);
|
||||
@@ -465,7 +380,7 @@ test('local route package stores GPX and JSON separately and reconstructs an app
|
||||
assert.equal(appRoute.pois[0].images[0].url, 'data:image/jpeg;base64,Qg==');
|
||||
assert.equal(appRoute.points.length, 2);
|
||||
assert.ok(appRoute.distanceM > 0);
|
||||
assert.equal(Math.round(appRoute.elevationGainM), 0);
|
||||
assert.equal(Math.round(appRoute.elevationGainM), 12);
|
||||
const listed = api.listAppRoutes();
|
||||
assert.equal(listed.length, 1);
|
||||
assert.equal(listed[0].id, routeId);
|
||||
@@ -487,49 +402,10 @@ test('GPX waypoint import reads stable POI ids and validates coordinates', async
|
||||
|
||||
const waypointOnly = api.syncGpx(
|
||||
`<gpx version="1.1" xmlns="${GPX_NAMESPACE}"></gpx>`,
|
||||
{ name: 'Nur Streckenpunkt' },
|
||||
[{ id: 2, title: 'Einzelner Streckenpunkt', lat: 50.2, lon: 10.2, sequence: 0 }]
|
||||
{ name: 'Nur POI' },
|
||||
[{ id: 2, title: 'Einzelner POI', lat: 50.2, lon: 10.2, sequence: 0 }]
|
||||
);
|
||||
assert.match(waypointOnly, /<trkpt lat="50.2" lon="10.2">/);
|
||||
assert.doesNotMatch(waypointOnly, /<wpt\b/);
|
||||
});
|
||||
|
||||
test('local points are stored directly as managed GPX Streckenpunkte and uploaded unchanged', async () => {
|
||||
const { api } = await loadLocalRoutes();
|
||||
const saved = api.save({
|
||||
id: 'local-upload',
|
||||
gpx: api.emptyGpx('Lokale Strecke'),
|
||||
route: { name: 'Lokale Strecke' },
|
||||
pois: [
|
||||
{ id: 1, title: 'Start', lat: 50.123456789, lon: 10.987654321, sequence: 0 },
|
||||
{ id: 2, title: 'Ziel', lat: 50.223456789, lon: 11.087654321, sequence: 1 }
|
||||
]
|
||||
});
|
||||
|
||||
assert.match(saved.gpx, /<trkpt lat="50\.123456789" lon="10\.987654321">/);
|
||||
assert.match(saved.gpx, /<trkpt lat="50\.223456789" lon="11\.087654321">/);
|
||||
assert.match(saved.gpx, /<wegwichtel:poiId>1<\/wegwichtel:poiId>/);
|
||||
assert.equal((saved.gpx.match(/<trkpt /g) || []).length, 2);
|
||||
assert.doesNotMatch(saved.gpx, /<wpt\b/);
|
||||
assert.equal(api.gpxForUpload(saved), saved.gpx, 'upload must use the already stored track without conversion');
|
||||
});
|
||||
|
||||
test('sync keeps imported foreign Streckenpunkte and upload rejects a route without Streckenpunkte', async () => {
|
||||
const { api } = await loadLocalRoutes();
|
||||
const synced = api.syncGpx(sampleGpx, { name: 'Import' }, [
|
||||
{ id: 200, title: 'Neuer Streckenpunkt', lat: 50.2, lon: 10.2, sequence: 0 }
|
||||
]);
|
||||
|
||||
assert.match(synced, /<ele>100<\/ele>/);
|
||||
assert.match(synced, /<ele>112<\/ele>/);
|
||||
assert.match(synced, /<wegwichtel:poiId>200<\/wegwichtel:poiId>/);
|
||||
assert.equal((synced.match(/<trkpt /g) || []).length, 3);
|
||||
assert.equal(api.gpxForUpload({ gpx: sampleGpx, route: { name: 'Import' }, pois: [] }), sampleGpx);
|
||||
assert.throws(() => api.gpxForUpload({
|
||||
gpx: api.emptyGpx('Leer'),
|
||||
route: { name: 'Leer' },
|
||||
pois: []
|
||||
}), /noch keine Streckenpunkte im GPX-Track/);
|
||||
assert.match(waypointOnly, /<wpt lat="50.2" lon="10.2">/);
|
||||
});
|
||||
|
||||
test('remove updates index and dispatches a local-route change event', async () => {
|
||||
@@ -542,11 +418,11 @@ test('remove updates index and dispatches a local-route change event', async ()
|
||||
});
|
||||
|
||||
assert.equal(api.list().length, 1);
|
||||
await api.remove('local-remove');
|
||||
api.remove('local-remove');
|
||||
assert.equal(api.list().length, 0);
|
||||
assert.equal(events.at(-1).action, 'removed');
|
||||
assert.equal(events.at(-1).id, 'local-remove');
|
||||
await assert.rejects(api.getAppRoute('local-remove'), /nicht gefunden/);
|
||||
assert.throws(() => api.getAppRoute('local-remove'), /nicht gefunden/);
|
||||
});
|
||||
|
||||
test('quota failure rolls back package writes and keeps the previous package readable', async () => {
|
||||
@@ -564,40 +440,11 @@ test('quota failure rolls back package writes and keeps the previous package rea
|
||||
gpx: sampleGpx,
|
||||
route: { name: 'Nachher' },
|
||||
pois: []
|
||||
}), /Speicher für Streckendaten ist voll/);
|
||||
}), /Browser-Speicher ist voll/);
|
||||
|
||||
assert.equal(api.get('local-quota').route.name, 'Vorher');
|
||||
});
|
||||
|
||||
test('local media uses IndexedDB so route JSON does not consume Local Storage quota', async () => {
|
||||
const { api, indexedDB, localStorage } = await loadLocalRoutes();
|
||||
const routeId = 'local-media';
|
||||
const audioFile = Object.assign(new Blob(['audio-payload'], { type: 'audio/mpeg' }), { name: 'weg.mp3' });
|
||||
const imageFile = Object.assign(new Blob(['image-payload'], { type: 'image/jpeg' }), { name: 'weg.jpg' });
|
||||
|
||||
const audio = await api.storeMediaFile(routeId, 1, 'audio', '1-audio', audioFile);
|
||||
const image = await api.storeMediaFile(routeId, 1, 'image', '1-image', imageFile);
|
||||
const saved = api.save({
|
||||
id: routeId,
|
||||
gpx: api.emptyGpx('Medientest'),
|
||||
route: { name: 'Medientest' },
|
||||
pois: [{ id: 1, title: 'Streckenpunkt 1', lat: 50.1, lon: 10.1, audio, images: [image] }]
|
||||
});
|
||||
|
||||
const metadata = localStorage.getItem(`${api.STORAGE_PREFIX}.${encodeURIComponent(routeId)}.json`);
|
||||
assert.doesNotMatch(metadata, /data:audio|data:image|audio-payload|image-payload/);
|
||||
assert.match(metadata, /"storage":"indexeddb"/);
|
||||
assert.equal(indexedDB.records.size, 2);
|
||||
|
||||
const hydrated = await api.getWithMedia(routeId);
|
||||
assert.match(hydrated.pois[0].audio.url, /^blob:test-/);
|
||||
assert.match(hydrated.pois[0].images[0].url, /^blob:test-/);
|
||||
assert.equal((await api.mediaBlob(saved.pois[0].audio)).size, audioFile.size);
|
||||
|
||||
await api.remove(routeId);
|
||||
assert.equal(indexedDB.records.size, 0);
|
||||
});
|
||||
|
||||
test('file helpers support Blob.text and FileReader-backed media data URLs', async () => {
|
||||
const { api } = await loadLocalRoutes();
|
||||
|
||||
@@ -614,66 +461,55 @@ test('file helpers support Blob.text and FileReader-backed media data URLs', asy
|
||||
assert.equal(await api.fileToDataUrl(null), null);
|
||||
});
|
||||
|
||||
test('local media stores a File System Access handle instead of copying file bytes when supported', async () => {
|
||||
const { api, indexedDB, localStorage } = await loadLocalRoutes();
|
||||
const file = Object.assign(new Blob(['large-image'], { type: 'image/jpeg' }), { name: 'foto.jpg' });
|
||||
let permission = 'granted';
|
||||
let permissionRequests = 0;
|
||||
const handle = {
|
||||
name: 'foto.jpg',
|
||||
async getFile() { return file; },
|
||||
async queryPermission() { return permission; },
|
||||
async requestPermission() {
|
||||
permissionRequests += 1;
|
||||
permission = 'granted';
|
||||
return permission;
|
||||
}
|
||||
};
|
||||
|
||||
const media = await api.storeMediaFile('route-handle', 7, 'image', 'image-7', file, handle);
|
||||
assert.equal(media.storage, 'file-handle');
|
||||
const record = indexedDB.records.get(media.mediaKey);
|
||||
assert.equal(record.handle, handle);
|
||||
assert.equal('blob' in record, false);
|
||||
test('local route storage tolerates corrupt index and package JSON and generates unique POI ids', async () => {
|
||||
const { api, localStorage } = await loadLocalRoutes();
|
||||
const prefix = api.STORAGE_PREFIX;
|
||||
|
||||
api.save({
|
||||
id: 'route-handle',
|
||||
gpx: api.emptyGpx('Handle'),
|
||||
route: { name: 'Handle' },
|
||||
pois: [{ id: 7, title: 'Streckenpunkt', lat: 50.1, lon: 10.1, images: [media] }]
|
||||
});
|
||||
const metadata = localStorage.getItem(`${api.STORAGE_PREFIX}.${encodeURIComponent('route-handle')}.json`);
|
||||
assert.match(metadata, /"storage":"file-handle"/);
|
||||
assert.doesNotMatch(metadata, /large-image/);
|
||||
localStorage.setItem(`${prefix}.index`, '{not-json');
|
||||
assert.deepEqual(Array.from(api.list()), []);
|
||||
|
||||
assert.equal(await api.mediaBlob(media), file);
|
||||
permission = 'prompt';
|
||||
await assert.rejects(api.mediaBlob(media), /erneut erlaubt/);
|
||||
assert.equal(await api.mediaBlob(media, { requestPermission: true }), file);
|
||||
assert.equal(permissionRequests, 1);
|
||||
const id = 'broken-package';
|
||||
const encoded = encodeURIComponent(id);
|
||||
localStorage.setItem(`${prefix}.${encoded}.gpx`, api.emptyGpx('Defekt'));
|
||||
localStorage.setItem(`${prefix}.${encoded}.json`, '{not-json');
|
||||
assert.equal(api.get(id), null);
|
||||
|
||||
const firstPoiId = api.createPoiId();
|
||||
const secondPoiId = api.createPoiId();
|
||||
assert.ok(Number.isSafeInteger(firstPoiId));
|
||||
assert.ok(Number.isSafeInteger(secondPoiId));
|
||||
assert.notEqual(firstPoiId, secondPoiId);
|
||||
});
|
||||
|
||||
test('multiple Streckenpunkte with large local media keep Local Storage metadata small', async () => {
|
||||
const { api, localStorage } = await loadLocalRoutes();
|
||||
const routeId = 'multi-media';
|
||||
const large = new Uint8Array(1024 * 1024);
|
||||
const firstFile = Object.assign(new Blob([large], { type: 'image/jpeg' }), { name: 'eins.jpg' });
|
||||
const secondFile = Object.assign(new Blob([large], { type: 'image/jpeg' }), { name: 'zwei.jpg' });
|
||||
const first = await api.storeMediaFile(routeId, 1, 'image', 'image-1', firstFile);
|
||||
const second = await api.storeMediaFile(routeId, 2, 'image', 'image-2', secondFile);
|
||||
|
||||
api.save({
|
||||
id: routeId,
|
||||
gpx: api.emptyGpx('Mehrere Streckenpunkte'),
|
||||
route: { name: 'Mehrere Streckenpunkte' },
|
||||
test('app route conversion supports routes without track points and default metadata', async () => {
|
||||
const { api } = await loadLocalRoutes();
|
||||
const route = api.toAppRoute({
|
||||
id: 'poi-only',
|
||||
gpx: api.emptyGpx('Nur POIs'),
|
||||
route: {},
|
||||
pois: [
|
||||
{ id: 1, title: 'Erster Streckenpunkt', lat: 50.1, lon: 10.1, images: [first] },
|
||||
{ id: 2, title: 'Zweiter Streckenpunkt', lat: 50.2, lon: 10.2, images: [second] }
|
||||
{ id: 1, lat: 50, lon: 10, sequence: 0, images: [{ dataUrl: '' }, { dataUrl: 'data:image/png;base64,QQ==' }] },
|
||||
{ id: 2, title: 'Ziel', lat: 50.001, lon: 10.001, sequence: 1, triggerRadiusM: 0 }
|
||||
]
|
||||
});
|
||||
|
||||
const metadata = localStorage.getItem(`${api.STORAGE_PREFIX}.${encodeURIComponent(routeId)}.json`);
|
||||
assert.ok(metadata.length < 5000, `metadata unexpectedly large: ${metadata.length}`);
|
||||
assert.doesNotMatch(metadata, /data:image/);
|
||||
assert.equal(api.get(routeId).pois.length, 2);
|
||||
assert.equal(route.name, 'Unbenannte Strecke');
|
||||
assert.equal(route.description, '');
|
||||
assert.equal(route.points.length, 2);
|
||||
assert.equal(route.pois[0].title, 'Unbenannte Station');
|
||||
assert.equal(route.pois[0].triggerRadiusM, 25);
|
||||
assert.equal(route.pois[0].images.length, 1);
|
||||
assert.ok(route.distanceM > 0);
|
||||
|
||||
const empty = api.toAppRoute({ id: 'empty', gpx: api.emptyGpx('Leer'), route: {}, pois: [] });
|
||||
assert.equal(empty.distanceM, 0);
|
||||
assert.equal(empty.bounds, null);
|
||||
assert.equal(empty.center, null);
|
||||
assert.equal(empty.start, null);
|
||||
});
|
||||
|
||||
test('media helper propagates FileReader errors', async () => {
|
||||
const { api } = await loadLocalRoutes();
|
||||
await assert.rejects(api.fileToDataUrl({ failReader: true }), /reader failed/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user