@sap/generator-fiori
Version:
Create an SAPUI5 application using SAP Fiori elements or a freestyle approach
1,121 lines (1,094 loc) • 94.1 kB
JavaScript
"use strict";
exports.id = 2944;
exports.ids = [2944];
exports.modules = {
/***/ 35445
(__unused_webpack_module, exports, __webpack_require__) {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.generateOPAFiles = generateOPAFiles;
exports.readManifest = readManifest;
exports.generatePageObjectFile = generatePageObjectFile;
const node_path_1 = __webpack_require__(76760);
const node_fs_1 = __webpack_require__(73024);
const mem_fs_1 = __webpack_require__(64812);
const mem_fs_editor_1 = __webpack_require__(90718);
const types_1 = __webpack_require__(30420);
const i18n_1 = __webpack_require__(60577);
const project_access_1 = __webpack_require__(20787);
const modelUtils_1 = __webpack_require__(15197);
const opaQUnitUtils_1 = __webpack_require__(91055);
const fiori_generator_shared_1 = __webpack_require__(58012);
const flpSandboxUtils_1 = __webpack_require__(4241);
/**
* Generate OPA test files for a Fiori elements for OData V4 application.
* Note: this can potentially overwrite existing files in the webapp/test folder.
*
* @param basePath - the absolute target path where the application will be generated
* @param opaConfig - parameters for the generation
* @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
* @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
* @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
* @param metadata - optional metadata for the OPA test generation
* @param fs - an optional reference to a mem-fs editor
* @param log - optional logger instance
* @param standalone - opa test generation run standalone, not during app generation
* @returns Reference to a mem-fs-editor
*/
async function generateOPAFiles(basePath, opaConfig, metadata, fs, log, standalone = false) {
const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
const manifest = readManifest(editor, basePath);
const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest);
const config = createConfig(manifest, opaConfig, hideFilterBar);
const rootCommonTemplateDirPath = (0, node_path_1.join)(__dirname, '../templates/common');
const rootV4TemplateDirPath = (0, node_path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
const testOutDirPath = (0, node_path_1.join)(await (0, project_access_1.getWebappPath)(basePath), 'test');
// Access ux-specification to get feature data for OPA test generation
const appFeatures = await (0, modelUtils_1.getAppFeatures)(basePath, editor, log, metadata, manifest);
// OPA Journey file
const startPages = config.pages.filter((page) => page.isStartup).map((page) => page.targetKey);
const LROP = findLROP(config.pages, manifest);
const journeyParams = {
startPages,
startLR: LROP.pageLR?.targetKey,
navigatedOP: LROP.pageOP?.targetKey,
hideFilterBar: config.hideFilterBar
};
const writeContext = { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams };
if (standalone) {
const hasJourneyRunner = (0, node_fs_1.existsSync)((0, node_path_1.join)(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'));
const virtualOPA5Configured = await (0, opaQUnitUtils_1.hasVirtualOPA5)(basePath);
if (hasJourneyRunner) {
writeJourneyFiles(appFeatures, writeContext, true, true, virtualOPA5Configured);
}
else {
const standaloneWriteContext = await resolveStandaloneWriteContext(basePath, testOutDirPath, writeContext, editor);
if (!virtualOPA5Configured) {
writeCommonAndPageFiles(standaloneWriteContext, rootCommonTemplateDirPath);
}
writeJourneyFiles(appFeatures, standaloneWriteContext, true, hasJourneyRunner, virtualOPA5Configured);
}
}
else {
writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath);
writeJourneyFiles(appFeatures, writeContext, false);
}
return editor;
}
/**
* Resolves the write context for standalone mode when no JourneyRunner.js exists yet.
* Moves any existing integration folder to integration_old, or adds the int-test script
* and resolves the htmlTarget from flpSandbox.html if present.
*
* @param basePath - the absolute target path of the application
* @param testOutDirPath - output test directory (.../webapp/test)
* @param writeContext - shared write context to base the resolved context on
* @param editor - a reference to a mem-fs editor
* @returns a new WriteContext with the resolved htmlTarget
*/
async function resolveStandaloneWriteContext(basePath, testOutDirPath, writeContext, editor) {
const { config } = writeContext;
let htmlTarget = (0, opaQUnitUtils_1.readHtmlTargetFromQUnitJs)(testOutDirPath, editor) ?? config.htmlTarget;
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(testOutDirPath, 'integration'))) {
editor.move((0, node_path_1.join)(testOutDirPath, 'integration', '**'), (0, node_path_1.join)(testOutDirPath, 'integration_old'));
await (0, opaQUnitUtils_1.addIntegrationOldToGitignore)(basePath, editor);
}
else {
const hasIntTestScript = checkScriptInPackageJson(editor, basePath, 'int-test');
if (!hasIntTestScript) {
const script = (0, fiori_generator_shared_1.getPackageScripts)({ localOnly: false, addTest: true })['int-test'];
if (script) {
await (0, project_access_1.updatePackageScript)(basePath, 'int-test', script, editor);
}
}
if ((0, node_fs_1.existsSync)((0, node_path_1.join)(testOutDirPath, 'flpSandbox.html'))) {
const hashFromFlpSandbox = (0, flpSandboxUtils_1.readHashFromFlpSandbox)((0, node_path_1.join)('test', 'flpSandbox.html'), await (0, project_access_1.getWebappPath)(basePath), editor);
if (hashFromFlpSandbox) {
htmlTarget = `test/flpSandbox.html#${hashFromFlpSandbox}`;
}
}
}
return { ...writeContext, config: { ...config, htmlTarget } };
}
/**
* Checks whether a script with the given name exists in the package.json.
*
* @param editor - a reference to a mem-fs editor
* @param basePath - the root folder of the app
* @param scriptName - the name of the script to check for
* @returns true if the script exists, false otherwise
*/
function checkScriptInPackageJson(editor, basePath, scriptName) {
const packageJsonPath = (0, node_path_1.join)(basePath, project_access_1.FileName.Package);
if (!editor.exists(packageJsonPath)) {
return false;
}
const packageJson = editor.readJSON(packageJsonPath);
return !!packageJson.scripts?.[scriptName];
}
/**
* Reads the manifest for an app.
*
* @param fs - a reference to a mem-fs editor
* @param basePath - the root folder of the app
* @returns the manifest object. An exception is thrown if the manifest cannot be read.
*/
function readManifest(fs, basePath) {
const manifest = fs.readJSON((0, node_path_1.join)(basePath, project_access_1.DirName.Webapp, project_access_1.FileName.Manifest));
if (!manifest) {
throw new types_1.ValidationError((0, i18n_1.t)('error.cannotReadManifest', {
filePath: (0, node_path_1.join)(basePath, project_access_1.DirName.Webapp, project_access_1.FileName.Manifest)
}));
}
return manifest;
}
/**
* Retrieves the application type of the main datasource (FreeStyle, FE V2 or FE V4).
*
* @param manifest - the app descriptor of the app
* @returns {{ applicationType: string, hideFilterBar: boolean }} An object containing the application type and hideFilterBar flag. An exception is thrown if it can't be found or if it's not supported
*/
function getAppTypeAndHideFilterBarFromManifest(manifest) {
const appTargets = manifest['sap.ui5']?.routing?.targets;
let hideFilterBar = false;
let isFEV4 = false;
for (const targetKey in appTargets) {
const target = appTargets[targetKey];
if (target.type === 'Component' && target.name && target.name in types_1.SupportedPageTypes) {
isFEV4 = true;
if (types_1.SupportedPageTypes[target.name] === 'ListReport') {
hideFilterBar = target.options?.settings?.hideFilterBar ?? false;
}
}
}
if (!isFEV4) {
throw new types_1.ValidationError((0, i18n_1.t)('error.badApplicationType'));
}
return { applicationType: 'v4', hideFilterBar }; // For the time being, only FE V4 is supported
}
/**
* Retrieves appID and appPath from the manifest.
*
* @param manifest - the app descriptor of the app
* @param forcedAppID - the appID in case we don't want to read it from the manifest
* @returns appID and appPath
*/
function getAppFromManifest(manifest, forcedAppID) {
const appID = forcedAppID ?? manifest['sap.app']?.id;
const appPath = appID?.split('.').join('/');
if (!appID || !appPath) {
throw new types_1.ValidationError((0, i18n_1.t)('error.cannotReadAppID'));
}
return { appID, appPath };
}
/**
* Create the page configuration object from the app descriptor and the target key.
*
* @param manifest - the app descriptor of the app
* @param targetKey - the key of the target in the manifest
* @param forcedAppID - the appID in case we don't want to read it from the manifest
* @returns Page configuration object, or undefined if the target type is not supported
*/
function createPageConfig(manifest, targetKey, forcedAppID) {
const appTargets = manifest['sap.ui5']?.routing?.targets;
const target = appTargets && appTargets[targetKey];
const { appID, appPath } = getAppFromManifest(manifest, forcedAppID);
if (target?.type === 'Component' &&
target?.name &&
target.name in types_1.SupportedPageTypes &&
target?.id &&
(target?.options?.settings?.entitySet || target?.options?.settings?.contextPath)) {
const pageConfig = {
appPath,
appID,
targetKey,
componentID: target.id,
template: types_1.SupportedPageTypes[target.name],
isStartup: false
};
if (target.options.settings.contextPath) {
pageConfig.contextPath = target.options.settings.contextPath;
}
else if (target.options.settings.entitySet) {
pageConfig.entitySet = target.options.settings.entitySet;
}
return pageConfig;
}
else {
return undefined;
}
}
/**
* Create the configuration object from the app descriptor.
*
* @param manifest - the app descriptor of the target app
* @param opaConfig - parameters for the generation
* @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
* @param opaConfig.htmlTarget - the name of the html file that will be used in the OPA journey file. If not specified, 'index.html' will be used
* @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
* @param hideFilterBar - whether the filter bar should be hidden in the generated tests
* @returns OPA test configuration object
*/
function createConfig(manifest, opaConfig, hideFilterBar) {
// General application info
const { appID, appPath } = getAppFromManifest(manifest, opaConfig.appID);
const config = {
appID,
appPath,
pages: [],
opaJourneyFileName: opaConfig.scriptName ?? 'FirstJourney',
htmlTarget: opaConfig.htmlTarget ?? 'index.html',
hideFilterBar
};
// Identify startup targets from the routes
const appRoutes = (manifest['sap.ui5']?.routing?.routes ?? []);
// Find the route with an empty pattern (except for the trailing query part)
const startupRoute = appRoutes.find((route) => {
return route.pattern.replace(':?query:', '') === '';
});
let startupTargets = startupRoute?.target ?? [];
if (!Array.isArray(startupTargets)) {
startupTargets = [startupTargets];
}
// Create page configurations in supported cases
const appTargets = manifest['sap.ui5']?.routing?.targets;
for (const targetKey in appTargets) {
const pageConfig = createPageConfig(manifest, targetKey, opaConfig.appID);
if (pageConfig) {
pageConfig.isStartup = startupTargets.includes(targetKey);
config.pages.push(pageConfig);
}
}
return config;
}
/**
* Finds the initial ListReport page and the first Object page from the app.
*
* @param pages - the page configs of the app
* @param manifest - the app descriptor of the target app
* @returns the page fonfigs for the LR and the OP if they're found
*/
function findLROP(pages, manifest) {
const pageLR = pages.find((page) => {
return page.isStartup && page.template === 'ListReport';
});
if (!pageLR) {
return {};
}
const appTargets = manifest['sap.ui5']?.routing?.targets;
const appRoutes = (manifest['sap.ui5']?.routing?.routes ?? []);
const target = appTargets?.[pageLR.targetKey];
if (!target?.options?.settings?.navigation) {
return { pageLR }; // No navigation from LR
}
// Find all targets that can be navigated from the LR page
const navigatedTargetKeys = [];
for (const navKey in target.options.settings.navigation) {
const navObject = target.options.settings.navigation[navKey];
const navigatedRoute = navObject.detail?.route &&
appRoutes.find((route) => {
return route.name === navObject.detail?.route;
});
if (Array.isArray(navigatedRoute?.target)) {
navigatedTargetKeys.push(...navigatedRoute.target);
}
else if (navigatedRoute?.target) {
navigatedTargetKeys.push(navigatedRoute.target);
}
}
// Find the first navigated page that is valid and not the starting LR
let pageOP;
for (let i = 0; i < navigatedTargetKeys.length && !pageOP; i++) {
if (navigatedTargetKeys[i] === pageLR.targetKey) {
continue; // This can happen in the FCL case where the LR is also part of the route's targets to the OP
}
pageOP = pages.find((page) => {
return page.targetKey === navigatedTargetKeys[i];
});
}
return { pageLR, pageOP };
}
/**
* Writes common test files, page objects, and the first journey file.
*
* @param writeContext - shared write context (config, paths, editor, journey params)
* @param rootCommonTemplateDirPath - template root directory for common files
*/
function writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath) {
const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
// Common test files
editor.copyTpl((0, node_path_1.join)(rootCommonTemplateDirPath), testOutDirPath,
// unit tests are not added for Fiori elements app
{ appId: config.appID }, undefined, {
globOptions: { dot: true }
});
config.pages.forEach((page) => {
writePageObject(page, rootV4TemplateDirPath, testOutDirPath, editor);
});
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'FirstJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${config.opaJourneyFileName}.js`), journeyParams, undefined, {
globOptions: { dot: true }
});
// Journey Runner
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'pages', 'JourneyRunner.js'), (0, node_path_1.join)(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'), config, undefined, {
globOptions: { dot: true }
});
}
/**
* Checks whether a page object file already exists for the given feature name.
* If it doesn't exist, finds the matching page config and writes the file.
*
* @param featureName - the feature/page name (equals the manifest targetKey)
* @param config - the OPA config containing all page configurations
* @param rootV4TemplateDirPath - template root directory for v4 templates
* @param testOutDirPath - output test directory (.../webapp/test)
* @param editor - a reference to a mem-fs editor
* @returns JourneyRunnerPage if the page was newly created, undefined otherwise
*/
function ensurePageExists(featureName, config, rootV4TemplateDirPath, testOutDirPath, editor) {
const pageFilePath = (0, node_path_1.join)(testOutDirPath, 'integration', 'pages', `${featureName}.js`);
if (editor.exists(pageFilePath)) {
return undefined;
}
const pageConfig = config.pages.find((p) => p.targetKey === featureName);
if (pageConfig) {
writePageObject(pageConfig, rootV4TemplateDirPath, testOutDirPath, editor);
return { targetKey: featureName, appPath: config.appPath };
}
return undefined;
}
/**
* Writes journey files for list report, object pages and FPM pages.
*
* @param appFeatures - object containing feature data for list report, object pages, and FPM
* @param writeContext - shared write context (config, paths, editor, journey params)
* @param isStandalone - whether the generation is run in standalone mode (not during app generation)
* @param hasJourneyRunner - whether a JourneyRunner.js already exists (standalone upgrade path)
* @param virtualOPA5Configured - whether virtual OPA5 is configured
*/
function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRunner = false, virtualOPA5Configured = false) {
const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
const generatedJourneyPages = [];
const newPages = [];
if (appFeatures.listReport?.name) {
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'ListReportJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${appFeatures.listReport.name}Journey.js`), {
...journeyParams,
...appFeatures.listReport
}, undefined, {
globOptions: { dot: true }
});
generatedJourneyPages.push(appFeatures.listReport.name);
const lrPage = ensurePageExists(appFeatures.listReport.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
if (lrPage) {
newPages.push(lrPage);
}
}
if (appFeatures.objectPages && appFeatures.objectPages.length > 0) {
appFeatures.objectPages.forEach((objectPage) => {
if (objectPage.name) {
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'ObjectPageJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${objectPage.name}Journey.js`), {
...journeyParams,
...objectPage,
isStandalone
}, undefined, {
globOptions: { dot: true }
});
generatedJourneyPages.push(objectPage.name);
const opPage = ensurePageExists(objectPage.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
if (opPage) {
newPages.push(opPage);
}
}
});
}
if (appFeatures.fpm?.name) {
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'FPMJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${appFeatures.fpm.name}Journey.js`), {
...journeyParams,
...appFeatures.fpm
}, undefined, {
globOptions: { dot: true }
});
generatedJourneyPages.push(appFeatures.fpm.name);
const fpmPage = ensurePageExists(appFeatures.fpm.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
if (fpmPage) {
newPages.push(fpmPage);
}
}
if (newPages.length > 0) {
(0, opaQUnitUtils_1.addPagesToJourneyRunner)(newPages, testOutDirPath, editor);
}
if (!virtualOPA5Configured) {
if (hasJourneyRunner) {
(0, opaQUnitUtils_1.addPathsToQUnitJs)(generatedJourneyPages.map((page) => {
return `${config.appPath}/test/integration/${page}Journey`;
}), testOutDirPath, editor);
}
else {
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'opaTests.*.*'), (0, node_path_1.join)(testOutDirPath, 'integration'), { ...config, generatedJourneyPages }, undefined, {
globOptions: { dot: true }
});
}
}
}
/**
* Writes a page object in a mem-fs-editor.
*
* @param pageConfig - the page configuration object
* @param rootTemplateDirPath - template root directory
* @param testOutDirPath - output test directory (.../webapp/test)
* @param fs - a reference to a mem-fs editor
*/
function writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, fs) {
fs.copyTpl((0, node_path_1.join)(rootTemplateDirPath, 'integration', 'pages', `${pageConfig.template}.js`), (0, node_path_1.join)(testOutDirPath, 'integration', 'pages', `${pageConfig.targetKey}.js`), pageConfig, undefined, {
globOptions: { dot: true }
});
}
/**
* Generate a page object file for a Fiori elements for OData V4 application.
* Note: this doesn't modify other existing files in the webapp/test folder.
*
* @param basePath - the absolute target path where the application will be generated
* @param pageObjectParameters - parameters for the page
* @param pageObjectParameters.targetKey - the key of the target in the manifest file corresponding to the page
* @param pageObjectParameters.appID - the appID. If not specified, will be read from the manifest in sap.app/id
* @param fs - an optional reference to a mem-fs editor
* @returns Reference to a mem-fs-editor
*/
async function generatePageObjectFile(basePath, pageObjectParameters, fs) {
const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
const manifest = readManifest(editor, basePath);
const { applicationType } = getAppTypeAndHideFilterBarFromManifest(manifest);
const pageConfig = createPageConfig(manifest, pageObjectParameters.targetKey, pageObjectParameters.appID);
if (pageConfig) {
const rootTemplateDirPath = (0, node_path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
const testOutDirPath = (0, node_path_1.join)(await (0, project_access_1.getWebappPath)(basePath), 'test');
writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, editor);
}
else {
throw new types_1.ValidationError((0, i18n_1.t)('error.cannotGeneratePageFile', {
targetKey: pageObjectParameters.targetKey
}));
}
return editor;
}
//# sourceMappingURL=fiori-elements-opa-writer.js.map
/***/ },
/***/ 53591
(__unused_webpack_module, exports, __webpack_require__) {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.generateFreestyleOPAFiles = generateFreestyleOPAFiles;
const node_path_1 = __webpack_require__(76760);
const mem_fs_1 = __webpack_require__(64812);
const mem_fs_editor_1 = __webpack_require__(90718);
const project_access_1 = __webpack_require__(20787);
const i18n_1 = __webpack_require__(60577);
const ui5_application_writer_1 = __webpack_require__(57727);
/**
* Updates tsconfig.json to include paths for unit and integration tests.
*
* @param {Editor} fs - The file system editor instance.
* @param {string} destinationRoot - The root directory where tsconfig.json exists.
* @param log
*/
function writeOPATsconfigJsonUpdates(fs, destinationRoot, log) {
try {
const tsconfig = fs.readJSON((0, node_path_1.join)(destinationRoot, project_access_1.FileName.Tsconfig)) ?? {};
tsconfig.compilerOptions = tsconfig.compilerOptions || {};
tsconfig.compilerOptions.paths = tsconfig.compilerOptions.paths || {};
tsconfig.compilerOptions.paths['unit/*'] = ['./webapp/test/unit/*'];
tsconfig.compilerOptions.paths['integration/*'] = ['./webapp/test/integration/*'];
fs.writeJSON((0, node_path_1.join)(destinationRoot, project_access_1.FileName.Tsconfig), tsconfig);
}
catch (error) {
log?.error((0, i18n_1.t)('error.errorWritingTsConfig', {
error: error
}));
}
}
/**
* Gets the template UI5 version based on the provided UI5 version.
*
* @param ui5Version - The UI5 version.
* @returns template UI5 version.
*/
function getTemplateUi5Version(ui5Version) {
if (!ui5Version) {
return ui5_application_writer_1.ui5LtsVersion_1_120;
}
return (0, ui5_application_writer_1.compareUI5VersionGte)(ui5Version, ui5_application_writer_1.ui5LtsVersion_1_120) ? ui5_application_writer_1.ui5LtsVersion_1_120 : ui5_application_writer_1.ui5LtsVersion_1_71;
}
/**
* Filters files based on the template UI5 version.
*
* @param files - Array of file paths.
* @param templateUi5Version - The current template Ui5 Version.
* @returns Files that either are testsuite files or reside in the current template UI5 version folder.
*/
function filterByUi5Version(files, templateUi5Version) {
return files.filter((filePath) => {
// Always include testsuite files.
if (filePath.includes('testsuite.qunit')) {
return true;
}
// For all other files, include only those in the current UI5 version directory.
return filePath.includes(`${node_path_1.sep}${templateUi5Version}${node_path_1.sep}`);
});
}
/**
* Filters files based on the TypeScript setting.
*
* @param files - Array of file paths.
* @param isTypeScript - If true, include .ts files; if false, include .js files.
* @returns Files filtered based on the file extension.
*/
function filterByTypeScript(files, isTypeScript) {
return files.filter((filePath) => {
if (filePath.endsWith('.ts')) {
return isTypeScript;
}
if (filePath.endsWith('.js')) {
return !isTypeScript;
}
// Keep all .html file types
return true;
});
}
/**
* Determines the destination file path based on the provided file path.
*
* @param {string} filePath - The original file path.
* @param {string} freestyleTemplateDir - The directory file path to be removed from the path.
* @param {string} commonTemplateDir - The directory file to be removed for common templates path.
* @param {string} templateUi5Version - The UI5 version to be replaced in the path.
* @returns {string} - The transformed destination file path.
*/
function getDestFilePath(filePath, freestyleTemplateDir, commonTemplateDir, templateUi5Version) {
if (filePath.includes(freestyleTemplateDir)) {
return filePath.replace(freestyleTemplateDir, '').replace(`${node_path_1.sep}${templateUi5Version}${node_path_1.sep}`, node_path_1.sep);
}
else if (filePath.includes(commonTemplateDir)) {
return filePath.replace(commonTemplateDir, '');
}
else {
return filePath;
}
}
/**
* Generates and copies freestyle test files based on configuration.
*
* @param {string} basePath - The base directory path.
* @param {FFOPAConfig} opaConfig - Configuration object.
* @param {Editor} fs - Optional file system editor instance.
* @param {Logger} log - Optional logger instance.
* @returns {Editor} - The modified file system editor.
*/
async function generateFreestyleOPAFiles(basePath, opaConfig, fs, log) {
const fsEditor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
const { enableTypeScript, ui5Version, viewName, appId } = opaConfig;
const freestyleTemplateDir = (0, node_path_1.join)(__dirname, '../templates/freestyle/webapp/test');
const commonTemplateDir = (0, node_path_1.join)(__dirname, '../templates/common');
const testOutDir = (0, node_path_1.join)(basePath, 'webapp/test');
const templateUi5Version = getTemplateUi5Version(ui5Version);
const appIdWithSlash = appId.replace(/[.]/g, '/'); // Replace all dots with slashes
const navigationIntent = appId.replace(/[./\\\-\s]/g, ''); // Remove all dots, slashes, dashes, and spaces
const templateFiles = await (0, project_access_1.getFilePaths)(freestyleTemplateDir);
const isTypeScript = Boolean(enableTypeScript);
const templateFilteredFiles = filterByUi5Version(templateFiles, templateUi5Version);
const filteredFiles = filterByTypeScript(templateFilteredFiles, isTypeScript);
// copy common templates
const commonFiles = await (0, project_access_1.getFilePaths)(commonTemplateDir);
const filteredCommonFiles = commonFiles.filter((filePath) => filePath.endsWith('.html'));
filteredFiles.push(...filteredCommonFiles);
const config = {
...opaConfig,
viewNamePage: `${viewName}Page`,
appIdWithSlash,
navigationIntent,
ui5Theme: opaConfig.ui5Theme ?? ''
};
// Rename files:
// - viewName.js files are renamed to include the view name in their file path
// - viewName.ts files are renamed with the view name appended with 'Page'
const renameMap = {
[(0, node_path_1.join)('/integration/pages/viewName.js')]: (0, node_path_1.join)(`integration/pages/${viewName}.js`),
[(0, node_path_1.join)('/integration/pages/viewName.ts')]: (0, node_path_1.join)(`integration/pages/${viewName}Page.ts`),
[(0, node_path_1.join)('/unit/controller/viewName.controller.js')]: (0, node_path_1.join)(`unit/controller/${viewName}.controller.js`),
[(0, node_path_1.join)('/unit/controller/viewName.controller.ts')]: (0, node_path_1.join)(`unit/controller/${viewName}Page.controller.ts`)
};
// copy templates
let freestyleTestTemplatesCopied = false;
try {
filteredFiles.forEach((filePath) => {
// remove template UI5 version from the path
const destFilePath = getDestFilePath(filePath, freestyleTemplateDir, commonTemplateDir, templateUi5Version);
const destinationFilePath = (0, node_path_1.join)(testOutDir, renameMap?.[destFilePath] ?? destFilePath);
fsEditor.copyTpl(filePath, destinationFilePath, config, undefined, {
globOptions: { dot: true }
});
});
freestyleTestTemplatesCopied = true;
}
catch (error) {
log?.error((0, i18n_1.t)('error.errorCopyingFreestyleTestTemplates', {
error: error
}));
}
if (freestyleTestTemplatesCopied && isTypeScript) {
writeOPATsconfigJsonUpdates(fsEditor, basePath, log);
}
return fsEditor;
}
//# sourceMappingURL=fiori-freestyle-opa-writer.js.map
/***/ },
/***/ 60577
(__unused_webpack_module, exports, __webpack_require__) {
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.i18n = void 0;
exports.initI18n = initI18n;
exports.t = t;
const i18next_1 = __importDefault(__webpack_require__(68801));
const ui5_test_writer_i18n_json_1 = __importDefault(__webpack_require__(5125));
const NS = 'ui5-test-writer';
exports.i18n = i18next_1.default.createInstance();
/**
* Initialize i18next with the translations for this module.
*/
async function initI18n() {
await exports.i18n.init({
resources: {
en: {
[NS]: ui5_test_writer_i18n_json_1.default
}
},
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 exports.i18n.t(key, options);
}
initI18n().catch(() => {
// Ignore any errors since the write will still work
});
//# sourceMappingURL=i18n.js.map
/***/ },
/***/ 53069
(__unused_webpack_module, exports, __webpack_require__) {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.generateFreestyleOPAFiles = exports.generateOPAFiles = void 0;
var fiori_elements_opa_writer_1 = __webpack_require__(35445);
Object.defineProperty(exports, "generateOPAFiles", ({ enumerable: true, get: function () { return fiori_elements_opa_writer_1.generateOPAFiles; } }));
var fiori_freestyle_opa_writer_1 = __webpack_require__(53591);
Object.defineProperty(exports, "generateFreestyleOPAFiles", ({ enumerable: true, get: function () { return fiori_freestyle_opa_writer_1.generateFreestyleOPAFiles; } }));
//# sourceMappingURL=index.js.map
/***/ },
/***/ 30420
(__unused_webpack_module, exports) {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.ValidationError = exports.SupportedPageTypes = void 0;
exports.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;
}
}
exports.ValidationError = ValidationError;
//# sourceMappingURL=types.js.map
/***/ },
/***/ 4241
(__unused_webpack_module, exports, __webpack_require__) {
/**
* Utility for reading the FLP sandbox HTML file and extracting the
* application hash (intent) from the sap-ushell-config applications object.
*/
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.readHashFromFlpSandbox = readHashFromFlpSandbox;
const node_path_1 = __webpack_require__(76760);
/**
* Regex to extract the first application key from the sap-ushell-config
* `applications` block. Matches patterns like:
*
* applications: {
* "fincashbankmanage-tile": {
*
* Captures the quoted key (e.g. `fincashbankmanage-tile`).
*/
const APPLICATIONS_KEY_REGEX = /applications\s*:\s*\{[^"]*"([^"]+)"\s*:/;
/**
* Reads an FLP sandbox HTML file and extracts the first application key
* from the `sap-ushell-config` `applications` object.
*
* @param htmlRelativePath - path to the HTML file relative to `webapp/`
* (e.g. `test/flpSandbox.html`)
* @param webappPath - path to the webapp directory
* @param fs - mem-fs-editor instance used to read the file
* @returns the application key (e.g. `fincashbankmanage-tile`), or undefined
*/
function readHashFromFlpSandbox(htmlRelativePath, webappPath, fs) {
try {
const filePath = (0, node_path_1.join)(webappPath, htmlRelativePath);
const content = fs.read(filePath);
const match = APPLICATIONS_KEY_REGEX.exec(content);
return match?.[1];
}
catch {
return undefined;
}
}
//# sourceMappingURL=flpSandboxUtils.js.map
/***/ },
/***/ 42696
(__unused_webpack_module, exports, __webpack_require__) {
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.buildButtonState = buildButtonState;
exports.safeCheckButtonVisibility = safeCheckButtonVisibility;
exports.safeCheckActionButtonStates = safeCheckActionButtonStates;
exports.isALPManifestTarget = isALPManifestTarget;
exports.isALPFromManifest = isALPFromManifest;
exports.getListReportFeatures = getListReportFeatures;
exports.getToolBarActions = getToolBarActions;
exports.checkButtonVisibility = checkButtonVisibility;
exports.getFilterFieldNames = getFilterFieldNames;
exports.checkActionButtonStates = checkActionButtonStates;
exports.getToolBarActionNames = getToolBarActionNames;
exports.getToolBarActionItems = getToolBarActionItems;
const modelUtils_1 = __webpack_require__(15197);
const edmx_parser_1 = __webpack_require__(39933);
const annotation_converter_1 = __webpack_require__(91125);
/**
* 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 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 action button states with error handling.
*
* @param metadata - The OData metadata XML content
* @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(metadata, entitySetName, actionNames, log) {
try {
return checkActionButtonStates(metadata, entitySetName, actionNames).actions;
}
catch (error) {
log?.debug(`Failed to check action button states: ${error instanceof Error ? error.message : String(error)}`);
return [];
}
}
/**
* 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 buttonVisibility = metadata && listReportPage.entitySet
? safeCheckButtonVisibility(metadata, listReportPage.entitySet, log)
: undefined;
const toolbarActions = getToolBarActionNames(listReportPage.model, log);
return {
name: listReportPage.name,
createButton: buildButtonState(buttonVisibility?.create),
deleteButton: buildButtonState(buttonVisibility?.delete),
filterBarItems: getFilterFieldNames(listReportPage.model, log),
tableColumns: (0, modelUtils_1.getTableColumnData)(listReportPage.model, log),
toolBarActions: metadata && listReportPage.entitySet
? safeCheckActionButtonStates(metadata, listReportPage.entitySet, toolbarActions, log)
: [],
isALP: manifest ? isALPFromManifest(manifest, listReportPage.name) : false
};
}
/**
* 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 = (0, modelUtils_1.getAggregations)(pageModel.root)['table'];
const tableAggregations = (0, modelUtils_1.getAggregations)(table);
const toolBar = tableAggregations['toolBar'];
const toolBarAggregations = (0, modelUtils_1.getAggregations)(toolBar);
const actions = toolBarAggregations['actions'];
const actionAggregations = (0, modelUtils_1.getAggregations)(actions);
return actionAggregations;
}
/**
* 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 {
const convertedMetadata = (0, annotation_converter_1.convert)((0, edmx_parser_1.parse)(metadataXml));
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: analyzeRestriction(insertRestrictions, 'Insertable'),
delete: analyzeRestriction(deleteRestrictions, 'Deletable')
};
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to analyze button visibility: ${errorMessage}`);
}
}
/**
* 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 = (0, modelUtils_1.getFilterFields)(pageModel);
filterBarItems = (0, modelUtils_1.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;
}
/**
* Analyzes a capability restriction annotation to determine button state.
*
* @param restriction The restriction annotation object (InsertRestrictions or DeleteRestrictions)
* @param propertyName The property name to check ('Insertable' or 'Deletable')
* @returns ButtonState for the button
*/
function analyzeRestriction(restriction, propertyName) {
const defaultState = { visible: true, enabled: true };
if (!restriction) {
return defaultState;
}
const value = restriction[propertyName];
if (value === undefined || value === null) {
return defaultState;
}
if (typeof value === 'boolean') {
return { visible: value, enabled: value };
}
if (typeof value === 'object' && value !== null) {
const path = value.$Path ?? value.path;
if (path) {
return { visible: true, enabled: 'dynamic', dynamicPath: path };
}
}
return defaultState;
}
/**
* 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 {
const convertedMetadata = (0, annotation_converter_1.convert)((0, edmx_parser_1.parse)(metadataXml));
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, entityType.name)
: extractAllActionStates(dataFieldForActions, convertedMetadata, entityType.name);
return { actions, entityType: entityType.name };
}
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
* @param entityTypeName The entity type name
* @returns List of action button states for the specified actions
*/
function findActionStates(dataFieldForActions, actionNames, metadata, entityTypeName) {
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, entityTypeName));
}
}
return actionStates;
}
/**
* Extracts action states for all DataFieldForAction items.
*
* @param dataFieldForActions List of DataFieldForAction items from UI.LineItem
* @param metadata The converted metadata
* @param entityTypeName The entity type name
* @returns List of all action button states
*/
function extractAllActionStates(dataFieldForActions, metadata, entityTypeName) {
return dataFieldForActions.map((item) => buildActionButtonState(item, metadata, entityTypeName));
}
/**
* Builds an ActionButtonState object from a DataFieldForAction item.
*
* @param item The DataFieldForAction item
* @param metadata The converted metadata
* @param entityTypeName The entity type name
* @returns ActionButtonState for the action
*/
function buildActionButtonState(item, metadata, entityTypeName) {
const actionMethod = extractActionMethodName(item.Action || '');
const operationAvailable = findOperationAvailableAnnotation(metadata, entityTypeName, 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 isEntityBound = item.ActionTarget?.isBound === true && item.ActionTarget?.parameters?.[0]?.isCollection !== true;
const { enabled, dynamicPath } = analyzeOperationAvailability(operationAvailable, isEntityBound);
return {
label: item.Label || '',
action: item.Action || '',
visible: true,
enabled,
dynamicPath,
invocationGrouping: item.InvocationGrouping ? extractEnumMemberValue(item.InvocationGrouping) : 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 path = operationAvailable.$Path ?? operationAvailable.path;
if (path) {
return { enabled: 'dynamic', dynamicPath: path };
}
}
return { enabled: true };
}
/**
* Extracts the action method name from a fully qualified action string.
*