From 2fb167fa3bc1aa3d46f4edc551d1b37a9c038cac Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 10 Jan 2018 22:16:27 -0600 Subject: [PATCH] Restore feature_highlight code From https://gitlab.com/gitlab-org/gitlab-ce/issues/36760 Was reverted in https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14373 --- app/assets/images/icons.json | 2 +- app/assets/images/icons.svg | 2 +- .../images/illustrations/cluster_popover.svg | 1 + .../feature_highlight/feature_highlight.js | 65 +++++ .../feature_highlight_helper.js | 59 +++++ .../feature_highlight_options.js | 12 + app/assets/javascripts/main.js | 1 + app/assets/stylesheets/framework.scss | 1 + .../framework/feature_highlight.scss | 103 ++++++++ app/helpers/user_callouts_helper.rb | 2 +- .../layouts/nav/sidebar/_project.html.haml | 24 ++ ...672-emphasize-gke-cluster-to-new-users.yml | 5 + doc/user/feature_highlight.md | 15 ++ doc/user/img/feature_highlight_example.png | Bin 0 -> 27262 bytes doc/user/index.md | 2 + package.json | 2 +- .../feature_highlight_helper_spec.js | 231 ++++++++++++++++++ .../feature_highlight_options_spec.js | 30 +++ .../feature_highlight_spec.js | 131 ++++++++++ yarn.lock | 6 +- 20 files changed, 687 insertions(+), 7 deletions(-) create mode 100644 app/assets/images/illustrations/cluster_popover.svg create mode 100644 app/assets/javascripts/feature_highlight/feature_highlight.js create mode 100644 app/assets/javascripts/feature_highlight/feature_highlight_helper.js create mode 100644 app/assets/javascripts/feature_highlight/feature_highlight_options.js create mode 100644 app/assets/stylesheets/framework/feature_highlight.scss create mode 100644 changelogs/unreleased/41672-emphasize-gke-cluster-to-new-users.yml create mode 100644 doc/user/feature_highlight.md create mode 100644 doc/user/img/feature_highlight_example.png create mode 100644 spec/javascripts/feature_highlight/feature_highlight_helper_spec.js create mode 100644 spec/javascripts/feature_highlight/feature_highlight_options_spec.js create mode 100644 spec/javascripts/feature_highlight/feature_highlight_spec.js diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json index 132a373baec..19843d24e22 100644 --- a/app/assets/images/icons.json +++ b/app/assets/images/icons.json @@ -1 +1 @@ -{"iconCount":189,"spriteSize":85900,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]} \ No newline at end of file +{"iconCount":191,"spriteSize":86607,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","soft-unwrap","soft-wrap","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]} \ No newline at end of file diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index 09efe331f93..6aec54d0543 100644 --- a/app/assets/images/icons.svg +++ b/app/assets/images/icons.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/illustrations/cluster_popover.svg b/app/assets/images/illustrations/cluster_popover.svg new file mode 100644 index 00000000000..202231373f1 --- /dev/null +++ b/app/assets/images/illustrations/cluster_popover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/javascripts/feature_highlight/feature_highlight.js b/app/assets/javascripts/feature_highlight/feature_highlight.js new file mode 100644 index 00000000000..d65cc6d5d7d --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight.js @@ -0,0 +1,65 @@ +import _ from 'underscore'; +import { + getSelector, + togglePopover, + inserted, + mouseenter, + mouseleave, +} from './feature_highlight_helper'; + +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 + .data('content', $popoverContent.prop('outerHTML')) + .popover({ + html: true, + // Override the existing template to add custom CSS classes + template: ` + + `, + }) + .on('mouseenter', mouseenter) + .on('mouseleave', debouncedMouseleave) + .on('inserted.bs.popover', inserted) + .on('show.bs.popover', () => { + window.addEventListener('scroll', hideOnScroll); + }) + .on('hide.bs.popover', () => { + window.removeEventListener('scroll', hideOnScroll); + }) + // Display feature highlight + .removeAttr('disabled'); +} + +export function findHighestPriorityFeature() { + let priorityFeature; + + const sortedFeatureEls = [].slice.call(document.querySelectorAll('.js-feature-highlight')).sort((a, b) => + (a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0)); + + const [priorityFeatureEl] = sortedFeatureEls; + if (priorityFeatureEl) { + priorityFeature = priorityFeatureEl.dataset.highlight; + } + + return priorityFeature; +} + +export function highlightFeatures() { + const priorityFeature = findHighestPriorityFeature(); + + if (priorityFeature) { + setupFeatureHighlightPopover(priorityFeature); + } + + return priorityFeature; +} diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js new file mode 100644 index 00000000000..939d12237f3 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js @@ -0,0 +1,59 @@ +import axios from '../lib/utils/axios_utils'; +import { __ } from '../locale'; +import Flash from '../flash'; +import LazyLoader from '../lazy_loader'; + +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, + }) + .catch(() => Flash(__('An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.'))); + + togglePopover.call(this, false); + 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; + const $popover = $(this); + const dismissWrapper = dismiss.bind($popover, highlightId); + + $(`#${popoverId} .dismiss-feature-highlight`) + .on('click', dismissWrapper); + + const lazyImg = $(`#${popoverId} .feature-highlight-illustration`)[0]; + if (lazyImg) { + LazyLoader.loadImage(lazyImg); + } +} diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_options.js b/app/assets/javascripts/feature_highlight/feature_highlight_options.js new file mode 100644 index 00000000000..212643b1e04 --- /dev/null +++ b/app/assets/javascripts/feature_highlight/feature_highlight_options.js @@ -0,0 +1,12 @@ +import { highlightFeatures } from './feature_highlight'; +import bp from '../breakpoints'; + +export default function domContentLoaded() { + if (bp.getBreakpointSize() === 'lg') { + highlightFeatures(); + return true; + } + return false; +} + +document.addEventListener('DOMContentLoaded', domContentLoaded); diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 39445a85c77..b99cb257ce3 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -26,6 +26,7 @@ import './gl_dropdown'; import initTodoToggle from './header'; import initImporterStatus from './importer_status'; import initLayoutNav from './layout_nav'; +import './feature_highlight/feature_highlight_options'; import LazyLoader from './lazy_loader'; import initLogoAnimation from './logo'; import './milestone_select'; diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss index c4aad24e9c1..887879ab715 100644 --- a/app/assets/stylesheets/framework.scss +++ b/app/assets/stylesheets/framework.scss @@ -61,3 +61,4 @@ @import "framework/responsive_tables"; @import "framework/stacked-progress-bar"; @import "framework/ci_variable_list"; +@import "framework/feature_highlight"; diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss new file mode 100644 index 00000000000..4f26cd015e4 --- /dev/null +++ b/app/assets/stylesheets/framework/feature_highlight.scss @@ -0,0 +1,103 @@ +.feature-highlight { + position: relative; + margin-left: $gl-padding; + width: 20px; + height: 20px; + cursor: pointer; + + &::before { + content: ''; + display: block; + position: absolute; + top: 6px; + left: 6px; + width: 8px; + height: 8px; + background-color: $blue-500; + border-radius: 50%; + box-shadow: 0 0 0 rgba($blue-500, 0.4); + animation: pulse-highlight 2s infinite; + } + + &:hover::before, + &.disable-animation::before { + animation: none; + } + + &[disabled]::before { + display: none; + } +} + +.is-showing-fly-out { + .feature-highlight { + display: none; + } +} + +.feature-highlight-popover-content { + display: none; + + hr { + margin: $gl-padding * 0.5 0; + } + + .btn-link { + svg { + @include btn-svg; + + path { + fill: currentColor; + } + } + } + + .feature-highlight-illustration { + width: 100%; + height: 100px; + padding-top: 12px; + padding-bottom: 12px; + + background-color: $indigo-50; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + border-bottom: 1px solid darken($gray-normal, 8%); + } +} + +.popover .feature-highlight-popover-content { + display: block; +} + +.feature-highlight-popover { + width: 240px; + padding: 0; + border: 1px solid $dropdown-border-color; + box-shadow: 0 2px 4px $dropdown-shadow-color; + + &.right > .arrow { + border-right-color: $dropdown-border-color; + } + + .popover-content { + padding: 0; + } +} + +.feature-highlight-popover-sub-content { + padding: 9px 14px; +} + +@include keyframes(pulse-highlight) { + 0% { + box-shadow: 0 0 0 0 rgba($blue-200, 0.4); + } + + 70% { + box-shadow: 0 0 0 10px transparent; + } + + 100% { + box-shadow: 0 0 0 0 transparent; + } +} diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb index 6368e248c6e..36abfaf19a5 100644 --- a/app/helpers/user_callouts_helper.rb +++ b/app/helpers/user_callouts_helper.rb @@ -9,6 +9,6 @@ module UserCalloutsHelper private def user_dismissed?(feature_name) - current_user&.callouts&.find_by(feature_name: feature_name) + current_user&.callouts&.find_by(feature_name: UserCallout.feature_names[feature_name]) end end diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml index abd07d71bcc..e20e58d27e1 100644 --- a/app/views/layouts/nav/sidebar/_project.html.haml +++ b/app/views/layouts/nav/sidebar/_project.html.haml @@ -184,10 +184,34 @@ Environments - if project_nav_tab? :clusters + - show_cluster_hint = show_gke_cluster_integration_callout?(@project) = nav_link(controller: [:clusters, :user, :gcp]) do = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do %span Clusters + - if show_cluster_hint + .feature-highlight.js-feature-highlight{ disabled: true, + data: { trigger: 'manual', + container: 'body', + toggle: 'popover', + placement: 'right', + highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION, + highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION], + dismiss_endpoint: user_callouts_path } } + - if show_cluster_hint + .feature-highlight-popover-content + = image_tag 'illustrations/cluster_popover.svg', class: 'feature-highlight-illustration' + .feature-highlight-popover-sub-content + %p= _('Allows you to add and manage Kubernetes clusters.') + %p + = _('Protip:') + = link_to 'Auto DevOps', help_page_path('topics/autodevops/index.md') + %span= _('uses clusters to deploy your code!') + %hr + %button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' } + %span= _("Got it!") + = sprite_icon('thumb-up') + - if @project.feature_available?(:builds, current_user) && !@project.empty_repo? = nav_link(path: 'pipelines#charts') do diff --git a/changelogs/unreleased/41672-emphasize-gke-cluster-to-new-users.yml b/changelogs/unreleased/41672-emphasize-gke-cluster-to-new-users.yml new file mode 100644 index 00000000000..6b0d443e097 --- /dev/null +++ b/changelogs/unreleased/41672-emphasize-gke-cluster-to-new-users.yml @@ -0,0 +1,5 @@ +--- +title: Add blue dot feature highlight to make GKE Clusters more visible to users +merge_request: 16379 +author: +type: added diff --git a/doc/user/feature_highlight.md b/doc/user/feature_highlight.md new file mode 100644 index 00000000000..bd98ea00757 --- /dev/null +++ b/doc/user/feature_highlight.md @@ -0,0 +1,15 @@ +# Feature highlight + +> [Introduced][ce-16379] in GitLab 10.5 + +Feature highlights are represented by a pulsing blue dot. Hovering over the dot +will open up callout with more information. +They are used to emphasize a certain feature and make something more visible to the user. + +You can dismiss any feature highlight permanently by clicking the "Got it" link +at the bottom of the callout. There isn't a way to restore the feature highlight +after it has been dismissed. + +![Clusters feature highlight](img/feature_highlight_example.png) + +[ce-16379]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/16379 diff --git a/doc/user/img/feature_highlight_example.png b/doc/user/img/feature_highlight_example.png new file mode 100644 index 0000000000000000000000000000000000000000..32ca05a608734bbd7e8dd4f9c8ec110ee822a885 GIT binary patch literal 27262 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 { + describe('getSelector', () => { + it('returns js-feature-highlight selector', () => { + const highlightId = 'highlightId'; + expect(getSelector(highlightId)).toEqual(`.js-feature-highlight[data-highlight=${highlightId}]`); + }); + }); + + 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 = { + hide: () => {}, + attr: () => '/-/callouts/dismiss', + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + + spyOn(togglePopover, 'call').and.callFake(() => {}); + spyOn(context, 'hide').and.callFake(() => {}); + dismiss.call(context); + }); + + afterEach(() => { + mock.restore(); + }); + + it('calls persistent dismissal endpoint', (done) => { + const spy = jasmine.createSpy('dismiss-endpoint-hit'); + mock.onPost('/-/callouts/dismiss').reply(spy); + + getSetTimeoutPromise() + .then(() => { + expect(spy).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); + + it('calls hide popover', () => { + expect(togglePopover.call).toHaveBeenCalledWith(context, false); + }); + + it('calls hide', () => { + expect(context.hide).toHaveBeenCalled(); + }); + }); + + 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 = { + getAttribute: () => 'popoverId', + dataset: { + highlight: 'some-feature', + }, + }; + + spyOn($.fn, 'on').and.callFake((event) => { + expect(event).toEqual('click'); + done(); + }); + inserted.call(context); + }); + }); +}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_options_spec.js b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js new file mode 100644 index 00000000000..7f9425d8abe --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_options_spec.js @@ -0,0 +1,30 @@ +import domContentLoaded from '~/feature_highlight/feature_highlight_options'; +import bp from '~/breakpoints'; + +describe('feature highlight options', () => { + describe('domContentLoaded', () => { + it('should not call highlightFeatures when breakpoint is xs', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('xs'); + + expect(domContentLoaded()).toBe(false); + }); + + it('should not call highlightFeatures when breakpoint is sm', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('sm'); + + expect(domContentLoaded()).toBe(false); + }); + + it('should not call highlightFeatures when breakpoint is md', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('md'); + + expect(domContentLoaded()).toBe(false); + }); + + it('should call highlightFeatures when breakpoint is lg', () => { + spyOn(bp, 'getBreakpointSize').and.returnValue('lg'); + + expect(domContentLoaded()).toBe(true); + }); + }); +}); diff --git a/spec/javascripts/feature_highlight/feature_highlight_spec.js b/spec/javascripts/feature_highlight/feature_highlight_spec.js new file mode 100644 index 00000000000..6e1b0429ab7 --- /dev/null +++ b/spec/javascripts/feature_highlight/feature_highlight_spec.js @@ -0,0 +1,131 @@ +import * as featureHighlightHelper from '~/feature_highlight/feature_highlight_helper'; +import * as featureHighlight from '~/feature_highlight/feature_highlight'; + +describe('feature highlight', () => { + beforeEach(() => { + setFixtures(` +
+
+ Trigger +
+
+
+ Content +
+ Dismiss +
+
+ `); + }); + + describe('setupFeatureHighlightPopover', () => { + const selector = '.js-feature-highlight[data-highlight=test]'; + beforeEach(() => { + spyOn(window, 'addEventListener'); + spyOn(window, 'removeEventListener'); + featureHighlight.setupFeatureHighlightPopover('test', 0); + }); + + it('setup popover content', () => { + const $popoverContent = $('.feature-highlight-popover-content'); + const outerHTML = $popoverContent.prop('outerHTML'); + + expect($(selector).data('content')).toEqual(outerHTML); + }); + + it('setup mouseenter', () => { + const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + $(selector).trigger('mouseenter'); + + expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), true); + }); + + it('setup debounced mouseleave', (done) => { + const toggleSpy = spyOn(featureHighlightHelper.togglePopover, 'call'); + $(selector).trigger('mouseleave'); + + // Even though we've set the debounce to 0ms, setTimeout is needed for the debounce + setTimeout(() => { + expect(toggleSpy).toHaveBeenCalledWith(jasmine.any(Object), false); + done(); + }, 0); + }); + + it('setup inserted.bs.popover', () => { + $(selector).trigger('mouseenter'); + const popoverId = $(selector).attr('aria-describedby'); + const spyEvent = spyOnEvent(`#${popoverId} .dismiss-feature-highlight`, 'click'); + + $(`#${popoverId} .dismiss-feature-highlight`).click(); + expect(spyEvent).toHaveBeenTriggered(); + }); + + 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)); + }); + + it('removes disabled attribute', () => { + expect($('.js-feature-highlight').is(':disabled')).toEqual(false); + }); + + it('displays popover', () => { + expect($(selector).attr('aria-describedby')).toBeFalsy(); + $(selector).trigger('mouseenter'); + expect($(selector).attr('aria-describedby')).toBeTruthy(); + }); + }); + + describe('findHighestPriorityFeature', () => { + beforeEach(() => { + setFixtures(` +
+
+
+ `); + }); + + it('should pick the highest priority feature highlight', () => { + setFixtures(` +
+
+
+ `); + + expect($('.js-feature-highlight').length).toBeGreaterThan(1); + expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority'); + }); + + it('should work when no priority is set', () => { + setFixtures(` +
+ `); + + expect(featureHighlight.findHighestPriorityFeature()).toEqual('test'); + }); + + it('should pick the highest priority feature highlight when some have no priority set', () => { + setFixtures(` +
+
+
+
+
+ `); + + expect($('.js-feature-highlight').length).toBeGreaterThan(1); + expect(featureHighlight.findHighestPriorityFeature()).toEqual('test-high-priority'); + }); + }); + + describe('highlightFeatures', () => { + it('calls setupFeatureHighlightPopover', () => { + expect(featureHighlight.highlightFeatures()).toEqual('test'); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index e6d5f239d83..bc5c19464fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -54,9 +54,9 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" -"@gitlab-org/gitlab-svgs@^1.7.0": - version "1.7.0" - resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.7.0.tgz#dbb1330a1b1ee478378dddab53fe1a881e810f5d" +"@gitlab-org/gitlab-svgs@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.8.0.tgz#95d6afa94395860699ddad60a82bd1bbbc2ba89f" "@types/jquery@^2.0.40": version "2.0.48"