From 6bd08dd3365b0a447579c8b326bcc7f0bff9cfaa Mon Sep 17 00:00:00 2001 From: abhiraj75 Date: Tue, 13 Jan 2026 04:06:39 +0530 Subject: [PATCH 1/5] test(accounts): migrate create account page tests to Vue Testing Library --- .../accounts/pages/__tests__/create.spec.js | 278 +++++++++--------- 1 file changed, 131 insertions(+), 147 deletions(-) diff --git a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js index a2c2f40d71..67d176ca32 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js +++ b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js @@ -1,182 +1,166 @@ -import { mount } from '@vue/test-utils'; +import Vuex from 'vuex'; +import { render, screen, waitFor } from '@testing-library/vue'; +import userEvent from '@testing-library/user-event'; import router from '../../router'; -import { uses, sources } from '../../constants'; import Create from '../Create'; -const connectionStateMocks = { - $store: { +const makeStore = ({ offline = false } = {}) => + new Vuex.Store({ state: { connection: { - offline: true, + offline, }, }, - }, -}; + }); -const defaultData = { - first_name: 'Test', - last_name: 'User', - email: 'test@test.com', - password1: 'tester123', - password2: 'tester123', - uses: ['tagging'], - storage: '', - other_use: '', - locations: ['China'], - source: 'demo', - organization: '', - conference: '', - other_source: '', - accepted_policy: true, - accepted_tos: true, -}; +const renderComponent = async ({ routeQuery = {}, offline = false } = {}) => { + if (router.currentRoute.path === '/create') { + await router.push('/').catch(() => {}); + } -async function makeWrapper(formData) { - const wrapper = mount(Create, { + await router.push({ name: 'Create', query: routeQuery }).catch(() => {}); + + return render(Create, { router, - computed: { - getPolicyAcceptedData() { - return () => { - return {}; - }; - }, + store: makeStore({ offline }), + stubs: { + PolicyModals: true, }, - stubs: ['PolicyModals'], - mocks: connectionStateMocks, }); - await wrapper.setData({ - form: { - ...defaultData, - ...formData, - }, - }); - const register = jest.spyOn(wrapper.vm, 'register'); - register.mockImplementation(() => Promise.resolve()); - return [wrapper, { register }]; -} -function makeFailedPromise(statusCode) { - return () => { - return new Promise((resolve, reject) => { - reject({ - response: { - status: statusCode || 500, - }, - }); - }); - }; -} - -describe('create', () => { - it('should trigger submit method when form is submitted', async () => { - const [wrapper] = await makeWrapper(); - const submit = jest.spyOn(wrapper.vm, 'submit'); - submit.mockImplementation(() => Promise.resolve()); - await wrapper.findComponent({ ref: 'form' }).trigger('submit'); - expect(submit).toHaveBeenCalled(); +}; + +describe('Create account page', () => { + test('smoke test: renders the create account page', async () => { + await renderComponent(); + + expect( + await screen.findByRole('heading', { name: /create an account/i }) + ).toBeInTheDocument(); }); - it('should call register with form data', async () => { - const [wrapper, mocks] = await makeWrapper(); - await wrapper.findComponent({ ref: 'form' }).trigger('submit'); - expect(mocks.register.mock.calls[0][0]).toEqual({ - ...defaultData, - locations: defaultData.locations.join('|'), - uses: defaultData.uses.join('|'), - policies: '{}', - }); + test('shows validation state when submitting empty form', async () => { + await renderComponent(); + + const finishButton = screen.getByRole('button', { name: /finish/i }); + await userEvent.click(finishButton); + + expect(finishButton).toBeDisabled(); }); - it('should automatically fill the email if provided in the query param', () => { - router.push({ name: 'Create', query: { email: 'newtest@test.com' } }); - const wrapper = mount(Create, { router, stubs: ['PolicyModals'], mocks: connectionStateMocks }); - expect(wrapper.vm.form.email).toBe('newtest@test.com'); + test('allows user to fill in text input fields', async () => { + await renderComponent(); + + 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'); + + expect(screen.getByLabelText(/first name/i)).toHaveValue('Test'); + expect(screen.getByLabelText(/last name/i)).toHaveValue('User'); + expect(screen.getByLabelText(/email/i)).toHaveValue('test@test.com'); + expect(screen.getByLabelText(/^password$/i)).toHaveValue('tester123'); + expect(screen.getByLabelText(/confirm password/i)).toHaveValue('tester123'); }); - describe('validation', () => { - it('should call register if form is valid', async () => { - const [wrapper, mocks] = await makeWrapper(); - wrapper.vm.submit(); - expect(mocks.register).toHaveBeenCalled(); - }); + test('allows user to check checkboxes', async () => { + await renderComponent(); - it('should fail if required fields are not set', async () => { - const form = { - first_name: '', - last_name: '', - email: '', - password1: '', - password2: '', - uses: [], - locations: [], - source: '', - accepted_policy: false, - accepted_tos: false, - }; - - for (const field of Object.keys(form)) { - const [wrapper, mocks] = await makeWrapper({ [field]: form[field] }); - await wrapper.vm.submit(); - expect(mocks.register).not.toHaveBeenCalled(); - } - }); + const contentSourcesCheckbox = screen.getByLabelText( + /tagging content sources/i + ); + const tosCheckbox = screen.getByLabelText( + /i have read and agree to terms of service/i + ); - it('should fail if password1 is too short', async () => { - const [wrapper, mocks] = await makeWrapper({ password1: '123' }); - await wrapper.vm.submit(); - expect(mocks.register).not.toHaveBeenCalled(); - }); + await userEvent.click(contentSourcesCheckbox); + await userEvent.click(tosCheckbox); + + expect(contentSourcesCheckbox).toBeChecked(); + expect(tosCheckbox).toBeChecked(); + }); - it('should fail if password1 and password2 do not match', async () => { - const [wrapper, mocks] = await makeWrapper({ password1: 'some other password' }); - await wrapper.vm.submit(); - expect(mocks.register).not.toHaveBeenCalled(); + test('automatically fills the email field when provided in the URL', async () => { + await renderComponent({ + routeQuery: { email: 'newtest@test.com' }, }); - it.each( - [uses.STORING, uses.OTHER], - 'should fail if uses field is set to fields that require more input that is not provided', - async use => { - const [wrapper, mocks] = await makeWrapper({ uses: [use] }); - await wrapper.vm.submit(); - expect(mocks.register).not.toHaveBeenCalled(); - }, + const emailInput = await screen.findByLabelText(/email/i); + + await waitFor(() => { + expect(emailInput).toHaveValue('newtest@test.com'); + }); + }); +// NOTE: +// Full form submission tests are intentionally skipped here. +// +// This page still relies on Vuetify components (v-select / v-autocomplete) +// for required fields such as "locations" and "source". +// These components do not reliably update their 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 previous Vue Test Utils tests worked around this by directly mutating +// component data (setData), which is intentionally avoided when using +// Testing Library. +// +// These tests will be re-enabled once this page is migrated to the +// Kolibri Design System as part of the Vuetify removal effort (see #5060). + + test.skip('creates an account when the user submits valid information', async () => { + const registerSpy = jest + .spyOn(Create.methods, 'register') + .mockResolvedValue(); + + await renderComponent(); + + 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) ); - it.each( - [sources.ORGANIZATION, sources.CONFERENCE, sources.OTHER], - 'should fail if source field is set to an option that requires more input that is not provided', - async source => { - const [wrapper, mocks] = await makeWrapper({ source }); - await wrapper.vm.submit(); - expect(mocks.register).not.toHaveBeenCalled(); - }, + await userEvent.click( + screen.getByLabelText(/i have read and agree to terms of service/i) ); - }); - describe('on backend failures', () => { - let wrapper, mocks; + const finishButton = screen.getByRole('button', { name: /finish/i }); - beforeEach(async () => { - [wrapper, mocks] = await makeWrapper(); + await waitFor(() => { + expect(finishButton).not.toBeDisabled(); }); - it('should say account with email already exists if register returns a 403', async () => { - mocks.register.mockImplementation(makeFailedPromise(403)); - await wrapper.vm.submit(); - expect(wrapper.vm.errors.email).toHaveLength(1); - }); + await userEvent.click(finishButton); - it('should say account has not been activated if register returns 405', async () => { - mocks.register.mockImplementation(makeFailedPromise(405)); - await wrapper.vm.submit(); - expect(wrapper.vm.$route.name).toBe('AccountNotActivated'); + await waitFor(() => { + expect(registerSpy).toHaveBeenCalled(); }); + }); - it('registrationFailed should be true if any other error is returned', async () => { - mocks.register.mockImplementation(makeFailedPromise()); - await wrapper.vm.submit(); - expect(wrapper.vm.valid).toBe(false); - expect(wrapper.vm.registrationFailed).toBe(true); - }); + test.skip('shows an offline error 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(); }); }); From c7dcc6a95f9c140fba2f817d0a0c9ba9f0b4a58b Mon Sep 17 00:00:00 2001 From: abhiraj75 Date: Tue, 13 Jan 2026 04:23:11 +0530 Subject: [PATCH 2/5] fixing documentation --- .../frontend/accounts/pages/__tests__/create.spec.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js index 67d176ca32..3efb5ad77b 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js +++ b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js @@ -105,7 +105,7 @@ describe('Create account page', () => { // Testing Library. // // These tests will be re-enabled once this page is migrated to the -// Kolibri Design System as part of the Vuetify removal effort (see #5060). +// Kolibri Design System as part of the Vuetify removal effort . test.skip('creates an account when the user submits valid information', async () => { const registerSpy = jest @@ -140,7 +140,12 @@ describe('Create account page', () => { expect(registerSpy).toHaveBeenCalled(); }); }); - +// NOTE: +// Offline submission depends on the same required Vuetify select fields +// as the successful submission flow. +// Since those fields cannot be reliably exercised via userEvent, +// this scenario cannot currently reach the submission state. +// This test will be re-enabled once Vuetify is removed . test.skip('shows an offline error when the user is offline', async () => { await renderComponent({ offline: true }); From 1a270193cbf2fc5cebc37d5ae708a35c3ba43da8 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 12:10:16 +0000 Subject: [PATCH 3/5] [pre-commit.ci lite] apply automatic fixes --- .../accounts/pages/__tests__/create.spec.js | 76 ++++++++----------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js index 3efb5ad77b..37db094c53 100644 --- a/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js +++ b/contentcuration/contentcuration/frontend/accounts/pages/__tests__/create.spec.js @@ -33,9 +33,7 @@ describe('Create account page', () => { test('smoke test: renders the create account page', async () => { await renderComponent(); - expect( - await screen.findByRole('heading', { name: /create an account/i }) - ).toBeInTheDocument(); + expect(await screen.findByRole('heading', { name: /create an account/i })).toBeInTheDocument(); }); test('shows validation state when submitting empty form', async () => { @@ -66,12 +64,8 @@ describe('Create account page', () => { test('allows user to check checkboxes', async () => { await renderComponent(); - const contentSourcesCheckbox = screen.getByLabelText( - /tagging content sources/i - ); - const tosCheckbox = screen.getByLabelText( - /i have read and agree to terms of service/i - ); + const contentSourcesCheckbox = screen.getByLabelText(/tagging content sources/i); + const tosCheckbox = screen.getByLabelText(/i have read and agree to terms of service/i); await userEvent.click(contentSourcesCheckbox); await userEvent.click(tosCheckbox); @@ -91,26 +85,24 @@ describe('Create account page', () => { expect(emailInput).toHaveValue('newtest@test.com'); }); }); -// NOTE: -// Full form submission tests are intentionally skipped here. -// -// This page still relies on Vuetify components (v-select / v-autocomplete) -// for required fields such as "locations" and "source". -// These components do not reliably update their 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 previous Vue Test Utils tests worked around this by directly mutating -// component data (setData), which is intentionally avoided when using -// Testing Library. -// -// These tests will be re-enabled once this page is migrated to the -// Kolibri Design System as part of the Vuetify removal effort . + // NOTE: + // Full form submission tests are intentionally skipped here. + // + // This page still relies on Vuetify components (v-select / v-autocomplete) + // for required fields such as "locations" and "source". + // These components do not reliably update their 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 previous Vue Test Utils tests worked around this by directly mutating + // component data (setData), which is intentionally avoided when using + // Testing Library. + // + // These tests will be re-enabled once this page is migrated to the + // Kolibri Design System as part of the Vuetify removal effort . test.skip('creates an account when the user submits valid information', async () => { - const registerSpy = jest - .spyOn(Create.methods, 'register') - .mockResolvedValue(); + const registerSpy = jest.spyOn(Create.methods, 'register').mockResolvedValue(); await renderComponent(); @@ -120,18 +112,14 @@ describe('Create account page', () => { 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(/tagging content sources/i)); - await userEvent.click( - screen.getByLabelText(/i have read and agree to terms of service/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 waitFor(() => { - expect(finishButton).not.toBeDisabled(); + expect(finishButton).toBeEnabled(); }); await userEvent.click(finishButton); @@ -140,12 +128,12 @@ describe('Create account page', () => { expect(registerSpy).toHaveBeenCalled(); }); }); -// NOTE: -// Offline submission depends on the same required Vuetify select fields -// as the successful submission flow. -// Since those fields cannot be reliably exercised via userEvent, -// this scenario cannot currently reach the submission state. -// This test will be re-enabled once Vuetify is removed . + // NOTE: + // Offline submission depends on the same required Vuetify select fields + // as the successful submission flow. + // Since those fields cannot be reliably exercised via userEvent, + // this scenario cannot currently reach the submission state. + // This test will be re-enabled once Vuetify is removed . test.skip('shows an offline error when the user is offline', async () => { await renderComponent({ offline: true }); @@ -155,13 +143,9 @@ describe('Create account page', () => { 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(/tagging content sources/i)); - await userEvent.click( - screen.getByLabelText(/i have read and agree to terms of service/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); From a42449177f47e0ac5176a6532f3e801ca1d16b5e Mon Sep 17 00:00:00 2001 From: abhiraj75 Date: Wed, 14 Jan 2026 16:26:52 +0530 Subject: [PATCH 4/5] Trigger CI From 3dc33c46f0c021334210f8daf1cfd783c76c1f47 Mon Sep 17 00:00:00 2001 From: abhiraj75 Date: Wed, 14 Jan 2026 16:31:17 +0530 Subject: [PATCH 5/5] Trigger CI