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
f38af547
Commit
f38af547
authored
Dec 06, 2021
by
Frédéric Caplette
Committed by
Andrew Fontaine
Dec 06, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix broken Segmented control in the pipeline page
parent
7ab1bd63
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
73 additions
and
46 deletions
+73
-46
app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
...cripts/pipelines/components/graph/graph_view_selector.vue
+24
-15
spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
.../frontend/pipelines/graph/graph_component_wrapper_spec.js
+22
-22
spec/frontend/pipelines/graph/graph_view_selector_spec.js
spec/frontend/pipelines/graph/graph_view_selector_spec.js
+27
-9
No files found.
app/assets/javascripts/pipelines/components/graph/graph_view_selector.vue
View file @
f38af547
<
script
>
<
script
>
import
{
GlAlert
,
Gl
LoadingIcon
,
GlSegmentedControl
,
GlToggle
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
,
Gl
Button
,
GlButtonGroup
,
GlLoadingIcon
,
GlToggle
}
from
'
@gitlab/ui
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
__
,
s__
}
from
'
~/locale
'
;
import
{
STAGE_VIEW
,
LAYER_VIEW
}
from
'
./constants
'
;
import
{
STAGE_VIEW
,
LAYER_VIEW
}
from
'
./constants
'
;
...
@@ -7,8 +7,9 @@ export default {
...
@@ -7,8 +7,9 @@ export default {
name
:
'
GraphViewSelector
'
,
name
:
'
GraphViewSelector
'
,
components
:
{
components
:
{
GlAlert
,
GlAlert
,
GlButton
,
GlButtonGroup
,
GlLoadingIcon
,
GlLoadingIcon
,
GlSegmentedControl
,
GlToggle
,
GlToggle
,
},
},
props
:
{
props
:
{
...
@@ -96,6 +97,9 @@ export default {
...
@@ -96,6 +97,9 @@ export default {
this
.
hoverTipDismissed
=
true
;
this
.
hoverTipDismissed
=
true
;
this
.
$emit
(
'
dismissHoverTip
'
);
this
.
$emit
(
'
dismissHoverTip
'
);
},
},
isCurrentType
(
type
)
{
return
this
.
segmentSelectedType
===
type
;
},
/*
/*
In both toggle methods, we use setTimeout so that the loading indicator displays,
In both toggle methods, we use setTimeout so that the loading indicator displays,
then the work is done to update the DOM. The process is:
then the work is done to update the DOM. The process is:
...
@@ -110,11 +114,14 @@ export default {
...
@@ -110,11 +114,14 @@ export default {
See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details.
See https://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ for more details.
*/
*/
toggleView
(
type
)
{
setViewType
(
type
)
{
this
.
isSwitcherLoading
=
true
;
if
(
!
this
.
isCurrentType
(
type
))
{
setTimeout
(()
=>
{
this
.
isSwitcherLoading
=
true
;
this
.
$emit
(
'
updateViewType
'
,
type
);
this
.
segmentSelectedType
=
type
;
});
setTimeout
(()
=>
{
this
.
$emit
(
'
updateViewType
'
,
type
);
});
}
},
},
toggleShowLinksActive
(
val
)
{
toggleShowLinksActive
(
val
)
{
this
.
isToggleLoading
=
true
;
this
.
isToggleLoading
=
true
;
...
@@ -136,14 +143,16 @@ export default {
...
@@ -136,14 +143,16 @@ export default {
size=
"lg"
size=
"lg"
/>
/>
<span
class=
"gl-font-weight-bold"
>
{{
$options
.
i18n
.
viewLabelText
}}
</span>
<span
class=
"gl-font-weight-bold"
>
{{
$options
.
i18n
.
viewLabelText
}}
</span>
<gl-segmented-control
<gl-button-group
class=
"gl-mx-4"
>
v-model=
"segmentSelectedType"
<gl-button
:options=
"viewTypesList"
v-for=
"viewType in viewTypesList"
:disabled=
"isSwitcherLoading"
:key=
"viewType.value"
data-testid=
"pipeline-view-selector"
:selected=
"isCurrentType(viewType.value)"
class=
"gl-mx-4"
@
click=
"setViewType(viewType.value)"
@
input=
"toggleView"
>
/>
{{
viewType
.
text
}}
</gl-button>
</gl-button-group>
<div
v-if=
"showLinksToggle"
class=
"gl-display-flex gl-align-items-center"
>
<div
v-if=
"showLinksToggle"
class=
"gl-display-flex gl-align-items-center"
>
<gl-toggle
<gl-toggle
...
...
spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
View file @
f38af547
import
{
GlAlert
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
,
Gl
Button
,
GlButtonGroup
,
Gl
LoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
MockAdapter
from
'
axios-mock-adapter
'
;
import
Vue
from
'
vue
'
;
import
Vue
,
{
nextTick
}
from
'
vue
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
VueApollo
from
'
vue-apollo
'
;
import
{
useLocalStorageSpy
}
from
'
helpers/local_storage_helper
'
;
import
{
useLocalStorageSpy
}
from
'
helpers/local_storage_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
import
createMockApollo
from
'
helpers/mock_apollo_helper
'
;
...
@@ -98,7 +98,6 @@ describe('Pipeline graph wrapper', () => {
...
@@ -98,7 +98,6 @@ describe('Pipeline graph wrapper', () => {
afterEach
(()
=>
{
afterEach
(()
=>
{
wrapper
.
destroy
();
wrapper
.
destroy
();
wrapper
=
null
;
});
});
beforeAll
(()
=>
{
beforeAll
(()
=>
{
...
@@ -136,7 +135,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -136,7 +135,7 @@ describe('Pipeline graph wrapper', () => {
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
createComponentWithApollo
();
createComponentWithApollo
();
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
does not display the loading icon
'
,
()
=>
{
it
(
'
does not display the loading icon
'
,
()
=>
{
...
@@ -165,7 +164,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -165,7 +164,7 @@ describe('Pipeline graph wrapper', () => {
getPipelineDetailsHandler
:
jest
.
fn
().
mockRejectedValue
(
new
Error
(
'
GraphQL error
'
)),
getPipelineDetailsHandler
:
jest
.
fn
().
mockRejectedValue
(
new
Error
(
'
GraphQL error
'
)),
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
does not display the loading icon
'
,
()
=>
{
it
(
'
does not display the loading icon
'
,
()
=>
{
...
@@ -189,7 +188,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -189,7 +188,7 @@ describe('Pipeline graph wrapper', () => {
},
},
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
does not display the loading icon
'
,
()
=>
{
it
(
'
does not display the loading icon
'
,
()
=>
{
...
@@ -211,7 +210,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -211,7 +210,7 @@ describe('Pipeline graph wrapper', () => {
createComponentWithApollo
();
createComponentWithApollo
();
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
.
queries
.
headerPipeline
,
'
refetch
'
);
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
.
queries
.
headerPipeline
,
'
refetch
'
);
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
.
queries
.
pipeline
,
'
refetch
'
);
jest
.
spyOn
(
wrapper
.
vm
.
$apollo
.
queries
.
pipeline
,
'
refetch
'
);
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
getGraph
().
vm
.
$emit
(
'
refreshPipelineGraph
'
);
getGraph
().
vm
.
$emit
(
'
refreshPipelineGraph
'
);
});
});
...
@@ -225,8 +224,8 @@ describe('Pipeline graph wrapper', () => {
...
@@ -225,8 +224,8 @@ describe('Pipeline graph wrapper', () => {
describe
(
'
when query times out
'
,
()
=>
{
describe
(
'
when query times out
'
,
()
=>
{
const
advanceApolloTimers
=
async
()
=>
{
const
advanceApolloTimers
=
async
()
=>
{
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
};
};
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
...
@@ -246,7 +245,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -246,7 +245,7 @@ describe('Pipeline graph wrapper', () => {
.
mockResolvedValueOnce
(
errorData
);
.
mockResolvedValueOnce
(
errorData
);
createComponentWithApollo
({
getPipelineDetailsHandler
:
failSucceedFail
});
createComponentWithApollo
({
getPipelineDetailsHandler
:
failSucceedFail
});
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
shows correct errors and does not overwrite populated data when data is empty
'
,
async
()
=>
{
it
(
'
shows correct errors and does not overwrite populated data when data is empty
'
,
async
()
=>
{
...
@@ -276,7 +275,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -276,7 +275,7 @@ describe('Pipeline graph wrapper', () => {
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
appears when pipeline uses needs
'
,
()
=>
{
it
(
'
appears when pipeline uses needs
'
,
()
=>
{
...
@@ -319,7 +318,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -319,7 +318,7 @@ describe('Pipeline graph wrapper', () => {
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
sets showLinks to true
'
,
async
()
=>
{
it
(
'
sets showLinks to true
'
,
async
()
=>
{
...
@@ -329,7 +328,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -329,7 +328,7 @@ describe('Pipeline graph wrapper', () => {
expect
(
getViewSelector
().
props
(
'
type
'
)).
toBe
(
LAYER_VIEW
);
expect
(
getViewSelector
().
props
(
'
type
'
)).
toBe
(
LAYER_VIEW
);
await
getDependenciesToggle
().
vm
.
$emit
(
'
change
'
,
true
);
await
getDependenciesToggle
().
vm
.
$emit
(
'
change
'
,
true
);
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
expect
(
wrapper
.
findComponent
(
LinksLayer
).
props
(
'
showLinks
'
)).
toBe
(
true
);
expect
(
wrapper
.
findComponent
(
LinksLayer
).
props
(
'
showLinks
'
)).
toBe
(
true
);
});
});
});
});
...
@@ -345,7 +344,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -345,7 +344,7 @@ describe('Pipeline graph wrapper', () => {
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
shows the hover tip in the view selector
'
,
async
()
=>
{
it
(
'
shows the hover tip in the view selector
'
,
async
()
=>
{
...
@@ -366,7 +365,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -366,7 +365,7 @@ describe('Pipeline graph wrapper', () => {
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
does not show the hover tip
'
,
async
()
=>
{
it
(
'
does not show the hover tip
'
,
async
()
=>
{
...
@@ -384,7 +383,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -384,7 +383,7 @@ describe('Pipeline graph wrapper', () => {
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
...
@@ -393,9 +392,10 @@ describe('Pipeline graph wrapper', () => {
...
@@ -393,9 +392,10 @@ describe('Pipeline graph wrapper', () => {
it
(
'
reads the view type from localStorage when available
'
,
()
=>
{
it
(
'
reads the view type from localStorage when available
'
,
()
=>
{
const
viewSelectorNeedsSegment
=
wrapper
const
viewSelectorNeedsSegment
=
wrapper
.
findAll
(
'
[data-testid="pipeline-view-selector"] > label
'
)
.
find
(
GlButtonGroup
)
.
findAllComponents
(
GlButton
)
.
at
(
1
);
.
at
(
1
);
expect
(
viewSelectorNeedsSegment
.
classes
()).
toContain
(
'
active
'
);
expect
(
viewSelectorNeedsSegment
.
classes
()).
toContain
(
'
selected
'
);
});
});
});
});
...
@@ -412,7 +412,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -412,7 +412,7 @@ describe('Pipeline graph wrapper', () => {
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
afterEach
(()
=>
{
afterEach
(()
=>
{
...
@@ -435,7 +435,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -435,7 +435,7 @@ describe('Pipeline graph wrapper', () => {
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
does not appear when pipeline does not use needs
'
,
()
=>
{
it
(
'
does not appear when pipeline does not use needs
'
,
()
=>
{
...
@@ -462,7 +462,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -462,7 +462,7 @@ describe('Pipeline graph wrapper', () => {
beforeEach
(
async
()
=>
{
beforeEach
(
async
()
=>
{
createComponentWithApollo
();
createComponentWithApollo
();
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
is not called
'
,
()
=>
{
it
(
'
is not called
'
,
()
=>
{
...
@@ -506,7 +506,7 @@ describe('Pipeline graph wrapper', () => {
...
@@ -506,7 +506,7 @@ describe('Pipeline graph wrapper', () => {
});
});
jest
.
runOnlyPendingTimers
();
jest
.
runOnlyPendingTimers
();
await
wrapper
.
vm
.
$
nextTick
();
await
nextTick
();
});
});
it
(
'
attempts to collect metrics
'
,
()
=>
{
it
(
'
attempts to collect metrics
'
,
()
=>
{
...
...
spec/frontend/pipelines/graph/graph_view_selector_spec.js
View file @
f38af547
import
{
GlAlert
,
Gl
LoadingIcon
,
GlSegmentedControl
}
from
'
@gitlab/ui
'
;
import
{
GlAlert
,
Gl
Button
,
GlButtonGroup
,
GlLoadingIcon
}
from
'
@gitlab/ui
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
mount
,
shallowMount
}
from
'
@vue/test-utils
'
;
import
{
LAYER_VIEW
,
STAGE_VIEW
}
from
'
~/pipelines/components/graph/constants
'
;
import
{
LAYER_VIEW
,
STAGE_VIEW
}
from
'
~/pipelines/components/graph/constants
'
;
import
GraphViewSelector
from
'
~/pipelines/components/graph/graph_view_selector.vue
'
;
import
GraphViewSelector
from
'
~/pipelines/components/graph/graph_view_selector.vue
'
;
...
@@ -7,9 +7,9 @@ describe('the graph view selector component', () => {
...
@@ -7,9 +7,9 @@ describe('the graph view selector component', () => {
let
wrapper
;
let
wrapper
;
const
findDependenciesToggle
=
()
=>
wrapper
.
find
(
'
[data-testid="show-links-toggle"]
'
);
const
findDependenciesToggle
=
()
=>
wrapper
.
find
(
'
[data-testid="show-links-toggle"]
'
);
const
findViewTypeSelector
=
()
=>
wrapper
.
findComponent
(
Gl
SegmentedControl
);
const
findViewTypeSelector
=
()
=>
wrapper
.
findComponent
(
Gl
ButtonGroup
);
const
findStageView
Label
=
()
=>
findViewTypeSelector
().
findAll
(
'
label
'
).
at
(
0
);
const
findStageView
Button
=
()
=>
findViewTypeSelector
().
findAllComponents
(
GlButton
).
at
(
0
);
const
findLayer
sViewLabel
=
()
=>
findViewTypeSelector
().
findAll
(
'
label
'
).
at
(
1
);
const
findLayer
ViewButton
=
()
=>
findViewTypeSelector
().
findAllComponents
(
GlButton
).
at
(
1
);
const
findSwitcherLoader
=
()
=>
wrapper
.
find
(
'
[data-testid="switcher-loading-state"]
'
);
const
findSwitcherLoader
=
()
=>
wrapper
.
find
(
'
[data-testid="switcher-loading-state"]
'
);
const
findToggleLoader
=
()
=>
findDependenciesToggle
().
find
(
GlLoadingIcon
);
const
findToggleLoader
=
()
=>
findDependenciesToggle
().
find
(
GlLoadingIcon
);
const
findHoverTip
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
const
findHoverTip
=
()
=>
wrapper
.
findComponent
(
GlAlert
);
...
@@ -51,8 +51,13 @@ describe('the graph view selector component', () => {
...
@@ -51,8 +51,13 @@ describe('the graph view selector component', () => {
createComponent
({
mountFn
:
mount
});
createComponent
({
mountFn
:
mount
});
});
});
it
(
'
shows the Stage view label as active in the selector
'
,
()
=>
{
it
(
'
shows the Stage view button as selected
'
,
()
=>
{
expect
(
findStageViewLabel
().
classes
()).
toContain
(
'
active
'
);
expect
(
findStageViewButton
().
classes
(
'
selected
'
)).
toBe
(
true
);
});
it
(
'
shows the Job dependencies view button not selected
'
,
()
=>
{
expect
(
findLayerViewButton
().
exists
()).
toBe
(
true
);
expect
(
findLayerViewButton
().
classes
(
'
selected
'
)).
toBe
(
false
);
});
});
it
(
'
does not show the Job dependencies (links) toggle
'
,
()
=>
{
it
(
'
does not show the Job dependencies (links) toggle
'
,
()
=>
{
...
@@ -70,8 +75,13 @@ describe('the graph view selector component', () => {
...
@@ -70,8 +75,13 @@ describe('the graph view selector component', () => {
});
});
});
});
it
(
'
shows the Job dependencies view label as active in the selector
'
,
()
=>
{
it
(
'
shows the Job dependencies view as selected
'
,
()
=>
{
expect
(
findLayersViewLabel
().
classes
()).
toContain
(
'
active
'
);
expect
(
findLayerViewButton
().
classes
(
'
selected
'
)).
toBe
(
true
);
});
it
(
'
shows the Stage button as not selected
'
,
()
=>
{
expect
(
findStageViewButton
().
exists
()).
toBe
(
true
);
expect
(
findStageViewButton
().
classes
(
'
selected
'
)).
toBe
(
false
);
});
});
it
(
'
shows the Job dependencies (links) toggle
'
,
()
=>
{
it
(
'
shows the Job dependencies (links) toggle
'
,
()
=>
{
...
@@ -94,7 +104,7 @@ describe('the graph view selector component', () => {
...
@@ -94,7 +104,7 @@ describe('the graph view selector component', () => {
expect
(
wrapper
.
emitted
().
updateViewType
).
toBeUndefined
();
expect
(
wrapper
.
emitted
().
updateViewType
).
toBeUndefined
();
expect
(
findSwitcherLoader
().
exists
()).
toBe
(
false
);
expect
(
findSwitcherLoader
().
exists
()).
toBe
(
false
);
await
findStageView
Label
().
trigger
(
'
click
'
);
await
findStageView
Button
().
trigger
(
'
click
'
);
/*
/*
Loading happens before the event is emitted or timers are run.
Loading happens before the event is emitted or timers are run.
Then we run the timer because the event is emitted in setInterval
Then we run the timer because the event is emitted in setInterval
...
@@ -123,6 +133,14 @@ describe('the graph view selector component', () => {
...
@@ -123,6 +133,14 @@ describe('the graph view selector component', () => {
expect
(
wrapper
.
emitted
().
updateShowLinksState
).
toHaveLength
(
1
);
expect
(
wrapper
.
emitted
().
updateShowLinksState
).
toHaveLength
(
1
);
expect
(
wrapper
.
emitted
().
updateShowLinksState
).
toEqual
([[
true
]]);
expect
(
wrapper
.
emitted
().
updateShowLinksState
).
toEqual
([[
true
]]);
});
});
it
(
'
does not emit an event if the click occurs on the currently selected view button
'
,
async
()
=>
{
expect
(
wrapper
.
emitted
().
updateShowLinksState
).
toBeUndefined
();
await
findLayerViewButton
().
trigger
(
'
click
'
);
expect
(
wrapper
.
emitted
().
updateShowLinksState
).
toBeUndefined
();
});
});
});
describe
(
'
hover tip callout
'
,
()
=>
{
describe
(
'
hover tip callout
'
,
()
=>
{
...
...
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