diff --git a/contentcuration/contentcuration/frontend/accounts/pages/Create.vue b/contentcuration/contentcuration/frontend/accounts/pages/Create.vue index ec8908cdef..d075f136c0 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/Create.vue +++ b/contentcuration/contentcuration/frontend/accounts/pages/Create.vue @@ -131,10 +131,7 @@

{{ $tr('locationLabel') }}*

- +
online: !offline, }, }, - actions: { - register: mockRegisterAction, + modules: { + account: { + namespaced: true, + actions: { + register: mockRegisterAction, + }, + }, + policies: { + namespaced: true, + getters: { + getPolicyAcceptedData: () => () => ({}), + }, + }, }, }); @@ -102,20 +123,7 @@ describe('Create account page', () => { }); }); - // NOTE: - // Full form submission tests are intentionally skipped here. - // - // The "locations" field uses CountryField, which internally uses VAutocomplete - // (a Vuetify component). VAutocomplete does not reliably update its v-model - // state when interacted with via Vue Testing Library's userEvent APIs, which - // prevents a fully user-centric submission flow from being exercised. - // - // The "source" field has been migrated to KSelect (KDS) and no longer has - // this limitation, but "locations" remains the blocker. - // - // These tests will be re-enabled once CountryField is migrated away from - // VAutocomplete as part of the ongoing Vuetify removal effort. - it.skip('creates an account when the user submits valid information', async () => { + it('creates an account when the user submits valid information', async () => { await renderComponent(); await userEvent.type(screen.getByLabelText(/first name/i), 'Test'); @@ -126,6 +134,12 @@ describe('Create account page', () => { await userEvent.click(screen.getByLabelText(/tagging content sources/i)); + await userEvent.type(screen.getByRole('combobox', { name: locationLabel$() }), KENYA); + await userEvent.click(await screen.findByRole('option', { name: KENYA })); + + await userEvent.click(screen.getByText(sourcePlaceholder$())); + await userEvent.click(await screen.findByText(websiteSourceOption$())); + await userEvent.click(screen.getByLabelText(/i have read and agree to terms of service/i)); const finishButton = screen.getByRole('button', { name: /finish/i }); @@ -137,27 +151,17 @@ describe('Create account page', () => { await userEvent.click(finishButton); await waitFor(() => { - expect(mockRegisterAction).toHaveBeenCalled(); + expect(mockRegisterAction).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ locations: KENYA }), + ); }); }); - // Skipped for the same reason as above — CountryField (VAutocomplete) blocker - it.skip('shows an offline error when the user is offline', async () => { + it('shows an offline error and disables Finish when the user is offline', async () => { await renderComponent({ offline: true }); - await userEvent.type(screen.getByLabelText(/first name/i), 'Test'); - await userEvent.type(screen.getByLabelText(/last name/i), 'User'); - await userEvent.type(screen.getByLabelText(/email/i), 'test@test.com'); - await userEvent.type(screen.getByLabelText(/^password$/i), 'tester123'); - await userEvent.type(screen.getByLabelText(/confirm password/i), 'tester123'); - - await userEvent.click(screen.getByLabelText(/tagging content sources/i)); - - await userEvent.click(screen.getByLabelText(/i have read and agree to terms of service/i)); - - const finishButton = screen.getByRole('button', { name: /finish/i }); - await userEvent.click(finishButton); - - expect(await screen.findByText(/offline/i)).toBeInTheDocument(); + expect(screen.getByTestId('studio-banner')).toHaveTextContent(registrationFailedOffline$()); + expect(screen.getByRole('button', { name: /finish/i })).toBeDisabled(); }); }); diff --git a/contentcuration/contentcuration/frontend/administration/pages/Users/UserTable.vue b/contentcuration/contentcuration/frontend/administration/pages/Users/UserTable.vue index c409ddd82b..40eeb617fa 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Users/UserTable.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Users/UserTable.vue @@ -57,7 +57,6 @@ diff --git a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue index ffceef6479..3223210536 100644 --- a/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue +++ b/contentcuration/contentcuration/frontend/channelEdit/components/sidePanels/SubmitToCommunityLibrarySidePanel/index.vue @@ -204,7 +204,6 @@ :disabled="!canBeEdited" :label="countryLabel$()" fullWidth - :hide-details="true" />
diff --git a/contentcuration/contentcuration/frontend/channelList/views/Channel/components/LanguageFilter.vue b/contentcuration/contentcuration/frontend/channelList/views/Channel/components/LanguageFilter.vue index 7f71ff50a8..50d9763440 100644 --- a/contentcuration/contentcuration/frontend/channelList/views/Channel/components/LanguageFilter.vue +++ b/contentcuration/contentcuration/frontend/channelList/views/Channel/components/LanguageFilter.vue @@ -64,12 +64,12 @@ }, }, messages() { - const { languageItemsSelectedLabel$, languageSelectionsClearedLabel$ } = commonStrings; + const { languageItemsSelectedLabel$, selectionsClearedLabel$ } = commonStrings; const { clearAllAction$ } = communityChannelsStrings; return createMultiSelectMessages({ clearText: clearAllAction$, itemsSelected: languageItemsSelectedLabel$, - cleared: languageSelectionsClearedLabel$, + cleared: selectionsClearedLabel$, }); }, }, diff --git a/contentcuration/contentcuration/frontend/settings/pages/Storage/RequestForm.vue b/contentcuration/contentcuration/frontend/settings/pages/Storage/RequestForm.vue index 9d8cca72c7..91883b31e5 100644 --- a/contentcuration/contentcuration/frontend/settings/pages/Storage/RequestForm.vue +++ b/contentcuration/contentcuration/frontend/settings/pages/Storage/RequestForm.vue @@ -133,11 +133,7 @@
- + - this.multiple - ? languageSelectionsClearedLabel$({ count }) - : optionRemovedLabel$({ label }), + this.multiple ? selectionsClearedLabel$({ count }) : optionRemovedLabel$({ label }), }); }, }, diff --git a/contentcuration/contentcuration/frontend/shared/views/form/CountryField.vue b/contentcuration/contentcuration/frontend/shared/views/form/CountryField.vue index 87492650f6..21b2fb4134 100644 --- a/contentcuration/contentcuration/frontend/shared/views/form/CountryField.vue +++ b/contentcuration/contentcuration/frontend/shared/views/form/CountryField.vue @@ -1,57 +1,43 @@ - - - diff --git a/contentcuration/contentcuration/frontend/shared/views/form/__tests__/countryField.spec.js b/contentcuration/contentcuration/frontend/shared/views/form/__tests__/countryField.spec.js index 69d01df9cf..4589fcaf77 100644 --- a/contentcuration/contentcuration/frontend/shared/views/form/__tests__/countryField.spec.js +++ b/contentcuration/contentcuration/frontend/shared/views/form/__tests__/countryField.spec.js @@ -1,55 +1,194 @@ -import { mount } from '@vue/test-utils'; +import { render, screen, within } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import VueRouter from 'vue-router'; import CountryField from '../CountryField.vue'; +import { commonStrings } from 'shared/strings/commonStrings'; +import { createTranslator } from 'shared/i18n'; -function makeWrapper() { - return mount(CountryField, { - propsData: { - required: true, +const mockSendPoliteMessage = jest.fn(); +jest.mock('kolibri-design-system/lib/composables/useKLiveRegion', () => ({ + __esModule: true, + default: () => ({ + sendPoliteMessage: mockSendPoliteMessage, + sendAssertiveMessage: jest.fn(), + }), +})); + +const { locationLabel$, locationRequiredMessage$, noCountriesFound$ } = createTranslator( + 'CountryField', + CountryField.$trs, +); +const { clearAction$, optionRemovedLabel$, selectionsClearedLabel$ } = commonStrings; + +const KENYA = 'Kenya'; +const CZECH_REPUBLIC = 'Czech Republic'; +const GERMANY = 'Germany'; +const GERMANY_IN_SPANISH = 'Alemania'; +const CUSTOM_LABEL = 'Target location'; + +function renderComponent(props = {}) { + return render(CountryField, { props, routes: new VueRouter() }); +} + +function renderRequiredField(value) { + const HostForm = { + components: { CountryField }, + data() { + return { value }; }, - }); + template: ` +
+ + +
+ `, + }; + return render(HostForm, { routes: new VueRouter() }); } -function runValidation(wrapper, value) { - return wrapper.vm.rules.every(rule => rule(value) === true); +function lastInput(emitted) { + const events = emitted().input; + return events[events.length - 1][0]; } -describe('countryField', () => { - let wrapper; +describe('CountryField', () => { beforeEach(() => { - wrapper = makeWrapper(); + mockSendPoliteMessage.mockClear(); + }); + + it('renders the default label', () => { + renderComponent(); + expect(screen.getByRole('combobox', { name: locationLabel$() })).toBeInTheDocument(); + }); + + it('renders a custom label', () => { + renderComponent({ label: CUSTOM_LABEL }); + expect(screen.getByRole('combobox', { name: CUSTOM_LABEL })).toBeInTheDocument(); + }); + + describe('in multiple selection mode', () => { + it('emits the selected countries as an array of English names', async () => { + const { emitted } = renderComponent({ value: [KENYA] }); + + await userEvent.type(screen.getByRole('combobox'), 'Czech'); + await userEvent.click(await screen.findByRole('option', { name: CZECH_REPUBLIC })); + + expect(lastInput(emitted)).toEqual([KENYA, CZECH_REPUBLIC]); + }); + + it('clears the search text after a selection', async () => { + renderComponent(); + + const input = screen.getByRole('combobox'); + await userEvent.type(input, 'Czech'); + await userEvent.click(await screen.findByRole('option', { name: CZECH_REPUBLIC })); + + expect(input).toHaveValue(''); + }); + + it('announces the number of cleared selections when clearing', async () => { + renderComponent({ value: [KENYA, CZECH_REPUBLIC] }); + + await userEvent.click(screen.getByRole('button', { name: clearAction$() })); + + expect(mockSendPoliteMessage).toHaveBeenCalledWith(selectionsClearedLabel$({ count: 2 })); + }); + }); + + describe('in single selection mode', () => { + it('emits the selected country as an English name', async () => { + const { emitted } = renderComponent({ multiple: false }); + + await userEvent.type(screen.getByRole('combobox'), 'Czech'); + await userEvent.click(await screen.findByRole('option', { name: CZECH_REPUBLIC })); + + expect(lastInput(emitted)).toBe(CZECH_REPUBLIC); + }); + + it('shows the selected country in the input', () => { + renderComponent({ multiple: false, value: KENYA }); + expect(screen.getByRole('combobox')).toHaveValue(KENYA); + }); + + it('renders with no selection when no value is given', () => { + renderComponent({ multiple: false }); + expect(screen.getByRole('combobox')).toHaveValue(''); + }); + + it('announces the removed country, not a count, when clearing', async () => { + renderComponent({ multiple: false, value: KENYA }); + + await userEvent.click(screen.getByRole('button', { name: clearAction$() })); + + expect(mockSendPoliteMessage).toHaveBeenCalledWith(optionRemovedLabel$({ label: KENYA })); + }); }); - it('should validate if no selections are made', () => { - expect(runValidation(wrapper, [])).toBe(false); - expect(runValidation(wrapper, ['item'])).toBe(true); + + describe('with a non-English interface language', () => { + beforeEach(() => { + window.languageCode = 'es'; + }); + + afterEach(() => { + delete window.languageCode; + }); + + it('shows translated country names but emits English names', async () => { + const { emitted } = renderComponent(); + + await userEvent.type(screen.getByRole('combobox'), GERMANY_IN_SPANISH); + await userEvent.click(await screen.findByRole('option', { name: GERMANY_IN_SPANISH })); + + expect(lastInput(emitted)).toEqual([GERMANY]); + }); }); - it('in multiple selection mode, search input is cleared after selection', async () => { - await wrapper.setProps({ multiple: true }); + it('shows a message when no country matches the search', async () => { + const { container } = renderComponent(); - const autocomplete = wrapper.findComponent({ name: 'VAutocomplete' }); - wrapper.vm.searchInput = 'Czech'; + await userEvent.type(screen.getByRole('combobox'), 'zzzzz'); - // Handling selection change uses `setTimeout` so we need to fake the timer - jest.useFakeTimers(); - autocomplete.vm.$emit('input', ['Czech Republic']); - jest.runAllTimers(); - jest.useRealTimers(); + expect(await within(container).findByText(noCountriesFound$())).toBeInTheDocument(); + expect(within(container).queryAllByRole('option', { hidden: true })).toHaveLength(0); + }); - expect(wrapper.vm.searchInput).toBe(''); + it('disables the input when disabled', () => { + renderComponent({ disabled: true }); + expect(screen.getByRole('combobox')).toBeDisabled(); }); - it('in single selection mode, search input is not cleared after selection', async () => { - await wrapper.setProps({ multiple: false }); + describe('when required', () => { + it('does not show an error before it is validated', () => { + renderRequiredField([]); + expect(screen.queryByText(locationRequiredMessage$())).not.toBeInTheDocument(); + }); + + it('shows an error when validated with no country selected', async () => { + renderRequiredField([]); + + await userEvent.click(screen.getByRole('button', { name: 'Validate' })); + + expect(screen.getByRole('combobox')).toHaveAccessibleDescription(locationRequiredMessage$()); + }); + + it('does not show an error when validated with a country selected', async () => { + renderRequiredField([KENYA]); + + await userEvent.click(screen.getByRole('button', { name: 'Validate' })); + + expect(screen.queryByText(locationRequiredMessage$())).not.toBeInTheDocument(); + }); + + it('clears the error once a country is selected', async () => { + renderRequiredField([]); - const autocomplete = wrapper.findComponent({ name: 'VAutocomplete' }); - wrapper.vm.searchInput = 'Czech'; + await userEvent.click(screen.getByRole('button', { name: 'Validate' })); + expect(screen.getByRole('combobox')).toHaveAccessibleDescription(locationRequiredMessage$()); - // Handling selection change uses `setTimeout` so we need to fake the timer - jest.useFakeTimers(); - autocomplete.vm.$emit('input', 'Czech Republic'); - jest.runAllTimers(); - jest.useRealTimers(); + await userEvent.type(screen.getByRole('combobox'), KENYA); + await userEvent.click(await screen.findByRole('option', { name: KENYA })); - expect(wrapper.vm.searchInput).not.toBe(''); + expect(screen.queryByText(locationRequiredMessage$())).not.toBeInTheDocument(); + }); }); });