Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
		
							parent
							
								
									cd54f7e81b
								
							
						
					
					
						commit
						58d68e313f
					
				
							
								
								
									
										2
									
								
								Gemfile
								
								
								
								
							
							
						
						
									
										2
									
								
								Gemfile
								
								
								
								
							|  | @ -413,7 +413,7 @@ group :test do | |||
| 
 | ||||
|   gem 'shoulda-matchers', '~> 4.0.1', require: false | ||||
|   gem 'email_spec', '~> 2.2.0' | ||||
|   gem 'webmock', '~> 3.5.1' | ||||
|   gem 'webmock', '~> 3.9.1' | ||||
|   gem 'rails-controller-testing' | ||||
|   gem 'concurrent-ruby', '~> 1.1' | ||||
|   gem 'test-prof', '~> 0.12.0' | ||||
|  |  | |||
|  | @ -547,7 +547,7 @@ GEM | |||
|       tilt | ||||
|     hana (1.3.6) | ||||
|     hangouts-chat (0.0.5) | ||||
|     hashdiff (0.3.8) | ||||
|     hashdiff (1.0.1) | ||||
|     hashie (3.6.0) | ||||
|     hashie-forbidden_attributes (0.1.1) | ||||
|       hashie (>= 3.0) | ||||
|  | @ -1214,10 +1214,10 @@ GEM | |||
|     webfinger (1.1.0) | ||||
|       activesupport | ||||
|       httpclient (>= 2.4) | ||||
|     webmock (3.5.1) | ||||
|     webmock (3.9.1) | ||||
|       addressable (>= 2.3.6) | ||||
|       crack (>= 0.3.2) | ||||
|       hashdiff | ||||
|       hashdiff (>= 0.4.0, < 2.0.0) | ||||
|     websocket-driver (0.7.1) | ||||
|       websocket-extensions (>= 0.1.0) | ||||
|     websocket-extensions (0.1.5) | ||||
|  | @ -1496,7 +1496,7 @@ DEPENDENCIES | |||
|   version_sorter (~> 2.2.4) | ||||
|   vmstat (~> 2.3.0) | ||||
|   webauthn (~> 2.3) | ||||
|   webmock (~> 3.5.1) | ||||
|   webmock (~> 3.9.1) | ||||
|   wikicloth (= 0.8.1) | ||||
|   yajl-ruby (~> 1.4.1) | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ import UsersSelect from './users_select'; | |||
| export default class IssuableContext { | ||||
|   constructor(currentUser) { | ||||
|     this.userSelect = new UsersSelect(currentUser); | ||||
|     this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search'); | ||||
| 
 | ||||
|     import(/* webpackChunkName: 'select2' */ 'select2/select2') | ||||
|       .then(() => { | ||||
|  |  | |||
|  | @ -40,6 +40,17 @@ export default class SidebarMediator { | |||
|     return this.service.update(field, data); | ||||
|   } | ||||
| 
 | ||||
|   saveReviewers(field) { | ||||
|     const selected = this.store.reviewers.map(u => u.id); | ||||
| 
 | ||||
|     // If there are no ids, that means we have to unassign (which is id = 0)
 | ||||
|     // And it only accepts an array, hence [0]
 | ||||
|     const reviewers = selected.length === 0 ? [0] : selected; | ||||
|     const data = { reviewer_ids: reviewers }; | ||||
| 
 | ||||
|     return this.service.update(field, data); | ||||
|   } | ||||
| 
 | ||||
|   setMoveToProjectId(projectId) { | ||||
|     this.store.setMoveToProjectId(projectId); | ||||
|   } | ||||
|  | @ -55,6 +66,7 @@ export default class SidebarMediator { | |||
| 
 | ||||
|   processFetchedData(data) { | ||||
|     this.store.setAssigneeData(data); | ||||
|     this.store.setReviewerData(data); | ||||
|     this.store.setTimeTrackingData(data); | ||||
|     this.store.setParticipantsData(data); | ||||
|     this.store.setSubscriptionsData(data); | ||||
|  |  | |||
|  | @ -18,8 +18,10 @@ export default class SidebarStore { | |||
|     this.humanTimeSpent = ''; | ||||
|     this.timeTrackingLimitToHours = timeTrackingLimitToHours; | ||||
|     this.assignees = []; | ||||
|     this.reviewers = []; | ||||
|     this.isFetching = { | ||||
|       assignees: true, | ||||
|       reviewers: true, | ||||
|       participants: true, | ||||
|       subscriptions: true, | ||||
|     }; | ||||
|  | @ -42,6 +44,13 @@ export default class SidebarStore { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setReviewerData(data) { | ||||
|     this.isFetching.reviewers = false; | ||||
|     if (data.reviewers) { | ||||
|       this.reviewers = data.reviewers; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   setTimeTrackingData(data) { | ||||
|     this.timeEstimate = data.time_estimate; | ||||
|     this.totalTimeSpent = data.total_time_spent; | ||||
|  | @ -75,20 +84,40 @@ export default class SidebarStore { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   addReviewer(reviewer) { | ||||
|     if (!this.findReviewer(reviewer)) { | ||||
|       this.reviewers.push(reviewer); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   findAssignee(findAssignee) { | ||||
|     return this.assignees.find(assignee => assignee.id === findAssignee.id); | ||||
|   } | ||||
| 
 | ||||
|   findReviewer(findReviewer) { | ||||
|     return this.reviewers.find(reviewer => reviewer.id === findReviewer.id); | ||||
|   } | ||||
| 
 | ||||
|   removeAssignee(removeAssignee) { | ||||
|     if (removeAssignee) { | ||||
|       this.assignees = this.assignees.filter(assignee => assignee.id !== removeAssignee.id); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   removeReviewer(removeReviewer) { | ||||
|     if (removeReviewer) { | ||||
|       this.reviewers = this.reviewers.filter(reviewer => reviewer.id !== removeReviewer.id); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   removeAllAssignees() { | ||||
|     this.assignees = []; | ||||
|   } | ||||
| 
 | ||||
|   removeAllReviewers() { | ||||
|     this.reviewers = []; | ||||
|   } | ||||
| 
 | ||||
|   setAssigneesFromRealtime(data) { | ||||
|     this.assignees = data; | ||||
|   } | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown'; | |||
| window.emitSidebarEvent = window.emitSidebarEvent || $.noop; | ||||
| 
 | ||||
| function UsersSelect(currentUser, els, options = {}) { | ||||
|   const elsClassName = els?.toString().match('.(.+$)')[1]; | ||||
|   const $els = $(els || '.js-user-search'); | ||||
|   this.users = this.users.bind(this); | ||||
|   this.user = this.user.bind(this); | ||||
|  | @ -127,11 +128,18 @@ function UsersSelect(currentUser, els, options = {}) { | |||
|             .find(`input[name='${$dropdown.data('fieldName')}'][value=${firstSelectedId}]`); | ||||
| 
 | ||||
|           firstSelected.remove(); | ||||
| 
 | ||||
|           if ($dropdown.hasClass(elsClassName)) { | ||||
|             emitSidebarEvent('sidebar.removeReviewer', { | ||||
|               id: firstSelectedId, | ||||
|             }); | ||||
|           } else { | ||||
|             emitSidebarEvent('sidebar.removeAssignee', { | ||||
|               id: firstSelectedId, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }; | ||||
| 
 | ||||
|     const getMultiSelectDropdownTitle = function(selectedUser, isSelected) { | ||||
|  | @ -392,8 +400,12 @@ function UsersSelect(currentUser, els, options = {}) { | |||
|       defaultLabel, | ||||
|       hidden() { | ||||
|         if ($dropdown.hasClass('js-multiselect')) { | ||||
|           if ($dropdown.hasClass(elsClassName)) { | ||||
|             emitSidebarEvent('sidebar.saveReviewers'); | ||||
|           } else { | ||||
|             emitSidebarEvent('sidebar.saveAssignees'); | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (!$dropdown.data('alwaysShowSelectbox')) { | ||||
|           $selectbox.hide(); | ||||
|  | @ -428,10 +440,18 @@ function UsersSelect(currentUser, els, options = {}) { | |||
|             previouslySelected.each((index, element) => { | ||||
|               element.remove(); | ||||
|             }); | ||||
|             if ($dropdown.hasClass(elsClassName)) { | ||||
|               emitSidebarEvent('sidebar.removeAllReviewers'); | ||||
|             } else { | ||||
|               emitSidebarEvent('sidebar.removeAllAssignees'); | ||||
|             } | ||||
|           } else if (isActive) { | ||||
|             // user selected
 | ||||
|             if ($dropdown.hasClass(elsClassName)) { | ||||
|               emitSidebarEvent('sidebar.addReviewer', user); | ||||
|             } else { | ||||
|               emitSidebarEvent('sidebar.addAssignee', user); | ||||
|             } | ||||
| 
 | ||||
|             // Remove unassigned selection (if it was previously selected)
 | ||||
|             const unassignedSelected = $dropdown | ||||
|  | @ -448,8 +468,12 @@ function UsersSelect(currentUser, els, options = {}) { | |||
|             } | ||||
| 
 | ||||
|             // User unselected
 | ||||
|             if ($dropdown.hasClass(elsClassName)) { | ||||
|               emitSidebarEvent('sidebar.removeReviewer', user); | ||||
|             } else { | ||||
|               emitSidebarEvent('sidebar.removeAssignee', user); | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           if (getSelected().find(u => u === gon.current_user_id)) { | ||||
|             $assignToMeLink.hide(); | ||||
|  |  | |||
|  | @ -139,7 +139,7 @@ export default { | |||
| <template> | ||||
|   <div class="branch-commit cgray"> | ||||
|     <template v-if="shouldShowRefInfo"> | ||||
|       <div class="icon-container"> | ||||
|       <div class="icon-container gl-display-inline-block"> | ||||
|         <gl-icon v-if="tag" name="tag" /> | ||||
|         <gl-icon v-else-if="mergeRequestRef" name="git-merge" /> | ||||
|         <gl-icon v-else name="branch" /> | ||||
|  |  | |||
|  | @ -0,0 +1,27 @@ | |||
| <script> | ||||
| import { GlTooltipDirective } from '@gitlab/ui'; | ||||
| 
 | ||||
| export default { | ||||
|   name: 'MemberSource', | ||||
|   directives: { | ||||
|     GlTooltip: GlTooltipDirective, | ||||
|   }, | ||||
|   props: { | ||||
|     memberSource: { | ||||
|       type: Object, | ||||
|       required: true, | ||||
|     }, | ||||
|     isDirectMember: { | ||||
|       type: Boolean, | ||||
|       required: true, | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <span v-if="isDirectMember">{{ __('Direct member') }}</span> | ||||
|   <a v-else v-gl-tooltip.hover :title="__('Inherited')" :href="memberSource.webUrl">{{ | ||||
|     memberSource.name | ||||
|   }}</a> | ||||
| </template> | ||||
|  | @ -4,6 +4,7 @@ import { GlTable } from '@gitlab/ui'; | |||
| import { FIELDS } from '../constants'; | ||||
| import initUserPopovers from '~/user_popovers'; | ||||
| import MemberAvatar from './member_avatar.vue'; | ||||
| import MemberSource from './member_source.vue'; | ||||
| import MembersTableCell from './members_table_cell.vue'; | ||||
| 
 | ||||
| export default { | ||||
|  | @ -12,6 +13,7 @@ export default { | |||
|     GlTable, | ||||
|     MemberAvatar, | ||||
|     MembersTableCell, | ||||
|     MemberSource, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(['members', 'tableFields']), | ||||
|  | @ -43,8 +45,10 @@ export default { | |||
|       </members-table-cell> | ||||
|     </template> | ||||
| 
 | ||||
|     <template #cell(source)> | ||||
|       <!-- Temporarily empty --> | ||||
|     <template #cell(source)="{ item: member }"> | ||||
|       <members-table-cell #default="{ isDirectMember }" :member="member"> | ||||
|         <member-source :is-direct-member="isDirectMember" :member-source="member.source" /> | ||||
|       </members-table-cell> | ||||
|     </template> | ||||
| 
 | ||||
|     <template #head(actions)="{ label }"> | ||||
|  |  | |||
|  | @ -1,4 +1,5 @@ | |||
| <script> | ||||
| import { mapState } from 'vuex'; | ||||
| import { MEMBER_TYPES } from '../constants'; | ||||
| 
 | ||||
| export default { | ||||
|  | @ -10,6 +11,7 @@ export default { | |||
|     }, | ||||
|   }, | ||||
|   computed: { | ||||
|     ...mapState(['sourceId']), | ||||
|     isGroup() { | ||||
|       return Boolean(this.member.sharedWithGroup); | ||||
|     }, | ||||
|  | @ -30,10 +32,14 @@ export default { | |||
| 
 | ||||
|       return MEMBER_TYPES.user; | ||||
|     }, | ||||
|     isDirectMember() { | ||||
|       return this.member.source?.id === this.sourceId; | ||||
|     }, | ||||
|   }, | ||||
|   render() { | ||||
|     return this.$scopedSlots.default({ | ||||
|       memberType: this.memberType, | ||||
|       isDirectMember: this.isDirectMember, | ||||
|     }); | ||||
|   }, | ||||
| }; | ||||
|  |  | |||
|  | @ -111,6 +111,10 @@ | |||
|         white-space: nowrap; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .pipeline-tags .label-container { | ||||
|       white-space: normal; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  | @ -124,22 +128,6 @@ | |||
| } | ||||
| 
 | ||||
| .ci-table { | ||||
|   .build.retried { | ||||
|     background-color: $gray-lightest; | ||||
|   } | ||||
| 
 | ||||
|   .commit-link { | ||||
|     a { | ||||
|       &:focus { | ||||
|         text-decoration: none; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     a:hover { | ||||
|       text-decoration: none; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .avatar { | ||||
|     margin-left: 0; | ||||
|     float: none; | ||||
|  | @ -191,45 +179,12 @@ | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .icon-container { | ||||
|     display: inline-block; | ||||
| 
 | ||||
|     &.commit-icon { | ||||
|       width: 15px; | ||||
|       text-align: center; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
|    * Play button with icon in dropdowns | ||||
|    */ | ||||
|   .no-btn { | ||||
|     border: 0; | ||||
|     background: none; | ||||
|     outline: none; | ||||
|     width: 100%; | ||||
|     text-align: left; | ||||
| 
 | ||||
|     .icon-play { | ||||
|       position: relative; | ||||
|       top: 2px; | ||||
|       margin-right: 5px; | ||||
|       height: 13px; | ||||
|       width: 12px; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .duration, | ||||
|   .finished-at { | ||||
|     color: $gl-text-color-secondary; | ||||
|     margin: 0; | ||||
|     white-space: nowrap; | ||||
| 
 | ||||
|     .fa { | ||||
|       font-size: 12px; | ||||
|       margin-right: 4px; | ||||
|     } | ||||
| 
 | ||||
|     svg { | ||||
|       width: 12px; | ||||
|       height: 12px; | ||||
|  | @ -241,14 +196,6 @@ | |||
|   .build-link a { | ||||
|     color: $gl-text-color; | ||||
|   } | ||||
| 
 | ||||
|   .btn-group.open .dropdown-toggle { | ||||
|     box-shadow: none; | ||||
|   } | ||||
| 
 | ||||
|   .pipeline-tags .label-container { | ||||
|     white-space: normal; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| .stage-cell { | ||||
|  |  | |||
|  | @ -386,6 +386,12 @@ module IssuablesHelper | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def reviewer_sidebar_data(reviewer, merge_request: nil) | ||||
|     { avatar_url: reviewer.avatar_url, name: reviewer.name, username: reviewer.username }.tap do |data| | ||||
|       data[:can_merge] = merge_request.can_be_merged_by?(reviewer) if merge_request | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   def issuable_squash_option?(issuable, project) | ||||
|     if issuable.persisted? | ||||
|       issuable.squash | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ | |||
|           %span.build-link ##{artifact.job_id} | ||||
| 
 | ||||
|         - if artifact.job.ref | ||||
|           .icon-container{ "aria-label" => artifact.job.tag? ? _('Tag') : _('Branch') } | ||||
|           .icon-container.gl-display-inline-block{ "aria-label" => artifact.job.tag? ? _('Tag') : _('Branch') } | ||||
|             = artifact.job.tag? ? sprite_icon('tag', css_class: 'sprite') : sprite_icon('branch', css_class: 'sprite') | ||||
|           = link_to artifact.job.ref, project_ref_path(@project, artifact.job.ref), class: 'ref-name' | ||||
|         - else | ||||
|  |  | |||
|  | @ -3,14 +3,14 @@ | |||
|     .count-badge.d-inline-flex.align-item-stretch.gl-mr-3 | ||||
|       - if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2 | ||||
|         = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'btn btn-default has-tooltip count-badge-button d-flex align-items-center fork-btn' do | ||||
|           = sprite_icon('fork', { css_class: 'icon' }) | ||||
|           = sprite_icon('fork', css_class: 'icon') | ||||
|           %span= s_('ProjectOverview|Fork') | ||||
|       - else | ||||
|         - can_create_fork = current_user.can?(:create_fork) | ||||
|         = link_to new_project_fork_path(@project), | ||||
|             class: "btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center fork-btn #{'has-tooltip disabled' unless can_create_fork}", | ||||
|             title: (s_('ProjectOverview|You have reached your project limit') unless can_create_fork) do | ||||
|           = sprite_icon('fork', { css_class: 'icon' }) | ||||
|           = sprite_icon('fork', css_class: 'icon') | ||||
|           %span= s_('ProjectOverview|Fork') | ||||
|       %span.fork-count.count-badge-count.d-flex.align-items-center | ||||
|         = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Fork'), s_('ProjectOverview|Forks'), @project.forks_count), class: 'count' do | ||||
|  |  | |||
|  | @ -2,10 +2,10 @@ | |||
|   .count-badge.d-inline-flex.align-item-stretch.gl-mr-3 | ||||
|     %button.count-badge-button.btn.btn-default.btn-xs.d-flex.align-items-center.star-btn.toggle-star{ type: "button", data: { endpoint: toggle_star_project_path(@project, :json) } } | ||||
|       - if current_user.starred?(@project) | ||||
|         = sprite_icon('star', { css_class: 'icon' }) | ||||
|         = sprite_icon('star', css_class: 'icon') | ||||
|         %span.starred= s_('ProjectOverview|Unstar') | ||||
|       - else | ||||
|         = sprite_icon('star-o', { css_class: 'icon' }) | ||||
|         = sprite_icon('star-o', css_class: 'icon') | ||||
|         %span= s_('ProjectOverview|Star') | ||||
|     %span.star-count.count-badge-count.d-flex.align-items-center | ||||
|       = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'count' do | ||||
|  | @ -14,7 +14,7 @@ | |||
| - else | ||||
|   .count-badge.d-inline-flex.align-item-stretch.gl-mr-3 | ||||
|     = link_to new_user_session_path, class: 'btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do | ||||
|       = sprite_icon('star-o', { css_class: 'icon' }) | ||||
|       = sprite_icon('star-o', css_class: 'icon') | ||||
|       %span= s_('ProjectOverview|Star') | ||||
|     %span.star-count.count-badge-count.d-flex.align-items-center | ||||
|       = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'count' do | ||||
|  |  | |||
|  | @ -21,7 +21,7 @@ | |||
| 
 | ||||
|     - if ref | ||||
|       - if job.ref | ||||
|         .icon-container | ||||
|         .icon-container.gl-display-inline-block | ||||
|           = job.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite') | ||||
|         = link_to job.ref, project_ref_path(job.project, job.ref), class: "ref-name" | ||||
|       - else | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| .table-mobile-content | ||||
|   .branch-commit.cgray | ||||
|     - if deployment.ref | ||||
|       %span.icon-container | ||||
|       %span.icon-container.gl-display-inline-block | ||||
|         = deployment.tag? ? icon('tag') : sprite_icon('fork', css_class: 'sprite') | ||||
|       = link_to deployment.ref, project_ref_path(@project, deployment.ref), class: "ref-name" | ||||
|     .icon-container.commit-icon | ||||
|  |  | |||
|  | @ -92,7 +92,7 @@ | |||
|       .loading.hide | ||||
|         .spinner.spinner-md | ||||
| 
 | ||||
| = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, source_branch: @merge_request.source_branch | ||||
| = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch | ||||
| 
 | ||||
| - if @merge_request.can_be_reverted?(current_user) | ||||
|   = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title | ||||
|  |  | |||
|  | @ -0,0 +1,56 @@ | |||
| - issuable_type = issuable_sidebar[:type] | ||||
| - signed_in = !!issuable_sidebar.dig(:current_user, :id) | ||||
| 
 | ||||
| #js-vue-sidebar-reviewers{ data: { field: issuable_type, signed_in: signed_in } } | ||||
|   .title.hide-collapsed | ||||
|     = _('Reviewer') | ||||
|     = loading_icon(css_class: 'gl-vertical-align-text-bottom') | ||||
| 
 | ||||
| .selectbox.hide-collapsed | ||||
|   - if reviewers.none? | ||||
|     = hidden_field_tag "#{issuable_type}[reviewer_ids][]", 0, id: nil | ||||
|   - else | ||||
|     - reviewers.each do |reviewer| | ||||
|       = hidden_field_tag "#{issuable_type}[reviewer_ids][]", reviewer.id, id: nil, data: reviewer_sidebar_data(reviewer, merge_request: @merge_request) | ||||
| 
 | ||||
|   - options = { toggle_class: 'js-reviewer-search js-author-search', | ||||
|     title: _('Request review from'), | ||||
|     filter: true, | ||||
|     dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author', | ||||
|     placeholder: _('Search users'), | ||||
|     data: { first_user: issuable_sidebar.dig(:current_user, :username), | ||||
|       current_user: true, | ||||
|       iid: issuable_sidebar[:iid], | ||||
|       issuable_type: issuable_type, | ||||
|       project_id: issuable_sidebar[:project_id], | ||||
|       author_id: issuable_sidebar[:author_id], | ||||
|       field_name: "#{issuable_type}[reviewer_ids][]", | ||||
|       issue_update: issuable_sidebar[:issuable_json_path], | ||||
|       ability_name: issuable_type, | ||||
|       null_user: true, | ||||
|       display: 'static' } } | ||||
| 
 | ||||
|   - dropdown_options = reviewers_dropdown_options(issuable_type) | ||||
|   - title = dropdown_options[:title] | ||||
|   - options[:toggle_class] += ' js-multiselect js-save-user-data' | ||||
|   - data = { field_name: "#{issuable_type}[reviewer_ids][]" } | ||||
|   - data[:multi_select] = true | ||||
|   - data['dropdown-title'] = title | ||||
|   - data['dropdown-header'] = dropdown_options[:data][:'dropdown-header'] | ||||
|   - data['max-select'] = dropdown_options[:data][:'max-select'] if dropdown_options[:data][:'max-select'] | ||||
|   - options[:data].merge!(data) | ||||
| 
 | ||||
|   - if experiment_enabled?(:invite_members_version_a) && can_import_members? | ||||
|     - options[:dropdown_class] += ' dropdown-extended-height' | ||||
|     - options[:footer_content] = true | ||||
|     - options[:wrapper_class] = 'js-sidebar-reviewer-dropdown' | ||||
| 
 | ||||
|     = dropdown_tag(title, options: options) do | ||||
|       %ul.dropdown-footer-list | ||||
|         %li | ||||
|           = link_to _('Invite Members'), | ||||
|             project_project_members_path(@project), | ||||
|             title: _('Invite Members'), | ||||
|             data: { 'is-link': true, 'track-event': 'click_invite_members', 'track-label': 'edit_reviewer' } | ||||
|   - else | ||||
|     = dropdown_tag(title, options: options) | ||||
|  | @ -1,7 +1,7 @@ | |||
| --- | ||||
| name: broadcast_issue_updates | ||||
| introduced_by_url:  | ||||
| rollout_issue_url:  | ||||
| group:  | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30732 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1210 | ||||
| group: group::project management | ||||
| type: development | ||||
| default_enabled: false | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| --- | ||||
| name: dashboard_pipeline_status | ||||
| introduced_by_url:  | ||||
| rollout_issue_url:  | ||||
| group:  | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22029 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/209061 | ||||
| group: group::continuous integration | ||||
| type: development | ||||
| default_enabled: true | ||||
|  |  | |||
|  | @ -1,7 +1,7 @@ | |||
| --- | ||||
| name: real_time_issue_sidebar | ||||
| introduced_by_url:  | ||||
| rollout_issue_url:  | ||||
| group:  | ||||
| introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/30239 | ||||
| rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/delivery/-/issues/1210 | ||||
| group: group::project management | ||||
| type: development | ||||
| default_enabled: false | ||||
|  |  | |||
|  | @ -647,7 +647,7 @@ tables: `namespaces`. This can be translated to: | |||
| ```sql | ||||
| ALTER TABLE namespaces | ||||
| ALTER COLUMN request_access_enabled | ||||
| DEFAULT false | ||||
| SET DEFAULT false | ||||
| ``` | ||||
| 
 | ||||
| In this particular case, the default value exists and we're just changing the metadata for | ||||
|  |  | |||
|  | @ -674,11 +674,6 @@ To delete an existing site profile: | |||
| ## Scanner profile | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/222767) in GitLab 13.4. | ||||
| > - [Deployed behind a feature flag](../../feature_flags.md), enabled by default. | ||||
| > - Enabled on GitLab.com. | ||||
| > - Can be enabled or disabled per-project. | ||||
| > - Recommended for production use. | ||||
| > - For GitLab self-managed instances, GitLab administrators can [disable this feature](#enable-or-disable-dast-scanner-profiles). | ||||
| 
 | ||||
| A scanner profile defines the scanner settings used to run an on-demand scan: | ||||
| 
 | ||||
|  | @ -713,29 +708,6 @@ To delete a scanner profile: | |||
| 1. Click **Manage** in the **DAST Profiles** row. | ||||
| 1. Click **{remove}** in the scanner profile's row. | ||||
| 
 | ||||
| ### Enable or disable DAST scanner profiles | ||||
| 
 | ||||
| The scanner profile feature is ready for production use. It's deployed behind a feature flag that | ||||
| is **enabled by default**. [GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md) can opt to disable it. | ||||
| 
 | ||||
| To disable it: | ||||
| 
 | ||||
| ```ruby | ||||
| # Instance-wide | ||||
| Feature.disable(:security_on_demand_scans_scanner_profiles) | ||||
| # or by project | ||||
| Feature.disable(:security_on_demand_scans_scanner_profiles, Project.find(<project id>)) | ||||
| ``` | ||||
| 
 | ||||
| To enable it: | ||||
| 
 | ||||
| ```ruby | ||||
| # Instance-wide | ||||
| Feature.enable(:security_on_demand_scans_scanner_profiles) | ||||
| # or by project | ||||
| Feature.enable(:security_on_demand_scans_scanner_profiles, Project.find(<project ID>)) | ||||
| ``` | ||||
| 
 | ||||
| ## On-demand scans | ||||
| 
 | ||||
| > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218465) in GitLab 13.2. | ||||
|  |  | |||
|  | @ -8934,6 +8934,9 @@ msgstr "" | |||
| msgid "Diffs|Something went wrong while fetching diff lines." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Direct member" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Direction" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -13614,6 +13617,9 @@ msgstr "" | |||
| msgid "Information about additional Pages templates and how to install them can be found in our %{pages_getting_started_guide}." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Inherited" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Inherited:" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -17673,12 +17679,6 @@ msgstr "" | |||
| msgid "On track" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "OnDemandScans|Attached branch" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "OnDemandScans|Attached branch is where the scan job runs." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "OnDemandScans|Could not fetch scanner profiles. Please refresh the page, or try again later." | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -17715,18 +17715,9 @@ msgstr "" | |||
| msgid "OnDemandScans|On-demand scans run outside the DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Learn more%{learnMoreLinkEnd}" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "OnDemandScans|Only a passive scan can be performed on demand." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "OnDemandScans|Passive" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "OnDemandScans|Run scan" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "OnDemandScans|Scan mode" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "OnDemandScans|Scanner profile" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -21668,6 +21659,9 @@ msgstr "" | |||
| msgid "Request parameter %{param} is missing." | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Request review from" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Request to link SAML account must be authorized" | ||||
| msgstr "" | ||||
| 
 | ||||
|  | @ -21938,6 +21932,9 @@ msgstr "" | |||
| msgid "ReviewApp|Enable Review App" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Reviewer" | ||||
| msgstr "" | ||||
| 
 | ||||
| msgid "Reviewing" | ||||
| msgstr "" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,168 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe 'Projects > Wiki > User previews markdown changes', :js do | ||||
|   let_it_be(:user) { create(:user) } | ||||
|   let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } | ||||
|   let(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'home', content: '[some link](other-page)') } | ||||
|   let(:wiki_content) do | ||||
|     <<-HEREDOC | ||||
| Some text so key event for [ does not trigger an incorrect replacement. | ||||
| [regular link](regular) | ||||
| [relative link 1](../relative) | ||||
| [relative link 2](./relative) | ||||
| [relative link 3](./e/f/relative) | ||||
| [spaced link](title with spaces) | ||||
|     HEREDOC | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     project.add_maintainer(user) | ||||
| 
 | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   context "while creating a new wiki page" do | ||||
|     context "when there are no spaces or hyphens in the page name" do | ||||
|       it "rewrites relative links as expected" do | ||||
|         create_wiki_page('a/b/c/d', content: wiki_content) | ||||
| 
 | ||||
|         expect(page).to have_content("regular link") | ||||
| 
 | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/relative\">relative link 1</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/c/relative\">relative link 2</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/c/e/f/relative\">relative link 3</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "when there are spaces in the page name" do | ||||
|       it "rewrites relative links as expected" do | ||||
|         create_wiki_page('a page/b page/c page/d page', content: wiki_content) | ||||
| 
 | ||||
|         expect(page).to have_content("regular link") | ||||
| 
 | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/relative\">relative link 1</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "when there are hyphens in the page name" do | ||||
|       it "rewrites relative links as expected" do | ||||
|         create_wiki_page('a-page/b-page/c-page/d-page', content: wiki_content) | ||||
| 
 | ||||
|         expect(page).to have_content("regular link") | ||||
| 
 | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/relative\">relative link 1</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>") | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context "while editing a wiki page" do | ||||
|     context "when there are no spaces or hyphens in the page name" do | ||||
|       it "rewrites relative links as expected" do | ||||
|         create_wiki_page('a/b/c/d') | ||||
|         click_link 'Edit' | ||||
| 
 | ||||
|         fill_in :wiki_content, with: wiki_content | ||||
|         click_on "Preview" | ||||
| 
 | ||||
|         expect(page).to have_content("regular link") | ||||
| 
 | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/relative\">relative link 1</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/c/relative\">relative link 2</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a/b/c/e/f/relative\">relative link 3</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "when there are spaces in the page name" do | ||||
|       it "rewrites relative links as expected" do | ||||
|         create_wiki_page('a page/b page/c page/d page') | ||||
|         click_link 'Edit' | ||||
| 
 | ||||
|         fill_in :wiki_content, with: wiki_content | ||||
|         click_on "Preview" | ||||
| 
 | ||||
|         expect(page).to have_content("regular link") | ||||
| 
 | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/relative\">relative link 1</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "when there are hyphens in the page name" do | ||||
|       it "rewrites relative links as expected" do | ||||
|         create_wiki_page('a-page/b-page/c-page/d-page') | ||||
|         click_link 'Edit' | ||||
| 
 | ||||
|         fill_in :wiki_content, with: wiki_content | ||||
|         click_on "Preview" | ||||
| 
 | ||||
|         expect(page).to have_content("regular link") | ||||
| 
 | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/regular\">regular link</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/relative\">relative link 1</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") | ||||
|         expect(page.html).to include("<a href=\"/#{project.full_path}/-/wikis/title%20with%20spaces\">spaced link</a>") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when rendering the preview' do | ||||
|       it 'renders content with CommonMark' do | ||||
|         create_wiki_page('a-page/b-page/c-page/common-mark') | ||||
|         click_link 'Edit' | ||||
| 
 | ||||
|         fill_in :wiki_content, with: "1. one\n  - sublist\n" | ||||
|         click_on "Preview" | ||||
| 
 | ||||
|         # the above generates two separate lists (not embedded) in CommonMark | ||||
|         expect(page).to have_content("sublist") | ||||
|         expect(page).not_to have_xpath("//ol//li//ul") | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   it "does not linkify double brackets inside code blocks as expected" do | ||||
|     wiki_content = <<-HEREDOC | ||||
|       `[[do_not_linkify]]` | ||||
|       ``` | ||||
|       [[also_do_not_linkify]] | ||||
|       ``` | ||||
|     HEREDOC | ||||
| 
 | ||||
|     create_wiki_page('linkify_test', wiki_content) | ||||
| 
 | ||||
|     expect(page).to have_content("do_not_linkify") | ||||
| 
 | ||||
|     expect(page.html).to include('[[do_not_linkify]]') | ||||
|     expect(page.html).to include('[[also_do_not_linkify]]') | ||||
|   end | ||||
| 
 | ||||
|   private | ||||
| 
 | ||||
|   def create_wiki_page(path, content = 'content') | ||||
|     visit project_wiki_path(project, wiki_page) | ||||
| 
 | ||||
|     click_link 'New page' | ||||
| 
 | ||||
|     fill_in :wiki_title, with: path | ||||
|     fill_in :wiki_content, with: content | ||||
| 
 | ||||
|     click_button 'Create page' | ||||
|   end | ||||
| end | ||||
|  | @ -1,20 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe 'Wiki shortcuts', :js do | ||||
|   let(:user) { create(:user) } | ||||
|   let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } | ||||
|   let(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'home', content: 'Home page') } | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|     visit project_wiki_path(project, wiki_page) | ||||
|   end | ||||
| 
 | ||||
|   it 'Visit edit wiki page using "e" keyboard shortcut' do | ||||
|     find('body').native.send_key('e') | ||||
| 
 | ||||
|     expect(find('.wiki-page-title')).to have_content('Edit Page') | ||||
|   end | ||||
| end | ||||
|  | @ -1,360 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require "spec_helper" | ||||
| 
 | ||||
| RSpec.describe "User creates wiki page" do | ||||
|   include WikiHelpers | ||||
| 
 | ||||
|   let(:user) { create(:user) } | ||||
|   let(:wiki) { ProjectWiki.new(project, user) } | ||||
|   let(:project) { create(:project) } | ||||
| 
 | ||||
|   before do | ||||
|     project.add_maintainer(user) | ||||
| 
 | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   context "when wiki is empty" do | ||||
|     before do |example| | ||||
|       visit(project_wikis_path(project)) | ||||
| 
 | ||||
|       wait_for_svg_to_be_loaded(example) | ||||
| 
 | ||||
|       click_link "Create your first page" | ||||
|     end | ||||
| 
 | ||||
|     context "in a user namespace" do | ||||
|       let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } | ||||
| 
 | ||||
|       it "shows validation error message" do | ||||
|         page.within(".wiki-form") do | ||||
|           fill_in(:wiki_content, with: "") | ||||
| 
 | ||||
|           click_on("Create page") | ||||
|         end | ||||
| 
 | ||||
|         expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") | ||||
| 
 | ||||
|         page.within(".wiki-form") do | ||||
|           fill_in(:wiki_content, with: "[link test](test)") | ||||
| 
 | ||||
|           click_on("Create page") | ||||
|         end | ||||
| 
 | ||||
|         expect(page).to have_content("Home").and have_content("link test") | ||||
| 
 | ||||
|         click_link("link test") | ||||
| 
 | ||||
|         expect(page).to have_content("Create New Page") | ||||
|       end | ||||
| 
 | ||||
|       it "shows non-escaped link in the pages list" do | ||||
|         fill_in(:wiki_title, with: "one/two/three-test") | ||||
| 
 | ||||
|         page.within(".wiki-form") do | ||||
|           fill_in(:wiki_content, with: "wiki content") | ||||
| 
 | ||||
|           click_on("Create page") | ||||
|         end | ||||
| 
 | ||||
|         expect(current_path).to include("one/two/three-test") | ||||
|         expect(page).to have_xpath("//a[@href='/#{project.full_path}/-/wikis/one/two/three-test']") | ||||
|       end | ||||
| 
 | ||||
|       it "has `Create home` as a commit message", :js do | ||||
|         wait_for_requests | ||||
| 
 | ||||
|         expect(page).to have_field("wiki[message]", with: "Create home") | ||||
|       end | ||||
| 
 | ||||
|       it "creates a page from the home page" do | ||||
|         fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n") | ||||
|         fill_in(:wiki_message, with: "Adding links to wiki") | ||||
| 
 | ||||
|         page.within(".wiki-form") do | ||||
|           click_button("Create page") | ||||
|         end | ||||
| 
 | ||||
|         expect(current_path).to eq(project_wiki_path(project, "home")) | ||||
|         expect(page).to have_content("test GitLab API doc Rake tasks Wiki header") | ||||
|                    .and have_content("Home") | ||||
|                    .and have_content("Last edited by #{user.name}") | ||||
|                    .and have_header_with_correct_id_and_link(1, "Wiki header", "wiki-header") | ||||
| 
 | ||||
|         click_link("test") | ||||
| 
 | ||||
|         expect(current_path).to eq(project_wiki_path(project, "test")) | ||||
| 
 | ||||
|         page.within(:css, ".nav-text") do | ||||
|           expect(page).to have_content("Create New Page") | ||||
|         end | ||||
| 
 | ||||
|         click_link("Home") | ||||
| 
 | ||||
|         expect(current_path).to eq(project_wiki_path(project, "home")) | ||||
| 
 | ||||
|         click_link("GitLab API") | ||||
| 
 | ||||
|         expect(current_path).to eq(project_wiki_path(project, "api")) | ||||
| 
 | ||||
|         page.within(:css, ".nav-text") do | ||||
|           expect(page).to have_content("Create") | ||||
|         end | ||||
| 
 | ||||
|         click_link("Home") | ||||
| 
 | ||||
|         expect(current_path).to eq(project_wiki_path(project, "home")) | ||||
| 
 | ||||
|         click_link("Rake tasks") | ||||
| 
 | ||||
|         expect(current_path).to eq(project_wiki_path(project, "raketasks")) | ||||
| 
 | ||||
|         page.within(:css, ".nav-text") do | ||||
|           expect(page).to have_content("Create") | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it "creates ASCII wiki with LaTeX blocks", :js do | ||||
|         stub_application_setting(plantuml_url: "http://localhost", plantuml_enabled: true) | ||||
| 
 | ||||
|         ascii_content = <<~MD | ||||
|           :stem: latexmath | ||||
| 
 | ||||
|           [stem] | ||||
|           ++++ | ||||
|           \\sqrt{4} = 2 | ||||
|           ++++ | ||||
| 
 | ||||
|           another part | ||||
| 
 | ||||
|           [latexmath] | ||||
|           ++++ | ||||
|           \\beta_x \\gamma | ||||
|           ++++ | ||||
| 
 | ||||
|           stem:[2+2] is 4 | ||||
|         MD | ||||
| 
 | ||||
|         find("#wiki_format option[value=asciidoc]").select_option | ||||
| 
 | ||||
|         fill_in(:wiki_content, with: ascii_content) | ||||
| 
 | ||||
|         page.within(".wiki-form") do | ||||
|           click_button("Create page") | ||||
|         end | ||||
| 
 | ||||
|         page.within ".md" do | ||||
|           expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4") | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it 'creates a wiki page with Org markup', :aggregate_failures do | ||||
|         org_content = <<~ORG | ||||
|           * Heading | ||||
|           ** Subheading | ||||
|           [[home][Link to Home]] | ||||
|         ORG | ||||
| 
 | ||||
|         page.within('.wiki-form') do | ||||
|           find('#wiki_format option[value=org]').select_option | ||||
|           fill_in(:wiki_content, with: org_content) | ||||
|           click_button('Create page') | ||||
|         end | ||||
| 
 | ||||
|         expect(page).to have_selector('h1', text: 'Heading') | ||||
|         expect(page).to have_selector('h2', text: 'Subheading') | ||||
|         expect(page).to have_link('Link to Home', href: "/#{project.full_path}/-/wikis/home") | ||||
|       end | ||||
| 
 | ||||
|       it_behaves_like 'wiki file attachments' | ||||
|     end | ||||
| 
 | ||||
|     context "in a group namespace", :js do | ||||
|       let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } | ||||
| 
 | ||||
|       it "has `Create home` as a commit message" do | ||||
|         wait_for_requests | ||||
| 
 | ||||
|         expect(page).to have_field("wiki[message]", with: "Create home") | ||||
|       end | ||||
| 
 | ||||
|       it "creates a page from the home page" do | ||||
|         page.within(".wiki-form") do | ||||
|           fill_in(:wiki_content, with: "My awesome wiki!") | ||||
| 
 | ||||
|           click_button("Create page") | ||||
|         end | ||||
| 
 | ||||
|         expect(page).to have_content("Home") | ||||
|                    .and have_content("Last edited by #{user.name}") | ||||
|                    .and have_content("My awesome wiki!") | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context "when wiki is not empty", :js do | ||||
|     before do | ||||
|       create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') | ||||
| 
 | ||||
|       visit(project_wikis_path(project)) | ||||
|     end | ||||
| 
 | ||||
|     context "in a user namespace" do | ||||
|       let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } | ||||
| 
 | ||||
|       context "via the `new wiki page` page" do | ||||
|         it "creates a page with a single word" do | ||||
|           click_link("New page") | ||||
| 
 | ||||
|           page.within(".wiki-form") do | ||||
|             fill_in(:wiki_title, with: "foo") | ||||
|             fill_in(:wiki_content, with: "My awesome wiki!") | ||||
|           end | ||||
| 
 | ||||
|           # Commit message field should have correct value. | ||||
|           expect(page).to have_field("wiki[message]", with: "Create foo") | ||||
| 
 | ||||
|           click_button("Create page") | ||||
| 
 | ||||
|           expect(page).to have_content("foo") | ||||
|                      .and have_content("Last edited by #{user.name}") | ||||
|                      .and have_content("My awesome wiki!") | ||||
|         end | ||||
| 
 | ||||
|         it "creates a page with spaces in the name" do | ||||
|           click_link("New page") | ||||
| 
 | ||||
|           page.within(".wiki-form") do | ||||
|             fill_in(:wiki_title, with: "Spaces in the name") | ||||
|             fill_in(:wiki_content, with: "My awesome wiki!") | ||||
|           end | ||||
| 
 | ||||
|           # Commit message field should have correct value. | ||||
|           expect(page).to have_field("wiki[message]", with: "Create Spaces in the name") | ||||
| 
 | ||||
|           click_button("Create page") | ||||
| 
 | ||||
|           expect(page).to have_content("Spaces in the name") | ||||
|                      .and have_content("Last edited by #{user.name}") | ||||
|                      .and have_content("My awesome wiki!") | ||||
|         end | ||||
| 
 | ||||
|         it "creates a page with hyphens in the name" do | ||||
|           click_link("New page") | ||||
| 
 | ||||
|           page.within(".wiki-form") do | ||||
|             fill_in(:wiki_title, with: "hyphens-in-the-name") | ||||
|             fill_in(:wiki_content, with: "My awesome wiki!") | ||||
|           end | ||||
| 
 | ||||
|           # Commit message field should have correct value. | ||||
|           expect(page).to have_field("wiki[message]", with: "Create hyphens in the name") | ||||
| 
 | ||||
|           page.within(".wiki-form") do | ||||
|             fill_in(:wiki_content, with: "My awesome wiki!") | ||||
| 
 | ||||
|             click_button("Create page") | ||||
|           end | ||||
| 
 | ||||
|           expect(page).to have_content("hyphens in the name") | ||||
|                      .and have_content("Last edited by #{user.name}") | ||||
|                      .and have_content("My awesome wiki!") | ||||
|         end | ||||
|       end | ||||
| 
 | ||||
|       it "shows the emoji autocompletion dropdown" do | ||||
|         click_link("New page") | ||||
| 
 | ||||
|         page.within(".wiki-form") do | ||||
|           find("#wiki_content").native.send_keys("") | ||||
| 
 | ||||
|           fill_in(:wiki_content, with: ":") | ||||
|         end | ||||
| 
 | ||||
|         expect(page).to have_selector(".atwho-view") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "in a group namespace" do | ||||
|       let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } | ||||
| 
 | ||||
|       context "via the `new wiki page` page" do | ||||
|         it "creates a page" do | ||||
|           click_link("New page") | ||||
| 
 | ||||
|           page.within(".wiki-form") do | ||||
|             fill_in(:wiki_title, with: "foo") | ||||
|             fill_in(:wiki_content, with: "My awesome wiki!") | ||||
|           end | ||||
| 
 | ||||
|           # Commit message field should have correct value. | ||||
|           expect(page).to have_field("wiki[message]", with: "Create foo") | ||||
| 
 | ||||
|           click_button("Create page") | ||||
| 
 | ||||
|           expect(page).to have_content("foo") | ||||
|                      .and have_content("Last edited by #{user.name}") | ||||
|                      .and have_content("My awesome wiki!") | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe 'sidebar feature' do | ||||
|     context 'when there are some existing pages' do | ||||
|       before do | ||||
|         create(:wiki_page, wiki: wiki, title: 'home', content: 'home') | ||||
|         create(:wiki_page, wiki: wiki, title: 'another', content: 'another') | ||||
|       end | ||||
| 
 | ||||
|       it 'renders a default sidebar when there is no customized sidebar' do | ||||
|         visit(project_wikis_path(project)) | ||||
| 
 | ||||
|         expect(page).to have_content('another') | ||||
|         expect(page).not_to have_link('View All Pages') | ||||
|       end | ||||
| 
 | ||||
|       context 'when there is a customized sidebar' do | ||||
|         before do | ||||
|           create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My customized sidebar') | ||||
|         end | ||||
| 
 | ||||
|         it 'renders my customized sidebar instead of the default one' do | ||||
|           visit(project_wikis_path(project)) | ||||
| 
 | ||||
|           expect(page).to have_content('My customized sidebar') | ||||
|           expect(page).not_to have_content('Another') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'when there are 15 existing pages' do | ||||
|       before do | ||||
|         (1..5).each { |i| create(:wiki_page, wiki: wiki, title: "my page #{i}") } | ||||
|         (6..10).each { |i| create(:wiki_page, wiki: wiki, title: "parent/my page #{i}") } | ||||
|         (11..15).each { |i| create(:wiki_page, wiki: wiki, title: "grandparent/parent/my page #{i}") } | ||||
|       end | ||||
| 
 | ||||
|       it 'shows all pages in the sidebar' do | ||||
|         visit(project_wikis_path(project)) | ||||
| 
 | ||||
|         (1..15).each { |i| expect(page).to have_content("my page #{i}") } | ||||
|         expect(page).not_to have_link('View All Pages') | ||||
|       end | ||||
| 
 | ||||
|       context 'when there are more than 15 existing pages' do | ||||
|         before do | ||||
|           create(:wiki_page, wiki: wiki, title: 'my page 16') | ||||
|         end | ||||
| 
 | ||||
|         it 'shows the first 15 pages in the sidebar' do | ||||
|           visit(project_wikis_path(project)) | ||||
| 
 | ||||
|           expect(page).to have_text('my page', count: 15) | ||||
|           expect(page).to have_link('View All Pages') | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,22 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe 'User deletes wiki page', :js do | ||||
|   let(:user) { create(:user) } | ||||
|   let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } | ||||
|   let(:wiki_page) { create(:wiki_page, wiki: project.wiki) } | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|     visit(project_wiki_path(project, wiki_page)) | ||||
|   end | ||||
| 
 | ||||
|   it 'deletes a page' do | ||||
|     click_on('Edit') | ||||
|     click_on('Delete') | ||||
|     find('.modal-footer .btn-danger').click | ||||
| 
 | ||||
|     expect(page).to have_content('Page was successfully deleted') | ||||
|   end | ||||
| end | ||||
|  | @ -1,263 +0,0 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe 'User updates wiki page' do | ||||
|   include WikiHelpers | ||||
| 
 | ||||
|   let(:user) { create(:user) } | ||||
| 
 | ||||
|   before do | ||||
|     project.add_maintainer(user) | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   context 'when wiki is empty' do | ||||
|     before do |example| | ||||
|       visit(project_wikis_path(project)) | ||||
| 
 | ||||
|       wait_for_svg_to_be_loaded(example) | ||||
| 
 | ||||
|       click_link "Create your first page" | ||||
|     end | ||||
| 
 | ||||
|     context 'in a user namespace' do | ||||
|       let(:project) { create(:project, :wiki_repo) } | ||||
| 
 | ||||
|       it 'redirects back to the home edit page' do | ||||
|         page.within(:css, '.wiki-form .form-actions') do | ||||
|           click_on('Cancel') | ||||
|         end | ||||
| 
 | ||||
|         expect(current_path).to eq wiki_path(project.wiki) | ||||
|       end | ||||
| 
 | ||||
|       it 'updates a page that has a path', :js do | ||||
|         fill_in(:wiki_title, with: 'one/two/three-test') | ||||
| 
 | ||||
|         page.within '.wiki-form' do | ||||
|           fill_in(:wiki_content, with: 'wiki content') | ||||
|           click_on('Create page') | ||||
|         end | ||||
| 
 | ||||
|         expect(current_path).to include('one/two/three-test') | ||||
|         expect(find('.wiki-pages')).to have_content('three') | ||||
| 
 | ||||
|         first(:link, text: 'three').click | ||||
| 
 | ||||
|         expect(find('.nav-text')).to have_content('three') | ||||
| 
 | ||||
|         click_on('Edit') | ||||
| 
 | ||||
|         expect(current_path).to include('one/two/three-test') | ||||
|         expect(page).to have_content('Edit Page') | ||||
| 
 | ||||
|         fill_in('Content', with: 'Updated Wiki Content') | ||||
|         click_on('Save changes') | ||||
| 
 | ||||
|         expect(page).to have_content('Updated Wiki Content') | ||||
|       end | ||||
| 
 | ||||
|       it_behaves_like 'wiki file attachments' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when wiki is not empty' do | ||||
|     let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } | ||||
|     let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, title: 'home', content: 'Home page') } | ||||
| 
 | ||||
|     before do | ||||
|       visit(project_wikis_path(project)) | ||||
| 
 | ||||
|       click_link('Edit') | ||||
|     end | ||||
| 
 | ||||
|     context 'in a user namespace' do | ||||
|       let(:project) { create(:project, :wiki_repo) } | ||||
| 
 | ||||
|       it 'updates a page', :js do | ||||
|         # Commit message field should have correct value. | ||||
|         expect(page).to have_field('wiki[message]', with: 'Update home') | ||||
| 
 | ||||
|         fill_in(:wiki_content, with: 'My awesome wiki!') | ||||
|         click_button('Save changes') | ||||
| 
 | ||||
|         expect(page).to have_content('Home') | ||||
|         expect(page).to have_content("Last edited by #{user.name}") | ||||
|         expect(page).to have_content('My awesome wiki!') | ||||
|       end | ||||
| 
 | ||||
|       it 'updates the commit message as the title is changed', :js do | ||||
|         fill_in(:wiki_title, with: '& < > \ \ { } &') | ||||
| 
 | ||||
|         expect(page).to have_field('wiki[message]', with: 'Update & < > \ \ { } &') | ||||
|       end | ||||
| 
 | ||||
|       it 'correctly escapes the commit message entities', :js do | ||||
|         fill_in(:wiki_title, with: 'Wiki title') | ||||
| 
 | ||||
|         expect(page).to have_field('wiki[message]', with: 'Update Wiki title') | ||||
|       end | ||||
| 
 | ||||
|       it 'shows a validation error message' do | ||||
|         fill_in(:wiki_content, with: '') | ||||
|         click_button('Save changes') | ||||
| 
 | ||||
|         expect(page).to have_selector('.wiki-form') | ||||
|         expect(page).to have_content('Edit Page') | ||||
|         expect(page).to have_content('The form contains the following error:') | ||||
|         expect(page).to have_content("Content can't be blank") | ||||
|         expect(find('textarea#wiki_content').value).to eq('') | ||||
|       end | ||||
| 
 | ||||
|       it 'shows the emoji autocompletion dropdown', :js do | ||||
|         find('#wiki_content').native.send_keys('') | ||||
|         fill_in(:wiki_content, with: ':') | ||||
| 
 | ||||
|         expect(page).to have_selector('.atwho-view') | ||||
|       end | ||||
| 
 | ||||
|       it 'shows the error message' do | ||||
|         wiki_page.update(content: 'Update') | ||||
| 
 | ||||
|         click_button('Save changes') | ||||
| 
 | ||||
|         expect(page).to have_content('Someone edited the page the same time you did.') | ||||
|       end | ||||
| 
 | ||||
|       it 'updates a page' do | ||||
|         fill_in('Content', with: 'Updated Wiki Content') | ||||
|         click_on('Save changes') | ||||
| 
 | ||||
|         expect(page).to have_content('Updated Wiki Content') | ||||
|       end | ||||
| 
 | ||||
|       it 'cancels editing of a page' do | ||||
|         page.within(:css, '.wiki-form .form-actions') do | ||||
|           click_on('Cancel') | ||||
|         end | ||||
| 
 | ||||
|         expect(current_path).to eq(project_wiki_path(project, wiki_page)) | ||||
|       end | ||||
| 
 | ||||
|       it_behaves_like 'wiki file attachments' | ||||
|     end | ||||
| 
 | ||||
|     context 'in a group namespace' do | ||||
|       let(:project) { create(:project, :wiki_repo, namespace: create(:group, :public)) } | ||||
| 
 | ||||
|       it 'updates a page', :js do | ||||
|         # Commit message field should have correct value. | ||||
|         expect(page).to have_field('wiki[message]', with: 'Update home') | ||||
| 
 | ||||
|         fill_in(:wiki_content, with: 'My awesome wiki!') | ||||
| 
 | ||||
|         click_button('Save changes') | ||||
| 
 | ||||
|         expect(page).to have_content('Home') | ||||
|         expect(page).to have_content("Last edited by #{user.name}") | ||||
|         expect(page).to have_content('My awesome wiki!') | ||||
|       end | ||||
| 
 | ||||
|       it_behaves_like 'wiki file attachments' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when the page is in a subdir' do | ||||
|     let!(:project) { create(:project, :wiki_repo) } | ||||
|     let(:project_wiki) { create(:project_wiki, project: project, user: project.creator) } | ||||
|     let(:page_name) { 'page_name' } | ||||
|     let(:page_dir) { "foo/bar/#{page_name}" } | ||||
|     let!(:wiki_page) { create(:wiki_page, wiki: project_wiki, title: page_dir, content: 'Home page') } | ||||
| 
 | ||||
|     before do | ||||
|       visit(project_wiki_edit_path(project, wiki_page)) | ||||
|     end | ||||
| 
 | ||||
|     it 'moves the page to the root folder' do | ||||
|       fill_in(:wiki_title, with: "/#{page_name}") | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(project_wiki_path(project, page_name)) | ||||
|     end | ||||
| 
 | ||||
|     it 'moves the page to other dir' do | ||||
|       new_page_dir = "foo1/bar1/#{page_name}" | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: new_page_dir) | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(project_wiki_path(project, new_page_dir)) | ||||
|     end | ||||
| 
 | ||||
|     it 'remains in the same place if title has not changed' do | ||||
|       original_path = project_wiki_path(project, wiki_page) | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: page_name) | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(original_path) | ||||
|     end | ||||
| 
 | ||||
|     it 'can be moved to a different dir with a different name' do | ||||
|       new_page_dir = "foo1/bar1/new_page_name" | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: new_page_dir) | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(project_wiki_path(project, new_page_dir)) | ||||
|     end | ||||
| 
 | ||||
|     it 'can be renamed and moved to the root folder' do | ||||
|       new_name = 'new_page_name' | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: "/#{new_name}") | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(project_wiki_path(project, new_name)) | ||||
|     end | ||||
| 
 | ||||
|     it 'squishes the title before creating the page' do | ||||
|       new_page_dir = "  foo1 /  bar1  /  #{page_name}  " | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: new_page_dir) | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(project_wiki_path(project, "foo1/bar1/#{page_name}")) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'wiki file attachments' | ||||
|   end | ||||
| 
 | ||||
|   context 'when an existing page exceeds the content size limit' do | ||||
|     let_it_be(:project) { create(:project, :wiki_repo) } | ||||
|     let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, content: "one\ntwo\nthree") } | ||||
| 
 | ||||
|     before do | ||||
|       stub_application_setting(wiki_page_max_content_bytes: 10) | ||||
| 
 | ||||
|       visit wiki_page_path(wiki_page.wiki, wiki_page, action: :edit) | ||||
|     end | ||||
| 
 | ||||
|     it 'allows changing the title if the content does not change' do | ||||
|       fill_in 'Title', with: 'new title' | ||||
|       click_on 'Save changes' | ||||
| 
 | ||||
|       expect(page).to have_content('Wiki was successfully updated.') | ||||
|     end | ||||
| 
 | ||||
|     it 'shows a validation error when trying to change the content' do | ||||
|       fill_in 'Content', with: 'new content' | ||||
|       click_on 'Save changes' | ||||
| 
 | ||||
|       expect(page).to have_content('The form contains the following error:') | ||||
|       expect(page).to have_content('Content is too long (11 Bytes). The maximum size is 10 Bytes.') | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -2,108 +2,86 @@ | |||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe 'User views empty wiki' do | ||||
|   let(:user) { create(:user) } | ||||
|   let(:confluence_link) { 'Enable the Confluence Wiki integration' } | ||||
|   let(:element) { page.find('.row.empty-state') } | ||||
| RSpec.describe 'Project > User views empty wiki' do | ||||
|   let_it_be(:user) { create(:user) } | ||||
| 
 | ||||
|   shared_examples 'empty wiki and accessible issues' do | ||||
|     it 'show "issue tracker" message' do | ||||
|       visit(project_wikis_path(project)) | ||||
|   let(:wiki) { create(:project_wiki, project: project) } | ||||
| 
 | ||||
|       expect(element).to have_content('This project has no wiki pages') | ||||
|       expect(element).to have_content('You must be a project member') | ||||
|       expect(element).to have_content('improve the wiki for this project') | ||||
|       expect(element).to have_link("issue tracker", href: project_issues_path(project)) | ||||
|       expect(element).to have_link("Suggest wiki improvement", href: new_project_issue_path(project)) | ||||
|       expect(element).to have_no_link(confluence_link) | ||||
|     end | ||||
|   end | ||||
|   it_behaves_like 'User views empty wiki' do | ||||
|     context 'when project is public' do | ||||
|       let(:project) { create(:project, :public) } | ||||
| 
 | ||||
|   shared_examples 'empty wiki and non-accessible issues' do | ||||
|     it 'does not show "issue tracker" message' do | ||||
|       visit(project_wikis_path(project)) | ||||
| 
 | ||||
|       expect(element).to have_content('This project has no wiki pages') | ||||
|       expect(element).to have_content('You must be a project member') | ||||
|       expect(element).to have_no_link('Suggest wiki improvement') | ||||
|       expect(element).to have_no_link(confluence_link) | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when user is logged out and issue tracker is public' do | ||||
|     let(:project) { create(:project, :public, :wiki_repo) } | ||||
| 
 | ||||
|     it_behaves_like 'empty wiki and accessible issues' | ||||
|   end | ||||
| 
 | ||||
|   context 'when user is logged in and not a member' do | ||||
|     let(:project) { create(:project, :public, :wiki_repo) } | ||||
| 
 | ||||
|     before do | ||||
|       sign_in(user) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'empty wiki and accessible issues' | ||||
|   end | ||||
|       it_behaves_like 'empty wiki message', issuable: true | ||||
| 
 | ||||
|       context 'when issue tracker is private' do | ||||
|     let(:project) { create(:project, :public, :wiki_repo, :issues_private) } | ||||
|         let(:project) { create(:project, :public, :issues_private) } | ||||
| 
 | ||||
|     it_behaves_like 'empty wiki and non-accessible issues' | ||||
|         it_behaves_like 'empty wiki message', issuable: false | ||||
|       end | ||||
| 
 | ||||
|       context 'when issue tracker is disabled' do | ||||
|     let(:project) { create(:project, :public, :wiki_repo, :issues_disabled) } | ||||
|         let(:project) { create(:project, :public, :issues_disabled) } | ||||
| 
 | ||||
|     it_behaves_like 'empty wiki and non-accessible issues' | ||||
|         it_behaves_like 'empty wiki message', issuable: false | ||||
|       end | ||||
| 
 | ||||
|   context 'when user is logged in and a member' do | ||||
|     let(:project) { create(:project, :public) } | ||||
| 
 | ||||
|       context 'and user is logged in' do | ||||
|         before do | ||||
|           sign_in(user) | ||||
|         end | ||||
| 
 | ||||
|         context 'and user is not a member' do | ||||
|           it_behaves_like 'empty wiki message', issuable: true | ||||
|         end | ||||
| 
 | ||||
|         context 'and user is a member' do | ||||
|           before do | ||||
|             project.add_developer(user) | ||||
|           end | ||||
| 
 | ||||
|     it 'shows "create first page" message' do | ||||
|       visit(project_wikis_path(project)) | ||||
| 
 | ||||
|       expect(element).to have_content('your project', count: 2) | ||||
| 
 | ||||
|       element.click_link 'Create your first page' | ||||
| 
 | ||||
|       expect(page).to have_button('Create page') | ||||
|           it_behaves_like 'empty wiki message', writable: true, issuable: true | ||||
|         end | ||||
| 
 | ||||
|     it 'does not show the "enable confluence" button' do | ||||
|       visit(project_wikis_path(project)) | ||||
| 
 | ||||
|       expect(element).to have_no_link(confluence_link) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|   context 'when user is logged in and an admin' do | ||||
|     let(:project) { create(:project, :public, :wiki_repo) } | ||||
|     context 'when project is private' do | ||||
|       let(:project) { create(:project, :private) } | ||||
| 
 | ||||
|       it_behaves_like 'wiki is not found' | ||||
| 
 | ||||
|       context 'and user is logged in' do | ||||
|         before do | ||||
|           sign_in(user) | ||||
|         end | ||||
| 
 | ||||
|         context 'and user is not a member' do | ||||
|           it_behaves_like 'wiki is not found' | ||||
|         end | ||||
| 
 | ||||
|         context 'and user is a member' do | ||||
|           before do | ||||
|             project.add_developer(user) | ||||
|           end | ||||
| 
 | ||||
|           it_behaves_like 'empty wiki message', writable: true, issuable: true | ||||
|         end | ||||
| 
 | ||||
|         context 'and user is a maintainer' do | ||||
|           before do | ||||
|             project.add_maintainer(user) | ||||
|           end | ||||
| 
 | ||||
|     it 'shows the "enable confluence" button' do | ||||
|       visit(project_wikis_path(project)) | ||||
|           it_behaves_like 'empty wiki message', writable: true, issuable: true, confluence: true | ||||
| 
 | ||||
|       expect(element).to have_link(confluence_link) | ||||
|           context 'and Confluence is already enabled' do | ||||
|             before do | ||||
|               create(:confluence_service, project: project) | ||||
|             end | ||||
| 
 | ||||
|     it 'does not show "enable confluence" button if confluence is already enabled' do | ||||
|       create(:confluence_service, project: project) | ||||
| 
 | ||||
|       visit(project_wikis_path(project)) | ||||
| 
 | ||||
|       expect(element).to have_no_link(confluence_link) | ||||
|             it_behaves_like 'empty wiki message', writable: true, issuable: true, confluence: false | ||||
|           end | ||||
|         end | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,20 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require "spec_helper" | ||||
| 
 | ||||
| RSpec.describe 'Project wikis' do | ||||
|   let_it_be(:user) { create(:user) } | ||||
| 
 | ||||
|   let(:wiki) { create(:project_wiki, user: user, project: project) } | ||||
|   let(:project) { create(:project, namespace: user.namespace, creator: user) } | ||||
| 
 | ||||
|   it_behaves_like 'User creates wiki page' | ||||
|   it_behaves_like 'User deletes wiki page' | ||||
|   it_behaves_like 'User previews wiki changes' | ||||
|   it_behaves_like 'User updates wiki page' | ||||
|   it_behaves_like 'User uses wiki shortcuts' | ||||
|   it_behaves_like 'User views AsciiDoc page with includes' | ||||
|   it_behaves_like 'User views a wiki page' | ||||
|   it_behaves_like 'User views wiki pages' | ||||
|   it_behaves_like 'User views wiki sidebar' | ||||
| end | ||||
|  | @ -3,6 +3,7 @@ | |||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe 'Releases (JavaScript fixtures)' do | ||||
|   include ApiHelpers | ||||
|   include JavaScriptFixturesHelpers | ||||
| 
 | ||||
|   let_it_be(:admin) { create(:admin) } | ||||
|  | @ -68,16 +69,14 @@ RSpec.describe 'Releases (JavaScript fixtures)' do | |||
|            link_type: :runbook) | ||||
|   end | ||||
| 
 | ||||
|   before(:all) do | ||||
|     clean_frontend_fixtures('api/releases/') | ||||
|   end | ||||
| 
 | ||||
|   after(:all) do | ||||
|     remove_repository(project) | ||||
|   end | ||||
| 
 | ||||
|   describe API::Releases, '(JavaScript fixtures)', type: :request do | ||||
|     include ApiHelpers | ||||
|   describe API::Releases, type: :request do | ||||
|     before(:all) do | ||||
|       clean_frontend_fixtures('api/releases/') | ||||
|     end | ||||
| 
 | ||||
|     it 'api/releases/release.json' do | ||||
|       get api("/projects/#{project.id}/releases/#{release.tag}", admin) | ||||
|  | @ -85,4 +84,22 @@ RSpec.describe 'Releases (JavaScript fixtures)' do | |||
|       expect(response).to be_successful | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   graphql_query_path = 'releases/queries/all_releases.query.graphql' | ||||
| 
 | ||||
|   describe "~/#{graphql_query_path}", type: :request do | ||||
|     include GraphqlHelpers | ||||
| 
 | ||||
|     before(:all) do | ||||
|       clean_frontend_fixtures('graphql/releases/') | ||||
|     end | ||||
| 
 | ||||
|     it "graphql/#{graphql_query_path}.json" do | ||||
|       query = File.read(File.join(Rails.root, '/app/assets/javascripts', graphql_query_path)) | ||||
| 
 | ||||
|       post_graphql(query, current_user: admin, variables: { fullPath: project.full_path }) | ||||
| 
 | ||||
|       expect_graphql_errors_to_be_empty | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -0,0 +1,71 @@ | |||
| import { mount, createWrapper } from '@vue/test-utils'; | ||||
| import { getByText as getByTextHelper } from '@testing-library/dom'; | ||||
| import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; | ||||
| import MemberSource from '~/vue_shared/components/members/table/member_source.vue'; | ||||
| 
 | ||||
| describe('MemberSource', () => { | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const createComponent = propsData => { | ||||
|     wrapper = mount(MemberSource, { | ||||
|       propsData: { | ||||
|         memberSource: { | ||||
|           id: 102, | ||||
|           name: 'Foo bar', | ||||
|           webUrl: 'https://gitlab.com/groups/foo-bar', | ||||
|         }, | ||||
|         ...propsData, | ||||
|       }, | ||||
|       directives: { | ||||
|         GlTooltip: createMockDirective(), | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const getByText = (text, options) => | ||||
|     createWrapper(getByTextHelper(wrapper.element, text, options)); | ||||
| 
 | ||||
|   const getTooltipDirective = elementWrapper => getBinding(elementWrapper.element, 'gl-tooltip'); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|   }); | ||||
| 
 | ||||
|   describe('direct member', () => { | ||||
|     it('displays "Direct member"', () => { | ||||
|       createComponent({ | ||||
|         isDirectMember: true, | ||||
|       }); | ||||
| 
 | ||||
|       expect(getByText('Direct member').exists()).toBe(true); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   describe('inherited member', () => { | ||||
|     let sourceGroupLink; | ||||
| 
 | ||||
|     beforeEach(() => { | ||||
|       createComponent({ | ||||
|         isDirectMember: false, | ||||
|       }); | ||||
| 
 | ||||
|       sourceGroupLink = getByText('Foo bar'); | ||||
|     }); | ||||
| 
 | ||||
|     it('displays a link to source group', () => { | ||||
|       createComponent({ | ||||
|         isDirectMember: false, | ||||
|       }); | ||||
| 
 | ||||
|       expect(sourceGroupLink.exists()).toBe(true); | ||||
|       expect(sourceGroupLink.attributes('href')).toBe('https://gitlab.com/groups/foo-bar'); | ||||
|     }); | ||||
| 
 | ||||
|     it('displays tooltip with "Inherited"', () => { | ||||
|       const tooltipDirective = getTooltipDirective(sourceGroupLink); | ||||
| 
 | ||||
|       expect(tooltipDirective).not.toBeUndefined(); | ||||
|       expect(sourceGroupLink.attributes('title')).toBe('Inherited'); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  | @ -1,4 +1,5 @@ | |||
| import { mount, createLocalVue } from '@vue/test-utils'; | ||||
| import Vuex from 'vuex'; | ||||
| import { MEMBER_TYPES } from '~/vue_shared/components/members/constants'; | ||||
| import { member as memberMock, group, invite, accessRequest } from '../mock_data'; | ||||
| import MembersTableCell from '~/vue_shared/components/members/table/members_table_cell.vue'; | ||||
|  | @ -10,6 +11,10 @@ describe('MemberList', () => { | |||
|         type: String, | ||||
|         required: true, | ||||
|       }, | ||||
|       isDirectMember: { | ||||
|         type: Boolean, | ||||
|         required: true, | ||||
|       }, | ||||
|     }, | ||||
|     render(createElement) { | ||||
|       return createElement('div', this.memberType); | ||||
|  | @ -17,20 +22,34 @@ describe('MemberList', () => { | |||
|   }; | ||||
| 
 | ||||
|   const localVue = createLocalVue(); | ||||
|   localVue.use(Vuex); | ||||
|   localVue.component('wrapped-component', WrappedComponent); | ||||
| 
 | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const createComponent = propsData => { | ||||
|     wrapper = mount(MembersTableCell, { | ||||
|       localVue, | ||||
|       propsData, | ||||
|       scopedSlots: { | ||||
|         default: '<wrapped-component :member-type="props.memberType" />', | ||||
|   const createStore = (state = {}) => { | ||||
|     return new Vuex.Store({ | ||||
|       state: { | ||||
|         sourceId: 1, | ||||
|         ...state, | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   let wrapper; | ||||
| 
 | ||||
|   const createComponent = (propsData, state = {}) => { | ||||
|     wrapper = mount(MembersTableCell, { | ||||
|       localVue, | ||||
|       propsData, | ||||
|       store: createStore(state), | ||||
|       scopedSlots: { | ||||
|         default: | ||||
|           '<wrapped-component :member-type="props.memberType" :is-direct-member="props.isDirectMember" />', | ||||
|       }, | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const findWrappedComponent = () => wrapper.find(WrappedComponent); | ||||
| 
 | ||||
|   afterEach(() => { | ||||
|     wrapper.destroy(); | ||||
|     wrapper = null; | ||||
|  | @ -47,7 +66,31 @@ describe('MemberList', () => { | |||
|     ({ member, expectedMemberType }) => { | ||||
|       createComponent({ member }); | ||||
| 
 | ||||
|       expect(wrapper.find(WrappedComponent).props('memberType')).toBe(expectedMemberType); | ||||
|       expect(findWrappedComponent().props('memberType')).toBe(expectedMemberType); | ||||
|     }, | ||||
|   ); | ||||
| 
 | ||||
|   describe('isDirectMember', () => { | ||||
|     it('returns `true` when member source has same ID as `sourceId`', () => { | ||||
|       createComponent({ | ||||
|         member: { | ||||
|           ...memberMock, | ||||
|           source: { | ||||
|             ...memberMock.source, | ||||
|             id: 1, | ||||
|           }, | ||||
|         }, | ||||
|       }); | ||||
| 
 | ||||
|       expect(findWrappedComponent().props('isDirectMember')).toBe(true); | ||||
|     }); | ||||
| 
 | ||||
|     it('returns `false` when member is inherited', () => { | ||||
|       createComponent({ | ||||
|         member: memberMock, | ||||
|       }); | ||||
| 
 | ||||
|       expect(findWrappedComponent().props('isDirectMember')).toBe(false); | ||||
|     }); | ||||
|   }); | ||||
| }); | ||||
|  |  | |||
|  | @ -5,7 +5,10 @@ import { | |||
|   getByTestId as getByTestIdHelper, | ||||
| } from '@testing-library/dom'; | ||||
| import MembersTable from '~/vue_shared/components/members/table/members_table.vue'; | ||||
| import MemberAvatar from '~/vue_shared/components/members/table/member_avatar.vue'; | ||||
| import MemberSource from '~/vue_shared/components/members/table/member_source.vue'; | ||||
| import * as initUserPopovers from '~/user_popovers'; | ||||
| import { member as memberMock, invite, accessRequest } from '../mock_data'; | ||||
| 
 | ||||
| const localVue = createLocalVue(); | ||||
| localVue.use(Vuex); | ||||
|  | @ -44,20 +47,31 @@ describe('MemberList', () => { | |||
| 
 | ||||
|   describe('fields', () => { | ||||
|     it.each` | ||||
|       field           | label | ||||
|       ${'source'}     | ${'Source'} | ||||
|       ${'granted'}    | ${'Access granted'} | ||||
|       ${'invited'}    | ${'Invited'} | ||||
|       ${'requested'}  | ${'Requested'} | ||||
|       ${'expires'}    | ${'Access expires'} | ||||
|       ${'maxRole'}    | ${'Max role'} | ||||
|       ${'expiration'} | ${'Expiration'} | ||||
|     `('renders the $label field', ({ field, label }) => {
 | ||||
|       field           | label               | member           | expectedComponent | ||||
|       ${'account'}    | ${'Account'}        | ${memberMock}    | ${MemberAvatar} | ||||
|       ${'source'}     | ${'Source'}         | ${memberMock}    | ${MemberSource} | ||||
|       ${'granted'}    | ${'Access granted'} | ${memberMock}    | ${null} | ||||
|       ${'invited'}    | ${'Invited'}        | ${invite}        | ${null} | ||||
|       ${'requested'}  | ${'Requested'}      | ${accessRequest} | ${null} | ||||
|       ${'expires'}    | ${'Access expires'} | ${memberMock}    | ${null} | ||||
|       ${'maxRole'}    | ${'Max role'}       | ${memberMock}    | ${null} | ||||
|       ${'expiration'} | ${'Expiration'}     | ${memberMock}    | ${null} | ||||
|     `('renders the $label field', ({ field, label, member, expectedComponent }) => {
 | ||||
|       createComponent({ | ||||
|         members: [member], | ||||
|         tableFields: [field], | ||||
|       }); | ||||
| 
 | ||||
|       expect(getByText(label, { selector: '[role="columnheader"]' }).exists()).toBe(true); | ||||
| 
 | ||||
|       if (expectedComponent) { | ||||
|         expect( | ||||
|           wrapper | ||||
|             .find(`[data-label="${label}"][role="cell"]`) | ||||
|             .find(expectedComponent) | ||||
|             .exists(), | ||||
|         ).toBe(true); | ||||
|       } | ||||
|     }); | ||||
| 
 | ||||
|     it('renders "Actions" field for screen readers', () => { | ||||
|  |  | |||
|  | @ -306,6 +306,38 @@ RSpec.describe IssuablesHelper do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#reviewer_sidebar_data' do | ||||
|     let(:user) { create(:user) } | ||||
| 
 | ||||
|     subject { helper.reviewer_sidebar_data(user, merge_request: merge_request) } | ||||
| 
 | ||||
|     context 'without merge_request' do | ||||
|       let(:merge_request) { nil } | ||||
| 
 | ||||
|       it 'returns hash of reviewer data' do | ||||
|         is_expected.to eql({ | ||||
|           avatar_url: user.avatar_url, | ||||
|           name: user.name, | ||||
|           username: user.username | ||||
|         }) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context 'with merge_request' do | ||||
|       let(:merge_request) { build(:merge_request) } | ||||
| 
 | ||||
|       where(can_merge: [true, false]) | ||||
| 
 | ||||
|       with_them do | ||||
|         before do | ||||
|           allow(merge_request).to receive(:can_be_merged_by?).and_return(can_merge) | ||||
|         end | ||||
| 
 | ||||
|         it { is_expected.to include({ can_merge: can_merge })} | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   describe '#issuable_squash_option?' do | ||||
|     using RSpec::Parameterized::TableSyntax | ||||
| 
 | ||||
|  |  | |||
|  | @ -41,11 +41,11 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red | |||
|   context 'for web IDE edit actions' do | ||||
|     it_behaves_like 'tracks and counts action' do | ||||
|       def track_action(params) | ||||
|         described_class.track_web_ide_edit_action(params) | ||||
|         described_class.track_web_ide_edit_action(**params) | ||||
|       end | ||||
| 
 | ||||
|       def count_unique(params) | ||||
|         described_class.count_web_ide_edit_actions(params) | ||||
|         described_class.count_web_ide_edit_actions(**params) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | @ -53,11 +53,11 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red | |||
|   context 'for SFE edit actions' do | ||||
|     it_behaves_like 'tracks and counts action' do | ||||
|       def track_action(params) | ||||
|         described_class.track_sfe_edit_action(params) | ||||
|         described_class.track_sfe_edit_action(**params) | ||||
|       end | ||||
| 
 | ||||
|       def count_unique(params) | ||||
|         described_class.count_sfe_edit_actions(params) | ||||
|         described_class.count_sfe_edit_actions(**params) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  | @ -65,11 +65,11 @@ RSpec.describe Gitlab::UsageDataCounters::EditorUniqueCounter, :clean_gitlab_red | |||
|   context 'for snippet editor edit actions' do | ||||
|     it_behaves_like 'tracks and counts action' do | ||||
|       def track_action(params) | ||||
|         described_class.track_snippet_editor_edit_action(params) | ||||
|         described_class.track_snippet_editor_edit_action(**params) | ||||
|       end | ||||
| 
 | ||||
|       def count_unique(params) | ||||
|         described_class.count_snippet_editor_edit_actions(params) | ||||
|         described_class.count_snippet_editor_edit_actions(**params) | ||||
|       end | ||||
|     end | ||||
|   end | ||||
|  |  | |||
|  | @ -8,11 +8,11 @@ RSpec.describe Gitlab::UsageDataCounters::TrackUniqueEvents, :clean_gitlab_redis | |||
|   let(:time) { Time.zone.now } | ||||
| 
 | ||||
|   def track_event(params) | ||||
|     track_unique_events.track_event(params) | ||||
|     track_unique_events.track_event(**params) | ||||
|   end | ||||
| 
 | ||||
|   def count_unique(params) | ||||
|     track_unique_events.count_unique_events(params) | ||||
|     track_unique_events.count_unique_events(**params) | ||||
|   end | ||||
| 
 | ||||
|   context 'tracking an event' do | ||||
|  |  | |||
|  | @ -13,16 +13,16 @@ module WikiHelpers | |||
|     find('.svg-content .js-lazy-loaded') if example.nil? || example.metadata.key?(:js) | ||||
|   end | ||||
| 
 | ||||
|   def upload_file_to_wiki(container, user, file_name) | ||||
|     opts = { | ||||
|   def upload_file_to_wiki(wiki, user, file_name) | ||||
|     params = { | ||||
|       file_name: file_name, | ||||
|       file_content: File.read(expand_fixture_path(file_name)) | ||||
|      } | ||||
| 
 | ||||
|     ::Wikis::CreateAttachmentService.new( | ||||
|       container: container, | ||||
|       container: wiki.container, | ||||
|       current_user: user, | ||||
|       params: opts | ||||
|     ).execute[:result][:file_path] | ||||
|       params: params | ||||
|     ).execute.dig(:result, :file_path) | ||||
|   end | ||||
| end | ||||
|  |  | |||
|  | @ -226,7 +226,7 @@ RSpec.shared_examples 'wiki controller actions' do | |||
|       where(:file_name) { ['dk.png', 'unsanitized.svg', 'git-cheat-sheet.pdf'] } | ||||
| 
 | ||||
|       with_them do | ||||
|         let(:id) { upload_file_to_wiki(container, user, file_name) } | ||||
|         let(:id) { upload_file_to_wiki(wiki, user, file_name) } | ||||
| 
 | ||||
|         it 'delivers the file with the correct headers' do | ||||
|           subject | ||||
|  |  | |||
|  | @ -1,14 +1,12 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Requires a context containing: | ||||
| #   project | ||||
| #   wiki | ||||
| 
 | ||||
| RSpec.shared_examples 'wiki file attachments' do | ||||
|   include DropzoneHelper | ||||
| 
 | ||||
|   context 'uploading attachments', :js do | ||||
|     let(:wiki) { project.wiki } | ||||
| 
 | ||||
|     def attach_with_dropzone(wait = false) | ||||
|       dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, wait) | ||||
|     end | ||||
|  | @ -0,0 +1,245 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Requires a context containing: | ||||
| #   wiki | ||||
| #   user | ||||
| 
 | ||||
| RSpec.shared_examples 'User creates wiki page' do | ||||
|   include WikiHelpers | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   context "when wiki is empty" do | ||||
|     before do |example| | ||||
|       visit wiki_path(wiki) | ||||
| 
 | ||||
|       wait_for_svg_to_be_loaded(example) | ||||
| 
 | ||||
|       click_link "Create your first page" | ||||
|     end | ||||
| 
 | ||||
|     it "shows validation error message" do | ||||
|       page.within(".wiki-form") do | ||||
|         fill_in(:wiki_content, with: "") | ||||
| 
 | ||||
|         click_on("Create page") | ||||
|       end | ||||
| 
 | ||||
|       expect(page).to have_content("The form contains the following error:").and have_content("Content can't be blank") | ||||
| 
 | ||||
|       page.within(".wiki-form") do | ||||
|         fill_in(:wiki_content, with: "[link test](test)") | ||||
| 
 | ||||
|         click_on("Create page") | ||||
|       end | ||||
| 
 | ||||
|       expect(page).to have_content("Home").and have_content("link test") | ||||
| 
 | ||||
|       click_link("link test") | ||||
| 
 | ||||
|       expect(page).to have_content("Create New Page") | ||||
|     end | ||||
| 
 | ||||
|     it "shows non-escaped link in the pages list" do | ||||
|       fill_in(:wiki_title, with: "one/two/three-test") | ||||
| 
 | ||||
|       page.within(".wiki-form") do | ||||
|         fill_in(:wiki_content, with: "wiki content") | ||||
| 
 | ||||
|         click_on("Create page") | ||||
|       end | ||||
| 
 | ||||
|       expect(current_path).to include("one/two/three-test") | ||||
|       expect(page).to have_link(href: wiki_page_path(wiki, 'one/two/three-test')) | ||||
|     end | ||||
| 
 | ||||
|     it "has `Create home` as a commit message", :js do | ||||
|       wait_for_requests | ||||
| 
 | ||||
|       expect(page).to have_field("wiki[message]", with: "Create home") | ||||
|     end | ||||
| 
 | ||||
|     it "creates a page from the home page" do | ||||
|       fill_in(:wiki_content, with: "[test](test)\n[GitLab API doc](api)\n[Rake tasks](raketasks)\n# Wiki header\n") | ||||
|       fill_in(:wiki_message, with: "Adding links to wiki") | ||||
| 
 | ||||
|       page.within(".wiki-form") do | ||||
|         click_button("Create page") | ||||
|       end | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, "home")) | ||||
|       expect(page).to have_content("test GitLab API doc Rake tasks Wiki header") | ||||
|                   .and have_content("Home") | ||||
|                   .and have_content("Last edited by #{user.name}") | ||||
|                   .and have_header_with_correct_id_and_link(1, "Wiki header", "wiki-header") | ||||
| 
 | ||||
|       click_link("test") | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, "test")) | ||||
| 
 | ||||
|       page.within(:css, ".nav-text") do | ||||
|         expect(page).to have_content("Create New Page") | ||||
|       end | ||||
| 
 | ||||
|       click_link("Home") | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, "home")) | ||||
| 
 | ||||
|       click_link("GitLab API") | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, "api")) | ||||
| 
 | ||||
|       page.within(:css, ".nav-text") do | ||||
|         expect(page).to have_content("Create") | ||||
|       end | ||||
| 
 | ||||
|       click_link("Home") | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, "home")) | ||||
| 
 | ||||
|       click_link("Rake tasks") | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, "raketasks")) | ||||
| 
 | ||||
|       page.within(:css, ".nav-text") do | ||||
|         expect(page).to have_content("Create") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it "creates ASCII wiki with LaTeX blocks", :js do | ||||
|       stub_application_setting(plantuml_url: "http://localhost", plantuml_enabled: true) | ||||
| 
 | ||||
|       ascii_content = <<~MD | ||||
|         :stem: latexmath | ||||
| 
 | ||||
|         [stem] | ||||
|         ++++ | ||||
|         \\sqrt{4} = 2 | ||||
|         ++++ | ||||
| 
 | ||||
|         another part | ||||
| 
 | ||||
|         [latexmath] | ||||
|         ++++ | ||||
|         \\beta_x \\gamma | ||||
|         ++++ | ||||
| 
 | ||||
|         stem:[2+2] is 4 | ||||
|       MD | ||||
| 
 | ||||
|       find("#wiki_format option[value=asciidoc]").select_option | ||||
| 
 | ||||
|       fill_in(:wiki_content, with: ascii_content) | ||||
| 
 | ||||
|       page.within(".wiki-form") do | ||||
|         click_button("Create page") | ||||
|       end | ||||
| 
 | ||||
|       page.within ".md" do | ||||
|         expect(page).to have_selector(".katex", count: 3).and have_content("2+2 is 4") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it 'creates a wiki page with Org markup', :aggregate_failures do | ||||
|       org_content = <<~ORG | ||||
|         * Heading | ||||
|         ** Subheading | ||||
|         [[home][Link to Home]] | ||||
|       ORG | ||||
| 
 | ||||
|       page.within('.wiki-form') do | ||||
|         find('#wiki_format option[value=org]').select_option | ||||
|         fill_in(:wiki_content, with: org_content) | ||||
|         click_button('Create page') | ||||
|       end | ||||
| 
 | ||||
|       expect(page).to have_selector('h1', text: 'Heading') | ||||
|       expect(page).to have_selector('h2', text: 'Subheading') | ||||
|       expect(page).to have_link(href: wiki_page_path(wiki, 'home')) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'wiki file attachments' | ||||
|   end | ||||
| 
 | ||||
|   context "when wiki is not empty", :js do | ||||
|     before do | ||||
|       create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') | ||||
| 
 | ||||
|       visit wiki_path(wiki) | ||||
|     end | ||||
| 
 | ||||
|     context "via the `new wiki page` page" do | ||||
|       it "creates a page with a single word" do | ||||
|         click_link("New page") | ||||
| 
 | ||||
|         page.within(".wiki-form") do | ||||
|           fill_in(:wiki_title, with: "foo") | ||||
|           fill_in(:wiki_content, with: "My awesome wiki!") | ||||
|         end | ||||
| 
 | ||||
|         # Commit message field should have correct value. | ||||
|         expect(page).to have_field("wiki[message]", with: "Create foo") | ||||
| 
 | ||||
|         click_button("Create page") | ||||
| 
 | ||||
|         expect(page).to have_content("foo") | ||||
|                     .and have_content("Last edited by #{user.name}") | ||||
|                     .and have_content("My awesome wiki!") | ||||
|       end | ||||
| 
 | ||||
|       it "creates a page with spaces in the name" do | ||||
|         click_link("New page") | ||||
| 
 | ||||
|         page.within(".wiki-form") do | ||||
|           fill_in(:wiki_title, with: "Spaces in the name") | ||||
|           fill_in(:wiki_content, with: "My awesome wiki!") | ||||
|         end | ||||
| 
 | ||||
|         # Commit message field should have correct value. | ||||
|         expect(page).to have_field("wiki[message]", with: "Create Spaces in the name") | ||||
| 
 | ||||
|         click_button("Create page") | ||||
| 
 | ||||
|         expect(page).to have_content("Spaces in the name") | ||||
|                     .and have_content("Last edited by #{user.name}") | ||||
|                     .and have_content("My awesome wiki!") | ||||
|       end | ||||
| 
 | ||||
|       it "creates a page with hyphens in the name" do | ||||
|         click_link("New page") | ||||
| 
 | ||||
|         page.within(".wiki-form") do | ||||
|           fill_in(:wiki_title, with: "hyphens-in-the-name") | ||||
|           fill_in(:wiki_content, with: "My awesome wiki!") | ||||
|         end | ||||
| 
 | ||||
|         # Commit message field should have correct value. | ||||
|         expect(page).to have_field("wiki[message]", with: "Create hyphens in the name") | ||||
| 
 | ||||
|         page.within(".wiki-form") do | ||||
|           fill_in(:wiki_content, with: "My awesome wiki!") | ||||
| 
 | ||||
|           click_button("Create page") | ||||
|         end | ||||
| 
 | ||||
|         expect(page).to have_content("hyphens in the name") | ||||
|                     .and have_content("Last edited by #{user.name}") | ||||
|                     .and have_content("My awesome wiki!") | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     it "shows the emoji autocompletion dropdown" do | ||||
|       click_link("New page") | ||||
| 
 | ||||
|       page.within(".wiki-form") do | ||||
|         find("#wiki_content").native.send_keys("") | ||||
| 
 | ||||
|         fill_in(:wiki_content, with: ":") | ||||
|       end | ||||
| 
 | ||||
|       expect(page).to have_selector(".atwho-view") | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,24 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Requires a context containing: | ||||
| #   wiki | ||||
| #   user | ||||
| 
 | ||||
| RSpec.shared_examples 'User deletes wiki page' do | ||||
|   include WikiHelpers | ||||
| 
 | ||||
|   let(:wiki_page) { create(:wiki_page, wiki: wiki) } | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|     visit wiki_page_path(wiki, wiki_page) | ||||
|   end | ||||
| 
 | ||||
|   it 'deletes a page', :js do | ||||
|     click_on('Edit') | ||||
|     click_on('Delete') | ||||
|     find('.modal-footer .btn-danger').click | ||||
| 
 | ||||
|     expect(page).to have_content('Page was successfully deleted') | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,110 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Requires a context containing: | ||||
| #   wiki | ||||
| #   user | ||||
| 
 | ||||
| RSpec.shared_examples 'User previews wiki changes' do | ||||
|   let(:wiki_page) { build(:wiki_page, wiki: wiki) } | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   shared_examples 'relative links' do | ||||
|     let_it_be(:page_content) do | ||||
|       <<~HEREDOC | ||||
|         Some text so key event for [ does not trigger an incorrect replacement. | ||||
|         [regular link](regular) | ||||
|         [relative link 1](../relative) | ||||
|         [relative link 2](./relative) | ||||
|         [relative link 3](./e/f/relative) | ||||
|         [spaced link](title with spaces) | ||||
|       HEREDOC | ||||
|     end | ||||
| 
 | ||||
|     def relative_path(path) | ||||
|       (Pathname.new(wiki.wiki_base_path) + File.dirname(wiki_page.path).tr(' ', '-') + path).to_s | ||||
|     end | ||||
| 
 | ||||
|     shared_examples "rewrites relative links" do | ||||
|       specify do | ||||
|         expect(element).to have_link('regular link',    href: wiki.wiki_base_path + '/regular') | ||||
|         expect(element).to have_link('spaced link',     href: wiki.wiki_base_path + '/title%20with%20spaces') | ||||
| 
 | ||||
|         expect(element).to have_link('relative link 1', href: relative_path('../relative')) | ||||
|         expect(element).to have_link('relative link 2', href: relative_path('./relative')) | ||||
|         expect(element).to have_link('relative link 3', href: relative_path('./e/f/relative')) | ||||
|       end | ||||
|     end | ||||
| 
 | ||||
|     context "when there are no spaces or hyphens in the page name" do | ||||
|       let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a/b/c/d', content: page_content) } | ||||
| 
 | ||||
|       it_behaves_like 'rewrites relative links' | ||||
|     end | ||||
| 
 | ||||
|     context "when there are spaces in the page name" do | ||||
|       let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a page/b page/c page/d page', content: page_content) } | ||||
| 
 | ||||
|       it_behaves_like 'rewrites relative links' | ||||
|     end | ||||
| 
 | ||||
|     context "when there are hyphens in the page name" do | ||||
|       let(:wiki_page) { build(:wiki_page, wiki: wiki, title: 'a-page/b-page/c-page/d-page', content: page_content) } | ||||
| 
 | ||||
|       it_behaves_like 'rewrites relative links' | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context "when rendering a new wiki page", :js do | ||||
|     before do | ||||
|       wiki_page.create # rubocop:disable Rails/SaveBang | ||||
|       visit wiki_page_path(wiki, wiki_page) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'relative links' do | ||||
|       let(:element) { page.find('.js-wiki-page-content') } | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context "when previewing an existing wiki page", :js do | ||||
|     let(:preview) { page.find('.md-preview-holder') } | ||||
| 
 | ||||
|     before do | ||||
|       wiki_page.create # rubocop:disable Rails/SaveBang | ||||
|       visit wiki_page_path(wiki, wiki_page, action: :edit) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'relative links' do | ||||
|       before do | ||||
|         click_on 'Preview' | ||||
|       end | ||||
| 
 | ||||
|       let(:element) { preview } | ||||
|     end | ||||
| 
 | ||||
|     it 'renders content with CommonMark' do | ||||
|       fill_in :wiki_content, with: "1. one\n  - sublist\n" | ||||
|       click_on "Preview" | ||||
| 
 | ||||
|       # the above generates two separate lists (not embedded) in CommonMark | ||||
|       expect(preview).to have_content("sublist") | ||||
|       expect(preview).not_to have_xpath("//ol//li//ul") | ||||
|     end | ||||
| 
 | ||||
|     it "does not linkify double brackets inside code blocks as expected" do | ||||
|       fill_in :wiki_content, with: <<-HEREDOC | ||||
|         `[[do_not_linkify]]` | ||||
|         ``` | ||||
|         [[also_do_not_linkify]] | ||||
|         ``` | ||||
|       HEREDOC | ||||
|       click_on "Preview" | ||||
| 
 | ||||
|       expect(preview).to have_content("do_not_linkify") | ||||
|       expect(preview).to have_content('[[do_not_linkify]]') | ||||
|       expect(preview).to have_content('[[also_do_not_linkify]]') | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,231 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Requires a context containing: | ||||
| #   wiki | ||||
| #   user | ||||
| 
 | ||||
| RSpec.shared_examples 'User updates wiki page' do | ||||
|   include WikiHelpers | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   context 'when wiki is empty' do | ||||
|     before do |example| | ||||
|       visit(wiki_path(wiki)) | ||||
| 
 | ||||
|       wait_for_svg_to_be_loaded(example) | ||||
| 
 | ||||
|       click_link "Create your first page" | ||||
|     end | ||||
| 
 | ||||
|     it 'redirects back to the home edit page' do | ||||
|       page.within(:css, '.wiki-form .form-actions') do | ||||
|         click_on('Cancel') | ||||
|       end | ||||
| 
 | ||||
|       expect(current_path).to eq wiki_path(wiki) | ||||
|     end | ||||
| 
 | ||||
|     it 'updates a page that has a path', :js do | ||||
|       fill_in(:wiki_title, with: 'one/two/three-test') | ||||
| 
 | ||||
|       page.within '.wiki-form' do | ||||
|         fill_in(:wiki_content, with: 'wiki content') | ||||
|         click_on('Create page') | ||||
|       end | ||||
| 
 | ||||
|       expect(current_path).to include('one/two/three-test') | ||||
|       expect(find('.wiki-pages')).to have_content('three') | ||||
| 
 | ||||
|       first(:link, text: 'three').click | ||||
| 
 | ||||
|       expect(find('.nav-text')).to have_content('three') | ||||
| 
 | ||||
|       click_on('Edit') | ||||
| 
 | ||||
|       expect(current_path).to include('one/two/three-test') | ||||
|       expect(page).to have_content('Edit Page') | ||||
| 
 | ||||
|       fill_in('Content', with: 'Updated Wiki Content') | ||||
|       click_on('Save changes') | ||||
| 
 | ||||
|       expect(page).to have_content('Updated Wiki Content') | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'wiki file attachments' | ||||
|   end | ||||
| 
 | ||||
|   context 'when wiki is not empty' do | ||||
|     let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') } | ||||
| 
 | ||||
|     before do | ||||
|       visit(wiki_path(wiki)) | ||||
| 
 | ||||
|       click_link('Edit') | ||||
|     end | ||||
| 
 | ||||
|     it 'updates a page', :js do | ||||
|       # Commit message field should have correct value. | ||||
|       expect(page).to have_field('wiki[message]', with: 'Update home') | ||||
| 
 | ||||
|       fill_in(:wiki_content, with: 'My awesome wiki!') | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(page).to have_content('Home') | ||||
|       expect(page).to have_content("Last edited by #{user.name}") | ||||
|       expect(page).to have_content('My awesome wiki!') | ||||
|     end | ||||
| 
 | ||||
|     it 'updates the commit message as the title is changed', :js do | ||||
|       fill_in(:wiki_title, with: '& < > \ \ { } &') | ||||
| 
 | ||||
|       expect(page).to have_field('wiki[message]', with: 'Update & < > \ \ { } &') | ||||
|     end | ||||
| 
 | ||||
|     it 'correctly escapes the commit message entities', :js do | ||||
|       fill_in(:wiki_title, with: 'Wiki title') | ||||
| 
 | ||||
|       expect(page).to have_field('wiki[message]', with: 'Update Wiki title') | ||||
|     end | ||||
| 
 | ||||
|     it 'shows a validation error message' do | ||||
|       fill_in(:wiki_content, with: '') | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(page).to have_selector('.wiki-form') | ||||
|       expect(page).to have_content('Edit Page') | ||||
|       expect(page).to have_content('The form contains the following error:') | ||||
|       expect(page).to have_content("Content can't be blank") | ||||
|       expect(find('textarea#wiki_content').value).to eq('') | ||||
|     end | ||||
| 
 | ||||
|     it 'shows the emoji autocompletion dropdown', :js do | ||||
|       find('#wiki_content').native.send_keys('') | ||||
|       fill_in(:wiki_content, with: ':') | ||||
| 
 | ||||
|       expect(page).to have_selector('.atwho-view') | ||||
|     end | ||||
| 
 | ||||
|     it 'shows the error message' do | ||||
|       wiki_page.update(content: 'Update') # rubocop:disable Rails/SaveBang | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(page).to have_content('Someone edited the page the same time you did.') | ||||
|     end | ||||
| 
 | ||||
|     it 'updates a page' do | ||||
|       fill_in('Content', with: 'Updated Wiki Content') | ||||
|       click_on('Save changes') | ||||
| 
 | ||||
|       expect(page).to have_content('Updated Wiki Content') | ||||
|     end | ||||
| 
 | ||||
|     it 'cancels editing of a page' do | ||||
|       page.within(:css, '.wiki-form .form-actions') do | ||||
|         click_on('Cancel') | ||||
|       end | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, wiki_page)) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'wiki file attachments' | ||||
|   end | ||||
| 
 | ||||
|   context 'when the page is in a subdir' do | ||||
|     let(:page_name) { 'page_name' } | ||||
|     let(:page_dir) { "foo/bar/#{page_name}" } | ||||
|     let!(:wiki_page) { create(:wiki_page, wiki: wiki, title: page_dir, content: 'Home page') } | ||||
| 
 | ||||
|     before do | ||||
|       visit wiki_page_path(wiki, wiki_page, action: :edit) | ||||
|     end | ||||
| 
 | ||||
|     it 'moves the page to the root folder' do | ||||
|       fill_in(:wiki_title, with: "/#{page_name}") | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, page_name)) | ||||
|     end | ||||
| 
 | ||||
|     it 'moves the page to other dir' do | ||||
|       new_page_dir = "foo1/bar1/#{page_name}" | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: new_page_dir) | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) | ||||
|     end | ||||
| 
 | ||||
|     it 'remains in the same place if title has not changed' do | ||||
|       original_path = wiki_page_path(wiki, wiki_page) | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: page_name) | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(original_path) | ||||
|     end | ||||
| 
 | ||||
|     it 'can be moved to a different dir with a different name' do | ||||
|       new_page_dir = "foo1/bar1/new_page_name" | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: new_page_dir) | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, new_page_dir)) | ||||
|     end | ||||
| 
 | ||||
|     it 'can be renamed and moved to the root folder' do | ||||
|       new_name = 'new_page_name' | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: "/#{new_name}") | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, new_name)) | ||||
|     end | ||||
| 
 | ||||
|     it 'squishes the title before creating the page' do | ||||
|       new_page_dir = "  foo1 /  bar1  /  #{page_name}  " | ||||
| 
 | ||||
|       fill_in(:wiki_title, with: new_page_dir) | ||||
| 
 | ||||
|       click_button('Save changes') | ||||
| 
 | ||||
|       expect(current_path).to eq(wiki_page_path(wiki, "foo1/bar1/#{page_name}")) | ||||
|     end | ||||
| 
 | ||||
|     it_behaves_like 'wiki file attachments' | ||||
|   end | ||||
| 
 | ||||
|   context 'when an existing page exceeds the content size limit' do | ||||
|     let!(:wiki_page) { create(:wiki_page, wiki: wiki, content: "one\ntwo\nthree") } | ||||
| 
 | ||||
|     before do | ||||
|       stub_application_setting(wiki_page_max_content_bytes: 10) | ||||
| 
 | ||||
|       visit wiki_page_path(wiki_page.wiki, wiki_page, action: :edit) | ||||
|     end | ||||
| 
 | ||||
|     it 'allows changing the title if the content does not change' do | ||||
|       fill_in 'Title', with: 'new title' | ||||
|       click_on 'Save changes' | ||||
| 
 | ||||
|       expect(page).to have_content('Wiki was successfully updated.') | ||||
|     end | ||||
| 
 | ||||
|     it 'shows a validation error when trying to change the content' do | ||||
|       fill_in 'Content', with: 'new content' | ||||
|       click_on 'Save changes' | ||||
| 
 | ||||
|       expect(page).to have_content('The form contains the following error:') | ||||
|       expect(page).to have_content('Content is too long (11 Bytes). The maximum size is 10 Bytes.') | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -0,0 +1,20 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Requires a context containing: | ||||
| #   wiki | ||||
| #   user | ||||
| 
 | ||||
| RSpec.shared_examples 'User uses wiki shortcuts' do | ||||
|   let(:wiki_page) { create(:wiki_page, wiki: wiki, title: 'home', content: 'Home page') } | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|     visit wiki_page_path(wiki, wiki_page) | ||||
|   end | ||||
| 
 | ||||
|   it 'Visit edit wiki page using "e" keyboard shortcut', :js do | ||||
|     find('body').native.send_key('e') | ||||
| 
 | ||||
|     expect(find('.wiki-page-title')).to have_content('Edit Page') | ||||
|   end | ||||
| end | ||||
|  | @ -1,11 +1,7 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| 
 | ||||
| RSpec.describe 'User views AsciiDoc page with includes', :js do | ||||
|   let_it_be(:user) { create(:user) } | ||||
| RSpec.shared_examples 'User views AsciiDoc page with includes' do | ||||
|   let_it_be(:wiki_content_selector) { '[data-qa-selector=wiki_page_content]' } | ||||
|   let(:project) { create(:project, :public, :wiki_repo) } | ||||
|   let!(:included_wiki_page) { create_wiki_page('included_page', content: 'Content from the included page')} | ||||
|   let!(:wiki_page) { create_wiki_page('home', content: "Content from the main page.\ninclude::included_page.asciidoc[]") } | ||||
| 
 | ||||
|  | @ -16,16 +12,16 @@ RSpec.describe 'User views AsciiDoc page with includes', :js do | |||
|       format: :asciidoc | ||||
|     } | ||||
| 
 | ||||
|     create(:wiki_page, wiki: project.wiki, **attrs) | ||||
|     create(:wiki_page, wiki: wiki, **attrs) | ||||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   context 'when the file being included exists' do | ||||
|   context 'when the file being included exists', :js do | ||||
|     it 'includes the file contents' do | ||||
|       visit(project_wiki_path(project, wiki_page)) | ||||
|       visit(wiki_page_path(wiki, wiki_page)) | ||||
| 
 | ||||
|       page.within(:css, wiki_content_selector) do | ||||
|         expect(page).to have_content('Content from the main page. Content from the included page') | ||||
|  | @ -34,8 +30,10 @@ RSpec.describe 'User views AsciiDoc page with includes', :js do | |||
| 
 | ||||
|     context 'when there are multiple versions of the wiki pages' do | ||||
|       before do | ||||
|         # rubocop:disable Rails/SaveBang | ||||
|         included_wiki_page.update(message: 'updated included file', content: 'Updated content from the included page') | ||||
|         wiki_page.update(message: 'updated wiki page', content: "Updated content from the main page.\ninclude::included_page.asciidoc[]") | ||||
|         # rubocop:enable Rails/SaveBang | ||||
|       end | ||||
| 
 | ||||
|       let(:latest_version_id) { wiki_page.versions.first.id } | ||||
|  | @ -43,7 +41,7 @@ RSpec.describe 'User views AsciiDoc page with includes', :js do | |||
| 
 | ||||
|       context 'viewing the latest version' do | ||||
|         it 'includes the latest content' do | ||||
|           visit(project_wiki_path(project, wiki_page, version_id: latest_version_id)) | ||||
|           visit(wiki_page_path(wiki, wiki_page, version_id: latest_version_id)) | ||||
| 
 | ||||
|           page.within(:css, wiki_content_selector) do | ||||
|             expect(page).to have_content('Updated content from the main page. Updated content from the included page') | ||||
|  | @ -53,7 +51,7 @@ RSpec.describe 'User views AsciiDoc page with includes', :js do | |||
| 
 | ||||
|       context 'viewing the original version' do | ||||
|         it 'includes the content from the original version' do | ||||
|           visit(project_wiki_path(project, wiki_page, version_id: oldest_version_id)) | ||||
|           visit(wiki_page_path(wiki, wiki_page, version_id: oldest_version_id)) | ||||
| 
 | ||||
|           page.within(:css, wiki_content_selector) do | ||||
|             expect(page).to have_content('Content from the main page. Content from the included page') | ||||
|  | @ -63,13 +61,13 @@ RSpec.describe 'User views AsciiDoc page with includes', :js do | |||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when the file being included does not exist' do | ||||
|   context 'when the file being included does not exist', :js do | ||||
|     before do | ||||
|       included_wiki_page.delete | ||||
|     end | ||||
| 
 | ||||
|     it 'outputs an error' do | ||||
|       visit(project_wiki_path(project, wiki_page)) | ||||
|       visit(wiki_page_path(wiki, wiki_page)) | ||||
| 
 | ||||
|       page.within(:css, wiki_content_selector) do | ||||
|         expect(page).to have_content('Content from the main page. [ERROR: include::included_page.asciidoc[] - unresolved directive]') | ||||
|  | @ -0,0 +1,62 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Requires a context containing: | ||||
| #   wiki | ||||
| 
 | ||||
| RSpec.shared_examples 'User views empty wiki' do | ||||
|   let(:element) { page.find('.row.empty-state') } | ||||
|   let(:container_name) { wiki.container.class.name.humanize(capitalize: false) } | ||||
|   let(:confluence_link) { 'Enable the Confluence Wiki integration' } | ||||
| 
 | ||||
|   shared_examples 'wiki is not found' do | ||||
|     it 'shows an error message' do | ||||
|       visit wiki_path(wiki) | ||||
| 
 | ||||
|       if @current_user | ||||
|         expect(page).to have_content('Page Not Found') | ||||
|       else | ||||
|         expect(page).to have_content('You need to sign in') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   shared_examples 'empty wiki message' do |writable: false, issuable: false, confluence: false| | ||||
|     # This mirrors the logic in: | ||||
|     # - app/views/shared/empty_states/_wikis.html.haml | ||||
|     # - WikiHelper#wiki_empty_state_messages | ||||
|     it 'shows the empty state message with the expected elements' do | ||||
|       visit wiki_path(wiki) | ||||
| 
 | ||||
|       if writable | ||||
|         expect(element).to have_content("The wiki lets you write documentation for your #{container_name}") | ||||
|       else | ||||
|         expect(element).to have_content("This #{container_name} has no wiki pages") | ||||
|         expect(element).to have_content("You must be a #{container_name} member") | ||||
|       end | ||||
| 
 | ||||
|       if issuable && !writable | ||||
|         expect(element).to have_content("improve the wiki for this #{container_name}") | ||||
|         expect(element).to have_link("issue tracker", href: project_issues_path(project)) | ||||
|         expect(element).to have_link("Suggest wiki improvement", href: new_project_issue_path(project)) | ||||
|       else | ||||
|         expect(element).not_to have_content("improve the wiki for this #{container_name}") | ||||
|         expect(element).not_to have_link("issue tracker") | ||||
|         expect(element).not_to have_link("Suggest wiki improvement") | ||||
|       end | ||||
| 
 | ||||
|       if confluence | ||||
|         expect(element).to have_link(confluence_link) | ||||
|       else | ||||
|         expect(element).not_to have_link(confluence_link) | ||||
|       end | ||||
| 
 | ||||
|       if writable | ||||
|         element.click_link 'Create your first page' | ||||
| 
 | ||||
|         expect(page).to have_button('Create page') | ||||
|       else | ||||
|         expect(element).not_to have_link('Create your first page') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
|  | @ -1,14 +1,13 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| # Requires a context containing: | ||||
| #   wiki | ||||
| #   user | ||||
| 
 | ||||
| RSpec.describe 'User views a wiki page' do | ||||
| RSpec.shared_examples 'User views a wiki page' do | ||||
|   include WikiHelpers | ||||
| 
 | ||||
|   let(:user) { create(:user) } | ||||
|   let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } | ||||
|   let(:path) { 'image.png' } | ||||
|   let(:wiki) { project.wiki } | ||||
|   let(:wiki_page) do | ||||
|     create(:wiki_page, | ||||
|            wiki: wiki, | ||||
|  | @ -16,13 +15,12 @@ RSpec.describe 'User views a wiki page' do | |||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     project.add_maintainer(user) | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   context 'when wiki is empty', :js do | ||||
|     before do | ||||
|       visit project_wikis_path(project) | ||||
|       visit wiki_path(wiki) | ||||
| 
 | ||||
|       wait_for_svg_to_be_loaded | ||||
| 
 | ||||
|  | @ -83,7 +81,7 @@ RSpec.describe 'User views a wiki page' do | |||
| 
 | ||||
|   context 'when a page does not have history' do | ||||
|     before do | ||||
|       visit(project_wiki_path(project, wiki_page)) | ||||
|       visit(wiki_page_path(wiki, wiki_page)) | ||||
|     end | ||||
| 
 | ||||
|     it 'shows all the pages' do | ||||
|  | @ -92,7 +90,7 @@ RSpec.describe 'User views a wiki page' do | |||
|     end | ||||
| 
 | ||||
|     context 'shows a file stored in a page' do | ||||
|       let(:path) { upload_file_to_wiki(project, user, 'dk.png') } | ||||
|       let(:path) { upload_file_to_wiki(wiki, user, 'dk.png') } | ||||
| 
 | ||||
|       it do | ||||
|         expect(page).to have_xpath("//img[@data-src='#{wiki.wiki_base_path}/#{path}']") | ||||
|  | @ -121,7 +119,7 @@ RSpec.describe 'User views a wiki page' do | |||
|     end | ||||
| 
 | ||||
|     it 'shows the page history' do | ||||
|       visit(project_wiki_path(project, wiki_page)) | ||||
|       visit(wiki_page_path(wiki, wiki_page)) | ||||
| 
 | ||||
|       expect(page).to have_selector('a.btn', text: 'Edit') | ||||
| 
 | ||||
|  | @ -133,12 +131,16 @@ RSpec.describe 'User views a wiki page' do | |||
|     end | ||||
| 
 | ||||
|     it 'does not show the "Edit" button' do | ||||
|       visit(project_wiki_path(project, wiki_page, version_id: wiki_page.versions.last.id)) | ||||
|       visit(wiki_page_path(wiki, wiki_page, version_id: wiki_page.versions.last.id)) | ||||
| 
 | ||||
|       expect(page).not_to have_selector('a.btn', text: 'Edit') | ||||
|     end | ||||
| 
 | ||||
|     context 'show the diff' do | ||||
|       before do | ||||
|         skip('Diffing for group wikis will be implemented in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42610') if wiki.container.is_a?(Group) | ||||
|       end | ||||
| 
 | ||||
|       def expect_diff_links(commit) | ||||
|         diff_path = wiki_page_path(wiki, wiki_page, version_id: commit, action: :diff) | ||||
| 
 | ||||
|  | @ -150,7 +152,7 @@ RSpec.describe 'User views a wiki page' do | |||
|       end | ||||
| 
 | ||||
|       it 'links to the correct diffs' do | ||||
|         visit project_wiki_history_path(project, wiki_page) | ||||
|         visit wiki_page_path(wiki, wiki_page, action: :history) | ||||
| 
 | ||||
|         commit1 = wiki.commit('HEAD^') | ||||
|         commit2 = wiki.commit | ||||
|  | @ -208,7 +210,7 @@ RSpec.describe 'User views a wiki page' do | |||
|     end | ||||
| 
 | ||||
|     it 'preserves the special characters' do | ||||
|       visit(project_wiki_path(project, wiki_page)) | ||||
|       visit(wiki_page_path(wiki, wiki_page)) | ||||
| 
 | ||||
|       expect(page).to have_css('.wiki-page-title', text: title) | ||||
|       expect(page).to have_css('.wiki-pages li', text: title) | ||||
|  | @ -223,7 +225,7 @@ RSpec.describe 'User views a wiki page' do | |||
|     end | ||||
| 
 | ||||
|     it 'safely displays the page' do | ||||
|       visit(project_wiki_path(project, wiki_page)) | ||||
|       visit(wiki_page_path(wiki, wiki_page)) | ||||
| 
 | ||||
|       expect(page).to have_css('.wiki-page-title', text: title) | ||||
|       expect(page).to have_content('foo bar') | ||||
|  | @ -236,7 +238,7 @@ RSpec.describe 'User views a wiki page' do | |||
|     end | ||||
| 
 | ||||
|     it 'safely displays the message' do | ||||
|       visit(project_wiki_history_path(project, wiki_page)) | ||||
|       visit(wiki_page_path(wiki, wiki_page, action: :history)) | ||||
| 
 | ||||
|       expect(page).to have_content('<script>alert(true)<script>') | ||||
|     end | ||||
|  | @ -248,7 +250,7 @@ RSpec.describe 'User views a wiki page' do | |||
|     before do | ||||
|       allow(Gitlab::EncodingHelper).to receive(:encode!).and_return(content) | ||||
| 
 | ||||
|       visit(project_wiki_path(project, wiki_page)) | ||||
|       visit(wiki_page_path(wiki, wiki_page)) | ||||
|     end | ||||
| 
 | ||||
|     it 'does not show "Edit" button' do | ||||
|  | @ -263,7 +265,7 @@ RSpec.describe 'User views a wiki page' do | |||
|   end | ||||
| 
 | ||||
|   it 'opens a default wiki page', :js do | ||||
|     visit project_path(project) | ||||
|     visit wiki.container.web_url | ||||
| 
 | ||||
|     find('.shortcuts-wiki').click | ||||
| 
 | ||||
|  | @ -1,23 +1,22 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| require 'spec_helper' | ||||
| # Requires a context containing: | ||||
| #   wiki | ||||
| #   user | ||||
| 
 | ||||
| RSpec.describe 'User views wiki pages' do | ||||
| RSpec.shared_examples 'User views wiki pages' do | ||||
|   include WikiHelpers | ||||
| 
 | ||||
|   let(:user) { create(:user) } | ||||
|   let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } | ||||
| 
 | ||||
|   let!(:wiki_page1) do | ||||
|     create(:wiki_page, wiki: project.wiki, title: '3 home', content: '3') | ||||
|     create(:wiki_page, wiki: wiki, title: '3 home', content: '3') | ||||
|   end | ||||
| 
 | ||||
|   let!(:wiki_page2) do | ||||
|     create(:wiki_page, wiki: project.wiki, title: '1 home', content: '1') | ||||
|     create(:wiki_page, wiki: wiki, title: '1 home', content: '1') | ||||
|   end | ||||
| 
 | ||||
|   let!(:wiki_page3) do | ||||
|     create(:wiki_page, wiki: project.wiki, title: '2 home', content: '2') | ||||
|     create(:wiki_page, wiki: wiki, title: '2 home', content: '2') | ||||
|   end | ||||
| 
 | ||||
|   let(:pages) do | ||||
|  | @ -25,9 +24,8 @@ RSpec.describe 'User views wiki pages' do | |||
|   end | ||||
| 
 | ||||
|   before do | ||||
|     project.add_maintainer(user) | ||||
|     sign_in(user) | ||||
|     visit(project_wikis_pages_path(project)) | ||||
|     visit(wiki_path(wiki, action: :pages)) | ||||
|   end | ||||
| 
 | ||||
|   context 'ordered by title' do | ||||
|  | @ -0,0 +1,68 @@ | |||
| # frozen_string_literal: true | ||||
| 
 | ||||
| # Requires a context containing: | ||||
| #   wiki | ||||
| #   user | ||||
| 
 | ||||
| RSpec.shared_examples 'User views wiki sidebar' do | ||||
|   include WikiHelpers | ||||
| 
 | ||||
|   before do | ||||
|     sign_in(user) | ||||
|   end | ||||
| 
 | ||||
|   context 'when there are some existing pages' do | ||||
|     before do | ||||
|       create(:wiki_page, wiki: wiki, title: 'home', content: 'home') | ||||
|       create(:wiki_page, wiki: wiki, title: 'another', content: 'another') | ||||
|     end | ||||
| 
 | ||||
|     it 'renders a default sidebar when there is no customized sidebar' do | ||||
|       visit wiki_path(wiki) | ||||
| 
 | ||||
|       expect(page).to have_content('another') | ||||
|       expect(page).not_to have_link('View All Pages') | ||||
|     end | ||||
| 
 | ||||
|     context 'when there is a customized sidebar' do | ||||
|       before do | ||||
|         create(:wiki_page, wiki: wiki, title: '_sidebar', content: 'My customized sidebar') | ||||
|       end | ||||
| 
 | ||||
|       it 'renders my customized sidebar instead of the default one' do | ||||
|         visit wiki_path(wiki) | ||||
| 
 | ||||
|         expect(page).to have_content('My customized sidebar') | ||||
|         expect(page).not_to have_content('Another') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| 
 | ||||
|   context 'when there are 15 existing pages' do | ||||
|     before do | ||||
|       (1..5).each { |i| create(:wiki_page, wiki: wiki, title: "my page #{i}") } | ||||
|       (6..10).each { |i| create(:wiki_page, wiki: wiki, title: "parent/my page #{i}") } | ||||
|       (11..15).each { |i| create(:wiki_page, wiki: wiki, title: "grandparent/parent/my page #{i}") } | ||||
|     end | ||||
| 
 | ||||
|     it 'shows all pages in the sidebar' do | ||||
|       visit wiki_path(wiki) | ||||
| 
 | ||||
|       (1..15).each { |i| expect(page).to have_content("my page #{i}") } | ||||
|       expect(page).not_to have_link('View All Pages') | ||||
|     end | ||||
| 
 | ||||
|     context 'when there are more than 15 existing pages' do | ||||
|       before do | ||||
|         create(:wiki_page, wiki: wiki, title: 'my page 16') | ||||
|       end | ||||
| 
 | ||||
|       it 'shows the first 15 pages in the sidebar' do | ||||
|         visit wiki_path(wiki) | ||||
| 
 | ||||
|         expect(page).to have_text('my page', count: 15) | ||||
|         expect(page).to have_link('View All Pages') | ||||
|       end | ||||
|     end | ||||
|   end | ||||
| end | ||||
		Loading…
	
		Reference in New Issue