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
341637eb
Commit
341637eb
authored
Dec 03, 2020
by
Nicolò Maria Mezzopera
Committed by
Natalia Tepluhina
Dec 03, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add graphql queries, mutations and fragments
- queries - mutations - fragments
parent
2f6b9388
Changes
19
Show whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
643 additions
and
236 deletions
+643
-236
app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
...pts/registry/explorer/components/list_page/image_list.vue
+15
-18
app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
...registry/explorer/components/list_page/image_list_row.vue
+24
-10
app/assets/javascripts/registry/explorer/constants/list.js
app/assets/javascripts/registry/explorer/constants/list.js
+3
-2
app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql
...r/graphql/fragments/container_repository.fragment.graphql
+11
-0
app/assets/javascripts/registry/explorer/graphql/index.js
app/assets/javascripts/registry/explorer/graphql/index.js
+14
-0
app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.graphql
...rer/graphql/mutations/delete_container_repository.graphql
+9
-0
app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.graphql
.../graphql/queries/get_group_container_repositories.graphql
+23
-0
app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.graphql
...raphql/queries/get_project_container_repositories.graphql
+23
-0
app/assets/javascripts/registry/explorer/index.js
app/assets/javascripts/registry/explorer/index.js
+2
-0
app/assets/javascripts/registry/explorer/pages/list.vue
app/assets/javascripts/registry/explorer/pages/list.vue
+107
-27
app/views/groups/registry/repositories/index.html.haml
app/views/groups/registry/repositories/index.html.haml
+1
-0
app/views/projects/registry/repositories/index.html.haml
app/views/projects/registry/repositories/index.html.haml
+1
-1
changelogs/unreleased/276432-refactor-container-registry-frontend-to-graphql.yml
...76432-refactor-container-registry-frontend-to-graphql.yml
+5
-0
spec/features/groups/container_registry_spec.rb
spec/features/groups/container_registry_spec.rb
+0
-7
spec/features/projects/container_registry_spec.rb
spec/features/projects/container_registry_spec.rb
+13
-6
spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
...stry/explorer/components/list_page/image_list_row_spec.js
+37
-24
spec/frontend/registry/explorer/components/list_page/image_list_spec.js
...registry/explorer/components/list_page/image_list_spec.js
+41
-19
spec/frontend/registry/explorer/mock_data.js
spec/frontend/registry/explorer/mock_data.js
+105
-21
spec/frontend/registry/explorer/pages/list_spec.js
spec/frontend/registry/explorer/pages/list_spec.js
+209
-101
No files found.
app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
View file @
341637eb
<
script
>
import
{
GlPagination
}
from
'
@gitlab/ui
'
;
import
{
Gl
Keyset
Pagination
}
from
'
@gitlab/ui
'
;
import
ImageListRow
from
'
./image_list_row.vue
'
;
export
default
{
name
:
'
ImageList
'
,
components
:
{
GlPagination
,
Gl
Keyset
Pagination
,
ImageListRow
,
},
props
:
{
...
...
@@ -13,19 +13,14 @@ export default {
type
:
Array
,
required
:
true
,
},
pag
ination
:
{
pag
eInfo
:
{
type
:
Object
,
required
:
true
,
},
},
computed
:
{
currentPage
:
{
get
()
{
return
this
.
pagination
.
page
;
},
set
(
page
)
{
this
.
$emit
(
'
pageChange
'
,
page
);
},
showPagination
()
{
return
this
.
pageInfo
.
hasPreviousPage
||
this
.
pageInfo
.
hasNextPage
;
},
},
};
...
...
@@ -40,13 +35,15 @@ export default {
:first=
"index === 0"
@
delete=
"$emit('delete', $event)"
/>
<gl-pagination
v-model=
"currentPage"
:per-page=
"pagination.perPage"
:total-items=
"pagination.total"
align=
"center"
class=
"w-100 gl-mt-3"
<div
class=
"gl-display-flex gl-justify-content-center"
>
<gl-keyset-pagination
v-if=
"showPagination"
:has-next-page=
"pageInfo.hasNextPage"
:has-previous-page=
"pageInfo.hasPreviousPage"
class=
"gl-mt-3"
@
prev=
"$emit('prev-page')"
@
next=
"$emit('next-page')"
/>
</div>
</div>
</
template
>
app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
View file @
341637eb
<
script
>
import
{
GlTooltipDirective
,
GlIcon
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
n__
}
from
'
~/locale
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
ListItem
from
'
~/vue_shared/components/registry/list_item.vue
'
;
import
DeleteButton
from
'
../delete_button.vue
'
;
...
...
@@ -11,6 +13,8 @@ import {
REMOVE_REPOSITORY_LABEL
,
ROW_SCHEDULED_FOR_DELETION
,
CLEANUP_TIMED_OUT_ERROR_MESSAGE
,
IMAGE_DELETE_SCHEDULED_STATUS
,
IMAGE_FAILED_DELETED_STATUS
,
}
from
'
../../constants/index
'
;
export
default
{
...
...
@@ -38,19 +42,29 @@ export default {
},
computed
:
{
disabledDelete
()
{
return
!
this
.
item
.
destroy_path
||
this
.
item
.
deleting
;
return
!
this
.
item
.
canDelete
||
this
.
deleting
;
},
id
()
{
return
getIdFromGraphQLId
(
this
.
item
.
id
);
},
deleting
()
{
return
this
.
item
.
status
===
IMAGE_DELETE_SCHEDULED_STATUS
;
},
failedDelete
()
{
return
this
.
item
.
status
===
IMAGE_FAILED_DELETED_STATUS
;
},
tagsCountText
()
{
return
n__
(
'
ContainerRegistry|%{count} Tag
'
,
'
ContainerRegistry|%{count} Tags
'
,
this
.
item
.
tags
_c
ount
,
this
.
item
.
tags
C
ount
,
);
},
warningIconText
()
{
if
(
this
.
item
.
failedDelete
)
{
if
(
this
.
failedDelete
)
{
return
ASYNC_DELETE_IMAGE_ERROR_MESSAGE
;
}
else
if
(
this
.
item
.
cleanup_policy_started_at
)
{
}
if
(
this
.
item
.
expirationPolicyStartedAt
)
{
return
CLEANUP_TIMED_OUT_ERROR_MESSAGE
;
}
return
null
;
...
...
@@ -63,23 +77,23 @@ export default {
<list-item
v-gl-tooltip=
"
{
placement: 'left',
disabled: !
item.
deleting,
disabled: !deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
v-bind="$attrs"
:disabled="
item.
deleting"
:disabled="deleting"
>
<template
#left-primary
>
<router-link
class=
"gl-text-body gl-font-weight-bold"
data-testid=
"details-link"
:to=
"
{ name: 'details', params: { id
: item.id
} }"
:to=
"
{ name: 'details', params: { id } }"
>
{{
item
.
path
}}
</router-link>
<clipboard-button
v-if=
"item.location"
:disabled=
"
item.
deleting"
:disabled=
"deleting"
:text=
"item.location"
:title=
"item.location"
category=
"tertiary"
...
...
@@ -97,7 +111,7 @@ export default {
<gl-icon
name=
"tag"
class=
"gl-mr-2"
/>
<gl-sprintf
:message=
"tagsCountText"
>
<template
#count
>
{{
item
.
tags
_c
ount
}}
{{
item
.
tags
C
ount
}}
</
template
>
</gl-sprintf>
</span>
...
...
@@ -106,7 +120,7 @@ export default {
<delete-button
:title=
"$options.i18n.REMOVE_REPOSITORY_LABEL"
:disabled=
"disabledDelete"
:tooltip-disabled=
"
Boolean(item.destroy_path)
"
:tooltip-disabled=
"
item.canDelete
"
:tooltip-title=
"$options.i18n.LIST_DELETE_BUTTON_DISABLED"
@
delete=
"$emit('delete', item)"
/>
...
...
app/assets/javascripts/registry/explorer/constants/list.js
View file @
341637eb
...
...
@@ -44,5 +44,6 @@ export const EMPTY_RESULT_MESSAGE = s__(
// Parameters
export
const
IMAGE_DELETE_SCHEDULED_STATUS
=
'
delete_scheduled
'
;
export
const
IMAGE_FAILED_DELETED_STATUS
=
'
delete_failed
'
;
export
const
IMAGE_DELETE_SCHEDULED_STATUS
=
'
DELETE_SCHEDULED
'
;
export
const
IMAGE_FAILED_DELETED_STATUS
=
'
DELETE_FAILED
'
;
export
const
GRAPHQL_PAGE_SIZE
=
10
;
app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql
0 → 100644
View file @
341637eb
fragment
ContainerRepositoryFields
on
ContainerRepository
{
id
name
path
status
location
canDelete
createdAt
tagsCount
expirationPolicyStartedAt
}
app/assets/javascripts/registry/explorer/graphql/index.js
0 → 100644
View file @
341637eb
import
Vue
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
createDefaultClient
from
'
~/lib/graphql
'
;
Vue
.
use
(
VueApollo
);
export
const
apolloProvider
=
new
VueApollo
({
defaultClient
:
createDefaultClient
(
{},
{
assumeImmutableResults
:
true
,
},
),
});
app/assets/javascripts/registry/explorer/graphql/mutations/delete_container_repository.graphql
0 → 100644
View file @
341637eb
mutation
destroyContainerRepository
(
$id
:
ContainerRepositoryID
!)
{
destroyContainerRepository
(
input
:
{
id
:
$id
})
{
containerRepository
{
id
status
}
errors
}
}
app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.graphql
0 → 100644
View file @
341637eb
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/container_repository.fragment.graphql"
query
getProjectContainerRepositories
(
$fullPath
:
ID
!
$name
:
String
$first
:
Int
$last
:
Int
$after
:
String
$before
:
String
)
{
group
(
fullPath
:
$fullPath
)
{
containerRepositoriesCount
containerRepositories
(
name
:
$name
,
after
:
$after
,
before
:
$before
,
first
:
$first
,
last
:
$last
)
{
nodes
{
...
ContainerRepositoryFields
}
pageInfo
{
...
PageInfo
}
}
}
}
app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.graphql
0 → 100644
View file @
341637eb
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
#import "../fragments/container_repository.fragment.graphql"
query
getProjectContainerRepositories
(
$fullPath
:
ID
!
$name
:
String
$first
:
Int
$last
:
Int
$after
:
String
$before
:
String
)
{
project
(
fullPath
:
$fullPath
)
{
containerRepositoriesCount
containerRepositories
(
name
:
$name
,
after
:
$after
,
before
:
$before
,
first
:
$first
,
last
:
$last
)
{
nodes
{
...
ContainerRepositoryFields
}
pageInfo
{
...
PageInfo
}
}
}
}
app/assets/javascripts/registry/explorer/index.js
View file @
341637eb
...
...
@@ -5,6 +5,7 @@ import RegistryExplorer from './pages/index.vue';
import
RegistryBreadcrumb
from
'
./components/registry_breadcrumb.vue
'
;
import
{
createStore
}
from
'
./stores
'
;
import
createRouter
from
'
./router
'
;
import
{
apolloProvider
}
from
'
./graphql/index
'
;
Vue
.
use
(
Translate
);
Vue
.
use
(
GlToast
);
...
...
@@ -27,6 +28,7 @@ export default () => {
el
,
store
,
router
,
apolloProvider
,
components
:
{
RegistryExplorer
,
},
...
...
app/assets/javascripts/registry/explorer/pages/list.vue
View file @
341637eb
<
script
>
import
{
mapState
,
mapActions
}
from
'
vuex
'
;
import
{
mapState
}
from
'
vuex
'
;
import
{
GlEmptyState
,
GlTooltipDirective
,
...
...
@@ -11,6 +11,7 @@ import {
GlSearchBoxByClick
,
}
from
'
@gitlab/ui
'
;
import
Tracking
from
'
~/tracking
'
;
import
createFlash
from
'
~/flash
'
;
import
ProjectEmptyState
from
'
../components/list_page/project_empty_state.vue
'
;
import
GroupEmptyState
from
'
../components/list_page/group_empty_state.vue
'
;
...
...
@@ -18,6 +19,10 @@ import RegistryHeader from '../components/list_page/registry_header.vue';
import
ImageList
from
'
../components/list_page/image_list.vue
'
;
import
CliCommands
from
'
../components/list_page/cli_commands.vue
'
;
import
getProjectContainerRepositories
from
'
../graphql/queries/get_project_container_repositories.graphql
'
;
import
getGroupContainerRepositories
from
'
../graphql/queries/get_group_container_repositories.graphql
'
;
import
deleteContainerRepository
from
'
../graphql/mutations/delete_container_repository.graphql
'
;
import
{
DELETE_IMAGE_SUCCESS_MESSAGE
,
DELETE_IMAGE_ERROR_MESSAGE
,
...
...
@@ -29,6 +34,8 @@ import {
IMAGE_REPOSITORY_LIST_LABEL
,
EMPTY_RESULT_TITLE
,
EMPTY_RESULT_MESSAGE
,
GRAPHQL_PAGE_SIZE
,
FETCH_IMAGES_LIST_ERROR_MESSAGE
,
}
from
'
../constants/index
'
;
export
default
{
...
...
@@ -66,21 +73,63 @@ export default {
EMPTY_RESULT_TITLE
,
EMPTY_RESULT_MESSAGE
,
},
apollo
:
{
images
:
{
query
()
{
return
this
.
graphQlQuery
;
},
variables
()
{
return
this
.
queryVariables
;
},
update
(
data
)
{
return
data
[
this
.
graphqlResource
]?.
containerRepositories
.
nodes
;
},
result
({
data
})
{
this
.
pageInfo
=
data
[
this
.
graphqlResource
]?.
containerRepositories
?.
pageInfo
;
this
.
containerRepositoriesCount
=
data
[
this
.
graphqlResource
]?.
containerRepositoriesCount
;
},
error
()
{
createFlash
({
message
:
FETCH_IMAGES_LIST_ERROR_MESSAGE
});
},
},
},
data
()
{
return
{
images
:
[],
pageInfo
:
{},
containerRepositoriesCount
:
0
,
itemToDelete
:
{},
deleteAlertType
:
null
,
search
:
null
,
isEmpty
:
false
,
searchValue
:
null
,
name
:
null
,
mutationLoading
:
false
,
};
},
computed
:
{
...
mapState
([
'
config
'
,
'
isLoading
'
,
'
images
'
,
'
pagination
'
]),
...
mapState
([
'
config
'
]),
graphqlResource
()
{
return
this
.
config
.
isGroupPage
?
'
group
'
:
'
project
'
;
},
graphQlQuery
()
{
return
this
.
config
.
isGroupPage
?
getGroupContainerRepositories
:
getProjectContainerRepositories
;
},
queryVariables
()
{
return
{
name
:
this
.
name
,
fullPath
:
this
.
config
.
isGroupPage
?
this
.
config
.
groupPath
:
this
.
config
.
projectPath
,
first
:
GRAPHQL_PAGE_SIZE
,
};
},
tracking
()
{
return
{
label
:
'
registry_repository_delete
'
,
};
},
isLoading
()
{
return
this
.
$apollo
.
queries
.
images
.
loading
||
this
.
mutationLoading
;
},
showCommands
()
{
return
Boolean
(
!
this
.
isLoading
&&
!
this
.
config
?.
isGroupPage
&&
this
.
images
?.
length
);
},
...
...
@@ -93,19 +142,7 @@ export default {
:
DELETE_IMAGE_ERROR_MESSAGE
;
},
},
mounted
()
{
this
.
loadImageList
(
this
.
$route
.
name
);
},
methods
:
{
...
mapActions
([
'
requestImagesList
'
,
'
requestDeleteImage
'
]),
loadImageList
(
fromName
)
{
if
(
!
fromName
||
!
this
.
images
?.
length
)
{
return
this
.
requestImagesList
().
then
(()
=>
{
this
.
isEmpty
=
this
.
images
.
length
===
0
;
});
}
return
Promise
.
resolve
();
},
deleteImage
(
item
)
{
this
.
track
(
'
click_button
'
);
this
.
itemToDelete
=
item
;
...
...
@@ -113,18 +150,59 @@ export default {
},
handleDeleteImage
()
{
this
.
track
(
'
confirm_delete
'
);
return
this
.
requestDeleteImage
(
this
.
itemToDelete
)
.
then
(()
=>
{
this
.
mutationLoading
=
true
;
return
this
.
$apollo
.
mutate
({
mutation
:
deleteContainerRepository
,
variables
:
{
id
:
this
.
itemToDelete
.
id
,
},
})
.
then
(({
data
})
=>
{
if
(
data
?.
destroyContainerRepository
?.
errors
[
0
])
{
this
.
deleteAlertType
=
'
danger
'
;
}
else
{
this
.
deleteAlertType
=
'
success
'
;
}
})
.
catch
(()
=>
{
this
.
deleteAlertType
=
'
danger
'
;
})
.
finally
(()
=>
{
this
.
mutationLoading
=
false
;
});
},
dismissDeleteAlert
()
{
this
.
deleteAlertType
=
null
;
this
.
itemToDelete
=
{};
},
fetchNextPage
()
{
if
(
this
.
pageInfo
?.
hasNextPage
)
{
this
.
$apollo
.
queries
.
images
.
fetchMore
({
variables
:
{
after
:
this
.
pageInfo
?.
endCursor
,
first
:
GRAPHQL_PAGE_SIZE
,
},
updateQuery
(
previousResult
,
{
fetchMoreResult
})
{
return
fetchMoreResult
;
},
});
}
},
fetchPreviousPage
()
{
if
(
this
.
pageInfo
?.
hasPreviousPage
)
{
this
.
$apollo
.
queries
.
images
.
fetchMore
({
variables
:
{
first
:
null
,
before
:
this
.
pageInfo
?.
startCursor
,
last
:
GRAPHQL_PAGE_SIZE
,
},
updateQuery
(
previousResult
,
{
fetchMoreResult
})
{
return
fetchMoreResult
;
},
});
}
},
},
};
</
script
>
...
...
@@ -134,7 +212,7 @@ export default {
<gl-alert
v-if=
"showDeleteAlert"
:variant=
"deleteAlertType"
class=
"
mt-2
"
class=
"
gl-mt-5
"
dismissible
@
dismiss=
"dismissDeleteAlert"
>
...
...
@@ -165,7 +243,7 @@ export default {
<
template
v-else
>
<registry-header
:images-count=
"
pagination.total
"
:images-count=
"
containerRepositoriesCount
"
:expiration-policy=
"config.expirationPolicy"
:help-page-path=
"config.helpPagePath"
:expiration-policy-help-page-path=
"config.expirationPolicyHelpPagePath"
...
...
@@ -176,7 +254,7 @@ export default {
</
template
>
</registry-header>
<div
v-if=
"isLoading"
class=
"
mt-2
"
>
<div
v-if=
"isLoading"
class=
"
gl-mt-5
"
>
<gl-skeleton-loader
v-for=
"index in $options.loader.repeat"
:key=
"index"
...
...
@@ -190,16 +268,17 @@ export default {
</gl-skeleton-loader>
</div>
<
template
v-else
>
<template
v-if=
"
!isEmpty
"
>
<template
v-if=
"
images.length > 0 || name
"
>
<div
class=
"gl-display-flex gl-p-1 gl-mt-3"
data-testid=
"listHeader"
>
<div
class=
"gl-flex-fill-1"
>
<h5>
{{
$options
.
i18n
.
IMAGE_REPOSITORY_LIST_LABEL
}}
</h5>
</div>
<div>
<gl-search-box-by-click
v-model=
"search"
v-model=
"search
Value
"
:placeholder=
"$options.i18n.SEARCH_PLACEHOLDER_TEXT"
@
submit=
"requestImagesList(
{ name: $event })"
@
clear=
"name = null"
@
submit=
"name = $event"
/>
</div>
</div>
...
...
@@ -207,9 +286,10 @@ export default {
<image-list
v-if=
"images.length"
:images=
"images"
:pagination=
"pagination"
@
pageChange=
"requestImagesList(
{ pagination: { page: $event }, name: search })"
:page-info=
"pageInfo"
@
delete=
"deleteImage"
@
prev-page=
"fetchPreviousPage"
@
next-page=
"fetchNextPage"
/>
<gl-empty-state
...
...
app/views/groups/registry/repositories/index.html.haml
View file @
341637eb
...
...
@@ -16,4 +16,5 @@
"cleanup_policies_help_page_path"
=>
help_page_path
(
'user/packages/container_registry/index'
,
anchor:
'how-the-cleanup-policy-works'
),
"is_admin"
:
current_user
&
.
admin
.
to_s
,
is_group_page:
"true"
,
"group_path"
:
@group
.
full_path
,
character_error:
@character_error
.
to_s
}
}
app/views/projects/registry/repositories/index.html.haml
View file @
341637eb
...
...
@@ -17,6 +17,6 @@
"garbage_collection_help_page_path"
=>
help_page_path
(
'administration/packages/container_registry'
,
anchor:
'container-registry-garbage-collection'
),
"run_cleanup_policies_help_page_path"
=>
help_page_path
(
'administration/packages/container_registry'
,
anchor:
'run-the-cleanup-policy-now'
),
"cleanup_policies_help_page_path"
=>
help_page_path
(
'user/packages/container_registry/index'
,
anchor:
'how-the-cleanup-policy-works'
),
"project_path"
:
@project
.
full_path
,
"is_admin"
:
current_user
&
.
admin
.
to_s
,
character_error:
@character_error
.
to_s
}
}
changelogs/unreleased/276432-refactor-container-registry-frontend-to-graphql.yml
0 → 100644
View file @
341637eb
---
title
:
Refactor container registry list page to grapqhl
merge_request
:
48602
author
:
type
:
changed
spec/features/groups/container_registry_spec.rb
View file @
341637eb
...
...
@@ -51,13 +51,6 @@ RSpec.describe 'Container Registry', :js do
expect
(
page
).
to
have_content
'my/image'
end
it
'image repository delete is disabled'
do
visit_container_registry
delete_btn
=
find
(
'[title="Remove repository"]'
)
expect
(
delete_btn
).
to
be_disabled
end
it
'navigates to repo details'
do
visit_container_registry_details
(
'my/image'
)
...
...
spec/features/projects/container_registry_spec.rb
View file @
341637eb
...
...
@@ -94,7 +94,8 @@ RSpec.describe 'Container Registry', :js do
end
it
(
'pagination navigate to the second page'
)
do
visit_second_page
visit_details_second_page
expect
(
page
).
to
have_content
'20'
end
end
...
...
@@ -116,22 +117,23 @@ RSpec.describe 'Container Registry', :js do
context
'when there are more than 10 images'
do
before
do
create_list
(
:container_repository
,
12
,
project:
project
)
project
.
container_repositories
<<
container_repository
create_list
(
:container_repository
,
12
,
project:
project
)
visit_container_registry
end
it
'shows pagination'
do
expect
(
page
).
to
have_css
'.gl-pagination'
expect
(
page
).
to
have_css
'.gl-
keyset-
pagination'
end
it
'pagination goes to second page'
do
visit_
second
_page
visit_
list_next
_page
expect
(
page
).
to
have_content
'my/image'
end
it
'pagination is preserved after navigating back from details'
do
visit_
second
_page
visit_
list_next
_page
click_link
'my/image'
breadcrumb
=
find
'.breadcrumbs'
breadcrumb
.
click_link
'Container Registry'
...
...
@@ -148,7 +150,12 @@ RSpec.describe 'Container Registry', :js do
click_link
name
end
def
visit_second_page
def
visit_list_next_page
pagination
=
find
'.gl-keyset-pagination'
pagination
.
click_button
'Next'
end
def
visit_details_second_page
pagination
=
find
'.gl-pagination'
pagination
.
click_link
'2'
end
...
...
spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js
View file @
341637eb
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlIcon
,
GlSprintf
}
from
'
@gitlab/ui
'
;
import
{
createMockDirective
,
getBinding
}
from
'
helpers/vue_mock_directive
'
;
import
{
getIdFromGraphQLId
}
from
'
~/graphql_shared/utils
'
;
import
ClipboardButton
from
'
~/vue_shared/components/clipboard_button.vue
'
;
import
Component
from
'
~/registry/explorer/components/list_page/image_list_row.vue
'
;
import
ListItem
from
'
~/vue_shared/components/registry/list_item.vue
'
;
...
...
@@ -11,13 +12,15 @@ import {
REMOVE_REPOSITORY_LABEL
,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE
,
CLEANUP_TIMED_OUT_ERROR_MESSAGE
,
IMAGE_DELETE_SCHEDULED_STATUS
,
IMAGE_FAILED_DELETED_STATUS
,
}
from
'
~/registry/explorer/constants
'
;
import
{
RouterLink
}
from
'
../../stubs
'
;
import
{
imagesListResponse
}
from
'
../../mock_data
'
;
describe
(
'
Image List Row
'
,
()
=>
{
let
wrapper
;
const
item
=
imagesListResponse
.
data
[
0
]
;
const
[
item
]
=
imagesListResponse
;
const
findDetailsLink
=
()
=>
wrapper
.
find
(
'
[data-testid="details-link"]
'
);
const
findTagsCount
=
()
=>
wrapper
.
find
(
'
[data-testid="tagsCount"]
'
);
...
...
@@ -50,13 +53,15 @@ describe('Image List Row', () => {
describe
(
'
main tooltip
'
,
()
=>
{
it
(
`the title is
${
ROW_SCHEDULED_FOR_DELETION
}
`
,
()
=>
{
mountComponent
();
const
tooltip
=
getBinding
(
wrapper
.
element
,
'
gl-tooltip
'
);
expect
(
tooltip
).
toBeDefined
();
expect
(
tooltip
.
value
.
title
).
toBe
(
ROW_SCHEDULED_FOR_DELETION
);
});
it
(
'
is disabled when item is being deleted
'
,
()
=>
{
mountComponent
({
item
:
{
...
item
,
deleting
:
true
}
});
mountComponent
({
item
:
{
...
item
,
status
:
IMAGE_DELETE_SCHEDULED_STATUS
}
});
const
tooltip
=
getBinding
(
wrapper
.
element
,
'
gl-tooltip
'
);
expect
(
tooltip
.
value
.
disabled
).
toBe
(
false
);
});
...
...
@@ -65,12 +70,13 @@ describe('Image List Row', () => {
describe
(
'
image title and path
'
,
()
=>
{
it
(
'
contains a link to the details page
'
,
()
=>
{
mountComponent
();
const
link
=
findDetailsLink
();
expect
(
link
.
html
()).
toContain
(
item
.
path
);
expect
(
link
.
props
(
'
to
'
)).
toMatchObject
({
name
:
'
details
'
,
params
:
{
id
:
item
.
id
,
id
:
getIdFromGraphQLId
(
item
.
id
)
,
},
});
});
...
...
@@ -85,16 +91,18 @@ describe('Image List Row', () => {
describe
(
'
warning icon
'
,
()
=>
{
it
.
each
`
failedDelete | cleanup_policy_started_a
t | shown | title
${
true
}
|
${
true
}
|
${
true
}
|
${
ASYNC_DELETE_IMAGE_ERROR_MESSAGE
}
${
false
}
|
${
true
}
|
${
true
}
|
${
CLEANUP_TIMED_OUT_ERROR_MESSAGE
}
${
false
}
|
${
false
}
|
${
false
}
|
${
''
}
status | expirationPolicyStartedA
t | shown | title
${
IMAGE_FAILED_DELETED_STATUS
}
|
${
true
}
|
${
true
}
|
${
ASYNC_DELETE_IMAGE_ERROR_MESSAGE
}
${
''
}
|
${
true
}
|
${
true
}
|
${
CLEANUP_TIMED_OUT_ERROR_MESSAGE
}
${
''
}
|
${
false
}
|
${
false
}
|
${
''
}
`
(
'
when failedDelete is $failedDelete and cleanup_policy_started_at is $cleanup_policy_started_at
'
,
({
cleanup_policy_started_at
,
failedDelete
,
shown
,
title
})
=>
{
mountComponent
({
item
:
{
...
item
,
failedDelete
,
cleanup_policy_started_at
}
});
'
when status is $status and expirationPolicyStartedAt is $expirationPolicyStartedAt
'
,
({
expirationPolicyStartedAt
,
status
,
shown
,
title
})
=>
{
mountComponent
({
item
:
{
...
item
,
status
,
expirationPolicyStartedAt
}
});
const
icon
=
findWarningIcon
();
expect
(
icon
.
exists
()).
toBe
(
shown
);
if
(
shown
)
{
const
tooltip
=
getBinding
(
icon
.
element
,
'
gl-tooltip
'
);
expect
(
tooltip
.
value
.
title
).
toBe
(
title
);
...
...
@@ -112,30 +120,33 @@ describe('Image List Row', () => {
it
(
'
has the correct props
'
,
()
=>
{
mountComponent
();
expect
(
findDeleteBtn
().
attributes
()).
toMatchObject
({
expect
(
findDeleteBtn
().
props
()).
toMatchObject
({
title
:
REMOVE_REPOSITORY_LABEL
,
tooltip
disabled
:
`
${
Boolean
(
item
.
destroy_path
)}
`
,
tooltip
t
itle
:
LIST_DELETE_BUTTON_DISABLED
,
tooltip
Disabled
:
item
.
canDelete
,
tooltip
T
itle
:
LIST_DELETE_BUTTON_DISABLED
,
});
});
it
(
'
emits a delete event
'
,
()
=>
{
mountComponent
();
findDeleteBtn
().
vm
.
$emit
(
'
delete
'
);
expect
(
wrapper
.
emitted
(
'
delete
'
)).
toEqual
([[
item
]]);
});
it
.
each
`
destroy_path | deleting
| state
${
null
}
|
${
null
}
|
${
'
true
'
}
${
null
}
|
${
true
}
|
${
'
true
'
}
${
'
foo
'
}
|
${
true
}
|
${
'
true
'
}
${
'
foo
'
}
|
${
false
}
|
${
undefined
}
canDelete | status
| state
${
false
}
|
${
''
}
|
${
true
}
${
false
}
|
${
IMAGE_DELETE_SCHEDULED_STATUS
}
|
${
true
}
${
true
}
|
${
IMAGE_DELETE_SCHEDULED_STATUS
}
|
${
true
}
${
true
}
|
${
''
}
|
${
false
}
`
(
'
disabled is $state when destroy_path is $destroy_path and deleting is $deleting
'
,
({
destroy_path
,
deleting
,
state
})
=>
{
mountComponent
({
item
:
{
...
item
,
destroy_path
,
deleting
}
});
expect
(
findDeleteBtn
().
attributes
(
'
disabled
'
)).
toBe
(
state
);
'
disabled is $state when canDelete is $canDelete and status is $status
'
,
({
canDelete
,
status
,
state
})
=>
{
mountComponent
({
item
:
{
...
item
,
canDelete
,
status
}
});
expect
(
findDeleteBtn
().
props
(
'
disabled
'
)).
toBe
(
state
);
},
);
});
...
...
@@ -155,11 +166,13 @@ describe('Image List Row', () => {
describe
(
'
tags count text
'
,
()
=>
{
it
(
'
with one tag in the image
'
,
()
=>
{
mountComponent
({
item
:
{
...
item
,
tags_count
:
1
}
});
mountComponent
({
item
:
{
...
item
,
tagsCount
:
1
}
});
expect
(
findTagsCount
().
text
()).
toMatchInterpolatedText
(
'
1 Tag
'
);
});
it
(
'
with more than one tag in the image
'
,
()
=>
{
mountComponent
({
item
:
{
...
item
,
tags_count
:
3
}
});
mountComponent
({
item
:
{
...
item
,
tagsCount
:
3
}
});
expect
(
findTagsCount
().
text
()).
toMatchInterpolatedText
(
'
3 Tags
'
);
});
});
...
...
spec/frontend/registry/explorer/components/list_page/image_list_spec.js
View file @
341637eb
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
GlPagination
}
from
'
@gitlab/ui
'
;
import
{
Gl
Keyset
Pagination
}
from
'
@gitlab/ui
'
;
import
Component
from
'
~/registry/explorer/components/list_page/image_list.vue
'
;
import
ImageListRow
from
'
~/registry/explorer/components/list_page/image_list_row.vue
'
;
import
{
imagesListResponse
,
imagePagination
}
from
'
../../mock_data
'
;
import
{
imagesListResponse
,
pageInfo
as
defaultPageInfo
}
from
'
../../mock_data
'
;
describe
(
'
Image List
'
,
()
=>
{
let
wrapper
;
const
findRow
=
()
=>
wrapper
.
findAll
(
ImageListRow
);
const
findPagination
=
()
=>
wrapper
.
find
(
GlPagination
);
const
findPagination
=
()
=>
wrapper
.
find
(
Gl
Keyset
Pagination
);
const
mountComponent
=
()
=>
{
const
mountComponent
=
(
pageInfo
=
defaultPageInfo
)
=>
{
wrapper
=
shallowMount
(
Component
,
{
propsData
:
{
images
:
imagesListResponse
.
data
,
pag
ination
:
imagePagination
,
images
:
imagesListResponse
,
pag
eInfo
,
},
});
};
beforeEach
(()
=>
{
mountComponent
();
});
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
=
null
;
...
...
@@ -31,10 +27,14 @@ describe('Image List', () => {
describe
(
'
list
'
,
()
=>
{
it
(
'
contains one list element for each image
'
,
()
=>
{
expect
(
findRow
().
length
).
toBe
(
imagesListResponse
.
data
.
length
);
mountComponent
();
expect
(
findRow
().
length
).
toBe
(
imagesListResponse
.
length
);
});
it
(
'
when delete event is emitted on the row it emits up a delete event
'
,
()
=>
{
mountComponent
();
findRow
()
.
at
(
0
)
.
vm
.
$emit
(
'
delete
'
,
'
foo
'
);
...
...
@@ -44,19 +44,41 @@ describe('Image List', () => {
describe
(
'
pagination
'
,
()
=>
{
it
(
'
exists
'
,
()
=>
{
mountComponent
();
expect
(
findPagination
().
exists
()).
toBe
(
true
);
});
it
(
'
is wired to the correct pagination props
'
,
()
=>
{
const
pagination
=
findPagination
();
expect
(
pagination
.
props
(
'
perPage
'
)).
toBe
(
imagePagination
.
perPage
);
expect
(
pagination
.
props
(
'
totalItems
'
)).
toBe
(
imagePagination
.
total
);
expect
(
pagination
.
props
(
'
value
'
)).
toBe
(
imagePagination
.
page
);
it
.
each
`
hasNextPage | hasPreviousPage | isVisible
${
true
}
|
${
true
}
|
${
true
}
${
true
}
|
${
false
}
|
${
true
}
${
false
}
|
${
true
}
|
${
true
}
`
(
'
when hasNextPage is $hasNextPage and hasPreviousPage is $hasPreviousPage: is $isVisible that the component is visible
'
,
({
hasNextPage
,
hasPreviousPage
,
isVisible
})
=>
{
mountComponent
({
hasNextPage
,
hasPreviousPage
});
expect
(
findPagination
().
exists
()).
toBe
(
isVisible
);
expect
(
findPagination
().
props
(
'
hasPreviousPage
'
)).
toBe
(
hasPreviousPage
);
expect
(
findPagination
().
props
(
'
hasNextPage
'
)).
toBe
(
hasNextPage
);
},
);
it
(
'
emits "prev-page" when the user clicks the back page button
'
,
()
=>
{
mountComponent
({
hasPreviousPage
:
true
});
findPagination
().
vm
.
$emit
(
'
prev
'
);
expect
(
wrapper
.
emitted
(
'
prev-page
'
)).
toEqual
([[]]);
});
it
(
'
emits a pageChange event when the page change
'
,
()
=>
{
findPagination
().
vm
.
$emit
(
GlPagination
.
model
.
event
,
2
);
expect
(
wrapper
.
emitted
(
'
pageChange
'
)).
toEqual
([[
2
]]);
it
(
'
emits "next-page" when the user clicks the forward page button
'
,
()
=>
{
mountComponent
({
hasNextPage
:
true
});
findPagination
().
vm
.
$emit
(
'
next
'
);
expect
(
wrapper
.
emitted
(
'
next-page
'
)).
toEqual
([[]]);
});
});
});
spec/frontend/registry/explorer/mock_data.js
View file @
341637eb
...
...
@@ -45,21 +45,32 @@ export const registryServerResponse = [
},
];
export
const
imagesListResponse
=
{
data
:
[
export
const
imagesListResponse
=
[
{
path
:
'
foo
'
,
location
:
'
location
'
,
destroy_path
:
'
path
'
,
__typename
:
'
ContainerRepository
'
,
id
:
'
gid://gitlab/ContainerRepository/26
'
,
name
:
'
rails-12009
'
,
path
:
'
gitlab-org/gitlab-test/rails-12009
'
,
status
:
null
,
location
:
'
0.0.0.0:5000/gitlab-org/gitlab-test/rails-12009
'
,
canDelete
:
true
,
createdAt
:
'
2020-11-03T13:29:21Z
'
,
tagsCount
:
18
,
expirationPolicyStartedAt
:
null
,
},
{
path
:
'
bar
'
,
location
:
'
location-2
'
,
destroy_path
:
'
path-2
'
,
__typename
:
'
ContainerRepository
'
,
id
:
'
gid://gitlab/ContainerRepository/11
'
,
name
:
'
rails-20572
'
,
path
:
'
gitlab-org/gitlab-test/rails-20572
'
,
status
:
null
,
location
:
'
0.0.0.0:5000/gitlab-org/gitlab-test/rails-20572
'
,
canDelete
:
true
,
createdAt
:
'
2020-09-21T06:57:43Z
'
,
tagsCount
:
1
,
expirationPolicyStartedAt
:
null
,
},
],
headers
,
};
];
export
const
tagsListResponse
=
{
data
:
[
...
...
@@ -90,12 +101,12 @@ export const tagsListResponse = {
headers
,
};
export
const
imagePagination
=
{
perPage
:
10
,
page
:
1
,
total
:
14
,
totalPages
:
2
,
nextPage
:
2
,
export
const
pageInfo
=
{
hasNextPage
:
true
,
hasPreviousPage
:
true
,
startCursor
:
'
eyJpZCI6IjI2In0
'
,
endCursor
:
'
eyJpZCI6IjgifQ
'
,
__typename
:
'
ContainerRepositoryConnection
'
,
};
export
const
imageDetailsMock
=
{
...
...
@@ -108,3 +119,76 @@ export const imageDetailsMock = {
cleanup_policy_started_at
:
null
,
delete_api_path
:
'
http://0.0.0.0:3000/api/v4/projects/1/registry/repositories/1
'
,
};
export
const
graphQLImageListMock
=
{
data
:
{
project
:
{
__typename
:
'
Project
'
,
containerRepositoriesCount
:
2
,
containerRepositories
:
{
__typename
:
'
ContainerRepositoryConnection
'
,
nodes
:
imagesListResponse
,
pageInfo
,
},
},
},
};
export
const
graphQLEmptyImageListMock
=
{
data
:
{
project
:
{
__typename
:
'
Project
'
,
containerRepositoriesCount
:
2
,
containerRepositories
:
{
__typename
:
'
ContainerRepositoryConnection
'
,
nodes
:
[],
pageInfo
,
},
},
},
};
export
const
graphQLEmptyGroupImageListMock
=
{
data
:
{
group
:
{
__typename
:
'
Group
'
,
containerRepositoriesCount
:
2
,
containerRepositories
:
{
__typename
:
'
ContainerRepositoryConnection
'
,
nodes
:
[],
pageInfo
,
},
},
},
};
export
const
deletedContainerRepository
=
{
id
:
'
gid://gitlab/ContainerRepository/11
'
,
status
:
'
DELETE_SCHEDULED
'
,
path
:
'
gitlab-org/gitlab-test/rails-12009
'
,
__typename
:
'
ContainerRepository
'
,
};
export
const
graphQLImageDeleteMock
=
{
data
:
{
destroyContainerRepository
:
{
containerRepository
:
{
...
deletedContainerRepository
,
},
errors
:
[],
__typename
:
'
DestroyContainerRepositoryPayload
'
,
},
},
};
export
const
graphQLImageDeleteMockError
=
{
data
:
{
destroyContainerRepository
:
{
containerRepository
:
{
...
deletedContainerRepository
,
},
errors
:
[
'
foo
'
],
__typename
:
'
DestroyContainerRepositoryPayload
'
,
},
},
};
spec/frontend/registry/explorer/pages/list_spec.js
View file @
341637eb
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
{
GlSkeletonLoader
,
GlSprintf
,
GlAlert
,
GlSearchBoxByClick
}
from
'
@gitlab/ui
'
;
import
createMockApollo
from
'
jest/helpers/mock_apollo_helper
'
;
import
waitForPromises
from
'
helpers/wait_for_promises
'
;
import
Tracking
from
'
~/tracking
'
;
import
component
from
'
~/registry/explorer/pages/list.vue
'
;
...
...
@@ -10,26 +12,36 @@ import RegistryHeader from '~/registry/explorer/components/list_page/registry_he
import
ImageList
from
'
~/registry/explorer/components/list_page/image_list.vue
'
;
import
TitleArea
from
'
~/vue_shared/components/registry/title_area.vue
'
;
import
{
createStore
}
from
'
~/registry/explorer/stores/
'
;
import
{
SET_MAIN_LOADING
,
SET_IMAGES_LIST_SUCCESS
,
SET_PAGINATION
,
SET_INITIAL_STATE
,
}
from
'
~/registry/explorer/stores/mutation_types
'
;
import
{
SET_INITIAL_STATE
}
from
'
~/registry/explorer/stores/mutation_types
'
;
import
{
DELETE_IMAGE_SUCCESS_MESSAGE
,
DELETE_IMAGE_ERROR_MESSAGE
,
IMAGE_REPOSITORY_LIST_LABEL
,
SEARCH_PLACEHOLDER_TEXT
,
}
from
'
~/registry/explorer/constants
'
;
import
{
imagesListResponse
}
from
'
../mock_data
'
;
import
getProjectContainerRepositories
from
'
~/registry/explorer/graphql/queries/get_project_container_repositories.graphql
'
;
import
getGroupContainerRepositories
from
'
~/registry/explorer/graphql/queries/get_group_container_repositories.graphql
'
;
import
deleteContainerRepository
from
'
~/registry/explorer/graphql/mutations/delete_container_repository.graphql
'
;
import
{
graphQLImageListMock
,
graphQLImageDeleteMock
,
deletedContainerRepository
,
graphQLImageDeleteMockError
,
graphQLEmptyImageListMock
,
graphQLEmptyGroupImageListMock
,
pageInfo
,
}
from
'
../mock_data
'
;
import
{
GlModal
,
GlEmptyState
}
from
'
../stubs
'
;
import
{
$toast
}
from
'
../../shared/mocks
'
;
const
localVue
=
createLocalVue
();
describe
(
'
List Page
'
,
()
=>
{
let
wrapper
;
let
dispatchSpy
;
let
store
;
let
apolloProvider
;
const
findDeleteModal
=
()
=>
wrapper
.
find
(
GlModal
);
const
findSkeletonLoader
=
()
=>
wrapper
.
find
(
GlSkeletonLoader
);
...
...
@@ -47,8 +59,30 @@ describe('List Page', () => {
const
findSearchBox
=
()
=>
wrapper
.
find
(
GlSearchBoxByClick
);
const
findEmptySearchMessage
=
()
=>
wrapper
.
find
(
'
[data-testid="emptySearch"]
'
);
const
mountComponent
=
({
mocks
}
=
{})
=>
{
const
waitForApolloRequestRender
=
async
()
=>
{
await
waitForPromises
();
await
wrapper
.
vm
.
$nextTick
();
};
const
mountComponent
=
({
mocks
,
resolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageListMock
),
groupResolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageListMock
),
mutationResolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageDeleteMock
),
}
=
{})
=>
{
localVue
.
use
(
VueApollo
);
const
requestHandlers
=
[
[
getProjectContainerRepositories
,
resolver
],
[
getGroupContainerRepositories
,
groupResolver
],
[
deleteContainerRepository
,
mutationResolver
],
];
apolloProvider
=
createMockApollo
(
requestHandlers
);
wrapper
=
shallowMount
(
component
,
{
localVue
,
apolloProvider
,
store
,
stubs
:
{
GlModal
,
...
...
@@ -69,37 +103,21 @@ describe('List Page', () => {
beforeEach
(()
=>
{
store
=
createStore
();
dispatchSpy
=
jest
.
spyOn
(
store
,
'
dispatch
'
);
dispatchSpy
.
mockResolvedValue
();
store
.
commit
(
SET_IMAGES_LIST_SUCCESS
,
imagesListResponse
.
data
);
store
.
commit
(
SET_PAGINATION
,
imagesListResponse
.
headers
);
});
afterEach
(()
=>
{
wrapper
.
destroy
();
});
describe
(
'
API calls
'
,
()
=>
{
it
.
each
`
imageList | name | called
${[]}
|
${
'
foo
'
}
|
${[
'
requestImagesList
'
]}
${
imagesListResponse
.
data
}
|
${
undefined
}
|
${[
'
requestImagesList
'
]}
${
imagesListResponse
.
data
}
|
${
'
foo
'
}
|
${
undefined
}
`
(
'
with images equal $imageList and name $name dispatch calls $called
'
,
({
imageList
,
name
,
called
})
=>
{
store
.
commit
(
SET_IMAGES_LIST_SUCCESS
,
imageList
);
dispatchSpy
.
mockClear
();
mountComponent
({
mocks
:
{
$route
:
{
name
}
}
});
expect
(
dispatchSpy
.
mock
.
calls
[
0
]).
toEqual
(
called
);
},
);
});
it
(
'
contains registry header
'
,
()
=>
{
it
(
'
contains registry header
'
,
async
()
=>
{
mountComponent
();
await
waitForApolloRequestRender
();
expect
(
findRegistryHeader
().
exists
()).
toBe
(
true
);
expect
(
findRegistryHeader
().
props
()).
toMatchObject
({
imagesCount
:
2
,
});
});
describe
(
'
connection error
'
,
()
=>
{
...
...
@@ -111,7 +129,6 @@ describe('List Page', () => {
beforeEach
(()
=>
{
store
.
commit
(
SET_INITIAL_STATE
,
config
);
mountComponent
();
});
afterEach
(()
=>
{
...
...
@@ -119,78 +136,103 @@ describe('List Page', () => {
});
it
(
'
should show an empty state
'
,
()
=>
{
mountComponent
();
expect
(
findEmptyState
().
exists
()).
toBe
(
true
);
});
it
(
'
empty state should have an svg-path
'
,
()
=>
{
mountComponent
();
expect
(
findEmptyState
().
attributes
(
'
svg-path
'
)).
toBe
(
config
.
containersErrorImage
);
});
it
(
'
empty state should have a description
'
,
()
=>
{
mountComponent
();
expect
(
findEmptyState
().
html
()).
toContain
(
'
connection error
'
);
});
it
(
'
should not show the loading or default state
'
,
()
=>
{
mountComponent
();
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
false
);
expect
(
findImageList
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
isLoading is true
'
,
()
=>
{
beforeEach
(()
=>
{
store
.
commit
(
SET_MAIN_LOADING
,
true
);
it
(
'
shows the skeleton loader
'
,
()
=>
{
mountComponent
();
});
afterEach
(()
=>
store
.
commit
(
SET_MAIN_LOADING
,
false
));
it
(
'
shows the skeleton loader
'
,
()
=>
{
expect
(
findSkeletonLoader
().
exists
()).
toBe
(
true
);
});
it
(
'
imagesList is not visible
'
,
()
=>
{
mountComponent
();
expect
(
findImageList
().
exists
()).
toBe
(
false
);
});
it
(
'
cli commands is not visible
'
,
()
=>
{
mountComponent
();
expect
(
findCliCommands
().
exists
()).
toBe
(
false
);
});
});
describe
(
'
list is empty
'
,
()
=>
{
beforeEach
(()
=>
{
store
.
commit
(
SET_IMAGES_LIST_SUCCESS
,
[]);
mountComponent
();
return
waitForPromises
();
});
describe
(
'
project page
'
,
()
=>
{
const
resolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLEmptyImageListMock
);
it
(
'
cli commands is not visible
'
,
async
()
=>
{
mountComponent
({
resolver
});
await
waitForApolloRequestRender
();
it
(
'
cli commands is not visible
'
,
()
=>
{
expect
(
findCliCommands
().
exists
()).
toBe
(
false
);
});
it
(
'
project empty state is visible
'
,
()
=>
{
it
(
'
project empty state is visible
'
,
async
()
=>
{
mountComponent
({
resolver
});
await
waitForApolloRequestRender
();
expect
(
findProjectEmptyState
().
exists
()).
toBe
(
true
);
});
});
describe
(
'
group page
'
,
()
=>
{
const
groupResolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLEmptyGroupImageListMock
);
describe
(
'
is group page is true
'
,
()
=>
{
beforeEach
(()
=>
{
store
.
commit
(
SET_INITIAL_STATE
,
{
isGroupPage
:
true
});
mountComponent
();
});
afterEach
(()
=>
{
store
.
commit
(
SET_INITIAL_STATE
,
{
isGroupPage
:
undefined
});
});
it
(
'
group empty state is visible
'
,
()
=>
{
it
(
'
group empty state is visible
'
,
async
()
=>
{
mountComponent
({
groupResolver
});
await
waitForApolloRequestRender
();
expect
(
findGroupEmptyState
().
exists
()).
toBe
(
true
);
});
it
(
'
cli commands is not visible
'
,
()
=>
{
it
(
'
cli commands is not visible
'
,
async
()
=>
{
mountComponent
({
groupResolver
});
await
waitForApolloRequestRender
();
expect
(
findCliCommands
().
exists
()).
toBe
(
false
);
});
it
(
'
list header is not visible
'
,
()
=>
{
it
(
'
list header is not visible
'
,
async
()
=>
{
mountComponent
({
groupResolver
});
await
waitForApolloRequestRender
();
expect
(
findListHeader
().
exists
()).
toBe
(
false
);
});
});
...
...
@@ -198,55 +240,91 @@ describe('List Page', () => {
describe
(
'
list is not empty
'
,
()
=>
{
describe
(
'
unfiltered state
'
,
()
=>
{
beforeEach
(
()
=>
{
it
(
'
quick start is visible
'
,
async
()
=>
{
mountComponent
();
});
it
(
'
quick start is visible
'
,
()
=>
{
await
waitForApolloRequestRender
();
expect
(
findCliCommands
().
exists
()).
toBe
(
true
);
});
it
(
'
list component is visible
'
,
()
=>
{
it
(
'
list component is visible
'
,
async
()
=>
{
mountComponent
();
await
waitForApolloRequestRender
();
expect
(
findImageList
().
exists
()).
toBe
(
true
);
});
it
(
'
list header is visible
'
,
()
=>
{
it
(
'
list header is visible
'
,
async
()
=>
{
mountComponent
();
await
waitForApolloRequestRender
();
const
header
=
findListHeader
();
expect
(
header
.
exists
()).
toBe
(
true
);
expect
(
header
.
text
()).
toBe
(
IMAGE_REPOSITORY_LIST_LABEL
);
});
describe
(
'
delete image
'
,
()
=>
{
const
itemToDelete
=
{
path
:
'
bar
'
};
it
(
'
should call deleteItem when confirming deletion
'
,
()
=>
{
dispatchSpy
.
mockResolvedValue
();
findImageList
().
vm
.
$emit
(
'
delete
'
,
itemToDelete
);
expect
(
wrapper
.
vm
.
itemToDelete
).
toEqual
(
itemToDelete
);
const
deleteImage
=
async
()
=>
{
await
wrapper
.
vm
.
$nextTick
();
findImageList
().
vm
.
$emit
(
'
delete
'
,
deletedContainerRepository
);
findDeleteModal
().
vm
.
$emit
(
'
ok
'
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
requestDeleteImage
'
,
wrapper
.
vm
.
itemToDelete
,
);
await
waitForApolloRequestRender
();
};
it
(
'
should call deleteItem when confirming deletion
'
,
async
()
=>
{
const
mutationResolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageDeleteMock
);
mountComponent
({
mutationResolver
});
await
deleteImage
();
expect
(
wrapper
.
vm
.
itemToDelete
).
toEqual
(
deletedContainerRepository
);
expect
(
mutationResolver
).
toHaveBeenCalledWith
({
id
:
deletedContainerRepository
.
id
});
const
updatedImage
=
findImageList
()
.
props
(
'
images
'
)
.
find
(
i
=>
i
.
id
===
deletedContainerRepository
.
id
);
expect
(
updatedImage
.
status
).
toBe
(
deletedContainerRepository
.
status
);
});
it
(
'
should show a success alert when delete request is successful
'
,
()
=>
{
dispatchSpy
.
mockResolvedValue
();
findImageList
().
vm
.
$emit
(
'
delete
'
,
itemToDelete
);
expect
(
wrapper
.
vm
.
itemToDelete
).
toEqual
(
itemToDelete
);
return
wrapper
.
vm
.
handleDeleteImage
().
then
(()
=>
{
it
(
'
should show a success alert when delete request is successful
'
,
async
()
=>
{
const
mutationResolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageDeleteMock
);
mountComponent
({
mutationResolver
});
await
deleteImage
();
const
alert
=
findDeleteAlert
();
expect
(
alert
.
exists
()).
toBe
(
true
);
expect
(
alert
.
text
().
replace
(
/
\s\s
+/gm
,
'
'
)).
toBe
(
DELETE_IMAGE_SUCCESS_MESSAGE
.
replace
(
'
%{title}
'
,
wrapper
.
vm
.
itemToDelete
.
path
),
);
});
describe
(
'
when delete request fails it shows an alert
'
,
()
=>
{
it
(
'
user recoverable error
'
,
async
()
=>
{
const
mutationResolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageDeleteMockError
);
mountComponent
({
mutationResolver
});
await
deleteImage
();
const
alert
=
findDeleteAlert
();
expect
(
alert
.
exists
()).
toBe
(
true
);
expect
(
alert
.
text
().
replace
(
/
\s\s
+/gm
,
'
'
)).
toBe
(
DELETE_IMAGE_ERROR_MESSAGE
.
replace
(
'
%{title}
'
,
wrapper
.
vm
.
itemToDelete
.
path
),
);
});
it
(
'
should show an error alert when delete request fails
'
,
()
=>
{
dispatchSpy
.
mockRejectedValue
();
findImageList
().
vm
.
$emit
(
'
delete
'
,
itemToDelete
);
expect
(
wrapper
.
vm
.
itemToDelete
).
toEqual
(
itemToDelete
);
return
wrapper
.
vm
.
handleDeleteImage
().
then
(()
=>
{
it
(
'
network error
'
,
async
()
=>
{
const
mutationResolver
=
jest
.
fn
().
mockRejectedValue
();
mountComponent
({
mutationResolver
});
await
deleteImage
();
const
alert
=
findDeleteAlert
();
expect
(
alert
.
exists
()).
toBe
(
true
);
expect
(
alert
.
text
().
replace
(
/
\s\s
+/gm
,
'
'
)).
toBe
(
...
...
@@ -258,38 +336,68 @@ describe('List Page', () => {
});
describe
(
'
search
'
,
()
=>
{
it
(
'
has a search box element
'
,
()
=>
{
const
doSearch
=
async
()
=>
{
await
waitForApolloRequestRender
();
findSearchBox
().
vm
.
$emit
(
'
submit
'
,
'
centos6
'
);
await
wrapper
.
vm
.
$nextTick
();
};
it
(
'
has a search box element
'
,
async
()
=>
{
mountComponent
();
await
waitForApolloRequestRender
();
const
searchBox
=
findSearchBox
();
expect
(
searchBox
.
exists
()).
toBe
(
true
);
expect
(
searchBox
.
attributes
(
'
placeholder
'
)).
toBe
(
SEARCH_PLACEHOLDER_TEXT
);
});
it
(
'
performs a search
'
,
()
=>
{
mountComponent
();
findSearchBox
().
vm
.
$emit
(
'
submit
'
,
'
foo
'
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
requestImagesList
'
,
{
name
:
'
foo
'
,
});
it
(
'
performs a search
'
,
async
()
=>
{
const
resolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageListMock
);
mountComponent
({
resolver
});
await
doSearch
();
expect
(
resolver
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
name
:
'
centos6
'
}));
});
it
(
'
when search result is empty displays an empty search message
'
,
()
=>
{
mountComponent
();
store
.
commit
(
SET_IMAGES_LIST_SUCCESS
,
[]);
return
wrapper
.
vm
.
$nextTick
().
then
(()
=>
{
it
(
'
when search result is empty displays an empty search message
'
,
async
()
=>
{
const
resolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageListMock
);
mountComponent
({
resolver
});
resolver
.
mockResolvedValue
(
graphQLEmptyImageListMock
);
await
doSearch
();
expect
(
findEmptySearchMessage
().
exists
()).
toBe
(
true
);
});
});
});
describe
(
'
pagination
'
,
()
=>
{
it
(
'
pageChange event triggers the appropriate store function
'
,
()
=>
{
mountComponent
();
findImageList
().
vm
.
$emit
(
'
pageChange
'
,
2
);
expect
(
store
.
dispatch
).
toHaveBeenCalledWith
(
'
requestImagesList
'
,
{
pagination
:
{
page
:
2
},
name
:
wrapper
.
vm
.
search
,
it
(
'
prev-page event triggers a fetchMore request
'
,
async
()
=>
{
const
resolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageListMock
);
mountComponent
({
resolver
});
await
waitForApolloRequestRender
();
findImageList
().
vm
.
$emit
(
'
prev-page
'
);
expect
(
resolver
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
first
:
null
,
before
:
pageInfo
.
startCursor
}),
);
});
it
(
'
next-page event triggers a fetchMore request
'
,
async
()
=>
{
const
resolver
=
jest
.
fn
().
mockResolvedValue
(
graphQLImageListMock
);
mountComponent
({
resolver
});
await
waitForApolloRequestRender
();
findImageList
().
vm
.
$emit
(
'
next-page
'
);
expect
(
resolver
).
toHaveBeenCalledWith
(
expect
.
objectContaining
({
after
:
pageInfo
.
endCursor
}),
);
});
});
});
...
...
@@ -324,11 +432,11 @@ describe('List Page', () => {
beforeEach
(()
=>
{
jest
.
spyOn
(
Tracking
,
'
event
'
);
dispatchSpy
.
mockResolvedValue
();
});
it
(
'
send an event when delete button is clicked
'
,
()
=>
{
findImageList
().
vm
.
$emit
(
'
delete
'
,
{});
testTrackingCall
(
'
click_button
'
);
});
...
...
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