@sap-ux/ui5-test-writer 0.9.3 → 0.9.4

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
@@ -107,6 +107,9 @@ export type BodySectionFeatureData = {
107
107
  fields: SectionFormField[];
108
108
  tableColumns: TableColumnFeatureData;
109
109
  subSections: BodySubSectionFeatureData[];
110
+ actions?: ActionButtonState[];
111
+ createButton?: ButtonState;
112
+ deleteButton?: ButtonState;
110
113
  };
111
114
  export type ObjectPageFeatures = {
112
115
  name?: string;
@@ -115,6 +118,8 @@ export type ObjectPageFeatures = {
115
118
  headerDescription?: string;
116
119
  headerSections?: HeaderSectionFeatureData[];
117
120
  bodySections?: BodySectionFeatureData[];
121
+ headerActions?: ActionButtonState[];
122
+ editButton?: ButtonState;
118
123
  };
119
124
  export type ListReportFeatures = {
120
125
  name?: string;
@@ -135,6 +140,10 @@ export type ListReportFeatures = {
135
140
  };
136
141
  export interface ActionButtonState {
137
142
  label: string;
143
+ /**
144
+ * For List Report actions: the full OData binding path (e.g. "namespace.ActionName(entityType=@odata.context)").
145
+ * For Object Page actions extracted from the spec model: the method name only (e.g. "Copy").
146
+ */
138
147
  action: string;
139
148
  visible: boolean;
140
149
  /**
@@ -153,6 +162,16 @@ export interface ActionButtonState {
153
162
  * The invocation grouping type if specified (e.g., "Isolated", "ChangeSet").
154
163
  */
155
164
  invocationGrouping?: string;
165
+ /**
166
+ * OData schema namespace used as the `service` parameter in iCheckAction({ service, action, unbound }).
167
+ * Populated for Object Page actions extracted via the spec model + metadata.
168
+ */
169
+ service?: string;
170
+ /**
171
+ * Whether the action is unbound (not bound to a specific entity instance).
172
+ * Populated for Object Page actions extracted via the spec model + metadata.
173
+ */
174
+ unbound?: boolean;
156
175
  }
157
176
  export type FPMFeatures = {
158
177
  name?: string;
@@ -0,0 +1,137 @@
1
+ import type { ConvertedMetadata } from '@sap-ux/vocabularies-types';
2
+ import type { ActionButtonState, ButtonState, ButtonVisibilityResult } from '../types';
3
+ import type { DataFieldForAction } from '@sap-ux/vocabularies-types/vocabularies/UI';
4
+ import type { OperationAvailable } from '@sap-ux/vocabularies-types/vocabularies/Core';
5
+ import type { DeleteRestrictionsType, InsertRestrictionsType, UpdateRestrictionsType } from '@sap-ux/vocabularies-types/vocabularies/Capabilities';
6
+ import type { Logger } from '@sap-ux/logger';
7
+ type OperationAvailableWithPaths = OperationAvailable & {
8
+ $Path?: string;
9
+ path?: string;
10
+ };
11
+ type RestrictionValueWithPaths = (boolean | {
12
+ $Path?: string;
13
+ path?: string;
14
+ }) | undefined;
15
+ /**
16
+ * Extracts the action method name from a fully qualified action string.
17
+ *
18
+ * @param actionName The fully qualified action name
19
+ * @returns The action method name
20
+ */
21
+ export declare function extractActionMethodName(actionName: string): string;
22
+ /**
23
+ * Finds the Core.OperationAvailable annotation for a specific action.
24
+ *
25
+ * @param metadata The converted metadata
26
+ * @param actionMethodName The action method name
27
+ * @returns The OperationAvailable annotation value or undefined if not found
28
+ */
29
+ export declare function findOperationAvailableAnnotation(metadata: ConvertedMetadata, actionMethodName: string): OperationAvailableWithPaths | undefined;
30
+ /**
31
+ * Analyzes Core.OperationAvailable annotation to determine action availability.
32
+ * Single-entity bound actions (requiring row selection) are disabled by default when no annotation is present.
33
+ *
34
+ * @param operationAvailable The OperationAvailable annotation value
35
+ * @param isEntityBound Whether the action is bound to a single entity (requires row selection to enable)
36
+ * @returns Object containing enabled state and optional dynamic path
37
+ */
38
+ export declare function analyzeOperationAvailability(operationAvailable: OperationAvailableWithPaths | undefined, isEntityBound?: boolean): {
39
+ enabled: boolean | 'dynamic';
40
+ dynamicPath?: string;
41
+ };
42
+ /**
43
+ * Extracts the enum member value from an annotation.
44
+ *
45
+ * @param enumValue The enum value object
46
+ * @returns The extracted enum value string
47
+ */
48
+ export declare function extractEnumMemberValue(enumValue: unknown): string | undefined;
49
+ /**
50
+ * Builds an ActionButtonState object from a DataFieldForAction annotation item.
51
+ *
52
+ * @param item The DataFieldForAction annotation item
53
+ * @param metadata The converted metadata
54
+ * @returns ActionButtonState for the action
55
+ */
56
+ export declare function buildActionButtonState(item: DataFieldForAction, metadata: ConvertedMetadata): ActionButtonState;
57
+ /**
58
+ * Builds an ActionButtonState from a spec model aggregation key.
59
+ *
60
+ * Key format: "DataFieldForAction::<namespace>.<Method>::<namespace>.<EntityType>"
61
+ * Example: "DataFieldForAction::com.example.Copy::com.example.POEntity".
62
+ *
63
+ * @param aggregationKey The spec model aggregation key for the action
64
+ * @param label Display label from the spec model item description
65
+ * @param convertedMetadata The converted OData metadata
66
+ * @param schemaNamespace The OData schema namespace (used as service identifier)
67
+ * @returns ActionButtonState or undefined if the key is not a DataFieldForAction key
68
+ */
69
+ export declare function buildActionStateFromSpecModelKey(aggregationKey: string, label: string | undefined, convertedMetadata: ConvertedMetadata, schemaNamespace: string): ActionButtonState | undefined;
70
+ /**
71
+ * Analyzes a restriction value (Insertable, Deletable, or Updatable) to determine button state.
72
+ *
73
+ * @param value The annotation value — boolean, path object, or undefined
74
+ * @returns ButtonState indicating visibility and enabled state
75
+ */
76
+ export declare function analyzeRestrictionValue(value: RestrictionValueWithPaths): ButtonState;
77
+ /**
78
+ * Analyzes InsertRestrictions annotation to determine create button visibility and enabled state.
79
+ *
80
+ * @param restriction The InsertRestrictions annotation for the entity set
81
+ * @returns ButtonState indicating visibility and enabled state based on the Insertable value
82
+ */
83
+ export declare function analyzeInsertRestrictions(restriction: InsertRestrictionsType | undefined): ButtonState;
84
+ /**
85
+ * Analyzes DeleteRestrictions annotation to determine delete button visibility and enabled state.
86
+ *
87
+ * @param restriction The DeleteRestrictions annotation for the entity set
88
+ * @returns ButtonState indicating visibility and enabled state based on the Deletable value
89
+ */
90
+ export declare function analyzeDeleteRestrictions(restriction: DeleteRestrictionsType | undefined): ButtonState;
91
+ /**
92
+ * Analyzes UpdateRestrictions annotation to determine edit button visibility and enabled state.
93
+ *
94
+ * @param restriction The UpdateRestrictions annotation for the entity set
95
+ * @returns ButtonState indicating visibility and enabled state based on the Updatable value
96
+ */
97
+ export declare function analyzeUpdateRestrictions(restriction: UpdateRestrictionsType | undefined): ButtonState;
98
+ /**
99
+ * Checks the visibility and enabled state of create and delete buttons for a given entity set
100
+ * by analyzing OData Capabilities annotations in the metadata.
101
+ *
102
+ * @param metadataXml The OData metadata XML content as a string
103
+ * @param entitySetName The name of the entity set to check
104
+ * @returns ButtonVisibilityResult containing the state of create and delete buttons
105
+ * @throws {Error} If metadata cannot be parsed or entity set is not found
106
+ */
107
+ export declare function checkButtonVisibility(metadataXml: string, entitySetName: string): ButtonVisibilityResult;
108
+ /**
109
+ * Checks the visibility and enabled state of the edit button for a given entity set
110
+ * by analyzing UpdateRestrictions in the metadata.
111
+ *
112
+ * @param metadataXml The OData metadata XML content as a string
113
+ * @param entitySetName The name of the entity set to check
114
+ * @returns ButtonState for the edit button
115
+ * @throws {Error} If metadata cannot be parsed or entity set is not found
116
+ */
117
+ export declare function checkEditVisibility(metadataXml: string, entitySetName: string): ButtonState;
118
+ /**
119
+ * Safely checks button visibility with error handling.
120
+ *
121
+ * @param metadata The OData metadata XML content
122
+ * @param entitySetName The name of the entity set
123
+ * @param log Optional logger instance
124
+ * @returns Button visibility result or undefined if error occurs
125
+ */
126
+ export declare function safeCheckButtonVisibility(metadata: string, entitySetName: string, log?: Logger): ButtonVisibilityResult | undefined;
127
+ /**
128
+ * Safely checks edit button visibility with error handling.
129
+ *
130
+ * @param metadata The OData metadata XML content
131
+ * @param entitySetName The name of the entity set
132
+ * @param log Optional logger instance
133
+ * @returns ButtonState for the edit button, or undefined if error occurs
134
+ */
135
+ export declare function safeCheckEditVisibility(metadata: string, entitySetName: string, log?: Logger): ButtonState | undefined;
136
+ export {};
137
+ //# sourceMappingURL=actionUtils.d.ts.map
@@ -0,0 +1,304 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractActionMethodName = extractActionMethodName;
4
+ exports.findOperationAvailableAnnotation = findOperationAvailableAnnotation;
5
+ exports.analyzeOperationAvailability = analyzeOperationAvailability;
6
+ exports.extractEnumMemberValue = extractEnumMemberValue;
7
+ exports.buildActionButtonState = buildActionButtonState;
8
+ exports.buildActionStateFromSpecModelKey = buildActionStateFromSpecModelKey;
9
+ exports.analyzeRestrictionValue = analyzeRestrictionValue;
10
+ exports.analyzeInsertRestrictions = analyzeInsertRestrictions;
11
+ exports.analyzeDeleteRestrictions = analyzeDeleteRestrictions;
12
+ exports.analyzeUpdateRestrictions = analyzeUpdateRestrictions;
13
+ exports.checkButtonVisibility = checkButtonVisibility;
14
+ exports.checkEditVisibility = checkEditVisibility;
15
+ exports.safeCheckButtonVisibility = safeCheckButtonVisibility;
16
+ exports.safeCheckEditVisibility = safeCheckEditVisibility;
17
+ const edmx_parser_1 = require("@sap-ux/edmx-parser");
18
+ const annotation_converter_1 = require("@sap-ux/annotation-converter");
19
+ const DATA_FIELD_FOR_ACTION = 'DataFieldForAction';
20
+ /**
21
+ * Extracts the action method name from a fully qualified action string.
22
+ *
23
+ * @param actionName The fully qualified action name
24
+ * @returns The action method name
25
+ */
26
+ function extractActionMethodName(actionName) {
27
+ const match = /\.([^.()]+)\(/.exec(actionName);
28
+ if (match?.[1]) {
29
+ return match[1];
30
+ }
31
+ const lastDotIndex = actionName.lastIndexOf('.');
32
+ const parenIndex = actionName.indexOf('(');
33
+ if (lastDotIndex >= 0 && parenIndex >= 0 && parenIndex > lastDotIndex) {
34
+ return actionName.substring(lastDotIndex + 1, parenIndex);
35
+ }
36
+ // Handle namespace-qualified name without parentheses (spec model key format: "namespace.Method")
37
+ if (lastDotIndex >= 0) {
38
+ return actionName.substring(lastDotIndex + 1);
39
+ }
40
+ return actionName;
41
+ }
42
+ /**
43
+ * Finds the Core.OperationAvailable annotation for a specific action.
44
+ *
45
+ * @param metadata The converted metadata
46
+ * @param actionMethodName The action method name
47
+ * @returns The OperationAvailable annotation value or undefined if not found
48
+ */
49
+ function findOperationAvailableAnnotation(metadata, actionMethodName) {
50
+ if (metadata.actions) {
51
+ const foundAction = metadata.actions.find((action) => action.name === actionMethodName || action.fullyQualifiedName?.includes(`.${actionMethodName}(`));
52
+ if (foundAction?.annotations?.Core?.OperationAvailable !== undefined) {
53
+ return foundAction.annotations.Core.OperationAvailable;
54
+ }
55
+ }
56
+ if (metadata.entityContainer?.annotations) {
57
+ const annotations = metadata.entityContainer.annotations;
58
+ const matchingKey = Object.keys(annotations).find((key) => key === actionMethodName || key.endsWith(`.${actionMethodName}`));
59
+ if (matchingKey && annotations[matchingKey]?.Core?.OperationAvailable !== undefined) {
60
+ return annotations[matchingKey].Core.OperationAvailable;
61
+ }
62
+ }
63
+ return undefined;
64
+ }
65
+ /**
66
+ * Analyzes Core.OperationAvailable annotation to determine action availability.
67
+ * Single-entity bound actions (requiring row selection) are disabled by default when no annotation is present.
68
+ *
69
+ * @param operationAvailable The OperationAvailable annotation value
70
+ * @param isEntityBound Whether the action is bound to a single entity (requires row selection to enable)
71
+ * @returns Object containing enabled state and optional dynamic path
72
+ */
73
+ function analyzeOperationAvailability(operationAvailable, isEntityBound) {
74
+ if (operationAvailable === undefined) {
75
+ return { enabled: !isEntityBound };
76
+ }
77
+ if (typeof operationAvailable === 'boolean') {
78
+ return { enabled: operationAvailable };
79
+ }
80
+ if (typeof operationAvailable === 'object' && operationAvailable !== null) {
81
+ const pathRecord = operationAvailable;
82
+ const path = pathRecord.$Path ?? pathRecord.path;
83
+ if (path) {
84
+ return { enabled: 'dynamic', dynamicPath: path };
85
+ }
86
+ }
87
+ return { enabled: true };
88
+ }
89
+ /**
90
+ * Extracts the enum member value from an annotation.
91
+ *
92
+ * @param enumValue The enum value object
93
+ * @returns The extracted enum value string
94
+ */
95
+ function extractEnumMemberValue(enumValue) {
96
+ if (typeof enumValue === 'string') {
97
+ return enumValue;
98
+ }
99
+ const enumRecord = enumValue;
100
+ if (enumRecord?.$EnumMember) {
101
+ const parts = enumRecord.$EnumMember.split('/');
102
+ return parts[1] ?? enumRecord.$EnumMember;
103
+ }
104
+ return undefined;
105
+ }
106
+ /**
107
+ * Builds an ActionButtonState object from a DataFieldForAction annotation item.
108
+ *
109
+ * @param item The DataFieldForAction annotation item
110
+ * @param metadata The converted metadata
111
+ * @returns ActionButtonState for the action
112
+ */
113
+ function buildActionButtonState(item, metadata) {
114
+ const actionString = item.Action || '';
115
+ const actionMethod = extractActionMethodName(actionString);
116
+ const operationAvailable = findOperationAvailableAnnotation(metadata, actionMethod);
117
+ // Bound actions whose binding parameter is a single entity (not a collection) require
118
+ // row selection to be invoked, so they are disabled by default (no row selected).
119
+ // Collection-bound actions operate on the entity set and are always enabled.
120
+ const actionTarget = item.ActionTarget;
121
+ const isEntityBound = actionTarget?.isBound === true && actionTarget?.parameters?.[0]?.isCollection !== true;
122
+ const { enabled, dynamicPath } = analyzeOperationAvailability(operationAvailable, isEntityBound);
123
+ return {
124
+ label: item.Label || '',
125
+ action: actionString,
126
+ visible: true,
127
+ enabled,
128
+ dynamicPath,
129
+ invocationGrouping: item.InvocationGrouping ? extractEnumMemberValue(item.InvocationGrouping) : undefined
130
+ };
131
+ }
132
+ /**
133
+ * Builds an ActionButtonState from a spec model aggregation key.
134
+ *
135
+ * Key format: "DataFieldForAction::<namespace>.<Method>::<namespace>.<EntityType>"
136
+ * Example: "DataFieldForAction::com.example.Copy::com.example.POEntity".
137
+ *
138
+ * @param aggregationKey The spec model aggregation key for the action
139
+ * @param label Display label from the spec model item description
140
+ * @param convertedMetadata The converted OData metadata
141
+ * @param schemaNamespace The OData schema namespace (used as service identifier)
142
+ * @returns ActionButtonState or undefined if the key is not a DataFieldForAction key
143
+ */
144
+ function buildActionStateFromSpecModelKey(aggregationKey, label, convertedMetadata, schemaNamespace) {
145
+ const keyParts = aggregationKey.split('::');
146
+ if (keyParts[0] !== DATA_FIELD_FOR_ACTION || !keyParts[1]) {
147
+ return undefined;
148
+ }
149
+ const actionFullName = keyParts[1]; // "namespace.Method"
150
+ const actionMethod = extractActionMethodName(actionFullName);
151
+ const actionDefinition = convertedMetadata.actions?.find((action) => action.name === actionMethod || action.fullyQualifiedName?.includes(`.${actionMethod}(`));
152
+ const firstParameter = actionDefinition?.parameters?.[0];
153
+ const isEntityBound = actionDefinition?.isBound === true && firstParameter?.isCollection !== true;
154
+ const operationAvailable = findOperationAvailableAnnotation(convertedMetadata, actionMethod);
155
+ const { enabled, dynamicPath } = analyzeOperationAvailability(operationAvailable, isEntityBound);
156
+ return {
157
+ label: label ?? '',
158
+ action: actionMethod,
159
+ service: schemaNamespace,
160
+ unbound: !isEntityBound,
161
+ visible: true,
162
+ enabled,
163
+ dynamicPath
164
+ };
165
+ }
166
+ /**
167
+ * Analyzes a restriction value (Insertable, Deletable, or Updatable) to determine button state.
168
+ *
169
+ * @param value The annotation value — boolean, path object, or undefined
170
+ * @returns ButtonState indicating visibility and enabled state
171
+ */
172
+ function analyzeRestrictionValue(value) {
173
+ const defaultState = { visible: true, enabled: true };
174
+ if (value === undefined || value === null) {
175
+ return defaultState;
176
+ }
177
+ if (typeof value === 'boolean') {
178
+ return { visible: value, enabled: value };
179
+ }
180
+ if (typeof value === 'object') {
181
+ const path = value.$Path ?? value.path;
182
+ if (path) {
183
+ return { visible: true, enabled: 'dynamic', dynamicPath: path };
184
+ }
185
+ }
186
+ return defaultState;
187
+ }
188
+ /**
189
+ * Analyzes InsertRestrictions annotation to determine create button visibility and enabled state.
190
+ *
191
+ * @param restriction The InsertRestrictions annotation for the entity set
192
+ * @returns ButtonState indicating visibility and enabled state based on the Insertable value
193
+ */
194
+ function analyzeInsertRestrictions(restriction) {
195
+ const value = restriction ? restriction['Insertable'] : undefined;
196
+ return analyzeRestrictionValue(value);
197
+ }
198
+ /**
199
+ * Analyzes DeleteRestrictions annotation to determine delete button visibility and enabled state.
200
+ *
201
+ * @param restriction The DeleteRestrictions annotation for the entity set
202
+ * @returns ButtonState indicating visibility and enabled state based on the Deletable value
203
+ */
204
+ function analyzeDeleteRestrictions(restriction) {
205
+ const value = restriction ? restriction['Deletable'] : undefined;
206
+ return analyzeRestrictionValue(value);
207
+ }
208
+ /**
209
+ * Analyzes UpdateRestrictions annotation to determine edit button visibility and enabled state.
210
+ *
211
+ * @param restriction The UpdateRestrictions annotation for the entity set
212
+ * @returns ButtonState indicating visibility and enabled state based on the Updatable value
213
+ */
214
+ function analyzeUpdateRestrictions(restriction) {
215
+ const value = restriction ? restriction['Updatable'] : undefined;
216
+ return analyzeRestrictionValue(value);
217
+ }
218
+ /**
219
+ * Checks the visibility and enabled state of create and delete buttons for a given entity set
220
+ * by analyzing OData Capabilities annotations in the metadata.
221
+ *
222
+ * @param metadataXml The OData metadata XML content as a string
223
+ * @param entitySetName The name of the entity set to check
224
+ * @returns ButtonVisibilityResult containing the state of create and delete buttons
225
+ * @throws {Error} If metadata cannot be parsed or entity set is not found
226
+ */
227
+ function checkButtonVisibility(metadataXml, entitySetName) {
228
+ try {
229
+ const convertedMetadata = (0, annotation_converter_1.convert)((0, edmx_parser_1.parse)(metadataXml));
230
+ const entitySet = convertedMetadata.entitySets.find((es) => es.name === entitySetName);
231
+ if (!entitySet) {
232
+ throw new Error(`Entity set '${entitySetName}' not found in metadata`);
233
+ }
234
+ const insertRestrictions = entitySet.annotations?.Capabilities?.InsertRestrictions;
235
+ const deleteRestrictions = entitySet.annotations?.Capabilities?.DeleteRestrictions;
236
+ return {
237
+ create: analyzeInsertRestrictions(insertRestrictions),
238
+ delete: analyzeDeleteRestrictions(deleteRestrictions)
239
+ };
240
+ }
241
+ catch (error) {
242
+ const errorMessage = error instanceof Error ? error.message : String(error);
243
+ throw new Error(`Failed to analyze button visibility: ${errorMessage}`);
244
+ }
245
+ }
246
+ /**
247
+ * Checks the visibility and enabled state of the edit button for a given entity set
248
+ * by analyzing UpdateRestrictions in the metadata.
249
+ *
250
+ * @param metadataXml The OData metadata XML content as a string
251
+ * @param entitySetName The name of the entity set to check
252
+ * @returns ButtonState for the edit button
253
+ * @throws {Error} If metadata cannot be parsed or entity set is not found
254
+ */
255
+ function checkEditVisibility(metadataXml, entitySetName) {
256
+ try {
257
+ const convertedMetadata = (0, annotation_converter_1.convert)((0, edmx_parser_1.parse)(metadataXml));
258
+ const entitySet = convertedMetadata.entitySets.find((es) => es.name === entitySetName);
259
+ if (!entitySet) {
260
+ throw new Error(`Entity set '${entitySetName}' not found in metadata`);
261
+ }
262
+ const updateRestrictions = entitySet.annotations?.Capabilities?.UpdateRestrictions;
263
+ return analyzeUpdateRestrictions(updateRestrictions);
264
+ }
265
+ catch (error) {
266
+ const errorMessage = error instanceof Error ? error.message : String(error);
267
+ throw new Error(`Failed to analyze edit visibility: ${errorMessage}`);
268
+ }
269
+ }
270
+ /**
271
+ * Safely checks button visibility with error handling.
272
+ *
273
+ * @param metadata The OData metadata XML content
274
+ * @param entitySetName The name of the entity set
275
+ * @param log Optional logger instance
276
+ * @returns Button visibility result or undefined if error occurs
277
+ */
278
+ function safeCheckButtonVisibility(metadata, entitySetName, log) {
279
+ try {
280
+ return checkButtonVisibility(metadata, entitySetName);
281
+ }
282
+ catch (error) {
283
+ log?.debug(`Failed to check button visibility: ${error instanceof Error ? error.message : String(error)}`);
284
+ return undefined;
285
+ }
286
+ }
287
+ /**
288
+ * Safely checks edit button visibility with error handling.
289
+ *
290
+ * @param metadata The OData metadata XML content
291
+ * @param entitySetName The name of the entity set
292
+ * @param log Optional logger instance
293
+ * @returns ButtonState for the edit button, or undefined if error occurs
294
+ */
295
+ function safeCheckEditVisibility(metadata, entitySetName, log) {
296
+ try {
297
+ return checkEditVisibility(metadata, entitySetName);
298
+ }
299
+ catch (error) {
300
+ log?.debug(`Failed to check edit visibility: ${error instanceof Error ? error.message : String(error)}`);
301
+ return undefined;
302
+ }
303
+ }
304
+ //# sourceMappingURL=actionUtils.js.map
@@ -1,8 +1,11 @@
1
1
  import type { Logger } from '@sap-ux/logger';
2
2
  import type { TreeAggregations, TreeModel } from '@sap/ux-specification/dist/types/src/parser';
3
- import type { ActionButtonsResult, ActionButtonState, ButtonState, ButtonVisibilityResult, FEV4ManifestTarget, ListReportFeatures } from '../types';
3
+ import type { ActionButtonsResult, ActionButtonState, ButtonState, FEV4ManifestTarget, ListReportFeatures } from '../types';
4
+ import { safeCheckButtonVisibility } from './actionUtils';
4
5
  import type { PageWithModelV4 } from '@sap/ux-specification/dist/types/src/parser/application';
5
6
  import type { Manifest } from '@sap-ux/project-access';
7
+ export { checkButtonVisibility, safeCheckEditVisibility } from './actionUtils';
8
+ export { safeCheckButtonVisibility };
6
9
  /**
7
10
  * Builds a button state object from button visibility result.
8
11
  *
@@ -14,15 +17,6 @@ export declare function buildButtonState(buttonState?: ButtonState): {
14
17
  enabled?: boolean | 'dynamic';
15
18
  dynamicPath?: string;
16
19
  };
17
- /**
18
- * Safely checks button visibility with error handling.
19
- *
20
- * @param metadata - The OData metadata XML content
21
- * @param entitySetName - The name of the entity set
22
- * @param log - Optional logger instance
23
- * @returns Button visibility result or undefined if error occurs
24
- */
25
- export declare function safeCheckButtonVisibility(metadata: string, entitySetName: string, log?: Logger): ButtonVisibilityResult | undefined;
26
20
  /**
27
21
  * Safely checks action button states with error handling.
28
22
  *
@@ -67,16 +61,6 @@ export declare function getListReportFeatures(listReportPage: PageWithModelV4, l
67
61
  * @returns The toolbar actions aggregation object.
68
62
  */
69
63
  export declare function getToolBarActions(pageModel: TreeModel): TreeAggregations;
70
- /**
71
- * Checks the visibility and enabled state of create and delete buttons for a given entity set
72
- * by analyzing OData Capabilities annotations in the metadata.
73
- *
74
- * @param metadataXml The OData metadata XML content as a string
75
- * @param entitySetName The name of the entity set to check
76
- * @returns ButtonVisibilityResult containing the state of create and delete buttons
77
- * @throws {Error} If metadata cannot be parsed or entity set is not found
78
- */
79
- export declare function checkButtonVisibility(metadataXml: string, entitySetName: string): ButtonVisibilityResult;
80
64
  /**
81
65
  * Retrieves filter field names from the page model using ux-specification.
82
66
  *
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.safeCheckButtonVisibility = exports.safeCheckEditVisibility = exports.checkButtonVisibility = void 0;
3
4
  exports.buildButtonState = buildButtonState;
4
- exports.safeCheckButtonVisibility = safeCheckButtonVisibility;
5
5
  exports.safeCheckActionButtonStates = safeCheckActionButtonStates;
6
6
  exports.isALPManifestTarget = isALPManifestTarget;
7
7
  exports.isALPFromManifest = isALPFromManifest;
8
8
  exports.getListReportFeatures = getListReportFeatures;
9
9
  exports.getToolBarActions = getToolBarActions;
10
- exports.checkButtonVisibility = checkButtonVisibility;
11
10
  exports.getFilterFieldNames = getFilterFieldNames;
12
11
  exports.checkActionButtonStates = checkActionButtonStates;
13
12
  exports.getToolBarActionNames = getToolBarActionNames;
@@ -15,6 +14,11 @@ exports.getToolBarActionItems = getToolBarActionItems;
15
14
  const modelUtils_1 = require("./modelUtils");
16
15
  const edmx_parser_1 = require("@sap-ux/edmx-parser");
17
16
  const annotation_converter_1 = require("@sap-ux/annotation-converter");
17
+ const actionUtils_1 = require("./actionUtils");
18
+ Object.defineProperty(exports, "safeCheckButtonVisibility", { enumerable: true, get: function () { return actionUtils_1.safeCheckButtonVisibility; } });
19
+ var actionUtils_2 = require("./actionUtils");
20
+ Object.defineProperty(exports, "checkButtonVisibility", { enumerable: true, get: function () { return actionUtils_2.checkButtonVisibility; } });
21
+ Object.defineProperty(exports, "safeCheckEditVisibility", { enumerable: true, get: function () { return actionUtils_2.safeCheckEditVisibility; } });
18
22
  /**
19
23
  * Builds a button state object from button visibility result.
20
24
  *
@@ -28,23 +32,6 @@ function buildButtonState(buttonState) {
28
32
  dynamicPath: buttonState?.enabled === 'dynamic' ? buttonState.dynamicPath : undefined
29
33
  };
30
34
  }
31
- /**
32
- * Safely checks button visibility with error handling.
33
- *
34
- * @param metadata - The OData metadata XML content
35
- * @param entitySetName - The name of the entity set
36
- * @param log - Optional logger instance
37
- * @returns Button visibility result or undefined if error occurs
38
- */
39
- function safeCheckButtonVisibility(metadata, entitySetName, log) {
40
- try {
41
- return checkButtonVisibility(metadata, entitySetName);
42
- }
43
- catch (error) {
44
- log?.debug(`Failed to check button visibility: ${error instanceof Error ? error.message : String(error)}`);
45
- return undefined;
46
- }
47
- }
48
35
  /**
49
36
  * Safely checks action button states with error handling.
50
37
  *
@@ -103,7 +90,7 @@ function isALPFromManifest(manifest, targetKey) {
103
90
  */
104
91
  function getListReportFeatures(listReportPage, log, metadata, manifest) {
105
92
  const buttonVisibility = metadata && listReportPage.entitySet
106
- ? safeCheckButtonVisibility(metadata, listReportPage.entitySet, log)
93
+ ? (0, actionUtils_1.safeCheckButtonVisibility)(metadata, listReportPage.entitySet, log)
107
94
  : undefined;
108
95
  const toolbarActions = getToolBarActionNames(listReportPage.model, log);
109
96
  return {
@@ -133,34 +120,6 @@ function getToolBarActions(pageModel) {
133
120
  const actionAggregations = (0, modelUtils_1.getAggregations)(actions);
134
121
  return actionAggregations;
135
122
  }
136
- /**
137
- * Checks the visibility and enabled state of create and delete buttons for a given entity set
138
- * by analyzing OData Capabilities annotations in the metadata.
139
- *
140
- * @param metadataXml The OData metadata XML content as a string
141
- * @param entitySetName The name of the entity set to check
142
- * @returns ButtonVisibilityResult containing the state of create and delete buttons
143
- * @throws {Error} If metadata cannot be parsed or entity set is not found
144
- */
145
- function checkButtonVisibility(metadataXml, entitySetName) {
146
- try {
147
- const convertedMetadata = (0, annotation_converter_1.convert)((0, edmx_parser_1.parse)(metadataXml));
148
- const entitySet = convertedMetadata.entitySets.find((es) => es.name === entitySetName);
149
- if (!entitySet) {
150
- throw new Error(`Entity set '${entitySetName}' not found in metadata`);
151
- }
152
- const insertRestrictions = entitySet.annotations?.Capabilities?.InsertRestrictions;
153
- const deleteRestrictions = entitySet.annotations?.Capabilities?.DeleteRestrictions;
154
- return {
155
- create: analyzeRestriction(insertRestrictions, 'Insertable'),
156
- delete: analyzeRestriction(deleteRestrictions, 'Deletable')
157
- };
158
- }
159
- catch (error) {
160
- const errorMessage = error instanceof Error ? error.message : String(error);
161
- throw new Error(`Failed to analyze button visibility: ${errorMessage}`);
162
- }
163
- }
164
123
  /**
165
124
  * Retrieves filter field names from the page model using ux-specification.
166
125
  *
@@ -182,33 +141,6 @@ function getFilterFieldNames(pageModel, log) {
182
141
  }
183
142
  return filterBarItems;
184
143
  }
185
- /**
186
- * Analyzes a capability restriction annotation to determine button state.
187
- *
188
- * @param restriction The restriction annotation object (InsertRestrictions or DeleteRestrictions)
189
- * @param propertyName The property name to check ('Insertable' or 'Deletable')
190
- * @returns ButtonState for the button
191
- */
192
- function analyzeRestriction(restriction, propertyName) {
193
- const defaultState = { visible: true, enabled: true };
194
- if (!restriction) {
195
- return defaultState;
196
- }
197
- const value = restriction[propertyName];
198
- if (value === undefined || value === null) {
199
- return defaultState;
200
- }
201
- if (typeof value === 'boolean') {
202
- return { visible: value, enabled: value };
203
- }
204
- if (typeof value === 'object' && value !== null) {
205
- const path = value.$Path ?? value.path;
206
- if (path) {
207
- return { visible: true, enabled: 'dynamic', dynamicPath: path };
208
- }
209
- }
210
- return defaultState;
211
- }
212
144
  /**
213
145
  * Checks the state of action buttons defined in UI.LineItem annotations for a given entity set.
214
146
  *
@@ -235,8 +167,8 @@ function checkActionButtonStates(metadataXml, entitySetName, actionNames) {
235
167
  }
236
168
  const dataFieldForActions = lineItemAnnotation.filter((item) => item.$Type === 'com.sap.vocabularies.UI.v1.DataFieldForAction');
237
169
  const actions = actionNames
238
- ? findActionStates(dataFieldForActions, actionNames, convertedMetadata, entityType.name)
239
- : extractAllActionStates(dataFieldForActions, convertedMetadata, entityType.name);
170
+ ? findActionStates(dataFieldForActions, actionNames, convertedMetadata)
171
+ : extractAllActionStates(dataFieldForActions, convertedMetadata);
240
172
  return { actions, entityType: entityType.name };
241
173
  }
242
174
  catch (error) {
@@ -250,18 +182,17 @@ function checkActionButtonStates(metadataXml, entitySetName, actionNames) {
250
182
  * @param dataFieldForActions List of DataFieldForAction items from UI.LineItem
251
183
  * @param actionNames List of action names to find
252
184
  * @param metadata The converted metadata
253
- * @param entityTypeName The entity type name
254
185
  * @returns List of action button states for the specified actions
255
186
  */
256
- function findActionStates(dataFieldForActions, actionNames, metadata, entityTypeName) {
187
+ function findActionStates(dataFieldForActions, actionNames, metadata) {
257
188
  const actionStates = [];
258
189
  for (const actionName of actionNames) {
259
190
  const item = dataFieldForActions.find((dfa) => {
260
- const actionMethod = extractActionMethodName(dfa.Action || '');
191
+ const actionMethod = (0, actionUtils_1.extractActionMethodName)(dfa.Action || '');
261
192
  return actionMethod === actionName || dfa.Label === actionName;
262
193
  });
263
194
  if (item) {
264
- actionStates.push(buildActionButtonState(item, metadata, entityTypeName));
195
+ actionStates.push((0, actionUtils_1.buildActionButtonState)(item, metadata));
265
196
  }
266
197
  }
267
198
  return actionStates;
@@ -271,117 +202,10 @@ function findActionStates(dataFieldForActions, actionNames, metadata, entityType
271
202
  *
272
203
  * @param dataFieldForActions List of DataFieldForAction items from UI.LineItem
273
204
  * @param metadata The converted metadata
274
- * @param entityTypeName The entity type name
275
205
  * @returns List of all action button states
276
206
  */
277
- function extractAllActionStates(dataFieldForActions, metadata, entityTypeName) {
278
- return dataFieldForActions.map((item) => buildActionButtonState(item, metadata, entityTypeName));
279
- }
280
- /**
281
- * Builds an ActionButtonState object from a DataFieldForAction item.
282
- *
283
- * @param item The DataFieldForAction item
284
- * @param metadata The converted metadata
285
- * @param entityTypeName The entity type name
286
- * @returns ActionButtonState for the action
287
- */
288
- function buildActionButtonState(item, metadata, entityTypeName) {
289
- const actionMethod = extractActionMethodName(item.Action || '');
290
- const operationAvailable = findOperationAvailableAnnotation(metadata, entityTypeName, actionMethod);
291
- // Bound actions whose binding parameter is a single entity (not a collection) require
292
- // row selection to be invoked, so they are disabled by default (no row selected).
293
- // Collection-bound actions operate on the entity set and are always enabled.
294
- const isEntityBound = item.ActionTarget?.isBound === true && item.ActionTarget?.parameters?.[0]?.isCollection !== true;
295
- const { enabled, dynamicPath } = analyzeOperationAvailability(operationAvailable, isEntityBound);
296
- return {
297
- label: item.Label || '',
298
- action: item.Action || '',
299
- visible: true,
300
- enabled,
301
- dynamicPath,
302
- invocationGrouping: item.InvocationGrouping ? extractEnumMemberValue(item.InvocationGrouping) : undefined
303
- };
304
- }
305
- /**
306
- * Analyzes Core.OperationAvailable annotation to determine action availability.
307
- * Single-entity bound actions (requiring row selection) are disabled by default when no annotation is present.
308
- *
309
- * @param operationAvailable The OperationAvailable annotation value
310
- * @param isEntityBound Whether the action is bound to a single entity (requires row selection to enable)
311
- * @returns Object containing enabled state and optional dynamic path
312
- */
313
- function analyzeOperationAvailability(operationAvailable, isEntityBound) {
314
- if (operationAvailable === undefined) {
315
- return { enabled: !isEntityBound };
316
- }
317
- if (typeof operationAvailable === 'boolean') {
318
- return { enabled: operationAvailable };
319
- }
320
- if (typeof operationAvailable === 'object' && operationAvailable !== null) {
321
- const path = operationAvailable.$Path ?? operationAvailable.path;
322
- if (path) {
323
- return { enabled: 'dynamic', dynamicPath: path };
324
- }
325
- }
326
- return { enabled: true };
327
- }
328
- /**
329
- * Extracts the action method name from a fully qualified action string.
330
- *
331
- * @param actionName The fully qualified action name
332
- * @returns The action method name
333
- */
334
- function extractActionMethodName(actionName) {
335
- const match = /\.([^.()]+)\(/.exec(actionName);
336
- if (match?.[1]) {
337
- return match[1];
338
- }
339
- const lastDotIndex = actionName.lastIndexOf('.');
340
- const parenIndex = actionName.indexOf('(');
341
- if (lastDotIndex !== -1 && parenIndex !== -1) {
342
- return actionName.substring(lastDotIndex + 1, parenIndex);
343
- }
344
- return actionName;
345
- }
346
- /**
347
- * Finds the Core.OperationAvailable annotation for a specific action.
348
- *
349
- * @param metadata The converted metadata
350
- * @param entityTypeName The entity type name
351
- * @param actionMethodName The action method name
352
- * @returns The OperationAvailable annotation value or undefined if not found
353
- */
354
- function findOperationAvailableAnnotation(metadata, entityTypeName, actionMethodName) {
355
- if (metadata.actions) {
356
- const action = metadata.actions.find((a) => a.name === actionMethodName || a.fullyQualifiedName?.includes(`.${actionMethodName}(`));
357
- if (action?.annotations?.Core?.OperationAvailable !== undefined) {
358
- return action.annotations.Core.OperationAvailable;
359
- }
360
- }
361
- if (metadata.entityContainer?.annotations) {
362
- const annotations = metadata.entityContainer.annotations;
363
- const matchingKey = Object.keys(annotations).find((key) => key.includes(actionMethodName));
364
- if (matchingKey && annotations[matchingKey]?.Core?.OperationAvailable !== undefined) {
365
- return annotations[matchingKey].Core.OperationAvailable;
366
- }
367
- }
368
- return undefined;
369
- }
370
- /**
371
- * Extracts the enum member value from an annotation.
372
- *
373
- * @param enumValue The enum value object
374
- * @returns The extracted enum value string
375
- */
376
- function extractEnumMemberValue(enumValue) {
377
- if (typeof enumValue === 'string') {
378
- return enumValue;
379
- }
380
- if (enumValue?.$EnumMember) {
381
- const parts = enumValue.$EnumMember.split('/');
382
- return parts[1] ?? enumValue.$EnumMember;
383
- }
384
- return undefined;
207
+ function extractAllActionStates(dataFieldForActions, metadata) {
208
+ return dataFieldForActions.map((item) => (0, actionUtils_1.buildActionButtonState)(item, metadata));
385
209
  }
386
210
  /**
387
211
  * Retrieves toolbar action names from the page model using ux-specification.
@@ -62,7 +62,7 @@ async function getAppFeatures(basePath, fs, log, metadata, manifest) {
62
62
  }
63
63
  if (objectPages) {
64
64
  log?.warn('Extracting Object Page features from application model');
65
- featureData.objectPages = await (0, objectPageUtils_1.getObjectPageFeatures)(objectPages, listReportPage?.name, log);
65
+ featureData.objectPages = await (0, objectPageUtils_1.getObjectPageFeatures)(objectPages, listReportPage?.name, log, projectMetadata);
66
66
  log?.warn('objectPages features extracted: ' + JSON.stringify(featureData.objectPages));
67
67
  }
68
68
  if (fpmPage) {
@@ -8,9 +8,10 @@ import type { PageWithModelV4 } from '@sap/ux-specification/dist/types/src/parse
8
8
  * @param objectPages - the array of object pages extracted from the application model
9
9
  * @param listReportPageKey - the key of the List Report page in the application model, used to find navigation routes to object pages
10
10
  * @param log - optional logger instance
11
+ * @param metadata - optional metadata for the OPA test generation
11
12
  * @returns a record of object page feature data
12
13
  */
13
- export declare function getObjectPageFeatures(objectPages: PageWithModelV4[], listReportPageKey?: string, log?: Logger): Promise<ObjectPageFeatures[]>;
14
+ export declare function getObjectPageFeatures(objectPages: PageWithModelV4[], listReportPageKey?: string, log?: Logger, metadata?: string): Promise<ObjectPageFeatures[]>;
14
15
  /**
15
16
  * Retrieves all Object Page definitions from the given application model, as long as the page is reachable via standard navigation routes.
16
17
  *
@@ -5,29 +5,43 @@ exports.getObjectPages = getObjectPages;
5
5
  const modelUtils_1 = require("./modelUtils");
6
6
  const tableUtils_1 = require("./tableUtils");
7
7
  const page_1 = require("@sap/ux-specification/dist/types/src/common/page");
8
+ const edmx_parser_1 = require("@sap-ux/edmx-parser");
9
+ const annotation_converter_1 = require("@sap-ux/annotation-converter");
10
+ const actionUtils_1 = require("./actionUtils");
8
11
  /**
9
12
  * Extracts feature data for object pages from the application model.
10
13
  *
11
14
  * @param objectPages - the array of object pages extracted from the application model
12
15
  * @param listReportPageKey - the key of the List Report page in the application model, used to find navigation routes to object pages
13
16
  * @param log - optional logger instance
17
+ * @param metadata - optional metadata for the OPA test generation
14
18
  * @returns a record of object page feature data
15
19
  */
16
- async function getObjectPageFeatures(objectPages, listReportPageKey, log) {
20
+ async function getObjectPageFeatures(objectPages, listReportPageKey, log, metadata) {
17
21
  const objectPageFeatures = [];
18
22
  if (!objectPages || objectPages.length === 0) {
19
23
  log?.warn('Object Pages not found in application model. Dynamic tests will not be generated for Object Pages.');
20
24
  return objectPageFeatures;
21
25
  }
22
26
  // attempt to get individual feature data for each object page
27
+ const convertedMetadata = metadata ? (0, annotation_converter_1.convert)((0, edmx_parser_1.parse)(metadata)) : undefined;
28
+ const schemaNamespace = convertedMetadata?.namespace ?? '';
23
29
  for (const objectPage of objectPages) {
24
30
  const pageFeatureData = {};
25
31
  pageFeatureData.name = objectPage.name;
26
32
  pageFeatureData.navigationParents = getObjectPageNavigationParents(objectPage.name, objectPages, listReportPageKey);
27
33
  // extract header sections (facets)
28
34
  pageFeatureData.headerSections = extractObjectPageHeaderSectionsData(objectPage);
29
- // extract body sections
30
- pageFeatureData.bodySections = extractObjectPageBodySectionsData(objectPage);
35
+ // extract body sections (includes section-level actions and standard create/delete buttons)
36
+ pageFeatureData.bodySections = extractObjectPageBodySectionsData(objectPage, convertedMetadata, schemaNamespace, metadata, log);
37
+ // extract header-level actions
38
+ pageFeatureData.headerActions = convertedMetadata
39
+ ? extractHeaderActions(objectPage, convertedMetadata, schemaNamespace)
40
+ : [];
41
+ // determine edit button visibility from UpdateRestrictions on the OP entity set
42
+ if (metadata && objectPage.entitySet) {
43
+ pageFeatureData.editButton = (0, actionUtils_1.safeCheckEditVisibility)(metadata, objectPage.entitySet, log);
44
+ }
31
45
  objectPageFeatures.push(pageFeatureData);
32
46
  }
33
47
  return objectPageFeatures;
@@ -110,9 +124,13 @@ function extractObjectPageHeaderSectionsData(objectPage) {
110
124
  * Extracts body sections data from an object page model.
111
125
  *
112
126
  * @param objectPage - object page from the application model
127
+ * @param convertedMetadata - optional converted OData metadata for action extraction
128
+ * @param schemaNamespace - optional OData schema namespace used as service identifier in action assertions
129
+ * @param metadata - optional raw metadata XML for resolving standard button visibility (Create/Delete)
130
+ * @param log - optional logger instance
113
131
  * @returns body sections data including sub-sections
114
132
  */
115
- function extractObjectPageBodySectionsData(objectPage) {
133
+ function extractObjectPageBodySectionsData(objectPage, convertedMetadata, schemaNamespace, metadata, log) {
116
134
  const bodySections = [];
117
135
  if (objectPage.model) {
118
136
  const sectionsAggregation = (0, modelUtils_1.getAggregations)(objectPage.model.root)['sections'];
@@ -120,20 +138,81 @@ function extractObjectPageBodySectionsData(objectPage) {
120
138
  Object.entries(sections).forEach(([sectionKey, section]) => {
121
139
  const sectionId = getSectionIdentifier(section) ?? sectionKey;
122
140
  const subSections = extractBodySubSectionsData(section, sectionId);
123
- bodySections.push({
141
+ const navigationProperty = getNavigationPropertyFromKey(sectionKey);
142
+ const sectionData = {
124
143
  id: sectionId,
125
- navigationProperty: getNavigationPropertyFromKey(sectionKey),
144
+ navigationProperty,
126
145
  isTable: !!section.isTable,
127
146
  custom: !!section.custom,
128
- order: section?.order ?? -1, // put a negative order number to signal that order was not in spec
147
+ order: section?.order ?? -1,
129
148
  fields: section.custom || section.isTable ? [] : extractFormFields(section),
130
149
  tableColumns: section.custom || !section.isTable ? {} : (0, tableUtils_1.extractTableColumnsFromNode)(section),
131
- subSections
132
- });
150
+ subSections,
151
+ actions: !section.custom && convertedMetadata && schemaNamespace
152
+ ? extractSectionActions(section, convertedMetadata, schemaNamespace)
153
+ : []
154
+ };
155
+ // For table sections, resolve Create/Delete visibility from target entity set
156
+ if (section.isTable && navigationProperty && metadata && convertedMetadata) {
157
+ const targetEntitySet = resolveNavigationTargetEntitySet(convertedMetadata, objectPage.entitySet, navigationProperty);
158
+ if (targetEntitySet) {
159
+ const buttonVisibility = (0, actionUtils_1.safeCheckButtonVisibility)(metadata, targetEntitySet, log);
160
+ sectionData.createButton = buttonVisibility?.create;
161
+ sectionData.deleteButton = buttonVisibility?.delete;
162
+ }
163
+ }
164
+ bodySections.push(sectionData);
133
165
  });
134
166
  }
135
167
  return bodySections;
136
168
  }
169
+ /**
170
+ * Extracts header-level action button states from an object page model.
171
+ *
172
+ * @param objectPage - object page from the application model
173
+ * @param convertedMetadata - converted OData metadata for resolving action availability
174
+ * @param schemaNamespace - OData schema namespace used as service identifier in action assertions
175
+ * @returns array of action button states for the header toolbar
176
+ */
177
+ function extractHeaderActions(objectPage, convertedMetadata, schemaNamespace) {
178
+ if (!objectPage.model) {
179
+ return [];
180
+ }
181
+ const headerAgg = (0, modelUtils_1.getAggregations)(objectPage.model.root)['header'];
182
+ const actionsAgg = (0, modelUtils_1.getAggregations)(headerAgg)['actions'];
183
+ const actionEntries = (0, modelUtils_1.getAggregations)(actionsAgg);
184
+ return Object.entries(actionEntries)
185
+ .map(([key, item]) => (0, actionUtils_1.buildActionStateFromSpecModelKey)(key, item.description, convertedMetadata, schemaNamespace))
186
+ .filter((actionState) => actionState !== undefined);
187
+ }
188
+ /**
189
+ * Extracts section-level action button states from a body section.
190
+ * For table sections, actions are extracted from the table toolbar; for form sections from the form actions aggregation.
191
+ *
192
+ * @param section - body section entry from the application model
193
+ * @param convertedMetadata - converted OData metadata for resolving action availability
194
+ * @param schemaNamespace - OData schema namespace used as service identifier in action assertions
195
+ * @returns array of action button states for the section toolbar
196
+ */
197
+ function extractSectionActions(section, convertedMetadata, schemaNamespace) {
198
+ let actionsAgg;
199
+ if (section.isTable) {
200
+ const tableAgg = (0, modelUtils_1.getAggregations)(section)['table'];
201
+ const toolBarAgg = (0, modelUtils_1.getAggregations)(tableAgg)['toolBar'];
202
+ actionsAgg = (0, modelUtils_1.getAggregations)(toolBarAgg)['actions'];
203
+ }
204
+ else {
205
+ const formAgg = (0, modelUtils_1.getAggregations)(section)['form'];
206
+ actionsAgg = (0, modelUtils_1.getAggregations)(formAgg)['actions'];
207
+ }
208
+ if (!actionsAgg) {
209
+ return [];
210
+ }
211
+ const actionEntries = (0, modelUtils_1.getAggregations)(actionsAgg);
212
+ return Object.entries(actionEntries)
213
+ .map(([key, item]) => (0, actionUtils_1.buildActionStateFromSpecModelKey)(key, item.description, convertedMetadata, schemaNamespace))
214
+ .filter((actionState) => actionState !== undefined);
215
+ }
137
216
  /**
138
217
  * Extracts sub-sections data from a body section.
139
218
  *
@@ -193,6 +272,27 @@ function getNavigationPropertyFromKey(sectionKey) {
193
272
  const prefix = sectionKey.split('::')[0];
194
273
  return prefix.startsWith('_') ? prefix : undefined;
195
274
  }
275
+ /**
276
+ * Resolves the target entity set name for a navigation property by looking up navigation
277
+ * property bindings in the source entity set's metadata.
278
+ *
279
+ * @param convertedMetadata - converted OData metadata
280
+ * @param sourceEntitySetName - the name of the source entity set (the Object Page's entity set)
281
+ * @param navigationProperty - the navigation property name (e.g. '_Booking')
282
+ * @returns the target entity set name, or undefined if resolution fails
283
+ */
284
+ function resolveNavigationTargetEntitySet(convertedMetadata, sourceEntitySetName, navigationProperty) {
285
+ if (!sourceEntitySetName) {
286
+ return undefined;
287
+ }
288
+ const sourceEntitySet = convertedMetadata.entitySets.find((es) => es.name === sourceEntitySetName);
289
+ if (!sourceEntitySet?.navigationPropertyBinding) {
290
+ return undefined;
291
+ }
292
+ const navPropName = navigationProperty.startsWith('_') ? navigationProperty.substring(1) : navigationProperty;
293
+ const binding = sourceEntitySet.navigationPropertyBinding[navPropName];
294
+ return binding?.name;
295
+ }
196
296
  /**
197
297
  * Gets the identifier of a section for OPA5 tests.
198
298
  *
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": "0.9.3",
4
+ "version": "0.9.4",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -39,6 +39,26 @@ sap.ui.define([
39
39
  Then.onThe<%- name%>.iSeeThisPage();
40
40
  });
41
41
 
42
+ <% if (headerActions?.length > 0 && !isStandalone) { -%>
43
+ opaTest("Check header actions of the Object Page", function (Given, When, Then) {
44
+ <% if (editButton?.visible) { -%>
45
+ // Ensure the opened entity is not in Draft state before uncommenting
46
+ // Then.onThe<%- name%>.onHeader().iCheckEdit({ visible: true });
47
+ // When.onThe<%- name%>.onHeader().iPressEdit();
48
+ <% } -%>
49
+ <% headerActions.forEach(function(action) { -%>
50
+ <% if (action.visible) { -%>
51
+ <% if (action.enabled === 'dynamic') { -%>
52
+ Then.onThe<%- name%>.onHeader().iCheckAction("<%- action.label %>" /* , { enabled: true } */);
53
+ <% } else { -%>
54
+ Then.onThe<%- name%>.onHeader().iCheckAction("<%- action.label %>", { enabled: <%- action.enabled === true %> });
55
+ <% } -%>
56
+ // When.onThe<%- name%>.onHeader().iPressAction("<%- action.label %>");
57
+ <% } -%>
58
+ <% }); -%>
59
+ });
60
+ <% } -%>
61
+
42
62
  <% if (headerSections?.length > 0) { -%>
43
63
  opaTest("Check header facets of the Object Page", function (Given, When, Then) {
44
64
  <% headerSections.forEach(function(section) { -%>
@@ -70,6 +90,37 @@ sap.ui.define([
70
90
  When.onThe<%- name%>.iPressSectionIconTabFilterButton("<%- section.id %>");
71
91
  <% } -%>
72
92
  Then.onThe<%- name%>.iCheckSection({ section: "<%- section.id %>" });
93
+ <% if (section.actions && section.actions.length > 0) { -%>
94
+ <% section.actions.forEach(function(action) { -%>
95
+ <% if (action.visible) { -%>
96
+ <% if (section.isTable && section.navigationProperty) { -%>
97
+ <% if (action.enabled === 'dynamic') { -%>
98
+ Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckAction("<%- action.label %>" /* , { enabled: true } */);
99
+ <% } else { -%>
100
+ Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckAction("<%- action.label %>", { enabled: <%- action.enabled === true %> });
101
+ <% } -%>
102
+ // When.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iPressAction("<%- action.label %>");
103
+ <% } else { -%>
104
+ <% if (action.enabled === 'dynamic') { -%>
105
+ Then.onThe<%- name%>.onForm({ section: "<%- section.id %>" }).iCheckAction("<%- action.label %>" /* , { enabled: true } */);
106
+ <% } else { -%>
107
+ Then.onThe<%- name%>.onForm({ section: "<%- section.id %>" }).iCheckAction("<%- action.label %>", { enabled: <%- action.enabled === true %> });
108
+ <% } -%>
109
+ // When.onThe<%- name%>.onForm({ section: "<%- section.id %>" }).iPressAction("<%- action.label %>");
110
+ <% } -%>
111
+ <% } -%>
112
+ <% }); -%>
113
+ <% } -%>
114
+ <% if (section.isTable && section.navigationProperty) { -%>
115
+ <% if (section.createButton?.visible) { -%>
116
+ Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckCreate({ visible: true });
117
+ // When.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iPressCreate();
118
+ <% } -%>
119
+ <% if (section.deleteButton?.visible) { -%>
120
+ Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckDelete({ visible: true });
121
+ // When.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iPressDelete();
122
+ <% } -%>
123
+ <% } -%>
73
124
  <% if (section?.subSections?.length > 0) { -%>
74
125
  <% section.subSections.forEach(function(subSection) { -%>
75
126
  //When.onThe<%- name%>.iGoToSection({ section: "<%- section.id %>", subSection: "<%- subSection.id %>" });