create-appraisejs 0.1.9 → 0.1.10-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -17
- package/dist/cli.d.ts +2 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.e2e.test.js +11 -8
- package/dist/cli.e2e.test.js.map +1 -1
- package/dist/cli.js +32 -48
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/config.test.js +9 -5
- package/dist/config.test.js.map +1 -1
- package/dist/copy-template.d.ts +1 -1
- package/dist/copy-template.d.ts.map +1 -1
- package/dist/copy-template.js +7 -3
- package/dist/copy-template.js.map +1 -1
- package/dist/copy-template.test.js +14 -9
- package/dist/copy-template.test.js.map +1 -1
- package/dist/create-project.d.ts +23 -0
- package/dist/create-project.d.ts.map +1 -0
- package/dist/create-project.js +58 -0
- package/dist/create-project.js.map +1 -0
- package/dist/create-project.test.d.ts +2 -0
- package/dist/create-project.test.d.ts.map +1 -0
- package/dist/create-project.test.js +80 -0
- package/dist/create-project.test.js.map +1 -0
- package/dist/install.d.ts +8 -4
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +22 -72
- package/dist/install.js.map +1 -1
- package/dist/install.test.js +26 -10
- package/dist/install.test.js.map +1 -1
- package/dist/package-manager.d.ts +11 -0
- package/dist/package-manager.d.ts.map +1 -0
- package/dist/package-manager.js +47 -0
- package/dist/package-manager.js.map +1 -0
- package/dist/package-manager.test.d.ts +2 -0
- package/dist/package-manager.test.d.ts.map +1 -0
- package/dist/package-manager.test.js +51 -0
- package/dist/package-manager.test.js.map +1 -0
- package/dist/prepare-template-utils.d.ts +10 -0
- package/dist/prepare-template-utils.d.ts.map +1 -0
- package/dist/prepare-template-utils.js +53 -0
- package/dist/prepare-template-utils.js.map +1 -0
- package/dist/prepare-template-utils.test.d.ts +2 -0
- package/dist/prepare-template-utils.test.d.ts.map +1 -0
- package/dist/prepare-template-utils.test.js +67 -0
- package/dist/prepare-template-utils.test.js.map +1 -0
- package/dist/prompts.d.ts +2 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +11 -3
- package/dist/prompts.js.map +1 -1
- package/dist/prompts.test.js +17 -7
- package/dist/prompts.test.js.map +1 -1
- package/dist/template-sync-utils.test.d.ts +2 -0
- package/dist/template-sync-utils.test.d.ts.map +1 -0
- package/dist/template-sync-utils.test.js +41 -0
- package/dist/template-sync-utils.test.js.map +1 -0
- package/package.json +3 -2
- package/templates/default/.appraise-template-meta.json +5 -0
- package/templates/default/.env.example +1 -1
- package/templates/default/.vscode/settings.json +10 -3
- package/templates/default/README.md +27 -25
- package/templates/default/automation/features/base/login.feature +15 -0
- package/templates/default/automation/locators/base/home.json +3 -0
- package/templates/default/automation/locators/base/login.json +6 -0
- package/templates/default/automation/locators/base/test.json +1 -0
- package/templates/default/automation/mapping/locator-map.json +14 -0
- package/templates/default/{src/tests → automation}/steps/actions/click.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/actions/hover.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/actions/input.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/actions/navigation.step.ts +1 -3
- package/templates/default/{src/tests → automation}/steps/actions/random_data.step.ts +1 -3
- package/templates/default/{src/tests → automation}/steps/actions/store.step.ts +1 -4
- package/templates/default/automation/steps/actions/wait.step.ts +91 -0
- package/templates/default/{src/tests → automation}/steps/validations/active_state_assertion.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/validations/navigation_assertion.step.ts +1 -2
- package/templates/default/{src/tests → automation}/steps/validations/text_assertion.step.ts +1 -4
- package/templates/default/{src/tests → automation}/steps/validations/visibility_assertion.step.ts +1 -4
- package/templates/default/cucumber.mjs +6 -6
- package/templates/default/eslint.config.mjs +5 -4
- package/templates/default/package-lock.json +322 -485
- package/templates/default/package.json +11 -6
- package/templates/default/packages/cucumber-runtime/package.json +13 -0
- package/templates/default/packages/cucumber-runtime/src/cache.util.ts +93 -0
- package/templates/default/packages/cucumber-runtime/src/cli.ts +68 -0
- package/templates/default/packages/cucumber-runtime/src/environment.util.ts +21 -0
- package/templates/default/packages/cucumber-runtime/src/executor.ts +32 -0
- package/templates/default/{src/tests/hooks → packages/cucumber-runtime/src}/hooks.ts +17 -32
- package/templates/default/packages/cucumber-runtime/src/index.ts +17 -0
- package/templates/default/{src/tests/utils → packages/cucumber-runtime/src}/locator.util.ts +50 -64
- package/templates/default/packages/cucumber-runtime/src/parameter-types.ts +7 -0
- package/templates/default/packages/cucumber-runtime/src/paths.ts +33 -0
- package/templates/default/packages/cucumber-runtime/src/random-data.util.ts +35 -0
- package/templates/default/packages/cucumber-runtime/src/types.ts +13 -0
- package/templates/default/{src/tests/config/executor → packages/cucumber-runtime/src}/world.ts +4 -1
- package/templates/default/packages/cucumber-runtime/tsconfig.json +11 -0
- package/templates/default/scripts/setup-env.ts +4 -4
- package/templates/default/scripts/sync-appraise-base-template.ts +123 -105
- package/templates/default/scripts/sync-environments.ts +8 -5
- package/templates/default/scripts/sync-locator-groups.ts +7 -10
- package/templates/default/scripts/sync-locators.ts +5 -9
- package/templates/default/scripts/sync-modules.ts +9 -17
- package/templates/default/scripts/sync-tags.ts +2 -2
- package/templates/default/scripts/sync-template-step-groups.ts +16 -6
- package/templates/default/scripts/sync-template-steps.ts +16 -5
- package/templates/default/scripts/sync-test-cases.ts +6 -3
- package/templates/default/scripts/sync-test-suites.ts +7 -4
- package/templates/default/src/actions/environments/environment-actions.ts +6 -23
- package/templates/default/src/actions/locator/locator-actions.ts +36 -93
- package/templates/default/src/actions/locator-groups/locator-group-actions.ts +24 -78
- package/templates/default/src/actions/modules/module-actions.ts +4 -2
- package/templates/default/src/actions/tags/tag-actions.ts +4 -1
- package/templates/default/src/actions/template-step/template-step-actions.ts +10 -101
- package/templates/default/src/actions/template-step-group/template-step-group-actions.ts +31 -130
- package/templates/default/src/actions/test-case/test-case-actions.ts +31 -94
- package/templates/default/src/actions/test-run/test-run-actions.ts +11 -13
- package/templates/default/src/actions/test-suite/test-suite-actions.ts +29 -82
- package/templates/default/src/app/(base)/locator-groups/page.tsx +1 -3
- package/templates/default/src/app/(base)/reports/page.tsx +1 -1
- package/templates/default/src/app/(base)/reports/test-cases/page.tsx +2 -2
- package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table-columns.tsx +1 -1
- package/templates/default/src/app/(base)/tags/page.tsx +2 -2
- package/templates/default/src/app/(base)/template-steps/page.tsx +1 -2
- package/templates/default/src/app/(base)/test-runs/page.tsx +2 -2
- package/templates/default/src/app/api/test-runs/[runId]/logs/route.ts +2 -1
- package/templates/default/src/app/api/test-runs/[runId]/trace/[testCaseId]/route.ts +2 -1
- package/templates/default/src/app/page.tsx +4 -5
- package/templates/default/src/components/diagram/dynamic-parameters.tsx +76 -40
- package/templates/default/src/components/diagram/options-header-node.tsx +1 -1
- package/templates/default/src/components/ui/data-table.tsx +33 -39
- package/templates/default/src/lib/automation/paths.ts +181 -0
- package/templates/default/src/lib/automation/projection-service.ts +230 -0
- package/templates/default/src/lib/environment-file-utils.ts +14 -51
- package/templates/default/src/lib/executor/local-executor-adapter.ts +101 -0
- package/templates/default/src/lib/executor/types.ts +24 -0
- package/templates/default/src/lib/feature-file-generator.ts +22 -112
- package/templates/default/src/lib/locator-group-file-utils.ts +57 -120
- package/templates/default/src/lib/process/task-spawner.ts +236 -0
- package/templates/default/src/lib/template-sync-utils.d.ts +7 -0
- package/templates/default/src/lib/template-sync-utils.d.ts.map +1 -0
- package/templates/default/src/lib/template-sync-utils.js +47 -0
- package/templates/default/src/lib/template-sync-utils.js.map +1 -0
- package/templates/default/src/lib/template-sync-utils.ts +63 -0
- package/templates/default/src/lib/test-run/process-manager.ts +9 -87
- package/templates/default/src/lib/test-run/test-run-executor.ts +7 -136
- package/templates/default/src/lib/test-run/winston-logger.ts +6 -35
- package/templates/default/src/lib/utils/template-step-file-generator.ts +22 -85
- package/templates/default/src/lib/utils/template-step-file-manager-intelligent.ts +7 -22
- package/templates/default/public/favicon.ico +0 -0
- package/templates/default/src/tests/executor.ts +0 -80
- package/templates/default/src/tests/mapping/locator-map.json +0 -1
- package/templates/default/src/tests/steps/actions/wait.step.ts +0 -107
- package/templates/default/src/tests/support/parameter-types.ts +0 -12
- package/templates/default/src/tests/utils/cache.util.ts +0 -260
- package/templates/default/src/tests/utils/cli.util.ts +0 -177
- package/templates/default/src/tests/utils/environment.util.ts +0 -65
- package/templates/default/src/tests/utils/random-data.util.ts +0 -45
- package/templates/default/src/tests/utils/spawner.util.ts +0 -617
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { join } from 'path'
|
|
2
|
+
import { spawnTask, taskSpawner, type SpawnedProcess, waitForTask, killTask } from '@/lib/process/task-spawner'
|
|
3
|
+
import { ensureAutomationWorkspaceReady, getAutomationReportsDir } from '@/lib/automation/paths'
|
|
4
|
+
import type { ExecutorAdapter, TestRunExecutionRequest, TestRunExecutionResult } from './types'
|
|
5
|
+
import { processManager } from '@/lib/test-run/process-manager'
|
|
6
|
+
|
|
7
|
+
function mapBrowserEngineToName(browserEngine: TestRunExecutionRequest['browserEngine']): 'chromium' | 'firefox' | 'webkit' {
|
|
8
|
+
switch (browserEngine) {
|
|
9
|
+
case 'CHROMIUM':
|
|
10
|
+
return 'chromium'
|
|
11
|
+
case 'FIREFOX':
|
|
12
|
+
return 'firefox'
|
|
13
|
+
case 'WEBKIT':
|
|
14
|
+
return 'webkit'
|
|
15
|
+
default:
|
|
16
|
+
return 'chromium'
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function combineTagExpressions(tags: TestRunExecutionRequest['tags']): string | null {
|
|
21
|
+
if (tags.length === 0) {
|
|
22
|
+
return null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (tags.length === 1) {
|
|
26
|
+
return tags[0].tagExpression
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return tags.map(tag => `(${tag.tagExpression})`).join(' or ')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function generateReportPath(testRunId: string): string {
|
|
33
|
+
const timestamp = Date.now()
|
|
34
|
+
return join(getAutomationReportsDir(), `cucumber-${testRunId}-${timestamp}.json`)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class LocalExecutorAdapter implements ExecutorAdapter {
|
|
38
|
+
async executeTestRun(config: TestRunExecutionRequest): Promise<TestRunExecutionResult> {
|
|
39
|
+
await ensureAutomationWorkspaceReady()
|
|
40
|
+
|
|
41
|
+
const { testRunId, environment, tags, testWorkersCount, browserEngine, headless = true } = config
|
|
42
|
+
const reportPath = generateReportPath(testRunId)
|
|
43
|
+
const browserName = mapBrowserEngineToName(browserEngine)
|
|
44
|
+
|
|
45
|
+
process.env.ENVIRONMENT = environment.name
|
|
46
|
+
process.env.HEADLESS = headless.toString()
|
|
47
|
+
process.env.BROWSER = browserName
|
|
48
|
+
process.env.REPORT_PATH = reportPath
|
|
49
|
+
|
|
50
|
+
const cucumberArgs: string[] = ['cucumber-js']
|
|
51
|
+
const tagExpression = combineTagExpressions(tags)
|
|
52
|
+
|
|
53
|
+
if (tagExpression) {
|
|
54
|
+
cucumberArgs.push('-t', tagExpression)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (testWorkersCount > 1) {
|
|
58
|
+
cucumberArgs.push('--parallel', testWorkersCount.toString())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const spawnedProcess = await spawnTask('npx', cucumberArgs, {
|
|
62
|
+
streamLogs: true,
|
|
63
|
+
prefixLogs: true,
|
|
64
|
+
logPrefix: `test-run-${testRunId}`,
|
|
65
|
+
captureOutput: true,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
processManager.register(testRunId, spawnedProcess)
|
|
69
|
+
spawnedProcess.process.on('exit', () => {
|
|
70
|
+
processManager.unregister(testRunId)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
process: spawnedProcess,
|
|
75
|
+
reportPath,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
waitForProcess(processName: string): Promise<number | null> {
|
|
80
|
+
return waitForTask(processName)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
killProcess(processName: string, signal?: NodeJS.Signals): boolean {
|
|
84
|
+
return killTask(processName, signal)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getProcess(processName: string): SpawnedProcess | undefined {
|
|
88
|
+
return taskSpawner.getProcess(processName)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
spawnTraceViewer(testCaseId: string, tracePath: string): Promise<SpawnedProcess> {
|
|
92
|
+
return taskSpawner.spawn('npx', ['playwright', 'show-trace', tracePath], {
|
|
93
|
+
streamLogs: true,
|
|
94
|
+
prefixLogs: true,
|
|
95
|
+
logPrefix: `trace-viewer-${testCaseId}`,
|
|
96
|
+
captureOutput: false,
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export const localExecutorAdapter = new LocalExecutorAdapter()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { BrowserEngine, Environment, Tag } from '@prisma/client'
|
|
2
|
+
import type { SpawnedProcess } from '@/lib/process/task-spawner'
|
|
3
|
+
|
|
4
|
+
export interface TestRunExecutionRequest {
|
|
5
|
+
testRunId: string
|
|
6
|
+
environment: Environment
|
|
7
|
+
tags: Tag[]
|
|
8
|
+
testWorkersCount: number
|
|
9
|
+
browserEngine: BrowserEngine
|
|
10
|
+
headless?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface TestRunExecutionResult {
|
|
14
|
+
process: SpawnedProcess
|
|
15
|
+
reportPath: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ExecutorAdapter {
|
|
19
|
+
executeTestRun(config: TestRunExecutionRequest): Promise<TestRunExecutionResult>
|
|
20
|
+
waitForProcess(processName: string): Promise<number | null>
|
|
21
|
+
killProcess(processName: string, signal?: NodeJS.Signals): boolean
|
|
22
|
+
getProcess(processName: string): SpawnedProcess | undefined
|
|
23
|
+
spawnTraceViewer(testCaseId: string, tracePath: string): Promise<SpawnedProcess>
|
|
24
|
+
}
|
|
@@ -2,68 +2,41 @@ import { promises as fs } from 'fs'
|
|
|
2
2
|
import { join, dirname } from 'path'
|
|
3
3
|
import prisma from '@/config/db-config'
|
|
4
4
|
import { buildModulePath } from '@/lib/path-helpers/module-path'
|
|
5
|
+
import { ensureAutomationWorkspaceReady, getAutomationFeaturesDir } from '@/lib/automation/paths'
|
|
5
6
|
|
|
6
|
-
/**
|
|
7
|
-
* Checks if a directory is empty (no files or subdirectories)
|
|
8
|
-
* @param dirPath - Path to the directory to check
|
|
9
|
-
* @returns Promise<boolean> - True if directory is empty, false otherwise
|
|
10
|
-
*/
|
|
11
7
|
async function isDirectoryEmpty(dirPath: string): Promise<boolean> {
|
|
12
8
|
try {
|
|
13
9
|
const entries = await fs.readdir(dirPath)
|
|
14
10
|
return entries.length === 0
|
|
15
|
-
} catch
|
|
16
|
-
// If directory doesn't exist or can't be read, consider it empty
|
|
17
|
-
console.warn(`Could not read directory ${dirPath}:`, error)
|
|
11
|
+
} catch {
|
|
18
12
|
return true
|
|
19
13
|
}
|
|
20
14
|
}
|
|
21
15
|
|
|
22
|
-
/**
|
|
23
|
-
* Removes empty directories up the hierarchy until a non-empty directory is found
|
|
24
|
-
* @param dirPath - Starting directory path to clean up
|
|
25
|
-
* @param basePath - Base path to stop cleaning (e.g., features directory)
|
|
26
|
-
* @returns Promise<void>
|
|
27
|
-
*/
|
|
28
16
|
async function removeEmptyDirectoriesUp(dirPath: string, basePath: string): Promise<void> {
|
|
29
17
|
let currentPath = dirPath
|
|
30
18
|
|
|
31
|
-
// Keep going up the directory tree until we reach the base path
|
|
32
19
|
while (currentPath !== basePath && currentPath !== dirname(currentPath)) {
|
|
33
20
|
try {
|
|
34
|
-
// Check if current directory is empty
|
|
35
21
|
if (await isDirectoryEmpty(currentPath)) {
|
|
36
22
|
await fs.rmdir(currentPath)
|
|
37
|
-
console.log(`Removed empty directory: ${currentPath}`)
|
|
38
|
-
// Move up one level
|
|
39
23
|
currentPath = dirname(currentPath)
|
|
40
24
|
} else {
|
|
41
|
-
// Directory is not empty, stop cleaning
|
|
42
25
|
break
|
|
43
26
|
}
|
|
44
|
-
} catch
|
|
45
|
-
// If we can't remove the directory or it doesn't exist, stop
|
|
46
|
-
console.warn(`Could not remove directory ${currentPath}:`, error)
|
|
27
|
+
} catch {
|
|
47
28
|
break
|
|
48
29
|
}
|
|
49
30
|
}
|
|
50
31
|
}
|
|
51
32
|
|
|
52
|
-
/**
|
|
53
|
-
* Generates a Gherkin feature file for a test suite
|
|
54
|
-
* @param testSuiteId - The ID of the test suite
|
|
55
|
-
* @param testSuiteName - The name of the test suite
|
|
56
|
-
* @param testSuiteDescription - The description of the test suite
|
|
57
|
-
* @param moduleName - The name of the module the test suite belongs to
|
|
58
|
-
* @returns Promise<string> - The path to the generated feature file
|
|
59
|
-
*/
|
|
60
33
|
export async function generateFeatureFile(
|
|
61
34
|
testSuiteId: string,
|
|
62
35
|
testSuiteName: string,
|
|
63
36
|
testSuiteDescription?: string,
|
|
64
37
|
): Promise<string> {
|
|
65
38
|
try {
|
|
66
|
-
|
|
39
|
+
await ensureAutomationWorkspaceReady()
|
|
67
40
|
const [testSuite, allModules] = await Promise.all([
|
|
68
41
|
prisma.testSuite.findUnique({
|
|
69
42
|
where: { id: testSuiteId },
|
|
@@ -85,35 +58,28 @@ export async function generateFeatureFile(
|
|
|
85
58
|
tags: true,
|
|
86
59
|
},
|
|
87
60
|
}),
|
|
88
|
-
prisma.module.findMany(),
|
|
61
|
+
prisma.module.findMany(),
|
|
89
62
|
])
|
|
90
63
|
|
|
91
64
|
if (!testSuite) {
|
|
92
65
|
throw new Error(`Test suite with ID ${testSuiteId} not found`)
|
|
93
66
|
}
|
|
94
67
|
|
|
95
|
-
// Build the module path for directory structure
|
|
96
68
|
const modulePath = buildModulePath(allModules, testSuite.module)
|
|
97
|
-
|
|
98
|
-
// Generate feature file content
|
|
99
69
|
const featureContent = generateFeatureContent(
|
|
100
|
-
testSuiteDescription || testSuiteName,
|
|
70
|
+
testSuiteDescription || testSuiteName,
|
|
101
71
|
testSuite.testCases,
|
|
102
72
|
testSuite.tags,
|
|
103
73
|
)
|
|
104
74
|
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
const moduleDir = join(featuresBaseDir, modulePath.substring(1)) // Remove leading slash
|
|
75
|
+
const featuresBaseDir = getAutomationFeaturesDir()
|
|
76
|
+
const moduleDir = join(featuresBaseDir, modulePath.substring(1))
|
|
108
77
|
await fs.mkdir(moduleDir, { recursive: true })
|
|
109
78
|
|
|
110
|
-
// Generate a safe filename from the test suite name only
|
|
111
79
|
const safeFileName = generateSafeFileName(testSuiteName)
|
|
112
80
|
const featureFilePath = join(moduleDir, `${safeFileName}.feature`)
|
|
113
81
|
|
|
114
|
-
// Write the feature file
|
|
115
82
|
await fs.writeFile(featureFilePath, featureContent, 'utf8')
|
|
116
|
-
|
|
117
83
|
return featureFilePath
|
|
118
84
|
} catch (error) {
|
|
119
85
|
console.error('Error generating feature file:', error)
|
|
@@ -121,9 +87,6 @@ export async function generateFeatureFile(
|
|
|
121
87
|
}
|
|
122
88
|
}
|
|
123
89
|
|
|
124
|
-
/**
|
|
125
|
-
* Generates the content for a Gherkin feature file
|
|
126
|
-
*/
|
|
127
90
|
function generateFeatureContent(
|
|
128
91
|
featureTitle: string,
|
|
129
92
|
testCases: Array<{
|
|
@@ -143,45 +106,35 @@ function generateFeatureContent(
|
|
|
143
106
|
): string {
|
|
144
107
|
const lines: string[] = []
|
|
145
108
|
|
|
146
|
-
// Warning header
|
|
147
109
|
lines.push('# AUTO-GENERATED FILE - DO NOT EDIT MANUALLY')
|
|
148
110
|
lines.push('# This file is automatically generated from Test Suite data.')
|
|
149
111
|
lines.push('# Any manual changes will be overwritten when the Test Suite is updated.')
|
|
150
112
|
lines.push('# To modify this feature, update the corresponding Test Suite in the application.')
|
|
151
113
|
lines.push('')
|
|
152
114
|
|
|
153
|
-
// Add feature-level tags (one tag per line)
|
|
154
115
|
if (testSuiteTags && testSuiteTags.length > 0) {
|
|
155
116
|
testSuiteTags.forEach(tag => {
|
|
156
117
|
lines.push(tag.tagExpression)
|
|
157
118
|
})
|
|
158
119
|
}
|
|
159
120
|
|
|
160
|
-
// Feature header - use description as feature title
|
|
161
121
|
lines.push(`Feature: ${featureTitle}`)
|
|
162
122
|
lines.push('')
|
|
163
123
|
|
|
164
|
-
// Get test suite tag expressions for deduplication
|
|
165
124
|
const testSuiteTagExpressions = new Set((testSuiteTags || []).map(tag => tag.tagExpression.toLowerCase()))
|
|
166
|
-
|
|
167
|
-
// Generate scenarios for each test case that has steps
|
|
168
125
|
let scenarioCount = 0
|
|
126
|
+
|
|
169
127
|
testCases.forEach(testCase => {
|
|
170
|
-
// Only generate scenario if test case has steps
|
|
171
128
|
if (testCase.steps && testCase.steps.length > 0) {
|
|
172
|
-
// Generate Gherkin steps from test case steps
|
|
173
129
|
const gherkinSteps = generateGherkinStepsFromTestCase(testCase.steps)
|
|
174
130
|
|
|
175
|
-
// Only add scenario if there are actual gherkin steps
|
|
176
131
|
if (gherkinSteps.length > 0) {
|
|
177
132
|
if (scenarioCount > 0) {
|
|
178
|
-
lines.push('')
|
|
133
|
+
lines.push('')
|
|
179
134
|
}
|
|
180
135
|
|
|
181
|
-
// Add scenario-level tags (skip if already present at feature level)
|
|
182
136
|
if (testCase.tags && testCase.tags.length > 0) {
|
|
183
137
|
testCase.tags.forEach(tag => {
|
|
184
|
-
// Only add tag if it's not already present at feature level
|
|
185
138
|
if (!testSuiteTagExpressions.has(tag.tagExpression.toLowerCase())) {
|
|
186
139
|
lines.push(` ${tag.tagExpression}`)
|
|
187
140
|
}
|
|
@@ -189,11 +142,9 @@ function generateFeatureContent(
|
|
|
189
142
|
}
|
|
190
143
|
|
|
191
144
|
lines.push(` Scenario: [${testCase.title}] ${testCase.description}`)
|
|
192
|
-
|
|
193
145
|
gherkinSteps.forEach(step => {
|
|
194
146
|
lines.push(` ${step}`)
|
|
195
147
|
})
|
|
196
|
-
|
|
197
148
|
scenarioCount++
|
|
198
149
|
}
|
|
199
150
|
}
|
|
@@ -202,9 +153,6 @@ function generateFeatureContent(
|
|
|
202
153
|
return lines.join('\n') + '\n'
|
|
203
154
|
}
|
|
204
155
|
|
|
205
|
-
/**
|
|
206
|
-
* Generates Gherkin steps from test case steps using the same logic as the frontend
|
|
207
|
-
*/
|
|
208
156
|
function generateGherkinStepsFromTestCase(
|
|
209
157
|
steps: Array<{
|
|
210
158
|
gherkinStep: string
|
|
@@ -215,9 +163,7 @@ function generateGherkinStepsFromTestCase(
|
|
|
215
163
|
return []
|
|
216
164
|
}
|
|
217
165
|
|
|
218
|
-
// Sort steps by order
|
|
219
166
|
const sortedSteps = steps.sort((a, b) => a.order - b.order)
|
|
220
|
-
|
|
221
167
|
let hasThenInPrevious = false
|
|
222
168
|
let hasWhenInPrevious = false
|
|
223
169
|
|
|
@@ -227,55 +173,43 @@ function generateGherkinStepsFromTestCase(
|
|
|
227
173
|
const hasGherkinKeyword = ['given', 'when', 'then', 'and', 'but'].includes(firstWord)
|
|
228
174
|
const stepWithoutKeyword = hasGherkinKeyword ? gherkinStep.split(' ').slice(1).join(' ') : gherkinStep
|
|
229
175
|
|
|
230
|
-
// First step always starts with Given
|
|
231
176
|
if (index === 0) {
|
|
232
177
|
return `Given ${stepWithoutKeyword}`
|
|
233
178
|
}
|
|
234
179
|
|
|
235
|
-
// Check if this step should be a Then statement
|
|
236
180
|
const isThenStatement =
|
|
237
181
|
firstWord === 'then' ||
|
|
238
182
|
stepWithoutKeyword.toLowerCase().startsWith('should') ||
|
|
239
183
|
stepWithoutKeyword.toLowerCase().startsWith('must') ||
|
|
240
184
|
stepWithoutKeyword.toLowerCase().startsWith('will')
|
|
241
185
|
|
|
242
|
-
// If we haven't seen a Then yet
|
|
243
186
|
if (!hasThenInPrevious) {
|
|
244
|
-
// If this is a Then statement
|
|
245
187
|
if (isThenStatement) {
|
|
246
188
|
hasThenInPrevious = true
|
|
247
189
|
return `Then ${stepWithoutKeyword}`
|
|
248
190
|
}
|
|
249
191
|
|
|
250
|
-
// If we haven't seen a When yet, use When
|
|
251
192
|
if (!hasWhenInPrevious) {
|
|
252
193
|
hasWhenInPrevious = true
|
|
253
194
|
return `When ${stepWithoutKeyword}`
|
|
254
195
|
}
|
|
255
|
-
|
|
196
|
+
|
|
256
197
|
return `And ${stepWithoutKeyword}`
|
|
257
198
|
}
|
|
258
199
|
|
|
259
|
-
// After Then
|
|
260
200
|
if (isThenStatement) {
|
|
261
|
-
// If it's another Then statement, use And
|
|
262
201
|
return `And ${stepWithoutKeyword}`
|
|
263
202
|
}
|
|
264
|
-
|
|
203
|
+
|
|
265
204
|
hasThenInPrevious = false
|
|
266
205
|
hasWhenInPrevious = true
|
|
267
206
|
return `When ${stepWithoutKeyword}`
|
|
268
207
|
})
|
|
269
208
|
}
|
|
270
209
|
|
|
271
|
-
/**
|
|
272
|
-
* Deletes a feature file for a test suite
|
|
273
|
-
* @param testSuiteId - The ID of the test suite
|
|
274
|
-
* @returns Promise<boolean> - True if file was deleted, false if file didn't exist
|
|
275
|
-
*/
|
|
276
210
|
export async function deleteFeatureFile(testSuiteId: string): Promise<boolean> {
|
|
277
211
|
try {
|
|
278
|
-
|
|
212
|
+
await ensureAutomationWorkspaceReady()
|
|
279
213
|
const [testSuite, allModules] = await Promise.all([
|
|
280
214
|
prisma.testSuite.findUnique({
|
|
281
215
|
where: { id: testSuiteId },
|
|
@@ -287,29 +221,22 @@ export async function deleteFeatureFile(testSuiteId: string): Promise<boolean> {
|
|
|
287
221
|
])
|
|
288
222
|
|
|
289
223
|
if (!testSuite) {
|
|
290
|
-
console.warn(`Test suite with ID ${testSuiteId} not found for feature file deletion`)
|
|
291
224
|
return false
|
|
292
225
|
}
|
|
293
226
|
|
|
294
|
-
// Build the module path for directory structure
|
|
295
227
|
const modulePath = buildModulePath(allModules, testSuite.module)
|
|
296
228
|
const safeFileName = generateSafeFileName(testSuite.name)
|
|
297
229
|
|
|
298
|
-
const featuresBaseDir =
|
|
299
|
-
const moduleDir = join(featuresBaseDir, modulePath.substring(1))
|
|
230
|
+
const featuresBaseDir = getAutomationFeaturesDir()
|
|
231
|
+
const moduleDir = join(featuresBaseDir, modulePath.substring(1))
|
|
300
232
|
const featureFilePath = join(moduleDir, `${safeFileName}.feature`)
|
|
301
233
|
|
|
302
234
|
try {
|
|
303
235
|
await fs.unlink(featureFilePath)
|
|
304
|
-
console.log(`Feature file deleted: ${featureFilePath}`)
|
|
305
|
-
|
|
306
|
-
// Clean up empty directories up the module hierarchy
|
|
307
236
|
await removeEmptyDirectoriesUp(moduleDir, featuresBaseDir)
|
|
308
|
-
|
|
309
237
|
return true
|
|
310
238
|
} catch (error: unknown) {
|
|
311
239
|
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
|
|
312
|
-
console.warn(`Feature file not found for deletion: ${featureFilePath}`)
|
|
313
240
|
return false
|
|
314
241
|
}
|
|
315
242
|
throw error
|
|
@@ -320,24 +247,19 @@ export async function deleteFeatureFile(testSuiteId: string): Promise<boolean> {
|
|
|
320
247
|
}
|
|
321
248
|
}
|
|
322
249
|
|
|
323
|
-
/**
|
|
324
|
-
* Regenerates all feature files from the current database state
|
|
325
|
-
* This is useful after merging changes or database migrations to ensure sync
|
|
326
|
-
* @returns Promise<string[]> - Array of generated feature file paths
|
|
327
|
-
*/
|
|
328
250
|
export async function regenerateAllFeatureFiles(): Promise<string[]> {
|
|
329
251
|
try {
|
|
330
|
-
|
|
252
|
+
await ensureAutomationWorkspaceReady()
|
|
253
|
+
const featuresBaseDir = getAutomationFeaturesDir()
|
|
331
254
|
|
|
332
|
-
// Clear existing feature files directory
|
|
333
|
-
const featuresBaseDir = join(process.cwd(), 'src', 'tests', 'features')
|
|
334
255
|
try {
|
|
335
256
|
await fs.rm(featuresBaseDir, { recursive: true, force: true })
|
|
336
257
|
} catch (error) {
|
|
337
258
|
console.warn('Could not clear features directory:', error)
|
|
338
259
|
}
|
|
339
260
|
|
|
340
|
-
|
|
261
|
+
await fs.mkdir(featuresBaseDir, { recursive: true })
|
|
262
|
+
|
|
341
263
|
const testSuites = await prisma.testSuite.findMany({
|
|
342
264
|
include: {
|
|
343
265
|
testCases: {
|
|
@@ -358,12 +280,9 @@ export async function regenerateAllFeatureFiles(): Promise<string[]> {
|
|
|
358
280
|
},
|
|
359
281
|
})
|
|
360
282
|
|
|
361
|
-
// Fetch all modules for path building
|
|
362
283
|
const allModules = await prisma.module.findMany()
|
|
363
|
-
|
|
364
284
|
const generatedFiles: string[] = []
|
|
365
285
|
|
|
366
|
-
// Generate feature file for each test suite
|
|
367
286
|
for (const testSuite of testSuites) {
|
|
368
287
|
try {
|
|
369
288
|
const modulePath = buildModulePath(allModules, testSuite.module)
|
|
@@ -373,24 +292,19 @@ export async function regenerateAllFeatureFiles(): Promise<string[]> {
|
|
|
373
292
|
testSuite.tags,
|
|
374
293
|
)
|
|
375
294
|
|
|
376
|
-
|
|
377
|
-
const moduleDir = join(featuresBaseDir, modulePath.substring(1)) // Remove leading slash
|
|
295
|
+
const moduleDir = join(featuresBaseDir, modulePath.substring(1))
|
|
378
296
|
await fs.mkdir(moduleDir, { recursive: true })
|
|
379
297
|
|
|
380
|
-
// Generate filename and write file
|
|
381
298
|
const safeFileName = generateSafeFileName(testSuite.name)
|
|
382
299
|
const featureFilePath = join(moduleDir, `${safeFileName}.feature`)
|
|
383
300
|
|
|
384
301
|
await fs.writeFile(featureFilePath, featureContent, 'utf8')
|
|
385
302
|
generatedFiles.push(featureFilePath)
|
|
386
|
-
|
|
387
|
-
console.log(`Generated: ${featureFilePath}`)
|
|
388
303
|
} catch (error) {
|
|
389
304
|
console.error(`Error generating feature file for test suite ${testSuite.name}:`, error)
|
|
390
305
|
}
|
|
391
306
|
}
|
|
392
307
|
|
|
393
|
-
console.log(`Regeneration complete. Generated ${generatedFiles.length} feature files.`)
|
|
394
308
|
return generatedFiles
|
|
395
309
|
} catch (error) {
|
|
396
310
|
console.error('Error during feature files regeneration:', error)
|
|
@@ -398,14 +312,10 @@ export async function regenerateAllFeatureFiles(): Promise<string[]> {
|
|
|
398
312
|
}
|
|
399
313
|
}
|
|
400
314
|
|
|
401
|
-
/**
|
|
402
|
-
* Generates a safe filename from test suite name
|
|
403
|
-
*/
|
|
404
315
|
function generateSafeFileName(testSuiteName: string): string {
|
|
405
|
-
// Convert to lowercase and replace spaces and special characters with hyphens
|
|
406
316
|
return testSuiteName
|
|
407
317
|
.toLowerCase()
|
|
408
318
|
.replace(/[^a-z0-9]+/g, '-')
|
|
409
|
-
.replace(/^-+|-+$/g, '')
|
|
410
|
-
.replace(/-+/g, '-')
|
|
319
|
+
.replace(/^-+|-+$/g, '')
|
|
320
|
+
.replace(/-+/g, '-')
|
|
411
321
|
}
|