/
TestingLifecycleHook.kt
234 lines (218 loc) · 11.1 KB
/
TestingLifecycleHook.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
package com.github.mvysny.kaributesting.v10
import com.github.mvysny.kaributools.label
import com.github.mvysny.kaributools.walk
import com.vaadin.flow.component.Component
import com.vaadin.flow.component.UI
import com.vaadin.flow.component.confirmdialog.ConfirmDialog
import com.vaadin.flow.component.contextmenu.MenuItemBase
import com.vaadin.flow.component.contextmenu.SubMenuBase
import com.vaadin.flow.component.dialog.Dialog
import com.vaadin.flow.component.grid.Grid
import com.vaadin.flow.component.littemplate.LitTemplate
import com.vaadin.flow.component.menubar.MenuBar
import com.vaadin.flow.component.sidenav.SideNav
import com.vaadin.flow.component.sidenav.SideNavItem
/**
* If you need to hook into the testing lifecycle (e.g. you need to wait for any async operations to finish),
* provide your own custom implementation of this interface, then set it into [testingLifecycleHook].
* Make sure to call the [TestingLifecycleHook.default] implementation,
* otherwise Karibu-Testing will not discover the children of basic components.
*
* ### Mocking server request end
*
* Since Karibu-Testing runs in the same JVM as the server and there is no browser, the boundaries between the client and
* the server become unclear. When looking into sources of any test method, it's really hard to tell where exactly the server request ends, and
* where another request starts.
*
* You can establish an explicit client boundary in your test, by explicitly calling [MockVaadin.clientRoundtrip]. However, since that
* would be both laborous and error-prone, the default operation is that Karibu Testing pretends as if there was a client-server
* roundtrip before every component lookup
* via the [_get]/[_find]/[_expectNone]/[_expectOne] call. Therefore, [MockVaadin.clientRoundtrip] is called from [awaitBeforeLookup] by default.
*
* ### Providing children of a PolymerTemplate/LitTemplate
*
* You can override [getAllChildren] to provide children of your particular view extending from PolymerTemplate/LitTemplate.
* See the Karibu-Testing documentation for more help.
*
* ### Delegating properly to the default impl
* ```
* class MyLifecycleHook(val delegate: TestingLifecycleHook) : TestingLifecycleHook by delegate {
* override fun awaitBeforeLookup() { delegate.awaitBeforeLookup() }
* }
* testingLifecycleHook = MyLifecycleHook(TestingLifecycleHook.default)
* ```
* @see TestingLifecycleHookVaadin14Default for the default implementation
*/
public interface TestingLifecycleHook {
/**
* Invoked before every component lookup. You can e.g. wait for any async operations to finish and for the server to settle down.
*
* See [TestingLifecycleHookVaadin14Default.awaitBeforeLookup] for the default implementation.
*/
public fun awaitBeforeLookup()
/**
* Invoked after every component lookup. You can e.g. wait for any async operations to finish and for the server to settle down.
* Invoked even if the `_get()`/`_find()`/`_expectNone()` function fails.
*
* See [TestingLifecycleHookVaadin14Default.awaitAfterLookup] for the default implementation.
*/
public fun awaitAfterLookup()
/**
* Provides all direct children of given component. May include virtual children.
* Only direct children are considered - don't return children of children.
*
* See [TestingLifecycleHookVaadin14Default.getAllChildren] for the default implementation.
*/
public fun getAllChildren(component: Component): List<Component>
/**
* Returns the label of the component. According to the [official recommendation](https://github.com/vaadin/flow-components/issues/5129), only Vaadin
* fields should implement [com.vaadin.flow.component.HasLabel]. However, for testing purposes
* it's convenient to be able to look up also Tab and SideNavItem by their labels (and possibly other components as well,
* including your own custom components). That's exactly what this function is for - to retrieve the
* label from components other than [com.vaadin.flow.component.HasLabel].
*
* The default implementation only covers the Vaadin built-in components.
*/
public fun getLabel(component: Component): String?
public companion object {
/**
* A default lifecycle hook that works well with all Vaadin versions.
*/
@JvmStatic
public val default: TestingLifecycleHook get() = TestingLifecycleHookVaadin14Default()
}
}
/**
* The default implementation of [TestingLifecycleHook] that works for all Vaadin 24+ versions.
*/
public open class TestingLifecycleHookVaadin14Default : TestingLifecycleHook {
/**
* Calls the [MockVaadin.clientRoundtrip] method. When overriding this method, you should
* also call [MockVaadin.clientRoundtrip] (or simply call super).
*/
override fun awaitBeforeLookup() {
if (UI.getCurrent() != null) {
MockVaadin.clientRoundtrip()
}
}
/**
* The function does nothing by default.
*/
override fun awaitAfterLookup() {
}
/**
* Provides all direct children of given component. Provides workarounds for certain components:
* * For [Grid.Column] the function will also return cell components nested in all headers and footers for that particular column.
* * For [MenuItemBase] the function returns all items of a sub-menu.
*/
override fun getAllChildren(component: Component): List<Component> = when {
component is Grid<*> -> {
// don't attach the header/footer components as a child of the Column component:
// that would make components in merged cells appear more than once.
// see https://github.com/mvysny/karibu-testing/issues/52
val headerComponents: List<Component> = component.headerRows
.flatMap { it.cells.map { it.component } }
.filterNotNull()
val footerComponents: List<Component> = component.footerRows
.flatMap { it.cells.map { it.component } }
.filterNotNull()
val editorComponents: List<Component> = component.columns
.mapNotNull { it.editorComponent }
val children = component.children.toList()
(headerComponents + footerComponents + editorComponents + children).distinct()
}
component is MenuItemBase<*, *, *> -> {
val items: List<Component> = (component.subMenu as SubMenuBase<*, *, *>).items
// also include component.children: https://github.com/mvysny/karibu-testing/issues/76
(component.children.toList() + items).distinct()
}
component is MenuBar -> {
// don't include virtual children since that would make the MenuItems appear two times.
component.children.toList()
}
component.isTemplate && !includeVirtualChildrenInTemplates -> {
// don't include virtual children; see [includeVirtualChildrenInTemplates] for more details.
component.children.toList()
}
component.javaClass.name == "com.vaadin.flow.component.grid.ColumnGroup" -> {
// don't include virtual children since that would include the header/footer components
// which would clash with Grid.Column later on
component.children.toList()
}
component is Grid.Column<*> -> {
// don't include virtual children since that would include the header/footer components
// which would clash with Grid.Column later on
component.children.toList()
}
// Also include virtual children.
// Issue: https://github.com/mvysny/karibu-testing/issues/85
else -> (component.children.toList() + component._getVirtualChildren()).distinct()
}
override fun getLabel(component: Component): String? = when (component) {
is SideNav -> component.label
is SideNavItem -> component.label
else -> component.label
}
}
internal val Component.isTemplate: Boolean get() = this is LitTemplate
/**
* [PolymerTemplate]s and LitTemplates are a bit tricky.
* The purpose of PolymerTemplates is to move as much code as possible to the client-side,
* while Karibu is designed to test server-side code only. The child components
* are either not accessible from the server-side altogether,
* or they are only "shallow shells" of components constructed server-side -
* almost none of their properties are transferred to the server-side.
*
* Also see [Polymer Templates / Lit Templates](https://github.com/mvysny/karibu-testing/tree/master/karibu-testing-v10#polymer-templates--lit-templates)
* for more info.
*
* Theese child components are still available on server-side and attached to the Template as virtual children, therefore
* it is possible to obtain them from the server-side. If you understand the risks
* and shortcomings of this, set this property to `true` to include virtual children in
* Karibu-recognized tree of components.
*/
public var includeVirtualChildrenInTemplates: Boolean = false
/**
* By default, Karibu fakes [MockPage.retrieveExtendedClientDetails]. However, it seems
* to interfere with Spring in some way, see [Issue #129](https://github.com/mvysny/karibu-testing/issues/129)
* for more details. To work around that ticket, set this to false.
*
* Turning this off will cause `@PreserveOnRefresh` not to work anymore, see [Issue #118](https://github.com/mvysny/karibu-testing/issues/118)
* for more details.
*/
public var fakeExtendedClientDetails: Boolean = true
/**
* If you need to hook into the testing lifecycle (e.g. you need to wait for any async operations to finish),
* set your custom implementation here. See [TestingLifecycleHook] for more info on
* where exactly you can hook into. The best way is to delegate to the [TestingLifecycleHook.default] implementation.
*/
public var testingLifecycleHook: TestingLifecycleHook = TestingLifecycleHook.default
/**
* Checks whether given [component] is a dialog and needs to be removed from the UI.
* See [cleanupDialogs] for more info.
*/
private fun isDialogAndNeedsRemoval(component: Component): Boolean {
if (component is Dialog && !component.isOpened) {
return true
}
// also support ConfirmDialog. Since Vaadin 24 it's no longer a Pro component.
if (component is ConfirmDialog && !component.isOpened) {
return true
}
return false
}
/**
* Flow Server does not close the dialog when [Dialog.close] is called; instead it tells client-side dialog to close,
* which then fires event back to the server that the dialog was closed, and removes itself from the DOM.
* Since there's no browser with browserless testing, we need to cleanup closed dialogs manually, hence this method.
*
* Also see [com.github.mvysny.kaributesting.v10.mock.MockedUI] for more details
*/
public fun cleanupDialogs() {
// Starting with Vaadin 23, nested dialogs are also nested within respective
// modal dialog within the UI. This is probably related to the "server-side
// modality curtain" feature. Also see https://github.com/mvysny/karibu-testing/issues/102
UI.getCurrent().walk()
.filter { isDialogAndNeedsRemoval(it) }
.forEach { it.element.removeFromParent() }
}