Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
157d5832
Commit
157d5832
authored
Mar 03, 2021
by
Peter Hegman
Committed by
Natalia Tepluhina
Mar 03, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add project selector to personal access token form
Allows users to scope a PAT to a project
parent
84bc3082
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
551 additions
and
29 deletions
+551
-29
app/assets/javascripts/access_tokens/components/projects_field.vue
...s/javascripts/access_tokens/components/projects_field.vue
+20
-2
app/assets/javascripts/access_tokens/components/projects_token_selector.vue
...ipts/access_tokens/components/projects_token_selector.vue
+141
-0
app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
.../access_tokens/graphql/queries/get_projects.query.graphql
+22
-0
app/assets/javascripts/access_tokens/index.js
app/assets/javascripts/access_tokens/index.js
+40
-9
locale/gitlab.pot
locale/gitlab.pot
+3
-0
spec/frontend/access_tokens/components/projects_field_spec.js
.../frontend/access_tokens/components/projects_field_spec.js
+50
-4
spec/frontend/access_tokens/components/projects_token_selector_spec.js
.../access_tokens/components/projects_token_selector_spec.js
+224
-0
spec/frontend/access_tokens/index_spec.js
spec/frontend/access_tokens/index_spec.js
+21
-11
spec/frontend/fixtures/projects.rb
spec/frontend/fixtures/projects.rb
+30
-3
No files found.
app/assets/javascripts/access_tokens/components/projects_field.vue
View file @
157d5832
<
script
>
import
{
GlFormGroup
,
GlFormRadio
,
GlFormText
}
from
'
@gitlab/ui
'
;
import
ProjectsTokenSelector
from
'
./projects_token_selector.vue
'
;
export
default
{
name
:
'
ProjectsField
'
,
ALL_PROJECTS
:
'
ALL_PROJECTS
'
,
SELECTED_PROJECTS
:
'
SELECTED_PROJECTS
'
,
components
:
{
GlFormGroup
,
GlFormRadio
,
GlFormText
},
components
:
{
GlFormGroup
,
GlFormRadio
,
GlFormText
,
ProjectsTokenSelector
},
props
:
{
inputAttrs
:
{
type
:
Object
,
...
...
@@ -15,8 +16,24 @@ export default {
data
()
{
return
{
selectedRadio
:
this
.
$options
.
ALL_PROJECTS
,
selectedProjects
:
[],
};
},
computed
:
{
allProjectsRadioSelected
()
{
return
this
.
selectedRadio
===
this
.
$options
.
ALL_PROJECTS
;
},
hiddenInputValue
()
{
return
this
.
allProjectsRadioSelected
?
null
:
this
.
selectedProjects
.
map
((
project
)
=>
project
.
id
).
join
(
'
,
'
);
},
},
methods
:
{
handleTokenSelectorFocus
()
{
this
.
selectedRadio
=
this
.
$options
.
SELECTED_PROJECTS
;
},
},
};
</
script
>
...
...
@@ -32,7 +49,8 @@ export default {
<gl-form-radio
v-model=
"selectedRadio"
:value=
"$options.SELECTED_PROJECTS"
>
{{
__
(
'
Selected projects
'
)
}}
</gl-form-radio>
<input
:id=
"inputAttrs.id"
type=
"hidden"
:name=
"inputAttrs.name"
/>
<input
:id=
"inputAttrs.id"
type=
"hidden"
:name=
"inputAttrs.name"
:value=
"hiddenInputValue"
/>
<projects-token-selector
v-model=
"selectedProjects"
@
focus=
"handleTokenSelectorFocus"
/>
</gl-form-group>
</div>
</
template
>
app/assets/javascripts/access_tokens/components/projects_token_selector.vue
0 → 100644
View file @
157d5832
<
script
>
import
{
GlTokenSelector
,
GlAvatar
,
GlAvatarLabeled
,
GlIntersectionObserver
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
produce
from
'
immer
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
getProjectsQuery
from
'
../graphql/queries/get_projects.query.graphql
'
;
const
DEBOUNCE_DELAY
=
250
;
const
PROJECTS_PER_PAGE
=
20
;
export
default
{
name
:
'
ProjectsTokenSelector
'
,
components
:
{
GlTokenSelector
,
GlAvatar
,
GlAvatarLabeled
,
GlIntersectionObserver
,
GlLoadingIcon
,
},
model
:
{
prop
:
'
selectedProjects
'
,
},
props
:
{
selectedProjects
:
{
type
:
Array
,
required
:
true
,
},
},
apollo
:
{
projects
:
{
query
:
getProjectsQuery
,
debounce
:
DEBOUNCE_DELAY
,
variables
()
{
return
{
search
:
this
.
searchQuery
,
after
:
null
,
first
:
PROJECTS_PER_PAGE
,
};
},
update
({
projects
})
{
return
{
list
:
projects
.
nodes
.
map
((
project
)
=>
({
...
project
,
id
:
getIdFromGraphQLId
(
project
.
id
),
})),
pageInfo
:
projects
.
pageInfo
,
};
},
result
()
{
this
.
isLoadingMoreProjects
=
false
;
this
.
isSearching
=
false
;
},
},
},
data
()
{
return
{
projects
:
{
list
:
[],
pageInfo
:
{},
},
searchQuery
:
''
,
isLoadingMoreProjects
:
false
,
isSearching
:
false
,
};
},
methods
:
{
handleSearch
(
query
)
{
this
.
isSearching
=
true
;
this
.
searchQuery
=
query
;
},
loadMoreProjects
()
{
this
.
isLoadingMoreProjects
=
true
;
this
.
$apollo
.
queries
.
projects
.
fetchMore
({
variables
:
{
after
:
this
.
projects
.
pageInfo
.
endCursor
,
first
:
PROJECTS_PER_PAGE
,
},
updateQuery
(
previousResult
,
{
fetchMoreResult
:
{
projects
:
newProjects
}
})
{
const
{
projects
:
previousProjects
}
=
previousResult
;
return
produce
(
previousResult
,
(
draftData
)
=>
{
/* eslint-disable no-param-reassign */
draftData
.
projects
.
nodes
=
[...
previousProjects
.
nodes
,
...
newProjects
.
nodes
];
draftData
.
projects
.
pageInfo
=
newProjects
.
pageInfo
;
/* eslint-enable no-param-reassign */
});
},
});
},
},
};
</
script
>
<
template
>
<div
class=
"gl-relative"
>
<gl-token-selector
:selected-tokens=
"selectedProjects"
:dropdown-items=
"projects.list"
:loading=
"isSearching"
:placeholder=
"__('Select projects')"
menu-class=
"gl-w-full! gl-max-w-full!"
@
input=
"$emit('input', $event)"
@
focus=
"$emit('focus', $event)"
@
text-input=
"handleSearch"
@
keydown
.
enter
.
prevent
>
<template
#token-content
="
{ token: project }">
<gl-avatar
:entity-id=
"project.id"
:entity-name=
"project.name"
:src=
"project.avatarUrl"
:size=
"16"
/>
{{
project
.
nameWithNamespace
}}
</
template
>
<
template
#dropdown-item-content=
"{ dropdownItem: project }"
>
<gl-avatar-labeled
:entity-id=
"project.id"
:entity-name=
"project.name"
:size=
"32"
:src=
"project.avatarUrl"
:label=
"project.name"
:sub-label=
"project.nameWithNamespace"
/>
</
template
>
<
template
#dropdown-footer
>
<gl-intersection-observer
v-if=
"projects.pageInfo.hasNextPage"
@
appear=
"loadMoreProjects"
>
<gl-loading-icon
v-if=
"isLoadingMoreProjects"
size=
"md"
/>
</gl-intersection-observer>
</
template
>
</gl-token-selector>
</div>
</template>
app/assets/javascripts/access_tokens/graphql/queries/get_projects.query.graphql
0 → 100644
View file @
157d5832
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query
getProjects
(
$search
:
String
!,
$after
:
String
=
""
,
$first
:
Int
!)
{
projects
(
search
:
$search
after
:
$after
first
:
$first
membership
:
true
searchNamespaces
:
true
sort
:
"UPDATED_ASC"
)
{
nodes
{
id
name
nameWithNamespace
avatarUrl
}
pageInfo
{
...
PageInfo
}
}
}
app/assets/javascripts/access_tokens/index.js
View file @
157d5832
import
Vue
from
'
vue
'
;
import
createFlash
from
'
~/flash
'
;
import
{
__
}
from
'
~/locale
'
;
import
ExpiresAtField
from
'
./components/expires_at_field.vue
'
;
...
...
@@ -43,10 +45,28 @@ export const initProjectsField = () => {
const
inputAttrs
=
getInputAttrs
(
el
);
if
(
window
.
gon
.
features
.
personalAccessTokensScopedToProjects
)
{
const
ProjectsField
=
()
=>
import
(
'
./components/projects_field.vue
'
);
return
new
Promise
((
resolve
)
=>
{
Promise
.
all
([
import
(
'
./components/projects_field.vue
'
),
import
(
'
vue-apollo
'
),
import
(
'
~/lib/graphql
'
),
])
.
then
(
([
{
default
:
ProjectsField
},
{
default
:
VueApollo
},
{
default
:
createDefaultClient
},
])
=>
{
const
apolloProvider
=
new
VueApollo
({
defaultClient
:
createDefaultClient
(),
});
return
new
Vue
({
Vue
.
use
(
VueApollo
);
resolve
(
new
Vue
({
el
,
apolloProvider
,
render
(
h
)
{
return
h
(
ProjectsField
,
{
props
:
{
...
...
@@ -54,6 +74,17 @@ export const initProjectsField = () => {
},
});
},
}),
);
},
)
.
catch
(()
=>
{
createFlash
({
message
:
__
(
'
An error occurred while loading the access tokens form, please try again.
'
,
),
});
});
});
}
...
...
locale/gitlab.pot
View file @
157d5832
...
...
@@ -3467,6 +3467,9 @@ msgstr ""
msgid "An error occurred while loading project creation UI"
msgstr ""
msgid "An error occurred while loading the access tokens form, please try again."
msgstr ""
msgid "An error occurred while loading the data. Please try again."
msgstr ""
...
...
spec/frontend/access_tokens/components/projects_field_spec.js
View file @
157d5832
import
{
within
}
from
'
@testing-library/dom
'
;
import
{
within
,
fireEvent
}
from
'
@testing-library/dom
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
ProjectsField
from
'
~/access_tokens/components/projects_field.vue
'
;
import
ProjectsTokenSelector
from
'
~/access_tokens/components/projects_token_selector.vue
'
;
describe
(
'
ProjectsField
'
,
()
=>
{
let
wrapper
;
...
...
@@ -18,6 +19,10 @@ describe('ProjectsField', () => {
const
queryByLabelText
=
(
text
)
=>
within
(
wrapper
.
element
).
queryByLabelText
(
text
);
const
queryByText
=
(
text
)
=>
within
(
wrapper
.
element
).
queryByText
(
text
);
const
findAllProjectsRadio
=
()
=>
queryByLabelText
(
'
All projects
'
);
const
findSelectedProjectsRadio
=
()
=>
queryByLabelText
(
'
Selected projects
'
);
const
findProjectsTokenSelector
=
()
=>
wrapper
.
findComponent
(
ProjectsTokenSelector
);
const
findHiddenInput
=
()
=>
wrapper
.
find
(
'
input[type="hidden"]
'
);
beforeEach
(()
=>
{
createComponent
();
...
...
@@ -34,25 +39,66 @@ describe('ProjectsField', () => {
});
it
(
'
renders "All projects" radio selected by default
'
,
()
=>
{
const
allProjectsRadio
=
queryByLabelText
(
'
All projects
'
);
const
allProjectsRadio
=
findAllProjectsRadio
(
);
expect
(
allProjectsRadio
).
not
.
toBe
(
null
);
expect
(
allProjectsRadio
.
checked
).
toBe
(
true
);
});
it
(
'
renders "Selected projects" radio unchecked by default
'
,
()
=>
{
const
selectedProjectsRadio
=
queryByLabelText
(
'
Selected projects
'
);
const
selectedProjectsRadio
=
findSelectedProjectsRadio
(
);
expect
(
selectedProjectsRadio
).
not
.
toBe
(
null
);
expect
(
selectedProjectsRadio
.
checked
).
toBe
(
false
);
});
it
(
'
renders `projects-token-selector` component
'
,
()
=>
{
expect
(
findProjectsTokenSelector
().
exists
()).
toBe
(
true
);
});
it
(
'
renders hidden input with correct `name` and `id` attributes
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
input[type="hidden"]
'
).
attributes
()).
toEqual
(
expect
(
findHiddenInput
(
).
attributes
()).
toEqual
(
expect
.
objectContaining
({
id
:
'
projects
'
,
name
:
'
projects
'
,
}),
);
});
describe
(
'
when `projects-token-selector` is focused
'
,
()
=>
{
beforeEach
(()
=>
{
findProjectsTokenSelector
().
vm
.
$emit
(
'
focus
'
);
});
it
(
'
auto selects the "Selected projects" radio
'
,
()
=>
{
expect
(
findSelectedProjectsRadio
().
checked
).
toBe
(
true
);
});
describe
(
'
when `projects-token-selector` is changed
'
,
()
=>
{
beforeEach
(()
=>
{
findProjectsTokenSelector
().
vm
.
$emit
(
'
input
'
,
[
{
id
:
1
,
},
{
id
:
2
,
},
]);
});
it
(
'
updates the hidden input value to a comma separated list of project IDs
'
,
()
=>
{
expect
(
findHiddenInput
().
attributes
(
'
value
'
)).
toBe
(
'
1,2
'
);
});
describe
(
'
when radio is changed back to "All projects"
'
,
()
=>
{
beforeEach
(()
=>
{
fireEvent
.
click
(
findAllProjectsRadio
());
});
it
(
'
removes the hidden input value
'
,
()
=>
{
expect
(
findHiddenInput
().
attributes
(
'
value
'
)).
toBe
(
''
);
});
});
});
});
});
spec/frontend/access_tokens/components/projects_token_selector_spec.js
0 → 100644
View file @
157d5832
import
{
GlAvatar
,
GlAvatarLabeled
,
GlIntersectionObserver
,
GlToken
,
GlTokenSelector
,
GlLoadingIcon
,
}
from
'
@gitlab/ui
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
produce
from
'
immer
'
;
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
{
getJSONFixture
}
from
'
helpers/fixtures
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
{
extendedWrapper
}
from
'
helpers/vue_test_utils_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
ProjectsTokenSelector
from
'
~/access_tokens/components/projects_token_selector.vue
'
;
import
getProjectsQuery
from
'
~/access_tokens/graphql/queries/get_projects.query.graphql
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
describe
(
'
ProjectsTokenSelector
'
,
()
=>
{
const
getProjectsQueryResponse
=
getJSONFixture
(
'
graphql/projects/access_tokens/get_projects.query.graphql.json
'
,
);
const
getProjectsQueryResponsePage2
=
produce
(
getProjectsQueryResponse
,
(
getProjectsQueryResponseDraft
)
=>
{
/* eslint-disable no-param-reassign */
getProjectsQueryResponseDraft
.
data
.
projects
.
pageInfo
.
hasNextPage
=
false
;
getProjectsQueryResponseDraft
.
data
.
projects
.
pageInfo
.
endCursor
=
null
;
getProjectsQueryResponseDraft
.
data
.
projects
.
nodes
.
splice
(
1
,
1
);
getProjectsQueryResponseDraft
.
data
.
projects
.
nodes
[
0
].
id
=
'
gid://gitlab/Project/100
'
;
/* eslint-enable no-param-reassign */
},
);
const
runDebounce
=
()
=>
jest
.
runAllTimers
();
const
{
pageInfo
,
nodes
:
projects
}
=
getProjectsQueryResponse
.
data
.
projects
;
const
project1
=
projects
[
0
];
const
project2
=
projects
[
1
];
let
wrapper
;
let
resolveGetProjectsQuery
;
const
getProjectsQueryRequestHandler
=
jest
.
fn
(
()
=>
new
Promise
((
resolve
)
=>
{
resolveGetProjectsQuery
=
resolve
;
}),
);
const
createComponent
=
({
propsData
=
{},
apolloProvider
=
createMockApollo
([[
getProjectsQuery
,
getProjectsQueryRequestHandler
]]),
resolveQueries
=
true
,
}
=
{})
=>
{
Vue
.
use
(
VueApollo
);
wrapper
=
extendedWrapper
(
mount
(
ProjectsTokenSelector
,
{
apolloProvider
,
propsData
:
{
selectedProjects
:
[],
...
propsData
,
},
stubs
:
[
'
gl-intersection-observer
'
],
}),
);
runDebounce
();
if
(
resolveQueries
)
{
resolveGetProjectsQuery
(
getProjectsQueryResponse
);
return
waitForPromises
();
}
return
Promise
.
resolve
();
};
const
findTokenSelector
=
()
=>
wrapper
.
findComponent
(
GlTokenSelector
);
const
findTokenSelectorInput
=
()
=>
findTokenSelector
().
find
(
'
input[type="text"]
'
);
const
findIntersectionObserver
=
()
=>
wrapper
.
findComponent
(
GlIntersectionObserver
);
it
(
'
renders dropdown items with project avatars
'
,
async
()
=>
{
await
createComponent
();
wrapper
.
findAllComponents
(
GlAvatarLabeled
).
wrappers
.
forEach
((
avatarLabeledWrapper
,
index
)
=>
{
const
project
=
projects
[
index
];
expect
(
avatarLabeledWrapper
.
attributes
()).
toEqual
(
expect
.
objectContaining
({
'
entity-id
'
:
`
${
getIdFromGraphQLId
(
project
.
id
)}
`
,
'
entity-name
'
:
project
.
name
,
...(
project
.
avatarUrl
&&
{
src
:
project
.
avatarUrl
}),
}),
);
expect
(
avatarLabeledWrapper
.
props
()).
toEqual
(
expect
.
objectContaining
({
label
:
project
.
name
,
subLabel
:
project
.
nameWithNamespace
,
}),
);
});
});
it
(
'
renders tokens with project avatars
'
,
()
=>
{
createComponent
({
propsData
:
{
selectedProjects
:
[{
...
project2
,
id
:
getIdFromGraphQLId
(
project2
.
id
)
}],
},
});
const
token
=
wrapper
.
findComponent
(
GlToken
);
const
avatar
=
token
.
findComponent
(
GlAvatar
);
expect
(
token
.
text
()).
toContain
(
project2
.
nameWithNamespace
);
expect
(
avatar
.
attributes
(
'
src
'
)).
toBe
(
project2
.
avatarUrl
);
expect
(
avatar
.
props
()).
toEqual
(
expect
.
objectContaining
({
entityId
:
getIdFromGraphQLId
(
project2
.
id
),
entityName
:
project2
.
name
,
}),
);
});
describe
(
'
when `enter` key is pressed
'
,
()
=>
{
it
(
'
calls `preventDefault` so form is not submitted when user selects a project from the dropdown
'
,
()
=>
{
createComponent
();
const
event
=
{
preventDefault
:
jest
.
fn
(),
};
findTokenSelectorInput
().
trigger
(
'
keydown.enter
'
,
event
);
expect
(
event
.
preventDefault
).
toHaveBeenCalled
();
});
});
describe
(
'
when text input is typed in
'
,
()
=>
{
const
searchTerm
=
'
foo bar
'
;
beforeEach
(
async
()
=>
{
await
createComponent
();
await
findTokenSelectorInput
().
setValue
(
searchTerm
);
runDebounce
();
});
it
(
'
makes GraphQL request with `search` variable set
'
,
async
()
=>
{
expect
(
getProjectsQueryRequestHandler
).
toHaveBeenLastCalledWith
({
search
:
searchTerm
,
after
:
null
,
first
:
20
,
});
});
it
(
'
sets loading state while waiting for GraphQL request to resolve
'
,
async
()
=>
{
expect
(
findTokenSelector
().
props
(
'
loading
'
)).
toBe
(
true
);
resolveGetProjectsQuery
(
getProjectsQueryResponse
);
await
waitForPromises
();
expect
(
findTokenSelector
().
props
(
'
loading
'
)).
toBe
(
false
);
});
});
describe
(
'
when there is a next page of projects and user scrolls to the bottom of the dropdown
'
,
()
=>
{
beforeEach
(
async
()
=>
{
await
createComponent
();
findIntersectionObserver
().
vm
.
$emit
(
'
appear
'
);
});
it
(
'
makes GraphQL request with `after` variable set
'
,
async
()
=>
{
expect
(
getProjectsQueryRequestHandler
).
toHaveBeenLastCalledWith
({
after
:
pageInfo
.
endCursor
,
first
:
20
,
search
:
''
,
});
});
it
(
'
displays loading icon while waiting for GraphQL request to resolve
'
,
async
()
=>
{
expect
(
wrapper
.
findComponent
(
GlLoadingIcon
).
exists
()).
toBe
(
true
);
resolveGetProjectsQuery
(
getProjectsQueryResponsePage2
);
await
waitForPromises
();
expect
(
wrapper
.
findComponent
(
GlLoadingIcon
).
exists
()).
toBe
(
false
);
});
});
describe
(
'
when there is not a next page of projects
'
,
()
=>
{
it
(
'
does not render `GlIntersectionObserver`
'
,
async
()
=>
{
createComponent
({
resolveQueries
:
false
});
resolveGetProjectsQuery
(
getProjectsQueryResponsePage2
);
await
waitForPromises
();
expect
(
findIntersectionObserver
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
when `GlTokenSelector` emits `input` event
'
,
()
=>
{
it
(
'
emits `input` event used by `v-model`
'
,
()
=>
{
findTokenSelector
().
vm
.
$emit
(
'
input
'
,
project1
);
expect
(
wrapper
.
emitted
(
'
input
'
)[
0
]).
toEqual
([
project1
]);
});
});
describe
(
'
when `GlTokenSelector` emits `focus` event
'
,
()
=>
{
it
(
'
emits `focus` event
'
,
()
=>
{
const
event
=
{
fakeEvent
:
'
foo
'
};
findTokenSelector
().
vm
.
$emit
(
'
focus
'
,
event
);
expect
(
wrapper
.
emitted
(
'
focus
'
)[
0
]).
toEqual
([
event
]);
});
});
});
spec/frontend/access_tokens/index_spec.js
View file @
157d5832
import
{
createWrapper
}
from
'
@vue/test-utils
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
Vue
from
'
vue
'
;
import
{
initExpiresAtField
,
initProjectsField
}
from
'
~/access_tokens
'
;
import
ExpiresAtField
from
'
~/access_tokens/components/expires_at_field.vue
'
;
import
ProjectsField
from
'
~/access_tokens/components/projects_field.vue
'
;
import
*
as
ExpiresAtField
from
'
~/access_tokens/components/expires_at_field.vue
'
;
import
*
as
ProjectsField
from
'
~/access_tokens/components/projects_field.vue
'
;
describe
(
'
access tokens
'
,
()
=>
{
const
FakeComponent
=
Vue
.
component
(
'
FakeComponent
'
,
{
props
:
{
inputAttrs
:
{
type
:
Object
,
required
:
true
,
},
},
render
:
()
=>
null
,
});
beforeEach
(()
=>
{
window
.
gon
=
{
features
:
{
personalAccessTokensScopedToProjects
:
true
}
};
});
afterEach
(()
=>
{
document
.
body
.
innerHTML
=
''
;
window
.
gon
=
{};
});
describe
.
each
`
...
...
@@ -34,15 +42,17 @@ describe('access tokens', () => {
mountEl
.
appendChild
(
input
);
document
.
body
.
appendChild
(
mountEl
);
});
it
(
`mounts component and sets \`inputAttrs\` prop`
,
async
()
=>
{
const
wrapper
=
createWrapper
(
initFunction
());
// Mock component so we don't have to deal with mocking Apollo
// eslint-disable-next-line no-param-reassign
expectedComponent
.
default
=
FakeComponent
;
});
// Wait for dynamic imports to resolve
await
waitForPromises
();
it
(
'
mounts component and sets `inputAttrs` prop
'
,
async
()
=>
{
const
vueInstance
=
await
initFunction
();
const
component
=
wrapper
.
findComponent
(
expectedComponent
);
const
wrapper
=
createWrapper
(
vueInstance
);
const
component
=
wrapper
.
findComponent
(
FakeComponent
);
expect
(
component
.
exists
()).
toBe
(
true
);
expect
(
component
.
props
(
'
inputAttrs
'
)).
toEqual
({
...
...
spec/frontend/fixtures/projects.rb
View file @
157d5832
...
...
@@ -3,13 +3,14 @@
require
'spec_helper'
RSpec
.
describe
'Projects (JavaScript fixtures)'
,
type: :controller
do
include
ApiHelpers
include
JavaScriptFixturesHelpers
runners_token
=
'runnerstoken:intabulasreferre'
let
(
:namespace
)
{
create
(
:namespace
,
name:
'frontend-fixtures'
)}
let
(
:project
)
{
create
(
:project
,
namespace:
namespace
,
path:
'builds-project'
,
runners_token:
runners_token
)
}
let
(
:project_with_repo
)
{
create
(
:project
,
:repository
,
description:
'Code and stuff'
)
}
let
(
:project
)
{
create
(
:project
,
namespace:
namespace
,
path:
'builds-project'
,
runners_token:
runners_token
,
avatar:
fixture_file_upload
(
'spec/fixtures/dk.png'
,
'image/png'
)
)
}
let
(
:project_with_repo
)
{
create
(
:project
,
:repository
,
description:
'Code and stuff'
,
avatar:
fixture_file_upload
(
'spec/fixtures/dk.png'
,
'image/png'
)
)
}
let
(
:project_variable_populated
)
{
create
(
:project
,
namespace:
namespace
,
path:
'builds-project2'
,
runners_token:
runners_token
)
}
let
(
:user
)
{
project
.
owner
}
...
...
@@ -22,7 +23,6 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
before
do
project_with_repo
.
add_maintainer
(
user
)
sign_in
(
user
)
allow
(
SecureRandom
).
to
receive
(
:hex
).
and_return
(
'securerandomhex:thereisnospoon'
)
end
after
do
...
...
@@ -48,4 +48,31 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
expect
(
response
).
to
be_successful
end
end
describe
GraphQL
::
Query
,
type: :request
do
include
GraphqlHelpers
context
'access token projects query'
do
before
do
project_variable_populated
.
add_maintainer
(
user
)
end
before
(
:all
)
do
clean_frontend_fixtures
(
'graphql/projects/access_tokens'
)
end
fragment_paths
=
[
'graphql_shared/fragments/pageInfo.fragment.graphql'
]
base_input_path
=
'access_tokens/graphql/queries/'
base_output_path
=
'graphql/projects/access_tokens/'
query_name
=
'get_projects.query.graphql'
it
"
#{
base_output_path
}#{
query_name
}
.json"
do
query
=
get_graphql_query_as_string
(
"
#{
base_input_path
}#{
query_name
}
"
,
fragment_paths
)
post_graphql
(
query
,
current_user:
user
,
variables:
{
search:
''
,
first:
2
})
expect_graphql_errors_to_be_empty
end
end
end
end
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment