From 9ff3f600c0090c3ec11526cc617fa825c238b560 Mon Sep 17 00:00:00 2001 From: Annabel Church <215145+arc64@users.noreply.github.com> Date: Thu, 5 Jun 2025 11:45:15 +0000 Subject: [PATCH 1/2] fix(settings): show active forced locale or language instead of 'No locale set' (fixes #41543) Signed-off-by: Annabel Church <215145+arc64@users.noreply.github.com> --- .../lib/Settings/Personal/PersonalInfo.php | 52 +++++- .../LanguageSection/LanguageSection.vue | 28 +++- .../__tests__/LanguageSection.spec.js | 152 ++++++++++++++++++ .../LocaleSection/LocaleSection.vue | 35 +++- .../__tests__/LocaleSection.spec.js | 152 ++++++++++++++++++ 5 files changed, 403 insertions(+), 16 deletions(-) create mode 100644 apps/settings/src/components/PersonalInfo/LanguageSection/__tests__/LanguageSection.spec.js create mode 100644 apps/settings/src/components/PersonalInfo/LocaleSection/__tests__/LocaleSection.spec.js diff --git a/apps/settings/lib/Settings/Personal/PersonalInfo.php b/apps/settings/lib/Settings/Personal/PersonalInfo.php index 84b379f2e879b..bd89e8b572599 100644 --- a/apps/settings/lib/Settings/Personal/PersonalInfo.php +++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php @@ -224,6 +224,24 @@ function (IAccountProperty $property) { return $emailMap; } + /** + * Validates a forced language setting against available languages + */ + private function validateForcedLanguage(string $forcedLanguage, array $languages): ?array { + $allLanguages = array_merge($languages['commonLanguages'], $languages['otherLanguages']); + $forcedLang = array_filter($allLanguages, fn($lang) => $lang['code'] === $forcedLanguage); + $forcedLang = reset($forcedLang); + + if ($forcedLang && isset($forcedLang['name'])) { + return [ + 'code' => $forcedLanguage, + 'name' => $forcedLang['name'] + ]; + } + + return null; + } + /** * returns the user's active language, common languages, and other languages in an * associative array @@ -231,11 +249,16 @@ function (IAccountProperty $property) { private function getLanguageMap(IUser $user): array { $forceLanguage = $this->config->getSystemValue('force_language', false); if ($forceLanguage !== false) { + $languages = $this->l10nFactory->getLanguages(); + $validated = $this->validateForcedLanguage($forceLanguage, $languages); + + if ($validated !== null) { + return ['forcedLanguage' => $validated]; + } return []; } $uid = $user->getUID(); - $userConfLang = $this->config->getUserValue($uid, 'core', 'lang', $this->l10nFactory->findLanguage()); $languages = $this->l10nFactory->getLanguages(); @@ -261,9 +284,32 @@ private function getLanguageMap(IUser $user): array { ); } + /** + * Validates a forced locale setting against available locales + */ + private function validateForcedLocale(string $forcedLocale, array $localeCodes): ?array { + $forcedLocaleObj = array_filter($localeCodes, fn($locale) => $locale['code'] === $forcedLocale); + $forcedLocaleObj = reset($forcedLocaleObj); + + if ($forcedLocaleObj && isset($forcedLocaleObj['name'])) { + return [ + 'code' => $forcedLocale, + 'name' => $forcedLocaleObj['name'] + ]; + } + + return null; + } + private function getLocaleMap(IUser $user): array { - $forceLanguage = $this->config->getSystemValue('force_locale', false); - if ($forceLanguage !== false) { + $forceLocale = $this->config->getSystemValue('force_locale', false); + if ($forceLocale !== false) { + $localeCodes = $this->l10nFactory->findAvailableLocales(); + $validated = $this->validateForcedLocale($forceLocale, $localeCodes); + + if ($validated !== null) { + return ['forcedLocale' => $validated]; + } return []; } diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue index 4e92436fd6369..f1971f77a8aaf 100644 --- a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue +++ b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue @@ -14,6 +14,9 @@ :other-languages="otherLanguages" :language.sync="language" /> + + {{ t('settings', 'Language is forced to {language} by the administrator', { language: forcedLanguage.name }) }} + {{ t('settings', 'No language set') }} @@ -22,14 +25,13 @@ diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/__tests__/LanguageSection.spec.js b/apps/settings/src/components/PersonalInfo/LanguageSection/__tests__/LanguageSection.spec.js new file mode 100644 index 0000000000000..f7a2a04c12419 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/LanguageSection/__tests__/LanguageSection.spec.js @@ -0,0 +1,152 @@ +import { mount } from '@vue/test-utils' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { loadState } from '@nextcloud/initial-state' +import LanguageSection from '../LanguageSection.vue' + +/** + * Mock child components + */ +vi.mock('../Language.vue', () => ({ + default: { + name: 'Language', + template: '
', + props: { + inputId: String, + commonLanguages: Array, + otherLanguages: Array, + language: Object, + }, + }, +})) + +vi.mock('../shared/HeaderBar.vue', () => ({ + default: { + name: 'HeaderBar', + template: '
', + props: { + inputId: String, + readable: String, + }, + }, +})) + +/** + * Mock Nextcloud modules + */ +vi.mock('../../../constants/AccountPropertyConstants.js', () => ({ + ACCOUNT_SETTING_PROPERTY_ENUM: { LANGUAGE: 'language' }, + ACCOUNT_SETTING_PROPERTY_READABLE_ENUM: { LANGUAGE: 'Language' }, +})) + +vi.mock('@nextcloud/initial-state', () => ({ + loadState: vi.fn(() => ({ + languageMap: { + activeLanguage: { code: 'en', name: 'English' }, + commonLanguages: [{ code: 'en', name: 'English' }], + otherLanguages: [{ code: 'de', name: 'German' }], + }, + })), +})) + +vi.mock('@nextcloud/l10n', () => ({ + t: (app, text, params) => { + if (params) { + return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match) + } + return text + }, + getLanguage: () => 'en', + isRTL: () => false, + translate: (app, text, params) => { + if (params) { + return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match) + } + return text + }, +})) + +describe('LanguageSection', () => { + let wrapper + + const mountComponent = () => { + return mount(LanguageSection) + } + + beforeEach(() => { + wrapper = mountComponent() + }) + + describe('when language is user-configurable', () => { + const validLanguageData = { + languageMap: { + activeLanguage: { code: 'en', name: 'English' }, + commonLanguages: [{ code: 'en', name: 'English' }], + otherLanguages: [{ code: 'de', name: 'German' }], + }, + } + + beforeEach(async () => { + vi.mocked(loadState).mockReturnValueOnce(validLanguageData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('enables language selection', () => { + expect(wrapper.vm.isEditable).toBe(true) + expect(wrapper.findComponent({ name: 'Language' }).exists()).toBe(true) + }) + + it('passes correct props to Language', () => { + const language = wrapper.findComponent({ name: 'Language' }) + expect(language.props('inputId')).toBe('account-setting-language') + expect(language.props('commonLanguages')).toEqual(validLanguageData.languageMap.commonLanguages) + expect(language.props('otherLanguages')).toEqual(validLanguageData.languageMap.otherLanguages) + expect(language.props('language')).toEqual(validLanguageData.languageMap.activeLanguage) + }) + }) + + describe('with empty language data', () => { + it('handles empty languageMap', async () => { + const emptyData = { languageMap: {} } + vi.mocked(loadState).mockReturnValueOnce(emptyData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + expect(wrapper.vm.isEditable).toBe(false) + expect(wrapper.vm.language).toBeNull() + expect(wrapper.vm.commonLanguages).toEqual([]) + expect(wrapper.vm.otherLanguages).toEqual([]) + expect(wrapper.vm.forcedLanguage).toBeNull() + expect(wrapper.findComponent({ name: 'Language' }).exists()).toBe(false) + }) + }) + + describe('when language is forced by administrator', () => { + const forcedLanguageData = { + languageMap: { + forcedLanguage: { code: 'de', name: 'German' }, + }, + } + + beforeEach(async () => { + vi.mocked(loadState).mockReturnValueOnce(forcedLanguageData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('disables language selection', () => { + expect(wrapper.vm.isEditable).toBe(false) + expect(wrapper.findComponent({ name: 'Language' }).exists()).toBe(false) + }) + + it('displays forced language message', () => { + expect(wrapper.text()).toContain('Language is forced to German by the administrator') + }) + + it('initializes with forced language state', () => { + expect(wrapper.vm.forcedLanguage).toEqual(forcedLanguageData.languageMap.forcedLanguage) + expect(wrapper.vm.language).toBeNull() + expect(wrapper.vm.commonLanguages).toEqual([]) + expect(wrapper.vm.otherLanguages).toEqual([]) + }) + }) +}) diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue index d4488e77efd9e..35ebaf2744f9c 100644 --- a/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue +++ b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue @@ -14,6 +14,9 @@ :other-locales="otherLocales" :locale.sync="locale" /> + + {{ t('settings', 'Locale is forced to {locale} by the administrator', { locale: forcedLocale.name }) }} + {{ t('settings', 'No locale set') }} @@ -22,14 +25,13 @@ diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/__tests__/LocaleSection.spec.js b/apps/settings/src/components/PersonalInfo/LocaleSection/__tests__/LocaleSection.spec.js new file mode 100644 index 0000000000000..b791b707c660f --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/LocaleSection/__tests__/LocaleSection.spec.js @@ -0,0 +1,152 @@ +import { mount } from '@vue/test-utils' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { loadState } from '@nextcloud/initial-state' +import LocaleSection from '../LocaleSection.vue' + +/** + * Mock child components + */ +vi.mock('../Locale.vue', () => ({ + default: { + name: 'Locale', + template: '
', + props: { + inputId: String, + localesForLanguage: Array, + otherLocales: Array, + locale: Object, + }, + }, +})) + +vi.mock('../shared/HeaderBar.vue', () => ({ + default: { + name: 'HeaderBar', + template: '
', + props: { + inputId: String, + readable: String, + }, + }, +})) + +/** + * Mock Nextcloud modules + */ +vi.mock('../../../constants/AccountPropertyConstants.js', () => ({ + ACCOUNT_SETTING_PROPERTY_ENUM: { LOCALE: 'locale' }, + ACCOUNT_SETTING_PROPERTY_READABLE_ENUM: { LOCALE: 'Locale' }, +})) + +vi.mock('@nextcloud/initial-state', () => ({ + loadState: vi.fn(() => ({ + localeMap: { + activeLocale: { code: 'en_GB', name: 'English (United Kingdom)' }, + localesForLanguage: [{ code: 'en_GB', name: 'English (United Kingdom)' }], + otherLocales: [{ code: 'en_US', name: 'English (United States)' }], + }, + })), +})) + +vi.mock('@nextcloud/l10n', () => ({ + t: (app, text, params) => { + if (params) { + return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match) + } + return text + }, + getLanguage: () => 'en', + isRTL: () => false, + translate: (app, text, params) => { + if (params) { + return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match) + } + return text + }, +})) + +describe('LocaleSection', () => { + let wrapper + + const mountComponent = () => { + return mount(LocaleSection) + } + + beforeEach(() => { + wrapper = mountComponent() + }) + + describe('when locale is user-configurable', () => { + const validLocaleData = { + localeMap: { + activeLocale: { code: 'en_GB', name: 'English (United Kingdom)' }, + localesForLanguage: [{ code: 'en_GB', name: 'English (United Kingdom)' }], + otherLocales: [{ code: 'en_US', name: 'English (United States)' }], + }, + } + + beforeEach(async () => { + vi.mocked(loadState).mockReturnValueOnce(validLocaleData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('enables locale selection', () => { + expect(wrapper.vm.isEditable).toBe(true) + expect(wrapper.findComponent({ name: 'Locale' }).exists()).toBe(true) + }) + + it('passes correct props to Locale', () => { + const locale = wrapper.findComponent({ name: 'Locale' }) + expect(locale.props('inputId')).toBe('account-setting-locale') + expect(locale.props('localesForLanguage')).toEqual(validLocaleData.localeMap.localesForLanguage) + expect(locale.props('otherLocales')).toEqual(validLocaleData.localeMap.otherLocales) + expect(locale.props('locale')).toEqual(validLocaleData.localeMap.activeLocale) + }) + }) + + describe('with empty locale data', () => { + it('handles empty localeMap', async () => { + const emptyData = { localeMap: {} } + vi.mocked(loadState).mockReturnValueOnce(emptyData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + expect(wrapper.vm.isEditable).toBe(false) + expect(wrapper.vm.locale).toBeNull() + expect(wrapper.vm.localesForLanguage).toEqual([]) + expect(wrapper.vm.otherLocales).toEqual([]) + expect(wrapper.vm.forcedLocale).toBeNull() + expect(wrapper.findComponent({ name: 'Locale' }).exists()).toBe(false) + }) + }) + + describe('when locale is forced by administrator', () => { + const forcedLocaleData = { + localeMap: { + forcedLocale: { code: 'uk_UA', name: 'Ukrainian' }, + }, + } + + beforeEach(async () => { + vi.mocked(loadState).mockReturnValueOnce(forcedLocaleData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('disables locale selection', () => { + expect(wrapper.vm.isEditable).toBe(false) + expect(wrapper.findComponent({ name: 'Locale' }).exists()).toBe(false) + }) + + it('displays forced locale message', () => { + expect(wrapper.text()).toContain('Locale is forced to Ukrainian by the administrator') + }) + + it('initializes with forced locale state', () => { + expect(wrapper.vm.forcedLocale).toEqual(forcedLocaleData.localeMap.forcedLocale) + expect(wrapper.vm.locale).toBeNull() + expect(wrapper.vm.localesForLanguage).toEqual([]) + expect(wrapper.vm.otherLocales).toEqual([]) + }) + }) +}) From 262c9f89441f9efbceb774614679fac1d9b10704 Mon Sep 17 00:00:00 2001 From: Annabel Church <215145+arc64@users.noreply.github.com> Date: Mon, 23 Jun 2025 13:12:59 +0200 Subject: [PATCH 2/2] fix(settings): improve forced locale/language validation and testability - Clarified the naming in the validate functions - Removed unnecessary mocks, and added stubs in tests. - Testing for existence of element with data-test attributes, and without relying on translated text. - Removed overly complex component mocking Signed-off-by: Annabel Church <215145+arc64@users.noreply.github.com> --- .../lib/Settings/Personal/PersonalInfo.php | 31 ++-- .../LanguageSection/LanguageSection.spec.js | 93 +++++++++++ .../LanguageSection/LanguageSection.vue | 10 +- .../__tests__/LanguageSection.spec.js | 152 ------------------ .../LocaleSection/LocaleSection.spec.js | 92 +++++++++++ .../LocaleSection/LocaleSection.vue | 7 +- .../__tests__/LocaleSection.spec.js | 152 ------------------ 7 files changed, 216 insertions(+), 321 deletions(-) create mode 100644 apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.spec.js delete mode 100644 apps/settings/src/components/PersonalInfo/LanguageSection/__tests__/LanguageSection.spec.js create mode 100644 apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.spec.js delete mode 100644 apps/settings/src/components/PersonalInfo/LocaleSection/__tests__/LocaleSection.spec.js diff --git a/apps/settings/lib/Settings/Personal/PersonalInfo.php b/apps/settings/lib/Settings/Personal/PersonalInfo.php index bd89e8b572599..b8c500711bbd7 100644 --- a/apps/settings/lib/Settings/Personal/PersonalInfo.php +++ b/apps/settings/lib/Settings/Personal/PersonalInfo.php @@ -228,14 +228,20 @@ function (IAccountProperty $property) { * Validates a forced language setting against available languages */ private function validateForcedLanguage(string $forcedLanguage, array $languages): ?array { - $allLanguages = array_merge($languages['commonLanguages'], $languages['otherLanguages']); - $forcedLang = array_filter($allLanguages, fn($lang) => $lang['code'] === $forcedLanguage); - $forcedLang = reset($forcedLang); - - if ($forcedLang && isset($forcedLang['name'])) { + $allLanguages = array_merge( + $languages['commonLanguages'] ?? [], + $languages['otherLanguages'] ?? [] + ); + $matchingLanguages = array_filter( + $allLanguages, + fn($lang) => $lang['code'] === $forcedLanguage + ); + $matchingLanguage = reset($matchingLanguages); + + if ($matchingLanguage && isset($matchingLanguage['name'])) { return [ 'code' => $forcedLanguage, - 'name' => $forcedLang['name'] + 'name' => $matchingLanguage['name'] ]; } @@ -288,13 +294,16 @@ private function getLanguageMap(IUser $user): array { * Validates a forced locale setting against available locales */ private function validateForcedLocale(string $forcedLocale, array $localeCodes): ?array { - $forcedLocaleObj = array_filter($localeCodes, fn($locale) => $locale['code'] === $forcedLocale); - $forcedLocaleObj = reset($forcedLocaleObj); - - if ($forcedLocaleObj && isset($forcedLocaleObj['name'])) { + $matchingLocales = array_filter( + $localeCodes, + fn($locale) => $locale['code'] === $forcedLocale + ); + $matchingLocale = reset($matchingLocales); + + if ($matchingLocale && isset($matchingLocale['name'])) { return [ 'code' => $forcedLocale, - 'name' => $forcedLocaleObj['name'] + 'name' => $matchingLocale['name'] ]; } diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.spec.js b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.spec.js new file mode 100644 index 0000000000000..f7063a2b31307 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.spec.js @@ -0,0 +1,93 @@ +import { mount } from '@vue/test-utils' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { loadState } from '@nextcloud/initial-state' +import LanguageSection from './LanguageSection.vue' + +/** + * Mock Nextcloud modules + */ +vi.mock('@nextcloud/initial-state', () => ({ + loadState: vi.fn(() => ({ + languageMap: { + activeLanguage: { code: 'en', name: 'English' }, + commonLanguages: [], + otherLanguages: [], + }, + })), +})) + +describe('LanguageSection', () => { + let wrapper + + const mountComponent = () => { + return mount(LanguageSection, { + stubs: { + Language: { + template: '
', + }, + HeaderBar: { + template: '
', + }, + }, + }) + } + + describe('when the language is user-configurable', () => { + beforeEach(async () => { + const userConfigurableData = { + languageMap: { + activeLanguage: { code: 'en', name: 'English' }, + commonLanguages: [{ code: 'en', name: 'English' }], + otherLanguages: [{ code: 'de', name: 'German' }], + }, + } + vi.mocked(loadState).mockReturnValueOnce(userConfigurableData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('shows the language select component', () => { + expect(wrapper.find('[data-test="language-select"]').exists()).toBe(true) + }) + + }) + + describe('when there is no language data', () => { + beforeEach(async () => { + const noLanguageData = { languageMap: {} } + vi.mocked(loadState).mockReturnValueOnce(noLanguageData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('shows no language component', () => { + expect(wrapper.find('[data-test="no-language-message"]').exists()).toBe(true) + }) + }) + + describe('when the language is forced by the administrator', () => { + beforeEach(async () => { + const forcedLanguageData = { + languageMap: { + forcedLanguage: { code: 'uk', name: 'Ukrainian' }, + }, + } + vi.mocked(loadState).mockReturnValueOnce(forcedLanguageData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('shows forced language component', () => { + expect(wrapper.find('[data-test="forced-language-message"]').exists()).toBe(true) + }) + + }) + + afterEach(() => { + if (wrapper) { + wrapper.destroy() + wrapper = null + } + vi.resetAllMocks() + }) +}) diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue index f1971f77a8aaf..0e4acf892442a 100644 --- a/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue +++ b/apps/settings/src/components/PersonalInfo/LanguageSection/LanguageSection.vue @@ -9,15 +9,18 @@ :readable="propertyReadable" /> - + {{ t('settings', 'Language is forced to {language} by the administrator', { language: forcedLanguage.name }) }} - + {{ t('settings', 'No language set') }} @@ -76,8 +79,7 @@ export default { methods: { t, - } - + }, } diff --git a/apps/settings/src/components/PersonalInfo/LanguageSection/__tests__/LanguageSection.spec.js b/apps/settings/src/components/PersonalInfo/LanguageSection/__tests__/LanguageSection.spec.js deleted file mode 100644 index f7a2a04c12419..0000000000000 --- a/apps/settings/src/components/PersonalInfo/LanguageSection/__tests__/LanguageSection.spec.js +++ /dev/null @@ -1,152 +0,0 @@ -import { mount } from '@vue/test-utils' -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { loadState } from '@nextcloud/initial-state' -import LanguageSection from '../LanguageSection.vue' - -/** - * Mock child components - */ -vi.mock('../Language.vue', () => ({ - default: { - name: 'Language', - template: '
', - props: { - inputId: String, - commonLanguages: Array, - otherLanguages: Array, - language: Object, - }, - }, -})) - -vi.mock('../shared/HeaderBar.vue', () => ({ - default: { - name: 'HeaderBar', - template: '
', - props: { - inputId: String, - readable: String, - }, - }, -})) - -/** - * Mock Nextcloud modules - */ -vi.mock('../../../constants/AccountPropertyConstants.js', () => ({ - ACCOUNT_SETTING_PROPERTY_ENUM: { LANGUAGE: 'language' }, - ACCOUNT_SETTING_PROPERTY_READABLE_ENUM: { LANGUAGE: 'Language' }, -})) - -vi.mock('@nextcloud/initial-state', () => ({ - loadState: vi.fn(() => ({ - languageMap: { - activeLanguage: { code: 'en', name: 'English' }, - commonLanguages: [{ code: 'en', name: 'English' }], - otherLanguages: [{ code: 'de', name: 'German' }], - }, - })), -})) - -vi.mock('@nextcloud/l10n', () => ({ - t: (app, text, params) => { - if (params) { - return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match) - } - return text - }, - getLanguage: () => 'en', - isRTL: () => false, - translate: (app, text, params) => { - if (params) { - return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match) - } - return text - }, -})) - -describe('LanguageSection', () => { - let wrapper - - const mountComponent = () => { - return mount(LanguageSection) - } - - beforeEach(() => { - wrapper = mountComponent() - }) - - describe('when language is user-configurable', () => { - const validLanguageData = { - languageMap: { - activeLanguage: { code: 'en', name: 'English' }, - commonLanguages: [{ code: 'en', name: 'English' }], - otherLanguages: [{ code: 'de', name: 'German' }], - }, - } - - beforeEach(async () => { - vi.mocked(loadState).mockReturnValueOnce(validLanguageData) - wrapper = mountComponent() - await wrapper.vm.$nextTick() - }) - - it('enables language selection', () => { - expect(wrapper.vm.isEditable).toBe(true) - expect(wrapper.findComponent({ name: 'Language' }).exists()).toBe(true) - }) - - it('passes correct props to Language', () => { - const language = wrapper.findComponent({ name: 'Language' }) - expect(language.props('inputId')).toBe('account-setting-language') - expect(language.props('commonLanguages')).toEqual(validLanguageData.languageMap.commonLanguages) - expect(language.props('otherLanguages')).toEqual(validLanguageData.languageMap.otherLanguages) - expect(language.props('language')).toEqual(validLanguageData.languageMap.activeLanguage) - }) - }) - - describe('with empty language data', () => { - it('handles empty languageMap', async () => { - const emptyData = { languageMap: {} } - vi.mocked(loadState).mockReturnValueOnce(emptyData) - wrapper = mountComponent() - await wrapper.vm.$nextTick() - expect(wrapper.vm.isEditable).toBe(false) - expect(wrapper.vm.language).toBeNull() - expect(wrapper.vm.commonLanguages).toEqual([]) - expect(wrapper.vm.otherLanguages).toEqual([]) - expect(wrapper.vm.forcedLanguage).toBeNull() - expect(wrapper.findComponent({ name: 'Language' }).exists()).toBe(false) - }) - }) - - describe('when language is forced by administrator', () => { - const forcedLanguageData = { - languageMap: { - forcedLanguage: { code: 'de', name: 'German' }, - }, - } - - beforeEach(async () => { - vi.mocked(loadState).mockReturnValueOnce(forcedLanguageData) - wrapper = mountComponent() - await wrapper.vm.$nextTick() - }) - - it('disables language selection', () => { - expect(wrapper.vm.isEditable).toBe(false) - expect(wrapper.findComponent({ name: 'Language' }).exists()).toBe(false) - }) - - it('displays forced language message', () => { - expect(wrapper.text()).toContain('Language is forced to German by the administrator') - }) - - it('initializes with forced language state', () => { - expect(wrapper.vm.forcedLanguage).toEqual(forcedLanguageData.languageMap.forcedLanguage) - expect(wrapper.vm.language).toBeNull() - expect(wrapper.vm.commonLanguages).toEqual([]) - expect(wrapper.vm.otherLanguages).toEqual([]) - }) - }) -}) diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.spec.js b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.spec.js new file mode 100644 index 0000000000000..c885b7f13a074 --- /dev/null +++ b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.spec.js @@ -0,0 +1,92 @@ +import { mount } from '@vue/test-utils' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { loadState } from '@nextcloud/initial-state' +import LocaleSection from './LocaleSection.vue' + +vi.mock('@nextcloud/initial-state', () => ({ + loadState: vi.fn(() => ({ + localeMap: { + activeLocale: { code: 'en_GB', name: 'English (United Kingdom)' }, + localesForLanguage: [], + otherLocales: [], + }, + })), +})) + +describe('LocaleSection', () => { + let wrapper + + const mountComponent = () => { + return mount(LocaleSection, { + stubs: { + Locale: { + template: '
', + }, + HeaderBar: { + template: '
', + }, + NcPasswordField: { + template: '', + }, + }, + }) + } + + describe('when the locale is user-configurable', () => { + beforeEach(async () => { + const userConfigurableData = { + localeMap: { + activeLocale: { code: 'en_GB', name: 'English (United Kingdom)' }, + localesForLanguage: [{ code: 'en_GB', name: 'English (United Kingdom)' }], + otherLocales: [{ code: 'en_US', name: 'English (United States)' }], + }, + } + vi.mocked(loadState).mockReturnValueOnce(userConfigurableData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('shows the locale select component', () => { + expect(wrapper.find('[data-test="locale-select"]').exists()).toBe(true) + }) + }) + + describe('when there is no locale data', () => { + beforeEach(async () => { + const noLocaleData = { localeMap: {} } + vi.mocked(loadState).mockReturnValueOnce(noLocaleData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('shows no locale component', () => { + expect(wrapper.find('[data-test="no-locale-message"]').exists()).toBe(true) + }) + }) + + describe('when the locale is forced by the administrator', () => { + beforeEach(async () => { + const forcedLocaleData = { + localeMap: { + forcedLocale: { code: 'uk_UA', name: 'Ukrainian' }, + }, + } + vi.mocked(loadState).mockReturnValueOnce(forcedLocaleData) + wrapper = mountComponent() + await wrapper.vm.$nextTick() + }) + + it('shows forced locale component', () => { + expect(wrapper.find('[data-test="forced-locale-message"]').exists()).toBe(true) + }) + + }) + + afterEach(() => { + if (wrapper) { + wrapper.destroy() + wrapper = null + } + vi.resetAllMocks() + }) +}) diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue index 35ebaf2744f9c..65f7eb2dda6db 100644 --- a/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue +++ b/apps/settings/src/components/PersonalInfo/LocaleSection/LocaleSection.vue @@ -9,15 +9,18 @@ :readable="propertyReadable" /> - + {{ t('settings', 'Locale is forced to {locale} by the administrator', { locale: forcedLocale.name }) }} - + {{ t('settings', 'No locale set') }} diff --git a/apps/settings/src/components/PersonalInfo/LocaleSection/__tests__/LocaleSection.spec.js b/apps/settings/src/components/PersonalInfo/LocaleSection/__tests__/LocaleSection.spec.js deleted file mode 100644 index b791b707c660f..0000000000000 --- a/apps/settings/src/components/PersonalInfo/LocaleSection/__tests__/LocaleSection.spec.js +++ /dev/null @@ -1,152 +0,0 @@ -import { mount } from '@vue/test-utils' -import { describe, it, expect, vi, beforeEach } from 'vitest' -import { loadState } from '@nextcloud/initial-state' -import LocaleSection from '../LocaleSection.vue' - -/** - * Mock child components - */ -vi.mock('../Locale.vue', () => ({ - default: { - name: 'Locale', - template: '
', - props: { - inputId: String, - localesForLanguage: Array, - otherLocales: Array, - locale: Object, - }, - }, -})) - -vi.mock('../shared/HeaderBar.vue', () => ({ - default: { - name: 'HeaderBar', - template: '
', - props: { - inputId: String, - readable: String, - }, - }, -})) - -/** - * Mock Nextcloud modules - */ -vi.mock('../../../constants/AccountPropertyConstants.js', () => ({ - ACCOUNT_SETTING_PROPERTY_ENUM: { LOCALE: 'locale' }, - ACCOUNT_SETTING_PROPERTY_READABLE_ENUM: { LOCALE: 'Locale' }, -})) - -vi.mock('@nextcloud/initial-state', () => ({ - loadState: vi.fn(() => ({ - localeMap: { - activeLocale: { code: 'en_GB', name: 'English (United Kingdom)' }, - localesForLanguage: [{ code: 'en_GB', name: 'English (United Kingdom)' }], - otherLocales: [{ code: 'en_US', name: 'English (United States)' }], - }, - })), -})) - -vi.mock('@nextcloud/l10n', () => ({ - t: (app, text, params) => { - if (params) { - return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match) - } - return text - }, - getLanguage: () => 'en', - isRTL: () => false, - translate: (app, text, params) => { - if (params) { - return text.replace(/\{(\w+)\}/g, (match, key) => params[key] || match) - } - return text - }, -})) - -describe('LocaleSection', () => { - let wrapper - - const mountComponent = () => { - return mount(LocaleSection) - } - - beforeEach(() => { - wrapper = mountComponent() - }) - - describe('when locale is user-configurable', () => { - const validLocaleData = { - localeMap: { - activeLocale: { code: 'en_GB', name: 'English (United Kingdom)' }, - localesForLanguage: [{ code: 'en_GB', name: 'English (United Kingdom)' }], - otherLocales: [{ code: 'en_US', name: 'English (United States)' }], - }, - } - - beforeEach(async () => { - vi.mocked(loadState).mockReturnValueOnce(validLocaleData) - wrapper = mountComponent() - await wrapper.vm.$nextTick() - }) - - it('enables locale selection', () => { - expect(wrapper.vm.isEditable).toBe(true) - expect(wrapper.findComponent({ name: 'Locale' }).exists()).toBe(true) - }) - - it('passes correct props to Locale', () => { - const locale = wrapper.findComponent({ name: 'Locale' }) - expect(locale.props('inputId')).toBe('account-setting-locale') - expect(locale.props('localesForLanguage')).toEqual(validLocaleData.localeMap.localesForLanguage) - expect(locale.props('otherLocales')).toEqual(validLocaleData.localeMap.otherLocales) - expect(locale.props('locale')).toEqual(validLocaleData.localeMap.activeLocale) - }) - }) - - describe('with empty locale data', () => { - it('handles empty localeMap', async () => { - const emptyData = { localeMap: {} } - vi.mocked(loadState).mockReturnValueOnce(emptyData) - wrapper = mountComponent() - await wrapper.vm.$nextTick() - expect(wrapper.vm.isEditable).toBe(false) - expect(wrapper.vm.locale).toBeNull() - expect(wrapper.vm.localesForLanguage).toEqual([]) - expect(wrapper.vm.otherLocales).toEqual([]) - expect(wrapper.vm.forcedLocale).toBeNull() - expect(wrapper.findComponent({ name: 'Locale' }).exists()).toBe(false) - }) - }) - - describe('when locale is forced by administrator', () => { - const forcedLocaleData = { - localeMap: { - forcedLocale: { code: 'uk_UA', name: 'Ukrainian' }, - }, - } - - beforeEach(async () => { - vi.mocked(loadState).mockReturnValueOnce(forcedLocaleData) - wrapper = mountComponent() - await wrapper.vm.$nextTick() - }) - - it('disables locale selection', () => { - expect(wrapper.vm.isEditable).toBe(false) - expect(wrapper.findComponent({ name: 'Locale' }).exists()).toBe(false) - }) - - it('displays forced locale message', () => { - expect(wrapper.text()).toContain('Locale is forced to Ukrainian by the administrator') - }) - - it('initializes with forced locale state', () => { - expect(wrapper.vm.forcedLocale).toEqual(forcedLocaleData.localeMap.forcedLocale) - expect(wrapper.vm.locale).toBeNull() - expect(wrapper.vm.localesForLanguage).toEqual([]) - expect(wrapper.vm.otherLocales).toEqual([]) - }) - }) -})