Skip to content

Commit d36d5d7

Browse files
fix(ui5-test-writer): fixes for app info generation (#4578)
* fixes * more fixes * Linting auto fix commit * sonar * sonar * sonar * tests * Linting auto fix commit * cleanup, alp fix * Linting auto fix commit * changeset * fix trailing line and missing comma * add tests --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 5b957ea commit d36d5d7

11 files changed

Lines changed: 926 additions & 91 deletions

File tree

.changeset/full-dots-punch.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-ux/ui5-test-writer': patch
3+
---
4+
5+
app info generation fixes

packages/ui5-test-writer/src/fiori-elements-opa-writer.ts

Lines changed: 130 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,85 @@ import { getAppFeatures } from './utils/modelUtils';
2020
import {
2121
addIntegrationOldToGitignore,
2222
addPathsToQUnitJs,
23+
addPagesToJourneyRunner,
2324
hasVirtualOPA5,
24-
readHtmlTargetFromQUnitJs
25+
readHtmlTargetFromQUnitJs,
26+
type JourneyRunnerPage
2527
} from './utils/opaQUnitUtils';
2628

29+
/**
30+
* Generate OPA test files for a Fiori elements for OData V4 application.
31+
* Note: this can potentially overwrite existing files in the webapp/test folder.
32+
*
33+
* @param basePath - the absolute target path where the application will be generated
34+
* @param opaConfig - parameters for the generation
35+
* @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
36+
* @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
37+
* @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
38+
* @param metadata - optional metadata for the OPA test generation
39+
* @param fs - an optional reference to a mem-fs editor
40+
* @param log - optional logger instance
41+
* @param standalone - opa test generation run standalone, not during app generation
42+
* @returns Reference to a mem-fs-editor
43+
*/
44+
export async function generateOPAFiles(
45+
basePath: string,
46+
opaConfig: { scriptName?: string; appID?: string; htmlTarget?: string },
47+
metadata?: string,
48+
fs?: Editor,
49+
log?: Logger,
50+
standalone = false
51+
): Promise<Editor> {
52+
const editor = fs ?? create(createStorage());
53+
54+
const manifest = readManifest(editor, basePath);
55+
const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest);
56+
57+
const config = createConfig(manifest, opaConfig, hideFilterBar);
58+
59+
const rootCommonTemplateDirPath = join(__dirname, '../templates/common');
60+
const rootV4TemplateDirPath = join(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
61+
const testOutDirPath = join(await getWebappPath(basePath), 'test');
62+
63+
// Access ux-specification to get feature data for OPA test generation
64+
const appFeatures = await getAppFeatures(basePath, editor, log, metadata, manifest);
65+
// OPA Journey file
66+
const startPages = config.pages.filter((page) => page.isStartup).map((page) => page.targetKey);
67+
const LROP = findLROP(config.pages, manifest);
68+
const journeyParams: JourneyParams = {
69+
startPages,
70+
startLR: LROP.pageLR?.targetKey,
71+
navigatedOP: LROP.pageOP?.targetKey,
72+
hideFilterBar: config.hideFilterBar
73+
};
74+
75+
const writeContext: WriteContext = { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams };
76+
77+
if (standalone) {
78+
const hasJourneyRunner = existsSync(join(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'));
79+
const virtualOPA5Configured = await hasVirtualOPA5(basePath);
80+
if (hasJourneyRunner) {
81+
writeJourneyFiles(appFeatures, writeContext, true, true, virtualOPA5Configured);
82+
} else {
83+
editor.move(join(testOutDirPath, 'integration', '**'), join(testOutDirPath, 'integration_old'));
84+
85+
await addIntegrationOldToGitignore(basePath, editor);
86+
const htmlTarget = readHtmlTargetFromQUnitJs(testOutDirPath, editor) ?? config.htmlTarget;
87+
const standaloneConfig = { ...config, htmlTarget };
88+
const standaloneWriteContext: WriteContext = { ...writeContext, config: standaloneConfig };
89+
if (!virtualOPA5Configured) {
90+
writeCommonAndPageFiles(standaloneWriteContext, rootCommonTemplateDirPath);
91+
}
92+
writeJourneyFiles(appFeatures, standaloneWriteContext, true, hasJourneyRunner, virtualOPA5Configured);
93+
}
94+
} else {
95+
writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath);
96+
writeJourneyFiles(appFeatures, writeContext, false);
97+
}
98+
99+
return editor;
100+
}
101+
27102
/**
28103
* Reads the manifest for an app.
29104
*
@@ -263,7 +338,6 @@ function writeCommonAndPageFiles(writeContext: WriteContext, rootCommonTemplateD
263338
}
264339
);
265340

266-
// Pages files (one for each page in the app)
267341
config.pages.forEach((page) => {
268342
writePageObject(page, rootV4TemplateDirPath, testOutDirPath, editor);
269343
});
@@ -290,6 +364,36 @@ function writeCommonAndPageFiles(writeContext: WriteContext, rootCommonTemplateD
290364
);
291365
}
292366

367+
/**
368+
* Checks whether a page object file already exists for the given feature name.
369+
* If it doesn't exist, finds the matching page config and writes the file.
370+
*
371+
* @param featureName - the feature/page name (equals the manifest targetKey)
372+
* @param config - the OPA config containing all page configurations
373+
* @param rootV4TemplateDirPath - template root directory for v4 templates
374+
* @param testOutDirPath - output test directory (.../webapp/test)
375+
* @param editor - a reference to a mem-fs editor
376+
* @returns JourneyRunnerPage if the page was newly created, undefined otherwise
377+
*/
378+
function ensurePageExists(
379+
featureName: string,
380+
config: FEV4OPAConfig,
381+
rootV4TemplateDirPath: string,
382+
testOutDirPath: string,
383+
editor: Editor
384+
): JourneyRunnerPage | undefined {
385+
const pageFilePath = join(testOutDirPath, 'integration', 'pages', `${featureName}.js`);
386+
if (editor.exists(pageFilePath)) {
387+
return undefined;
388+
}
389+
const pageConfig = config.pages.find((p) => p.targetKey === featureName);
390+
if (pageConfig) {
391+
writePageObject(pageConfig, rootV4TemplateDirPath, testOutDirPath, editor);
392+
return { targetKey: featureName, appPath: config.appPath };
393+
}
394+
return undefined;
395+
}
396+
293397
/**
294398
* Writes journey files for list report, object pages and FPM pages.
295399
*
@@ -308,6 +412,7 @@ function writeJourneyFiles(
308412
): void {
309413
const { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams } = writeContext;
310414
const generatedJourneyPages: string[] = [];
415+
const newPages: JourneyRunnerPage[] = [];
311416

312417
if (appFeatures.listReport?.name) {
313418
editor.copyTpl(
@@ -323,6 +428,16 @@ function writeJourneyFiles(
323428
}
324429
);
325430
generatedJourneyPages.push(appFeatures.listReport.name);
431+
const lrPage = ensurePageExists(
432+
appFeatures.listReport.name,
433+
config,
434+
rootV4TemplateDirPath,
435+
testOutDirPath,
436+
editor
437+
);
438+
if (lrPage) {
439+
newPages.push(lrPage);
440+
}
326441
}
327442

328443
if (appFeatures.objectPages && appFeatures.objectPages.length > 0) {
@@ -342,6 +457,10 @@ function writeJourneyFiles(
342457
}
343458
);
344459
generatedJourneyPages.push(objectPage.name);
460+
const opPage = ensurePageExists(objectPage.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
461+
if (opPage) {
462+
newPages.push(opPage);
463+
}
345464
}
346465
});
347466
}
@@ -360,6 +479,14 @@ function writeJourneyFiles(
360479
}
361480
);
362481
generatedJourneyPages.push(appFeatures.fpm.name);
482+
const fpmPage = ensurePageExists(appFeatures.fpm.name, config, rootV4TemplateDirPath, testOutDirPath, editor);
483+
if (fpmPage) {
484+
newPages.push(fpmPage);
485+
}
486+
}
487+
488+
if (newPages.length > 0) {
489+
addPagesToJourneyRunner(newPages, testOutDirPath, editor);
363490
}
364491

365492
if (!virtualOPA5Configured) {
@@ -410,79 +537,6 @@ function writePageObject(
410537
);
411538
}
412539

413-
/**
414-
* Generate OPA test files for a Fiori elements for OData V4 application.
415-
* Note: this can potentially overwrite existing files in the webapp/test folder.
416-
*
417-
* @param basePath - the absolute target path where the application will be generated
418-
* @param opaConfig - parameters for the generation
419-
* @param opaConfig.scriptName - the name of the OPA journey file. If not specified, 'FirstJourney' will be used
420-
* @param opaConfig.htmlTarget - the name of the html that will be used in OPA journey file. If not specified, 'index.html' will be used
421-
* @param opaConfig.appID - the appID. If not specified, will be read from the manifest in sap.app/id
422-
* @param metadata - optional metadata for the OPA test generation
423-
* @param fs - an optional reference to a mem-fs editor
424-
* @param log - optional logger instance
425-
* @param standalone - opa test generation run standalone, not during app generation
426-
* @returns Reference to a mem-fs-editor
427-
*/
428-
export async function generateOPAFiles(
429-
basePath: string,
430-
opaConfig: { scriptName?: string; appID?: string; htmlTarget?: string },
431-
metadata?: string,
432-
fs?: Editor,
433-
log?: Logger,
434-
standalone = false
435-
): Promise<Editor> {
436-
const editor = fs ?? create(createStorage());
437-
438-
const manifest = readManifest(editor, basePath);
439-
const { applicationType, hideFilterBar } = getAppTypeAndHideFilterBarFromManifest(manifest);
440-
441-
const config = createConfig(manifest, opaConfig, hideFilterBar);
442-
443-
const rootCommonTemplateDirPath = join(__dirname, '../templates/common');
444-
const rootV4TemplateDirPath = join(__dirname, `../templates/${applicationType}`); // Only v4 is supported for the time being
445-
const testOutDirPath = join(await getWebappPath(basePath), 'test');
446-
447-
// Access ux-specification to get feature data for OPA test generation
448-
const appFeatures = await getAppFeatures(basePath, editor, log, metadata);
449-
// OPA Journey file
450-
const startPages = config.pages.filter((page) => page.isStartup).map((page) => page.targetKey);
451-
const LROP = findLROP(config.pages, manifest);
452-
const journeyParams: JourneyParams = {
453-
startPages,
454-
startLR: LROP.pageLR?.targetKey,
455-
navigatedOP: LROP.pageOP?.targetKey,
456-
hideFilterBar: config.hideFilterBar
457-
};
458-
459-
const writeContext: WriteContext = { config, rootV4TemplateDirPath, testOutDirPath, editor, journeyParams };
460-
461-
if (standalone) {
462-
const hasJourneyRunner = existsSync(join(testOutDirPath, 'integration', 'pages', 'JourneyRunner.js'));
463-
const virtualOPA5Configured = await hasVirtualOPA5(basePath);
464-
if (hasJourneyRunner) {
465-
writeJourneyFiles(appFeatures, writeContext, true, true, virtualOPA5Configured);
466-
} else {
467-
editor.move(join(testOutDirPath, 'integration', '**'), join(testOutDirPath, 'integration_old'));
468-
469-
await addIntegrationOldToGitignore(basePath, editor);
470-
const htmlTarget = readHtmlTargetFromQUnitJs(testOutDirPath, editor) ?? config.htmlTarget;
471-
const standaloneConfig = { ...config, htmlTarget };
472-
const standaloneWriteContext: WriteContext = { ...writeContext, config: standaloneConfig };
473-
if (!virtualOPA5Configured) {
474-
writeCommonAndPageFiles(standaloneWriteContext, rootCommonTemplateDirPath);
475-
}
476-
writeJourneyFiles(appFeatures, standaloneWriteContext, true, true, virtualOPA5Configured);
477-
}
478-
} else {
479-
writeCommonAndPageFiles(writeContext, rootCommonTemplateDirPath);
480-
writeJourneyFiles(appFeatures, writeContext, false);
481-
}
482-
483-
return editor;
484-
}
485-
486540
/**
487541
* Generate a page object file for a Fiori elements for OData V4 application.
488542
* Note: this doesn't modify other existing files in the webapp/test folder.
@@ -499,7 +553,7 @@ export async function generatePageObjectFile(
499553
pageObjectParameters: { targetKey: string; appID?: string },
500554
fs?: Editor
501555
): Promise<Editor> {
502-
const editor = fs || create(createStorage());
556+
const editor = fs ?? create(createStorage());
503557

504558
const manifest = readManifest(editor, basePath);
505559
const { applicationType } = getAppTypeAndHideFilterBarFromManifest(manifest);

packages/ui5-test-writer/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ export type FEV4ManifestTarget = {
5050
};
5151
};
5252
};
53+
views?: {
54+
paths?: Array<{
55+
primary?: unknown[];
56+
secondary?: unknown[];
57+
defaultPath?: string;
58+
}>;
59+
};
5360
};
5461
};
5562
};
@@ -127,6 +134,7 @@ export type ListReportFeatures = {
127134
filterBarItems?: string[];
128135
tableColumns?: Record<string, Record<string, string | number | boolean>>;
129136
toolBarActions?: ActionButtonState[];
137+
isALP?: boolean;
130138
};
131139

132140
export interface ActionButtonState {

packages/ui5-test-writer/src/utils/listReportUtils.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
ActionButtonState,
66
ButtonState,
77
ButtonVisibilityResult,
8+
FEV4ManifestTarget,
89
ListReportFeatures
910
} from '../types';
1011
import {
@@ -18,6 +19,7 @@ import type { ConvertedMetadata, EntitySet } from '@sap-ux/vocabularies-types';
1819
import { parse } from '@sap-ux/edmx-parser';
1920
import { convert } from '@sap-ux/annotation-converter';
2021
import type { PageWithModelV4 } from '@sap/ux-specification/dist/types/src/parser/application';
22+
import type { Manifest } from '@sap-ux/project-access';
2123

2224
/**
2325
* Builds a button state object from button visibility result.
@@ -81,18 +83,55 @@ export function safeCheckActionButtonStates(
8183
}
8284
}
8385

86+
/**
87+
* Returns true when a ListReport manifest target is configured as an Analytical List Page.
88+
* ALP targets have a `views.paths` array where at least one entry contains a `primary` array,
89+
* indicating the dual-view (chart + table) layout used by ALP.
90+
*
91+
* @param target - the manifest routing target to inspect
92+
* @returns true if the target represents an ALP configuration
93+
*/
94+
export function isALPManifestTarget(target: FEV4ManifestTarget): boolean {
95+
return (
96+
target.options?.settings?.views?.paths?.some(
97+
(path) => Array.isArray(path.primary) && path.primary.length > 0
98+
) ?? false
99+
);
100+
}
101+
102+
/**
103+
* Returns true if any ListReport target in the manifest is configured as an Analytical List Page.
104+
*
105+
* @param manifest - the application manifest
106+
* @param targetKey - optional specific target key to check; if omitted all ListReport targets are checked
107+
* @returns true if the target (or any ListReport target) is an ALP
108+
*/
109+
export function isALPFromManifest(manifest: Manifest, targetKey?: string): boolean {
110+
const targets = manifest['sap.ui5']?.routing?.targets;
111+
if (!targets) {
112+
return false;
113+
}
114+
const keysToCheck = targetKey ? [targetKey] : Object.keys(targets);
115+
return keysToCheck.some((key) => {
116+
const target = targets[key] as FEV4ManifestTarget;
117+
return target?.name === 'sap.fe.templates.ListReport' && isALPManifestTarget(target);
118+
});
119+
}
120+
84121
/**
85122
* Gets List Report features from the page model using ux-specification.
86123
*
87124
* @param listReportPage - the List Report page containing the tree model with feature definitions
88125
* @param log - optional logger instance
89126
* @param metadata - optional metadata for the OPA test generation
127+
* @param manifest - optional application manifest, used to detect ALP configuration
90128
* @returns feature data extracted from the List Report page model
91129
*/
92130
export function getListReportFeatures(
93131
listReportPage: PageWithModelV4,
94132
log?: Logger,
95-
metadata?: string
133+
metadata?: string,
134+
manifest?: Manifest
96135
): ListReportFeatures {
97136
const buttonVisibility =
98137
metadata && listReportPage.entitySet
@@ -109,7 +148,8 @@ export function getListReportFeatures(
109148
toolBarActions:
110149
metadata && listReportPage.entitySet
111150
? safeCheckActionButtonStates(metadata, listReportPage.entitySet, toolbarActions, log)
112-
: []
151+
: [],
152+
isALP: manifest ? isALPFromManifest(manifest, listReportPage.name) : false
113153
};
114154
}
115155

0 commit comments

Comments
 (0)