diff options
18 files changed, 402 insertions, 230 deletions
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js index c50ac667c20..2d5bae9a9c4 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -1,19 +1,19 @@ import $ from 'jquery'; -import _ from 'underscore'; import { getSelector, - togglePopover, inserted, - mouseenter, - mouseleave, } from './feature_highlight_helper'; +import { + togglePopover, + mouseenter, + debouncedMouseleave, +} from '../shared/popover'; export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { const $selector = $(getSelector(id)); const $parent = $selector.parent(); const $popoverContent = $parent.siblings('.feature-highlight-popover-content'); const hideOnScroll = togglePopover.bind($selector, false); - const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout); $selector // Setup popover @@ -29,13 +29,10 @@ export function setupFeatureHighlightPopover(id, debounceTimeout = 300) { `, }) .on('mouseenter', mouseenter) - .on('mouseleave', debouncedMouseleave) + .on('mouseleave', debouncedMouseleave(debounceTimeout)) .on('inserted.bs.popover', inserted) .on('show.bs.popover', () => { - window.addEventListener('scroll', hideOnScroll); - }) - .on('hide.bs.popover', () => { - window.removeEventListener('scroll', hideOnScroll); + window.addEventListener('scroll', hideOnScroll, { once: true }); }) // Display feature highlight .removeAttr('disabled'); diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js index f480e72961c..d5b97ebb264 100644 --- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -3,20 +3,10 @@ import axios from '../lib/utils/axios_utils'; import { __ } from '../locale'; import Flash from '../flash'; import LazyLoader from '../lazy_loader'; +import { togglePopover } from '../shared/popover'; export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`; -export function togglePopover(show) { - const isAlreadyShown = this.hasClass('js-popover-show'); - if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) { - return false; - } - this.popover(show ? 'show' : 'hide'); - this.toggleClass('disable-animation js-popover-show', show); - - return true; -} - export function dismiss(highlightId) { axios.post(this.attr('data-dismiss-endpoint'), { feature_name: highlightId, @@ -27,23 +17,6 @@ export function dismiss(highlightId) { this.hide(); } -export function mouseleave() { - if (!$('.popover:hover').length > 0) { - const $featureHighlight = $(this); - togglePopover.call($featureHighlight, false); - } -} - -export function mouseenter() { - const $featureHighlight = $(this); - - const showedPopover = togglePopover.call($featureHighlight, true); - if (showedPopover) { - $('.popover') - .on('mouseleave', mouseleave.bind($featureHighlight)); - } -} - export function inserted() { const popoverId = this.getAttribute('aria-describedby'); const highlightId = this.dataset.highlight; diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js index e6e3a66aa20..325fa570f37 100644 --- a/app/assets/javascripts/milestone.js +++ b/app/assets/javascripts/milestone.js @@ -1,6 +1,7 @@ import $ from 'jquery'; import axios from './lib/utils/axios_utils'; import flash from './flash'; +import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover'; export default class Milestone { constructor() { @@ -43,4 +44,25 @@ export default class Milestone { .catch(() => flash('Error loading milestone tab')); } } + + static initDeprecationMessage() { + const deprecationMesssageContainer = document.querySelector('.js-milestone-deprecation-message'); + + if (!deprecationMesssageContainer) return; + + const deprecationMessage = deprecationMesssageContainer.querySelector('.js-milestone-deprecation-message-template').innerHTML; + const $popover = $('.js-popover-link', deprecationMesssageContainer); + const hideOnScroll = togglePopover.bind($popover, false); + + $popover.popover({ + content: deprecationMessage, + html: true, + placement: 'bottom', + }) + .on('mouseenter', mouseenter) + .on('mouseleave', debouncedMouseleave()) + .on('show.bs.popover', () => { + window.addEventListener('scroll', hideOnScroll, { once: true }); + }); + } } diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js index 397149aaa9e..8b529585898 100644 --- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js +++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js @@ -6,4 +6,6 @@ document.addEventListener('DOMContentLoaded', () => { new Milestone(); // eslint-disable-line no-new new Sidebar(); // eslint-disable-line no-new new MountMilestoneSidebar(); // eslint-disable-line no-new + + Milestone.initDeprecationMessage(); }); diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js index 88f40b5278e..74cc4ba42c1 100644 --- a/app/assets/javascripts/pages/groups/milestones/show/index.js +++ b/app/assets/javascripts/pages/groups/milestones/show/index.js @@ -1,3 +1,8 @@ import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show'; +import Milestone from '~/milestone'; -document.addEventListener('DOMContentLoaded', initMilestonesShow); +document.addEventListener('DOMContentLoaded', () => { + initMilestonesShow(); + + Milestone.initDeprecationMessage(); +}); diff --git a/app/assets/javascripts/shared/popover.js b/app/assets/javascripts/shared/popover.js new file mode 100644 index 00000000000..3fc03553bdd --- /dev/null +++ b/app/assets/javascripts/shared/popover.js @@ -0,0 +1,33 @@ +import $ from 'jquery'; +import _ from 'underscore'; + +export function togglePopover(show) { + const isAlreadyShown = this.hasClass('js-popover-show'); + if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) { + return false; + } + this.popover(show ? 'show' : 'hide'); + this.toggleClass('disable-animation js-popover-show', show); + + return true; +} + +export function mouseleave() { + if (!$('.popover:hover').length > 0) { + const $popover = $(this); + togglePopover.call($popover, false); + } +} + +export function mouseenter() { + const $popover = $(this); + + const showedPopover = togglePopover.call($popover, true); + if (showedPopover) { + $('.popover').on('mouseleave', mouseleave.bind($popover)); + } +} + +export function debouncedMouseleave(debounceTimeout = 300) { + return _.debounce(mouseleave, debounceTimeout); +} diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss index 6433b0c7855..02f3896d591 100644 --- a/app/assets/stylesheets/framework/banner.scss +++ b/app/assets/stylesheets/framework/banner.scss @@ -1,7 +1,7 @@ .banner-callout { display: flex; position: relative; - flex-wrap: wrap; + align-items: start; .banner-close { position: absolute; @@ -16,10 +16,25 @@ } .banner-graphic { - margin: 20px auto; + margin: 0 $gl-padding $gl-padding 0; } &.banner-non-empty-state { border-bottom: 1px solid $border-color; } + + @media (max-width: $screen-xs-max) { + justify-content: center; + flex-direction: column; + align-items: center; + + .banner-title, + .banner-buttons { + text-align: center; + } + + .banner-graphic { + margin-left: $gl-padding; + } + } } diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 6b89387ab5f..48f20029383 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -422,25 +422,43 @@ } } -.btn-link.btn-secondary-hover-link { - color: $gl-text-color-secondary; +.btn-link { + padding: 0; + background-color: transparent; + color: $blue-600; + font-weight: normal; + border-radius: 0; + border-color: transparent; &:hover, &:active, &:focus { - color: $gl-link-color; - text-decoration: none; + color: $blue-800; + text-decoration: underline; + background-color: transparent; + border-color: transparent; } -} -.btn-link.btn-primary-hover-link { - color: inherit; + &.btn-secondary-hover-link { + color: $gl-text-color-secondary; - &:hover, - &:active, - &:focus { - color: $gl-link-color; - text-decoration: none; + &:hover, + &:active, + &:focus { + color: $gl-link-color; + text-decoration: none; + } + } + + &.btn-primary-hover-link { + color: inherit; + + &:hover, + &:active, + &:focus { + color: $gl-link-color; + text-decoration: none; + } } } diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss index e5afa8fffcb..3af8d80daab 100644 --- a/app/assets/stylesheets/pages/milestone.scss +++ b/app/assets/stylesheets/pages/milestone.scss @@ -194,3 +194,38 @@ .issuable-row { background-color: $white-light; } + +.milestone-deprecation-message { + .popover { + padding: 0; + } + + .popover-content { + padding: 0; + } +} + +.milestone-popover-body { + padding: $gl-padding-8; + background-color: $gray-light; +} + +.milestone-popover-footer { + padding: $gl-padding-8 $gl-padding; + border-top: 1px solid $white-dark; +} + +.milestone-popover-instructions-list { + padding-left: 2em; + + > li { + padding-left: 1em; + } +} + +@media (max-width: $screen-xs-max) { + .milestone-banner-text, + .milestone-banner-link { + display: inline; + } +} diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml index 934d65e8b42..e9ac192f5f7 100644 --- a/app/views/shared/_auto_devops_callout.html.haml +++ b/app/views/shared/_auto_devops_callout.html.haml @@ -1,14 +1,14 @@ -.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } +.js-autodevops-banner.banner-callout.banner-non-empty-state.append-bottom-20.prepend-top-10{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } } .banner-graphic = custom_icon('icon_autodevops') - .prepend-top-10.prepend-left-10.append-bottom-10 - %h5= s_('AutoDevOps|Auto DevOps (Beta)') + .banner-body.prepend-left-10.append-bottom-10 + %h5.banner-title= s_('AutoDevOps|Auto DevOps (Beta)') %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.') %p - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } - .prepend-top-10 + .banner-buttons = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', diff --git a/app/views/shared/milestones/_deprecation_message.html.haml b/app/views/shared/milestones/_deprecation_message.html.haml new file mode 100644 index 00000000000..4a8f90937ea --- /dev/null +++ b/app/views/shared/milestones/_deprecation_message.html.haml @@ -0,0 +1,14 @@ +.banner-callout.compact.milestone-deprecation-message.js-milestone-deprecation-message.prepend-top-20 + .banner-graphic= image_tag 'illustrations/milestone_removing-page.svg' + .banner-body.prepend-left-10.append-right-10 + %h5.banner-title.prepend-top-0= _('This page will be removed in a future release.') + %p.milestone-banner-text= _('Use group milestones to manage issues from multiple projects in the same milestone.') + = button_tag _('Promote these project milestones into a group milestone.'), class: 'btn btn-link js-popover-link text-align-left milestone-banner-link' + .milestone-banner-buttons.prepend-top-20= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-default', target: '_blank' + + %template.js-milestone-deprecation-message-template + .milestone-popover-body + %ol.milestone-popover-instructions-list.append-bottom-0 + %li= _('Click any <strong>project name</strong> in the project list below to navigate to the project milestone.').html_safe + %li= _('Click the <strong>Promote</strong> button in the top right corner to promote it to a group milestone.').html_safe + .milestone-popover-footer= link_to _('Learn more'), help_page_url('user/project/milestones/index', anchor: 'promoting-project-milestones-to-group-milestones'), class: 'btn btn-link prepend-left-0', target: '_blank' diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml index f302299eb24..797ff034bb2 100644 --- a/app/views/shared/milestones/_top.html.haml +++ b/app/views/shared/milestones/_top.html.haml @@ -1,7 +1,8 @@ -- page_title @milestone.title +- page_title milestone.title - @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title) - group = local_assigns[:group] +- is_dynamic_milestone = milestone.legacy_group_milestone? || milestone.dashboard_milestone? .detail-page-header %a.btn.btn-default.btn-grouped.pull-right.visible-xs-block.js-sidebar-toggle{ href: "#" } @@ -31,21 +32,23 @@ - else = link_to 'Reopen Milestone', group_milestone_route(milestone, {state_event: :activate }), method: :put, class: "btn btn-grouped btn-reopen" += render 'shared/milestones/deprecation_message' if is_dynamic_milestone + .detail-page-description.milestone-detail %h2.title = markdown_field(milestone, :title) - - if @milestone.group_milestone? && @milestone.description.present? + - if milestone.group_milestone? && milestone.description.present? %div .description .wiki - = markdown_field(@milestone, :description) + = markdown_field(milestone, :description) - if milestone.complete?(current_user) && milestone.active? .alert.alert-success.prepend-top-default - close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.' %span All issues for this milestone are closed. #{close_msg} -- if @milestone.legacy_group_milestone? || @milestone.dashboard_milestone? +- if is_dynamic_milestone .table-holder %table.table %thead @@ -68,7 +71,7 @@ Open %td = ms.expires_at -- elsif @milestone.group_milestone? +- elsif milestone.group_milestone? %br View = link_to 'Issues', issues_group_path(@group, milestone_title: milestone.title) diff --git a/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml b/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml new file mode 100644 index 00000000000..3e1ac7b795d --- /dev/null +++ b/changelogs/unreleased/deprecation-warning-for-dynamic-milestones.yml @@ -0,0 +1,5 @@ +--- +title: Add deprecation message to dynamic milestone pages +merge_request: 17505 +author: +type: added diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb index 19152bf1f0f..6c51e4bbe26 100644 --- a/spec/features/milestone_spec.rb +++ b/spec/features/milestone_spec.rb @@ -108,4 +108,18 @@ feature 'Milestone' do expect(page).to have_selector('.js-delete-milestone-button', count: 0) end end + + feature 'deprecation popover', :js do + it 'opens deprecation popover' do + milestone = create(:milestone, project: project) + + visit group_milestone_path(group, milestone, title: milestone.title) + + expect(page).to have_selector('.milestone-deprecation-message') + + find('.milestone-deprecation-message .js-popover-link').click + + expect(page).to have_selector('.milestone-deprecation-message .popover') + end + end end diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js index 480c138b9db..2ab6a0077b5 100644 --- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js +++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js @@ -3,12 +3,11 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import { getSelector, - togglePopover, dismiss, - mouseleave, - mouseenter, inserted, } from '~/feature_highlight/feature_highlight_helper'; +import { togglePopover } from '~/shared/popover'; + import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper'; describe('feature highlight helper', () => { @@ -19,110 +18,6 @@ describe('feature highlight helper', () => { }); }); - describe('togglePopover', () => { - describe('togglePopover(true)', () => { - it('returns true when popover is shown', () => { - const context = { - hasClass: () => false, - popover: () => {}, - toggleClass: () => {}, - }; - - expect(togglePopover.call(context, true)).toEqual(true); - }); - - it('returns false when popover is already shown', () => { - const context = { - hasClass: () => true, - }; - - expect(togglePopover.call(context, true)).toEqual(false); - }); - - it('shows popover', (done) => { - const context = { - hasClass: () => false, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake((method) => { - expect(method).toEqual('show'); - done(); - }); - - togglePopover.call(context, true); - }); - - it('adds disable-animation and js-popover-show class', (done) => { - const context = { - hasClass: () => false, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'toggleClass').and.callFake((classNames, show) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - expect(show).toEqual(true); - done(); - }); - - togglePopover.call(context, true); - }); - }); - - describe('togglePopover(false)', () => { - it('returns true when popover is hidden', () => { - const context = { - hasClass: () => true, - popover: () => {}, - toggleClass: () => {}, - }; - - expect(togglePopover.call(context, false)).toEqual(true); - }); - - it('returns false when popover is already hidden', () => { - const context = { - hasClass: () => false, - }; - - expect(togglePopover.call(context, false)).toEqual(false); - }); - - it('hides popover', (done) => { - const context = { - hasClass: () => true, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'popover').and.callFake((method) => { - expect(method).toEqual('hide'); - done(); - }); - - togglePopover.call(context, false); - }); - - it('removes disable-animation and js-popover-show class', (done) => { - const context = { - hasClass: () => true, - popover: () => {}, - toggleClass: () => {}, - }; - - spyOn(context, 'toggleClass').and.callFake((classNames, show) => { - expect(classNames).toEqual('disable-animation js-popover-show'); - expect(show).toEqual(false); - done(); - }); - - togglePopover.call(context, false); - }); - }); - }); - describe('dismiss', () => { let mock; const context = { @@ -163,56 +58,6 @@ describe('feature highlight helper', () => { }); }); - describe('mouseleave', () => { - it('calls hide popover if .popover:hover is false', () => { - const fakeJquery = { - length: 0, - }; - - spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); - spyOn(togglePopover, 'call'); - mouseleave(); - expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false); - }); - - it('does not call hide popover if .popover:hover is true', () => { - const fakeJquery = { - length: 1, - }; - - spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); - spyOn(togglePopover, 'call'); - mouseleave(); - expect(togglePopover.call).not.toHaveBeenCalledWith(false); - }); - }); - - describe('mouseenter', () => { - const context = {}; - - it('shows popover', () => { - spyOn(togglePopover, 'call').and.returnValue(false); - mouseenter.call(context); - expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true); - }); - - it('registers mouseleave event if popover is showed', (done) => { - spyOn(togglePopover, 'call').and.returnValue(true); - spyOn($.fn, 'on').and.callFake((eventName) => { - expect(eventName).toEqual('mouseleave'); - done(); - }); - mouseenter.call(context); - }); - - it('does not register mouseleave event if popover is not showed', () => { - spyOn(togglePopover, 'call').and.returnValue(false); - const spy = spyOn($.fn, 'on').and.callFake(() => {}); - mouseenter.call(context); - expect(spy).not.toHaveBeenCalled(); - }); - }); - describe('inserted', () => { it('registers click event callback', (done) => { const context = { diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js index d2dd39d49d1..ec46d4f905a 100644 --- a/spec/javascripts/feature_highlight/feature_highlight_spec.js +++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js @@ -1,6 +1,6 @@ import $ from 'jquery'; -import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; import * as featureHighlight from '~/feature_highlight/feature_highlight'; +import * as popover from '~/shared/popover'; import axios from '~/lib/utils/axios_utils'; import MockAdapter from 'axios-mock-adapter'; @@ -29,7 +29,6 @@ describe('feature highlight', () => { mock = new MockAdapter(axios); mock.onGet('/test').reply(200); spyOn(window, 'addEventListener'); - spyOn(window, 'removeEventListener'); featureHighlight.setupFeatureHighlightPopover('test', 0); }); @@ -45,14 +44,14 @@ describe('feature highlight', () => { }); it('setup mouseenter', () => { - const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + const toggleSpy = spyOn(popover.togglePopover, 'call'); $(selector).trigger('mouseenter'); expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true); }); it('setup debounced mouseleave', (done) => { - const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + const toggleSpy = spyOn(popover.togglePopover, 'call'); $(selector).trigger('mouseleave'); // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce @@ -64,12 +63,7 @@ describe('feature highlight', () => { it('setup show.bs.popover', () => { $(selector).trigger('show.bs.popover'); - expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); - }); - - it('setup hide.bs.popover', () => { - $(selector).trigger('hide.bs.popover'); - expect(window.removeEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function)); + expect(window.addEventListener).toHaveBeenCalledWith('scroll', jasmine.any(Function), { once: true }); }); it('removes disabled attribute', () => { @@ -85,7 +79,7 @@ describe('feature highlight', () => { it('toggles when clicked', () => { $(selector).trigger('mouseenter'); const popoverId = $(selector).attr('aria-describedby'); - const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + const toggleSpy = spyOn(popover.togglePopover, 'call'); $(`#${popoverId} .dismiss-feature-highlight`).click(); diff --git a/spec/javascripts/shared/popover_spec.js b/spec/javascripts/shared/popover_spec.js new file mode 100644 index 00000000000..1d574c9424b --- /dev/null +++ b/spec/javascripts/shared/popover_spec.js @@ -0,0 +1,162 @@ +import $ from 'jquery'; +import { + togglePopover, + mouseleave, + mouseenter, +} from '~/shared/popover'; + +describe('popover', () => { + describe('togglePopover', () => { + describe('togglePopover(true)', () => { + it('returns true when popover is shown', () => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + expect(togglePopover.call(context, true)).toEqual(true); + }); + + it('returns false when popover is already shown', () => { + const context = { + hasClass: () => true, + }; + + expect(togglePopover.call(context, true)).toEqual(false); + }); + + it('shows popover', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('show'); + done(); + }); + + togglePopover.call(context, true); + }); + + it('adds disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => false, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'toggleClass').and.callFake((classNames, show) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + expect(show).toEqual(true); + done(); + }); + + togglePopover.call(context, true); + }); + }); + + describe('togglePopover(false)', () => { + it('returns true when popover is hidden', () => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + expect(togglePopover.call(context, false)).toEqual(true); + }); + + it('returns false when popover is already hidden', () => { + const context = { + hasClass: () => false, + }; + + expect(togglePopover.call(context, false)).toEqual(false); + }); + + it('hides popover', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'popover').and.callFake((method) => { + expect(method).toEqual('hide'); + done(); + }); + + togglePopover.call(context, false); + }); + + it('removes disable-animation and js-popover-show class', (done) => { + const context = { + hasClass: () => true, + popover: () => {}, + toggleClass: () => {}, + }; + + spyOn(context, 'toggleClass').and.callFake((classNames, show) => { + expect(classNames).toEqual('disable-animation js-popover-show'); + expect(show).toEqual(false); + done(); + }); + + togglePopover.call(context, false); + }); + }); + }); + + describe('mouseleave', () => { + it('calls hide popover if .popover:hover is false', () => { + const fakeJquery = { + length: 0, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(togglePopover, 'call'); + mouseleave(); + expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), false); + }); + + it('does not call hide popover if .popover:hover is true', () => { + const fakeJquery = { + length: 1, + }; + + spyOn($.fn, 'init').and.callFake(selector => (selector === '.popover:hover' ? fakeJquery : $.fn)); + spyOn(togglePopover, 'call'); + mouseleave(); + expect(togglePopover.call).not.toHaveBeenCalledWith(false); + }); + }); + + describe('mouseenter', () => { + const context = {}; + + it('shows popover', () => { + spyOn(togglePopover, 'call').and.returnValue(false); + mouseenter.call(context); + expect(togglePopover.call).toHaveBeenCalledWith(jasmine.any(Object), true); + }); + + it('registers mouseleave event if popover is showed', (done) => { + spyOn(togglePopover, 'call').and.returnValue(true); + spyOn($.fn, 'on').and.callFake((eventName) => { + expect(eventName).toEqual('mouseleave'); + done(); + }); + mouseenter.call(context); + }); + + it('does not register mouseleave event if popover is not showed', () => { + spyOn(togglePopover, 'call').and.returnValue(false); + const spy = spyOn($.fn, 'on').and.callFake(() => {}); + mouseenter.call(context); + expect(spy).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/views/shared/milestones/_top.html.haml.rb b/spec/views/shared/milestones/_top.html.haml.rb new file mode 100644 index 00000000000..516d81c87ac --- /dev/null +++ b/spec/views/shared/milestones/_top.html.haml.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'shared/milestones/_top.html.haml' do + set(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let(:milestone) { create(:milestone, project: project) } + + before do + allow(milestone).to receive(:milestones) { [] } + end + + it 'renders a deprecation message for a legacy milestone' do + allow(milestone).to receive(:legacy_group_milestone?) { true } + + render 'shared/milestones/top', milestone: milestone + + expect(rendered).to have_css('.milestone-deprecation-message') + end + + it 'renders a deprecation message for a dashboard milestone' do + allow(milestone).to receive(:dashboard_milestone?) { true } + + render 'shared/milestones/top', milestone: milestone + + expect(rendered).to have_css('.milestone-deprecation-message') + end + + it 'does not render a deprecation message for a non-legacy and non-dashboard milestone' do + assign :group, group + + render 'shared/milestones/top', milestone: milestone + + expect(rendered).not_to have_css('.milestone-deprecation-message') + end +end |