@sap-ux/ui5-test-writer 1.1.13 → 1.2.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.
Files changed (31) hide show
  1. package/dist/fiori-elements-opa-writer.js +360 -143
  2. package/dist/index.d.ts +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/translations/ui5-test-writer.i18n.json +10 -0
  5. package/dist/types.d.ts +10 -0
  6. package/dist/utils/fileWritingUtils.d.ts +55 -0
  7. package/dist/utils/fileWritingUtils.js +103 -0
  8. package/dist/utils/journeyRunnerUtils.d.ts +64 -0
  9. package/dist/utils/journeyRunnerUtils.js +251 -0
  10. package/dist/utils/opaJourneyTypesUtils.d.ts +27 -0
  11. package/dist/utils/opaJourneyTypesUtils.js +167 -0
  12. package/dist/utils/opaQUnitUtils.d.ts +6 -92
  13. package/dist/utils/opaQUnitUtils.js +20 -374
  14. package/dist/utils/virtualOpaUtils.d.ts +21 -0
  15. package/dist/utils/virtualOpaUtils.js +51 -0
  16. package/package.json +2 -1
  17. package/templates/v4/integration/FPMJourney.js +3 -3
  18. package/templates/v4/integration/FirstJourney.ts +5 -5
  19. package/templates/v4/integration/ListReportJourney.js +25 -25
  20. package/templates/v4/integration/ListReportJourney.ts +25 -25
  21. package/templates/v4/integration/ObjectPageJourney.js +37 -37
  22. package/templates/v4/integration/ObjectPageJourney.ts +38 -38
  23. package/templates/v4/integration/opaTests.qunit.js +5 -2
  24. package/templates/v4/integration/pages/FPM.js +17 -0
  25. package/templates/v4/integration/pages/JourneyRunner.js +6 -6
  26. package/templates/v4/integration/pages/JourneyRunner.ts +21 -12
  27. package/templates/v4/integration/pages/ListReport.js +17 -0
  28. package/templates/v4/integration/pages/ListReport.ts +17 -0
  29. package/templates/v4/integration/pages/ObjectPage.js +17 -0
  30. package/templates/v4/integration/pages/ObjectPage.ts +17 -0
  31. package/templates/v4/integration/types/OpaJourneyTypes.d.ts +18 -5
@@ -5,34 +5,15 @@
5
5
  */
6
6
  import { join } from 'node:path';
7
7
  import { readHashFromFlpSandbox } from './flpSandboxUtils.js';
8
- import { getAllUi5YamlFileNames, readUi5Yaml, FileName } from '@sap-ux/project-access';
9
- import { DotFileExtension } from '../types.js';
8
+ import { MAX_FILE_CONTENT_LENGTH } from './fileWritingUtils.js';
9
+ import { t } from '../i18n.js';
10
10
  /** Relative path from the test output directory to opaTests.qunit.js */
11
11
  const OPA_QUNIT_FILE = join('integration', 'opaTests.qunit.js');
12
12
  /**
13
- * The regex matches the opening bracket of the sap.ui.require array and
14
- * captures everything up to (but not including) the closing bracket followed
15
- * by `], function`. This lets us splice new entries in without disturbing
16
- * any other part of the file.
17
- *
18
- * Matches:
19
- * sap.ui.require(\n [\n "a",\n "b",\n ], function
20
- * ^^^^^^^^^^^^^^^^^
21
- * captured as group 1
22
- *
23
- * The `d` flag enables `match.indices` so we can read the capture group's
24
- * exact start/end positions without fragile string searching.
13
+ * Captures the body of the `sap.ui.require([...], function ...)` array (group 1).
14
+ * The `d` flag exposes `match.indices` for precise splice positions.
25
15
  */
26
16
  const SAP_UI_REQUIRE_ARRAY_REGEX = /sap\.ui\.require\s*\(\s*\[([^\]]*)\]\s*,\s*function/d;
27
- /** ReDoS mitigation: files larger than this are returned unchanged rather than matched with regex. */
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 `\$&`);
36
17
  /**
37
18
  * Splices new module paths into the sap.ui.require array of the content string.
38
19
  * Entries that are already present are skipped. All other content is preserved exactly.
@@ -80,7 +61,8 @@ export function spliceModulesIntoQUnitContent(fileContent, moduleNames) {
80
61
  const commaFix = needsComma ? ',' : '';
81
62
  const trailingWhitespace = fileContent.slice(trimmedBefore.length, insertPosition);
82
63
  const after = fileContent.slice(insertPosition);
83
- return `${trimmedBefore}${commaFix}\n${newLines}\n${trailingWhitespace}${after}`;
64
+ const separator = trailingWhitespace.length > 0 ? '' : '\n';
65
+ return `${trimmedBefore}${commaFix}\n${newLines}${separator}${trailingWhitespace}${after}`;
84
66
  }
85
67
  /**
86
68
  * Regex to extract the html launch target from a `launchUrl` line of the form:
@@ -89,7 +71,7 @@ export function spliceModulesIntoQUnitContent(fileContent, moduleNames) {
89
71
  */
90
72
  const LAUNCH_URL_REGEX = /\.toUrl\s*\([^)]+\)\s*\+\s*'([^']+)'/;
91
73
  /**
92
- * Reads opaTests.qunit.js from webapp/test/integration_old and extracts the html
74
+ * Reads opaTests.qunit.js from webapp/test/integration and extracts the html
93
75
  * launch target (path, query parameters, and hash fragment) from the launchUrl
94
76
  * line, e.g. `test/flpSandbox.html?sap-ui-xx-viewCache=false#myApp-tile`.
95
77
  * Returns undefined if the file cannot be read or the pattern is not found.
@@ -100,10 +82,10 @@ const LAUNCH_URL_REGEX = /\.toUrl\s*\([^)]+\)\s*\+\s*'([^']+)'/;
100
82
  */
101
83
  export function readHtmlTargetFromQUnitJs(testPath, fs) {
102
84
  try {
103
- const integrationOldDir = join(testPath, 'integration_old');
104
- let filePath = join(integrationOldDir, 'opaTests.qunit.js');
85
+ const integrationDir = join(testPath, 'integration');
86
+ let filePath = join(integrationDir, 'opaTests.qunit.js');
105
87
  if (!fs.exists(filePath)) {
106
- filePath = join(integrationOldDir, 'Opa.qunit.js');
88
+ filePath = join(integrationDir, 'Opa.qunit.js');
107
89
  }
108
90
  const content = fs.read(filePath);
109
91
  const match = LAUNCH_URL_REGEX.exec(content);
@@ -125,27 +107,6 @@ export function readHtmlTargetFromQUnitJs(testPath, fs) {
125
107
  return undefined;
126
108
  }
127
109
  }
128
- /** The gitignore entry added for the moved integration test folder */
129
- const INTEGRATION_OLD_GITIGNORE_ENTRY = '/webapp/test/integration_old';
130
- /**
131
- * Appends `/webapp/test/integration_old` to the project's `.gitignore`.
132
- * Creates the file if it does not exist. Skips if the entry is already present.
133
- *
134
- * @param basePath - project root (contains .gitignore)
135
- * @param fs - mem-fs-editor instance used to read and write the file
136
- */
137
- export async function addIntegrationOldToGitignore(basePath, fs) {
138
- const filePath = join(basePath, '.gitignore');
139
- const existing = fs.exists(filePath) ? fs.read(filePath) : '';
140
- const lines = existing.split('\n');
141
- if (lines.some((line) => line.trim() === INTEGRATION_OLD_GITIGNORE_ENTRY)) {
142
- return;
143
- }
144
- const updated = existing.endsWith('\n') || existing === ''
145
- ? `${existing}${INTEGRATION_OLD_GITIGNORE_ENTRY}\n`
146
- : `${existing}\n${INTEGRATION_OLD_GITIGNORE_ENTRY}\n`;
147
- fs.write(filePath, updated);
148
- }
149
110
  /**
150
111
  * Reads opaTests.qunit.js from the project, adds module paths to the
151
112
  * sap.ui.require array, and writes the updated content back.
@@ -155,342 +116,27 @@ export async function addIntegrationOldToGitignore(basePath, fs) {
155
116
  * @param filePaths - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
156
117
  * @param projectPath - path to the test output directory (`.../webapp/test`)
157
118
  * @param fs - mem-fs-editor instance used to read and write the file
119
+ * @param log - optional logger instance used to surface warnings when the file
120
+ * cannot be read or updated
121
+ * @returns true if the file was written, false otherwise
158
122
  */
159
- export function addPathsToQUnitJs(filePaths, projectPath, fs) {
123
+ export function addPathsToQUnitJs(filePaths, projectPath, fs, log) {
160
124
  try {
161
125
  const filePath = join(projectPath, OPA_QUNIT_FILE);
162
126
  const content = fs.read(filePath);
163
- const updated = spliceModulesIntoQUnitContent(content, filePaths);
164
- if (updated !== content) {
165
- fs.write(filePath, updated);
166
- }
167
- }
168
- catch {
169
- // If the file doesn't exist or can't be read, do nothing
170
- }
171
- }
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}`);
179
- /**
180
- * Splices new page entries into the three locations of an existing JourneyRunner.js:
181
- * - the sap.ui.define dependency array
182
- * - the function parameter list
183
- * - the pages object literal
184
- *
185
- * Pages already present (detected by their module path in the define array) are skipped.
186
- * All other content — formatting, comments, whitespace — is preserved exactly.
187
- *
188
- * Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
189
- * ReDoS on crafted inputs. Valid generated files are well within this limit.
190
- *
191
- * @param fileContent - the full content of the JourneyRunner.js file
192
- * @param pages - pages to add
193
- * @returns the updated file content, or the original content unchanged if nothing was added
194
- */
195
- export function splicePageIntoJourneyRunner(fileContent, pages) {
196
- if (fileContent.length > MAX_FILE_CONTENT_LENGTH) {
197
- return fileContent;
198
- }
199
- // Determine which pages are not yet present by checking the define array
200
- const toAdd = pages.filter((page) => {
201
- const modulePath = `${page.appPath}/test/integration/pages/${page.targetKey}`;
202
- return !fileContent.includes(`"${modulePath}"`);
203
- });
204
- if (toAdd.length === 0) {
205
- return fileContent;
206
- }
207
- let result = fileContent;
208
- // 1. Splice into the sap.ui.define([...]) array.
209
- // Captures everything between the opening `[` and the closing `]` before `, function`.
210
- const defineArrayRegex = /sap\.ui\.define\s*\(\s*\[([^\]]*)\]\s*,\s*function/d;
211
- const defineMatch = defineArrayRegex.exec(result);
212
- if (defineMatch?.indices?.[1]) {
213
- const [bodyStart, bodyEnd] = defineMatch.indices[1];
214
- const arrayBody = result.slice(bodyStart, bodyEnd);
215
- // Detect indentation from the first existing module entry line
216
- const indentMatch = /^([ \t]+)"/m.exec(arrayBody);
217
- const indent = indentMatch ? indentMatch[1] : '\t';
218
- const newEntries = toAdd
219
- .map((page) => `${indent}"${page.appPath}/test/integration/pages/${page.targetKey}",`)
220
- .join('\n');
221
- // Ensure the last existing entry ends with a comma before we insert after it.
222
- // The trimmed body ends at bodyEnd; look back from there for the last non-whitespace char.
223
- const trimmedEnd = result.slice(0, bodyEnd).trimEnd();
224
- const needsComma = !trimmedEnd.endsWith(',');
225
- const commaFix = needsComma ? ',' : '';
226
- const trailingWhitespace = result.slice(trimmedEnd.length, bodyEnd);
227
- result = `${trimmedEnd}${commaFix}\n${newEntries}` + `${trailingWhitespace}${result.slice(bodyEnd)}`;
228
- }
229
- // 2. Splice into the function parameter list: `function (JourneyRunner, A, B)`.
230
- // Captures everything between `(` and `)` of the function signature.
231
- const funcParamRegex = /\]\s*,\s*function\s*\(([^)]*)\)\s*\{/d;
232
- const funcMatch = funcParamRegex.exec(result);
233
- if (funcMatch?.indices?.[1]) {
234
- const [, paramEnd] = funcMatch.indices[1];
235
- const newParams = toAdd.map((page) => `, ${page.targetKey}`).join('');
236
- result = `${result.slice(0, paramEnd)}${newParams}${result.slice(paramEnd)}`;
237
- }
238
- // 3. Splice into the pages object: `pages: { onTheFoo: Foo, ... }`.
239
- // Captures everything between `pages: {` and the closing `}`.
240
- const pagesObjectRegex = /pages\s*:\s*\{([^}]*)\}/d;
241
- const pagesMatch = pagesObjectRegex.exec(result);
242
- if (pagesMatch?.indices?.[1]) {
243
- const [, pagesBodyEnd] = pagesMatch.indices[1];
244
- const pagesBody = result.slice(pagesMatch.indices[1][0], pagesBodyEnd);
245
- // Detect indentation from the first existing page entry
246
- const pageIndentMatch = /^([ \t]+)on/m.exec(pagesBody);
247
- const pageIndent = pageIndentMatch ? pageIndentMatch[1] : '\t\t\t';
248
- const newPageEntries = toAdd
249
- .map((page) => `${pageIndent}onThe${page.targetKey}: ${page.targetKey},`)
250
- .join('\n');
251
- // Ensure the last existing entry ends with a comma before we insert after it.
252
- const trimmedPagesEnd = result.slice(0, pagesBodyEnd).trimEnd();
253
- const needsComma = !trimmedPagesEnd.endsWith(',');
254
- const commaFix = needsComma ? ',' : '';
255
- const trailingWhitespace = result.slice(trimmedPagesEnd.length, pagesBodyEnd);
256
- result =
257
- `${trimmedPagesEnd}${commaFix}\n${newPageEntries}` + `${trailingWhitespace}${result.slice(pagesBodyEnd)}`;
258
- }
259
- return result;
260
- }
261
- /**
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;
127
+ if (content.length > MAX_FILE_CONTENT_LENGTH) {
128
+ log?.warn(t('warn.cannotUpdateOpaTestsQunit'));
129
+ return false;
317
130
  }
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`.
428
- *
429
- * @param pages - pages to add
430
- * @param testOutDirPath - path to the test output directory (`.../webapp/test`)
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'
433
- */
434
- export function addPagesToJourneyRunner(pages, testOutDirPath, fs, dotFileExtension = DotFileExtension.JS) {
435
- if (pages.length === 0) {
436
- return;
437
- }
438
- try {
439
- const filePath = join(testOutDirPath, getJourneyRunnerFilePath(dotFileExtension));
440
- const content = fs.read(filePath);
441
- const splice = dotFileExtension === DotFileExtension.TS ? splicePageIntoJourneyRunnerTs : splicePageIntoJourneyRunner;
442
- const updated = splice(content, pages);
131
+ const updated = spliceModulesIntoQUnitContent(content, filePaths);
443
132
  if (updated !== content) {
444
133
  fs.write(filePath, updated);
134
+ return true;
445
135
  }
446
136
  }
447
137
  catch {
448
- // If the file doesn't exist or can't be read, do nothing
449
- }
450
- }
451
- /**
452
- * Returns true if any UI5 yaml file in the project contains a `fiori-tools-preview`
453
- * middleware whose `test` array includes an entry with `framework: OPA5`.
454
- *
455
- * @param basePath - project root directory
456
- * @returns true when OPA5 is configured in a preview middleware, false otherwise
457
- */
458
- export async function hasVirtualOPA5(basePath) {
459
- const yamlFileNames = await getAllUi5YamlFileNames(basePath);
460
- for (const fileName of yamlFileNames) {
461
- try {
462
- const ui5Config = await readUi5Yaml(basePath, fileName);
463
- const previewMiddleware = ui5Config.findCustomMiddleware('fiori-tools-preview');
464
- const testEntries = previewMiddleware?.configuration?.test;
465
- if (testEntries?.some((entry) => entry.framework === 'OPA5')) {
466
- return true;
467
- }
468
- }
469
- catch {
470
- // Skip yaml files that cannot be read
471
- }
138
+ log?.warn(t('warn.cannotUpdateOpaTestsQunit'));
472
139
  }
473
140
  return false;
474
141
  }
475
- /**
476
- * Updates the fiori-tools-preview middleware in ui5-mock.yaml to support virtual OPA test endpoints.
477
- * Adds test framework entries to ui5-mock.yaml.
478
- *
479
- * @param basePath - the absolute target path of the application
480
- * @param testFrameworks - the test framework entries to add to ui5-mock.yaml
481
- * @param fs - the memfs editor instance
482
- */
483
- export async function addVirtualTestConfig(basePath, testFrameworks, fs) {
484
- const yamlPath = join(basePath, FileName.Ui5MockYaml);
485
- if (!fs.exists(yamlPath)) {
486
- return;
487
- }
488
- const yamlConfig = await readUi5Yaml(basePath, FileName.Ui5MockYaml, fs);
489
- const previewMiddleware = yamlConfig.findCustomMiddleware('fiori-tools-preview');
490
- if (previewMiddleware?.configuration && !previewMiddleware.configuration.test?.length) {
491
- previewMiddleware.configuration.test = [...testFrameworks];
492
- yamlConfig.updateCustomMiddleware(previewMiddleware);
493
- fs.write(yamlPath, yamlConfig.toString());
494
- }
495
- }
496
142
  //# sourceMappingURL=opaQUnitUtils.js.map
@@ -0,0 +1,21 @@
1
+ import type { Editor } from 'mem-fs-editor';
2
+ import type { TestConfig as PreviewMiddlewareTestConfig } from '@sap-ux/preview-middleware';
3
+ /**
4
+ * Returns true if any UI5 yaml file in the project has a preview middleware
5
+ * (`fiori-tools-preview` or `preview-middleware`) whose `test` array includes an entry
6
+ * with `framework: OPA5`.
7
+ *
8
+ * @param basePath - project root directory
9
+ * @returns true when OPA5 is configured in a preview middleware, false otherwise
10
+ */
11
+ export declare function hasVirtualOPA5(basePath: string): Promise<boolean>;
12
+ /**
13
+ * Updates the fiori-tools-preview middleware in ui5-mock.yaml to support virtual OPA test endpoints.
14
+ * Adds test framework entries to ui5-mock.yaml.
15
+ *
16
+ * @param basePath - the absolute target path of the application
17
+ * @param testFrameworks - the test framework entries to add to ui5-mock.yaml
18
+ * @param fs - the memfs editor instance
19
+ */
20
+ export declare function addVirtualTestConfig(basePath: string, testFrameworks: PreviewMiddlewareTestConfig[], fs: Editor): Promise<void>;
21
+ //# sourceMappingURL=virtualOpaUtils.d.ts.map
@@ -0,0 +1,51 @@
1
+ import { join } from 'node:path';
2
+ import { getAllUi5YamlFileNames, readUi5Yaml, FileName } from '@sap-ux/project-access';
3
+ /** Custom middleware names that may carry a virtual-test config. */
4
+ const PREVIEW_MIDDLEWARE_NAMES = ['fiori-tools-preview', 'preview-middleware'];
5
+ /**
6
+ * Returns true if any UI5 yaml file in the project has a preview middleware
7
+ * (`fiori-tools-preview` or `preview-middleware`) whose `test` array includes an entry
8
+ * with `framework: OPA5`.
9
+ *
10
+ * @param basePath - project root directory
11
+ * @returns true when OPA5 is configured in a preview middleware, false otherwise
12
+ */
13
+ export async function hasVirtualOPA5(basePath) {
14
+ const yamlFileNames = await getAllUi5YamlFileNames(basePath);
15
+ for (const fileName of yamlFileNames) {
16
+ try {
17
+ const ui5Config = await readUi5Yaml(basePath, fileName);
18
+ const previewMiddleware = PREVIEW_MIDDLEWARE_NAMES.map((name) => ui5Config.findCustomMiddleware(name)).find((middleware) => middleware !== undefined);
19
+ const testEntries = previewMiddleware?.configuration?.test;
20
+ if (testEntries?.some((entry) => entry.framework === 'OPA5')) {
21
+ return true;
22
+ }
23
+ }
24
+ catch {
25
+ // Skip yaml files that cannot be read
26
+ }
27
+ }
28
+ return false;
29
+ }
30
+ /**
31
+ * Updates the fiori-tools-preview middleware in ui5-mock.yaml to support virtual OPA test endpoints.
32
+ * Adds test framework entries to ui5-mock.yaml.
33
+ *
34
+ * @param basePath - the absolute target path of the application
35
+ * @param testFrameworks - the test framework entries to add to ui5-mock.yaml
36
+ * @param fs - the memfs editor instance
37
+ */
38
+ export async function addVirtualTestConfig(basePath, testFrameworks, fs) {
39
+ const yamlPath = join(basePath, FileName.Ui5MockYaml);
40
+ if (!fs.exists(yamlPath)) {
41
+ return;
42
+ }
43
+ const yamlConfig = await readUi5Yaml(basePath, FileName.Ui5MockYaml, fs);
44
+ const previewMiddleware = yamlConfig.findCustomMiddleware('fiori-tools-preview');
45
+ if (previewMiddleware?.configuration && !previewMiddleware.configuration.test?.length) {
46
+ previewMiddleware.configuration.test = [...testFrameworks];
47
+ yamlConfig.updateCustomMiddleware(previewMiddleware);
48
+ fs.write(yamlPath, yamlConfig.toString());
49
+ }
50
+ }
51
+ //# sourceMappingURL=virtualOpaUtils.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": "1.1.13",
4
+ "version": "1.2.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/SAP/open-ux-tools.git",
@@ -57,6 +57,7 @@
57
57
  "lint:fix": "eslint --fix",
58
58
  "test": "cross-env NODE_OPTIONS='--experimental-vm-modules' jest --ci --forceExit --detectOpenHandles --colors",
59
59
  "test-u": "cross-env NODE_OPTIONS='--experimental-vm-modules' jest --ci --forceExit --detectOpenHandles --colors -u",
60
+ "run-generator": "npm run build && npx ts-node --esm test/manual/run-generator.ts",
60
61
  "link": "pnpm link --global",
61
62
  "unlink": "pnpm unlink --global"
62
63
  }
@@ -27,14 +27,14 @@ sap.ui.define([
27
27
  opaTest("Start application", function (Given, When, Then) {
28
28
  Given.iStartMyApp();
29
29
  <%_ startPages.forEach(function(pageName) { %>
30
- Then.onThe<%- pageName %>.iSeeThisPage();
30
+ Then.onThe<%- pageName %>Generated.iSeeThisPage();
31
31
  <%_ if (filterBarItems && filterBarItems.length > 0) { -%>
32
32
  <%_ filterBarItems.forEach(function(item) { _%>
33
- Then.onThe<%- pageName%>.onFilterBar().iCheckFilterField("<%- item %>");
33
+ Then.onThe<%- pageName%>Generated.onFilterBar().iCheckFilterField("<%- item %>");
34
34
  <%_ }); -%>
35
35
  <%_ } -%>
36
36
  <%_ if (tableColumns && Object.keys(tableColumns).length > 0) { _%>
37
- Then.onThe<%- pageName %>.onTable().iCheckColumns(<%- Object.keys(tableColumns).length %>, <%- JSON.stringify(tableColumns) %>);
37
+ Then.onThe<%- pageName %>Generated.onTable().iCheckColumns(<%- Object.keys(tableColumns).length %>, <%- JSON.stringify(tableColumns) %>);
38
38
  <%_ } %>
39
39
  <%_ }); -%>
40
40
  });
@@ -7,19 +7,19 @@ function journey() {
7
7
 
8
8
  opaTest("Start application", function (Given: Given, _When: When<% if (startLR) { %>, Then: Then<% } else { %>, _Then: Then<% } %>) {
9
9
  Given.iStartMyApp();
10
- <% if (startLR) { %>Then.onThe<%- startLR %>.iSeeThisPage();<%} %>
10
+ <% if (startLR) { %>Then.onThe<%- startLR %>Generated.iSeeThisPage();<%} %>
11
11
  });
12
12
 
13
13
  <% if (startLR) { %>
14
14
  opaTest("Navigate to ObjectPage", function (_Given: Given, When: When, Then: Then) {
15
15
  // Note: this test will fail if the ListReport page doesn't show any data
16
16
  <% if (!hideFilterBar) { %>
17
- When.onThe<%- startLR%>.onFilterBar().iExecuteSearch();
17
+ When.onThe<%- startLR%>Generated.onFilterBar().iExecuteSearch();
18
18
  <%} %>
19
- Then.onThe<%- startLR%>.onTable("").iCheckRows();
19
+ Then.onThe<%- startLR%>Generated.onTable("").iCheckRows();
20
20
  <% if (navigatedOP) { %>
21
- When.onThe<%- startLR%>.onTable("").iPressRow(0);
22
- Then.onThe<%- navigatedOP%>.iSeeThisPage();
21
+ When.onThe<%- startLR%>Generated.onTable("").iPressRow(0);
22
+ Then.onThe<%- navigatedOP%>Generated.iSeeThisPage();
23
23
  <%} %>
24
24
  });
25
25
  <%} %>