Commit 05d71e43 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch 'ps-fix-fixture-gen' into 'master'

Splitting up users_select spec into EE and CE [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!59702
parents 2d49e2ad 5d73c95d
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures in EE context)', type: :controller do
include JavaScriptFixturesHelpers
let(:project) { create(:project, :repository, path: 'merge-requests-project') }
let(:user) { project.owner }
let(:merge_request) { create(:merge_request, source_project: project) }
render_views
before(:all) do
clean_frontend_fixtures('ee/merge_requests/')
end
before do
sign_in(user)
end
it 'ee/merge_requests/merge_request_with_multiple_assignees_feature.html' do
stub_licensed_features(multiple_merge_request_assignees: true)
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
id: merge_request.to_param
}, format: :html
expect(response).to be_successful
end
end
import {
createInputsModelExpectation,
createAssignedExpectation,
createTestContext,
findDropdownItemsModel,
findDropdownItem,
findAssigneesInputsModel,
getUsersFixtureAt,
setAssignees,
toggleDropdown,
waitForDropdownItems,
} from 'jest/users_select/test_helper';
describe('EE ~/users_select/index with multiple assignees', () => {
const context = createTestContext({
fixturePath: 'ee/merge_requests/merge_request_with_multiple_assignees_feature.html',
});
beforeEach(() => {
context.setup();
});
afterEach(() => {
context.teardown();
});
describe('when opened', () => {
beforeEach(async () => {
context.createSubject();
toggleDropdown();
await waitForDropdownItems();
});
describe('when users are selected', () => {
const selectedUsers = [getUsersFixtureAt(2), getUsersFixtureAt(4)];
const expectation = createAssignedExpectation({
header: 'Assignee(s)',
assigned: selectedUsers,
});
beforeEach(() => {
selectedUsers.forEach((user) => {
findDropdownItem(user).click();
});
});
it('shows assignee', () => {
expect(findDropdownItemsModel()).toEqual(expectation);
});
it('shows assignee even after close and open', () => {
toggleDropdown();
toggleDropdown();
expect(findDropdownItemsModel()).toEqual(expectation);
});
it('updates field', () => {
expect(findAssigneesInputsModel()).toEqual(createInputsModelExpectation(selectedUsers));
});
});
});
describe('with preselected user and opened', () => {
const expectation = createAssignedExpectation({
header: 'Assignee(s)',
assigned: [getUsersFixtureAt(0)],
});
beforeEach(async () => {
setAssignees(getUsersFixtureAt(0));
context.createSubject();
toggleDropdown();
await waitForDropdownItems();
});
it('shows users', () => {
expect(findDropdownItemsModel()).toEqual(expectation);
});
// Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/325991
describe('when closed and reopened', () => {
beforeEach(() => {
toggleDropdown();
toggleDropdown();
});
it('shows users', () => {
expect(findDropdownItemsModel()).toEqual(expectation);
});
});
});
});
......@@ -62,6 +62,12 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
remove_repository(project)
end
it 'merge_requests/merge_request_with_single_assignee_feature.html' do
stub_licensed_features(multiple_merge_request_assignees: false)
render_merge_request(merge_request)
end
it 'merge_requests/merge_request_of_current_user.html' do
merge_request.update(author: user)
......
import { waitFor } from '@testing-library/dom';
import MockAdapter from 'axios-mock-adapter';
import { cloneDeep } from 'lodash';
import { getJSONFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
import UsersSelect from '~/users_select';
// TODO: generate this from a fixture that guarantees the same output in CE and EE [(see issue)][1].
// Hardcoding this HTML temproarily fixes a FOSS ~"master::broken" [(see issue)][2].
// [1]: https://gitlab.com/gitlab-org/gitlab/-/issues/327809
// [2]: https://gitlab.com/gitlab-org/gitlab/-/issues/327805
const getUserSearchHTML = () => `
<div class="js-sidebar-assignee-data selectbox hide-collapsed">
<input type="hidden" name="merge_request[assignee_ids][]" value="0">
<div class="dropdown js-sidebar-assignee-dropdown">
<button class="dropdown-menu-toggle js-user-search js-author-search js-multiselect js-save-user-data js-invite-members-track" type="button" data-first-user="frontend-fixtures" data-current-user="true" data-iid="1" data-issuable-type="merge_request" data-project-id="1" data-author-id="1" data-field-name="merge_request[assignee_ids][]" data-issue-update="http://test.host/frontend-fixtures/merge-requests-project/-/merge_requests/1.json" data-ability-name="merge_request" data-null-user="true" data-display="static" data-multi-select="true" data-dropdown-title="Select assignee(s)" data-dropdown-header="Assignee(s)" data-track-event="show_invite_members" data-toggle="dropdown"><span class="dropdown-toggle-text ">Select assignee(s)</span><svg class="s16 dropdown-menu-toggle-icon gl-top-3" data-testid="chevron-down-icon"><use xlink:href="http://test.host/assets/icons-16c30bec0d8a57f0a33e6f6215c6aff7a6ec5e4a7e6b7de733a6b648541a336a.svg#chevron-down"></use></svg></button><div class="dropdown-menu dropdown-select dropdown-menu-user dropdown-menu-selectable dropdown-menu-author dropdown-extended-height">
<div class="dropdown-title gl-display-flex">
<span class="gl-ml-auto">Assign to</span><button class="dropdown-title-button dropdown-menu-close gl-ml-auto" aria-label="Close" type="button"><svg class="s16 dropdown-menu-close-icon" data-testid="close-icon"><use xlink:href="http://test.host/assets/icons-16c30bec0d8a57f0a33e6f6215c6aff7a6ec5e4a7e6b7de733a6b648541a336a.svg#close"></use></svg></button>
</div>
<div class="dropdown-input">
<input type="search" id="" data-qa-selector="dropdown_input_field" class="dropdown-input-field" placeholder="Search users" autocomplete="off"><svg class="s16 dropdown-input-search" data-testid="search-icon"><use xlink:href="http://test.host/assets/icons-16c30bec0d8a57f0a33e6f6215c6aff7a6ec5e4a7e6b7de733a6b648541a336a.svg#search"></use></svg><svg class="s16 dropdown-input-clear js-dropdown-input-clear" data-testid="close-icon"><use xlink:href="http://test.host/assets/icons-16c30bec0d8a57f0a33e6f6215c6aff7a6ec5e4a7e6b7de733a6b648541a336a.svg#close"></use></svg>
</div>
<div data-qa-selector="dropdown_list_content" class="dropdown-content "></div>
<div class="dropdown-footer">
<ul class="dropdown-footer-list">
<li>
<div class="js-invite-members-trigger" data-display-text="Invite Members" data-event="click_invite_members" data-label="edit_assignee" data-trigger-element="anchor"></div>
</li>
</ul>
</div>
<div class="dropdown-loading"><div class="gl-spinner-container"><span class="gl-spinner gl-spinner-orange gl-spinner-md gl-mt-7" aria-label="Loading"></span></div></div>
</div>
</div>
</div>
`;
const USER_SEARCH_HTML = getUserSearchHTML();
const AUTOCOMPLETE_USERS = getJSONFixture('autocomplete/users.json');
import {
createInputsModelExpectation,
createUnassignedExpectation,
createAssignedExpectation,
createTestContext,
findDropdownItemsModel,
findDropdownItem,
findAssigneesInputsModel,
getUsersFixtureAt,
setAssignees,
toggleDropdown,
waitForDropdownItems,
} from './test_helper';
describe('~/users_select/index', () => {
let subject;
let mock;
const createSubject = (currentUser = null) => {
if (subject) {
throw new Error('test subject is already created');
}
subject = new UsersSelect(currentUser);
};
// finders -------------------------------------------------------------------
const findAssigneesInputs = () =>
document.querySelectorAll('input[name="merge_request[assignee_ids][]');
const findAssigneesInputsModel = () =>
Array.from(findAssigneesInputs()).map((input) => ({
value: input.value,
dataset: { ...input.dataset },
}));
const findUserSearchButton = () => document.querySelector('.js-user-search');
const findDropdownItem = ({ id }) => document.querySelector(`li[data-user-id="${id}"] a`);
const findDropdownItemsModel = () =>
Array.from(document.querySelectorAll('.dropdown-content li')).map((el) => {
if (el.classList.contains('divider')) {
return {
type: 'divider',
};
} else if (el.classList.contains('dropdown-header')) {
return {
type: 'dropdown-header',
text: el.textContent,
};
}
return {
type: 'user',
userId: el.dataset.userId,
};
});
// arrange/act helpers -------------------------------------------------------
const setAssignees = (...users) => {
findAssigneesInputs().forEach((x) => x.remove());
const container = document.querySelector('.js-sidebar-assignee-data');
container.prepend(
...users.map((user) => {
const input = document.createElement('input');
input.name = 'merge_request[assignee_ids][]';
input.value = user.id.toString();
input.setAttribute('data-avatar-url', user.avatar_url);
input.setAttribute('data-name', user.name);
input.setAttribute('data-username', user.username);
input.setAttribute('data-can-merge', user.can_merge);
return input;
}),
);
};
const toggleDropdown = () => findUserSearchButton().click();
const waitForDropdownItems = () =>
waitFor(() => expect(findDropdownItem(AUTOCOMPLETE_USERS[0])).not.toBeNull());
// assertion helpers ---------------------------------------------------------
const createUnassignedExpectation = () => {
return [
{ type: 'user', userId: '0' },
{ type: 'divider' },
...AUTOCOMPLETE_USERS.map((x) => ({ type: 'user', userId: x.id.toString() })),
];
};
const createAssignedExpectation = (...selectedUsers) => {
const selectedIds = new Set(selectedUsers.map((x) => x.id));
const unselectedUsers = AUTOCOMPLETE_USERS.filter((x) => !selectedIds.has(x.id));
return [
{ type: 'user', userId: '0' },
{ type: 'divider' },
{ type: 'dropdown-header', text: 'Assignee(s)' },
...selectedUsers.map((x) => ({ type: 'user', userId: x.id.toString() })),
{ type: 'divider' },
...unselectedUsers.map((x) => ({ type: 'user', userId: x.id.toString() })),
];
};
const context = createTestContext({
fixturePath: 'merge_requests/merge_request_with_single_assignee_feature.html',
});
beforeEach(() => {
const rootEl = document.createElement('div');
rootEl.innerHTML = USER_SEARCH_HTML;
document.body.appendChild(rootEl);
mock = new MockAdapter(axios);
mock.onGet('/-/autocomplete/users.json').reply(200, cloneDeep(AUTOCOMPLETE_USERS));
context.setup();
});
afterEach(() => {
document.body.innerHTML = '';
subject = null;
context.teardown();
});
describe('when opened', () => {
beforeEach(async () => {
createSubject();
context.createSubject();
toggleDropdown();
await waitForDropdownItems();
......@@ -150,8 +38,12 @@ describe('~/users_select/index', () => {
});
describe('when users are selected', () => {
const selectedUsers = [AUTOCOMPLETE_USERS[2], AUTOCOMPLETE_USERS[4]];
const expectation = createAssignedExpectation(...selectedUsers);
const selectedUsers = [getUsersFixtureAt(2), getUsersFixtureAt(4)];
const lastSelected = selectedUsers[selectedUsers.length - 1];
const expectation = createAssignedExpectation({
header: 'Assignee',
assigned: [lastSelected],
});
beforeEach(() => {
selectedUsers.forEach((user) => {
......@@ -163,42 +55,22 @@ describe('~/users_select/index', () => {
expect(findDropdownItemsModel()).toEqual(expectation);
});
it('shows assignee even after close and open', () => {
toggleDropdown();
toggleDropdown();
expect(findDropdownItemsModel()).toEqual(expectation);
});
it('updates field', () => {
expect(findAssigneesInputsModel()).toEqual(
selectedUsers.map((user) => ({
value: user.id.toString(),
dataset: {
approved: user.approved.toString(),
avatar_url: user.avatar_url,
can_merge: user.can_merge.toString(),
can_update_merge_request: user.can_update_merge_request.toString(),
id: user.id.toString(),
name: user.name,
show_status: user.show_status.toString(),
state: user.state,
username: user.username,
web_url: user.web_url,
},
})),
);
expect(findAssigneesInputsModel()).toEqual(createInputsModelExpectation([lastSelected]));
});
});
});
describe('with preselected user and opened', () => {
const expectation = createAssignedExpectation(AUTOCOMPLETE_USERS[0]);
const expectation = createAssignedExpectation({
header: 'Assignee',
assigned: [getUsersFixtureAt(0)],
});
beforeEach(async () => {
setAssignees(AUTOCOMPLETE_USERS[0]);
setAssignees(getUsersFixtureAt(0));
createSubject();
context.createSubject();
toggleDropdown();
await waitForDropdownItems();
......
import { waitFor } from '@testing-library/dom';
import MockAdapter from 'axios-mock-adapter';
import { memoize, cloneDeep } from 'lodash';
import { getFixture, getJSONFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
import UsersSelect from '~/users_select';
// fixtures -------------------------------------------------------------------
const getUserSearchHTML = memoize((fixturePath) => {
const html = getFixture(fixturePath);
const parser = new DOMParser();
const el = parser.parseFromString(html, 'text/html').querySelector('.assignee');
return el.outerHTML;
});
const getUsersFixture = memoize(() => getJSONFixture('autocomplete/users.json'));
export const getUsersFixtureAt = (idx) => getUsersFixture()[idx];
// test context ---------------------------------------------------------------
export const createTestContext = ({ fixturePath }) => {
let mock = null;
let subject = null;
const setup = () => {
const rootEl = document.createElement('div');
rootEl.innerHTML = getUserSearchHTML(fixturePath);
document.body.appendChild(rootEl);
mock = new MockAdapter(axios);
mock.onGet('/-/autocomplete/users.json').reply(200, cloneDeep(getUsersFixture()));
};
const teardown = () => {
mock.restore();
document.body.innerHTML = '';
subject = null;
};
const createSubject = () => {
if (subject) {
throw new Error('test subject is already created');
}
subject = new UsersSelect(null);
};
return {
setup,
teardown,
createSubject,
};
};
// finders -------------------------------------------------------------------
export const findAssigneesInputs = () =>
document.querySelectorAll('input[name="merge_request[assignee_ids][]');
export const findAssigneesInputsModel = () =>
Array.from(findAssigneesInputs()).map((input) => ({
value: input.value,
dataset: { ...input.dataset },
}));
export const findUserSearchButton = () => document.querySelector('.js-user-search');
export const findDropdownItem = ({ id }) => document.querySelector(`li[data-user-id="${id}"] a`);
export const findDropdownItemsModel = () =>
Array.from(document.querySelectorAll('.dropdown-content li')).map((el) => {
if (el.classList.contains('divider')) {
return {
type: 'divider',
};
} else if (el.classList.contains('dropdown-header')) {
return {
type: 'dropdown-header',
text: el.textContent,
};
}
return {
type: 'user',
userId: el.dataset.userId,
};
});
// arrange/act helpers -------------------------------------------------------
export const setAssignees = (...users) => {
findAssigneesInputs().forEach((x) => x.remove());
const container = document.querySelector('.js-sidebar-assignee-data');
container.prepend(
...users.map((user) => {
const input = document.createElement('input');
input.name = 'merge_request[assignee_ids][]';
input.value = user.id.toString();
input.setAttribute('data-avatar-url', user.avatar_url);
input.setAttribute('data-name', user.name);
input.setAttribute('data-username', user.username);
input.setAttribute('data-can-merge', user.can_merge);
return input;
}),
);
};
export const toggleDropdown = () => findUserSearchButton().click();
export const waitForDropdownItems = () =>
waitFor(() => expect(findDropdownItem(getUsersFixtureAt(0))).not.toBeNull());
// assertion helpers ---------------------------------------------------------
export const createUnassignedExpectation = () => {
return [
{ type: 'user', userId: '0' },
{ type: 'divider' },
...getUsersFixture().map((x) => ({
type: 'user',
userId: x.id.toString(),
})),
];
};
export const createAssignedExpectation = ({ header, assigned }) => {
const assignedIds = new Set(assigned.map((x) => x.id));
const unassignedIds = getUsersFixture().filter((x) => !assignedIds.has(x.id));
return [
{ type: 'user', userId: '0' },
{ type: 'divider' },
{ type: 'dropdown-header', text: header },
...assigned.map((x) => ({ type: 'user', userId: x.id.toString() })),
{ type: 'divider' },
...unassignedIds.map((x) => ({ type: 'user', userId: x.id.toString() })),
];
};
export const createInputsModelExpectation = (users) =>
users.map((user) => ({
value: user.id.toString(),
dataset: {
approved: user.approved.toString(),
avatar_url: user.avatar_url,
can_merge: user.can_merge.toString(),
can_update_merge_request: user.can_update_merge_request.toString(),
id: user.id.toString(),
name: user.name,
show_status: user.show_status.toString(),
state: user.state,
username: user.username,
web_url: user.web_url,
},
}));
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment