@sap-ux/ui5-test-writer 0.7.102 → 0.7.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fiori-elements-opa-writer.d.ts +11 -10
- package/dist/fiori-elements-opa-writer.js +170 -74
- package/dist/types.d.ts +22 -0
- package/dist/utils/flpSandboxUtils.d.ts +17 -0
- package/dist/utils/flpSandboxUtils.js +40 -0
- package/dist/utils/listReportUtils.d.ts +21 -2
- package/dist/utils/listReportUtils.js +35 -2
- package/dist/utils/modelUtils.d.ts +3 -1
- package/dist/utils/modelUtils.js +3 -2
- package/dist/utils/opaQUnitUtils.d.ts +93 -0
- package/dist/utils/opaQUnitUtils.js +303 -0
- package/package.json +2 -2
- package/templates/v4/integration/ListReportJourney.js +1 -1
- package/templates/v4/integration/ObjectPageJourney.js +3 -1
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import type { Editor } from 'mem-fs-editor';
|
|
2
2
|
import type { Manifest } from '@sap-ux/project-access';
|
|
3
3
|
import type { Logger } from '@sap-ux/logger';
|
|
4
|
-
/**
|
|
5
|
-
* Reads the manifest for an app.
|
|
6
|
-
*
|
|
7
|
-
* @param fs - a reference to a mem-fs editor
|
|
8
|
-
* @param basePath - the root folder of the app
|
|
9
|
-
* @returns the manifest object. An exception is thrown if the manifest cannot be read.
|
|
10
|
-
*/
|
|
11
|
-
export declare function readManifest(fs: Editor, basePath: string): Manifest;
|
|
12
4
|
/**
|
|
13
5
|
* Generate OPA test files for a Fiori elements for OData V4 application.
|
|
14
6
|
* Note: this can potentially overwrite existing files in the webapp/test folder.
|
|
@@ -21,13 +13,22 @@ export declare function readManifest(fs: Editor, basePath: string): Manifest;
|
|
|
21
13
|
* @param metadata - optional metadata for the OPA test generation
|
|
22
14
|
* @param fs - an optional reference to a mem-fs editor
|
|
23
15
|
* @param log - optional logger instance
|
|
16
|
+
* @param standalone - opa test generation run standalone, not during app generation
|
|
24
17
|
* @returns Reference to a mem-fs-editor
|
|
25
18
|
*/
|
|
26
19
|
export declare function generateOPAFiles(basePath: string, opaConfig: {
|
|
27
20
|
scriptName?: string;
|
|
28
21
|
appID?: string;
|
|
29
22
|
htmlTarget?: string;
|
|
30
|
-
}, metadata?: string, fs?: Editor, log?: Logger): Promise<Editor>;
|
|
23
|
+
}, metadata?: string, fs?: Editor, log?: Logger, standalone?: boolean): Promise<Editor>;
|
|
24
|
+
/**
|
|
25
|
+
* Reads the manifest for an app.
|
|
26
|
+
*
|
|
27
|
+
* @param fs - a reference to a mem-fs editor
|
|
28
|
+
* @param basePath - the root folder of the app
|
|
29
|
+
* @returns the manifest object. An exception is thrown if the manifest cannot be read.
|
|
30
|
+
*/
|
|
31
|
+
export declare function readManifest(fs: Editor, basePath: string): Manifest;
|
|
31
32
|
/**
|
|
32
33
|
* Generate a page object file for a Fiori elements for OData V4 application.
|
|
33
34
|
* Note: this doesn't modify other existing files in the webapp/test folder.
|
|
@@ -42,5 +43,5 @@ export declare function generateOPAFiles(basePath: string, opaConfig: {
|
|
|
42
43
|
export declare function generatePageObjectFile(basePath: string, pageObjectParameters: {
|
|
43
44
|
targetKey: string;
|
|
44
45
|
appID?: string;
|
|
45
|
-
}, fs?: Editor): Editor
|
|
46
|
+
}, fs?: Editor): Promise<Editor>;
|
|
46
47
|
//# sourceMappingURL=fiori-elements-opa-writer.d.ts.map
|
|
@@ -1,15 +1,76 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.readManifest = readManifest;
|
|
4
3
|
exports.generateOPAFiles = generateOPAFiles;
|
|
4
|
+
exports.readManifest = readManifest;
|
|
5
5
|
exports.generatePageObjectFile = generatePageObjectFile;
|
|
6
6
|
const node_path_1 = require("node:path");
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
7
8
|
const mem_fs_1 = require("mem-fs");
|
|
8
9
|
const mem_fs_editor_1 = require("mem-fs-editor");
|
|
9
10
|
const types_1 = require("./types");
|
|
10
11
|
const i18n_1 = require("./i18n");
|
|
11
12
|
const project_access_1 = require("@sap-ux/project-access");
|
|
12
13
|
const modelUtils_1 = require("./utils/modelUtils");
|
|
14
|
+
const opaQUnitUtils_1 = require("./utils/opaQUnitUtils");
|
|
15
|
+
/**
|
|
16
|
+
* Generate OPA test files for a Fiori elements for OData V4 application.
|
|
17
|
+
* Note: this can potentially overwrite existing files in the webapp/test folder.
|
|
18
|
+
*
|
|
19
|
+
* @param basePath - the absolute target path where the application will be generated
|
|
20
|
+
* @param opaConfig - parameters for the generation
|
|
21
|
+
* @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
|
|
22
|
+
* @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
|
|
23
|
+
* @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
|
|
24
|
+
* @param metadata - optional metadata for the OPA test generation
|
|
25
|
+
* @param fs - an optional reference to a mem-fs editor
|
|
26
|
+
* @param log - optional logger instance
|
|
27
|
+
* @param standalone - opa test generation run standalone, not during app generation
|
|
28
|
+
* @returns Reference to a mem-fs-editor
|
|
29
|
+
*/
|
|
30
|
+
async function generateOPAFiles(basePath, opaConfig, metadata, fs, log, standalone = false) {
|
|
31
|
+
const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
|
|
32
|
+
const manifest = readManifest(editor, basePath);
|
|
33
|
+
const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest);
|
|
34
|
+
const config = createConfig(manifest, opaConfig, hideFilterBar);
|
|
35
|
+
const rootCommonTemplateDirPath = (0, node_path_1.join)(__dirname, '../templates/common');
|
|
36
|
+
const rootV4TemplateDirPath = (0, node_path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
|
|
37
|
+
const testOutDirPath = (0, node_path_1.join)(await (0, project_access_1.getWebappPath)(basePath), 'test');
|
|
38
|
+
// Access ux-specification to get feature data for OPA test generation
|
|
39
|
+
const appFeatures = await (0, modelUtils_1.getAppFeatures)(basePath, editor, log, metadata, manifest);
|
|
40
|
+
// OPA Journey file
|
|
41
|
+
const startPages = config.pages.filter((page) => page.isStartup).map((page) => page.targetKey);
|
|
42
|
+
const LROP = findLROP(config.pages, manifest);
|
|
43
|
+
const journeyParams = {
|
|
44
|
+
startPages,
|
|
45
|
+
startLR: LROP.pageLR?.targetKey,
|
|
46
|
+
navigatedOP: LROP.pageOP?.targetKey,
|
|
47
|
+
hideFilterBar: config.hideFilterBar
|
|
48
|
+
};
|
|
49
|
+
const writeContext = { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams };
|
|
50
|
+
if (standalone) {
|
|
51
|
+
const hasJourneyRunner = (0, node_fs_1.existsSync)((0, node_path_1.join)(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'));
|
|
52
|
+
const virtualOPA5Configured = await (0, opaQUnitUtils_1.hasVirtualOPA5)(basePath);
|
|
53
|
+
if (hasJourneyRunner) {
|
|
54
|
+
writeJourneyFiles(appFeatures, writeContext, true, true, virtualOPA5Configured);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
editor.move((0, node_path_1.join)(testOutDirPath, 'integration', '**'), (0, node_path_1.join)(testOutDirPath, 'integration_old'));
|
|
58
|
+
await (0, opaQUnitUtils_1.addIntegrationOldToGitignore)(basePath, editor);
|
|
59
|
+
const htmlTarget = (0, opaQUnitUtils_1.readHtmlTargetFromQUnitJs)(testOutDirPath, editor) ?? config.htmlTarget;
|
|
60
|
+
const standaloneConfig = { ...config, htmlTarget };
|
|
61
|
+
const standaloneWriteContext = { ...writeContext, config: standaloneConfig };
|
|
62
|
+
if (!virtualOPA5Configured) {
|
|
63
|
+
writeCommonAndPageFiles(standaloneWriteContext, rootCommonTemplateDirPath);
|
|
64
|
+
}
|
|
65
|
+
writeJourneyFiles(appFeatures, standaloneWriteContext, true, hasJourneyRunner, virtualOPA5Configured);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath);
|
|
70
|
+
writeJourneyFiles(appFeatures, writeContext, false);
|
|
71
|
+
}
|
|
72
|
+
return editor;
|
|
73
|
+
}
|
|
13
74
|
/**
|
|
14
75
|
* Reads the manifest for an app.
|
|
15
76
|
*
|
|
@@ -193,103 +254,138 @@ function findLROP(pages, manifest) {
|
|
|
193
254
|
return { pageLR, pageOP };
|
|
194
255
|
}
|
|
195
256
|
/**
|
|
196
|
-
* Writes
|
|
257
|
+
* Writes common test files, page objects, and the first journey file.
|
|
197
258
|
*
|
|
198
|
-
* @param
|
|
199
|
-
* @param
|
|
200
|
-
* @param testOutDirPath - output test directory (.../webapp/test)
|
|
201
|
-
* @param fs - a reference to a mem-fs editor
|
|
202
|
-
*/
|
|
203
|
-
function writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, fs) {
|
|
204
|
-
fs.copyTpl((0, node_path_1.join)(rootTemplateDirPath, `integration/pages/${pageConfig.template}.js`), (0, node_path_1.join)(testOutDirPath, `integration/pages/${pageConfig.targetKey}.js`), pageConfig, undefined, {
|
|
205
|
-
globOptions: { dot: true }
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
/**
|
|
209
|
-
* Generate OPA test files for a Fiori elements for OData V4 application.
|
|
210
|
-
* Note: this can potentially overwrite existing files in the webapp/test folder.
|
|
211
|
-
*
|
|
212
|
-
* @param basePath - the absolute target path where the application will be generated
|
|
213
|
-
* @param opaConfig - parameters for the generation
|
|
214
|
-
* @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
|
|
215
|
-
* @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
|
|
216
|
-
* @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
|
|
217
|
-
* @param metadata - optional metadata for the OPA test generation
|
|
218
|
-
* @param fs - an optional reference to a mem-fs editor
|
|
219
|
-
* @param log - optional logger instance
|
|
220
|
-
* @returns Reference to a mem-fs-editor
|
|
259
|
+
* @param writeContext - shared write context (config, paths, editor, journey params)
|
|
260
|
+
* @param rootCommonTemplateDirPath - template root directory for common files
|
|
221
261
|
*/
|
|
222
|
-
|
|
223
|
-
const
|
|
224
|
-
const manifest = readManifest(editor, basePath);
|
|
225
|
-
const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest);
|
|
226
|
-
const config = createConfig(manifest, opaConfig, hideFilterBar);
|
|
227
|
-
const rootCommonTemplateDirPath = (0, node_path_1.join)(__dirname, '../templates/common');
|
|
228
|
-
const rootV4TemplateDirPath = (0, node_path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
|
|
229
|
-
const testOutDirPath = (0, node_path_1.join)(basePath, 'webapp/test');
|
|
262
|
+
function writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath) {
|
|
263
|
+
const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
|
|
230
264
|
// Common test files
|
|
231
265
|
editor.copyTpl((0, node_path_1.join)(rootCommonTemplateDirPath), testOutDirPath,
|
|
232
266
|
// unit tests are not added for Fiori elements app
|
|
233
267
|
{ appId: config.appID }, undefined, {
|
|
234
268
|
globOptions: { dot: true }
|
|
235
269
|
});
|
|
236
|
-
// Pages files (one for each page in the app)
|
|
237
270
|
config.pages.forEach((page) => {
|
|
238
271
|
writePageObject(page, rootV4TemplateDirPath, testOutDirPath, editor);
|
|
239
272
|
});
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
const journeyParams = {
|
|
246
|
-
startPages,
|
|
247
|
-
startLR: LROP.pageLR?.targetKey,
|
|
248
|
-
navigatedOP: LROP.pageOP?.targetKey,
|
|
249
|
-
hideFilterBar: config.hideFilterBar
|
|
250
|
-
};
|
|
251
|
-
const generatedJourneyPages = [];
|
|
252
|
-
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration/FirstJourney.js'), (0, node_path_1.join)(testOutDirPath, `integration/${config.opaJourneyFileName}.js`), journeyParams, undefined, {
|
|
273
|
+
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'FirstJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${config.opaJourneyFileName}.js`), journeyParams, undefined, {
|
|
274
|
+
globOptions: { dot: true }
|
|
275
|
+
});
|
|
276
|
+
// Journey Runner
|
|
277
|
+
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'pages', 'JourneyRunner.js'), (0, node_path_1.join)(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'), config, undefined, {
|
|
253
278
|
globOptions: { dot: true }
|
|
254
279
|
});
|
|
255
|
-
|
|
256
|
-
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Checks whether a page object file already exists for the given feature name.
|
|
283
|
+
* If it doesn't exist, finds the matching page config and writes the file.
|
|
284
|
+
*
|
|
285
|
+
* @param featureName - the feature/page name (equals the manifest targetKey)
|
|
286
|
+
* @param config - the OPA config containing all page configurations
|
|
287
|
+
* @param rootV4TemplateDirPath - template root directory for v4 templates
|
|
288
|
+
* @param testOutDirPath - output test directory (.../webapp/test)
|
|
289
|
+
* @param editor - a reference to a mem-fs editor
|
|
290
|
+
* @returns JourneyRunnerPage if the page was newly created, undefined otherwise
|
|
291
|
+
*/
|
|
292
|
+
function ensurePageExists(featureName, config, rootV4TemplateDirPath, testOutDirPath, editor) {
|
|
293
|
+
const pageFilePath = (0, node_path_1.join)(testOutDirPath, 'integration', 'pages', `${featureName}.js`);
|
|
294
|
+
if (editor.exists(pageFilePath)) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
const pageConfig = config.pages.find((p) => p.targetKey === featureName);
|
|
298
|
+
if (pageConfig) {
|
|
299
|
+
writePageObject(pageConfig, rootV4TemplateDirPath, testOutDirPath, editor);
|
|
300
|
+
return { targetKey: featureName, appPath: config.appPath };
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Writes journey files for list report, object pages and FPM pages.
|
|
306
|
+
*
|
|
307
|
+
* @param appFeatures - object containing feature data for list report, object pages, and FPM
|
|
308
|
+
* @param writeContext - shared write context (config, paths, editor, journey params)
|
|
309
|
+
* @param isStandalone - whether the generation is run in standalone mode (not during app generation)
|
|
310
|
+
* @param hasJourneyRunner - whether a JourneyRunner.js already exists (standalone upgrade path)
|
|
311
|
+
* @param virtualOPA5Configured - whether virtual OPA5 is configured
|
|
312
|
+
*/
|
|
313
|
+
function writeJourneyFiles(appFeatures, writeContext, isStandalone, hasJourneyRunner = false, virtualOPA5Configured = false) {
|
|
314
|
+
const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
|
|
315
|
+
const generatedJourneyPages = [];
|
|
316
|
+
const newPages = [];
|
|
317
|
+
if (appFeatures.listReport?.name) {
|
|
318
|
+
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'ListReportJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${appFeatures.listReport.name}Journey.js`), {
|
|
257
319
|
...journeyParams,
|
|
258
|
-
...listReport
|
|
320
|
+
...appFeatures.listReport
|
|
259
321
|
}, undefined, {
|
|
260
322
|
globOptions: { dot: true }
|
|
261
323
|
});
|
|
262
|
-
generatedJourneyPages.push(listReport.name);
|
|
324
|
+
generatedJourneyPages.push(appFeatures.listReport.name);
|
|
325
|
+
const lrPage = ensurePageExists(appFeatures.listReport.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
|
|
326
|
+
if (lrPage) {
|
|
327
|
+
newPages.push(lrPage);
|
|
328
|
+
}
|
|
263
329
|
}
|
|
264
|
-
if (objectPages && objectPages.length > 0) {
|
|
265
|
-
objectPages.forEach((objectPage) => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
330
|
+
if (appFeatures.objectPages && appFeatures.objectPages.length > 0) {
|
|
331
|
+
appFeatures.objectPages.forEach((objectPage) => {
|
|
332
|
+
if (objectPage.name) {
|
|
333
|
+
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'ObjectPageJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${objectPage.name}Journey.js`), {
|
|
334
|
+
...journeyParams,
|
|
335
|
+
...objectPage,
|
|
336
|
+
isStandalone
|
|
337
|
+
}, undefined, {
|
|
338
|
+
globOptions: { dot: true }
|
|
339
|
+
});
|
|
340
|
+
generatedJourneyPages.push(objectPage.name);
|
|
341
|
+
const opPage = ensurePageExists(objectPage.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
|
|
342
|
+
if (opPage) {
|
|
343
|
+
newPages.push(opPage);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
273
346
|
});
|
|
274
347
|
}
|
|
275
|
-
if (fpm) {
|
|
276
|
-
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration
|
|
348
|
+
if (appFeatures.fpm?.name) {
|
|
349
|
+
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'FPMJourney.js'), (0, node_path_1.join)(testOutDirPath, 'integration', `${appFeatures.fpm.name}Journey.js`), {
|
|
277
350
|
...journeyParams,
|
|
278
|
-
...fpm
|
|
351
|
+
...appFeatures.fpm
|
|
279
352
|
}, undefined, {
|
|
280
353
|
globOptions: { dot: true }
|
|
281
354
|
});
|
|
282
|
-
generatedJourneyPages.push(fpm.name);
|
|
355
|
+
generatedJourneyPages.push(appFeatures.fpm.name);
|
|
356
|
+
const fpmPage = ensurePageExists(appFeatures.fpm.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
|
|
357
|
+
if (fpmPage) {
|
|
358
|
+
newPages.push(fpmPage);
|
|
359
|
+
}
|
|
283
360
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
361
|
+
if (newPages.length > 0) {
|
|
362
|
+
(0, opaQUnitUtils_1.addPagesToJourneyRunner)(newPages, testOutDirPath, editor);
|
|
363
|
+
}
|
|
364
|
+
if (!virtualOPA5Configured) {
|
|
365
|
+
if (hasJourneyRunner) {
|
|
366
|
+
(0, opaQUnitUtils_1.addPathsToQUnitJs)(generatedJourneyPages.map((page) => {
|
|
367
|
+
return `${config.appPath}/test/integration/${page}Journey`;
|
|
368
|
+
}), testOutDirPath, editor);
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
editor.copyTpl((0, node_path_1.join)(rootV4TemplateDirPath, 'integration', 'opaTests.*.*'), (0, node_path_1.join)(testOutDirPath, 'integration'), { ...config, generatedJourneyPages }, undefined, {
|
|
372
|
+
globOptions: { dot: true }
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Writes a page object in a mem-fs-editor.
|
|
379
|
+
*
|
|
380
|
+
* @param pageConfig - the page configuration object
|
|
381
|
+
* @param rootTemplateDirPath - template root directory
|
|
382
|
+
* @param testOutDirPath - output test directory (.../webapp/test)
|
|
383
|
+
* @param fs - a reference to a mem-fs editor
|
|
384
|
+
*/
|
|
385
|
+
function writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, fs) {
|
|
386
|
+
fs.copyTpl((0, node_path_1.join)(rootTemplateDirPath, 'integration', 'pages', `${pageConfig.template}.js`), (0, node_path_1.join)(testOutDirPath, 'integration', 'pages', `${pageConfig.targetKey}.js`), pageConfig, undefined, {
|
|
290
387
|
globOptions: { dot: true }
|
|
291
388
|
});
|
|
292
|
-
return editor;
|
|
293
389
|
}
|
|
294
390
|
/**
|
|
295
391
|
* Generate a page object file for a Fiori elements for OData V4 application.
|
|
@@ -302,14 +398,14 @@ async function generateOPAFiles(basePath, opaConfig, metadata, fs, log) {
|
|
|
302
398
|
* @param fs - an optional reference to a mem-fs editor
|
|
303
399
|
* @returns Reference to a mem-fs-editor
|
|
304
400
|
*/
|
|
305
|
-
function generatePageObjectFile(basePath, pageObjectParameters, fs) {
|
|
306
|
-
const editor = fs
|
|
401
|
+
async function generatePageObjectFile(basePath, pageObjectParameters, fs) {
|
|
402
|
+
const editor = fs ?? (0, mem_fs_editor_1.create)((0, mem_fs_1.create)());
|
|
307
403
|
const manifest = readManifest(editor, basePath);
|
|
308
404
|
const { applicationType } = getAppTypeAndHideFilterBarFromManifest(manifest);
|
|
309
405
|
const pageConfig = createPageConfig(manifest, pageObjectParameters.targetKey, pageObjectParameters.appID);
|
|
310
406
|
if (pageConfig) {
|
|
311
407
|
const rootTemplateDirPath = (0, node_path_1.join)(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
|
|
312
|
-
const testOutDirPath = (0, node_path_1.join)(basePath, '
|
|
408
|
+
const testOutDirPath = (0, node_path_1.join)(await (0, project_access_1.getWebappPath)(basePath), 'test');
|
|
313
409
|
writePageObject(pageConfig, rootTemplateDirPath, testOutDirPath, editor);
|
|
314
410
|
}
|
|
315
411
|
else {
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Editor } from 'mem-fs-editor';
|
|
1
2
|
export declare const SupportedPageTypes: {
|
|
2
3
|
[id: string]: string;
|
|
3
4
|
};
|
|
@@ -20,6 +21,12 @@ export type FEV4OPAConfig = {
|
|
|
20
21
|
hideFilterBar: boolean;
|
|
21
22
|
filterBarItems?: string[];
|
|
22
23
|
};
|
|
24
|
+
export type JourneyParams = {
|
|
25
|
+
startPages: string[];
|
|
26
|
+
startLR: string | undefined;
|
|
27
|
+
navigatedOP: string | undefined;
|
|
28
|
+
hideFilterBar: boolean;
|
|
29
|
+
};
|
|
23
30
|
export type FEV4ManifestTarget = {
|
|
24
31
|
type?: string;
|
|
25
32
|
name?: string;
|
|
@@ -36,6 +43,13 @@ export type FEV4ManifestTarget = {
|
|
|
36
43
|
};
|
|
37
44
|
};
|
|
38
45
|
};
|
|
46
|
+
views?: {
|
|
47
|
+
paths?: Array<{
|
|
48
|
+
primary?: unknown[];
|
|
49
|
+
secondary?: unknown[];
|
|
50
|
+
defaultPath?: string;
|
|
51
|
+
}>;
|
|
52
|
+
};
|
|
39
53
|
};
|
|
40
54
|
};
|
|
41
55
|
};
|
|
@@ -103,6 +117,7 @@ export type ListReportFeatures = {
|
|
|
103
117
|
filterBarItems?: string[];
|
|
104
118
|
tableColumns?: Record<string, Record<string, string | number | boolean>>;
|
|
105
119
|
toolBarActions?: ActionButtonState[];
|
|
120
|
+
isALP?: boolean;
|
|
106
121
|
};
|
|
107
122
|
export interface ActionButtonState {
|
|
108
123
|
label: string;
|
|
@@ -135,6 +150,13 @@ export type AppFeatures = {
|
|
|
135
150
|
objectPages?: ObjectPageFeatures[];
|
|
136
151
|
fpm?: FPMFeatures;
|
|
137
152
|
};
|
|
153
|
+
export type WriteContext = {
|
|
154
|
+
config: FEV4OPAConfig;
|
|
155
|
+
rootV4TemplateDirPath: string;
|
|
156
|
+
testOutDirPath: string;
|
|
157
|
+
editor: Editor;
|
|
158
|
+
journeyParams: JourneyParams;
|
|
159
|
+
};
|
|
138
160
|
export type FormField = {
|
|
139
161
|
fieldGroupQualifier?: string;
|
|
140
162
|
field?: string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for reading the FLP sandbox HTML file and extracting the
|
|
3
|
+
* application hash (intent) from the sap-ushell-config applications object.
|
|
4
|
+
*/
|
|
5
|
+
import type { Editor } from 'mem-fs-editor';
|
|
6
|
+
/**
|
|
7
|
+
* Reads an FLP sandbox HTML file and extracts the first application key
|
|
8
|
+
* from the `sap-ushell-config` `applications` object.
|
|
9
|
+
*
|
|
10
|
+
* @param htmlRelativePath - path to the HTML file relative to `webapp/`
|
|
11
|
+
* (e.g. `test/flpSandbox.html`)
|
|
12
|
+
* @param webappPath - path to the webapp directory
|
|
13
|
+
* @param fs - mem-fs-editor instance used to read the file
|
|
14
|
+
* @returns the application key (e.g. `fincashbankmanage-tile`), or undefined
|
|
15
|
+
*/
|
|
16
|
+
export declare function readHashFromFlpSandbox(htmlRelativePath: string, webappPath: string, fs: Editor): string | undefined;
|
|
17
|
+
//# sourceMappingURL=flpSandboxUtils.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Utility for reading the FLP sandbox HTML file and extracting the
|
|
4
|
+
* application hash (intent) from the sap-ushell-config applications object.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.readHashFromFlpSandbox = readHashFromFlpSandbox;
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
/**
|
|
10
|
+
* Regex to extract the first application key from the sap-ushell-config
|
|
11
|
+
* `applications` block. Matches patterns like:
|
|
12
|
+
*
|
|
13
|
+
* applications: {
|
|
14
|
+
* "fincashbankmanage-tile": {
|
|
15
|
+
*
|
|
16
|
+
* Captures the quoted key (e.g. `fincashbankmanage-tile`).
|
|
17
|
+
*/
|
|
18
|
+
const APPLICATIONS_KEY_REGEX = /applications\s*:\s*\{[^"]*"([^"]+)"\s*:/;
|
|
19
|
+
/**
|
|
20
|
+
* Reads an FLP sandbox HTML file and extracts the first application key
|
|
21
|
+
* from the `sap-ushell-config` `applications` object.
|
|
22
|
+
*
|
|
23
|
+
* @param htmlRelativePath - path to the HTML file relative to `webapp/`
|
|
24
|
+
* (e.g. `test/flpSandbox.html`)
|
|
25
|
+
* @param webappPath - path to the webapp directory
|
|
26
|
+
* @param fs - mem-fs-editor instance used to read the file
|
|
27
|
+
* @returns the application key (e.g. `fincashbankmanage-tile`), or undefined
|
|
28
|
+
*/
|
|
29
|
+
function readHashFromFlpSandbox(htmlRelativePath, webappPath, fs) {
|
|
30
|
+
try {
|
|
31
|
+
const filePath = (0, node_path_1.join)(webappPath, htmlRelativePath);
|
|
32
|
+
const content = fs.read(filePath);
|
|
33
|
+
const match = APPLICATIONS_KEY_REGEX.exec(content);
|
|
34
|
+
return match?.[1];
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=flpSandboxUtils.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Logger } from '@sap-ux/logger';
|
|
2
2
|
import type { TreeAggregations, TreeModel } from '@sap/ux-specification/dist/types/src/parser';
|
|
3
|
-
import type { ActionButtonsResult, ActionButtonState, ButtonState, ButtonVisibilityResult, ListReportFeatures } from '../types';
|
|
3
|
+
import type { ActionButtonsResult, ActionButtonState, ButtonState, ButtonVisibilityResult, FEV4ManifestTarget, ListReportFeatures } from '../types';
|
|
4
4
|
import type { PageWithModelV4 } from '@sap/ux-specification/dist/types/src/parser/application';
|
|
5
|
+
import type { Manifest } from '@sap-ux/project-access';
|
|
5
6
|
/**
|
|
6
7
|
* Builds a button state object from button visibility result.
|
|
7
8
|
*
|
|
@@ -32,15 +33,33 @@ export declare function safeCheckButtonVisibility(metadata: string, entitySetNam
|
|
|
32
33
|
* @returns Array of action button states or empty array if error occurs
|
|
33
34
|
*/
|
|
34
35
|
export declare function safeCheckActionButtonStates(metadata: string, entitySetName: string, actionNames: string[], log?: Logger): ActionButtonState[];
|
|
36
|
+
/**
|
|
37
|
+
* Returns true when a ListReport manifest target is configured as an Analytical List Page.
|
|
38
|
+
* ALP targets have a `views.paths` array where at least one entry contains a `primary` array,
|
|
39
|
+
* indicating the dual-view (chart + table) layout used by ALP.
|
|
40
|
+
*
|
|
41
|
+
* @param target - the manifest routing target to inspect
|
|
42
|
+
* @returns true if the target represents an ALP configuration
|
|
43
|
+
*/
|
|
44
|
+
export declare function isALPManifestTarget(target: FEV4ManifestTarget): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Returns true if any ListReport target in the manifest is configured as an Analytical List Page.
|
|
47
|
+
*
|
|
48
|
+
* @param manifest - the application manifest
|
|
49
|
+
* @param targetKey - optional specific target key to check; if omitted all ListReport targets are checked
|
|
50
|
+
* @returns true if the target (or any ListReport target) is an ALP
|
|
51
|
+
*/
|
|
52
|
+
export declare function isALPFromManifest(manifest: Manifest, targetKey?: string): boolean;
|
|
35
53
|
/**
|
|
36
54
|
* Gets List Report features from the page model using ux-specification.
|
|
37
55
|
*
|
|
38
56
|
* @param listReportPage - the List Report page containing the tree model with feature definitions
|
|
39
57
|
* @param log - optional logger instance
|
|
40
58
|
* @param metadata - optional metadata for the OPA test generation
|
|
59
|
+
* @param manifest - optional application manifest, used to detect ALP configuration
|
|
41
60
|
* @returns feature data extracted from the List Report page model
|
|
42
61
|
*/
|
|
43
|
-
export declare function getListReportFeatures(listReportPage: PageWithModelV4, log?: Logger, metadata?: string): ListReportFeatures;
|
|
62
|
+
export declare function getListReportFeatures(listReportPage: PageWithModelV4, log?: Logger, metadata?: string, manifest?: Manifest): ListReportFeatures;
|
|
44
63
|
/**
|
|
45
64
|
* Retrieves toolbar action definitions from the given tree model.
|
|
46
65
|
*
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.buildButtonState = buildButtonState;
|
|
4
4
|
exports.safeCheckButtonVisibility = safeCheckButtonVisibility;
|
|
5
5
|
exports.safeCheckActionButtonStates = safeCheckActionButtonStates;
|
|
6
|
+
exports.isALPManifestTarget = isALPManifestTarget;
|
|
7
|
+
exports.isALPFromManifest = isALPFromManifest;
|
|
6
8
|
exports.getListReportFeatures = getListReportFeatures;
|
|
7
9
|
exports.getToolBarActions = getToolBarActions;
|
|
8
10
|
exports.checkButtonVisibility = checkButtonVisibility;
|
|
@@ -61,15 +63,45 @@ function safeCheckActionButtonStates(metadata, entitySetName, actionNames, log)
|
|
|
61
63
|
return [];
|
|
62
64
|
}
|
|
63
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Returns true when a ListReport manifest target is configured as an Analytical List Page.
|
|
68
|
+
* ALP targets have a `views.paths` array where at least one entry contains a `primary` array,
|
|
69
|
+
* indicating the dual-view (chart + table) layout used by ALP.
|
|
70
|
+
*
|
|
71
|
+
* @param target - the manifest routing target to inspect
|
|
72
|
+
* @returns true if the target represents an ALP configuration
|
|
73
|
+
*/
|
|
74
|
+
function isALPManifestTarget(target) {
|
|
75
|
+
return (target.options?.settings?.views?.paths?.some((path) => Array.isArray(path.primary) && path.primary.length > 0) ?? false);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Returns true if any ListReport target in the manifest is configured as an Analytical List Page.
|
|
79
|
+
*
|
|
80
|
+
* @param manifest - the application manifest
|
|
81
|
+
* @param targetKey - optional specific target key to check; if omitted all ListReport targets are checked
|
|
82
|
+
* @returns true if the target (or any ListReport target) is an ALP
|
|
83
|
+
*/
|
|
84
|
+
function isALPFromManifest(manifest, targetKey) {
|
|
85
|
+
const targets = manifest['sap.ui5']?.routing?.targets;
|
|
86
|
+
if (!targets) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const keysToCheck = targetKey ? [targetKey] : Object.keys(targets);
|
|
90
|
+
return keysToCheck.some((key) => {
|
|
91
|
+
const target = targets[key];
|
|
92
|
+
return target?.name === 'sap.fe.templates.ListReport' && isALPManifestTarget(target);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
64
95
|
/**
|
|
65
96
|
* Gets List Report features from the page model using ux-specification.
|
|
66
97
|
*
|
|
67
98
|
* @param listReportPage - the List Report page containing the tree model with feature definitions
|
|
68
99
|
* @param log - optional logger instance
|
|
69
100
|
* @param metadata - optional metadata for the OPA test generation
|
|
101
|
+
* @param manifest - optional application manifest, used to detect ALP configuration
|
|
70
102
|
* @returns feature data extracted from the List Report page model
|
|
71
103
|
*/
|
|
72
|
-
function getListReportFeatures(listReportPage, log, metadata) {
|
|
104
|
+
function getListReportFeatures(listReportPage, log, metadata, manifest) {
|
|
73
105
|
const buttonVisibility = metadata && listReportPage.entitySet
|
|
74
106
|
? safeCheckButtonVisibility(metadata, listReportPage.entitySet, log)
|
|
75
107
|
: undefined;
|
|
@@ -82,7 +114,8 @@ function getListReportFeatures(listReportPage, log, metadata) {
|
|
|
82
114
|
tableColumns: (0, modelUtils_1.getTableColumnData)(listReportPage.model, log),
|
|
83
115
|
toolBarActions: metadata && listReportPage.entitySet
|
|
84
116
|
? safeCheckActionButtonStates(metadata, listReportPage.entitySet, toolbarActions, log)
|
|
85
|
-
: []
|
|
117
|
+
: [],
|
|
118
|
+
isALP: manifest ? isALPFromManifest(manifest, listReportPage.name) : false
|
|
86
119
|
};
|
|
87
120
|
}
|
|
88
121
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Editor } from 'mem-fs-editor';
|
|
2
|
+
import type { Manifest } from '@sap-ux/project-access';
|
|
2
3
|
import type { Logger } from '@sap-ux/logger';
|
|
3
4
|
import type { PageWithModelV4 } from '@sap/ux-specification/dist/types/src/parser/application';
|
|
4
5
|
import type { TreeAggregation, TreeAggregations, TreeModel, ApplicationModel } from '@sap/ux-specification/dist/types/src/parser';
|
|
@@ -49,9 +50,10 @@ export interface PageWithModelV4WithProperties extends PageWithModelV4 {
|
|
|
49
50
|
* @param fs - optional mem-fs editor instance
|
|
50
51
|
* @param log - optional logger instance
|
|
51
52
|
* @param metadata - optional metadata for the OPA test generation
|
|
53
|
+
* @param manifest - optional application manifest, used to detect ALP configuration
|
|
52
54
|
* @returns feature data extracted from the application model
|
|
53
55
|
*/
|
|
54
|
-
export declare function getAppFeatures(basePath: string, fs?: Editor, log?: Logger, metadata?: string): Promise<AppFeatures>;
|
|
56
|
+
export declare function getAppFeatures(basePath: string, fs?: Editor, log?: Logger, metadata?: string, manifest?: Manifest): Promise<AppFeatures>;
|
|
55
57
|
/**
|
|
56
58
|
* Retrieves table column data from the page model using ux-specification.
|
|
57
59
|
*
|
package/dist/utils/modelUtils.js
CHANGED
|
@@ -20,9 +20,10 @@ const listReportUtils_1 = require("./listReportUtils");
|
|
|
20
20
|
* @param fs - optional mem-fs editor instance
|
|
21
21
|
* @param log - optional logger instance
|
|
22
22
|
* @param metadata - optional metadata for the OPA test generation
|
|
23
|
+
* @param manifest - optional application manifest, used to detect ALP configuration
|
|
23
24
|
* @returns feature data extracted from the application model
|
|
24
25
|
*/
|
|
25
|
-
async function getAppFeatures(basePath, fs, log, metadata) {
|
|
26
|
+
async function getAppFeatures(basePath, fs, log, metadata, manifest) {
|
|
26
27
|
const featureData = {};
|
|
27
28
|
let listReportPage = null;
|
|
28
29
|
let objectPages = null;
|
|
@@ -57,7 +58,7 @@ async function getAppFeatures(basePath, fs, log, metadata) {
|
|
|
57
58
|
// attempt to get individual feature data
|
|
58
59
|
try {
|
|
59
60
|
if (listReportPage) {
|
|
60
|
-
featureData.listReport = (0, listReportUtils_1.getListReportFeatures)(listReportPage, log, projectMetadata);
|
|
61
|
+
featureData.listReport = (0, listReportUtils_1.getListReportFeatures)(listReportPage, log, projectMetadata, manifest);
|
|
61
62
|
}
|
|
62
63
|
if (objectPages) {
|
|
63
64
|
log?.warn('Extracting Object Page features from application model');
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility for reading and updating a generated opaTests.qunit.js file.
|
|
3
|
+
* The file is modified in-place: only the sap.ui.require array is changed;
|
|
4
|
+
* all other content (formatting, comments, whitespace) is preserved exactly.
|
|
5
|
+
*/
|
|
6
|
+
import type { Editor } from 'mem-fs-editor';
|
|
7
|
+
/**
|
|
8
|
+
* Splices new module paths into the sap.ui.require array of the content string.
|
|
9
|
+
* Entries that are already present are skipped. All other content is preserved exactly.
|
|
10
|
+
*
|
|
11
|
+
* Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
|
|
12
|
+
* ReDoS on crafted inputs. Valid generated files are well within this limit.
|
|
13
|
+
*
|
|
14
|
+
* @param fileContent - the full content of the opaTests.qunit.js file
|
|
15
|
+
* @param moduleNames - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
|
|
16
|
+
* @returns the updated file content, or the original content unchanged if nothing was added
|
|
17
|
+
*/
|
|
18
|
+
export declare function spliceModulesIntoQUnitContent(fileContent: string, moduleNames: string[]): string;
|
|
19
|
+
/**
|
|
20
|
+
* Reads opaTests.qunit.js from webapp/test/integration_old and extracts the html
|
|
21
|
+
* launch target (path, query parameters, and hash fragment) from the launchUrl
|
|
22
|
+
* line, e.g. `test/flpSandbox.html?sap-ui-xx-viewCache=false#myApp-tile`.
|
|
23
|
+
* Returns undefined if the file cannot be read or the pattern is not found.
|
|
24
|
+
*
|
|
25
|
+
* @param testPath - path to the test output directory (`.../webapp/test`)
|
|
26
|
+
* @param fs - mem-fs-editor instance used to read the file
|
|
27
|
+
* @returns the html target string, or undefined if not found
|
|
28
|
+
*/
|
|
29
|
+
export declare function readHtmlTargetFromQUnitJs(testPath: string, fs: Editor): string | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Appends `/webapp/test/integration_old` to the project's `.gitignore`.
|
|
32
|
+
* Creates the file if it does not exist. Skips if the entry is already present.
|
|
33
|
+
*
|
|
34
|
+
* @param basePath - project root (contains .gitignore)
|
|
35
|
+
* @param fs - mem-fs-editor instance used to read and write the file
|
|
36
|
+
*/
|
|
37
|
+
export declare function addIntegrationOldToGitignore(basePath: string, fs: Editor): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Reads opaTests.qunit.js from the project, adds module paths to the
|
|
40
|
+
* sap.ui.require array, and writes the updated content back.
|
|
41
|
+
* Entries that are already present are skipped.
|
|
42
|
+
* All other file content is preserved exactly.
|
|
43
|
+
*
|
|
44
|
+
* @param filePaths - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
|
|
45
|
+
* @param projectPath - path to the test output directory (`.../webapp/test`)
|
|
46
|
+
* @param fs - mem-fs-editor instance used to read and write the file
|
|
47
|
+
*/
|
|
48
|
+
export declare function addPathsToQUnitJs(filePaths: string[], projectPath: string, fs: Editor): void;
|
|
49
|
+
/**
|
|
50
|
+
* Page entry to splice into an existing JourneyRunner.js.
|
|
51
|
+
*/
|
|
52
|
+
export interface JourneyRunnerPage {
|
|
53
|
+
/** The page's targetKey, used as both the variable name and `onThe<targetKey>` key */
|
|
54
|
+
targetKey: string;
|
|
55
|
+
/** The app module path prefix (e.g. "project1/test/integration/pages") */
|
|
56
|
+
appPath: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Splices new page entries into the three locations of an existing JourneyRunner.js:
|
|
60
|
+
* - the sap.ui.define dependency array
|
|
61
|
+
* - the function parameter list
|
|
62
|
+
* - the pages object literal
|
|
63
|
+
*
|
|
64
|
+
* Pages already present (detected by their module path in the define array) are skipped.
|
|
65
|
+
* All other content — formatting, comments, whitespace — is preserved exactly.
|
|
66
|
+
*
|
|
67
|
+
* Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
|
|
68
|
+
* ReDoS on crafted inputs. Valid generated files are well within this limit.
|
|
69
|
+
*
|
|
70
|
+
* @param fileContent - the full content of the JourneyRunner.js file
|
|
71
|
+
* @param pages - pages to add
|
|
72
|
+
* @returns the updated file content, or the original content unchanged if nothing was added
|
|
73
|
+
*/
|
|
74
|
+
export declare function splicePageIntoJourneyRunner(fileContent: string, pages: JourneyRunnerPage[]): string;
|
|
75
|
+
/**
|
|
76
|
+
* Reads JourneyRunner.js from the project, adds new page entries to all three
|
|
77
|
+
* locations (define array, function params, pages object), and writes the updated
|
|
78
|
+
* content back. Pages already present are skipped.
|
|
79
|
+
*
|
|
80
|
+
* @param pages - pages to add
|
|
81
|
+
* @param testOutDirPath - path to the test output directory (`.../webapp/test`)
|
|
82
|
+
* @param fs - mem-fs-editor instance used to read and write the file
|
|
83
|
+
*/
|
|
84
|
+
export declare function addPagesToJourneyRunner(pages: JourneyRunnerPage[], testOutDirPath: string, fs: Editor): void;
|
|
85
|
+
/**
|
|
86
|
+
* Returns true if any UI5 yaml file in the project contains a `fiori-tools-preview`
|
|
87
|
+
* middleware whose `test` array includes an entry with `framework: OPA5`.
|
|
88
|
+
*
|
|
89
|
+
* @param basePath - project root directory
|
|
90
|
+
* @returns true when OPA5 is configured in a preview middleware, false otherwise
|
|
91
|
+
*/
|
|
92
|
+
export declare function hasVirtualOPA5(basePath: string): Promise<boolean>;
|
|
93
|
+
//# sourceMappingURL=opaQUnitUtils.d.ts.map
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Utility for reading and updating a generated opaTests.qunit.js file.
|
|
4
|
+
* The file is modified in-place: only the sap.ui.require array is changed;
|
|
5
|
+
* all other content (formatting, comments, whitespace) is preserved exactly.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.spliceModulesIntoQUnitContent = spliceModulesIntoQUnitContent;
|
|
9
|
+
exports.readHtmlTargetFromQUnitJs = readHtmlTargetFromQUnitJs;
|
|
10
|
+
exports.addIntegrationOldToGitignore = addIntegrationOldToGitignore;
|
|
11
|
+
exports.addPathsToQUnitJs = addPathsToQUnitJs;
|
|
12
|
+
exports.splicePageIntoJourneyRunner = splicePageIntoJourneyRunner;
|
|
13
|
+
exports.addPagesToJourneyRunner = addPagesToJourneyRunner;
|
|
14
|
+
exports.hasVirtualOPA5 = hasVirtualOPA5;
|
|
15
|
+
const node_path_1 = require("node:path");
|
|
16
|
+
const flpSandboxUtils_1 = require("./flpSandboxUtils");
|
|
17
|
+
const project_access_1 = require("@sap-ux/project-access");
|
|
18
|
+
/** Relative path from the test output directory to opaTests.qunit.js */
|
|
19
|
+
const OPA_QUNIT_FILE = (0, node_path_1.join)('integration', 'opaTests.qunit.js');
|
|
20
|
+
/**
|
|
21
|
+
* The regex matches the opening bracket of the sap.ui.require array and
|
|
22
|
+
* captures everything up to (but not including) the closing bracket followed
|
|
23
|
+
* by `], function`. This lets us splice new entries in without disturbing
|
|
24
|
+
* any other part of the file.
|
|
25
|
+
*
|
|
26
|
+
* Matches:
|
|
27
|
+
* sap.ui.require(\n [\n "a",\n "b",\n ], function
|
|
28
|
+
* ^^^^^^^^^^^^^^^^^
|
|
29
|
+
* captured as group 1
|
|
30
|
+
*
|
|
31
|
+
* The `d` flag enables `match.indices` so we can read the capture group's
|
|
32
|
+
* exact start/end positions without fragile string searching.
|
|
33
|
+
*/
|
|
34
|
+
const SAP_UI_REQUIRE_ARRAY_REGEX = /sap\.ui\.require\s*\(\s*\[([^\]]*)\]\s*,\s*function/d;
|
|
35
|
+
/** ReDoS mitigation: files larger than this are returned unchanged rather than matched with regex. */
|
|
36
|
+
const MAX_FILE_CONTENT_LENGTH = 10000;
|
|
37
|
+
/**
|
|
38
|
+
* Splices new module paths into the sap.ui.require array of the content string.
|
|
39
|
+
* Entries that are already present are skipped. All other content is preserved exactly.
|
|
40
|
+
*
|
|
41
|
+
* Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
|
|
42
|
+
* ReDoS on crafted inputs. Valid generated files are well within this limit.
|
|
43
|
+
*
|
|
44
|
+
* @param fileContent - the full content of the opaTests.qunit.js file
|
|
45
|
+
* @param moduleNames - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
|
|
46
|
+
* @returns the updated file content, or the original content unchanged if nothing was added
|
|
47
|
+
*/
|
|
48
|
+
function spliceModulesIntoQUnitContent(fileContent, moduleNames) {
|
|
49
|
+
if (fileContent.length > MAX_FILE_CONTENT_LENGTH) {
|
|
50
|
+
return fileContent;
|
|
51
|
+
}
|
|
52
|
+
const match = SAP_UI_REQUIRE_ARRAY_REGEX.exec(fileContent);
|
|
53
|
+
if (!match) {
|
|
54
|
+
return fileContent;
|
|
55
|
+
}
|
|
56
|
+
const arrayBody = match[1]; // everything between `[` and `]`
|
|
57
|
+
// Collect existing quoted entries so we don't add duplicates
|
|
58
|
+
const existingEntries = new Set();
|
|
59
|
+
const entryRegex = /"([^"]+)"/g;
|
|
60
|
+
let entryMatch;
|
|
61
|
+
while ((entryMatch = entryRegex.exec(arrayBody)) !== null) {
|
|
62
|
+
existingEntries.add(entryMatch[1]);
|
|
63
|
+
}
|
|
64
|
+
const toAdd = moduleNames.filter((name) => !existingEntries.has(name));
|
|
65
|
+
if (toAdd.length === 0) {
|
|
66
|
+
return fileContent;
|
|
67
|
+
}
|
|
68
|
+
// Detect the indentation used by the existing entries (e.g. four spaces)
|
|
69
|
+
const indentMatch = /^([ \t]+)"/m.exec(arrayBody);
|
|
70
|
+
const indent = indentMatch ? indentMatch[1] : ' ';
|
|
71
|
+
// Build the lines to insert, each terminated with a trailing comma
|
|
72
|
+
const newLines = toAdd.map((name) => `${indent}"${name}",`).join('\n');
|
|
73
|
+
// Insert just before the closing `]` using the capture group's end index.
|
|
74
|
+
const insertPosition = match.indices?.[1]?.[1];
|
|
75
|
+
if (insertPosition === undefined) {
|
|
76
|
+
return fileContent;
|
|
77
|
+
}
|
|
78
|
+
// Ensure the last existing entry ends with a comma before inserting after it.
|
|
79
|
+
const trimmedBefore = fileContent.slice(0, insertPosition).trimEnd();
|
|
80
|
+
const needsComma = !trimmedBefore.endsWith(',');
|
|
81
|
+
const commaFix = needsComma ? ',' : '';
|
|
82
|
+
const trailingWhitespace = fileContent.slice(trimmedBefore.length, insertPosition);
|
|
83
|
+
const after = fileContent.slice(insertPosition);
|
|
84
|
+
return `${trimmedBefore}${commaFix}\n${newLines}\n${trailingWhitespace}${after}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Regex to extract the html launch target from a `launchUrl` line of the form:
|
|
88
|
+
* sap.ui.require.toUrl('...') + '/some/path.html?params#hash'
|
|
89
|
+
* Captures the path/query/hash portion after the closing `') + '`.
|
|
90
|
+
*/
|
|
91
|
+
const LAUNCH_URL_REGEX = /\.toUrl\s*\([^)]+\)\s*\+\s*'([^']+)'/;
|
|
92
|
+
/**
|
|
93
|
+
* Reads opaTests.qunit.js from webapp/test/integration_old and extracts the html
|
|
94
|
+
* launch target (path, query parameters, and hash fragment) from the launchUrl
|
|
95
|
+
* line, e.g. `test/flpSandbox.html?sap-ui-xx-viewCache=false#myApp-tile`.
|
|
96
|
+
* Returns undefined if the file cannot be read or the pattern is not found.
|
|
97
|
+
*
|
|
98
|
+
* @param testPath - path to the test output directory (`.../webapp/test`)
|
|
99
|
+
* @param fs - mem-fs-editor instance used to read the file
|
|
100
|
+
* @returns the html target string, or undefined if not found
|
|
101
|
+
*/
|
|
102
|
+
function readHtmlTargetFromQUnitJs(testPath, fs) {
|
|
103
|
+
try {
|
|
104
|
+
const integrationOldDir = (0, node_path_1.join)(testPath, 'integration_old');
|
|
105
|
+
let filePath = (0, node_path_1.join)(integrationOldDir, 'opaTests.qunit.js');
|
|
106
|
+
if (!fs.exists(filePath)) {
|
|
107
|
+
filePath = (0, node_path_1.join)(integrationOldDir, 'Opa.qunit.js');
|
|
108
|
+
}
|
|
109
|
+
const content = fs.read(filePath);
|
|
110
|
+
const match = LAUNCH_URL_REGEX.exec(content);
|
|
111
|
+
const launchUrl = match?.[1].replace(/^\//, '');
|
|
112
|
+
if (!launchUrl) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
// If the launch URL already contains a hash fragment, use it as-is
|
|
116
|
+
if (launchUrl.includes('#')) {
|
|
117
|
+
return launchUrl;
|
|
118
|
+
}
|
|
119
|
+
// No hash fragment — read the referenced HTML file to extract the
|
|
120
|
+
// application key from the sap-ushell-config applications object
|
|
121
|
+
const htmlPath = launchUrl.split('?')[0];
|
|
122
|
+
const hash = (0, flpSandboxUtils_1.readHashFromFlpSandbox)(htmlPath, (0, node_path_1.join)(testPath, '..'), fs);
|
|
123
|
+
return hash ? `${launchUrl}#${hash}` : launchUrl;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** The gitignore entry added for the moved integration test folder */
|
|
130
|
+
const INTEGRATION_OLD_GITIGNORE_ENTRY = '/webapp/test/integration_old';
|
|
131
|
+
/**
|
|
132
|
+
* Appends `/webapp/test/integration_old` to the project's `.gitignore`.
|
|
133
|
+
* Creates the file if it does not exist. Skips if the entry is already present.
|
|
134
|
+
*
|
|
135
|
+
* @param basePath - project root (contains .gitignore)
|
|
136
|
+
* @param fs - mem-fs-editor instance used to read and write the file
|
|
137
|
+
*/
|
|
138
|
+
async function addIntegrationOldToGitignore(basePath, fs) {
|
|
139
|
+
const filePath = (0, node_path_1.join)(basePath, '.gitignore');
|
|
140
|
+
const existing = fs.exists(filePath) ? fs.read(filePath) : '';
|
|
141
|
+
const lines = existing.split('\n');
|
|
142
|
+
if (lines.some((line) => line.trim() === INTEGRATION_OLD_GITIGNORE_ENTRY)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const updated = existing.endsWith('\n') || existing === ''
|
|
146
|
+
? `${existing}${INTEGRATION_OLD_GITIGNORE_ENTRY}\n`
|
|
147
|
+
: `${existing}\n${INTEGRATION_OLD_GITIGNORE_ENTRY}\n`;
|
|
148
|
+
fs.write(filePath, updated);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Reads opaTests.qunit.js from the project, adds module paths to the
|
|
152
|
+
* sap.ui.require array, and writes the updated content back.
|
|
153
|
+
* Entries that are already present are skipped.
|
|
154
|
+
* All other file content is preserved exactly.
|
|
155
|
+
*
|
|
156
|
+
* @param filePaths - module paths to add (e.g. ["myApp/test/integration/SomeJourney"])
|
|
157
|
+
* @param projectPath - path to the test output directory (`.../webapp/test`)
|
|
158
|
+
* @param fs - mem-fs-editor instance used to read and write the file
|
|
159
|
+
*/
|
|
160
|
+
function addPathsToQUnitJs(filePaths, projectPath, fs) {
|
|
161
|
+
try {
|
|
162
|
+
const filePath = (0, node_path_1.join)(projectPath, OPA_QUNIT_FILE);
|
|
163
|
+
const content = fs.read(filePath);
|
|
164
|
+
const updated = spliceModulesIntoQUnitContent(content, filePaths);
|
|
165
|
+
if (updated !== content) {
|
|
166
|
+
fs.write(filePath, updated);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
// If the file doesn't exist or can't be read, do nothing
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** Relative path from the test output directory to JourneyRunner.js */
|
|
174
|
+
const JOURNEY_RUNNER_FILE = (0, node_path_1.join)('integration', 'pages', 'JourneyRunner.js');
|
|
175
|
+
/**
|
|
176
|
+
* Splices new page entries into the three locations of an existing JourneyRunner.js:
|
|
177
|
+
* - the sap.ui.define dependency array
|
|
178
|
+
* - the function parameter list
|
|
179
|
+
* - the pages object literal
|
|
180
|
+
*
|
|
181
|
+
* Pages already present (detected by their module path in the define array) are skipped.
|
|
182
|
+
* All other content — formatting, comments, whitespace — is preserved exactly.
|
|
183
|
+
*
|
|
184
|
+
* Note: files exceeding MAX_FILE_CONTENT_LENGTH characters are returned unchanged to prevent
|
|
185
|
+
* ReDoS on crafted inputs. Valid generated files are well within this limit.
|
|
186
|
+
*
|
|
187
|
+
* @param fileContent - the full content of the JourneyRunner.js file
|
|
188
|
+
* @param pages - pages to add
|
|
189
|
+
* @returns the updated file content, or the original content unchanged if nothing was added
|
|
190
|
+
*/
|
|
191
|
+
function splicePageIntoJourneyRunner(fileContent, pages) {
|
|
192
|
+
if (fileContent.length > MAX_FILE_CONTENT_LENGTH) {
|
|
193
|
+
return fileContent;
|
|
194
|
+
}
|
|
195
|
+
// Determine which pages are not yet present by checking the define array
|
|
196
|
+
const toAdd = pages.filter((page) => {
|
|
197
|
+
const modulePath = `${page.appPath}/test/integration/pages/${page.targetKey}`;
|
|
198
|
+
return !fileContent.includes(`"${modulePath}"`);
|
|
199
|
+
});
|
|
200
|
+
if (toAdd.length === 0) {
|
|
201
|
+
return fileContent;
|
|
202
|
+
}
|
|
203
|
+
let result = fileContent;
|
|
204
|
+
// 1. Splice into the sap.ui.define([...]) array.
|
|
205
|
+
// Captures everything between the opening `[` and the closing `]` before `, function`.
|
|
206
|
+
const defineArrayRegex = /sap\.ui\.define\s*\(\s*\[([^\]]*)\]\s*,\s*function/d;
|
|
207
|
+
const defineMatch = defineArrayRegex.exec(result);
|
|
208
|
+
if (defineMatch?.indices?.[1]) {
|
|
209
|
+
const [bodyStart, bodyEnd] = defineMatch.indices[1];
|
|
210
|
+
const arrayBody = result.slice(bodyStart, bodyEnd);
|
|
211
|
+
// Detect indentation from the first existing module entry line
|
|
212
|
+
const indentMatch = /^([ \t]+)"/m.exec(arrayBody);
|
|
213
|
+
const indent = indentMatch ? indentMatch[1] : '\t';
|
|
214
|
+
const newEntries = toAdd
|
|
215
|
+
.map((page) => `${indent}"${page.appPath}/test/integration/pages/${page.targetKey}",`)
|
|
216
|
+
.join('\n');
|
|
217
|
+
// Ensure the last existing entry ends with a comma before we insert after it.
|
|
218
|
+
// The trimmed body ends at bodyEnd; look back from there for the last non-whitespace char.
|
|
219
|
+
const trimmedEnd = result.slice(0, bodyEnd).trimEnd();
|
|
220
|
+
const needsComma = !trimmedEnd.endsWith(',');
|
|
221
|
+
const commaFix = needsComma ? ',' : '';
|
|
222
|
+
const trailingWhitespace = result.slice(trimmedEnd.length, bodyEnd);
|
|
223
|
+
result = `${trimmedEnd}${commaFix}\n${newEntries}` + `${trailingWhitespace}${result.slice(bodyEnd)}`;
|
|
224
|
+
}
|
|
225
|
+
// 2. Splice into the function parameter list: `function (JourneyRunner, A, B)`.
|
|
226
|
+
// Captures everything between `(` and `)` of the function signature.
|
|
227
|
+
const funcParamRegex = /\]\s*,\s*function\s*\(([^)]*)\)\s*\{/d;
|
|
228
|
+
const funcMatch = funcParamRegex.exec(result);
|
|
229
|
+
if (funcMatch?.indices?.[1]) {
|
|
230
|
+
const [, paramEnd] = funcMatch.indices[1];
|
|
231
|
+
const newParams = toAdd.map((page) => `, ${page.targetKey}`).join('');
|
|
232
|
+
result = `${result.slice(0, paramEnd)}${newParams}${result.slice(paramEnd)}`;
|
|
233
|
+
}
|
|
234
|
+
// 3. Splice into the pages object: `pages: { onTheFoo: Foo, ... }`.
|
|
235
|
+
// Captures everything between `pages: {` and the closing `}`.
|
|
236
|
+
const pagesObjectRegex = /pages\s*:\s*\{([^}]*)\}/d;
|
|
237
|
+
const pagesMatch = pagesObjectRegex.exec(result);
|
|
238
|
+
if (pagesMatch?.indices?.[1]) {
|
|
239
|
+
const [, pagesBodyEnd] = pagesMatch.indices[1];
|
|
240
|
+
const pagesBody = result.slice(pagesMatch.indices[1][0], pagesBodyEnd);
|
|
241
|
+
// Detect indentation from the first existing page entry
|
|
242
|
+
const pageIndentMatch = /^([ \t]+)on/m.exec(pagesBody);
|
|
243
|
+
const pageIndent = pageIndentMatch ? pageIndentMatch[1] : '\t\t\t';
|
|
244
|
+
const newPageEntries = toAdd
|
|
245
|
+
.map((page) => `${pageIndent}onThe${page.targetKey}: ${page.targetKey},`)
|
|
246
|
+
.join('\n');
|
|
247
|
+
// Ensure the last existing entry ends with a comma before we insert after it.
|
|
248
|
+
const trimmedPagesEnd = result.slice(0, pagesBodyEnd).trimEnd();
|
|
249
|
+
const needsComma = !trimmedPagesEnd.endsWith(',');
|
|
250
|
+
const commaFix = needsComma ? ',' : '';
|
|
251
|
+
const trailingWhitespace = result.slice(trimmedPagesEnd.length, pagesBodyEnd);
|
|
252
|
+
result =
|
|
253
|
+
`${trimmedPagesEnd}${commaFix}\n${newPageEntries}` + `${trailingWhitespace}${result.slice(pagesBodyEnd)}`;
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Reads JourneyRunner.js from the project, adds new page entries to all three
|
|
259
|
+
* locations (define array, function params, pages object), and writes the updated
|
|
260
|
+
* content back. Pages already present are skipped.
|
|
261
|
+
*
|
|
262
|
+
* @param pages - pages to add
|
|
263
|
+
* @param testOutDirPath - path to the test output directory (`.../webapp/test`)
|
|
264
|
+
* @param fs - mem-fs-editor instance used to read and write the file
|
|
265
|
+
*/
|
|
266
|
+
function addPagesToJourneyRunner(pages, testOutDirPath, fs) {
|
|
267
|
+
try {
|
|
268
|
+
const filePath = (0, node_path_1.join)(testOutDirPath, JOURNEY_RUNNER_FILE);
|
|
269
|
+
const content = fs.read(filePath);
|
|
270
|
+
const updated = splicePageIntoJourneyRunner(content, pages);
|
|
271
|
+
if (updated !== content) {
|
|
272
|
+
fs.write(filePath, updated);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// If the file doesn't exist or can't be read, do nothing
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Returns true if any UI5 yaml file in the project contains a `fiori-tools-preview`
|
|
281
|
+
* middleware whose `test` array includes an entry with `framework: OPA5`.
|
|
282
|
+
*
|
|
283
|
+
* @param basePath - project root directory
|
|
284
|
+
* @returns true when OPA5 is configured in a preview middleware, false otherwise
|
|
285
|
+
*/
|
|
286
|
+
async function hasVirtualOPA5(basePath) {
|
|
287
|
+
const yamlFileNames = await (0, project_access_1.getAllUi5YamlFileNames)(basePath);
|
|
288
|
+
for (const fileName of yamlFileNames) {
|
|
289
|
+
try {
|
|
290
|
+
const ui5Config = await (0, project_access_1.readUi5Yaml)(basePath, fileName);
|
|
291
|
+
const previewMiddleware = ui5Config.findCustomMiddleware('fiori-tools-preview');
|
|
292
|
+
const testEntries = previewMiddleware?.configuration?.test;
|
|
293
|
+
if (testEntries?.some((entry) => entry.framework === 'OPA5')) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Skip yaml files that cannot be read
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return false;
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=opaQUnitUtils.js.map
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sap-ux/ui5-test-writer",
|
|
3
3
|
"description": "SAP UI5 tests writer",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.104",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "https://github.com/SAP/open-ux-tools.git",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"@sap/ux-specification": "1.144.0",
|
|
28
28
|
"@sap-ux/edmx-parser": "0.10.0",
|
|
29
29
|
"@sap-ux/annotation-converter": "0.10.21",
|
|
30
|
-
"@sap-ux/project-access": "1.35.20",
|
|
31
30
|
"@sap-ux/ui5-application-writer": "1.8.5",
|
|
31
|
+
"@sap-ux/project-access": "1.35.20",
|
|
32
32
|
"@sap-ux/logger": "0.8.5"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
@@ -41,7 +41,7 @@ sap.ui.define([
|
|
|
41
41
|
|
|
42
42
|
opaTest("Check table columns and actions", function (Given, When, Then) {
|
|
43
43
|
<%_ if (toolBarActions && toolBarActions.length > 0) { -%>
|
|
44
|
-
<%_ if (createButton.visible) { _%>
|
|
44
|
+
<%_ if (createButton.visible && !isALP) { _%>
|
|
45
45
|
Then.onThe<%- startLR%>.onTable().iCheckCreate({ visible: true });
|
|
46
46
|
// Then.onthe<%- startLR%>.onTable().iPressCreate();
|
|
47
47
|
<%_ } _%>
|
|
@@ -60,13 +60,15 @@ sap.ui.define([
|
|
|
60
60
|
});
|
|
61
61
|
<% } -%>
|
|
62
62
|
|
|
63
|
-
<% if (bodySections?.length > 0) { -%>
|
|
63
|
+
<% if (bodySections?.length > 0 && !isStandalone) { -%>
|
|
64
64
|
opaTest("Check body sections of the Object Page", function (Given, When, Then) {
|
|
65
65
|
<% if (bodySections?.length > 1) { -%>
|
|
66
66
|
Then.onThe<%- name%>.iCheckNumberOfSections(<%- bodySections.length %>);
|
|
67
67
|
<% } -%>
|
|
68
68
|
<% bodySections.forEach(function(section) { -%>
|
|
69
|
+
<% if (bodySections.length > 1) { -%>
|
|
69
70
|
When.onThe<%- name%>.iPressSectionIconTabFilterButton("<%- section.id %>");
|
|
71
|
+
<% } -%>
|
|
70
72
|
Then.onThe<%- name%>.iCheckSection({ section: "<%- section.id %>" });
|
|
71
73
|
<% if (section?.subSections?.length > 0) { -%>
|
|
72
74
|
<% section.subSections.forEach(function(subSection) { -%>
|