20 Commits

Author SHA1 Message Date
jakob.scheid 17072dbc5a test(settings): add test cases for the selection setting allowMultiple property to the settings validator unit test 2026-06-04 16:38:05 +02:00
jakob.scheid 1d90612bbd test(settings): add unit test for the settings validator validateSettingsConfig function 2026-06-04 16:22:34 +02:00
jakob.scheid f322f19f9e test(settings): add test cases for the settings validator validateSelectionOptions function for test cases that should throw an error 2026-06-04 15:27:55 +02:00
jakob.scheid d0dc66940a test(settings): add test cases for the settings validator validateEntry function for test cases that should throw an error 2026-06-04 15:26:55 +02:00
jakob.scheid 8ff26f4bb8 fix(settings): check selection default value in the settings validator 2026-06-04 15:24:30 +02:00
jakob.scheid 12f67f17ed fix(settings): check the selection options and allowMultiple before and not after the default check in the settings validator 2026-06-04 15:19:12 +02:00
jakob.scheid 8850732ec6 fix(settings): fix indentation in validateEntry in the settings validator 2026-06-04 15:18:11 +02:00
jakob.scheid 940eb84202 fix(settings): check the selection options and allowMultiple after and not in the default check in the settings validator 2026-06-04 15:17:32 +02:00
jakob.scheid c2a9d80cf5 fix(settings): check the name and i18n before the checks for a section in validateEntry in the settings validator 2026-06-04 15:15:37 +02:00
jakob.scheid 8fc2b14ef1 test(settings): add unit test for the settings validator validateEntry function for test cases that should throw an error 2026-06-04 15:12:49 +02:00
jakob.scheid 6731b1981b fix(settings): do not enforce allowMultiple set for selection settings 2026-06-04 15:08:33 +02:00
jakob.scheid 4584fd92d1 test(settings): add unit test for the settings validator validateEntry function for test cases that should not throw an error 2026-06-04 14:53:47 +02:00
jakob.scheid 42bd37e9e4 test(settings): add unit test for the settings validator validateSelectionOptions function for test cases that should throw an error 2026-06-04 14:40:34 +02:00
jakob.scheid d9906c782f test(settings): add unit test for the settings validator validateSelectionOptions function for test cases that should not throw an error 2026-06-04 14:38:05 +02:00
jakob.scheid c1ce580ed7 test(settings): update test description for the unit test for the settings validator assertString function for test cases that should throw an error 2026-06-04 14:02:48 +02:00
jakob.scheid e0419d57cc test(settings): add unit test for the settings validator assertType function for test cases that should throw an error 2026-06-04 14:02:09 +02:00
jakob.scheid 751b2663b3 test(settings): add unit test for the settings validator assertType function for test cases that should not throw an error 2026-06-04 13:59:14 +02:00
jakob.scheid c53d98a95c test(settings): add unit test for the settings validator assertString function for test cases that should not throw an error 2026-06-04 13:56:16 +02:00
jakob.scheid 4d612f2c19 test(settings): add unit test for the settings validator assertString function for test cases that should throw an error 2026-06-04 13:53:47 +02:00
jakob.scheid 4d8521c130 test(settings): Add settings validator utility test boilerplate 2026-06-04 12:55:23 +02:00
11 changed files with 28 additions and 221 deletions
+1 -2
View File
@@ -17,11 +17,10 @@
"vue-router": "^5.0.6"
},
"devDependencies": {
"@vue/test-utils": "^2.4.6",
"@vitejs/plugin-vue": "^6.0.6",
"jsdom": "^29.1.1",
"vite": "^8.0.10",
"vite-plugin-html": "^3.2.2",
"vitest": "^4.1.7"
}
}
}
+2 -5
View File
@@ -42,18 +42,15 @@ watch(colorScheme, val => updateColorScheme(val))
</template>
<style scoped>
#app-wrapper {
.main-content {
--main-content-padding-x: 30px;
--main-content-padding-y: 40px;
--main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x);
}
.main-content {
flex-grow: 1;
}
@media (max-width: 48rem) {
#app-wrapper {
.main-content {
--main-content-padding-x: 15px;
}
}
@@ -1,114 +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.
*/
import { expect, test, describe, vi, beforeEach } from 'vitest';
import { mount } from '@vue/test-utils';
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
import { loadLanguage } from '@/i18n';
vi.mock('@/i18n', () => ({
loadLanguage: vi.fn(() => Promise.resolve()),
LANGUAGES_RTL: ['ar', 'he'],
SUPPORTED_LANGUAGES: ['en', 'de', 'ar']
}));
vi.mock('vue-i18n', () => ({
useI18n: () => ({
t: (key) => key,
locale: { value: 'de' }
})
}));
vi.mock('@/features/icons/components/Icon.vue', () => ({
default: {
name: 'Icon',
template: '<span>Icon</span>'
}
}));
describe('LanguageSwitchButton.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
document.documentElement.lang = '';
document.documentElement.dir = '';
});
test('renders correctly with initial state closed', () => {
const wrapper = mount(LanguageSwitchButton);
expect(wrapper.find('.language-button').exists()).toBe(true);
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
expect(wrapper.find('.language-button').attributes('aria-expanded')).toBe('false');
});
test('opens the dropdown when language button is clicked', async () => {
const wrapper = mount(LanguageSwitchButton);
const button = wrapper.find('.language-button');
await button.trigger('click');
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
expect(button.attributes('aria-expanded')).toBe('true');
});
const languageTestCases = [
{ code: 'en', expectedDir: 'ltr' },
{ code: 'de', expectedDir: 'ltr' },
{ code: 'ar', expectedDir: 'rtl' }
];
test.for(languageTestCases)('selectLanguage($code) sets localStorage, html attributes and changes layout direction to $expectedDir', async ({ code, expectedDir }) => {
const wrapper = mount(LanguageSwitchButton);
await wrapper.find('.language-button').trigger('click');
const options = wrapper.findAll('.language-dropdown li');
const optionToClick = options.find(opt => opt.text().includes(code));
await optionToClick.trigger('click');
expect(loadLanguage).toHaveBeenCalledWith(code);
expect(localStorage.getItem('locale')).toBe(code);
expect(document.documentElement.lang).toBe(code);
expect(document.documentElement.dir).toBe(expectedDir);
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
});
test('closes the dropdown when clicking outside the component', async () => {
const wrapper = mount(LanguageSwitchButton, {
attachTo: document.body
});
await wrapper.find('.language-button').trigger('click');
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
await new Promise(resolve => setTimeout(resolve, 0));
const externalDiv = document.createElement('div');
document.body.appendChild(externalDiv);
const clickEvent = new MouseEvent('click', { bubbles: true });
externalDiv.dispatchEvent(clickEvent);
await wrapper.vm.$nextTick();
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
wrapper.unmount();
externalDiv.remove();
});
});
+1 -1
View File
@@ -42,7 +42,7 @@ import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px var(--main-content-padding-x);
padding: 18px 40px;
height: 42px;
}
+1 -3
View File
@@ -73,8 +73,7 @@ const submitSearch = function () {
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding));
padding: var(--padding);
padding-left: var(--padding-left);
width: 100%;
box-sizing: border-box;
width: calc(100% - var(--padding-left));
}
.search-wrapper input {
@@ -84,7 +83,6 @@ const submitSearch = function () {
font-size: 1rem;
background: transparent;
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
padding-left: 12px;
}
.search-button {
@@ -53,6 +53,10 @@ limitations under the License.
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
*/
/**
* @typedef {Object} SettingsConfig
* @property {SettingConfigEntry[]} contents
*/
/**
* @typedef {{ type: 'bool', name: string, i18n: string, description?: string, default: boolean }} BoolSettingConfig
* @typedef {{ type: 'number', name: string, i18n: string, description?: string, default: number }} NumberSettingConfig
@@ -60,6 +64,5 @@ limitations under the License.
* @typedef {{ type: 'selection', name: string, i18n: string, description?: string, default: string | string[], allowMultiple?: boolean, options: SelectionOption[] }} SelectionSettingConfig
* @typedef {{ type: 'section', name: string, i18n?: string, description?: string, content: SettingConfigEntry[] }} SectionSettingConfig
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
* @typedef {{ contents: SettingConfigEntry[] }} FirstLevelSettingConfigEntry
* @typedef {{ contents: FirstLevelSettingConfigEntry[] }} SettingsConfig
* @typedef {{ contents: SettingConfigEntry[] }} SettingsConfig
*/
@@ -33,6 +33,7 @@ describe('validateSettingsConfig', () => {
{ raw: {
contents: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
@@ -48,6 +49,7 @@ describe('validateSettingsConfig', () => {
{ raw: {
contents: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
@@ -59,6 +61,7 @@ describe('validateSettingsConfig', () => {
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
@@ -74,6 +77,7 @@ describe('validateSettingsConfig', () => {
{ raw: {
contents: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
@@ -85,6 +89,7 @@ describe('validateSettingsConfig', () => {
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
@@ -97,6 +102,7 @@ describe('validateSettingsConfig', () => {
]
},
{
type: 'section',
name: 'aSection',
i18n: 'settings.settings.aSection',
content: [
@@ -135,6 +141,7 @@ describe('validateSettingsConfig', () => {
{ raw: {
contents: [
{
type: 'section',
name: 'general',
i18n: 'settings.settings.general',
content: [
@@ -146,6 +153,7 @@ describe('validateSettingsConfig', () => {
]
},
{
type: 'section',
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
@@ -158,6 +166,7 @@ describe('validateSettingsConfig', () => {
]
},
{
type: 'section',
name: 'aSection',
i18n: 'settings.settings.aSection',
content: [
@@ -194,74 +203,6 @@ describe('validateSettingsConfig', () => {
},
]
}, expected: true },
{ raw: {
contents: [
{
type: 'bool',
name: 'aStandaloneBooleanSetting',
i18n: 'settings.settings.aStandaloneBooleanSetting',
default: true
},
{
name: 'general',
i18n: 'settings.settings.general',
content: [
{
type: 'bool',
name: 'Enable feature 42',
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'copyOfGeneral',
i18n: 'settings.settings.copyOfGeneral',
content: [
{
type: 'bool',
name: 'Enable feature 42',
default: false,
i18n: 'settings.settings.general.enableFeature42'
}
]
},
{
name: 'aSection',
i18n: 'settings.settings.aSection',
content: [
{
type: 'bool',
name: 'Enable feature 43',
i18n: 'settings.settings.aSection.enableFeature43'
},
{
type: 'selection',
name: 'language',
i18n: 'settings.settings.aSection.language.label',
default: 'en',
allowMultiple: false,
options: [
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
]
},
{
type: 'section',
name: 'section2',
i18n: 'settings.settings.aSection.section2.label',
content: [
{
type: 'string',
name: 'string',
i18n: 'settings.settings.aSection.sections.string',
default: 'str'
}
]
},
]
},
]
}, expected: false },
{ raw: {
contents: [
{
@@ -83,19 +83,6 @@ export const validateEntry = function validateEntry (entry, path) {
}
}
export const validateFirstLevelSection = function validateFirstLevelSection (section, path) {
assertString(section.name);
assertString(section.i18n);
if (!Array.isArray(section.content)) {
throw new Error(`[settings] "${path}.content" must be an array`);
};
section.content.forEach((entry, i) =>
validateEntry(entry, `${path}.content[${i}]`)
);
};
/**
* Validates a raw settings config object.
* @param {unknown} raw
@@ -110,7 +97,7 @@ export function validateSettingsConfig(raw) {
throw new Error('[settings] "contents" must be an array');
}
raw.contents.forEach((entry, i) =>
validateFirstLevelSection(entry, `contents[${i}]`)
validateEntry(entry, `contents[${i}]`)
);
return { valid: true, config: raw };
} catch (e) {
+5 -8
View File
@@ -16,18 +16,15 @@ limitations under the License.
import { createApp } from 'vue'
import App from './App.vue'
import { i18n, loadLanguage } from './i18n'
import getCurrentLanguage from './utils/currentLanguage'
import { i18n, loadLanguage } from './i18n';
import getCurrentLanguage from './utils/currentLanguage';
import router from './router'
import './styles/common.css'
import './styles/variables/colors.css'
(async () => {
await loadLanguage(getCurrentLanguage())
await loadLanguage(getCurrentLanguage());
createApp(App)
createApp(App)
.use(router)
.use(i18n)
.mount('#app')
})()
.mount('#app')
+1 -1
View File
@@ -43,7 +43,7 @@ const { t } = useI18n();
.error-message{
margin: 0;
font-weight: 600;
font-size: 1.75rem;
font-size: 2vw;
}
#link {
align-items: center;
+2 -3
View File
@@ -67,10 +67,9 @@ const submitSearch = function () {
max-width: 100%;
}
.slogan {
margin-top: 1rem;
.slogan{
margin: 0;
font-size: small;
line-height: normal;
}
.search-container {