Skip to content

Commit 287e3a4

Browse files
sap-sebelaodocirl
andauthored
feat(ui5-test-writer): Generate tests for Form and Table content of Object Page Sections (#4593)
* (feat) ui5-test-writer: Generate tests for form and Table content of Object Page Sections - generate checks for form fields in op sections - genereate checks for table columns in op sections - new tableUtils file for shared table-related stuff valid for both LR and OP * Fix fallback and check when missing navigationProperty * Adjust based on AI review, update test * Clean up types * Fix TS type issues caused by type adjustments --------- Co-authored-by: David O'Connor <[email protected]>
1 parent 3945459 commit 287e3a4

11 files changed

Lines changed: 1000 additions & 210 deletions

File tree

.changeset/breezy-candies-go.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-ux/ui5-test-writer': patch
3+
---
4+
5+
Generate tests for Form and Table content in Object Page Sections

packages/ui5-test-writer/src/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,34 @@ export type ObjectPageNavigationParents = {
9595
parentOPTableSection?: string;
9696
};
9797

98+
export type SectionFormField = {
99+
property: string;
100+
};
101+
102+
export type TableColumn = {
103+
header?: string;
104+
};
105+
106+
export type TableColumnFeatureData = Record<string, TableColumn>;
107+
98108
export type BodySubSectionFeatureData = {
99109
id: string;
110+
navigationProperty?: string;
100111
isTable: boolean;
101112
custom: boolean;
102113
order: number;
114+
fields: SectionFormField[];
115+
tableColumns: TableColumnFeatureData;
103116
};
104117

105118
export type BodySectionFeatureData = {
106119
id: string;
120+
navigationProperty?: string;
107121
isTable: boolean;
108122
custom: boolean;
109123
order: number;
124+
fields: SectionFormField[];
125+
tableColumns: TableColumnFeatureData;
110126
subSections: BodySubSectionFeatureData[];
111127
};
112128

packages/ui5-test-writer/src/utils/modelUtils.ts

Lines changed: 2 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
import type { AppFeatures, FPMFeatures } from '../types';
1515
import { getObjectPageFeatures, getObjectPages } from './objectPageUtils';
1616
import { getFilterFieldNames, getListReportFeatures } from './listReportUtils';
17+
import { extractTableColumnsFromNode } from './tableUtils';
1718

1819
export interface AggregationItem extends TreeAggregation {
1920
description: string;
@@ -128,44 +129,6 @@ export async function getAppFeatures(
128129
return featureData;
129130
}
130131

131-
/**
132-
* Gets identifier of a column for OPA5 tests.
133-
* If the column is custom, the identifier is taken from the 'Key' entry in the schema keys.
134-
* If the column is not custom, the identifier is taken from the 'Value' entry in the schema keys.
135-
* If no such entry is found, undefined is returned.
136-
*
137-
* @param column - column module from ux specification
138-
* @param column.custom boolean indicating whether the column is custom
139-
* @param column.schema schema of the column
140-
* @param column.schema.keys keys of the column; expected to have an entry with the name 'Key' or 'Value'
141-
* @returns identifier of the column for OPA5 tests; can be the name or index
142-
*/
143-
function getColumnIdentifier(column: {
144-
custom: boolean;
145-
schema: { keys: { name: string; value: string }[] };
146-
}): string | undefined {
147-
const key = column.custom ? 'Key' : 'Value';
148-
const keyEntry = column.schema.keys.find((entry: { name: string; value: string }) => entry.name === key);
149-
return keyEntry?.value;
150-
}
151-
152-
/**
153-
* Transforms column aggregations from the ux specification model into a map of columns for OPA5 tests.
154-
*
155-
* @param columnAggregations column aggregations from the ux specification model
156-
* @returns a map of columns for OPA5 tests
157-
*/
158-
function transformTableColumns(columnAggregations: Record<string, any>): Record<string, any> {
159-
const columns: Record<string, any> = {};
160-
Object.values(columnAggregations).forEach((columnAggregation, index) => {
161-
columns[getColumnIdentifier(columnAggregation) ?? index] = {
162-
header: columnAggregation.description
163-
// TODO possibly more reliable properties could be used?
164-
};
165-
});
166-
return columns;
167-
}
168-
169132
/**
170133
* Retrieves table column data from the page model using ux-specification.
171134
*
@@ -180,8 +143,7 @@ export function getTableColumnData(
180143
let tableColumns: Record<string, Record<string, string | number | boolean>> = {};
181144

182145
try {
183-
const columnAggregations = getTableColumns(pageModel);
184-
tableColumns = transformTableColumns(columnAggregations);
146+
tableColumns = extractTableColumnsFromNode(pageModel.root);
185147
} catch (error) {
186148
log?.debug(error);
187149
}
@@ -292,17 +254,3 @@ export function getFilterFields(pageModel: TreeModel): TreeAggregations {
292254
const selectionFieldsAggregations = getAggregations(selectionFields);
293255
return selectionFieldsAggregations;
294256
}
295-
296-
/**
297-
* Retrieves the table columns aggregation from the given tree model.
298-
*
299-
* @param pageModel - The tree model containing table column definitions.
300-
* @returns The table columns aggregation object.
301-
*/
302-
export function getTableColumns(pageModel: TreeModel): TreeAggregations {
303-
const table = getAggregations(pageModel.root)['table'];
304-
const tableAggregations = getAggregations(table);
305-
const columns = tableAggregations['columns'];
306-
const columnAggregations = getAggregations(columns);
307-
return columnAggregations;
308-
}

packages/ui5-test-writer/src/utils/objectPageUtils.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Logger } from '@sap-ux/logger';
22
import type { ApplicationModel } from '@sap/ux-specification/dist/types/src/parser';
33
import type {
44
FormField,
5+
SectionFormField,
56
BodySectionFeatureData,
67
BodySubSectionFeatureData,
78
HeaderSectionFeatureData,
@@ -17,6 +18,7 @@ import {
1718
type SectionItem,
1819
getAggregations
1920
} from './modelUtils';
21+
import { extractTableColumnsFromNode } from './tableUtils';
2022
import { PageTypeV4 } from '@sap/ux-specification/dist/types/src/common/page';
2123

2224
/**
@@ -158,9 +160,12 @@ function extractObjectPageBodySectionsData(objectPage: PageWithModelV4): BodySec
158160
const subSections = extractBodySubSectionsData(section, sectionId);
159161
bodySections.push({
160162
id: sectionId,
163+
navigationProperty: getNavigationPropertyFromKey(sectionKey),
161164
isTable: !!section.isTable,
162165
custom: !!section.custom,
163166
order: section?.order ?? -1, // put a negative order number to signal that order was not in spec
167+
fields: section.custom || section.isTable ? [] : extractFormFields(section),
168+
tableColumns: section.custom || !section.isTable ? {} : extractTableColumnsFromNode(section),
164169
subSections
165170
});
166171
});
@@ -184,14 +189,53 @@ function extractBodySubSectionsData(section: SectionItem, parentSectionId: strin
184189
const subSectionId = getSectionIdentifier(subSection) ?? `${parentSectionId}_${subSectionKey}`;
185190
subSections.push({
186191
id: subSectionId,
192+
navigationProperty: getNavigationPropertyFromKey(subSectionKey),
187193
isTable: !!subSection.isTable,
188194
custom: !!subSection.custom,
189-
order: subSection?.order ?? -1 // put a negative order number to signal that order was not in spec
195+
order: subSection?.order ?? -1, // put a negative order number to signal that order was not in spec
196+
fields: subSection.custom || subSection.isTable ? [] : extractFormFields(subSection),
197+
tableColumns: subSection.custom || !subSection.isTable ? {} : extractTableColumnsFromNode(subSection)
190198
});
191199
});
192200
return subSections;
193201
}
194202

203+
/**
204+
* Extracts form field property paths from a body sub-section's form aggregation.
205+
*
206+
* @param subSection - body sub-section entry from the application model
207+
* @returns array of form field property paths for use with iCheckField({ property })
208+
*/
209+
function extractFormFields(subSection: BodySectionItem): SectionFormField[] {
210+
const fields: SectionFormField[] = [];
211+
const formAggregation = getAggregations(subSection)['form'] as AggregationItem;
212+
if (!formAggregation) {
213+
return fields;
214+
}
215+
const fieldsAggregation = getAggregations(formAggregation)['fields'] as AggregationItem;
216+
const fieldItems = getAggregations(fieldsAggregation) as Record<string, FieldItem>;
217+
Object.values(fieldItems).forEach((field) => {
218+
const property = field.schema?.keys?.find((key) => key.name === 'Value')?.value;
219+
if (property) {
220+
fields.push({ property });
221+
}
222+
});
223+
return fields;
224+
}
225+
226+
/**
227+
* Extracts the OData navigation property from a spec model section key.
228+
* Section keys for table sections follow the pattern `_NavProperty::@annotation`, so the
229+
* navigation property is the part before `::` when it starts with an underscore.
230+
*
231+
* @param sectionKey - the key of the section in the spec model aggregations
232+
* @returns navigation property (e.g. '_Booking'), or undefined for non-navigation sections
233+
*/
234+
function getNavigationPropertyFromKey(sectionKey: string): string | undefined {
235+
const prefix = sectionKey.split('::')[0];
236+
return prefix.startsWith('_') ? prefix : undefined;
237+
}
238+
195239
/**
196240
* Gets the identifier of a section for OPA5 tests.
197241
*
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { TreeAggregation, TreeAggregations } from '@sap/ux-specification/dist/types/src/parser';
2+
import { getAggregations } from './modelUtils';
3+
import type { TableColumn, TableColumnFeatureData } from '../types';
4+
5+
type ColumnModelItem = {
6+
custom?: boolean;
7+
description?: string;
8+
schema: { keys: { name: string; value: string }[] };
9+
};
10+
11+
export type ColumnAggregations = TreeAggregations & {
12+
[key: string]: ColumnModelItem;
13+
};
14+
15+
/**
16+
* Gets the identifier of a column for OPA5 tests.
17+
* Custom columns use the 'Key' entry; standard columns use the 'Value' entry from the schema keys.
18+
*
19+
* @param column - column item from ux specification
20+
* @returns identifier of the column for OPA5 tests; undefined if no matching key entry is found
21+
*/
22+
export function getColumnIdentifier(column: ColumnModelItem): string | undefined {
23+
const key = column.custom ? 'Key' : 'Value';
24+
return column.schema.keys.find((k) => k.name === key)?.value;
25+
}
26+
27+
/**
28+
* Transforms column aggregations from the ux specification model into a map of columns for OPA5 tests.
29+
* Each column entry includes the column header label for display verification.
30+
*
31+
* @param columnAggregations - column aggregations from the ux specification model
32+
* @returns a map of column identifiers to column state objects for use with iCheckColumns()
33+
*/
34+
export function transformTableColumns(columnAggregations: ColumnAggregations): TableColumnFeatureData {
35+
const columns: TableColumnFeatureData = {};
36+
Object.values(columnAggregations).forEach((column, index) => {
37+
const id = getColumnIdentifier(column) ?? String(index);
38+
const state: TableColumn = {};
39+
if (column.description) {
40+
state['header'] = column.description;
41+
}
42+
columns[id] = state;
43+
});
44+
return columns;
45+
}
46+
47+
/**
48+
* Extracts table column data from a spec model node that contains a 'table' aggregation.
49+
* Covers both page-level nodes (List Report, FPM) via their root and section-level nodes
50+
* (Object Page body sections) — both are TreeAggregation nodes that expose a 'table' aggregation.
51+
*
52+
* @param node - tree aggregation node that exposes a 'table' aggregation
53+
* @returns a map of column identifiers to column state objects for use with iCheckColumns()
54+
*/
55+
export function extractTableColumnsFromNode(node: TreeAggregation): TableColumnFeatureData {
56+
const tableAggregation = getAggregations(node)['table'];
57+
if (!tableAggregation) {
58+
return {};
59+
}
60+
const columnsAggregation = getAggregations(tableAggregation)['columns'];
61+
if (!columnsAggregation) {
62+
return {};
63+
}
64+
const columnItems = getAggregations(columnsAggregation);
65+
return transformTableColumns(columnItems as ColumnAggregations);
66+
}

packages/ui5-test-writer/templates/v4/integration/ObjectPageJourney.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,25 @@ sap.ui.define([
7474
<% section.subSections.forEach(function(subSection) { -%>
7575
//When.onThe<%- name%>.iGoToSection({ section: "<%- section.id %>", subSection: "<%- subSection.id %>" });
7676
Then.onThe<%- name%>.iCheckSubSection({ section: "<%- subSection.id %>" });
77+
<% if (subSection.fields && subSection.fields.length > 0) { -%>
78+
<% subSection.fields.forEach(function(field) { -%>
79+
Then.onThe<%- name%>.onForm({ section: "<%- subSection.id %>" }).iCheckField({ property: "<%- field.property %>" });
7780
<% }) -%>
7881
<% } -%>
82+
<% if (subSection.tableColumns && Object.keys(subSection.tableColumns).length > 0 && subSection.navigationProperty) { -%>
83+
Then.onThe<%- name%>.onTable({ property: "<%- subSection.navigationProperty %>" }).iCheckColumns(<%- JSON.stringify(subSection.tableColumns) %>);
84+
<% } -%>
85+
<% }) -%>
86+
<% } else { -%>
87+
<% if (section.fields && section.fields.length > 0) { -%>
88+
<% section.fields.forEach(function(field) { -%>
89+
Then.onThe<%- name%>.onForm({ section: "<%- section.id %>" }).iCheckField({ property: "<%- field.property %>" });
90+
<% }) -%>
91+
<% } -%>
92+
<% if (section.tableColumns && Object.keys(section.tableColumns).length > 0 && section.navigationProperty) { -%>
93+
Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckColumns(<%- JSON.stringify(section.tableColumns) %>);
94+
<% } -%>
95+
<% } -%>
7996
<% }) -%>
8097
});
8198
<% } -%>

packages/ui5-test-writer/test/test-input/constants.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/ui5-test-writer/test/unit/fiori-elements.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,15 @@ describe('ui5-test-writer', () => {
506506
expect(bookingObjPageJourneyContent).toContain('iCheckSection({ section: "FlightData" })');
507507
expect(bookingObjPageJourneyContent).toContain('iPressSectionIconTabFilterButton("PriceData")');
508508
expect(bookingObjPageJourneyContent).toContain('iCheckSection({ section: "PriceData" })');
509+
expect(bookingObjPageJourneyContent).toContain(
510+
'onForm({ section: "BookingData" }).iCheckField({ property: "BookingId" })'
511+
);
512+
expect(bookingObjPageJourneyContent).toContain(
513+
'onForm({ section: "BookingData" }).iCheckField({ property: "FlightDate" })'
514+
);
515+
expect(bookingObjPageJourneyContent).toContain('onTable({ property: "_Supplements" }).iCheckColumns(');
516+
expect(bookingObjPageJourneyContent).toContain('"ConnectionId":{"header":"Connection"}');
517+
expect(bookingObjPageJourneyContent).toContain('"AirportCode":{"header":"Airport"}');
509518
});
510519
});
511520
});

0 commit comments

Comments
 (0)