Skip to content

Commit d5cea76

Browse files
TASK-1828886-1: Embedded data automatic tests + fixes (#98)
1 parent 5b53664 commit d5cea76

File tree

21 files changed

+1757
-117
lines changed

21 files changed

+1757
-117
lines changed

core/src/commonMain/kotlin/com/pega/constellation/sdk/kmp/core/components/containers/FieldGroupTemplate.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import kotlinx.serialization.json.JsonObject
1818
class FieldGroupTemplateComponent(context: ComponentContext) : BaseComponent(context) {
1919
var items by mutableStateOf(emptyList<Item>())
2020
private set
21+
var label by mutableStateOf("")
22+
private set
23+
var showLabel by mutableStateOf(true)
24+
private set
2125
var allowAddItems by mutableStateOf(false)
2226
private set
2327
var addButtonLabel by mutableStateOf("")
@@ -38,6 +42,8 @@ class FieldGroupTemplateComponent(context: ComponentContext) : BaseComponent(con
3842
)
3943
}
4044
}
45+
label = props.getString("label")
46+
showLabel = props.getString("showLabel").toBoolean()
4147
allowAddItems = props.getBoolean("allowAddItems")
4248
addButtonLabel = props.getString("addButtonLabel")
4349
}

samples/android-cmp-app/src/androidInstrumentedTest/kotlin/com/pega/constellation/sdk/kmp/samples/androidcmpapp/test/ComposeTest.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import com.pega.constellation.sdk.kmp.samples.basecmpapp.ui.screens.pega.PegaVie
2626
import com.pega.constellation.sdk.kmp.samples.basecmpapp.ui.screens.services.ServicesViewModel
2727
import com.pega.constellation.sdk.kmp.test.mock.MockHttpClient
2828
import com.pega.constellation.sdk.kmp.test.mock.PegaVersion
29+
import com.pega.constellation.sdk.kmp.ui.components.cmp.controls.form.internal.AppContext
2930
import kotlinx.coroutines.CoroutineScope
3031
import kotlinx.coroutines.Dispatchers
3132
import okhttp3.OkHttpClient
@@ -49,6 +50,7 @@ abstract class ComposeTest(
4950
fun setUp() {
5051
hideKeyboard()
5152
Injector.init(authManager, engine)
53+
AppContext.init(context)
5254
}
5355

5456
@OptIn(ExperimentalTestApi::class)

samples/android-cmp-app/src/androidInstrumentedTest/kotlin/com/pega/constellation/sdk/kmp/samples/androidcmpapp/test/ComposeTestUtils.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import androidx.compose.ui.test.ComposeUiTest
44
import androidx.compose.ui.test.ExperimentalTestApi
55
import androidx.compose.ui.test.assertCountEquals
66
import androidx.compose.ui.test.hasText
7+
import androidx.compose.ui.test.isRoot
8+
import androidx.compose.ui.test.printToLog
79
import androidx.compose.ui.test.waitUntilExactlyOneExists
810
import androidx.compose.ui.test.waitUntilNodeCount
911

@@ -28,3 +30,6 @@ fun ComposeUiTest.waitForNodes(text: String, count: Int, substring: Boolean = fa
2830
onAllNodes(hasText(text, substring)).assertCountEquals(count)
2931
}
3032
}
33+
34+
@OptIn(ExperimentalTestApi::class)
35+
fun ComposeUiTest.printAllFormNodes() = onAllNodes(isRoot())[1].printToLog("FORM NODES")
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,161 @@
11
package com.pega.constellation.sdk.kmp.samples.androidcmpapp.test.cases
22

3+
import androidx.compose.ui.test.ComposeUiTest
34
import androidx.compose.ui.test.ExperimentalTestApi
5+
import androidx.compose.ui.test.SemanticsMatcher
6+
import androidx.compose.ui.test.SemanticsNodeInteractionCollection
7+
import androidx.compose.ui.test.filter
8+
import androidx.compose.ui.test.hasAnyAncestor
49
import androidx.compose.ui.test.hasContentDescription
5-
import androidx.compose.ui.test.hasSetTextAction
10+
import androidx.compose.ui.test.hasTestTag
611
import androidx.compose.ui.test.hasText
12+
import androidx.compose.ui.test.onFirst
713
import androidx.compose.ui.test.onNodeWithContentDescription
814
import androidx.compose.ui.test.onNodeWithText
15+
import androidx.compose.ui.test.onSiblings
916
import androidx.compose.ui.test.performClick
17+
import androidx.compose.ui.test.performScrollTo
1018
import androidx.compose.ui.test.performTextInput
1119
import androidx.compose.ui.test.runComposeUiTest
12-
import androidx.compose.ui.test.waitUntilDoesNotExist
20+
import androidx.compose.ui.test.waitUntilExactlyOneExists
1321
import androidx.compose.ui.test.waitUntilNodeCount
1422
import com.pega.constellation.sdk.kmp.samples.androidcmpapp.test.ComposeTest
1523
import com.pega.constellation.sdk.kmp.samples.androidcmpapp.test.waitForNode
1624
import com.pega.constellation.sdk.kmp.samples.androidcmpapp.test.waitForNodes
1725
import com.pega.constellation.sdk.kmp.test.mock.PegaVersion
26+
import java.time.LocalDateTime
1827
import kotlin.test.Test
1928

2029
@OptIn(ExperimentalTestApi::class)
21-
class EmbeddedDataTest : ComposeTest(PegaVersion.v24_1_0) {
30+
class EmbeddedDataTest : ComposeTest(PegaVersion.v24_2_2) {
31+
2232
@Test
23-
fun test_embedded_data() = runComposeUiTest {
24-
setupApp("DIXL-MediaCo-Work-EmbeddedData")
33+
fun test_embedded_data_repeating_view() = runComposeUiTest {
34+
setupApp("O40M3A-MarekCo-Work-EmbeddedDataTest")
2535

2636
// create case
2737
onNodeWithText("New Service").performClick()
2838

2939
// verify form title and instruction
30-
waitForNode("Create EmbeddedData (E-", substring = true)
31-
waitForNode("Embedded Data use-case", substring = true)
32-
waitForNode("EmbeddedData cars editable")
33-
waitForNode("EmbeddedData cars readonly")
40+
waitForNode("ED repeating view editable (", substring = true)
41+
waitForNode("ED repeating view editable & readonly instruction")
42+
43+
// verify repeating views presence
44+
waitForNode("Cars repeating view editable")
45+
waitForNode("Cars repeating view readonly")
3446

3547
// remove and verify empty list
48+
waitUntilExactlyOneExists(hasContentDescription("Delete item 1"))
3649
onNodeWithContentDescription("Delete item 1").performClick()
3750
waitForNodes("No items", count = 2)
3851
waitUntilNodeCount(hasContentDescription("No items"), count = 2)
39-
40-
onNodeWithText("Add", substring = true).performClick()
41-
42-
// verify empty record
43-
waitForNodes("Row 1", 2)
44-
waitForNode("Details")
45-
waitForNodes("Brand", 2)
46-
waitForNodes("Model", 2)
47-
waitForNodes("---", 2)
48-
49-
// enter data
50-
onNodeWithText("Client name").performTextInput("Lukasz")
51-
onNode(hasText("Brand") and hasSetTextAction()).performTextInput("Audi")
52-
onNode(hasText("Model") and hasSetTextAction()).performTextInput("A5")
53-
onNodeWithText("Row 1").performClick() // remove focus to propagate data
54-
55-
// verify data propagation
56-
waitForNodes("Lukasz", 2)
57-
waitForNodes("Audi", 2)
58-
waitForNodes("A5", 2)
59-
60-
// adding 2nd record
6152
onNodeWithText("Add", substring = true).performClick()
6253

63-
// enter data in Row 2
64-
waitForNodes("Row 2", count = 2)
65-
onNode(hasText("Client name") and !hasText("Lukasz") and hasSetTextAction()).performTextInput(
66-
"Marek"
67-
)
68-
onNode(hasText("Brand") and !hasText("Audi") and hasSetTextAction()).performTextInput("Ford")
69-
onNode(hasText("Model") and !hasText("A5") and hasSetTextAction()).performTextInput("Focus")
70-
onNodeWithText("Row 2").performClick() // remove focus to propagate data
71-
72-
// verify data propagation for Row 2
73-
waitForNodes("Marek", 2)
74-
waitForNodes("Ford", 2)
75-
waitForNodes("Focus", 2)
76-
77-
// remove Row 1 and verify
78-
onNodeWithContentDescription("Delete item 2").performClick()
79-
80-
waitUntilDoesNotExist(hasText("Row 2"))
81-
waitUntilDoesNotExist(hasText("Marek"))
82-
waitUntilDoesNotExist(hasText("Ford"))
83-
waitUntilDoesNotExist(hasText("Focus"))
84-
85-
// go to next step
54+
// verify error banner
8655
onNodeWithText("Next").performClick()
56+
waitForNode("brand: Cannot be blank", substring = true)
57+
58+
// enter data in row 1 and verify
59+
onAllNodes(hasAnyAncestor(hasTestTag("field_group_template_[Cars repeating view editable]"))).let {
60+
it.findFirstWithText("brand").performTextInput("Audi")
61+
it.findFirstWithText("model").performTextInput("A5")
62+
it.findFirstWithText("Price").performTextInput("123000")
63+
it.findFirstWithText("IsFirstOwner").onSiblings().onFirst().performClick()
64+
it.findFirstWithText("interior").performScrollTo().performClick()
65+
onNodeWithText("comfort").performClick()
66+
it.findFirstWithText("Insurance").performScrollTo().performClick()
67+
onNodeWithText("gold").performClick()
68+
it.findFirstWithText("client meeting date").performScrollTo().performClick()
69+
onNodeWithText("Today, ", substring = true).performClick()
70+
onNodeWithText("OK").performClick()
71+
it.findFirstWithText("Client meeting time").performScrollTo().performClick()
72+
onNodeWithText("OK").performClick()
73+
it.findFirstWithText("Transaction date time").performScrollTo().performScrollTo()
74+
.performClick()
75+
onNodeWithText("Today, ", substring = true).performClick()
76+
onNodeWithText("OK").performClick()
77+
onNodeWithText("OK").performClick()
78+
it.findFirstWithText("Notes").performScrollTo().performTextInput("This is a note")
79+
// remove focus to propagate data
80+
onNodeWithText("Cars repeating view readonly").performScrollTo().performClick()
81+
82+
// verify editable data
83+
verifyEmbeddedDataRecord(
84+
nodes = it,
85+
expectedDate = LocalDateTime.now().toString().substring(0, 10),
86+
isEditable = true
87+
)
88+
}
89+
// verify if row 1 data propagated to readonly duplicated view
90+
onAllNodes(hasAnyAncestor(hasTestTag("field_group_template_[Cars repeating view readonly]"))).let {
91+
verifyEmbeddedDataRecord(
92+
nodes = it,
93+
expectedDate = LocalDateTime.now().toString().substring(0, 10),
94+
isEditable = false
95+
)
96+
}
97+
98+
// adding row 2
99+
onNodeWithText("Add", substring = true).performScrollTo().performClick()
100+
101+
// enter data in row 2
102+
waitForNodes("cars 2", count = 2)
103+
onAllNodes(hasAnyAncestor(hasTestTag("field_group_template_[Cars repeating view editable]"))).let { nodes ->
104+
nodes.filter(hasAnyAncestor(hasTestTag("field_group_item_2"))).let {
105+
it.findFirstWithText("brand").performTextInput("Ford")
106+
nodes.findFirstWithText("cars 2").performClick() // remove focus to propagate data
107+
it.findFirstWithText("Ford").assertExists()
108+
}
109+
}
110+
// verify data in row 2 propagated to duplicated
111+
onAllNodes(hasAnyAncestor(hasTestTag("field_group_template_[Cars repeating view readonly]"))).let { nodes ->
112+
nodes.filter(hasAnyAncestor(hasTestTag("field_group_item_2"))).let {
113+
waitUntilAtLeastOneExists(it, hasText("Ford"), timeoutMillis = 5000L)
114+
}
115+
}
116+
117+
// 2nd step
118+
onNodeWithText("Next").performClick()
119+
waitForNode("ED repeating view readonly (", substring = true)
120+
121+
// verify row 1 on second step
122+
onAllNodes(hasAnyAncestor(hasTestTag("field_group_template_[Cars repeating view readonly]"))).let {
123+
verifyEmbeddedDataRecord(it, expectedDate = "2025-12-16", isEditable = false)
124+
}
125+
}
126+
127+
fun ComposeUiTest.verifyEmbeddedDataRecord(
128+
nodes: SemanticsNodeInteractionCollection,
129+
expectedDate: String,
130+
isEditable: Boolean
131+
) {
132+
with(nodes) {
133+
findFirstWithText("Audi").assertExists()
134+
findFirstWithText("A5").assertExists()
135+
findFirstWithText("123000").assertExists()
136+
if (!isEditable) {
137+
findFirstWithText("Yes").assertExists()
138+
}
139+
findFirstWithText("comfort").assertExists()
140+
findFirstWithText("gold").assertExists()
141+
findFirstWithText(expectedDate).assertExists()
142+
findFirstWithText("12:00 AM").assertExists()
143+
findFirstWithText("$expectedDate 12:00 AM").assertExists()
144+
findFirstWithText("Notes").performScrollTo()
145+
waitUntilAtLeastOneExists(nodes, hasText("This is a note"), timeoutMillis = 5000L)
146+
}
147+
}
87148

88-
// verify form components on 2nd step
89-
waitForNode("Verify EmbeddedData (E-", substring = true)
90-
waitForNode("EmbeddedData cars readonly")
91-
waitForNode("Lukasz")
92-
waitForNode("Details")
93-
waitForNode("Brand")
94-
waitForNode("Audi")
95-
waitForNode("Model")
96-
waitForNode("A5")
149+
fun SemanticsNodeInteractionCollection.findFirstWithText(text: String) =
150+
this.filter(hasText(text)).onFirst()
151+
152+
fun ComposeUiTest.waitUntilAtLeastOneExists(
153+
nodes: SemanticsNodeInteractionCollection,
154+
matcher: SemanticsMatcher,
155+
timeoutMillis: Long = 5000L
156+
) {
157+
waitUntil("exactly 1 nodes match (${matcher.description})", timeoutMillis) {
158+
nodes.filter(matcher).fetchSemanticsNodes().size == 1
159+
}
97160
}
98161
}

samples/swiftui-components-app/swiftui-components-app/SDKSupport/UI/Containers/FieldGroupTemplateComponentView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ struct FieldGroupTemplateComponentView: View {
1010

1111
var body: some View {
1212
VStack(alignment: .leading, spacing: 5) {
13+
if state.component.showLabel && !state.component.label.isEmpty {
14+
Text(state.component.label)
15+
}
1316
ForEach(state.component.items, id: \.id) {
1417
Text($0.heading).font(.title2)
1518
$0.component.renderView()

scripts/dxcomponents/components/containers/templates/field-group-template.component.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export class FieldGroupTemplateComponent extends BaseComponent {
1212
};
1313

1414
inheritedProps$;
15-
showLabel$ = true;
16-
label$;
15+
showLabel = true;
16+
label;
1717
contextClass;
1818
referenceList;
1919
referenceListLength;
@@ -71,8 +71,8 @@ export class FieldGroupTemplateComponent extends BaseComponent {
7171
const label = this.configProps.label;
7272
const showLabel = this.configProps.showLabel;
7373
// label & showLabel within inheritedProps takes precedence over configProps
74-
this.label$ = this.inheritedProps$.label !== undefined ? this.inheritedProps$.label : label;
75-
this.showLabel$ = this.inheritedProps$.showLabel !== undefined ? this.inheritedProps$.showLabel : showLabel;
74+
this.label = this.inheritedProps$.label !== undefined ? this.inheritedProps$.label : label;
75+
this.showLabel = this.inheritedProps$.showLabel !== undefined ? this.inheritedProps$.showLabel : showLabel;
7676
this.contextClass = this.configProps.contextClass;
7777
const lookForChildInConfig = this.configProps.lookForChildInConfig;
7878
this.heading = this.configProps.heading ?? "Row";
@@ -173,6 +173,8 @@ export class FieldGroupTemplateComponent extends BaseComponent {
173173
allowDelete: child.allowDelete,
174174
};
175175
}),
176+
label: this.label,
177+
showLabel: this.showLabel,
176178
allowAddItems: this.allowedActions.add,
177179
addButtonLabel: this.getAddButtonLabel(),
178180
};

scripts/dxcomponents/components/containers/templates/simple-table-manual.component.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class SimpleTableManualComponent extends BaseComponent {
108108
const conditions = this.#calculateConditions(editMode, allowActions, allowTableEdit)
109109

110110
this.referenceListStr = getContext(this.pConn).referenceListStr;
111-
this.props.label = labelProp || propertyLabel;
111+
this.props.label = this.pConn.getInheritedProps().label ?? labelProp ?? propertyLabel;
112112
this.targetClassLabel = targetClassLabel;
113113
this.props.addButtonLabel = targetClassLabel ? `+ Add ${targetClassLabel}` : "+ Add";
114114
this.referenceList = referenceList;

scripts/dxcomponents/components/containers/view.component.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export class ViewComponent extends ContainerBaseComponent {
1919
UNSUPPORTED_FORM_TEMPLATES = ["TwoColumn", "ThreeColumn", "WideNarrow"];
2020

2121
SUPPORTED_TEMPLATES = [...this.SUPPORTED_FORM_TEMPLATES, "SimpleTable", "DataReference"];
22+
NO_HEADER_TEMPLATES = ['SimpleTable'];
2223

2324
jsComponentPConnectData = {};
2425
props = {
@@ -79,7 +80,7 @@ export class ViewComponent extends ContainerBaseComponent {
7980
const showLabel = configProps.showLabel || this.DETAILS_TEMPLATES.includes(template) || this.props.showLabel;
8081

8182
this.props.label = inheritedProps.label ?? label;
82-
this.props.showLabel = inheritedProps.showLabel ?? showLabel;
83+
this.props.showLabel = (inheritedProps.showLabel ?? showLabel) && !this.NO_HEADER_TEMPLATES.includes(template);
8384
this.props.visible = configProps.visibility ?? this.props.visible;
8485

8586
if (this.READ_ONLY_DETAILS_TEMPLATES.includes(template)) {

scripts/init/init.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ async function onPCoreReady(renderObj) {
5050
}
5151

5252
function sendEventToComponent(id, event) {
53-
bridge.onEvent(id, JSON.parse(event));
53+
const escapedEvent = event.replace(/\n/g, '\\n').replace(/\t/g, '\\t');
54+
bridge.onEvent(id, JSON.parse(escapedEvent));
5455
}
5556

5657
window.sendEventToComponent = sendEventToComponent;

0 commit comments

Comments
 (0)