Files
gpx.studio/website/src/lib/state/setting.test.ts
T
vcoppeandvcoppe 8fe7d8d64d Migration to new Rust-based engine (#401)
* wasm first steps

* parsing start

* parsing continued

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* progress

* fixes

* reorganize

* change type

* fix

* progress

* refactoring

* progress

* progress

* progress

* keep style info

* progress

* progress

* remove (copy) in name

* progress

* merge methods into 1

* splice

* progress

* progress

* progress

* cleaning

* progress

* progress

* progress

* visibility

* progress

* progress

* progress

* progress

* progress

* fixes

* progress

* fix markers

* fix

* progress

* progress

* progress

* progress

* progress

* progress

* small fix

* use engine info

* fix popups

* small fix

* progress

* progress

* progress

* clean tool

* handle self closing elements

* reduce data exchanges across boundary

* copy on wasm side to avoid any data corruption

* speed fix

* routing

* refactoring

* reverse

* reverse

* crop

* split

* merge

* progress

* time tool

* fix bounds for waypoints

* reduce

* fix reactivity

* handle corrupt time data

* export

* progress

* remove gpx js package

* remove todo

* update readme

* cleaning

* progress

* fix link fields

* remove unused fields

* refactoring and tests

* import and export of routes, ref #192

* per-chunk categories storage

* cross-tab sync

* outdated state

* refactoring

* reorder methods

* refactoring

* fix

* improve flow

* renaming

* progress

* progress

* fix

* progress

* per file history

* keep original flag

* use history variant

* fix

* progress

* file explorer

* progress

* fix

* sticky folders

* improved sticky folders

* fix sortable

* fix sortable

* fix

* fix tab size

* no file delete on desktop

* fix routing on other file

* fix sorting

* remember language

* open links in browser

* commit missing changes

* avoid selecting on resize

* clarify filename

* explorer actions

* fix folder styling

* drop anywhere in folder

* cleaning

* fixes

* remove focus-ring

* full screen

* small fix

* consistent highlight color

* improved preview

* performance improvements

* sparser data transfers

* sparser data transfers

* show popup once data is loaded

* fix routing control size

* fix

* sparse desktop build

* remove custom ign styles

* use new endpoint for openhikingmap

* strip native menu

* reorder context menu items

* route full support

* route - track conversion

* create route file action

* route export, filter out anchors at high zoom level

* clarify name handling

* move hide & center to view

* routes continued

* update docs

* fix power format, closes #378

* lint fixes

* fix display of additional data with gaps, closes #361

* fix linting issues

* fix import modules

* fix multidrag

* change base to resolve

* fix type errors

* fix layer typings

* fix last type error

* each keys

* improve typings

* deploy to cloudflare

---------

Co-authored-by: vcoppe <vcoppe@hexaly.com>
2026-10-31 18:08:22 +01:00

314 lines
11 KiB
TypeScript

import assert from 'node:assert/strict';
import { afterEach, beforeEach, describe, it } from 'node:test';
import { get } from 'svelte/store';
import { isCustomLayer, type LayerTreeType } from '$lib/data/layers';
import {
filterLayerTree,
getArrayValidator,
getLayerTreeValidator,
getLayerValidator,
getListValidator,
getRecordValidator,
getValueValidator,
isNumber,
isString,
Setting,
SettingInitOnFirstRead,
storedValue,
useSettingsStorage,
} from '$lib/state/setting';
/** What the settings were told to keep. */
let saved: Record<string, string | undefined>;
beforeEach(() => {
saved = {};
useSettingsStorage({
save: (key, json) => {
saved[key] = json;
},
remove: (key) => {
saved[key] = undefined;
},
});
});
afterEach(() => useSettingsStorage(undefined));
describe('reading saved values', () => {
it('reads JSON, and nothing when it cannot', () => {
assert.deepEqual(storedValue({ a: '3' }, 'a'), { value: 3 });
assert.deepEqual(storedValue({ a: '"text"' }, 'a'), { value: 'text' });
assert.deepEqual(storedValue({ a: 'null' }, 'a'), { value: null });
assert.equal(storedValue({ a: '{broken' }, 'a'), undefined);
assert.equal(storedValue({}, 'a'), undefined);
// a key of Object.prototype is not a saved key
assert.equal(storedValue({}, 'toString'), undefined);
});
});
describe('validators', () => {
it('keeps an allowed value, else the fallback', () => {
const validate = getValueValidator(['metric', 'imperial'], 'metric');
assert.equal(validate('imperial'), 'imperial');
assert.equal(validate('nautical'), 'metric');
assert.equal(validate(undefined), 'metric');
assert.equal(validate(null), 'metric');
assert.equal(validate(3), 'metric');
const optional = getValueValidator<string | undefined>(['slope', undefined], undefined);
assert.equal(optional('slope'), 'slope');
assert.equal(optional('other'), undefined);
});
it('keeps the allowed values of a list', () => {
const validate = getArrayValidator(['hr', 'cad', 'power']);
assert.deepEqual(validate(['power', 'speed', 'hr', 'hr']), ['power', 'hr', 'hr']);
assert.deepEqual(validate([]), []);
});
it('keeps nothing of a saved value that is not a list', () => {
const validate = getArrayValidator(['hr', 'cad', 'power']);
for (const saved of [null, 'hr', 3, { 0: 'hr' }]) {
assert.deepEqual(validate(saved), []);
}
});
it('keeps the items of a list that are what it holds', () => {
const validate = getListValidator(isString);
assert.deepEqual(validate(['a', 3, null, 'b']), ['a', 'b']);
assert.deepEqual(validate('a'), []);
});
it('keeps the entries of a record that are what it holds, else takes the fallback', () => {
const fallback = { a: 0.5 };
const validate = getRecordValidator(isNumber, fallback);
assert.deepEqual(validate({ a: 0.4, b: 'x', c: 1 }), { a: 0.4, c: 1 });
for (const saved of [null, 3, ['a'], 'a']) {
assert.equal(validate(saved), fallback);
}
});
it('recognizes a custom layer', () => {
const layer = {
id: 'custom-0',
name: 'Layer',
tileUrls: ['https://tiles/{z}/{x}/{y}.png'],
maxZoom: 20,
layerType: 'basemap',
resourceType: 'raster',
value: { version: 8, sources: {}, layers: [] },
};
assert.ok(isCustomLayer(layer));
assert.ok(isCustomLayer({ ...layer, resourceType: 'vector', value: 'https://style.json' }));
assert.ok(!isCustomLayer({ ...layer, layerType: 'other' }));
assert.ok(!isCustomLayer({ ...layer, tileUrls: [3] }));
assert.ok(!isCustomLayer({ ...layer, value: null }));
const { name: _name, ...nameless } = layer;
assert.ok(!isCustomLayer(nameless));
assert.ok(!isCustomLayer(null));
});
it('accepts the known layers, and the custom and extension ones', () => {
const validate = getLayerValidator({ libertyTopo: 'url', osm: 'url' }, 'libertyTopo');
assert.equal(validate('osm'), 'osm');
assert.equal(validate('custom-1234'), 'custom-1234');
assert.equal(validate('extension-some-id'), 'extension-some-id');
assert.equal(validate('gone'), 'libertyTopo');
assert.equal(validate('toString'), 'libertyTopo');
for (const saved of [null, 3, ['osm'], { id: 'osm' }]) {
assert.equal(validate(saved), 'libertyTopo');
}
});
});
describe('layer trees', () => {
const allowed: LayerTreeType = {
world: { a: true, b: false },
countries: { france: { c: false } },
};
it('keeps the choices made on the layers that are allowed', () => {
const filtered = filterLayerTree({ world: { a: false, b: true } }, allowed);
assert.deepEqual(filtered.world as LayerTreeType, { a: false, b: true });
});
it('adds the layers that are new, with their default', () => {
const filtered = filterLayerTree({ world: { a: false } }, allowed);
assert.deepEqual(filtered, {
world: { a: false, b: false },
countries: { france: { c: false } },
});
assert.deepEqual(filterLayerTree({}, allowed), allowed);
});
it('drops the layers that are not allowed anymore, but not the custom ones', () => {
const filtered = filterLayerTree(
{
world: { a: true, gone: true, 'custom-1': true, 'extension-x': false },
old: { x: true },
},
allowed
);
assert.deepEqual(filtered.world, {
a: true,
b: false,
'custom-1': true,
'extension-x': false,
});
assert.equal('old' in filtered, true);
assert.deepEqual(filtered.old, {});
});
it('does not take a branch for a layer, or a layer for a branch', () => {
const filtered = filterLayerTree({ world: true, countries: { france: false } }, allowed);
assert.deepEqual(filtered.world, { a: true, b: false });
assert.deepEqual((filtered.countries as LayerTreeType).france, { c: false });
assert.deepEqual(getLayerTreeValidator(allowed)({ world: { a: false } }).world, {
a: false,
b: false,
});
// a branch saved for a layer, and values that are not choices
assert.deepEqual(filterLayerTree({ world: { a: { x: true }, b: 'yes' } }, allowed).world, {
a: true,
b: false,
});
});
it('takes the defaults when what was saved is not a tree', () => {
for (const saved of [null, true, 'world', 3, ['world']]) {
assert.deepEqual(filterLayerTree(saved, allowed), allowed);
}
assert.deepEqual(
filterLayerTree({ world: null, old: null, other: [true] }, allowed),
allowed
);
});
it('does not share its branches with the allowed tree', () => {
const filtered = filterLayerTree({}, allowed);
(filtered.world as LayerTreeType).a = false;
assert.equal((allowed.world as LayerTreeType).a, true);
});
});
describe('Setting', () => {
it('has its initial value until it is connected', () => {
const setting = new Setting('key', 'initial', getValueValidator(['initial'], 'initial'));
assert.equal(get(setting), 'initial');
setting.connect({});
assert.equal(get(setting), 'initial');
setting.connect({ other: '"x"' });
assert.equal(get(setting), 'initial');
});
it('takes the saved value, validated', () => {
const setting = new Setting(
'key',
'metric',
getValueValidator(['metric', 'imperial'], 'metric')
);
setting.connect({ key: '"imperial"' });
assert.equal(get(setting), 'imperial');
setting.connect({ key: '"furlongs"' });
assert.equal(get(setting), 'metric');
setting.connect({ key: '{broken' });
assert.equal(get(setting), 'metric');
// connecting does not save anything
assert.deepEqual(saved, {});
});
it('takes a saved boolean or number of the type of its initial value', () => {
const flag = new Setting('flag', false);
flag.connect({ flag: 'true' });
assert.equal(get(flag), true);
flag.connect({ flag: '"yes"' });
assert.equal(get(flag), false);
const size = new SettingInitOnFirstRead('size', 170);
size.connect({ size: '240' });
assert.equal(get(size), 240);
size.connect({ size: 'null' });
assert.equal(get(size), 170);
});
it('saves what it is set to, when it changes', () => {
const setting = new Setting('key', 1);
setting.set(2);
assert.equal(get(setting), 2);
assert.deepEqual(saved, { key: '2' });
saved = {};
setting.set(2);
assert.deepEqual(saved, {});
setting.update((value) => value + 1);
assert.equal(get(setting), 3);
assert.deepEqual(saved, { key: '3' });
});
it('always saves objects, which may have been changed in place', () => {
const value = { a: 1 };
const setting = new Setting('key', value, getRecordValidator(isNumber, value));
setting.set(value);
assert.deepEqual(saved, { key: '{"a":1}' });
});
it('forgets what it saved when it has no value', () => {
const setting = new Setting<string | undefined>(
'key',
'x',
getValueValidator<string | undefined>(['x', undefined], undefined)
);
setting.set(undefined);
assert.equal(get(setting), undefined);
assert.ok('key' in saved);
assert.equal(saved.key, undefined);
});
it('tells its subscribers', () => {
const setting = new Setting('key', 1);
const seen: number[] = [];
const unsubscribe = setting.subscribe((value) => seen.push(value));
setting.set(2);
setting.set(2);
unsubscribe();
setting.set(3);
assert.deepEqual(seen, [1, 2]);
});
});
describe('SettingInitOnFirstRead', () => {
it('has no value until it is connected', () => {
const setting = new SettingInitOnFirstRead(
'key',
{ a: true },
getLayerTreeValidator({ a: true })
);
assert.equal(get(setting), undefined);
setting.connect({});
assert.deepEqual(get(setting), { a: true });
});
it('takes the saved value, validated, instead of the initial one', () => {
const setting = new SettingInitOnFirstRead('key', ['hr'], getArrayValidator(['hr', 'cad']));
setting.connect({ key: '["cad","nope"]' });
assert.deepEqual(get(setting), ['cad']);
// nothing is saved when it is connected, that is what `initialize` is for
assert.deepEqual(saved, {});
});
it('can be initialized, which saves the initial value', () => {
const setting = new SettingInitOnFirstRead('key', 5);
setting.initialize();
assert.equal(get(setting), 5);
assert.deepEqual(saved, { key: '5' });
});
});
describe('Setting without storage', () => {
it('changes, but keeps nothing', () => {
useSettingsStorage(undefined);
const setting = new Setting('key', 1);
setting.set(2);
assert.equal(get(setting), 2);
assert.deepEqual(saved, {});
});
});