@sap-ux/ui5-test-writer 0.7.102 → 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.
@@ -21,13 +13,22 @@ export declare function readManifest(fs: Editor, basePath: string): Manifest;
21
13
  * @param metadata - optional metadata for the OPA test generation
22
14
  * @param fs - an optional reference to a mem-fs editor
23
15
  * @param log - optional logger instance
16
+ * @param standalone - opa test generation run standalone, not during app generation
24
17
  * @returns Reference to a mem-fs-editor
25
18
  */
26
19
  export declare function generateOPAFiles(basePath: string, opaConfig: {
27
20
  scriptName?: string;
28
21
  appID?: string;
29
22
  htmlTarget?: string;
30
- }, metadata?: string, fs?: Editor, log?: Logger): Promise<Editor>;
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;
31
32
  /**
32
33
  * Generate a page object file for a Fiori elements for OData V4 application.
33
34
  * Note: this doesn't modify other existing files in the webapp/test folder.
@@ -42,5 +43,5 @@ export declare function generateOPAFiles(basePath: string, opaConfig: {
42
43
  export declare function generatePageObjectFile(basePath: string, pageObjectParameters: {
43
44
  targetKey: string;
44
45
  appID?: string;
45
- }, fs?: Editor): Editor;
46
+ }, fs?: Editor): Promise<Editor>;
46
47
  //# sourceMappingURL=fiori-elements-opa-writer.d.ts.map
@@ -1,15 +1,76 @@
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
+ const node_fs_1 = require("node:fs");
7
8
  const mem_fs_1 = require("mem-fs");
8
9
  const mem_fs_editor_1 = require("mem-fs-editor");
9
10
  const types_1 = require("./types");
10
11
  const i18n_1 = require("./i18n");
11
12
  const project_access_1 = require("@sap-ux/project-access");
12
13
  const modelUtils_1 = require("./utils/modelUtils");
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
+ }
13
74
  /**
14
75
  * Reads the manifest for an app.
15
76
  *
@@ -193,103 +254,138 @@ function findLROP(pages, manifest) {
193
254
  return { pageLR, pageOP };
194
255
  }
195
256
  /**
196
- * Writes a page object in a mem-fs-editor.
257
+ * Writes common test files, page objects, and the first journey file.
197
258
  *
198
- * @param pageConfig - the page configuration object
199
- * @param rootTemplateDirPath - template root directory
200
- * @param testOutDirPath - output test directory (.../webapp/test)
201
- * @param fs - a reference to a mem-fs editor
202
- */
203
- function writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, fs) {
204
- 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, {
205
- globOptions: { dot: true }
206
- });
207
- }
208
- /**
209
- * Generate OPA test files for a Fiori elements for OData V4 application.
210
- * Note: this can potentially overwrite existing files in the webapp/test folder.
211
- *
212
- * @param basePath - the absolute target path where the application will be generated
213
- * @param opaConfig - parameters for the generation
214
- * @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
215
- * @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
216
- * @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
217
- * @param metadata - optional metadata for the OPA test generation
218
- * @param fs - an optional reference to a mem-fs editor
219
- * @param log - optional logger instance
220
- * @returns Reference to a mem-fs-editor
259
+ * @param writeContext - shared write context (config, paths, editor, journey params)
260
+ * @param rootCommonTemplateDirPath - template root directory for common files
221
261
  */
222
- async function generateOPAFiles(basePath, opaConfig, metadata, fs, log) {
223
- const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
224
- const manifest = readManifest(editor, basePath);
225
- const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest);
226
- const config = createConfig(manifest, opaConfig, hideFilterBar);
227
- const rootCommonTemplateDirPath = (0, node_path_1.join)(__dirname, '../templates/common');
228
- const rootV4TemplateDirPath = (0, node_path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
229
- const testOutDirPath = (0, node_path_1.join)(basePath, 'webapp/test');
262
+ function writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath) {
263
+ const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
230
264
  // Common test files
231
265
  editor.copyTpl((0, node_path_1.join)(rootCommonTemplateDirPath), testOutDirPath,
232
266
  // unit tests are not added for Fiori elements app
233
267
  { appId: config.appID }, undefined, {
234
268
  globOptions: { dot: true }
235
269
  });
236
- // Pages files (one for each page in the app)
237
270
  config.pages.forEach((page) => {
238
271
  writePageObject(page, rootV4TemplateDirPath, testOutDirPath, editor);
239
272
  });
240
- // OPA Journey file
241
- const startPages = config.pages.filter((page) => page.isStartup).map((page) => page.targetKey);
242
- const LROP = findLROP(config.pages, manifest);
243
- // Access ux-specification to get feature data for OPA test generation
244
- const { listReport, objectPages, fpm } = await (0, modelUtils_1.getAppFeatures)(basePath, editor, log, metadata);
245
- const journeyParams = {
246
- startPages,
247
- startLR: LROP.pageLR?.targetKey,
248
- navigatedOP: LROP.pageOP?.targetKey,
249
- hideFilterBar: config.hideFilterBar
250
- };
251
- const generatedJourneyPages = [];
252
- editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration/FirstJourney.js'), (0, node_path_1.join)(testOutDirPath, `integration/${config.opaJourneyFileName}.js`), journeyParams, undefined, {
273
+ editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'FirstJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${config.opaJourneyFileName}.js`), journeyParams, undefined, {
274
+ globOptions: { dot: true }
275
+ });
276
+ // Journey Runner
277
+ editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'pages', 'JourneyRunner.js'), (0, node_path_1.join)(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'), config, undefined, {
253
278
  globOptions: { dot: true }
254
279
  });
255
- if (listReport) {
256
- editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration/ListReportJourney.js'), (0, node_path_1.join)(testOutDirPath, `integration/${listReport.name}Journey.js`), {
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
+ }
304
+ /**
305
+ * Writes journey files for list report, object pages and FPM pages.
306
+ *
307
+ * @param appFeatures - object containing feature data for list report, object pages, and FPM
308
+ * @param writeContext - shared write context (config, paths, editor, journey params)
309
+ * @param isStandalone - whether the generation is run in standalone mode (not during app generation)
310
+ * @param hasJourneyRunner - whether a JourneyRunner.js already exists (standalone upgrade path)
311
+ * @param virtualOPA5Configured - whether virtual OPA5 is configured
312
+ */
313
+ function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRunner = false, virtualOPA5Configured = false) {
314
+ const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
315
+ const generatedJourneyPages = [];
316
+ const newPages = [];
317
+ if (appFeatures.listReport?.name) {
318
+ editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'ListReportJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${appFeatures.listReport.name}Journey.js`), {
257
319
  ...journeyParams,
258
- ...listReport
320
+ ...appFeatures.listReport
259
321
  }, undefined, {
260
322
  globOptions: { dot: true }
261
323
  });
262
- generatedJourneyPages.push(listReport.name);
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
+ }
263
329
  }
264
- if (objectPages && objectPages.length > 0) {
265
- objectPages.forEach((objectPage) => {
266
- editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration/ObjectPageJourney.js'), (0, node_path_1.join)(testOutDirPath, `integration/${objectPage.name}Journey.js`), {
267
- ...journeyParams,
268
- ...objectPage
269
- }, undefined, {
270
- globOptions: { dot: true }
271
- });
272
- generatedJourneyPages.push(objectPage.name);
330
+ if (appFeatures.objectPages && appFeatures.objectPages.length > 0) {
331
+ appFeatures.objectPages.forEach((objectPage) => {
332
+ if (objectPage.name) {
333
+ editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'ObjectPageJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${objectPage.name}Journey.js`), {
334
+ ...journeyParams,
335
+ ...objectPage,
336
+ isStandalone
337
+ }, undefined, {
338
+ globOptions: { dot: true }
339
+ });
340
+ generatedJourneyPages.push(objectPage.name);
341
+ const opPage = ensurePageExists(objectPage.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
342
+ if (opPage) {
343
+ newPages.push(opPage);
344
+ }
345
+ }
273
346
  });
274
347
  }
275
- if (fpm) {
276
- editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration/FPMJourney.js'), (0, node_path_1.join)(testOutDirPath, `integration/${fpm.name}Journey.js`), {
348
+ if (appFeatures.fpm?.name) {
349
+ editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'FPMJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${appFeatures.fpm.name}Journey.js`), {
277
350
  ...journeyParams,
278
- ...fpm
351
+ ...appFeatures.fpm
279
352
  }, undefined, {
280
353
  globOptions: { dot: true }
281
354
  });
282
- generatedJourneyPages.push(fpm.name);
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
+ }
283
360
  }
284
- // Integration (OPA) test files - version-specific
285
- editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'opaTests.*.*'), (0, node_path_1.join)(testOutDirPath, 'integration'), { ...config, generatedJourneyPages }, undefined, {
286
- globOptions: { dot: true }
287
- });
288
- // Journey Runner
289
- editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'pages', 'JourneyRunner.js'), (0, node_path_1.join)(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'), config, undefined, {
361
+ if (newPages.length > 0) {
362
+ (0, opaQUnitUtils_1.addPagesToJourneyRunner)(newPages, testOutDirPath, editor);
363
+ }
364
+ if (!virtualOPA5Configured) {
365
+ if (hasJourneyRunner) {
366
+ (0, opaQUnitUtils_1.addPathsToQUnitJs)(generatedJourneyPages.map((page) => {
367
+ return `${config.appPath}/test/integration/${page}Journey`;
368
+ }), testOutDirPath, editor);
369
+ }
370
+ else {
371
+ editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'opaTests.*.*'), (0, node_path_1.join)(testOutDirPath, 'integration'), { ...config, generatedJourneyPages }, undefined, {
372
+ globOptions: { dot: true }
373
+ });
374
+ }
375
+ }
376
+ }
377
+ /**
378
+ * Writes a page object in a mem-fs-editor.
379
+ *
380
+ * @param pageConfig - the page configuration object
381
+ * @param rootTemplateDirPath - template root directory
382
+ * @param testOutDirPath - output test directory (.../webapp/test)
383
+ * @param fs - a reference to a mem-fs editor
384
+ */
385
+ function writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, fs) {
386
+ 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, {
290
387
  globOptions: { dot: true }
291
388
  });
292
- return editor;
293
389
  }
294
390
  /**
295
391
  * Generate a page object file for a Fiori elements for OData V4 application.
@@ -302,14 +398,14 @@ async function generateOPAFiles(basePath, opaConfig, metadata, fs, log) {
302
398
  * @param fs - an optional reference to a mem-fs editor
303
399
  * @returns Reference to a mem-fs-editor
304
400
  */
305
- function generatePageObjectFile(basePath, pageObjectParameters, fs) {
306
- const editor = fs || (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
401
+ async function generatePageObjectFile(basePath, pageObjectParameters, fs) {
402
+ const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
307
403
  const manifest = readManifest(editor, basePath);
308
404
  const { applicationType } = getAppTypeAndHideFilterBarFromManifest(manifest);
309
405
  const pageConfig = createPageConfig(manifest, pageObjectParameters.targetKey, pageObjectParameters.appID);
310
406
  if (pageConfig) {
311
407
  const rootTemplateDirPath = (0, node_path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
312
- const testOutDirPath = (0, node_path_1.join)(basePath, 'webapp/test');
408
+ const testOutDirPath = (0, node_path_1.join)(await (0, project_access_1.getWebappPath)(basePath), 'test');
313
409
  writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, editor);
314
410
  }
315
411
  else {
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { Editor } from 'mem-fs-editor';
1
2
  export declare const SupportedPageTypes: {
2
3
  [id: string]: string;
3
4
  };
@@ -20,6 +21,12 @@ export type FEV4OPAConfig = {
20
21
  hideFilterBar: boolean;
21
22
  filterBarItems?: string[];
22
23
  };
24
+ export type JourneyParams = {
25
+ startPages: string[];
26
+ startLR: string | undefined;
27
+ navigatedOP: string | undefined;
28
+ hideFilterBar: boolean;
29
+ };
23
30
  export type FEV4ManifestTarget = {
24
31
  type?: string;
25
32
  name?: string;
@@ -36,6 +43,13 @@ export type FEV4ManifestTarget = {
36
43
  };
37
44
  };
38
45
  };
46
+ views?: {
47
+ paths?: Array<{
48
+ primary?: unknown[];
49
+ secondary?: unknown[];
50
+ defaultPath?: string;
51
+ }>;
52
+ };
39
53
  };
40
54
  };
41
55
  };
@@ -103,6 +117,7 @@ export type ListReportFeatures = {
103
117
  filterBarItems?: string[];
104
118
  tableColumns?: Record<string, Record<string, string | number | boolean>>;
105
119
  toolBarActions?: ActionButtonState[];
120
+ isALP?: boolean;
106
121
  };
107
122
  export interface ActionButtonState {
108
123
  label: string;
@@ -135,6 +150,13 @@ export type AppFeatures = {
135
150
  objectPages?: ObjectPageFeatures[];
136
151
  fpm?: FPMFeatures;
137
152
  };
153
+ export type WriteContext = {
154
+ config: FEV4OPAConfig;
155
+ rootV4TemplateDirPath: string;
156
+ testOutDirPath: string;
157
+ editor: Editor;
158
+ journeyParams: JourneyParams;
159
+ };
138
160
  export type FormField = {
139
161
  fieldGroupQualifier?: string;
140
162
  field?: string;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Utility for reading the FLP sandbox HTML file and extracting the
3
+ * application hash (intent) from the sap-ushell-config applications object.
4
+ */
5
+ import type { Editor } from 'mem-fs-editor';
6
+ /**
7
+ * Reads an FLP sandbox HTML file and extracts the first application key
8
+ * from the `sap-ushell-config` `applications` object.
9
+ *
10
+ * @param htmlRelativePath - path to the HTML file relative to `webapp/`
11
+ * (e.g. `test/flpSandbox.html`)
12
+ * @param webappPath - path to the webapp directory
13
+ * @param fs - mem-fs-editor instance used to read the file
14
+ * @returns the application key (e.g. `fincashbankmanage-tile`), or undefined
15
+ */
16
+ export declare function readHashFromFlpSandbox(htmlRelativePath: string, webappPath: string, fs: Editor): string | undefined;
17
+ //# sourceMappingURL=flpSandboxUtils.d.ts.map
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * Utility for reading the FLP sandbox HTML file and extracting the
4
+ * application hash (intent) from the sap-ushell-config applications object.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.readHashFromFlpSandbox = readHashFromFlpSandbox;
8
+ const node_path_1 = require("node:path");
9
+ /**
10
+ * Regex to extract the first application key from the sap-ushell-config
11
+ * `applications` block. Matches patterns like:
12
+ *
13
+ * applications: {
14
+ * "fincashbankmanage-tile": {
15
+ *
16
+ * Captures the quoted key (e.g. `fincashbankmanage-tile`).
17
+ */
18
+ const APPLICATIONS_KEY_REGEX = /applications\s*:\s*\{[^"]*"([^"]+)"\s*:/;
19
+ /**
20
+ * Reads an FLP sandbox HTML file and extracts the first application key
21
+ * from the `sap-ushell-config` `applications` object.
22
+ *
23
+ * @param htmlRelativePath - path to the HTML file relative to `webapp/`
24
+ * (e.g. `test/flpSandbox.html`)
25
+ * @param webappPath - path to the webapp directory
26
+ * @param fs - mem-fs-editor instance used to read the file
27
+ * @returns the application key (e.g. `fincashbankmanage-tile`), or undefined
28
+ */
29
+ function readHashFromFlpSandbox(htmlRelativePath, webappPath, fs) {
30
+ try {
31
+ const filePath = (0, node_path_1.join)(webappPath, htmlRelativePath);
32
+ const content = fs.read(filePath);
33
+ const match = APPLICATIONS_KEY_REGEX.exec(content);
34
+ return match?.[1];
35
+ }
36
+ catch {
37
+ return undefined;
38
+ }
39
+ }
40
+ //# sourceMappingURL=flpSandboxUtils.js.map
@@ -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');
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Utility for reading and updating a generated opaTests.qunit.js file.
3
+ * The file is modified in-place: only the sap.ui.require array is changed;
4
+ * all other content (formatting, comments, whitespace) is preserved exactly.
5
+ */
6
+ import type { Editor } from 'mem-fs-editor';
7
+ /**
8
+ * Splices new module paths into the sap.ui.require array of the content string.
9
+ * Entries that are already present are skipped. All other content is preserved exactly.
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
+ *
14
+ * @param fileContent - the full content of the opaTests.qunit.js file
15
+ * @param moduleNames - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
16
+ * @returns the updated file content, or the original content unchanged if nothing was added
17
+ */
18
+ export declare function spliceModulesIntoQUnitContent(fileContent: string, moduleNames: string[]): string;
19
+ /**
20
+ * Reads opaTests.qunit.js from webapp/test/integration_old and extracts the html
21
+ * launch target (path, query parameters, and hash fragment) from the launchUrl
22
+ * line, e.g. `test/flpSandbox.html?sap-ui-xx-viewCache=false#myApp-tile`.
23
+ * Returns undefined if the file cannot be read or the pattern is not found.
24
+ *
25
+ * @param testPath - path to the test output directory (`.../webapp/test`)
26
+ * @param fs - mem-fs-editor instance used to read the file
27
+ * @returns the html target string, or undefined if not found
28
+ */
29
+ export declare function readHtmlTargetFromQUnitJs(testPath: string, fs: Editor): string | undefined;
30
+ /**
31
+ * Appends `/webapp/test/integration_old` to the project's `.gitignore`.
32
+ * Creates the file if it does not exist. Skips if the entry is already present.
33
+ *
34
+ * @param basePath - project root (contains .gitignore)
35
+ * @param fs - mem-fs-editor instance used to read and write the file
36
+ */
37
+ export declare function addIntegrationOldToGitignore(basePath: string, fs: Editor): Promise<void>;
38
+ /**
39
+ * Reads opaTests.qunit.js from the project, adds module paths to the
40
+ * sap.ui.require array, and writes the updated content back.
41
+ * Entries that are already present are skipped.
42
+ * All other file content is preserved exactly.
43
+ *
44
+ * @param filePaths - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
45
+ * @param projectPath - path to the test output directory (`.../webapp/test`)
46
+ * @param fs - mem-fs-editor instance used to read and write the file
47
+ */
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;
85
+ /**
86
+ * Returns true if any UI5 yaml file in the project contains a `fiori-tools-preview`
87
+ * middleware whose `test` array includes an entry with `framework: OPA5`.
88
+ *
89
+ * @param basePath - project root directory
90
+ * @returns true when OPA5 is configured in a preview middleware, false otherwise
91
+ */
92
+ export declare function hasVirtualOPA5(basePath: string): Promise<boolean>;
93
+ //# sourceMappingURL=opaQUnitUtils.d.ts.map
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ /**
3
+ * Utility for reading and updating a generated opaTests.qunit.js file.
4
+ * The file is modified in-place: only the sap.ui.require array is changed;
5
+ * all other content (formatting, comments, whitespace) is preserved exactly.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.spliceModulesIntoQUnitContent = spliceModulesIntoQUnitContent;
9
+ exports.readHtmlTargetFromQUnitJs = readHtmlTargetFromQUnitJs;
10
+ exports.addIntegrationOldToGitignore = addIntegrationOldToGitignore;
11
+ exports.addPathsToQUnitJs = addPathsToQUnitJs;
12
+ exports.splicePageIntoJourneyRunner = splicePageIntoJourneyRunner;
13
+ exports.addPagesToJourneyRunner = addPagesToJourneyRunner;
14
+ exports.hasVirtualOPA5 = hasVirtualOPA5;
15
+ const node_path_1 = require("node:path");
16
+ const flpSandboxUtils_1 = require("./flpSandboxUtils");
17
+ const project_access_1 = require("@sap-ux/project-access");
18
+ /** Relative path from the test output directory to opaTests.qunit.js */
19
+ const OPA_QUNIT_FILE = (0, node_path_1.join)('integration', 'opaTests.qunit.js');
20
+ /**
21
+ * The regex matches the opening bracket of the sap.ui.require array and
22
+ * captures everything up to (but not including) the closing bracket followed
23
+ * by `], function`. This lets us splice new entries in without disturbing
24
+ * any other part of the file.
25
+ *
26
+ * Matches:
27
+ * sap.ui.require(\n [\n "a",\n "b",\n ], function
28
+ * ^^^^^^^^^^^^^^^^^
29
+ * captured as group 1
30
+ *
31
+ * The `d` flag enables `match.indices` so we can read the capture group's
32
+ * exact start/end positions without fragile string searching.
33
+ */
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;
37
+ /**
38
+ * Splices new module paths into the sap.ui.require array of the content string.
39
+ * Entries that are already present are skipped. All other content is preserved exactly.
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
+ *
44
+ * @param fileContent - the full content of the opaTests.qunit.js file
45
+ * @param moduleNames - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
46
+ * @returns the updated file content, or the original content unchanged if nothing was added
47
+ */
48
+ function spliceModulesIntoQUnitContent(fileContent, moduleNames) {
49
+ if (fileContent.length > MAX_FILE_CONTENT_LENGTH) {
50
+ return fileContent;
51
+ }
52
+ const match = SAP_UI_REQUIRE_ARRAY_REGEX.exec(fileContent);
53
+ if (!match) {
54
+ return fileContent;
55
+ }
56
+ const arrayBody = match[1]; // everything between `[` and `]`
57
+ // Collect existing quoted entries so we don't add duplicates
58
+ const existingEntries = new Set();
59
+ const entryRegex = /"([^"]+)"/g;
60
+ let entryMatch;
61
+ while ((entryMatch = entryRegex.exec(arrayBody)) !== null) {
62
+ existingEntries.add(entryMatch[1]);
63
+ }
64
+ const toAdd = moduleNames.filter((name) => !existingEntries.has(name));
65
+ if (toAdd.length === 0) {
66
+ return fileContent;
67
+ }
68
+ // Detect the indentation used by the existing entries (e.g. four spaces)
69
+ const indentMatch = /^([ \t]+)"/m.exec(arrayBody);
70
+ const indent = indentMatch ? indentMatch[1] : ' ';
71
+ // Build the lines to insert, each terminated with a trailing comma
72
+ const newLines = toAdd.map((name) => `${indent}"${name}",`).join('\n');
73
+ // Insert just before the closing `]` using the capture group's end index.
74
+ const insertPosition = match.indices?.[1]?.[1];
75
+ if (insertPosition === undefined) {
76
+ return fileContent;
77
+ }
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);
83
+ const after = fileContent.slice(insertPosition);
84
+ return `${trimmedBefore}${commaFix}\n${newLines}\n${trailingWhitespace}${after}`;
85
+ }
86
+ /**
87
+ * Regex to extract the html launch target from a `launchUrl` line of the form:
88
+ * sap.ui.require.toUrl('...') + '/some/path.html?params#hash'
89
+ * Captures the path/query/hash portion after the closing `') + '`.
90
+ */
91
+ const LAUNCH_URL_REGEX = /\.toUrl\s*\([^)]+\)\s*\+\s*'([^']+)'/;
92
+ /**
93
+ * Reads opaTests.qunit.js from webapp/test/integration_old and extracts the html
94
+ * launch target (path, query parameters, and hash fragment) from the launchUrl
95
+ * line, e.g. `test/flpSandbox.html?sap-ui-xx-viewCache=false#myApp-tile`.
96
+ * Returns undefined if the file cannot be read or the pattern is not found.
97
+ *
98
+ * @param testPath - path to the test output directory (`.../webapp/test`)
99
+ * @param fs - mem-fs-editor instance used to read the file
100
+ * @returns the html target string, or undefined if not found
101
+ */
102
+ function readHtmlTargetFromQUnitJs(testPath, fs) {
103
+ try {
104
+ const integrationOldDir = (0, node_path_1.join)(testPath, 'integration_old');
105
+ let filePath = (0, node_path_1.join)(integrationOldDir, 'opaTests.qunit.js');
106
+ if (!fs.exists(filePath)) {
107
+ filePath = (0, node_path_1.join)(integrationOldDir, 'Opa.qunit.js');
108
+ }
109
+ const content = fs.read(filePath);
110
+ const match = LAUNCH_URL_REGEX.exec(content);
111
+ const launchUrl = match?.[1].replace(/^\//, '');
112
+ if (!launchUrl) {
113
+ return undefined;
114
+ }
115
+ // If the launch URL already contains a hash fragment, use it as-is
116
+ if (launchUrl.includes('#')) {
117
+ return launchUrl;
118
+ }
119
+ // No hash fragment — read the referenced HTML file to extract the
120
+ // application key from the sap-ushell-config applications object
121
+ const htmlPath = launchUrl.split('?')[0];
122
+ const hash = (0, flpSandboxUtils_1.readHashFromFlpSandbox)(htmlPath, (0, node_path_1.join)(testPath, '..'), fs);
123
+ return hash ? `${launchUrl}#${hash}` : launchUrl;
124
+ }
125
+ catch {
126
+ return undefined;
127
+ }
128
+ }
129
+ /** The gitignore entry added for the moved integration test folder */
130
+ const INTEGRATION_OLD_GITIGNORE_ENTRY = '/webapp/test/integration_old';
131
+ /**
132
+ * Appends `/webapp/test/integration_old` to the project's `.gitignore`.
133
+ * Creates the file if it does not exist. Skips if the entry is already present.
134
+ *
135
+ * @param basePath - project root (contains .gitignore)
136
+ * @param fs - mem-fs-editor instance used to read and write the file
137
+ */
138
+ async function addIntegrationOldToGitignore(basePath, fs) {
139
+ const filePath = (0, node_path_1.join)(basePath, '.gitignore');
140
+ const existing = fs.exists(filePath) ? fs.read(filePath) : '';
141
+ const lines = existing.split('\n');
142
+ if (lines.some((line) => line.trim() === INTEGRATION_OLD_GITIGNORE_ENTRY)) {
143
+ return;
144
+ }
145
+ const updated = existing.endsWith('\n') || existing === ''
146
+ ? `${existing}${INTEGRATION_OLD_GITIGNORE_ENTRY}\n`
147
+ : `${existing}\n${INTEGRATION_OLD_GITIGNORE_ENTRY}\n`;
148
+ fs.write(filePath, updated);
149
+ }
150
+ /**
151
+ * Reads opaTests.qunit.js from the project, adds module paths to the
152
+ * sap.ui.require array, and writes the updated content back.
153
+ * Entries that are already present are skipped.
154
+ * All other file content is preserved exactly.
155
+ *
156
+ * @param filePaths - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
157
+ * @param projectPath - path to the test output directory (`.../webapp/test`)
158
+ * @param fs - mem-fs-editor instance used to read and write the file
159
+ */
160
+ function addPathsToQUnitJs(filePaths, projectPath, fs) {
161
+ try {
162
+ const filePath = (0, node_path_1.join)(projectPath, OPA_QUNIT_FILE);
163
+ const content = fs.read(filePath);
164
+ const updated = spliceModulesIntoQUnitContent(content, filePaths);
165
+ if (updated !== content) {
166
+ fs.write(filePath, updated);
167
+ }
168
+ }
169
+ catch {
170
+ // If the file doesn't exist or can't be read, do nothing
171
+ }
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
+ }
279
+ /**
280
+ * Returns true if any UI5 yaml file in the project contains a `fiori-tools-preview`
281
+ * middleware whose `test` array includes an entry with `framework: OPA5`.
282
+ *
283
+ * @param basePath - project root directory
284
+ * @returns true when OPA5 is configured in a preview middleware, false otherwise
285
+ */
286
+ async function hasVirtualOPA5(basePath) {
287
+ const yamlFileNames = await (0, project_access_1.getAllUi5YamlFileNames)(basePath);
288
+ for (const fileName of yamlFileNames) {
289
+ try {
290
+ const ui5Config = await (0, project_access_1.readUi5Yaml)(basePath, fileName);
291
+ const previewMiddleware = ui5Config.findCustomMiddleware('fiori-tools-preview');
292
+ const testEntries = previewMiddleware?.configuration?.test;
293
+ if (testEntries?.some((entry) => entry.framework === 'OPA5')) {
294
+ return true;
295
+ }
296
+ }
297
+ catch {
298
+ // Skip yaml files that cannot be read
299
+ }
300
+ }
301
+ return false;
302
+ }
303
+ //# sourceMappingURL=opaQUnitUtils.js.map
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.102",
4
+ "version": "0.7.104",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -27,8 +27,8 @@
27
27
  "@sap/ux-specification": "1.144.0",
28
28
  "@sap-ux/edmx-parser": "0.10.0",
29
29
  "@sap-ux/annotation-converter": "0.10.21",
30
- "@sap-ux/project-access": "1.35.20",
31
30
  "@sap-ux/ui5-application-writer": "1.8.5",
31
+ "@sap-ux/project-access": "1.35.20",
32
32
  "@sap-ux/logger": "0.8.5"
33
33
  },
34
34
  "devDependencies": {
@@ -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,13 +60,15 @@ 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 (bodySections.length > 1) { -%>
69
70
  When.onThe<%- name%>.iPressSectionIconTabFilterButton("<%- section.id %>");
71
+ <% } -%>
70
72
  Then.onThe<%- name%>.iCheckSection({ section: "<%- section.id %>" });
71
73
  <% if (section?.subSections?.length > 0) { -%>
72
74
  <% section.subSections.forEach(function(subSection) { -%>