@sap-ux/ui5-test-writer 1.1.13 → 1.2.0
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/dist/fiori-elements-opa-writer.js +360 -143
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/translations/ui5-test-writer.i18n.json +10 -0
- package/dist/types.d.ts +10 -0
- package/dist/utils/fileWritingUtils.d.ts +55 -0
- package/dist/utils/fileWritingUtils.js +103 -0
- package/dist/utils/journeyRunnerUtils.d.ts +64 -0
- package/dist/utils/journeyRunnerUtils.js +251 -0
- package/dist/utils/opaJourneyTypesUtils.d.ts +27 -0
- package/dist/utils/opaJourneyTypesUtils.js +167 -0
- package/dist/utils/opaQUnitUtils.d.ts +6 -92
- package/dist/utils/opaQUnitUtils.js +20 -374
- package/dist/utils/virtualOpaUtils.d.ts +21 -0
- package/dist/utils/virtualOpaUtils.js +51 -0
- package/package.json +2 -1
- package/templates/v4/integration/FPMJourney.js +3 -3
- package/templates/v4/integration/FirstJourney.ts +5 -5
- package/templates/v4/integration/ListReportJourney.js +25 -25
- package/templates/v4/integration/ListReportJourney.ts +25 -25
- package/templates/v4/integration/ObjectPageJourney.js +37 -37
- package/templates/v4/integration/ObjectPageJourney.ts +38 -38
- package/templates/v4/integration/opaTests.qunit.js +5 -2
- package/templates/v4/integration/pages/FPM.js +17 -0
- package/templates/v4/integration/pages/JourneyRunner.js +6 -6
- package/templates/v4/integration/pages/JourneyRunner.ts +21 -12
- package/templates/v4/integration/pages/ListReport.js +17 -0
- package/templates/v4/integration/pages/ObjectPage.js +17 -0
- package/templates/v4/integration/types/OpaJourneyTypes.d.ts +5 -5
|
@@ -5,34 +5,15 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { readHashFromFlpSandbox } from './flpSandboxUtils.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
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/
|
|
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
|
|
104
|
-
let filePath = join(
|
|
85
|
+
const integrationDir = join(testPath, 'integration');
|
|
86
|
+
let filePath = join(integrationDir, 'opaTests.qunit.js');
|
|
105
87
|
if (!fs.exists(filePath)) {
|
|
106
|
-
filePath = join(
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
4
|
+
"version": "1.2.0",
|
|
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
|
|
30
|
+
Then.onThe<%- pageName %>Generated.iSeeThisPage();
|
|
31
31
|
<%_ if (filterBarItems && filterBarItems.length > 0) { -%>
|
|
32
32
|
<%_ filterBarItems.forEach(function(item) { _%>
|
|
33
|
-
Then.onThe<%- pageName
|
|
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
|
|
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
|
|
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
|
|
17
|
+
When.onThe<%- startLR%>Generated.onFilterBar().iExecuteSearch();
|
|
18
18
|
<%} %>
|
|
19
|
-
Then.onThe<%- startLR
|
|
19
|
+
Then.onThe<%- startLR%>Generated.onTable("").iCheckRows();
|
|
20
20
|
<% if (navigatedOP) { %>
|
|
21
|
-
When.onThe<%- startLR
|
|
22
|
-
Then.onThe<%- navigatedOP
|
|
21
|
+
When.onThe<%- startLR%>Generated.onTable("").iPressRow(0);
|
|
22
|
+
Then.onThe<%- navigatedOP%>Generated.iSeeThisPage();
|
|
23
23
|
<%} %>
|
|
24
24
|
});
|
|
25
25
|
<%} %>
|