generated from SAP/repository-template
-
Notifications
You must be signed in to change notification settings - Fork 50
Generate Manual test case for ADP Test #3602
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
77057cf
generate manual test case
Jimmy-Joseph19 dfc5548
fix: feedback
Jimmy-Joseph19 f023ec0
fix: missing step
Jimmy-Joseph19 b721a8a
fix: lint
Jimmy-Joseph19 4e74dcc
mege: main into generateManualTestCase
Jimmy-Joseph19 41a1bc5
fix: issue
Jimmy-Joseph19 c2abd60
fix: issue2
Jimmy-Joseph19 8b64237
generate project configuration file
Jimmy-Joseph19 0c1c328
Linting auto fix commit
github-actions[bot] fab4c99
failing test
Jimmy-Joseph19 a89e5a6
fix: generated manual test
Jimmy-Joseph19 0dcfec9
fix: generated manual test 2
Jimmy-Joseph19 d4e6ded
Merge branch 'main' into generateManualTestCase
Jimmy-Joseph19 8b9c775
Merge branch 'main' into generateManualTestCase
Jimmy-Joseph19 8dbf21c
update plazwright and use new describe api of locator
Jimmy-Joseph19 f53ef7d
remove comment
Jimmy-Joseph19 f0b0978
Merge branch 'main' into generateManualTestCase
Jimmy-Joseph19 13086b4
Linting auto fix commit
github-actions[bot] 66a01b4
fix remove redundant describe
Jimmy-Joseph19 f5137a8
Merge branch 'main' into generateManualTestCase
Jimmy-Joseph19 de73682
Merge branch 'main' into generateManualTestCase
Klaus-Keller 6199f74
Merge branch 'main' into generateManualTestCase
Jimmy-Joseph19 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,334 @@ | ||
import { writeFile, mkdir } from 'fs/promises'; | ||
import { join, basename } from 'path'; | ||
import { existsSync } 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[]; | ||
} | ||
|
||
export default class ManualTestCaseReporter implements Reporter { | ||
private manualTestCases: Record<string, ManualTestCase> = {}; | ||
private config: FullConfig; | ||
private fileTotalTests: Record<string, number> = {}; | ||
private fileCompletedTests: Record<string, number> = {}; | ||
// only for latest ui5 reporter is enabled | ||
private isReporterDisabled = false; | ||
private createEmptyManualTestCase(test: TestCase): ManualTestCase { | ||
return { | ||
name: test.title, | ||
filePath: '', | ||
steps: [] | ||
}; | ||
} | ||
|
||
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; | ||
} | ||
}); | ||
} | ||
|
||
onStepBegin(test: TestCase, result: TestResult, step: TestStep): void { | ||
if (this.isReporterDisabled) return; | ||
const skipPatterns = [ | ||
/^Before Hooks$/, | ||
/^After Hooks$/, | ||
/^fixture: /, | ||
/^attach "/, | ||
/^browserType\./, | ||
/^browser\./, | ||
/^browserContext\./, | ||
/^expect\.toBeVisible$/, | ||
/^expect\.toBe$/, | ||
/^expect\.toEqual$/, | ||
/^expect\.poll\.toEqual$/, | ||
/^expect\.toBeEnabled$/, | ||
/^expect\.toBeDisabled$/, | ||
/^expect\.poll$/, | ||
/^locator\.textContent/, | ||
/^locator\.click\(iframe.*internal:control=enter-frame.*\)$/, | ||
/^locator\.hover\(iframe.*internal:control=enter-frame.*\)$/, | ||
/^locator\.fill\(iframe.*internal:control=enter-frame.*\)$/, | ||
/^Click on in Application Preview$/, | ||
/^locator\.getByTestId\(\'saved-changes-stack\'\)/, | ||
/^Verifying Changes.../ | ||
]; | ||
|
||
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 }); | ||
} | ||
} | ||
} | ||
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); | ||
} | ||
} | ||
|
||
/** | ||
* Called when all tests have finished. | ||
* | ||
* @param result - Result of the entire test run | ||
*/ | ||
async onEnd(result: FullResult) { | ||
if (this.isReporterDisabled) return; | ||
} | ||
|
||
/** | ||
* 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<void> { | ||
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 += `<a id="${anchorName}"></a>\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<void> { | ||
// Check if all tests for this file are complete and generate documentation if they are | ||
if (this.isFileComplete(testFile)) { | ||
await this.generateFileDocumentation(testFile); | ||
} | ||
} | ||
|
||
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) ? require('fs').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; | ||
} | ||
} | ||
} | ||
|
||
function parseActionStep(stepTitle: string): string { | ||
// Action mapping - common Playwright methods to human verbs | ||
const actionMap: Record<string, string> = { | ||
'click': 'Click on', | ||
'hover': 'Hover over' | ||
}; | ||
|
||
// Element type mapping - detect element types from selectors/roles | ||
const elementMap: Record<string, string> = { | ||
'button': 'button' | ||
}; | ||
|
||
// Try to find action verb from the main step title | ||
let action = ''; | ||
for (const [actionKey, actionVerb] of Object.entries(actionMap)) { | ||
if (stepTitle.includes(`.${actionKey}`) || stepTitle.startsWith(actionKey)) { | ||
action = actionVerb; | ||
break; | ||
} | ||
} | ||
|
||
let element = ''; | ||
const getByMethodMap: Record<string, string> = { | ||
'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<string, RegExp> = { | ||
'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 | ||
const resultBuilders = [ | ||
() => (action && element && name ? `${action} ${element} \`${name}\`` : null), | ||
() => (action && element ? `${action} ${element}` : null), | ||
() => (action && name ? `${action}\`${name}\`` : null), | ||
() => (action ? action : null), | ||
() => stepTitle | ||
]; | ||
|
||
let result = ``; | ||
for (const builder of resultBuilders) { | ||
const buildResult = builder(); | ||
if (buildResult) { | ||
result = buildResult; | ||
break; | ||
} | ||
} | ||
return result; | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.