Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,197 +1,155 @@
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(() => {});
}

await router.push({ name: 'Create', query: routeQuery }).catch(() => {});

async function makeWrapper(formData) {
const wrapper = mount(Create, {
return render(Create, {
router,
computed: {
getPolicyAcceptedData() {
return () => {
return {};
};
},
},
stubs: ['PolicyModals'],
mocks: connectionStateMocks,
});
await wrapper.setData({
form: {
...defaultData,
...formData,
store: makeStore({ offline }),
stubs: {
PolicyModals: true,
},
});
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();
});
};

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: '{}',
});
});
describe('Create account page', () => {
test('smoke test: renders the create account page', async () => {
await renderComponent();

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');
expect(await screen.findByRole('heading', { name: /create an account/i })).toBeInTheDocument();
});

describe('validation', () => {
it('should call register if form is valid', async () => {
const [wrapper, mocks] = await makeWrapper();
wrapper.vm.submit();
expect(mocks.register).toHaveBeenCalled();
});
test('shows validation state when submitting empty form', 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 finishButton = screen.getByRole('button', { name: /finish/i });
await userEvent.click(finishButton);

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();
});
expect(finishButton).toBeDisabled();
});

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('allows user to fill in text input fields', async () => {
await renderComponent();

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();
},
);

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.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('on backend failures', () => {
let wrapper, mocks;
test('allows user to check checkboxes', async () => {
await renderComponent();

beforeEach(async () => {
[wrapper, mocks] = await makeWrapper();
});
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);

expect(contentSourcesCheckbox).toBeChecked();
expect(tosCheckbox).toBeChecked();
});

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);
test('automatically fills the email field when provided in the URL', async () => {
await renderComponent({
routeQuery: { email: 'newtest@test.com' },
});

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');
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 .

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));

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).toBeEnabled();
});

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);
await userEvent.click(finishButton);

await waitFor(() => {
expect(registerSpy).toHaveBeenCalled();
});
});
describe('double-submit prevention', () => {
it('should prevent multiple API calls on rapid clicks', async () => {
const [wrapper, mocks] = await makeWrapper();
// 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 });

// Click submit multiple times
const p1 = wrapper.vm.submit();
const p2 = wrapper.vm.submit();
const p3 = wrapper.vm.submit();
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 Promise.all([p1, p2, p3]);
await userEvent.click(screen.getByLabelText(/tagging content sources/i));

// Only 1 API call should be made
expect(mocks.register).toHaveBeenCalledTimes(1);
});
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();
});
});