@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.
Files changed (29) 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/ObjectPage.js +17 -0
  29. package/templates/v4/integration/types/OpaJourneyTypes.d.ts +5 -5
@@ -6,5 +6,15 @@
6
6
  "cannotGeneratePageFile": "Cannot generate the page file for target: {{ targetKey }}.",
7
7
  "errorCopyingFreestyleTestTemplates": "An error occurred when copying freestyle test templates: {{ error }}",
8
8
  "errorWritingTsConfig": "An error occurred when writing the `tsconfig.json` file: {{ error }}"
9
+ },
10
+ "warn": {
11
+ "cannotUpdateJourneyRunner": "Could not update `JourneyRunner.js`. The existing file may be missing or unreadable. New pages were not registered.",
12
+ "cannotUpdateOpaTestsQunit": "Could not update `opaTests.qunit.js`. The existing file may be missing or unreadable. New journey paths were not registered.",
13
+ "cannotUpdateOpaJourneyTypes": "Could not update `OpaJourneyTypes.d.ts`. The existing file may be missing or unreadable. New journey type definitions were not registered."
14
+ },
15
+ "info": {
16
+ "opaJourneyTypesNotUpdated": "`OpaJourneyTypes.d.ts` was not updated due to an incompatible existing test setup.",
17
+ "incompatibleTestSetupSkipped": "`testsuite.qunit` and `opaTests.qunit` files were not updated due to an incompatible existing test setup.",
18
+ "manualIntegrationRequired": "Please ensure the generated journeys and pages are integrated into your existing OPA5 test setup."
9
19
  }
10
20
  }
package/dist/types.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Editor } from 'mem-fs-editor';
2
+ import type { Logger } from '@sap-ux/logger';
2
3
  export declare const DotFileExtension: {
3
4
  readonly JS: ".js";
4
5
  readonly TS: ".ts";
@@ -31,6 +32,8 @@ export type FEV4OPAPageConfig = {
31
32
  contextPath?: string;
32
33
  targetKey: string;
33
34
  isStartup: boolean;
35
+ fileName?: string;
36
+ fileExtension?: string;
34
37
  };
35
38
  export type FEV4OPAConfig = {
36
39
  appID: string;
@@ -40,6 +43,7 @@ export type FEV4OPAConfig = {
40
43
  htmlTarget: string;
41
44
  hideFilterBar: boolean;
42
45
  filterBarItems?: string[];
46
+ useVirtualPreviewEndpoints: boolean;
43
47
  };
44
48
  export type JourneyParams = {
45
49
  startPages: string[];
@@ -209,11 +213,17 @@ export type AppFeatures = {
209
213
  };
210
214
  export type WriteContext = {
211
215
  config: FEV4OPAConfig;
216
+ basePath: string;
217
+ rootCommonTemplateDirPath: string;
212
218
  rootV4TemplateDirPath: string;
213
219
  testOutDirPath: string;
214
220
  editor: Editor;
221
+ log?: Logger;
215
222
  journeyParams: JourneyParams;
223
+ hasPreexistingTests?: boolean;
224
+ incompatibleTestSetup?: boolean;
216
225
  dotFileExtension: DotFileExtension;
226
+ modifiedFiles: string[];
217
227
  };
218
228
  export type FormField = {
219
229
  fieldGroupQualifier?: string;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Shared helpers for utilities that read, splice, and write back generated test files in place.
3
+ */
4
+ /**
5
+ * Maximum file size, in characters, that the in-place splicers will attempt to update.
6
+ * Files larger than this are returned unchanged to prevent ReDoS on crafted inputs.
7
+ * Valid generator output is well within this limit.
8
+ */
9
+ export declare const MAX_FILE_CONTENT_LENGTH = 20000;
10
+ /**
11
+ * Escape regex metacharacters in `value` so it can be safely embedded in a `RegExp` pattern.
12
+ *
13
+ * @param value - the string to escape
14
+ * @returns the escaped string
15
+ */
16
+ export declare function escapeRegex(value: string): string;
17
+ /**
18
+ * Walk forward from `openBraceIdx` and return the index of the matching closing `}`,
19
+ * accounting for nested braces.
20
+ *
21
+ * @param content - the file content
22
+ * @param openBraceIdx - the index of the `{` that opens the block
23
+ * @returns the index of the matching closing `}` (or `content.length` if unterminated)
24
+ */
25
+ export declare function findMatchingClosingBrace(content: string, openBraceIdx: number): number;
26
+ /**
27
+ * Return the index immediately after the last `import ... from "..."` line in `content`,
28
+ * or `-1` if the content has no `import` lines.
29
+ *
30
+ * @param content - the file content to scan
31
+ * @returns the index immediately after the last import line, or -1 if none found
32
+ */
33
+ export declare function findLastImportEnd(content: string): number;
34
+ /**
35
+ * Insert `newImportLines` after the last existing `import` line in `content`.
36
+ * Returns `content` unchanged if it has no `import` lines.
37
+ *
38
+ * @param content - the file content
39
+ * @param newImportLines - the import lines to insert (no leading newline)
40
+ * @returns the updated content
41
+ */
42
+ export declare function insertAfterLastImport(content: string, newImportLines: string[]): string;
43
+ /**
44
+ * Locate a braced block introduced by a header pattern (e.g. `pages: {`) and return the
45
+ * indices of its opening and matching closing braces, accounting for nested braces.
46
+ *
47
+ * @param content - the file content
48
+ * @param headerRegex - regex matching the block header up to and including (or just before) the `{`
49
+ * @returns the brace indices, or undefined if the block can't be located
50
+ */
51
+ export declare function findBracedBlock(content: string, headerRegex: RegExp): {
52
+ openBraceIdx: number;
53
+ closeBraceIdx: number;
54
+ } | undefined;
55
+ //# sourceMappingURL=fileWritingUtils.d.ts.map
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Shared helpers for utilities that read, splice, and write back generated test files in place.
3
+ */
4
+ /**
5
+ * Maximum file size, in characters, that the in-place splicers will attempt to update.
6
+ * Files larger than this are returned unchanged to prevent ReDoS on crafted inputs.
7
+ * Valid generator output is well within this limit.
8
+ */
9
+ export const MAX_FILE_CONTENT_LENGTH = 20000;
10
+ /**
11
+ * Escape regex metacharacters in `value` so it can be safely embedded in a `RegExp` pattern.
12
+ *
13
+ * @param value - the string to escape
14
+ * @returns the escaped string
15
+ */
16
+ export function escapeRegex(value) {
17
+ return value.replace(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
18
+ }
19
+ /**
20
+ * Walk forward from `openBraceIdx` and return the index of the matching closing `}`,
21
+ * accounting for nested braces.
22
+ *
23
+ * @param content - the file content
24
+ * @param openBraceIdx - the index of the `{` that opens the block
25
+ * @returns the index of the matching closing `}` (or `content.length` if unterminated)
26
+ */
27
+ export function findMatchingClosingBrace(content, openBraceIdx) {
28
+ let depth = 1;
29
+ let index = openBraceIdx + 1;
30
+ while (index < content.length && depth > 0) {
31
+ const character = content[index];
32
+ if (character === '{') {
33
+ depth++;
34
+ }
35
+ else if (character === '}') {
36
+ depth--;
37
+ }
38
+ if (depth === 0) {
39
+ break;
40
+ }
41
+ index++;
42
+ }
43
+ return index;
44
+ }
45
+ /**
46
+ * Return the index immediately after the last `import ... from "..."` line in `content`,
47
+ * or `-1` if the content has no `import` lines.
48
+ *
49
+ * @param content - the file content to scan
50
+ * @returns the index immediately after the last import line, or -1 if none found
51
+ */
52
+ export function findLastImportEnd(content) {
53
+ const importLineRegex = /^import\b[^\n]*?\bfrom[ \t]+["'][^"']+["'];?[ \t]*$/gm;
54
+ let lastImportEnd = -1;
55
+ let importMatch;
56
+ while ((importMatch = importLineRegex.exec(content)) !== null) {
57
+ lastImportEnd = importMatch.index + importMatch[0].length;
58
+ }
59
+ return lastImportEnd;
60
+ }
61
+ /**
62
+ * Insert `newImportLines` after the last existing `import` line in `content`.
63
+ * Returns `content` unchanged if it has no `import` lines.
64
+ *
65
+ * @param content - the file content
66
+ * @param newImportLines - the import lines to insert (no leading newline)
67
+ * @returns the updated content
68
+ */
69
+ export function insertAfterLastImport(content, newImportLines) {
70
+ if (newImportLines.length === 0) {
71
+ return content;
72
+ }
73
+ const lastImportEnd = findLastImportEnd(content);
74
+ if (lastImportEnd < 0) {
75
+ return content;
76
+ }
77
+ const newImports = newImportLines.map((line) => `\n${line}`).join('');
78
+ return `${content.slice(0, lastImportEnd)}${newImports}${content.slice(lastImportEnd)}`;
79
+ }
80
+ /**
81
+ * Locate a braced block introduced by a header pattern (e.g. `pages: {`) and return the
82
+ * indices of its opening and matching closing braces, accounting for nested braces.
83
+ *
84
+ * @param content - the file content
85
+ * @param headerRegex - regex matching the block header up to and including (or just before) the `{`
86
+ * @returns the brace indices, or undefined if the block can't be located
87
+ */
88
+ export function findBracedBlock(content, headerRegex) {
89
+ const match = headerRegex.exec(content);
90
+ if (!match) {
91
+ return undefined;
92
+ }
93
+ const openBraceIdx = content.indexOf('{', match.index + match[0].length - 1);
94
+ if (openBraceIdx < 0) {
95
+ return undefined;
96
+ }
97
+ const closeBraceIdx = findMatchingClosingBrace(content, openBraceIdx);
98
+ if (closeBraceIdx >= content.length) {
99
+ return undefined;
100
+ }
101
+ return { openBraceIdx, closeBraceIdx };
102
+ }
103
+ //# sourceMappingURL=fileWritingUtils.js.map
@@ -0,0 +1,64 @@
1
+ import type { Editor } from 'mem-fs-editor';
2
+ import type { Logger } from '@sap-ux/logger';
3
+ import { DotFileExtension } from '../types.js';
4
+ /**
5
+ * Page entry to splice into an existing JourneyRunner.js / JourneyRunner.ts.
6
+ *
7
+ * The variable name and `onThe...` key emitted by the splicer always use the `Generated` suffix
8
+ * (e.g. `TravelListGenerated`, `onTheTravelListGenerated`) so that generator-owned `.gen` page
9
+ * entries can coexist with hand-authored pages bound to the same `targetKey`.
10
+ */
11
+ export interface OpaPageWriteInfo {
12
+ /** The page's targetKey; used as the base for the `onThe<targetKey>Generated` key. */
13
+ targetKey: string;
14
+ /** The app module path prefix (e.g. "project1/test/integration/pages") */
15
+ appPath: string;
16
+ /** The file name of the page object, including the `.gen` suffix (e.g. "TravelList.gen") */
17
+ fileName: string;
18
+ /** The file extension of the page object, including the leading dot (e.g. ".js" or ".ts") */
19
+ dotFileExtension: string;
20
+ /** The framework page template (`'ListReport'` or `'ObjectPage'`); only needed by the TS splicer. */
21
+ template?: string;
22
+ /** The app id (sap.app.id from the manifest); only needed by the TS splicer. */
23
+ appID?: string;
24
+ /** The component id (defined in the target section); only needed by the TS splicer. */
25
+ componentID?: string;
26
+ /** The entity set name (if the page uses an entitySet rather than a contextPath); only needed by the TS splicer. */
27
+ entitySet?: string;
28
+ /** The context path (if the page uses a contextPath rather than an entitySet); only needed by the TS splicer. */
29
+ contextPath?: string;
30
+ }
31
+ /**
32
+ * Splice new page entries into the three locations of an existing AMD `JourneyRunner.js`:
33
+ * the `sap.ui.define` dependency array, the function parameter list, and the `pages` object literal.
34
+ * Pages already referenced by their `.gen` module path are skipped; all other content is preserved.
35
+ *
36
+ * @param fileContent - the full content of the JourneyRunner.js file
37
+ * @param pages - pages to add
38
+ * @returns the updated file content, or the original content unchanged if nothing was added
39
+ */
40
+ export declare function splicePageIntoJourneyRunner(fileContent: string, pages: OpaPageWriteInfo[]): string;
41
+ /**
42
+ * Read the JourneyRunner from disk, splice new page entries into it, and write the result back.
43
+ * Pages already present are skipped. Dispatches between the AMD (`.js`) and ES module (`.ts`) splicers.
44
+ *
45
+ * @param pages - pages to add
46
+ * @param testOutDirPath - path to the test output directory (`.../webapp/test`)
47
+ * @param fs - mem-fs-editor instance used to read and write the file
48
+ * @param dotFileExtension - file extension of the JourneyRunner ('.ts' or '.js'); defaults to '.js'
49
+ * @param log - optional logger instance used to surface warnings when the file
50
+ * cannot be read or updated
51
+ * @returns true if the file was written, false otherwise
52
+ */
53
+ export declare function addPagesToJourneyRunner(pages: OpaPageWriteInfo[], testOutDirPath: string, fs: Editor, dotFileExtension?: DotFileExtension, log?: Logger): boolean;
54
+ /**
55
+ * Splice new page entries into an existing TypeScript `JourneyRunner.ts` by adding the
56
+ * required `Custom<targetKey>Generated` imports and an entry inside `pages: { ... }`.
57
+ * Pages already imported are skipped; all other content is preserved.
58
+ *
59
+ * @param fileContent - the full content of the JourneyRunner.ts file
60
+ * @param pages - pages to add
61
+ * @returns the updated file content, or the original content unchanged if nothing was added
62
+ */
63
+ export declare function splicePageIntoJourneyRunnerTs(fileContent: string, pages: OpaPageWriteInfo[]): string;
64
+ //# sourceMappingURL=journeyRunnerUtils.d.ts.map
@@ -0,0 +1,251 @@
1
+ import { join } from 'node:path';
2
+ import { t } from '../i18n.js';
3
+ import { DotFileExtension } from '../types.js';
4
+ import { MAX_FILE_CONTENT_LENGTH, escapeRegex, findBracedBlock, insertAfterLastImport } from './fileWritingUtils.js';
5
+ /**
6
+ * Returns the relative path from the test output directory to the JourneyRunner file
7
+ * for the requested file extension.
8
+ *
9
+ * @param dotFileExtension - file extension ('.ts' or '.js')
10
+ * @returns the relative path
11
+ */
12
+ function getJourneyRunnerFilePath(dotFileExtension) {
13
+ return join('integration', 'pages', `JourneyRunner${dotFileExtension}`);
14
+ }
15
+ /**
16
+ * Splice new page entries into the three locations of an existing AMD `JourneyRunner.js`:
17
+ * the `sap.ui.define` dependency array, the function parameter list, and the `pages` object literal.
18
+ * Pages already referenced by their `.gen` module path are skipped; all other content is preserved.
19
+ *
20
+ * @param fileContent - the full content of the JourneyRunner.js file
21
+ * @param pages - pages to add
22
+ * @returns the updated file content, or the original content unchanged if nothing was added
23
+ */
24
+ export function splicePageIntoJourneyRunner(fileContent, pages) {
25
+ if (fileContent.length > MAX_FILE_CONTENT_LENGTH) {
26
+ return fileContent;
27
+ }
28
+ // Determine which pages are not yet present by checking the define array
29
+ const toAdd = pages.filter((page) => {
30
+ const modulePath = `${page.appPath}/test/integration/pages/${page.fileName}`;
31
+ return !fileContent.includes(`"${modulePath}"`);
32
+ });
33
+ if (toAdd.length === 0) {
34
+ return fileContent;
35
+ }
36
+ let result = fileContent;
37
+ result = spliceIntoDefineArray(result, toAdd);
38
+ result = spliceIntoFunctionParams(result, toAdd);
39
+ result = spliceIntoJsPagesObject(result, toAdd);
40
+ return result;
41
+ }
42
+ /**
43
+ * Insert new entries before the closing `]` of the `sap.ui.define([...])` dependency array.
44
+ *
45
+ * @param content - the file content
46
+ * @param toAdd - pages to add
47
+ * @returns the updated content (or unchanged if the define array can't be located)
48
+ */
49
+ function spliceIntoDefineArray(content, toAdd) {
50
+ const defineArrayRegex = /sap\.ui\.define\s*\(\s*\[([^\]]*)\]\s*,\s*function/d;
51
+ const match = defineArrayRegex.exec(content);
52
+ if (!match?.indices?.[1]) {
53
+ return content;
54
+ }
55
+ const [bodyStart, bodyEnd] = match.indices[1];
56
+ const arrayBody = content.slice(bodyStart, bodyEnd);
57
+ const indentMatch = /^([ \t]+)"/m.exec(arrayBody);
58
+ const indent = indentMatch ? indentMatch[1] : '\t';
59
+ const newEntries = toAdd
60
+ .map((page) => `${indent}"${page.appPath}/test/integration/pages/${page.fileName}",`)
61
+ .join('\n');
62
+ return insertBeforePosition(content, bodyEnd, newEntries);
63
+ }
64
+ /**
65
+ * Append the new `, <Target>Generated` parameters to the JourneyRunner module factory's signature.
66
+ *
67
+ * @param content - the file content
68
+ * @param toAdd - pages to add
69
+ * @returns the updated content (or unchanged if the function signature can't be located)
70
+ */
71
+ function spliceIntoFunctionParams(content, toAdd) {
72
+ const funcParamRegex = /\]\s*,\s*function\s*\(([^)]*)\)\s*\{/d;
73
+ const match = funcParamRegex.exec(content);
74
+ if (!match?.indices?.[1]) {
75
+ return content;
76
+ }
77
+ const [, paramEnd] = match.indices[1];
78
+ const newParams = toAdd.map((page) => `, ${page.targetKey}Generated`).join('');
79
+ return `${content.slice(0, paramEnd)}${newParams}${content.slice(paramEnd)}`;
80
+ }
81
+ /**
82
+ * Insert new `onThe<Target>Generated: <Target>Generated,` entries into the AMD `pages: { ... }`
83
+ * object literal, using brace counting to handle nested braces in user hand-edits.
84
+ *
85
+ * @param content - the file content
86
+ * @param toAdd - pages to add
87
+ * @returns the updated content (or unchanged if the pages object can't be located)
88
+ */
89
+ function spliceIntoJsPagesObject(content, toAdd) {
90
+ const block = findBracedBlock(content, /pages\s*:\s*\{/);
91
+ if (!block) {
92
+ return content;
93
+ }
94
+ const { openBraceIdx, closeBraceIdx } = block;
95
+ const pagesBody = content.slice(openBraceIdx + 1, closeBraceIdx);
96
+ const pageIndentMatch = /^([ \t]+)on/m.exec(pagesBody);
97
+ const pageIndent = pageIndentMatch ? pageIndentMatch[1] : '\t\t\t';
98
+ const newPageEntries = toAdd
99
+ .map((page) => `${pageIndent}onThe${page.targetKey}Generated: ${page.targetKey}Generated,`)
100
+ .join('\n');
101
+ return insertBeforePosition(content, closeBraceIdx, newPageEntries);
102
+ }
103
+ /**
104
+ * Insert `newEntries` immediately before `position` in `content`, preserving the trailing
105
+ * whitespace at `position` and ensuring the preceding token ends with a comma.
106
+ *
107
+ * @param content - the file content
108
+ * @param position - the index to insert before
109
+ * @param newEntries - the new lines (no leading newline) to insert
110
+ * @returns the updated content
111
+ */
112
+ function insertBeforePosition(content, position, newEntries) {
113
+ const trimmedEnd = content.slice(0, position).trimEnd();
114
+ const commaFix = trimmedEnd.endsWith(',') ? '' : ',';
115
+ const trailingWhitespace = content.slice(trimmedEnd.length, position);
116
+ return `${trimmedEnd}${commaFix}\n${newEntries}${trailingWhitespace}${content.slice(position)}`;
117
+ }
118
+ /**
119
+ * Read the JourneyRunner from disk, splice new page entries into it, and write the result back.
120
+ * Pages already present are skipped. Dispatches between the AMD (`.js`) and ES module (`.ts`) splicers.
121
+ *
122
+ * @param pages - pages to add
123
+ * @param testOutDirPath - path to the test output directory (`.../webapp/test`)
124
+ * @param fs - mem-fs-editor instance used to read and write the file
125
+ * @param dotFileExtension - file extension of the JourneyRunner ('.ts' or '.js'); defaults to '.js'
126
+ * @param log - optional logger instance used to surface warnings when the file
127
+ * cannot be read or updated
128
+ * @returns true if the file was written, false otherwise
129
+ */
130
+ export function addPagesToJourneyRunner(pages, testOutDirPath, fs, dotFileExtension = DotFileExtension.JS, log) {
131
+ if (pages.length === 0) {
132
+ return false;
133
+ }
134
+ try {
135
+ const filePath = join(testOutDirPath, getJourneyRunnerFilePath(dotFileExtension));
136
+ const content = fs.read(filePath);
137
+ if (content.length > MAX_FILE_CONTENT_LENGTH) {
138
+ log?.warn(t('warn.cannotUpdateJourneyRunner'));
139
+ return false;
140
+ }
141
+ const splice = dotFileExtension === DotFileExtension.TS ? splicePageIntoJourneyRunnerTs : splicePageIntoJourneyRunner;
142
+ const updated = splice(content, pages);
143
+ if (updated !== content) {
144
+ fs.write(filePath, updated);
145
+ return true;
146
+ }
147
+ }
148
+ catch {
149
+ log?.warn(t('warn.cannotUpdateJourneyRunner'));
150
+ }
151
+ return false;
152
+ }
153
+ /**
154
+ * Filter `pages` down to those whose `Custom<targetKey>Generated` import line is not yet present.
155
+ * The import path matched is `./<fileName>` (with the `.gen` suffix), so user-authored bindings
156
+ * to `./<targetKey>` are correctly treated as different.
157
+ *
158
+ * @param fileContent - the existing JourneyRunner.ts content
159
+ * @param pages - candidate pages to splice in
160
+ * @returns the subset of pages that need to be added
161
+ */
162
+ function findPagesToAdd(fileContent, pages) {
163
+ return pages.filter((page) => {
164
+ const importPattern = new RegExp(String.raw `from\s+"\./${escapeRegex(page.fileName)}"`);
165
+ return !importPattern.test(fileContent);
166
+ });
167
+ }
168
+ /**
169
+ * Build the source-code block for a single new entry in the `pages: { ... }` object literal.
170
+ *
171
+ * @param page - the page to render
172
+ * @param pageIndent - leading whitespace for the entry's outer line
173
+ * @param innerIndent - leading whitespace for the entry's nested lines
174
+ * @returns the multi-line source block
175
+ */
176
+ function buildPageEntry(page, pageIndent, innerIndent) {
177
+ const framework = page.template ?? 'ListReport';
178
+ const innerProps = [
179
+ `${innerIndent} appId: "${page.appID ?? ''}"`,
180
+ `${innerIndent} componentId: "${page.componentID ?? ''}"`
181
+ ];
182
+ if (page.entitySet) {
183
+ innerProps.push(`${innerIndent} entitySet: "${page.entitySet}"`);
184
+ }
185
+ if (page.contextPath) {
186
+ innerProps.push(`${innerIndent} contextPath: "${page.contextPath}"`);
187
+ }
188
+ return [
189
+ `${pageIndent}onThe${page.targetKey}Generated: new ${framework}(`,
190
+ `${innerIndent}{`,
191
+ innerProps.join(',\n'),
192
+ `${innerIndent}},`,
193
+ `${innerIndent}Custom${page.targetKey}Generated`,
194
+ `${pageIndent})`
195
+ ].join('\n');
196
+ }
197
+ /**
198
+ * Insert new page entries inside the `pages: { ... }` object literal of `content`.
199
+ * Returns `content` unchanged if it has no `pages: {` block.
200
+ *
201
+ * @param content - the file content
202
+ * @param toAdd - the pages to add
203
+ * @returns the updated content
204
+ */
205
+ function insertIntoPagesObject(content, toAdd) {
206
+ const block = findBracedBlock(content, /pages\s*:\s*\{/);
207
+ if (!block) {
208
+ return content;
209
+ }
210
+ const { openBraceIdx, closeBraceIdx } = block;
211
+ const pagesBody = content.slice(openBraceIdx + 1, closeBraceIdx);
212
+ // Detect indentation from the first existing page entry
213
+ const pageIndentMatch = /^([ \t]+)on/m.exec(pagesBody);
214
+ const pageIndent = pageIndentMatch ? pageIndentMatch[1] : ' ';
215
+ const innerIndent = pageIndent + ' ';
216
+ const newPageEntries = toAdd.map((page) => buildPageEntry(page, pageIndent, innerIndent)).join(',\n');
217
+ // Ensure the last existing entry ends with a comma before we insert after it.
218
+ const trimmedPagesEnd = content.slice(0, closeBraceIdx).trimEnd();
219
+ const needsComma = !trimmedPagesEnd.endsWith(',') && !trimmedPagesEnd.endsWith('{');
220
+ const commaFix = needsComma ? ',' : '';
221
+ const trailingWhitespace = content.slice(trimmedPagesEnd.length, closeBraceIdx);
222
+ return `${trimmedPagesEnd}${commaFix}\n${newPageEntries}${trailingWhitespace}${content.slice(closeBraceIdx)}`;
223
+ }
224
+ /**
225
+ * Splice new page entries into an existing TypeScript `JourneyRunner.ts` by adding the
226
+ * required `Custom<targetKey>Generated` imports and an entry inside `pages: { ... }`.
227
+ * Pages already imported are skipped; all other content is preserved.
228
+ *
229
+ * @param fileContent - the full content of the JourneyRunner.ts file
230
+ * @param pages - pages to add
231
+ * @returns the updated file content, or the original content unchanged if nothing was added
232
+ */
233
+ export function splicePageIntoJourneyRunnerTs(fileContent, pages) {
234
+ if (fileContent.length > MAX_FILE_CONTENT_LENGTH) {
235
+ return fileContent;
236
+ }
237
+ const toAdd = findPagesToAdd(fileContent, pages);
238
+ if (toAdd.length === 0) {
239
+ return fileContent;
240
+ }
241
+ // Determine which framework imports (ListReport / ObjectPage) are missing and need to be added.
242
+ const frameworkTemplates = Array.from(new Set(toAdd.map((page) => page.template).filter((template) => Boolean(template))));
243
+ const missingFrameworkImports = frameworkTemplates.filter((template) => !fileContent.includes(`from "sap/fe/test/${template}"`));
244
+ const newImportLines = [
245
+ ...missingFrameworkImports.map((template) => `import ${template} from "sap/fe/test/${template}";`),
246
+ ...toAdd.map((page) => `import Custom${page.targetKey}Generated from "./${page.fileName}";`)
247
+ ];
248
+ const withImports = insertAfterLastImport(fileContent, newImportLines);
249
+ return insertIntoPagesObject(withImports, toAdd);
250
+ }
251
+ //# sourceMappingURL=journeyRunnerUtils.js.map
@@ -0,0 +1,27 @@
1
+ import type { Editor } from 'mem-fs-editor';
2
+ import type { Logger } from '@sap-ux/logger';
3
+ import type { OpaPageWriteInfo } from './journeyRunnerUtils.js';
4
+ /**
5
+ * Splice new journey entries into an existing `OpaJourneyTypes.d.ts`: framework imports,
6
+ * per-page custom actions/assertions imports, and `onThe<targetKey>Generated` members in
7
+ * the `When` and `Then` unions. Pages already imported are skipped; all other content is preserved.
8
+ *
9
+ * @param fileContent - the full content of the OpaJourneyTypes.d.ts file
10
+ * @param pages - pages to add
11
+ * @returns the updated file content, or the original content unchanged if nothing was added
12
+ */
13
+ export declare function spliceJourneysIntoOpaJourneyTypes(fileContent: string, pages: OpaPageWriteInfo[]): string;
14
+ /**
15
+ * Read `OpaJourneyTypes.d.ts` from the project, splice entries for new pages, and write
16
+ * the result back. Pages already present are skipped. If the file does not exist or
17
+ * cannot be read, logs a warning and returns without writing.
18
+ *
19
+ * @param pages - pages whose journeys should be reflected in OpaJourneyTypes.d.ts
20
+ * @param testOutDirPath - path to the test output directory (`.../webapp/test`)
21
+ * @param fs - mem-fs-editor instance used to read and write the file
22
+ * @param log - optional logger instance used to surface warnings when the file
23
+ * cannot be read or updated
24
+ * @returns true if the file was written, false otherwise
25
+ */
26
+ export declare function addJourneysToOpaJourneyTypes(pages: OpaPageWriteInfo[], testOutDirPath: string, fs: Editor, log?: Logger): boolean;
27
+ //# sourceMappingURL=opaJourneyTypesUtils.d.ts.map