diff options
27 files changed, 513 insertions, 78 deletions
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 710207db0c7..4699ef5a51c 100644 --- a/app/assets/javascripts/boards/components/issue_card_inner.js +++ b/app/assets/javascripts/boards/components/issue_card_inner.js @@ -1,4 +1,5 @@ import Vue from 'vue'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import eventHub from '../eventhub'; const Store = gl.issueBoards.BoardsStore; @@ -38,6 +39,9 @@ gl.issueBoards.IssueCardInner = Vue.extend({ maxCounter: 99, }; }, + components: { + userAvatarLink, + }, computed: { numberOverLimit() { return this.issue.assignees.length - this.limitBeforeCounter; @@ -146,23 +150,16 @@ gl.issueBoards.IssueCardInner = Vue.extend({ </span> </h4> <div class="card-assignee"> - <a - class="has-tooltip js-no-trigger" - :href="assigneeUrl(assignee)" - :title="assigneeUrlTitle(assignee)" + <user-avatar-link v-for="(assignee, index) in issue.assignees" v-if="shouldRenderAssignee(index)" - data-container="body" - data-placement="bottom" - > - <img - class="avatar avatar-inline s20" - :src="assignee.avatar" - width="20" - height="20" - :alt="avatarUrlTitle(assignee)" - /> - </a> + class="js-no-trigger" + :link-href="assigneeUrl(assignee)" + :img-alt="avatarUrlTitle(assignee)" + :img-src="assignee.avatar" + :tooltip-text="assigneeUrlTitle(assignee)" + tooltip-placement="bottom" + /> <span class="avatar-counter has-tooltip" :title="assigneeCounterTooltip" diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js index 0d9ad197abf..eeb61826ace 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js @@ -1,6 +1,7 @@ /* eslint-disable no-param-reassign */ import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; @@ -10,6 +11,9 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ items: Array, stage: Object, }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -19,7 +23,8 @@ global.cycleAnalytics.StageCodeComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="mergeRequest in items" class="stage-event-item"> <div class="item-details"> - <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> <h5 class="item-title merge-merquest-title"> <a :href="mergeRequest.url"> {{ mergeRequest.title }} diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js index ad285874643..09fb390787d 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ - import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; @@ -10,6 +10,9 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ items: Array, stage: Object, }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -19,7 +22,8 @@ global.cycleAnalytics.StageIssueComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="issue in items" class="stage-event-item"> <div class="item-details"> - <img class="avatar" :src="issue.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="issue.author.avatarUrl"/> <h5 class="item-title issue-title"> <a class="issue-title" :href="issue.url"> {{ issue.title }} diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js index dec1704395e..cd7a94b67c1 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import iconCommit from '../svg/icon_commit.svg'; const global = window.gl || (window.gl = {}); @@ -10,11 +11,12 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ items: Array, stage: Object, }, - + components: { + userAvatarImage, + }, data() { return { iconCommit }; }, - template: ` <div> <div class="events-description"> @@ -24,7 +26,8 @@ global.cycleAnalytics.StagePlanComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="commit in items" class="stage-event-item"> <div class="item-details item-conmmit-component"> - <img class="avatar" :src="commit.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="commit.author.avatarUrl"/> <h5 class="item-title commit-title"> <a :href="commit.commitUrl"> {{ commit.title }} diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js index a14ebc3ece9..bdf86b4ff3c 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ - import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; @@ -10,6 +10,9 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ items: Array, stage: Object, }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -19,7 +22,8 @@ global.cycleAnalytics.StageProductionComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="issue in items" class="stage-event-item"> <div class="item-details"> - <img class="avatar" :src="issue.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="issue.author.avatarUrl"/> <h5 class="item-title issue-title"> <a class="issue-title" :href="issue.url"> {{ issue.title }} diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js index 1a5bf9bc0b5..cfb7a4ab576 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js @@ -1,6 +1,6 @@ /* eslint-disable no-param-reassign */ - import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const global = window.gl || (window.gl = {}); global.cycleAnalytics = global.cycleAnalytics || {}; @@ -10,6 +10,9 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ items: Array, stage: Object, }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -19,7 +22,8 @@ global.cycleAnalytics.StageReviewComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="mergeRequest in items" class="stage-event-item"> <div class="item-details"> - <img class="avatar" :src="mergeRequest.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="mergeRequest.author.avatarUrl"/> <h5 class="item-title merge-merquest-title"> <a :href="mergeRequest.url"> {{ mergeRequest.title }} diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js index 1f7c673b1d4..97a849c4feb 100644 --- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js +++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js @@ -1,5 +1,6 @@ /* eslint-disable no-param-reassign */ import Vue from 'vue'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; import iconBranch from '../svg/icon_branch.svg'; const global = window.gl || (window.gl = {}); @@ -13,6 +14,9 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ data() { return { iconBranch }; }, + components: { + userAvatarImage, + }, template: ` <div> <div class="events-description"> @@ -22,7 +26,8 @@ global.cycleAnalytics.StageStagingComponent = Vue.extend({ <ul class="stage-event-list"> <li v-for="build in items" class="stage-event-item item-build-component"> <div class="item-details"> - <img class="avatar" :src="build.author.avatarUrl"> + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image :img-src="build.author.avatarUrl"/> <h5 class="item-title"> <a :href="build.url" class="pipeline-id">#{{ build.id }}</a> <i class="fa fa-code-fork"></i> diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js index 5f533b5761c..517bdb6be09 100644 --- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js +++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import collapseIcon from '../icons/collapse_icon.svg'; +import userAvatarImage from '../../vue_shared/components/user_avatar/user_avatar_image.vue'; const DiffNoteAvatars = Vue.extend({ props: ['discussionId'], @@ -15,22 +16,24 @@ const DiffNoteAvatars = Vue.extend({ collapseIcon, }; }, + components: { + userAvatarImage, + }, template: ` <div class="diff-comment-avatar-holders" v-show="notesCount !== 0"> <div v-if="!isVisible"> - <img v-for="note in notesSubset" - class="avatar diff-comment-avatar has-tooltip js-diff-comment-avatar" - width="19" - height="19" - role="button" - data-container="body" - data-placement="top" - data-html="true" + <!-- FIXME: Pass an alt attribute here for accessibility --> + <user-avatar-image + v-for="note in notesSubset" + class="diff-comment-avatar js-diff-comment-avatar" + @click.native="clickedAvatar($event)" + :img-src="note.authorAvatar" + :tooltip-text="getTooltipText(note)" :data-line-type="lineType" - :title="note.authorName + ': ' + note.noteTruncated" - :src="note.authorAvatar" - @click="clickedAvatar($event)" /> + :size="19" + data-html="true" + /> <span v-if="notesCount > shownAvatars" class="diff-comments-more-count has-tooltip js-diff-comment-avatar" data-container="body" @@ -150,6 +153,9 @@ const DiffNoteAvatars = Vue.extend({ setDiscussionVisible() { this.isVisible = $(`.diffs .notes[data-discussion-id="${this.discussion.id}"]`).is(':visible'); }, + getTooltipText(note) { + return `${note.authorName}: ${note.noteTruncated}`; + }, }, }); diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue index 1f01629aa1b..012ff1f975b 100644 --- a/app/assets/javascripts/environments/components/environment_item.vue +++ b/app/assets/javascripts/environments/components/environment_item.vue @@ -1,6 +1,7 @@ <script> import Timeago from 'timeago.js'; import _ from 'underscore'; +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; import '../../lib/utils/text_utility'; import ActionsComponent from './environment_actions.vue'; import ExternalUrlComponent from './environment_external_url.vue'; @@ -20,6 +21,7 @@ const timeagoInstance = new Timeago(); export default { components: { + userAvatarLink, 'commit-component': CommitComponent, 'actions-component': ActionsComponent, 'external-url-component': ExternalUrlComponent, @@ -468,15 +470,13 @@ export default { <span v-if="!model.isFolder && deploymentHasUser"> by - <a - :href="deploymentUser.web_url" - class="js-deploy-user-container"> - <img - class="avatar has-tooltip s20" - :src="deploymentUser.avatar_url" - :alt="userImageAltDescription" - :title="deploymentUser.username" /> - </a> + <user-avatar-link + class="js-deploy-user-container" + :link-href="deploymentUser.web_url" + :img-src="deploymentUser.avatar_url" + :img-alt="userImageAltDescription" + :tooltip-text="deploymentUser.username" + /> </span> </td> diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.js b/app/assets/javascripts/pipelines/components/pipeline_url.js index ea8aaca6c9c..7cd2e0f9366 100644 --- a/app/assets/javascripts/pipelines/components/pipeline_url.js +++ b/app/assets/javascripts/pipelines/components/pipeline_url.js @@ -1,3 +1,5 @@ +import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + export default { props: [ 'pipeline', @@ -7,6 +9,9 @@ export default { return !!this.pipeline.user; }, }, + components: { + userAvatarLink, + }, template: ` <td> <a @@ -15,18 +20,13 @@ export default { <span class="pipeline-id">#{{pipeline.id}}</span> </a> <span>by</span> - <a - class="js-pipeline-url-user" + <user-avatar-link v-if="user" - :href="pipeline.user.web_url"> - <img - v-if="user" - class="avatar has-tooltip s20 " - :title="pipeline.user.name" - data-container="body" - :src="pipeline.user.avatar_url" - > - </a> + class="js-pipeline-url-user" + :link-href="pipeline.user.web_url" + :img-src="pipeline.user.avatar_url" + :tooltip-text="pipeline.user.name" + /> <span v-if="!user" class="js-pipeline-url-api api"> diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js index 9b060a0a35f..23bc5fbc034 100644 --- a/app/assets/javascripts/vue_shared/components/commit.js +++ b/app/assets/javascripts/vue_shared/components/commit.js @@ -1,4 +1,5 @@ import commitIconSvg from 'icons/_icon_commit.svg'; +import userAvatarLink from './user_avatar/user_avatar_link.vue'; export default { props: { @@ -110,6 +111,9 @@ export default { return { commitIconSvg }; }, + components: { + userAvatarLink, + }, template: ` <div class="branch-commit"> @@ -133,16 +137,14 @@ export default { <p class="commit-title"> <span v-if="title"> - <a v-if="hasAuthor" + <user-avatar-link + v-if="hasAuthor" class="avatar-image-container" - :href="author.web_url"> - <img - class="avatar has-tooltip s20" - :src="author.avatar_url" - :alt="userImageAltDescription" - :title="author.username" /> - </a> - + :link-href="author.web_url" + :img-src="author.avatar_url" + :img-alt="userImageAltDescription" + :tooltip-text="author.username" + /> <a class="commit-row-message" :href="commitUrl"> {{title}} diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue new file mode 100644 index 00000000000..4891e7f927c --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -0,0 +1,79 @@ +<script> + +/* This is a re-usable vue component for rendering a user avatar that + does not need to link to the user's profile. The image and an optional + tooltip can be configured by props passed to this component. + + Sample configuration: + + <user-avatar-image + :img-src="userAvatarSrc" + :img-alt="tooltipText" + :tooltip-text="tooltipText" + tooltip-placement="top" + /> + +*/ + +import defaultAvatarUrl from 'images/no_avatar.png'; +import UserAvatarSizeMixin from './user_avatar_size_mixin'; +import TooltipMixin from '../../mixins/tooltip'; + +export default { + name: 'UserAvatarImage', + mixins: [UserAvatarSizeMixin, TooltipMixin], + props: { + imgSrc: { + type: String, + required: false, + default: defaultAvatarUrl, + }, + cssClasses: { + type: String, + required: false, + default: '', + }, + imgAlt: { + type: String, + required: false, + default: 'user avatar', + }, + size: { + type: Number, + required: false, + default: 20, + }, + tooltipText: { + type: String, + required: false, + default: '', + }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + }, + computed: { + tooltipContainer() { + return this.tooltipText ? 'body' : null; + }, + imgCssClasses() { + return `avatar ${this.avatarSizeClass} ${this.cssClasses}`; + }, + }, +}; +</script> + +<template> + <img + :class="imgCssClasses" + :src="imgSrc" + :style="avatarSizeStylesMap" + :alt="imgAlt" + :data-container="tooltipContainer" + :data-placement="tooltipPlacement" + :title="tooltipText" + ref="tooltip" + /> +</template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue new file mode 100644 index 00000000000..95898d54cf7 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_link.vue @@ -0,0 +1,80 @@ +<script> + +/* This is a re-usable vue component for rendering a user avatar wrapped in + a clickable link (likely to the user's profile). The link, image, and + tooltip can be configured by props passed to this component. + + Sample configuration: + + <user-avatar-link + :link-href="userProfileUrl" + :img-src="userAvatarSrc" + :img-alt="tooltipText" + :img-size="20" + :tooltip-text="tooltipText" + tooltip-placement="top" + /> + +*/ + +import userAvatarImage from './user_avatar_image.vue'; + +export default { + name: 'UserAvatarLink', + components: { + userAvatarImage, + }, + props: { + linkHref: { + type: String, + required: false, + default: '', + }, + imgSrc: { + type: String, + required: false, + default: '', + }, + imgAlt: { + type: String, + required: false, + default: '', + }, + imgCssClasses: { + type: String, + required: false, + default: '', + }, + imgSize: { + type: Number, + required: false, + default: 20, + }, + tooltipText: { + type: String, + required: false, + default: '', + }, + tooltipPlacement: { + type: String, + required: false, + default: 'top', + }, + }, +}; +</script> + +<template> + <a + class="user-avatar-link" + :href="linkHref"> + <user-avatar-image + :img-src="imgSrc" + :img-alt="imgAlt" + :css-classes="imgCssClasses" + :size="imgSize" + :tooltip-text="tooltipText" + :tooltip-placement="tooltipPlacement" + /> + </a> +</template> diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_size_mixin.js b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_size_mixin.js new file mode 100644 index 00000000000..b6155ffd28e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_size_mixin.js @@ -0,0 +1,13 @@ +export default { + computed: { + avatarSizeStylesMap() { + return { + width: `${this.size}px`, + height: `${this.size}px`, + }; + }, + avatarSizeClass() { + return `s${this.size}`; + }, + }, +}; diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue new file mode 100644 index 00000000000..39b4d37c91e --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue @@ -0,0 +1,42 @@ +<script> + +/* This is a re-usable vue component for rendering a user avatar svg (typically + for a blank state). It will receive styles comparable to the user avatar, + but no image is loaded, it isn't wrapped in a link, and tooltips aren't supported. + The svg and avatar size can be configured by props passed to this component. + + Sample configuration: + + <user-avatar-svg + :svg="potentialApproverSvg" + :size="20" + /> + +*/ + +import UserAvatarSizeMixin from './user_avatar_size_mixin'; + +export default { + mixins: [UserAvatarSizeMixin], + props: { + svg: { + type: String, + required: true, + }, + size: { + type: Number, + required: false, + default: 20, + }, + }, +}; +</script> + +<template> + <svg + :class="avatarSizeClass" + :style="avatarSizeStylesMap" + v-html="svg"> + </svg> +</template> + diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 1b7d4e42258..ef864e8f6a9 100644 --- a/app/assets/stylesheets/framework/icons.scss +++ b/app/assets/stylesheets/framework/icons.scss @@ -65,3 +65,7 @@ text-decoration: none; } } + +.user-avatar-link { + text-decoration: none; +} diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index a42ae7e55a5..48d3b7b1d07 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -68,10 +68,6 @@ margin: 0; } - .avatar-image-container { - text-decoration: none; - } - .icon-play { height: 13px; width: 12px; diff --git a/config/webpack.config.js b/config/webpack.config.js index 0781017c89f..7bc225968de 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -90,9 +90,9 @@ var config = { loader: 'raw-loader', }, { - test: /\.gif$/, + test: /\.(gif|png)$/, loader: 'url-loader', - query: { mimetype: 'image/gif' }, + options: { limit: 2048 }, }, { test: /\.(worker\.js|pdf|bmpr)$/, @@ -190,6 +190,7 @@ var config = { 'emojis': path.join(ROOT_PATH, 'fixtures/emojis'), 'empty_states': path.join(ROOT_PATH, 'app/views/shared/empty_states'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), + 'images': path.join(ROOT_PATH, 'app/assets/images'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vue$': 'vue/dist/vue.esm.js', } diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index b2e170513c4..ccf047d3efa 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -91,7 +91,7 @@ feature 'Diff note avatars', feature: true, js: true do page.within find("[id='#{position.line_code(project.repository)}']") do find('.diff-notes-collapse').click - expect(first('img.js-diff-comment-avatar')["title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") + expect(first('img.js-diff-comment-avatar')["data-original-title"]).to eq("#{note.author.name}: #{note.note.truncate(17)}") end end diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js index fddde799d01..bd9b4fbfdd3 100644 --- a/spec/javascripts/boards/issue_card_spec.js +++ b/spec/javascripts/boards/issue_card_spec.js @@ -129,7 +129,7 @@ describe('Issue card component', () => { it('sets title', () => { expect( - component.$el.querySelector('.card-assignee a').getAttribute('title'), + component.$el.querySelector('.card-assignee img').getAttribute('data-original-title'), ).toContain(`Assigned to ${user.name}`); }); diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js index 53931d67ad7..0bcc3905702 100644 --- a/spec/javascripts/pipelines/pipeline_url_spec.js +++ b/spec/javascripts/pipelines/pipeline_url_spec.js @@ -60,7 +60,7 @@ describe('Pipeline Url Component', () => { expect( component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'), ).toEqual(mockData.pipeline.user.web_url); - expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name); + expect(image.getAttribute('data-original-title')).toEqual(mockData.pipeline.user.name); expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url); }); diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js index 242010ba688..0638483e7aa 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/javascripts/vue_shared/components/commit_spec.js @@ -86,7 +86,7 @@ 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('title'), + component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('data-original-title'), ).toContain(props.author.username); expect( component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt'), diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js index 14280751053..286118917e8 100644 --- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js +++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js @@ -79,7 +79,7 @@ describe('Pipelines Table Row', () => { ).toEqual(pipeline.user.web_url); expect( - component.$el.querySelector('td:nth-child(2) img').getAttribute('title'), + component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'), ).toEqual(pipeline.user.name); }); }); @@ -102,7 +102,7 @@ describe('Pipelines Table Row', () => { } const commitAuthorLink = commitAuthorElement.getAttribute('href'); - const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('title'); + const commitAuthorName = commitAuthorElement.querySelector('img.avatar').getAttribute('data-original-title'); return { commitAuthorElement, commitAuthorLink, commitAuthorName }; }; diff --git a/spec/javascripts/vue_shared/components/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar_image_spec.js new file mode 100644 index 00000000000..0b5ec736b1e --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_image_spec.js @@ -0,0 +1,60 @@ +import Vue from 'vue'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; + +const UserAvatarImageComponent = Vue.extend(UserAvatarImage); + +describe('User Avatar Image Component', function () { + describe('Initialization', function () { + beforeEach(function () { + this.propsData = { + size: 99, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + cssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', + }; + + this.userAvatarImage = new UserAvatarImageComponent({ + propsData: this.propsData, + }).$mount(); + + this.imageElement = this.userAvatarImage.$el.outerHTML; + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarImage).toBeDefined(); + }); + + it('should have <img> as a child element', function () { + const componentImgTag = this.userAvatarImage.$el.outerHTML; + expect(componentImgTag).toContain('<img'); + }); + + it('should return neccessary props as defined', function () { + _.each(this.propsData, (val, key) => { + expect(this.userAvatarImage[key]).toBeDefined(); + }); + }); + + it('should properly compute tooltipContainer', function () { + expect(this.userAvatarImage.tooltipContainer).toBe('body'); + }); + + it('should properly render tooltipContainer', function () { + expect(this.imageElement).toContain('data-container="body"'); + }); + + it('should properly compute avatarSizeClass', function () { + expect(this.userAvatarImage.avatarSizeClass).toBe('s99'); + }); + + it('should properly compute imgCssClasses', function () { + expect(this.userAvatarImage.imgCssClasses).toBe('avatar s99 myextraavatarclass'); + }); + + it('should properly render imgCssClasses', function () { + expect(this.imageElement).toContain('avatar s99 myextraavatarclass'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar_link_spec.js b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js new file mode 100644 index 00000000000..770daa9f0de --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js @@ -0,0 +1,58 @@ +import Vue from 'vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; + +describe('User Avatar Link Component', function () { + beforeEach(function () { + this.propsData = { + linkHref: 'myavatarurl.com', + imgSize: 99, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + imgCssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', + }; + + const UserAvatarLinkComponent = Vue.extend(UserAvatarLink); + + this.userAvatarLink = new UserAvatarLinkComponent({ + propsData: this.propsData, + }).$mount(); + + this.userAvatarImage = this.userAvatarLink.$children[0]; + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarLink).toBeDefined(); + }); + + it('should have user-avatar-image registered as child component', function () { + expect(this.userAvatarLink.$options.components.userAvatarImage).toBeDefined(); + }); + + it('user-avatar-link should have user-avatar-image as child component', function () { + expect(this.userAvatarImage).toBeDefined(); + }); + + it('should render <a> as a child element', function () { + const componentLinkTag = this.userAvatarLink.$el.outerHTML; + expect(componentLinkTag).toContain('<a'); + }); + + it('should have <img> as a child element', function () { + const componentImgTag = this.userAvatarLink.$el.outerHTML; + expect(componentImgTag).toContain('<img'); + }); + + it('should return neccessary props as defined', function () { + _.each(this.propsData, (val, key) => { + expect(this.userAvatarLink[key]).toBeDefined(); + }); + }); + + it('should include props in the rendered output', function () { + _.each(this.propsData, (val) => { + expect(this.userAvatarLink.$el.outerHTML).toContain(val); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar_size_mixin_spec.js b/spec/javascripts/vue_shared/components/user_avatar_size_mixin_spec.js new file mode 100644 index 00000000000..b37813cdb3d --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_size_mixin_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import UserAvatarSizeMixin from '~/vue_shared/components/user_avatar/user_avatar_size_mixin'; + +describe('User Avatar Size Mixin', () => { + beforeEach(() => { + this.vueInstance = new Vue({ + data: { + size: 99, + }, + mixins: [UserAvatarSizeMixin], + }); + }); + + describe('#avatarSizeClass', () => { + it('should be a defined computed value', () => { + expect(this.vueInstance.avatarSizeClass).toBeDefined(); + }); + + it('should correctly transform size into the class name', () => { + expect(this.vueInstance.avatarSizeClass).toBe('s99'); + }); + }); + + describe('#avatarSizeStylesMap', () => { + it('should be a defined computed value', () => { + expect(this.vueInstance.avatarSizeStylesMap).toBeDefined(); + }); + + it('should return a correctly formatted styles width', () => { + expect(this.vueInstance.avatarSizeStylesMap.width).toBe('99px'); + }); + + it('should return a correctly formatted styles height', () => { + expect(this.vueInstance.avatarSizeStylesMap.height).toBe('99px'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js b/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js new file mode 100644 index 00000000000..809886c5dbd --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import UserAvatarSvg from '~/vue_shared/components/user_avatar/user_avatar_svg.vue'; +import avatarSvg from 'icons/_icon_random.svg'; + +const UserAvatarSvgComponent = Vue.extend(UserAvatarSvg); + +describe('User Avatar Svg Component', function () { + describe('Initialization', function () { + beforeEach(function () { + this.propsData = { + size: 99, + svg: avatarSvg, + }; + + this.userAvatarSvg = new UserAvatarSvgComponent({ + propsData: this.propsData, + }).$mount(); + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarSvg).toBeDefined(); + }); + + it('should have <svg> as a child element', function () { + expect(this.userAvatarSvg.$el.tagName).toEqual('svg'); + expect(this.userAvatarSvg.$el.innerHTML).toContain('<path'); + }); + + it('should return neccessary props as defined', function () { + _.each(this.propsData, (val, key) => { + expect(this.userAvatarSvg[key]).toBeDefined(); + }); + }); + }); +}); |