diff options
author | Constance Okoghenun <cokoghenun@gitlab.com> | 2018-11-07 17:20:17 +0000 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2018-11-07 17:20:17 +0000 |
commit | baa37edd93d47e836835617ef08d6fc85ad3a689 (patch) | |
tree | 9241261a47917e76dc845eea5f47939d475d6685 /spec | |
parent | 06e8cf58558cccc5a8556e94c93aa4bf25dc083e (diff) | |
download | gitlab-ce-baa37edd93d47e836835617ef08d6fc85ad3a689.tar.gz |
Resolve "Issue board card design"
Diffstat (limited to 'spec')
17 files changed, 204 insertions, 64 deletions
diff --git a/spec/features/boards/add_issues_modal_spec.rb b/spec/features/boards/add_issues_modal_spec.rb index eebc987499d..030993462b5 100644 --- a/spec/features/boards/add_issues_modal_spec.rb +++ b/spec/features/boards/add_issues_modal_spec.rb @@ -160,7 +160,7 @@ describe 'Issue Boards add issue modal', :js do it 'changes button text with plural' do page.within('.add-issues-modal') do - all('.board-card .board-card-number').each do |el| + all('.board-card .js-board-card-number-container').each do |el| el.click end diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index ec0ca21450a..21779336559 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -78,7 +78,7 @@ describe 'Issue Boards', :js do end it 'moves from bottom to top' do - drag(from_index: 2, to_index: 0) + drag(from_index: 2, to_index: 0, duration: 1020) wait_for_requests @@ -130,7 +130,7 @@ describe 'Issue Boards', :js do end it 'moves to bottom of another list' do - drag(list_from_index: 1, list_to_index: 2, to_index: 2) + drag(list_from_index: 1, list_to_index: 2, to_index: 2, duration: 1020) wait_for_requests diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb index d3da8cc6752..b58c433bbfe 100644 --- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb +++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb @@ -89,16 +89,17 @@ describe 'Merge request > User sees avatars on diff notes', :js do page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').send_keys(:return) - expect(page).to have_selector('img.js-diff-comment-avatar', count: 1) + expect(page).to have_selector('.js-diff-comment-avatar img', count: 1) end end it 'shows comment on note avatar' do page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').send_keys(:return) - - expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") + first('.js-diff-comment-avatar img').hover end + + expect(page).to have_content "#{note.author.name}: #{note.note.truncate(17)}" end it 'toggles comments when clicking avatar' do @@ -109,7 +110,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do expect(page).not_to have_selector('.notes_holder') page.within find_line(position.line_code(project.repository)) do - first('img.js-diff-comment-avatar').click + first('.js-diff-comment-avatar img').click end expect(page).to have_selector('.notes_holder') @@ -125,7 +126,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do wait_for_requests page.within find_line(position.line_code(project.repository)) do - expect(page).not_to have_selector('img.js-diff-comment-avatar') + expect(page).not_to have_selector('.js-diff-comment-avatar img') end end @@ -143,7 +144,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').send_keys(:return) - expect(page).to have_selector('img.js-diff-comment-avatar', count: 2) + expect(page).to have_selector('.js-diff-comment-avatar img', count: 2) end end @@ -162,7 +163,7 @@ describe 'Merge request > User sees avatars on diff notes', :js do page.within find_line(position.line_code(project.repository)) do find('.diff-notes-collapse').send_keys(:return) - expect(page).to have_selector('img.js-diff-comment-avatar', count: 3) + expect(page).to have_selector('.js-diff-comment-avatar img', count: 3) expect(find('.diff-comments-more-count')).to have_content '+1' end end diff --git a/spec/fixtures/api/schemas/entities/issue_board.json b/spec/fixtures/api/schemas/entities/issue_board.json index 8d821ebb843..3e252ddd13c 100644 --- a/spec/fixtures/api/schemas/entities/issue_board.json +++ b/spec/fixtures/api/schemas/entities/issue_board.json @@ -8,6 +8,7 @@ "due_date": { "type": "date" }, "project_id": { "type": "integer" }, "relative_position": { "type": ["integer", "null"] }, + "time_estimate": { "type": "integer" }, "weight": { "type": "integer" }, "project": { "type": "object", diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json index 4878df43d28..a83ec55cede 100644 --- a/spec/fixtures/api/schemas/issue.json +++ b/spec/fixtures/api/schemas/issue.json @@ -13,6 +13,7 @@ "confidential": { "type": "boolean" }, "due_date": { "type": ["date", "null"] }, "relative_position": { "type": "integer" }, + "time_estimate": { "type": "integer" }, "issue_sidebar_endpoint": { "type": "string" }, "toggle_subscription_endpoint": { "type": "string" }, "assignable_labels_endpoint": { "type": "string" }, diff --git a/spec/javascripts/boards/components/issue_due_date_spec.js b/spec/javascripts/boards/components/issue_due_date_spec.js new file mode 100644 index 00000000000..9e49330c052 --- /dev/null +++ b/spec/javascripts/boards/components/issue_due_date_spec.js @@ -0,0 +1,64 @@ +import Vue from 'vue'; +import dateFormat from 'dateformat'; +import IssueDueDate from '~/boards/components/issue_due_date.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Issue Due Date component', () => { + let vm; + let date; + const Component = Vue.extend(IssueDueDate); + const createComponent = (dueDate = new Date()) => + mountComponent(Component, { date: dateFormat(dueDate, 'yyyy-mm-dd', true) }); + + beforeEach(() => { + date = new Date(); + vm = createComponent(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render "Today" if the due date is today', () => { + const timeContainer = vm.$el.querySelector('time'); + + expect(timeContainer.textContent.trim()).toEqual('Today'); + }); + + it('should render "Yesterday" if the due date is yesterday', () => { + date.setDate(date.getDate() - 1); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Yesterday'); + }); + + it('should render "Tomorrow" if the due date is one day from now', () => { + date.setDate(date.getDate() + 1); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('Tomorrow'); + }); + + it('should render day of the week if due date is one week away', () => { + date.setDate(date.getDate() + 5); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').textContent.trim()).toEqual(dateFormat(date, 'dddd', true)); + }); + + it('should render month and day for other dates', () => { + date.setDate(date.getDate() + 17); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').textContent.trim()).toEqual( + dateFormat(date, 'mmm d', true), + ); + }); + + it('should contain the correct `.text-danger` css class for overdue issue', () => { + date.setDate(date.getDate() - 17); + vm = createComponent(date); + + expect(vm.$el.querySelector('time').classList.contains('text-danger')).toEqual(true); + }); +}); diff --git a/spec/javascripts/boards/components/issue_time_estimate_spec.js b/spec/javascripts/boards/components/issue_time_estimate_spec.js new file mode 100644 index 00000000000..ba65d3287da --- /dev/null +++ b/spec/javascripts/boards/components/issue_time_estimate_spec.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; +import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Issue Tine Estimate component', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(IssueTimeEstimate); + vm = mountComponent(Component, { + estimate: 374460, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders the correct time estimate', () => { + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('2w 3d 1m'); + }); + + it('renders expanded time estimate in tooltip', () => { + expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain( + '2 weeks 3 days 1 minute', + ); + }); + + it('prevents tooltip xss', done => { + const alertSpy = spyOn(window, 'alert'); + vm.estimate = 'Foo <script>alert("XSS")</script>'; + + vm.$nextTick(() => { + expect(alertSpy).not.toHaveBeenCalled(); + expect(vm.$el.querySelector('time').textContent.trim()).toEqual('0m'); + expect(vm.$el.querySelector('.js-issue-time-estimate').textContent).toContain('0m'); + done(); + }); + }); +}); diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index 58b7d45d913..6eda5047dd0 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -117,11 +117,9 @@ describe('Issue card component', () => { }); it('sets title', () => { - expect( - component.$el - .querySelector('.board-card-assignee img') - .getAttribute('data-original-title'), - ).toContain(`Assigned to ${user.name}`); + expect(component.$el.querySelector('.js-assignee-tooltip').textContent).toContain( + `${user.name}`, + ); }); it('sets users path', () => { @@ -154,7 +152,7 @@ describe('Issue card component', () => { it('displays defaults avatar if users avatar is null', () => { expect(component.$el.querySelector('.board-card-assignee img')).not.toBeNull(); expect(component.$el.querySelector('.board-card-assignee img').getAttribute('src')).toBe( - 'default_avatar?width=20', + 'default_avatar?width=24', ); }); }); @@ -163,7 +161,6 @@ describe('Issue card component', () => { describe('multiple assignees', () => { beforeEach(done => { component.issue.assignees = [ - user, new ListAssignee({ id: 2, name: 'user2', @@ -187,11 +184,11 @@ describe('Issue card component', () => { Vue.nextTick(() => done()); }); - it('renders all four assignees', () => { - expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(4); + it('renders all three assignees', () => { + expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3); }); - describe('more than four assignees', () => { + describe('more than three assignees', () => { beforeEach(done => { component.issue.assignees.push( new ListAssignee({ @@ -207,12 +204,12 @@ describe('Issue card component', () => { it('renders more avatar counter', () => { expect( - component.$el.querySelector('.board-card-assignee .avatar-counter').innerText, + component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(), ).toEqual('+2'); }); - it('renders three assignees', () => { - expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(3); + it('renders two assignees', () => { + expect(component.$el.querySelectorAll('.board-card-assignee .avatar').length).toEqual(2); }); it('renders 99+ avatar counter', done => { @@ -228,7 +225,7 @@ describe('Issue card component', () => { Vue.nextTick(() => { expect( - component.$el.querySelector('.board-card-assignee .avatar-counter').innerText, + component.$el.querySelector('.board-card-assignee .avatar-counter').innerText.trim(), ).toEqual('99+'); done(); }); diff --git a/spec/javascripts/jobs/components/job_app_spec.js b/spec/javascripts/jobs/components/job_app_spec.js index 98c995393b9..fcf3780f0ea 100644 --- a/spec/javascripts/jobs/components/job_app_spec.js +++ b/spec/javascripts/jobs/components/job_app_spec.js @@ -102,7 +102,7 @@ describe('Job App ', () => { .querySelector('.header-main-content') .textContent.replace(/\s+/g, ' ') .trim(), - ).toEqual('passed Job #4757 triggered 1 year ago by Root'); + ).toContain('passed Job #4757 triggered 1 year ago by Root'); done(); }, 0); }); @@ -128,7 +128,7 @@ describe('Job App ', () => { .querySelector('.header-main-content') .textContent.replace(/\s+/g, ' ') .trim(), - ).toEqual('passed Job #4757 created 3 weeks ago by Root'); + ).toContain('passed Job #4757 created 3 weeks ago by Root'); done(); }, 0); }); diff --git a/spec/javascripts/lib/utils/datetime_utility_spec.js b/spec/javascripts/lib/utils/datetime_utility_spec.js index d699e66b8ca..bebe76f76c5 100644 --- a/spec/javascripts/lib/utils/datetime_utility_spec.js +++ b/spec/javascripts/lib/utils/datetime_utility_spec.js @@ -336,6 +336,12 @@ describe('prettyTime methods', () => { expect(timeString).toBe('0m'); }); + + it('should return non-condensed representation of time object', () => { + const timeObject = { weeks: 1, days: 0, hours: 1, minutes: 0 }; + + expect(datetimeUtility.stringifyTime(timeObject, true)).toEqual('1 week 1 hour'); + }); }); describe('abbreviateTime', () => { diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js index 473a062fc40..556a0976b29 100644 --- a/spec/javascripts/pipelines/header_component_spec.js +++ b/spec/javascripts/pipelines/header_component_spec.js @@ -51,7 +51,7 @@ describe('Pipeline details header', () => { .querySelector('.header-main-content') .textContent.replace(/\s+/g, ' ') .trim(), - ).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo'); + ).toContain('failed Pipeline #123 triggered 3 weeks ago by Foo'); }); describe('action buttons', () => { diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index c9011b403b7..d6c44f4c976 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -63,12 +63,15 @@ describe('Pipeline Url Component', () => { }).$mount(); const image = component.$el.querySelector('.js-pipeline-url-user img'); + const tooltip = component.$el.querySelector( + '.js-pipeline-url-user .js-user-avatar-image-toolip', + ); expect(component.$el.querySelector('.js-pipeline-url-user').getAttribute('href')).toEqual( mockData.pipeline.user.web_url, ); - expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name); + expect(tooltip.textContent.trim()).toEqual(mockData.pipeline.user.name); expect(image.getAttribute('src')).toEqual(`${mockData.pipeline.user.avatar_url}?width=20`); }); diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js index 506d01f5ec1..4c575536f0e 100644 --- a/spec/javascripts/pipelines/pipelines_table_row_spec.js +++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js @@ -86,8 +86,8 @@ describe('Pipelines Table Row', () => { expect( component.$el - .querySelector('.table-section:nth-child(2) img') - .getAttribute('data-original-title'), + .querySelector('.table-section:nth-child(2) .js-user-avatar-image-toolip') + .textContent.trim(), ).toEqual(pipeline.user.name); }); }); @@ -112,8 +112,8 @@ describe('Pipelines Table Row', () => { const commitAuthorLink = commitAuthorElement.getAttribute('href'); const commitAuthorName = commitAuthorElement - .querySelector('img.avatar') - .getAttribute('data-original-title'); + .querySelector('.js-user-avatar-image-toolip') + .textContent.trim(); return { commitAuthorElement, commitAuthorLink, commitAuthorName }; }; diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 97dacec1fce..18fcdf7ede1 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -98,8 +98,8 @@ describe('Commit component', () => { it('Should render the author avatar with title and alt attributes', () => { expect( component.$el - .querySelector('.commit-title .avatar-image-container img') - .getAttribute('data-original-title'), + .querySelector('.commit-title .avatar-image-container .js-user-avatar-image-toolip') + .textContent.trim(), ).toContain(props.author.username); expect( diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js index 3bf497bc00b..7a741bdc067 100644 --- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js +++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js @@ -73,7 +73,7 @@ describe('Header CI Component', () => { }); it('should render user icon and name', () => { - expect(vm.$el.querySelector('.js-user-link').textContent.trim()).toEqual(props.user.name); + expect(vm.$el.querySelector('.js-user-link').innerText.trim()).toContain(props.user.name); }); it('should render provided actions', () => { 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 dc7652c77f7..5c4aa7cf844 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 @@ -1,7 +1,7 @@ import Vue from 'vue'; import { placeholderImage } from '~/lazy_loader'; import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; +import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper'; const DEFAULT_PROPS = { size: 99, @@ -32,18 +32,12 @@ describe('User Avatar Image Component', function() { }); it('should have <img> as a child element', function() { - expect(vm.$el.tagName).toBe('IMG'); - expect(vm.$el.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(vm.$el.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt); - }); - - it('should properly compute tooltipContainer', function() { - expect(vm.tooltipContainer).toBe('body'); - }); + const imageElement = vm.$el.querySelector('img'); - it('should properly render tooltipContainer', function() { - expect(vm.$el.getAttribute('data-container')).toBe('body'); + expect(imageElement).not.toBe(null); + expect(imageElement.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt); }); it('should properly compute avatarSizeClass', function() { @@ -51,7 +45,7 @@ describe('User Avatar Image Component', function() { }); it('should properly render img css', function() { - const { classList } = vm.$el; + const { classList } = vm.$el.querySelector('img'); const containsAvatar = classList.contains('avatar'); const containsSizeClass = classList.contains('s99'); const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses); @@ -73,12 +67,41 @@ describe('User Avatar Image Component', function() { }); it('should add lazy attributes', function() { - const { classList } = vm.$el; - const lazyClass = classList.contains('lazy'); + const imageElement = vm.$el.querySelector('img'); + const lazyClass = imageElement.classList.contains('lazy'); expect(lazyClass).toBe(true); - expect(vm.$el.getAttribute('src')).toBe(placeholderImage); - expect(vm.$el.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.getAttribute('src')).toBe(placeholderImage); + expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + }); + }); + + describe('dynamic tooltip content', () => { + const props = DEFAULT_PROPS; + const slots = { + default: ['Action!'], + }; + + beforeEach(() => { + vm = mountComponentWithSlots(UserAvatarImage, { props, slots }).$mount(); + }); + + it('renders the tooltip slot', () => { + expect(vm.$el.querySelector('.js-user-avatar-image-toolip')).not.toBe(null); + }); + + it('renders the tooltip content', () => { + expect(vm.$el.querySelector('.js-user-avatar-image-toolip').textContent).toContain( + slots.default[0], + ); + }); + + it('does not render tooltip data attributes for on avatar image', () => { + const avatarImg = vm.$el.querySelector('img'); + + expect(avatarImg.dataset.originalTitle).not.toBeDefined(); + expect(avatarImg.dataset.placement).not.toBeDefined(); + expect(avatarImg.dataset.container).not.toBeDefined(); }); }); }); 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 e022245d3ea..0151ad23ba2 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 @@ -60,39 +60,43 @@ describe('User Avatar Link Component', function() { it('should only render image tag in link', function() { const childElements = this.userAvatarLink.$el.childNodes; - expect(childElements[0].tagName).toBe('IMG'); + expect(this.userAvatarLink.$el.querySelector('img')).not.toBe('null'); // Vue will render the hidden component as <!----> expect(childElements[1].tagName).toBeUndefined(); }); it('should render avatar image tooltip', function() { - expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual( - this.propsData.tooltipText, - ); + expect(this.userAvatarLink.shouldShowUsername).toBe(false); + expect(this.userAvatarLink.avatarTooltipText).toEqual(this.propsData.tooltipText); }); }); describe('username', function() { it('should not render avatar image tooltip', function() { - expect(this.userAvatarLink.$el.querySelector('img').dataset.originalTitle).toEqual(''); + expect( + this.userAvatarLink.$el.querySelector('.js-user-avatar-image-toolip').innerText.trim(), + ).toEqual(''); }); it('should render username prop in <span>', function() { - expect(this.userAvatarLink.$el.querySelector('span').innerText.trim()).toEqual( - this.propsData.username, - ); + expect( + this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').innerText.trim(), + ).toEqual(this.propsData.username); }); it('should render text tooltip for <span>', function() { - expect(this.userAvatarLink.$el.querySelector('span').dataset.originalTitle).toEqual( - this.propsData.tooltipText, - ); + expect( + this.userAvatarLink.$el.querySelector('.js-user-avatar-link-username').dataset + .originalTitle, + ).toEqual(this.propsData.tooltipText); }); it('should render text tooltip placement for <span>', function() { expect( - this.userAvatarLink.$el.querySelector('span').getAttribute('tooltip-placement'), + this.userAvatarLink.$el + .querySelector('.js-user-avatar-link-username') + .getAttribute('tooltip-placement'), ).toEqual(this.propsData.tooltipPlacement); }); }); |