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
@@ -127,7 +127,7 @@ export function DataTable<TData, TValue>({
127
127
  <div className="flex justify-end">
128
128
  <div className="mb-4 flex gap-2">
129
129
  {createLink && (
130
- <Button variant="default" size="icon">
130
+ <Button variant="default" size="icon" asChild>
131
131
  <Link href={createLink}>
132
132
  <PlusCircle className="h-4 w-4" />
133
133
  </Link>
@@ -154,46 +154,40 @@ export function DataTable<TData, TValue>({
154
154
  </DropdownMenu>
155
155
  )}
156
156
  {modifyLink && (
157
- <Button
158
- variant="outline"
159
- size="icon"
160
- disabled={table.getSelectedRowModel().rows.length === 0 || table.getSelectedRowModel().rows.length > 1}
161
- >
162
- <Link
163
- href={`${modifyLink}/${
164
- table.getSelectedRowModel().rows.length > 0
165
- ? (
166
- table.getSelectedRowModel().rows[0].original as {
167
- id: string
168
- }
169
- ).id
170
- : ''
171
- }`}
172
- >
173
- <Pencil className="h-4 w-4" />
174
- </Link>
175
- </Button>
157
+ (() => {
158
+ const disabled =
159
+ table.getSelectedRowModel().rows.length === 0 || table.getSelectedRowModel().rows.length > 1
160
+ const href = `${modifyLink}/${disabled ? '' : (table.getSelectedRowModel().rows[0].original as { id: string }).id}`
161
+ return disabled ? (
162
+ <Button variant="outline" size="icon" disabled>
163
+ <Pencil className="h-4 w-4" />
164
+ </Button>
165
+ ) : (
166
+ <Button variant="outline" size="icon" asChild>
167
+ <Link href={href}>
168
+ <Pencil className="h-4 w-4" />
169
+ </Link>
170
+ </Button>
171
+ )
172
+ })()
176
173
  )}
177
174
  {viewLink && (
178
- <Button
179
- variant="outline"
180
- size="icon"
181
- disabled={table.getSelectedRowModel().rows.length === 0 || table.getSelectedRowModel().rows.length > 1}
182
- >
183
- <Link
184
- href={`${viewLink}/${
185
- table.getSelectedRowModel().rows.length > 0
186
- ? (
187
- table.getSelectedRowModel().rows[0].original as {
188
- id: string
189
- }
190
- ).id
191
- : ''
192
- }`}
193
- >
194
- <Eye className="h-4 w-4" />
195
- </Link>
196
- </Button>
175
+ (() => {
176
+ const disabled =
177
+ table.getSelectedRowModel().rows.length === 0 || table.getSelectedRowModel().rows.length > 1
178
+ const href = `${viewLink}/${disabled ? '' : (table.getSelectedRowModel().rows[0].original as { id: string }).id}`
179
+ return disabled ? (
180
+ <Button variant="outline" size="icon" disabled>
181
+ <Eye className="h-4 w-4" />
182
+ </Button>
183
+ ) : (
184
+ <Button variant="outline" size="icon" asChild>
185
+ <Link href={href}>
186
+ <Eye className="h-4 w-4" />
187
+ </Link>
188
+ </Button>
189
+ )
190
+ })()
197
191
  )}
198
192
  {deleteAction && (
199
193
  <DeletePrompt
@@ -0,0 +1,181 @@
1
+ import { promises as fs } from 'fs'
2
+ import path from 'path'
3
+
4
+ const repoRoot = process.cwd()
5
+ const automationRoot = path.join(repoRoot, 'automation')
6
+ const legacyTestsRoot = path.join(repoRoot, 'src', 'tests')
7
+ const runtimeImport =
8
+ "import { When, Then, CustomWorld, expect, SelectorName, resolveLocator, getEnvironment, generateRandomData, RandomDataType } from '../../../packages/cucumber-runtime/src/index.js'"
9
+
10
+ const mutableLegacyDirectories = ['features', 'locators', 'mapping', 'reports', 'steps'] as const
11
+
12
+ let automationWorkspaceReadyPromise: Promise<void> | null = null
13
+
14
+ async function pathExists(targetPath: string): Promise<boolean> {
15
+ try {
16
+ await fs.access(targetPath)
17
+ return true
18
+ } catch {
19
+ return false
20
+ }
21
+ }
22
+
23
+ async function rewriteLegacyStepImports(): Promise<void> {
24
+ const stepsDir = getAutomationStepsDir()
25
+
26
+ if (!(await pathExists(stepsDir))) {
27
+ return
28
+ }
29
+
30
+ const stepFiles = await fs.readdir(stepsDir, { recursive: true })
31
+ for (const entry of stepFiles) {
32
+ if (typeof entry !== 'string' || !entry.endsWith('.ts')) {
33
+ continue
34
+ }
35
+
36
+ const filePath = path.join(stepsDir, entry)
37
+ let content = await fs.readFile(filePath, 'utf8')
38
+
39
+ content = content
40
+ .replace(/^import \{ (When|Then) \} from '@cucumber\/cucumber';?\r?\n/gm, '')
41
+ .replace(/^import \{ CustomWorld(?:, expect)? \} from '\.\.\/\.\.\/config\/executor\/world\.js';?\r?\n/gm, '')
42
+ .replace(/^import \{ SelectorName \} from '(?:@\/types\/locator\/locator\.type|\.\.\/\.\.\/\.\.\/types\/locator\/locator\.type)';?\r?\n/gm, '')
43
+ .replace(/^import \{ resolveLocator \} from '\.\.\/\.\.\/utils\/locator\.util\.js';?\r?\n/gm, '')
44
+ .replace(/^import \{ getEnvironment \} from '\.\.\/\.\.\/utils\/environment\.util\.js';?\r?\n/gm, '')
45
+ .replace(/^import \{ generateRandomData, RandomDataType \} from '\.\.\/\.\.\/utils\/random-data\.util\.js';?\r?\n/gm, '')
46
+ .trimStart()
47
+
48
+ if (!content.startsWith(runtimeImport)) {
49
+ content = `${runtimeImport}\n${content}`
50
+ }
51
+
52
+ await fs.writeFile(filePath, content)
53
+ }
54
+ }
55
+
56
+ async function copyLegacyMutableWorkspace(): Promise<void> {
57
+ const legacyExists = await pathExists(legacyTestsRoot)
58
+ const automationExists = await pathExists(automationRoot)
59
+
60
+ if (!legacyExists || automationExists) {
61
+ return
62
+ }
63
+
64
+ await fs.mkdir(automationRoot, { recursive: true })
65
+
66
+ const legacyEnvironmentsDir = path.join(legacyTestsRoot, 'config', 'environments')
67
+ if (await pathExists(legacyEnvironmentsDir)) {
68
+ await fs.cp(legacyEnvironmentsDir, getAutomationEnvironmentsDir(), { recursive: true })
69
+ }
70
+
71
+ for (const directory of mutableLegacyDirectories) {
72
+ const sourceDirectory = path.join(legacyTestsRoot, directory)
73
+ const destinationDirectory = path.join(automationRoot, directory)
74
+
75
+ if (await pathExists(sourceDirectory)) {
76
+ await fs.cp(sourceDirectory, destinationDirectory, { recursive: true })
77
+ }
78
+ }
79
+ }
80
+
81
+ async function copyLegacyEnvironmentsIfNeeded(): Promise<void> {
82
+ const legacyEnvironmentsDir = path.join(legacyTestsRoot, 'config', 'environments')
83
+ const automationEnvironmentsFile = path.join(getAutomationEnvironmentsDir(), 'environments.json')
84
+
85
+ if (!(await pathExists(legacyEnvironmentsDir)) || (await pathExists(automationEnvironmentsFile))) {
86
+ return
87
+ }
88
+
89
+ await fs.mkdir(getAutomationEnvironmentsDir(), { recursive: true })
90
+ await fs.cp(legacyEnvironmentsDir, getAutomationEnvironmentsDir(), { recursive: true })
91
+ }
92
+
93
+ async function ensureMutableAutomationDirectories(): Promise<void> {
94
+ const requiredDirectories = [
95
+ getAutomationRoot(),
96
+ getAutomationConfigDir(),
97
+ getAutomationEnvironmentsDir(),
98
+ getAutomationFeaturesDir(),
99
+ getAutomationLocatorsDir(),
100
+ getAutomationMappingDir(),
101
+ getAutomationReportsDir(),
102
+ getAutomationReportLogsDir(),
103
+ getAutomationReportTracesDir(),
104
+ getAutomationStepsDir(),
105
+ getAutomationActionStepsDir(),
106
+ getAutomationValidationStepsDir(),
107
+ ]
108
+
109
+ await Promise.all(requiredDirectories.map(directory => fs.mkdir(directory, { recursive: true })))
110
+ }
111
+
112
+ async function removeLegacyRuntimeArtifactsFromAutomation(): Promise<void> {
113
+ await fs.rm(path.join(getAutomationConfigDir(), 'executor'), { recursive: true, force: true })
114
+ await fs.rm(path.join(getAutomationRoot(), 'hooks'), { recursive: true, force: true })
115
+ await fs.rm(path.join(getAutomationRoot(), 'support'), { recursive: true, force: true })
116
+ await fs.rm(path.join(getAutomationRoot(), 'utils'), { recursive: true, force: true })
117
+ }
118
+
119
+ export async function ensureAutomationWorkspaceReady(): Promise<void> {
120
+ automationWorkspaceReadyPromise ??= (async () => {
121
+ await copyLegacyMutableWorkspace()
122
+ await copyLegacyEnvironmentsIfNeeded()
123
+ await ensureMutableAutomationDirectories()
124
+ await removeLegacyRuntimeArtifactsFromAutomation()
125
+ await rewriteLegacyStepImports()
126
+ })()
127
+
128
+ return automationWorkspaceReadyPromise
129
+ }
130
+
131
+ export function getAutomationRoot(): string {
132
+ return automationRoot
133
+ }
134
+
135
+ export function getLegacyTestsRoot(): string {
136
+ return legacyTestsRoot
137
+ }
138
+
139
+ export function getAutomationConfigDir(): string {
140
+ return path.join(getAutomationRoot(), 'config')
141
+ }
142
+
143
+ export function getAutomationEnvironmentsDir(): string {
144
+ return path.join(getAutomationConfigDir(), 'environments')
145
+ }
146
+
147
+ export function getAutomationFeaturesDir(): string {
148
+ return path.join(getAutomationRoot(), 'features')
149
+ }
150
+
151
+ export function getAutomationLocatorsDir(): string {
152
+ return path.join(getAutomationRoot(), 'locators')
153
+ }
154
+
155
+ export function getAutomationMappingDir(): string {
156
+ return path.join(getAutomationRoot(), 'mapping')
157
+ }
158
+
159
+ export function getAutomationReportsDir(): string {
160
+ return path.join(getAutomationRoot(), 'reports')
161
+ }
162
+
163
+ export function getAutomationReportLogsDir(): string {
164
+ return path.join(getAutomationReportsDir(), 'logs')
165
+ }
166
+
167
+ export function getAutomationReportTracesDir(): string {
168
+ return path.join(getAutomationReportsDir(), 'traces')
169
+ }
170
+
171
+ export function getAutomationStepsDir(): string {
172
+ return path.join(getAutomationRoot(), 'steps')
173
+ }
174
+
175
+ export function getAutomationActionStepsDir(): string {
176
+ return path.join(getAutomationStepsDir(), 'actions')
177
+ }
178
+
179
+ export function getAutomationValidationStepsDir(): string {
180
+ return path.join(getAutomationStepsDir(), 'validations')
181
+ }
@@ -0,0 +1,230 @@
1
+ import prisma from '@/config/db-config'
2
+ import { createOrUpdateEnvironmentsFile } from '@/lib/environment-file-utils'
3
+ import {
4
+ createEmptyLocatorGroupFile,
5
+ createOrUpdateLocatorGroupFile,
6
+ deleteLocatorGroupFile,
7
+ moveLocatorGroupFile,
8
+ removeLocatorMapEntry,
9
+ renameLocatorGroupFile,
10
+ updateLocatorMapFile,
11
+ } from '@/lib/locator-group-file-utils'
12
+ import { deleteFeatureFile, generateFeatureFile, regenerateAllFeatureFiles } from '@/lib/feature-file-generator'
13
+ import {
14
+ createTemplateStepGroupFile,
15
+ removeTemplateStepGroupFile,
16
+ renameTemplateStepGroupFile,
17
+ } from '@/lib/utils/template-step-file-manager-intelligent'
18
+ import { generateFileContent, writeTemplateStepFile } from '@/lib/utils/template-step-file-generator'
19
+ import { ensureAutomationWorkspaceReady } from './paths'
20
+
21
+ type TemplateStepGroupType = 'ACTION' | 'VALIDATION'
22
+
23
+ function getTemplateStepGroupType(type: string | null | undefined): TemplateStepGroupType {
24
+ return type === 'VALIDATION' ? 'VALIDATION' : 'ACTION'
25
+ }
26
+
27
+ class AutomationProjectionService {
28
+ async syncEnvironments(): Promise<boolean> {
29
+ await ensureAutomationWorkspaceReady()
30
+ return createOrUpdateEnvironmentsFile()
31
+ }
32
+
33
+ async createEmptyLocatorGroup(locatorGroupId: string): Promise<boolean> {
34
+ await ensureAutomationWorkspaceReady()
35
+ return createEmptyLocatorGroupFile(locatorGroupId)
36
+ }
37
+
38
+ async syncLocatorGroup(locatorGroupId: string): Promise<boolean> {
39
+ await ensureAutomationWorkspaceReady()
40
+ return createOrUpdateLocatorGroupFile(locatorGroupId)
41
+ }
42
+
43
+ async renameLocatorGroup(locatorGroupId: string, newName: string, oldName?: string): Promise<boolean> {
44
+ await ensureAutomationWorkspaceReady()
45
+ return renameLocatorGroupFile(locatorGroupId, newName, oldName)
46
+ }
47
+
48
+ async moveLocatorGroup(locatorGroupId: string, previousFilePath?: string): Promise<boolean> {
49
+ await ensureAutomationWorkspaceReady()
50
+ return moveLocatorGroupFile(locatorGroupId, previousFilePath)
51
+ }
52
+
53
+ async deleteLocatorGroup(locatorGroupId: string): Promise<boolean> {
54
+ await ensureAutomationWorkspaceReady()
55
+ return deleteLocatorGroupFile(locatorGroupId)
56
+ }
57
+
58
+ async syncLocatorMap(
59
+ currentLocatorGroupRoute: string,
60
+ newLocatorGroupRoute: string,
61
+ currentLocatorGroupName: string,
62
+ newLocatorGroupName: string,
63
+ ): Promise<boolean>
64
+ async syncLocatorMap(newLocatorGroupName: string, newLocatorGroupRoute: string): Promise<boolean>
65
+ async syncLocatorMap(param1: string, param2: string, param3?: string, param4?: string): Promise<boolean> {
66
+ await ensureAutomationWorkspaceReady()
67
+ if (param3 === undefined || param4 === undefined) {
68
+ return updateLocatorMapFile(param1, param2)
69
+ }
70
+
71
+ return updateLocatorMapFile(param1, param2, param3, param4)
72
+ }
73
+
74
+ async deleteLocatorMapEntries(locatorGroupNames: string[]): Promise<boolean> {
75
+ await ensureAutomationWorkspaceReady()
76
+ return removeLocatorMapEntry(locatorGroupNames)
77
+ }
78
+
79
+ async syncTemplateStepGroup(groupId: string): Promise<void> {
80
+ await ensureAutomationWorkspaceReady()
81
+
82
+ const group = await prisma.templateStepGroup.findUnique({
83
+ where: { id: groupId },
84
+ include: {
85
+ templateSteps: {
86
+ orderBy: {
87
+ createdAt: 'asc',
88
+ },
89
+ },
90
+ },
91
+ })
92
+
93
+ if (!group) {
94
+ return
95
+ }
96
+
97
+ const groupType = getTemplateStepGroupType((group as { type?: string | null }).type)
98
+
99
+ if (group.templateSteps.length === 0) {
100
+ await createTemplateStepGroupFile(group.name, groupType, group.description)
101
+ return
102
+ }
103
+
104
+ const content = generateFileContent(group.templateSteps)
105
+ await writeTemplateStepFile(group.name, content, groupType)
106
+ }
107
+
108
+ async deleteTemplateStepGroup(groupId: string): Promise<void> {
109
+ await ensureAutomationWorkspaceReady()
110
+
111
+ const group = await prisma.templateStepGroup.findUnique({
112
+ where: { id: groupId },
113
+ select: {
114
+ name: true,
115
+ type: true,
116
+ },
117
+ })
118
+
119
+ if (!group) {
120
+ return
121
+ }
122
+
123
+ await removeTemplateStepGroupFile(group.name, getTemplateStepGroupType(group.type))
124
+ }
125
+
126
+ async renameTemplateStepGroup(
127
+ groupId: string,
128
+ newName: string,
129
+ newType: string,
130
+ newDescription?: string | null,
131
+ ): Promise<void> {
132
+ await ensureAutomationWorkspaceReady()
133
+
134
+ const currentGroup = await prisma.templateStepGroup.findUnique({
135
+ where: { id: groupId },
136
+ select: {
137
+ name: true,
138
+ type: true,
139
+ },
140
+ })
141
+
142
+ if (!currentGroup) {
143
+ return
144
+ }
145
+
146
+ await renameTemplateStepGroupFile(
147
+ currentGroup.name,
148
+ newName,
149
+ getTemplateStepGroupType(currentGroup.type),
150
+ getTemplateStepGroupType(newType),
151
+ newDescription,
152
+ )
153
+ }
154
+
155
+ async syncTemplateStep(stepId: string): Promise<void> {
156
+ const step = await prisma.templateStep.findUnique({
157
+ where: { id: stepId },
158
+ select: { templateStepGroupId: true },
159
+ })
160
+
161
+ if (!step?.templateStepGroupId) {
162
+ return
163
+ }
164
+
165
+ await this.syncTemplateStepGroup(step.templateStepGroupId)
166
+ }
167
+
168
+ async deleteTemplateStep(stepId: string): Promise<void> {
169
+ const step = await prisma.templateStep.findUnique({
170
+ where: { id: stepId },
171
+ select: { templateStepGroupId: true },
172
+ })
173
+
174
+ if (!step?.templateStepGroupId) {
175
+ return
176
+ }
177
+
178
+ await this.syncTemplateStepGroup(step.templateStepGroupId)
179
+ }
180
+
181
+ async generateFeature(testSuiteId: string): Promise<string> {
182
+ await ensureAutomationWorkspaceReady()
183
+
184
+ const testSuite = await prisma.testSuite.findUnique({
185
+ where: { id: testSuiteId },
186
+ select: {
187
+ name: true,
188
+ description: true,
189
+ },
190
+ })
191
+
192
+ if (!testSuite) {
193
+ throw new Error(`Test suite ${testSuiteId} not found`)
194
+ }
195
+
196
+ return generateFeatureFile(testSuiteId, testSuite.name, testSuite.description || undefined)
197
+ }
198
+
199
+ async deleteFeature(testSuiteId: string): Promise<boolean> {
200
+ await ensureAutomationWorkspaceReady()
201
+ return deleteFeatureFile(testSuiteId)
202
+ }
203
+
204
+ async regenerateAllFeatures(): Promise<string[]> {
205
+ await ensureAutomationWorkspaceReady()
206
+ return regenerateAllFeatureFiles()
207
+ }
208
+
209
+ async regenerateAllPathDependentArtifacts(): Promise<void> {
210
+ await ensureAutomationWorkspaceReady()
211
+
212
+ const locatorGroups = await prisma.locatorGroup.findMany({
213
+ select: { id: true },
214
+ })
215
+
216
+ const templateStepGroups = await prisma.templateStepGroup.findMany({
217
+ select: { id: true },
218
+ })
219
+
220
+ await Promise.all([
221
+ this.syncEnvironments(),
222
+ ...locatorGroups.map(locatorGroup => this.syncLocatorGroup(locatorGroup.id)),
223
+ ...templateStepGroups.map(group => this.syncTemplateStepGroup(group.id)),
224
+ ])
225
+
226
+ await this.regenerateAllFeatures()
227
+ }
228
+ }
229
+
230
+ export const automationProjectionService = new AutomationProjectionService()
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from 'fs'
2
2
  import * as path from 'path'
3
3
  import prisma from '@/config/db-config'
4
+ import { ensureAutomationWorkspaceReady, getAutomationEnvironmentsDir } from '@/lib/automation/paths'
4
5
 
5
6
  interface EnvironmentConfig {
6
7
  baseUrl: string
@@ -9,29 +10,15 @@ interface EnvironmentConfig {
9
10
  password: string
10
11
  }
11
12
 
12
- /**
13
- * Gets the file path for the environments.json file
14
- */
15
13
  export function getEnvironmentsFilePath(): string {
16
- return path.join('src', 'tests', 'config', 'environments', 'environments.json')
14
+ return path.join(getAutomationEnvironmentsDir(), 'environments.json')
17
15
  }
18
16
 
19
- /**
20
- * Ensures the config directory exists
21
- */
22
17
  export async function ensureConfigDirectoryExists(): Promise<void> {
23
- const filePath = getEnvironmentsFilePath()
24
- const dir = path.dirname(filePath)
25
- try {
26
- await fs.access(dir)
27
- } catch {
28
- await fs.mkdir(dir, { recursive: true })
29
- }
18
+ await ensureAutomationWorkspaceReady()
19
+ await fs.mkdir(path.dirname(getEnvironmentsFilePath()), { recursive: true })
30
20
  }
31
21
 
32
- /**
33
- * Generates JSON content for environments from database
34
- */
35
22
  export async function generateEnvironmentsContent(): Promise<Record<string, EnvironmentConfig>> {
36
23
  try {
37
24
  const environments = await prisma.environment.findMany({
@@ -57,17 +44,14 @@ export async function generateEnvironmentsContent(): Promise<Record<string, Envi
57
44
  }
58
45
  }
59
46
 
60
- /**
61
- * Creates or updates the environments.json file
62
- */
63
47
  export async function createOrUpdateEnvironmentsFile(): Promise<boolean> {
64
48
  try {
49
+ await ensureAutomationWorkspaceReady()
65
50
  const filePath = getEnvironmentsFilePath()
66
51
  await ensureConfigDirectoryExists()
67
52
 
68
53
  const content = await generateEnvironmentsContent()
69
54
 
70
- // If no environments exist, delete the file
71
55
  if (Object.keys(content).length === 0) {
72
56
  await deleteEnvironmentsFile()
73
57
  return true
@@ -81,18 +65,15 @@ export async function createOrUpdateEnvironmentsFile(): Promise<boolean> {
81
65
  }
82
66
  }
83
67
 
84
- /**
85
- * Deletes the environments.json file
86
- */
87
68
  export async function deleteEnvironmentsFile(): Promise<boolean> {
88
69
  try {
70
+ await ensureAutomationWorkspaceReady()
89
71
  const filePath = getEnvironmentsFilePath()
90
72
 
91
- // Check if file exists before trying to delete
92
73
  try {
93
74
  await fs.access(filePath)
94
75
  } catch {
95
- return true // File doesn't exist, nothing to delete
76
+ return true
96
77
  }
97
78
 
98
79
  await fs.unlink(filePath)
@@ -103,20 +84,18 @@ export async function deleteEnvironmentsFile(): Promise<boolean> {
103
84
  }
104
85
  }
105
86
 
106
- /**
107
- * Reads and parses the content of the environments.json file
108
- */
109
87
  export async function readEnvironmentsFile(): Promise<{
110
88
  filePath: string
111
89
  content: Record<string, EnvironmentConfig>
112
90
  } | null> {
113
91
  try {
92
+ await ensureAutomationWorkspaceReady()
114
93
  const filePath = getEnvironmentsFilePath()
115
94
 
116
95
  try {
117
96
  await fs.access(filePath)
118
97
  } catch {
119
- return null // File doesn't exist
98
+ return null
120
99
  }
121
100
 
122
101
  const fileContent = await fs.readFile(filePath, 'utf-8')
@@ -129,12 +108,9 @@ export async function readEnvironmentsFile(): Promise<{
129
108
  }
130
109
  }
131
110
 
132
- /**
133
- * Updates a specific environment entry in the environments.json file
134
- */
135
111
  export async function updateEnvironmentEntry(environmentId: string, oldName?: string): Promise<boolean> {
136
112
  try {
137
- // Get the environment from database
113
+ await ensureAutomationWorkspaceReady()
138
114
  const environment = await prisma.environment.findUnique({
139
115
  where: { id: environmentId },
140
116
  })
@@ -145,24 +121,21 @@ export async function updateEnvironmentEntry(environmentId: string, oldName?: st
145
121
  }
146
122
 
147
123
  const filePath = getEnvironmentsFilePath()
148
-
149
- // Read existing content
150
124
  let environmentsConfig: Record<string, EnvironmentConfig> = {}
125
+
151
126
  try {
152
127
  await fs.access(filePath)
153
128
  const fileContent = await fs.readFile(filePath, 'utf-8')
154
129
  environmentsConfig = JSON.parse(fileContent)
155
130
  } catch {
156
- // File doesn't exist, start with empty object
131
+ environmentsConfig = {}
157
132
  }
158
133
 
159
- // Remove old entry if name changed
160
134
  if (oldName) {
161
135
  const oldKey = oldName.toLowerCase().replace(/\s+/g, '_')
162
136
  delete environmentsConfig[oldKey]
163
137
  }
164
138
 
165
- // Add/update the environment entry
166
139
  const envKey = environment.name.toLowerCase().replace(/\s+/g, '_')
167
140
  environmentsConfig[envKey] = {
168
141
  baseUrl: environment.baseUrl,
@@ -171,10 +144,7 @@ export async function updateEnvironmentEntry(environmentId: string, oldName?: st
171
144
  password: environment.password || '',
172
145
  }
173
146
 
174
- // Ensure directory exists
175
147
  await ensureConfigDirectoryExists()
176
-
177
- // Write updated content
178
148
  await fs.writeFile(filePath, JSON.stringify(environmentsConfig, null, 2))
179
149
  return true
180
150
  } catch (error) {
@@ -183,35 +153,28 @@ export async function updateEnvironmentEntry(environmentId: string, oldName?: st
183
153
  }
184
154
  }
185
155
 
186
- /**
187
- * Removes a specific environment entry from the environments.json file
188
- */
189
156
  export async function removeEnvironmentEntry(environmentName: string): Promise<boolean> {
190
157
  try {
158
+ await ensureAutomationWorkspaceReady()
191
159
  const filePath = getEnvironmentsFilePath()
192
160
 
193
- // Check if file exists
194
161
  try {
195
162
  await fs.access(filePath)
196
163
  } catch {
197
- return true // File doesn't exist, nothing to remove
164
+ return true
198
165
  }
199
166
 
200
- // Read existing content
201
167
  const fileContent = await fs.readFile(filePath, 'utf-8')
202
168
  const environmentsConfig: Record<string, EnvironmentConfig> = JSON.parse(fileContent)
203
169
 
204
- // Remove the environment entry
205
170
  const envKey = environmentName.toLowerCase().replace(/\s+/g, '_')
206
171
  delete environmentsConfig[envKey]
207
172
 
208
- // If no environments left, delete the file
209
173
  if (Object.keys(environmentsConfig).length === 0) {
210
174
  await deleteEnvironmentsFile()
211
175
  return true
212
176
  }
213
177
 
214
- // Write updated content
215
178
  await fs.writeFile(filePath, JSON.stringify(environmentsConfig, null, 2))
216
179
  return true
217
180
  } catch (error) {