@sap-ux/ui5-test-writer 1.0.14 → 1.1.1

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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # [`@sap-ux/ui5-test-writer`](https://github.com/SAP/open-ux-tools/tree/main/packages/ui5-test-writer)
4
4
 
5
- OPA files writer for use within Yeoman generator and other prompting libraries.
5
+ OPA files writer for use within Yeoman generator and other prompting libraries.
6
6
 
7
7
 
8
8
  ## Installation
@@ -17,7 +17,7 @@ Pnpm
17
17
 
18
18
  ## Usage
19
19
 
20
- 2 public methods are available: one to generate all OPA test files for a Fiori elements for OData V4 application, another one to generate an additional page object file for a Fiori elements for OData V4 application.
20
+ The `generateOPAFiles` function creates an OPA5 test suite for a Fiori elements for OData V4 application.
21
21
 
22
22
  ### Generate all OPA test files for a Fiori elements for OData V4 application
23
23
 
@@ -56,25 +56,25 @@ await exampleWriter();
56
56
 
57
57
  ```
58
58
 
59
- ### Generate an additional page object file
59
+ ### TypeScript output
60
+
61
+ Pass `enableTypeScript: true` in the options to generate the OPA test suite as TypeScript instead of JavaScript. The generated files use ES module syntax (`import` / `export default`) instead of AMD `sap.ui.define`, and include typed `Given` / `When` / `Then` parameters in journey functions.
60
62
 
61
- Calling the `generatePageObjectFile` function
62
63
  ```javascript
63
- import { generatePageObjectFile } from '@sap-ux/ui5-test-writer'
64
+ import { generateOPAFiles } from '@sap-ux/ui5-test-writer'
64
65
 
65
- const exampleWriter = async () => {
66
- const myProjectPath = 'path/to/my/project'; // Path to the root of the Fiori app
67
- const targetKey = 'MyNewPage'; // Key of the target in the app descriptor (in sap.ui5/routing/targets)
68
- const fs = await generatePageObjectFile(myProjectPath, { targetKey });
69
- return new Promise((resolve) => {
70
- fs.commit(resolve); // When using with Yeoman it handle the fs commit.
71
- });
72
- }
66
+ const fs = await generateOPAFiles(myProjectPath, { enableTypeScript: true });
67
+ ```
73
68
 
74
- // Calling the function
75
- await exampleWriter();
69
+ What changes in the generated output:
76
70
 
77
- ```
71
+ - Page objects, journeys, and `JourneyRunner` are emitted as `.ts` files.
72
+ - A `webapp/test/integration/types/OpaJourneyTypes.d.ts` file is generated with `Given` / `When` / `Then` definitions tailored to the app's pages.
73
+ - The QUnit bootstrap (`opaTests.qunit.js`) and the HTML harness stay as `.js` / `.html` (they are loaded directly by the browser).
74
+
75
+ When run in standalone mode against an existing project, `enableTypeScript` is auto-detected from the presence of a `tsconfig.json`. An explicit value passed in the options always takes precedence.
76
+
77
+ Scope: TypeScript output is currently supported for List Report and Object Page templates.
78
78
 
79
79
  ## Keywords
80
80
  SAP Fiori Elements
@@ -1,28 +1,20 @@
1
1
  import type { Editor } from 'mem-fs-editor';
2
2
  import type { Manifest } from '@sap-ux/project-access';
3
+ import type { OPAGenerationOptions } from './types.js';
3
4
  import type { Logger } from '@sap-ux/logger';
4
5
  /**
5
6
  * Generate OPA test files for a Fiori elements for OData V4 application.
6
7
  * Note: this can potentially overwrite existing files in the webapp/test folder.
7
8
  *
8
9
  * @param basePath - the absolute target path where the application will be generated
9
- * @param opaConfig - parameters for the generation
10
- * @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
11
- * @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
12
- * @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
13
- * @param opaConfig.useVirtualPreviewEndpoints - when true, OPA harness files are served virtually; skip writing them to disk
10
+ * @param options - OPA generation options
14
11
  * @param metadata - optional metadata for the OPA test generation
15
12
  * @param fs - an optional reference to a mem-fs editor
16
13
  * @param log - optional logger instance
17
14
  * @param standalone - opa test generation run standalone, not during app generation
18
15
  * @returns Reference to a mem-fs-editor
19
16
  */
20
- export declare function generateOPAFiles(basePath: string, opaConfig: {
21
- scriptName?: string;
22
- appID?: string;
23
- htmlTarget?: string;
24
- useVirtualPreviewEndpoints?: boolean;
25
- }, metadata?: string, fs?: Editor, log?: Logger, standalone?: boolean): Promise<Editor>;
17
+ export declare function generateOPAFiles(basePath: string, options: OPAGenerationOptions, metadata?: string, fs?: Editor, log?: Logger, standalone?: boolean): Promise<Editor>;
26
18
  /**
27
19
  * Reads the manifest for an app.
28
20
  *
@@ -3,7 +3,7 @@ import { fileURLToPath } from 'node:url';
3
3
  import { existsSync } from 'node:fs';
4
4
  import { create as createStorage } from 'mem-fs';
5
5
  import { create } from 'mem-fs-editor';
6
- import { SupportedPageTypes, ValidationError } from './types.js';
6
+ import { SupportedPageTypes, ValidationError, DotFileExtension } from './types.js';
7
7
  import { t } from './i18n.js';
8
8
  import { FileName, DirName, getWebappPath, updatePackageScript } from '@sap-ux/project-access';
9
9
  import { getAppFeatures } from './utils/modelUtils.js';
@@ -16,22 +16,22 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
16
16
  * Note: this can potentially overwrite existing files in the webapp/test folder.
17
17
  *
18
18
  * @param basePath - the absolute target path where the application will be generated
19
- * @param opaConfig - parameters for the generation
20
- * @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
21
- * @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
22
- * @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
23
- * @param opaConfig.useVirtualPreviewEndpoints - when true, OPA harness files are served virtually; skip writing them to disk
19
+ * @param options - OPA generation options
24
20
  * @param metadata - optional metadata for the OPA test generation
25
21
  * @param fs - an optional reference to a mem-fs editor
26
22
  * @param log - optional logger instance
27
23
  * @param standalone - opa test generation run standalone, not during app generation
28
24
  * @returns Reference to a mem-fs-editor
29
25
  */
30
- export async function generateOPAFiles(basePath, opaConfig, metadata, fs, log, standalone = false) {
26
+ export async function generateOPAFiles(basePath, options, metadata, fs, log, standalone = false) {
31
27
  const editor = fs ?? create(createStorage());
32
28
  const manifest = readManifest(editor, basePath);
33
29
  const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest);
34
- const config = createConfig(manifest, opaConfig, hideFilterBar);
30
+ const config = createConfig(manifest, options, hideFilterBar);
31
+ // In standalone mode, auto-detect TS vs JS from the project (presence of `tsconfig.json`)
32
+ // when the caller has not made an explicit choice. This enforces "TS app → TS tests, JS app → JS tests".
33
+ const enableTypeScript = options.enableTypeScript ?? (standalone && existsSync(join(basePath, FileName.Tsconfig)));
34
+ const dotFileExtension = enableTypeScript ? DotFileExtension.TS : DotFileExtension.JS;
35
35
  const rootCommonTemplateDirPath = join(__dirname, '../templates/common');
36
36
  const rootV4TemplateDirPath = join(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
37
37
  const testOutDirPath = join(await getWebappPath(basePath), 'test');
@@ -46,28 +46,43 @@ export async function generateOPAFiles(basePath, opaConfig, metadata, fs, log, s
46
46
  navigatedOP: LROP.pageOP?.targetKey,
47
47
  hideFilterBar: config.hideFilterBar
48
48
  };
49
- const writeContext = { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams };
49
+ const writeContext = {
50
+ config,
51
+ rootV4TemplateDirPath,
52
+ testOutDirPath,
53
+ editor,
54
+ journeyParams,
55
+ dotFileExtension
56
+ };
57
+ // The active context is the one used to actually emit files. In standalone mode without an
58
+ // existing JourneyRunner it is replaced with the resolved standalone context (which may
59
+ // override fields like `htmlTarget`); otherwise it stays as the original `writeContext`.
60
+ let activeContext = writeContext;
50
61
  if (standalone) {
51
- const hasJourneyRunner = existsSync(join(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'));
62
+ const hasJourneyRunner = existsSync(join(testOutDirPath, 'integration', 'pages', `JourneyRunner${dotFileExtension}`));
52
63
  const virtualOPA5Configured = await hasVirtualOPA5(basePath);
53
64
  if (hasJourneyRunner) {
54
65
  writeJourneyFiles(appFeatures, writeContext, true, true, virtualOPA5Configured);
55
66
  }
56
67
  else {
57
- const standaloneWriteContext = await resolveStandaloneWriteContext(basePath, testOutDirPath, writeContext, editor);
68
+ activeContext = await resolveStandaloneWriteContext(basePath, testOutDirPath, writeContext, editor);
58
69
  if (!virtualOPA5Configured) {
59
- writeCommonAndPageFiles(standaloneWriteContext, rootCommonTemplateDirPath);
70
+ writeCommonAndPageFiles(activeContext, rootCommonTemplateDirPath);
60
71
  }
61
- writeJourneyFiles(appFeatures, standaloneWriteContext, true, hasJourneyRunner, virtualOPA5Configured);
72
+ writeJourneyFiles(appFeatures, activeContext, true, hasJourneyRunner, virtualOPA5Configured);
62
73
  }
63
74
  }
64
75
  else {
65
- writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath, opaConfig.useVirtualPreviewEndpoints ?? false);
66
- writeJourneyFiles(appFeatures, writeContext, false, false, opaConfig.useVirtualPreviewEndpoints ?? false);
67
- if (opaConfig.useVirtualPreviewEndpoints) {
76
+ const useVirtualPreviewEndpoints = options.useVirtualPreviewEndpoints ?? false;
77
+ writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath, useVirtualPreviewEndpoints);
78
+ writeJourneyFiles(appFeatures, writeContext, false, false, useVirtualPreviewEndpoints);
79
+ if (useVirtualPreviewEndpoints) {
68
80
  await addVirtualTestConfig(basePath, [{ framework: 'OPA5', path: '/test/integration/opaTests.qunit.html' }, { framework: 'Testsuite' }], editor);
69
81
  }
70
82
  }
83
+ if (enableTypeScript) {
84
+ writeOpaJourneyTypes(activeContext);
85
+ }
71
86
  return editor;
72
87
  }
73
88
  /**
@@ -217,22 +232,19 @@ function createPageConfig(manifest, targetKey, forcedAppID) {
217
232
  * Create the configuration object from the app descriptor.
218
233
  *
219
234
  * @param manifest - the app descriptor of the target app
220
- * @param opaConfig - parameters for the generation
221
- * @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
222
- * @param opaConfig.htmlTarget - the name of the html file that will be used in the OPA journey file. If not specified, 'index.html' will be used
223
- * @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
235
+ * @param options - OPA generation options
224
236
  * @param hideFilterBar - whether the filter bar should be hidden in the generated tests
225
237
  * @returns OPA test configuration object
226
238
  */
227
- function createConfig(manifest, opaConfig, hideFilterBar) {
239
+ function createConfig(manifest, options, hideFilterBar) {
228
240
  // General application info
229
- const { appID, appPath } = getAppFromManifest(manifest, opaConfig.appID);
241
+ const { appID, appPath } = getAppFromManifest(manifest, options.appID);
230
242
  const config = {
231
243
  appID,
232
244
  appPath,
233
245
  pages: [],
234
- opaJourneyFileName: opaConfig.scriptName ?? 'FirstJourney',
235
- htmlTarget: opaConfig.htmlTarget ?? 'index.html',
246
+ opaJourneyFileName: options.scriptName ?? 'FirstJourney',
247
+ htmlTarget: options.htmlTarget ?? 'index.html',
236
248
  hideFilterBar
237
249
  };
238
250
  // Identify startup targets from the routes
@@ -248,7 +260,7 @@ function createConfig(manifest, opaConfig, hideFilterBar) {
248
260
  // Create page configurations in supported cases
249
261
  const appTargets = manifest['sap.ui5']?.routing?.targets;
250
262
  for (const targetKey in appTargets) {
251
- const pageConfig = createPageConfig(manifest, targetKey, opaConfig.appID);
263
+ const pageConfig = createPageConfig(manifest, targetKey, options.appID);
252
264
  if (pageConfig) {
253
265
  pageConfig.isStartup = startupTargets.includes(targetKey);
254
266
  config.pages.push(pageConfig);
@@ -311,7 +323,7 @@ function findLROP(pages, manifest) {
311
323
  * @param useVirtualPreviewEndpoints - when true, testsuite harness files are served virtually; skip writing them to disk
312
324
  */
313
325
  function writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath, useVirtualPreviewEndpoints = false) {
314
- const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
326
+ const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams, dotFileExtension } = writeContext;
315
327
  // Common test files (testsuite served virtually when useVirtualPreviewEndpoints is enabled)
316
328
  if (!useVirtualPreviewEndpoints) {
317
329
  editor.copyTpl(join(rootCommonTemplateDirPath), testOutDirPath,
@@ -321,36 +333,59 @@ function writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath, useVir
321
333
  });
322
334
  }
323
335
  config.pages.forEach((page) => {
324
- writePageObject(page, rootV4TemplateDirPath, testOutDirPath, editor);
336
+ writePageObject(page, rootV4TemplateDirPath, testOutDirPath, editor, dotFileExtension);
325
337
  });
326
- editor.copyTpl(join(rootV4TemplateDirPath, 'integration', 'FirstJourney.js'), join(testOutDirPath, 'integration', `${config.opaJourneyFileName}.js`), journeyParams, undefined, {
338
+ editor.copyTpl(join(rootV4TemplateDirPath, 'integration', `FirstJourney${dotFileExtension}`), join(testOutDirPath, 'integration', `${config.opaJourneyFileName}${dotFileExtension}`), { ...journeyParams, appPath: config.appPath }, undefined, {
327
339
  globOptions: { dot: true }
328
340
  });
329
341
  // Journey Runner
330
- editor.copyTpl(join(rootV4TemplateDirPath, 'integration', 'pages', 'JourneyRunner.js'), join(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'), config, undefined, {
342
+ editor.copyTpl(join(rootV4TemplateDirPath, 'integration', 'pages', `JourneyRunner${dotFileExtension}`), join(testOutDirPath, 'integration', 'pages', `JourneyRunner${dotFileExtension}`), config, undefined, {
343
+ globOptions: { dot: true }
344
+ });
345
+ }
346
+ /**
347
+ * Writes the OpaJourneyTypes.d.ts type definition file used by generated TypeScript OPA tests.
348
+ *
349
+ * @param writeContext - shared write context (config, paths, editor, journey params)
350
+ */
351
+ function writeOpaJourneyTypes(writeContext) {
352
+ const { config, rootV4TemplateDirPath, testOutDirPath, editor } = writeContext;
353
+ editor.copyTpl(join(rootV4TemplateDirPath, 'integration', 'types', 'OpaJourneyTypes.d.ts'), join(testOutDirPath, 'integration', 'types', 'OpaJourneyTypes.d.ts'), config, undefined, {
331
354
  globOptions: { dot: true }
332
355
  });
333
356
  }
334
357
  /**
335
358
  * Checks whether a page object file already exists for the given feature name.
336
- * If it doesn't exist, finds the matching page config and writes the file.
359
+ * Both `.ts` and `.js` extensions are checked to avoid creating duplicate page objects
360
+ * when regenerating in a different language than the existing tests use.
361
+ * If neither exists, finds the matching page config and writes the file.
337
362
  *
338
363
  * @param featureName - the feature/page name (equals the manifest targetKey)
339
364
  * @param config - the OPA config containing all page configurations
340
365
  * @param rootV4TemplateDirPath - template root directory for v4 templates
341
366
  * @param testOutDirPath - output test directory (.../webapp/test)
342
367
  * @param editor - a reference to a mem-fs editor
368
+ * @param dotFileExtension - file extension ('.ts' or '.js')
343
369
  * @returns JourneyRunnerPage if the page was newly created, undefined otherwise
344
370
  */
345
- function ensurePageExists(featureName, config, rootV4TemplateDirPath, testOutDirPath, editor) {
346
- const pageFilePath = join(testOutDirPath, 'integration', 'pages', `${featureName}.js`);
347
- if (editor.exists(pageFilePath)) {
371
+ function ensurePageExists(featureName, config, rootV4TemplateDirPath, testOutDirPath, editor, dotFileExtension) {
372
+ const pagesDir = join(testOutDirPath, 'integration', 'pages');
373
+ if (editor.exists(join(pagesDir, `${featureName}${DotFileExtension.TS}`)) ||
374
+ editor.exists(join(pagesDir, `${featureName}${DotFileExtension.JS}`))) {
348
375
  return undefined;
349
376
  }
350
377
  const pageConfig = config.pages.find((p) => p.targetKey === featureName);
351
378
  if (pageConfig) {
352
- writePageObject(pageConfig, rootV4TemplateDirPath, testOutDirPath, editor);
353
- return { targetKey: featureName, appPath: config.appPath };
379
+ writePageObject(pageConfig, rootV4TemplateDirPath, testOutDirPath, editor, dotFileExtension);
380
+ return {
381
+ targetKey: featureName,
382
+ appPath: config.appPath,
383
+ template: pageConfig.template,
384
+ appID: config.appID,
385
+ componentID: pageConfig.componentID,
386
+ entitySet: pageConfig.entitySet,
387
+ contextPath: pageConfig.contextPath
388
+ };
354
389
  }
355
390
  return undefined;
356
391
  }
@@ -360,22 +395,23 @@ function ensurePageExists(featureName, config, rootV4TemplateDirPath, testOutDir
360
395
  * @param appFeatures - object containing feature data for list report, object pages, and FPM
361
396
  * @param writeContext - shared write context (config, paths, editor, journey params)
362
397
  * @param isStandalone - whether the generation is run in standalone mode (not during app generation)
363
- * @param hasJourneyRunner - whether a JourneyRunner.js already exists (standalone upgrade path)
398
+ * @param hasJourneyRunner - whether a JourneyRunner already exists (standalone upgrade path)
364
399
  * @param virtualOPA5Configured - whether virtual OPA5 is configured
365
400
  */
366
401
  function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRunner = false, virtualOPA5Configured = false) {
367
- const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
402
+ const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams, dotFileExtension } = writeContext;
368
403
  const generatedJourneyPages = [];
369
404
  const newPages = [];
370
405
  if (appFeatures.listReport?.name) {
371
- editor.copyTpl(join(rootV4TemplateDirPath, 'integration', 'ListReportJourney.js'), join(testOutDirPath, 'integration', `${appFeatures.listReport.name}Journey.js`), {
406
+ editor.copyTpl(join(rootV4TemplateDirPath, 'integration', `ListReportJourney${dotFileExtension}`), join(testOutDirPath, 'integration', `${appFeatures.listReport.name}Journey${dotFileExtension}`), {
372
407
  ...journeyParams,
373
- ...appFeatures.listReport
408
+ ...appFeatures.listReport,
409
+ appPath: config.appPath
374
410
  }, undefined, {
375
411
  globOptions: { dot: true }
376
412
  });
377
413
  generatedJourneyPages.push(appFeatures.listReport.name);
378
- const lrPage = ensurePageExists(appFeatures.listReport.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
414
+ const lrPage = ensurePageExists(appFeatures.listReport.name, config, rootV4TemplateDirPath, testOutDirPath, editor, dotFileExtension);
379
415
  if (lrPage) {
380
416
  newPages.push(lrPage);
381
417
  }
@@ -383,15 +419,16 @@ function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRu
383
419
  if (appFeatures.objectPages && appFeatures.objectPages.length > 0) {
384
420
  appFeatures.objectPages.forEach((objectPage) => {
385
421
  if (objectPage.name) {
386
- editor.copyTpl(join(rootV4TemplateDirPath, 'integration', 'ObjectPageJourney.js'), join(testOutDirPath, 'integration', `${objectPage.name}Journey.js`), {
422
+ editor.copyTpl(join(rootV4TemplateDirPath, 'integration', `ObjectPageJourney${dotFileExtension}`), join(testOutDirPath, 'integration', `${objectPage.name}Journey${dotFileExtension}`), {
387
423
  ...journeyParams,
388
424
  ...objectPage,
389
- isStandalone
425
+ isStandalone,
426
+ appPath: config.appPath
390
427
  }, undefined, {
391
428
  globOptions: { dot: true }
392
429
  });
393
430
  generatedJourneyPages.push(objectPage.name);
394
- const opPage = ensurePageExists(objectPage.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
431
+ const opPage = ensurePageExists(objectPage.name, config, rootV4TemplateDirPath, testOutDirPath, editor, dotFileExtension);
395
432
  if (opPage) {
396
433
  newPages.push(opPage);
397
434
  }
@@ -399,6 +436,13 @@ function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRu
399
436
  });
400
437
  }
401
438
  if (appFeatures.fpm?.name) {
439
+ // FPM TypeScript support is out of scope for the initial TS OPA5 work
440
+ // (LROP only). The FPM journey path below is hardcoded `.js` and there is
441
+ // no `FPM.ts` template, so we force `DotFileExtension.JS` for the FPM
442
+ // page-object regardless of the configured `dotFileExtension`. Otherwise
443
+ // an LR-OP-FPM mix with `enableTypeScript` would crash in `writePageObject`
444
+ // when trying to load the missing `FPM.ts` template.
445
+ // Future work: add FPM.ts/FPMJourney.ts templates and switch to `dotFileExtension`.
402
446
  editor.copyTpl(join(rootV4TemplateDirPath, 'integration', 'FPMJourney.js'), join(testOutDirPath, 'integration', `${appFeatures.fpm.name}Journey.js`), {
403
447
  ...journeyParams,
404
448
  ...appFeatures.fpm
@@ -406,13 +450,13 @@ function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRu
406
450
  globOptions: { dot: true }
407
451
  });
408
452
  generatedJourneyPages.push(appFeatures.fpm.name);
409
- const fpmPage = ensurePageExists(appFeatures.fpm.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
453
+ const fpmPage = ensurePageExists(appFeatures.fpm.name, config, rootV4TemplateDirPath, testOutDirPath, editor, DotFileExtension.JS);
410
454
  if (fpmPage) {
411
455
  newPages.push(fpmPage);
412
456
  }
413
457
  }
414
458
  if (newPages.length > 0) {
415
- addPagesToJourneyRunner(newPages, testOutDirPath, editor);
459
+ addPagesToJourneyRunner(newPages, testOutDirPath, editor, dotFileExtension);
416
460
  }
417
461
  if (!virtualOPA5Configured) {
418
462
  if (hasJourneyRunner) {
@@ -434,9 +478,12 @@ function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRu
434
478
  * @param rootTemplateDirPath - template root directory
435
479
  * @param testOutDirPath - output test directory (.../webapp/test)
436
480
  * @param fs - a reference to a mem-fs editor
481
+ * @param dotFileExtension - file extension ('.ts' or '.js')
437
482
  */
438
- function writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, fs) {
439
- fs.copyTpl(join(rootTemplateDirPath, 'integration', 'pages', `${pageConfig.template}.js`), join(testOutDirPath, 'integration', 'pages', `${pageConfig.targetKey}.js`), pageConfig, undefined, {
483
+ function writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, fs, dotFileExtension) {
484
+ // FPM has no .ts template; force .js regardless of the configured extension
485
+ const ext = pageConfig.template === 'FPM' ? DotFileExtension.JS : dotFileExtension;
486
+ fs.copyTpl(join(rootTemplateDirPath, 'integration', 'pages', `${pageConfig.template}${ext}`), join(testOutDirPath, 'integration', 'pages', `${pageConfig.targetKey}${ext}`), pageConfig, undefined, {
440
487
  globOptions: { dot: true }
441
488
  });
442
489
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { generateOPAFiles } from './fiori-elements-opa-writer.js';
2
2
  export { generateFreestyleOPAFiles } from './fiori-freestyle-opa-writer.js';
3
3
  export { addVirtualTestConfig } from './utils/opaQUnitUtils.js';
4
+ export type { OPAGenerationOptions } from './types.js';
4
5
  //# sourceMappingURL=index.d.ts.map
package/dist/types.d.ts CHANGED
@@ -1,4 +1,24 @@
1
1
  import type { Editor } from 'mem-fs-editor';
2
+ export declare const DotFileExtension: {
3
+ readonly JS: ".js";
4
+ readonly TS: ".ts";
5
+ };
6
+ export type DotFileExtension = (typeof DotFileExtension)[keyof typeof DotFileExtension];
7
+ /**
8
+ * Options accepted by the public OPA test generation entry point.
9
+ */
10
+ export type OPAGenerationOptions = {
11
+ /** The name of the OPA journey file. If not specified, 'FirstJourney' will be used. */
12
+ scriptName?: string;
13
+ /** The appID. If not specified, will be read from the manifest in sap.app/id. */
14
+ appID?: string;
15
+ /** The name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used. */
16
+ htmlTarget?: string;
17
+ /** When true, OPA harness files are served virtually; skip writing them to disk. */
18
+ useVirtualPreviewEndpoints?: boolean;
19
+ /** If true, generate TypeScript files instead of JavaScript. */
20
+ enableTypeScript?: boolean;
21
+ };
2
22
  export declare const SupportedPageTypes: {
3
23
  [id: string]: string;
4
24
  };
@@ -193,6 +213,7 @@ export type WriteContext = {
193
213
  testOutDirPath: string;
194
214
  editor: Editor;
195
215
  journeyParams: JourneyParams;
216
+ dotFileExtension: DotFileExtension;
196
217
  };
197
218
  export type FormField = {
198
219
  fieldGroupQualifier?: string;
package/dist/types.js CHANGED
@@ -1,3 +1,7 @@
1
+ export const DotFileExtension = {
2
+ JS: '.js',
3
+ TS: '.ts'
4
+ };
1
5
  export const SupportedPageTypes = {
2
6
  'sap.fe.templates.ListReport': 'ListReport',
3
7
  'sap.fe.templates.ObjectPage': 'ObjectPage',
@@ -4,6 +4,7 @@
4
4
  * all other content (formatting, comments, whitespace) is preserved exactly.
5
5
  */
6
6
  import type { Editor } from 'mem-fs-editor';
7
+ import { DotFileExtension } from '../types.js';
7
8
  import type { TestConfig as PreviewMiddlewareTestConfig } from '@sap-ux/preview-middleware';
8
9
  /**
9
10
  * Splices new module paths into the sap.ui.require array of the content string.
@@ -48,13 +49,23 @@ export declare function addIntegrationOldToGitignore(basePath: string, fs: Edito
48
49
  */
49
50
  export declare function addPathsToQUnitJs(filePaths: string[], projectPath: string, fs: Editor): void;
50
51
  /**
51
- * Page entry to splice into an existing JourneyRunner.js.
52
+ * Page entry to splice into an existing JourneyRunner.js / JourneyRunner.ts.
52
53
  */
53
54
  export interface JourneyRunnerPage {
54
55
  /** The page's targetKey, used as both the variable name and `onThe<targetKey>` key */
55
56
  targetKey: string;
56
57
  /** The app module path prefix (e.g. "project1/test/integration/pages") */
57
58
  appPath: string;
59
+ /** The framework page template (`'ListReport'` or `'ObjectPage'`); used by the TS splicer to construct `new <FW>(definition, Custom<Page>)`. */
60
+ template?: string;
61
+ /** The app id (sap.app.id from the manifest); only needed by the TS splicer. */
62
+ appID?: string;
63
+ /** The component id (defined in the target section); only needed by the TS splicer. */
64
+ componentID?: string;
65
+ /** The entity set name (if the page uses an entitySet rather than a contextPath); only needed by the TS splicer. */
66
+ entitySet?: string;
67
+ /** The context path (if the page uses a contextPath rather than an entitySet); only needed by the TS splicer. */
68
+ contextPath?: string;
58
69
  }
59
70
  /**
60
71
  * Splices new page entries into the three locations of an existing JourneyRunner.js:
@@ -74,15 +85,32 @@ export interface JourneyRunnerPage {
74
85
  */
75
86
  export declare function splicePageIntoJourneyRunner(fileContent: string, pages: JourneyRunnerPage[]): string;
76
87
  /**
77
- * Reads JourneyRunner.js from the project, adds new page entries to all three
78
- * locations (define array, function params, pages object), and writes the updated
79
- * content back. Pages already present are skipped.
88
+ * Splices new page entries into an existing TypeScript JourneyRunner.ts:
89
+ * - adds a default-import line after the last existing page import
90
+ * - adds an entry inside the `pages: { ... }` object literal
91
+ *
92
+ * Pages already present (detected by their import line) are skipped.
93
+ * All other content — formatting, comments, whitespace — is preserved exactly.
94
+ *
95
+ * Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
96
+ * ReDoS on crafted inputs. Valid generated files are well within this limit.
97
+ *
98
+ * @param fileContent - the full content of the JourneyRunner.ts file
99
+ * @param pages - pages to add
100
+ * @returns the updated file content, or the original content unchanged if nothing was added
101
+ */
102
+ export declare function splicePageIntoJourneyRunnerTs(fileContent: string, pages: JourneyRunnerPage[]): string;
103
+ /**
104
+ * Reads JourneyRunner from the project, adds new page entries, and writes the updated
105
+ * content back. Pages already present are skipped. Both AMD (`.js`) and ES module
106
+ * (`.ts`) variants are supported and dispatched on `dotFileExtension`.
80
107
  *
81
108
  * @param pages - pages to add
82
109
  * @param testOutDirPath - path to the test output directory (`.../webapp/test`)
83
110
  * @param fs - mem-fs-editor instance used to read and write the file
111
+ * @param dotFileExtension - file extension of the JourneyRunner ('.ts' or '.js'); defaults to '.js'
84
112
  */
85
- export declare function addPagesToJourneyRunner(pages: JourneyRunnerPage[], testOutDirPath: string, fs: Editor): void;
113
+ export declare function addPagesToJourneyRunner(pages: JourneyRunnerPage[], testOutDirPath: string, fs: Editor, dotFileExtension?: DotFileExtension): void;
86
114
  /**
87
115
  * Returns true if any UI5 yaml file in the project contains a `fiori-tools-preview`
88
116
  * middleware whose `test` array includes an entry with `framework: OPA5`.
@@ -6,6 +6,7 @@
6
6
  import { join } from 'node:path';
7
7
  import { readHashFromFlpSandbox } from './flpSandboxUtils.js';
8
8
  import { getAllUi5YamlFileNames, readUi5Yaml, FileName } from '@sap-ux/project-access';
9
+ import { DotFileExtension } from '../types.js';
9
10
  /** Relative path from the test output directory to opaTests.qunit.js */
10
11
  const OPA_QUNIT_FILE = join('integration', 'opaTests.qunit.js');
11
12
  /**
@@ -25,6 +26,13 @@ const OPA_QUNIT_FILE = join('integration', 'opaTests.qunit.js');
25
26
  const SAP_UI_REQUIRE_ARRAY_REGEX = /sap\.ui\.require\s*\(\s*\[([^\]]*)\]\s*,\s*function/d;
26
27
  /** ReDoS mitigation: files larger than this are returned unchanged rather than matched with regex. */
27
28
  const MAX_FILE_CONTENT_LENGTH = 10000;
29
+ /**
30
+ * Escapes regex metacharacters in a string so it can be safely embedded in a `RegExp` pattern.
31
+ *
32
+ * @param value - the string to escape
33
+ * @returns the escaped string
34
+ */
35
+ const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
28
36
  /**
29
37
  * Splices new module paths into the sap.ui.require array of the content string.
30
38
  * Entries that are already present are skipped. All other content is preserved exactly.
@@ -161,8 +169,13 @@ export function addPathsToQUnitJs(filePaths, projectPath, fs) {
161
169
  // If the file doesn't exist or can't be read, do nothing
162
170
  }
163
171
  }
164
- /** Relative path from the test output directory to JourneyRunner.js */
165
- const JOURNEY_RUNNER_FILE = join('integration', 'pages', 'JourneyRunner.js');
172
+ /**
173
+ * Builds the relative path from the test output directory to the JourneyRunner file.
174
+ *
175
+ * @param dotFileExtension - file extension ('.ts' or '.js')
176
+ * @returns the relative path
177
+ */
178
+ const getJourneyRunnerFilePath = (dotFileExtension) => join('integration', 'pages', `JourneyRunner${dotFileExtension}`);
166
179
  /**
167
180
  * Splices new page entries into the three locations of an existing JourneyRunner.js:
168
181
  * - the sap.ui.define dependency array
@@ -246,19 +259,187 @@ export function splicePageIntoJourneyRunner(fileContent, pages) {
246
259
  return result;
247
260
  }
248
261
  /**
249
- * Reads JourneyRunner.js from the project, adds new page entries to all three
250
- * locations (define array, function params, pages object), and writes the updated
251
- * content back. Pages already present are skipped.
262
+ * Filters the input page list down to those whose `Custom<targetKey>` import line is not yet
263
+ * present in the existing JourneyRunner.ts content.
264
+ *
265
+ * @param fileContent - the existing JourneyRunner.ts content
266
+ * @param pages - the candidate pages to splice in
267
+ * @returns the subset of pages that need to be added
268
+ */
269
+ function findPagesToAdd(fileContent, pages) {
270
+ return pages.filter((page) => {
271
+ const importPattern = new RegExp(String.raw `from\s+"\./${escapeRegex(page.targetKey)}"`);
272
+ return !importPattern.test(fileContent);
273
+ });
274
+ }
275
+ /**
276
+ * Returns the offset of the character immediately after the last `import ... from "..."` line in
277
+ * the given content, or -1 if no import line is found.
278
+ *
279
+ * Uses `\b` word boundaries (zero-width) around `import` and `from` so the `[^\n]*?` middle
280
+ * quantifier doesn't sit next to another quantifier that could match the same characters. This
281
+ * avoids the consecutive-overlapping-quantifier shape that triggers catastrophic backtracking.
282
+ *
283
+ * @param content - the file content to scan
284
+ * @returns the index immediately after the last import line, or -1 if none found
285
+ */
286
+ function findLastImportEnd(content) {
287
+ const importLineRegex = /^import\b[^\n]*?\bfrom[ \t]+["'][^"']+["'];?[ \t]*$/gm;
288
+ let lastImportEnd = -1;
289
+ let importMatch;
290
+ while ((importMatch = importLineRegex.exec(content)) !== null) {
291
+ lastImportEnd = importMatch.index + importMatch[0].length;
292
+ }
293
+ return lastImportEnd;
294
+ }
295
+ /**
296
+ * Walks forward from the opening `{` of a `pages: { ... }` object literal, counting braces, and
297
+ * returns the index of the matching closing `}`. The pages object now contains nested `{}` (the
298
+ * page-definition object) so a regex with `[^}]*` would stop at the first inner closing brace.
299
+ *
300
+ * @param content - the file content
301
+ * @param openBraceIdx - the index of the `{` that opens the pages object
302
+ * @returns the index of the matching closing `}` (or `content.length` if not found)
303
+ */
304
+ function findMatchingClosingBrace(content, openBraceIdx) {
305
+ let depth = 1;
306
+ let i = openBraceIdx + 1;
307
+ while (i < content.length && depth > 0) {
308
+ const ch = content[i];
309
+ if (ch === '{') {
310
+ depth++;
311
+ }
312
+ else if (ch === '}') {
313
+ depth--;
314
+ }
315
+ if (depth === 0) {
316
+ break;
317
+ }
318
+ i++;
319
+ }
320
+ return i;
321
+ }
322
+ /**
323
+ * Builds the source-code block for a single new entry in the `pages: { ... }` object literal.
324
+ *
325
+ * @param page - the page to render
326
+ * @param pageIndent - leading whitespace for the entry's outer line
327
+ * @param innerIndent - leading whitespace for the entry's nested lines
328
+ * @returns the multi-line source block
329
+ */
330
+ function buildPageEntry(page, pageIndent, innerIndent) {
331
+ const fw = page.template ?? 'ListReport';
332
+ return [
333
+ `${pageIndent}onThe${page.targetKey}: new ${fw}(`,
334
+ `${innerIndent}{`,
335
+ `${innerIndent} appId: "${page.appID ?? ''}",`,
336
+ `${innerIndent} componentId: "${page.componentID ?? ''}",`,
337
+ `${innerIndent} entitySet: "${page.entitySet ?? ''}",`,
338
+ `${innerIndent} contextPath: "${page.contextPath ?? ''}"`,
339
+ `${innerIndent}},`,
340
+ `${innerIndent}Custom${page.targetKey}`,
341
+ `${pageIndent})`
342
+ ].join('\n');
343
+ }
344
+ /**
345
+ * Inserts the given import lines after the last existing `import` line in `content`. If `content`
346
+ * has no `import` lines, returns it unchanged.
347
+ *
348
+ * @param content - the file content
349
+ * @param newImportLines - the import lines to insert (each should NOT include a leading newline)
350
+ * @returns the updated content
351
+ */
352
+ function insertAfterLastImport(content, newImportLines) {
353
+ if (newImportLines.length === 0) {
354
+ return content;
355
+ }
356
+ const lastImportEnd = findLastImportEnd(content);
357
+ if (lastImportEnd < 0) {
358
+ return content;
359
+ }
360
+ const newImports = newImportLines.map((line) => `\n${line}`).join('');
361
+ return `${content.slice(0, lastImportEnd)}${newImports}${content.slice(lastImportEnd)}`;
362
+ }
363
+ /**
364
+ * Inserts the given new page entries inside the `pages: { ... }` object literal of `content`. If
365
+ * `content` has no `pages: {` block, returns it unchanged.
366
+ *
367
+ * @param content - the file content
368
+ * @param toAdd - the pages to add
369
+ * @returns the updated content
370
+ */
371
+ function insertIntoPagesObject(content, toAdd) {
372
+ const pagesStartMatch = /pages\s*:\s*\{/.exec(content);
373
+ if (!pagesStartMatch) {
374
+ return content;
375
+ }
376
+ const openBraceIdx = content.indexOf('{', pagesStartMatch.index);
377
+ const pagesBodyEnd = findMatchingClosingBrace(content, openBraceIdx);
378
+ const pagesBody = content.slice(openBraceIdx + 1, pagesBodyEnd);
379
+ // Detect indentation from the first existing page entry
380
+ const pageIndentMatch = /^([ \t]+)on/m.exec(pagesBody);
381
+ const pageIndent = pageIndentMatch ? pageIndentMatch[1] : ' ';
382
+ const innerIndent = pageIndent + ' ';
383
+ const newPageEntries = toAdd.map((page) => buildPageEntry(page, pageIndent, innerIndent)).join(',\n');
384
+ // Ensure the last existing entry ends with a comma before we insert after it.
385
+ const trimmedPagesEnd = content.slice(0, pagesBodyEnd).trimEnd();
386
+ const needsComma = !trimmedPagesEnd.endsWith(',') && !trimmedPagesEnd.endsWith('{');
387
+ const commaFix = needsComma ? ',' : '';
388
+ const trailingWhitespace = content.slice(trimmedPagesEnd.length, pagesBodyEnd);
389
+ return `${trimmedPagesEnd}${commaFix}\n${newPageEntries}${trailingWhitespace}${content.slice(pagesBodyEnd)}`;
390
+ }
391
+ /**
392
+ * Splices new page entries into an existing TypeScript JourneyRunner.ts:
393
+ * - adds a default-import line after the last existing page import
394
+ * - adds an entry inside the `pages: { ... }` object literal
395
+ *
396
+ * Pages already present (detected by their import line) are skipped.
397
+ * All other content — formatting, comments, whitespace — is preserved exactly.
398
+ *
399
+ * Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
400
+ * ReDoS on crafted inputs. Valid generated files are well within this limit.
401
+ *
402
+ * @param fileContent - the full content of the JourneyRunner.ts file
403
+ * @param pages - pages to add
404
+ * @returns the updated file content, or the original content unchanged if nothing was added
405
+ */
406
+ export function splicePageIntoJourneyRunnerTs(fileContent, pages) {
407
+ if (fileContent.length > MAX_FILE_CONTENT_LENGTH) {
408
+ return fileContent;
409
+ }
410
+ const toAdd = findPagesToAdd(fileContent, pages);
411
+ if (toAdd.length === 0) {
412
+ return fileContent;
413
+ }
414
+ // Determine which framework imports (ListReport / ObjectPage) are missing and need to be added.
415
+ const frameworkTemplates = Array.from(new Set(toAdd.map((page) => page.template).filter((t) => Boolean(t))));
416
+ const missingFrameworkImports = frameworkTemplates.filter((tpl) => !fileContent.includes(`from "sap/fe/test/${tpl}"`));
417
+ const newImportLines = [
418
+ ...missingFrameworkImports.map((tpl) => `import ${tpl} from "sap/fe/test/${tpl}";`),
419
+ ...toAdd.map((page) => `import Custom${page.targetKey} from "./${page.targetKey}";`)
420
+ ];
421
+ const withImports = insertAfterLastImport(fileContent, newImportLines);
422
+ return insertIntoPagesObject(withImports, toAdd);
423
+ }
424
+ /**
425
+ * Reads JourneyRunner from the project, adds new page entries, and writes the updated
426
+ * content back. Pages already present are skipped. Both AMD (`.js`) and ES module
427
+ * (`.ts`) variants are supported and dispatched on `dotFileExtension`.
252
428
  *
253
429
  * @param pages - pages to add
254
430
  * @param testOutDirPath - path to the test output directory (`.../webapp/test`)
255
431
  * @param fs - mem-fs-editor instance used to read and write the file
432
+ * @param dotFileExtension - file extension of the JourneyRunner ('.ts' or '.js'); defaults to '.js'
256
433
  */
257
- export function addPagesToJourneyRunner(pages, testOutDirPath, fs) {
434
+ export function addPagesToJourneyRunner(pages, testOutDirPath, fs, dotFileExtension = DotFileExtension.JS) {
435
+ if (pages.length === 0) {
436
+ return;
437
+ }
258
438
  try {
259
- const filePath = join(testOutDirPath, JOURNEY_RUNNER_FILE);
439
+ const filePath = join(testOutDirPath, getJourneyRunnerFilePath(dotFileExtension));
260
440
  const content = fs.read(filePath);
261
- const updated = splicePageIntoJourneyRunner(content, pages);
441
+ const splice = dotFileExtension === DotFileExtension.TS ? splicePageIntoJourneyRunnerTs : splicePageIntoJourneyRunner;
442
+ const updated = splice(content, pages);
262
443
  if (updated !== content) {
263
444
  fs.write(filePath, updated);
264
445
  }
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": "1.0.14",
4
+ "version": "1.1.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -30,9 +30,9 @@
30
30
  "@sap-ux/annotation-converter": "0.10.21",
31
31
  "@sap-ux/ui5-application-writer": "2.0.3",
32
32
  "@sap-ux/logger": "1.0.1",
33
+ "@sap-ux/fiori-generator-shared": "1.0.9",
33
34
  "@sap-ux/project-access": "2.1.2",
34
- "@sap-ux/preview-middleware": "1.0.14",
35
- "@sap-ux/fiori-generator-shared": "1.0.9"
35
+ "@sap-ux/preview-middleware": "1.0.14"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@jest/globals": "30.3.0",
@@ -0,0 +1,32 @@
1
+ import opaTest from "sap/ui/test/opaQunit";
2
+ import type { Given, When, Then } from "./types/OpaJourneyTypes";
3
+ import runner from "./pages/JourneyRunner";
4
+
5
+ function journey() {
6
+ QUnit.module("First journey");
7
+
8
+ opaTest("Start application", function (Given: Given, _When: When<% if (startLR) { %>, Then: Then<% } else { %>, _Then: Then<% } %>) {
9
+ Given.iStartMyApp();
10
+ <% if (startLR) { %>Then.onThe<%- startLR %>.iSeeThisPage();<%} %>
11
+ });
12
+
13
+ <% if (startLR) { %>
14
+ opaTest("Navigate to ObjectPage", function (_Given: Given, When: When, Then: Then) {
15
+ // Note: this test will fail if the ListReport page doesn't show any data
16
+ <% if (!hideFilterBar) { %>
17
+ When.onThe<%- startLR%>.onFilterBar().iExecuteSearch();
18
+ <%} %>
19
+ Then.onThe<%- startLR%>.onTable("").iCheckRows();
20
+ <% if (navigatedOP) { %>
21
+ When.onThe<%- startLR%>.onTable("").iPressRow(0);
22
+ Then.onThe<%- navigatedOP%>.iSeeThisPage();
23
+ <%} %>
24
+ });
25
+ <%} %>
26
+ opaTest("Teardown", function (Given: Given) {
27
+ // Cleanup
28
+ Given.iTearDownMyApp();
29
+ });
30
+ }
31
+
32
+ runner.run([journey]);
@@ -0,0 +1,117 @@
1
+ /******************************************************************************
2
+ * ╔═══════════════════════════════════════════════════════════════════════╗ *
3
+ * ║ ║ *
4
+ * ║ WARNING: AUTO-GENERATED FILE ║ *
5
+ * ║ ║ *
6
+ * ║ This file is automatically generated by SAP Fiori tools and is ║ *
7
+ * ║ overwritten when you run the OPA test generator again. ║ *
8
+ * ║ ║ *
9
+ * ║ To add your own custom tests: ║ *
10
+ * ║ - Create a new journey test file in this directory. ║ *
11
+ * ║ - Follow the same pattern as this file. ║ *
12
+ * ║ - Add the new file to the opaTests.qunit.js config file. ║ *
13
+ * ║ - Custom journey files are not overwritten. ║ *
14
+ * ║ ║ *
15
+ * ╚═══════════════════════════════════════════════════════════════════════╝ *
16
+ ******************************************************************************/
17
+
18
+ import opaTest from "sap/ui/test/opaQunit";
19
+ import type { Given, When, Then } from "./types/OpaJourneyTypes";
20
+ <%_
21
+ const usesFilterFieldIdentifier =
22
+ (!hideFilterBar && filterBarItems && filterBarItems.length > 0) ||
23
+ (semanticKey && semanticKey.missingFromFilterBar && semanticKey.missingFromFilterBar.length > 0);
24
+ -%>
25
+ <% if (usesFilterFieldIdentifier) { -%>
26
+ import type { FilterFieldIdentifier } from "sap/fe/test/api/FilterBarAPI";
27
+ <% } -%>
28
+ import runner from "./pages/JourneyRunner";
29
+
30
+ function journey() {
31
+ QUnit.module("<%- name%>ListReport journey");
32
+
33
+ opaTest("Start application", function (Given: Given, _When: When, Then: Then) {
34
+ Given.iStartMyApp();
35
+ <%_ startPages.forEach(function(pageName) { %>
36
+ Then.onThe<%- pageName %>.iSeeThisPage();
37
+ <%_ }); -%>
38
+ });
39
+
40
+ <%_ if (!hideFilterBar && filterBarItems && filterBarItems.length > 0) { -%>
41
+ opaTest("Check filter bar", function (_Given: Given, _When: When, Then: Then) {
42
+ <%_ filterBarItems.forEach(function(item) { _%>
43
+ Then.onThe<%- startLR%>.onFilterBar().iCheckFilterField("<%- item %>" as unknown as FilterFieldIdentifier);
44
+ <%_ }); -%>
45
+ });
46
+ <%_ } -%>
47
+ <%_ if (semanticKey && semanticKey.missingFromFilterBar && semanticKey.missingFromFilterBar.length > 0) { %>
48
+ opaTest("Add semantic key properties to filter bar", function (_Given: Given, When: When, Then: Then) {
49
+ Then.onThe<%- startLR%>.onFilterBar().iOpenFilterAdaptation();
50
+ <%_ semanticKey.missingFromFilterBar.forEach(function(property) { _%>
51
+ When.onThe<%- startLR%>.onFilterBar().iAddAdaptationFilterField("<%- property %>");
52
+ <%_ }); -%>
53
+ Then.onThe<%- startLR%>.onFilterBar().iConfirmFilterAdaptation();
54
+ <%_ semanticKey.missingFromFilterBar.forEach(function(property) { _%>
55
+ Then.onThe<%- startLR%>.onFilterBar().iCheckFilterField("<%- property %>" as unknown as FilterFieldIdentifier);
56
+ <%_ }); -%>
57
+ <%_ semanticKey.missingFromFilterBar.forEach(function(property) { _%>
58
+ // Then.onThe<%- startLR%>.onFilterBar().iChangeFilterField({ property: "<%- property %>" });
59
+ <%_ }); -%>
60
+ // Then.onThe<%- startLR%>.onFilterBar().iExecuteSearch();
61
+ // Then.onThe<%- startLR%>.onTable("").iCheckRows();
62
+ // Then.onThe<%- startLR%>.onTable("").iSelectRows(0);
63
+ // Then.onThe<%- startLR%>.onTable("").iCheckAction("<Action Name>", { enabled: true });
64
+ });
65
+ <%_ } -%>
66
+
67
+ // Note: this test will only work if the ListReport page has a search field and shows data that matches the search term. Please ensure that the test data and search term are set up accordingly.
68
+ // opaTest("Perform a global search and check the result", function (Given: Given, When: When, Then: Then) {
69
+ // When.onThe<%- startLR%>.onFilterBar().iChangeSearchField("Search Term");
70
+ // When.onThe<%- startLR%>.onFilterBar().iExecuteSearch();
71
+ // Then.onThe<%- startLR%>.onTable("").iCheckRows();
72
+ // });
73
+
74
+ <%_ if ((toolBarActions && toolBarActions.length > 0 ) || (tableColumns && Object.keys(tableColumns).length > 0)) { -%>
75
+ opaTest("Check table columns and actions", function (_Given: Given, _When: When, Then: Then) {
76
+ <%_ if (toolBarActions && toolBarActions.length > 0) { -%>
77
+ <%_ if (createButton.visible && !isALP) { _%>
78
+ Then.onThe<%- startLR%>.onTable("").iCheckCreate({ visible: true });
79
+ // Then.onthe<%- startLR%>.onTable("").iPressCreate();
80
+ <%_ } _%>
81
+ <%_ if (deleteButton.visible) { _%>
82
+ // Then.onthe<%- startLR%>.onTable("").iPressDelete();
83
+ Then.onThe<%- startLR%>.onTable("").iCheckDelete({ visible: true });
84
+ <%_ } _%>
85
+ <%_ toolBarActions.forEach(function(item) { _%>
86
+ <%_ if (item.visible) { _%>
87
+ // Then.onThe<%- startLR%>.onTable("").iPressAction("<%- item.label %>");
88
+ Then.onThe<%- startLR%>.onTable("").iCheckAction("<%- item.label %>", { enabled: <%- item.enabled === true %> });
89
+ <%_ } _%>
90
+ <%_ }); -%>
91
+ <%_ } -%>
92
+ <%_ if (tableColumns && Object.keys(tableColumns).length > 0) { -%>
93
+ Then.onThe<%- startLR %>.onTable("").iCheckColumns(undefined, <%- JSON.stringify(tableColumns) %>);
94
+ <%_ } %>
95
+ });
96
+ <%_ } %>
97
+
98
+ <% if (startLR) { %>
99
+ opaTest("Navigate to ObjectPage", function (_Given: Given, When: When, Then: Then) {
100
+ // Note: this test will fail if the ListReport page doesn't show any data
101
+ <% if (!hideFilterBar) { %>
102
+ When.onThe<%- startLR%>.onFilterBar().iExecuteSearch();
103
+ <%} %>
104
+ Then.onThe<%- startLR%>.onTable("").iCheckRows();
105
+ <% if (navigatedOP) { %>
106
+ When.onThe<%- startLR%>.onTable("").iPressRow(0);
107
+ Then.onThe<%- navigatedOP%>.iSeeThisPage();
108
+ <%} %>
109
+ });
110
+ <%} %>
111
+ opaTest("Teardown", function (Given: Given) {
112
+ // Cleanup
113
+ Given.iTearDownMyApp();
114
+ });
115
+ }
116
+
117
+ runner.run([journey]);
@@ -0,0 +1,176 @@
1
+ /******************************************************************************
2
+ * ╔═══════════════════════════════════════════════════════════════════════╗ *
3
+ * ║ ║ *
4
+ * ║ WARNING: AUTO-GENERATED FILE ║ *
5
+ * ║ ║ *
6
+ * ║ This file is automatically generated by SAP Fiori tools and is ║ *
7
+ * ║ overwritten when you run the OPA test generator again. ║ *
8
+ * ║ ║ *
9
+ * ║ To add your own custom tests: ║ *
10
+ * ║ - Create a new journey test file in this directory. ║ *
11
+ * ║ - Follow the same pattern as this file. ║ *
12
+ * ║ - Add the new file to the opaTests.qunit.js config file. ║ *
13
+ * ║ - Custom journey files are not overwritten. ║ *
14
+ * ║ ║ *
15
+ * ╚═══════════════════════════════════════════════════════════════════════╝ *
16
+ ******************************************************************************/
17
+
18
+ import opaTest from "sap/ui/test/opaQunit";
19
+ import type { Given, When, Then } from "./types/OpaJourneyTypes";
20
+ <%_
21
+ const usesFieldIdentifier = (headerSections || []).some(function(section) {
22
+ return section.form && section.fields && section.fields.length > 0;
23
+ });
24
+ const usesFormIdentifier = !isStandalone && (bodySections || []).some(function(section) {
25
+ const subSectionsHaveForm = (section.subSections || []).some(function(sub) {
26
+ return sub.fields && sub.fields.length > 0;
27
+ });
28
+ const sectionHasFormFields = !(section.subSections && section.subSections.length > 0) && section.fields && section.fields.length > 0;
29
+ const hasFormAction = (section.actions || []).some(function(action) {
30
+ return action.visible && !(section.isTable && section.navigationProperty);
31
+ });
32
+ return subSectionsHaveForm || sectionHasFormFields || hasFormAction;
33
+ });
34
+ -%>
35
+ <% if (usesFieldIdentifier) { -%>
36
+ import type { FieldIdentifier } from "sap/fe/test/api/BaseAPI";
37
+ <% } -%>
38
+ <% if (usesFormIdentifier) { -%>
39
+ import type { FormIdentifier } from "sap/fe/test/api/FormAPI";
40
+ <% } -%>
41
+ import runner from "./pages/JourneyRunner";
42
+
43
+ function journey() {
44
+ QUnit.module("<%- name%>ObjectPage journey");
45
+
46
+ opaTest("Navigate to <%- name%>ObjectPage", function (Given: Given, When: When, Then: Then) {
47
+ Given.iStartMyApp();
48
+ <% if (!hideFilterBar) { %>
49
+ When.onThe<%- navigationParents.parentLRName%>.onFilterBar().iExecuteSearch();
50
+ <% } %>
51
+ Then.onThe<%- navigationParents.parentLRName%>.onTable("").iCheckRows();
52
+ When.onThe<%- navigationParents.parentLRName%>.onTable("").iPressRow(0);
53
+ <% if(navigationParents.parentOPName) { %>
54
+ Then.onThe<%- navigationParents.parentOPName%>.iSeeThisPage();
55
+ Then.onThe<%- navigationParents.parentOPName%>.onTable({ property: "<%- navigationParents.parentOPTableSection %>" }).iCheckRows();
56
+ When.onThe<%- navigationParents.parentOPName%>.onTable({ property: "<%- navigationParents.parentOPTableSection %>" }).iPressRow(0);
57
+ <% } %>
58
+ Then.onThe<%- name%>.iSeeThisPage();
59
+ });
60
+
61
+ <% if (headerActions?.length > 0 && !isStandalone) { -%>
62
+ opaTest("Check header actions of the Object Page", function (_Given: Given, _When: When, Then: Then) {
63
+ <% if (editButton?.visible) { -%>
64
+ // Ensure the opened entity is not in Draft state before uncommenting
65
+ // Then.onThe<%- name%>.onHeader().iCheckEdit({ visible: true });
66
+ // When.onThe<%- name%>.onHeader().iPressEdit();
67
+ <% } -%>
68
+ <% headerActions.forEach(function(action) { -%>
69
+ <% if (action.visible) { -%>
70
+ <% if (action.enabled === 'dynamic') { -%>
71
+ Then.onThe<%- name%>.onHeader().iCheckAction("<%- action.label %>" /* , { enabled: true } */);
72
+ <% } else { -%>
73
+ Then.onThe<%- name%>.onHeader().iCheckAction("<%- action.label %>", { enabled: <%- action.enabled === true %> });
74
+ <% } -%>
75
+ // When.onThe<%- name%>.onHeader().iPressAction("<%- action.label %>");
76
+ <% } -%>
77
+ <% }); -%>
78
+ });
79
+ <% } -%>
80
+
81
+ <% if (headerSections?.length > 0) { -%>
82
+ opaTest("Check header facets of the Object Page", function (_Given: Given, _When: When, Then: Then) {
83
+ <% headerSections.forEach(function(section) { -%>
84
+ <% if (section.microChart) { -%>
85
+ Then.onThe<%- name%>.onHeader().iCheckMicroChart("<%- section.title %>", "");
86
+ <% } else { -%>
87
+ Then.onThe<%- name%>.onHeader().iCheckHeaderFacet({ facetId: "<%- section.facetId %>" });
88
+ <% if (section.form) { -%>
89
+ <% section.fields.forEach(function(field) { -%>
90
+ Then.onThe<%- name%>.onHeader().iCheckFieldInFieldGroup({
91
+ fieldGroup: "FieldGroup::<%- field.fieldGroupQualifier %>",
92
+ field: "<%- field.field %>",
93
+ targetAnnotation: "<%- field.targetAnnotation %>"
94
+ } as unknown as FieldIdentifier);
95
+ <% }) -%>
96
+ <% } -%>
97
+ <% } -%>
98
+ <% }) -%>
99
+ });
100
+ <% } -%>
101
+
102
+ <% if (bodySections?.length > 0 && !isStandalone) { -%>
103
+ opaTest("Check body sections of the Object Page", function (_Given: Given, <% if (bodySections?.length > 1) { %>When: When<% } else { %>_When: When<% } %>, Then: Then) {
104
+ <% if (bodySections?.length > 1) { -%>
105
+ Then.onThe<%- name%>.iCheckNumberOfSections(<%- bodySections.length %>);
106
+ <% } -%>
107
+ <% bodySections.forEach(function(section) { -%>
108
+ <% if (bodySections.length > 1) { -%>
109
+ When.onThe<%- name%>.iPressSectionIconTabFilterButton("<%- section.id %>");
110
+ <% } -%>
111
+ Then.onThe<%- name%>.iCheckSection({ section: "<%- section.id %>" }, {});
112
+ <% if (section.actions && section.actions.length > 0) { -%>
113
+ <% section.actions.forEach(function(action) { -%>
114
+ <% if (action.visible) { -%>
115
+ <% if (section.isTable && section.navigationProperty) { -%>
116
+ <% if (action.enabled === 'dynamic') { -%>
117
+ Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckAction("<%- action.label %>" /* , { enabled: true } */);
118
+ <% } else { -%>
119
+ Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckAction("<%- action.label %>", { enabled: <%- action.enabled === true %> });
120
+ <% } -%>
121
+ // When.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iPressAction("<%- action.label %>");
122
+ <% } else { -%>
123
+ <% if (action.enabled === 'dynamic') { -%>
124
+ Then.onThe<%- name%>.onForm({ section: "<%- section.id %>" } as unknown as FormIdentifier).iCheckAction("<%- action.label %>" /* , { enabled: true } */);
125
+ <% } else { -%>
126
+ Then.onThe<%- name%>.onForm({ section: "<%- section.id %>" } as unknown as FormIdentifier).iCheckAction("<%- action.label %>", { enabled: <%- action.enabled === true %> });
127
+ <% } -%>
128
+ // When.onThe<%- name%>.onForm({ section: "<%- section.id %>" } as unknown as FormIdentifier).iPressAction("<%- action.label %>");
129
+ <% } -%>
130
+ <% } -%>
131
+ <% }); -%>
132
+ <% } -%>
133
+ <% if (section.isTable && section.navigationProperty) { -%>
134
+ <% if (section.createButton?.visible) { -%>
135
+ Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckCreate({ visible: true });
136
+ // When.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iPressCreate();
137
+ <% } -%>
138
+ <% if (section.deleteButton?.visible) { -%>
139
+ Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckDelete({ visible: true });
140
+ // When.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iPressDelete();
141
+ <% } -%>
142
+ <% } -%>
143
+ <% if (section?.subSections?.length > 0) { -%>
144
+ <% section.subSections.forEach(function(subSection) { -%>
145
+ //When.onThe<%- name%>.iGoToSection({ section: "<%- section.id %>", subSection: "<%- subSection.id %>" });
146
+ Then.onThe<%- name%>.iCheckSubSection({ section: "<%- subSection.id %>" });
147
+ <% if (subSection.fields && subSection.fields.length > 0) { -%>
148
+ <% subSection.fields.forEach(function(field) { -%>
149
+ Then.onThe<%- name%>.onForm({ section: "<%- subSection.id %>" } as unknown as FormIdentifier).iCheckField({ property: "<%- field.property %>" });
150
+ <% }) -%>
151
+ <% } -%>
152
+ <% if (subSection.tableColumns && Object.keys(subSection.tableColumns).length > 0 && subSection.navigationProperty) { -%>
153
+ Then.onThe<%- name%>.onTable({ property: "<%- subSection.navigationProperty %>" }).iCheckColumns(<%- JSON.stringify(subSection.tableColumns) %>);
154
+ <% } -%>
155
+ <% }) -%>
156
+ <% } else { -%>
157
+ <% if (section.fields && section.fields.length > 0) { -%>
158
+ <% section.fields.forEach(function(field) { -%>
159
+ Then.onThe<%- name%>.onForm({ section: "<%- section.id %>" } as unknown as FormIdentifier).iCheckField({ property: "<%- field.property %>" });
160
+ <% }) -%>
161
+ <% } -%>
162
+ <% if (section.tableColumns && Object.keys(section.tableColumns).length > 0 && section.navigationProperty) { -%>
163
+ Then.onThe<%- name%>.onTable({ property: "<%- section.navigationProperty %>" }).iCheckColumns(<%- JSON.stringify(section.tableColumns) %>);
164
+ <% } -%>
165
+ <% } -%>
166
+ <% }) -%>
167
+ });
168
+ <% } -%>
169
+
170
+ opaTest("Teardown", function (Given: Given) {
171
+ // Cleanup
172
+ Given.iTearDownMyApp();
173
+ });
174
+ }
175
+
176
+ runner.run([journey]);
@@ -0,0 +1,28 @@
1
+ import JourneyRunner from "sap/fe/test/JourneyRunner";
2
+ <% if (pages.some((p) => p.template === 'ListReport')) { -%>
3
+ import ListReport from "sap/fe/test/ListReport";
4
+ <% } -%>
5
+ <% if (pages.some((p) => p.template === 'ObjectPage')) { -%>
6
+ import ObjectPage from "sap/fe/test/ObjectPage";
7
+ <% } -%>
8
+ <%- pages.map((page) => 'import Custom' + page.targetKey + ' from "./' + page.targetKey + '";').join('\n') %>
9
+
10
+ const runner = new JourneyRunner({
11
+ launchUrl: sap.ui.require.toUrl("<%- appPath %>") + "/<%- htmlTarget %>",
12
+ pages: {
13
+ <%- pages.map((page) =>
14
+ ' onThe' + page.targetKey + ': new ' + page.template + '(\n' +
15
+ ' {\n' +
16
+ ' appId: "' + page.appID + '",\n' +
17
+ ' componentId: "' + page.componentID + '",\n' +
18
+ ' entitySet: "' + (page.entitySet || '') + '",\n' +
19
+ ' contextPath: "' + (page.contextPath || '') + '"\n' +
20
+ ' },\n' +
21
+ ' Custom' + page.targetKey + '\n' +
22
+ ' )'
23
+ ).join(',\n') %>
24
+ },
25
+ async: true
26
+ });
27
+
28
+ export default runner;
@@ -0,0 +1,8 @@
1
+ export const actions = {};
2
+
3
+ export const assertions = {};
4
+
5
+ export default class ListReport {
6
+ actions = actions;
7
+ assertions = assertions;
8
+ }
@@ -0,0 +1,18 @@
1
+ import type Opa5 from "sap/ui/test/Opa5";
2
+ import Press from "sap/ui/test/actions/Press";
3
+
4
+ export const actions = {
5
+ iPressSectionIconTabFilterButton(this: Opa5, section: string) {
6
+ return this.waitFor({
7
+ id: new RegExp(`.*--fe::FacetSection::${section}-anchor$`),
8
+ actions: new Press()
9
+ });
10
+ }
11
+ };
12
+
13
+ export const assertions = {};
14
+
15
+ export default class ObjectPage {
16
+ actions = actions;
17
+ assertions = assertions;
18
+ }
@@ -0,0 +1,43 @@
1
+ import type Opa5 from "sap/ui/test/Opa5";
2
+ <% if (pages.some(p => p.template === 'ListReport')) { -%>
3
+ import type { actions as ListReportActions, assertions as ListReportAssertions } from "sap/fe/test/ListReport";
4
+ <% } -%>
5
+ <% if (pages.some(p => p.template === 'ObjectPage')) { -%>
6
+ import type { actions as ObjectPageActions, assertions as ObjectPageAssertions } from "sap/fe/test/ObjectPage";
7
+ <% } -%>
8
+ <% if (pages.some(p => p.template === 'ListReport' || p.template === 'ObjectPage')) { -%>
9
+ import type { actions as TemplatePageActions, assertions as TemplatePageAssertions } from "sap/fe/test/TemplatePage";
10
+ <% } -%>
11
+ import type Shell from "sap/fe/test/Shell";
12
+ import type BaseArrangements from "sap/fe/test/BaseArrangements";
13
+ <% pages.filter((p) => p.template === 'ListReport' || p.template === 'ObjectPage').forEach(function(page) { -%>
14
+ import type { actions as <%- page.targetKey %>CustomActions, assertions as <%- page.targetKey %>CustomAssertions } from "../pages/<%- page.targetKey %>";
15
+ <% }); -%>
16
+
17
+ export type Given = Opa5 & BaseArrangements & {
18
+ iTearDownMyApp: () => Given;
19
+ iStartMyApp: (sAppHash?: string, mInUrlParameters?: object) => Given;
20
+ and: Given;
21
+ };
22
+
23
+ export type When = Opa5 & BaseArrangements & {
24
+ <% pages.forEach(function(page) { -%>
25
+ <% if (page.template === 'ListReport') { -%>
26
+ onThe<%- page.targetKey %>: Opa5 & ListReportActions & TemplatePageActions & typeof <%- page.targetKey %>CustomActions;
27
+ <% } else if (page.template === 'ObjectPage') { -%>
28
+ onThe<%- page.targetKey %>: Opa5 & ObjectPageActions & TemplatePageActions & typeof <%- page.targetKey %>CustomActions;
29
+ <% } -%>
30
+ <% }); -%>
31
+ onTheShell: Shell;
32
+ };
33
+
34
+ export type Then = Opa5 & BaseArrangements & {
35
+ <% pages.forEach(function(page) { -%>
36
+ <% if (page.template === 'ListReport') { -%>
37
+ onThe<%- page.targetKey %>: Opa5 & ListReportAssertions & TemplatePageAssertions & typeof <%- page.targetKey %>CustomAssertions;
38
+ <% } else if (page.template === 'ObjectPage') { -%>
39
+ onThe<%- page.targetKey %>: Opa5 & ObjectPageAssertions & TemplatePageAssertions & typeof <%- page.targetKey %>CustomAssertions;
40
+ <% } -%>
41
+ <% }); -%>
42
+ onTheShell: Shell;
43
+ };