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.
Files changed (159) hide show
  1. package/README.md +24 -17
  2. package/dist/cli.d.ts +2 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.e2e.test.js +11 -8
  5. package/dist/cli.e2e.test.js.map +1 -1
  6. package/dist/cli.js +32 -48
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +5 -1
  10. package/dist/config.js.map +1 -1
  11. package/dist/config.test.js +9 -5
  12. package/dist/config.test.js.map +1 -1
  13. package/dist/copy-template.d.ts +1 -1
  14. package/dist/copy-template.d.ts.map +1 -1
  15. package/dist/copy-template.js +7 -3
  16. package/dist/copy-template.js.map +1 -1
  17. package/dist/copy-template.test.js +14 -9
  18. package/dist/copy-template.test.js.map +1 -1
  19. package/dist/create-project.d.ts +23 -0
  20. package/dist/create-project.d.ts.map +1 -0
  21. package/dist/create-project.js +58 -0
  22. package/dist/create-project.js.map +1 -0
  23. package/dist/create-project.test.d.ts +2 -0
  24. package/dist/create-project.test.d.ts.map +1 -0
  25. package/dist/create-project.test.js +80 -0
  26. package/dist/create-project.test.js.map +1 -0
  27. package/dist/install.d.ts +8 -4
  28. package/dist/install.d.ts.map +1 -1
  29. package/dist/install.js +22 -72
  30. package/dist/install.js.map +1 -1
  31. package/dist/install.test.js +26 -10
  32. package/dist/install.test.js.map +1 -1
  33. package/dist/package-manager.d.ts +11 -0
  34. package/dist/package-manager.d.ts.map +1 -0
  35. package/dist/package-manager.js +47 -0
  36. package/dist/package-manager.js.map +1 -0
  37. package/dist/package-manager.test.d.ts +2 -0
  38. package/dist/package-manager.test.d.ts.map +1 -0
  39. package/dist/package-manager.test.js +51 -0
  40. package/dist/package-manager.test.js.map +1 -0
  41. package/dist/prepare-template-utils.d.ts +10 -0
  42. package/dist/prepare-template-utils.d.ts.map +1 -0
  43. package/dist/prepare-template-utils.js +53 -0
  44. package/dist/prepare-template-utils.js.map +1 -0
  45. package/dist/prepare-template-utils.test.d.ts +2 -0
  46. package/dist/prepare-template-utils.test.d.ts.map +1 -0
  47. package/dist/prepare-template-utils.test.js +67 -0
  48. package/dist/prepare-template-utils.test.js.map +1 -0
  49. package/dist/prompts.d.ts +2 -0
  50. package/dist/prompts.d.ts.map +1 -1
  51. package/dist/prompts.js +11 -3
  52. package/dist/prompts.js.map +1 -1
  53. package/dist/prompts.test.js +17 -7
  54. package/dist/prompts.test.js.map +1 -1
  55. package/dist/template-sync-utils.test.d.ts +2 -0
  56. package/dist/template-sync-utils.test.d.ts.map +1 -0
  57. package/dist/template-sync-utils.test.js +41 -0
  58. package/dist/template-sync-utils.test.js.map +1 -0
  59. package/package.json +3 -2
  60. package/templates/default/.appraise-template-meta.json +5 -0
  61. package/templates/default/.env.example +1 -1
  62. package/templates/default/.vscode/settings.json +10 -3
  63. package/templates/default/README.md +27 -25
  64. package/templates/default/automation/features/base/login.feature +15 -0
  65. package/templates/default/automation/locators/base/home.json +3 -0
  66. package/templates/default/automation/locators/base/login.json +6 -0
  67. package/templates/default/automation/locators/base/test.json +1 -0
  68. package/templates/default/automation/mapping/locator-map.json +14 -0
  69. package/templates/default/{src/tests → automation}/steps/actions/click.step.ts +1 -4
  70. package/templates/default/{src/tests → automation}/steps/actions/hover.step.ts +1 -4
  71. package/templates/default/{src/tests → automation}/steps/actions/input.step.ts +1 -4
  72. package/templates/default/{src/tests → automation}/steps/actions/navigation.step.ts +1 -3
  73. package/templates/default/{src/tests → automation}/steps/actions/random_data.step.ts +1 -3
  74. package/templates/default/{src/tests → automation}/steps/actions/store.step.ts +1 -4
  75. package/templates/default/automation/steps/actions/wait.step.ts +91 -0
  76. package/templates/default/{src/tests → automation}/steps/validations/active_state_assertion.step.ts +1 -4
  77. package/templates/default/{src/tests → automation}/steps/validations/navigation_assertion.step.ts +1 -2
  78. package/templates/default/{src/tests → automation}/steps/validations/text_assertion.step.ts +1 -4
  79. package/templates/default/{src/tests → automation}/steps/validations/visibility_assertion.step.ts +1 -4
  80. package/templates/default/cucumber.mjs +6 -6
  81. package/templates/default/eslint.config.mjs +5 -4
  82. package/templates/default/package-lock.json +322 -485
  83. package/templates/default/package.json +11 -6
  84. package/templates/default/packages/cucumber-runtime/package.json +13 -0
  85. package/templates/default/packages/cucumber-runtime/src/cache.util.ts +93 -0
  86. package/templates/default/packages/cucumber-runtime/src/cli.ts +68 -0
  87. package/templates/default/packages/cucumber-runtime/src/environment.util.ts +21 -0
  88. package/templates/default/packages/cucumber-runtime/src/executor.ts +32 -0
  89. package/templates/default/{src/tests/hooks → packages/cucumber-runtime/src}/hooks.ts +17 -32
  90. package/templates/default/packages/cucumber-runtime/src/index.ts +17 -0
  91. package/templates/default/{src/tests/utils → packages/cucumber-runtime/src}/locator.util.ts +50 -64
  92. package/templates/default/packages/cucumber-runtime/src/parameter-types.ts +7 -0
  93. package/templates/default/packages/cucumber-runtime/src/paths.ts +33 -0
  94. package/templates/default/packages/cucumber-runtime/src/random-data.util.ts +35 -0
  95. package/templates/default/packages/cucumber-runtime/src/types.ts +13 -0
  96. package/templates/default/{src/tests/config/executor → packages/cucumber-runtime/src}/world.ts +4 -1
  97. package/templates/default/packages/cucumber-runtime/tsconfig.json +11 -0
  98. package/templates/default/scripts/setup-env.ts +4 -4
  99. package/templates/default/scripts/sync-appraise-base-template.ts +123 -105
  100. package/templates/default/scripts/sync-environments.ts +8 -5
  101. package/templates/default/scripts/sync-locator-groups.ts +7 -10
  102. package/templates/default/scripts/sync-locators.ts +5 -9
  103. package/templates/default/scripts/sync-modules.ts +9 -17
  104. package/templates/default/scripts/sync-tags.ts +2 -2
  105. package/templates/default/scripts/sync-template-step-groups.ts +16 -6
  106. package/templates/default/scripts/sync-template-steps.ts +16 -5
  107. package/templates/default/scripts/sync-test-cases.ts +6 -3
  108. package/templates/default/scripts/sync-test-suites.ts +7 -4
  109. package/templates/default/src/actions/environments/environment-actions.ts +6 -23
  110. package/templates/default/src/actions/locator/locator-actions.ts +36 -93
  111. package/templates/default/src/actions/locator-groups/locator-group-actions.ts +24 -78
  112. package/templates/default/src/actions/modules/module-actions.ts +4 -2
  113. package/templates/default/src/actions/tags/tag-actions.ts +4 -1
  114. package/templates/default/src/actions/template-step/template-step-actions.ts +10 -101
  115. package/templates/default/src/actions/template-step-group/template-step-group-actions.ts +31 -130
  116. package/templates/default/src/actions/test-case/test-case-actions.ts +31 -94
  117. package/templates/default/src/actions/test-run/test-run-actions.ts +11 -13
  118. package/templates/default/src/actions/test-suite/test-suite-actions.ts +29 -82
  119. package/templates/default/src/app/(base)/locator-groups/page.tsx +1 -3
  120. package/templates/default/src/app/(base)/reports/page.tsx +1 -1
  121. package/templates/default/src/app/(base)/reports/test-cases/page.tsx +2 -2
  122. package/templates/default/src/app/(base)/reports/test-cases/test-cases-metric-table-columns.tsx +1 -1
  123. package/templates/default/src/app/(base)/tags/page.tsx +2 -2
  124. package/templates/default/src/app/(base)/template-steps/page.tsx +1 -2
  125. package/templates/default/src/app/(base)/test-runs/page.tsx +2 -2
  126. package/templates/default/src/app/api/test-runs/[runId]/logs/route.ts +2 -1
  127. package/templates/default/src/app/api/test-runs/[runId]/trace/[testCaseId]/route.ts +2 -1
  128. package/templates/default/src/app/page.tsx +4 -5
  129. package/templates/default/src/components/diagram/dynamic-parameters.tsx +76 -40
  130. package/templates/default/src/components/diagram/options-header-node.tsx +1 -1
  131. package/templates/default/src/components/ui/data-table.tsx +33 -39
  132. package/templates/default/src/lib/automation/paths.ts +181 -0
  133. package/templates/default/src/lib/automation/projection-service.ts +230 -0
  134. package/templates/default/src/lib/environment-file-utils.ts +14 -51
  135. package/templates/default/src/lib/executor/local-executor-adapter.ts +101 -0
  136. package/templates/default/src/lib/executor/types.ts +24 -0
  137. package/templates/default/src/lib/feature-file-generator.ts +22 -112
  138. package/templates/default/src/lib/locator-group-file-utils.ts +57 -120
  139. package/templates/default/src/lib/process/task-spawner.ts +236 -0
  140. package/templates/default/src/lib/template-sync-utils.d.ts +7 -0
  141. package/templates/default/src/lib/template-sync-utils.d.ts.map +1 -0
  142. package/templates/default/src/lib/template-sync-utils.js +47 -0
  143. package/templates/default/src/lib/template-sync-utils.js.map +1 -0
  144. package/templates/default/src/lib/template-sync-utils.ts +63 -0
  145. package/templates/default/src/lib/test-run/process-manager.ts +9 -87
  146. package/templates/default/src/lib/test-run/test-run-executor.ts +7 -136
  147. package/templates/default/src/lib/test-run/winston-logger.ts +6 -35
  148. package/templates/default/src/lib/utils/template-step-file-generator.ts +22 -85
  149. package/templates/default/src/lib/utils/template-step-file-manager-intelligent.ts +7 -22
  150. package/templates/default/public/favicon.ico +0 -0
  151. package/templates/default/src/tests/executor.ts +0 -80
  152. package/templates/default/src/tests/mapping/locator-map.json +0 -1
  153. package/templates/default/src/tests/steps/actions/wait.step.ts +0 -107
  154. package/templates/default/src/tests/support/parameter-types.ts +0 -12
  155. package/templates/default/src/tests/utils/cache.util.ts +0 -260
  156. package/templates/default/src/tests/utils/cli.util.ts +0 -177
  157. package/templates/default/src/tests/utils/environment.util.ts +0 -65
  158. package/templates/default/src/tests/utils/random-data.util.ts +0 -45
  159. 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 (error) {
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 (error) {
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
- // Fetch test suite with test cases, steps, tags, and all modules for path building
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(), // Get all modules to build hierarchy path
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, // Use description as feature title, fallback to name
70
+ testSuiteDescription || testSuiteName,
101
71
  testSuite.testCases,
102
72
  testSuite.tags,
103
73
  )
104
74
 
105
- // Create the features directory with module path
106
- const featuresBaseDir = join(process.cwd(), 'src', 'tests', 'features')
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('') // Add blank line between scenarios
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
- // After When, use And
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
- // After Then, use When for new actions
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
- // Fetch test suite and all modules to build the correct path
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 = join(process.cwd(), 'src', 'tests', 'features')
299
- const moduleDir = join(featuresBaseDir, modulePath.substring(1)) // Remove leading slash
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
- console.log('Starting regeneration of all feature files...')
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
- // Fetch all test suites from database
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
- // Create the features directory with module path
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, '') // Remove leading/trailing hyphens
410
- .replace(/-+/g, '-') // Replace multiple consecutive hyphens with single hyphen
319
+ .replace(/^-+|-+$/g, '')
320
+ .replace(/-+/g, '-')
411
321
  }