diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js index 710207db0c7416934af32fef28402e551d9d6bf8..4699ef5a51c7b6d2539878d16406d4318750747f 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 0d9ad197abf18df85f21b52104ee6f8ba7734378..eeb61826ace1970f448393fd00cc9dc0f9ca1a1e 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 ad285874643fd879ed4715bfb287067b81301b68..09fb390787d572903dc2aa63ae0f64f81efb4130 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 dec1704395e268d870cd3cd98f129dc658649c40..cd7a94b67c14a7ca51802bfdc3ef1fc0308a290c 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 a14ebc3ece9429bdb76a6371ae711f68bc94797a..bdf86b4ff3ca6d6119892b2bc702f236756acf42 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 1a5bf9bc0b5118955dce4cc33c9f06e1995f556d..cfb7a4ab5760b123f8a62cb74c37a1f35c99be99 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 1f7c673b1d45197a748f44923fb8fd963831bf5c..97a849c4febe6ca9e99d0cedd4c48edb7ce61fc4 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 5f533b5761c073ac1c9deff591b0f8fee058d0a3..517bdb6be092a9acdc20dfe8878d7877748ca490 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 1f01629aa1b2a95f2656bed10d6cdd53262ab797..012ff1f975b28f205a2f0a2bd078333aacf1bc58 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 ea8aaca6c9c3a487377a59cfa6a233f5c351179a..7cd2e0f936602951f48e704a96fc6d3a498c9bcc 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 9b060a0a35fac66e09d1267df96d5220edcf0694..23bc5fbc03430d59b364856e757707639da661c8 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 0000000000000000000000000000000000000000..b8db6afda12a806c4ff9f454808690a14a2aae59 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue @@ -0,0 +1,80 @@ +<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 TooltipMixin from '../../mixins/tooltip'; + +export default { + name: 'UserAvatarImage', + mixins: [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; + }, + avatarSizeClass() { + return `s${this.size}`; + }, + }, +}; +</script> + +<template> + <img + class="avatar" + :class="[avatarSizeClass, cssClasses]" + :src="imgSrc" + :width="size" + :height="size" + :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 0000000000000000000000000000000000000000..95898d54cf7bdc50904fd8bf6c001b9bc9b11c0f --- /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_svg.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue new file mode 100644 index 0000000000000000000000000000000000000000..d2ff2ac006e3f33edfd90d73bf43bb6f109cbcfa --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_svg.vue @@ -0,0 +1,45 @@ +<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" + /> + +*/ + +export default { + props: { + svg: { + type: String, + required: true, + }, + size: { + type: Number, + required: false, + default: 20, + }, + }, + computed: { + avatarSizeClass() { + return `s${this.size}`; + }, + }, +}; +</script> + +<template> + <svg + :class="avatarSizeClass" + :height="size" + :width="size" + v-html="svg"> + </svg> +</template> + diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss index 91c1ebd5a7de3a70a43f6b10c60f74ed8736d700..4ae2b164d2ebfc1372edb2f22790582cac3323fc 100644 --- a/app/assets/stylesheets/framework/avatar.scss +++ b/app/assets/stylesheets/framework/avatar.scss @@ -10,6 +10,8 @@ border-radius: $avatar_radius; border: 1px solid $avatar-border; &.s16 { @include avatar-size(16px, 6px); } + &.s18 { @include avatar-size(18px, 6px); } + &.s19 { @include avatar-size(19px, 6px); } &.s20 { @include avatar-size(20px, 7px); } &.s24 { @include avatar-size(24px, 8px); } &.s26 { @include avatar-size(26px, 8px); } diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss index 1b7d4e4225813a89dc07272791233cbefbeadebe..ef864e8f6a9da32666484b032f1078912272e9cd 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 a42ae7e55a544f32a895294edb675626cc81f304..48d3b7b1d07e6ada1e1c8b893811d58329d4d248 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 0781017c89ff1891905ba058a95e8d0e07afc2b5..7bc225968dea561c45b47d51f12ab952dbeb5aa8 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 b2e170513c4a4b13b2154be79e810b606ce5062a..ccf047d3efa3cd5510b5650640e58db2ae0138be 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 fddde799d01acfbf90fff0be7c4cbb8dbc92f1e6..bd9b4fbfdd3b79c6c3a568323932449c130083d2 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 53931d67ad78e5f68a3a15d625e693be23e13045..0bcc390570262d3c75e476823664642162832736 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 242010ba68817ea5ef53c32263b3ca0f186e3032..0638483e7aab8325577fc9f9e8019424478f007d 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 14280751053fb54bf945defa36758ea56fc727a6..286118917e88b3f2a903e0fd02b465f36af03b35 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 0000000000000000000000000000000000000000..8daa761027454fe4fa4ade9cddcb067c1427d972 --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_image_spec.js @@ -0,0 +1,54 @@ +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(); + }); + + it('should return a defined Vue component', function () { + expect(this.userAvatarImage).toBeDefined(); + }); + + it('should have <img> as a child element', function () { + expect(this.userAvatarImage.$el.tagName).toBe('IMG'); + }); + + it('should properly compute tooltipContainer', function () { + expect(this.userAvatarImage.tooltipContainer).toBe('body'); + }); + + it('should properly render tooltipContainer', function () { + expect(this.userAvatarImage.$el.getAttribute('data-container')).toBe('body'); + }); + + it('should properly compute avatarSizeClass', function () { + expect(this.userAvatarImage.avatarSizeClass).toBe('s99'); + }); + + it('should properly render img css', function () { + const classList = this.userAvatarImage.$el.classList; + const containsAvatar = classList.contains('avatar'); + const containsSizeClass = classList.contains('s99'); + const containsCustomClass = classList.contains('myextraavatarclass'); + + expect(containsAvatar).toBe(true); + expect(containsSizeClass).toBe(true); + expect(containsCustomClass).toBe(true); + }); + }); +}); 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 0000000000000000000000000000000000000000..52e450e9ba51962c44262b655b7ccbd532f73439 --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_link_spec.js @@ -0,0 +1,50 @@ +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 () { + expect(this.userAvatarLink.$el.tagName).toBe('A'); + }); + + it('should have <img> as a child element', function () { + expect(this.userAvatarLink.$el.querySelector('img')).not.toBeNull(); + }); + + it('should return neccessary props as defined', function () { + _.each(this.propsData, (val, key) => { + expect(this.userAvatarLink[key]).toBeDefined(); + }); + }); +}); 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 0000000000000000000000000000000000000000..b8d639ffbec77c94d54f401c5f394767f61f4e87 --- /dev/null +++ b/spec/javascripts/vue_shared/components/user_avatar_svg_spec.js @@ -0,0 +1,29 @@ +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'); + }); + }); +});