UNPKG

@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
"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. *