From da9abbae5ef21c73281359008909a526855e0292 Mon Sep 17 00:00:00 2001 From: vcoppe Date: Fri, 9 Oct 2026 08:58:00 +0200 Subject: [PATCH] refactoring and tests --- website/package-lock.json | 567 ++------------ website/package.json | 6 +- website/src/lib/assets/colors.test.ts | 51 ++ website/src/lib/assets/layers.test.ts | 103 +++ website/src/lib/components/docs/docs.ts | 83 +-- .../src/lib/components/docs/guides.test.ts | 81 ++ website/src/lib/components/docs/guides.ts | 84 +++ .../components/embedding/embedding.test.ts | 148 ++++ .../src/lib/components/embedding/embedding.ts | 10 +- .../src/lib/components/export/Export.svelte | 4 +- .../lib/components/export/file-names.test.ts | 97 +++ .../src/lib/components/export/file-names.ts | 47 ++ .../src/lib/components/export/utils.svelte.ts | 12 +- .../map/layer-control/extension-api.test.ts | 149 ++++ .../map/layer-control/utils.test.ts | 74 ++ .../toolbar/tools/routing/routing.test.ts | 282 +++++++ .../toolbar/tools/routing/routing.ts | 110 ++- website/src/lib/engine/convert.ts | 33 + website/src/lib/engine/engine.test.ts | 405 ++++++++++ website/src/lib/{ => engine}/engine.ts | 701 ++---------------- website/src/lib/engine/file-state.ts | 101 +++ website/src/lib/engine/index.ts | 13 + website/src/lib/engine/observer.ts | 50 ++ website/src/lib/engine/statistics.ts | 179 +++++ website/src/lib/engine/storage.ts | 88 +++ website/src/lib/engine/types.ts | 247 ++++++ website/src/lib/file-colors.test.ts | 63 ++ website/src/lib/geo.test.ts | 31 + website/src/lib/geo.ts | 14 +- website/src/lib/i18n-keys.test.ts | 79 ++ website/src/lib/legacy-import.test.ts | 189 +++++ website/src/lib/logic/setting.test.ts | 220 ++++++ website/src/lib/logic/setting.ts | 173 +++++ website/src/lib/logic/settings.test.ts | 75 ++ website/src/lib/logic/settings.ts | 180 +---- website/src/lib/unit-conversions.test.ts | 75 ++ website/src/lib/unit-conversions.ts | 94 +++ website/src/lib/units.ts | 103 +-- website/src/lib/utils.test.ts | 46 ++ website/src/test/hooks.mjs | 27 + website/src/test/register.mjs | 12 + website/src/test/stubs/app-environment.ts | 8 + website/src/test/stubs/app-paths.ts | 5 + website/src/test/stubs/app-state.ts | 2 + website/src/test/stubs/env-static-public.ts | 1 + 45 files changed, 3558 insertions(+), 1564 deletions(-) create mode 100644 website/src/lib/assets/colors.test.ts create mode 100644 website/src/lib/assets/layers.test.ts create mode 100644 website/src/lib/components/docs/guides.test.ts create mode 100644 website/src/lib/components/docs/guides.ts create mode 100644 website/src/lib/components/embedding/embedding.test.ts create mode 100644 website/src/lib/components/export/file-names.test.ts create mode 100644 website/src/lib/components/export/file-names.ts create mode 100644 website/src/lib/components/map/layer-control/extension-api.test.ts create mode 100644 website/src/lib/components/map/layer-control/utils.test.ts create mode 100644 website/src/lib/components/toolbar/tools/routing/routing.test.ts create mode 100644 website/src/lib/engine/convert.ts create mode 100644 website/src/lib/engine/engine.test.ts rename website/src/lib/{ => engine}/engine.ts (53%) create mode 100644 website/src/lib/engine/file-state.ts create mode 100644 website/src/lib/engine/index.ts create mode 100644 website/src/lib/engine/observer.ts create mode 100644 website/src/lib/engine/statistics.ts create mode 100644 website/src/lib/engine/storage.ts create mode 100644 website/src/lib/engine/types.ts create mode 100644 website/src/lib/file-colors.test.ts create mode 100644 website/src/lib/geo.test.ts create mode 100644 website/src/lib/i18n-keys.test.ts create mode 100644 website/src/lib/legacy-import.test.ts create mode 100644 website/src/lib/logic/setting.test.ts create mode 100644 website/src/lib/logic/setting.ts create mode 100644 website/src/lib/logic/settings.test.ts create mode 100644 website/src/lib/unit-conversions.test.ts create mode 100644 website/src/lib/unit-conversions.ts create mode 100644 website/src/lib/utils.test.ts create mode 100644 website/src/test/hooks.mjs create mode 100644 website/src/test/register.mjs create mode 100644 website/src/test/stubs/app-environment.ts create mode 100644 website/src/test/stubs/app-paths.ts create mode 100644 website/src/test/stubs/app-state.ts create mode 100644 website/src/test/stubs/env-static-public.ts diff --git a/website/package-lock.json b/website/package-lock.json index 26c96efd6..ac87c7bf6 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -29,7 +29,7 @@ "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/kit": "^2.21.2", - "@sveltejs/vite-plugin-svelte": "^5.1.0", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.1.8", "@types/eslint": "^9.6.1", "@types/events": "^3.0.3", @@ -68,7 +68,7 @@ "tw-animate-css": "^1.4.0", "typescript": "^5.8.3", "vaul-svelte": "^1.0.0-next.7", - "vite": "^6.3.5", + "vite": "^7.3.7", "vite-plugin-wasm": "^3.6.0" } }, @@ -2313,43 +2313,42 @@ } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-5.1.1.tgz", - "integrity": "sha512-Y1Cs7hhTc+a5E9Va/xwKlAJoariQyHY+5zBgCZg4PFWNYQ1nMN9sjK1zhw1gK69DuqVP++sht/1GZg1aRwmAXQ==", + "version": "6.2.4", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz", + "integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==", "dev": true, "license": "MIT", "dependencies": { - "@sveltejs/vite-plugin-svelte-inspector": "^4.0.1", - "debug": "^4.4.1", + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", - "kleur": "^4.1.5", - "magic-string": "^0.30.17", - "vitefu": "^1.0.6" + "magic-string": "^0.30.21", + "obug": "^2.1.0", + "vitefu": "^1.1.1" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { "svelte": "^5.0.0", - "vite": "^6.0.0" + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@sveltejs/vite-plugin-svelte-inspector": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-4.0.1.tgz", - "integrity": "sha512-J/Nmb2Q2y7mck2hyCX4ckVHcR5tu2J+MtBEQqpDrrgELZ2uvraQcK/ioCV61AqkdXFgriksOKIceDcQmqnGhVw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz", + "integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^4.3.7" + "obug": "^2.1.0" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22" + "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^5.0.0", + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", - "vite": "^6.0.0" + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@swc/helpers": { @@ -4821,6 +4820,20 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -6068,24 +6081,24 @@ } }, "node_modules/vite": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", - "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.7.tgz", + "integrity": "sha512-AHEzTORMbi7/7aXp7w7KZ5+DCUdSeLsJz0grY8xHpc9DHdYb0vQu6DqJfm2tOw3gDExNgPgnJTXxM6syYFHaCA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", - "fdir": "^6.4.4", - "picomatch": "^4.0.2", - "postcss": "^8.5.3", - "rollup": "^4.34.9", - "tinyglobby": "^0.2.13" + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -6094,14 +6107,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", - "less": "*", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -6167,490 +6180,6 @@ "vite": "^2 || ^3 || ^4 || ^5 || ^6 || ^7 || ^8" } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/vitefu": { "version": "1.1.3", "dev": true, diff --git a/website/package.json b/website/package.json index aed3ecb59..374c433ae 100644 --- a/website/package.json +++ b/website/package.json @@ -9,7 +9,7 @@ "postbuild": "npx tsx src/lib/scripts/sitemap.ts", "preview": "vite preview", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "test": "node --import tsx --test \"src/**/*.test.ts\"", + "test": "node --import tsx --import ./src/test/register.mjs --experimental-wasm-modules --disable-warning=ExperimentalWarning --test \"src/**/*.test.ts\"", "generate:example": "node --import tsx src/lib/scripts/example-statistics.ts", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --check . --config ../.prettierrc --ignore-path ../.prettierignore --ignore-path ./.gitignore && eslint .", @@ -22,7 +22,7 @@ "@sveltejs/adapter-static": "^3.0.8", "@sveltejs/enhanced-img": "^0.6.0", "@sveltejs/kit": "^2.21.2", - "@sveltejs/vite-plugin-svelte": "^5.1.0", + "@sveltejs/vite-plugin-svelte": "^6.2.4", "@tailwindcss/vite": "^4.1.8", "@types/eslint": "^9.6.1", "@types/events": "^3.0.3", @@ -61,7 +61,7 @@ "tw-animate-css": "^1.4.0", "typescript": "^5.8.3", "vaul-svelte": "^1.0.0-next.7", - "vite": "^6.3.5", + "vite": "^7.3.7", "vite-plugin-wasm": "^3.6.0" }, "type": "module", diff --git a/website/src/lib/assets/colors.test.ts b/website/src/lib/assets/colors.test.ts new file mode 100644 index 000000000..eff2aa805 --- /dev/null +++ b/website/src/lib/assets/colors.test.ts @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { + getHighwayColor, + getSlopeColor, + getSurfaceColor, + highwayColors, + surfaceColors, +} from './colors'; + +describe('colors of the OSM data', () => { + it('has the color of a surface, or the one of the missing ones', () => { + assert.equal(getSurfaceColor('asphalt'), surfaceColors.asphalt); + assert.equal(getSurfaceColor('grass'), '#61b55c'); + assert.equal(getSurfaceColor('something new'), surfaceColors.missing); + assert.equal(getSurfaceColor(''), surfaceColors.missing); + }); + + it('has the color of a highway, or the one of the missing ones', () => { + assert.equal(getHighwayColor('motorway', undefined, undefined), highwayColors.motorway); + assert.equal(getHighwayColor('path', undefined, undefined), highwayColors.path); + assert.equal(getHighwayColor('something new', undefined, undefined), highwayColors.missing); + }); +}); + +describe('slope colors', () => { + const hue = (color: string) => Number(/hsl\(([-\d.e]+),/.exec(color)![1]); + const lightness = (color: string) => Number(/,([\d.]+)%\)$/.exec(color)![1]); + + it('is yellow on the flat, redder uphill and greener downhill, symmetrically', () => { + const flat = getSlopeColor(0); + assert.equal(hue(flat), 60); + assert.equal(lightness(flat), 90); + // steeper is further from the flat color, in both directions + assert.ok(hue(getSlopeColor(10)) < 60); + assert.ok(hue(getSlopeColor(-10)) > 60); + assert.ok(lightness(getSlopeColor(10)) < 90); + assert.ok(hue(getSlopeColor(20)) < hue(getSlopeColor(10))); + const up = getSlopeColor(7); + const down = getSlopeColor(-7); + assert.ok(Math.abs(hue(up) + hue(down) - 120) < 1e-9); + assert.ok(Math.abs(lightness(up) - lightness(down)) < 1e-9); + }); + + it('does not go further than 20 %', () => { + assert.equal(getSlopeColor(20), getSlopeColor(35)); + assert.equal(getSlopeColor(1000), getSlopeColor(20)); + assert.equal(getSlopeColor(-20), getSlopeColor(-55)); + assert.notEqual(getSlopeColor(19), getSlopeColor(20)); + }); +}); diff --git a/website/src/lib/assets/layers.test.ts b/website/src/lib/assets/layers.test.ts new file mode 100644 index 000000000..cc99e58ca --- /dev/null +++ b/website/src/lib/assets/layers.test.ts @@ -0,0 +1,103 @@ +// The layers are plain data, which the settings and the menus rely on being consistent. +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { + basemaps, + basemapTree, + defaultBasemap, + defaultBasemapTree, + defaultOpacities, + defaultOverlays, + defaultOverlayTree, + defaultOverpassQueries, + defaultOverpassTree, + defaultTerrainSource, + overlays, + overlayTree, + overpassQueryData, + overpassTree, + terrainSources, + type LayerTreeType, +} from './layers'; +import { getLayers } from '$lib/components/map/layer-control/utils'; + +const leaves = (tree: LayerTreeType) => Object.keys(getLayers(tree)); + +describe('layers', () => { + it('has a basemap and an overlay for every leaf of their trees', () => { + for (const id of leaves(basemapTree)) assert.ok(id in basemaps, `basemap ${id}`); + for (const id of leaves(overlayTree)) assert.ok(id in overlays, `overlay ${id}`); + for (const id of leaves(overpassTree)) assert.ok(id in overpassQueryData, `query ${id}`); + }); + + it('offers every layer and every query in its trees', () => { + const missingFrom = (ids: string[], tree: LayerTreeType) => { + const offered = new Set(leaves(tree)); + return ids.filter((id) => !offered.has(id)); + }; + const basemapIds = Object.keys(basemaps); + assert.deepEqual(missingFrom(basemapIds, basemapTree), [], 'basemapTree'); + assert.deepEqual(missingFrom(basemapIds, defaultBasemapTree), [], 'defaultBasemapTree'); + const overlayIds = Object.keys(overlays); + assert.deepEqual(missingFrom(overlayIds, overlayTree), [], 'overlayTree'); + assert.deepEqual(missingFrom(overlayIds, defaultOverlayTree), [], 'defaultOverlayTree'); + const queryIds = Object.keys(overpassQueryData); + assert.deepEqual(missingFrom(queryIds, overpassTree), [], 'overpassTree'); + assert.deepEqual(missingFrom(queryIds, defaultOverpassTree), [], 'defaultOverpassTree'); + assert.deepEqual( + missingFrom(queryIds, defaultOverpassQueries), + [], + 'defaultOverpassQueries' + ); + }); + + it('only offers by default what exists', () => { + assert.ok(defaultBasemap in basemaps); + for (const id of leaves(defaultBasemapTree)) assert.ok(id in basemaps, `basemap ${id}`); + for (const id of leaves(defaultOverlayTree)) assert.ok(id in overlays, `overlay ${id}`); + for (const id of leaves(defaultOverpassTree)) assert.ok(id in overpassQueryData, `${id}`); + for (const id of leaves(defaultOverlays)) assert.ok(id in overlays, `overlay ${id}`); + for (const id of leaves(defaultOverpassQueries)) { + assert.ok(id in overpassQueryData, `query ${id}`); + } + assert.ok(defaultTerrainSource in terrainSources); + }); + + it('has the default selections inside the default trees', () => { + const inTree = (selection: LayerTreeType, tree: LayerTreeType) => { + const offered = new Set(leaves(tree)); + for (const id of leaves(selection)) assert.ok(offered.has(id), `${id}`); + }; + inTree(defaultOverlays, defaultOverlayTree); + inTree(defaultOverpassQueries, defaultOverpassTree); + }); + + it('has the default tree inside the whole tree', () => { + const all = new Set(leaves(basemapTree)); + for (const id of leaves(defaultBasemapTree)) assert.ok(all.has(id), `${id}`); + }); + + it('has no id used by two leaves of a tree', () => { + for (const tree of [basemapTree, overlayTree, overpassTree, defaultBasemapTree]) { + const ids: string[] = []; + const walk = (node: LayerTreeType) => + Object.entries(node).forEach(([id, value]) => + typeof value === 'boolean' ? ids.push(id) : walk(value) + ); + walk(tree); + assert.equal(new Set(ids).size, ids.length); + } + }); + + it('has opacities between 0 and 1, for overlays', () => { + for (const [id, opacity] of Object.entries(defaultOpacities)) { + assert.ok(opacity >= 0 && opacity <= 1, id); + } + }); + + it('has the queries it needs', () => { + for (const [id, data] of Object.entries(overpassQueryData)) { + assert.ok(Object.keys(data).length > 0, id); + } + }); +}); diff --git a/website/src/lib/components/docs/docs.ts b/website/src/lib/components/docs/docs.ts index 753241bb8..6a210f55a 100644 --- a/website/src/lib/components/docs/docs.ts +++ b/website/src/lib/components/docs/docs.ts @@ -16,27 +16,6 @@ import { } from '@lucide/svelte'; import type { Component } from 'svelte'; -export const guides: Record = { - 'getting-started': [], - menu: ['file', 'edit', 'view', 'settings'], - 'files-and-stats': [], - toolbar: [ - 'routing', - 'poi', - 'scissors', - 'time', - 'merge', - 'extract', - 'elevation', - 'minify', - 'clean', - ], - 'map-controls': [], - gpx: [], - integration: [], - faq: [], -}; - export const guideIcons: Record> = { 'getting-started': '🚀', menu: '📂 ⚙️', @@ -61,64 +40,4 @@ export const guideIcons: Record> = { faq: '🔮', }; -export function getPreviousGuide(currentGuide: string): string | undefined { - let subguides = currentGuide.split('/'); - - if (subguides.length === 1) { - let keys = Object.keys(guides); - let index = keys.indexOf(currentGuide); - if (index === 0) { - return undefined; - } - let previousGuide = keys[index - 1]; - if (previousGuide === undefined) { - return undefined; - } else if (guides[previousGuide].length === 0) { - return previousGuide; - } else { - return `${previousGuide}/${guides[previousGuide][guides[previousGuide].length - 1]}`; - } - } else { - if (guides.hasOwnProperty(subguides[0])) { - let subguideIndex = guides[subguides[0]].indexOf(subguides[1]); - if (subguideIndex > 0) { - return `${subguides[0]}/${guides[subguides[0]][subguideIndex - 1]}`; - } else { - return subguides[0]; - } - } else { - return undefined; - } - } -} - -export function getNextGuide(currentGuide: string): string | undefined { - let subguides = currentGuide.split('/'); - - if (subguides.length === 1) { - if (guides.hasOwnProperty(currentGuide)) { - if (guides[currentGuide].length === 0) { - let keys = Object.keys(guides); - let index = keys.indexOf(currentGuide); - return keys[index + 1]; - } else { - return `${currentGuide}/${guides[currentGuide][0]}`; - } - } else { - return undefined; - } - } else { - if (guides.hasOwnProperty(subguides[0])) { - let subguideIndex = guides[subguides[0]].indexOf(subguides[1]); - if (subguideIndex < guides[subguides[0]].length - 1) { - return `${subguides[0]}/${guides[subguides[0]][subguideIndex + 1]}`; - } else { - let keys = Object.keys(guides); - let index = keys.indexOf(subguides[0]); - return keys[index + 1]; - } - } else { - return undefined; - } - } -} +export { guides, getNextGuide, getPreviousGuide } from './guides'; diff --git a/website/src/lib/components/docs/guides.test.ts b/website/src/lib/components/docs/guides.test.ts new file mode 100644 index 000000000..d31803e7a --- /dev/null +++ b/website/src/lib/components/docs/guides.test.ts @@ -0,0 +1,81 @@ +import assert from 'node:assert/strict'; +import { existsSync } from 'node:fs'; +import { describe, it } from 'node:test'; +import { languages } from '$lib/languages'; +import { getNextGuide, getPreviousGuide, guides } from './guides'; + +/** Every page of the help in reading order, as the links at the bottom of the pages follow them. */ +const pages = Object.entries(guides).flatMap(([guide, subguides]) => [ + guide, + ...subguides.map((subguide) => `${guide}/${subguide}`), +]); + +describe('guides', () => { + it('go from one page to the next, subguides included', () => { + assert.equal(getNextGuide('getting-started'), 'menu'); + assert.equal(getNextGuide('menu'), 'menu/file'); + assert.equal(getNextGuide('menu/file'), 'menu/edit'); + assert.equal(getNextGuide('menu/settings'), 'files-and-stats'); + assert.equal(getNextGuide('toolbar/clean'), 'map-controls'); + assert.equal(getNextGuide('integration'), 'faq'); + }); + + it('go from one page to the previous one, subguides included', () => { + assert.equal(getPreviousGuide('menu'), 'getting-started'); + assert.equal(getPreviousGuide('menu/file'), 'menu'); + assert.equal(getPreviousGuide('menu/edit'), 'menu/file'); + assert.equal(getPreviousGuide('files-and-stats'), 'menu/settings'); + assert.equal(getPreviousGuide('map-controls'), 'toolbar/clean'); + assert.equal(getPreviousGuide('faq'), 'integration'); + }); + + it('stop at the ends', () => { + assert.equal(getPreviousGuide('getting-started'), undefined); + assert.equal(getNextGuide('faq'), undefined); + }); + + it('know nothing of pages that do not exist', () => { + assert.equal(getNextGuide('nope'), undefined); + assert.equal(getNextGuide('nope/sub'), undefined); + assert.equal(getPreviousGuide('nope'), undefined); + assert.equal(getPreviousGuide('nope/sub'), undefined); + }); + + it('walk through every page in both directions', () => { + const forward = [pages[0]]; + for (let next = getNextGuide(pages[0]); next; next = getNextGuide(next)) { + forward.push(next); + } + assert.deepEqual(forward, pages); + + const backward = [pages.at(-1)!]; + for ( + let previous = getPreviousGuide(pages.at(-1)!); + previous; + previous = getPreviousGuide(previous) + ) { + backward.push(previous); + } + assert.deepEqual(backward.reverse(), pages); + }); + + it('are written in every language of the site', () => { + const root = new URL('../../docs/', import.meta.url); + const missing: string[] = []; + for (const language of Object.keys(languages)) { + for (const page of pages) { + if (!existsSync(new URL(`${language}/${page}.mdx`, root))) { + missing.push(`${language}/${page}`); + } + } + } + assert.deepEqual(missing, []); + }); + + it('have a translation of the interface for every language of the site', () => { + const root = new URL('../../../locales/', import.meta.url); + for (const language of Object.keys(languages)) { + assert.ok(existsSync(new URL(`${language}.json`, root)), language); + } + }); +}); diff --git a/website/src/lib/components/docs/guides.ts b/website/src/lib/components/docs/guides.ts new file mode 100644 index 000000000..dbc3e6609 --- /dev/null +++ b/website/src/lib/components/docs/guides.ts @@ -0,0 +1,84 @@ +// The guides of the help section, and how they follow one another. The icons are in `docs.ts`. + +export const guides: Record = { + 'getting-started': [], + menu: ['file', 'edit', 'view', 'settings'], + 'files-and-stats': [], + toolbar: [ + 'routing', + 'poi', + 'scissors', + 'time', + 'merge', + 'extract', + 'elevation', + 'minify', + 'clean', + ], + 'map-controls': [], + gpx: [], + integration: [], + faq: [], +}; + +export function getPreviousGuide(currentGuide: string): string | undefined { + const subguides = currentGuide.split('/'); + + if (subguides.length === 1) { + const keys = Object.keys(guides); + const index = keys.indexOf(currentGuide); + if (index === 0) { + return undefined; + } + const previousGuide = keys[index - 1]; + if (previousGuide === undefined) { + return undefined; + } else if (guides[previousGuide].length === 0) { + return previousGuide; + } else { + return `${previousGuide}/${guides[previousGuide][guides[previousGuide].length - 1]}`; + } + } else { + if (guides.hasOwnProperty(subguides[0])) { + const subguideIndex = guides[subguides[0]].indexOf(subguides[1]); + if (subguideIndex > 0) { + return `${subguides[0]}/${guides[subguides[0]][subguideIndex - 1]}`; + } else { + return subguides[0]; + } + } else { + return undefined; + } + } +} + +export function getNextGuide(currentGuide: string): string | undefined { + const subguides = currentGuide.split('/'); + + if (subguides.length === 1) { + if (guides.hasOwnProperty(currentGuide)) { + if (guides[currentGuide].length === 0) { + const keys = Object.keys(guides); + const index = keys.indexOf(currentGuide); + return keys[index + 1]; + } else { + return `${currentGuide}/${guides[currentGuide][0]}`; + } + } else { + return undefined; + } + } else { + if (guides.hasOwnProperty(subguides[0])) { + const subguideIndex = guides[subguides[0]].indexOf(subguides[1]); + if (subguideIndex < guides[subguides[0]].length - 1) { + return `${subguides[0]}/${guides[subguides[0]][subguideIndex + 1]}`; + } else { + const keys = Object.keys(guides); + const index = keys.indexOf(subguides[0]); + return keys[index + 1]; + } + } else { + return undefined; + } + } +} diff --git a/website/src/lib/components/embedding/embedding.test.ts b/website/src/lib/components/embedding/embedding.test.ts new file mode 100644 index 000000000..c933f9ecd --- /dev/null +++ b/website/src/lib/components/embedding/embedding.test.ts @@ -0,0 +1,148 @@ +import assert from 'node:assert/strict'; +import { describe, it } from 'node:test'; +import { + allowedEmbeddingBasemaps, + convertOldEmbeddingOptions, + defaultEmbeddingOptions, + getCleanedEmbeddingOptions, + getFilesFromEmbeddingOptions, + getMergedEmbeddingOptions, + getURLForGoogleDriveFile, +} from './embedding'; + +describe('embedding options', () => { + it('are the defaults when nothing is given', () => { + assert.deepEqual(getMergedEmbeddingOptions({}), defaultEmbeddingOptions); + // and a copy of them + const merged = getMergedEmbeddingOptions({}); + merged.elevation.show = false; + assert.equal(defaultEmbeddingOptions.elevation.show, true); + }); + + it('override the defaults, also inside the groups of options', () => { + const merged = getMergedEmbeddingOptions({ + basemap: 'osm', + distanceUnits: 'imperial', + elevation: { fill: 'slope', hr: true }, + files: ['https://example.com/a.gpx'], + }); + assert.equal(merged.basemap, 'osm'); + assert.equal(merged.distanceUnits, 'imperial'); + assert.equal(merged.elevation.fill, 'slope'); + assert.equal(merged.elevation.hr, true); + // the options of a group that are not given stay as they are + assert.equal(merged.elevation.height, 170); + assert.equal(merged.elevation.show, true); + assert.deepEqual(merged.files, ['https://example.com/a.gpx']); + assert.equal(merged.velocityUnits, 'speed'); + }); + + it('take a list or a null as a value, not as a group', () => { + const merged = getMergedEmbeddingOptions({ ids: ['a', 'b'], key: null }); + assert.deepEqual(merged.ids, ['a', 'b']); + assert.equal(merged.key, null); + }); + + it('are cleaned of what is a default', () => { + const options = getMergedEmbeddingOptions({ basemap: 'osm', elevation: { hr: true } }); + assert.deepEqual(getCleanedEmbeddingOptions(options), { + basemap: 'osm', + elevation: { hr: true }, + }); + assert.deepEqual(getCleanedEmbeddingOptions(defaultEmbeddingOptions), {}); + // merging what was cleaned gives the options back + assert.deepEqual(getMergedEmbeddingOptions(getCleanedEmbeddingOptions(options)), options); + }); + + it('are cleaned of the key of the site, and keep the key of the user', () => { + assert.deepEqual( + getCleanedEmbeddingOptions({ ...defaultEmbeddingOptions, key: 'test-key' }), + {} + ); + assert.deepEqual(getCleanedEmbeddingOptions({ ...defaultEmbeddingOptions, key: 'mine' }), { + key: 'mine', + }); + }); + + it('are not changed by cleaning them', () => { + const options = getMergedEmbeddingOptions({ elevation: { hr: true } }); + const before = JSON.stringify(options); + getCleanedEmbeddingOptions(options); + assert.equal(JSON.stringify(options), before); + }); +}); + +describe('embedded files', () => { + it('are the urls, then the files of Google Drive', () => { + const options = getMergedEmbeddingOptions({ + files: ['https://example.com/a.gpx'], + ids: ['abc', 'def'], + }); + assert.deepEqual(getFilesFromEmbeddingOptions(options), [ + 'https://example.com/a.gpx', + getURLForGoogleDriveFile('abc'), + getURLForGoogleDriveFile('def'), + ]); + assert.ok(getURLForGoogleDriveFile('abc').includes('/files/abc?alt=media')); + assert.deepEqual(getFilesFromEmbeddingOptions(getMergedEmbeddingOptions({})), []); + }); + + it('can use the basemaps of the site except a few', () => { + assert.ok(allowedEmbeddingBasemaps.includes('libertyTopo')); + assert.ok(allowedEmbeddingBasemaps.includes('osm')); + assert.ok(!allowedEmbeddingBasemaps.includes('ordnanceSurvey')); + assert.ok(allowedEmbeddingBasemaps.includes(defaultEmbeddingOptions.basemap)); + }); +}); + +describe('options of the old embedding', () => { + const convert = (query: string) => convertOldEmbeddingOptions(new URLSearchParams(query)); + + it('are converted to the new ones', () => { + assert.deepEqual(convert(''), { key: 'test-key', files: [], ids: [] }); + assert.deepEqual( + convert( + 'source=otm&imperial&running&distance&direction&slope&state=' + + encodeURIComponent(JSON.stringify({ ids: ['a'], urls: ['https://x/y.gpx'] })) + ), + { + key: 'test-key', + files: ['https://x/y.gpx'], + ids: ['a'], + basemap: 'openTopoMap', + distanceUnits: 'imperial', + velocityUnits: 'pace', + distanceMarkers: true, + directionMarkers: true, + elevation: { fill: 'slope' }, + } + ); + }); + + it('convert the names of the sources', () => { + assert.equal(convert('source=satellite').basemap, 'libertySatellite'); + assert.equal(convert('source=otm').basemap, 'openTopoMap'); + assert.equal(convert('source=ohm').basemap, 'openHikingMap'); + assert.equal(convert('source=unknown').basemap, undefined); + }); + + it('ignore a state that is not valid', () => { + const empty = { key: 'test-key', files: [], ids: [] }; + assert.deepEqual(convert('state=%7Bbroken'), empty); + assert.deepEqual(convert('state=null'), empty); + assert.deepEqual(convert('state=42'), empty); + assert.deepEqual(convert(`state=${encodeURIComponent('{"ids":"abc","urls":3}')}`), empty); + assert.deepEqual(convert(`state=${encodeURIComponent('{"ids":["a"]}')}&imperial`), { + ...empty, + ids: ['a'], + distanceUnits: 'imperial', + }); + }); + + it('can be merged with the defaults', () => { + const merged = getMergedEmbeddingOptions(convert('slope&source=satellite')); + assert.equal(merged.elevation.fill, 'slope'); + assert.equal(merged.elevation.height, 170); + assert.equal(merged.basemap, 'libertySatellite'); + }); +}); diff --git a/website/src/lib/components/embedding/embedding.ts b/website/src/lib/components/embedding/embedding.ts index 05914fc8f..1bb29550d 100644 --- a/website/src/lib/components/embedding/embedding.ts +++ b/website/src/lib/components/embedding/embedding.ts @@ -115,11 +115,15 @@ export function convertOldEmbeddingOptions(options: URLSearchParams): any { ids: [], }; if (options.has('state')) { - let state = JSON.parse(options.get('state')!); - if (state.ids) { + // the state comes from the url: it may be anything + let state: any = {}; + try { + state = JSON.parse(options.get('state')!) ?? {}; + } catch {} + if (Array.isArray(state.ids)) { newOptions.ids.push(...state.ids); } - if (state.urls) { + if (Array.isArray(state.urls)) { newOptions.files.push(...state.urls); } } diff --git a/website/src/lib/components/export/Export.svelte b/website/src/lib/components/export/Export.svelte index 5e610cda2..a383cbf34 100644 --- a/website/src/lib/components/export/Export.svelte +++ b/website/src/lib/components/export/Export.svelte @@ -185,9 +185,7 @@ {i18n._('quantities.power')} -
+