diff --git a/package.json b/package.json index 4bfe6239103..7b890f054a8 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": true, "devDependencies": { "@changesets/cli": "2.27.10", - "@playwright/test": "1.50.1", + "@playwright/test": "1.56.1", "@types/jest": "30.0.0", "@types/node": "18.11.9", "@typescript-eslint/eslint-plugin": "^7.18.0", diff --git a/packages/playwright/package.json b/packages/playwright/package.json index 7bfcbc464c3..8f940c714a5 100644 --- a/packages/playwright/package.json +++ b/packages/playwright/package.json @@ -33,7 +33,7 @@ "!dist/**/*.map" ], "dependencies": { - "@playwright/test": "1.50.1", + "@playwright/test": "1.56.1", "@sap-ux/logger": "0.7.0", "fs-extra": "11.1.1", "jest-dev-server": "11.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ce3b174518..7a008d43dbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,8 +19,8 @@ importers: specifier: 2.27.10 version: 2.27.10 '@playwright/test': - specifier: 1.50.1 - version: 1.50.1 + specifier: 1.56.1 + version: 1.56.1 '@types/jest': specifier: 30.0.0 version: 30.0.0 @@ -3109,8 +3109,8 @@ importers: packages/playwright: dependencies: '@playwright/test': - specifier: 1.50.1 - version: 1.50.1 + specifier: 1.56.1 + version: 1.56.1 '@sap-ux/logger': specifier: 0.7.0 version: link:../logger @@ -8884,11 +8884,12 @@ packages: engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} dev: true - /@playwright/test@1.50.1: - resolution: {integrity: sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==} + /@playwright/test@1.56.1: + resolution: {integrity: sha512-vSMYtL/zOcFpvJCW71Q/OEGQb7KYBPAdKh35WNSkaZA75JlAO8ED8UN6GUNTm3drWomcbcqRPFqQbLae8yBTdg==} engines: {node: '>=18'} + hasBin: true dependencies: - playwright: 1.50.1 + playwright: 1.56.1 /@pnpm/config.env-replace@1.1.0: resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} @@ -21398,15 +21399,17 @@ packages: /platform@1.3.6: resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==} - /playwright-core@1.50.1: - resolution: {integrity: sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==} + /playwright-core@1.56.1: + resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} engines: {node: '>=18'} + hasBin: true - /playwright@1.50.1: - resolution: {integrity: sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==} + /playwright@1.56.1: + resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} engines: {node: '>=18'} + hasBin: true dependencies: - playwright-core: 1.50.1 + playwright-core: 1.56.1 optionalDependencies: fsevents: 2.3.2 diff --git a/tests/integration/adaptation-editor/.gitignore b/tests/integration/adaptation-editor/.gitignore index 591a02d97f9..06400ceed9e 100644 --- a/tests/integration/adaptation-editor/.gitignore +++ b/tests/integration/adaptation-editor/.gitignore @@ -3,3 +3,4 @@ node_modules dist blob-report versions.json +test-project-map.json diff --git a/tests/integration/adaptation-editor/manual-test-case-reporter.ts b/tests/integration/adaptation-editor/manual-test-case-reporter.ts new file mode 100644 index 00000000000..6a206d38ca8 --- /dev/null +++ b/tests/integration/adaptation-editor/manual-test-case-reporter.ts @@ -0,0 +1,444 @@ +import { writeFile, mkdir } from 'fs/promises'; +import { join, basename } from 'path'; +import { existsSync, readFileSync } from 'fs'; +import type { + FullConfig, + FullResult, + Reporter, + Suite, + TestCase, + TestResult, + TestStep +} from '@playwright/test/reporter'; + +interface ManualTestCaseStep { + name: string; +} +interface ManualTestCase { + name: string; + filePath: string; + steps: ManualTestCaseStep[]; + projectConfig?: any; // added to capture resolved config or annotation +} + +/** + * Playwright reporter for generating manual test case documentation. + */ +export default class ManualTestCaseReporter implements Reporter { + private manualTestCases: Record = {}; + private config: FullConfig; + private fileTotalTests: Record = {}; + private fileCompletedTests: Record = {}; + // only for latest ui5 reporter is enabled + private isReporterDisabled = false; + private projectConfigMap: Record = {}; // projectName -> project.use + private processedFiles: Set = new Set(); // ensure per-file work runs once + + /** + * Creates an empty manual test case object for the given test. + * + * @param test The test case for which to create the manual test case object. + * @returns An empty ManualTestCase object. + */ + private createEmptyManualTestCase(test: TestCase): ManualTestCase { + return { + name: test.title, + filePath: '', + steps: [] + }; + } + + /** + * On begin handler. + * + * @param config - config + * @param suite - test suite + */ + onBegin(config: FullConfig, suite: Suite) { + // Only run this reporter for the latest version + const latestVersion = this.getLatestVersion(); + const currentProject = suite.suites && suite.suites.length > 0 ? suite.suites[0].title : null; + if (latestVersion && currentProject && currentProject !== latestVersion) { + this.isReporterDisabled = true; + return; + } + + this.config = config; + + const allTests = suite.allTests(); + allTests.forEach((test) => { + if (test.location.file) { + const filename = basename(test.location.file); + const fileNameWithoutExt = filename.replace(/\.spec\.ts$/, ''); + + // Initialize counters if not already set + this.fileTotalTests[fileNameWithoutExt] = (this.fileTotalTests[fileNameWithoutExt] || 0) + 1; + this.fileCompletedTests[fileNameWithoutExt] = this.fileCompletedTests[fileNameWithoutExt] || 0; + } + }); + } + + /** + * On step begin handler. + * + * @param test - test case. + * @param result - test result. + * @param step - test step + */ + onStepBegin(test: TestCase, result: TestResult, step: TestStep): void { + if (this.isReporterDisabled) { + return; + } + const skipPatterns = [ + /^Before Hooks$/, + /^Close context$/, + /^Launch browser/, + /^After Hooks$/, + /^fixture: /, + /^Fixture/, + /^attach "/, + /^browserType\./, + /^browser\./, + /^browserContext\./, + /^Expect \"toBeVisible\"$/, + /^Expect \"toBe\"$/, + /^Expect \"toEqual\"$/, + /^Expect\.poll\.toEqual$/, + /^Expect \"toBeEnabled\"$/, + /^Expect \"toBeDisabled\"$/, + /^Expect \"poll toEqual\"$/, + /^locator\.textContent/, + /^locator\.count/, + /^Click on in Application Preview$/, + /^Query count getByTestId\(\'saved-changes-stack\'\)/, + /^Verifying Changes.../, + /^page\.goto\(/, + /^Create context$/, + /^Create page$/, + /^Navigate to "\/adaptation-editor\.html\?fiori-tools-rta-mode=true"$/, + /locator\('iframe\[title="Application Preview"\]'\)/ + ]; + + const shouldSkip = skipPatterns.some((pattern) => pattern.test(step.title)); + + if (!shouldSkip) { + this.manualTestCases[test.title].steps ??= []; + const parsedStep = parseActionStep(step.title); + const lastStep = this.manualTestCases[test.title].steps[this.manualTestCases[test.title].steps.length - 1]; + const isDuplicate = lastStep && parsedStep === lastStep.name; + if (!isDuplicate) { + this.manualTestCases[test.title].steps.push({ name: parsedStep }); + } + } + } + /** + * On test begin handler. + * + * @param test - test case + * @param _result - test result + */ + onTestBegin(test: TestCase, _result: TestResult) { + if (this.isReporterDisabled) { + return; + } + const testCase = this.createEmptyManualTestCase(test); + this.manualTestCases[test.title] = testCase; + } + + /** + * Called when a test ends. + * + * @param test - The test case that just ended + * @param _result - Result of the test run + */ + async onTestEnd(test: TestCase, _result: TestResult) { + if (this.isReporterDisabled) { + return; + } + if (test.location.file) { + const filename = basename(test.location.file); + this.manualTestCases[test.title].filePath = filename; + const fileNameWithoutExt = filename.replace(/\.spec\.ts$/, ''); + this.fileCompletedTests[fileNameWithoutExt] = (this.fileCompletedTests[fileNameWithoutExt] || 0) + 1; + await this.checkAndGenerateFileDocumentation(fileNameWithoutExt); + // Ensure we parse and store projectConfig for this file only once. + if (!this.processedFiles.has(fileNameWithoutExt)) { + this.processedFiles.add(fileNameWithoutExt); + try { + const ann = (test as any).annotations?.find((a: any) => a.type === 'projectConfig'); + if (ann?.description) { + const parsed = JSON.parse(ann.description); + const isAdp = parsed.projectConfig?.kind === 'adp'; + this.projectConfigMap[fileNameWithoutExt] = { ...parsed, isAdp }; + } + } catch { + // ignore parse errors + } + } + } + } + + /** + * Called when all tests have finished. + * + * @param _result - Result of the entire test run + */ + async onEnd(_result: FullResult) { + if (this.isReporterDisabled) { + return; + } + // write the consolidated test -> project config mapping JSON + try { + const outPath = join(process.cwd(), 'test-project-map.json'); + const toWrite = JSON.parse(JSON.stringify(this.projectConfigMap ?? {})); + await writeFile(outPath, JSON.stringify(toWrite, null, 2), { encoding: 'utf-8' }); + console.log(`Project config map written to ${outPath}`); + } catch (err) { + console.error('Failed to write project config map JSON:', err); + } + } + + /** + * Generates documentation for tests from a specific file. + * + * @param fileBaseName Base name of the file to generate documentation for + */ + private async generateFileDocumentation(fileBaseName: string): Promise { + try { + // Find all test cases belonging to the specified file (match with or without .spec.ts extension) + const testsFromFile = Object.entries(this.manualTestCases) + .filter( + ([_, testCase]) => + testCase.filePath === fileBaseName || testCase.filePath === `${fileBaseName}.spec.ts` + ) + .map(([_, testCase]) => testCase); + + if (testsFromFile.length === 0) { + console.log(`No tests found for ${fileBaseName}, skipping documentation generation`); + return; + } + + console.log(`Generating documentation for ${testsFromFile.length} tests from ${fileBaseName}`); + + const outputDir = join(process.cwd(), 'manual_test_description_generated'); + if (!existsSync(outputDir)) { + await mkdir(outputDir, { recursive: true }); + } + + const titleCased = fileBaseName + .split('-') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); + + let content = `# ${titleCased} Test Documentation\n\n`; + content += '## Table of Contents\n\n'; + + testsFromFile.forEach((test) => { + const anchorName = test.name + .replace(/\s+/g, '-') + .replace(/[^a-zA-Z0-9-_]/g, '') + .toLowerCase(); + content += `- [${test.name}](#${anchorName})\n`; + }); + + content += '\n'; + + testsFromFile.forEach((test) => { + const anchorName = test.name + .replace(/\s+/g, '-') + .replace(/[^a-zA-Z0-9-_]/g, '') + .toLowerCase(); + + content += `\n`; + content += `## ${test.name}\n\n`; + + if (test.steps && test.steps.length > 0) { + content += '### Steps\n\n'; + test.steps.forEach((step, index) => { + content += `${index + 1}. ${step.name.replace(/^1\.\s*/, '')}\n`; + }); + } else { + content += '*No steps recorded for this test.*\n'; + } + + content += '\n---\n\n'; + }); + + const outputFile = join(outputDir, `${fileBaseName}.md`); + await writeFile(outputFile, content); + console.log(`Documentation written to ${outputFile}`); + } catch (error) { + console.error(`Error generating documentation for ${fileBaseName}:`, error); + } + } + + /** + * Checks if all tests from a specific file have completed. + * + * @param filename The filename to check (e.g., 'list-report-v2.spec.ts') + * @returns True if all tests from the file have completed + */ + private isFileComplete(filename: string): boolean { + if (!this.fileTotalTests[filename]) { + return false; + } + + const completed = this.fileCompletedTests[filename] || 0; + const total = this.fileTotalTests[filename]; + + const isComplete = completed >= total; + console.log(`File ${filename}: ${completed}/${total} tests completed, complete status: ${isComplete}`); + + return isComplete; + } + + /** + * Generate documentation immediately if a file is complete. + * + * @param testFile The name of the test file + */ + private async checkAndGenerateFileDocumentation(testFile: string): Promise { + // Check if all tests for this file are complete and generate documentation if they are + if (this.isFileComplete(testFile)) { + await this.generateFileDocumentation(testFile); + } + } + + /** + * Gets the latest UI5 version from environment or versions.json. + * + * @returns The latest version string, or null if not found. + */ + private getLatestVersion(): string | null { + if (process.env.HIGHEST_UI5_VERSION) { + return process.env.HIGHEST_UI5_VERSION; + } + + try { + const versionsPath = join(process.cwd(), 'versions.json'); + + const versionsContent = existsSync(versionsPath) ? readFileSync(versionsPath, 'utf8') : null; + + if (!versionsContent) { + return null; + } + + const versions = JSON.parse(versionsContent) as string[]; + return versions[0]; // First version is the latest + } catch (error) { + return null; + } + } +} + +/** + * Parses a Playwright step title into a human-readable action description. + * + * @param stepTitle The title of the step to parse. + * @returns A human-readable string describing the action. + */ +function parseActionStep(stepTitle: string): string { + // Action mapping - common Playwright methods to human verbs with optional prefix and suffix + const actionMap: Record = { + 'click': { prefix: 'Click on' }, + 'hover': { prefix: 'Hover over' }, + 'isDisabled': { prefix: 'Check if', suffix: 'is disabled' } + }; + + // Element type mapping - detect element types from selectors/roles + const elementMap: Record = { + 'button': 'button' + }; + + // Try to find action verb from the main step title + let actionInfo: { prefix: string; suffix?: string } | null = null; + for (const [actionKey, actionVerb] of Object.entries(actionMap)) { + if (stepTitle.includes(`.${actionKey}`) || stepTitle.startsWith(actionKey)) { + actionInfo = actionVerb; + break; + } + } + + let element = ''; + const getByMethodMap: Record = { + 'getByRole': 'role-based' + }; + + for (const [method, elementType] of Object.entries(getByMethodMap)) { + const methodMatch = stepTitle.match(new RegExp(`${method}\\(`)); + if (methodMatch) { + if (method === 'getByRole') { + // Special handling for getByRole - extract the role type + const roleMatch = stepTitle.match(/getByRole\('(\w+)'/); + if (roleMatch) { + const roleType = roleMatch[1]; + element = elementMap[roleType] || roleType; + } + } else { + element = elementType; + } + break; + } + } + + // Try to extract element name using a map of patterns + let name = ''; + const nameExtractionPatterns: Record = { + 'getByRole': /getByRole\('\w+',\s*{\s*name:\s*'([^']+)'|getByRole\('\w+',\s*{\s*name:\s*"([^"]+)"/ + }; + + // Try each pattern until we find a match + for (const [_method, pattern] of Object.entries(nameExtractionPatterns)) { + const nameMatch = stepTitle.match(pattern); + if (nameMatch) { + name = nameMatch[1] || nameMatch[2]; + break; + } + } + + // Build human-readable step using priority order with prefix and suffix + const resultBuilders = [ + () => { + if (actionInfo && element && name) { + const { prefix, suffix } = actionInfo; + if (suffix) { + return `${prefix} \`${name}\` ${suffix}`; + } + return `${prefix} ${element} \`${name}\``; + } + return null; + }, + () => { + if (actionInfo && element) { + const { prefix, suffix } = actionInfo; + if (suffix) { + return `${prefix} ${element} ${suffix}`; + } + return `${prefix} ${element}`; + } + return null; + }, + () => { + if (actionInfo && name) { + const { prefix, suffix } = actionInfo; + if (suffix) { + return `${prefix} \`${name}\` ${suffix}`; + } + return `${prefix} \`${name}\``; + } + return null; + }, + () => (actionInfo ? actionInfo.prefix : null), + () => stepTitle + ]; + + let result = ``; + for (const builder of resultBuilders) { + const buildResult = builder(); + if (buildResult) { + result = buildResult; + break; + } + } + return result; +} diff --git a/tests/integration/adaptation-editor/manual_test_description_generated/list-report-v2.md b/tests/integration/adaptation-editor/manual_test_description_generated/list-report-v2.md new file mode 100644 index 00000000000..be37e0e902a --- /dev/null +++ b/tests/integration/adaptation-editor/manual_test_description_generated/list-report-v2.md @@ -0,0 +1,405 @@ +# List Report V2 Test Documentation + +## Table of Contents + +- [1. Enable/Disable clear filter bar button](#1-enabledisable-clear-filter-bar-button) +- [2. Add controller to page](#2-add-controller-to-page) +- [3. Change table columns](#3-change-table-columns) +- [4. Add Custom Table Action](#4-add-custom-table-action) +- [5. Add Custom Table Column](#5-add-custom-table-column) +- [6. Enable/Disable Semantic Date Range in Filter Bar](#6-enabledisable-semantic-date-range-in-filter-bar) +- [7. Enable Variant Management in Tables and Charts](#7-enable-variant-management-in-tables-and-charts) +- [8. Change table actions](#8-change-table-actions) +- [9. Add New Annotation File](#9-add-new-annotation-file) + + +## 1. Enable/Disable clear filter bar button + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Check `Clear` Button in the List Report filter bar is hidden +3. Click `Enable "Clear" Button in Filter Bar` button in the Quick Actions Panel +4. Check `Clear` Button in the List Report filter bar is visible +5. Click `Save` button in the toolBar +6. Check `Save` button in the toolbar is disabled +7. Verify changes: + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "propertyChange", + "content": { + "property": "showClearOnFB", + "newValue": true + } +} +``` + + +8. Click `Disable "Clear" Button in Filter Bar` button in the Quick Actions Panel +9. Check `Clear` Button in the List Report filter bar is hidden +10. Click `Save` button in the toolBar +11. Check `Save` button in the toolbar is disabled +12. Verify changes: + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "propertyChange", + "content": { + "property": "showClearOnFB", + "newValue": false + } +} +``` + + + +--- + + +## 2. Add controller to page + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Add Controller to Page` button in the Quick Actions Panel +3. Fill `Controller Name` field with `TestController` in the dialog `Extend With Controller` +4. Click on `Create` button in the dialog `Extend With Controller` +5. Click `Save` button in the toolBar +6. Verify changes: + +**Coding** + +**TestController.js** +```js +/ControllerExtension\.extend\("adp\.fiori\.elements\.v2\.TestController"/ +``` + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "codeExt", + "content": { + "codeRef": "coding/TestController.js" + } +} +``` + + +7. Click `Reload` link in the Changes Panel +8. Click `Show Page Controller` button in the Quick Actions Panel +9. Check filename `adp.fiori.elements.v2/changes/coding/TestController.js` is visible +10. Check `Open in VS Code` button is visible + +--- + + +## 3. Change table columns + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Change Table Columns` button in the Quick Actions Panel +3. Check `String Property, Boolean Property, Currency` exist in the `View Settings` dialog + +--- + + +## 4. Add Custom Table Action + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Add Custom Table Action` button in the Quick Actions Panel +3. Fill `Fragment Name` field with `table-action` in the dialog `Add Custom Table Action` +4. Click on `Create` button in the dialog `Add Custom Table Action` +5. Click `Save and Reload` button in the toolBar +6. Check `Save` button in the toolbar is disabled +7. Verify changes: + +**Fragment(s)** + +**table-action.fragment.xml** +```xml + + + + + + +``` + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "addXML", + "content": { + "targetAggregation": "content", + "fragmentPath": "fragments/table-action.fragment.xml" + } +} +``` + + + +--- + + +## 5. Add Custom Table Column + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click `UI Adaptation` button in the toolBar +5. Click `Add Custom Table Column` button in the Quick Actions Panel +6. Fill `Column Fragment Name` field with `table-column` in the dialog `Add Custom Table Column` +7. Fill `Cell Fragment Name` field with `table-cell` in the dialog `Add Custom Table Column` +8. Click on `Create` button in the dialog `Add Custom Table Column` +9. Click `Save and Reload` button in the toolBar +10. Check `Save` button in the toolbar is disabled +11. Verify changes: + +**Fragment(s)** + +**table-cell.fragment.xml** +```xml + + + + +``` + +**table-column.fragment.xml** +```xml + + + + + + + + + + + +``` + +**Change(s)** + +**Change** 1 +```json +{ + "fileType": "change", + "changeType": "addXML", + "content": { + "targetAggregation": "columns", + "fragmentPath": "fragments/table-column.fragment.xml" + } +} +``` + +**Change** 2 +```json +{ + "fileType": "change", + "changeType": "addXML", + "content": { + "boundAggregation": "items", + "targetAggregation": "cells", + "fragmentPath": "fragments/table-cell.fragment.xml" + } +} +``` + + +12. Click `Navigation` button in the toolBar +13. Click on `Go` button. +14. Check Column Name is `New Column` +15. Check Column Data is `Sample data` + +--- + + +## 6. Enable/Disable Semantic Date Range in Filter Bar + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on value help button of `Date Property` filter +4. Check semantic date `Yesterday` visible in filter +5. Click `UI Adaptation` button in the toolBar +6. Click `Disable Semantic Date Range in Filter Bar` button in the Quick Actions Panel +7. Click `Save and Reload` button in the toolBar +8. Verify changes: + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "appdescr_ui_generic_app_changePageConfiguration", + "content": { + "entityPropertyChange": { + "propertyPath": "component/settings/filterSettings/dateSettings", + "propertyValue": { + "useDateRange": false + } + } + } +} +``` + + +9. Click `Navigation` button in the toolBar +10. Click on value help button of `Date Property` filter +11. Click `UI Adaptation` button in the toolBar +12. Click `Enable Semantic Date Range in Filter Bar` button in the Quick Actions Panel +13. Click `Save and Reload` button in the toolBar +14. Verify changes: + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "appdescr_ui_generic_app_changePageConfiguration", + "content": { + "entityPropertyChange": { + "propertyPath": "component/settings/filterSettings/dateSettings", + "propertyValue": { + "useDateRange": true + } + } + } +} +``` + + + +--- + + +## 7. Enable Variant Management in Tables and Charts + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Enable Variant Management in Tables and Charts` button in the Quick Actions Panel +3. Click `Save and Reload` button in the toolBar +4. Check `Save` button in the toolbar is disabled +5. Verify changes: + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "appdescr_ui_generic_app_changePageConfiguration", + "content": { + "parentPage": { + "component": "sap.suite.ui.generic.template.ListReport" + }, + "entityPropertyChange": { + "propertyPath": "component/settings", + "propertyValue": { + "smartVariantManagement": false + } + } + } +} +``` + + + +--- + + +## 8. Change table actions + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Change Table Actions` button in the Quick Actions Panel +3. Check `Button - Create, Button - Delete, Button - Add Card to Insights` exist in the `Rearrange Toolbar Content` dialog +4. Hover over row `2` and click on `Move up` button in the row of `Rearrange Toolbar Content` table +5. Check `Button - Delete, Button - Create, Button - Add Card to Insights` exist in the `Rearrange Toolbar Content` dialog +6. Click on `OK` button of the dialog `Rearrange Toolbar Content` +7. Click `Save` button in the toolBar +8. Check `Save` button in the toolbar is disabled +9. Check saved changes stack contains `1` `Toolbar Content Move Change` change(s) + +--- + + +## 9. Add New Annotation File + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `UI Adaptation` button in the toolBar +3. Click `Add Local Annotation File` button in the Quick Actions Panel +4. Click `Save and Reload` button in the toolBar +5. Check `Save` button in the toolbar is disabled +6. Verify changes: + +**Annotations** +```xml + + + + + + + + + + + + + + + + + + +``` + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "appdescr_app_addAnnotationsToOData", + "content": { + "dataSourceId": "mainService", + "annotations": [ + {} + ] + } +} +``` + + +7. Click `Show Local Annotation File` button in the Quick Actions Panel +8. Check filename `adp.fiori.elements.v2/changes/annotations/annotation_.xml` is visible in the dialog +9. Check button `Show File in VSCode` is visible in the dialog + +--- + diff --git a/tests/integration/adaptation-editor/manual_test_description_generated/object-page-v2.md b/tests/integration/adaptation-editor/manual_test_description_generated/object-page-v2.md new file mode 100644 index 00000000000..29de42bc820 --- /dev/null +++ b/tests/integration/adaptation-editor/manual_test_description_generated/object-page-v2.md @@ -0,0 +1,358 @@ +# Object Page V2 Test Documentation + +## Table of Contents + +- [1. Object Page: Enable Empty row mode](#1-object-page-enable-empty-row-mode) +- [2. Change table actions](#2-change-table-actions) +- [3. Add controller to page](#3-add-controller-to-page) +- [4. Add Custom Table Action](#4-add-custom-table-action) +- [5. Change table columns](#5-change-table-columns) +- [6. Add Custom Table Column](#6-add-custom-table-column) +- [7. Add Header Field](#7-add-header-field) +- [8. Add Custom Section](#8-add-custom-section) + + +## 1. Object Page: Enable Empty row mode + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click on row `1` of `Root Entities` table +5. Click `UI Adaptation` button in the toolBar +6. Click `Enable Empty Row Mode for Tables` button in the Quick Actions Panel +7. Click `Save and Reload` button in the toolBar +8. Check `Save` button in the toolbar is disabled +9. Verify changes: + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "appdescr_ui_generic_app_changePageConfiguration", + "content": { + "parentPage": { + "component": "sap.suite.ui.generic.template.ObjectPage" + }, + "entityPropertyChange": { + "propertyPath": "component/settings/sections/toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection/createMode", + "propertyValue": "creationRows" + } + } +} +``` + + + +--- + + +## 2. Change table actions + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click on row `1` of `Root Entities` table +5. Click `UI Adaptation` button in the toolBar +6. Click `Change Table Actions` button in the Quick Actions Panel +7. Check `SearchField - fiori.elements.v2.0::sap.suite.ui.generic.template.ObjectPage.view.Details::RootEntity--toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection::Table::Toolbar::SearchField, Button - Create, Button - Delete` exist in the `Rearrange Toolbar Content` dialog +8. Hover over row `1` and click on Move down button in the row of `Rearrange Toolbar Content` table +9. Check `Button - Create, SearchField - fiori.elements.v2.0::sap.suite.ui.generic.template.ObjectPage.view.Details::RootEntity--toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection::Table::Toolbar::SearchField, Button - Delete` exist in the `Rearrange Toolbar Content` dialog +10. Click on `OK` button of the dialog `Rearrange Toolbar Content` +11. Click `Save` button in the toolBar +12. Check saved changes stack contains `1` `Toolbar Content Move Change` change(s) + +--- + + +## 3. Add controller to page + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click `UI Adaptation` button in the toolBar +5. Click `Add Controller to Page` button in the Quick Actions Panel +6. Fill `Controller Name` field with `TestController` in the dialog `Extend With Controller` +7. Click on `Create` button in the dialog `Extend With Controller` +8. Click `Save` button in the toolBar +9. Verify changes: + +**Coding** + +**TestController.js** +```js +/ControllerExtension\.extend\("adp\.fiori\.elements\.v2\.TestController"/ +``` + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "codeExt", + "content": { + "codeRef": "coding/TestController.js" + } +} +``` + + +10. Click `Reload` link in the Changes Panel +11. Click `Show Page Controller` button in the Quick Actions Panel +12. Check file name `adp.fiori.elements.v2/changes/coding/TestController.js` is visible in dialog +13. Check `Open in VS Code` button visible in dialog. + +--- + + +## 4. Add Custom Table Action + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click `UI Adaptation` button in the toolBar +5. Click `Add Custom Table Action` button in the Quick Actions Panel +6. Fill `Fragment Name` field with `op-table-action` in the dialog `Add Custom Table Action` +7. Click on `Create` button in the dialog `Add Custom Table Action` +8. Click `Save and Reload` button in the toolBar +9. Check `Save` button in the toolbar is disabled +10. Verify changes: + +**Fragment(s)** + +**op-table-action.fragment.xml** +```xml + + + + + +``` + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "addXML", + "content": { + "targetAggregation": "content", + "fragmentPath": "fragments/op-table-action.fragment.xml" + } +} +``` + + + +--- + + +## 5. Change table columns + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click on row `1` of `Root Entities` table +5. Click `UI Adaptation` button in the toolBar +6. Click `Change Table Columns` button in the Quick Actions Panel +7. Check `String Property, Date Property` exist in the `View Settings` dialog + +--- + + +## 6. Add Custom Table Column + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click on row `1` of `Root Entities` table +5. Click `UI Adaptation` button in the toolBar +6. Click `Add Custom Table Column` button in the Quick Actions Panel +7. Fill `Column Fragment Name` field with `table-column` in the dialog `Add Custom Table Column` +8. Fill `Cell Fragment Name` field with `table-cell` in the dialog `Add Custom Table Column` +9. Click on `Create` button in the dialog `Add Custom Table Column` +10. Click `Save and Reload` button in the toolBar +11. Check `Save` button in the toolbar is disabled +12. Verify changes: + +**Fragment(s)** + +**table-cell.fragment.xml** +```xml + + + + +``` + +**table-column.fragment.xml** +```xml + + + + + + + + + + + +``` + +**Change(s)** + +**Change** 1 +```json +{ + "fileType": "change", + "changeType": "addXML", + "content": { + "targetAggregation": "columns", + "fragmentPath": "fragments/table-column.fragment.xml" + } +} +``` + +**Change** 2 +```json +{ + "fileType": "change", + "changeType": "addXML", + "content": { + "boundAggregation": "items", + "targetAggregation": "cells", + "fragmentPath": "fragments/table-cell.fragment.xml" + } +} +``` + + +13. Check Column Name is `New Column` +14. Check Column Data is `Sample data` + +--- + + +## 7. Add Header Field + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click on row `1` of `Root Entities` table +5. Click `UI Adaptation` button in the toolBar +6. Click `Add Header Field` button in the Quick Actions Panel +7. Fill `Fragment Name` field with `op-header-field` in the dialog `Add Header Field` +8. Click on `Create` button in the dialog `Add Header Field` +9. Click `Save and Reload` button in the toolBar +10. Check `Save` button in the toolbar is disabled +11. Verify changes: + +**Fragment(s)** + +**op-header-field.fragment.xml** +```xml + + + + + + +``` + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "addXML", + "content": { + "targetAggregation": {}, + "fragmentPath": "fragments/op-header-field.fragment.xml" + } +} +``` + + + +--- + + +## 8. Add Custom Section + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click on row `1` of `Root Entities` table +5. Click `UI Adaptation` button in the toolBar +6. Click `Add Custom Section` button in the Quick Actions Panel +7. Fill `Fragment Name` field with `op-section` in the dialog `Add Custom Section` +8. Click on `Create` button in the dialog `Add Custom Section` +9. Click `Save and Reload` button in the toolBar +10. Check `Save` button in the toolbar is disabled +11. Verify changes: + +**Fragment(s)** + +**op-section.fragment.xml** +```xml + + + + + + + + + + +``` + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "addXML", + "content": { + "targetAggregation": "sections", + "fragmentPath": "fragments/op-section.fragment.xml" + } +} +``` + + + +--- + diff --git a/tests/integration/adaptation-editor/manual_test_description_generated/object-page-v2a.md b/tests/integration/adaptation-editor/manual_test_description_generated/object-page-v2a.md new file mode 100644 index 00000000000..72aa534be18 --- /dev/null +++ b/tests/integration/adaptation-editor/manual_test_description_generated/object-page-v2a.md @@ -0,0 +1,47 @@ +# Object Page V2a Test Documentation + +## Table of Contents + +- [1. Enable Variant Management in Tables](#1-enable-variant-management-in-tables) + + +## 1. Enable Variant Management in Tables + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click on row `1` of `Root Entities` table +5. Click `UI Adaptation` button in the toolBar +6. Click `Enable Variant Management in Tables` button in the Quick Actions Panel +7. Click `Save and Reload` button in the toolBar +8. Check `Save` button in the toolbar is disabled +9. Verify changes: + +**Change(s)** + +```json +{ + "fileType": "change", + "changeType": "appdescr_ui_generic_app_changePageConfiguration", + "content": { + "parentPage": { + "component": "sap.suite.ui.generic.template.ObjectPage", + "entitySet": "RootEntity" + }, + "entityPropertyChange": { + "propertyPath": "component/settings/sections/toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection/tableSettings", + "operation": "UPSERT", + "propertyValue": { + "variantManagement": true + } + } + } +} +``` + + + +--- + diff --git a/tests/integration/adaptation-editor/manual_test_description_generated/object-page-v2b.md b/tests/integration/adaptation-editor/manual_test_description_generated/object-page-v2b.md new file mode 100644 index 00000000000..bdda2283a22 --- /dev/null +++ b/tests/integration/adaptation-editor/manual_test_description_generated/object-page-v2b.md @@ -0,0 +1,21 @@ +# Object Page V2b Test Documentation + +## Table of Contents + +- [1. Change table columns (analytical table)](#1-change-table-columns-analytical-table) + + +## 1. Change table columns (analytical table) + +### Steps + +1. Check `UIAdaptation` mode in the toolbar is enabled +2. Click `Navigation` button in the toolBar +3. Click on `Go` button. +4. Click on row `1` of `Root Entities` table +5. Click `UI Adaptation` button in the toolBar +6. Click `Change Table Columns` button in the Quick Actions Panel +7. Check `String Property, Date Property` exist in the `View Settings` dialog + +--- + diff --git a/tests/integration/adaptation-editor/playwright.config.ts b/tests/integration/adaptation-editor/playwright.config.ts index 6290b8eb9dd..60384016de0 100644 --- a/tests/integration/adaptation-editor/playwright.config.ts +++ b/tests/integration/adaptation-editor/playwright.config.ts @@ -2,7 +2,7 @@ import { exit } from 'process'; import { join } from 'node:path'; import { readFileSync } from 'node:fs'; import { defineConfig, devices } from '@sap-ux-private/playwright'; -import type { PlaywrightTestConfig, Project } from '@sap-ux-private/playwright'; +import type { PlaywrightTestConfig, Project, ReporterDescription } from '@sap-ux-private/playwright'; /** * Read environment variables from `.env` file. @@ -14,6 +14,10 @@ import type { TestOptions } from './src/fixture'; let versions; try { versions = JSON.parse(readFileSync(join(__dirname, 'versions.json').toString()) as unknown as string) as string[]; + + if (versions && versions.length > 0) { + process.env.HIGHEST_UI5_VERSION = versions[0]; + } } catch (error) { if (error.code === 'ENOENT') { console.error( @@ -23,6 +27,22 @@ try { } throw error; } + +/** + * Get the appropriate reporters based on environment and version. + * + * @returns Array of reporter configurations + */ +function getReporters(): ReporterDescription[] { + // Always include HTML reporter + const reporters: ReporterDescription[] = [['html', { open: 'never' }]]; + if (!process.env.CI) { + reporters.push(['./manual-test-case-reporter.ts']); + } + + return reporters; +} + /** * See https://playwright.dev/docs/test-configuration. */ @@ -37,7 +57,7 @@ const config: PlaywrightTestConfig = { /* Opt out of parallel tests on CI. */ workers: 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: [['html', { open: 'never' }]], + reporter: getReporters(), /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ diff --git a/tests/integration/adaptation-editor/src/fixture.ts b/tests/integration/adaptation-editor/src/fixture.ts index 3b8af9b4730..68b1dcb49cd 100644 --- a/tests/integration/adaptation-editor/src/fixture.ts +++ b/tests/integration/adaptation-editor/src/fixture.ts @@ -21,6 +21,7 @@ import { satisfies } from 'semver'; export type TestOptions = { previewFrame: FrameLocator; testSkipper: boolean; + projectConfigAnnotation: boolean; }; // Avoid installing npm packages every time, but use symlink instead @@ -82,6 +83,28 @@ export const test = base.extend({ } ], projectConfig: [SIMPLE_APP, { option: true, scope: 'worker' }], + // Inject projectConfig into test annotations so reporters can consume it reliably + projectConfigAnnotation: [ + async ({ projectConfig }, use, testInfo): Promise => { + try { + const payload = (() => { + try { + return JSON.stringify({ projectConfig }); + } catch { + return JSON.stringify({ projectConfig: projectConfig.id ?? projectConfig }); + } + })(); + testInfo.annotations.push({ + type: 'projectConfig', + description: payload + }); + } catch { + // ignore annotation failures + } + await use(true); + }, + { scope: 'test', auto: true } + ], log: [ async ({}, use, testInfo): Promise => { const logger = createLogger(testInfo.parallelIndex); @@ -159,7 +182,12 @@ export const test = base.extend({ await page.goto( `http://localhost:${projectServer}${ADAPTATION_EDITOR_PATH}?fiori-tools-rta-mode=true#app-preview` ); - await expect(page.getByRole('button', { name: 'UI Adaptation' })).toBeEnabled({ timeout: 15_000 }); + await expect( + page.getByRole('button', { name: 'UI Adaptation' }), + 'Check `UIAdaptation` mode in the toolbar is enabled' + ).toBeEnabled({ + timeout: 15_000 + }); // Each test will get a "page" that already has the person name. await use(page); diff --git a/tests/integration/adaptation-editor/src/scenarios/quick-actions/list-report-v2.spec.ts b/tests/integration/adaptation-editor/src/scenarios/quick-actions/list-report-v2.spec.ts index 13c73e46281..4bcd226e67c 100644 --- a/tests/integration/adaptation-editor/src/scenarios/quick-actions/list-report-v2.spec.ts +++ b/tests/integration/adaptation-editor/src/scenarios/quick-actions/list-report-v2.spec.ts @@ -1,12 +1,9 @@ -import { readdir } from 'fs/promises'; -import { join } from 'node:path'; - import { expect } from '@sap-ux-private/playwright'; import { lt, satisfies } from 'semver'; import { test } from '../../fixture'; import { ADP_FIORI_ELEMENTS_V2 } from '../../project'; -import { AdaptationEditorShell, AdpDialog, ListReport, readChanges, TableSettings } from './test-utils'; +import { AdaptationEditorShell, AdpDialog, ListReport, TableSettings, verifyChanges } from './test-utils'; test.use({ projectConfig: ADP_FIORI_ELEMENTS_V2 }); @@ -16,53 +13,40 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { const editor = new AdaptationEditorShell(page, ui5Version); await editor.reloadCompleted(); - await expect(lr.clearButton).toBeHidden(); + await expect(lr.clearButton, `Check \`Clear\` Button in the List Report filter bar is hidden`).toBeHidden(); await editor.quickActions.enableClearButton.click(); - await expect(lr.clearButton).toBeVisible(); + await expect(lr.clearButton, `Check \`Clear\` Button in the List Report filter bar is visible`).toBeVisible(); await editor.toolbar.saveButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'propertyChange', - content: expect.objectContaining({ property: 'showClearOnFB', newValue: true }) - }) - ]) - }) - ); - await editor.quickActions.disableClearButton.click(); + await editor.toolbar.isDisabled(); + await verifyChanges(projectCopy, { + changes: [ + { + fileType: 'change', + changeType: 'propertyChange', + content: { property: 'showClearOnFB', newValue: true } + } + ] + }); - await expect(lr.clearButton).toBeHidden(); + await editor.quickActions.disableClearButton.click(); + await expect(lr.clearButton, `Check \`Clear\` Button in the List Report filter bar is hidden`).toBeHidden(); await editor.toolbar.saveButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'propertyChange', - content: expect.objectContaining({ property: 'showClearOnFB', newValue: false }) - }) - ]) - }) - ); + await editor.toolbar.isDisabled(); + await verifyChanges(projectCopy, { + changes: [ + { + fileType: 'change', + changeType: 'propertyChange', + content: { property: 'showClearOnFB', newValue: false } + } + ] + }); }); test( @@ -78,11 +62,10 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { const lr = new ListReport(previewFrame); const dialog = new AdpDialog(previewFrame, ui5Version); const editor = new AdaptationEditorShell(page, ui5Version); - await editor.quickActions.addControllerToPage.click(); - await previewFrame.getByRole('textbox', { name: 'Controller Name' }).fill('TestController'); - await dialog.createButton.click(); + await dialog.fillField('Controller Name', 'TestController'); + await dialog.clickCreateButton(); if (lt(ui5Version, '1.136.0')) { await expect(page.getByText('Changes detected!')).toBeVisible(); @@ -90,40 +73,18 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { await editor.toolbar.saveButton.click(); } - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - coding: expect.objectContaining({ - ['TestController.js']: expect.stringMatching( - /ControllerExtension\.extend\("adp\.fiori\.elements\.v2\.TestController"/ - ) - }), - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'codeExt', - content: expect.objectContaining({ codeRef: 'coding/TestController.js' }) - }) - ]) - }) - ); - - await expect - .poll( - async () => { - const changesDirectory = join(projectCopy, 'webapp', 'changes', 'coding'); - const codingChanges = await readdir(changesDirectory); - return codingChanges.length; - }, + await verifyChanges(projectCopy, { + changes: [ { - message: 'make sure controller file is created', - timeout: 4_000 + fileType: 'change', + changeType: 'codeExt', + content: { codeRef: 'coding/TestController.js' } } - ) - .toEqual(1); + ], + coding: { + ['TestController.js']: /ControllerExtension\.extend\("adp\.fiori\.elements\.v2\.TestController"/ + } + }); await editor.changesPanel.reloadButton.click(); @@ -134,10 +95,10 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { await editor.quickActions.showPageController.click(); await expect( - previewFrame.getByText('adp.fiori.elements.v2/changes/coding/TestController.js') + previewFrame.getByText('adp.fiori.elements.v2/changes/coding/TestController.js'), + `Check filename \`adp.fiori.elements.v2/changes/coding/TestController.js\` is visible` ).toBeVisible(); - - await expect(previewFrame.getByRole('button', { name: 'Open in VS Code' })).toBeVisible(); + await dialog.openInVSCodeVisible(); } ); @@ -155,9 +116,7 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { await editor.quickActions.changeTableColumns.click(); - await expect(tableSettings.tableSettingsDialog.getByText('String Property')).toBeVisible(); - await expect(tableSettings.tableSettingsDialog.getByText('Boolean Property')).toBeVisible(); - await expect(tableSettings.tableSettingsDialog.getByText('Currency')).toBeVisible(); + await tableSettings.expectItemsToBeVisible(['String Property', 'Boolean Property', 'Currency']); } ); @@ -175,41 +134,33 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { await editor.quickActions.addCustomTableAction.click(); - await previewFrame.getByRole('textbox', { name: 'Fragment Name' }).fill('table-action'); - await dialog.createButton.click(); + await dialog.fillField('Fragment Name', 'table-action'); + await dialog.clickCreateButton(); await editor.toolbar.saveAndReloadButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); + await editor.toolbar.isDisabled(); - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - fragments: expect.objectContaining({ - 'table-action.fragment.xml': expect.stringMatching( - new RegExp(` + await verifyChanges(projectCopy, { + changes: [ + { + fileType: 'change', + changeType: 'addXML', + content: { + targetAggregation: 'content', + fragmentPath: 'fragments/table-action.fragment.xml' + } + } + ], + fragments: { + 'table-action.fragment.xml': ` -`) - ) - }), - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'addXML', - content: expect.objectContaining({ - targetAggregation: 'content', - fragmentPath: 'fragments/table-action.fragment.xml' - }) - }) - ]) - }) - ); +` + } + }); } ); @@ -225,38 +176,47 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { const dialog = new AdpDialog(previewFrame, ui5Version); const lr = new ListReport(previewFrame); const editor = new AdaptationEditorShell(page, ui5Version); - if (await editor.quickActions.addCustomTableColumn.isDisabled()) { await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); + await lr.clickOnGoButton(); await editor.toolbar.uiAdaptationModeButton.click(); } await editor.quickActions.addCustomTableColumn.click(); - await previewFrame.getByRole('textbox', { name: 'Column Fragment Name' }).fill('table-column'); - await previewFrame.getByRole('textbox', { name: 'Cell Fragment Name' }).fill('table-cell'); - await dialog.createButton.click(); + await dialog.fillField('Column Fragment Name', 'table-column'); + await dialog.fillField('Cell Fragment Name', 'table-cell'); + await dialog.clickCreateButton(); await editor.toolbar.saveAndReloadButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - fragments: expect.objectContaining({ - 'table-cell.fragment.xml': expect.stringMatching( - new RegExp(` + await editor.toolbar.isDisabled(); + await verifyChanges(projectCopy, { + changes: [ + { + fileType: 'change', + changeType: 'addXML', + content: { + targetAggregation: 'columns', + fragmentPath: 'fragments/table-column.fragment.xml' + } + }, + { + fileType: 'change', + changeType: 'addXML', + content: { + boundAggregation: 'items', + targetAggregation: 'cells', + fragmentPath: 'fragments/table-cell.fragment.xml' + } + } + ], + fragments: { + 'table-cell.fragment.xml': ` -`) - ), - 'table-column.fragment.xml': expect.stringMatching( - new RegExp(` +`, + 'table-column.fragment.xml': ` { value='\\\\{"columnKey": "column-[a-z0-9]+", "columnIndex": "3"}' /> -`) - ) - }), - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'addXML', - content: expect.objectContaining({ - targetAggregation: 'columns', - fragmentPath: 'fragments/table-column.fragment.xml' - }) - }), - expect.objectContaining({ - fileType: 'change', - changeType: 'addXML', - content: expect.objectContaining({ - boundAggregation: 'items', - targetAggregation: 'cells', - fragmentPath: 'fragments/table-cell.fragment.xml' - }) - }) - ]) - }) - ); - +` + } + }); await expect(lr.goButton).toBeVisible(); await editor.reloadCompleted(); await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); - await expect(previewFrame.getByRole('columnheader', { name: 'New column' }).locator('div')).toBeVisible(); + await lr.clickOnGoButton(); + await expect( + previewFrame.getByRole('columnheader', { name: 'New column' }).locator('div'), + `Check Column Name is \`New Column\`` + ).toBeVisible(); if (satisfies(ui5Version, '<1.120.0')) { await expect(previewFrame.getByRole('cell', { name: 'Sample data' })).toBeVisible(); } else { - await expect(previewFrame.getByRole('gridcell', { name: 'Sample data' })).toBeVisible(); + await expect( + previewFrame.getByRole('gridcell', { name: 'Sample data' }), + `Check Column Data is \`Sample data\`` + ).toBeVisible(); } } ); - test( '6. Enable/Disable Semantic Date Range in Filter Bar', { @@ -321,21 +264,36 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { } }, async ({ page, previewFrame, projectCopy, ui5Version }) => { + async function clickOnValueHelp(): Promise { + await test.step(`Click on value help button of \`Date Property\` filter`, async () => { + if (satisfies(ui5Version, '~1.96.0')) { + // Try getByTitle first, fallback to aria-label if not found + const btn = previewFrame.getByTitle('Open Picker'); + if (await btn.count()) { + await btn.click(); + } else { + await previewFrame.locator('[aria-label="Open Picker"]').click(); + } + } else { + // click on second filter value help + await previewFrame + .locator( + '[id="fiori\\.elements\\.v2\\.0\\:\\:sap\\.suite\\.ui\\.generic\\.template\\.ListReport\\.view\\.ListReport\\:\\:RootEntity--listReportFilter-filterItemControl_BASIC-DateProperty-input-vhi"]' + ) + .click(); + } + }); + } const lr = new ListReport(previewFrame); const editor = new AdaptationEditorShell(page, ui5Version); await editor.toolbar.navigationModeButton.click(); - if (satisfies(ui5Version, '~1.96.0')) { - await previewFrame.getByTitle('Open Picker').click(); - } else { - // click on second filter value help - await previewFrame - .locator( - '[id="fiori\\.elements\\.v2\\.0\\:\\:sap\\.suite\\.ui\\.generic\\.template\\.ListReport\\.view\\.ListReport\\:\\:RootEntity--listReportFilter-filterItemControl_BASIC-DateProperty-input-vhi"]' - ) - .click(); - } - await expect(previewFrame.getByText('Yesterday')).toBeVisible(); + await clickOnValueHelp(); + + await expect( + previewFrame.getByText('Yesterday'), + `Check semantic date \`Yesterday\` visible in filter` + ).toBeVisible(); await editor.toolbar.uiAdaptationModeButton.click(); await editor.quickActions.disableSemanticDateRange.click(); @@ -343,35 +301,37 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { await editor.toolbar.saveAndReloadButton.click(); await expect(editor.toolbar.saveButton).toBeDisabled(); - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'appdescr_ui_generic_app_changePageConfiguration', - content: expect.objectContaining({ - entityPropertyChange: expect.objectContaining({ - propertyPath: 'component/settings/filterSettings/dateSettings', - propertyValue: expect.objectContaining({ - useDateRange: false - }) - }) - }) - }) - ]) - }) - ); + await verifyChanges(projectCopy, { + changes: [ + { + fileType: 'change', + changeType: 'appdescr_ui_generic_app_changePageConfiguration', + content: { + entityPropertyChange: { + propertyPath: 'component/settings/filterSettings/dateSettings', + propertyValue: { + useDateRange: false + } + } + } + } + ] + }); await expect(lr.goButton).toBeVisible(); await editor.reloadCompleted(); await editor.toolbar.navigationModeButton.click(); - await previewFrame.getByLabel('Open Picker').click(); + await test.step(`Click on value help button of \`Date Property\` filter`, async () => { + const btn = previewFrame.getByTitle('Open Picker'); + if (await btn.count()) { + await btn.click(); + } else { + await previewFrame.locator('[aria-label="Open Picker"]').click(); + } + }); + await expect(previewFrame.getByRole('button', { name: new Date().getFullYear().toString() })).toBeVisible(); await editor.toolbar.uiAdaptationModeButton.click(); @@ -379,29 +339,22 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { await editor.toolbar.saveAndReloadButton.click(); await expect(editor.toolbar.saveButton).toBeDisabled(); - - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'appdescr_ui_generic_app_changePageConfiguration', - content: expect.objectContaining({ - entityPropertyChange: expect.objectContaining({ - propertyPath: 'component/settings/filterSettings/dateSettings', - propertyValue: expect.objectContaining({ - useDateRange: true - }) - }) - }) - }) - ]) - }) - ); + await verifyChanges(projectCopy, { + changes: [ + { + fileType: 'change', + changeType: 'appdescr_ui_generic_app_changePageConfiguration', + content: { + entityPropertyChange: { + propertyPath: 'component/settings/filterSettings/dateSettings', + propertyValue: { + useDateRange: true + } + } + } + } + ] + }); } ); @@ -419,33 +372,26 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { await editor.quickActions.enableVariantManagementInTablesAndCharts.click(); await editor.toolbar.saveAndReloadButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'appdescr_ui_generic_app_changePageConfiguration', - content: expect.objectContaining({ - parentPage: expect.objectContaining({ - component: 'sap.suite.ui.generic.template.ListReport' - }), - entityPropertyChange: expect.objectContaining({ - propertyPath: 'component/settings', - propertyValue: expect.objectContaining({ - smartVariantManagement: false - }) - }) - }) - }) - ]) - }) - ); + await editor.toolbar.isDisabled(); + await verifyChanges(projectCopy, { + changes: [ + { + fileType: 'change', + changeType: 'appdescr_ui_generic_app_changePageConfiguration', + content: { + parentPage: { + component: 'sap.suite.ui.generic.template.ListReport' + }, + entityPropertyChange: { + propertyPath: 'component/settings', + propertyValue: { + smartVariantManagement: false + } + } + } + } + ] + }); } ); @@ -458,34 +404,34 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { } }, async ({ page, previewFrame, ui5Version }) => { - const tableSettings = new TableSettings(previewFrame); + const tableSettings = new TableSettings(previewFrame, 'Rearrange Toolbar Content'); const editor = new AdaptationEditorShell(page, ui5Version); await editor.quickActions.changeTableActions.click(); - let actionTexts = await tableSettings.getActionSettingsTexts(); - expect(actionTexts).toEqual(['Button - Create', 'Button - Delete', 'Button - Add Card to Insights']); + await tableSettings.expectItemsToBeVisible([ + 'Button - Create', + 'Button - Delete', + 'Button - Add Card to Insights' + ]); await tableSettings.moveActionUp(1); + await tableSettings.expectItemsToBeVisible([ + 'Button - Delete', + 'Button - Create', + 'Button - Add Card to Insights' + ]); - actionTexts = await tableSettings.getActionSettingsTexts(); - expect(actionTexts).toEqual(['Button - Delete', 'Button - Create', 'Button - Add Card to Insights']); - - await tableSettings.actionSettingsDialog.getByRole('button').filter({ hasText: 'OK' }).click(); + await tableSettings.closeOrConfirmDialog(); await editor.toolbar.saveButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); + await editor.toolbar.isDisabled(); - await expect(page.getByTestId('saved-changes-stack')).toBeVisible(); - const changes = await page - .getByTestId('saved-changes-stack') - .getByText('Toolbar Content Move Change') - .all(); - expect(changes.length).toBe(1); + await editor.changesPanel.expectSavedChangesStack(page, 'Toolbar Content Move Change', 1); } ); test( - 'Add New Annotation File', + '9. Add New Annotation File', { annotation: { type: 'skipUI5Version', @@ -499,46 +445,50 @@ test.describe(`@quick-actions @fe-v2 @list-report`, () => { await editor.quickActions.addLocalAnnotationFile.click(); await editor.toolbar.saveAndReloadButton.click(); - - await expect(editor.toolbar.saveButton).toBeDisabled(); - await page.waitForTimeout(2000); // wait for changes to be processed - await expect - .poll( - async () => { - const changes = await readChanges(projectCopy); - const annotationFile = Object.keys(changes.annotations)[0]; - expect(changes.annotations[annotationFile]).toContain( - ` + + + + + + + + + + + + + + + + +` + }, + changes: [ { - message: 'make sure change file is created' + fileType: 'change', + changeType: 'appdescr_app_addAnnotationsToOData', + content: { + dataSourceId: 'mainService', + annotations: [/customer\.annotation\.annotation_\d+/] + } } - ) - .toEqual( - expect.objectContaining({ - annotations: expect.any(Object), // Generic - just check it exists - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'appdescr_app_addAnnotationsToOData', - content: expect.objectContaining({ - dataSourceId: 'mainService', - annotations: expect.arrayContaining([ - expect.stringMatching(/customer\.annotation\.annotation_\d+/) - ]) - }) - }) - ]) - }) - ); + ] + }); + await editor.reloadCompleted(); await editor.quickActions.showLocalAnnotationFile.click(); await expect( - previewFrame.getByText(/adp\.fiori\.elements\.v2\/changes\/annotations\/annotation_\d+\.xml/) + previewFrame.getByText(/adp\.fiori\.elements\.v2\/changes\/annotations\/annotation_\d+\.xml/), + `Check filename \`adp.fiori.elements.v2/changes/annotations/annotation_.xml\` is visible in the dialog` + ).toBeVisible(); + await expect( + previewFrame.getByRole('button', { name: 'Show File in VSCode' }), + `Check button \`Show File in VSCode\` is visible in the dialog` ).toBeVisible(); - await expect(previewFrame.getByRole('button', { name: 'Show File in VSCode' })).toBeVisible(); } ); }); diff --git a/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2.spec.ts b/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2.spec.ts index 55f201517f2..03096b8807c 100644 --- a/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2.spec.ts +++ b/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2.spec.ts @@ -1,10 +1,8 @@ import { expect } from '@sap-ux-private/playwright'; import { test } from '../../fixture'; -import { AdaptationEditorShell, AdpDialog, ListReport, TableSettings, readChanges } from './test-utils'; +import { AdaptationEditorShell, AdpDialog, ListReport, TableSettings, verifyChanges } from './test-utils'; import { ADP_FIORI_ELEMENTS_V2 } from '../../project'; import { lt, satisfies } from 'semver'; -import { join } from 'node:path'; -import { readdir } from 'fs/promises'; test.use({ projectConfig: { @@ -20,7 +18,7 @@ test.use({ }); test.describe(`@quick-actions @fe-v2 @object-page`, () => { test( - '1. Object Page: Enable Empty row mode and Change table actions', + '1. Object Page: Enable Empty row mode', { annotation: { type: 'skipUI5Version', @@ -30,76 +28,74 @@ test.describe(`@quick-actions @fe-v2 @object-page`, () => { async ({ page, projectCopy, previewFrame, ui5Version }) => { const editor = new AdaptationEditorShell(page, ui5Version); const lr = new ListReport(previewFrame); - const tableSettings = new TableSettings(previewFrame); - await test.step('Select first row and Navigate to Object Page', async () => { - await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); - await lr.locatorForListReportTableRow(0).click(); - }); + await editor.toolbar.navigationModeButton.click(); + await lr.clickOnGoButton(); + await lr.clickOnTableNthRow(0); - await test.step('1.1 Enable Empty row mode', async () => { - await editor.toolbar.uiAdaptationModeButton.click(); - await editor.quickActions.enableEmptyRowMode.click(); - await editor.toolbar.saveAndReloadButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'appdescr_ui_generic_app_changePageConfiguration', - content: expect.objectContaining({ - parentPage: expect.objectContaining({ - component: 'sap.suite.ui.generic.template.ObjectPage' - }), - entityPropertyChange: expect.objectContaining({ - propertyPath: - 'component/settings/sections/toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection/createMode', - propertyValue: 'creationRows' - }) - }) - }) - ]) - }) - ); + await editor.toolbar.uiAdaptationModeButton.click(); + await editor.quickActions.enableEmptyRowMode.click(); + await editor.toolbar.saveAndReloadButton.click(); + await editor.toolbar.isDisabled(); + await verifyChanges(projectCopy, { + changes: [ + { + fileType: 'change', + changeType: 'appdescr_ui_generic_app_changePageConfiguration', + content: { + parentPage: { + component: 'sap.suite.ui.generic.template.ObjectPage' + }, + entityPropertyChange: { + propertyPath: + 'component/settings/sections/toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection/createMode', + propertyValue: 'creationRows' + } + } + } + ] }); + } + ); - await test.step('1.2 OP - Change table actions', async () => { - await editor.toolbar.uiAdaptationModeButton.click(); - await editor.quickActions.changeTableActions.click(); - let actionTexts = await tableSettings.getActionSettingsTexts(); - expect(actionTexts).toEqual([ - 'SearchField - fiori.elements.v2.0::sap.suite.ui.generic.template.ObjectPage.view.Details::RootEntity--toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection::Table::Toolbar::SearchField', - 'Button - Create', - 'Button - Delete' - ]); - await tableSettings.moveActionDown(0); - actionTexts = await tableSettings.getActionSettingsTexts(); - expect(actionTexts).toEqual([ - 'Button - Create', - 'SearchField - fiori.elements.v2.0::sap.suite.ui.generic.template.ObjectPage.view.Details::RootEntity--toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection::Table::Toolbar::SearchField', - 'Button - Delete' - ]); - await tableSettings.actionSettingsDialog.getByRole('button').filter({ hasText: 'OK' }).click(); - await editor.toolbar.saveButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - await expect(page.getByTestId('saved-changes-stack')).toBeVisible(); - const changes = await page - .getByTestId('saved-changes-stack') - .getByText('Toolbar Content Move Change') - .all(); - expect(changes.length).toBe(1); - }); + test( + '2. Change table actions', + { + annotation: { + type: 'skipUI5Version', + description: '<1.120.23' + } + }, + async ({ page, previewFrame, ui5Version }) => { + const editor = new AdaptationEditorShell(page, ui5Version); + const lr = new ListReport(previewFrame); + const tableSettings = new TableSettings(previewFrame, 'Rearrange Toolbar Content'); + + await editor.toolbar.navigationModeButton.click(); + await lr.clickOnGoButton(); + await lr.clickOnTableNthRow(0); + await editor.toolbar.uiAdaptationModeButton.click(); + await editor.quickActions.changeTableActions.click(); + await tableSettings.expectItemsToBeVisible([ + 'SearchField - fiori.elements.v2.0::sap.suite.ui.generic.template.ObjectPage.view.Details::RootEntity--toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection::Table::Toolbar::SearchField', + 'Button - Create', + 'Button - Delete' + ]); + await tableSettings.moveActionDown(0); + await tableSettings.expectItemsToBeVisible([ + 'Button - Create', + 'SearchField - fiori.elements.v2.0::sap.suite.ui.generic.template.ObjectPage.view.Details::RootEntity--toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection::Table::Toolbar::SearchField', + 'Button - Delete' + ]); + await tableSettings.closeOrConfirmDialog(); + await editor.toolbar.saveButton.click(); + await expect(editor.toolbar.saveButton).toBeDisabled(); + await editor.changesPanel.expectSavedChangesStack(page, 'Toolbar Content Move Change', 1); } ); test( - 'Add controller to page', + '3. Add controller to page', { annotation: { type: 'skipUI5Version', @@ -114,72 +110,53 @@ test.describe(`@quick-actions @fe-v2 @object-page`, () => { await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); + await lr.clickOnGoButton(); await lr.locatorForListReportTableRow(0).click(); await editor.toolbar.uiAdaptationModeButton.click(); - + if (satisfies(ui5Version, '~1.71.0')) { + await page.waitForTimeout(1000); + } await editor.quickActions.addControllerToPage.click(); - await previewFrame.getByRole('textbox', { name: 'Controller Name' }).fill('TestController'); - await dialog.createButton.click(); - + await dialog.fillField('Controller Name', 'TestController'); + await dialog.clickCreateButton(); if (lt(ui5Version, '1.136.0')) { await expect(page.getByText('Changes detected!')).toBeVisible(); } else { await editor.toolbar.saveButton.click(); } - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - coding: expect.objectContaining({ - ['TestController.js']: expect.stringMatching( - /ControllerExtension\.extend\("adp\.fiori\.elements\.v2\.TestController"/ - ) - }), - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'codeExt', - content: expect.objectContaining({ codeRef: 'coding/TestController.js' }) - }) - ]) - }) - ); - - await expect - .poll( - async () => { - const changesDirectory = join(projectCopy, 'webapp', 'changes', 'coding'); - const codingChanges = await readdir(changesDirectory); - return codingChanges.length; - }, + await verifyChanges(projectCopy, { + coding: { + ['TestController.js']: /ControllerExtension\.extend\("adp\.fiori\.elements\.v2\.TestController"/ + }, + changes: [ { - message: 'make sure controller file is created', - timeout: 4_000 + fileType: 'change', + changeType: 'codeExt', + content: { codeRef: 'coding/TestController.js' } } - ) - .toEqual(1); + ] + }); await editor.changesPanel.reloadButton.click(); - await editor.reloadCompleted(); - await editor.quickActions.showPageController.click(); await expect( - previewFrame.getByText('adp.fiori.elements.v2/changes/coding/TestController.js') + previewFrame.getByText('adp.fiori.elements.v2/changes/coding/TestController.js'), + `Check file name \`adp.fiori.elements.v2/changes/coding/TestController.js\` is visible in dialog` ).toBeVisible(); - await expect(previewFrame.getByRole('button', { name: 'Open in VS Code' })).toBeVisible(); + await expect( + previewFrame.getByRole('button', { name: 'Open in VS Code' }), + `Check \`Open in VS Code\` button visible in dialog.` + ).toBeVisible(); } ); test( - 'Add Custom Table Action', + '4. Add Custom Table Action', { annotation: { type: 'skipUI5Version', @@ -193,52 +170,42 @@ test.describe(`@quick-actions @fe-v2 @object-page`, () => { await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); + await lr.clickOnGoButton(); await lr.locatorForListReportTableRow(0).click(); await editor.toolbar.uiAdaptationModeButton.click(); await editor.quickActions.addCustomTableAction.click(); - await previewFrame.getByRole('textbox', { name: 'Fragment Name' }).fill('op-table-action'); - await dialog.createButton.click(); + await dialog.fillField('Fragment Name', 'op-table-action'); + await dialog.clickCreateButton(); await editor.toolbar.saveAndReloadButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - fragments: expect.objectContaining({ - 'op-table-action.fragment.xml': expect.stringMatching( - new RegExp(` + await editor.toolbar.isDisabled(); + await verifyChanges(projectCopy, { + fragments: { + 'op-table-action.fragment.xml': ` - -`) - ) - }), - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'addXML', - content: expect.objectContaining({ - targetAggregation: 'content', - fragmentPath: 'fragments/op-table-action.fragment.xml' - }) - }) - ]) - }) - ); +` + }, + changes: [ + { + fileType: 'change', + changeType: 'addXML', + content: { + targetAggregation: 'content', + fragmentPath: 'fragments/op-table-action.fragment.xml' + } + } + ] + }); } ); test( - 'Change table columns', + '5. Change table columns', { annotation: { type: 'skipUI5Version', @@ -251,19 +218,17 @@ test.describe(`@quick-actions @fe-v2 @object-page`, () => { const editor = new AdaptationEditorShell(page, ui5Version); await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); - await lr.locatorForListReportTableRow(0).click(); + await lr.clickOnGoButton(); + await lr.clickOnTableNthRow(0); await editor.toolbar.uiAdaptationModeButton.click(); await editor.quickActions.changeTableColumns.click(); - - await expect(tableSettings.tableSettingsDialog.getByText('String Property')).toBeVisible(); - await expect(tableSettings.tableSettingsDialog.getByText('Date Property')).toBeVisible(); + await tableSettings.expectItemsToBeVisible(['String Property', 'Date Property']); } ); test( - 'Add Custom Table Column', + '6. Add Custom Table Column', { annotation: { type: 'skipUI5Version', @@ -276,37 +241,29 @@ test.describe(`@quick-actions @fe-v2 @object-page`, () => { const editor = new AdaptationEditorShell(page, ui5Version); await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); - await lr.locatorForListReportTableRow(0).click(); + await lr.clickOnGoButton(); + await lr.clickOnTableNthRow(0); await editor.toolbar.uiAdaptationModeButton.click(); await editor.reloadCompleted(); await editor.quickActions.addCustomTableColumn.click(); - await previewFrame.getByRole('textbox', { name: 'Column Fragment Name' }).fill('table-column'); - await previewFrame.getByRole('textbox', { name: 'Cell Fragment Name' }).fill('table-cell'); - await dialog.createButton.click(); + await dialog.fillField('Column Fragment Name', 'table-column'); + await dialog.fillField('Cell Fragment Name', 'table-cell'); + await dialog.clickCreateButton(); await editor.toolbar.saveAndReloadButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); + await editor.toolbar.isDisabled(); - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - fragments: expect.objectContaining({ - 'table-cell.fragment.xml': expect.stringMatching( - new RegExp(` + await verifyChanges(projectCopy, { + fragments: { + 'table-cell.fragment.xml': ` -`) - ), - 'table-column.fragment.xml': expect.stringMatching( - new RegExp(` +`, + 'table-column.fragment.xml': ` { value='\\\\{"columnKey": "column-[a-z0-9]+", "columnIndex": "3"}' /> -`) - ) - }), - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'addXML', - content: expect.objectContaining({ - targetAggregation: 'columns', - fragmentPath: 'fragments/table-column.fragment.xml' - }) - }), - expect.objectContaining({ - fileType: 'change', - changeType: 'addXML', - content: expect.objectContaining({ - boundAggregation: 'items', - targetAggregation: 'cells', - fragmentPath: 'fragments/table-cell.fragment.xml' - }) - }) - ]) - }) - ); +` + }, + changes: [ + { + fileType: 'change', + changeType: 'addXML', + content: { + targetAggregation: 'columns', + fragmentPath: 'fragments/table-column.fragment.xml' + } + }, + { + fileType: 'change', + changeType: 'addXML', + content: { + boundAggregation: 'items', + targetAggregation: 'cells', + fragmentPath: 'fragments/table-cell.fragment.xml' + } + } + ] + }); await editor.reloadCompleted(); - await expect(previewFrame.getByRole('columnheader', { name: 'New column' }).locator('div')).toBeVisible(); + await expect( + previewFrame.getByRole('columnheader', { name: 'New column' }).locator('div'), + `Check Column Name is \`New Column\`` + ).toBeVisible(); if (satisfies(ui5Version, '<1.120.0')) { await expect(previewFrame.getByRole('cell', { name: 'Sample data' }).first()).toBeVisible(); } else { - await expect(previewFrame.getByRole('gridcell', { name: 'Sample data' }).first()).toBeVisible(); + await expect( + previewFrame.getByRole('gridcell', { name: 'Sample data' }).first(), + `Check Column Data is \`Sample data\`` + ).toBeVisible(); } } ); test( - 'Add Header Field', + '7. Add Header Field', { annotation: { type: 'skipUI5Version', @@ -371,30 +332,21 @@ test.describe(`@quick-actions @fe-v2 @object-page`, () => { await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); - await lr.locatorForListReportTableRow(0).click(); + await lr.clickOnGoButton(); + await lr.clickOnTableNthRow(0); await editor.toolbar.uiAdaptationModeButton.click(); await editor.quickActions.addHeaderField.click(); - await previewFrame.getByRole('textbox', { name: 'Fragment Name' }).fill('op-header-field'); - await dialog.createButton.click(); - + await dialog.fillField('Fragment Name', 'op-header-field'); + await dialog.clickCreateButton(); await editor.toolbar.saveAndReloadButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - fragments: expect.objectContaining({ - 'op-header-field.fragment.xml': expect.stringMatching( - new RegExp( - ` + await editor.toolbar.isDisabled(); + await verifyChanges(projectCopy, { + fragments: { + 'op-header-field.fragment.xml': ` { -`, - 'm' - ) - ) - }), - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'addXML', - content: expect.objectContaining({ - targetAggregation: expect.stringMatching(/^(headerContent|items)$/), - fragmentPath: 'fragments/op-header-field.fragment.xml' - }) - }) - ]) - }) - ); +` + }, + changes: [ + { + fileType: 'change', + changeType: 'addXML', + content: { + targetAggregation: /^(headerContent|items)$/, + fragmentPath: 'fragments/op-header-field.fragment.xml' + } + } + ] + }); } ); - - test('Add Custom Section', {}, async ({ page, previewFrame, projectCopy, ui5Version }) => { + test('8. Add Custom Section', {}, async ({ page, previewFrame, projectCopy, ui5Version }) => { const lr = new ListReport(previewFrame); const dialog = new AdpDialog(previewFrame, ui5Version); const editor = new AdaptationEditorShell(page, ui5Version); await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); - await lr.locatorForListReportTableRow(0).click(); + await lr.clickOnGoButton(); + await lr.clickOnTableNthRow(0); await editor.toolbar.uiAdaptationModeButton.click(); await editor.quickActions.addCustomSection.click(); - await previewFrame.getByRole('textbox', { name: 'Fragment Name' }).fill('op-section'); - await dialog.createButton.click(); + await dialog.fillField('Fragment Name', 'op-section'); + await dialog.clickCreateButton(); await editor.toolbar.saveAndReloadButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - fragments: expect.objectContaining({ - 'op-section.fragment.xml': expect.stringMatching( - new RegExp( - ` + await editor.toolbar.isDisabled(); + await verifyChanges(projectCopy, { + fragments: { + ['op-section.fragment.xml']: ` { -`, - 'm' - ) - ) - }), - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'addXML', - content: expect.objectContaining({ - targetAggregation: 'sections', - fragmentPath: 'fragments/op-section.fragment.xml' - }) - }) - ]) - }) - ); +` + }, + changes: [ + { + fileType: 'change', + changeType: 'addXML', + content: { + targetAggregation: 'sections', + fragmentPath: 'fragments/op-section.fragment.xml' + } + } + ] + }); }); }); diff --git a/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2a.spec.ts b/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2a.spec.ts index 0b21aac72f3..833f621a8d2 100644 --- a/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2a.spec.ts +++ b/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2a.spec.ts @@ -1,7 +1,6 @@ -import { expect } from '@sap-ux-private/playwright'; import { test } from '../../fixture'; import { ADP_FIORI_ELEMENTS_V2 } from '../../project'; -import { AdaptationEditorShell, ListReport, readChanges } from './test-utils'; +import { AdaptationEditorShell, ListReport, verifyChanges } from './test-utils'; test.use({ projectConfig: { ...ADP_FIORI_ELEMENTS_V2, @@ -17,7 +16,7 @@ test.use({ }); test.describe(`@quick-actions @fe-v2 @object-page @op-variant-management`, () => { test( - 'Enable Variant Management in Tables', + '1. Enable Variant Management in Tables', { annotation: { type: 'skipUI5Version', @@ -29,43 +28,36 @@ test.describe(`@quick-actions @fe-v2 @object-page @op-variant-management`, () => const lr = new ListReport(previewFrame); await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); - await lr.locatorForListReportTableRow(0).click(); + await lr.clickOnGoButton(); + await lr.clickOnTableNthRow(0); await editor.toolbar.uiAdaptationModeButton.click(); await editor.quickActions.enableOPVariantManagementInTable.click(); await editor.toolbar.saveAndReloadButton.click(); - await expect(editor.toolbar.saveButton).toBeDisabled(); - - await expect - .poll(async () => readChanges(projectCopy), { - message: 'make sure change file is created' - }) - .toEqual( - expect.objectContaining({ - changes: expect.arrayContaining([ - expect.objectContaining({ - fileType: 'change', - changeType: 'appdescr_ui_generic_app_changePageConfiguration', - content: expect.objectContaining({ - parentPage: expect.objectContaining({ - component: 'sap.suite.ui.generic.template.ObjectPage', - entitySet: 'RootEntity' - }), - entityPropertyChange: expect.objectContaining({ - propertyPath: - 'component/settings/sections/toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection/tableSettings', - operation: 'UPSERT', - propertyValue: expect.objectContaining({ - variantManagement: true - }) - }) - }) - }) - ]) - }) - ); + await editor.toolbar.isDisabled(); + await verifyChanges(projectCopy, { + changes: [ + { + fileType: 'change', + changeType: 'appdescr_ui_generic_app_changePageConfiguration', + content: { + parentPage: { + component: 'sap.suite.ui.generic.template.ObjectPage', + entitySet: 'RootEntity' + }, + entityPropertyChange: { + propertyPath: + 'component/settings/sections/toFirstAssociatedEntity::com.sap.vocabularies.UI.v1.LineItem::tableSection/tableSettings', + operation: 'UPSERT', + propertyValue: { + variantManagement: true + } + } + } + } + ] + }); } ); }); diff --git a/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2b.spec.ts b/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2b.spec.ts index 277de9b69e4..e5d7ffe66d0 100644 --- a/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2b.spec.ts +++ b/tests/integration/adaptation-editor/src/scenarios/quick-actions/object-page-v2b.spec.ts @@ -1,4 +1,3 @@ -import { expect } from '@sap-ux-private/playwright'; import { test } from '../../fixture'; import { ADP_FIORI_ELEMENTS_V2 } from '../../project'; import { AdaptationEditorShell, ListReport, TableSettings } from './test-utils'; @@ -17,7 +16,7 @@ test.use({ }); test.describe(`@quick-actions @fe-v2 @object-page @op-analytical-table`, () => { test( - 'Change table columns (analytical table)', + '1. Change table columns (analytical table)', { annotation: { type: 'skipUI5Version', @@ -26,18 +25,16 @@ test.describe(`@quick-actions @fe-v2 @object-page @op-analytical-table`, () => { }, async ({ page, previewFrame, ui5Version }) => { const lr = new ListReport(previewFrame); - const tableSettings = new TableSettings(previewFrame); const editor = new AdaptationEditorShell(page, ui5Version); await editor.toolbar.navigationModeButton.click(); - await lr.goButton.click(); - await lr.locatorForListReportTableRow(0).click(); + await lr.clickOnGoButton(); + await lr.clickOnTableNthRow(0); await editor.toolbar.uiAdaptationModeButton.click(); await editor.quickActions.changeTableColumns.click(); - - await expect(tableSettings.tableSettingsDialog.getByText('String Property')).toBeVisible(); - await expect(tableSettings.tableSettingsDialog.getByText('Date Property')).toBeVisible(); + const tableSettings = new TableSettings(previewFrame); + await tableSettings.expectItemsToBeVisible(['String Property', 'Date Property']); } ); }); diff --git a/tests/integration/adaptation-editor/src/scenarios/quick-actions/test-utils.ts b/tests/integration/adaptation-editor/src/scenarios/quick-actions/test-utils.ts index aa2b34d84d2..8bc90acbadd 100644 --- a/tests/integration/adaptation-editor/src/scenarios/quick-actions/test-utils.ts +++ b/tests/integration/adaptation-editor/src/scenarios/quick-actions/test-utils.ts @@ -1,15 +1,27 @@ import { readdir, readFile } from 'fs/promises'; import { join } from 'node:path'; -import { expect, type FrameLocator, type Page, type Locator } from '@sap-ux-private/playwright'; +import { expect, test, type FrameLocator, type Page, type Locator } from '@sap-ux-private/playwright'; import { gte } from 'semver'; interface Changes { annotations: Record; - coding: Record; + coding: Record; fragments: Record; changes: object[]; } +/** + * Creates a locator for a button element with description for better test reporting. + * + * @param page - Page or FrameLocator to search within + * @param name - Name of the button to locate + * @param context - Context description for the button (e.g., 'Quick Actions Panel') + * @returns Locator for the button with added description + */ +export function getButtonLocator(page: Page | FrameLocator, name: string, context: string): Locator { + return page.getByRole('button', { name }).describe(`\`${name}\` button in the ${context}`); +} + /** * Class representing a List Report in the Adaptation Editor. */ @@ -26,7 +38,7 @@ export class ListReport { * @returns Locator for the "Clear" button. */ get clearButton(): Locator { - return this.frame.getByRole('button', { name: 'Clear' }); + return this.frame.getByRole('button', { name: 'Clear' }).describe('Clear button in the List Report filter bar'); } /** * Returns a locator for the specified table row in the List Report. @@ -38,6 +50,46 @@ export class ListReport { const dataRows = this.frame.locator('tbody > tr'); return dataRows.nth(index).locator('.sapMListTblNavCol').first(); } + + /** + * Clicks on the go button. + */ + async clickOnGoButton(): Promise { + await test.step(`Click on \`Go\` button.`, async () => { + await this.goButton.click(); + }); + } + /** + * Clicks on the specified row in the List Report table. + * + * @param index - table row index + */ + async clickOnTableNthRow(index: number): Promise { + const tableTitle = await this.getTableTitleText(); + // Extract only the string part before parentheses (e.g., "Root Entities" from "Root Entities (2)") + const match = tableTitle.match(/^(.+?)\s*\(/); + const tableName = match ? match[1] : tableTitle; + await test.step(`Click on row \`${index + 1}\` of \`${tableName}\` table `, async () => { + await this.locatorForListReportTableRow(index).click(); + }); + } + + /** + * @returns Locator for the table title/header. + */ + get tableTitle(): Locator { + return this.frame.locator('.sapMTitle.sapUiCompSmartTableHeader'); + } + + /** + * Gets the table title text. + * + * @returns Promise resolving to the table title text. + */ + async getTableTitleText(): Promise { + return (await this.tableTitle.textContent()) ?? ''; + } + /** * @param frame - FrameLocator for the List Report. */ @@ -51,37 +103,21 @@ export class ListReport { */ export class TableSettings { private readonly frame: FrameLocator; + private dialogName: string; /** * @returns Locator for the dialog containing table settings. */ get dialog(): Locator { - return this.frame.getByLabel('View Settings'); - } - /** - * @returns Locator for the table settings dialog. - */ - get tableSettingsDialog(): Locator { - return this.dialog; - } - /** - * @returns Locator for the table settings dialog. - */ - get actionSettingsDialog(): Locator { - return this.frame.getByLabel('Rearrange Toolbar Content'); - } - /** - * @returns Locator for the table settings dialog. - */ - get tableActionsSettingsDialog(): Locator { - return this.actionSettingsDialog; + return this.frame.getByLabel(this.dialogName); } + /** * Returns an array of all visible texts in the first column of the rearrange toolbar content table. * * @returns {Promise} An array of visible texts from the first column. */ - async getActionSettingsTexts(): Promise { - const rows = this.actionSettingsDialog.locator('tbody > tr:not(.sapMListTblHeader)'); + async getSettingsTexts(): Promise { + const rows = this.dialog.locator('tbody > tr:not(.sapMListTblHeader)'); const count = await rows.count(); const texts: string[] = []; for (let i = 0; i < count; i++) { @@ -96,25 +132,60 @@ export class TableSettings { * @param index - The zero-based index of the action to move up. */ async moveActionUp(index: number): Promise { - const rows = this.actionSettingsDialog.locator('tbody > tr:not(.sapMListTblHeader)'); - await rows.nth(index).hover(); - await rows.nth(index).getByRole('button', { name: 'Move Up' }).click(); + const rows = this.dialog.locator('tbody > tr:not(.sapMListTblHeader)'); + await test.step(`Hover over row \`${index + 1}\` and click on \`Move up\` button in the row of \`${ + this.dialogName + }\` table`, async () => { + await rows.nth(index).hover(); + await rows.nth(index).getByRole('button', { name: 'Move Up' }).click(); + }); + } + /** + * Confirm or cancel the dialog. + * + * @param text - Dialog's confirm button text. + */ + async closeOrConfirmDialog(text = 'OK'): Promise { + await test.step(`Click on \`${text}\` button of the dialog \`${this.dialogName}\``, async () => { + await this.dialog.getByRole('button', { name: 'OK' }).click(); + }); + } + /** + * @param frame - FrameLocator for the dialog. + * @param dialogName - Name of the dialog. + */ + constructor(frame: FrameLocator, dialogName = 'View Settings') { + this.frame = frame; + this.dialogName = dialogName; } + /** * Moves an action down by clicking the "Move Down" button in the specified row. * * @param index - The zero-based index of the action to move down. */ async moveActionDown(index: number): Promise { - const rows = this.actionSettingsDialog.locator('tbody > tr:not(.sapMListTblHeader)'); - await rows.nth(index).hover(); - await rows.nth(index).getByRole('button', { name: 'Move Down' }).click(); + const rows = this.dialog.locator('tbody > tr:not(.sapMListTblHeader)'); + await test.step(`Hover over row \`${index + 1}\` and click on Move down button in the row of \`${ + this.dialogName + }\` table`, async () => { + await rows.nth(index).hover(); + await rows.nth(index).getByRole('button', { name: 'Move Down' }).click(); + }); } + /** - * @param frame - FrameLocator for the dialog. + * Checks given elements are visible. + * + * @param texts - list of texts to checked. */ - constructor(frame: FrameLocator) { - this.frame = frame; + async expectItemsToBeVisible(texts: string[]): Promise { + const textsList = texts.join(', '); + await test.step(`Check \`${textsList}\` exist in the \`${this.dialogName}\` dialog`, async () => { + for (const text of texts) { + await expect(this.dialog.getByText(text)).toBeVisible(); + } + }); } } @@ -123,123 +194,135 @@ export class TableSettings { */ class QuickActionPanel { private readonly page: Page; + private readonly context: string = `Quick Actions Panel`; + + /** + * Helper method to get a button locator with description. + * + * @param name - Button name/label + * @returns Locator with description + */ + private getButtonLocator(name: string): Locator { + return getButtonLocator(this.page, name, this.context); + } /** * @returns Locator for the button to add a controller to the page. */ get addControllerToPage(): Locator { - return this.page.getByRole('button', { name: 'Add Controller to Page' }); + return this.getButtonLocator('Add Controller to Page'); } /** * @returns Locator for the button to show the page controller. */ get showPageController(): Locator { - return this.page.getByRole('button', { name: 'Show Page Controller' }); + return this.getButtonLocator('Show Page Controller'); } /** * @returns Locator for the button to show the local annotation file. */ get showLocalAnnotationFile(): Locator { - return this.page.getByRole('button', { name: 'Show Local Annotation File' }); + return this.getButtonLocator('Show Local Annotation File'); } /** * @returns Locator for the button to enable the "Clear" button in the filter bar. */ get enableClearButton(): Locator { - return this.page.getByRole('button', { name: 'Enable "Clear" Button in Filter Bar' }); + return this.getButtonLocator('Enable "Clear" Button in Filter Bar'); } /** * @returns Locator for the button to disable the "Clear" button in the filter bar. */ get disableClearButton(): Locator { - return this.page.getByRole('button', { name: 'Disable "Clear" Button in Filter Bar' }); + return this.getButtonLocator('Disable "Clear" Button in Filter Bar'); } /** * @returns Locator for the button to enable the semantic date range in the filter bar. */ get enableSemanticDateRange(): Locator { - return this.page.getByRole('button', { name: 'Enable Semantic Date Range in Filter Bar' }); + return this.getButtonLocator('Enable Semantic Date Range in Filter Bar'); } /** * @returns Locator for the button to disable the semantic date range in the filter bar. */ get disableSemanticDateRange(): Locator { - return this.page.getByRole('button', { name: 'Disable Semantic Date Range in Filter Bar' }); + return this.getButtonLocator('Disable Semantic Date Range in Filter Bar'); } /** * @returns Locator for the button to change table columns. */ get changeTableColumns(): Locator { - return this.page.getByRole('button', { name: 'Change Table Columns' }); + return this.getButtonLocator('Change Table Columns'); } /** * @returns Locator for the button to change Table Actions. */ get changeTableActions(): Locator { - return this.page.getByRole('button', { name: 'Change Table Actions' }); + return this.getButtonLocator('Change Table Actions'); } /** * @returns Locator for the button to add a custom table action. */ get addCustomTableAction(): Locator { - return this.page.getByRole('button', { name: 'Add Custom Table Action' }); + return this.getButtonLocator('Add Custom Table Action'); } /** * @returns Locator for the button to add a custom table column. */ get addCustomTableColumn(): Locator { - return this.page.getByRole('button', { name: 'Add Custom Table Column' }); + return this.getButtonLocator('Add Custom Table Column'); } /** * @returns Locator for the button to enable variant management in tables and charts. */ get enableVariantManagementInTablesAndCharts(): Locator { - return this.page.getByRole('button', { name: 'Enable Variant Management in Tables and Charts' }); + return this.getButtonLocator('Enable Variant Management in Tables and Charts'); } /** * @returns Locator for the button to enable variant management in tables. */ get enableOPVariantManagementInTable(): Locator { - return this.page.getByRole('button', { name: 'Enable Variant Management in Tables' }); + return this.getButtonLocator('Enable Variant Management in Tables'); } /** * @returns Locator for the button to enable empty row mode for tables. */ get enableEmptyRowMode(): Locator { - return this.page.getByRole('button', { name: 'Enable Empty Row Mode for Tables' }); + return this.getButtonLocator('Enable Empty Row Mode for Tables'); } /** * @returns Locator for the button to Add Header Field. */ get addHeaderField(): Locator { - return this.page.getByRole('button', { name: 'Add Header Field' }); + return this.getButtonLocator('Add Header Field'); } /** * @returns Locator for the button to Add Custom Section. */ get addCustomSection(): Locator { - return this.page.getByRole('button', { name: 'Add Custom Section' }); + return this.getButtonLocator('Add Custom Section'); } /** * @returns Locator for the button to Add Local Annotation File. */ get addLocalAnnotationFile(): Locator { - return this.page.getByRole('button', { name: 'Add Local Annotation File' }); + return this.getButtonLocator('Add Local Annotation File'); } /** + * Constructor for QuickActionPanel. * - * @param page + * @param page - Page object. */ constructor(page: Page) { this.page = page; @@ -251,30 +334,41 @@ class QuickActionPanel { */ class Toolbar { private readonly page: Page; + private readonly context: string = 'toolBar'; /** * @returns Locator for the "Save" button. */ get saveButton(): Locator { - return this.page.getByRole('button', { name: 'Save' }); + return getButtonLocator(this.page, 'Save', this.context); } /** * @returns Locator for the "Save and Reload" button. */ get saveAndReloadButton(): Locator { - return this.page.getByRole('button', { name: 'Save and Reload' }); + return getButtonLocator(this.page, 'Save and Reload', this.context); } /** * @returns Locator for the "UI Adaptation" mode button. */ get uiAdaptationModeButton(): Locator { - return this.page.getByRole('button', { name: 'UI Adaptation' }); + return getButtonLocator(this.page, 'UI Adaptation', this.context); } /** * @returns Locator for the "Navigation" mode button. */ get navigationModeButton(): Locator { - return this.page.getByRole('button', { name: 'Navigation' }); + return getButtonLocator(this.page, 'Navigation', this.context); + } + + /** + * Checks if the "Save" button is disabled. + * + * @returns Promise that resolves when the "Save" button is verified to be disabled. + */ + async isDisabled(): Promise { + return await expect(this.saveButton, `Check \`Save\` button in the toolbar is disabled`).toBeDisabled(); } + /** * @param page - Page object for the toolbar. */ @@ -287,11 +381,14 @@ class Toolbar { */ class ChangesPanel { private readonly page: Page; + private readonly context: string = 'Changes Panel'; + /** * @returns Locator for the "Reload" button. */ get reloadButton(): Locator { - return this.page.getByRole('link', { name: 'Reload' }); + // This was using 'link' role before, so we need a special case for it + return this.page.getByRole('link', { name: 'Reload' }).describe('`Reload` link in the Changes Panel'); } /** * @param page - Page object for the Changes Panel. @@ -299,6 +396,21 @@ class ChangesPanel { constructor(page: Page) { this.page = page; } + /** + * Reusable function to check saved changes stack for specific change types. + * + * @param page - Page object to search within. + * @param changeText - Text to search for within the saved-changes-stack. + * @param expectedCount - Expected number of changes. + * @returns Promise that resolves when the assertion passes. + */ + async expectSavedChangesStack(page: Page, changeText: string, expectedCount: number): Promise { + await test.step(`Check saved changes stack contains \`${expectedCount}\` \`${changeText}\` change(s)`, async () => { + await expect(page.getByTestId('saved-changes-stack')).toBeVisible(); + const changes = await page.getByTestId('saved-changes-stack').getByText(changeText).all(); + expect(changes.length).toBe(expectedCount); + }); + } } /** @@ -310,7 +422,6 @@ export class AdaptationEditorShell { readonly quickActions: QuickActionPanel; readonly toolbar: Toolbar; readonly changesPanel: ChangesPanel; - async reloadCompleted(): Promise { await expect(this.toolbar.uiAdaptationModeButton).toBeEnabled({ timeout: 15_000 }); } @@ -344,6 +455,53 @@ export class AdpDialog { } } + /** + * Gets the name (title) of the dialog. + * + * @returns Promise resolving to the dialog title as a string. + */ + async getName(): Promise { + let heading = this.frame.getByRole('dialog').getByRole('heading'); + if (typeof heading.first === 'function') { + heading = heading.first(); + } + const title = (await heading.textContent()) ?? ''; + return title; + } + + /** + * Fill input field with the given value. + * + * @param fieldName - name of input field + * @param value - value to fill in. + */ + async fillField(fieldName: string, value: string): Promise { + const title = await this.getName(); + await test.step(`Fill \`${fieldName}\` field with \`${value}\` in the dialog \`${title}\``, async () => { + const field = this.frame.getByRole('textbox', { name: fieldName }); + await field.fill(value); + }); + } + + async clickCreateButton(): Promise { + const title = await this.getName(); + await test.step(`Click on \`Create\` button in the dialog \`${title}\``, async () => { + await this.createButton.click(); + }); + } + + /** + * Checks if the "Open in VS Code" button is visible. + * + * @returns Promise that resolves when the button is visible. + */ + async openInVSCodeVisible(): Promise { + return expect( + this.frame.getByRole('button', { name: 'Open in VS Code' }), + 'Check `Open in VS Code` button is visible' + ).toBeVisible(); + } + /** * @param frame - FrameLocator for the dialog. * @param ui5Version - UI5 version. @@ -377,9 +535,12 @@ export async function readChanges(root: string): Promise { result.changes.push(JSON.parse(text) as object); } else if (file.name === 'annotations' || file.name === 'coding' || file.name === 'fragments') { const children = await readdir(join(changesDirectory, file.name), { withFileTypes: true }); + let index = 0; for (const child of children) { if (child.isFile()) { - result[file.name][child.name] = await readFile( + const fileName = file.name === 'annotations' ? `file${index}` : child.name; // annotation file name contains timestamp + index++; + result[file.name][fileName] = await readFile( join(changesDirectory, file.name, child.name), { encoding: 'utf-8' @@ -397,3 +558,216 @@ export async function readChanges(root: string): Promise { } return result; } + +/** + * Compare changes against expected changes and create appropriate matchers. + * + * @param projectCopy path to projectCopy to check for changes + * @param expected The actual changes object from test results + * @returns An expect matcher for use in tests + */ +export async function expectChanges(projectCopy: string, expected: Partial): Promise { + const matcher: Record = {}; + + // Process file-based properties (fragments, annotations, coding) in a single pattern + const fileBasedProperties: (keyof Changes)[] = ['fragments', 'annotations', 'coding']; + + for (const prop of fileBasedProperties) { + if (expected[prop]) { + const matchers: Record = {}; + for (const [filename, content] of Object.entries(expected[prop])) { + matchers[filename] = expect.stringMatching(new RegExp(content)); + } + matcher[prop] = expect.objectContaining(matchers); + } + } + + // Handle changes array with deep conversion + if (expected.changes) { + const changeMatchers = expected.changes.map((change) => convertToExpectMatchers(change)); + matcher.changes = expect.arrayContaining(changeMatchers); + } + + await expect + .poll(async () => readChanges(projectCopy), { + message: 'make sure change file is created' + }) + .toEqual(matcher); +} + +/** + * Recursively convert an object to expect matchers. + * + * @param obj Object to convert to expect matchers + * @returns Converted object with expect matchers + */ +function convertToExpectMatchers(obj: any): unknown { + // Base case: null or undefined + if (obj === null || obj === undefined) { + return obj; + } + + // Handle arrays + if (Array.isArray(obj)) { + return expect.arrayContaining(obj.map((item) => convertToExpectMatchers(item))); + } + + // Handle objects + if (typeof obj === 'object') { + const result: Record = {}; + + for (const [key, value] of Object.entries(obj)) { + // Recursively convert nested objects/arrays + if (value !== null && typeof value === 'object') { + result[key] = convertToExpectMatchers(value) as any; + } + // Convert strings to stringMatching + else if (typeof value === 'string') { + result[key] = expect.stringMatching(value); + } + // Keep other primitive types as is + else { + result[key] = value; + } + } + + return expect.objectContaining(result); + } + + return obj; +} + +/** + * Verify changes against expected changes. + * + * @param projectCopy Project copy to check for changes + * @param expected Expected changes to verify + */ +export async function verifyChanges(projectCopy: any, expected: Partial): Promise { + // Build detailed description for changes being verified in markdown format + let description = 'Verify changes:\n\n'; + // Add formatted sections to description + description += formatFragmentsForMarkdown(expected.fragments ?? {}); + description += formatAnnotationsForMarkdown(expected.annotations); + description += formatCodingForMarkdown(expected.coding); + description += formatChangesForMarkdown(expected.changes); + const matcher: Record = {}; + + // Process file-based properties (fragments, annotations, coding) in a single pattern + const fileBasedProperties: (keyof Changes)[] = ['fragments', 'annotations', 'coding']; + + for (const prop of fileBasedProperties) { + if (expected[prop]) { + const matchers: Record = {}; + for (const [filename, content] of Object.entries(expected[prop])) { + matchers[filename] = expect.stringMatching(new RegExp(content)); + } + matcher[prop] = expect.objectContaining(matchers); + } + } + + await test.step(description, async () => { + await expect + .poll(async () => readChanges(projectCopy), { + message: '' + }) + .toEqual(expect.objectContaining(matcher)); + }); +} + +/** + * Replace literal occurrences like "[a-z0-9]+" in content with a readable placeholder. + * + * @param input - string to sanitize + * @returns sanitized string + */ +function sanitizeIds(input: string): string { + return input + .replace(/\[a-z0-9\]\+/g, '') + .replace(/\[0-9\]\+/g, '') + .replace(/\\\[a-z0-9\\\]\+/g, '') + .replace(/\\\[0-9\\\]\+/g, ''); +} + +/** + * Format fragments for markdown output. + * + * @param fragments Fragment files to format + * @returns Markdown formatted string + */ +function formatFragmentsForMarkdown(fragments: Record): string { + if (!fragments || Object.keys(fragments).length === 0) { + return ''; + } + + let result = '**Fragment(s)**\n\n'; + for (const [filename, content] of Object.entries(fragments)) { + const sanitized = sanitizeIds(content); + result += `**${filename}**\n\`\`\`xml\n${sanitized}\n\`\`\`\n\n`; + } + return result; +} + +/** + * Format annotations for markdown output. + * + * @param annotations Annotation files to format + * @returns Markdown formatted string + */ +function formatAnnotationsForMarkdown(annotations: Record | undefined): string { + if (!annotations) { + return ''; + } + + let result = '**Annotations**\n'; + if (Object.keys(annotations).length > 0) { + for (const [_filename, content] of Object.entries(annotations)) { + result += `\`\`\`xml\n${sanitizeIds(content)}\n\`\`\`\n\n`; + } + } + return result; +} + +/** + * Format coding files for markdown output. + * + * @param coding Coding files to format + * @returns Markdown formatted string + */ +function formatCodingForMarkdown(coding: Record | undefined): string { + if (!coding) { + return ''; + } + + let result = '**Coding**\n\n'; + if (Object.keys(coding).length > 0) { + for (const [filename, content] of Object.entries(coding)) { + const text = typeof content === 'string' ? content : String(content); + result += `**${filename}**\n\`\`\`js\n${sanitizeIds(text)}\n\`\`\`\n\n`; + } + } + return result; +} + +/** + * Format changes for markdown output. + * + * @param changes Changes to format + * @returns Markdown formatted string + */ +function formatChangesForMarkdown(changes: object[] | undefined): string { + if (!changes) { + return ''; + } + + let result = '**Change(s)**\n\n'; + if (changes.length > 0) { + changes.forEach((change, index) => { + if (changes.length > 1) { + result += `**Change** ${index + 1}\n`; + } + result += `\`\`\`json\n${JSON.stringify(change, null, 2)}\n\`\`\`\n\n`; + }); + } + return result; +} diff --git a/tests/integration/adaptation-editor/tsconfig.eslint.json b/tests/integration/adaptation-editor/tsconfig.eslint.json index c05815726c9..6f1a2120c40 100644 --- a/tests/integration/adaptation-editor/tsconfig.eslint.json +++ b/tests/integration/adaptation-editor/tsconfig.eslint.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "include": ["src", "test", ".eslintrc.js", "playwright.config.ts"] + "include": ["src", "test", ".eslintrc.js", "playwright.config.ts", "manual-test-case-reporter.ts"] } diff --git a/tests/integration/adaptation-editor/tsconfig.json b/tests/integration/adaptation-editor/tsconfig.json index 640f50bd6a3..f80b191cd65 100644 --- a/tests/integration/adaptation-editor/tsconfig.json +++ b/tests/integration/adaptation-editor/tsconfig.json @@ -1,8 +1,8 @@ { "extends": "../../../tsconfig.json", "include": [ - "src", - "src/**/*.json" + "src/**/*.ts", + "src/**/*.json" ], "compilerOptions": { "rootDir": "src",