@sap-ux/ui5-test-writer 0.7.103 → 0.7.104

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,6 @@
1
1
  import type { Editor } from 'mem-fs-editor';
2
2
  import type { Manifest } from '@sap-ux/project-access';
3
3
  import type { Logger } from '@sap-ux/logger';
4
- /**
5
- * Reads the manifest for an app.
6
- *
7
- * @param fs - a reference to a mem-fs editor
8
- * @param basePath - the root folder of the app
9
- * @returns the manifest object. An exception is thrown if the manifest cannot be read.
10
- */
11
- export declare function readManifest(fs: Editor, basePath: string): Manifest;
12
4
  /**
13
5
  * Generate OPA test files for a Fiori elements for OData V4 application.
14
6
  * Note: this can potentially overwrite existing files in the webapp/test folder.
@@ -29,6 +21,14 @@ export declare function generateOPAFiles(basePath: string, opaConfig: {
29
21
  appID?: string;
30
22
  htmlTarget?: string;
31
23
  }, metadata?: string, fs?: Editor, log?: Logger, standalone?: boolean): Promise<Editor>;
24
+ /**
25
+ * Reads the manifest for an app.
26
+ *
27
+ * @param fs - a reference to a mem-fs editor
28
+ * @param basePath - the root folder of the app
29
+ * @returns the manifest object. An exception is thrown if the manifest cannot be read.
30
+ */
31
+ export declare function readManifest(fs: Editor, basePath: string): Manifest;
32
32
  /**
33
33
  * Generate a page object file for a Fiori elements for OData V4 application.
34
34
  * Note: this doesn't modify other existing files in the webapp/test folder.
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.readManifest = readManifest;
4
3
  exports.generateOPAFiles = generateOPAFiles;
4
+ exports.readManifest = readManifest;
5
5
  exports.generatePageObjectFile = generatePageObjectFile;
6
6
  const node_path_1 = require("node:path");
7
7
  const node_fs_1 = require("node:fs");
@@ -12,6 +12,65 @@ const i18n_1 = require("./i18n");
12
12
  const project_access_1 = require("@sap-ux/project-access");
13
13
  const modelUtils_1 = require("./utils/modelUtils");
14
14
  const opaQUnitUtils_1 = require("./utils/opaQUnitUtils");
15
+ /**
16
+ * Generate OPA test files for a Fiori elements for OData V4 application.
17
+ * Note: this can potentially overwrite existing files in the webapp/test folder.
18
+ *
19
+ * @param basePath - the absolute target path where the application will be generated
20
+ * @param opaConfig - parameters for the generation
21
+ * @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
22
+ * @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
23
+ * @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
24
+ * @param metadata - optional metadata for the OPA test generation
25
+ * @param fs - an optional reference to a mem-fs editor
26
+ * @param log - optional logger instance
27
+ * @param standalone - opa test generation run standalone, not during app generation
28
+ * @returns Reference to a mem-fs-editor
29
+ */
30
+ async function generateOPAFiles(basePath, opaConfig, metadata, fs, log, standalone = false) {
31
+ const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
32
+ const manifest = readManifest(editor, basePath);
33
+ const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest);
34
+ const config = createConfig(manifest, opaConfig, hideFilterBar);
35
+ const rootCommonTemplateDirPath = (0, node_path_1.join)(__dirname, '../templates/common');
36
+ const rootV4TemplateDirPath = (0, node_path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
37
+ const testOutDirPath = (0, node_path_1.join)(await (0, project_access_1.getWebappPath)(basePath), 'test');
38
+ // Access ux-specification to get feature data for OPA test generation
39
+ const appFeatures = await (0, modelUtils_1.getAppFeatures)(basePath, editor, log, metadata, manifest);
40
+ // OPA Journey file
41
+ const startPages = config.pages.filter((page) => page.isStartup).map((page) => page.targetKey);
42
+ const LROP = findLROP(config.pages, manifest);
43
+ const journeyParams = {
44
+ startPages,
45
+ startLR: LROP.pageLR?.targetKey,
46
+ navigatedOP: LROP.pageOP?.targetKey,
47
+ hideFilterBar: config.hideFilterBar
48
+ };
49
+ const writeContext = { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams };
50
+ if (standalone) {
51
+ const hasJourneyRunner = (0, node_fs_1.existsSync)((0, node_path_1.join)(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'));
52
+ const virtualOPA5Configured = await (0, opaQUnitUtils_1.hasVirtualOPA5)(basePath);
53
+ if (hasJourneyRunner) {
54
+ writeJourneyFiles(appFeatures, writeContext, true, true, virtualOPA5Configured);
55
+ }
56
+ else {
57
+ editor.move((0, node_path_1.join)(testOutDirPath, 'integration', '**'), (0, node_path_1.join)(testOutDirPath, 'integration_old'));
58
+ await (0, opaQUnitUtils_1.addIntegrationOldToGitignore)(basePath, editor);
59
+ const htmlTarget = (0, opaQUnitUtils_1.readHtmlTargetFromQUnitJs)(testOutDirPath, editor) ?? config.htmlTarget;
60
+ const standaloneConfig = { ...config, htmlTarget };
61
+ const standaloneWriteContext = { ...writeContext, config: standaloneConfig };
62
+ if (!virtualOPA5Configured) {
63
+ writeCommonAndPageFiles(standaloneWriteContext, rootCommonTemplateDirPath);
64
+ }
65
+ writeJourneyFiles(appFeatures, standaloneWriteContext, true, hasJourneyRunner, virtualOPA5Configured);
66
+ }
67
+ }
68
+ else {
69
+ writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath);
70
+ writeJourneyFiles(appFeatures, writeContext, false);
71
+ }
72
+ return editor;
73
+ }
15
74
  /**
16
75
  * Reads the manifest for an app.
17
76
  *
@@ -208,7 +267,6 @@ function writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath) {
208
267
  { appId: config.appID }, undefined, {
209
268
  globOptions: { dot: true }
210
269
  });
211
- // Pages files (one for each page in the app)
212
270
  config.pages.forEach((page) => {
213
271
  writePageObject(page, rootV4TemplateDirPath, testOutDirPath, editor);
214
272
  });
@@ -220,6 +278,29 @@ function writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath) {
220
278
  globOptions: { dot: true }
221
279
  });
222
280
  }
281
+ /**
282
+ * Checks whether a page object file already exists for the given feature name.
283
+ * If it doesn't exist, finds the matching page config and writes the file.
284
+ *
285
+ * @param featureName - the feature/page name (equals the manifest targetKey)
286
+ * @param config - the OPA config containing all page configurations
287
+ * @param rootV4TemplateDirPath - template root directory for v4 templates
288
+ * @param testOutDirPath - output test directory (.../webapp/test)
289
+ * @param editor - a reference to a mem-fs editor
290
+ * @returns JourneyRunnerPage if the page was newly created, undefined otherwise
291
+ */
292
+ function ensurePageExists(featureName, config, rootV4TemplateDirPath, testOutDirPath, editor) {
293
+ const pageFilePath = (0, node_path_1.join)(testOutDirPath, 'integration', 'pages', `${featureName}.js`);
294
+ if (editor.exists(pageFilePath)) {
295
+ return undefined;
296
+ }
297
+ const pageConfig = config.pages.find((p) => p.targetKey === featureName);
298
+ if (pageConfig) {
299
+ writePageObject(pageConfig, rootV4TemplateDirPath, testOutDirPath, editor);
300
+ return { targetKey: featureName, appPath: config.appPath };
301
+ }
302
+ return undefined;
303
+ }
223
304
  /**
224
305
  * Writes journey files for list report, object pages and FPM pages.
225
306
  *
@@ -232,6 +313,7 @@ function writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath) {
232
313
  function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRunner = false, virtualOPA5Configured = false) {
233
314
  const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
234
315
  const generatedJourneyPages = [];
316
+ const newPages = [];
235
317
  if (appFeatures.listReport?.name) {
236
318
  editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'ListReportJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${appFeatures.listReport.name}Journey.js`), {
237
319
  ...journeyParams,
@@ -240,6 +322,10 @@ function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRu
240
322
  globOptions: { dot: true }
241
323
  });
242
324
  generatedJourneyPages.push(appFeatures.listReport.name);
325
+ const lrPage = ensurePageExists(appFeatures.listReport.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
326
+ if (lrPage) {
327
+ newPages.push(lrPage);
328
+ }
243
329
  }
244
330
  if (appFeatures.objectPages && appFeatures.objectPages.length > 0) {
245
331
  appFeatures.objectPages.forEach((objectPage) => {
@@ -252,6 +338,10 @@ function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRu
252
338
  globOptions: { dot: true }
253
339
  });
254
340
  generatedJourneyPages.push(objectPage.name);
341
+ const opPage = ensurePageExists(objectPage.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
342
+ if (opPage) {
343
+ newPages.push(opPage);
344
+ }
255
345
  }
256
346
  });
257
347
  }
@@ -263,6 +353,13 @@ function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRu
263
353
  globOptions: { dot: true }
264
354
  });
265
355
  generatedJourneyPages.push(appFeatures.fpm.name);
356
+ const fpmPage = ensurePageExists(appFeatures.fpm.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
357
+ if (fpmPage) {
358
+ newPages.push(fpmPage);
359
+ }
360
+ }
361
+ if (newPages.length > 0) {
362
+ (0, opaQUnitUtils_1.addPagesToJourneyRunner)(newPages, testOutDirPath, editor);
266
363
  }
267
364
  if (!virtualOPA5Configured) {
268
365
  if (hasJourneyRunner) {
@@ -290,65 +387,6 @@ function writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, fs) {
290
387
  globOptions: { dot: true }
291
388
  });
292
389
  }
293
- /**
294
- * Generate OPA test files for a Fiori elements for OData V4 application.
295
- * Note: this can potentially overwrite existing files in the webapp/test folder.
296
- *
297
- * @param basePath - the absolute target path where the application will be generated
298
- * @param opaConfig - parameters for the generation
299
- * @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
300
- * @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
301
- * @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
302
- * @param metadata - optional metadata for the OPA test generation
303
- * @param fs - an optional reference to a mem-fs editor
304
- * @param log - optional logger instance
305
- * @param standalone - opa test generation run standalone, not during app generation
306
- * @returns Reference to a mem-fs-editor
307
- */
308
- async function generateOPAFiles(basePath, opaConfig, metadata, fs, log, standalone = false) {
309
- const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
310
- const manifest = readManifest(editor, basePath);
311
- const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest);
312
- const config = createConfig(manifest, opaConfig, hideFilterBar);
313
- const rootCommonTemplateDirPath = (0, node_path_1.join)(__dirname, '../templates/common');
314
- const rootV4TemplateDirPath = (0, node_path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
315
- const testOutDirPath = (0, node_path_1.join)(await (0, project_access_1.getWebappPath)(basePath), 'test');
316
- // Access ux-specification to get feature data for OPA test generation
317
- const appFeatures = await (0, modelUtils_1.getAppFeatures)(basePath, editor, log, metadata);
318
- // OPA Journey file
319
- const startPages = config.pages.filter((page) => page.isStartup).map((page) => page.targetKey);
320
- const LROP = findLROP(config.pages, manifest);
321
- const journeyParams = {
322
- startPages,
323
- startLR: LROP.pageLR?.targetKey,
324
- navigatedOP: LROP.pageOP?.targetKey,
325
- hideFilterBar: config.hideFilterBar
326
- };
327
- const writeContext = { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams };
328
- if (standalone) {
329
- const hasJourneyRunner = (0, node_fs_1.existsSync)((0, node_path_1.join)(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'));
330
- const virtualOPA5Configured = await (0, opaQUnitUtils_1.hasVirtualOPA5)(basePath);
331
- if (hasJourneyRunner) {
332
- writeJourneyFiles(appFeatures, writeContext, true, true, virtualOPA5Configured);
333
- }
334
- else {
335
- editor.move((0, node_path_1.join)(testOutDirPath, 'integration', '**'), (0, node_path_1.join)(testOutDirPath, 'integration_old'));
336
- await (0, opaQUnitUtils_1.addIntegrationOldToGitignore)(basePath, editor);
337
- const htmlTarget = (0, opaQUnitUtils_1.readHtmlTargetFromQUnitJs)(testOutDirPath, editor) ?? config.htmlTarget;
338
- const standaloneConfig = { ...config, htmlTarget };
339
- const standaloneWriteContext = { ...writeContext, config: standaloneConfig };
340
- if (!virtualOPA5Configured) {
341
- writeCommonAndPageFiles(standaloneWriteContext, rootCommonTemplateDirPath);
342
- }
343
- writeJourneyFiles(appFeatures, standaloneWriteContext, true, true, virtualOPA5Configured);
344
- }
345
- }
346
- else {
347
- writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath);
348
- writeJourneyFiles(appFeatures, writeContext, false);
349
- }
350
- return editor;
351
- }
352
390
  /**
353
391
  * Generate a page object file for a Fiori elements for OData V4 application.
354
392
  * Note: this doesn't modify other existing files in the webapp/test folder.
@@ -361,7 +399,7 @@ async function generateOPAFiles(basePath, opaConfig, metadata, fs, log, standalo
361
399
  * @returns Reference to a mem-fs-editor
362
400
  */
363
401
  async function generatePageObjectFile(basePath, pageObjectParameters, fs) {
364
- const editor = fs || (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
402
+ const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
365
403
  const manifest = readManifest(editor, basePath);
366
404
  const { applicationType } = getAppTypeAndHideFilterBarFromManifest(manifest);
367
405
  const pageConfig = createPageConfig(manifest, pageObjectParameters.targetKey, pageObjectParameters.appID);
package/dist/types.d.ts CHANGED
@@ -43,6 +43,13 @@ export type FEV4ManifestTarget = {
43
43
  };
44
44
  };
45
45
  };
46
+ views?: {
47
+ paths?: Array<{
48
+ primary?: unknown[];
49
+ secondary?: unknown[];
50
+ defaultPath?: string;
51
+ }>;
52
+ };
46
53
  };
47
54
  };
48
55
  };
@@ -110,6 +117,7 @@ export type ListReportFeatures = {
110
117
  filterBarItems?: string[];
111
118
  tableColumns?: Record<string, Record<string, string | number | boolean>>;
112
119
  toolBarActions?: ActionButtonState[];
120
+ isALP?: boolean;
113
121
  };
114
122
  export interface ActionButtonState {
115
123
  label: string;
@@ -1,7 +1,8 @@
1
1
  import type { Logger } from '@sap-ux/logger';
2
2
  import type { TreeAggregations, TreeModel } from '@sap/ux-specification/dist/types/src/parser';
3
- import type { ActionButtonsResult, ActionButtonState, ButtonState, ButtonVisibilityResult, ListReportFeatures } from '../types';
3
+ import type { ActionButtonsResult, ActionButtonState, ButtonState, ButtonVisibilityResult, FEV4ManifestTarget, ListReportFeatures } from '../types';
4
4
  import type { PageWithModelV4 } from '@sap/ux-specification/dist/types/src/parser/application';
5
+ import type { Manifest } from '@sap-ux/project-access';
5
6
  /**
6
7
  * Builds a button state object from button visibility result.
7
8
  *
@@ -32,15 +33,33 @@ export declare function safeCheckButtonVisibility(metadata: string, entitySetNam
32
33
  * @returns Array of action button states or empty array if error occurs
33
34
  */
34
35
  export declare function safeCheckActionButtonStates(metadata: string, entitySetName: string, actionNames: string[], log?: Logger): ActionButtonState[];
36
+ /**
37
+ * Returns true when a ListReport manifest target is configured as an Analytical List Page.
38
+ * ALP targets have a `views.paths` array where at least one entry contains a `primary` array,
39
+ * indicating the dual-view (chart + table) layout used by ALP.
40
+ *
41
+ * @param target - the manifest routing target to inspect
42
+ * @returns true if the target represents an ALP configuration
43
+ */
44
+ export declare function isALPManifestTarget(target: FEV4ManifestTarget): boolean;
45
+ /**
46
+ * Returns true if any ListReport target in the manifest is configured as an Analytical List Page.
47
+ *
48
+ * @param manifest - the application manifest
49
+ * @param targetKey - optional specific target key to check; if omitted all ListReport targets are checked
50
+ * @returns true if the target (or any ListReport target) is an ALP
51
+ */
52
+ export declare function isALPFromManifest(manifest: Manifest, targetKey?: string): boolean;
35
53
  /**
36
54
  * Gets List Report features from the page model using ux-specification.
37
55
  *
38
56
  * @param listReportPage - the List Report page containing the tree model with feature definitions
39
57
  * @param log - optional logger instance
40
58
  * @param metadata - optional metadata for the OPA test generation
59
+ * @param manifest - optional application manifest, used to detect ALP configuration
41
60
  * @returns feature data extracted from the List Report page model
42
61
  */
43
- export declare function getListReportFeatures(listReportPage: PageWithModelV4, log?: Logger, metadata?: string): ListReportFeatures;
62
+ export declare function getListReportFeatures(listReportPage: PageWithModelV4, log?: Logger, metadata?: string, manifest?: Manifest): ListReportFeatures;
44
63
  /**
45
64
  * Retrieves toolbar action definitions from the given tree model.
46
65
  *
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.buildButtonState = buildButtonState;
4
4
  exports.safeCheckButtonVisibility = safeCheckButtonVisibility;
5
5
  exports.safeCheckActionButtonStates = safeCheckActionButtonStates;
6
+ exports.isALPManifestTarget = isALPManifestTarget;
7
+ exports.isALPFromManifest = isALPFromManifest;
6
8
  exports.getListReportFeatures = getListReportFeatures;
7
9
  exports.getToolBarActions = getToolBarActions;
8
10
  exports.checkButtonVisibility = checkButtonVisibility;
@@ -61,15 +63,45 @@ function safeCheckActionButtonStates(metadata, entitySetName, actionNames, log)
61
63
  return [];
62
64
  }
63
65
  }
66
+ /**
67
+ * Returns true when a ListReport manifest target is configured as an Analytical List Page.
68
+ * ALP targets have a `views.paths` array where at least one entry contains a `primary` array,
69
+ * indicating the dual-view (chart + table) layout used by ALP.
70
+ *
71
+ * @param target - the manifest routing target to inspect
72
+ * @returns true if the target represents an ALP configuration
73
+ */
74
+ function isALPManifestTarget(target) {
75
+ return (target.options?.settings?.views?.paths?.some((path) => Array.isArray(path.primary) && path.primary.length > 0) ?? false);
76
+ }
77
+ /**
78
+ * Returns true if any ListReport target in the manifest is configured as an Analytical List Page.
79
+ *
80
+ * @param manifest - the application manifest
81
+ * @param targetKey - optional specific target key to check; if omitted all ListReport targets are checked
82
+ * @returns true if the target (or any ListReport target) is an ALP
83
+ */
84
+ function isALPFromManifest(manifest, targetKey) {
85
+ const targets = manifest['sap.ui5']?.routing?.targets;
86
+ if (!targets) {
87
+ return false;
88
+ }
89
+ const keysToCheck = targetKey ? [targetKey] : Object.keys(targets);
90
+ return keysToCheck.some((key) => {
91
+ const target = targets[key];
92
+ return target?.name === 'sap.fe.templates.ListReport' && isALPManifestTarget(target);
93
+ });
94
+ }
64
95
  /**
65
96
  * Gets List Report features from the page model using ux-specification.
66
97
  *
67
98
  * @param listReportPage - the List Report page containing the tree model with feature definitions
68
99
  * @param log - optional logger instance
69
100
  * @param metadata - optional metadata for the OPA test generation
101
+ * @param manifest - optional application manifest, used to detect ALP configuration
70
102
  * @returns feature data extracted from the List Report page model
71
103
  */
72
- function getListReportFeatures(listReportPage, log, metadata) {
104
+ function getListReportFeatures(listReportPage, log, metadata, manifest) {
73
105
  const buttonVisibility = metadata && listReportPage.entitySet
74
106
  ? safeCheckButtonVisibility(metadata, listReportPage.entitySet, log)
75
107
  : undefined;
@@ -82,7 +114,8 @@ function getListReportFeatures(listReportPage, log, metadata) {
82
114
  tableColumns: (0, modelUtils_1.getTableColumnData)(listReportPage.model, log),
83
115
  toolBarActions: metadata && listReportPage.entitySet
84
116
  ? safeCheckActionButtonStates(metadata, listReportPage.entitySet, toolbarActions, log)
85
- : []
117
+ : [],
118
+ isALP: manifest ? isALPFromManifest(manifest, listReportPage.name) : false
86
119
  };
87
120
  }
88
121
  /**
@@ -1,4 +1,5 @@
1
1
  import type { Editor } from 'mem-fs-editor';
2
+ import type { Manifest } from '@sap-ux/project-access';
2
3
  import type { Logger } from '@sap-ux/logger';
3
4
  import type { PageWithModelV4 } from '@sap/ux-specification/dist/types/src/parser/application';
4
5
  import type { TreeAggregation, TreeAggregations, TreeModel, ApplicationModel } from '@sap/ux-specification/dist/types/src/parser';
@@ -49,9 +50,10 @@ export interface PageWithModelV4WithProperties extends PageWithModelV4 {
49
50
  * @param fs - optional mem-fs editor instance
50
51
  * @param log - optional logger instance
51
52
  * @param metadata - optional metadata for the OPA test generation
53
+ * @param manifest - optional application manifest, used to detect ALP configuration
52
54
  * @returns feature data extracted from the application model
53
55
  */
54
- export declare function getAppFeatures(basePath: string, fs?: Editor, log?: Logger, metadata?: string): Promise<AppFeatures>;
56
+ export declare function getAppFeatures(basePath: string, fs?: Editor, log?: Logger, metadata?: string, manifest?: Manifest): Promise<AppFeatures>;
55
57
  /**
56
58
  * Retrieves table column data from the page model using ux-specification.
57
59
  *
@@ -20,9 +20,10 @@ const listReportUtils_1 = require("./listReportUtils");
20
20
  * @param fs - optional mem-fs editor instance
21
21
  * @param log - optional logger instance
22
22
  * @param metadata - optional metadata for the OPA test generation
23
+ * @param manifest - optional application manifest, used to detect ALP configuration
23
24
  * @returns feature data extracted from the application model
24
25
  */
25
- async function getAppFeatures(basePath, fs, log, metadata) {
26
+ async function getAppFeatures(basePath, fs, log, metadata, manifest) {
26
27
  const featureData = {};
27
28
  let listReportPage = null;
28
29
  let objectPages = null;
@@ -57,7 +58,7 @@ async function getAppFeatures(basePath, fs, log, metadata) {
57
58
  // attempt to get individual feature data
58
59
  try {
59
60
  if (listReportPage) {
60
- featureData.listReport = (0, listReportUtils_1.getListReportFeatures)(listReportPage, log, projectMetadata);
61
+ featureData.listReport = (0, listReportUtils_1.getListReportFeatures)(listReportPage, log, projectMetadata, manifest);
61
62
  }
62
63
  if (objectPages) {
63
64
  log?.warn('Extracting Object Page features from application model');
@@ -8,6 +8,9 @@ import type { Editor } from 'mem-fs-editor';
8
8
  * Splices new module paths into the sap.ui.require array of the content string.
9
9
  * Entries that are already present are skipped. All other content is preserved exactly.
10
10
  *
11
+ * Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
12
+ * ReDoS on crafted inputs. Valid generated files are well within this limit.
13
+ *
11
14
  * @param fileContent - the full content of the opaTests.qunit.js file
12
15
  * @param moduleNames - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
13
16
  * @returns the updated file content, or the original content unchanged if nothing was added
@@ -43,6 +46,42 @@ export declare function addIntegrationOldToGitignore(basePath: string, fs: Edito
43
46
  * @param fs - mem-fs-editor instance used to read and write the file
44
47
  */
45
48
  export declare function addPathsToQUnitJs(filePaths: string[], projectPath: string, fs: Editor): void;
49
+ /**
50
+ * Page entry to splice into an existing JourneyRunner.js.
51
+ */
52
+ export interface JourneyRunnerPage {
53
+ /** The page's targetKey, used as both the variable name and `onThe<targetKey>` key */
54
+ targetKey: string;
55
+ /** The app module path prefix (e.g. "project1/test/integration/pages") */
56
+ appPath: string;
57
+ }
58
+ /**
59
+ * Splices new page entries into the three locations of an existing JourneyRunner.js:
60
+ * - the sap.ui.define dependency array
61
+ * - the function parameter list
62
+ * - the pages object literal
63
+ *
64
+ * Pages already present (detected by their module path in the define array) are skipped.
65
+ * All other content — formatting, comments, whitespace — is preserved exactly.
66
+ *
67
+ * Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
68
+ * ReDoS on crafted inputs. Valid generated files are well within this limit.
69
+ *
70
+ * @param fileContent - the full content of the JourneyRunner.js file
71
+ * @param pages - pages to add
72
+ * @returns the updated file content, or the original content unchanged if nothing was added
73
+ */
74
+ export declare function splicePageIntoJourneyRunner(fileContent: string, pages: JourneyRunnerPage[]): string;
75
+ /**
76
+ * Reads JourneyRunner.js from the project, adds new page entries to all three
77
+ * locations (define array, function params, pages object), and writes the updated
78
+ * content back. Pages already present are skipped.
79
+ *
80
+ * @param pages - pages to add
81
+ * @param testOutDirPath - path to the test output directory (`.../webapp/test`)
82
+ * @param fs - mem-fs-editor instance used to read and write the file
83
+ */
84
+ export declare function addPagesToJourneyRunner(pages: JourneyRunnerPage[], testOutDirPath: string, fs: Editor): void;
46
85
  /**
47
86
  * Returns true if any UI5 yaml file in the project contains a `fiori-tools-preview`
48
87
  * middleware whose `test` array includes an entry with `framework: OPA5`.
@@ -9,6 +9,8 @@ exports.spliceModulesIntoQUnitContent = spliceModulesIntoQUnitContent;
9
9
  exports.readHtmlTargetFromQUnitJs = readHtmlTargetFromQUnitJs;
10
10
  exports.addIntegrationOldToGitignore = addIntegrationOldToGitignore;
11
11
  exports.addPathsToQUnitJs = addPathsToQUnitJs;
12
+ exports.splicePageIntoJourneyRunner = splicePageIntoJourneyRunner;
13
+ exports.addPagesToJourneyRunner = addPagesToJourneyRunner;
12
14
  exports.hasVirtualOPA5 = hasVirtualOPA5;
13
15
  const node_path_1 = require("node:path");
14
16
  const flpSandboxUtils_1 = require("./flpSandboxUtils");
@@ -29,16 +31,24 @@ const OPA_QUNIT_FILE = (0, node_path_1.join)('integration', 'opaTests.qunit.js')
29
31
  * The `d` flag enables `match.indices` so we can read the capture group's
30
32
  * exact start/end positions without fragile string searching.
31
33
  */
32
- const SAP_UI_REQUIRE_ARRAY_REGEX = /sap\.ui\.require\s*\(\s*\[([\s\S]*?)\]\s*,\s*function/d;
34
+ const SAP_UI_REQUIRE_ARRAY_REGEX = /sap\.ui\.require\s*\(\s*\[([^\]]*)\]\s*,\s*function/d;
35
+ /** ReDoS mitigation: files larger than this are returned unchanged rather than matched with regex. */
36
+ const MAX_FILE_CONTENT_LENGTH = 10000;
33
37
  /**
34
38
  * Splices new module paths into the sap.ui.require array of the content string.
35
39
  * Entries that are already present are skipped. All other content is preserved exactly.
36
40
  *
41
+ * Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
42
+ * ReDoS on crafted inputs. Valid generated files are well within this limit.
43
+ *
37
44
  * @param fileContent - the full content of the opaTests.qunit.js file
38
45
  * @param moduleNames - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
39
46
  * @returns the updated file content, or the original content unchanged if nothing was added
40
47
  */
41
48
  function spliceModulesIntoQUnitContent(fileContent, moduleNames) {
49
+ if (fileContent.length > MAX_FILE_CONTENT_LENGTH) {
50
+ return fileContent;
51
+ }
42
52
  const match = SAP_UI_REQUIRE_ARRAY_REGEX.exec(fileContent);
43
53
  if (!match) {
44
54
  return fileContent;
@@ -65,10 +75,13 @@ function spliceModulesIntoQUnitContent(fileContent, moduleNames) {
65
75
  if (insertPosition === undefined) {
66
76
  return fileContent;
67
77
  }
68
- const before = fileContent.slice(0, insertPosition);
78
+ // Ensure the last existing entry ends with a comma before inserting after it.
79
+ const trimmedBefore = fileContent.slice(0, insertPosition).trimEnd();
80
+ const needsComma = !trimmedBefore.endsWith(',');
81
+ const commaFix = needsComma ? ',' : '';
82
+ const trailingWhitespace = fileContent.slice(trimmedBefore.length, insertPosition);
69
83
  const after = fileContent.slice(insertPosition);
70
- const leadingNewline = arrayBody.endsWith('\n') ? '' : '\n';
71
- return `${before}${leadingNewline}${newLines}\n${after}`;
84
+ return `${trimmedBefore}${commaFix}\n${newLines}\n${trailingWhitespace}${after}`;
72
85
  }
73
86
  /**
74
87
  * Regex to extract the html launch target from a `launchUrl` line of the form:
@@ -157,6 +170,112 @@ function addPathsToQUnitJs(filePaths, projectPath, fs) {
157
170
  // If the file doesn't exist or can't be read, do nothing
158
171
  }
159
172
  }
173
+ /** Relative path from the test output directory to JourneyRunner.js */
174
+ const JOURNEY_RUNNER_FILE = (0, node_path_1.join)('integration', 'pages', 'JourneyRunner.js');
175
+ /**
176
+ * Splices new page entries into the three locations of an existing JourneyRunner.js:
177
+ * - the sap.ui.define dependency array
178
+ * - the function parameter list
179
+ * - the pages object literal
180
+ *
181
+ * Pages already present (detected by their module path in the define array) are skipped.
182
+ * All other content — formatting, comments, whitespace — is preserved exactly.
183
+ *
184
+ * Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
185
+ * ReDoS on crafted inputs. Valid generated files are well within this limit.
186
+ *
187
+ * @param fileContent - the full content of the JourneyRunner.js file
188
+ * @param pages - pages to add
189
+ * @returns the updated file content, or the original content unchanged if nothing was added
190
+ */
191
+ function splicePageIntoJourneyRunner(fileContent, pages) {
192
+ if (fileContent.length > MAX_FILE_CONTENT_LENGTH) {
193
+ return fileContent;
194
+ }
195
+ // Determine which pages are not yet present by checking the define array
196
+ const toAdd = pages.filter((page) => {
197
+ const modulePath = `${page.appPath}/test/integration/pages/${page.targetKey}`;
198
+ return !fileContent.includes(`"${modulePath}"`);
199
+ });
200
+ if (toAdd.length === 0) {
201
+ return fileContent;
202
+ }
203
+ let result = fileContent;
204
+ // 1. Splice into the sap.ui.define([...]) array.
205
+ // Captures everything between the opening `[` and the closing `]` before `, function`.
206
+ const defineArrayRegex = /sap\.ui\.define\s*\(\s*\[([^\]]*)\]\s*,\s*function/d;
207
+ const defineMatch = defineArrayRegex.exec(result);
208
+ if (defineMatch?.indices?.[1]) {
209
+ const [bodyStart, bodyEnd] = defineMatch.indices[1];
210
+ const arrayBody = result.slice(bodyStart, bodyEnd);
211
+ // Detect indentation from the first existing module entry line
212
+ const indentMatch = /^([ \t]+)"/m.exec(arrayBody);
213
+ const indent = indentMatch ? indentMatch[1] : '\t';
214
+ const newEntries = toAdd
215
+ .map((page) => `${indent}"${page.appPath}/test/integration/pages/${page.targetKey}",`)
216
+ .join('\n');
217
+ // Ensure the last existing entry ends with a comma before we insert after it.
218
+ // The trimmed body ends at bodyEnd; look back from there for the last non-whitespace char.
219
+ const trimmedEnd = result.slice(0, bodyEnd).trimEnd();
220
+ const needsComma = !trimmedEnd.endsWith(',');
221
+ const commaFix = needsComma ? ',' : '';
222
+ const trailingWhitespace = result.slice(trimmedEnd.length, bodyEnd);
223
+ result = `${trimmedEnd}${commaFix}\n${newEntries}` + `${trailingWhitespace}${result.slice(bodyEnd)}`;
224
+ }
225
+ // 2. Splice into the function parameter list: `function (JourneyRunner, A, B)`.
226
+ // Captures everything between `(` and `)` of the function signature.
227
+ const funcParamRegex = /\]\s*,\s*function\s*\(([^)]*)\)\s*\{/d;
228
+ const funcMatch = funcParamRegex.exec(result);
229
+ if (funcMatch?.indices?.[1]) {
230
+ const [, paramEnd] = funcMatch.indices[1];
231
+ const newParams = toAdd.map((page) => `, ${page.targetKey}`).join('');
232
+ result = `${result.slice(0, paramEnd)}${newParams}${result.slice(paramEnd)}`;
233
+ }
234
+ // 3. Splice into the pages object: `pages: { onTheFoo: Foo, ... }`.
235
+ // Captures everything between `pages: {` and the closing `}`.
236
+ const pagesObjectRegex = /pages\s*:\s*\{([^}]*)\}/d;
237
+ const pagesMatch = pagesObjectRegex.exec(result);
238
+ if (pagesMatch?.indices?.[1]) {
239
+ const [, pagesBodyEnd] = pagesMatch.indices[1];
240
+ const pagesBody = result.slice(pagesMatch.indices[1][0], pagesBodyEnd);
241
+ // Detect indentation from the first existing page entry
242
+ const pageIndentMatch = /^([ \t]+)on/m.exec(pagesBody);
243
+ const pageIndent = pageIndentMatch ? pageIndentMatch[1] : '\t\t\t';
244
+ const newPageEntries = toAdd
245
+ .map((page) => `${pageIndent}onThe${page.targetKey}: ${page.targetKey},`)
246
+ .join('\n');
247
+ // Ensure the last existing entry ends with a comma before we insert after it.
248
+ const trimmedPagesEnd = result.slice(0, pagesBodyEnd).trimEnd();
249
+ const needsComma = !trimmedPagesEnd.endsWith(',');
250
+ const commaFix = needsComma ? ',' : '';
251
+ const trailingWhitespace = result.slice(trimmedPagesEnd.length, pagesBodyEnd);
252
+ result =
253
+ `${trimmedPagesEnd}${commaFix}\n${newPageEntries}` + `${trailingWhitespace}${result.slice(pagesBodyEnd)}`;
254
+ }
255
+ return result;
256
+ }
257
+ /**
258
+ * Reads JourneyRunner.js from the project, adds new page entries to all three
259
+ * locations (define array, function params, pages object), and writes the updated
260
+ * content back. Pages already present are skipped.
261
+ *
262
+ * @param pages - pages to add
263
+ * @param testOutDirPath - path to the test output directory (`.../webapp/test`)
264
+ * @param fs - mem-fs-editor instance used to read and write the file
265
+ */
266
+ function addPagesToJourneyRunner(pages, testOutDirPath, fs) {
267
+ try {
268
+ const filePath = (0, node_path_1.join)(testOutDirPath, JOURNEY_RUNNER_FILE);
269
+ const content = fs.read(filePath);
270
+ const updated = splicePageIntoJourneyRunner(content, pages);
271
+ if (updated !== content) {
272
+ fs.write(filePath, updated);
273
+ }
274
+ }
275
+ catch {
276
+ // If the file doesn't exist or can't be read, do nothing
277
+ }
278
+ }
160
279
  /**
161
280
  * Returns true if any UI5 yaml file in the project contains a `fiori-tools-preview`
162
281
  * middleware whose `test` array includes an entry with `framework: OPA5`.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sap-ux/ui5-test-writer",
3
3
  "description": "SAP UI5 tests writer",
4
- "version": "0.7.103",
4
+ "version": "0.7.104",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -28,8 +28,8 @@
28
28
  "@sap-ux/edmx-parser": "0.10.0",
29
29
  "@sap-ux/annotation-converter": "0.10.21",
30
30
  "@sap-ux/ui5-application-writer": "1.8.5",
31
- "@sap-ux/logger": "0.8.5",
32
- "@sap-ux/project-access": "1.35.20"
31
+ "@sap-ux/project-access": "1.35.20",
32
+ "@sap-ux/logger": "0.8.5"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/ejs": "3.1.5",
@@ -41,7 +41,7 @@ sap.ui.define([
41
41
 
42
42
  opaTest("Check table columns and actions", function (Given, When, Then) {
43
43
  <%_ if (toolBarActions && toolBarActions.length > 0) { -%>
44
- <%_ if (createButton.visible) { _%>
44
+ <%_ if (createButton.visible && !isALP) { _%>
45
45
  Then.onThe<%- startLR%>.onTable().iCheckCreate({ visible: true });
46
46
  // Then.onthe<%- startLR%>.onTable().iPressCreate();
47
47
  <%_ } _%>
@@ -60,18 +60,16 @@ sap.ui.define([
60
60
  });
61
61
  <% } -%>
62
62
 
63
- <% if (bodySections?.length > 0) { -%>
63
+ <% if (bodySections?.length > 0 && !isStandalone) { -%>
64
64
  opaTest("Check body sections of the Object Page", function (Given, When, Then) {
65
65
  <% if (bodySections?.length > 1) { -%>
66
66
  Then.onThe<%- name%>.iCheckNumberOfSections(<%- bodySections.length %>);
67
67
  <% } -%>
68
68
  <% bodySections.forEach(function(section) { -%>
69
- <% if (!isStandalone) { -%>
70
69
  <% if (bodySections.length > 1) { -%>
71
70
  When.onThe<%- name%>.iPressSectionIconTabFilterButton("<%- section.id %>");
72
71
  <% } -%>
73
72
  Then.onThe<%- name%>.iCheckSection({ section: "<%- section.id %>" });
74
- <% } -%>
75
73
  <% if (section?.subSections?.length > 0) { -%>
76
74
  <% section.subSections.forEach(function(subSection) { -%>
77
75
  //When.onThe<%- name%>.iGoToSection({ section: "<%- section.id %>", subSection: "<%- subSection.id %>" });