@sap-ux/ui5-test-writer 1.2.18 → 1.2.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/types.d.ts CHANGED
@@ -101,13 +101,18 @@ export interface FFOPAConfig {
101
101
  ui5Theme?: string;
102
102
  useVirtualPreviewEndpoints?: boolean;
103
103
  }
104
+ export type ObjectPageNavigationParent = {
105
+ name: string;
106
+ navigationProperty: string;
107
+ };
104
108
  export type ObjectPageNavigationParents = {
105
109
  parentLRName?: string;
106
- parentOPName?: string;
107
- parentOPTableSection?: string;
110
+ parentOPs: ObjectPageNavigationParent[];
108
111
  };
109
112
  export type SectionFormField = {
110
113
  property: string;
114
+ connectedFields?: string;
115
+ fieldGroup?: string;
111
116
  };
112
117
  export type TableColumn = {
113
118
  header?: string;
@@ -107,4 +107,14 @@ export declare function getSelectionFieldItems(selectionFieldsAgg: TreeAggregati
107
107
  * @returns An array of filter field descriptions.
108
108
  */
109
109
  export declare function getFilterFields(pageModel: TreeModel): TreeAggregations;
110
+ /**
111
+ * Parses a `DataFieldForAnnotation::<property>::<targetAnnotation>` style identifier.
112
+ *
113
+ * @param name - aggregation key or `field.name` from the spec model
114
+ * @returns the parsed property and target annotation, or undefined for non-annotation entries
115
+ */
116
+ export declare function parseDataFieldForAnnotationName(name: string | undefined): {
117
+ property: string;
118
+ targetAnnotation: string;
119
+ } | undefined;
110
120
  //# sourceMappingURL=modelUtils.d.ts.map
@@ -174,4 +174,25 @@ export function getFilterFields(pageModel) {
174
174
  const selectionFieldsAggregations = getAggregations(selectionFields);
175
175
  return selectionFieldsAggregations;
176
176
  }
177
+ /**
178
+ * Parses a `DataFieldForAnnotation::<property>::<targetAnnotation>` style identifier.
179
+ *
180
+ * @param name - aggregation key or `field.name` from the spec model
181
+ * @returns the parsed property and target annotation, or undefined for non-annotation entries
182
+ */
183
+ export function parseDataFieldForAnnotationName(name) {
184
+ if (!name) {
185
+ return undefined;
186
+ }
187
+ const segments = name.split('::');
188
+ if (segments.length < 3 || segments[0] !== 'DataFieldForAnnotation') {
189
+ return undefined;
190
+ }
191
+ const property = segments[1];
192
+ const targetAnnotation = segments[2];
193
+ if (!property || !targetAnnotation) {
194
+ return undefined;
195
+ }
196
+ return { property, targetAnnotation };
197
+ }
177
198
  //# sourceMappingURL=modelUtils.js.map
@@ -1,4 +1,4 @@
1
- import { getAggregations } from './modelUtils.js';
1
+ import { getAggregations, parseDataFieldForAnnotationName } from './modelUtils.js';
2
2
  import { extractTableColumnsFromNode } from './tableUtils.js';
3
3
  import { PageTypeV4 } from '@sap/ux-specification/dist/types/src/common/page.js';
4
4
  import { parse } from '@sap-ux/edmx-parser';
@@ -60,26 +60,40 @@ export function getObjectPages(applicationModel) {
60
60
  return objectPages;
61
61
  }
62
62
  /**
63
- * Finds parent pages for the object page, and returns their identifiers.
63
+ * Finds the chain of parent Object Pages leading from the List Report down to the target page.
64
64
  *
65
65
  * @param targetObjectPageKey - key of the target object page
66
66
  * @param objectPages - the array of object pages extracted from the application model
67
67
  * @param listReportPageKey - the key of the List Report page in the application model, used to find navigation routes to object pages
68
- * @returns navigation data including parent page identifiers
68
+ * @returns navigation data including the ordered ancestor Object Page chain
69
69
  */
70
70
  function getObjectPageNavigationParents(targetObjectPageKey, objectPages, listReportPageKey) {
71
- const navigationParents = {
72
- parentLRName: listReportPageKey ?? '' // app is possibly malformed if no LR found
73
- };
74
- objectPages.forEach((objectPage) => {
75
- const navigationRoutes = getNavigationRoutes(objectPage);
76
- const routeToTargetOP = navigationRoutes.find((nav) => nav.route === targetObjectPageKey);
77
- if (routeToTargetOP) {
78
- navigationParents.parentOPName = objectPage.name;
79
- navigationParents.parentOPTableSection = routeToTargetOP.identifier;
71
+ const parentOPs = [];
72
+ const visited = new Set([targetObjectPageKey]); // guard against infinite loop in case of invalid manifest entries
73
+ let cursor = targetObjectPageKey;
74
+ while (true) {
75
+ const childKey = cursor;
76
+ let parent;
77
+ let parentNavigationProperty;
78
+ for (const objectPage of objectPages) {
79
+ const route = getNavigationRoutes(objectPage).find((navigation) => navigation.route === childKey);
80
+ if (route) {
81
+ parent = objectPage;
82
+ parentNavigationProperty = route.identifier;
83
+ break;
84
+ }
80
85
  }
81
- });
82
- return navigationParents;
86
+ if (!parent?.name || !parentNavigationProperty || visited.has(parent.name)) {
87
+ break;
88
+ }
89
+ visited.add(parent.name);
90
+ parentOPs.unshift({ name: parent.name, navigationProperty: parentNavigationProperty });
91
+ cursor = parent.name;
92
+ }
93
+ return {
94
+ parentLRName: listReportPageKey ?? '', // app is possibly malformed if no LR found
95
+ parentOPs
96
+ };
83
97
  }
84
98
  /**
85
99
  * Extracts header sections data from an object page model.
@@ -133,7 +147,7 @@ function extractObjectPageBodySectionsData(objectPage, convertedMetadata, schema
133
147
  const sections = getAggregations(sectionsAggregation);
134
148
  Object.entries(sections).forEach(([sectionKey, section]) => {
135
149
  const sectionId = getSectionIdentifier(section) ?? sectionKey;
136
- const subSections = extractBodySubSectionsData(section, sectionId);
150
+ const subSections = extractBodySubSectionsData(section, sectionId, convertedMetadata, objectPage.entitySet);
137
151
  const navigationProperty = getNavigationPropertyFromKey(sectionKey);
138
152
  const isTable = isTableSection(section);
139
153
  const sectionData = {
@@ -142,7 +156,9 @@ function extractObjectPageBodySectionsData(objectPage, convertedMetadata, schema
142
156
  isTable,
143
157
  custom: !!section.custom,
144
158
  order: section?.order ?? -1,
145
- fields: section.custom || isTable ? [] : extractFormFields(section),
159
+ fields: section.custom || isTable
160
+ ? []
161
+ : extractFormFields(section, convertedMetadata, objectPage.entitySet),
146
162
  tableColumns: section.custom || !isTable ? {} : extractTableColumnsFromNode(section),
147
163
  subSections,
148
164
  actions: !section.custom && convertedMetadata && schemaNamespace
@@ -215,9 +231,11 @@ function extractSectionActions(section, convertedMetadata, schemaNamespace) {
215
231
  *
216
232
  * @param section - body section entry from the application model
217
233
  * @param parentSectionId - identifier of the parent section (used as fallback key prefix)
234
+ * @param convertedMetadata - optional converted OData metadata for drilling into ConnectedFields / FieldGroup wrappers
235
+ * @param entitySetName - the entity set the section is bound to (used to locate the entity type)
218
236
  * @returns array of sub-section feature data
219
237
  */
220
- function extractBodySubSectionsData(section, parentSectionId) {
238
+ function extractBodySubSectionsData(section, parentSectionId, convertedMetadata, entitySetName) {
221
239
  const subSections = [];
222
240
  const subSectionsAggregation = getAggregations(section)['subSections'];
223
241
  const subSectionItems = getAggregations(subSectionsAggregation);
@@ -230,7 +248,7 @@ function extractBodySubSectionsData(section, parentSectionId) {
230
248
  isTable,
231
249
  custom: !!subSection.custom,
232
250
  order: subSection?.order ?? -1, // put a negative order number to signal that order was not in spec
233
- fields: subSection.custom || isTable ? [] : extractFormFields(subSection),
251
+ fields: subSection.custom || isTable ? [] : extractFormFields(subSection, convertedMetadata, entitySetName),
234
252
  tableColumns: subSection.custom || !isTable ? {} : extractTableColumnsFromNode(subSection)
235
253
  });
236
254
  });
@@ -240,9 +258,11 @@ function extractBodySubSectionsData(section, parentSectionId) {
240
258
  * Extracts form field property paths from a body sub-section's form aggregation.
241
259
  *
242
260
  * @param subSection - body sub-section entry from the application model
261
+ * @param convertedMetadata - optional converted OData metadata for drilling into ConnectedFields / FieldGroup wrappers
262
+ * @param entitySetName - the entity set the sub-section is bound to (used to locate the entity type)
243
263
  * @returns array of form field property paths for use with iCheckField({ property })
244
264
  */
245
- function extractFormFields(subSection) {
265
+ function extractFormFields(subSection, convertedMetadata, entitySetName) {
246
266
  const fields = [];
247
267
  const formAggregation = getAggregations(subSection)['form'];
248
268
  if (!formAggregation) {
@@ -250,14 +270,71 @@ function extractFormFields(subSection) {
250
270
  }
251
271
  const fieldsAggregation = getAggregations(formAggregation)['fields'];
252
272
  const fieldItems = getAggregations(fieldsAggregation);
253
- Object.values(fieldItems).forEach((field) => {
254
- const property = field.schema?.keys?.find((key) => key.name === 'Value')?.value;
255
- if (property) {
256
- fields.push({ property });
273
+ const entityType = convertedMetadata && entitySetName ? resolveEntityType(convertedMetadata, entitySetName) : undefined;
274
+ Object.values(fieldItems).forEach((fieldItem) => {
275
+ const annotationParts = parseDataFieldForAnnotationName(fieldItem.name);
276
+ const valueProperty = fieldItem.schema?.keys?.find((key) => key.name === 'Value')?.value;
277
+ const baseProperty = valueProperty ?? annotationParts?.property;
278
+ if (!baseProperty) {
279
+ return;
280
+ }
281
+ if (annotationParts) {
282
+ const qualifier = annotationParts.targetAnnotation;
283
+ if (annotationParts.property === 'ConnectedFields' && entityType) {
284
+ resolveConnectedFieldsInnerProperties(entityType, qualifier).forEach((property) => {
285
+ fields.push({ property, connectedFields: qualifier });
286
+ });
287
+ }
288
+ else if (annotationParts.property === 'FieldGroup' && entityType) {
289
+ resolveFieldGroupInnerProperties(entityType, qualifier).forEach((property) => {
290
+ fields.push({ property, fieldGroup: qualifier });
291
+ });
292
+ }
293
+ // ConnectedFields/FieldGroup without metadata: skip
294
+ // Unknown annotation wrapper type: skip
295
+ }
296
+ else {
297
+ fields.push({ property: baseProperty });
257
298
  }
258
299
  });
259
300
  return fields;
260
301
  }
302
+ /**
303
+ * Resolves the inner `Value` paths of a `@UI.ConnectedFields#<qualifier>` annotation.
304
+ *
305
+ * @param entityType - the entity type carrying the annotation
306
+ * @param qualifier - the annotation qualifier
307
+ * @returns the inner DataField property paths
308
+ */
309
+ function resolveConnectedFieldsInnerProperties(entityType, qualifier) {
310
+ const annotation = entityType.annotations?.UI?.[`ConnectedFields#${qualifier}`];
311
+ const dictionary = annotation?.Data ?? {};
312
+ return Object.values(dictionary)
313
+ .map((dataField) => dataField?.Value?.path)
314
+ .filter((path) => Boolean(path));
315
+ }
316
+ /**
317
+ * Resolves the inner `Value` paths of a `@UI.FieldGroup#<qualifier>` annotation.
318
+ *
319
+ * @param entityType - the entity type carrying the annotation
320
+ * @param qualifier - the annotation qualifier
321
+ * @returns the inner DataField property paths
322
+ */
323
+ function resolveFieldGroupInnerProperties(entityType, qualifier) {
324
+ const annotation = entityType.annotations?.UI?.[`FieldGroup#${qualifier}`];
325
+ const dataFields = annotation?.Data ?? [];
326
+ return dataFields.map((dataField) => dataField?.Value?.path).filter((path) => Boolean(path));
327
+ }
328
+ /**
329
+ * Looks up the entity type for the given entity set name in the converted metadata.
330
+ *
331
+ * @param convertedMetadata - the converted OData metadata
332
+ * @param entitySetName - the entity set name
333
+ * @returns the entity type, or undefined if not found
334
+ */
335
+ function resolveEntityType(convertedMetadata, entitySetName) {
336
+ return convertedMetadata.entitySets.find((es) => es.name === entitySetName)?.entityType;
337
+ }
261
338
  /**
262
339
  * Extracts the OData navigation property from a spec model section key.
263
340
  * Section keys for table sections follow the pattern `_NavProperty::@annotation`, so the
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sap-ux/ui5-test-writer",
3
3
  "description": "SAP UI5 tests writer",
4
- "version": "1.2.18",
4
+ "version": "1.2.20",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -33,11 +33,11 @@ sap.ui.define([
33
33
  Then.onThe<%- navigationParents.parentLRName%>Generated.onTable().iCheckRows();
34
34
  When.onThe<%- navigationParents.parentLRName%>Generated.onTable().iPressRow(0);
35
35
  <% } -%>
36
- <% if(navigationParents.parentOPName) { %>
37
- Then.onThe<%- navigationParents.parentOPName%>Generated.iSeeThisPage();
38
- Then.onThe<%- navigationParents.parentOPName%>Generated.onTable({ property: "<%- navigationParents.parentOPTableSection %>" }).iCheckRows();
39
- When.onThe<%- navigationParents.parentOPName%>Generated.onTable({ property: "<%- navigationParents.parentOPTableSection %>" }).iPressRow(0);
40
- <% } %>
36
+ <% navigationParents.parentOPs.forEach(function(parent) { %>
37
+ Then.onThe<%- parent.name %>Generated.iSeeThisPage();
38
+ Then.onThe<%- parent.name %>Generated.onTable({ property: "<%- parent.navigationProperty %>" }).iCheckRows();
39
+ When.onThe<%- parent.name %>Generated.onTable({ property: "<%- parent.navigationProperty %>" }).iPressRow(0);
40
+ <% }); %>
41
41
  Then.onThe<%- name%>Generated.iSeeThisPage();
42
42
  });
43
43
 
@@ -129,7 +129,7 @@ sap.ui.define([
129
129
  Then.onThe<%- name%>Generated.iCheckSubSection({ section: "<%- subSection.id %>" });
130
130
  <% if (subSection.fields && subSection.fields.length > 0) { -%>
131
131
  <% subSection.fields.forEach(function(field) { -%>
132
- Then.onThe<%- name%>Generated.onForm({ section: "<%- subSection.id %>" }).iCheckField({ property: "<%- field.property %>" });
132
+ Then.onThe<%- name%>Generated.onForm({ section: "<%- subSection.id %>" }).iCheckField({ property: "<%- field.property %>"<% if (field.connectedFields) { %>, connectedFields: "<%- field.connectedFields %>"<% } %><% if (field.fieldGroup) { %>, fieldGroup: "<%- field.fieldGroup %>"<% } %> });
133
133
  <% }) -%>
134
134
  <% } -%>
135
135
  <% if (subSection.tableColumns && Object.keys(subSection.tableColumns).length > 0 && subSection.navigationProperty) { -%>
@@ -139,7 +139,7 @@ sap.ui.define([
139
139
  <% } else { -%>
140
140
  <% if (section.fields && section.fields.length > 0) { -%>
141
141
  <% section.fields.forEach(function(field) { -%>
142
- Then.onThe<%- name%>Generated.onForm({ section: "<%- section.id %>" }).iCheckField({ property: "<%- field.property %>" });
142
+ Then.onThe<%- name%>Generated.onForm({ section: "<%- section.id %>" }).iCheckField({ property: "<%- field.property %>"<% if (field.connectedFields) { %>, connectedFields: "<%- field.connectedFields %>"<% } %><% if (field.fieldGroup) { %>, fieldGroup: "<%- field.fieldGroup %>"<% } %> });
143
143
  <% }) -%>
144
144
  <% } -%>
145
145
  <% if (section.tableColumns && Object.keys(section.tableColumns).length > 0 && section.navigationProperty) { -%>
@@ -52,11 +52,11 @@ function journey() {
52
52
  Then.onThe<%- navigationParents.parentLRName%>Generated.onTable("").iCheckRows();
53
53
  When.onThe<%- navigationParents.parentLRName%>Generated.onTable("").iPressRow(0);
54
54
  <% } -%>
55
- <% if(navigationParents.parentOPName) { %>
56
- Then.onThe<%- navigationParents.parentOPName%>Generated.iSeeThisPage();
57
- Then.onThe<%- navigationParents.parentOPName%>Generated.onTable({ property: "<%- navigationParents.parentOPTableSection %>" }).iCheckRows();
58
- When.onThe<%- navigationParents.parentOPName%>Generated.onTable({ property: "<%- navigationParents.parentOPTableSection %>" }).iPressRow(0);
59
- <% } %>
55
+ <% navigationParents.parentOPs.forEach(function(parent) { %>
56
+ Then.onThe<%- parent.name %>Generated.iSeeThisPage();
57
+ Then.onThe<%- parent.name %>Generated.onTable({ property: "<%- parent.navigationProperty %>" }).iCheckRows();
58
+ When.onThe<%- parent.name %>Generated.onTable({ property: "<%- parent.navigationProperty %>" }).iPressRow(0);
59
+ <% }); %>
60
60
  Then.onThe<%- name%>Generated.iSeeThisPage();
61
61
  });
62
62
 
@@ -148,7 +148,7 @@ function journey() {
148
148
  Then.onThe<%- name%>Generated.iCheckSubSection({ section: "<%- subSection.id %>" });
149
149
  <% if (subSection.fields && subSection.fields.length > 0) { -%>
150
150
  <% subSection.fields.forEach(function(field) { -%>
151
- Then.onThe<%- name%>Generated.onForm({ section: "<%- subSection.id %>" } as unknown as FormIdentifier).iCheckField({ property: "<%- field.property %>" });
151
+ Then.onThe<%- name%>Generated.onForm({ section: "<%- subSection.id %>" } as unknown as FormIdentifier).iCheckField({ property: "<%- field.property %>"<% if (field.connectedFields) { %>, connectedFields: "<%- field.connectedFields %>"<% } %><% if (field.fieldGroup) { %>, fieldGroup: "<%- field.fieldGroup %>"<% } %> });
152
152
  <% }) -%>
153
153
  <% } -%>
154
154
  <% if (subSection.tableColumns && Object.keys(subSection.tableColumns).length > 0 && subSection.navigationProperty) { -%>
@@ -158,7 +158,7 @@ function journey() {
158
158
  <% } else { -%>
159
159
  <% if (section.fields && section.fields.length > 0) { -%>
160
160
  <% section.fields.forEach(function(field) { -%>
161
- Then.onThe<%- name%>Generated.onForm({ section: "<%- section.id %>" } as unknown as FormIdentifier).iCheckField({ property: "<%- field.property %>" });
161
+ Then.onThe<%- name%>Generated.onForm({ section: "<%- section.id %>" } as unknown as FormIdentifier).iCheckField({ property: "<%- field.property %>"<% if (field.connectedFields) { %>, connectedFields: "<%- field.connectedFields %>"<% } %><% if (field.fieldGroup) { %>, fieldGroup: "<%- field.fieldGroup %>"<% } %> });
162
162
  <% }) -%>
163
163
  <% } -%>
164
164
  <% if (section.tableColumns && Object.keys(section.tableColumns).length > 0 && section.navigationProperty) { -%>