88 Commits
Author SHA1 Message Date
jakob.scheid 99ae2725cd test(settings): refactor switch component test
Added and used getWrapper function to switch component test to mount the
component and avoid repeating code.
2026-07-28 12:51:32 +02:00
jakob.scheid c72c5dac46 test(settings): refactor selection component test
Use exampleOptions instead of an own options array, which was equal to
exampleOptions.
2026-07-28 12:43:41 +02:00
jakob.scheid a73cc925fd test(settings): refactor selection test getWrapper function
Updated the getWrapper function to use object destructuring.
2026-07-28 12:41:42 +02:00
jakob.scheid 9473a42037 style(settings): remove unnecessary commas
Removed unnecessary commas in settings input component and settings page
component.
2026-07-28 12:35:27 +02:00
jakob.scheid 702f6d4787 style(settings): remove unnecessary comma
Removed unnecessary trailing comma in the selection component.
2026-07-28 12:33:40 +02:00
jakob.scheid 98a07496f9 fix(settings): update settings input to use default value
The SettingsInput component now uses the default value if there is a
default value. Tests for this were added.
2026-07-28 12:28:06 +02:00
jakob.scheid 67981f6181 test(settings): refactor getWrapper function in settings input test
Updated the getWrapper function to use object destructuring.
2026-07-28 12:20:17 +02:00
jakob.scheid f33f00d7a8 docs(changelog): add settings renderer 2026-07-28 11:20:50 +02:00
jakob.scheid bce7f98cdb test(settings): add tests for settings page
Added tests for the SettingsPage component:
  - correct heading
  - correct settings
2026-07-28 11:01:39 +02:00
jakob.scheid 20a6245cb6 test(settings): add tests for settings page link 2026-07-27 19:22:58 +02:00
jakob.scheid b96bafa8bf test(settings): add tests for settings view
Added tests for the settings view (SettingsView.vue):
  - heading translation
  - sidebar top-level sections
  - correct main element
2026-07-27 19:05:07 +02:00
jakob.scheid 2d7d521e18 refactor(settings): move setttings path normalization utility to a separate file
Moved the normalizePagePath utility from the settings page composable to
a separate utility file and restructured tests accordingly.
2026-07-27 19:05:06 +02:00
jakob.scheid afa7b11534 refactor(settings): remove unused utility
Removed the utility getSectionHeadingLevel and its tests.
2026-07-27 19:05:06 +02:00
jakob.scheid 72c88838cb feat(settings): add page path normalization type check
Added a type check to the normalizePagePath function. The function now
returns an empty array if the path is not a string.
2026-07-27 19:05:05 +02:00
jakob.scheid e14ce45849 feat(settings): omit empty settings page segments
The composable useSettingsPage now omits empty segments in the path.
2026-07-27 19:05:04 +02:00
jakob.scheid 966a984eb0 feat(settings): filter characters that would cause a broken URL from settings page path 2026-07-27 19:05:04 +02:00
jakob.scheid d094ec1f0a test(settings): distinguish between unit tests and integration tests
Separated unit tests and integration tests in the test for
useSettingsPage more clearly.
2026-07-27 19:05:03 +02:00
jakob.scheid 0d38e02767 refactor(settings): push route object instead of raw string
Modified the useSettingsPage composable to use a route object instead
of a raw string in goToSettingsPage and moved settings route child
routes into the settings route itself using 'rest(.*)*'.
2026-07-27 19:05:02 +02:00
jakob.scheid 61a41ec813 feat(settings): make settings page composable more compatible
The composable now uses replace instead of replaceAll which is not
supported by some old browsers.
2026-07-27 19:05:01 +02:00
jakob.scheid a1da2eb2e6 fix(settings): omit leading/trailing and consecuting dots in noralizePagePath 2026-07-27 19:05:00 +02:00
jakob.scheid 89dbf8f0e7 fix(settings): return router.push promise
The settings page composable function to go to a settings page now
returns the promise router.push returns.
2026-07-26 18:45:04 +02:00
jakob.scheid f5af9b3675 test(settings): add tests for settings page composable 2026-07-26 16:04:23 +02:00
jakob.scheid cd6f2da17f test(settings): add tests for selection setting
Added tests for the selection setting component:
  - correct display of options
  - shows values correctly
  - store of values
  - correct translation
2026-07-26 15:44:41 +02:00
jakob.scheid 32b9b860f6 feat(settings): wrap selection option label
Added a wrapper for the settings selection option label to make it
easier to identify (internal).
2026-07-25 16:47:43 +02:00
jakob.scheid eb50936a06 fix(settings): use wrapper only if settings shown
The .settings-main-content wrapper in the settings view is now only
used if there are settings shown. It is not used if the settings are
still loading or no section is selected.
2026-07-25 16:13:47 +02:00
jakob.scheid 5e37cdd778 feat(settings): wrap selection setting label
Added a wrapper for the selection setting label to make it easier to
find (internal).
2026-07-25 15:07:08 +02:00
jakob.scheid bce61bcafb fix(settings): add section fallback
Added a simple message as a fallback that is used if no section is
selected.
2026-07-25 15:06:41 +02:00
jakob.scheid 9ff74157e8 fix(settings): fix handling of non-existent paths
Previously, the settings view now does not check whether the settings
content is falsy. But, if the setting does not exist, the setting itself
is undefined. So, if the specified settings path did not exist, an
error was thrown.
Now, the setting is checked for being falsy, not the setting content.
2026-07-25 13:45:43 +02:00
jakob.scheid 443967d314 chore: resolve merge conflicts 2026-07-25 13:42:04 +02:00
jakob.scheid 35f9a7363d Merge branch 'main' into feature/settings-renderer 2026-07-25 13:33:41 +02:00
jakob.scheid 61cb9d1790 Merge remote-tracking branch 'refs/remotes/origin/feature/settings-renderer' into feature/settings-renderer 2026-07-24 12:53:23 +02:00
jakob.scheid 06fa759df3 test(settings): add settings input tests 2026-07-24 12:52:54 +02:00
jakob.scheid 68e8ff3e7d feat(test-utils): add store setup functionality to mountComponent utility 2026-07-24 12:52:54 +02:00
jakob.scheid 62fa9e8799 fix(settings): prevent setting input done button from animating initially when mounting 2026-07-24 12:52:53 +02:00
jakob.scheid 53326b7e2f feat(settings): make setting input reactive to stored setting 2026-07-24 12:52:53 +02:00
jakob.scheid bcc4afe7fa feat(test-utils): add pinia options to mountComponent test utility 2026-07-24 12:52:37 +02:00
jakob.scheid 99393c08f7 test(settings): add settings input tests 2026-07-24 12:49:17 +02:00
jakob.scheid f2d228c24e feat(test-utils): add store setup functionality to mountComponent utility 2026-07-24 12:19:17 +02:00
jakob.scheid 9ce4cec0bf fix(settings): prevent setting input done button from animating initially when mounting 2026-07-24 12:17:48 +02:00
jakob.scheid f81ce63062 feat(settings): make setting input reactive to stored setting 2026-07-24 12:04:42 +02:00
jakob.scheid 7abf25aa19 feat(tests): add pinia options to mountComponent test utility 2026-07-24 11:36:50 +02:00
jakob.scheid bbae64771a test(settings): add switch component tests 2026-07-23 14:54:19 +02:00
jakob.scheid e6f5f5083f chore(dependencies): add @pinia/testing dependency 2026-07-23 13:18:34 +02:00
jakob.scheid a9e9bffd50 feat(settings): add settings page link 2026-07-23 12:42:10 +02:00
jakob.scheid fe7453ef10 fix(settings): add key prop to settings in settings page 2026-07-23 12:40:33 +02:00
jakob.scheid d0f86bfcec feat(settings): implement settings pages navigation 2026-07-23 12:32:31 +02:00
jakob.scheid 5c46f1bba4 feat(settigns): add settings page composable 2026-07-23 12:23:49 +02:00
jakob.scheid 60c1f290ff feat(settings): add utility functions to get settings
Utility function getSetting for getting a first-level setting from a
list of settings and unit tests.
Utility function getSettingRecursively to get any setting from a list
of settings. The setting can be at any nesting level in the settings
list and unit tests.
2026-07-23 11:38:58 +02:00
jakob.scheid ff0a6502fc feat(icons): add chevron right icon 2026-07-22 14:04:38 +02:00
jakob.scheid 840d01e1a1 refactor(settings): move settings button style into a CSS module 2026-07-22 13:53:08 +02:00
jakob.scheid 2a1ebcf343 fix(settings): fix switch label cursor 2026-07-22 13:36:34 +02:00
jakob.scheid 129d380f56 feat(settings): add selection setting 2026-07-22 13:27:57 +02:00
jakob.scheid dc366d9efa fix(settings): prevent input setting done button from extending beyond viewport border 2026-07-22 00:42:40 +02:00
jakob.scheid 6385842b54 fix(settings): make switch setting style scoped 2026-07-21 19:28:55 +02:00
jakob.scheid 601e8284c1 refactor(settings): rename variable
Renamed the variable 'inputClass' to 'inputType' because this name is
more accurate
2026-07-21 18:37:39 +02:00
jakob.scheid 82a11af017 feat(settings): improve switch setting accessibility 2026-07-21 17:29:05 +02:00
jakob.scheid 45b52c2c11 feat(settings): improve input setting accessibility 2026-07-21 17:17:59 +02:00
jakob.scheid e365e1750a feat(settings): add animation to settings input done button 2026-07-21 16:50:15 +02:00
jakob.scheid 71a2d5a8d7 feat(settings): add settings component for inputs 2026-07-21 16:37:45 +02:00
jakob.scheid 4281942ea2 feat(icons): add check mark icon 2026-07-21 16:36:18 +02:00
jakob.scheid dda36061ab feat(style): add common input style 2026-07-21 16:32:20 +02:00
jakob.scheid 1f9d7ebc86 refactor(settings): remove unused setting class 2026-07-21 01:52:11 +02:00
jakob.scheid 4c64459488 feat(settings): remove large settings padding on small screens 2026-07-21 01:49:03 +02:00
jakob.scheid c805228523 feat(settings): add support for boolean settings 2026-07-20 17:37:10 +02:00
jakob.scheid 10dc005cce feat(settings): make switch use the settings store 2026-07-20 17:36:46 +02:00
jakob.scheid 267f009d2f feat(settings): display current settings page 2026-07-20 17:33:56 +02:00
jakob.scheid f71ebb174b feat(settings): add settings store 2026-07-20 17:30:24 +02:00
jakob.scheid ea8faa5deb Revert "feat(dependencies): add lodash and its types to package.json and package-lock.json" 2026-07-20 17:12:02 +02:00
jakob.scheid e7972e5689 chore(dependencies): add pinia-plugin-persistedstate dependency 2026-07-20 16:40:27 +02:00
jakob.scheid 34010ebd27 feat(dependencies): add lodash and its types to package.json and package-lock.json 2026-06-07 21:59:10 +02:00
jakob.scheid 1270291364 set up Pinia 2026-06-07 16:17:53 +02:00
jakob.scheid 5b9f32c5ee feat(settings): add switch transitions 2026-06-06 23:32:55 +02:00
jakob.scheid a3dd10582b feat(settings): add switch component 2026-06-06 23:32:09 +02:00
jakob.scheid 443567237d feat(settings): add utility function to determine the level of a settings section heading 2026-06-06 18:35:26 +02:00
jakob.scheid b0b2678dff feat(settings): get current section using watchEffect 2026-06-06 11:19:02 +02:00
jakob.scheid 4738f59a00 feat(settings): make active section content reactive 2026-06-06 01:52:10 +02:00
jakob.scheid 42ff52a4de set button cursor to pointer 2026-06-06 01:34:33 +02:00
jakob.scheid 980805730a feat(settings): go to the settings route if the settings sub-route does not exist 2026-06-05 15:31:25 +02:00
jakob.scheid 96d4d92f68 feat(settings): focus sidebar section button that is currently active 2026-06-05 15:28:57 +02:00
jakob.scheid f947c4bd33 feat(settings): remove sidebar section link button container 2026-06-05 15:18:48 +02:00
jakob.scheid 2c81a7cdf7 feat(settings): show first-level sections in the sidebar 2026-06-05 13:51:58 +02:00
jakob.scheid 1a13b78eff add common class for button links 2026-06-05 13:49:56 +02:00
jakob.scheid c38b38707c underline focused links that are no buttons 2026-06-05 13:47:14 +02:00
jakob.scheid 3ea55d24dc add button CSS class 2026-06-05 13:45:54 +02:00
jakob.scheid 1f4037f64c feature(router): remove trailing slash(es) 2026-06-05 13:45:31 +02:00
jakob.scheid cf781ff3f5 feature(settings): load settings configuration 2026-06-05 13:45:11 +02:00
jakob.scheid 8acb675a03 fix(footer): update settings router link target 2026-06-05 13:02:26 +02:00
jakob.scheid 8d6e87af16 feature(settings): catch all subpaths of the settings path in the router 2026-06-05 12:51:08 +02:00
31 changed files with 8603 additions and 9538 deletions
-4
View File
@@ -5,7 +5,3 @@ The frontend of the search engine Seekra.
## Setup
Please execute the setup script in `scripts/` for your platform when cloning the repository.
## Legal
After each update to the dependencies, please run the script `scripts/generate-license-files.js` to regenerate the third-party license files.
+7668 -15
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env node
/*
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import fs from 'node:fs/promises';
import { generateLicenseFile } from 'generate-license-file';
const packageJson = './package.json';
const packageJsonBackup = `${packageJson}~`;
await generateLicenseFile(
packageJson,
'THIRD_PARTY_LICENSES'
);
const pkg = JSON.parse(
await fs.readFile(packageJson, 'utf-8')
);
await fs.rename(packageJson, packageJsonBackup);
const devPackage = {
...pkg,
dependencies: pkg.devDependencies ?? {},
devDependencies: {}
};
/*
The file including the dependencies for the current license file has
to be always package.json due to a bug of generate-license file. See
https://github.com/TobyAndToby/generate-license-file/issues/752
*/
await fs.writeFile(
packageJson,
JSON.stringify(devPackage)
);
await generateLicenseFile(
packageJson,
'./THIRD_PARTY_LICENSES_DEV'
);
await fs.rename(packageJsonBackup, packageJson);
-14
View File
@@ -1,17 +1,3 @@
#!/usr/bin/env pwsh
# Copyright 2026 Seekra
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
git config --local commit.template .gitmessage
-14
View File
@@ -1,17 +1,3 @@
#!/usr/bin/bash
# Copyright 2026 Seekra
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
git config --local commit.template .gitmessage
+15 -31
View File
@@ -18,40 +18,21 @@ limitations under the License.
import Navbar from './features/nav/components/Navbar.vue';
import Footer from './features/footer/components/Footer.vue';
import { updatePageTitle } from './router';
import { useColorScheme } from './features/colorScheme/composables/useColorScheme';
import { ref, provide, watch, watchEffect } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
import { ref, provide, watch } from 'vue';
const { getColorScheme, updateColorScheme } = useColorScheme();
const colorScheme = ref(getColorScheme());
provide('colorScheme', colorScheme);
watch(colorScheme, (newValue) => {
updateColorScheme(newValue);
document.body.style.setProperty(colorScheme, {
auto: 'normal',
dark: 'dark',
light: 'light'
});
if (newValue === 'dark') {
document.body.classList.add('dark');
} else {
document.body.classList.remove('dark');
}
if (newValue === 'auto') {
document.body.classList.add('color-scheme-auto');
} else {
document.body.classList.remove('color-scheme-auto');
}
});
watchEffect(() => updatePageTitle(route));
watch(colorScheme, val => updateColorScheme(val))
</script>
<template>
<div id="app-wrapper">
<div
:style="{ colorScheme: colorScheme === 'auto' ? 'normal' : (colorScheme === 'dark' ? 'dark' : 'light')}"
:class="{ dark: colorScheme === 'dark', 'color-scheme-auto': colorScheme === 'auto' }"
id="app-wrapper"
>
<Navbar />
<router-view class="main-content" />
@@ -65,11 +46,6 @@ watchEffect(() => updatePageTitle(route));
--main-content-padding-x: 30px;
--main-content-padding-y: 40px;
--main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x);
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--light-bg);
color: var(--dark);
}
.main-content {
@@ -81,4 +57,12 @@ watchEffect(() => updatePageTitle(route));
--main-content-padding-x: 15px;
}
}
#app-wrapper {
min-height: 100vh;
display: flex;
flex-direction: column;
background-color: var(--light-bg);
color: var(--dark);
}
</style>
-38
View File
@@ -1,38 +0,0 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path
stroke="currentColor"
stroke-width="2"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
d="
M6 4
h12
a4 4 0 0 1 4 4
v8
a4 4 0 0 1 -4 4
h-12
a4 4 0 0 1 -4 -4
v-8
a4 4 0 0 1 4 -4
m2 0
v16
"
/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

@@ -39,7 +39,7 @@ vi.mock('@/features/icons/components/Icon.vue', () => ({
}
}));
describe('LanguageSwitchButton', () => {
describe('LanguageSwitchButton.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
+5 -11
View File
@@ -17,7 +17,7 @@ limitations under the License.
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
import { useSettingsStore } from '../stores/settingsStore';
import { computed, ref, useId, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@@ -56,25 +56,19 @@ const store = useSettingsStore();
const optionType = computed(() => props.setting.allowMultiple ? 'checkbox' : 'radio');
const selected = ref(normalizeSelectedValue(store.get(props.path) ?? props.setting.default));
watch(selected, (newValue) => {
watch(selected, (newValue, oldValue) => {
store.set(props.path, newValue);
});
const labelId = useId();
</script>
<template>
<div>
<div class="label" :id="labelId">
<div class="label">
{{ t(props.setting.i18n) }}
</div>
<ul class="options-list" :aria-labelledby="labelId">
<ul class="options-list">
<li v-for="option in props.setting.options" :key="option.name">
<label
class="settings-button"
:title="t(option.i18n)"
:aria-label="t('settings.selection.option.ariaLabel', { option: t(option.i18n) })"
>
<label class="settings-button">
<input
:type="optionType"
:name="props.path"
@@ -35,7 +35,16 @@ const getWrapper = function getWrapper ({ i18n = 'switch1', translations = {} }
describe('Switch', () => {
test('shows value from store', async () => {
const wrapper = getWrapper();
const wrapper = mountComponent(Switch, {
attrs: {
setting: {
name: 'switch',
i18n: 'switch1',
type: 'bool'
},
path: 'switch'
}
});
const store = useSettingsStore();
store.set('switch', true);
@@ -116,14 +116,14 @@ const veryNestedSettingsList = [
];
test.for([
({ settings: [], path: ['a'], expected: undefined }),
({ settings: [], path: 'a', expected: undefined }),
({ settings: [
{
type: 'bool',
name: '1a',
i18n: '1a'
}
], path: ['1a'], expected: {
], path: '1a', expected: {
type: 'bool',
name: '1a',
i18n: '1a'
@@ -146,7 +146,7 @@ test.for([
}
]
}
], path: ['1a'], expected: {
], path: '1a', expected: {
type: 'bool',
name: '1a',
i18n: '1a'
@@ -169,7 +169,7 @@ test.for([
}
]
}
], path: ['1b', '2a'], expected: {
], path: '1b.2a', expected: {
type: 'bool',
name: '2a',
i18n: '2a'
@@ -198,7 +198,7 @@ test.for([
}
]
}
], path: ['1b', '2b'], expected: {
], path: '1b.2b', expected: {
type: 'number',
name: '2b',
i18n: '2b',
@@ -234,7 +234,7 @@ test.for([
}
]
}
], path: ['1b', '2b', '3a'], expected: {
], path: '1b.2b.3a', expected: {
type: 'string',
name: '3a',
i18n: '3a'
@@ -269,7 +269,7 @@ test.for([
}
]
}
], path: ['1b'], expected: {
], path: '1b', expected: {
type: 'section',
name: '1b',
i18n: '1b',
@@ -323,7 +323,7 @@ test.for([
}
]
}
], path: ['1b', '2b'], expected: {
], path: '1b.2b', expected: {
type: 'section',
name: '2b',
i18n: '2b',
@@ -366,7 +366,7 @@ test.for([
}
]
}
], path: ['1b', '2b', '3b'], expected: undefined }),
], path: '1b.2b.3b', expected: undefined }),
({ settings: [
{
type: 'bool',
@@ -397,7 +397,7 @@ test.for([
}
]
}
], path: ['1b', '2a', '3a'], expected: undefined }),
], path: '1b.2a.3a', expected: undefined }),
({ settings: [
{
type: 'bool',
@@ -428,7 +428,7 @@ test.for([
}
]
}
], path: ['1b', '2c', '3a'], expected: undefined }),
], path: '1b.2c.3a', expected: undefined }),
({ settings: [
{
type: 'bool',
@@ -459,7 +459,7 @@ test.for([
}
]
}
], path: ['1a', '2b', '3a'], expected: undefined }),
], path: '1a.2b.3a', expected: undefined }),
({ settings: [
{
type: 'bool',
@@ -490,13 +490,13 @@ test.for([
}
]
}
], path: ['1c', '2b', '3a'], expected: undefined }),
({ settings: veryNestedSettingsList, path: ['1b', '2b', '3b', '4b', '5b', '6b', '7a'], expected: {
], path: '1c.2b.3a', expected: undefined }),
({ settings: veryNestedSettingsList, path: '1b.2b.3b.4b.5b.6b.7a', expected: {
type: 'bool',
name: '7a',
i18n: '7a'
} }),
({ settings: veryNestedSettingsList, path: ['1b', '2b', '3b', '4b', '5b'], expected: {
({ settings: veryNestedSettingsList, path: '1b.2b.3b.4b.5b', expected: {
type: 'section',
name: '5b',
i18n: '5b',
@@ -520,7 +520,7 @@ test.for([
}
]
} }),
({ settings: veryNestedSettingsList, path: ['1b', '2b', '3b', '4ba', '5b', '6b', '7a'], expected: undefined })
({ settings: veryNestedSettingsList, path: '1b.2b.3b.4ba.5b.6b.7a', expected: undefined })
])('returns $expected', ({ settings, path, expected }) => {
expect(getSettingRecursively(path, settings)).toStrictEqual(expected);
});
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -21,7 +21,9 @@ export const getSetting = function getSetting (name, settings) {
export const getSettingRecursively = function getSettingRecursively (sectionPath, settings) {
const oneSettingsLevel = function oneSettingsLevel (cursor, acc) {
const setting = getSetting(cursor[0], acc.content);
if (!setting) return;
if (!setting) {
return;
};
if (setting.type === undefined) setting.type = 'section';
if (setting.type === 'section') {
@@ -38,5 +40,5 @@ export const getSettingRecursively = function getSettingRecursively (sectionPath
return;
};
};
return oneSettingsLevel(sectionPath, { content: settings });
return oneSettingsLevel(sectionPath.split('.'), { content: settings });
};
@@ -73,27 +73,12 @@ export const validateEntry = function validateEntry (entry, path) {
throw new Error(`[settings] "${path}.default" must be a string`);
}
if (entry.type === 'selection') {
if (entry.allowMultiple) {
if (!Array.isArray(entry.default)) {
throw new Error(`[settings] "${path}.default" must be an array`);
}
const allOptions = entry.options.map((option) => option.name);
entry.default.forEach((defaultValue, index) => {
if (typeof defaultValue !== 'string') {
throw new Error(`[settings] "${path}.default[${index}]" must be a string`);
}
if (!allOptions.includes(defaultValue)) {
throw new Error(`[settings] option "${path}.default[${index}]" does not exist`);
}
});
} else {
if (typeof entry.default !== 'string') {
throw new Error(`[settings] "${path}.default" must be a string`);
};
if (!entry.options.map((option) => option.name).includes(entry.default)) {
throw new Error(`[settings] option "${path}.default" does not exist`);
};
}
if (typeof entry.default !== 'string') {
throw new Error(`[settings] "${path}.default" must be a string`);
};
if (!entry.options.map((option) => option.name).includes(entry.default)) {
throw new Error(`[settings] option "${path}.default" does not exist`);
};
};
}
}
+1 -1
View File
@@ -43,7 +43,7 @@ watchEffect(() => {
const updateSettings = function updateSettings () {
if (activeSection.value) {
const setting = getSettingRecursively(activeSection.value.split('.'), settings.value);
const setting = getSettingRecursively(activeSection.value, settings.value);
if (!setting) {
router.push('/settings');
} else {
+2 -47
View File
@@ -14,60 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
-->
<script setup>
import SidebarExpandButton from './SidebarExpandButton.vue';
const props = defineProps({
'expanded': {
default: false,
required: false,
type: Boolean
}
});
const emit = defineEmits(['toggleExpanded']);
const toggleSidebar = function toggleSidebar () {
emit('toggleExpanded');
};
</script>
<template>
<nav class="sidebar" :class="{ expanded }">
<div class="sidebar-controls">
<SidebarExpandButton @click="toggleSidebar" class="sidebar-expand-button" />
</div>
<div class="sidebar-content" v-if="props.expanded">
<slot />
</div>
<nav class="sidebar">
<slot />
</nav>
</template>
<style scoped>
.sidebar {
border-right: 1px solid var(--border);
}
.sidebar-controls {
display: flex;
align-items: center;
justify-content: end;
padding: 1em 20px;
padding-bottom: 0;
margin-bottom: 8px;
}
.sidebar:not(.expanded) .sidebar-controls {
padding: 1em;
}
@media (max-width: 48rem) {
.sidebar:not(.expanded) .sidebar-expand-button {
display: none;
}
}
.sidebar-content {
padding: 20px;
padding-top: 0;
}
</style>
@@ -1,41 +0,0 @@
<!--
Copyright 2026 Seekra
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<script setup>
import Icon from '@/features/icons/components/Icon.vue';
const emit = defineEmits(['click']);
const click = function click (event) {
emit('click', event);
};
</script>
<template>
<button class="button sidebar-expand-button">
<Icon name="sidebar-expand" @click="click" />
</button>
</template>
<style scoped>
.sidebar-expand-button {
display: flex;
justify-content: center;
align-items: center;
padding: 4px;
border-radius: 10px;
}
</style>
+3 -64
View File
@@ -16,30 +16,11 @@ limitations under the License.
<script setup>
import Sidebar from '@/features/sidebar/components/Sidebar.vue';
import SidebarExpandButton from '@/features/sidebar/components/SidebarExpandButton.vue';
import { ref } from 'vue';
const props = defineProps({
'expanded': {
default: false,
required: false,
type: Boolean
}
});
const expanded = ref(props.expanded);
const toggleExpanded = function toggleExpanded () {
expanded.value = !expanded.value;
};
</script>
<template>
<div class="layout-container" :class="{ expanded }">
<div class="sidebar-expand-button-container">
<SidebarExpandButton class="sidebar-expand-button" @click="toggleExpanded" />
</div>
<Sidebar @toggle-expanded="toggleExpanded" :expanded="expanded" class="sidebar">
<div class="layout-container">
<Sidebar>
<slot name="sidebar" />
</Sidebar>
<main class="main-content">
@@ -51,53 +32,11 @@ const toggleExpanded = function toggleExpanded () {
<style scoped>
.layout-container {
display: grid;
grid-template-columns: calc(32px + 2em) 1fr;
grid-template-rows: auto 1fr;
grid-template-areas:
"sidebar-expand sidebar-expand"
"sidebar main-content";
grid-template-columns: min(24%, 280px) 1fr;
border-top: 1px solid var(--border);
}
.layout-container.expanded {
grid-template-columns: min(24%, 280px) 1fr;
}
.sidebar-expand-button-container {
grid-area: sidebar-expand;
display: none;
}
.sidebar {
grid-area: sidebar;
}
@media (max-width: 48rem) {
.layout-container {
grid-template-columns: 0 1fr;
}
.layout-container.expanded {
grid-template-columns: 1fr 0;
}
.layout-container:not(.expanded) .sidebar-expand-button-container {
justify-content: end;
display: block;
padding: 20px;
}
.layout-container:not(.expanded) .main-content {
padding-top: 0;
}
.layout-container.expanded .main-content {
display: none;
}
}
.main-content {
grid-area: main-content;
padding: 20px;
}
</style>
+1 -6
View File
@@ -55,11 +55,6 @@
"ariaLabel": "Geht zur Einstellungsseite {page}"
}
},
"noSectionSelected": "Kein Einstellungsabschnitt ist ausgewählt.",
"selection": {
"option": {
"ariaLabel": "Wählt die Option {option} aus"
}
}
"noSectionSelected": "Kein Einstellungsabschnitt ist ausgewählt."
}
}
+1 -6
View File
@@ -55,11 +55,6 @@
"ariaLabel": "Goes to settings page {page}"
}
},
"noSectionSelected": "No settings section is selected.",
"selection": {
"option": {
"ariaLabel": "Selects option {option}"
}
}
"noSectionSelected": "No settings section is selected."
}
}
+1 -24
View File
@@ -38,28 +38,5 @@
}
}
},
"slogan": "Hecho para buscar.",
"settings": {
"input": {
"done": {
"title": "Guardar valor",
"ariaLabel": "Guarda el valor introducido"
}
},
"switch": {
"title": "Cambiar valor",
"ariaLabel": "Cambia el valor"
},
"pages": {
"link": {
"ariaLabel": "Va a la página de configuración {page}"
}
},
"noSectionSelected": "No se ha seleccionado ninguna sección de configuración.",
"selection": {
"option": {
"ariaLabel": "Selecciona la opción {option}"
}
}
}
"slogan": "Hecho para buscar."
}
+1 -24
View File
@@ -38,28 +38,5 @@
}
}
},
"slogan": "Conçu pour chercher.",
"settings": {
"input": {
"done": {
"title": "Enregistrer la valeur",
"ariaLabel": "Enregistre la valeur saisie"
}
},
"switch": {
"title": "Basculer la valeur",
"ariaLabel": "Bascule la valeur"
},
"pages": {
"link": {
"ariaLabel": "Accéder à la page de paramètres {page}"
}
},
"noSectionSelected": "Aucune section des paramètres n'est sélectionnée.",
"selection": {
"option": {
"ariaLabel": "Sélectionne l'option {option}"
}
}
}
"slogan": "Conçu pour chercher."
}
+1 -24
View File
@@ -38,28 +38,5 @@
}
}
},
"slogan": "Costruito per cercare.",
"settings": {
"input": {
"done": {
"title": "Salva valore",
"ariaLabel": "Salva il valore inserito"
}
},
"switch": {
"title": "Attiva/disattiva valore",
"ariaLabel": "Attiva o disattiva il valore"
},
"pages": {
"link": {
"ariaLabel": "Vai alla pagina delle impostazioni {page}"
}
},
"noSectionSelected": "Nessuna sezione delle impostazioni è selezionata.",
"selection": {
"option": {
"ariaLabel": "Seleziona l'opzione {option}"
}
}
}
"slogan": "Costruito per cercare."
}
+1 -24
View File
@@ -38,28 +38,5 @@
}
}
},
"slogan": "Feito para pesquisar.",
"settings": {
"input": {
"done": {
"title": "Guardar valor",
"ariaLabel": "Guarda o valor introduzido"
}
},
"switch": {
"title": "Alternar valor",
"ariaLabel": "Alterna o valor"
},
"pages": {
"link": {
"ariaLabel": "Ir para a página de definições {page}"
}
},
"noSectionSelected": "Nenhuma secção de definições está selecionada.",
"selection": {
"option": {
"ariaLabel": "Seleciona a opção {option}"
}
}
}
"slogan": "Feito para pesquisar."
}
+14 -13
View File
@@ -59,19 +59,6 @@ const router = createRouter({
routes
});
export const updatePageTitle = function updatePageTitle (route) {
const title =
typeof route.meta.title === 'function'
? route.meta.title(route)
: route.meta.title;
if (title) {
document.title = `${title} - Seekra`;
} else {
document.title = 'Seekra';
};
};
// remove trailing slash(es)
router.beforeEach((route) => {
if (route.path !== '/' && route.path.endsWith('/')) {
@@ -84,4 +71,18 @@ router.beforeEach((route) => {
};
});
// set page title
router.afterEach(to => {
const title =
typeof to.meta.title === 'function'
? to.meta.title(to)
: to.meta.title;
if (title) {
document.title = `${title} - Seekra`;
} else {
document.title = 'Seekra';
};
});
export default router;
+2 -4
View File
@@ -15,8 +15,6 @@ limitations under the License.
*/
body {
background-color: var(--light-bg);
color: var(--dark);
margin: 0;
font-size: 16px;
}
@@ -25,7 +23,7 @@ body {
text-decoration: none;
}
.link:hover:not(.button-link), .link:focus-visible:not(.button-link) {
.link:hover:not(.button-link), .link:focus:not(.button-link) {
outline: none;
text-decoration: underline;
}
@@ -78,7 +76,7 @@ input {
color: inherit;
}
.button:focus-visible {
.button:focus {
background-color: var(--light-hover);
outline: none;
}
+3 -3
View File
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
body {
#app-wrapper {
--primary-color-l-8: oklch(0.897 0.202 260);
--primary-color-l-7: oklch(0.847 0.202 260);
--primary-color-l-6: oklch(0.797 0.202 260);
@@ -87,7 +87,7 @@ body {
}
@media (prefers-color-scheme: dark) {
body.color-scheme-auto {
#app-wrapper.color-scheme-auto {
--dark-l-8: var(--white-d-8);
--dark-l-7: var(--white-d-7);
--dark-l-6: var(--white-d-6);
@@ -121,7 +121,7 @@ body {
}
}
body.dark {
#app-wrapper.dark {
--dark-l-8: var(--white-d-8);
--dark-l-7: var(--white-d-7);
--dark-l-6: var(--white-d-6);
+89 -91
View File
@@ -14,111 +14,109 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { describe, expect, test } from 'vitest';
import { expect, test } from 'vitest';
import { ensureUnit } from '../cssDimensions';
describe('cssDimensions', () => {
test.for([
{ dimension: null, expected: '0px' },
{ dimension: undefined, expected: '0px' },
test.for([
{ dimension: null, expected: '0px' },
{ dimension: undefined, expected: '0px' },
{ dimension: 0, expected: '0px' },
{ dimension: -0, expected: '0px' },
{ dimension: -1, expected: '-1px' },
{ dimension: -42, expected: '-42px' },
{ dimension: 1, expected: '1px' },
{ dimension: 42, expected: '42px' },
{ dimension: 0, expected: '0px' },
{ dimension: -0, expected: '0px' },
{ dimension: -1, expected: '-1px' },
{ dimension: -42, expected: '-42px' },
{ dimension: 1, expected: '1px' },
{ dimension: 42, expected: '42px' },
{ dimension: 0.0, expected: '0px' },
{ dimension: -0.0, expected: '0px' },
{ dimension: -1.0, expected: '-1px' },
{ dimension: -42.0, expected: '-42px' },
{ dimension: 1.0, expected: '1px' },
{ dimension: 42.0, expected: '42px' },
{ dimension: 0.0, expected: '0px' },
{ dimension: -0.0, expected: '0px' },
{ dimension: -1.0, expected: '-1px' },
{ dimension: -42.0, expected: '-42px' },
{ dimension: 1.0, expected: '1px' },
{ dimension: 42.0, expected: '42px' },
{ dimension: 0.42, expected: '0.42px' },
{ dimension: -0.42, expected: '-0.42px' },
{ dimension: -1.42, expected: '-1.42px' },
{ dimension: -42.42, expected: '-42.42px' },
{ dimension: 1.42, expected: '1.42px' },
{ dimension: 42.42, expected: '42.42px' },
{ dimension: 0.42, expected: '0.42px' },
{ dimension: -0.42, expected: '-0.42px' },
{ dimension: -1.42, expected: '-1.42px' },
{ dimension: -42.42, expected: '-42.42px' },
{ dimension: 1.42, expected: '1.42px' },
{ dimension: 42.42, expected: '42.42px' },
{ dimension: '0', expected: '0px' },
{ dimension: '-0', expected: '-0px' },
{ dimension: '-1', expected: '-1px' },
{ dimension: '-42', expected: '-42px' },
{ dimension: '1', expected: '1px' },
{ dimension: '42', expected: '42px' },
{ dimension: '0', expected: '0px' },
{ dimension: '-0', expected: '-0px' },
{ dimension: '-1', expected: '-1px' },
{ dimension: '-42', expected: '-42px' },
{ dimension: '1', expected: '1px' },
{ dimension: '42', expected: '42px' },
{ dimension: '0.0', expected: '0.0px' },
{ dimension: '-0.0', expected: '-0.0px' },
{ dimension: '-1.0', expected: '-1.0px' },
{ dimension: '-42.0', expected: '-42.0px' },
{ dimension: '1.0', expected: '1.0px' },
{ dimension: '42.0', expected: '42.0px' },
{ dimension: '0.0', expected: '0.0px' },
{ dimension: '-0.0', expected: '-0.0px' },
{ dimension: '-1.0', expected: '-1.0px' },
{ dimension: '-42.0', expected: '-42.0px' },
{ dimension: '1.0', expected: '1.0px' },
{ dimension: '42.0', expected: '42.0px' },
{ dimension: '0.42', expected: '0.42px' },
{ dimension: '-0.42', expected: '-0.42px' },
{ dimension: '-1.42', expected: '-1.42px' },
{ dimension: '-42.42', expected: '-42.42px' },
{ dimension: '1.42', expected: '1.42px' },
{ dimension: '42.42', expected: '42.42px' },
{ dimension: '0.42', expected: '0.42px' },
{ dimension: '-0.42', expected: '-0.42px' },
{ dimension: '-1.42', expected: '-1.42px' },
{ dimension: '-42.42', expected: '-42.42px' },
{ dimension: '1.42', expected: '1.42px' },
{ dimension: '42.42', expected: '42.42px' },
{ dimension: '0px', expected: '0px' },
{ dimension: '-0px', expected: '-0px' },
{ dimension: '-1px', expected: '-1px' },
{ dimension: '-42px', expected: '-42px' },
{ dimension: '1px', expected: '1px' },
{ dimension: '42px', expected: '42px' },
{ dimension: '0px', expected: '0px' },
{ dimension: '-0px', expected: '-0px' },
{ dimension: '-1px', expected: '-1px' },
{ dimension: '-42px', expected: '-42px' },
{ dimension: '1px', expected: '1px' },
{ dimension: '42px', expected: '42px' },
{ dimension: '0.0px', expected: '0.0px' },
{ dimension: '-0.0px', expected: '-0.0px' },
{ dimension: '-1.0px', expected: '-1.0px' },
{ dimension: '-42.0px', expected: '-42.0px' },
{ dimension: '1.0px', expected: '1.0px' },
{ dimension: '42.0px', expected: '42.0px' },
{ dimension: '0.0px', expected: '0.0px' },
{ dimension: '-0.0px', expected: '-0.0px' },
{ dimension: '-1.0px', expected: '-1.0px' },
{ dimension: '-42.0px', expected: '-42.0px' },
{ dimension: '1.0px', expected: '1.0px' },
{ dimension: '42.0px', expected: '42.0px' },
{ dimension: '0.42px', expected: '0.42px' },
{ dimension: '-0.42px', expected: '-0.42px' },
{ dimension: '-1.42px', expected: '-1.42px' },
{ dimension: '-42.42px', expected: '-42.42px' },
{ dimension: '1.42px', expected: '1.42px' },
{ dimension: '42.42px', expected: '42.42px' },
{ dimension: '0.42px', expected: '0.42px' },
{ dimension: '-0.42px', expected: '-0.42px' },
{ dimension: '-1.42px', expected: '-1.42px' },
{ dimension: '-42.42px', expected: '-42.42px' },
{ dimension: '1.42px', expected: '1.42px' },
{ dimension: '42.42px', expected: '42.42px' },
{ dimension: '0em', expected: '0em' },
{ dimension: '-0em', expected: '-0em' },
{ dimension: '-1em', expected: '-1em' },
{ dimension: '-42em', expected: '-42em' },
{ dimension: '1em', expected: '1em' },
{ dimension: '42em', expected: '42em' },
{ dimension: '0em', expected: '0em' },
{ dimension: '-0em', expected: '-0em' },
{ dimension: '-1em', expected: '-1em' },
{ dimension: '-42em', expected: '-42em' },
{ dimension: '1em', expected: '1em' },
{ dimension: '42em', expected: '42em' },
{ dimension: '0.0em', expected: '0.0em' },
{ dimension: '-0.0em', expected: '-0.0em' },
{ dimension: '-1.0em', expected: '-1.0em' },
{ dimension: '-42.0em', expected: '-42.0em' },
{ dimension: '1.0em', expected: '1.0em' },
{ dimension: '42.0em', expected: '42.0em' },
{ dimension: '0.0em', expected: '0.0em' },
{ dimension: '-0.0em', expected: '-0.0em' },
{ dimension: '-1.0em', expected: '-1.0em' },
{ dimension: '-42.0em', expected: '-42.0em' },
{ dimension: '1.0em', expected: '1.0em' },
{ dimension: '42.0em', expected: '42.0em' },
{ dimension: '0.42em', expected: '0.42em' },
{ dimension: '-0.42em', expected: '-0.42em' },
{ dimension: '-1.42em', expected: '-1.42em' },
{ dimension: '-42.42em', expected: '-42.42em' },
{ dimension: '1.42em', expected: '1.42em' },
{ dimension: '42.42em', expected: '42.42em' },
{ dimension: '0.42em', expected: '0.42em' },
{ dimension: '-0.42em', expected: '-0.42em' },
{ dimension: '-1.42em', expected: '-1.42em' },
{ dimension: '-42.42em', expected: '-42.42em' },
{ dimension: '1.42em', expected: '1.42em' },
{ dimension: '42.42em', expected: '42.42em' },
{ dimension: 'calc(42px - 1em)', expected: 'calc(42px - 1em)' },
{ dimension: 'calc(42px)', expected: 'calc(42px)' },
{ dimension: 'calc(42vh)', expected: 'calc(42vh)' },
{ dimension: 'min(42px, 1em)', expected: 'min(42px, 1em)' },
{ dimension: 'max(42px, 5rem)', expected: 'max(42px, 5rem)' },
{ dimension: 'clamp(42vh, 23vw, 13cap)', expected: 'clamp(42vh, 23vw, 13cap)' },
{ dimension: 'calc(42px - 1em)', expected: 'calc(42px - 1em)' },
{ dimension: 'calc(42px)', expected: 'calc(42px)' },
{ dimension: 'calc(42vh)', expected: 'calc(42vh)' },
{ dimension: 'min(42px, 1em)', expected: 'min(42px, 1em)' },
{ dimension: 'max(42px, 5rem)', expected: 'max(42px, 5rem)' },
{ dimension: 'clamp(42vh, 23vw, 13cap)', expected: 'clamp(42vh, 23vw, 13cap)' },
{ dimension: 'calc(42px-1em)', expected: 'calc(42px-1em)' },
{ dimension: 'min(42px,1em)', expected: 'min(42px,1em)' },
{ dimension: 'max(42px,5rem)', expected: 'max(42px,5rem)' },
{ dimension: 'clamp(42vh,23vw,13cap)', expected: 'clamp(42vh,23vw,13cap)' },
{ dimension: 'clamp( 42vh,23vw,13cap )', expected: 'clamp( 42vh,23vw,13cap )' }
])('ensureUnit returns $expected with input $dimension', ({ dimension, expected }) => {
expect(ensureUnit(dimension)).toBe(expected);
});
{ dimension: 'calc(42px-1em)', expected: 'calc(42px-1em)' },
{ dimension: 'min(42px,1em)', expected: 'min(42px,1em)' },
{ dimension: 'max(42px,5rem)', expected: 'max(42px,5rem)' },
{ dimension: 'clamp(42vh,23vw,13cap)', expected: 'clamp(42vh,23vw,13cap)' },
{ dimension: 'clamp( 42vh,23vw,13cap )', expected: 'clamp( 42vh,23vw,13cap )' }
])('ensureUnit returns $expected with input $dimension', ({ dimension, expected }) => {
expect(ensureUnit(dimension)).toBe(expected);
});
+35 -35
View File
@@ -14,48 +14,48 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { describe, expect, test } from 'vitest';
import { expect, test } from 'vitest';
import getCurrentLanguage from '../currentLanguage';
describe('currentLanguage', () => {
test.for([
{ navigatorLanguage: 'en', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr', localStorageLanguage: null, expected: 'fr'},
const locales = [
{ navigatorLanguage: 'en', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-US', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-AU', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-FR', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-US', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-AU', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-FR', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-us', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-au', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'en-us', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'en-au', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'fr-fr', localStorageLanguage: null, expected: 'fr'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'zh-Hant-TW', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'uz-Latn-UZ', localStorageLanguage: null, expected: 'uz'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'zh-Hant-TW', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'uz-Latn-UZ', localStorageLanguage: null, expected: 'uz'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE-u-co-phonebk', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: null, expected: 'en'},
{ navigatorLanguage: 'de-DE-u-co-phonebk', localStorageLanguage: null, expected: 'de'},
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', localStorageLanguage: null, expected: 'zh'},
{ navigatorLanguage: 'en', localStorageLanguage: 'de', expected: 'de'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: 'fr', expected: 'fr'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: 'zh', expected: 'zh'}
])('returns the language $expected (navigator: $navigatorLanguage; local storage: $localStorageLanguage)', ({ navigatorLanguage, localStorageLanguage, expected }) => {
Object.defineProperty(navigator, 'language', {
value: navigatorLanguage,
configurable: true
});
{ navigatorLanguage: 'en', localStorageLanguage: 'de', expected: 'de'},
{ navigatorLanguage: 'de-DE', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'de-de', localStorageLanguage: 'en', expected: 'en'},
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: 'fr', expected: 'fr'},
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: 'zh', expected: 'zh'}
];
if (localStorageLanguage) {
localStorage.setItem('locale', localStorageLanguage);
};
expect(getCurrentLanguage()).toBe(expected);
test.for(locales)('returns the language $expected (navigator: $navigatorLanguage; local storage: $localStorageLanguage)', ({ navigatorLanguage, localStorageLanguage, expected }) => {
Object.defineProperty(navigator, 'language', {
value: navigatorLanguage,
configurable: true
});
if (localStorageLanguage) {
localStorage.setItem('locale', localStorageLanguage);
};
expect(getCurrentLanguage()).toBe(expected);
});