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 +113,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 +124,12 @@ describe('Create account page', () => { await userEvent.click(screen.getByLabelText(/tagging content sources/i)); + await userEvent.type(screen.getByRole('combobox', { name: /select all that apply/i }), 'Kenya'); + await userEvent.click(await screen.findByRole('option', { name: 'Kenya' })); + + await userEvent.click(screen.getByText(/select one/i)); + await userEvent.click(await screen.findByText(/learning equality website/i)); + await userEvent.click(screen.getByLabelText(/i have read and agree to terms of service/i)); const finishButton = screen.getByRole('button', { name: /finish/i }); @@ -141,8 +145,7 @@ describe('Create account page', () => { }); }); - // 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 when the user is offline', async () => { await renderComponent({ offline: true }); await userEvent.type(screen.getByLabelText(/first name/i), 'Test'); @@ -153,11 +156,19 @@ describe('Create account page', () => { await userEvent.click(screen.getByLabelText(/tagging content sources/i)); + await userEvent.type(screen.getByRole('combobox', { name: /select all that apply/i }), 'Kenya'); + await userEvent.click(await screen.findByRole('option', { name: 'Kenya' })); + + await userEvent.click(screen.getByText(/select one/i)); + await userEvent.click(await screen.findByText(/learning equality website/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( + await screen.findByText(/please connect to the internet to create an account/i), + ).toBeInTheDocument(); }); }); 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/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 @@
- + - - - + - - - 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..62a9fe8268 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,130 @@ -import { mount } from '@vue/test-utils'; +import { render, screen } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; +import VueRouter from 'vue-router'; import CountryField from '../CountryField.vue'; -function makeWrapper() { - return mount(CountryField, { - propsData: { - required: true, - }, - }); +function renderComponent(props = {}) { + return render(CountryField, { props, routes: new VueRouter() }); +} + +// Tabbing straight out of an open dropdown moves focus into its listbox, +// so close it first to actually leave the field. +async function leaveField() { + await userEvent.click(screen.getByRole('combobox')); + await userEvent.keyboard('{Escape}'); + await userEvent.tab(); } -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; - beforeEach(() => { - wrapper = makeWrapper(); +describe('CountryField', () => { + it('renders the default label', () => { + renderComponent(); + expect(screen.getByRole('combobox', { name: 'Select all that apply' })).toBeInTheDocument(); }); - it('should validate if no selections are made', () => { - expect(runValidation(wrapper, [])).toBe(false); - expect(runValidation(wrapper, ['item'])).toBe(true); + + it('renders a custom label', () => { + renderComponent({ label: 'Target location' }); + expect(screen.getByRole('combobox', { name: 'Target location' })).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('in multiple selection mode, search input is cleared after selection', async () => { - await wrapper.setProps({ multiple: true }); + describe('in single selection mode', () => { + it('emits the selected country as an English name', async () => { + const { emitted } = renderComponent({ multiple: false }); - const autocomplete = wrapper.findComponent({ name: 'VAutocomplete' }); - wrapper.vm.searchInput = 'Czech'; + await userEvent.type(screen.getByRole('combobox'), 'Czech'); + await userEvent.click(await screen.findByRole('option', { name: 'Czech Republic' })); - // 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(lastInput(emitted)).toBe('Czech Republic'); + }); - expect(wrapper.vm.searchInput).toBe(''); + 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('in single selection mode, search input is not cleared after selection', async () => { - await wrapper.setProps({ multiple: false }); + 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'), 'Alemania'); + await userEvent.click(await screen.findByRole('option', { name: 'Alemania' })); + + expect(lastInput(emitted)).toEqual(['Germany']); + }); + }); + + it('shows a message when no country matches the search', async () => { + renderComponent(); + + await userEvent.type(screen.getByRole('combobox'), 'zzzzz'); + + expect(await screen.findByText('No countries found')).toBeInTheDocument(); + }); + + it('disables the input when disabled', () => { + renderComponent({ disabled: true }); + expect(screen.getByRole('combobox')).toBeDisabled(); + }); + + describe('when required', () => { + it('shows an error after leaving the field empty', async () => { + renderComponent({ required: true }); + + await leaveField(); + + expect(await screen.findByText('Field is required')).toBeInTheDocument(); + }); + + it('does not show an error before the field is touched', () => { + renderComponent({ required: true }); + expect(screen.queryByText('Field is required')).not.toBeInTheDocument(); + }); - const autocomplete = wrapper.findComponent({ name: 'VAutocomplete' }); - wrapper.vm.searchInput = 'Czech'; + it('does not show an error when a country is selected', async () => { + renderComponent({ required: true, value: ['Kenya'] }); - // 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 leaveField(); - expect(wrapper.vm.searchInput).not.toBe(''); + expect(screen.queryByText('Field is required')).not.toBeInTheDocument(); + }); }); });