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
2ba8c796
Commit
2ba8c796
authored
Nov 10, 2020
by
Daniel Tian
Committed by
Jose Ivan Vargas
Nov 10, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Improve vulnerability filter component logic
parent
6b18dd94
Changes
9
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
442 additions
and
363 deletions
+442
-363
ee/app/assets/javascripts/security_dashboard/components/filters/filter_body.vue
...pts/security_dashboard/components/filters/filter_body.vue
+1
-1
ee/app/assets/javascripts/security_dashboard/components/filters/standard_filter.vue
...security_dashboard/components/filters/standard_filter.vue
+76
-14
ee/app/assets/javascripts/security_dashboard/components/first_class_vulnerability_filters.vue
...ashboard/components/first_class_vulnerability_filters.vue
+24
-67
ee/app/assets/javascripts/security_dashboard/helpers.js
ee/app/assets/javascripts/security_dashboard/helpers.js
+34
-34
ee/app/assets/javascripts/security_dashboard/store/modules/filters/constants.js
...pts/security_dashboard/store/modules/filters/constants.js
+4
-0
ee/spec/frontend/security_dashboard/components/filters/filter_body_spec.js
...security_dashboard/components/filters/filter_body_spec.js
+6
-4
ee/spec/frontend/security_dashboard/components/filters/standard_filter_spec.js
...rity_dashboard/components/filters/standard_filter_spec.js
+277
-67
ee/spec/frontend/security_dashboard/components/filters_spec.js
...ec/frontend/security_dashboard/components/filters_spec.js
+2
-2
ee/spec/frontend/security_dashboard/components/first_class_vulnerability_filters_spec.js
...oard/components/first_class_vulnerability_filters_spec.js
+18
-174
No files found.
ee/app/assets/javascripts/security_dashboard/components/filters/filter_body.vue
View file @
2ba8c796
...
...
@@ -31,7 +31,7 @@ export default {
},
computed
:
{
firstSelectedOption
()
{
return
this
.
selectedOptions
[
0
]
||
'
-
'
;
return
this
.
selectedOptions
[
0
]
?.
name
||
'
-
'
;
},
extraOptionCount
()
{
return
this
.
selectedOptions
.
length
-
1
;
...
...
ee/app/assets/javascripts/security_dashboard/components/filters/standard_filter.vue
View file @
2ba8c796
<
script
>
import
{
isEqual
,
xor
}
from
'
lodash
'
;
import
FilterBody
from
'
./filter_body.vue
'
;
import
FilterItem
from
'
./filter_item.vue
'
;
export
default
{
components
:
{
FilterBody
,
FilterItem
,
},
components
:
{
FilterBody
,
FilterItem
},
props
:
{
filter
:
{
type
:
Object
,
required
:
true
,
},
showSearchBox
:
{
type
:
Boolean
,
required
:
false
,
default
:
false
,
},
},
data
()
{
return
{
searchTerm
:
''
,
selectedOptions
:
undefined
,
};
},
computed
:
{
selection
()
{
return
this
.
filter
.
selection
;
selectedSet
()
{
return
new
Set
(
this
.
selectedOptions
);
},
isNoOptionsSelected
()
{
return
this
.
selectedOptions
.
length
<=
0
;
},
selectedOptionsOrAll
()
{
return
this
.
selectedOptions
.
length
?
this
.
selectedOptions
:
[
this
.
filter
.
allOption
];
},
queryObject
()
{
// This is the object used to update the querystring.
return
{
[
this
.
filter
.
id
]:
this
.
selectedOptionsOrAll
.
map
(
x
=>
x
.
id
)
};
},
filterObject
()
{
// This is the object used by the GraphQL query.
return
{
[
this
.
filter
.
id
]:
this
.
selectedOptions
.
map
(
x
=>
x
.
id
)
};
},
filteredOptions
()
{
return
this
.
filter
.
options
.
filter
(
option
=>
option
.
name
.
toLowerCase
().
includes
(
this
.
searchTerm
.
toLowerCase
()),
);
},
selectedOptionsNames
()
{
return
Array
.
from
(
this
.
selection
).
map
(
id
=>
this
.
filter
.
options
.
find
(
x
=>
x
.
id
===
id
).
name
);
routeQueryIds
()
{
const
ids
=
this
.
$route
.
query
[
this
.
filter
.
id
]
||
[];
return
Array
.
isArray
(
ids
)
?
ids
:
[
ids
];
},
routeQueryOptions
()
{
const
options
=
this
.
filter
.
options
.
filter
(
x
=>
this
.
routeQueryIds
.
includes
(
x
.
id
));
const
hasAllId
=
this
.
routeQueryIds
.
includes
(
this
.
filter
.
allOption
.
id
);
if
(
options
.
length
&&
!
hasAllId
)
{
return
options
;
}
return
hasAllId
?
[]
:
this
.
filter
.
defaultOptions
;
},
},
watch
:
{
selectedOptions
()
{
this
.
$emit
(
'
filter-changed
'
,
this
.
filterObject
);
},
},
created
()
{
this
.
selectedOptions
=
this
.
routeQueryOptions
;
// When the user clicks the forward/back browser buttons, update the selected options.
window
.
addEventListener
(
'
popstate
'
,
()
=>
{
this
.
selectedOptions
=
this
.
routeQueryOptions
;
});
},
methods
:
{
clickFilter
(
option
)
{
this
.
$emit
(
'
setFilter
'
,
{
filterId
:
this
.
filter
.
id
,
optionId
:
option
.
id
});
toggleOption
(
option
)
{
// Toggle the option's existence in the array.
this
.
selectedOptions
=
xor
(
this
.
selectedOptions
,
[
option
]);
this
.
updateRouteQuery
();
},
deselectAllOptions
()
{
this
.
selectedOptions
=
[];
this
.
updateRouteQuery
();
},
updateRouteQuery
()
{
const
query
=
{
query
:
{
...
this
.
$route
.
query
,
...
this
.
queryObject
}
};
// To avoid a console error, don't update the querystring if it's the same as the current one.
if
(
!
isEqual
(
this
.
routeQueryIds
,
this
.
queryObject
[
this
.
filter
.
id
]))
{
this
.
$router
.
push
(
query
);
}
},
isSelected
(
option
)
{
return
this
.
select
ion
.
has
(
option
.
id
);
return
this
.
select
edSet
.
has
(
option
);
},
},
};
...
...
@@ -46,15 +100,23 @@ export default {
<filter-body
v-model.trim=
"searchTerm"
:name=
"filter.name"
:selected-options=
"selectedOptions
Names
"
:show-search-box=
"
filter.options.length >= 20
"
:selected-options=
"selectedOptions
OrAll
"
:show-search-box=
"
showSearchBox
"
>
<filter-item
v-if=
"filter.allOption && !searchTerm.length"
:is-checked=
"isNoOptionsSelected"
:text=
"filter.allOption.name"
data-testid=
"allOption"
@
click=
"deselectAllOptions"
/>
<filter-item
v-for=
"option in filteredOptions"
:key=
"option.id"
:is-checked=
"isSelected(option)"
:text=
"option.name"
@
click=
"clickFilter(option)"
data-testid=
"filterOption"
@
click=
"toggleOption(option)"
/>
</filter-body>
</
template
>
ee/app/assets/javascripts/security_dashboard/components/first_class_vulnerability_filters.vue
View file @
2ba8c796
<
script
>
import
{
isEqual
}
from
'
lodash
'
;
import
{
ALL
,
STATE
}
from
'
ee/security_dashboard/store/modules/filters/constant
s
'
;
import
{
setFilter
}
from
'
ee/security_dashboard/store/modules/filters/utils
'
;
import
StandardFilter
from
'
ee/security_dashboard/components/filters/standard_filter.vue
'
;
import
{
initFirstClassVulnerabilityFilters
,
mapProjects
}
from
'
ee/security_dashboard/helpers
'
;
import
{
debounce
}
from
'
lodash
'
;
import
{
stateFilter
,
severityFilter
,
scannerFilter
,
getProjectFilter
}
from
'
../helper
s
'
;
import
StandardFilter
from
'
./filters/standard_filter.vue
'
;
const
searchBoxOptionCount
=
20
;
// Number of options before the search box is shown.
export
default
{
components
:
{
...
...
@@ -12,73 +12,28 @@ export default {
props
:
{
projects
:
{
type
:
Array
,
required
:
false
,
default
:
undefined
},
},
data
()
{
return
{
filters
:
initFirstClassVulnerabilityFilters
(
this
.
projects
),
};
},
data
:
()
=>
({
filterQuery
:
{},
}),
computed
:
{
selectedFilters
()
{
return
this
.
filters
.
reduce
((
acc
,
{
id
,
selection
})
=>
{
if
(
!
selection
.
has
(
ALL
))
{
acc
[
id
]
=
Array
.
from
(
selection
);
}
return
acc
;
},
{});
},
},
watch
:
{
/**
* Initially the project list empty. We fetch them dynamically from GraphQL while
* fetching the list of vulnerabilities. We display the project filter with the base
* option and when the projects are fetched we add them to the list.
*/
projects
(
newProjects
,
oldProjects
)
{
if
(
oldProjects
.
length
===
0
)
{
const
projectFilter
=
this
.
filters
[
3
];
projectFilter
.
options
=
[
projectFilter
.
options
[
0
],
...
mapProjects
(
this
.
projects
)];
}
},
'
$route.query
'
:
{
immediate
:
true
,
handler
(
newQuery
)
{
let
changed
;
this
.
filters
.
forEach
((
filter
,
i
)
=>
{
let
urlFilter
=
newQuery
[
filter
.
id
];
if
(
typeof
urlFilter
===
'
undefined
'
)
{
urlFilter
=
[
ALL
];
}
else
if
(
!
Array
.
isArray
(
urlFilter
))
{
urlFilter
=
[
urlFilter
];
}
if
(
isEqual
(
this
.
selectedFilters
[
filter
.
id
],
newQuery
[
filter
.
id
])
===
false
)
{
changed
=
true
;
this
.
filters
[
i
].
selection
=
new
Set
(
urlFilter
);
}
});
if
(
changed
)
{
this
.
$emit
(
'
filterChange
'
,
this
.
selectedFilters
);
}
},
filters
()
{
return
this
.
projects
?
[
stateFilter
,
severityFilter
,
scannerFilter
,
getProjectFilter
(
this
.
projects
)]
:
[
stateFilter
,
severityFilter
,
scannerFilter
];
},
},
created
()
{
if
(
Object
.
keys
(
this
.
selectedFilters
).
length
===
0
)
{
this
.
$router
.
push
({
query
:
{
state
:
[
STATE
.
DETECTED
,
STATE
.
CONFIRMED
]
}
});
}
},
methods
:
{
setFilter
(
options
)
{
this
.
filters
=
setFilter
(
this
.
filters
,
options
);
this
.
$router
.
push
({
query
:
this
.
selectedFilters
});
this
.
$emit
(
'
filterChange
'
,
this
.
selectedFilters
);
updateFilterQuery
(
query
)
{
this
.
filterQuery
=
{
...
this
.
filterQuery
,
...
query
};
this
.
emitFilterChange
();
},
// All the filters will emit @filter-changed when the page is first loaded, which will trigger
// this method multiple times. We'll debounce it so that it only runs once.
emitFilterChange
:
debounce
(
function
emit
()
{
this
.
$emit
(
'
filterChange
'
,
this
.
filterQuery
);
}),
},
searchBoxOptionCount
,
};
</
script
>
...
...
@@ -90,7 +45,9 @@ export default {
:key=
"filter.id"
class=
"col-sm-6 col-md-4 col-lg-2 p-2"
:filter=
"filter"
@
setFilter=
"setFilter"
:data-testid=
"filter.id"
:show-search-box=
"filter.options.length >= $options.searchBoxOptionCount"
@
filter-changed=
"updateFilterQuery"
/>
</div>
</div>
...
...
ee/app/assets/javascripts/security_dashboard/helpers.js
View file @
2ba8c796
import
isPlainObject
from
'
lodash/isPlainObject
'
;
import
{
ALL
,
BASE_FILTERS
}
from
'
ee/security_dashboard/store/modules/filters/constants
'
;
import
{
BASE_FILTERS
}
from
'
ee/security_dashboard/store/modules/filters/constants
'
;
import
{
REPORT_TYPES
,
SEVERITY_LEVELS
}
from
'
ee/security_dashboard/store/constants
'
;
import
{
VULNERABILITY_STATES
}
from
'
ee/vulnerabilities/constants
'
;
import
{
convertObjectPropsToSnakeCase
}
from
'
~/lib/utils/common_utils
'
;
...
...
@@ -11,41 +11,41 @@ const parseOptions = obj =>
export
const
mapProjects
=
projects
=>
projects
.
map
(
p
=>
({
id
:
p
.
id
.
split
(
'
/
'
).
pop
(),
name
:
p
.
name
}));
export
const
initFirstClassVulnerabilityFilters
=
projects
=>
{
const
filters
=
[
{
name
:
s__
(
'
SecurityReports|Status
'
),
id
:
'
state
'
,
options
:
[
{
id
:
ALL
,
name
:
s__
(
'
VulnerabilityStatusTypes|All
'
)
},
...
parseOptions
(
VULNERABILITY_STATES
),
],
selection
:
new
Set
([
ALL
]),
},
{
name
:
s__
(
'
SecurityReports|Severity
'
),
id
:
'
severity
'
,
options
:
[
BASE_FILTERS
.
severity
,
...
parseOptions
(
SEVERITY_LEVELS
)],
selection
:
new
Set
([
ALL
]),
},
{
name
:
s__
(
'
Reports|Scanner
'
),
id
:
'
reportType
'
,
options
:
[
BASE_FILTERS
.
report_type
,
...
parseOptions
(
REPORT_TYPES
)],
selection
:
new
Set
([
ALL
]),
},
];
const
stateOptions
=
parseOptions
(
VULNERABILITY_STATES
);
const
defaultStateOptions
=
stateOptions
.
filter
(
x
=>
[
'
DETECTED
'
,
'
CONFIRMED
'
].
includes
(
x
.
id
));
if
(
Array
.
isArray
(
projects
))
{
filters
.
push
({
name
:
s__
(
'
SecurityReports|Project
'
),
id
:
'
projectId
'
,
options
:
[
BASE_FILTERS
.
project_id
,
...
mapProjects
(
projects
)],
selection
:
new
Set
([
ALL
]),
});
}
export
const
stateFilter
=
{
name
:
s__
(
'
SecurityReports|Status
'
),
id
:
'
state
'
,
options
:
stateOptions
,
allOption
:
BASE_FILTERS
.
state
,
defaultOptions
:
defaultStateOptions
,
};
export
const
severityFilter
=
{
name
:
s__
(
'
SecurityReports|Severity
'
),
id
:
'
severity
'
,
options
:
parseOptions
(
SEVERITY_LEVELS
),
allOption
:
BASE_FILTERS
.
severity
,
defaultOptions
:
[],
};
export
const
scannerFilter
=
{
name
:
s__
(
'
Reports|Scanner
'
),
id
:
'
reportType
'
,
options
:
parseOptions
(
REPORT_TYPES
),
allOption
:
BASE_FILTERS
.
report_type
,
defaultOptions
:
[],
};
return
filters
;
export
const
getProjectFilter
=
projects
=>
{
return
{
name
:
s__
(
'
SecurityReports|Project
'
),
id
:
'
projectId
'
,
options
:
mapProjects
(
projects
),
allOption
:
BASE_FILTERS
.
project_id
,
defaultOptions
:
[],
};
};
/**
...
...
ee/app/assets/javascripts/security_dashboard/store/modules/filters/constants.js
View file @
2ba8c796
...
...
@@ -7,6 +7,10 @@ export const STATE = {
};
export
const
BASE_FILTERS
=
{
state
:
{
name
:
s__
(
'
VulnerabilityStatusTypes|All
'
),
id
:
ALL
,
},
severity
:
{
name
:
s__
(
'
ciReport|All severities
'
),
id
:
ALL
,
...
...
ee/spec/frontend/security_dashboard/components/filters/filter_body_spec.js
View file @
2ba8c796
...
...
@@ -33,17 +33,19 @@ describe('Filter Body component', () => {
describe
(
'
dropdown button
'
,
()
=>
{
it
(
'
shows the selected option name if only one option is selected
'
,
()
=>
{
const
props
=
{
selectedOptions
:
[
'
Some Selected Option
'
]
};
const
option
=
{
name
:
'
Some Selected Option
'
};
const
props
=
{
selectedOptions
:
[
option
]
};
createComponent
(
props
);
expect
(
dropdownButton
().
text
()).
toBe
(
props
.
selectedOptions
[
0
]
);
expect
(
dropdownButton
().
text
()).
toBe
(
option
.
name
);
});
it
(
'
shows the selected option name and "+x more" if more than one option is selected
'
,
()
=>
{
const
props
=
{
selectedOptions
:
[
'
Option 1
'
,
'
Option 2
'
,
'
Option 3
'
]
};
const
options
=
[{
name
:
'
Option 1
'
},
{
name
:
'
Option 2
'
},
{
name
:
'
Option 3
'
}];
const
props
=
{
selectedOptions
:
options
};
createComponent
(
props
);
expect
(
dropdownButton
().
text
()).
toMatch
(
/Option 1
\s
+
\+
2 more/
);
expect
(
dropdownButton
().
text
()).
toMatch
InterpolatedText
(
'
Option 1 +2 more
'
);
});
});
...
...
ee/spec/frontend/security_dashboard/components/filters/standard_filter_spec.js
View file @
2ba8c796
import
{
GlDropdown
,
GlDropdownItem
,
GlSearchBoxByType
}
from
'
@gitlab/ui
'
;
import
StandardFilter
from
'
ee/security_dashboard/components/filters/standard_filter.vue
'
;
import
{
mount
}
from
'
@vue/test-utils
'
;
import
{
trimText
}
from
'
helpers/text_helper
'
;
import
FilterBody
from
'
ee/security_dashboard/components/filters/filter_body.vue
'
;
import
{
createLocalVue
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
VueRouter
from
'
vue-router
'
;
const
generateOption
=
index
=>
({
name
:
`Option
${
index
}
`
,
id
:
`option-
${
index
}
`
,
});
const
localVue
=
createLocalVue
();
localVue
.
use
(
VueRouter
);
const
router
=
new
VueRouter
();
const
generateOptions
=
length
=>
Array
.
from
({
length
}).
map
((
_
,
i
)
=>
({
name
:
`Option
${
i
}
`
,
id
:
`option-
${
i
}
`
,
index
:
i
}));
const
generateOptions
=
length
=>
{
return
Array
.
from
({
length
}).
map
((
_
,
i
)
=>
generateOption
(
i
));
const
filter
=
{
id
:
'
filter
'
,
name
:
'
filter
'
,
options
:
generateOptions
(
12
),
allOption
:
{
id
:
'
allOptionId
'
},
defaultOptions
:
[],
};
const
optionsAt
=
indexes
=>
filter
.
options
.
filter
(
x
=>
indexes
.
includes
(
x
.
index
));
const
optionIdsAt
=
indexes
=>
optionsAt
(
indexes
).
map
(
x
=>
x
.
id
);
describe
(
'
Standard Filter component
'
,
()
=>
{
let
wrapper
;
const
createWrapper
=
propsData
=>
{
wrapper
=
mount
(
StandardFilter
,
{
propsData
});
const
createWrapper
=
(
filterOptions
,
showSearchBox
)
=>
{
wrapper
=
shallowMount
(
StandardFilter
,
{
localVue
,
router
,
propsData
:
{
filter
:
{
...
filter
,
...
filterOptions
},
showSearchBox
},
});
};
const
dropdownItems
=
()
=>
wrapper
.
findAll
(
'
[data-testid="filterOption"]
'
);
const
dropdownItemAt
=
index
=>
dropdownItems
().
at
(
index
);
const
allOptionItem
=
()
=>
wrapper
.
find
(
'
[data-testid="allOption"]
'
);
const
isChecked
=
item
=>
item
.
props
(
'
isChecked
'
);
const
filterQuery
=
()
=>
wrapper
.
vm
.
$route
.
query
[
filter
.
id
];
const
filterBody
=
()
=>
wrapper
.
find
(
FilterBody
);
const
clickAllOptionItem
=
async
()
=>
{
allOptionItem
().
vm
.
$emit
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
};
const
clickItemAt
=
async
index
=>
{
dropdownItemAt
(
index
).
vm
.
$emit
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
};
const
findSearchBox
=
()
=>
wrapper
.
find
(
GlSearchBoxByType
);
const
isDropdownOpen
=
()
=>
wrapper
.
find
(
GlDropdown
).
classes
(
'
show
'
);
const
dropdownItemsCount
=
()
=>
wrapper
.
findAll
(
GlDropdownItem
).
length
;
const
expectSelectedItems
=
indexes
=>
{
const
checkedIndexes
=
dropdownItems
().
wrappers
.
map
(
item
=>
isChecked
(
item
));
const
expectedIndexes
=
Array
.
from
({
length
:
checkedIndexes
.
length
}).
map
((
_
,
index
)
=>
indexes
.
includes
(
index
),
);
expect
(
checkedIndexes
).
toEqual
(
expectedIndexes
);
};
const
expectAllOptionSelected
=
()
=>
{
expect
(
isChecked
(
allOptionItem
())).
toBe
(
true
);
const
checkedIndexes
=
dropdownItems
().
wrappers
.
map
(
item
=>
isChecked
(
item
));
const
expectedIndexes
=
new
Array
(
checkedIndexes
.
length
).
fill
(
false
);
expect
(
checkedIndexes
).
toEqual
(
expectedIndexes
);
};
afterEach
(()
=>
{
// Clear out the querystring if one exists. It persists between tests.
if
(
filterQuery
())
{
wrapper
.
vm
.
$router
.
push
(
'
/
'
);
}
wrapper
.
destroy
();
});
describe
(
'
severity
'
,
()
=>
{
let
options
;
describe
(
'
filter options
'
,
()
=>
{
it
(
'
shows the filter options
'
,
()
=>
{
createWrapper
();
expect
(
dropdownItems
()).
toHaveLength
(
filter
.
options
.
length
);
});
it
(
'
initially selects the default options
'
,
()
=>
{
const
ids
=
[
2
,
5
,
7
];
createWrapper
({
defaultOptions
:
optionsAt
(
ids
)
});
expectSelectedItems
(
ids
);
});
it
(
'
initially selects the All option if there are no default options
'
,
()
=>
{
createWrapper
();
expectAllOptionSelected
();
});
});
describe
(
'
search box
'
,
()
=>
{
it
.
each
`
phrase | showSearchBox
${
'
shows
'
}
|
${
true
}
${
'
does not show
'
}
|
${
false
}
`
(
'
$phrase search box if showSearchBox is $showSearchBox
'
,
({
showSearchBox
})
=>
{
createWrapper
({},
showSearchBox
);
expect
(
filterBody
().
props
(
'
showSearchBox
'
)).
toBe
(
showSearchBox
);
});
it
(
'
filters options when something is typed in the search box
'
,
async
()
=>
{
const
expectedItems
=
filter
.
options
.
map
(
x
=>
x
.
name
).
filter
(
x
=>
x
.
includes
(
'
1
'
));
createWrapper
({},
true
);
filterBody
().
vm
.
$emit
(
'
input
'
,
'
1
'
);
await
wrapper
.
vm
.
$nextTick
();
expect
(
dropdownItems
()).
toHaveLength
(
3
);
expect
(
dropdownItems
().
wrappers
.
map
(
x
=>
x
.
props
(
'
text
'
))).
toEqual
(
expectedItems
);
});
});
describe
(
'
selecting options
'
,
()
=>
{
beforeEach
(()
=>
{
options
=
generateOptions
(
8
);
const
filter
=
{
name
:
'
Severity
'
,
id
:
'
severity
'
,
options
,
selection
:
new
Set
([
options
[
0
].
id
,
options
[
1
].
id
,
options
[
2
].
id
]),
createWrapper
({
defaultOptions
:
optionsAt
([
1
,
2
,
3
])
});
});
it
(
'
de-selects every option and selects the All option when all option is clicked
'
,
async
()
=>
{
const
clickAndCheck
=
async
()
=>
{
await
clickAllOptionItem
();
expectAllOptionSelected
();
};
createWrapper
({
filter
});
// Click the all option 3 times. We're checking that it doesn't toggle.
await
clickAndCheck
();
await
clickAndCheck
();
await
clickAndCheck
();
});
it
(
'
should display all 8 severity options
'
,
()
=>
{
expect
(
dropdownItemsCount
()).
toEqual
(
8
);
it
(
`toggles an option's selection when it it repeatedly clicked`
,
async
()
=>
{
const
item
=
dropdownItems
().
at
(
5
);
let
checkedState
=
isChecked
(
item
);
const
clickAndCheck
=
async
()
=>
{
await
clickItemAt
(
5
);
expect
(
isChecked
(
item
)).
toBe
(
!
checkedState
);
checkedState
=
!
checkedState
;
};
// Click the option 3 times. We're checking that toggles.
await
clickAndCheck
();
await
clickAndCheck
();
await
clickAndCheck
();
});
it
(
'
should display a check next to only the selected items
'
,
()
=>
{
expect
(
wrapper
.
findAll
(
`[data-testid="mobile-issue-close-icon"]:not(.gl-visibility-hidden)`
),
).
toHaveLength
(
3
);
it
(
'
multi-selects options when multiple items are clicked
'
,
async
()
=>
{
await
[
5
,
6
,
7
].
forEach
(
clickItemAt
);
expectSelectedItems
([
1
,
2
,
3
,
5
,
6
,
7
]
);
});
it
(
'
s
hould correctly display the selected text
'
,
()
=>
{
const
selectedText
=
trimText
(
wrapper
.
find
(
'
.dropdown-toggle
'
).
text
()
);
it
(
'
s
elects the All option when last selected option is unselected
'
,
async
()
=>
{
await
[
1
,
2
,
3
].
forEach
(
clickItemAt
);
expect
(
selectedText
).
toBe
(
`
${
options
[
0
].
name
}
+2 more`
);
expect
AllOptionSelected
(
);
});
it
(
'
should display "Severity" as the option name
'
,
()
=>
{
expect
(
wrapper
.
find
(
'
[data-testid="name"]
'
).
text
()).
toEqual
(
'
Severity
'
);
it
(
'
emits filter-changed event with default options when created
'
,
async
()
=>
{
const
expectedIds
=
optionIdsAt
([
1
,
2
,
3
]);
expect
(
wrapper
.
emitted
(
'
filter-changed
'
)).
toHaveLength
(
1
);
expect
(
wrapper
.
emitted
(
'
filter-changed
'
)[
0
][
0
]).
toEqual
({
[
filter
.
id
]:
expectedIds
});
});
it
(
'
should not have a search box
'
,
()
=>
{
expect
(
findSearchBox
().
exists
()).
toBe
(
false
);
it
(
'
emits filter-changed event when an option is clicked
'
,
async
()
=>
{
const
expectedIds
=
optionIdsAt
([
1
,
2
,
3
,
4
]);
await
clickItemAt
(
4
);
expect
(
wrapper
.
emitted
(
'
filter-changed
'
)).
toHaveLength
(
2
);
expect
(
wrapper
.
emitted
(
'
filter-changed
'
)[
1
][
0
]).
toEqual
({
[
filter
.
id
]:
expectedIds
});
});
});
it
(
'
should not be open
'
,
()
=>
{
expect
(
isDropdownOpen
()).
toBe
(
false
);
describe
(
'
filter querystring
'
,
()
=>
{
const
updateRouteQuery
=
async
ids
=>
{
// window.history.back() won't change the location nor fire the popstate event, so we need
// to fake it by doing it manually.
router
.
replace
({
query
:
{
[
filter
.
id
]:
ids
}
});
window
.
dispatchEvent
(
new
Event
(
'
popstate
'
));
await
wrapper
.
vm
.
$nextTick
();
};
describe
(
'
clicking on items
'
,
()
=>
{
it
(
'
updates the querystring when options are clicked
'
,
async
()
=>
{
createWrapper
();
const
clickedIds
=
[];
[
1
,
3
,
5
].
forEach
(
index
=>
{
clickItemAt
(
index
);
clickedIds
.
push
(
optionIdsAt
([
index
])[
0
]);
expect
(
filterQuery
()).
toEqual
(
clickedIds
);
});
});
it
(
'
sets the querystring properly when the All option is clicked
'
,
async
()
=>
{
createWrapper
();
[
1
,
2
,
3
,
4
].
forEach
(
clickItemAt
);
expect
(
filterQuery
()).
toHaveLength
(
4
);
await
clickAllOptionItem
();
expect
(
filterQuery
()).
toEqual
([
filter
.
allOption
.
id
]);
});
});
describe
(
'
when the dropdown is open
'
,
()
=>
{
beforeEach
(
done
=>
{
wrapper
.
find
(
'
.dropdown-toggle
'
).
trigger
(
'
click
'
);
wrapper
.
vm
.
$root
.
$on
(
'
bv::dropdown::shown
'
,
()
=>
done
());
describe
(
'
querystring on page load
'
,
()
=>
{
it
(
'
selects correct items
'
,
()
=>
{
updateRouteQuery
(
optionIdsAt
([
1
,
3
,
5
,
7
]));
createWrapper
();
expectSelectedItems
([
1
,
3
,
5
,
7
]);
});
it
(
'
s
hould keep the menu open after clicking on an item
'
,
async
()
=>
{
expect
(
isDropdownOpen
()).
toBe
(
true
);
wrapper
.
find
(
GlDropdownItem
).
trigger
(
'
click
'
);
await
wrapper
.
vm
.
$nextTick
();
it
(
'
s
elects only valid items when querystring has valid and invalid IDs
'
,
async
()
=>
{
const
ids
=
optionIdsAt
([
2
,
4
,
6
]).
concat
([
'
some
'
,
'
invalid
'
,
'
ids
'
]
);
updateRouteQuery
(
ids
);
createWrapper
();
expect
(
isDropdownOpen
()).
toBe
(
true
);
expectSelectedItems
([
2
,
4
,
6
]);
});
it
(
'
selects default options if querystring only has invalid items
'
,
async
()
=>
{
updateRouteQuery
([
'
some
'
,
'
invalid
'
,
'
ids
'
]);
createWrapper
({
defaultOptions
:
optionsAt
([
4
,
5
,
8
])
});
expectSelectedItems
([
4
,
5
,
8
]);
});
it
(
'
selects All option if querystring only has invalid IDs and there are no default options
'
,
async
()
=>
{
updateRouteQuery
([
'
some
'
,
'
invalid
'
,
'
ids
'
]);
createWrapper
();
expectAllOptionSelected
();
});
});
});
describe
(
'
Project
'
,
()
=>
{
describe
(
'
when there are lots of projects
'
,
()
=>
{
const
LOTS
=
30
;
describe
(
'
changing the querystring
'
,
()
=>
{
it
(
'
selects the correct options
'
,
async
()
=>
{
createWrapper
();
const
indexes
=
[
3
,
5
,
7
];
await
updateRouteQuery
(
optionIdsAt
(
indexes
));
expectSelectedItems
(
indexes
);
});
it
(
'
select default options when querystring is blank
'
,
async
()
=>
{
createWrapper
({
defaultOptions
:
optionsAt
([
2
,
5
,
8
])
});
await
clickItemAt
(
3
);
expectSelectedItems
([
2
,
3
,
5
,
8
]);
await
updateRouteQuery
([]);
expectSelectedItems
([
2
,
5
,
8
]);
});
beforeEach
(()
=>
{
const
options
=
generateOptions
(
LOTS
);
const
filter
=
{
name
:
'
Project
'
,
id
:
'
project
'
,
options
,
selection
:
new
Set
([
options
[
0
].
id
]),
};
it
(
'
selects All option when querystring is blank and there are no default options
'
,
async
()
=>
{
createWrapper
();
createWrapper
({
filter
});
await
clickItemAt
(
3
);
expectSelectedItems
([
3
]);
await
updateRouteQuery
([]);
expectAllOptionSelected
();
});
it
(
'
should display a search box
'
,
()
=>
{
expect
(
findSearchBox
().
exists
()).
toBe
(
true
);
it
(
'
selects All option when querystring has all option ID
'
,
async
()
=>
{
createWrapper
({
defaultOptions
:
optionsAt
([
2
,
4
,
8
])
});
expectSelectedItems
([
2
,
4
,
8
]);
await
updateRouteQuery
([
filter
.
allOption
.
id
]);
expectAllOptionSelected
();
});
it
(
'
selects All option if querystring has all option ID as well as other IDs
'
,
async
()
=>
{
createWrapper
({
defaultOptions
:
optionsAt
([
5
,
6
,
9
])
});
await
updateRouteQuery
([
filter
.
allOption
.
id
,
...
optionIdsAt
([
1
,
2
])]);
expectAllOptionSelected
();
});
it
(
`should show all projects`
,
()
=>
{
expect
(
dropdownItemsCount
()).
toBe
(
LOTS
);
it
(
'
selects only valid items when querystring has valid and invalid IDs
'
,
async
()
=>
{
createWrapper
();
const
ids
=
optionIdsAt
([
3
,
7
,
9
]).
concat
([
'
some
'
,
'
invalid
'
,
'
ids
'
]);
await
updateRouteQuery
(
ids
);
expectSelectedItems
([
3
,
7
,
9
]);
});
it
(
'
selects default options if querystring only has invalid IDs
'
,
async
()
=>
{
createWrapper
({
defaultOptions
:
optionsAt
([
1
,
3
,
4
])
});
await
clickItemAt
(
8
);
expectSelectedItems
([
1
,
3
,
4
,
8
]);
await
updateRouteQuery
([
'
some
'
,
'
invalid
'
,
'
ids
'
]);
expectSelectedItems
([
1
,
3
,
4
]);
});
it
(
'
selects All option if querystring only has invalid IDs and there are no default options
'
,
async
()
=>
{
createWrapper
();
await
clickItemAt
(
8
);
expectSelectedItems
([
8
]);
await
updateRouteQuery
([
'
some
'
,
'
invalid
'
,
'
ids
'
]);
expectAllOptionSelected
();
});
it
(
'
should show only matching projects when a search term is entered
'
,
async
()
=>
{
findSearchBox
().
vm
.
$emit
(
'
input
'
,
'
0
'
);
it
(
'
does not change querystring for another filter when updating querystring for current filter
'
,
async
()
=>
{
createWrapper
();
const
ids
=
optionIdsAt
([
1
,
2
,
3
]);
const
other
=
[
'
6
'
,
'
7
'
,
'
8
'
];
const
query
=
{
[
filter
.
id
]:
ids
,
other
};
router
.
replace
({
query
});
window
.
dispatchEvent
(
new
Event
(
'
popstate
'
));
await
wrapper
.
vm
.
$nextTick
();
expect
(
dropdownItemsCount
()).
toBe
(
3
);
expectSelectedItems
([
1
,
2
,
3
]);
expect
(
wrapper
.
vm
.
$route
.
query
.
other
).
toEqual
(
other
);
});
});
});
...
...
ee/spec/frontend/security_dashboard/components/filters_spec.js
View file @
2ba8c796
import
Vuex
from
'
vuex
'
;
import
Filters
from
'
ee/security_dashboard/components/filters.vue
'
;
import
createStore
from
'
ee/security_dashboard/store
'
;
import
{
m
ount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
shallowM
ount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
const
localVue
=
createLocalVue
();
localVue
.
use
(
Vuex
);
...
...
@@ -11,7 +11,7 @@ describe('Filter component', () => {
let
store
;
const
createWrapper
=
(
props
=
{})
=>
{
wrapper
=
m
ount
(
Filters
,
{
wrapper
=
shallowM
ount
(
Filters
,
{
localVue
,
store
,
propsData
:
{
...
...
ee/spec/frontend/security_dashboard/components/first_class_vulnerability_filters_spec.js
View file @
2ba8c796
import
VueRouter
from
'
vue-router
'
;
import
{
createLocalVue
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
initFirstClassVulnerabilityFilters
}
from
'
ee/security_dashboard/helpers
'
;
import
Filters
from
'
ee/security_dashboard/components/first_class_vulnerability_filters.vue
'
;
import
StandardFilter
from
'
ee/security_dashboard/components/filters/standard_filter.vue
'
;
...
...
@@ -10,7 +9,6 @@ localVue.use(VueRouter);
describe
(
'
First class vulnerability filters component
'
,
()
=>
{
let
wrapper
;
let
filters
;
const
projects
=
[
{
id
:
'
gid://gitlab/Project/11
'
,
name
:
'
GitLab Org
'
},
...
...
@@ -18,11 +16,8 @@ describe('First class vulnerability filters component', () => {
];
const
findFilters
=
()
=>
wrapper
.
findAll
(
StandardFilter
);
const
findStateFilter
=
()
=>
findFilters
().
at
(
0
);
const
findSeverityFilter
=
()
=>
findFilters
().
at
(
1
);
const
findReportTypeFilter
=
()
=>
findFilters
().
at
(
2
);
const
findProjectFilter
=
()
=>
findFilters
().
at
(
3
);
const
findLastFilter
=
()
=>
findFilters
().
at
(
filters
.
length
-
1
);
const
findStateFilter
=
()
=>
wrapper
.
find
(
'
[data-testid="state"]
'
);
const
findProjectFilter
=
()
=>
wrapper
.
find
(
'
[data-testid="projectId"]
'
);
const
createComponent
=
({
propsData
,
listeners
}
=
{})
=>
{
return
shallowMount
(
Filters
,
{
localVue
,
router
,
propsData
,
listeners
});
...
...
@@ -36,190 +31,39 @@ describe('First class vulnerability filters component', () => {
describe
(
'
on render without project filter
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
();
filters
=
initFirstClassVulnerabilityFilters
();
});
it
(
'
should render the filters
'
,
()
=>
{
expect
(
findFilters
()).
toHaveLength
(
filters
.
length
);
it
(
'
should render the
default
filters
'
,
()
=>
{
expect
(
findFilters
()).
toHaveLength
(
3
);
});
it
(
'
should call the setFilter mutation when setting a filter
'
,
()
=>
{
const
stub
=
jest
.
fn
();
it
(
'
should emit filterChange when a filter is changed
'
,
()
=>
{
const
options
=
{
foo
:
'
bar
'
};
findStateFilter
().
vm
.
$emit
(
'
filter-changed
'
,
options
);
wrapper
.
setMethods
({
setFilter
:
stub
});
findStateFilter
().
vm
.
$emit
(
'
setFilter
'
,
options
);
expect
(
stub
).
toHaveBeenCalledWith
(
options
);
expect
(
wrapper
.
emitted
(
'
filterChange
'
)[
0
][
0
]).
toEqual
(
options
);
});
});
describe
(
'
when project filter is populated dynamically
'
,
()
=>
{
beforeEach
(()
=>
{
filters
=
initFirstClassVulnerabilityFilters
([]);
wrapper
=
createComponent
({
propsData
:
{
projects
:
[]
}
});
wrapper
=
createComponent
();
});
it
(
'
should render the project filter with one option
'
,
()
=>
{
expect
(
findLastFilter
().
props
(
'
filter
'
)).
toEqual
({
id
:
'
projectId
'
,
name
:
'
Project
'
,
options
:
[{
id
:
'
all
'
,
name
:
'
All projects
'
}],
selection
:
new
Set
([
'
all
'
]),
});
it
(
'
should render the project filter with no options
'
,
async
()
=>
{
wrapper
.
setProps
({
projects
:
[]
});
await
wrapper
.
vm
.
$nextTick
();
expect
(
findProjectFilter
().
props
(
'
filter
'
).
options
).
toHaveLength
(
0
);
});
it
(
'
should
set the projects dynamically
'
,
()
=>
{
it
(
'
should
render the project filter with the expected options
'
,
async
()
=>
{
wrapper
.
setProps
({
projects
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
findLastFilter
().
props
(
'
filter
'
)).
toEqual
(
expect
.
objectContaining
({
options
:
[
{
id
:
'
all
'
,
name
:
'
All projects
'
},
{
id
:
'
11
'
,
name
:
'
GitLab Org
'
},
{
id
:
'
12
'
,
name
:
'
GitLab Com
'
},
],
}),
);
});
});
});
describe
(
'
when project filter is ready on mount
'
,
()
=>
{
beforeEach
(()
=>
{
filters
=
initFirstClassVulnerabilityFilters
([]);
wrapper
=
createComponent
({
propsData
:
{
projects
}
});
});
it
(
'
should set the projects dynamically
'
,
()
=>
{
expect
(
findLastFilter
().
props
(
'
filter
'
)).
toEqual
(
expect
.
objectContaining
({
options
:
[
{
id
:
'
all
'
,
name
:
'
All projects
'
},
{
id
:
'
11
'
,
name
:
'
GitLab Org
'
},
{
id
:
'
12
'
,
name
:
'
GitLab Com
'
},
],
}),
);
});
});
describe
(
'
when no filter is persisted in the URL
'
,
()
=>
{
beforeEach
(()
=>
{
wrapper
=
createComponent
({
propsData
:
{
projects
},
});
});
it
(
'
should redirect the user to an updated the URL and default the filters to CONFIRMED + DETECTED state
'
,
()
=>
{
expect
(
findStateFilter
().
props
(
'
filter
'
)).
toEqual
(
expect
.
objectContaining
({
selection
:
new
Set
([
'
DETECTED
'
,
'
CONFIRMED
'
]),
}),
);
});
});
describe
.
each
`
filter | value | selector
${
'
state
'
}
|
${
'
DETECTED,DISMISSED
'
}
|
${
findStateFilter
}
${
'
severity
'
}
|
${
'
MEDIUM
'
}
|
${
findSeverityFilter
}
${
'
reportType
'
}
|
${
'
SAST
'
}
|
${
findReportTypeFilter
}
${
'
projectId
'
}
|
${
'
12
'
}
|
${
findProjectFilter
}
`
(
'
when filters are persisted
'
,
({
filter
,
value
,
selector
})
=>
{
describe
(
`with filter set to
${
filter
}
:
${
value
}
`
,
()
=>
{
let
filterChangeSpy
;
beforeEach
(()
=>
{
filterChangeSpy
=
jest
.
fn
();
wrapper
=
createComponent
({
propsData
:
{
projects
},
listeners
:
{
filterChange
:
filterChangeSpy
},
});
// reset the router query in-between test cases
router
.
push
({
query
:
{}
});
router
.
push
({
query
:
{
[
filter
]:
value
.
split
(
'
,
'
)
}
},
()
=>
{});
});
it
(
`should have the
${
filter
}
filter as pre-selected`
,
()
=>
{
expect
(
selector
().
props
(
'
filter
'
).
selection
).
toEqual
(
new
Set
(
value
.
split
(
'
,
'
)));
});
it
(
'
should emit a filterChange event
'
,
()
=>
{
expect
(
wrapper
.
emitted
().
filterChange
).
toBeTruthy
();
});
it
(
'
should not trigger the filterChange additonally when the filters do not change
'
,
()
=>
{
router
.
push
({
query
:
{
...
wrapper
.
vm
.
$route
.
query
,
'
some-unrelated-query-param
'
:
'
true
'
,
},
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
filterChangeSpy
).
toHaveBeenCalledTimes
(
1
);
});
});
it
(
'
should trigger the filterChange when the filters are reset
'
,
()
=>
{
router
.
push
({
query
:
{}
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
filterChangeSpy
).
toHaveBeenNthCalledWith
(
2
,
{});
});
});
it
(
'
should reset the filters when the URL contains no more filters
'
,
()
=>
{
router
.
push
({
query
:
{}
});
return
wrapper
.
vm
.
$nextTick
(()
=>
{
expect
(
selector
().
props
(
'
filter
'
).
selection
).
toEqual
(
new
Set
([
'
all
'
]));
});
});
});
});
describe
.
each
`
filter | selector | index
${
'
state
'
}
|
${
findStateFilter
}
|
${
0
}
${
'
severity
'
}
|
${
findSeverityFilter
}
|
${
1
}
${
'
reportType
'
}
|
${
findReportTypeFilter
}
|
${
2
}
${
'
projectId
'
}
|
${
findProjectFilter
}
|
${
3
}
`
(
'
when setFilter is called
'
,
({
filter
,
selector
,
index
})
=>
{
describe
(
filter
,
()
=>
{
let
filterId
;
let
optionId
;
let
routePushSpy
;
beforeEach
(()
=>
{
filters
=
initFirstClassVulnerabilityFilters
(
projects
);
filterId
=
filters
[
index
].
id
;
optionId
=
filters
[
index
].
options
[
1
].
id
;
wrapper
=
createComponent
({
propsData
:
{
projects
}
});
routePushSpy
=
jest
.
spyOn
(
router
,
'
push
'
);
selector
().
vm
.
$emit
(
'
setFilter
'
,
{
optionId
,
filterId
});
});
afterEach
(()
=>
{
// This will reset the query state
router
.
push
(
'
/
'
);
});
it
(
'
should set the filter
'
,
()
=>
{
expect
(
selector
().
props
(
'
filter
'
).
selection
).
toEqual
(
new
Set
([
optionId
]));
});
it
(
'
should emit a filterChange event
'
,
()
=>
{
expect
(
wrapper
.
emitted
().
filterChange
).
toBeTruthy
();
});
await
wrapper
.
vm
.
$nextTick
();
it
(
'
should update the path
'
,
()
=>
{
expect
(
routePushSpy
).
toHaveBeenCalledWith
({
query
:
{
[
filterId
]:
[
optionId
]
},
});
});
expect
(
findProjectFilter
().
props
(
'
filter
'
).
options
).
toEqual
([
{
id
:
'
11
'
,
name
:
projects
[
0
].
name
},
{
id
:
'
12
'
,
name
:
projects
[
1
].
name
},
]);
});
});
});
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