Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Zallmann <tzallmann@gitlab.com>2018-12-08 06:12:23 +0300
committerClement Ho <clemmakesapps@gmail.com>2018-12-08 06:12:23 +0300
commitddd4cc649f31b490a73e65808d828b2f51ca1917 (patch)
treefed840b9cd9f9ddc3a8ef96814508c0470d5b38a /spec/javascripts
parenta89b5269502a0250c7507f3bb1350a7ba602822b (diff)
Resolve "Extended user centric tooltips"
Diffstat (limited to 'spec/javascripts')
-rw-r--r--spec/javascripts/api_spec.js34
-rw-r--r--spec/javascripts/lib/utils/users_cache_spec.js110
-rw-r--r--spec/javascripts/notes/components/note_edited_text_spec.js2
-rw-r--r--spec/javascripts/notes/components/note_header_spec.js3
-rw-r--r--spec/javascripts/user_popovers_spec.js66
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js133
8 files changed, 360 insertions, 5 deletions
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 46f72214831..9d55c615450 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -333,6 +333,40 @@ describe('Api', () => {
});
});
+ describe('user', () => {
+ it('fetches single user', done => {
+ const userId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}`;
+ mock.onGet(expectedUrl).reply(200, {
+ name: 'testuser',
+ });
+
+ Api.user(userId)
+ .then(({ data }) => {
+ expect(data.name).toBe('testuser');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('user status', () => {
+ it('fetches single user status', done => {
+ const userId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/status`;
+ mock.onGet(expectedUrl).reply(200, {
+ message: 'testmessage',
+ });
+
+ Api.userStatus(userId)
+ .then(({ data }) => {
+ expect(data.message).toBe('testmessage');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('commitPipelines', () => {
it('fetches pipelines for a given commit', done => {
const projectId = 'example/foobar';
diff --git a/spec/javascripts/lib/utils/users_cache_spec.js b/spec/javascripts/lib/utils/users_cache_spec.js
index 6adc19bdd51..acb5e024acd 100644
--- a/spec/javascripts/lib/utils/users_cache_spec.js
+++ b/spec/javascripts/lib/utils/users_cache_spec.js
@@ -3,7 +3,9 @@ import UsersCache from '~/lib/utils/users_cache';
describe('UsersCache', () => {
const dummyUsername = 'win';
- const dummyUser = 'has a farm';
+ const dummyUserId = 123;
+ const dummyUser = { name: 'has a farm', username: 'farmer' };
+ const dummyUserStatus = 'my status';
beforeEach(() => {
UsersCache.internalStorage = {};
@@ -135,4 +137,110 @@ describe('UsersCache', () => {
.catch(done.fail);
});
});
+
+ describe('retrieveById', () => {
+ let apiSpy;
+
+ beforeEach(() => {
+ spyOn(Api, 'user').and.callFake(id => apiSpy(id));
+ });
+
+ it('stores and returns data from API call if cache is empty', done => {
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.resolve({
+ data: dummyUser,
+ });
+ };
+
+ UsersCache.retrieveById(dummyUserId)
+ .then(user => {
+ expect(user).toBe(dummyUser);
+ expect(UsersCache.internalStorage[dummyUserId]).toBe(dummyUser);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('returns undefined if Ajax call fails and cache is empty', done => {
+ const dummyError = new Error('server exploded');
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.reject(dummyError);
+ };
+
+ UsersCache.retrieveById(dummyUserId)
+ .then(user => fail(`Received unexpected user: ${JSON.stringify(user)}`))
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('makes no Ajax call if matching data exists', done => {
+ UsersCache.internalStorage[dummyUserId] = dummyUser;
+ apiSpy = () => fail(new Error('expected no Ajax call!'));
+
+ UsersCache.retrieveById(dummyUserId)
+ .then(user => {
+ expect(user).toBe(dummyUser);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('retrieveStatusById', () => {
+ let apiSpy;
+
+ beforeEach(() => {
+ spyOn(Api, 'userStatus').and.callFake(id => apiSpy(id));
+ });
+
+ it('stores and returns data from API call if cache is empty', done => {
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.resolve({
+ data: dummyUserStatus,
+ });
+ };
+
+ UsersCache.retrieveStatusById(dummyUserId)
+ .then(userStatus => {
+ expect(userStatus).toBe(dummyUserStatus);
+ expect(UsersCache.internalStorage[dummyUserId].status).toBe(dummyUserStatus);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('returns undefined if Ajax call fails and cache is empty', done => {
+ const dummyError = new Error('server exploded');
+ apiSpy = id => {
+ expect(id).toBe(dummyUserId);
+ return Promise.reject(dummyError);
+ };
+
+ UsersCache.retrieveStatusById(dummyUserId)
+ .then(userStatus => fail(`Received unexpected user: ${JSON.stringify(userStatus)}`))
+ .catch(error => {
+ expect(error).toBe(dummyError);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('makes no Ajax call if matching data exists', done => {
+ UsersCache.internalStorage[dummyUserId] = { status: dummyUserStatus };
+ apiSpy = () => fail(new Error('expected no Ajax call!'));
+
+ UsersCache.retrieveStatusById(dummyUserId)
+ .then(userStatus => {
+ expect(userStatus).toBe(dummyUserStatus);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
});
diff --git a/spec/javascripts/notes/components/note_edited_text_spec.js b/spec/javascripts/notes/components/note_edited_text_spec.js
index e0b991c32ec..e4c8d954d50 100644
--- a/spec/javascripts/notes/components/note_edited_text_spec.js
+++ b/spec/javascripts/notes/components/note_edited_text_spec.js
@@ -39,7 +39,7 @@ describe('note_edited_text', () => {
});
it('should render provided user information', () => {
- const authorLink = vm.$el.querySelector('.js-vue-author');
+ const authorLink = vm.$el.querySelector('.js-user-link');
expect(authorLink.getAttribute('href')).toEqual(props.editedBy.path);
expect(authorLink.textContent.trim()).toEqual(props.editedBy.name);
diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js
index 379780f43a0..6d1a7ef370f 100644
--- a/spec/javascripts/notes/components/note_header_spec.js
+++ b/spec/javascripts/notes/components/note_header_spec.js
@@ -42,6 +42,9 @@ describe('note_header component', () => {
it('should render user information', () => {
expect(vm.$el.querySelector('.note-header-author-name').textContent.trim()).toEqual('Root');
expect(vm.$el.querySelector('.note-header-info a').getAttribute('href')).toEqual('/root');
+ expect(vm.$el.querySelector('.note-header-info a').dataset.userId).toEqual('1');
+ expect(vm.$el.querySelector('.note-header-info a').dataset.username).toEqual('root');
+ expect(vm.$el.querySelector('.note-header-info a').classList).toContain('js-user-link');
});
it('should render timestamp link', () => {
diff --git a/spec/javascripts/user_popovers_spec.js b/spec/javascripts/user_popovers_spec.js
new file mode 100644
index 00000000000..6cf8dd81b36
--- /dev/null
+++ b/spec/javascripts/user_popovers_spec.js
@@ -0,0 +1,66 @@
+import initUserPopovers from '~/user_popovers';
+import UsersCache from '~/lib/utils/users_cache';
+
+describe('User Popovers', () => {
+ const selector = '.js-user-link';
+
+ const dummyUser = { name: 'root' };
+ const dummyUserStatus = { message: 'active' };
+
+ const triggerEvent = (eventName, el) => {
+ const event = document.createEvent('MouseEvents');
+ event.initMouseEvent(eventName, true, true, window);
+
+ el.dispatchEvent(event);
+ };
+
+ beforeEach(() => {
+ setFixtures(`
+ <a href="/root" data-user-id="1" class="js-user-link" data-username="root" data-original-title="" title="">
+ Root
+ </a>
+ `);
+
+ const usersCacheSpy = () => Promise.resolve(dummyUser);
+ spyOn(UsersCache, 'retrieveById').and.callFake(userId => usersCacheSpy(userId));
+
+ const userStatusCacheSpy = () => Promise.resolve(dummyUserStatus);
+ spyOn(UsersCache, 'retrieveStatusById').and.callFake(userId => userStatusCacheSpy(userId));
+
+ initUserPopovers(document.querySelectorAll('.js-user-link'));
+ });
+
+ it('Should Show+Hide Popover on mouseenter and mouseleave', done => {
+ triggerEvent('mouseenter', document.querySelector(selector));
+
+ setTimeout(() => {
+ const shownPopover = document.querySelector('.popover');
+
+ expect(shownPopover).not.toBeNull();
+
+ expect(shownPopover.innerHTML).toContain(dummyUser.name);
+ expect(UsersCache.retrieveById).toHaveBeenCalledWith('1');
+
+ triggerEvent('mouseleave', document.querySelector(selector));
+
+ setTimeout(() => {
+ // After Mouse leave it should be hidden now
+ expect(document.querySelector('.popover')).toBeNull();
+ done();
+ });
+ }, 210); // We need to wait until the 200ms mouseover delay is over, only then the popover will be visible
+ });
+
+ it('Should Not show a popover on short mouse over', done => {
+ triggerEvent('mouseenter', document.querySelector(selector));
+
+ setTimeout(() => {
+ expect(document.querySelector('.popover')).toBeNull();
+ expect(UsersCache.retrieveById).not.toHaveBeenCalledWith('1');
+
+ triggerEvent('mouseleave', document.querySelector(selector));
+
+ done();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index 5c4aa7cf844..c5045afc5b0 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import { placeholderImage } from '~/lazy_loader';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper';
+import defaultAvatarUrl from '~/../images/no_avatar.png';
const DEFAULT_PROPS = {
size: 99,
@@ -76,6 +77,18 @@ describe('User Avatar Image Component', function() {
});
});
+ describe('Initialization without src', function() {
+ beforeEach(function() {
+ vm = mountComponent(UserAvatarImage);
+ });
+
+ it('should have default avatar image', function() {
+ const imageElement = vm.$el.querySelector('img');
+
+ expect(imageElement.getAttribute('src')).toBe(defaultAvatarUrl);
+ });
+ });
+
describe('dynamic tooltip content', () => {
const props = DEFAULT_PROPS;
const slots = {
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
index 0151ad23ba2..f2472fd377c 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -74,9 +74,7 @@ describe('User Avatar Link Component', function() {
describe('username', function() {
it('should not render avatar image tooltip', function() {
- expect(
- this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip').innerText.trim(),
- ).toEqual('');
+ expect(this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip')).toBeNull();
});
it('should render username prop in <span>', function() {
diff --git a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
new file mode 100644
index 00000000000..1578b0f81f9
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js
@@ -0,0 +1,133 @@
+import Vue from 'vue';
+import userPopover from '~/vue_shared/components/user_popover/user_popover.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const DEFAULT_PROPS = {
+ loaded: true,
+ user: {
+ username: 'root',
+ name: 'Administrator',
+ location: 'Vienna',
+ bio: null,
+ organization: null,
+ status: null,
+ },
+};
+
+const UserPopover = Vue.extend(userPopover);
+
+describe('User Popover Component', () => {
+ let vm;
+
+ beforeEach(() => {
+ setFixtures(`
+ <a href="/root" data-user-id="1" class="js-user-link" title="testuser">
+ Root
+ </a>
+ `);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('Empty', () => {
+ beforeEach(() => {
+ vm = mountComponent(UserPopover, {
+ target: document.querySelector('.js-user-link'),
+ user: {
+ name: null,
+ username: null,
+ location: null,
+ bio: null,
+ organization: null,
+ status: null,
+ },
+ });
+ });
+
+ it('should return skeleton loaders', () => {
+ expect(vm.$el.querySelectorAll('.animation-container').length).toBe(4);
+ });
+ });
+
+ describe('basic data', () => {
+ it('should show basic fields', () => {
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.name);
+ expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.username);
+ expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.location);
+ });
+ });
+
+ describe('job data', () => {
+ it('should show only bio if no organization is available', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.bio = 'Engineer';
+
+ vm = mountComponent(UserPopover, {
+ ...testProps,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('Engineer');
+ });
+
+ it('should show only organization if no bio is available', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.organization = 'GitLab';
+
+ vm = mountComponent(UserPopover, {
+ ...testProps,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('GitLab');
+ });
+
+ it('should have full job line when we have bio and organization', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.bio = 'Engineer';
+ testProps.user.organization = 'GitLab';
+
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('Engineer at GitLab');
+ });
+ });
+
+ describe('status data', () => {
+ it('should show only message', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.status = { message: 'Hello World' };
+
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ });
+
+ expect(vm.$el.textContent).toContain('Hello World');
+ });
+
+ it('should show message and emoji', () => {
+ const testProps = Object.assign({}, DEFAULT_PROPS);
+ testProps.user.status = { emoji: 'basketball_player', message: 'Hello World' };
+
+ vm = mountComponent(UserPopover, {
+ ...DEFAULT_PROPS,
+ target: document.querySelector('.js-user-link'),
+ status: { emoji: 'basketball_player', message: 'Hello World' },
+ });
+
+ expect(vm.$el.textContent).toContain('Hello World');
+ expect(vm.$el.innerHTML).toContain('<gl-emoji data-name="basketball_player"');
+ });
+ });
+});