diff options
84 files changed, 7666 insertions, 0 deletions
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js new file mode 100644 index 00000000000..303a5bf4a53 --- /dev/null +++ b/app/assets/javascripts/compare.js @@ -0,0 +1,86 @@ +/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, no-var, object-shorthand, consistent-return, no-unused-vars, comma-dangle, vars-on-top, prefer-template, max-len */ + +import $ from 'jquery'; +import { localTimeAgo } from './lib/utils/datetime_utility'; +import axios from './lib/utils/axios_utils'; + +export default class Compare { + constructor(opts) { + this.opts = opts; + this.source_loading = $(".js-source-loading"); + this.target_loading = $(".js-target-loading"); + $('.js-compare-dropdown').each((function(_this) { + return function(i, dropdown) { + var $dropdown; + $dropdown = $(dropdown); + return $dropdown.glDropdown({ + selectable: true, + fieldName: $dropdown.data('fieldName'), + filterable: true, + id: function(obj, $el) { + return $el.data('id'); + }, + toggleLabel: function(obj, $el) { + return $el.text().trim(); + }, + clicked: function(e, el) { + if ($dropdown.is('.js-target-branch')) { + return _this.getTargetHtml(); + } else if ($dropdown.is('.js-source-branch')) { + return _this.getSourceHtml(); + } else if ($dropdown.is('.js-target-project')) { + return _this.getTargetProject(); + } + } + }); + }; + })(this)); + this.initialState(); + } + + initialState() { + this.getSourceHtml(); + this.getTargetHtml(); + } + + getTargetProject() { + $('.mr_target_commit').empty(); + + return axios.get(this.opts.targetProjectUrl, { + params: { + target_project_id: $("input[name='merge_request[target_project_id]']").val(), + }, + }).then(({ data }) => { + $('.js-target-branch-dropdown .dropdown-content').html(data); + }); + } + + getSourceHtml() { + return this.constructor.sendAjax(this.opts.sourceBranchUrl, this.source_loading, '.mr_source_commit', { + ref: $("input[name='merge_request[source_branch]']").val() + }); + } + + getTargetHtml() { + return this.constructor.sendAjax(this.opts.targetBranchUrl, this.target_loading, '.mr_target_commit', { + target_project_id: $("input[name='merge_request[target_project_id]']").val(), + ref: $("input[name='merge_request[target_branch]']").val() + }); + } + + static sendAjax(url, loading, target, params) { + const $target = $(target); + + loading.show(); + $target.empty(); + + return axios.get(url, { + params, + }).then(({ data }) => { + loading.hide(); + $target.html(data); + const className = '.' + $target[0].className.replace(' ', '.'); + localTimeAgo($('.js-timeago', className)); + }); + } +} diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue new file mode 100644 index 00000000000..627fbeb9adf --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_context_bar.vue @@ -0,0 +1,42 @@ +<script> +import icon from '~/vue_shared/components/icon.vue'; +import panelResizer from '~/vue_shared/components/panel_resizer.vue'; +import repoCommitSection from './repo_commit_section.vue'; +import ResizablePanel from './resizable_panel.vue'; + +export default { + components: { + repoCommitSection, + icon, + panelResizer, + ResizablePanel, + }, + props: { + noChangesStateSvgPath: { + type: String, + required: true, + }, + committedStateSvgPath: { + type: String, + required: true, + }, + }, +}; +</script> + +<template> + <resizable-panel + :collapsible="true" + :initial-width="340" + side="right" + > + <div + class="multi-file-commit-panel-section" + > + <repo-commit-section + :no-changes-state-svg-path="noChangesStateSvgPath" + :committed-state-svg-path="committedStateSvgPath" + /> + </div> + </resizable-panel> +</template> diff --git a/app/assets/javascripts/ide/components/ide_external_links.vue b/app/assets/javascripts/ide/components/ide_external_links.vue new file mode 100644 index 00000000000..c6f6e0d2348 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_external_links.vue @@ -0,0 +1,43 @@ +<script> +import icon from '~/vue_shared/components/icon.vue'; + +export default { + components: { + icon, + }, + props: { + projectUrl: { + type: String, + required: true, + }, + }, + computed: { + goBackUrl() { + return document.referrer || this.projectUrl; + }, + }, +}; +</script> + +<template> + <nav + class="ide-external-links" + v-once + > + <p> + <a + :href="goBackUrl" + class="ide-sidebar-link" + > + <icon + :size="16" + class="append-right-8" + name="go-back" + /> + <span class="ide-external-links-text"> + {{ s__('Go back') }} + </span> + </a> + </p> + </nav> +</template> diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue new file mode 100644 index 00000000000..eb2749e6151 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue @@ -0,0 +1,47 @@ +<script> + import icon from '~/vue_shared/components/icon.vue'; + import repoTree from './ide_repo_tree.vue'; + import newDropdown from './new_dropdown/index.vue'; + + export default { + components: { + repoTree, + icon, + newDropdown, + }, + props: { + projectId: { + type: String, + required: true, + }, + branch: { + type: Object, + required: true, + }, + }, + }; +</script> + +<template> + <div class="branch-container"> + <div class="branch-header"> + <div class="branch-header-title str-truncated ref-name"> + <icon + name="branch" + :size="12" + /> + {{ branch.name }} + </div> + <div class="branch-header-btns"> + <new-dropdown + :project-id="projectId" + :branch="branch.name" + path="" + /> + </div> + </div> + <repo-tree + :tree="branch.tree" + /> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue new file mode 100644 index 00000000000..a6f40286ac1 --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_project_tree.vue @@ -0,0 +1,65 @@ +<script> +import ProjectAvatarImage from '~/vue_shared/components/project_avatar/image.vue'; +import Identicon from '../../vue_shared/components/identicon.vue'; +import BranchesTree from './ide_project_branches_tree.vue'; +import ExternalLinks from './ide_external_links.vue'; + +export default { + components: { + BranchesTree, + ExternalLinks, + ProjectAvatarImage, + Identicon, + }, + props: { + project: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <div class="projects-sidebar"> + <div class="context-header"> + <a + :title="project.name" + :href="project.web_url" + > + <div + v-if="project.avatar_url" + class="avatar-container s40 project-avatar" + > + <project-avatar-image + class="avatar-container project-avatar" + :link-href="project.path" + :img-src="project.avatar_url" + :img-alt="project.name" + :img-size="40" + /> + </div> + <identicon + v-else + size-class="s40" + :entity-id="project.id" + :entity-name="project.name" + /> + <div class="sidebar-context-title"> + {{ project.name }} + </div> + </a> + </div> + <external-links + :project-url="project.web_url" + /> + <div class="multi-file-commit-panel-inner-scroll"> + <branches-tree + v-for="branch in project.branches" + :key="branch.name" + :project-id="project.path_with_namespace" + :branch="branch" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue new file mode 100644 index 00000000000..e6af88e04bc --- /dev/null +++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue @@ -0,0 +1,41 @@ +<script> +import SkeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue'; +import RepoFile from './repo_file.vue'; + +export default { + components: { + RepoFile, + SkeletonLoadingContainer, + }, + props: { + tree: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <div + class="ide-file-list" + > + <template v-if="tree.loading"> + <div + class="multi-file-loading-container" + v-for="n in 3" + :key="n" + > + <skeleton-loading-container /> + </div> + </template> + <template v-else> + <repo-file + v-for="file in tree.tree" + :key="file.key" + :file="file" + :level="0" + /> + </template> + </div> +</template> diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue new file mode 100644 index 00000000000..0cdffbde05b --- /dev/null +++ b/app/assets/javascripts/pipelines/components/async_button.vue @@ -0,0 +1,95 @@ +<script> + /* eslint-disable no-alert */ + + import eventHub from '../event_hub'; + import loadingIcon from '../../vue_shared/components/loading_icon.vue'; + import icon from '../../vue_shared/components/icon.vue'; + import tooltip from '../../vue_shared/directives/tooltip'; + + export default { + directives: { + tooltip, + }, + components: { + loadingIcon, + icon, + }, + props: { + endpoint: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + icon: { + type: String, + required: true, + }, + cssClass: { + type: String, + required: true, + }, + pipelineId: { + type: Number, + required: true, + }, + type: { + type: String, + required: true, + }, + }, + data() { + return { + isLoading: false, + }; + }, + computed: { + buttonClass() { + return `btn ${this.cssClass}`; + }, + }, + created() { + // We're using eventHub to listen to the modal here instead of + // using props because it would would make the parent components + // much more complex to keep track of the loading state of each button + eventHub.$on('postAction', this.setLoading); + }, + beforeDestroy() { + eventHub.$off('postAction', this.setLoading); + }, + methods: { + onClick() { + eventHub.$emit('openConfirmationModal', { + pipelineId: this.pipelineId, + endpoint: this.endpoint, + type: this.type, + }); + }, + setLoading(endpoint) { + if (endpoint === this.endpoint) { + this.isLoading = true; + } + }, + }, + }; +</script> + +<template> + <button + v-tooltip + type="button" + @click="onClick" + :class="buttonClass" + :title="title" + :aria-label="title" + data-container="body" + data-placement="top" + :disabled="isLoading"> + <icon + :name="icon" + /> + <loading-icon v-if="isLoading" /> + </button> +</template> diff --git a/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js new file mode 100644 index 00000000000..bf987562647 --- /dev/null +++ b/app/assets/javascripts/sidebar/components/time_tracking/spent_only_pane.js @@ -0,0 +1,15 @@ +export default { + name: 'time-tracking-spent-only-pane', + props: { + timeSpentHumanReadable: { + type: String, + required: true, + }, + }, + template: ` + <div class="time-tracking-spend-only-pane"> + <span class="bold">Spent:</span> + {{ timeSpentHumanReadable }} + </div> + `, +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue new file mode 100644 index 00000000000..f0298f732ea --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue @@ -0,0 +1,20 @@ +<script> + export default { + name: 'MRWidgetMaintainerEdit', + props: { + maintainerEditAllowed: { + type: Boolean, + default: false, + required: false, + }, + }, + }; +</script> + +<template> + <section class="mr-info-list mr-links"> + <p v-if="maintainerEditAllowed"> + {{ s__("mrWidget|Allows edits from maintainers") }} + </p> + </section> +</template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js new file mode 100644 index 00000000000..bf8628d18a6 --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.js @@ -0,0 +1,15 @@ +/* +The squash-before-merge button is EE only, but it's located right in the middle +of the readyToMerge state component template. + +If we didn't declare this component in CE, we'd need to maintain a separate copy +of the readyToMergeState template in EE, which is pretty big and likely to change. + +Instead, in CE, we declare the component, but it's hidden and is configured to do nothing. +In EE, the configuration extends this object to add a functioning squash-before-merge +button. +*/ + +export default { + template: '', +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js new file mode 100644 index 00000000000..345f9ac1b4b --- /dev/null +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js @@ -0,0 +1,277 @@ +import Project from '~/pages/projects/project'; +import SmartInterval from '~/smart_interval'; +import Flash from '../flash'; +import { + WidgetHeader, + WidgetMergeHelp, + WidgetPipeline, + Deployment, + WidgetMaintainerEdit, + WidgetRelatedLinks, + MergedState, + ClosedState, + MergingState, + RebaseState, + WorkInProgressState, + ArchivedState, + ConflictsState, + NothingToMergeState, + MissingBranchState, + NotAllowedState, + ReadyToMergeState, + ShaMismatchState, + UnresolvedDiscussionsState, + PipelineBlockedState, + PipelineFailedState, + FailedToMerge, + MergeWhenPipelineSucceedsState, + AutoMergeFailed, + CheckingState, + MRWidgetStore, + MRWidgetService, + eventHub, + stateMaps, + SquashBeforeMerge, + notify, + SourceBranchRemovalStatus, +} from './dependencies'; +import { setFavicon } from '../lib/utils/common_utils'; + +export default { + el: '#js-vue-mr-widget', + name: 'MRWidget', + props: { + mrData: { + type: Object, + required: false, + }, + }, + data() { + const store = new MRWidgetStore(this.mrData || window.gl.mrWidgetData); + const service = this.createService(store); + return { + mr: store, + service, + }; + }, + computed: { + componentName() { + return stateMaps.stateToComponentMap[this.mr.state]; + }, + shouldRenderMergeHelp() { + return stateMaps.statesToShowHelpWidget.indexOf(this.mr.state) > -1; + }, + shouldRenderPipelines() { + return this.mr.hasCI; + }, + shouldRenderRelatedLinks() { + return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState; + }, + shouldRenderSourceBranchRemovalStatus() { + return !this.mr.canRemoveSourceBranch && this.mr.shouldRemoveSourceBranch && + (!this.mr.isNothingToMergeState && !this.mr.isMergedState); + }, + }, + methods: { + createService(store) { + const endpoints = { + mergePath: store.mergePath, + mergeCheckPath: store.mergeCheckPath, + cancelAutoMergePath: store.cancelAutoMergePath, + removeWIPPath: store.removeWIPPath, + sourceBranchPath: store.sourceBranchPath, + ciEnvironmentsStatusPath: store.ciEnvironmentsStatusPath, + statusPath: store.statusPath, + mergeActionsContentPath: store.mergeActionsContentPath, + rebasePath: store.rebasePath, + }; + return new MRWidgetService(endpoints); + }, + checkStatus(cb) { + return this.service.checkStatus() + .then(res => res.data) + .then((data) => { + this.handleNotification(data); + this.mr.setData(data); + this.setFaviconHelper(); + + if (cb) { + cb.call(null, data); + } + }) + .catch(() => new Flash('Something went wrong. Please try again.')); + }, + initPolling() { + this.pollingInterval = new SmartInterval({ + callback: this.checkStatus, + startingInterval: 10000, + maxInterval: 30000, + hiddenInterval: 120000, + incrementByFactorOf: 5000, + }); + }, + initDeploymentsPolling() { + this.deploymentsInterval = new SmartInterval({ + callback: this.fetchDeployments, + startingInterval: 30000, + maxInterval: 120000, + hiddenInterval: 240000, + incrementByFactorOf: 15000, + immediateExecution: true, + }); + }, + setFaviconHelper() { + if (this.mr.ciStatusFaviconPath) { + setFavicon(this.mr.ciStatusFaviconPath); + } + }, + fetchDeployments() { + return this.service.fetchDeployments() + .then(res => res.data) + .then((data) => { + if (data.length) { + this.mr.deployments = data; + } + }) + .catch(() => { + new Flash('Something went wrong while fetching the environments for this merge request. Please try again.'); // eslint-disable-line + }); + }, + fetchActionsContent() { + this.service.fetchMergeActionsContent() + .then((res) => { + if (res.data) { + const el = document.createElement('div'); + el.innerHTML = res.data; + document.body.appendChild(el); + Project.initRefSwitcher(); + } + }) + .catch(() => new Flash('Something went wrong. Please try again.')); + }, + handleNotification(data) { + if (data.ci_status === this.mr.ciStatus) return; + if (!data.pipeline) return; + + const label = data.pipeline.details.status.label; + const title = `Pipeline ${label}`; + const message = `Pipeline ${label} for "${data.title}"`; + + notify.notifyMe(title, message, this.mr.gitlabLogo); + }, + resumePolling() { + this.pollingInterval.resume(); + }, + stopPolling() { + this.pollingInterval.stopTimer(); + }, + bindEventHubListeners() { + eventHub.$on('MRWidgetUpdateRequested', (cb) => { + this.checkStatus(cb); + }); + + // `params` should be an Array contains a Boolean, like `[true]` + // Passing parameter as Boolean didn't work. + eventHub.$on('SetBranchRemoveFlag', (params) => { + this.mr.isRemovingSourceBranch = params[0]; + }); + + eventHub.$on('FailedToMerge', (mergeError) => { + this.mr.state = 'failedToMerge'; + this.mr.mergeError = mergeError; + }); + + eventHub.$on('UpdateWidgetData', (data) => { + this.mr.setData(data); + }); + + eventHub.$on('FetchActionsContent', () => { + this.fetchActionsContent(); + }); + + eventHub.$on('EnablePolling', () => { + this.resumePolling(); + }); + + eventHub.$on('DisablePolling', () => { + this.stopPolling(); + }); + }, + handleMounted() { + this.setFaviconHelper(); + this.initDeploymentsPolling(); + }, + }, + created() { + this.initPolling(); + this.bindEventHubListeners(); + }, + mounted() { + this.handleMounted(); + }, + components: { + 'mr-widget-header': WidgetHeader, + 'mr-widget-merge-help': WidgetMergeHelp, + 'mr-widget-pipeline': WidgetPipeline, + Deployment, + 'mr-widget-maintainer-edit': WidgetMaintainerEdit, + 'mr-widget-related-links': WidgetRelatedLinks, + 'mr-widget-merged': MergedState, + 'mr-widget-closed': ClosedState, + 'mr-widget-merging': MergingState, + 'mr-widget-failed-to-merge': FailedToMerge, + 'mr-widget-wip': WorkInProgressState, + 'mr-widget-archived': ArchivedState, + 'mr-widget-conflicts': ConflictsState, + 'mr-widget-nothing-to-merge': NothingToMergeState, + 'mr-widget-not-allowed': NotAllowedState, + 'mr-widget-missing-branch': MissingBranchState, + 'mr-widget-ready-to-merge': ReadyToMergeState, + 'mr-widget-sha-mismatch': ShaMismatchState, + 'mr-widget-squash-before-merge': SquashBeforeMerge, + 'mr-widget-checking': CheckingState, + 'mr-widget-unresolved-discussions': UnresolvedDiscussionsState, + 'mr-widget-pipeline-blocked': PipelineBlockedState, + 'mr-widget-pipeline-failed': PipelineFailedState, + 'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState, + 'mr-widget-auto-merge-failed': AutoMergeFailed, + 'mr-widget-rebase': RebaseState, + SourceBranchRemovalStatus, + }, + template: ` + <div class="mr-state-widget prepend-top-default"> + <mr-widget-header :mr="mr" /> + <mr-widget-pipeline + v-if="shouldRenderPipelines" + :pipeline="mr.pipeline" + :ci-status="mr.ciStatus" + :has-ci="mr.hasCI" + /> + <deployment + v-for="deployment in mr.deployments" + :key="deployment.id" + :deployment="deployment" + /> + <div class="mr-widget-section"> + <component + :is="componentName" + :mr="mr" + :service="service" /> + <mr-widget-maintainer-edit + :maintainerEditAllowed="mr.maintainerEditAllowed" /> + <mr-widget-related-links + v-if="shouldRenderRelatedLinks" + :state="mr.state" + :related-links="mr.relatedLinks" /> + <source-branch-removal-status + v-if="shouldRenderSourceBranchRemovalStatus" + /> + </div> + <div + class="mr-widget-footer" + v-if="shouldRenderMergeHelp"> + <mr-widget-merge-help /> + </div> + </div> + `, +}; diff --git a/app/assets/stylesheets/framework/emoji_sprites.scss b/app/assets/stylesheets/framework/emoji_sprites.scss new file mode 100644 index 00000000000..0174e17b660 --- /dev/null +++ b/app/assets/stylesheets/framework/emoji_sprites.scss @@ -0,0 +1,1813 @@ +.emoji-zzz { background-position: 0 0; } +.emoji-1234 { background-position: -20px 0; } +.emoji-1F627 { background-position: 0 -20px; } +.emoji-8ball { background-position: -20px -20px; } +.emoji-a { background-position: -40px 0; } +.emoji-ab { background-position: -40px -20px; } +.emoji-abc { background-position: 0 -40px; } +.emoji-abcd { background-position: -20px -40px; } +.emoji-accept { background-position: -40px -40px; } +.emoji-aerial_tramway { background-position: -60px 0; } +.emoji-airplane { background-position: -60px -20px; } +.emoji-airplane_arriving { background-position: -60px -40px; } +.emoji-airplane_departure { background-position: 0 -60px; } +.emoji-airplane_small { background-position: -20px -60px; } +.emoji-alarm_clock { background-position: -40px -60px; } +.emoji-alembic { background-position: -60px -60px; } +.emoji-alien { background-position: -80px 0; } +.emoji-ambulance { background-position: -80px -20px; } +.emoji-amphora { background-position: -80px -40px; } +.emoji-anchor { background-position: -80px -60px; } +.emoji-angel { background-position: 0 -80px; } +.emoji-angel_tone1 { background-position: -20px -80px; } +.emoji-angel_tone2 { background-position: -40px -80px; } +.emoji-angel_tone3 { background-position: -60px -80px; } +.emoji-angel_tone4 { background-position: -80px -80px; } +.emoji-angel_tone5 { background-position: -100px 0; } +.emoji-anger { background-position: -100px -20px; } +.emoji-anger_right { background-position: -100px -40px; } +.emoji-angry { background-position: -100px -60px; } +.emoji-ant { background-position: -100px -80px; } +.emoji-apple { background-position: 0 -100px; } +.emoji-aquarius { background-position: -20px -100px; } +.emoji-aries { background-position: -40px -100px; } +.emoji-arrow_backward { background-position: -60px -100px; } +.emoji-arrow_double_down { background-position: -80px -100px; } +.emoji-arrow_double_up { background-position: -100px -100px; } +.emoji-arrow_down { background-position: -120px 0; } +.emoji-arrow_down_small { background-position: -120px -20px; } +.emoji-arrow_forward { background-position: -120px -40px; } +.emoji-arrow_heading_down { background-position: -120px -60px; } +.emoji-arrow_heading_up { background-position: -120px -80px; } +.emoji-arrow_left { background-position: -120px -100px; } +.emoji-arrow_lower_left { background-position: 0 -120px; } +.emoji-arrow_lower_right { background-position: -20px -120px; } +.emoji-arrow_right { background-position: -40px -120px; } +.emoji-arrow_right_hook { background-position: -60px -120px; } +.emoji-arrow_up { background-position: -80px -120px; } +.emoji-arrow_up_down { background-position: -100px -120px; } +.emoji-arrow_up_small { background-position: -120px -120px; } +.emoji-arrow_upper_left { background-position: -140px 0; } +.emoji-arrow_upper_right { background-position: -140px -20px; } +.emoji-arrows_clockwise { background-position: -140px -40px; } +.emoji-arrows_counterclockwise { background-position: -140px -60px; } +.emoji-art { background-position: -140px -80px; } +.emoji-articulated_lorry { background-position: -140px -100px; } +.emoji-asterisk { background-position: -140px -120px; } +.emoji-astonished { background-position: 0 -140px; } +.emoji-athletic_shoe { background-position: -20px -140px; } +.emoji-atm { background-position: -40px -140px; } +.emoji-atom { background-position: -60px -140px; } +.emoji-avocado { background-position: -80px -140px; } +.emoji-b { background-position: -100px -140px; } +.emoji-baby { background-position: -120px -140px; } +.emoji-baby_bottle { background-position: -140px -140px; } +.emoji-baby_chick { background-position: -160px 0; } +.emoji-baby_symbol { background-position: -160px -20px; } +.emoji-baby_tone1 { background-position: -160px -40px; } +.emoji-baby_tone2 { background-position: -160px -60px; } +.emoji-baby_tone3 { background-position: -160px -80px; } +.emoji-baby_tone4 { background-position: -160px -100px; } +.emoji-baby_tone5 { background-position: -160px -120px; } +.emoji-back { background-position: -160px -140px; } +.emoji-bacon { background-position: 0 -160px; } +.emoji-badminton { background-position: -20px -160px; } +.emoji-baggage_claim { background-position: -40px -160px; } +.emoji-balloon { background-position: -60px -160px; } +.emoji-ballot_box { background-position: -80px -160px; } +.emoji-ballot_box_with_check { background-position: -100px -160px; } +.emoji-bamboo { background-position: -120px -160px; } +.emoji-banana { background-position: -140px -160px; } +.emoji-bangbang { background-position: -160px -160px; } +.emoji-bank { background-position: -180px 0; } +.emoji-bar_chart { background-position: -180px -20px; } +.emoji-barber { background-position: -180px -40px; } +.emoji-baseball { background-position: -180px -60px; } +.emoji-basketball { background-position: -180px -80px; } +.emoji-basketball_player { background-position: -180px -100px; } +.emoji-basketball_player_tone1 { background-position: -180px -120px; } +.emoji-basketball_player_tone2 { background-position: -180px -140px; } +.emoji-basketball_player_tone3 { background-position: -180px -160px; } +.emoji-basketball_player_tone4 { background-position: 0 -180px; } +.emoji-basketball_player_tone5 { background-position: -20px -180px; } +.emoji-bat { background-position: -40px -180px; } +.emoji-bath { background-position: -60px -180px; } +.emoji-bath_tone1 { background-position: -80px -180px; } +.emoji-bath_tone2 { background-position: -100px -180px; } +.emoji-bath_tone3 { background-position: -120px -180px; } +.emoji-bath_tone4 { background-position: -140px -180px; } +.emoji-bath_tone5 { background-position: -160px -180px; } +.emoji-bathtub { background-position: -180px -180px; } +.emoji-battery { background-position: -200px 0; } +.emoji-beach { background-position: -200px -20px; } +.emoji-beach_umbrella { background-position: -200px -40px; } +.emoji-bear { background-position: -200px -60px; } +.emoji-bed { background-position: -200px -80px; } +.emoji-bee { background-position: -200px -100px; } +.emoji-beer { background-position: -200px -120px; } +.emoji-beers { background-position: -200px -140px; } +.emoji-beetle { background-position: -200px -160px; } +.emoji-beginner { background-position: -200px -180px; } +.emoji-bell { background-position: 0 -200px; } +.emoji-bellhop { background-position: -20px -200px; } +.emoji-bento { background-position: -40px -200px; } +.emoji-bicyclist { background-position: -60px -200px; } +.emoji-bicyclist_tone1 { background-position: -80px -200px; } +.emoji-bicyclist_tone2 { background-position: -100px -200px; } +.emoji-bicyclist_tone3 { background-position: -120px -200px; } +.emoji-bicyclist_tone4 { background-position: -140px -200px; } +.emoji-bicyclist_tone5 { background-position: -160px -200px; } +.emoji-bike { background-position: -180px -200px; } +.emoji-bikini { background-position: -200px -200px; } +.emoji-biohazard { background-position: -220px 0; } +.emoji-bird { background-position: -220px -20px; } +.emoji-birthday { background-position: -220px -40px; } +.emoji-black_circle { background-position: -220px -60px; } +.emoji-black_heart { background-position: -220px -80px; } +.emoji-black_joker { background-position: -220px -100px; } +.emoji-black_large_square { background-position: -220px -120px; } +.emoji-black_medium_small_square { background-position: -220px -140px; } +.emoji-black_medium_square { background-position: -220px -160px; } +.emoji-black_nib { background-position: -220px -180px; } +.emoji-black_small_square { background-position: -220px -200px; } +.emoji-black_square_button { background-position: 0 -220px; } +.emoji-blossom { background-position: -20px -220px; } +.emoji-blowfish { background-position: -40px -220px; } +.emoji-blue_book { background-position: -60px -220px; } +.emoji-blue_car { background-position: -80px -220px; } +.emoji-blue_heart { background-position: -100px -220px; } +.emoji-blush { background-position: -120px -220px; } +.emoji-boar { background-position: -140px -220px; } +.emoji-bomb { background-position: -160px -220px; } +.emoji-book { background-position: -180px -220px; } +.emoji-bookmark { background-position: -200px -220px; } +.emoji-bookmark_tabs { background-position: -220px -220px; } +.emoji-books { background-position: -240px 0; } +.emoji-boom { background-position: -240px -20px; } +.emoji-boot { background-position: -240px -40px; } +.emoji-bouquet { background-position: -240px -60px; } +.emoji-bow { background-position: -240px -80px; } +.emoji-bow_and_arrow { background-position: -240px -100px; } +.emoji-bow_tone1 { background-position: -240px -120px; } +.emoji-bow_tone2 { background-position: -240px -140px; } +.emoji-bow_tone3 { background-position: -240px -160px; } +.emoji-bow_tone4 { background-position: -240px -180px; } +.emoji-bow_tone5 { background-position: -240px -200px; } +.emoji-bowling { background-position: -240px -220px; } +.emoji-boxing_glove { background-position: 0 -240px; } +.emoji-boy { background-position: -20px -240px; } +.emoji-boy_tone1 { background-position: -40px -240px; } +.emoji-boy_tone2 { background-position: -60px -240px; } +.emoji-boy_tone3 { background-position: -80px -240px; } +.emoji-boy_tone4 { background-position: -100px -240px; } +.emoji-boy_tone5 { background-position: -120px -240px; } +.emoji-bread { background-position: -140px -240px; } +.emoji-bride_with_veil { background-position: -160px -240px; } +.emoji-bride_with_veil_tone1 { background-position: -180px -240px; } +.emoji-bride_with_veil_tone2 { background-position: -200px -240px; } +.emoji-bride_with_veil_tone3 { background-position: -220px -240px; } +.emoji-bride_with_veil_tone4 { background-position: -240px -240px; } +.emoji-bride_with_veil_tone5 { background-position: -260px 0; } +.emoji-bridge_at_night { background-position: -260px -20px; } +.emoji-briefcase { background-position: -260px -40px; } +.emoji-broken_heart { background-position: -260px -60px; } +.emoji-bug { background-position: -260px -80px; } +.emoji-bulb { background-position: -260px -100px; } +.emoji-bullettrain_front { background-position: -260px -120px; } +.emoji-bullettrain_side { background-position: -260px -140px; } +.emoji-burrito { background-position: -260px -160px; } +.emoji-bus { background-position: -260px -180px; } +.emoji-busstop { background-position: -260px -200px; } +.emoji-bust_in_silhouette { background-position: -260px -220px; } +.emoji-busts_in_silhouette { background-position: -260px -240px; } +.emoji-butterfly { background-position: 0 -260px; } +.emoji-cactus { background-position: -20px -260px; } +.emoji-cake { background-position: -40px -260px; } +.emoji-calendar { background-position: -60px -260px; } +.emoji-calendar_spiral { background-position: -80px -260px; } +.emoji-call_me { background-position: -100px -260px; } +.emoji-call_me_tone1 { background-position: -120px -260px; } +.emoji-call_me_tone2 { background-position: -140px -260px; } +.emoji-call_me_tone3 { background-position: -160px -260px; } +.emoji-call_me_tone4 { background-position: -180px -260px; } +.emoji-call_me_tone5 { background-position: -200px -260px; } +.emoji-calling { background-position: -220px -260px; } +.emoji-camel { background-position: -240px -260px; } +.emoji-camera { background-position: -260px -260px; } +.emoji-camera_with_flash { background-position: -280px 0; } +.emoji-camping { background-position: -280px -20px; } +.emoji-cancer { background-position: -280px -40px; } +.emoji-candle { background-position: -280px -60px; } +.emoji-candy { background-position: -280px -80px; } +.emoji-canoe { background-position: -280px -100px; } +.emoji-capital_abcd { background-position: -280px -120px; } +.emoji-capricorn { background-position: -280px -140px; } +.emoji-card_box { background-position: -280px -160px; } +.emoji-card_index { background-position: -280px -180px; } +.emoji-carousel_horse { background-position: -280px -200px; } +.emoji-carrot { background-position: -280px -220px; } +.emoji-cartwheel { background-position: -280px -240px; } +.emoji-cartwheel_tone1 { background-position: -280px -260px; } +.emoji-cartwheel_tone2 { background-position: 0 -280px; } +.emoji-cartwheel_tone3 { background-position: -20px -280px; } +.emoji-cartwheel_tone4 { background-position: -40px -280px; } +.emoji-cartwheel_tone5 { background-position: -60px -280px; } +.emoji-cat { background-position: -80px -280px; } +.emoji-cat2 { background-position: -100px -280px; } +.emoji-cd { background-position: -120px -280px; } +.emoji-chains { background-position: -140px -280px; } +.emoji-champagne { background-position: -160px -280px; } +.emoji-champagne_glass { background-position: -180px -280px; } +.emoji-chart { background-position: -200px -280px; } +.emoji-chart_with_downwards_trend { background-position: -220px -280px; } +.emoji-chart_with_upwards_trend { background-position: -240px -280px; } +.emoji-checkered_flag { background-position: -260px -280px; } +.emoji-cheese { background-position: -280px -280px; } +.emoji-cherries { background-position: -300px 0; } +.emoji-cherry_blossom { background-position: -300px -20px; } +.emoji-chestnut { background-position: -300px -40px; } +.emoji-chicken { background-position: -300px -60px; } +.emoji-children_crossing { background-position: -300px -80px; } +.emoji-chipmunk { background-position: -300px -100px; } +.emoji-chocolate_bar { background-position: -300px -120px; } +.emoji-christmas_tree { background-position: -300px -140px; } +.emoji-church { background-position: -300px -160px; } +.emoji-cinema { background-position: -300px -180px; } +.emoji-circus_tent { background-position: -300px -200px; } +.emoji-city_dusk { background-position: -300px -220px; } +.emoji-city_sunset { background-position: -300px -240px; } +.emoji-cityscape { background-position: -300px -260px; } +.emoji-cl { background-position: -300px -280px; } +.emoji-clap { background-position: 0 -300px; } +.emoji-clap_tone1 { background-position: -20px -300px; } +.emoji-clap_tone2 { background-position: -40px -300px; } +.emoji-clap_tone3 { background-position: -60px -300px; } +.emoji-clap_tone4 { background-position: -80px -300px; } +.emoji-clap_tone5 { background-position: -100px -300px; } +.emoji-clapper { background-position: -120px -300px; } +.emoji-classical_building { background-position: -140px -300px; } +.emoji-clipboard { background-position: -160px -300px; } +.emoji-clock { background-position: -180px -300px; } +.emoji-clock1 { background-position: -200px -300px; } +.emoji-clock10 { background-position: -220px -300px; } +.emoji-clock1030 { background-position: -240px -300px; } +.emoji-clock11 { background-position: -260px -300px; } +.emoji-clock1130 { background-position: -280px -300px; } +.emoji-clock12 { background-position: -300px -300px; } +.emoji-clock1230 { background-position: -320px 0; } +.emoji-clock130 { background-position: -320px -20px; } +.emoji-clock2 { background-position: -320px -40px; } +.emoji-clock230 { background-position: -320px -60px; } +.emoji-clock3 { background-position: -320px -80px; } +.emoji-clock330 { background-position: -320px -100px; } +.emoji-clock4 { background-position: -320px -120px; } +.emoji-clock430 { background-position: -320px -140px; } +.emoji-clock5 { background-position: -320px -160px; } +.emoji-clock530 { background-position: -320px -180px; } +.emoji-clock6 { background-position: -320px -200px; } +.emoji-clock630 { background-position: -320px -220px; } +.emoji-clock7 { background-position: -320px -240px; } +.emoji-clock730 { background-position: -320px -260px; } +.emoji-clock8 { background-position: -320px -280px; } +.emoji-clock830 { background-position: -320px -300px; } +.emoji-clock9 { background-position: 0 -320px; } +.emoji-clock930 { background-position: -20px -320px; } +.emoji-closed_book { background-position: -40px -320px; } +.emoji-closed_lock_with_key { background-position: -60px -320px; } +.emoji-closed_umbrella { background-position: -80px -320px; } +.emoji-cloud { background-position: -100px -320px; } +.emoji-cloud_lightning { background-position: -120px -320px; } +.emoji-cloud_rain { background-position: -140px -320px; } +.emoji-cloud_snow { background-position: -160px -320px; } +.emoji-cloud_tornado { background-position: -180px -320px; } +.emoji-clown { background-position: -200px -320px; } +.emoji-clubs { background-position: -220px -320px; } +.emoji-cocktail { background-position: -240px -320px; } +.emoji-coffee { background-position: -260px -320px; } +.emoji-coffin { background-position: -280px -320px; } +.emoji-cold_sweat { background-position: -300px -320px; } +.emoji-comet { background-position: -320px -320px; } +.emoji-compression { background-position: -340px 0; } +.emoji-computer { background-position: -340px -20px; } +.emoji-confetti_ball { background-position: -340px -40px; } +.emoji-confounded { background-position: -340px -60px; } +.emoji-confused { background-position: -340px -80px; } +.emoji-congratulations { background-position: -340px -100px; } +.emoji-construction { background-position: -340px -120px; } +.emoji-construction_site { background-position: -340px -140px; } +.emoji-construction_worker { background-position: -340px -160px; } +.emoji-construction_worker_tone1 { background-position: -340px -180px; } +.emoji-construction_worker_tone2 { background-position: -340px -200px; } +.emoji-construction_worker_tone3 { background-position: -340px -220px; } +.emoji-construction_worker_tone4 { background-position: -340px -240px; } +.emoji-construction_worker_tone5 { background-position: -340px -260px; } +.emoji-control_knobs { background-position: -340px -280px; } +.emoji-convenience_store { background-position: -340px -300px; } +.emoji-cookie { background-position: -340px -320px; } +.emoji-cooking { background-position: 0 -340px; } +.emoji-cool { background-position: -20px -340px; } +.emoji-cop { background-position: -40px -340px; } +.emoji-cop_tone1 { background-position: -60px -340px; } +.emoji-cop_tone2 { background-position: -80px -340px; } +.emoji-cop_tone3 { background-position: -100px -340px; } +.emoji-cop_tone4 { background-position: -120px -340px; } +.emoji-cop_tone5 { background-position: -140px -340px; } +.emoji-copyright { background-position: -160px -340px; } +.emoji-corn { background-position: -180px -340px; } +.emoji-couch { background-position: -200px -340px; } +.emoji-couple { background-position: -220px -340px; } +.emoji-couple_mm { background-position: -240px -340px; } +.emoji-couple_with_heart { background-position: -260px -340px; } +.emoji-couple_ww { background-position: -280px -340px; } +.emoji-couplekiss { background-position: -300px -340px; } +.emoji-cow { background-position: -320px -340px; } +.emoji-cow2 { background-position: -340px -340px; } +.emoji-cowboy { background-position: -360px 0; } +.emoji-crab { background-position: -360px -20px; } +.emoji-crayon { background-position: -360px -40px; } +.emoji-credit_card { background-position: -360px -60px; } +.emoji-crescent_moon { background-position: -360px -80px; } +.emoji-cricket { background-position: -360px -100px; } +.emoji-crocodile { background-position: -360px -120px; } +.emoji-croissant { background-position: -360px -140px; } +.emoji-cross { background-position: -360px -160px; } +.emoji-crossed_flags { background-position: -360px -180px; } +.emoji-crossed_swords { background-position: -360px -200px; } +.emoji-crown { background-position: -360px -220px; } +.emoji-cruise_ship { background-position: -360px -240px; } +.emoji-cry { background-position: -360px -260px; } +.emoji-crying_cat_face { background-position: -360px -280px; } +.emoji-crystal_ball { background-position: -360px -300px; } +.emoji-cucumber { background-position: -360px -320px; } +.emoji-cupid { background-position: -360px -340px; } +.emoji-curly_loop { background-position: 0 -360px; } +.emoji-currency_exchange { background-position: -20px -360px; } +.emoji-curry { background-position: -40px -360px; } +.emoji-custard { background-position: -60px -360px; } +.emoji-customs { background-position: -80px -360px; } +.emoji-cyclone { background-position: -100px -360px; } +.emoji-dagger { background-position: -120px -360px; } +.emoji-dancer { background-position: -140px -360px; } +.emoji-dancer_tone1 { background-position: -160px -360px; } +.emoji-dancer_tone2 { background-position: -180px -360px; } +.emoji-dancer_tone3 { background-position: -200px -360px; } +.emoji-dancer_tone4 { background-position: -220px -360px; } +.emoji-dancer_tone5 { background-position: -240px -360px; } +.emoji-dancers { background-position: -260px -360px; } +.emoji-dango { background-position: -280px -360px; } +.emoji-dark_sunglasses { background-position: -300px -360px; } +.emoji-dart { background-position: -320px -360px; } +.emoji-dash { background-position: -340px -360px; } +.emoji-date { background-position: -360px -360px; } +.emoji-deciduous_tree { background-position: -380px 0; } +.emoji-deer { background-position: -380px -20px; } +.emoji-department_store { background-position: -380px -40px; } +.emoji-desert { background-position: -380px -60px; } +.emoji-desktop { background-position: -380px -80px; } +.emoji-diamond_shape_with_a_dot_inside { background-position: -380px -100px; } +.emoji-diamonds { background-position: -380px -120px; } +.emoji-disappointed { background-position: -380px -140px; } +.emoji-disappointed_relieved { background-position: -380px -160px; } +.emoji-dividers { background-position: -380px -180px; } +.emoji-dizzy { background-position: -380px -200px; } +.emoji-dizzy_face { background-position: -380px -220px; } +.emoji-do_not_litter { background-position: -380px -240px; } +.emoji-dog { background-position: -380px -260px; } +.emoji-dog2 { background-position: -380px -280px; } +.emoji-dollar { background-position: -380px -300px; } +.emoji-dolls { background-position: -380px -320px; } +.emoji-dolphin { background-position: -380px -340px; } +.emoji-door { background-position: -380px -360px; } +.emoji-doughnut { background-position: 0 -380px; } +.emoji-dove { background-position: -20px -380px; } +.emoji-dragon { background-position: -40px -380px; } +.emoji-dragon_face { background-position: -60px -380px; } +.emoji-dress { background-position: -80px -380px; } +.emoji-dromedary_camel { background-position: -100px -380px; } +.emoji-drooling_face { background-position: -120px -380px; } +.emoji-droplet { background-position: -140px -380px; } +.emoji-drum { background-position: -160px -380px; } +.emoji-duck { background-position: -180px -380px; } +.emoji-dvd { background-position: -200px -380px; } +.emoji-e-mail { background-position: -220px -380px; } +.emoji-eagle { background-position: -240px -380px; } +.emoji-ear { background-position: -260px -380px; } +.emoji-ear_of_rice { background-position: -280px -380px; } +.emoji-ear_tone1 { background-position: -300px -380px; } +.emoji-ear_tone2 { background-position: -320px -380px; } +.emoji-ear_tone3 { background-position: -340px -380px; } +.emoji-ear_tone4 { background-position: -360px -380px; } +.emoji-ear_tone5 { background-position: -380px -380px; } +.emoji-earth_africa { background-position: -400px 0; } +.emoji-earth_americas { background-position: -400px -20px; } +.emoji-earth_asia { background-position: -400px -40px; } +.emoji-egg { background-position: -400px -60px; } +.emoji-eggplant { background-position: -400px -80px; } +.emoji-eight { background-position: -400px -100px; } +.emoji-eight_pointed_black_star { background-position: -400px -120px; } +.emoji-eight_spoked_asterisk { background-position: -400px -140px; } +.emoji-eject { background-position: -400px -160px; } +.emoji-electric_plug { background-position: -400px -180px; } +.emoji-elephant { background-position: -400px -200px; } +.emoji-end { background-position: -400px -220px; } +.emoji-envelope { background-position: -400px -240px; } +.emoji-envelope_with_arrow { background-position: -400px -260px; } +.emoji-euro { background-position: -400px -280px; } +.emoji-european_castle { background-position: -400px -300px; } +.emoji-european_post_office { background-position: -400px -320px; } +.emoji-evergreen_tree { background-position: -400px -340px; } +.emoji-exclamation { background-position: -400px -360px; } +.emoji-expressionless { background-position: -400px -380px; } +.emoji-eye { background-position: 0 -400px; } +.emoji-eye_in_speech_bubble { background-position: -20px -400px; } +.emoji-eyeglasses { background-position: -40px -400px; } +.emoji-eyes { background-position: -60px -400px; } +.emoji-face_palm { background-position: -80px -400px; } +.emoji-face_palm_tone1 { background-position: -100px -400px; } +.emoji-face_palm_tone2 { background-position: -120px -400px; } +.emoji-face_palm_tone3 { background-position: -140px -400px; } +.emoji-face_palm_tone4 { background-position: -160px -400px; } +.emoji-face_palm_tone5 { background-position: -180px -400px; } +.emoji-factory { background-position: -200px -400px; } +.emoji-fallen_leaf { background-position: -220px -400px; } +.emoji-family { background-position: -240px -400px; } +.emoji-family_mmb { background-position: -260px -400px; } +.emoji-family_mmbb { background-position: -280px -400px; } +.emoji-family_mmg { background-position: -300px -400px; } +.emoji-family_mmgb { background-position: -320px -400px; } +.emoji-family_mmgg { background-position: -340px -400px; } +.emoji-family_mwbb { background-position: -360px -400px; } +.emoji-family_mwg { background-position: -380px -400px; } +.emoji-family_mwgb { background-position: -400px -400px; } +.emoji-family_mwgg { background-position: -420px 0; } +.emoji-family_wwb { background-position: -420px -20px; } +.emoji-family_wwbb { background-position: -420px -40px; } +.emoji-family_wwg { background-position: -420px -60px; } +.emoji-family_wwgb { background-position: -420px -80px; } +.emoji-family_wwgg { background-position: -420px -100px; } +.emoji-fast_forward { background-position: -420px -120px; } +.emoji-fax { background-position: -420px -140px; } +.emoji-fearful { background-position: -420px -160px; } +.emoji-feet { background-position: -420px -180px; } +.emoji-fencer { background-position: -420px -200px; } +.emoji-ferris_wheel { background-position: -420px -220px; } +.emoji-ferry { background-position: -420px -240px; } +.emoji-field_hockey { background-position: -420px -260px; } +.emoji-file_cabinet { background-position: -420px -280px; } +.emoji-file_folder { background-position: -420px -300px; } +.emoji-film_frames { background-position: -420px -320px; } +.emoji-fingers_crossed { background-position: -420px -340px; } +.emoji-fingers_crossed_tone1 { background-position: -420px -360px; } +.emoji-fingers_crossed_tone2 { background-position: -420px -380px; } +.emoji-fingers_crossed_tone3 { background-position: -420px -400px; } +.emoji-fingers_crossed_tone4 { background-position: 0 -420px; } +.emoji-fingers_crossed_tone5 { background-position: -20px -420px; } +.emoji-fire { background-position: -40px -420px; } +.emoji-fire_engine { background-position: -60px -420px; } +.emoji-fireworks { background-position: -80px -420px; } +.emoji-first_place { background-position: -100px -420px; } +.emoji-first_quarter_moon { background-position: -120px -420px; } +.emoji-first_quarter_moon_with_face { background-position: -140px -420px; } +.emoji-fish { background-position: -160px -420px; } +.emoji-fish_cake { background-position: -180px -420px; } +.emoji-fishing_pole_and_fish { background-position: -200px -420px; } +.emoji-fist { background-position: -220px -420px; } +.emoji-fist_tone1 { background-position: -240px -420px; } +.emoji-fist_tone2 { background-position: -260px -420px; } +.emoji-fist_tone3 { background-position: -280px -420px; } +.emoji-fist_tone4 { background-position: -300px -420px; } +.emoji-fist_tone5 { background-position: -320px -420px; } +.emoji-five { background-position: -340px -420px; } +.emoji-flag_ac { background-position: -360px -420px; } +.emoji-flag_ad { background-position: -380px -420px; } +.emoji-flag_ae { background-position: -400px -420px; } +.emoji-flag_af { background-position: -420px -420px; } +.emoji-flag_ag { background-position: -440px 0; } +.emoji-flag_ai { background-position: -440px -20px; } +.emoji-flag_al { background-position: -440px -40px; } +.emoji-flag_am { background-position: -440px -60px; } +.emoji-flag_ao { background-position: -440px -80px; } +.emoji-flag_aq { background-position: -440px -100px; } +.emoji-flag_ar { background-position: -440px -120px; } +.emoji-flag_as { background-position: -440px -140px; } +.emoji-flag_at { background-position: -440px -160px; } +.emoji-flag_au { background-position: -440px -180px; } +.emoji-flag_aw { background-position: -440px -200px; } +.emoji-flag_ax { background-position: -440px -220px; } +.emoji-flag_az { background-position: -440px -240px; } +.emoji-flag_ba { background-position: -440px -260px; } +.emoji-flag_bb { background-position: -440px -280px; } +.emoji-flag_bd { background-position: -440px -300px; } +.emoji-flag_be { background-position: -440px -320px; } +.emoji-flag_bf { background-position: -440px -340px; } +.emoji-flag_bg { background-position: -440px -360px; } +.emoji-flag_bh { background-position: -440px -380px; } +.emoji-flag_bi { background-position: -440px -400px; } +.emoji-flag_bj { background-position: -440px -420px; } +.emoji-flag_bl { background-position: 0 -440px; } +.emoji-flag_black { background-position: -20px -440px; } +.emoji-flag_bm { background-position: -40px -440px; } +.emoji-flag_bn { background-position: -60px -440px; } +.emoji-flag_bo { background-position: -80px -440px; } +.emoji-flag_bq { background-position: -100px -440px; } +.emoji-flag_br { background-position: -120px -440px; } +.emoji-flag_bs { background-position: -140px -440px; } +.emoji-flag_bt { background-position: -160px -440px; } +.emoji-flag_bv { background-position: -180px -440px; } +.emoji-flag_bw { background-position: -200px -440px; } +.emoji-flag_by { background-position: -220px -440px; } +.emoji-flag_bz { background-position: -240px -440px; } +.emoji-flag_ca { background-position: -260px -440px; } +.emoji-flag_cc { background-position: -280px -440px; } +.emoji-flag_cd { background-position: -300px -440px; } +.emoji-flag_cf { background-position: -320px -440px; } +.emoji-flag_cg { background-position: -340px -440px; } +.emoji-flag_ch { background-position: -360px -440px; } +.emoji-flag_ci { background-position: -380px -440px; } +.emoji-flag_ck { background-position: -400px -440px; } +.emoji-flag_cl { background-position: -420px -440px; } +.emoji-flag_cm { background-position: -440px -440px; } +.emoji-flag_cn { background-position: -460px 0; } +.emoji-flag_co { background-position: -460px -20px; } +.emoji-flag_cp { background-position: -460px -40px; } +.emoji-flag_cr { background-position: -460px -60px; } +.emoji-flag_cu { background-position: -460px -80px; } +.emoji-flag_cv { background-position: -460px -100px; } +.emoji-flag_cw { background-position: -460px -120px; } +.emoji-flag_cx { background-position: -460px -140px; } +.emoji-flag_cy { background-position: -460px -160px; } +.emoji-flag_cz { background-position: -460px -180px; } +.emoji-flag_de { background-position: -460px -200px; } +.emoji-flag_dg { background-position: -460px -220px; } +.emoji-flag_dj { background-position: -460px -240px; } +.emoji-flag_dk { background-position: -460px -260px; } +.emoji-flag_dm { background-position: -460px -280px; } +.emoji-flag_do { background-position: -460px -300px; } +.emoji-flag_dz { background-position: -460px -320px; } +.emoji-flag_ea { background-position: -460px -340px; } +.emoji-flag_ec { background-position: -460px -360px; } +.emoji-flag_ee { background-position: -460px -380px; } +.emoji-flag_eg { background-position: -460px -400px; } +.emoji-flag_eh { background-position: -460px -420px; } +.emoji-flag_er { background-position: -460px -440px; } +.emoji-flag_es { background-position: 0 -460px; } +.emoji-flag_et { background-position: -20px -460px; } +.emoji-flag_eu { background-position: -40px -460px; } +.emoji-flag_fi { background-position: -60px -460px; } +.emoji-flag_fj { background-position: -80px -460px; } +.emoji-flag_fk { background-position: -100px -460px; } +.emoji-flag_fm { background-position: -120px -460px; } +.emoji-flag_fo { background-position: -140px -460px; } +.emoji-flag_fr { background-position: -160px -460px; } +.emoji-flag_ga { background-position: -180px -460px; } +.emoji-flag_gb { background-position: -200px -460px; } +.emoji-flag_gd { background-position: -220px -460px; } +.emoji-flag_ge { background-position: -240px -460px; } +.emoji-flag_gf { background-position: -260px -460px; } +.emoji-flag_gg { background-position: -280px -460px; } +.emoji-flag_gh { background-position: -300px -460px; } +.emoji-flag_gi { background-position: -320px -460px; } +.emoji-flag_gl { background-position: -340px -460px; } +.emoji-flag_gm { background-position: -360px -460px; } +.emoji-flag_gn { background-position: -380px -460px; } +.emoji-flag_gp { background-position: -400px -460px; } +.emoji-flag_gq { background-position: -420px -460px; } +.emoji-flag_gr { background-position: -440px -460px; } +.emoji-flag_gs { background-position: -460px -460px; } +.emoji-flag_gt { background-position: -480px 0; } +.emoji-flag_gu { background-position: -480px -20px; } +.emoji-flag_gw { background-position: -480px -40px; } +.emoji-flag_gy { background-position: -480px -60px; } +.emoji-flag_hk { background-position: -480px -80px; } +.emoji-flag_hm { background-position: -480px -100px; } +.emoji-flag_hn { background-position: -480px -120px; } +.emoji-flag_hr { background-position: -480px -140px; } +.emoji-flag_ht { background-position: -480px -160px; } +.emoji-flag_hu { background-position: -480px -180px; } +.emoji-flag_ic { background-position: -480px -200px; } +.emoji-flag_id { background-position: -480px -220px; } +.emoji-flag_ie { background-position: -480px -240px; } +.emoji-flag_il { background-position: -480px -260px; } +.emoji-flag_im { background-position: -480px -280px; } +.emoji-flag_in { background-position: -480px -300px; } +.emoji-flag_io { background-position: -480px -320px; } +.emoji-flag_iq { background-position: -480px -340px; } +.emoji-flag_ir { background-position: -480px -360px; } +.emoji-flag_is { background-position: -480px -380px; } +.emoji-flag_it { background-position: -480px -400px; } +.emoji-flag_je { background-position: -480px -420px; } +.emoji-flag_jm { background-position: -480px -440px; } +.emoji-flag_jo { background-position: -480px -460px; } +.emoji-flag_jp { background-position: 0 -480px; } +.emoji-flag_ke { background-position: -20px -480px; } +.emoji-flag_kg { background-position: -40px -480px; } +.emoji-flag_kh { background-position: -60px -480px; } +.emoji-flag_ki { background-position: -80px -480px; } +.emoji-flag_km { background-position: -100px -480px; } +.emoji-flag_kn { background-position: -120px -480px; } +.emoji-flag_kp { background-position: -140px -480px; } +.emoji-flag_kr { background-position: -160px -480px; } +.emoji-flag_kw { background-position: -180px -480px; } +.emoji-flag_ky { background-position: -200px -480px; } +.emoji-flag_kz { background-position: -220px -480px; } +.emoji-flag_la { background-position: -240px -480px; } +.emoji-flag_lb { background-position: -260px -480px; } +.emoji-flag_lc { background-position: -280px -480px; } +.emoji-flag_li { background-position: -300px -480px; } +.emoji-flag_lk { background-position: -320px -480px; } +.emoji-flag_lr { background-position: -340px -480px; } +.emoji-flag_ls { background-position: -360px -480px; } +.emoji-flag_lt { background-position: -380px -480px; } +.emoji-flag_lu { background-position: -400px -480px; } +.emoji-flag_lv { background-position: -420px -480px; } +.emoji-flag_ly { background-position: -440px -480px; } +.emoji-flag_ma { background-position: -460px -480px; } +.emoji-flag_mc { background-position: -480px -480px; } +.emoji-flag_md { background-position: -500px 0; } +.emoji-flag_me { background-position: -500px -20px; } +.emoji-flag_mf { background-position: -500px -40px; } +.emoji-flag_mg { background-position: -500px -60px; } +.emoji-flag_mh { background-position: -500px -80px; } +.emoji-flag_mk { background-position: -500px -100px; } +.emoji-flag_ml { background-position: -500px -120px; } +.emoji-flag_mm { background-position: -500px -140px; } +.emoji-flag_mn { background-position: -500px -160px; } +.emoji-flag_mo { background-position: -500px -180px; } +.emoji-flag_mp { background-position: -500px -200px; } +.emoji-flag_mq { background-position: -500px -220px; } +.emoji-flag_mr { background-position: -500px -240px; } +.emoji-flag_ms { background-position: -500px -260px; } +.emoji-flag_mt { background-position: -500px -280px; } +.emoji-flag_mu { background-position: -500px -300px; } +.emoji-flag_mv { background-position: -500px -320px; } +.emoji-flag_mw { background-position: -500px -340px; } +.emoji-flag_mx { background-position: -500px -360px; } +.emoji-flag_my { background-position: -500px -380px; } +.emoji-flag_mz { background-position: -500px -400px; } +.emoji-flag_na { background-position: -500px -420px; } +.emoji-flag_nc { background-position: -500px -440px; } +.emoji-flag_ne { background-position: -500px -460px; } +.emoji-flag_nf { background-position: -500px -480px; } +.emoji-flag_ng { background-position: 0 -500px; } +.emoji-flag_ni { background-position: -20px -500px; } +.emoji-flag_nl { background-position: -40px -500px; } +.emoji-flag_no { background-position: -60px -500px; } +.emoji-flag_np { background-position: -80px -500px; } +.emoji-flag_nr { background-position: -100px -500px; } +.emoji-flag_nu { background-position: -120px -500px; } +.emoji-flag_nz { background-position: -140px -500px; } +.emoji-flag_om { background-position: -160px -500px; } +.emoji-flag_pa { background-position: -180px -500px; } +.emoji-flag_pe { background-position: -200px -500px; } +.emoji-flag_pf { background-position: -220px -500px; } +.emoji-flag_pg { background-position: -240px -500px; } +.emoji-flag_ph { background-position: -260px -500px; } +.emoji-flag_pk { background-position: -280px -500px; } +.emoji-flag_pl { background-position: -300px -500px; } +.emoji-flag_pm { background-position: -320px -500px; } +.emoji-flag_pn { background-position: -340px -500px; } +.emoji-flag_pr { background-position: -360px -500px; } +.emoji-flag_ps { background-position: -380px -500px; } +.emoji-flag_pt { background-position: -400px -500px; } +.emoji-flag_pw { background-position: -420px -500px; } +.emoji-flag_py { background-position: -440px -500px; } +.emoji-flag_qa { background-position: -460px -500px; } +.emoji-flag_re { background-position: -480px -500px; } +.emoji-flag_ro { background-position: -500px -500px; } +.emoji-flag_rs { background-position: -520px 0; } +.emoji-flag_ru { background-position: -520px -20px; } +.emoji-flag_rw { background-position: -520px -40px; } +.emoji-flag_sa { background-position: -520px -60px; } +.emoji-flag_sb { background-position: -520px -80px; } +.emoji-flag_sc { background-position: -520px -100px; } +.emoji-flag_sd { background-position: -520px -120px; } +.emoji-flag_se { background-position: -520px -140px; } +.emoji-flag_sg { background-position: -520px -160px; } +.emoji-flag_sh { background-position: -520px -180px; } +.emoji-flag_si { background-position: -520px -200px; } +.emoji-flag_sj { background-position: -520px -220px; } +.emoji-flag_sk { background-position: -520px -240px; } +.emoji-flag_sl { background-position: -520px -260px; } +.emoji-flag_sm { background-position: -520px -280px; } +.emoji-flag_sn { background-position: -520px -300px; } +.emoji-flag_so { background-position: -520px -320px; } +.emoji-flag_sr { background-position: -520px -340px; } +.emoji-flag_ss { background-position: -520px -360px; } +.emoji-flag_st { background-position: -520px -380px; } +.emoji-flag_sv { background-position: -520px -400px; } +.emoji-flag_sx { background-position: -520px -420px; } +.emoji-flag_sy { background-position: -520px -440px; } +.emoji-flag_sz { background-position: -520px -460px; } +.emoji-flag_ta { background-position: -520px -480px; } +.emoji-flag_tc { background-position: -520px -500px; } +.emoji-flag_td { background-position: 0 -520px; } +.emoji-flag_tf { background-position: -20px -520px; } +.emoji-flag_tg { background-position: -40px -520px; } +.emoji-flag_th { background-position: -60px -520px; } +.emoji-flag_tj { background-position: -80px -520px; } +.emoji-flag_tk { background-position: -100px -520px; } +.emoji-flag_tl { background-position: -120px -520px; } +.emoji-flag_tm { background-position: -140px -520px; } +.emoji-flag_tn { background-position: -160px -520px; } +.emoji-flag_to { background-position: -180px -520px; } +.emoji-flag_tr { background-position: -200px -520px; } +.emoji-flag_tt { background-position: -220px -520px; } +.emoji-flag_tv { background-position: -240px -520px; } +.emoji-flag_tw { background-position: -260px -520px; } +.emoji-flag_tz { background-position: -280px -520px; } +.emoji-flag_ua { background-position: -300px -520px; } +.emoji-flag_ug { background-position: -320px -520px; } +.emoji-flag_um { background-position: -340px -520px; } +.emoji-flag_us { background-position: -360px -520px; } +.emoji-flag_uy { background-position: -380px -520px; } +.emoji-flag_uz { background-position: -400px -520px; } +.emoji-flag_va { background-position: -420px -520px; } +.emoji-flag_vc { background-position: -440px -520px; } +.emoji-flag_ve { background-position: -460px -520px; } +.emoji-flag_vg { background-position: -480px -520px; } +.emoji-flag_vi { background-position: -500px -520px; } +.emoji-flag_vn { background-position: -520px -520px; } +.emoji-flag_vu { background-position: -540px 0; } +.emoji-flag_wf { background-position: -540px -20px; } +.emoji-flag_white { background-position: -540px -40px; } +.emoji-flag_ws { background-position: -540px -60px; } +.emoji-flag_xk { background-position: -540px -80px; } +.emoji-flag_ye { background-position: -540px -100px; } +.emoji-flag_yt { background-position: -540px -120px; } +.emoji-flag_za { background-position: -540px -140px; } +.emoji-flag_zm { background-position: -540px -160px; } +.emoji-flag_zw { background-position: -540px -180px; } +.emoji-flags { background-position: -540px -200px; } +.emoji-flashlight { background-position: -540px -220px; } +.emoji-fleur-de-lis { background-position: -540px -240px; } +.emoji-floppy_disk { background-position: -540px -260px; } +.emoji-flower_playing_cards { background-position: -540px -280px; } +.emoji-flushed { background-position: -540px -300px; } +.emoji-fog { background-position: -540px -320px; } +.emoji-foggy { background-position: -540px -340px; } +.emoji-football { background-position: -540px -360px; } +.emoji-footprints { background-position: -540px -380px; } +.emoji-fork_and_knife { background-position: -540px -400px; } +.emoji-fork_knife_plate { background-position: -540px -420px; } +.emoji-fountain { background-position: -540px -440px; } +.emoji-four { background-position: -540px -460px; } +.emoji-four_leaf_clover { background-position: -540px -480px; } +.emoji-fox { background-position: -540px -500px; } +.emoji-frame_photo { background-position: -540px -520px; } +.emoji-free { background-position: 0 -540px; } +.emoji-french_bread { background-position: -20px -540px; } +.emoji-fried_shrimp { background-position: -40px -540px; } +.emoji-fries { background-position: -60px -540px; } +.emoji-frog { background-position: -80px -540px; } +.emoji-frowning { background-position: -100px -540px; } +.emoji-frowning2 { background-position: -120px -540px; } +.emoji-fuelpump { background-position: -140px -540px; } +.emoji-full_moon { background-position: -160px -540px; } +.emoji-full_moon_with_face { background-position: -180px -540px; } +.emoji-game_die { background-position: -200px -540px; } +.emoji-gay_pride_flag { background-position: -220px -540px; } +.emoji-gear { background-position: -240px -540px; } +.emoji-gem { background-position: -260px -540px; } +.emoji-gemini { background-position: -280px -540px; } +.emoji-ghost { background-position: -300px -540px; } +.emoji-gift { background-position: -320px -540px; } +.emoji-gift_heart { background-position: -340px -540px; } +.emoji-girl { background-position: -360px -540px; } +.emoji-girl_tone1 { background-position: -380px -540px; } +.emoji-girl_tone2 { background-position: -400px -540px; } +.emoji-girl_tone3 { background-position: -420px -540px; } +.emoji-girl_tone4 { background-position: -440px -540px; } +.emoji-girl_tone5 { background-position: -460px -540px; } +.emoji-globe_with_meridians { background-position: -480px -540px; } +.emoji-goal { background-position: -500px -540px; } +.emoji-goat { background-position: -520px -540px; } +.emoji-golf { background-position: -540px -540px; } +.emoji-golfer { background-position: -560px 0; } +.emoji-gorilla { background-position: -560px -20px; } +.emoji-grapes { background-position: -560px -40px; } +.emoji-green_apple { background-position: -560px -60px; } +.emoji-green_book { background-position: -560px -80px; } +.emoji-green_heart { background-position: -560px -100px; } +.emoji-grey_exclamation { background-position: -560px -120px; } +.emoji-grey_question { background-position: -560px -140px; } +.emoji-grimacing { background-position: -560px -160px; } +.emoji-grin { background-position: -560px -180px; } +.emoji-grinning { background-position: -560px -200px; } +.emoji-guardsman { background-position: -560px -220px; } +.emoji-guardsman_tone1 { background-position: -560px -240px; } +.emoji-guardsman_tone2 { background-position: -560px -260px; } +.emoji-guardsman_tone3 { background-position: -560px -280px; } +.emoji-guardsman_tone4 { background-position: -560px -300px; } +.emoji-guardsman_tone5 { background-position: -560px -320px; } +.emoji-guitar { background-position: -560px -340px; } +.emoji-gun { background-position: -560px -360px; } +.emoji-haircut { background-position: -560px -380px; } +.emoji-haircut_tone1 { background-position: -560px -400px; } +.emoji-haircut_tone2 { background-position: -560px -420px; } +.emoji-haircut_tone3 { background-position: -560px -440px; } +.emoji-haircut_tone4 { background-position: -560px -460px; } +.emoji-haircut_tone5 { background-position: -560px -480px; } +.emoji-hamburger { background-position: -560px -500px; } +.emoji-hammer { background-position: -560px -520px; } +.emoji-hammer_pick { background-position: -560px -540px; } +.emoji-hamster { background-position: 0 -560px; } +.emoji-hand_splayed { background-position: -20px -560px; } +.emoji-hand_splayed_tone1 { background-position: -40px -560px; } +.emoji-hand_splayed_tone2 { background-position: -60px -560px; } +.emoji-hand_splayed_tone3 { background-position: -80px -560px; } +.emoji-hand_splayed_tone4 { background-position: -100px -560px; } +.emoji-hand_splayed_tone5 { background-position: -120px -560px; } +.emoji-handbag { background-position: -140px -560px; } +.emoji-handball { background-position: -160px -560px; } +.emoji-handball_tone1 { background-position: -180px -560px; } +.emoji-handball_tone2 { background-position: -200px -560px; } +.emoji-handball_tone3 { background-position: -220px -560px; } +.emoji-handball_tone4 { background-position: -240px -560px; } +.emoji-handball_tone5 { background-position: -260px -560px; } +.emoji-handshake { background-position: -280px -560px; } +.emoji-handshake_tone1 { background-position: -300px -560px; } +.emoji-handshake_tone2 { background-position: -320px -560px; } +.emoji-handshake_tone3 { background-position: -340px -560px; } +.emoji-handshake_tone4 { background-position: -360px -560px; } +.emoji-handshake_tone5 { background-position: -380px -560px; } +.emoji-hash { background-position: -400px -560px; } +.emoji-hatched_chick { background-position: -420px -560px; } +.emoji-hatching_chick { background-position: -440px -560px; } +.emoji-head_bandage { background-position: -460px -560px; } +.emoji-headphones { background-position: -480px -560px; } +.emoji-hear_no_evil { background-position: -500px -560px; } +.emoji-heart { background-position: -520px -560px; } +.emoji-heart_decoration { background-position: -540px -560px; } +.emoji-heart_exclamation { background-position: -560px -560px; } +.emoji-heart_eyes { background-position: -580px 0; } +.emoji-heart_eyes_cat { background-position: -580px -20px; } +.emoji-heartbeat { background-position: -580px -40px; } +.emoji-heartpulse { background-position: -580px -60px; } +.emoji-hearts { background-position: -580px -80px; } +.emoji-heavy_check_mark { background-position: -580px -100px; } +.emoji-heavy_division_sign { background-position: -580px -120px; } +.emoji-heavy_dollar_sign { background-position: -580px -140px; } +.emoji-heavy_minus_sign { background-position: -580px -160px; } +.emoji-heavy_multiplication_x { background-position: -580px -180px; } +.emoji-heavy_plus_sign { background-position: -580px -200px; } +.emoji-helicopter { background-position: -580px -220px; } +.emoji-helmet_with_cross { background-position: -580px -240px; } +.emoji-herb { background-position: -580px -260px; } +.emoji-hibiscus { background-position: -580px -280px; } +.emoji-high_brightness { background-position: -580px -300px; } +.emoji-high_heel { background-position: -580px -320px; } +.emoji-hockey { background-position: -580px -340px; } +.emoji-hole { background-position: -580px -360px; } +.emoji-homes { background-position: -580px -380px; } +.emoji-honey_pot { background-position: -580px -400px; } +.emoji-horse { background-position: -580px -420px; } +.emoji-horse_racing { background-position: -580px -440px; } +.emoji-horse_racing_tone1 { background-position: -580px -460px; } +.emoji-horse_racing_tone2 { background-position: -580px -480px; } +.emoji-horse_racing_tone3 { background-position: -580px -500px; } +.emoji-horse_racing_tone4 { background-position: -580px -520px; } +.emoji-horse_racing_tone5 { background-position: -580px -540px; } +.emoji-hospital { background-position: -580px -560px; } +.emoji-hot_pepper { background-position: 0 -580px; } +.emoji-hotdog { background-position: -20px -580px; } +.emoji-hotel { background-position: -40px -580px; } +.emoji-hotsprings { background-position: -60px -580px; } +.emoji-hourglass { background-position: -80px -580px; } +.emoji-hourglass_flowing_sand { background-position: -100px -580px; } +.emoji-house { background-position: -120px -580px; } +.emoji-house_abandoned { background-position: -140px -580px; } +.emoji-house_with_garden { background-position: -160px -580px; } +.emoji-hugging { background-position: -180px -580px; } +.emoji-hushed { background-position: -200px -580px; } +.emoji-ice_cream { background-position: -220px -580px; } +.emoji-ice_skate { background-position: -240px -580px; } +.emoji-icecream { background-position: -260px -580px; } +.emoji-id { background-position: -280px -580px; } +.emoji-ideograph_advantage { background-position: -300px -580px; } +.emoji-imp { background-position: -320px -580px; } +.emoji-inbox_tray { background-position: -340px -580px; } +.emoji-incoming_envelope { background-position: -360px -580px; } +.emoji-information_desk_person { background-position: -380px -580px; } +.emoji-information_desk_person_tone1 { background-position: -400px -580px; } +.emoji-information_desk_person_tone2 { background-position: -420px -580px; } +.emoji-information_desk_person_tone3 { background-position: -440px -580px; } +.emoji-information_desk_person_tone4 { background-position: -460px -580px; } +.emoji-information_desk_person_tone5 { background-position: -480px -580px; } +.emoji-information_source { background-position: -500px -580px; } +.emoji-innocent { background-position: -520px -580px; } +.emoji-interrobang { background-position: -540px -580px; } +.emoji-iphone { background-position: -560px -580px; } +.emoji-island { background-position: -580px -580px; } +.emoji-izakaya_lantern { background-position: -600px 0; } +.emoji-jack_o_lantern { background-position: -600px -20px; } +.emoji-japan { background-position: -600px -40px; } +.emoji-japanese_castle { background-position: -600px -60px; } +.emoji-japanese_goblin { background-position: -600px -80px; } +.emoji-japanese_ogre { background-position: -600px -100px; } +.emoji-jeans { background-position: -600px -120px; } +.emoji-joy { background-position: -600px -140px; } +.emoji-joy_cat { background-position: -600px -160px; } +.emoji-joystick { background-position: -600px -180px; } +.emoji-juggling { background-position: -600px -200px; } +.emoji-juggling_tone1 { background-position: -600px -220px; } +.emoji-juggling_tone2 { background-position: -600px -240px; } +.emoji-juggling_tone3 { background-position: -600px -260px; } +.emoji-juggling_tone4 { background-position: -600px -280px; } +.emoji-juggling_tone5 { background-position: -600px -300px; } +.emoji-kaaba { background-position: -600px -320px; } +.emoji-key { background-position: -600px -340px; } +.emoji-key2 { background-position: -600px -360px; } +.emoji-keyboard { background-position: -600px -380px; } +.emoji-kimono { background-position: -600px -400px; } +.emoji-kiss { background-position: -600px -420px; } +.emoji-kiss_mm { background-position: -600px -440px; } +.emoji-kiss_ww { background-position: -600px -460px; } +.emoji-kissing { background-position: -600px -480px; } +.emoji-kissing_cat { background-position: -600px -500px; } +.emoji-kissing_closed_eyes { background-position: -600px -520px; } +.emoji-kissing_heart { background-position: -600px -540px; } +.emoji-kissing_smiling_eyes { background-position: -600px -560px; } +.emoji-kiwi { background-position: -600px -580px; } +.emoji-knife { background-position: 0 -600px; } +.emoji-koala { background-position: -20px -600px; } +.emoji-koko { background-position: -40px -600px; } +.emoji-label { background-position: -60px -600px; } +.emoji-large_blue_circle { background-position: -80px -600px; } +.emoji-large_blue_diamond { background-position: -100px -600px; } +.emoji-large_orange_diamond { background-position: -120px -600px; } +.emoji-last_quarter_moon { background-position: -140px -600px; } +.emoji-last_quarter_moon_with_face { background-position: -160px -600px; } +.emoji-laughing { background-position: -180px -600px; } +.emoji-leaves { background-position: -200px -600px; } +.emoji-ledger { background-position: -220px -600px; } +.emoji-left_facing_fist { background-position: -240px -600px; } +.emoji-left_facing_fist_tone1 { background-position: -260px -600px; } +.emoji-left_facing_fist_tone2 { background-position: -280px -600px; } +.emoji-left_facing_fist_tone3 { background-position: -300px -600px; } +.emoji-left_facing_fist_tone4 { background-position: -320px -600px; } +.emoji-left_facing_fist_tone5 { background-position: -340px -600px; } +.emoji-left_luggage { background-position: -360px -600px; } +.emoji-left_right_arrow { background-position: -380px -600px; } +.emoji-leftwards_arrow_with_hook { background-position: -400px -600px; } +.emoji-lemon { background-position: -420px -600px; } +.emoji-leo { background-position: -440px -600px; } +.emoji-leopard { background-position: -460px -600px; } +.emoji-level_slider { background-position: -480px -600px; } +.emoji-levitate { background-position: -500px -600px; } +.emoji-libra { background-position: -520px -600px; } +.emoji-lifter { background-position: -540px -600px; } +.emoji-lifter_tone1 { background-position: -560px -600px; } +.emoji-lifter_tone2 { background-position: -580px -600px; } +.emoji-lifter_tone3 { background-position: -600px -600px; } +.emoji-lifter_tone4 { background-position: -620px 0; } +.emoji-lifter_tone5 { background-position: -620px -20px; } +.emoji-light_rail { background-position: -620px -40px; } +.emoji-link { background-position: -620px -60px; } +.emoji-lion_face { background-position: -620px -80px; } +.emoji-lips { background-position: -620px -100px; } +.emoji-lipstick { background-position: -620px -120px; } +.emoji-lizard { background-position: -620px -140px; } +.emoji-lock { background-position: -620px -160px; } +.emoji-lock_with_ink_pen { background-position: -620px -180px; } +.emoji-lollipop { background-position: -620px -200px; } +.emoji-loop { background-position: -620px -220px; } +.emoji-loud_sound { background-position: -620px -240px; } +.emoji-loudspeaker { background-position: -620px -260px; } +.emoji-love_hotel { background-position: -620px -280px; } +.emoji-love_letter { background-position: -620px -300px; } +.emoji-low_brightness { background-position: -620px -320px; } +.emoji-lying_face { background-position: -620px -340px; } +.emoji-m { background-position: -620px -360px; } +.emoji-mag { background-position: -620px -380px; } +.emoji-mag_right { background-position: -620px -400px; } +.emoji-mahjong { background-position: -620px -420px; } +.emoji-mailbox { background-position: -620px -440px; } +.emoji-mailbox_closed { background-position: -620px -460px; } +.emoji-mailbox_with_mail { background-position: -620px -480px; } +.emoji-mailbox_with_no_mail { background-position: -620px -500px; } +.emoji-man { background-position: -620px -520px; } +.emoji-man_dancing { background-position: -620px -540px; } +.emoji-man_dancing_tone1 { background-position: -620px -560px; } +.emoji-man_dancing_tone2 { background-position: -620px -580px; } +.emoji-man_dancing_tone3 { background-position: -620px -600px; } +.emoji-man_dancing_tone4 { background-position: 0 -620px; } +.emoji-man_dancing_tone5 { background-position: -20px -620px; } +.emoji-man_in_tuxedo { background-position: -40px -620px; } +.emoji-man_in_tuxedo_tone1 { background-position: -60px -620px; } +.emoji-man_in_tuxedo_tone2 { background-position: -80px -620px; } +.emoji-man_in_tuxedo_tone3 { background-position: -100px -620px; } +.emoji-man_in_tuxedo_tone4 { background-position: -120px -620px; } +.emoji-man_in_tuxedo_tone5 { background-position: -140px -620px; } +.emoji-man_tone1 { background-position: -160px -620px; } +.emoji-man_tone2 { background-position: -180px -620px; } +.emoji-man_tone3 { background-position: -200px -620px; } +.emoji-man_tone4 { background-position: -220px -620px; } +.emoji-man_tone5 { background-position: -240px -620px; } +.emoji-man_with_gua_pi_mao { background-position: -260px -620px; } +.emoji-man_with_gua_pi_mao_tone1 { background-position: -280px -620px; } +.emoji-man_with_gua_pi_mao_tone2 { background-position: -300px -620px; } +.emoji-man_with_gua_pi_mao_tone3 { background-position: -320px -620px; } +.emoji-man_with_gua_pi_mao_tone4 { background-position: -340px -620px; } +.emoji-man_with_gua_pi_mao_tone5 { background-position: -360px -620px; } +.emoji-man_with_turban { background-position: -380px -620px; } +.emoji-man_with_turban_tone1 { background-position: -400px -620px; } +.emoji-man_with_turban_tone2 { background-position: -420px -620px; } +.emoji-man_with_turban_tone3 { background-position: -440px -620px; } +.emoji-man_with_turban_tone4 { background-position: -460px -620px; } +.emoji-man_with_turban_tone5 { background-position: -480px -620px; } +.emoji-mans_shoe { background-position: -500px -620px; } +.emoji-map { background-position: -520px -620px; } +.emoji-maple_leaf { background-position: -540px -620px; } +.emoji-martial_arts_uniform { background-position: -560px -620px; } +.emoji-mask { background-position: -580px -620px; } +.emoji-massage { background-position: -600px -620px; } +.emoji-massage_tone1 { background-position: -620px -620px; } +.emoji-massage_tone2 { background-position: -640px 0; } +.emoji-massage_tone3 { background-position: -640px -20px; } +.emoji-massage_tone4 { background-position: -640px -40px; } +.emoji-massage_tone5 { background-position: -640px -60px; } +.emoji-meat_on_bone { background-position: -640px -80px; } +.emoji-medal { background-position: -640px -100px; } +.emoji-mega { background-position: -640px -120px; } +.emoji-melon { background-position: -640px -140px; } +.emoji-menorah { background-position: -640px -160px; } +.emoji-mens { background-position: -640px -180px; } +.emoji-metal { background-position: -640px -200px; } +.emoji-metal_tone1 { background-position: -640px -220px; } +.emoji-metal_tone2 { background-position: -640px -240px; } +.emoji-metal_tone3 { background-position: -640px -260px; } +.emoji-metal_tone4 { background-position: -640px -280px; } +.emoji-metal_tone5 { background-position: -640px -300px; } +.emoji-metro { background-position: -640px -320px; } +.emoji-microphone { background-position: -640px -340px; } +.emoji-microphone2 { background-position: -640px -360px; } +.emoji-microscope { background-position: -640px -380px; } +.emoji-middle_finger { background-position: -640px -400px; } +.emoji-middle_finger_tone1 { background-position: -640px -420px; } +.emoji-middle_finger_tone2 { background-position: -640px -440px; } +.emoji-middle_finger_tone3 { background-position: -640px -460px; } +.emoji-middle_finger_tone4 { background-position: -640px -480px; } +.emoji-middle_finger_tone5 { background-position: -640px -500px; } +.emoji-military_medal { background-position: -640px -520px; } +.emoji-milk { background-position: -640px -540px; } +.emoji-milky_way { background-position: -640px -560px; } +.emoji-minibus { background-position: -640px -580px; } +.emoji-minidisc { background-position: -640px -600px; } +.emoji-mobile_phone_off { background-position: -640px -620px; } +.emoji-money_mouth { background-position: 0 -640px; } +.emoji-money_with_wings { background-position: -20px -640px; } +.emoji-moneybag { background-position: -40px -640px; } +.emoji-monkey { background-position: -60px -640px; } +.emoji-monkey_face { background-position: -80px -640px; } +.emoji-monorail { background-position: -100px -640px; } +.emoji-mortar_board { background-position: -120px -640px; } +.emoji-mosque { background-position: -140px -640px; } +.emoji-motor_scooter { background-position: -160px -640px; } +.emoji-motorboat { background-position: -180px -640px; } +.emoji-motorcycle { background-position: -200px -640px; } +.emoji-motorway { background-position: -220px -640px; } +.emoji-mount_fuji { background-position: -240px -640px; } +.emoji-mountain { background-position: -260px -640px; } +.emoji-mountain_bicyclist { background-position: -280px -640px; } +.emoji-mountain_bicyclist_tone1 { background-position: -300px -640px; } +.emoji-mountain_bicyclist_tone2 { background-position: -320px -640px; } +.emoji-mountain_bicyclist_tone3 { background-position: -340px -640px; } +.emoji-mountain_bicyclist_tone4 { background-position: -360px -640px; } +.emoji-mountain_bicyclist_tone5 { background-position: -380px -640px; } +.emoji-mountain_cableway { background-position: -400px -640px; } +.emoji-mountain_railway { background-position: -420px -640px; } +.emoji-mountain_snow { background-position: -440px -640px; } +.emoji-mouse { background-position: -460px -640px; } +.emoji-mouse2 { background-position: -480px -640px; } +.emoji-mouse_three_button { background-position: -500px -640px; } +.emoji-movie_camera { background-position: -520px -640px; } +.emoji-moyai { background-position: -540px -640px; } +.emoji-mrs_claus { background-position: -560px -640px; } +.emoji-mrs_claus_tone1 { background-position: -580px -640px; } +.emoji-mrs_claus_tone2 { background-position: -600px -640px; } +.emoji-mrs_claus_tone3 { background-position: -620px -640px; } +.emoji-mrs_claus_tone4 { background-position: -640px -640px; } +.emoji-mrs_claus_tone5 { background-position: -660px 0; } +.emoji-muscle { background-position: -660px -20px; } +.emoji-muscle_tone1 { background-position: -660px -40px; } +.emoji-muscle_tone2 { background-position: -660px -60px; } +.emoji-muscle_tone3 { background-position: -660px -80px; } +.emoji-muscle_tone4 { background-position: -660px -100px; } +.emoji-muscle_tone5 { background-position: -660px -120px; } +.emoji-mushroom { background-position: -660px -140px; } +.emoji-musical_keyboard { background-position: -660px -160px; } +.emoji-musical_note { background-position: -660px -180px; } +.emoji-musical_score { background-position: -660px -200px; } +.emoji-mute { background-position: -660px -220px; } +.emoji-nail_care { background-position: -660px -240px; } +.emoji-nail_care_tone1 { background-position: -660px -260px; } +.emoji-nail_care_tone2 { background-position: -660px -280px; } +.emoji-nail_care_tone3 { background-position: -660px -300px; } +.emoji-nail_care_tone4 { background-position: -660px -320px; } +.emoji-nail_care_tone5 { background-position: -660px -340px; } +.emoji-name_badge { background-position: -660px -360px; } +.emoji-nauseated_face { background-position: -660px -380px; } +.emoji-necktie { background-position: -660px -400px; } +.emoji-negative_squared_cross_mark { background-position: -660px -420px; } +.emoji-nerd { background-position: -660px -440px; } +.emoji-neutral_face { background-position: -660px -460px; } +.emoji-new { background-position: -660px -480px; } +.emoji-new_moon { background-position: -660px -500px; } +.emoji-new_moon_with_face { background-position: -660px -520px; } +.emoji-newspaper { background-position: -660px -540px; } +.emoji-newspaper2 { background-position: -660px -560px; } +.emoji-ng { background-position: -660px -580px; } +.emoji-night_with_stars { background-position: -660px -600px; } +.emoji-nine { background-position: -660px -620px; } +.emoji-no_bell { background-position: -660px -640px; } +.emoji-no_bicycles { background-position: 0 -660px; } +.emoji-no_entry { background-position: -20px -660px; } +.emoji-no_entry_sign { background-position: -40px -660px; } +.emoji-no_good { background-position: -60px -660px; } +.emoji-no_good_tone1 { background-position: -80px -660px; } +.emoji-no_good_tone2 { background-position: -100px -660px; } +.emoji-no_good_tone3 { background-position: -120px -660px; } +.emoji-no_good_tone4 { background-position: -140px -660px; } +.emoji-no_good_tone5 { background-position: -160px -660px; } +.emoji-no_mobile_phones { background-position: -180px -660px; } +.emoji-no_mouth { background-position: -200px -660px; } +.emoji-no_pedestrians { background-position: -220px -660px; } +.emoji-no_smoking { background-position: -240px -660px; } +.emoji-non-potable_water { background-position: -260px -660px; } +.emoji-nose { background-position: -280px -660px; } +.emoji-nose_tone1 { background-position: -300px -660px; } +.emoji-nose_tone2 { background-position: -320px -660px; } +.emoji-nose_tone3 { background-position: -340px -660px; } +.emoji-nose_tone4 { background-position: -360px -660px; } +.emoji-nose_tone5 { background-position: -380px -660px; } +.emoji-notebook { background-position: -400px -660px; } +.emoji-notebook_with_decorative_cover { background-position: -420px -660px; } +.emoji-notepad_spiral { background-position: -440px -660px; } +.emoji-notes { background-position: -460px -660px; } +.emoji-nut_and_bolt { background-position: -480px -660px; } +.emoji-o { background-position: -500px -660px; } +.emoji-o2 { background-position: -520px -660px; } +.emoji-ocean { background-position: -540px -660px; } +.emoji-octagonal_sign { background-position: -560px -660px; } +.emoji-octopus { background-position: -580px -660px; } +.emoji-oden { background-position: -600px -660px; } +.emoji-office { background-position: -620px -660px; } +.emoji-oil { background-position: -640px -660px; } +.emoji-ok { background-position: -660px -660px; } +.emoji-ok_hand { background-position: -680px 0; } +.emoji-ok_hand_tone1 { background-position: -680px -20px; } +.emoji-ok_hand_tone2 { background-position: -680px -40px; } +.emoji-ok_hand_tone3 { background-position: -680px -60px; } +.emoji-ok_hand_tone4 { background-position: -680px -80px; } +.emoji-ok_hand_tone5 { background-position: -680px -100px; } +.emoji-ok_woman { background-position: -680px -120px; } +.emoji-ok_woman_tone1 { background-position: -680px -140px; } +.emoji-ok_woman_tone2 { background-position: -680px -160px; } +.emoji-ok_woman_tone3 { background-position: -680px -180px; } +.emoji-ok_woman_tone4 { background-position: -680px -200px; } +.emoji-ok_woman_tone5 { background-position: -680px -220px; } +.emoji-older_man { background-position: -680px -240px; } +.emoji-older_man_tone1 { background-position: -680px -260px; } +.emoji-older_man_tone2 { background-position: -680px -280px; } +.emoji-older_man_tone3 { background-position: -680px -300px; } +.emoji-older_man_tone4 { background-position: -680px -320px; } +.emoji-older_man_tone5 { background-position: -680px -340px; } +.emoji-older_woman { background-position: -680px -360px; } +.emoji-older_woman_tone1 { background-position: -680px -380px; } +.emoji-older_woman_tone2 { background-position: -680px -400px; } +.emoji-older_woman_tone3 { background-position: -680px -420px; } +.emoji-older_woman_tone4 { background-position: -680px -440px; } +.emoji-older_woman_tone5 { background-position: -680px -460px; } +.emoji-om_symbol { background-position: -680px -480px; } +.emoji-on { background-position: -680px -500px; } +.emoji-oncoming_automobile { background-position: -680px -520px; } +.emoji-oncoming_bus { background-position: -680px -540px; } +.emoji-oncoming_police_car { background-position: -680px -560px; } +.emoji-oncoming_taxi { background-position: -680px -580px; } +.emoji-one { background-position: -680px -600px; } +.emoji-open_file_folder { background-position: -680px -620px; } +.emoji-open_hands { background-position: -680px -640px; } +.emoji-open_hands_tone1 { background-position: -680px -660px; } +.emoji-open_hands_tone2 { background-position: 0 -680px; } +.emoji-open_hands_tone3 { background-position: -20px -680px; } +.emoji-open_hands_tone4 { background-position: -40px -680px; } +.emoji-open_hands_tone5 { background-position: -60px -680px; } +.emoji-open_mouth { background-position: -80px -680px; } +.emoji-ophiuchus { background-position: -100px -680px; } +.emoji-orange_book { background-position: -120px -680px; } +.emoji-orthodox_cross { background-position: -140px -680px; } +.emoji-outbox_tray { background-position: -160px -680px; } +.emoji-owl { background-position: -180px -680px; } +.emoji-ox { background-position: -200px -680px; } +.emoji-package { background-position: -220px -680px; } +.emoji-page_facing_up { background-position: -240px -680px; } +.emoji-page_with_curl { background-position: -260px -680px; } +.emoji-pager { background-position: -280px -680px; } +.emoji-paintbrush { background-position: -300px -680px; } +.emoji-palm_tree { background-position: -320px -680px; } +.emoji-pancakes { background-position: -340px -680px; } +.emoji-panda_face { background-position: -360px -680px; } +.emoji-paperclip { background-position: -380px -680px; } +.emoji-paperclips { background-position: -400px -680px; } +.emoji-park { background-position: -420px -680px; } +.emoji-parking { background-position: -440px -680px; } +.emoji-part_alternation_mark { background-position: -460px -680px; } +.emoji-partly_sunny { background-position: -480px -680px; } +.emoji-passport_control { background-position: -500px -680px; } +.emoji-pause_button { background-position: -520px -680px; } +.emoji-peace { background-position: -540px -680px; } +.emoji-peach { background-position: -560px -680px; } +.emoji-peanuts { background-position: -580px -680px; } +.emoji-pear { background-position: -600px -680px; } +.emoji-pen_ballpoint { background-position: -620px -680px; } +.emoji-pen_fountain { background-position: -640px -680px; } +.emoji-pencil { background-position: -660px -680px; } +.emoji-pencil2 { background-position: -680px -680px; } +.emoji-penguin { background-position: -700px 0; } +.emoji-pensive { background-position: -700px -20px; } +.emoji-performing_arts { background-position: -700px -40px; } +.emoji-persevere { background-position: -700px -60px; } +.emoji-person_frowning { background-position: -700px -80px; } +.emoji-person_frowning_tone1 { background-position: -700px -100px; } +.emoji-person_frowning_tone2 { background-position: -700px -120px; } +.emoji-person_frowning_tone3 { background-position: -700px -140px; } +.emoji-person_frowning_tone4 { background-position: -700px -160px; } +.emoji-person_frowning_tone5 { background-position: -700px -180px; } +.emoji-person_with_blond_hair { background-position: -700px -200px; } +.emoji-person_with_blond_hair_tone1 { background-position: -700px -220px; } +.emoji-person_with_blond_hair_tone2 { background-position: -700px -240px; } +.emoji-person_with_blond_hair_tone3 { background-position: -700px -260px; } +.emoji-person_with_blond_hair_tone4 { background-position: -700px -280px; } +.emoji-person_with_blond_hair_tone5 { background-position: -700px -300px; } +.emoji-person_with_pouting_face { background-position: -700px -320px; } +.emoji-person_with_pouting_face_tone1 { background-position: -700px -340px; } +.emoji-person_with_pouting_face_tone2 { background-position: -700px -360px; } +.emoji-person_with_pouting_face_tone3 { background-position: -700px -380px; } +.emoji-person_with_pouting_face_tone4 { background-position: -700px -400px; } +.emoji-person_with_pouting_face_tone5 { background-position: -700px -420px; } +.emoji-pick { background-position: -700px -440px; } +.emoji-pig { background-position: -700px -460px; } +.emoji-pig2 { background-position: -700px -480px; } +.emoji-pig_nose { background-position: -700px -500px; } +.emoji-pill { background-position: -700px -520px; } +.emoji-pineapple { background-position: -700px -540px; } +.emoji-ping_pong { background-position: -700px -560px; } +.emoji-pisces { background-position: -700px -580px; } +.emoji-pizza { background-position: -700px -600px; } +.emoji-place_of_worship { background-position: -700px -620px; } +.emoji-play_pause { background-position: -700px -640px; } +.emoji-point_down { background-position: -700px -660px; } +.emoji-point_down_tone1 { background-position: -700px -680px; } +.emoji-point_down_tone2 { background-position: 0 -700px; } +.emoji-point_down_tone3 { background-position: -20px -700px; } +.emoji-point_down_tone4 { background-position: -40px -700px; } +.emoji-point_down_tone5 { background-position: -60px -700px; } +.emoji-point_left { background-position: -80px -700px; } +.emoji-point_left_tone1 { background-position: -100px -700px; } +.emoji-point_left_tone2 { background-position: -120px -700px; } +.emoji-point_left_tone3 { background-position: -140px -700px; } +.emoji-point_left_tone4 { background-position: -160px -700px; } +.emoji-point_left_tone5 { background-position: -180px -700px; } +.emoji-point_right { background-position: -200px -700px; } +.emoji-point_right_tone1 { background-position: -220px -700px; } +.emoji-point_right_tone2 { background-position: -240px -700px; } +.emoji-point_right_tone3 { background-position: -260px -700px; } +.emoji-point_right_tone4 { background-position: -280px -700px; } +.emoji-point_right_tone5 { background-position: -300px -700px; } +.emoji-point_up { background-position: -320px -700px; } +.emoji-point_up_2 { background-position: -340px -700px; } +.emoji-point_up_2_tone1 { background-position: -360px -700px; } +.emoji-point_up_2_tone2 { background-position: -380px -700px; } +.emoji-point_up_2_tone3 { background-position: -400px -700px; } +.emoji-point_up_2_tone4 { background-position: -420px -700px; } +.emoji-point_up_2_tone5 { background-position: -440px -700px; } +.emoji-point_up_tone1 { background-position: -460px -700px; } +.emoji-point_up_tone2 { background-position: -480px -700px; } +.emoji-point_up_tone3 { background-position: -500px -700px; } +.emoji-point_up_tone4 { background-position: -520px -700px; } +.emoji-point_up_tone5 { background-position: -540px -700px; } +.emoji-police_car { background-position: -560px -700px; } +.emoji-poodle { background-position: -580px -700px; } +.emoji-poop { background-position: -600px -700px; } +.emoji-popcorn { background-position: -620px -700px; } +.emoji-post_office { background-position: -640px -700px; } +.emoji-postal_horn { background-position: -660px -700px; } +.emoji-postbox { background-position: -680px -700px; } +.emoji-potable_water { background-position: -700px -700px; } +.emoji-potato { background-position: -720px 0; } +.emoji-pouch { background-position: -720px -20px; } +.emoji-poultry_leg { background-position: -720px -40px; } +.emoji-pound { background-position: -720px -60px; } +.emoji-pouting_cat { background-position: -720px -80px; } +.emoji-pray { background-position: -720px -100px; } +.emoji-pray_tone1 { background-position: -720px -120px; } +.emoji-pray_tone2 { background-position: -720px -140px; } +.emoji-pray_tone3 { background-position: -720px -160px; } +.emoji-pray_tone4 { background-position: -720px -180px; } +.emoji-pray_tone5 { background-position: -720px -200px; } +.emoji-prayer_beads { background-position: -720px -220px; } +.emoji-pregnant_woman { background-position: -720px -240px; } +.emoji-pregnant_woman_tone1 { background-position: -720px -260px; } +.emoji-pregnant_woman_tone2 { background-position: -720px -280px; } +.emoji-pregnant_woman_tone3 { background-position: -720px -300px; } +.emoji-pregnant_woman_tone4 { background-position: -720px -320px; } +.emoji-pregnant_woman_tone5 { background-position: -720px -340px; } +.emoji-prince { background-position: -720px -360px; } +.emoji-prince_tone1 { background-position: -720px -380px; } +.emoji-prince_tone2 { background-position: -720px -400px; } +.emoji-prince_tone3 { background-position: -720px -420px; } +.emoji-prince_tone4 { background-position: -720px -440px; } +.emoji-prince_tone5 { background-position: -720px -460px; } +.emoji-princess { background-position: -720px -480px; } +.emoji-princess_tone1 { background-position: -720px -500px; } +.emoji-princess_tone2 { background-position: -720px -520px; } +.emoji-princess_tone3 { background-position: -720px -540px; } +.emoji-princess_tone4 { background-position: -720px -560px; } +.emoji-princess_tone5 { background-position: -720px -580px; } +.emoji-printer { background-position: -720px -600px; } +.emoji-projector { background-position: -720px -620px; } +.emoji-punch { background-position: -720px -640px; } +.emoji-punch_tone1 { background-position: -720px -660px; } +.emoji-punch_tone2 { background-position: -720px -680px; } +.emoji-punch_tone3 { background-position: -720px -700px; } +.emoji-punch_tone4 { background-position: 0 -720px; } +.emoji-punch_tone5 { background-position: -20px -720px; } +.emoji-purple_heart { background-position: -40px -720px; } +.emoji-purse { background-position: -60px -720px; } +.emoji-pushpin { background-position: -80px -720px; } +.emoji-put_litter_in_its_place { background-position: -100px -720px; } +.emoji-question { background-position: -120px -720px; } +.emoji-rabbit { background-position: -140px -720px; } +.emoji-rabbit2 { background-position: -160px -720px; } +.emoji-race_car { background-position: -180px -720px; } +.emoji-racehorse { background-position: -200px -720px; } +.emoji-radio { background-position: -220px -720px; } +.emoji-radio_button { background-position: -240px -720px; } +.emoji-radioactive { background-position: -260px -720px; } +.emoji-rage { background-position: -280px -720px; } +.emoji-railway_car { background-position: -300px -720px; } +.emoji-railway_track { background-position: -320px -720px; } +.emoji-rainbow { background-position: -340px -720px; } +.emoji-raised_back_of_hand { background-position: -360px -720px; } +.emoji-raised_back_of_hand_tone1 { background-position: -380px -720px; } +.emoji-raised_back_of_hand_tone2 { background-position: -400px -720px; } +.emoji-raised_back_of_hand_tone3 { background-position: -420px -720px; } +.emoji-raised_back_of_hand_tone4 { background-position: -440px -720px; } +.emoji-raised_back_of_hand_tone5 { background-position: -460px -720px; } +.emoji-raised_hand { background-position: -480px -720px; } +.emoji-raised_hand_tone1 { background-position: -500px -720px; } +.emoji-raised_hand_tone2 { background-position: -520px -720px; } +.emoji-raised_hand_tone3 { background-position: -540px -720px; } +.emoji-raised_hand_tone4 { background-position: -560px -720px; } +.emoji-raised_hand_tone5 { background-position: -580px -720px; } +.emoji-raised_hands { background-position: -600px -720px; } +.emoji-raised_hands_tone1 { background-position: -620px -720px; } +.emoji-raised_hands_tone2 { background-position: -640px -720px; } +.emoji-raised_hands_tone3 { background-position: -660px -720px; } +.emoji-raised_hands_tone4 { background-position: -680px -720px; } +.emoji-raised_hands_tone5 { background-position: -700px -720px; } +.emoji-raising_hand { background-position: -720px -720px; } +.emoji-raising_hand_tone1 { background-position: -740px 0; } +.emoji-raising_hand_tone2 { background-position: -740px -20px; } +.emoji-raising_hand_tone3 { background-position: -740px -40px; } +.emoji-raising_hand_tone4 { background-position: -740px -60px; } +.emoji-raising_hand_tone5 { background-position: -740px -80px; } +.emoji-ram { background-position: -740px -100px; } +.emoji-ramen { background-position: -740px -120px; } +.emoji-rat { background-position: -740px -140px; } +.emoji-record_button { background-position: -740px -160px; } +.emoji-recycle { background-position: -740px -180px; } +.emoji-red_car { background-position: -740px -200px; } +.emoji-red_circle { background-position: -740px -220px; } +.emoji-registered { background-position: -740px -240px; } +.emoji-relaxed { background-position: -740px -260px; } +.emoji-relieved { background-position: -740px -280px; } +.emoji-reminder_ribbon { background-position: -740px -300px; } +.emoji-repeat { background-position: -740px -320px; } +.emoji-repeat_one { background-position: -740px -340px; } +.emoji-restroom { background-position: -740px -360px; } +.emoji-revolving_hearts { background-position: -740px -380px; } +.emoji-rewind { background-position: -740px -400px; } +.emoji-rhino { background-position: -740px -420px; } +.emoji-ribbon { background-position: -740px -440px; } +.emoji-rice { background-position: -740px -460px; } +.emoji-rice_ball { background-position: -740px -480px; } +.emoji-rice_cracker { background-position: -740px -500px; } +.emoji-rice_scene { background-position: -740px -520px; } +.emoji-right_facing_fist { background-position: -740px -540px; } +.emoji-right_facing_fist_tone1 { background-position: -740px -560px; } +.emoji-right_facing_fist_tone2 { background-position: -740px -580px; } +.emoji-right_facing_fist_tone3 { background-position: -740px -600px; } +.emoji-right_facing_fist_tone4 { background-position: -740px -620px; } +.emoji-right_facing_fist_tone5 { background-position: -740px -640px; } +.emoji-ring { background-position: -740px -660px; } +.emoji-robot { background-position: -740px -680px; } +.emoji-rocket { background-position: -740px -700px; } +.emoji-rofl { background-position: -740px -720px; } +.emoji-roller_coaster { background-position: 0 -740px; } +.emoji-rolling_eyes { background-position: -20px -740px; } +.emoji-rooster { background-position: -40px -740px; } +.emoji-rose { background-position: -60px -740px; } +.emoji-rosette { background-position: -80px -740px; } +.emoji-rotating_light { background-position: -100px -740px; } +.emoji-round_pushpin { background-position: -120px -740px; } +.emoji-rowboat { background-position: -140px -740px; } +.emoji-rowboat_tone1 { background-position: -160px -740px; } +.emoji-rowboat_tone2 { background-position: -180px -740px; } +.emoji-rowboat_tone3 { background-position: -200px -740px; } +.emoji-rowboat_tone4 { background-position: -220px -740px; } +.emoji-rowboat_tone5 { background-position: -240px -740px; } +.emoji-rugby_football { background-position: -260px -740px; } +.emoji-runner { background-position: -280px -740px; } +.emoji-runner_tone1 { background-position: -300px -740px; } +.emoji-runner_tone2 { background-position: -320px -740px; } +.emoji-runner_tone3 { background-position: -340px -740px; } +.emoji-runner_tone4 { background-position: -360px -740px; } +.emoji-runner_tone5 { background-position: -380px -740px; } +.emoji-running_shirt_with_sash { background-position: -400px -740px; } +.emoji-sa { background-position: -420px -740px; } +.emoji-sagittarius { background-position: -440px -740px; } +.emoji-sailboat { background-position: -460px -740px; } +.emoji-sake { background-position: -480px -740px; } +.emoji-salad { background-position: -500px -740px; } +.emoji-sandal { background-position: -520px -740px; } +.emoji-santa { background-position: -540px -740px; } +.emoji-santa_tone1 { background-position: -560px -740px; } +.emoji-santa_tone2 { background-position: -580px -740px; } +.emoji-santa_tone3 { background-position: -600px -740px; } +.emoji-santa_tone4 { background-position: -620px -740px; } +.emoji-santa_tone5 { background-position: -640px -740px; } +.emoji-satellite { background-position: -660px -740px; } +.emoji-satellite_orbital { background-position: -680px -740px; } +.emoji-saxophone { background-position: -700px -740px; } +.emoji-scales { background-position: -720px -740px; } +.emoji-school { background-position: -740px -740px; } +.emoji-school_satchel { background-position: -760px 0; } +.emoji-scissors { background-position: -760px -20px; } +.emoji-scooter { background-position: -760px -40px; } +.emoji-scorpion { background-position: -760px -60px; } +.emoji-scorpius { background-position: -760px -80px; } +.emoji-scream { background-position: -760px -100px; } +.emoji-scream_cat { background-position: -760px -120px; } +.emoji-scroll { background-position: -760px -140px; } +.emoji-seat { background-position: -760px -160px; } +.emoji-second_place { background-position: -760px -180px; } +.emoji-secret { background-position: -760px -200px; } +.emoji-see_no_evil { background-position: -760px -220px; } +.emoji-seedling { background-position: -760px -240px; } +.emoji-selfie { background-position: -760px -260px; } +.emoji-selfie_tone1 { background-position: -760px -280px; } +.emoji-selfie_tone2 { background-position: -760px -300px; } +.emoji-selfie_tone3 { background-position: -760px -320px; } +.emoji-selfie_tone4 { background-position: -760px -340px; } +.emoji-selfie_tone5 { background-position: -760px -360px; } +.emoji-seven { background-position: -760px -380px; } +.emoji-shallow_pan_of_food { background-position: -760px -400px; } +.emoji-shamrock { background-position: -760px -420px; } +.emoji-shark { background-position: -760px -440px; } +.emoji-shaved_ice { background-position: -760px -460px; } +.emoji-sheep { background-position: -760px -480px; } +.emoji-shell { background-position: -760px -500px; } +.emoji-shield { background-position: -760px -520px; } +.emoji-shinto_shrine { background-position: -760px -540px; } +.emoji-ship { background-position: -760px -560px; } +.emoji-shirt { background-position: -760px -580px; } +.emoji-shopping_bags { background-position: -760px -600px; } +.emoji-shopping_cart { background-position: -760px -620px; } +.emoji-shower { background-position: -760px -640px; } +.emoji-shrimp { background-position: -760px -660px; } +.emoji-shrug { background-position: -760px -680px; } +.emoji-shrug_tone1 { background-position: -760px -700px; } +.emoji-shrug_tone2 { background-position: -760px -720px; } +.emoji-shrug_tone3 { background-position: -760px -740px; } +.emoji-shrug_tone4 { background-position: 0 -760px; } +.emoji-shrug_tone5 { background-position: -20px -760px; } +.emoji-signal_strength { background-position: -40px -760px; } +.emoji-six { background-position: -60px -760px; } +.emoji-six_pointed_star { background-position: -80px -760px; } +.emoji-ski { background-position: -100px -760px; } +.emoji-skier { background-position: -120px -760px; } +.emoji-skull { background-position: -140px -760px; } +.emoji-skull_crossbones { background-position: -160px -760px; } +.emoji-sleeping { background-position: -180px -760px; } +.emoji-sleeping_accommodation { background-position: -200px -760px; } +.emoji-sleepy { background-position: -220px -760px; } +.emoji-slight_frown { background-position: -240px -760px; } +.emoji-slight_smile { background-position: -260px -760px; } +.emoji-slot_machine { background-position: -280px -760px; } +.emoji-small_blue_diamond { background-position: -300px -760px; } +.emoji-small_orange_diamond { background-position: -320px -760px; } +.emoji-small_red_triangle { background-position: -340px -760px; } +.emoji-small_red_triangle_down { background-position: -360px -760px; } +.emoji-smile { background-position: -380px -760px; } +.emoji-smile_cat { background-position: -400px -760px; } +.emoji-smiley { background-position: -420px -760px; } +.emoji-smiley_cat { background-position: -440px -760px; } +.emoji-smiling_imp { background-position: -460px -760px; } +.emoji-smirk { background-position: -480px -760px; } +.emoji-smirk_cat { background-position: -500px -760px; } +.emoji-smoking { background-position: -520px -760px; } +.emoji-snail { background-position: -540px -760px; } +.emoji-snake { background-position: -560px -760px; } +.emoji-sneezing_face { background-position: -580px -760px; } +.emoji-snowboarder { background-position: -600px -760px; } +.emoji-snowflake { background-position: -620px -760px; } +.emoji-snowman { background-position: -640px -760px; } +.emoji-snowman2 { background-position: -660px -760px; } +.emoji-sob { background-position: -680px -760px; } +.emoji-soccer { background-position: -700px -760px; } +.emoji-soon { background-position: -720px -760px; } +.emoji-sos { background-position: -740px -760px; } +.emoji-sound { background-position: -760px -760px; } +.emoji-space_invader { background-position: -780px 0; } +.emoji-spades { background-position: -780px -20px; } +.emoji-spaghetti { background-position: -780px -40px; } +.emoji-sparkle { background-position: -780px -60px; } +.emoji-sparkler { background-position: -780px -80px; } +.emoji-sparkles { background-position: -780px -100px; } +.emoji-sparkling_heart { background-position: -780px -120px; } +.emoji-speak_no_evil { background-position: -780px -140px; } +.emoji-speaker { background-position: -780px -160px; } +.emoji-speaking_head { background-position: -780px -180px; } +.emoji-speech_balloon { background-position: -780px -200px; } +.emoji-speech_left { background-position: -780px -220px; } +.emoji-speedboat { background-position: -780px -240px; } +.emoji-spider { background-position: -780px -260px; } +.emoji-spider_web { background-position: -780px -280px; } +.emoji-spoon { background-position: -780px -300px; } +.emoji-spy { background-position: -780px -320px; } +.emoji-spy_tone1 { background-position: -780px -340px; } +.emoji-spy_tone2 { background-position: -780px -360px; } +.emoji-spy_tone3 { background-position: -780px -380px; } +.emoji-spy_tone4 { background-position: -780px -400px; } +.emoji-spy_tone5 { background-position: -780px -420px; } +.emoji-squid { background-position: -780px -440px; } +.emoji-stadium { background-position: -780px -460px; } +.emoji-star { background-position: -780px -480px; } +.emoji-star2 { background-position: -780px -500px; } +.emoji-star_and_crescent { background-position: -780px -520px; } +.emoji-star_of_david { background-position: -780px -540px; } +.emoji-stars { background-position: -780px -560px; } +.emoji-station { background-position: -780px -580px; } +.emoji-statue_of_liberty { background-position: -780px -600px; } +.emoji-steam_locomotive { background-position: -780px -620px; } +.emoji-stew { background-position: -780px -640px; } +.emoji-stop_button { background-position: -780px -660px; } +.emoji-stopwatch { background-position: -780px -680px; } +.emoji-straight_ruler { background-position: -780px -700px; } +.emoji-strawberry { background-position: -780px -720px; } +.emoji-stuck_out_tongue { background-position: -780px -740px; } +.emoji-stuck_out_tongue_closed_eyes { background-position: -780px -760px; } +.emoji-stuck_out_tongue_winking_eye { background-position: 0 -780px; } +.emoji-stuffed_flatbread { background-position: -20px -780px; } +.emoji-sun_with_face { background-position: -40px -780px; } +.emoji-sunflower { background-position: -60px -780px; } +.emoji-sunglasses { background-position: -80px -780px; } +.emoji-sunny { background-position: -100px -780px; } +.emoji-sunrise { background-position: -120px -780px; } +.emoji-sunrise_over_mountains { background-position: -140px -780px; } +.emoji-surfer { background-position: -160px -780px; } +.emoji-surfer_tone1 { background-position: -180px -780px; } +.emoji-surfer_tone2 { background-position: -200px -780px; } +.emoji-surfer_tone3 { background-position: -220px -780px; } +.emoji-surfer_tone4 { background-position: -240px -780px; } +.emoji-surfer_tone5 { background-position: -260px -780px; } +.emoji-sushi { background-position: -280px -780px; } +.emoji-suspension_railway { background-position: -300px -780px; } +.emoji-sweat { background-position: -320px -780px; } +.emoji-sweat_drops { background-position: -340px -780px; } +.emoji-sweat_smile { background-position: -360px -780px; } +.emoji-sweet_potato { background-position: -380px -780px; } +.emoji-swimmer { background-position: -400px -780px; } +.emoji-swimmer_tone1 { background-position: -420px -780px; } +.emoji-swimmer_tone2 { background-position: -440px -780px; } +.emoji-swimmer_tone3 { background-position: -460px -780px; } +.emoji-swimmer_tone4 { background-position: -480px -780px; } +.emoji-swimmer_tone5 { background-position: -500px -780px; } +.emoji-symbols { background-position: -520px -780px; } +.emoji-synagogue { background-position: -540px -780px; } +.emoji-syringe { background-position: -560px -780px; } +.emoji-taco { background-position: -580px -780px; } +.emoji-tada { background-position: -600px -780px; } +.emoji-tanabata_tree { background-position: -620px -780px; } +.emoji-tangerine { background-position: -640px -780px; } +.emoji-taurus { background-position: -660px -780px; } +.emoji-taxi { background-position: -680px -780px; } +.emoji-tea { background-position: -700px -780px; } +.emoji-telephone { background-position: -720px -780px; } +.emoji-telephone_receiver { background-position: -740px -780px; } +.emoji-telescope { background-position: -760px -780px; } +.emoji-ten { background-position: -780px -780px; } +.emoji-tennis { background-position: -800px 0; } +.emoji-tent { background-position: -800px -20px; } +.emoji-thermometer { background-position: -800px -40px; } +.emoji-thermometer_face { background-position: -800px -60px; } +.emoji-thinking { background-position: -800px -80px; } +.emoji-third_place { background-position: -800px -100px; } +.emoji-thought_balloon { background-position: -800px -120px; } +.emoji-three { background-position: -800px -140px; } +.emoji-thumbsdown { background-position: -800px -160px; } +.emoji-thumbsdown_tone1 { background-position: -800px -180px; } +.emoji-thumbsdown_tone2 { background-position: -800px -200px; } +.emoji-thumbsdown_tone3 { background-position: -800px -220px; } +.emoji-thumbsdown_tone4 { background-position: -800px -240px; } +.emoji-thumbsdown_tone5 { background-position: -800px -260px; } +.emoji-thumbsup { background-position: -800px -280px; } +.emoji-thumbsup_tone1 { background-position: -800px -300px; } +.emoji-thumbsup_tone2 { background-position: -800px -320px; } +.emoji-thumbsup_tone3 { background-position: -800px -340px; } +.emoji-thumbsup_tone4 { background-position: -800px -360px; } +.emoji-thumbsup_tone5 { background-position: -800px -380px; } +.emoji-thunder_cloud_rain { background-position: -800px -400px; } +.emoji-ticket { background-position: -800px -420px; } +.emoji-tickets { background-position: -800px -440px; } +.emoji-tiger { background-position: -800px -460px; } +.emoji-tiger2 { background-position: -800px -480px; } +.emoji-timer { background-position: -800px -500px; } +.emoji-tired_face { background-position: -800px -520px; } +.emoji-tm { background-position: -800px -540px; } +.emoji-toilet { background-position: -800px -560px; } +.emoji-tokyo_tower { background-position: -800px -580px; } +.emoji-tomato { background-position: -800px -600px; } +.emoji-tone1 { background-position: -800px -620px; } +.emoji-tone2 { background-position: -800px -640px; } +.emoji-tone3 { background-position: -800px -660px; } +.emoji-tone4 { background-position: -800px -680px; } +.emoji-tone5 { background-position: -800px -700px; } +.emoji-tongue { background-position: -800px -720px; } +.emoji-tools { background-position: -800px -740px; } +.emoji-top { background-position: -800px -760px; } +.emoji-tophat { background-position: -800px -780px; } +.emoji-track_next { background-position: 0 -800px; } +.emoji-track_previous { background-position: -20px -800px; } +.emoji-trackball { background-position: -40px -800px; } +.emoji-tractor { background-position: -60px -800px; } +.emoji-traffic_light { background-position: -80px -800px; } +.emoji-train { background-position: -100px -800px; } +.emoji-train2 { background-position: -120px -800px; } +.emoji-tram { background-position: -140px -800px; } +.emoji-triangular_flag_on_post { background-position: -160px -800px; } +.emoji-triangular_ruler { background-position: -180px -800px; } +.emoji-trident { background-position: -200px -800px; } +.emoji-triumph { background-position: -220px -800px; } +.emoji-trolleybus { background-position: -240px -800px; } +.emoji-trophy { background-position: -260px -800px; } +.emoji-tropical_drink { background-position: -280px -800px; } +.emoji-tropical_fish { background-position: -300px -800px; } +.emoji-truck { background-position: -320px -800px; } +.emoji-trumpet { background-position: -340px -800px; } +.emoji-tulip { background-position: -360px -800px; } +.emoji-tumbler_glass { background-position: -380px -800px; } +.emoji-turkey { background-position: -400px -800px; } +.emoji-turtle { background-position: -420px -800px; } +.emoji-tv { background-position: -440px -800px; } +.emoji-twisted_rightwards_arrows { background-position: -460px -800px; } +.emoji-two { background-position: -480px -800px; } +.emoji-two_hearts { background-position: -500px -800px; } +.emoji-two_men_holding_hands { background-position: -520px -800px; } +.emoji-two_women_holding_hands { background-position: -540px -800px; } +.emoji-u5272 { background-position: -560px -800px; } +.emoji-u5408 { background-position: -580px -800px; } +.emoji-u55b6 { background-position: -600px -800px; } +.emoji-u6307 { background-position: -620px -800px; } +.emoji-u6708 { background-position: -640px -800px; } +.emoji-u6709 { background-position: -660px -800px; } +.emoji-u6e80 { background-position: -680px -800px; } +.emoji-u7121 { background-position: -700px -800px; } +.emoji-u7533 { background-position: -720px -800px; } +.emoji-u7981 { background-position: -740px -800px; } +.emoji-u7a7a { background-position: -760px -800px; } +.emoji-umbrella { background-position: -780px -800px; } +.emoji-umbrella2 { background-position: -800px -800px; } +.emoji-unamused { background-position: -820px 0; } +.emoji-underage { background-position: -820px -20px; } +.emoji-unicorn { background-position: -820px -40px; } +.emoji-unlock { background-position: -820px -60px; } +.emoji-up { background-position: -820px -80px; } +.emoji-upside_down { background-position: -820px -100px; } +.emoji-urn { background-position: -820px -120px; } +.emoji-v { background-position: -820px -140px; } +.emoji-v_tone1 { background-position: -820px -160px; } +.emoji-v_tone2 { background-position: -820px -180px; } +.emoji-v_tone3 { background-position: -820px -200px; } +.emoji-v_tone4 { background-position: -820px -220px; } +.emoji-v_tone5 { background-position: -820px -240px; } +.emoji-vertical_traffic_light { background-position: -820px -260px; } +.emoji-vhs { background-position: -820px -280px; } +.emoji-vibration_mode { background-position: -820px -300px; } +.emoji-video_camera { background-position: -820px -320px; } +.emoji-video_game { background-position: -820px -340px; } +.emoji-violin { background-position: -820px -360px; } +.emoji-virgo { background-position: -820px -380px; } +.emoji-volcano { background-position: -820px -400px; } +.emoji-volleyball { background-position: -820px -420px; } +.emoji-vs { background-position: -820px -440px; } +.emoji-vulcan { background-position: -820px -460px; } +.emoji-vulcan_tone1 { background-position: -820px -480px; } +.emoji-vulcan_tone2 { background-position: -820px -500px; } +.emoji-vulcan_tone3 { background-position: -820px -520px; } +.emoji-vulcan_tone4 { background-position: -820px -540px; } +.emoji-vulcan_tone5 { background-position: -820px -560px; } +.emoji-walking { background-position: -820px -580px; } +.emoji-walking_tone1 { background-position: -820px -600px; } +.emoji-walking_tone2 { background-position: -820px -620px; } +.emoji-walking_tone3 { background-position: -820px -640px; } +.emoji-walking_tone4 { background-position: -820px -660px; } +.emoji-walking_tone5 { background-position: -820px -680px; } +.emoji-waning_crescent_moon { background-position: -820px -700px; } +.emoji-waning_gibbous_moon { background-position: -820px -720px; } +.emoji-warning { background-position: -820px -740px; } +.emoji-wastebasket { background-position: -820px -760px; } +.emoji-watch { background-position: -820px -780px; } +.emoji-water_buffalo { background-position: -820px -800px; } +.emoji-water_polo { background-position: 0 -820px; } +.emoji-water_polo_tone1 { background-position: -20px -820px; } +.emoji-water_polo_tone2 { background-position: -40px -820px; } +.emoji-water_polo_tone3 { background-position: -60px -820px; } +.emoji-water_polo_tone4 { background-position: -80px -820px; } +.emoji-water_polo_tone5 { background-position: -100px -820px; } +.emoji-watermelon { background-position: -120px -820px; } +.emoji-wave { background-position: -140px -820px; } +.emoji-wave_tone1 { background-position: -160px -820px; } +.emoji-wave_tone2 { background-position: -180px -820px; } +.emoji-wave_tone3 { background-position: -200px -820px; } +.emoji-wave_tone4 { background-position: -220px -820px; } +.emoji-wave_tone5 { background-position: -240px -820px; } +.emoji-wavy_dash { background-position: -260px -820px; } +.emoji-waxing_crescent_moon { background-position: -280px -820px; } +.emoji-waxing_gibbous_moon { background-position: -300px -820px; } +.emoji-wc { background-position: -320px -820px; } +.emoji-weary { background-position: -340px -820px; } +.emoji-wedding { background-position: -360px -820px; } +.emoji-whale { background-position: -380px -820px; } +.emoji-whale2 { background-position: -400px -820px; } +.emoji-wheel_of_dharma { background-position: -420px -820px; } +.emoji-wheelchair { background-position: -440px -820px; } +.emoji-white_check_mark { background-position: -460px -820px; } +.emoji-white_circle { background-position: -480px -820px; } +.emoji-white_flower { background-position: -500px -820px; } +.emoji-white_large_square { background-position: -520px -820px; } +.emoji-white_medium_small_square { background-position: -540px -820px; } +.emoji-white_medium_square { background-position: -560px -820px; } +.emoji-white_small_square { background-position: -580px -820px; } +.emoji-white_square_button { background-position: -600px -820px; } +.emoji-white_sun_cloud { background-position: -620px -820px; } +.emoji-white_sun_rain_cloud { background-position: -640px -820px; } +.emoji-white_sun_small_cloud { background-position: -660px -820px; } +.emoji-wilted_rose { background-position: -680px -820px; } +.emoji-wind_blowing_face { background-position: -700px -820px; } +.emoji-wind_chime { background-position: -720px -820px; } +.emoji-wine_glass { background-position: -740px -820px; } +.emoji-wink { background-position: -760px -820px; } +.emoji-wolf { background-position: -780px -820px; } +.emoji-woman { background-position: -800px -820px; } +.emoji-woman_tone1 { background-position: -820px -820px; } +.emoji-woman_tone2 { background-position: -840px 0; } +.emoji-woman_tone3 { background-position: -840px -20px; } +.emoji-woman_tone4 { background-position: -840px -40px; } +.emoji-woman_tone5 { background-position: -840px -60px; } +.emoji-womans_clothes { background-position: -840px -80px; } +.emoji-womans_hat { background-position: -840px -100px; } +.emoji-womens { background-position: -840px -120px; } +.emoji-worried { background-position: -840px -140px; } +.emoji-wrench { background-position: -840px -160px; } +.emoji-wrestlers { background-position: -840px -180px; } +.emoji-wrestlers_tone1 { background-position: -840px -200px; } +.emoji-wrestlers_tone2 { background-position: -840px -220px; } +.emoji-wrestlers_tone3 { background-position: -840px -240px; } +.emoji-wrestlers_tone4 { background-position: -840px -260px; } +.emoji-wrestlers_tone5 { background-position: -840px -280px; } +.emoji-writing_hand { background-position: -840px -300px; } +.emoji-writing_hand_tone1 { background-position: -840px -320px; } +.emoji-writing_hand_tone2 { background-position: -840px -340px; } +.emoji-writing_hand_tone3 { background-position: -840px -360px; } +.emoji-writing_hand_tone4 { background-position: -840px -380px; } +.emoji-writing_hand_tone5 { background-position: -840px -400px; } +.emoji-x { background-position: -840px -420px; } +.emoji-yellow_heart { background-position: -840px -440px; } +.emoji-yen { background-position: -840px -460px; } +.emoji-yin_yang { background-position: -840px -480px; } +.emoji-yum { background-position: -840px -500px; } +.emoji-zap { background-position: -840px -520px; } +.emoji-zero { background-position: -840px -540px; } +.emoji-zipper_mouth { background-position: -840px -560px; } +.emoji-100 { background-position: -840px -580px; } + +.emoji-icon { + background-image: image-url('emoji.png'); + background-repeat: no-repeat; + color: transparent; + text-indent: -99em; + height: 20px; + width: 20px; + + @media only screen and (-webkit-min-device-pixel-ratio: 2), + only screen and (min--moz-device-pixel-ratio: 2), + only screen and (-o-min-device-pixel-ratio: 2/1), + only screen and (min-device-pixel-ratio: 2), + only screen and (min-resolution: 192dpi), + only screen and (min-resolution: 2dppx) { + background-image: image-url('emoji@2x.png'); + background-size: 860px 840px; + } +} diff --git a/app/assets/stylesheets/pages/repo.scss.orig b/app/assets/stylesheets/pages/repo.scss.orig new file mode 100644 index 00000000000..57b995adb64 --- /dev/null +++ b/app/assets/stylesheets/pages/repo.scss.orig @@ -0,0 +1,786 @@ +.project-refs-form, +.project-refs-target-form { + display: inline-block; +} + +.fade-enter, +.fade-leave-to { + opacity: 0; +} + +.commit-message { + @include str-truncated(250px); +} + +.editable-mode { + display: inline-block; +} + +.ide-view { + display: flex; + height: calc(100vh - #{$header-height}); + margin-top: 40px; + color: $almost-black; + border-top: 1px solid $white-dark; + border-bottom: 1px solid $white-dark; + + &.is-collapsed { + .ide-file-list { + max-width: 250px; + } + } + + .file-status-icon { + width: 10px; + height: 10px; + } +} + +.ide-file-list { + flex: 1; + + .file { + cursor: pointer; + + &.file-open { + background: $white-normal; + } + + .ide-file-name { + flex: 1; + white-space: nowrap; + text-overflow: ellipsis; + + svg { + vertical-align: middle; + margin-right: 2px; + } + + .loading-container { + margin-right: 4px; + display: inline-block; + } + } + + .ide-file-changed-icon { + margin-left: auto; + } + + .ide-new-btn { + display: none; + margin-bottom: -4px; + margin-right: -8px; + } + + &:hover { + .ide-new-btn { + display: block; + } + } + + &.folder { + svg { + fill: $gl-text-color-secondary; + } + } + } + + a { + color: $gl-text-color; + } + + th { + position: sticky; + top: 0; + } +} + +.file-name, +.file-col-commit-message { + display: flex; + overflow: visible; + padding: 6px 12px; +} + +.multi-file-loading-container { + margin-top: 10px; + padding: 10px; + + .animation-container { + background: $gray-light; + + div { + background: $gray-light; + } + } +} + +.multi-file-table-col-commit-message { + white-space: nowrap; + width: 50%; +} + +.multi-file-edit-pane { + display: flex; + flex-direction: column; + flex: 1; + border-left: 1px solid $white-dark; + overflow: hidden; +} + +.multi-file-tabs { + display: flex; + background-color: $white-normal; + box-shadow: inset 0 -1px $white-dark; + + > ul { + display: flex; + overflow-x: auto; + } + + li { + position: relative; + } + + .dropdown { + display: flex; + margin-left: auto; + margin-bottom: 1px; + padding: 0 $grid-size; + border-left: 1px solid $white-dark; + background-color: $white-light; + + &.shadow { + box-shadow: 0 0 10px $dropdown-shadow-color; + } + + .btn { + margin-top: auto; + margin-bottom: auto; + } + } +} + +.multi-file-tab { + @include str-truncated(150px); + padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding; + background-color: $gray-normal; + border-right: 1px solid $white-dark; + border-bottom: 1px solid $white-dark; + cursor: pointer; + + svg { + vertical-align: middle; + } + + &.active { + background-color: $white-light; + border-bottom-color: $white-light; + } +} + +.multi-file-tab-close { + position: absolute; + right: 8px; + top: 50%; + width: 16px; + height: 16px; + padding: 0; + background: none; + border: 0; + border-radius: $border-radius-default; + color: $theme-gray-900; + transform: translateY(-50%); + + svg { + position: relative; + top: -1px; + } + + &:hover { + background-color: $theme-gray-200; + } + + &:focus { + background-color: $blue-500; + color: $white-light; + outline: 0; + + svg { + fill: currentColor; + } + } +} + +.multi-file-edit-pane-content { + flex: 1; + height: 0; +} + +.blob-editor-container { + flex: 1; + height: 0; + display: flex; + flex-direction: column; + justify-content: center; + + .vertical-center { + min-height: auto; + } + + .monaco-editor .lines-content .cigr { + display: none; + } + + .monaco-diff-editor.vs { + .editor.modified { + box-shadow: none; + } + + .diagonal-fill { + display: none !important; + } + + .diffOverview { + background-color: $white-light; + border-left: 1px solid $white-dark; + cursor: ns-resize; + } + + .diffViewport { + display: none; + } + + .char-insert { + background-color: $line-added-dark; + } + + .char-delete { + background-color: $line-removed-dark; + } + + .line-numbers { + color: $black-transparent; + } + + .view-overlays { + .line-insert { + background-color: $line-added; + } + + .line-delete { + background-color: $line-removed; + } + } + + .margin { + background-color: $gray-light; + border-right: 1px solid $white-normal; + + .line-insert { + border-right: 1px solid $line-added-dark; + } + + .line-delete { + border-right: 1px solid $line-removed-dark; + } + } + + .margin-view-overlays .insert-sign, + .margin-view-overlays .delete-sign { + opacity: 0.4; + } + + .cursors-layer { + display: none; + } + } +} + +.multi-file-editor-holder { + height: 100%; +} + +.multi-file-editor-btn-group { + padding: $gl-bar-padding $gl-padding; + border-top: 1px solid $white-dark; + border-bottom: 1px solid $white-dark; + background: $white-light; +} + +.ide-status-bar { + padding: $gl-bar-padding $gl-padding; + background: $white-light; + display: flex; + justify-content: space-between; + + svg { + vertical-align: middle; + } +} + +// Not great, but this is to deal with our current output +.multi-file-preview-holder { + height: 100%; + overflow: scroll; + + .file-content.code { + display: flex; + + i { + margin-left: -10px; + } + } + + .line-numbers { + min-width: 50px; + } + + .file-content, + .line-numbers, + .blob-content, + .code { + min-height: 100%; + } +} + +.file-content.blob-no-preview { + a { + margin-left: auto; + margin-right: auto; + } +} + +.multi-file-commit-panel { + display: flex; + position: relative; + flex-direction: column; + width: 340px; + padding: 0; + background-color: $gray-light; + padding-right: 3px; + + .projects-sidebar { + display: flex; + flex-direction: column; + + .context-header { + width: auto; + margin-right: 0; + } + } + + .multi-file-commit-panel-inner { + display: flex; + flex: 1; + flex-direction: column; + } + + .multi-file-commit-panel-inner-scroll { + display: flex; + flex: 1; + flex-direction: column; + overflow: auto; + } + + &.is-collapsed { + width: 60px; + + .multi-file-commit-list { + padding-top: $gl-padding; + overflow: hidden; + } + + .multi-file-context-bar-icon { + align-items: center; + + svg { + float: none; + margin: 0; + } + } + } + + .branch-container { + border-left: 4px solid $indigo-700; + margin-bottom: $gl-bar-padding; + } + + .branch-header { + background: $white-dark; + display: flex; + } + + .branch-header-title { + flex: 1; + padding: $grid-size $gl-padding; + color: $indigo-700; + font-weight: $gl-font-weight-bold; + + svg { + vertical-align: middle; + } + } + + .branch-header-btns { + padding: $gl-vert-padding $gl-padding; + } + + .left-collapse-btn { + display: none; + background: $gray-light; + text-align: left; + border-top: 1px solid $white-dark; + + svg { + vertical-align: middle; + } + } +} + +.multi-file-context-bar-icon { + padding: 10px; + + svg { + margin-right: 10px; + float: left; + } +} + +.multi-file-commit-panel-section { + display: flex; + flex-direction: column; + flex: 1; +} + +.multi-file-commit-empty-state-container { + align-items: center; + justify-content: center; +} + +.multi-file-commit-panel-header { + display: flex; + align-items: center; + margin-bottom: 12px; + border-bottom: 1px solid $white-dark; + padding: $gl-btn-padding 0; + + &.is-collapsed { + border-bottom: 1px solid $white-dark; + + svg { + margin-left: auto; + margin-right: auto; + } + + .multi-file-commit-panel-collapse-btn { + margin-right: auto; + margin-left: auto; + border-left: 0; + } + } +} + +.multi-file-commit-panel-header-title { + display: flex; + flex: 1; + padding: 0 $gl-btn-padding; + + svg { + margin-right: $gl-btn-padding; + } +} + +.multi-file-commit-panel-collapse-btn { + border-left: 1px solid $white-dark; +} + +.multi-file-commit-list { + flex: 1; + overflow: auto; + padding: $gl-padding 0; + min-height: 60px; +} + +.multi-file-commit-list-item { + display: flex; + padding: 0; + align-items: center; + + .multi-file-discard-btn { + display: none; + margin-left: auto; + color: $gl-link-color; + padding: 0 2px; + + &:focus, + &:hover { + text-decoration: underline; + } + } + + &:hover { + background: $white-normal; + + .multi-file-discard-btn { + display: block; + } + } +} + +.multi-file-addition { + fill: $green-500; +} + +.multi-file-modified { + fill: $orange-500; +} + +.multi-file-commit-list-collapsed { + display: flex; + flex-direction: column; + + > svg { + margin-left: auto; + margin-right: auto; + } + + .file-status-icon { + width: 10px; + height: 10px; + margin-left: 3px; + } +} + +.multi-file-commit-list-path { + padding: $grid-size / 2; + padding-left: $gl-padding; + background: none; + border: 0; + text-align: left; + width: 100%; + min-width: 0; + + svg { + min-width: 16px; + vertical-align: middle; + display: inline-block; + } + + &:hover, + &:focus { + outline: 0; + } +} + +.multi-file-commit-list-file-path { + @include str-truncated(100%); + + &:hover { + text-decoration: underline; + } + + &:active { + text-decoration: none; + } +} + +.multi-file-commit-form { + padding: $gl-padding; + border-top: 1px solid $white-dark; + + .btn { + font-size: $gl-font-size; + } +} + +.multi-file-commit-message.form-control { + height: 160px; + resize: none; +} + +.dirty-diff { + // !important need to override monaco inline style + width: 4px !important; + left: 0 !important; + + &-modified { + background-color: $blue-500; + } + + &-added { + background-color: $green-600; + } + + &-removed { + height: 0 !important; + width: 0 !important; + bottom: -2px; + border-style: solid; + border-width: 5px; + border-color: transparent transparent transparent $red-500; + + &::before { + content: ''; + position: absolute; + left: 0; + top: 0; + width: 100px; + height: 1px; + background-color: rgba($red-500, 0.5); + } + } +} + +.ide-loading { + display: flex; + height: 100vh; + align-items: center; + justify-content: center; +} + +.ide-empty-state { + display: flex; + height: 100vh; + align-items: center; + justify-content: center; +} + +.ide-new-btn { + .dropdown-toggle svg { + margin-top: -2px; + margin-bottom: 2px; + } + + .dropdown-menu { + left: auto; + right: 0; + + label { + font-weight: $gl-font-weight-normal; + padding: 5px 8px; + margin-bottom: 0; + } + } +} + +.ide { + overflow: hidden; + + &.nav-only { + .flash-container { + margin-top: $header-height; + margin-bottom: 0; + } + + .alert-wrapper .flash-container .flash-alert:last-child, + .alert-wrapper .flash-container .flash-notice:last-child { + margin-bottom: 0; + } + + .content-wrapper { + margin-top: $header-height; + padding-bottom: 0; + } + + &.flash-shown { + .content-wrapper { + margin-top: 0; + } + + .ide-view { + height: calc(100vh - #{$header-height + $flash-height}); + } + } + + .projects-sidebar { + .multi-file-commit-panel-inner-scroll { + flex: 1; + } + } + } +} + +.with-performance-bar .ide.nav-only { + .flash-container { + margin-top: #{$header-height + $performance-bar-height}; + } + + .content-wrapper { + margin-top: #{$header-height + $performance-bar-height}; + padding-bottom: 0; + } + + .ide-view { + height: calc(100vh - #{$header-height + $performance-bar-height}); + } + + &.flash-shown { + .content-wrapper { + margin-top: 0; + } + + .ide-view { + height: calc( + 100vh - #{$header-height + $performance-bar-height + $flash-height} + ); + } + } +} + +.dragHandle { + position: absolute; + top: 0; + bottom: 0; + width: 3px; + background-color: $white-dark; + + &.dragright { + right: 0; + } + + &.dragleft { + left: 0; + } +} + +.ide-commit-radios { + label { + font-weight: normal; + } + + .help-block { + margin-top: 0; + line-height: 0; + } +} + +.ide-commit-new-branch { + margin-left: 25px; +} + +.ide-external-links { + p { + margin: 0; + } +} + +.ide-sidebar-link { + padding: $gl-padding-8 $gl-padding; + background: $indigo-700; + color: $white-light; + text-decoration: none; + display: flex; + align-items: center; + + &:focus, + &:hover { + color: $white-light; + text-decoration: underline; + background: $indigo-500; + } + + &:active { + background: $indigo-800; + } +} diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml new file mode 100644 index 00000000000..6a681736b6f --- /dev/null +++ b/app/views/projects/runners/_form.html.haml @@ -0,0 +1,55 @@ += form_for runner, url: runner_form_url, html: { class: 'form-horizontal' } do |f| + = form_errors(runner) + .form-group + = label :active, "Active", class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :active + %span.light Paused Runners don't accept new jobs + .form-group + = label :protected, "Protected", class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :access_level, {}, 'ref_protected', 'not_protected' + %span.light This runner will only run on pipelines triggered on protected branches + .form-group + = label :run_untagged, 'Run untagged jobs', class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :run_untagged + %span.light Indicates whether this runner can pick jobs without tags + .form-group + = label :locked, 'Lock to current projects', class: 'control-label' + .col-sm-10 + .checkbox + = f.check_box :locked + %span.light When a runner is locked, it cannot be assigned to other projects + .form-group + = label_tag :token, class: 'control-label' do + Token + .col-sm-10 + = f.text_field :token, class: 'form-control', readonly: true + .form-group + = label_tag :ip_address, class: 'control-label' do + IP Address + .col-sm-10 + = f.text_field :ip_address, class: 'form-control', readonly: true + .form-group + = label_tag :description, class: 'control-label' do + Description + .col-sm-10 + = f.text_field :description, class: 'form-control' + .form-group + = label_tag :maximum_timeout_human_readable, class: 'control-label' do + Maximum job timeout + .col-sm-10 + = f.text_field :maximum_timeout_human_readable, class: 'form-control' + .help-block This timeout will take precedence when lower than Project-defined timeout + .form-group + = label_tag :tag_list, class: 'control-label' do + Tags + .col-sm-10 + = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' + .help-block You can setup jobs to only use Runners with specific tags. Separate tags with commas. + .form-actions + = f.submit 'Save changes', class: 'btn btn-save' diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml new file mode 100644 index 00000000000..322152cfaca --- /dev/null +++ b/app/views/projects/runners/show.html.haml @@ -0,0 +1,67 @@ +- page_title "#{@runner.description} ##{@runner.id}", "Runners" + +%h3.page-title + Runner ##{@runner.id} + .pull-right + - if @runner.shared? + %span.runner-state.runner-state-shared + Shared + - else + %span.runner-state.runner-state-specific + Specific + +.table-holder + %table.table + %thead + %tr + %th Property Name + %th Value + %tr + %td Active + %td= @runner.active? ? 'Yes' : 'No' + %tr + %td Protected + %td= @runner.ref_protected? ? 'Yes' : 'No' + %tr + %td Can run untagged jobs + %td= @runner.run_untagged? ? 'Yes' : 'No' + %tr + %td Locked to this project + %td= @runner.locked? ? 'Yes' : 'No' + %tr + %td Tags + %td + - @runner.tag_list.sort.each do |tag| + %span.label.label-primary + = tag + %tr + %td Name + %td= @runner.name + %tr + %td Version + %td= @runner.version + %tr + %td IP Address + %td= @runner.ip_address + %tr + %td Revision + %td= @runner.revision + %tr + %td Platform + %td= @runner.platform + %tr + %td Architecture + %td= @runner.architecture + %tr + %td Description + %td= @runner.description + %tr + %td Maximum job timeout + %td= @runner.maximum_timeout_human_readable + %tr + %td Last contact + %td + - if @runner.contacted_at + = time_ago_with_tooltip @runner.contacted_at + - else + Never diff --git a/bin/spinach b/bin/spinach new file mode 100755 index 00000000000..eda81c9ed8a --- /dev/null +++ b/bin/spinach @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby + +# Remove this block when removing rails5? code. +gemfile = %w[1 true].include?(ENV["RAILS5"]) ? "Gemfile.rails5" : "Gemfile" +ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__) + +begin + load File.expand_path('../spring', __FILE__) +rescue LoadError => e + raise unless e.message.include?('spring') +end +require 'bundler/setup' +load Gem.bin_path('spinach', 'spinach') diff --git a/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml b/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml new file mode 100644 index 00000000000..80b5b4a8abe --- /dev/null +++ b/changelogs/unreleased/44775-avatar-on-os-fails-with-cdn.yml @@ -0,0 +1,5 @@ +--- +title: Fixed wrong avatar URL when the avatar is on object storage. +merge_request: 18092 +author: +type: fixed diff --git a/changelogs/unreleased/add-jwt-strategy-to-gitlab-suite.yml b/changelogs/unreleased/add-jwt-strategy-to-gitlab-suite.yml new file mode 100644 index 00000000000..22a839cef56 --- /dev/null +++ b/changelogs/unreleased/add-jwt-strategy-to-gitlab-suite.yml @@ -0,0 +1,5 @@ +--- +title: Ports omniauth-jwt gem onto GitLab OmniAuth Strategies suite +merge_request: 18580 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-fix-maintainer-push-error.yml b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml new file mode 100644 index 00000000000..66ab8fbf884 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix errors on pushing to an empty repository +merge_request: 18462 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-fix-openid-redirect.yml b/changelogs/unreleased/bvl-fix-openid-redirect.yml new file mode 100644 index 00000000000..83ee6d953e4 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-openid-redirect.yml @@ -0,0 +1,5 @@ +--- +title: Fix redirection error for applications using OpenID +merge_request: 18599 +author: +type: fixed diff --git a/changelogs/unreleased/dm-commit-trailer-without-gravatar.yml b/changelogs/unreleased/dm-commit-trailer-without-gravatar.yml new file mode 100644 index 00000000000..9f057c67122 --- /dev/null +++ b/changelogs/unreleased/dm-commit-trailer-without-gravatar.yml @@ -0,0 +1,5 @@ +--- +title: Fix commit trailer rendering when Gravatar is disabled +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/fix-file-store-artifacts-and-lfs.yml b/changelogs/unreleased/fix-file-store-artifacts-and-lfs.yml new file mode 100644 index 00000000000..7e97f245e66 --- /dev/null +++ b/changelogs/unreleased/fix-file-store-artifacts-and-lfs.yml @@ -0,0 +1,5 @@ +--- +title: Fix file_store for artifacts and lfs when saving +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/issue_45463.yml b/changelogs/unreleased/issue_45463.yml new file mode 100644 index 00000000000..a350568d04b --- /dev/null +++ b/changelogs/unreleased/issue_45463.yml @@ -0,0 +1,5 @@ +--- +title: Fix users not seeing labels from private groups when being a member of a child project +merge_request: +author: +type: fixed diff --git a/changelogs/unreleased/update-doorkeeper-changelog.yml b/changelogs/unreleased/update-doorkeeper-changelog.yml new file mode 100644 index 00000000000..b47bdf4a28d --- /dev/null +++ b/changelogs/unreleased/update-doorkeeper-changelog.yml @@ -0,0 +1,5 @@ +--- +title: Update doorkeeper to 4.3.2 to fix GitLab OAuth authentication +merge_request: 18543 +author: +type: fixed diff --git a/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb b/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb new file mode 100644 index 00000000000..f1e851a210b --- /dev/null +++ b/ee/app/controllers/ee/ldap/omniauth_callbacks_controller.rb @@ -0,0 +1,22 @@ +module EE + module Ldap + module OmniauthCallbacksController + extend ::Gitlab::Utils::Override + + override :sign_in_and_redirect + def sign_in_and_redirect(user) + # The counter gets incremented in `sign_in_and_redirect` + show_ldap_sync_flash if user.sign_in_count == 0 + + super + end + + private + + def show_ldap_sync_flash + flash[:notice] = 'LDAP sync in progress. This could take a few minutes. '\ + 'Refresh the page to see the changes.' + end + end + end +end diff --git a/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb b/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb new file mode 100644 index 00000000000..0835ff35846 --- /dev/null +++ b/ee/spec/controllers/ldap/omniauth_callbacks_controller_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Ldap::OmniauthCallbacksController do + include_context 'Ldap::OmniauthCallbacksController' + + it "displays LDAP sync flash on first sign in" do + post provider + + expect(flash[:notice]).to match(/LDAP sync in progress*/) + end + + it "skips LDAP sync flash on subsequent sign ins" do + user.update!(sign_in_count: 1) + + post provider + + expect(flash[:notice]).to eq nil + end + + context 'access denied' do + let(:valid_login?) { false } + + it 'logs a failure event' do + stub_licensed_features(extended_audit_events: true) + + expect { post provider }.to change(SecurityEvent, :count).by(1) + end + end +end diff --git a/features/project/builds/artifacts.feature b/features/project/builds/artifacts.feature new file mode 100644 index 00000000000..5abc24949cf --- /dev/null +++ b/features/project/builds/artifacts.feature @@ -0,0 +1,65 @@ +Feature: Project Builds Artifacts + Background: + Given I sign in as a user + And I own a project + And project has CI enabled + And project has a recent build + + Scenario: I download build artifacts + Given recent build has artifacts available + When I visit recent build details page + And I click artifacts download button + Then download of build artifacts archive starts + + Scenario: I browse build artifacts + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build details page + And I click artifacts browse button + Then I should see content of artifacts archive + And I should see the build header + + Scenario: I browse subdirectory of build artifacts + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build details page + And I click artifacts browse button + And I click link to subdirectory within build artifacts + Then I should see content of subdirectory within artifacts archive + And I should see the directory name in the breadcrumb + + Scenario: I browse directory with UTF-8 characters in name + Given recent build has artifacts available + And recent build has artifacts metadata available + And recent build artifacts contain directory with UTF-8 characters + When I visit recent build details page + And I click artifacts browse button + And I navigate to directory with UTF-8 characters in name + Then I should see content of directory with UTF-8 characters in name + + Scenario: I try to browse directory with invalid UTF-8 characters in name + Given recent build has artifacts available + And recent build has artifacts metadata available + And recent build artifacts contain directory with invalid UTF-8 characters + When I visit recent build details page + And I click artifacts browse button + And I navigate to parent directory of directory with invalid name + Then I should not see directory with invalid name on the list + + @javascript + Scenario: I download a single file from build artifacts + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build details page + And I click artifacts browse button + And I click a link to file within build artifacts + Then I see a download link + + @javascript + Scenario: I click on a row in an artifacts table + Given recent build has artifacts available + And recent build has artifacts metadata available + When I visit recent build details page + And I click artifacts browse button + And I click a first row within build artifacts table + Then page with a coresponding path is loading diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature new file mode 100644 index 00000000000..3459cce03f9 --- /dev/null +++ b/features/project/commits/commits.feature @@ -0,0 +1,96 @@ +@project_commits +Feature: Project Commits + Background: + Given I sign in as a user + And I own a project + And I visit my project's commits page + + Scenario: I browse commits list for master branch + Then I see project commits + And I should not see button to create a new merge request + Then I click the "Compare" tab + And I should not see button to create a new merge request + + Scenario: I browse commits list for feature branch without a merge request + Given I visit commits list page for feature branch + Then I see feature branch commits + And I see button to create a new merge request + Then I click the "Compare" tab + And I see button to create a new merge request + + Scenario: I browse commits list for feature branch with an open merge request + Given project have an open merge request + And I visit commits list page for feature branch + Then I see feature branch commits + And I should not see button to create a new merge request + And I should see button to the merge request + Then I click the "Compare" tab + And I should not see button to create a new merge request + And I should see button to the merge request + + Scenario: I browse atom feed of commits list for master branch + Given I click atom feed link + Then I see commits atom feed + + Scenario: I browse commit from list + Given I click on commit link + Then I see commit info + And I see side-by-side diff button + + Scenario: I browse commit from list and create a new tag + Given I click on commit link + And I click on tag link + Then I see commit SHA pre-filled + + Scenario: I browse commit with ci from list + Given commit has ci status + And repository contains ".gitlab-ci.yml" file + When I click on commit link + Then I see commit ci info + + Scenario: I browse commit with side-by-side diff view + Given I click on commit link + And I click side-by-side diff button + Then I see inline diff button + + @javascript + Scenario: I compare branches without a merge request + Given I visit compare refs page + And I fill compare fields with branches + Then I see compared branches + And I see button to create a new merge request + + @javascript + Scenario: I compare branches with an open merge request + Given project have an open merge request + And I visit compare refs page + And I fill compare fields with branches + Then I see compared branches + And I should not see button to create a new merge request + And I should see button to the merge request + + @javascript + Scenario: I compare refs + Given I visit compare refs page + And I fill compare fields with refs + Then I see compared refs + And I unfold diff + Then I should see additional file lines + + Scenario: I browse commits for a specific path + Given I visit my project's commits page for a specific path + Then I see breadcrumb links + + # TODO: Implement feature in graphs + #Scenario: I browse commits stats + #Given I visit my project's commits stats page + #Then I see commits stats + + Scenario: I browse a commit with an image + Given I visit a commit with an image that changed + Then The diff links to both the previous and current image + + @javascript + Scenario: I filter commits by message + When I search "submodules" commits + Then I should see only "submodules" commits diff --git a/features/project/commits/diff_comments.feature b/features/project/commits/diff_comments.feature new file mode 100644 index 00000000000..35687aac9ea --- /dev/null +++ b/features/project/commits/diff_comments.feature @@ -0,0 +1,96 @@ +@project_commits +Feature: Project Commits Diff Comments + Background: + Given I sign in as a user + And I own project "Shop" + And I visit project commit page + + @javascript + Scenario: I can comment on a commit diff + Given I leave a diff comment like "Typo, please fix" + Then I should see a diff comment saying "Typo, please fix" + + @javascript + Scenario: I can add a diff comment with a single emoji + Given I open a diff comment form + And I write a diff comment like ":smile:" + Then I should see a diff comment with an emoji image + + @javascript + Scenario: I get a temporary form for the first comment on a diff line + Given I open a diff comment form + Then I should see a temporary diff comment form + + @javascript + Scenario: I have a cancel button on the diff form + Given I open a diff comment form + Then I should see the cancel comment button + + @javascript + Scenario: I can cancel a diff form + Given I open a diff comment form + And I cancel the diff comment + Then I should not see the diff comment form + + @javascript + Scenario: I can't open a second form for a diff line + Given I open a diff comment form + And I open a diff comment form + Then I should only see one diff form + + @javascript + Scenario: I can have multiple forms + Given I open a diff comment form + And I write a diff comment like ":-1: I don't like this" + And I open another diff comment form + Then I should see a diff comment form with ":-1: I don't like this" + And I should see an empty diff comment form + + @javascript + Scenario: I can preview multiple forms separately + Given I preview a diff comment text like "Should fix it :smile:" + And I preview another diff comment text like "DRY this up" + Then I should see two separate previews + + @javascript + Scenario: I have a reply button in discussions + Given I leave a diff comment like "Typo, please fix" + Then I should see a discussion reply button + + @javascript + Scenario: I can preview with text + Given I open a diff comment form + And I write a diff comment like ":-1: I don't like this" + Then The diff comment preview tab should display rendered Markdown + + @javascript + Scenario: I preview a diff comment + Given I preview a diff comment text like "Should fix it :smile:" + Then I should see the diff comment preview + And I should not see the diff comment text field + + @javascript + Scenario: I can edit after preview + Given I preview a diff comment text like "Should fix it :smile:" + Then I should see the diff comment write tab + + @javascript + Scenario: The form gets removed after posting + Given I preview a diff comment text like "Should fix it :smile:" + And I submit the diff comment + Then I should not see the diff comment form + And I should see a discussion reply button + + @javascript + Scenario: I can add a comment on a side-by-side commit diff (left side) + Given I open a diff comment form + And I click side-by-side diff button + When I leave a diff comment in a parallel view on the left side like "Old comment" + Then I should see a diff comment on the left side saying "Old comment" + + @javascript + Scenario: I can add a comment on a side-by-side commit diff (right side) + Given I open a diff comment form + And I click side-by-side diff button + When I leave a diff comment in a parallel view on the right side like "New comment" + Then I should see a diff comment on the right side saying "New comment" diff --git a/features/project/deploy_keys.feature b/features/project/deploy_keys.feature new file mode 100644 index 00000000000..6f1ed9ff5b6 --- /dev/null +++ b/features/project/deploy_keys.feature @@ -0,0 +1,46 @@ +Feature: Project Deploy Keys + Background: + Given I sign in as a user + And I own project "Shop" + + @javascript + Scenario: I should see deploy keys list + Given project has deploy key + When I visit project deploy keys page + Then I should see project deploy key + + @javascript + Scenario: I should see project deploy keys + Given other projects have deploy keys + When I visit project deploy keys page + Then I should see other project deploy key + And I should only see the same deploy key once + + @javascript + Scenario: I should see public deploy keys + Given public deploy key exists + When I visit project deploy keys page + Then I should see public deploy key + + @javascript + Scenario: I add new deploy key + Given I visit project deploy keys page + And I submit new deploy key + Then I should be on deploy keys page + And I should see newly created deploy key + + @javascript + Scenario: I attach other project deploy key to project + Given other projects have deploy keys + And I visit project deploy keys page + When I click attach deploy key + Then I should be on deploy keys page + And I should see newly created deploy key + + @javascript + Scenario: I attach public deploy key to project + Given public deploy key exists + And I visit project deploy keys page + When I click attach deploy key + Then I should be on deploy keys page + And I should see newly created deploy key diff --git a/features/project/ff_merge_requests.feature b/features/project/ff_merge_requests.feature new file mode 100644 index 00000000000..39035d551d1 --- /dev/null +++ b/features/project/ff_merge_requests.feature @@ -0,0 +1,41 @@ +Feature: Project Ff Merge Requests + Background: + Given I sign in as a user + And I own project "Shop" + And project "Shop" have "Bug NS-05" open merge request with diffs inside + And merge request "Bug NS-05" is mergeable + + @javascript + Scenario: I do ff-only merge for rebased branch + Given ff merge enabled + And merge request "Bug NS-05" is rebased + When I visit merge request page "Bug NS-05" + Then I should see ff-only merge button + When I accept this merge request + Then I should see merged request + + @javascript + Scenario: I do ff-only merge for merged branch + Given ff merge enabled + And merge request "Bug NS-05" merged target + When I visit merge request page "Bug NS-05" + Then I should see ff-only merge button + When I accept this merge request + Then I should see merged request + + @javascript + Scenario: I do rebase before ff-only merge + Given ff merge enabled + And rebase before merge enabled + When I visit merge request page "Bug NS-05" + Then I should see rebase button + When I press rebase button + Then I should see rebase in progress message + + @javascript + Scenario: I do rebase before regular merge + Given rebase before merge enabled + When I visit merge request page "Bug NS-05" + Then I should see rebase button + When I press rebase button + Then I should see rebase in progress message diff --git a/features/project/forked_merge_requests.feature b/features/project/forked_merge_requests.feature new file mode 100644 index 00000000000..9809b0ea0fe --- /dev/null +++ b/features/project/forked_merge_requests.feature @@ -0,0 +1,51 @@ +Feature: Project Forked Merge Requests + Background: + Given I sign in as a user + And I am a member of project "Shop" + And I have a project forked off of "Shop" called "Forked Shop" + + @javascript + Scenario: I submit new unassigned merge request to a forked project + Given I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + And I fill out a "Merge Request On Forked Project" merge request + And I submit the merge request + Then I should see merge request "Merge Request On Forked Project" + + # TODO: Improve it so it does not fail randomly + # + #@javascript + #Scenario: I can edit a forked merge request + #Given I visit project "Forked Shop" merge requests page + #And I click link "New Merge Request" + #And I fill out a "Merge Request On Forked Project" merge request + #And I submit the merge request + #And I should see merge request "Merge Request On Forked Project" + #And I click link edit "Merge Request On Forked Project" + #Then I see the edit page prefilled for "Merge Request On Forked Project" + #And I update the merge request title + #And I save the merge request + #Then I should see the edited merge request + + Scenario: I cannot submit an invalid merge request + Given I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + And I fill out an invalid "Merge Request On Forked Project" merge request + Then I should see validation errors + + @javascript + Scenario: Merge request should target fork repository by default + Given I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + Then the target repository should be the original repository + + @javascript + Scenario: I see the users in the target project for a new merge request + Given I sign in as an admin + And I have a project forked off of "Shop" called "Forked Shop" + Then I visit project "Forked Shop" merge requests page + And I click link "New Merge Request" + And I fill out a "Merge Request On Forked Project" merge request + When I click "Assign to" dropdown" + Then I should see the target project ID in the input selector + And I should see the users from the target project ID diff --git a/features/project/issues/references.feature b/features/project/issues/references.feature new file mode 100644 index 00000000000..4ae2d653337 --- /dev/null +++ b/features/project/issues/references.feature @@ -0,0 +1,33 @@ +@project_issues +Feature: Project Issues References + Background: + Given I sign in as "John Doe" + And public project "Community" + And "John Doe" owns public project "Community" + And project "Community" has "Community issue" open issue + And I logout + And I sign in as "Mary Jane" + And private project "Enterprise" + And "Mary Jane" owns private project "Enterprise" + And project "Enterprise" has "Enterprise issue" open issue + And project "Enterprise" has "Enterprise fix" open merge request + And I visit issue page "Enterprise issue" + And I leave a comment referencing issue "Community issue" + And I visit merge request page "Enterprise fix" + And I leave a comment referencing issue "Community issue" + And I logout + + @javascript + Scenario: Viewing the public issue as a "John Doe" + Given I sign in as "John Doe" + When I visit issue page "Community issue" + Then I should not see any related merge requests + And I should see no notes at all + + @javascript + Scenario: Viewing the public issue as "Mary Jane" + Given I sign in as "Mary Jane" + When I visit issue page "Community issue" + Then I should see the "Enterprise fix" related merge request + And I should see a note linking to "Enterprise fix" merge request + And I should see a note linking to "Enterprise issue" issue diff --git a/features/project/merge_requests/references.feature b/features/project/merge_requests/references.feature new file mode 100644 index 00000000000..571612261a9 --- /dev/null +++ b/features/project/merge_requests/references.feature @@ -0,0 +1,31 @@ +@project_merge_requests +Feature: Project Merge Requests References + Background: + Given I sign in as "John Doe" + And public project "Community" + And "John Doe" owns public project "Community" + And project "Community" has "Community fix" open merge request + And I logout + And I sign in as "Mary Jane" + And private project "Enterprise" + And "Mary Jane" owns private project "Enterprise" + And project "Enterprise" has "Enterprise issue" open issue + And project "Enterprise" has "Enterprise fix" open merge request + And I visit issue page "Enterprise issue" + And I leave a comment referencing issue "Community fix" + And I visit merge request page "Enterprise fix" + And I leave a comment referencing issue "Community fix" + And I logout + + @javascript + Scenario: Viewing the public issue as a "John Doe" + Given I sign in as "John Doe" + When I visit issue page "Community fix" + Then I should see no notes at all + + @javascript + Scenario: Viewing the public issue as "Mary Jane" + Given I sign in as "Mary Jane" + When I visit issue page "Community fix" + And I should see a note linking to "Enterprise fix" merge request + And I should see a note linking to "Enterprise issue" issue diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb new file mode 100644 index 00000000000..97bcca7730b --- /dev/null +++ b/features/steps/group/members.rb @@ -0,0 +1,68 @@ +class Spinach::Features::GroupMembers < Spinach::FeatureSteps + include WaitForRequests + include SharedAuthentication + include SharedPaths + include SharedGroup + include SharedUser + + step 'I should see user "John Doe" in team list' do + expect(group_members_list).to have_content("John Doe") + end + + step 'I should not see user "Mary Jane" in team list' do + expect(group_members_list).not_to have_content("Mary Jane") + end + + step 'I click on the "Remove User From Group" button for "John Doe"' do + find(:css, '.project-members-page li', text: "John Doe").find(:css, 'a.btn-remove').click + # poltergeist always confirms popups. + end + + step 'I click on the "Remove User From Group" button for "Mary Jane"' do + find(:css, 'li', text: "Mary Jane").find(:css, 'a.btn-remove').click + # poltergeist always confirms popups. + end + + step 'I should not see the "Remove User From Group" button for "John Doe"' do + expect(find(:css, '.project-members-page li', text: "John Doe")).not_to have_selector(:css, 'a.btn-remove') + # poltergeist always confirms popups. + end + + step 'I should not see the "Remove User From Group" button for "Mary Jane"' do + expect(find(:css, 'li', text: "Mary Jane")).not_to have_selector(:css, 'a.btn-remove') + # poltergeist always confirms popups. + end + + step 'I change the "Mary Jane" role to "Developer"' do + member = mary_jane_member + + page.within "#group_member_#{member.id}" do + click_button member.human_access + + page.within '.dropdown-menu' do + click_link 'Developer' + end + + wait_for_requests + end + end + + step 'I should see "Mary Jane" as "Developer"' do + member = mary_jane_member + + page.within "#group_member_#{member.id}" do + expect(page).to have_content "Developer" + end + end + + private + + def mary_jane_member + user = User.find_by(name: "Mary Jane") + owned_group.members.find_by(user_id: user.id) + end + + def group_members_list + find(".panel .content-list") + end +end diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb new file mode 100644 index 00000000000..f8eb0f01de8 --- /dev/null +++ b/features/steps/profile/notifications.rb @@ -0,0 +1,20 @@ +class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + + step 'I visit profile notifications page' do + visit profile_notifications_path + end + + step 'I should see global notifications settings' do + expect(page).to have_content "Notifications" + end + + step 'I select Mention setting from dropdown' do + first(:link, "On mention").click + end + + step 'I should see Notification saved message' do + expect(page).to have_content 'On mention' + end +end diff --git a/features/steps/project/builds/artifacts.rb b/features/steps/project/builds/artifacts.rb new file mode 100644 index 00000000000..4b72355b125 --- /dev/null +++ b/features/steps/project/builds/artifacts.rb @@ -0,0 +1,98 @@ +class Spinach::Features::ProjectBuildsArtifacts < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedBuilds + include RepoHelpers + include WaitForRequests + + step 'I click artifacts download button' do + click_link 'Download' + end + + step 'I click artifacts browse button' do + click_link 'Browse' + expect(page).not_to have_selector('.build-sidebar') + end + + step 'I should see content of artifacts archive' do + page.within('.tree-table') do + expect(page).to have_no_content '..' + expect(page).to have_content 'other_artifacts_0.1.2' + expect(page).to have_content 'ci_artifacts.txt' + expect(page).to have_content 'rails_sample.jpg' + end + end + + step 'I should see the build header' do + page.within('.build-header') do + expect(page).to have_content "Job ##{@build.id} in pipeline ##{@pipeline.id} for #{@pipeline.short_sha}" + end + end + + step 'I click link to subdirectory within build artifacts' do + page.within('.tree-table') { click_link 'other_artifacts_0.1.2' } + end + + step 'I should see content of subdirectory within artifacts archive' do + page.within('.tree-table') do + expect(page).to have_content '..' + expect(page).to have_content 'another-subdirectory' + expect(page).to have_content 'doc_sample.txt' + end + end + + step 'I should see the directory name in the breadcrumb' do + page.within('.repo-breadcrumb') do + expect(page).to have_content 'other_artifacts_0.1.2' + end + end + + step 'recent build artifacts contain directory with UTF-8 characters' do + # metadata fixture contains relevant directory + end + + step 'I navigate to directory with UTF-8 characters in name' do + page.within('.tree-table') { click_link 'tests_encoding' } + page.within('.tree-table') { click_link 'utf8 test dir ✓' } + end + + step 'I should see content of directory with UTF-8 characters in name' do + page.within('.tree-table') do + expect(page).to have_content '..' + expect(page).to have_content 'regular_file_2' + end + end + + step 'recent build artifacts contain directory with invalid UTF-8 characters' do + # metadata fixture contains relevant directory + end + + step 'I navigate to parent directory of directory with invalid name' do + page.within('.tree-table') { click_link 'tests_encoding' } + end + + step 'I should not see directory with invalid name on the list' do + page.within('.tree-table') do + expect(page).to have_no_content('non-utf8-dir') + end + end + + step 'I click a link to file within build artifacts' do + page.within('.tree-table') { find_link('ci_artifacts.txt').click } + wait_for_requests + end + + step 'I see a download link' do + expect(page).to have_link 'download it' + end + + step 'I click a first row within build artifacts table' do + row = first('tr[data-link]') + @row_path = row['data-link'] + row.click + end + + step 'page with a coresponding path is loading' do + expect(current_path).to eq @row_path + end +end diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb new file mode 100644 index 00000000000..3ecd4c8b672 --- /dev/null +++ b/features/steps/project/commits/branches.rb @@ -0,0 +1,32 @@ +class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + + step 'I click link "All"' do + click_link "All" + end + + step 'I click link "Protected"' do + click_link "Protected" + end + + step 'I click new branch link' do + click_link "New branch" + end + + step 'I submit new branch form with invalid name' do + fill_in 'branch_name', with: '1.0 stable' + page.find("body").click # defocus the branch_name input + select_branch('master') + click_button 'Create branch' + end + + def select_branch(branch_name) + find('.git-revision-dropdown-toggle').click + + page.within '#new-branch-form .dropdown-menu' do + click_link branch_name + end + end +end diff --git a/features/steps/project/commits/comments.rb b/features/steps/project/commits/comments.rb new file mode 100644 index 00000000000..3d4d8ad6368 --- /dev/null +++ b/features/steps/project/commits/comments.rb @@ -0,0 +1,6 @@ +class Spinach::Features::ProjectCommitsComments < Spinach::FeatureSteps + include SharedAuthentication + include SharedNote + include SharedPaths + include SharedProject +end diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb new file mode 100644 index 00000000000..959cf7d3e54 --- /dev/null +++ b/features/steps/project/commits/commits.rb @@ -0,0 +1,192 @@ +class Spinach::Features::ProjectCommits < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + include SharedDiffNote + include RepoHelpers + + step 'I see project commits' do + commit = @project.repository.commit + expect(page).to have_content(@project.name) + expect(page).to have_content(commit.message[0..20]) + expect(page).to have_content(commit.short_id) + end + + step 'I click atom feed link' do + click_link "Commits feed" + end + + step 'I see commits atom feed' do + commit = @project.repository.commit + expect(response_headers['Content-Type']).to have_content("application/atom+xml") + expect(body).to have_selector("title", text: "#{@project.name}:master commits") + expect(body).to have_selector("author email", text: commit.author_email) + expect(body).to have_selector("entry summary", text: commit.description[0..10].delete("\r\n")) + end + + step 'I click on tag link' do + click_link "Tag" + end + + step 'I see commit SHA pre-filled' do + expect(page).to have_selector("input[value='#{sample_commit.id}']") + end + + step 'I click on commit link' do + visit project_commit_path(@project, sample_commit.id) + end + + step 'I see commit info' do + expect(page).to have_content sample_commit.message + expect(page).to have_content "Showing #{sample_commit.files_changed_count} changed files" + end + + step 'I fill compare fields with branches' do + select_using_dropdown('from', 'feature') + select_using_dropdown('to', 'master') + + click_button 'Compare' + end + + step 'I fill compare fields with refs' do + select_using_dropdown('from', sample_commit.parent_id, true) + select_using_dropdown('to', sample_commit.id, true) + + click_button "Compare" + end + + step 'I unfold diff' do + @diff = first('.js-unfold') + @diff.click + sleep 2 + end + + step 'I should see additional file lines' do + page.within @diff.query_scope do + expect(first('.new_line').text).not_to have_content "..." + end + end + + step 'I see compared refs' do + expect(page).to have_content "Commits (1)" + expect(page).to have_content "Showing 2 changed files" + end + + step 'I visit commits list page for feature branch' do + visit project_commits_path(@project, 'feature', { limit: 5 }) + end + + step 'I see feature branch commits' do + commit = @project.repository.commit('0b4bc9a') + expect(page).to have_content(@project.name) + expect(page).to have_content(commit.message[0..12]) + expect(page).to have_content(commit.short_id) + end + + step 'project have an open merge request' do + create(:merge_request, + title: 'Feature', + source_project: @project, + source_branch: 'feature', + target_branch: 'master', + author: @project.users.first + ) + end + + step 'I click the "Compare" tab' do + click_link('Compare') + end + + step 'I fill compare fields with branches' do + select_using_dropdown('from', 'master') + select_using_dropdown('to', 'feature') + + click_button 'Compare' + end + + step 'I see compared branches' do + expect(page).to have_content 'Commits (1)' + expect(page).to have_content 'Showing 1 changed file with 5 additions and 0 deletions' + end + + step 'I see button to create a new merge request' do + expect(page).to have_link 'Create merge request' + end + + step 'I should not see button to create a new merge request' do + expect(page).not_to have_link 'Create merge request' + end + + step 'I should see button to the merge request' do + merge_request = MergeRequest.find_by(title: 'Feature') + expect(page).to have_link "View open merge request", href: project_merge_request_path(@project, merge_request) + end + + step 'I see breadcrumb links' do + expect(page).to have_selector('ul.breadcrumb') + expect(page).to have_selector('ul.breadcrumb a', count: 4) + end + + step 'I see commits stats' do + expect(page).to have_content 'Top 50 Committers' + expect(page).to have_content 'Committers' + expect(page).to have_content 'Total commits' + expect(page).to have_content 'Authors' + end + + step 'I visit a commit with an image that changed' do + visit project_commit_path(@project, sample_image_commit.id) + end + + step 'The diff links to both the previous and current image' do + links = page.all('.file-actions a') + expect(links[0]['href']).to match %r{blob/#{sample_image_commit.old_blob_id}} + expect(links[1]['href']).to match %r{blob/#{sample_image_commit.new_blob_id}} + end + + step 'I see inline diff button' do + expect(page).to have_content "Inline" + end + + step 'I click side-by-side diff button' do + find('#parallel-diff-btn').click + end + + step 'commit has ci status' do + @project.enable_ci + @pipeline = create(:ci_pipeline, project: @project, sha: sample_commit.id) + create(:ci_build, pipeline: @pipeline) + end + + step 'repository contains ".gitlab-ci.yml" file' do + allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(String.new) + end + + step 'I see commit ci info' do + expect(page).to have_content "Pipeline ##{@pipeline.id} pending" + end + + step 'I search "submodules" commits' do + fill_in 'commits-search', with: 'submodules' + end + + step 'I should see only "submodules" commits' do + expect(page).to have_content "More submodules" + expect(page).not_to have_content "Change some files" + end + + def select_using_dropdown(dropdown_type, selection, is_commit = false) + dropdown = find(".js-compare-#{dropdown_type}-dropdown") + dropdown.find(".compare-dropdown-toggle").click + dropdown.find('.dropdown-menu', visible: true) + dropdown.fill_in("Filter by Git revision", with: selection) + + if is_commit + dropdown.find('input[type="search"]').send_keys(:return) + else + find_link(selection, visible: true).click + end + + dropdown.find('.dropdown-menu', visible: false) + end +end diff --git a/features/steps/project/commits/diff_comments.rb b/features/steps/project/commits/diff_comments.rb new file mode 100644 index 00000000000..b9d8cf2c5a5 --- /dev/null +++ b/features/steps/project/commits/diff_comments.rb @@ -0,0 +1,6 @@ +class Spinach::Features::ProjectCommitsDiffComments < Spinach::FeatureSteps + include SharedAuthentication + include SharedDiffNote + include SharedPaths + include SharedProject +end diff --git a/features/steps/project/create.rb b/features/steps/project/create.rb new file mode 100644 index 00000000000..60fa232672e --- /dev/null +++ b/features/steps/project/create.rb @@ -0,0 +1,23 @@ +class Spinach::Features::ProjectCreate < Spinach::FeatureSteps + include SharedAuthentication + include SharedPaths + include SharedUser + + step 'fill project form with valid data' do + fill_in 'project_path', with: 'Empty' + page.within '#content-body' do + click_button "Create project" + end + end + + step 'I should see project page' do + expect(page).to have_content "Empty" + expect(current_path).to eq project_path(Project.last) + end + + step 'I should see empty project instructions' do + expect(page).to have_content "git init" + expect(page).to have_content "git remote" + expect(page).to have_content Project.last.url_to_repo + end +end diff --git a/features/steps/project/deploy_keys.rb b/features/steps/project/deploy_keys.rb new file mode 100644 index 00000000000..9db31522c5c --- /dev/null +++ b/features/steps/project/deploy_keys.rb @@ -0,0 +1,89 @@ +class Spinach::Features::ProjectDeployKeys < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + + step 'project has deploy key' do + create(:deploy_keys_project, project: @project) + end + + step 'I should see project deploy key' do + page.within(find('.deploy-keys')) do + expect(page).to have_content deploy_key.title + end + end + + step 'I should see other project deploy key' do + page.within(find('.deploy-keys')) do + expect(page).to have_content other_deploy_key.title + end + end + + step 'I should see public deploy key' do + page.within(find('.deploy-keys')) do + expect(page).to have_content public_deploy_key.title + end + end + + step 'I click \'New Deploy Key\'' do + click_link 'New deploy key' + end + + step 'I submit new deploy key' do + fill_in "deploy_key_title", with: "laptop" + fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop" + click_button "Add key" + end + + step 'I should be on deploy keys page' do + expect(current_path).to eq project_settings_repository_path(@project) + end + + step 'I should see newly created deploy key' do + @project.reload + page.within(find('.deploy-keys')) do + expect(page).to have_content(deploy_key.title) + end + end + + step 'other projects have deploy keys' do + @second_project = create(:project, namespace: create(:group)) + @second_project.add_master(current_user) + create(:deploy_keys_project, project: @second_project) + + @third_project = create(:project, namespace: create(:group)) + @third_project.add_master(current_user) + create(:deploy_keys_project, project: @third_project, deploy_key: @second_project.deploy_keys.first) + end + + step 'I should only see the same deploy key once' do + page.within(find('.deploy-keys')) do + expect(page).to have_selector('ul li', count: 1) + end + end + + step 'public deploy key exists' do + create(:deploy_key, public: true) + end + + step 'I click attach deploy key' do + page.within(find('.deploy-keys')) do + click_button 'Enable' + expect(page).not_to have_selector('.fa-spinner') + end + end + + protected + + def deploy_key + @project.deploy_keys.last + end + + def other_deploy_key + @second_project.deploy_keys.last + end + + def public_deploy_key + DeployKey.are_public.last + end +end diff --git a/features/steps/project/ff_merge_requests.rb b/features/steps/project/ff_merge_requests.rb new file mode 100644 index 00000000000..27efcfd65b6 --- /dev/null +++ b/features/steps/project/ff_merge_requests.rb @@ -0,0 +1,87 @@ +class Spinach::Features::ProjectFfMergeRequests < Spinach::FeatureSteps + include SharedAuthentication + include SharedIssuable + include SharedProject + include SharedNote + include SharedPaths + include SharedMarkdown + include SharedDiffNote + include SharedUser + include WaitForRequests + + step 'project "Shop" have "Bug NS-05" open merge request with diffs inside' do + create(:merge_request_with_diffs, + title: "Bug NS-05", + source_project: project, + target_project: project, + author: project.users.first) + end + + step 'merge request is mergeable' do + expect(page).to have_button 'Merge' + end + + step 'I should see ff-only merge button' do + expect(page).to have_content "Fast-forward merge without a merge commit" + expect(page).to have_button 'Merge' + end + + step 'merge request "Bug NS-05" is mergeable' do + merge_request.mark_as_mergeable + end + + step 'I accept this merge request' do + page.within '.mr-state-widget' do + click_button "Merge" + end + end + + step 'I should see merged request' do + page.within '.status-box' do + expect(page).to have_content "Merged" + wait_for_requests + end + end + + step 'ff merge enabled' do + project = merge_request.target_project + project.merge_requests_ff_only_enabled = true + project.save! + end + + step 'I should see rebase button' do + expect(page).to have_button "Rebase" + end + + step 'merge request "Bug NS-05" is rebased' do + merge_request.source_branch = 'flatten-dir' + merge_request.target_branch = 'improve/awesome' + merge_request.reload_diff + merge_request.save! + end + + step 'merge request "Bug NS-05" merged target' do + merge_request.source_branch = 'merged-target' + merge_request.target_branch = 'improve/awesome' + merge_request.reload_diff + merge_request.save! + end + + step 'rebase before merge enabled' do + project = merge_request.target_project + project.merge_requests_rebase_enabled = true + project.save! + end + + step 'I press rebase button' do + click_button "Rebase" + end + + step "I should see rebase in progress message" do + expect(page).to have_content("Rebase in progress") + end + + def merge_request + @merge_request ||= MergeRequest.find_by!(title: "Bug NS-05") + end +end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb new file mode 100644 index 00000000000..fd51ee1a316 --- /dev/null +++ b/features/steps/project/forked_merge_requests.rb @@ -0,0 +1,139 @@ +class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedNote + include SharedPaths + include Select2Helper + include WaitForRequests + include ProjectForksHelper + + step 'I am a member of project "Shop"' do + @project = ::Project.find_by(name: "Shop") + @project ||= create(:project, :repository, name: "Shop") + @project.add_reporter(@user) + end + + step 'I have a project forked off of "Shop" called "Forked Shop"' do + @forked_project = fork_project(@project, @user, + namespace: @user.namespace, + repository: true) + end + + step 'I click link "New Merge Request"' do + page.within '#content-body' do + page.has_link?('New Merge Request') ? click_link("New Merge Request") : click_link('New merge request') + end + end + + step 'I should see merge request "Merge Request On Forked Project"' do + expect(@project.merge_requests.size).to be >= 1 + @merge_request = @project.merge_requests.last + expect(current_path).to eq project_merge_request_path(@project, @merge_request) + expect(@merge_request.title).to eq "Merge Request On Forked Project" + expect(@merge_request.source_project).to eq @forked_project + expect(@merge_request.source_branch).to eq "fix" + expect(@merge_request.target_branch).to eq "master" + expect(page).to have_content @forked_project.full_path + expect(page).to have_content @project.full_path + expect(page).to have_content @merge_request.source_branch + expect(page).to have_content @merge_request.target_branch + + wait_for_requests + end + + step 'I fill out a "Merge Request On Forked Project" merge request' do + expect(page).to have_content('Source branch') + expect(page).to have_content('Target branch') + + first('.js-source-project').click + first('.dropdown-source-project a', text: @forked_project.full_path) + + first('.js-target-project').click + first('.dropdown-target-project a', text: @project.full_path) + + first('.js-source-branch').click + wait_for_requests + first('.dropdown-source-branch .dropdown-content a', text: 'fix').click + + click_button "Compare branches and continue" + + expect(page).to have_css("h3.page-title", text: "New Merge Request") + + page.within 'form#new_merge_request' do + fill_in "merge_request_title", with: "Merge Request On Forked Project" + end + end + + step 'I submit the merge request' do + click_button "Submit merge request" + end + + step 'I update the merge request title' do + fill_in "merge_request_title", with: "An Edited Forked Merge Request" + end + + step 'I save the merge request' do + click_button "Save changes" + end + + step 'I should see the edited merge request' do + expect(page).to have_content "An Edited Forked Merge Request" + expect(@project.merge_requests.size).to be >= 1 + @merge_request = @project.merge_requests.last + expect(current_path).to eq project_merge_request_path(@project, @merge_request) + expect(@merge_request.source_project).to eq @forked_project + expect(@merge_request.source_branch).to eq "fix" + expect(@merge_request.target_branch).to eq "master" + expect(page).to have_content @forked_project.full_path + expect(page).to have_content @project.full_path + expect(page).to have_content @merge_request.source_branch + expect(page).to have_content @merge_request.target_branch + end + + step 'I should see last push widget' do + expect(page).to have_content "You pushed to new_design" + expect(page).to have_link "Create Merge Request" + end + + step 'I click link edit "Merge Request On Forked Project"' do + find("#edit_merge_request").click + end + + step 'I see the edit page prefilled for "Merge Request On Forked Project"' do + expect(current_path).to eq edit_project_merge_request_path(@project, @merge_request) + expect(page).to have_content "Edit merge request #{@merge_request.to_reference}" + expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project" + end + + step 'I fill out an invalid "Merge Request On Forked Project" merge request' do + expect(find_by_id("merge_request_source_project_id", visible: false).value).to eq @forked_project.id.to_s + expect(find_by_id("merge_request_target_project_id", visible: false).value).to eq @project.id.to_s + expect(find_by_id("merge_request_source_branch", visible: false).value).to eq nil + expect(find_by_id("merge_request_target_branch", visible: false).value).to eq "master" + click_button "Compare branches" + end + + step 'I should see validation errors' do + expect(page).to have_content "You must select source and target branch" + end + + step 'the target repository should be the original repository' do + expect(find_by_id("merge_request_target_project_id").value).to eq "#{@project.id}" + end + + step 'I click "Assign to" dropdown"' do + click_button 'Assignee' + end + + step 'I should see the target project ID in the input selector' do + expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}" + end + + step 'I should see the users from the target project ID' do + page.within '.dropdown-menu-user' do + expect(page).to have_content 'Unassigned' + expect(page).to have_content current_user.name + expect(page).to have_content @project.users.first.name + end + end +end diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb new file mode 100644 index 00000000000..b467af53c98 --- /dev/null +++ b/features/steps/project/issues/filter_labels.rb @@ -0,0 +1,61 @@ +class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + include Select2Helper + + step 'I should see "Bugfix1" in issues list' do + page.within ".issues-list" do + expect(page).to have_content "Bugfix1" + end + end + + step 'I should see "Bugfix2" in issues list' do + page.within ".issues-list" do + expect(page).to have_content "Bugfix2" + end + end + + step 'I should not see "Bugfix2" in issues list' do + page.within ".issues-list" do + expect(page).not_to have_content "Bugfix2" + end + end + + step 'I should not see "Feature1" in issues list' do + page.within ".issues-list" do + expect(page).not_to have_content "Feature1" + end + end + + step 'I click "dropdown close button"' do + page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click + sleep 2 + end + + step 'I click link "feature"' do + page.within ".labels-filter" do + click_link "feature" + end + end + + step 'project "Shop" has issue "Bugfix1" with labels: "bug", "feature"' do + project = Project.find_by(name: "Shop") + issue = create(:issue, title: "Bugfix1", project: project) + issue.labels << project.labels.find_by(title: 'bug') + issue.labels << project.labels.find_by(title: 'feature') + end + + step 'project "Shop" has issue "Bugfix2" with labels: "bug", "enhancement"' do + project = Project.find_by(name: "Shop") + issue = create(:issue, title: "Bugfix2", project: project) + issue.labels << project.labels.find_by(title: 'bug') + issue.labels << project.labels.find_by(title: 'enhancement') + end + + step 'project "Shop" has issue "Feature1" with labels: "feature"' do + project = Project.find_by(name: "Shop") + issue = create(:issue, title: "Feature1", project: project) + issue.labels << project.labels.find_by(title: 'feature') + end +end diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb new file mode 100644 index 00000000000..baa78c23203 --- /dev/null +++ b/features/steps/project/issues/issues.rb @@ -0,0 +1,175 @@ +class Spinach::Features::ProjectIssues < Spinach::FeatureSteps + include SharedAuthentication + include SharedIssuable + include SharedProject + include SharedNote + include SharedPaths + include SharedMarkdown + include SharedUser + + step 'I should not see "Release 0.3" in issues' do + expect(page).not_to have_content "Release 0.3" + end + + step 'I click link "Closed"' do + find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click + end + + step 'I should see "Release 0.3" in issues' do + expect(page).to have_content "Release 0.3" + end + + step 'I should not see "Release 0.4" in issues' do + expect(page).not_to have_content "Release 0.4" + end + + step 'I click link "All"' do + find('.issues-state-filters [data-state="all"] span', text: 'All').click + # Waits for load + expect(find('.issues-state-filters > .active')).to have_content 'All' + end + + step 'I should see issue "Tweet control"' do + expect(page).to have_content "Tweet control" + end + + step 'I click "author" dropdown' do + page.find('.js-author-search').click + sleep 1 + end + + step 'I see current user as the first user' do + expect(page).to have_selector('.dropdown-content', visible: true) + users = page.all('.dropdown-menu-author .dropdown-content li a') + expect(users[0].text).to eq 'Any Author' + expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}" + end + + step 'I click link "500 error on profile"' do + click_link "500 error on profile" + end + + step 'I should see label \'bug\' with issue' do + page.within '.issuable-show-labels' do + expect(page).to have_content 'bug' + end + end + + step 'I fill in issue search with "Re"' do + filter_issue "Re" + end + + step 'I fill in issue search with "Bu"' do + filter_issue "Bu" + end + + step 'I fill in issue search with ".3"' do + filter_issue ".3" + end + + step 'I fill in issue search with "Something"' do + filter_issue "Something" + end + + step 'I fill in issue search with ""' do + filter_issue "" + end + + step 'project "Shop" has milestone "v2.2"' do + milestone = create(:milestone, title: "v2.2", project: project) + + 3.times { create(:issue, project: project, milestone: milestone) } + end + + step 'project "Shop" has milestone "v3.0"' do + milestone = create(:milestone, title: "v3.0", project: project) + + 3.times { create(:issue, project: project, milestone: milestone) } + end + + When 'I select milestone "v3.0"' do + select "v3.0", from: "milestone_id" + end + + step 'I should see selected milestone with title "v3.0"' do + issues_milestone_selector = "#issue_milestone_id_chzn > a" + expect(find(issues_milestone_selector)).to have_content("v3.0") + end + + When 'I select first assignee from "Shop" project' do + first_assignee = project.users.first + select first_assignee.name, from: "assignee_id" + end + + step 'I should see first assignee from "Shop" as selected assignee' do + issues_assignee_selector = "#issue_assignee_id_chzn > a" + + assignee_name = project.users.first.name + expect(find(issues_assignee_selector)).to have_content(assignee_name) + end + + step 'The list should be sorted by "Least popular"' do + page.within '.issues-list' do + page.within 'li.issue:nth-child(1)' do + expect(page).to have_content 'Tweet control' + expect(page).to have_content '1 2' + end + + page.within 'li.issue:nth-child(2)' do + expect(page).to have_content 'Release 0.4' + expect(page).to have_content '2 1' + end + + page.within 'li.issue:nth-child(3)' do + expect(page).to have_content 'Bugfix' + expect(page).not_to have_content '0 0' + end + end + end + + When 'I visit empty project page' do + project = Project.find_by(name: 'Empty Project') + visit project_path(project) + end + + When "I visit project \"Community\" issues page" do + project = Project.find_by(name: 'Community') + visit project_issues_path(project) + end + + step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do + create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project) + end + + step 'project \'Shop\' has issue \'Feature1\' with description: \'Feature submitted for issue1\'' do + create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project) + end + + step 'I fill in issue search with \'Description for issue1\'' do + filter_issue 'Description for issue' + end + + step 'I fill in issue search with \'issue1\'' do + filter_issue 'issue1' + end + + step 'I fill in issue search with \'Rock and roll\'' do + filter_issue 'Rock and roll' + end + + step 'I should see \'Bugfix1\' in issues' do + expect(page).to have_content 'Bugfix1' + end + + step 'I should see \'Feature1\' in issues' do + expect(page).to have_content 'Feature1' + end + + step 'I should not see \'Bugfix1\' in issues' do + expect(page).not_to have_content 'Bugfix1' + end + + def filter_issue(text) + fill_in 'issuable_search', with: text + end +end diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb new file mode 100644 index 00000000000..30927306a4f --- /dev/null +++ b/features/steps/project/issues/milestones.rb @@ -0,0 +1,20 @@ +class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + include SharedMarkdown + + step 'project "Shop" has milestone "v2.2"' do + project = Project.find_by(name: "Shop") + milestone = create(:milestone, + title: "v2.2", + project: project, + description: "# Description header" + ) + 3.times { create(:issue, project: project, milestone: milestone) } + end + + When 'I click link "All Issues"' do + click_link 'All Issues' + end +end diff --git a/features/steps/project/issues/references.rb b/features/steps/project/issues/references.rb new file mode 100644 index 00000000000..69e8b5cbde5 --- /dev/null +++ b/features/steps/project/issues/references.rb @@ -0,0 +1,7 @@ +class Spinach::Features::ProjectIssuesReferences < Spinach::FeatureSteps + include SharedAuthentication + include SharedIssuable + include SharedNote + include SharedProject + include SharedUser +end diff --git a/features/steps/project/merge_requests/references.rb b/features/steps/project/merge_requests/references.rb new file mode 100644 index 00000000000..ab2ae6847a2 --- /dev/null +++ b/features/steps/project/merge_requests/references.rb @@ -0,0 +1,7 @@ +class Spinach::Features::ProjectMergeRequestsReferences < Spinach::FeatureSteps + include SharedAuthentication + include SharedIssuable + include SharedNote + include SharedProject + include SharedUser +end diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb new file mode 100644 index 00000000000..afaad4b255e --- /dev/null +++ b/features/steps/project/source/browse_files.rb @@ -0,0 +1,435 @@ +# coding: utf-8 +class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps + include SharedAuthentication + include SharedProject + include SharedPaths + include RepoHelpers + include WaitForRequests + + step "I don't have write access" do + @project = create(:project, :repository, name: "Other Project", path: "other-project") + @project.add_reporter(@user) + visit project_tree_path(@project, root_ref) + end + + step 'I should see files from repository' do + expect(page).to have_content "VERSION" + expect(page).to have_content ".gitignore" + expect(page).to have_content "LICENSE" + end + + step 'I should see files from repository for "6d39438"' do + expect(current_path).to eq project_tree_path(@project, "6d39438") + expect(page).to have_content ".gitignore" + expect(page).to have_content "LICENSE" + end + + step 'I see the ".gitignore"' do + expect(page).to have_content '.gitignore' + end + + step 'I don\'t see the ".gitignore"' do + expect(page).not_to have_content '.gitignore' + end + + step 'I click on ".gitignore" file in repo' do + click_link ".gitignore" + end + + step 'I should see its content' do + wait_for_requests + expect(page).to have_content old_gitignore_content + end + + step 'I should see its new content' do + wait_for_requests + expect(page).to have_content new_gitignore_content + end + + step 'I click link "Raw"' do + click_link 'Open raw' + end + + step 'I should see raw file content' do + expect(source).to eq '' # Body is filled in by gitlab-workhorse + end + + step 'I click button "Edit"' do + find('.js-edit-blob').click + end + + step 'I cannot see the edit button' do + expect(page).not_to have_link 'edit' + end + + step 'I click button "Fork"' do + click_link 'Fork' + end + + step 'I edit code' do + expect(page).to have_selector('.file-editor') + set_new_content + end + + step 'I fill the new file name' do + fill_in :file_name, with: new_file_name + end + + step 'I fill the new branch name' do + fill_in :branch_name, with: 'new_branch_name', visible: true + end + + step 'I fill the new file name with a new directory' do + fill_in :file_name, with: new_file_name_with_directory + end + + step 'I fill the commit message' do + fill_in :commit_message, with: 'New commit message', visible: true + end + + step 'I click link "Diff"' do + click_link 'Preview changes' + end + + step 'I click on "Commit changes"' do + click_button 'Commit changes' + end + + step 'I click on "Changes" tab' do + click_link 'Changes' + end + + step 'I click on "Create directory"' do + click_button 'Create directory' + end + + step 'I click on "Delete"' do + click_on 'Delete' + end + + step 'I click on "Delete file"' do + click_button 'Delete file' + end + + step 'I click on "Replace"' do + click_on "Replace" + end + + step 'I click on "Replace file"' do + click_button 'Replace file' + end + + step 'I see diff' do + expect(page).to have_css '.line_holder.new' + end + + step 'I click on "New file" link in repo' do + find('.add-to-tree').click + click_link 'New file' + expect(page).to have_selector('.file-editor') + end + + step 'I click on "Upload file" link in repo' do + find('.add-to-tree').click + click_link 'Upload file' + end + + step 'I click on "New directory" link in repo' do + find('.add-to-tree').click + click_link 'New directory' + end + + step 'I fill the new directory name' do + fill_in :dir_name, with: new_dir_name + end + + step 'I fill an existing directory name' do + fill_in :dir_name, with: 'files' + end + + step 'I can see new file page' do + expect(page).to have_content "New File" + expect(page).to have_content "Commit message" + end + + step 'I click on "Upload file"' do + click_button 'Upload file' + end + + step 'I can see the new commit message' do + expect(page).to have_content "New commit message" + end + + step 'I upload a new text file' do + drop_in_dropzone test_text_file + end + + step 'I fill the upload file commit message' do + page.within('#modal-upload-blob') do + fill_in :commit_message, with: 'New commit message' + end + end + + step 'I replace it with a text file' do + drop_in_dropzone test_text_file + end + + step 'I fill the replace file commit message' do + page.within('#modal-upload-blob') do + fill_in :commit_message, with: 'Replacement file commit message' + end + end + + step 'I can see the replacement commit message' do + expect(page).to have_content "Replacement file commit message" + end + + step 'I can see the new text file' do + expect(page).to have_content "Lorem ipsum dolor sit amet" + expect(page).to have_content "Sed ut perspiciatis unde omnis" + end + + step 'I click on files directory' do + click_link 'files' + end + + step 'I click on History link' do + click_link 'History' + end + + step 'I see Browse dir link' do + expect(page).to have_link 'Browse Directory' + expect(page).not_to have_link 'Browse Code' + end + + step 'I click on readme file' do + page.within '.tree-table' do + click_link 'README.md' + end + end + + step 'I see Browse file link' do + expect(page).to have_link 'Browse File' + expect(page).not_to have_link 'Browse Files' + end + + step 'I see Browse code link' do + expect(page).to have_link 'Browse Files' + expect(page).not_to have_link 'Browse Directory' + end + + step 'I click on Permalink' do + click_link 'Permalink' + end + + step 'I am redirected to the files URL' do + expect(current_path).to eq project_tree_path(@project, 'master') + end + + step 'I am redirected to the ".gitignore"' do + expect(current_path).to eq(project_blob_path(@project, 'master/.gitignore')) + end + + step 'I am redirected to the permalink URL' do + expect(current_path).to( + eq(project_blob_path(@project, + @project.repository.commit.sha + + '/.gitignore')) + ) + end + + step 'I am redirected to the new file' do + expect(current_path).to eq( + project_blob_path(@project, 'master/' + new_file_name)) + end + + step 'I am redirected to the new file with directory' do + expect(current_path).to eq( + project_blob_path(@project, 'master/' + new_file_name_with_directory)) + end + + step 'I am redirected to the new merge request page' do + expect(current_path).to eq(project_new_merge_request_path(@project)) + end + + step "I am redirected to the fork's new merge request page" do + fork = @user.fork_of(@project) + expect(current_path).to eq(project_new_merge_request_path(fork)) + end + + step 'I am redirected to the root directory' do + expect(current_path).to eq( + project_tree_path(@project, 'master')) + end + + step "I don't see the permalink link" do + expect(page).not_to have_link('permalink') + end + + step 'I see "Unable to create directory"' do + expect(page).to have_content('A directory with this name already exists') + end + + step 'I see "Path can contain only..."' do + expect(page).to have_content('Path can contain only') + end + + step 'I see a commit error message' do + expect(page).to have_content('Your changes could not be committed') + end + + step "I switch ref to 'test'" do + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link "'test'" + end + end + + step "I switch ref to fix" do + first('.js-project-refs-dropdown').click + + page.within '.project-refs-form' do + click_link 'fix' + end + end + + step "I see the ref 'test' has been selected" do + expect(page).to have_selector '.dropdown-toggle-text', text: "'test'" + end + + step "I visit the 'test' tree" do + visit project_tree_path(@project, "'test'") + end + + step "I visit the fix tree" do + visit project_tree_path(@project, "fix/.testdir") + end + + step 'I see the commit data' do + expect(page).to have_css('.tree-commit-link', visible: true) + expect(page).not_to have_content('Loading commit data...') + end + + step 'I see the commit data for a directory with a leading dot' do + expect(page).to have_css('.tree-commit-link', visible: true) + expect(page).not_to have_content('Loading commit data...') + end + + step 'I click on "files/lfs/lfs_object.iso" file in repo' do + allow_any_instance_of(Project).to receive(:lfs_enabled?).and_return(true) + visit project_tree_path(@project, "lfs") + click_link 'files' + click_link "lfs" + click_link "lfs_object.iso" + end + + step 'I should see download link and object size' do + expect(page).to have_content 'Download (1.5 MB)' + end + + step 'I should not see lfs pointer details' do + expect(page).not_to have_content 'version https://git-lfs.github.com/spec/v1' + expect(page).not_to have_content 'oid sha256:91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' + expect(page).not_to have_content 'size 1575078' + end + + step 'I should see buttons for allowed commands' do + page.within '.content' do + expect(page).to have_link 'Download' + expect(page).to have_content 'History' + expect(page).to have_content 'Permalink' + expect(page).not_to have_content 'Edit' + expect(page).not_to have_content 'Blame' + expect(page).to have_content 'Delete' + expect(page).to have_content 'Replace' + end + end + + step 'I should see a Fork/Cancel combo' do + expect(page).to have_link 'Fork' + expect(page).to have_button 'Cancel' + end + + step 'I should see a notice about a new fork having been created' do + expect(page).to have_content "You're not allowed to make changes to this project directly. A fork of this project has been created that you can make changes in, so you can submit a merge request." + end + + # SVG files + step 'I upload a new SVG file' do + drop_in_dropzone test_svg_file + end + + step 'I visit the SVG file' do + visit project_blob_path(@project, 'new_branch_name/logo_sample.svg') + end + + step 'I can see the new rendered SVG image' do + expect(page).to have_css('.file-content img') + end + + private + + def set_new_content + find('#editor') + execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')") + end + + # Content of the gitignore file on the seed repository. + def old_gitignore_content + '*.rbc' + end + + # Constant value that differs from the content + # of the gitignore of the seed repository. + def new_gitignore_content + old_gitignore_content + 'a' + end + + # Constant value that is a valid filename and + # not a filename present at root of the seed repository. + def new_file_name + 'not_a_file.md' + end + + # Constant value that is a valid filename with directory and + # not a filename present at root of the seed repository. + def new_file_name_with_directory + 'foo/bar/baz.txt' + end + + # Constant value that is a valid directory and + # not a directory present at root of the seed repository. + def new_dir_name + 'new_dir/subdir' + end + + def drop_in_dropzone(file_path) + # Generate a fake input selector + page.execute_script <<-JS + var fakeFileInput = window.$('<input/>').attr( + {id: 'fakeFileInput', type: 'file'} + ).appendTo('body'); + JS + # Attach the file to the fake input selector with Capybara + attach_file("fakeFileInput", file_path) + # Add the file to a fileList array and trigger the fake drop event + page.execute_script <<-JS + var fileList = [$('#fakeFileInput')[0].files[0]]; + var e = jQuery.Event('drop', { dataTransfer : { files : fileList } }); + $('.dropzone')[0].dropzone.listeners[0].events.drop(e); + JS + end + + def test_text_file + File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') + end + + def test_image_file + File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') + end + + def test_svg_file + File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg') + end +end diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb new file mode 100644 index 00000000000..104d024fee2 --- /dev/null +++ b/features/steps/shared/active_tab.rb @@ -0,0 +1,32 @@ +module SharedActiveTab + include Spinach::DSL + include WaitForRequests + + after do + wait_for_requests if javascript_test? + end + + def ensure_active_main_tab(content) + expect(find('.sidebar-top-level-items > li.active')).to have_content(content) + end + + def ensure_active_sub_tab(content) + expect(find('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)')).to have_content(content) + end + + def ensure_active_sub_nav(content) + expect(find('.layout-nav .controls li.active')).to have_content(content) + end + + step 'no other main tabs should be active' do + expect(page).to have_selector('.sidebar-top-level-items > li.active', count: 1) + end + + step 'no other sub tabs should be active' do + expect(page).to have_selector('.sidebar-sub-level-items > li.active:not(.fly-out-top-item)', count: 1) + end + + step 'no other sub navs should be active' do + expect(page).to have_selector('.layout-nav .controls li.active', count: 1) + end +end diff --git a/features/steps/shared/admin.rb b/features/steps/shared/admin.rb new file mode 100644 index 00000000000..ac0a1764147 --- /dev/null +++ b/features/steps/shared/admin.rb @@ -0,0 +1,11 @@ +module SharedAdmin + include Spinach::DSL + + step 'there are projects in system' do + 2.times { create(:project, :repository) } + end + + step 'system has users' do + 2.times { create(:user) } + end +end diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb new file mode 100644 index 00000000000..97fac595d8e --- /dev/null +++ b/features/steps/shared/authentication.rb @@ -0,0 +1,75 @@ +require Rails.root.join('features', 'support', 'login_helpers') + +module SharedAuthentication + include Spinach::DSL + include LoginHelpers + + step 'I sign in as a user' do + sign_out(@user) if @user + + @user = create(:user) + sign_in(@user) + end + + step 'I sign in via the UI' do + gitlab_sign_in(create(:user)) + end + + step 'I sign in as an admin' do + sign_out(@user) if @user + + @user = create(:admin) + sign_in(@user) + end + + step 'I sign in as "John Doe"' do + gitlab_sign_in(user_exists("John Doe")) + end + + step 'I sign in as "Mary Jane"' do + gitlab_sign_in(user_exists("Mary Jane")) + end + + step 'I should be redirected to sign in page' do + expect(current_path).to eq new_user_session_path + end + + step "I logout" do + gitlab_sign_out + end + + step "I logout directly" do + gitlab_sign_out + end + + def current_user + @user || User.reorder(nil).first + end + + private + + def gitlab_sign_in(user) + visit new_user_session_path + + fill_in "user_login", with: user.email + fill_in "user_password", with: "12345678" + check 'user_remember_me' + click_button "Sign in" + + @user = user + end + + def gitlab_sign_out + return unless @user + + if Capybara.current_driver == Capybara.javascript_driver + find('.header-user-dropdown-toggle').click + click_link 'Sign out' + expect(page).to have_button('Sign in') + else + sign_out(@user) + end + + @user = nil + end +end diff --git a/features/steps/shared/builds.rb b/features/steps/shared/builds.rb new file mode 100644 index 00000000000..c2197584d8d --- /dev/null +++ b/features/steps/shared/builds.rb @@ -0,0 +1,53 @@ +module SharedBuilds + include Spinach::DSL + + step 'project has CI enabled' do + @project.enable_ci + end + + step 'project has coverage enabled' do + @project.update_attribute(:build_coverage_regex, /Coverage (\d+)%/) + end + + step 'project has a recent build' do + @pipeline = create(:ci_empty_pipeline, project: @project, sha: @project.commit.sha, ref: 'master') + @build = create(:ci_build, :running, :coverage, :trace_artifact, pipeline: @pipeline) + end + + step 'recent build is successful' do + @build.success + end + + step 'recent build failed' do + @build.drop + end + + step 'project has another build that is running' do + create(:ci_build, pipeline: @pipeline, name: 'second build', status_event: 'run') + end + + step 'I visit recent build details page' do + visit project_job_path(@project, @build) + end + + step 'recent build has artifacts available' do + artifacts = Rails.root + 'spec/fixtures/ci_build_artifacts.zip' + archive = fixture_file_upload(artifacts, 'application/zip') + @build.update_attributes(legacy_artifacts_file: archive) + end + + step 'recent build has artifacts metadata available' do + metadata = Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' + gzip = fixture_file_upload(metadata, 'application/x-gzip') + @build.update_attributes(legacy_artifacts_metadata: gzip) + end + + step 'recent build has a build trace' do + @build.trace.set('job trace') + end + + step 'download of build artifacts archive starts' do + expect(page.response_headers['Content-Type']).to eq 'application/zip' + expect(page.response_headers['Content-Transfer-Encoding']).to eq 'binary' + end +end diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb new file mode 100644 index 00000000000..aa32528a7ca --- /dev/null +++ b/features/steps/shared/diff_note.rb @@ -0,0 +1,237 @@ +module SharedDiffNote + include Spinach::DSL + include RepoHelpers + include WaitForRequests + + after do + wait_for_requests if javascript_test? + end + + step 'I cancel the diff comment' do + page.within(diff_file_selector) do + find(".js-close-discussion-note-form").click + end + end + + step 'I delete a diff comment' do + find('.note').hover + find(".js-note-delete").click + end + + step 'I haven\'t written any diff comment text' do + page.within(diff_file_selector) do + fill_in "note[note]", with: "" + end + end + + step 'I leave a diff comment like "Typo, please fix"' do + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + + page.within("form[data-line-code='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "Typo, please fix" + find(".js-comment-button").click + end + end + end + + step 'I leave a diff comment in a parallel view on the left side like "Old comment"' do + click_parallel_diff_line(sample_commit.del_line_code, 'old') + page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.del_line_code}']") do + fill_in "note[note]", with: "Old comment" + find(".js-comment-button").click + end + end + + step 'I leave a diff comment in a parallel view on the right side like "New comment"' do + click_parallel_diff_line(sample_commit.line_code, 'new') + page.within("#{diff_file_selector} form[data-line-code='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "New comment" + find(".js-comment-button").click + end + end + + step 'I preview a diff comment text like "Should fix it :smile:"' do + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + + page.within("form[data-line-code='#{sample_commit.line_code}']") do + fill_in "note[note]", with: "Should fix it :smile:" + find('.js-md-preview-button').click + end + end + end + + step 'I preview another diff comment text like "DRY this up"' do + page.within(diff_file_selector) do + click_diff_line(sample_commit.del_line_code) + + page.within("form[data-line-code='#{sample_commit.del_line_code}']") do + fill_in "note[note]", with: "DRY this up" + find('.js-md-preview-button').click + end + end + end + + step 'I open a diff comment form' do + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + end + end + + step 'I open another diff comment form' do + page.within(diff_file_selector) do + click_diff_line(sample_commit.del_line_code) + end + end + + step 'I write a diff comment like ":-1: I don\'t like this"' do + page.within(diff_file_selector) do + fill_in "note[note]", with: ":-1: I don\'t like this" + end + end + + step 'I write a diff comment like ":smile:"' do + page.within(diff_file_selector) do + click_diff_line(sample_commit.line_code) + + page.within("form[data-line-code='#{sample_commit.line_code}']") do + fill_in 'note[note]', with: ':smile:' + click_button('Comment') + end + end + end + + step 'I submit the diff comment' do + page.within(diff_file_selector) do + click_button("Comment") + end + end + + step 'I should not see the diff comment form' do + page.within(diff_file_selector) do + expect(page).not_to have_css("form.new_note") + end + end + + step 'The diff comment preview tab should say there is nothing to do' do + page.within(diff_file_selector) do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_content('Nothing to preview.') + end + end + + step 'I should not see the diff comment text field' do + page.within(diff_file_selector) do + expect(find('.js-note-text')).not_to be_visible + end + end + + step 'I should only see one diff form' do + page.within(diff_file_selector) do + expect(page).to have_css("form.new-note", count: 1) + end + end + + step 'I should see a diff comment form with ":-1: I don\'t like this"' do + page.within(diff_file_selector) do + expect(page).to have_field("note[note]", with: ":-1: I don\'t like this") + end + end + + step 'I should see a diff comment saying "Typo, please fix"' do + page.within("#{diff_file_selector} .note") do + expect(page).to have_content("Typo, please fix") + end + end + + step 'I should see a diff comment on the left side saying "Old comment"' do + page.within("#{diff_file_selector} .notes_content.parallel.old") do + expect(page).to have_content("Old comment") + end + end + + step 'I should see a diff comment on the right side saying "New comment"' do + page.within("#{diff_file_selector} .notes_content.parallel.new") do + expect(page).to have_content("New comment") + end + end + + step 'I should see a discussion reply button' do + page.within(diff_file_selector) do + expect(page).to have_button('Reply...') + end + end + + step 'I should see a temporary diff comment form' do + page.within(diff_file_selector) do + expect(page).to have_css(".js-temp-notes-holder form.new-note") + end + end + + step 'I should see an empty diff comment form' do + page.within(diff_file_selector) do + expect(page).to have_field("note[note]", with: "") + end + end + + step 'I should see the cancel comment button' do + page.within("#{diff_file_selector} form") do + expect(page).to have_css(".js-close-discussion-note-form", text: "Cancel") + end + end + + step 'I should see the diff comment preview' do + page.within("#{diff_file_selector} form") do + expect(page).to have_css('.js-md-preview', visible: true) + end + end + + step 'I should see the diff comment write tab' do + page.within(diff_file_selector) do + expect(page).to have_css('.js-md-write-button', visible: true) + end + end + + step 'The diff comment preview tab should display rendered Markdown' do + page.within(diff_file_selector) do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true) + end + end + + step 'I should see two separate previews' do + page.within(diff_file_selector) do + expect(page).to have_css('.js-md-preview', visible: true, count: 2) + expect(page).to have_content('Should fix it') + expect(page).to have_content('DRY this up') + end + end + + step 'I should see a diff comment with an emoji image' do + page.within("#{diff_file_selector} .note") do + expect(page).to have_xpath("//gl-emoji[@data-name='smile']") + end + end + + step 'I click side-by-side diff button' do + find('#parallel-diff-btn').click + end + + step 'I see side-by-side diff button' do + expect(page).to have_content "Side-by-side" + end + + def diff_file_selector + '.diff-file:nth-of-type(1)' + end + + def click_diff_line(code) + find(".line_holder[id='#{code}'] button").click + end + + def click_parallel_diff_line(code, line_type) + find(".line_holder.parallel td[id='#{code}']").find(:xpath, 'preceding-sibling::*[1][self::td]').hover + find(".line_holder.parallel button[data-line-code='#{code}']").click + end +end diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb new file mode 100644 index 00000000000..0a0588346b1 --- /dev/null +++ b/features/steps/shared/group.rb @@ -0,0 +1,50 @@ +module SharedGroup + include Spinach::DSL + + step 'current user is developer of group "Owned"' do + is_member_of(current_user.name, "Owned", Gitlab::Access::DEVELOPER) + end + + step '"John Doe" is owner of group "Owned"' do + is_member_of("John Doe", "Owned", Gitlab::Access::OWNER) + end + + step '"John Doe" is guest of group "Guest"' do + is_member_of("John Doe", "Guest", Gitlab::Access::GUEST) + end + + step '"Mary Jane" is owner of group "Owned"' do + is_member_of("Mary Jane", "Owned", Gitlab::Access::OWNER) + end + + step '"Mary Jane" is guest of group "Owned"' do + is_member_of("Mary Jane", "Owned", Gitlab::Access::GUEST) + end + + step '"Mary Jane" is guest of group "Guest"' do + is_member_of("Mary Jane", "Guest", Gitlab::Access::GUEST) + end + + step 'I should see group "TestGroup"' do + expect(page).to have_content "TestGroup" + end + + step 'I should not see group "TestGroup"' do + expect(page).not_to have_content "TestGroup" + end + + protected + + def is_member_of(username, groupname, role) + user = User.find_by(name: username) || create(:user, name: username) + group = Group.find_by(name: groupname) || create(:group, name: groupname) + group.add_user(user, role) + project ||= create(:project, :repository, namespace: group) + create(:closed_issue_event, project: project) + project.add_master(user) + end + + def owned_group + @owned_group ||= Group.find_by(name: "Owned") + end +end diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb new file mode 100644 index 00000000000..a9174efd334 --- /dev/null +++ b/features/steps/shared/issuable.rb @@ -0,0 +1,167 @@ +module SharedIssuable + include Spinach::DSL + + def edit_issuable + find('.js-issuable-edit', visible: true).click + end + + step 'project "Community" has "Community issue" open issue' do + create_issuable_for_project( + project_name: 'Community', + title: 'Community issue' + ) + end + + step 'project "Community" has "Community fix" open merge request' do + create_issuable_for_project( + project_name: 'Community', + type: :merge_request, + title: 'Community fix' + ) + end + + step 'project "Enterprise" has "Enterprise issue" open issue' do + create_issuable_for_project( + project_name: 'Enterprise', + title: 'Enterprise issue' + ) + end + + step 'project "Enterprise" has "Enterprise fix" open merge request' do + create_issuable_for_project( + project_name: 'Enterprise', + type: :merge_request, + title: 'Enterprise fix' + ) + end + + step 'I leave a comment referencing issue "Community issue"' do + leave_reference_comment( + issuable: Issue.find_by(title: 'Community issue'), + from_project_name: 'Enterprise' + ) + end + + step 'I leave a comment referencing issue "Community fix"' do + leave_reference_comment( + issuable: MergeRequest.find_by(title: 'Community fix'), + from_project_name: 'Enterprise' + ) + end + + step 'I visit issue page "Enterprise issue"' do + issue = Issue.find_by(title: 'Enterprise issue') + visit project_issue_path(issue.project, issue) + end + + step 'I visit merge request page "Enterprise fix"' do + mr = MergeRequest.find_by(title: 'Enterprise fix') + visit project_merge_request_path(mr.target_project, mr) + end + + step 'I visit issue page "Community issue"' do + issue = Issue.find_by(title: 'Community issue') + visit project_issue_path(issue.project, issue) + end + + step 'I visit issue page "Community fix"' do + mr = MergeRequest.find_by(title: 'Community fix') + visit project_merge_request_path(mr.target_project, mr) + end + + step 'I should not see any related merge requests' do + page.within '.issue-details' do + expect(page).not_to have_content('#merge-requests .merge-requests-title') + end + end + + step 'I should see the "Enterprise fix" related merge request' do + page.within '#merge-requests .merge-requests-title' do + expect(page).to have_content('1 Related Merge Request') + end + + page.within '#merge-requests ul' do + expect(page).to have_content('Enterprise fix') + end + end + + step 'I should see a note linking to "Enterprise fix" merge request' do + visible_note( + issuable: MergeRequest.find_by(title: 'Enterprise fix'), + from_project_name: 'Community', + user_name: 'Mary Jane' + ) + end + + step 'I should see a note linking to "Enterprise issue" issue' do + visible_note( + issuable: Issue.find_by(title: 'Enterprise issue'), + from_project_name: 'Community', + user_name: 'Mary Jane' + ) + end + + step 'I click link "Edit" for the merge request' do + edit_issuable + end + + step 'I sort the list by "Least popular"' do + find('button.dropdown-toggle').click + + page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do + click_link 'Least popular' + end + end + + step 'I click link "Next" in the sidebar' do + page.within '.issuable-sidebar' do + click_link 'Next' + end + end + + def create_issuable_for_project(project_name:, title:, type: :issue) + project = Project.find_by(name: project_name) + + attrs = { + title: title, + author: project.users.first, + description: '# Description header' + } + + case type + when :issue + attrs[:project] = project + when :merge_request + attrs.merge!( + source_project: project, + target_project: project, + source_branch: 'fix', + target_branch: 'master' + ) + end + + create(type, attrs) + end + + def leave_reference_comment(issuable:, from_project_name:) + project = Project.find_by(name: from_project_name) + + page.within('.js-main-target-form') do + fill_in 'note[note]', with: "##{issuable.to_reference(project)}" + click_button 'Comment' + end + end + + def visible_note(issuable:, from_project_name:, user_name:) + project = Project.find_by(name: from_project_name) + + expect(page).to have_content(user_name) + expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}") + end + + def expect_sidebar_content(content) + page.within '.issuable-sidebar' do + expect(page).to have_content content + end + end +end diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb new file mode 100644 index 00000000000..65118f07ca2 --- /dev/null +++ b/features/steps/shared/markdown.rb @@ -0,0 +1,11 @@ +module SharedMarkdown + include Spinach::DSL + + step 'I should not see the Markdown preview' do + expect(find('.gfm-form .js-md-preview')).not_to be_visible + end + + step 'I haven\'t written any description text' do + find('.gfm-form').fill_in 'Description', with: '' + end +end diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb new file mode 100644 index 00000000000..bf1b88c60d7 --- /dev/null +++ b/features/steps/shared/note.rb @@ -0,0 +1,25 @@ +module SharedNote + include Spinach::DSL + include WaitForRequests + + after do + wait_for_requests if javascript_test? + end + + step 'I haven\'t written any comment text' do + page.within(".js-main-target-form") do + fill_in "note[note]", with: "" + end + end + + step 'The comment preview tab should say there is nothing to do' do + page.within(".js-main-target-form") do + find('.js-md-preview-button').click + expect(find('.js-md-preview')).to have_content('Nothing to preview.') + end + end + + step 'I should see no notes at all' do + expect(page).not_to have_css('.note') + end +end diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb new file mode 100644 index 00000000000..014e6ad625b --- /dev/null +++ b/features/steps/shared/paths.rb @@ -0,0 +1,434 @@ +module SharedPaths + include Spinach::DSL + include RepoHelpers + include DashboardHelper + include WaitForRequests + + step 'I visit new project page' do + visit new_project_path + end + + step 'I visit login page' do + visit new_user_session_path + end + + # ---------------------------------------- + # User + # ---------------------------------------- + + step 'I visit user "John Doe" page' do + visit user_path("john_doe") + end + + # ---------------------------------------- + # Group + # ---------------------------------------- + + step 'I visit group "Owned" page' do + visit group_path(Group.find_by(name: "Owned")) + end + + step 'I visit group "Owned" activity page' do + visit activity_group_path(Group.find_by(name: "Owned")) + end + + step 'I visit group "Owned" issues page' do + visit issues_group_path(Group.find_by(name: "Owned")) + end + + step 'I visit group "Owned" merge requests page' do + visit merge_requests_group_path(Group.find_by(name: "Owned")) + end + + step 'I visit group "Owned" milestones page' do + visit group_milestones_path(Group.find_by(name: "Owned")) + end + + step 'I visit group "Owned" members page' do + visit group_group_members_path(Group.find_by(name: "Owned")) + end + + step 'I visit group "Owned" settings page' do + visit edit_group_path(Group.find_by(name: "Owned")) + end + + step 'I visit group "Owned" projects page' do + visit projects_group_path(Group.find_by(name: "Owned")) + end + + step 'I visit group "Guest" page' do + visit group_path(Group.find_by(name: "Guest")) + end + + step 'I visit group "Guest" issues page' do + visit issues_group_path(Group.find_by(name: "Guest")) + end + + step 'I visit group "Guest" merge requests page' do + visit merge_requests_group_path(Group.find_by(name: "Guest")) + end + + step 'I visit group "Guest" members page' do + visit group_group_members_path(Group.find_by(name: "Guest")) + end + + step 'I visit group "Guest" settings page' do + visit edit_group_path(Group.find_by(name: "Guest")) + end + + # ---------------------------------------- + # Dashboard + # ---------------------------------------- + + step 'I visit dashboard page' do + visit dashboard_projects_path + end + + step 'I visit dashboard activity page' do + visit activity_dashboard_path + end + + step 'I visit dashboard projects page' do + visit projects_dashboard_path + end + + step 'I visit dashboard issues page' do + visit assigned_issues_dashboard_path + end + + step 'I visit dashboard search page' do + visit search_path + end + + step 'I visit dashboard help page' do + visit help_path + end + + step 'I visit dashboard groups page' do + visit dashboard_groups_path + end + + step 'I should be redirected to the dashboard groups page' do + expect(current_path).to eq dashboard_groups_path + end + + step 'I visit dashboard starred projects page' do + visit starred_dashboard_projects_path + end + + # ---------------------------------------- + # Profile + # ---------------------------------------- + + step 'I visit profile page' do + visit profile_path + end + + step 'I visit profile applications page' do + visit applications_profile_path + end + + step 'I visit profile password page' do + visit edit_profile_password_path + end + + step 'I visit profile account page' do + visit profile_account_path + end + + step 'I visit profile SSH keys page' do + visit profile_keys_path + end + + step 'I visit profile preferences page' do + visit profile_preferences_path + end + + step 'I visit Authentication log page' do + visit audit_log_profile_path + end + + # ---------------------------------------- + # Admin + # ---------------------------------------- + + step 'I visit admin page' do + visit admin_root_path + end + + step 'I visit abuse reports page' do + visit admin_abuse_reports_path + end + + step 'I visit admin projects page' do + visit admin_projects_path + end + + step 'I visit admin users page' do + visit admin_users_path + end + + step 'I visit admin logs page' do + visit admin_logs_path + end + + step 'I visit admin messages page' do + visit admin_broadcast_messages_path + end + + step 'I visit admin hooks page' do + visit admin_hooks_path + end + + step 'I visit admin Resque page' do + visit admin_background_jobs_path + end + + step 'I visit admin teams page' do + visit admin_teams_path + end + + step 'I visit spam logs page' do + visit admin_spam_logs_path + end + + # ---------------------------------------- + # Generic Project + # ---------------------------------------- + + step "I visit my project's settings page" do + visit edit_project_path(@project) + end + + step 'I visit a binary file in the repo' do + visit project_blob_path(@project, + File.join(root_ref, 'files/images/logo-black.png')) + end + + step "I visit my project's commits page" do + visit project_commits_path(@project, root_ref, { limit: 5 }) + end + + step "I visit my project's commits page for a specific path" do + visit project_commits_path(@project, root_ref + "/files/ruby/regex.rb", { limit: 5 }) + end + + step 'I visit my project\'s commits stats page' do + visit stats_project_repository_path(@project) + end + + step "I visit my project's graph page" do + # Stub Graph max_size to speed up test (10 commits vs. 650) + Network::Graph.stub(max_count: 10) + + visit project_network_path(@project, root_ref) + end + + step "I visit my project's issues page" do + visit project_issues_path(@project) + end + + step "I visit my project's merge requests page" do + visit project_merge_requests_path(@project) + end + + step "I visit my project's members page" do + visit project_project_members_path(@project) + end + + step "I visit my project's wiki page" do + visit project_wiki_path(@project, :home) + end + + step 'I visit project hooks page' do + visit project_settings_integrations_path(@project) + end + + step 'I visit project deploy keys page' do + visit project_deploy_keys_path(@project) + end + + step 'I visit project find file page' do + visit project_find_file_path(@project, root_ref) + end + + # ---------------------------------------- + # "Shop" Project + # ---------------------------------------- + + step 'I visit project "Shop" page' do + visit project_path(project) + end + + step 'I visit project "Forked Shop" merge requests page' do + visit project_merge_requests_path(@forked_project) + end + + step 'I visit edit project "Shop" page' do + visit edit_project_path(project) + end + + step 'I visit compare refs page' do + visit project_compare_index_path(@project) + end + + step 'I visit project commits page' do + visit project_commits_path(@project, root_ref, { limit: 5 }) + end + + step 'I visit project commits page for stable branch' do + visit project_commits_path(@project, 'stable', { limit: 5 }) + end + + step 'I visit blob file from repo' do + visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path)) + end + + step 'I visit ".gitignore" file in repo' do + visit project_blob_path(@project, File.join(root_ref, '.gitignore')) + end + + step 'I am on the new file page' do + expect(current_path).to eq(project_create_blob_path(@project, root_ref)) + end + + step 'I am on the ".gitignore" edit file page' do + expect(current_path).to eq( + project_edit_blob_path(@project, File.join(root_ref, '.gitignore'))) + end + + step 'I visit project source page for "6d39438"' do + visit project_tree_path(@project, "6d39438") + end + + step 'I visit project source page for' \ + ' "6d394385cf567f80a8fd85055db1ab4c5295806f"' do + visit project_tree_path(@project, + '6d394385cf567f80a8fd85055db1ab4c5295806f') + end + + step 'I visit project tags page' do + visit project_tags_path(@project) + end + + step 'I visit project commit page' do + visit project_commit_path(@project, sample_commit.id) + end + + step 'I visit issue page "Release 0.4"' do + issue = Issue.find_by(title: "Release 0.4") + visit project_issue_path(issue.project, issue) + end + + step 'I visit project "Forum" labels page' do + project = Project.find_by(name: 'Forum') + visit project_labels_path(project) + end + + step 'I visit project "Shop" new label page' do + project = Project.find_by(name: 'Shop') + visit new_project_label_path(project) + end + + step 'I visit project "Forum" new label page' do + project = Project.find_by(name: 'Forum') + visit new_project_label_path(project) + end + + step 'I visit merge request page "Bug NS-04"' do + visit merge_request_path("Bug NS-04") + wait_for_requests + end + + step 'I visit merge request page "Bug NS-05"' do + visit merge_request_path("Bug NS-05") + wait_for_requests + end + + step 'I visit merge request page "Bug NS-07"' do + visit merge_request_path("Bug NS-07") + wait_for_requests + end + + step 'I visit merge request page "Bug NS-08"' do + visit merge_request_path("Bug NS-08") + wait_for_requests + end + + step 'I visit merge request page "Bug CO-01"' do + mr = MergeRequest.find_by(title: "Bug CO-01") + visit project_merge_request_path(mr.target_project, mr) + wait_for_requests + end + + step 'I visit forked project "Shop" merge requests page' do + visit project_merge_requests_path(project) + end + + step 'I visit project "Shop" team page' do + visit project_project_members_path(project) + end + + step 'I visit project wiki page' do + visit project_wiki_path(@project, :home) + end + + # ---------------------------------------- + # Visibility Projects + # ---------------------------------------- + + step 'I visit project "Community" source page' do + project = Project.find_by(name: 'Community') + visit project_tree_path(project, root_ref) + end + + step 'I visit project "Internal" page' do + project = Project.find_by(name: "Internal") + visit project_path(project) + end + + step 'I visit project "Enterprise" page' do + project = Project.find_by(name: "Enterprise") + visit project_path(project) + end + + # ---------------------------------------- + # Empty Projects + # ---------------------------------------- + + step "I should not see command line instructions" do + expect(page).not_to have_css('.empty_wrapper') + end + + # ---------------------------------------- + # Public Projects + # ---------------------------------------- + step 'I visit the public groups area' do + visit explore_groups_path + end + + # ---------------------------------------- + # Snippets + # ---------------------------------------- + + step 'I visit project "Shop" snippets page' do + visit project_snippets_path(project) + end + + step 'I visit snippets page' do + visit explore_snippets_path + end + + def root_ref + @project.repository.root_ref + end + + def project + Project.find_by!(name: 'Shop') + end + + def merge_request_path(title) + mr = MergeRequest.find_by(title: title) + project_merge_request_path(mr.target_project, mr) + end +end diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb new file mode 100644 index 00000000000..a1945cf5f3d --- /dev/null +++ b/features/steps/shared/project.rb @@ -0,0 +1,132 @@ +module SharedProject + include Spinach::DSL + + # Create a project without caring about what it's called + step "I own a project" do + @project = create(:project, :repository, namespace: @user.namespace) + @project.add_master(@user) + end + + step "I own a project in some group namespace" do + @group = create(:group, name: 'some group') + @project = create(:project, namespace: @group) + @project.add_master(@user) + end + + # Create a specific project called "Shop" + step 'I own project "Shop"' do + @project = Project.find_by(name: "Shop") + @project ||= create(:project, :repository, name: "Shop", namespace: @user.namespace) + @project.add_master(@user) + end + + def current_project + @project ||= Project.first + end + + # ---------------------------------------- + # Visibility of archived project + # ---------------------------------------- + + step 'I should not see project "Archive"' do + project = Project.find_by(name: "Archive") + expect(page).not_to have_content project.full_name + end + + step 'I should see project "Archive"' do + project = Project.find_by(name: "Archive") + expect(page).to have_content project.full_name + end + + # ---------------------------------------- + # Visibility level + # ---------------------------------------- + + step 'private project "Enterprise"' do + create(:project, :private, :repository, name: 'Enterprise') + end + + step 'I should see project "Enterprise"' do + expect(page).to have_content "Enterprise" + end + + step 'I should not see project "Enterprise"' do + expect(page).not_to have_content "Enterprise" + end + + step 'internal project "Internal"' do + create(:project, :internal, :repository, name: 'Internal') + end + + step 'I should see project "Internal"' do + page.within '.js-projects-list-holder' do + expect(page).to have_content "Internal" + end + end + + step 'I should not see project "Internal"' do + page.within '.js-projects-list-holder' do + expect(page).not_to have_content "Internal" + end + end + + step 'public project "Community"' do + create(:project, :public, :repository, name: 'Community') + end + + step 'I should see project "Community"' do + expect(page).to have_content "Community" + end + + step 'I should not see project "Community"' do + expect(page).not_to have_content "Community" + end + + step '"John Doe" owns private project "Enterprise"' do + user_owns_project( + user_name: 'John Doe', + project_name: 'Enterprise' + ) + end + + step '"Mary Jane" owns private project "Enterprise"' do + user_owns_project( + user_name: 'Mary Jane', + project_name: 'Enterprise' + ) + end + + step '"John Doe" owns internal project "Internal"' do + user_owns_project( + user_name: 'John Doe', + project_name: 'Internal', + visibility: :internal + ) + end + + step '"John Doe" owns public project "Community"' do + user_owns_project( + user_name: 'John Doe', + project_name: 'Community', + visibility: :public + ) + end + + step 'public empty project "Empty Public Project"' do + create :project_empty_repo, :public, name: "Empty Public Project" + end + + step 'project "Shop" has labels: "bug", "feature", "enhancement"' do + project = Project.find_by(name: "Shop") + create(:label, project: project, title: 'bug') + create(:label, project: project, title: 'feature') + create(:label, project: project, title: 'enhancement') + end + + def user_owns_project(user_name:, project_name:, visibility: :private) + user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore) + project = Project.find_by(name: project_name) + project ||= create(:project, visibility, name: project_name, namespace: user.namespace) + project.add_master(user) + end +end diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb new file mode 100644 index 00000000000..5a516ee33bc --- /dev/null +++ b/features/steps/shared/project_tab.rb @@ -0,0 +1,66 @@ +require_relative 'active_tab' + +module SharedProjectTab + include Spinach::DSL + include SharedActiveTab + + step 'the active main tab should be Project' do + ensure_active_main_tab('Overview') + end + + step 'the active main tab should be Repository' do + ensure_active_main_tab('Repository') + end + + step 'the active main tab should be Issues' do + ensure_active_main_tab('Issues') + end + + step 'the active sub tab should be Members' do + ensure_active_sub_tab('Members') + end + + step 'the active main tab should be Merge Requests' do + ensure_active_main_tab('Merge Requests') + end + + step 'the active main tab should be Snippets' do + ensure_active_main_tab('Snippets') + end + + step 'the active main tab should be Wiki' do + ensure_active_main_tab('Wiki') + end + + step 'the active main tab should be Members' do + ensure_active_main_tab('Members') + end + + step 'the active main tab should be Settings' do + ensure_active_main_tab('Settings') + end + + step 'the active sub tab should be Graph' do + ensure_active_sub_tab('Graph') + end + + step 'the active sub tab should be Files' do + ensure_active_sub_tab('Files') + end + + step 'the active sub tab should be Commits' do + ensure_active_sub_tab('Commits') + end + + step 'the active sub tab should be Home' do + ensure_active_sub_tab('Details') + end + + step 'the active sub tab should be Activity' do + ensure_active_sub_tab('Activity') + end + + step 'the active sub tab should be Charts' do + ensure_active_sub_tab('Charts') + end +end diff --git a/features/steps/shared/shortcuts.rb b/features/steps/shared/shortcuts.rb new file mode 100644 index 00000000000..a75a8474d26 --- /dev/null +++ b/features/steps/shared/shortcuts.rb @@ -0,0 +1,18 @@ +module SharedShortcuts + include Spinach::DSL + + step 'I press "g" and "p"' do + find('body').native.send_key('g') + find('body').native.send_key('p') + end + + step 'I press "g" and "i"' do + find('body').native.send_key('g') + find('body').native.send_key('i') + end + + step 'I press "g" and "m"' do + find('body').native.send_key('g') + find('body').native.send_key('m') + end +end diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb new file mode 100644 index 00000000000..07fff16e867 --- /dev/null +++ b/features/steps/shared/sidebar_active_tab.rb @@ -0,0 +1,31 @@ +module SharedSidebarActiveTab + include Spinach::DSL + + step 'no other main tabs should be active' do + expect(page).to have_selector('.nav-sidebar li.active', count: 1) + end + + def ensure_active_main_tab(content) + expect(find('.nav-sidebar li.active')).to have_content(content) + end + + step 'the active main tab should be Home' do + ensure_active_main_tab('Projects') + end + + step 'the active main tab should be Groups' do + ensure_active_main_tab('Groups') + end + + step 'the active main tab should be Projects' do + ensure_active_main_tab('Projects') + end + + step 'the active main tab should be Issues' do + ensure_active_main_tab('Issues') + end + + step 'the active main tab should be Merge Requests' do + ensure_active_main_tab('Merge Requests') + end +end diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb new file mode 100644 index 00000000000..9cadc91769d --- /dev/null +++ b/features/steps/shared/user.rb @@ -0,0 +1,41 @@ +module SharedUser + include Spinach::DSL + + step 'User "John Doe" exists' do + user_exists("John Doe", { username: "john_doe" }) + end + + step 'User "Mary Jane" exists' do + user_exists("Mary Jane", { username: "mary_jane" }) + end + + step 'gitlab user "Mike"' do + create(:user, name: "Mike") + end + + protected + + def user_exists(name, options = {}) + User.find_by(name: name) || create(:user, { name: name, admin: false }.merge(options)) + end + + step 'I have no ssh keys' do + @user.keys.delete_all + end + + step 'I click on "Personal projects" tab' do + page.within '.nav-links' do + click_link 'Personal projects' + end + + expect(page).to have_css('.tab-content #projects.active') + end + + step 'I click on "Contributed projects" tab' do + page.within '.nav-links' do + click_link 'Contributed projects' + end + + expect(page).to have_css('.tab-content #contributed.active') + end +end diff --git a/features/support/capybara.rb b/features/support/capybara.rb new file mode 100644 index 00000000000..8879c9ab650 --- /dev/null +++ b/features/support/capybara.rb @@ -0,0 +1,50 @@ +require 'capybara-screenshot/spinach' + +# Give CI some extra time +timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 60 : 30 + +Capybara.register_driver :chrome do |app| + capabilities = Selenium::WebDriver::Remote::Capabilities.chrome( + # This enables access to logs with `page.driver.manage.get_log(:browser)` + loggingPrefs: { + browser: "ALL", + client: "ALL", + driver: "ALL", + server: "ALL" + } + ) + + options = Selenium::WebDriver::Chrome::Options.new + options.add_argument("window-size=1240,1400") + + # Chrome won't work properly in a Docker container in sandbox mode + options.add_argument("no-sandbox") + + # Run headless by default unless CHROME_HEADLESS specified + options.add_argument("headless") unless ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i + + # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252 + options.add_argument("disable-dev-shm-usage") if ENV['CI'] || ENV['CI_SERVER'] + + Capybara::Selenium::Driver.new( + app, + browser: :chrome, + desired_capabilities: capabilities, + options: options + ) +end + +Capybara.javascript_driver = :chrome +Capybara.default_max_wait_time = timeout +Capybara.ignore_hidden_elements = false + +# Keep only the screenshots generated from the last failing test suite +Capybara::Screenshot.prune_strategy = :keep_last_run +# From https://github.com/mattheworiordan/capybara-screenshot/issues/84#issuecomment-41219326 +Capybara::Screenshot.register_driver(:chrome) do |driver, path| + driver.browser.save_screenshot(path) +end + +Spinach.hooks.before_run do + TestEnv.eager_load_driver_server +end diff --git a/features/support/db_cleaner.rb b/features/support/db_cleaner.rb new file mode 100644 index 00000000000..31c922d23c3 --- /dev/null +++ b/features/support/db_cleaner.rb @@ -0,0 +1,11 @@ +require 'database_cleaner' + +DatabaseCleaner[:active_record].strategy = :deletion + +Spinach.hooks.before_scenario do + DatabaseCleaner.start +end + +Spinach.hooks.after_scenario do + DatabaseCleaner.clean +end diff --git a/features/support/env.rb b/features/support/env.rb new file mode 100644 index 00000000000..8fa2fcb6e3e --- /dev/null +++ b/features/support/env.rb @@ -0,0 +1,60 @@ +require './spec/simplecov_env' +SimpleCovEnv.start! + +ENV['RAILS_ENV'] = 'test' +require './config/environment' +require 'rspec/expectations' + +if ENV['CI'] + require 'knapsack' + Knapsack::Adapters::SpinachAdapter.bind +end + +WebMock.enable! + +%w(select2_helper test_env repo_helpers wait_for_requests project_forks_helper).each do |f| + require Rails.root.join('spec', 'support', 'helpers', f) +end + +%w(sidekiq webmock).each do |f| + require Rails.root.join('spec', 'support', f) +end + +Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file } + +Spinach.hooks.before_run do + include RSpec::Mocks::ExampleMethods + include ActiveJob::TestHelper + include FactoryBot::Syntax::Methods + include GitlabRoutingHelper + + RSpec::Mocks.setup + TestEnv.init(mailer: false) + + # skip pre-receive hook check so we can use + # web editor and merge + TestEnv.disable_pre_receive +end + +Spinach.hooks.after_scenario do |scenario_data, step_definitions| + if scenario_data.tags.include?('javascript') + include WaitForRequests + block_and_wait_for_requests_complete + end +end + +module StdoutReporterWithScenarioLocation + # Override the standard reporter to show filename and line number next to each + # scenario for easy, focused re-runs + def before_scenario_run(scenario, step_definitions = nil) + @max_step_name_length = scenario.steps.map(&:name).map(&:length).max if scenario.steps.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables + name = scenario.name + + # This number has no significance, it's just to line things up + max_length = @max_step_name_length + 19 # rubocop:disable Gitlab/ModuleWithInstanceVariables + out.puts "\n #{'Scenario:'.green} #{name.light_green.ljust(max_length)}" \ + " # #{scenario.feature.filename}:#{scenario.line}" + end +end + +Spinach::Reporter::Stdout.prepend(StdoutReporterWithScenarioLocation) diff --git a/features/support/gitaly.rb b/features/support/gitaly.rb new file mode 100644 index 00000000000..3cd5f4ce497 --- /dev/null +++ b/features/support/gitaly.rb @@ -0,0 +1,3 @@ +Spinach.hooks.before_scenario do + allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true) +end diff --git a/features/support/login_helpers.rb b/features/support/login_helpers.rb new file mode 100644 index 00000000000..540ff25a4f2 --- /dev/null +++ b/features/support/login_helpers.rb @@ -0,0 +1,19 @@ +module LoginHelpers + # After inclusion, IntegrationHelpers calls these two methods that aren't + # supported by Spinach, so we perform the end results ourselves + class << self + def setup(*args) + Spinach.hooks.before_scenario do + Warden.test_mode! + end + end + + def teardown(*args) + Spinach.hooks.after_scenario do + Warden.test_reset! + end + end + end + + include Devise::Test::IntegrationHelpers +end diff --git a/features/support/rerun.rb b/features/support/rerun.rb new file mode 100644 index 00000000000..60b78f9d050 --- /dev/null +++ b/features/support/rerun.rb @@ -0,0 +1,16 @@ +# The spinach-rerun-reporter doesn't define the on_undefined_step +# See it here: https://github.com/javierav/spinach-rerun-reporter/blob/master/lib/spinach/reporter/rerun.rb +require 'spinach-rerun-reporter' + +module Spinach + class Reporter + class Rerun + def on_undefined_step(step_data, failure, step_definitions = nil) + super step_data, failure, step_definitions + + # save feature file and scenario line + @rerun << "#{current_feature.filename}:#{current_scenario.line}" + end + end + end +end diff --git a/lib/gitlab/middleware/webpack_proxy.rb b/lib/gitlab/middleware/webpack_proxy.rb new file mode 100644 index 00000000000..6aecf63231f --- /dev/null +++ b/lib/gitlab/middleware/webpack_proxy.rb @@ -0,0 +1,26 @@ +# This Rack middleware is intended to proxy the webpack assets directory to the +# webpack-dev-server. It is only intended for use in development. + +# :nocov: +module Gitlab + module Middleware + class WebpackProxy < Rack::Proxy + def initialize(app = nil, opts = {}) + @proxy_host = opts.fetch(:proxy_host, 'localhost') + @proxy_port = opts.fetch(:proxy_port, 3808) + @proxy_path = opts[:proxy_path] if opts[:proxy_path] + + super(app, backend: "http://#{@proxy_host}:#{@proxy_port}", **opts) + end + + def perform_request(env) + if @proxy_path && env['PATH_INFO'].start_with?("/#{@proxy_path}") + super(env) + else + @app.call(env) + end + end + end + end +end +# :nocov: diff --git a/lib/tasks/spinach.rake b/lib/tasks/spinach.rake new file mode 100644 index 00000000000..19ff13f06c0 --- /dev/null +++ b/lib/tasks/spinach.rake @@ -0,0 +1,60 @@ +Rake::Task["spinach"].clear if Rake::Task.task_defined?('spinach') + +namespace :spinach do + namespace :project do + desc "GitLab | Spinach | Run project commits, issues and merge requests spinach features" + task :half do + run_spinach_tests('@project_commits,@project_issues,@project_merge_requests') + end + + desc "GitLab | Spinach | Run remaining project spinach features" + task :rest do + run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets,~@project_commits,~@project_issues,~@project_merge_requests') + end + end + + desc "GitLab | Spinach | Run project spinach features" + task :project do + run_spinach_tests('~@admin,~@dashboard,~@profile,~@public,~@snippets') + end + + desc "GitLab | Spinach | Run other spinach features" + task :other do + run_spinach_tests('@admin,@dashboard,@profile,@public,@snippets') + end + + desc "GitLab | Spinach | Run other spinach features" + task :builds do + run_spinach_tests('@builds') + end +end + +desc "GitLab | Run spinach" +task :spinach do + run_spinach_tests(nil) +end + +def run_system_command(cmd) + system({ 'RAILS_ENV' => 'test', 'force' => 'yes' }, *cmd) +end + +def run_spinach_command(args) + run_system_command(%w(spinach -r rerun) + args) +end + +def run_spinach_tests(tags) + success = run_spinach_command(%W(--tags #{tags})) + 3.times do |_| + break if success + break unless File.exist?('tmp/spinach-rerun.txt') + + tests = File.foreach('tmp/spinach-rerun.txt').map(&:chomp) + puts '' + puts "Spinach tests for #{tags}: Retrying tests... #{tests}".color(:red) + puts '' + sleep(3) + success = run_spinach_command(tests) + end + + raise("spinach tests for #{tags} failed!") unless success +end diff --git a/spec/features/projects/artifacts/browse_spec.rb b/spec/features/projects/artifacts/browse_spec.rb new file mode 100644 index 00000000000..cb69aff8d5f --- /dev/null +++ b/spec/features/projects/artifacts/browse_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +feature 'Browse artifact', :js do + let(:project) { create(:project, :public) } + let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:browse_url) do + browse_path('other_artifacts_0.1.2') + end + + def browse_path(path) + browse_project_job_artifacts_path(project, job, path) + end + + context 'when visiting old URL' do + before do + visit browse_url.sub('/-/jobs', '/builds') + end + + it "redirects to new URL" do + expect(page.current_path).to eq(browse_url) + end + end + + context 'when browsing a directory with an text file' do + let(:txt_entry) { job.artifacts_metadata_entry('other_artifacts_0.1.2/doc_sample.txt') } + + before do + allow(Gitlab.config.pages).to receive(:enabled).and_return(true) + allow(Gitlab.config.pages).to receive(:artifacts_server).and_return(true) + end + + context 'when the project is public' do + it "shows external link icon and styles" do + visit browse_url + + link = first('.tree-item-file-external-link') + + expect(page).to have_link('doc_sample.txt', href: file_project_job_artifacts_path(project, job, path: txt_entry.blob.path)) + expect(link[:target]).to eq('_blank') + expect(link[:rel]).to include('noopener') + expect(link[:rel]).to include('noreferrer') + expect(page).to have_selector('.js-artifact-tree-external-icon') + end + end + + context 'when the project is private' do + let!(:private_project) { create(:project, :private) } + let(:pipeline) { create(:ci_empty_pipeline, project: private_project) } + let(:job) { create(:ci_build, :artifacts, pipeline: pipeline) } + let(:user) { create(:user) } + + before do + private_project.add_developer(user) + + sign_in(user) + end + + it 'shows internal link styles' do + visit browse_project_job_artifacts_path(private_project, job, 'other_artifacts_0.1.2') + + expect(page).to have_link('doc_sample.txt') + expect(page).not_to have_selector('.js-artifact-tree-external-icon') + end + end + end +end diff --git a/spec/features/projects/artifacts/download_spec.rb b/spec/features/projects/artifacts/download_spec.rb new file mode 100644 index 00000000000..6f76c14910b --- /dev/null +++ b/spec/features/projects/artifacts/download_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +feature 'Download artifact' do + let(:project) { create(:project, :public) } + let(:pipeline) { create(:ci_empty_pipeline, status: :success, project: project) } + let(:job) { create(:ci_build, :artifacts, :success, pipeline: pipeline) } + + shared_examples 'downloading' do + it 'downloads the zip' do + expect(page.response_headers['Content-Disposition']) + .to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"}) + + # Check the content does match, but don't print this as error message + expect(page.source.b == job.artifacts_file.file.read.b) + end + end + + context 'when downloading' do + before do + visit download_url + end + + context 'via job id' do + let(:download_url) do + download_project_job_artifacts_path(project, job) + end + + it_behaves_like 'downloading' + end + + context 'via branch name and job name' do + let(:download_url) do + latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name) + end + + it_behaves_like 'downloading' + end + end + + context 'when visiting old URL' do + before do + visit download_url.sub('/-/jobs', '/builds') + end + + context 'via job id' do + let(:download_url) do + download_project_job_artifacts_path(project, job) + end + + it_behaves_like 'downloading' + end + + context 'via branch name and job name' do + let(:download_url) do + latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name) + end + + it_behaves_like 'downloading' + end + end +end diff --git a/spec/features/projects/commit/user_comments_on_commit_spec.rb b/spec/features/projects/commit/user_comments_on_commit_spec.rb new file mode 100644 index 00000000000..5174f793367 --- /dev/null +++ b/spec/features/projects/commit/user_comments_on_commit_spec.rb @@ -0,0 +1,110 @@ +require "spec_helper" + +describe "User comments on commit", :js do + include Spec::Support::Helpers::Features::NotesHelpers + include RepoHelpers + + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + COMMENT_TEXT = "XML attached".freeze + + before do + sign_in(user) + project.add_developer(user) + + visit(project_commit_path(project, sample_commit.id)) + end + + context "when adding new comment" do + it "adds comment" do + EMOJI = ":+1:".freeze + + page.within(".js-main-target-form") do + expect(page).not_to have_link("Cancel") + + fill_in("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}") + + # Check on `Preview` tab + click_link("Preview") + + expect(find(".js-md-preview")).to have_content(COMMENT_TEXT).and have_css("gl-emoji") + expect(page).not_to have_css(".js-note-text") + + # Check on `Write` tab + click_link("Write") + + expect(page).to have_field("note[note]", with: "#{COMMENT_TEXT} #{EMOJI}") + + # Submit comment from the `Preview` tab to get rid of a separate `it` block + # which would specially tests if everything gets cleared from the note form. + click_link("Preview") + click_button("Comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(COMMENT_TEXT).and have_css("gl-emoji") + end + + page.within(".js-main-target-form") do + expect(page).to have_field("note[note]", with: "").and have_no_css(".js-md-preview") + end + end + end + + context "when editing comment" do + before do + add_note(COMMENT_TEXT) + end + + it "edits comment" do + NEW_COMMENT_TEXT = "+1 Awesome!".freeze + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + note.find(".js-note-edit").click + end + + page.find(".current-note-edit-form textarea") + + page.within(".current-note-edit-form") do + fill_in("note[note]", with: NEW_COMMENT_TEXT) + click_button("Save comment") + end + + wait_for_requests + + page.within(".note") do + expect(page).to have_content(NEW_COMMENT_TEXT) + end + end + end + + context "when deleting comment" do + before do + add_note(COMMENT_TEXT) + end + + it "deletes comment" do + page.within(".note") do + expect(page).to have_content(COMMENT_TEXT) + end + + page.within(".main-notes-list") do + note = find(".note") + note.hover + + find(".more-actions").click + find(".more-actions .dropdown-menu li", match: :first) + + accept_confirm { find(".js-note-delete").click } + end + + expect(page).not_to have_css(".note") + end + end +end diff --git a/spec/javascripts/ide/components/ide_context_bar_spec.js b/spec/javascripts/ide/components/ide_context_bar_spec.js new file mode 100644 index 00000000000..e17b051f137 --- /dev/null +++ b/spec/javascripts/ide/components/ide_context_bar_spec.js @@ -0,0 +1,37 @@ +import Vue from 'vue'; +import store from '~/ide/stores'; +import ideContextBar from '~/ide/components/ide_context_bar.vue'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; + +describe('Multi-file editor right context bar', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(ideContextBar); + + vm = createComponentWithStore(Component, store, { + noChangesStateSvgPath: 'svg', + committedStateSvgPath: 'svg', + }); + + vm.$store.state.rightPanelCollapsed = false; + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('collapsed', () => { + beforeEach(done => { + vm.$store.state.rightPanelCollapsed = true; + + Vue.nextTick(done); + }); + + it('adds collapsed class', () => { + expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_external_links_spec.js b/spec/javascripts/ide/components/ide_external_links_spec.js new file mode 100644 index 00000000000..9f6cb459f3b --- /dev/null +++ b/spec/javascripts/ide/components/ide_external_links_spec.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; +import ideExternalLinks from '~/ide/components/ide_external_links.vue'; +import createComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('ide external links component', () => { + let vm; + let fakeReferrer; + let Component; + + const fakeProjectUrl = '/project/'; + + beforeEach(() => { + Component = Vue.extend(ideExternalLinks); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('goBackUrl', () => { + it('renders the Go Back link with the referrer when present', () => { + fakeReferrer = '/example/README.md'; + spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer); + + vm = createComponent(Component, { + projectUrl: fakeProjectUrl, + }).$mount(); + + expect(vm.goBackUrl).toEqual(fakeReferrer); + }); + + it('renders the Go Back link with the project url when referrer is not present', () => { + fakeReferrer = ''; + spyOnProperty(document, 'referrer').and.returnValue(fakeReferrer); + + vm = createComponent(Component, { + projectUrl: fakeProjectUrl, + }).$mount(); + + expect(vm.goBackUrl).toEqual(fakeProjectUrl); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_project_tree_spec.js b/spec/javascripts/ide/components/ide_project_tree_spec.js new file mode 100644 index 00000000000..657682cb39c --- /dev/null +++ b/spec/javascripts/ide/components/ide_project_tree_spec.js @@ -0,0 +1,39 @@ +import Vue from 'vue'; +import ProjectTree from '~/ide/components/ide_project_tree.vue'; +import createComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('IDE project tree', () => { + const Component = Vue.extend(ProjectTree); + let vm; + + beforeEach(() => { + vm = createComponent(Component, { + project: { + id: 1, + name: 'test', + web_url: gl.TEST_HOST, + avatar_url: '', + branches: [], + }, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders identicon when projct has no avatar', () => { + expect(vm.$el.querySelector('.identicon')).not.toBeNull(); + }); + + it('renders avatar image if project has avatar', done => { + vm.project.avatar_url = gl.TEST_HOST; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.identicon')).toBeNull(); + expect(vm.$el.querySelector('img.avatar')).not.toBeNull(); + + done(); + }); + }); +}); diff --git a/spec/javascripts/ide/components/ide_repo_tree_spec.js b/spec/javascripts/ide/components/ide_repo_tree_spec.js new file mode 100644 index 00000000000..e0fbc90ca61 --- /dev/null +++ b/spec/javascripts/ide/components/ide_repo_tree_spec.js @@ -0,0 +1,43 @@ +import Vue from 'vue'; +import ideRepoTree from '~/ide/components/ide_repo_tree.vue'; +import createComponent from '../../helpers/vue_mount_component_helper'; +import { file } from '../helpers'; + +describe('IdeRepoTree', () => { + let vm; + let tree; + + beforeEach(() => { + const IdeRepoTree = Vue.extend(ideRepoTree); + + tree = { + tree: [file()], + loading: false, + }; + + vm = createComponent(IdeRepoTree, { + tree, + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('renders a sidebar', () => { + expect(vm.$el.querySelector('.loading-file')).toBeNull(); + expect(vm.$el.querySelector('.file')).not.toBeNull(); + }); + + it('renders 3 loading files if tree is loading', done => { + tree.loading = true; + + vm.$nextTick(() => { + expect( + vm.$el.querySelectorAll('.multi-file-loading-container').length, + ).toEqual(3); + + done(); + }); + }); +}); diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js new file mode 100644 index 00000000000..e0ea3649646 --- /dev/null +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -0,0 +1,62 @@ +import Vue from 'vue'; +import asyncButtonComp from '~/pipelines/components/async_button.vue'; +import eventHub from '~/pipelines/event_hub'; + +describe('Pipelines Async Button', () => { + let component; + let AsyncButtonComponent; + + beforeEach(() => { + AsyncButtonComponent = Vue.extend(asyncButtonComp); + + component = new AsyncButtonComponent({ + propsData: { + endpoint: '/foo', + title: 'Foo', + icon: 'repeat', + cssClass: 'bar', + pipelineId: 123, + type: 'explode', + }, + }).$mount(); + }); + + it('should render a button', () => { + expect(component.$el.tagName).toEqual('BUTTON'); + }); + + it('should render svg icon', () => { + expect(component.$el.querySelector('svg')).not.toBeNull(); + }); + + it('should render the provided title', () => { + expect(component.$el.getAttribute('data-original-title')).toContain('Foo'); + expect(component.$el.getAttribute('aria-label')).toContain('Foo'); + }); + + it('should render the provided cssClass', () => { + expect(component.$el.getAttribute('class')).toContain('bar'); + }); + + describe('With confirm dialog', () => { + it('should call the service when confimation is positive', () => { + eventHub.$on('openConfirmationModal', (data) => { + expect(data.pipelineId).toEqual(123); + expect(data.type).toEqual('explode'); + }); + + component = new AsyncButtonComponent({ + propsData: { + endpoint: '/foo', + title: 'Foo', + icon: 'fa fa-foo', + cssClass: 'bar', + pipelineId: 123, + type: 'explode', + }, + }).$mount(); + + component.$el.click(); + }); + }); +}); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js new file mode 100644 index 00000000000..cee22d5342a --- /dev/null +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_maintainer_edit_spec.js @@ -0,0 +1,40 @@ +import Vue from 'vue'; +import maintainerEditComponent from '~/vue_merge_request_widget/components/mr_widget_maintainer_edit.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('RWidgetMaintainerEdit', () => { + let Component; + let vm; + + beforeEach(() => { + Component = Vue.extend(maintainerEditComponent); + }); + + afterEach(() => { + vm.$destroy(); + }); + + describe('when a maintainer is allowed to edit', () => { + beforeEach(() => { + vm = mountComponent(Component, { + maintainerEditAllowed: true, + }); + }); + + it('it renders the message', () => { + expect(vm.$el.textContent.trim()).toEqual('Allows edits from maintainers'); + }); + }); + + describe('when a maintainer is not allowed to edit', () => { + beforeEach(() => { + vm = mountComponent(Component, { + maintainerEditAllowed: false, + }); + }); + + it('hides the message', () => { + expect(vm.$el.textContent.trim()).toEqual(''); + }); + }); +}); diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb new file mode 100644 index 00000000000..9d806fc524d --- /dev/null +++ b/spec/lib/gitlab/email/email_shared_blocks.rb @@ -0,0 +1,41 @@ +require 'gitlab/email/receiver' + +shared_context :email_shared_context do + let(:mail_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } + let(:receiver) { Gitlab::Email::Receiver.new(email_raw) } + let(:markdown) { "" } + + def setup_attachment + allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( + [ + { + url: "uploads/image.png", + alt: "image", + markdown: markdown + } + ] + ) + end +end + +shared_examples :reply_processing_shared_examples do + context "when the user could not be found" do + before do + user.destroy + end + + it "raises a UserNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError) + end + end + + context "when the user is not authorized to the project" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + it "raises a ProjectNotFound" do + expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) + end + end +end |