@sap/generator-fiori
Version:
Create an SAPUI5 application using SAP Fiori elements or a freestyle approach
1,143 lines (1,122 loc) • 119 kB
JavaScript
"use strict";
exports.id = 2944;
exports.ids = [2944];
exports.modules = {
/***/ 6435
(__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) {
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
Vo: () => (/* reexport */ addVirtualTestConfig),
kJ: () => (/* reexport */ generateFreestyleOPAFiles),
fY: () => (/* reexport */ generateOPAFiles)
});
// EXTERNAL MODULE: external "node:path"
var external_node_path_ = __webpack_require__(76760);
// EXTERNAL MODULE: external "node:url"
var external_node_url_ = __webpack_require__(73136);
// EXTERNAL MODULE: external "node:fs"
var external_node_fs_ = __webpack_require__(73024);
// EXTERNAL MODULE: ../../../node_modules/mem-fs/index.js
var mem_fs = __webpack_require__(64812);
// EXTERNAL MODULE: ../../../node_modules/mem-fs-editor/lib/index.js
var lib = __webpack_require__(90718);
;// ../../../node_modules/@sap-ux/ui5-test-writer/dist/types.js
const DotFileExtension = {
JS: '.js',
TS: '.ts'
};
const SupportedPageTypes = {
'sap.fe.templates.ListReport': 'ListReport',
'sap.fe.templates.ObjectPage': 'ObjectPage',
'sap.fe.core.fpm': 'FPM'
};
/**
* General validation error thrown if app config options contain invalid combinations
*/
class ValidationError extends Error {
/**
* ValidationError constructor.
*
* @param message - the error message
*/
constructor(message) {
super(`Validation error: ${message}`);
this.name = this.constructor.name;
}
}
//# sourceMappingURL=types.js.map
// EXTERNAL MODULE: ../../../node_modules/i18next/dist/cjs/i18next.js
var i18next = __webpack_require__(68801);
;// ../../../node_modules/@sap-ux/ui5-test-writer/dist/translations/ui5-test-writer.i18n.json
const ui5_test_writer_i18n_namespaceObject = /*#__PURE__*/JSON.parse('{"error":{"cannotReadManifest":"Cannot read the `manifest.json` file: {{ filePath }}.","cannotReadAppID":"Cannot read `appID` in the `manifest.json` file.","badApplicationType":"Cannot determine the application type from the `manifest.json` file or it uses an unsupported type.","cannotGeneratePageFile":"Cannot generate the page file for target: {{ targetKey }}.","errorCopyingFreestyleTestTemplates":"An error occurred when copying freestyle test templates: {{ error }}","errorWritingTsConfig":"An error occurred when writing the `tsconfig.json` file: {{ error }}"}}');
;// ../../../node_modules/@sap-ux/ui5-test-writer/dist/i18n.js
const NS = 'ui5-test-writer';
const i18n = i18next.createInstance();
/**
* Initialize i18next with the translations for this module.
*/
async function initI18n() {
await i18n.init({
resources: {
en: {
[NS]: ui5_test_writer_i18n_namespaceObject
}
},
lng: 'en',
fallbackLng: 'en',
defaultNS: NS,
ns: [NS],
showSupportNotice: false
});
}
/**
* Helper function facading the call to i18next.
*
* @param key i18n key
* @param options additional options
* @returns {string} localized string stored for the given key
*/
function t(key, options) {
return i18n.t(key, options);
}
void initI18n().catch(() => undefined);
//# sourceMappingURL=i18n.js.map
// EXTERNAL MODULE: ../../../node_modules/@sap-ux/project-access/dist/index.js + 26 modules
var dist = __webpack_require__(7083);
// EXTERNAL MODULE: ../../../node_modules/@sap-ux/ui5-test-writer/node_modules/@sap/ux-specification/dist/types/src/common/index.js
var common = __webpack_require__(97242);
;// ../../../node_modules/@sap-ux/ui5-test-writer/dist/utils/tableUtils.js
/**
* Gets the identifier of a column for OPA5 tests.
* Custom columns use the 'Key' entry; standard columns use the 'Value' entry from the schema keys.
*
* @param column - column item from ux specification
* @returns identifier of the column for OPA5 tests; undefined if no matching key entry is found
*/
function getColumnIdentifier(column) {
const key = column.custom ? 'Key' : 'Value';
return column.schema.keys.find((k) => k.name === key)?.value;
}
/**
* Transforms column aggregations from the ux specification model into a map of columns for OPA5 tests.
* Each column entry includes the column header label for display verification.
*
* @param columnAggregations - column aggregations from the ux specification model
* @returns a map of column identifiers to column state objects for use with iCheckColumns()
*/
function transformTableColumns(columnAggregations) {
const columns = {};
Object.values(columnAggregations).forEach((column, index) => {
const id = getColumnIdentifier(column) ?? String(index);
const state = {};
if (column.description) {
state['header'] = column.description;
}
columns[id] = state;
});
return columns;
}
/**
* Extracts table column data from a spec model node that contains a 'table' aggregation.
* Covers both page-level nodes (List Report, FPM) via their root and section-level nodes
* (Object Page body sections) — both are TreeAggregation nodes that expose a 'table' aggregation.
*
* @param node - tree aggregation node that exposes a 'table' aggregation
* @returns a map of column identifiers to column state objects for use with iCheckColumns()
*/
function extractTableColumnsFromNode(node) {
const tableAggregation = getAggregations(node)['table'];
if (!tableAggregation) {
return {};
}
const columnsAggregation = getAggregations(tableAggregation)['columns'];
if (!columnsAggregation) {
return {};
}
const columnItems = getAggregations(columnsAggregation);
return transformTableColumns(columnItems);
}
//# sourceMappingURL=tableUtils.js.map
// EXTERNAL MODULE: ../../../node_modules/@sap-ux/ui5-test-writer/node_modules/@sap/ux-specification/dist/types/src/common/page.js
var common_page = __webpack_require__(30911);
// EXTERNAL MODULE: ../../../node_modules/@sap-ux/edmx-parser/dist/index.js
var edmx_parser_dist = __webpack_require__(39933);
// EXTERNAL MODULE: ../../../node_modules/@sap-ux/annotation-converter/dist/index.js
var annotation_converter_dist = __webpack_require__(91125);
;// ../../../node_modules/@sap-ux/ui5-test-writer/dist/utils/actionUtils.js
const DATA_FIELD_FOR_ACTION = 'DataFieldForAction';
/**
* Extracts the action method name from a fully qualified action string.
*
* @param actionName The fully qualified action name
* @returns The action method name
*/
function extractActionMethodName(actionName) {
const match = /\.([^.()]+)\(/.exec(actionName);
if (match?.[1]) {
return match[1];
}
const lastDotIndex = actionName.lastIndexOf('.');
const parenIndex = actionName.indexOf('(');
if (lastDotIndex >= 0 && parenIndex >= 0 && parenIndex > lastDotIndex) {
return actionName.substring(lastDotIndex + 1, parenIndex);
}
// Handle namespace-qualified name without parentheses (spec model key format: "namespace.Method")
if (lastDotIndex >= 0) {
return actionName.substring(lastDotIndex + 1);
}
return actionName;
}
/**
* Finds the Core.OperationAvailable annotation for a specific action.
*
* @param metadata The converted metadata
* @param actionMethodName The action method name
* @returns The OperationAvailable annotation value or undefined if not found
*/
function findOperationAvailableAnnotation(metadata, actionMethodName) {
if (metadata.actions) {
const foundAction = metadata.actions.find((action) => action.name === actionMethodName || action.fullyQualifiedName?.includes(`.${actionMethodName}(`));
if (foundAction?.annotations?.Core?.OperationAvailable !== undefined) {
return foundAction.annotations.Core.OperationAvailable;
}
}
if (metadata.entityContainer?.annotations) {
const annotations = metadata.entityContainer.annotations;
const matchingKey = Object.keys(annotations).find((key) => key === actionMethodName || key.endsWith(`.${actionMethodName}`));
if (matchingKey && annotations[matchingKey]?.Core?.OperationAvailable !== undefined) {
return annotations[matchingKey].Core.OperationAvailable;
}
}
return undefined;
}
/**
* Analyzes Core.OperationAvailable annotation to determine action availability.
* Single-entity bound actions (requiring row selection) are disabled by default when no annotation is present.
*
* @param operationAvailable The OperationAvailable annotation value
* @param isEntityBound Whether the action is bound to a single entity (requires row selection to enable)
* @returns Object containing enabled state and optional dynamic path
*/
function analyzeOperationAvailability(operationAvailable, isEntityBound) {
if (operationAvailable === undefined) {
return { enabled: !isEntityBound };
}
if (typeof operationAvailable === 'boolean') {
return { enabled: operationAvailable };
}
if (typeof operationAvailable === 'object' && operationAvailable !== null) {
const pathRecord = operationAvailable;
const path = pathRecord.$Path ?? pathRecord.path;
if (path) {
return { enabled: 'dynamic', dynamicPath: path };
}
}
return { enabled: true };
}
/**
* Extracts the enum member value from an annotation.
*
* @param enumValue The enum value object
* @returns The extracted enum value string
*/
function extractEnumMemberValue(enumValue) {
if (typeof enumValue === 'string') {
return enumValue;
}
const enumRecord = enumValue;
if (enumRecord?.$EnumMember) {
const parts = enumRecord.$EnumMember.split('/');
return parts[1] ?? enumRecord.$EnumMember;
}
return undefined;
}
/**
* Builds an ActionButtonState object from a DataFieldForAction annotation item.
*
* @param item The DataFieldForAction annotation item
* @param metadata The converted metadata
* @returns ActionButtonState for the action
*/
function buildActionButtonState(item, metadata) {
const actionString = item.Action || '';
const actionMethod = extractActionMethodName(actionString);
const operationAvailable = findOperationAvailableAnnotation(metadata, actionMethod);
// Bound actions whose binding parameter is a single entity (not a collection) require
// row selection to be invoked, so they are disabled by default (no row selected).
// Collection-bound actions operate on the entity set and are always enabled.
const actionTarget = item.ActionTarget;
const isEntityBound = actionTarget?.isBound === true && actionTarget?.parameters?.[0]?.isCollection !== true;
const { enabled, dynamicPath } = analyzeOperationAvailability(operationAvailable, isEntityBound);
return {
label: item.Label || '',
action: actionString,
visible: true,
enabled,
dynamicPath,
invocationGrouping: item.InvocationGrouping ? extractEnumMemberValue(item.InvocationGrouping) : undefined
};
}
/**
* Builds an ActionButtonState from a spec model aggregation key.
*
* Key format: "DataFieldForAction::<namespace>.<Method>::<namespace>.<EntityType>"
* Example: "DataFieldForAction::com.example.Copy::com.example.POEntity".
*
* @param aggregationKey The spec model aggregation key for the action
* @param label Display label from the spec model item description
* @param convertedMetadata The converted OData metadata
* @param schemaNamespace The OData schema namespace (used as service identifier)
* @returns ActionButtonState or undefined if the key is not a DataFieldForAction key
*/
function buildActionStateFromSpecModelKey(aggregationKey, label, convertedMetadata, schemaNamespace) {
const keyParts = aggregationKey.split('::');
if (keyParts[0] !== DATA_FIELD_FOR_ACTION || !keyParts[1]) {
return undefined;
}
const actionFullName = keyParts[1]; // "namespace.Method"
const actionMethod = extractActionMethodName(actionFullName);
const actionDefinition = convertedMetadata.actions?.find((action) => action.name === actionMethod || action.fullyQualifiedName?.includes(`.${actionMethod}(`));
const firstParameter = actionDefinition?.parameters?.[0];
const isEntityBound = actionDefinition?.isBound === true && firstParameter?.isCollection !== true;
const operationAvailable = findOperationAvailableAnnotation(convertedMetadata, actionMethod);
const { enabled, dynamicPath } = analyzeOperationAvailability(operationAvailable, isEntityBound);
return {
label: label ?? '',
action: actionMethod,
service: schemaNamespace,
unbound: !isEntityBound,
visible: true,
enabled,
dynamicPath
};
}
/**
* Analyzes a restriction value (Insertable, Deletable, or Updatable) to determine button state.
*
* @param value The annotation value — boolean, path object, or undefined
* @returns ButtonState indicating visibility and enabled state
*/
function analyzeRestrictionValue(value) {
const defaultState = { visible: true, enabled: true };
if (value === undefined || value === null) {
return defaultState;
}
if (typeof value === 'boolean') {
return { visible: value, enabled: value };
}
if (typeof value === 'object') {
const path = value.$Path ?? value.path;
if (path) {
return { visible: true, enabled: 'dynamic', dynamicPath: path };
}
}
return defaultState;
}
/**
* Analyzes InsertRestrictions annotation to determine create button visibility and enabled state.
*
* @param restriction The InsertRestrictions annotation for the entity set
* @returns ButtonState indicating visibility and enabled state based on the Insertable value
*/
function analyzeInsertRestrictions(restriction) {
const value = restriction ? restriction['Insertable'] : undefined;
return analyzeRestrictionValue(value);
}
/**
* Analyzes DeleteRestrictions annotation to determine delete button visibility and enabled state.
*
* @param restriction The DeleteRestrictions annotation for the entity set
* @returns ButtonState indicating visibility and enabled state based on the Deletable value
*/
function analyzeDeleteRestrictions(restriction) {
const value = restriction ? restriction['Deletable'] : undefined;
return analyzeRestrictionValue(value);
}
/**
* Analyzes UpdateRestrictions annotation to determine edit button visibility and enabled state.
*
* @param restriction The UpdateRestrictions annotation for the entity set
* @returns ButtonState indicating visibility and enabled state based on the Updatable value
*/
function analyzeUpdateRestrictions(restriction) {
const value = restriction ? restriction['Updatable'] : undefined;
return analyzeRestrictionValue(value);
}
/**
* Checks the visibility and enabled state of create and delete buttons for a given entity set
* by analyzing OData Capabilities annotations in the converted metadata.
*
* @param convertedMetadata The already-converted OData metadata
* @param entitySetName The name of the entity set to check
* @returns ButtonVisibilityResult containing the state of create and delete buttons
* @throws {Error} If entity set is not found
*/
function checkButtonVisibilityFromMetadata(convertedMetadata, entitySetName) {
const entitySet = convertedMetadata.entitySets.find((es) => es.name === entitySetName);
if (!entitySet) {
throw new Error(`Entity set '${entitySetName}' not found in metadata`);
}
const insertRestrictions = entitySet.annotations?.Capabilities?.InsertRestrictions;
const deleteRestrictions = entitySet.annotations?.Capabilities?.DeleteRestrictions;
return {
create: analyzeInsertRestrictions(insertRestrictions),
delete: analyzeDeleteRestrictions(deleteRestrictions)
};
}
/**
* Checks the visibility and enabled state of create and delete buttons for a given entity set
* by analyzing OData Capabilities annotations in the metadata.
*
* @param metadataXml The OData metadata XML content as a string
* @param entitySetName The name of the entity set to check
* @returns ButtonVisibilityResult containing the state of create and delete buttons
* @throws {Error} If metadata cannot be parsed or entity set is not found
*/
function checkButtonVisibility(metadataXml, entitySetName) {
try {
return checkButtonVisibilityFromMetadata((0,annotation_converter_dist.convert)((0,edmx_parser_dist/* parse */.qg)(metadataXml)), entitySetName);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to analyze button visibility: ${errorMessage}`);
}
}
/**
* Checks the visibility and enabled state of the edit button for a given entity set
* by analyzing UpdateRestrictions in the metadata.
*
* @param metadataXml The OData metadata XML content as a string
* @param entitySetName The name of the entity set to check
* @returns ButtonState for the edit button
* @throws {Error} If metadata cannot be parsed or entity set is not found
*/
function checkEditVisibility(metadataXml, entitySetName) {
try {
const convertedMetadata = (0,annotation_converter_dist.convert)((0,edmx_parser_dist/* parse */.qg)(metadataXml));
const entitySet = convertedMetadata.entitySets.find((es) => es.name === entitySetName);
if (!entitySet) {
throw new Error(`Entity set '${entitySetName}' not found in metadata`);
}
const updateRestrictions = entitySet.annotations?.Capabilities?.UpdateRestrictions;
return analyzeUpdateRestrictions(updateRestrictions);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to analyze edit visibility: ${errorMessage}`);
}
}
/**
* Safely checks button visibility from already-converted metadata, with error handling.
*
* @param convertedMetadata The already-converted OData metadata
* @param entitySetName The name of the entity set
* @param log Optional logger instance
* @returns Button visibility result or undefined if error occurs
*/
function safeCheckButtonVisibilityFromMetadata(convertedMetadata, entitySetName, log) {
try {
return checkButtonVisibilityFromMetadata(convertedMetadata, entitySetName);
}
catch (error) {
log?.debug(`Failed to check button visibility: ${error instanceof Error ? error.message : String(error)}`);
return undefined;
}
}
/**
* Safely checks button visibility with error handling.
*
* @param metadata The OData metadata XML content
* @param entitySetName The name of the entity set
* @param log Optional logger instance
* @returns Button visibility result or undefined if error occurs
*/
function safeCheckButtonVisibility(metadata, entitySetName, log) {
try {
return checkButtonVisibility(metadata, entitySetName);
}
catch (error) {
log?.debug(`Failed to check button visibility: ${error instanceof Error ? error.message : String(error)}`);
return undefined;
}
}
/**
* Safely checks edit button visibility with error handling.
*
* @param metadata The OData metadata XML content
* @param entitySetName The name of the entity set
* @param log Optional logger instance
* @returns ButtonState for the edit button, or undefined if error occurs
*/
function safeCheckEditVisibility(metadata, entitySetName, log) {
try {
return checkEditVisibility(metadata, entitySetName);
}
catch (error) {
log?.debug(`Failed to check edit visibility: ${error instanceof Error ? error.message : String(error)}`);
return undefined;
}
}
//# sourceMappingURL=actionUtils.js.map
;// ../../../node_modules/@sap-ux/ui5-test-writer/dist/utils/objectPageUtils.js
/**
* Extracts feature data for object pages from the application model.
*
* @param objectPages - the array of object pages extracted from the application model
* @param listReportPageKey - the key of the List Report page in the application model, used to find navigation routes to object pages
* @param log - optional logger instance
* @param metadata - optional metadata for the OPA test generation
* @returns a record of object page feature data
*/
async function getObjectPageFeatures(objectPages, listReportPageKey, log, metadata) {
const objectPageFeatures = [];
if (!objectPages || objectPages.length === 0) {
log?.warn('Object Pages not found in application model. Dynamic tests will not be generated for Object Pages.');
return objectPageFeatures;
}
// attempt to get individual feature data for each object page
const convertedMetadata = metadata ? (0,annotation_converter_dist.convert)((0,edmx_parser_dist/* parse */.qg)(metadata)) : undefined;
const schemaNamespace = convertedMetadata?.namespace ?? '';
for (const objectPage of objectPages) {
const pageFeatureData = {};
pageFeatureData.name = objectPage.name;
pageFeatureData.navigationParents = getObjectPageNavigationParents(objectPage.name, objectPages, listReportPageKey);
// extract header sections (facets)
pageFeatureData.headerSections = extractObjectPageHeaderSectionsData(objectPage);
// extract body sections (includes section-level actions and standard create/delete buttons)
pageFeatureData.bodySections = extractObjectPageBodySectionsData(objectPage, convertedMetadata, schemaNamespace, metadata, log);
// extract header-level actions
pageFeatureData.headerActions = convertedMetadata
? extractHeaderActions(objectPage, convertedMetadata, schemaNamespace)
: [];
// determine edit button visibility from UpdateRestrictions on the OP entity set
if (metadata && objectPage.entitySet) {
pageFeatureData.editButton = safeCheckEditVisibility(metadata, objectPage.entitySet, log);
}
objectPageFeatures.push(pageFeatureData);
}
return objectPageFeatures;
}
/**
* Retrieves all Object Page definitions from the given application model, as long as the page is reachable via standard navigation routes.
*
* @param applicationModel - The application model containing page definitions.
* @returns An array of Object Page definitions.
*/
function getObjectPages(applicationModel) {
const objectPages = [];
for (const pageKey in applicationModel.pages) {
const page = applicationModel.pages[pageKey];
if (page.pageType === common_page.PageTypeV4.ObjectPage) {
page.name = pageKey; // store page key as name for later identification
objectPages.push(page);
}
}
return objectPages;
}
/**
* Finds parent pages for the object page, and returns their identifiers.
*
* @param targetObjectPageKey - key of the target object page
* @param objectPages - the array of object pages extracted from the application model
* @param listReportPageKey - the key of the List Report page in the application model, used to find navigation routes to object pages
* @returns navigation data including parent page identifiers
*/
function getObjectPageNavigationParents(targetObjectPageKey, objectPages, listReportPageKey) {
const navigationParents = {
parentLRName: listReportPageKey ?? '' // app is possibly malformed if no LR found
};
objectPages.forEach((objectPage) => {
const navigationRoutes = getNavigationRoutes(objectPage);
const routeToTargetOP = navigationRoutes.find((nav) => nav.route === targetObjectPageKey);
if (routeToTargetOP) {
navigationParents.parentOPName = objectPage.name;
navigationParents.parentOPTableSection = routeToTargetOP.identifier;
}
});
return navigationParents;
}
/**
* Extracts header sections data from an object page model.
*
* @param objectPage - object page from the application model
* @returns header sections data
*/
function extractObjectPageHeaderSectionsData(objectPage) {
const headerSections = [];
if (objectPage.model) {
const headerAggregation = getAggregations(objectPage.model.root)['header'];
const sectionsAggregation = getAggregations(headerAggregation)['sections'];
const sections = getAggregations(sectionsAggregation);
Object.values(sections).forEach((section) => {
const facetId = getSectionIdentifier(section);
if (!facetId) {
// if no identifier can be found for the section, it is not possible to reliably identify it in tests, so skip it
return;
}
const sectionData = {
facetId: facetId,
stashed: getSectionStashedFlag(section),
custom: section.custom,
microChart: isSectionMicroChart(section),
form: isFormSection(section),
// collection: false // TODO: find out how to identify collection facets
title: section.title
};
if (sectionData.form) {
sectionData.fields = getHeaderSectionFormFields(section);
}
headerSections.push(sectionData);
});
}
return headerSections;
}
/**
* Extracts body sections data from an object page model.
*
* @param objectPage - object page from the application model
* @param convertedMetadata - optional converted OData metadata for action extraction
* @param schemaNamespace - optional OData schema namespace used as service identifier in action assertions
* @param metadata - optional raw metadata XML for resolving standard button visibility (Create/Delete)
* @param log - optional logger instance
* @returns body sections data including sub-sections
*/
function extractObjectPageBodySectionsData(objectPage, convertedMetadata, schemaNamespace, metadata, log) {
const bodySections = [];
if (objectPage.model) {
const sectionsAggregation = getAggregations(objectPage.model.root)['sections'];
const sections = getAggregations(sectionsAggregation);
Object.entries(sections).forEach(([sectionKey, section]) => {
const sectionId = getSectionIdentifier(section) ?? sectionKey;
const subSections = extractBodySubSectionsData(section, sectionId);
const navigationProperty = getNavigationPropertyFromKey(sectionKey);
const sectionData = {
id: sectionId,
navigationProperty,
isTable: !!section.isTable,
custom: !!section.custom,
order: section?.order ?? -1,
fields: section.custom || section.isTable ? [] : extractFormFields(section),
tableColumns: section.custom || !section.isTable ? {} : extractTableColumnsFromNode(section),
subSections,
actions: !section.custom && convertedMetadata && schemaNamespace
? extractSectionActions(section, convertedMetadata, schemaNamespace)
: []
};
// For table sections, resolve Create/Delete visibility from target entity set
if (section.isTable && navigationProperty && metadata && convertedMetadata) {
const targetEntitySet = resolveNavigationTargetEntitySet(convertedMetadata, objectPage.entitySet, navigationProperty);
if (targetEntitySet) {
const buttonVisibility = safeCheckButtonVisibility(metadata, targetEntitySet, log);
sectionData.createButton = buttonVisibility?.create;
sectionData.deleteButton = buttonVisibility?.delete;
}
}
bodySections.push(sectionData);
});
}
return bodySections;
}
/**
* Extracts header-level action button states from an object page model.
*
* @param objectPage - object page from the application model
* @param convertedMetadata - converted OData metadata for resolving action availability
* @param schemaNamespace - OData schema namespace used as service identifier in action assertions
* @returns array of action button states for the header toolbar
*/
function extractHeaderActions(objectPage, convertedMetadata, schemaNamespace) {
if (!objectPage.model) {
return [];
}
const headerAgg = getAggregations(objectPage.model.root)['header'];
const actionsAgg = getAggregations(headerAgg)['actions'];
const actionEntries = getAggregations(actionsAgg);
return Object.entries(actionEntries)
.map(([key, item]) => buildActionStateFromSpecModelKey(key, item.description, convertedMetadata, schemaNamespace))
.filter((actionState) => actionState !== undefined);
}
/**
* Extracts section-level action button states from a body section.
* For table sections, actions are extracted from the table toolbar; for form sections from the form actions aggregation.
*
* @param section - body section entry from the application model
* @param convertedMetadata - converted OData metadata for resolving action availability
* @param schemaNamespace - OData schema namespace used as service identifier in action assertions
* @returns array of action button states for the section toolbar
*/
function extractSectionActions(section, convertedMetadata, schemaNamespace) {
let actionsAgg;
if (section.isTable) {
const tableAgg = getAggregations(section)['table'];
const toolBarAgg = getAggregations(tableAgg)['toolBar'];
actionsAgg = getAggregations(toolBarAgg)['actions'];
}
else {
const formAgg = getAggregations(section)['form'];
actionsAgg = getAggregations(formAgg)['actions'];
}
if (!actionsAgg) {
return [];
}
const actionEntries = getAggregations(actionsAgg);
return Object.entries(actionEntries)
.map(([key, item]) => buildActionStateFromSpecModelKey(key, item.description, convertedMetadata, schemaNamespace))
.filter((actionState) => actionState !== undefined);
}
/**
* Extracts sub-sections data from a body section.
*
* @param section - body section entry from the application model
* @param parentSectionId - identifier of the parent section (used as fallback key prefix)
* @returns array of sub-section feature data
*/
function extractBodySubSectionsData(section, parentSectionId) {
const subSections = [];
const subSectionsAggregation = getAggregations(section)['subSections'];
const subSectionItems = getAggregations(subSectionsAggregation);
Object.entries(subSectionItems).forEach(([subSectionKey, subSection]) => {
const subSectionId = getSectionIdentifier(subSection) ?? `${parentSectionId}_${subSectionKey}`;
subSections.push({
id: subSectionId,
navigationProperty: getNavigationPropertyFromKey(subSectionKey),
isTable: !!subSection.isTable,
custom: !!subSection.custom,
order: subSection?.order ?? -1, // put a negative order number to signal that order was not in spec
fields: subSection.custom || subSection.isTable ? [] : extractFormFields(subSection),
tableColumns: subSection.custom || !subSection.isTable ? {} : extractTableColumnsFromNode(subSection)
});
});
return subSections;
}
/**
* Extracts form field property paths from a body sub-section's form aggregation.
*
* @param subSection - body sub-section entry from the application model
* @returns array of form field property paths for use with iCheckField({ property })
*/
function extractFormFields(subSection) {
const fields = [];
const formAggregation = getAggregations(subSection)['form'];
if (!formAggregation) {
return fields;
}
const fieldsAggregation = getAggregations(formAggregation)['fields'];
const fieldItems = getAggregations(fieldsAggregation);
Object.values(fieldItems).forEach((field) => {
const property = field.schema?.keys?.find((key) => key.name === 'Value')?.value;
if (property) {
fields.push({ property });
}
});
return fields;
}
/**
* Extracts the OData navigation property from a spec model section key.
* Section keys for table sections follow the pattern `_NavProperty::@annotation`, so the
* navigation property is the part before `::` when it starts with an underscore.
*
* @param sectionKey - the key of the section in the spec model aggregations
* @returns navigation property (e.g. '_Booking'), or undefined for non-navigation sections
*/
function getNavigationPropertyFromKey(sectionKey) {
const prefix = sectionKey.split('::')[0];
return prefix.startsWith('_') ? prefix : undefined;
}
/**
* Resolves the target entity set name for a navigation property by looking up navigation
* property bindings in the source entity set's metadata.
*
* @param convertedMetadata - converted OData metadata
* @param sourceEntitySetName - the name of the source entity set (the Object Page's entity set)
* @param navigationProperty - the navigation property name (e.g. '_Booking')
* @returns the target entity set name, or undefined if resolution fails
*/
function resolveNavigationTargetEntitySet(convertedMetadata, sourceEntitySetName, navigationProperty) {
if (!sourceEntitySetName) {
return undefined;
}
const sourceEntitySet = convertedMetadata.entitySets.find((es) => es.name === sourceEntitySetName);
if (!sourceEntitySet?.navigationPropertyBinding) {
return undefined;
}
const navPropName = navigationProperty.startsWith('_') ? navigationProperty.substring(1) : navigationProperty;
const binding = sourceEntitySet.navigationPropertyBinding[navPropName];
return binding?.name;
}
/**
* Gets the identifier of a section for OPA5 tests.
*
* @param section - section entry from ux specification
* @returns identifier of the section for OPA5 tests
*/
function getSectionIdentifier(section) {
return getSectionIdentifierFromKey(section) ?? getSectionIdentifierFromTitle(section);
}
/**
* Gets the identifier of a section from the 'ID' or 'Key' entry in the schema keys for OPA5 tests.
* If no such entry is found, undefined is returned.
*
* @param section - section entry from ux specification
* @returns identifier of the section for OPA5 tests; can be undefined if no 'ID' or 'Key' entry is found
*/
function getSectionIdentifierFromKey(section) {
const keyEntry = section?.schema?.keys?.find((key) => key.name === 'ID' || key.name === 'Key');
return keyEntry ? keyEntry.value.replace('#', '::') : undefined;
}
/**
* Gets the identifier of a section from its title for OPA5 tests.
*
* @param section - section entry from ux specification
* @returns identifier of the section for OPA5 tests; can be undefined if title is not in expected format
*/
function getSectionIdentifierFromTitle(section) {
return section.title?.slice(section.title?.lastIndexOf('.') + 1).replace('#', '::') ?? undefined;
}
/**
* Gets the stashed flag of a header section for OPA5 tests.
*
* @param headerSection - header section entry from ux specification
* @returns stashed flag of the header section for OPA5 tests; can be a boolean or a string depending on the specification version
*/
function getSectionStashedFlag(headerSection) {
return headerSection?.properties?.stashed?.freeText ?? false;
}
/**
* Gets form fields of a header section for OPA5 tests.
*
* @param section - section entry from ux specification
* @returns an array of form fields with their identifiers and bound properties for OPA5 tests
*/
function getHeaderSectionFormFields(section) {
const formFields = [];
const formAggregation = getAggregations(section)?.form;
const fieldsAggregation = getAggregations(formAggregation)?.fields;
const fields = getAggregations(fieldsAggregation);
if (fields) {
Object.keys(fields).forEach((fieldKey) => {
const field = fields[fieldKey];
const fieldData = getFormFieldData(field, formAggregation);
if (fieldData) {
formFields.push(fieldData);
}
});
}
return formFields;
}
/**
* Gets field data for a form field in a header section for OPA5 tests, including its identifier, bound property, and target annotation.
*
* @param field - field entry from ux specification
* @param formAggregation - form aggregation entry from ux specification, used to get field group qualifier for the field
* @returns field data including its identifier, bound property, and target annotation for OPA5 tests; can be undefined if the field type is not supported or necessary information is missing
*/
function getFormFieldData(field, formAggregation) {
if (!field.name) {
return undefined;
}
let [_, propertyName, targetAnnotation] = field.name.split('::');
// fall back to Value property in case of malformed or otherwise irregular field name
if (!propertyName) {
propertyName = field.schema.keys.find((key) => key.name === 'Value')?.value;
}
const fieldIdentifier = {
fieldGroupQualifier: getFieldGroupQualifier(formAggregation),
field: propertyName,
targetAnnotation: targetAnnotation
};
// avoid creating identifier if field property could not be determined
return fieldIdentifier.field ? fieldIdentifier : undefined;
}
/**
* Gets the field group qualifier of a form aggregation for OPA5 tests.
*
* @param formAggregation - form aggregation entry from ux specification
* @returns field group qualifier for OPA5 tests; can be undefined if not found
*/
function getFieldGroupQualifier(formAggregation) {
const fullTarget = formAggregation?.schema?.keys?.find((key) => key.name === 'Target')?.value;
return fullTarget?.split('#')[1];
}
/**
* Checks if the section contains a microChart based on it's name.
*
* @param section - section entry from ux specification
* @returns true if the section seems to contain a microChart, false otherwise
*/
function isSectionMicroChart(section) {
return section?.schema?.dataType === 'ChartDefinition';
}
/**
* Checks if the section contains a form based on it's aggregations.
*
* @param section - section entry from ux specification
* @returns true if the section seems to contain a form, false otherwise
*/
function isFormSection(section) {
return getAggregations(section)?.form !== undefined;
}
/**
* Retrieves navigation targets from the given page model.
*
* @param pageModel - The page model containing navigation definitions.
* @returns An array of navigation target identifiers.
*/
function getNavigationRoutes(pageModel) {
const navigationTargets = [];
if (!pageModel?.navigation) {
return navigationTargets;
}
Object.keys(pageModel.navigation).forEach((navigationKey) => {
if (pageModel.navigation) {
const navigationEntry = pageModel.navigation[navigationKey];
navigationTargets.push({
identifier: navigationKey,
route: typeof navigationEntry === 'string' ? navigationEntry : navigationEntry.route
});
}
});
return navigationTargets;
}
//# sourceMappingURL=objectPageUtils.js.map
;// ../../../node_modules/@sap-ux/ui5-test-writer/dist/utils/listReportUtils.js
/* unused harmony import specifier */ var parse;
/* unused harmony import specifier */ var convert;
/**
* Builds a button state object from button visibility result.
*
* @param buttonState - The button state from visibility check
* @returns Button state object with visible, enabled, and optional dynamicPath properties
*/
function buildButtonState(buttonState) {
return {
visible: !!buttonState?.visible,
enabled: buttonState?.enabled,
dynamicPath: buttonState?.enabled === 'dynamic' ? buttonState.dynamicPath : undefined
};
}
/**
* Safely checks action button states with error handling.
*
* @param convertedMetadata - The already-converted OData metadata
* @param entitySetName - The name of the entity set
* @param actionNames - List of action names to check
* @param log - Optional logger instance
* @returns Array of action button states or empty array if error occurs
*/
function safeCheckActionButtonStates(convertedMetadata, entitySetName, actionNames, log) {
try {
return checkActionButtonStatesFromMetadata(convertedMetadata, entitySetName, actionNames).actions;
}
catch (error) {
log?.debug(`Failed to check action button states: ${error instanceof Error ? error.message : String(error)}`);
return [];
}
}
/**
* Safely gets semantic key properties with error handling.
*
* @param convertedMetadata - The already-converted OData metadata
* @param entitySetName - The name of the entity set
* @param log - Optional logger instance
* @returns Array of semantic key properties or undefined if error occurs
*/
function safeGetSemanticKeyProperties(convertedMetadata, entitySetName, log) {
try {
return getSemanticKeyPropertiesFromMetadata(convertedMetadata, entitySetName, true);
}
catch (error) {
log?.debug(`Failed to get semantic key properties: ${error instanceof Error ? error.message : String(error)}`);
return undefined;
}
}
/**
* Returns true when a ListReport manifest target is configured as an Analytical List Page.
* ALP targets have a `views.paths` array where at least one entry contains a `primary` array,
* indicating the dual-view (chart + table) layout used by ALP.
*
* @param target - the manifest routing target to inspect
* @returns true if the target represents an ALP configuration
*/
function isALPManifestTarget(target) {
return (target.options?.settings?.views?.paths?.some((path) => Array.isArray(path.primary) && path.primary.length > 0) ?? false);
}
/**
* Returns true if any ListReport target in the manifest is configured as an Analytical List Page.
*
* @param manifest - the application manifest
* @param targetKey - optional specific target key to check; if omitted all ListReport targets are checked
* @returns true if the target (or any ListReport target) is an ALP
*/
function isALPFromManifest(manifest, targetKey) {
const targets = manifest['sap.ui5']?.routing?.targets;
if (!targets) {
return false;
}
const keysToCheck = targetKey ? [targetKey] : Object.keys(targets);
return keysToCheck.some((key) => {
const target = targets[key];
return target?.name === 'sap.fe.templates.ListReport' && isALPManifestTarget(target);
});
}
/**
* Gets List Report features from the page model using ux-specification.
*
* @param listReportPage - the List Report page containing the tree model with feature definitions
* @param log - optional logger instance
* @param metadata - optional metadata for the OPA test generation
* @param manifest - optional application manifest, used to detect ALP configuration
* @returns feature data extracted from the List Report page model
*/
function getListReportFeatures(listReportPage, log, metadata, manifest) {
const toolbarActions = getToolBarActionNames(listReportPage.model, log);
const filterBarItems = getFilterFieldNames(listReportPage.model, log);
let buttonVisibility;
let semanticKeyProperties;
let toolBarActions = [];
if (metadata && listReportPage.entitySet) {
const entitySetName = listReportPage.entitySet;
try {
const convertedMetadata = (0,annotation_converter_dist.convert)((0,edmx_parser_dist/* parse */.qg)(metadata));
buttonVisibility = safeCheckButtonVisibilityFromMetadata(convertedMetadata, entitySetName, log);
semanticKeyProperties = safeGetSemanticKeyProperties(convertedMetadata, entitySetName, log);
toolBarActions = safeCheckActionButtonStates(convertedMetadata, entitySetName, toolbarActions, log);
}
catch (error) {
log?.debug(`Failed to parse metadata: ${error instanceof Error ? error.message : String(error)}`);
}
}
const missingKeys = semanticKeyProperties?.length && filterBarItems.length
? semanticKeyProperties.filter((key) => !filterBarItems.includes(key))
: undefined;
return {
name: listReportPage.name,
createButton: buildButtonState(buttonVisibility?.create),
deleteButton: buildButtonState(buttonVisibility?.delete),
filterBarItems,
tableColumns: getTableColumnData(listReportPage.model, log),
toolBarActions,
isALP: manifest ? isALPFromManifest(manifest, listReportPage.name) : false,
semanticKey: {
semanticKeyProperties,
missingFromFilterBar: missingKeys?.length ? missingKeys : undefined
}
};
}
/**
* Retrieves toolbar action definitions from the given tree model.
*
* @param pageModel - The tree model containing toolbar definitions.
* @returns The toolbar actions aggregation object.
*/
function getToolBarActions(pageModel) {
const table = getAggregations(pageModel.root)['table'];
const tableAggregations = getAggregations(table);
const toolBar = tableAggregations['toolBar'];
const toolBarAggregations = getAggregations(toolBar);
const actions = toolBarAggregations['actions'];
const actionAggregations = getAggregations(actions);
return actionAggregations;
}
/**
* Retrieves filter field names from the page model using ux-specification.
*
* @param pageModel - the tree model containing filter bar definitions
* @param log - optional logger instance
* @returns - an array of filter field names
*/
function getFilterFieldNames(pageModel, log) {
let filterBarItems = [];
try {
const filterBarAggregations = getFilterFields(pageModel);
filterBarItems = getSelectionFieldItems(filterBarAggregations);
}
catch (error) {
log?.debug(error);
}
if (!filterBarItems?.length) {
log?.warn('Unable to extract filter fields from project model using specification. No filter field tests will be generated.');
}
return filterBarItems;
}
/**
* Checks the state of action buttons defined in UI.LineItem annotations for a given entity set.
*
* @param convertedMetadata The already-converted OData metadata
* @param entitySetName The name of the entity set to check
* @param actionNames Optional list of action names to filter (e.g., ['Check', 'deductDiscount']). If not provided, returns all actions.
* @returns ActionButtonsResult containing the list of action buttons and their states
* @throws {Error} If entity set is not found
*/
function checkActionButtonStatesFromMetadata(convertedMetadata, entitySetName, actionNames) {
const entitySet = convertedMetadata.entitySets.find((es) => es.name === entitySetName);
if (!entitySet) {
throw new Error(`Entity set '${entitySetName}' not found in metadata`);
}
const entityType = entitySet.entityType;
if (!entityType) {
throw new Error(`Entity type not found for entity set '${entitySetName}'`);
}
const lineItemAnnotation = entityType.annotations?.UI?.LineItem;
if (!lineItemAnnotation || !Array.isArray(lineItemAnnotation)) {
return { actions: [], entityType: entityType.name };
}
const dataFieldForActions = lineItemAnnotation.filter((item) => item.$Type === 'com.sap.vocabularies.UI.v1.DataFieldForAction');
const actions = actionNames
? findActionStates(dataFieldForActions, actionNames, convertedMetadata)
: extractAllActionStates(dataFieldForActions, convertedMetadata);
return { actions, entityType: entityType.name };
}
/**
* Checks the state of action buttons defined in UI.LineItem annotations for a given entity set.
*
* @param metadataXml The OData metadata XML content as a string
* @param entitySetName The name of the entity set to check
* @param actionNames Optional list of action names to filter (e.g., ['Check', 'deductDiscount']). If not provided, returns all actions.
* @returns ActionButtonsResult containing the list of action buttons and their states
* @throws {Error} If metadata cannot be parsed or entity set is not found
*/
function checkActionButtonStates(metadataXml, entitySetName, actionNames) {
try {
return checkActionButtonStatesFromMetadata(convert(parse(metadataXml)), entitySetName, actionNames);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to analyze action button states: ${errorMessage}`);
}
}
/**
* Finds action states for a specific list of action names.
*
* @param dataFieldForActions List of DataFieldForAction items from UI.LineItem
* @param actionNames List of action names to find
* @param metadata The converted metadata
* @returns List of action button states for the specified actions
*/
function findActionStates(dataFieldForActions, actionNames, metadata) {
const actionStates = [];
for (const actionName of actionNames) {
const item = dataFieldForActions.find((dfa) => {
const actionMethod = extractActionMethodName(dfa.Action || '');
return actionMethod === actionName || dfa.Label === actionName;
});
if (item) {
actionStates.push(buildActionButtonState(item, metadata));
}
}
return actionStates;
}
/**
* Extracts action states for all DataFieldForAction items.
*
* @param dataFieldForActions List of DataFieldForAction items from UI.LineItem
* @param metadata The converted metadata
* @returns List of all action button states
*/
function extractAllActionStates(dataFieldForActions, metadata) {
return dataFieldForActions.map((item) => buildActionButtonState(item, metadata));
}
/**
* Retrieves toolbar action names from the page model using ux-specification.
*
* @param pageModel - the tree model containing