create-appraisejs 0.1.8 → 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
@@ -2,35 +2,36 @@ import { promises as fs } from 'fs'
2
2
  import path from 'path'
3
3
  import prisma from '@/config/db-config'
4
4
  import { buildModulePath } from '@/lib/path-helpers/module-path'
5
+ import {
6
+ ensureAutomationWorkspaceReady,
7
+ getAutomationLocatorsDir,
8
+ getAutomationMappingDir,
9
+ } from '@/lib/automation/paths'
5
10
 
6
- /**
7
- * Gets the file path for a locator group based on its module hierarchy
8
- */
9
11
  export async function getLocatorGroupFilePath(locatorGroupId: string): Promise<string | null> {
10
12
  try {
13
+ await ensureAutomationWorkspaceReady()
11
14
  const locatorGroup = await prisma.locatorGroup.findUnique({
12
15
  where: { id: locatorGroupId },
13
16
  include: { module: true },
14
17
  })
15
18
 
16
- if (!locatorGroup) return null
19
+ if (!locatorGroup) {
20
+ return null
21
+ }
17
22
 
18
23
  const allModules = await prisma.module.findMany()
19
24
  const modulePath = buildModulePath(allModules, locatorGroup.module)
20
-
21
25
  const sanitizedPath = modulePath.replace(/^\//, '').replace(/\//g, path.sep)
22
26
  const fileName = `${locatorGroup.name}.json`
23
27
 
24
- return path.join('src', 'tests', 'locators', sanitizedPath, fileName)
28
+ return path.join(getAutomationLocatorsDir(), sanitizedPath, fileName)
25
29
  } catch (error) {
26
30
  console.error('Error getting locator group file path:', error)
27
31
  return null
28
32
  }
29
33
  }
30
34
 
31
- /**
32
- * Generates JSON content for a locator group from its locators
33
- */
34
35
  export async function generateLocatorGroupContent(locatorGroupId: string): Promise<Record<string, string>> {
35
36
  try {
36
37
  const locatorGroup = await prisma.locatorGroup.findUnique({
@@ -42,7 +43,9 @@ export async function generateLocatorGroupContent(locatorGroupId: string): Promi
42
43
  },
43
44
  })
44
45
 
45
- if (!locatorGroup) return {}
46
+ if (!locatorGroup) {
47
+ return {}
48
+ }
46
49
 
47
50
  return Object.fromEntries(locatorGroup.locators.map(locator => [locator.name, locator.value]))
48
51
  } catch (error) {
@@ -51,29 +54,21 @@ export async function generateLocatorGroupContent(locatorGroupId: string): Promi
51
54
  }
52
55
  }
53
56
 
54
- /**
55
- * Ensures a directory exists, creating it if necessary
56
- */
57
57
  export async function ensureDirectoryExists(filePath: string): Promise<void> {
58
- const dir = path.dirname(filePath)
59
- try {
60
- await fs.access(dir)
61
- } catch {
62
- await fs.mkdir(dir, { recursive: true })
63
- }
58
+ await ensureAutomationWorkspaceReady()
59
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
64
60
  }
65
61
 
66
- /**
67
- * Creates or updates a locator group JSON file
68
- */
69
62
  export async function createOrUpdateLocatorGroupFile(locatorGroupId: string): Promise<boolean> {
70
63
  try {
64
+ await ensureAutomationWorkspaceReady()
71
65
  const filePath = await getLocatorGroupFilePath(locatorGroupId)
72
- if (!filePath) return false
66
+ if (!filePath) {
67
+ return false
68
+ }
73
69
 
74
70
  await ensureDirectoryExists(filePath)
75
71
  const content = await generateLocatorGroupContent(locatorGroupId)
76
-
77
72
  await fs.writeFile(filePath, JSON.stringify(content, null, 2))
78
73
  return true
79
74
  } catch (error) {
@@ -82,19 +77,18 @@ export async function createOrUpdateLocatorGroupFile(locatorGroupId: string): Pr
82
77
  }
83
78
  }
84
79
 
85
- /**
86
- * Deletes a locator group JSON file and cleans up empty directories
87
- */
88
- export async function deleteLocatorGroupFile(locatorGroupId: string): Promise<boolean> {
80
+ export async function deleteLocatorGroupFile(locatorGroupId: string, filePathOverride?: string): Promise<boolean> {
89
81
  try {
90
- const filePath = await getLocatorGroupFilePath(locatorGroupId)
91
- if (!filePath) return false
82
+ await ensureAutomationWorkspaceReady()
83
+ const filePath = filePathOverride ?? (await getLocatorGroupFilePath(locatorGroupId))
84
+ if (!filePath) {
85
+ return false
86
+ }
92
87
 
93
- // Check if file exists before trying to delete
94
88
  try {
95
89
  await fs.access(filePath)
96
90
  } catch {
97
- return true // File doesn't exist, nothing to delete
91
+ return true
98
92
  }
99
93
 
100
94
  await fs.unlink(filePath)
@@ -106,39 +100,26 @@ export async function deleteLocatorGroupFile(locatorGroupId: string): Promise<bo
106
100
  }
107
101
  }
108
102
 
109
- /**
110
- * Renames a locator group file when the name changes
111
- */
112
103
  export async function renameLocatorGroupFile(
113
- oldLocatorGroupId: string,
104
+ locatorGroupId: string,
114
105
  newName: string,
115
106
  oldName?: string,
116
107
  ): Promise<boolean> {
117
108
  try {
118
- // Get the current file path (with new name) for the directory
119
- const currentFilePath = await getLocatorGroupFilePath(oldLocatorGroupId)
120
- if (!currentFilePath) return false
121
-
122
- // If oldName is provided, construct the old file path manually
123
- // Otherwise, try to get it from the current path (fallback)
124
- let oldFilePath: string
125
- if (oldName) {
126
- oldFilePath = path.join(path.dirname(currentFilePath), `${oldName}.json`)
127
- } else {
128
- oldFilePath = currentFilePath
109
+ await ensureAutomationWorkspaceReady()
110
+ const currentFilePath = await getLocatorGroupFilePath(locatorGroupId)
111
+ if (!currentFilePath) {
112
+ return false
129
113
  }
130
114
 
115
+ const oldFilePath = oldName ? path.join(path.dirname(currentFilePath), `${oldName}.json`) : currentFilePath
131
116
  const newFilePath = path.join(path.dirname(currentFilePath), `${newName}.json`)
132
117
 
133
118
  try {
134
119
  await fs.access(oldFilePath)
135
- console.log('oldFilePath exists:', oldFilePath)
136
120
  await fs.rename(oldFilePath, newFilePath)
137
- console.log('File renamed successfully from', oldFilePath, 'to', newFilePath)
138
- } catch (error) {
139
- console.log('File not found at old path, creating new one:', error)
140
- // File doesn't exist, create new one
141
- return await createOrUpdateLocatorGroupFile(oldLocatorGroupId)
121
+ } catch {
122
+ return createOrUpdateLocatorGroupFile(locatorGroupId)
142
123
  }
143
124
 
144
125
  return true
@@ -148,27 +129,25 @@ export async function renameLocatorGroupFile(
148
129
  }
149
130
  }
150
131
 
151
- /**
152
- * Moves a locator group file when the module changes
153
- */
154
- export async function moveLocatorGroupFile(locatorGroupId: string): Promise<boolean> {
132
+ export async function moveLocatorGroupFile(locatorGroupId: string, previousFilePath?: string): Promise<boolean> {
155
133
  try {
156
- // Delete old file and create new one in correct location
157
- await deleteLocatorGroupFile(locatorGroupId)
158
- return await createOrUpdateLocatorGroupFile(locatorGroupId)
134
+ await ensureAutomationWorkspaceReady()
135
+ if (previousFilePath) {
136
+ await deleteLocatorGroupFile(locatorGroupId, previousFilePath)
137
+ }
138
+
139
+ return createOrUpdateLocatorGroupFile(locatorGroupId)
159
140
  } catch (error) {
160
141
  console.error('Error moving locator group file:', error)
161
142
  return false
162
143
  }
163
144
  }
164
145
 
165
- /**
166
- * Cleans up empty directories recursively
167
- */
168
146
  async function cleanupEmptyDirectories(filePath: string): Promise<void> {
169
147
  let currentDir = path.dirname(filePath)
148
+ const locatorsRoot = getAutomationLocatorsDir()
170
149
 
171
- while (currentDir !== 'tests' && currentDir !== '.') {
150
+ while (currentDir.startsWith(locatorsRoot) && currentDir !== locatorsRoot && currentDir !== path.dirname(currentDir)) {
172
151
  try {
173
152
  const files = await fs.readdir(currentDir)
174
153
  if (files.length === 0) {
@@ -183,13 +162,13 @@ async function cleanupEmptyDirectories(filePath: string): Promise<void> {
183
162
  }
184
163
  }
185
164
 
186
- /**
187
- * Creates an empty JSON file for a new locator group
188
- */
189
165
  export async function createEmptyLocatorGroupFile(locatorGroupId: string): Promise<boolean> {
190
166
  try {
167
+ await ensureAutomationWorkspaceReady()
191
168
  const filePath = await getLocatorGroupFilePath(locatorGroupId)
192
- if (!filePath) return false
169
+ if (!filePath) {
170
+ return false
171
+ }
193
172
 
194
173
  await ensureDirectoryExists(filePath)
195
174
  await fs.writeFile(filePath, JSON.stringify({}, null, 2))
@@ -200,46 +179,31 @@ export async function createEmptyLocatorGroupFile(locatorGroupId: string): Promi
200
179
  }
201
180
  }
202
181
 
203
- /**
204
- * Reads and parses the content of a locator group file
205
- */
206
182
  export async function readLocatorGroupFile(
207
183
  locatorGroupId: string,
208
184
  ): Promise<{ filePath: string; content: Record<string, string> } | null> {
209
185
  try {
186
+ await ensureAutomationWorkspaceReady()
210
187
  const filePath = await getLocatorGroupFilePath(locatorGroupId)
211
- if (!filePath) return null
188
+ if (!filePath) {
189
+ return null
190
+ }
212
191
 
213
192
  const fileContent = await fs.readFile(filePath, 'utf-8')
214
- const jsonContent = JSON.parse(fileContent)
215
-
216
- return { filePath, content: jsonContent }
193
+ return { filePath, content: JSON.parse(fileContent) }
217
194
  } catch (error) {
218
195
  console.error('Error reading locator group file:', error)
219
196
  return null
220
197
  }
221
198
  }
222
199
 
223
- /**
224
- * Updates the locator map file with locator group information
225
- * Overload for updating existing entries (4 parameters)
226
- */
227
200
  export async function updateLocatorMapFile(
228
201
  currentLocatorGroupRoute: string,
229
202
  newLocatorGroupRoute: string,
230
203
  currentLocatorGroupName: string,
231
204
  newLocatorGroupName: string,
232
205
  ): Promise<boolean>
233
-
234
- /**
235
- * Updates the locator map file with locator group information
236
- * Overload for adding new entries (2 parameters)
237
- */
238
206
  export async function updateLocatorMapFile(newLocatorGroupName: string, newLocatorGroupRoute: string): Promise<boolean>
239
-
240
- /**
241
- * Implementation of updateLocatorMapFile with proper overload handling
242
- */
243
207
  export async function updateLocatorMapFile(
244
208
  param1: string,
245
209
  param2: string,
@@ -247,54 +211,43 @@ export async function updateLocatorMapFile(
247
211
  param4?: string,
248
212
  ): Promise<boolean> {
249
213
  try {
250
- const locatorMapPath = path.join('src', 'tests', 'mapping', 'locator-map.json')
251
-
252
- // Ensure the mapping directory exists
214
+ await ensureAutomationWorkspaceReady()
215
+ const locatorMapPath = path.join(getAutomationMappingDir(), 'locator-map.json')
253
216
  await ensureDirectoryExists(locatorMapPath)
254
217
 
255
218
  let locatorMap: Array<{ name: string; path: string }> = []
256
219
 
257
- // Read existing locator map or create empty array
258
220
  try {
259
221
  const fileContent = await fs.readFile(locatorMapPath, 'utf-8')
260
222
  locatorMap = JSON.parse(fileContent)
261
223
  } catch {
262
- // File doesn't exist, start with empty array
263
224
  locatorMap = []
264
225
  }
265
226
 
266
- // Determine if this is a 2-param call (new entry) or 4-param call (update)
267
227
  const isNewEntry = param3 === undefined && param4 === undefined
268
228
 
269
229
  if (isNewEntry) {
270
- // 2 params: newLocatorGroupName, newLocatorGroupRoute
271
230
  const name = param1
272
231
  const route = param2
273
-
274
- // Check for uniqueness
275
232
  const existingEntry = locatorMap.find(entry => entry.name === name)
276
233
  if (existingEntry) {
277
234
  console.error(`Locator group with name "${name}" already exists in locator map`)
278
235
  return false
279
236
  }
280
237
 
281
- // Add new entry
282
238
  locatorMap.push({ name, path: route })
283
239
  } else {
284
- // 4 params: update existing entry
285
240
  const currentLocatorGroupRoute = param1
286
241
  const newLocatorGroupRoute = param2
287
242
  const currentLocatorGroupName = param3!
288
243
  const newLocatorGroupName = param4!
289
244
 
290
- // Find the entry to update
291
245
  const entryIndex = locatorMap.findIndex(entry => entry.name === currentLocatorGroupName)
292
246
  if (entryIndex === -1) {
293
247
  console.error(`Locator group with name "${currentLocatorGroupName}" not found in locator map`)
294
248
  return false
295
249
  }
296
250
 
297
- // Check if new name is unique (if name is changing)
298
251
  if (currentLocatorGroupName !== newLocatorGroupName) {
299
252
  const existingEntry = locatorMap.find(entry => entry.name === newLocatorGroupName)
300
253
  if (existingEntry) {
@@ -303,15 +256,12 @@ export async function updateLocatorMapFile(
303
256
  }
304
257
  }
305
258
 
306
- // Update the entry
307
259
  const updatedEntry = { ...locatorMap[entryIndex] }
308
260
 
309
- // Update name if it changed
310
261
  if (currentLocatorGroupName !== newLocatorGroupName) {
311
262
  updatedEntry.name = newLocatorGroupName
312
263
  }
313
264
 
314
- // Update path if it changed
315
265
  if (currentLocatorGroupRoute !== newLocatorGroupRoute) {
316
266
  updatedEntry.path = newLocatorGroupRoute
317
267
  }
@@ -319,7 +269,6 @@ export async function updateLocatorMapFile(
319
269
  locatorMap[entryIndex] = updatedEntry
320
270
  }
321
271
 
322
- // Write the updated locator map back to file
323
272
  await fs.writeFile(locatorMapPath, JSON.stringify(locatorMap, null, 2))
324
273
  return true
325
274
  } catch (error) {
@@ -328,40 +277,28 @@ export async function updateLocatorMapFile(
328
277
  }
329
278
  }
330
279
 
331
- /**
332
- * Removes locator group entries from the locator map file
333
- * @param locatorGroupNames - Array of locator group names to remove
334
- */
335
280
  export async function removeLocatorMapEntry(locatorGroupNames: string[]): Promise<boolean> {
336
281
  try {
337
- const locatorMapPath = path.join('src', 'tests', 'mapping', 'locator-map.json')
282
+ await ensureAutomationWorkspaceReady()
283
+ const locatorMapPath = path.join(getAutomationMappingDir(), 'locator-map.json')
338
284
 
339
- // Check if file exists
340
285
  try {
341
286
  await fs.access(locatorMapPath)
342
287
  } catch {
343
- // File doesn't exist, nothing to remove
344
288
  return true
345
289
  }
346
290
 
347
- // Read existing locator map
348
291
  const fileContent = await fs.readFile(locatorMapPath, 'utf-8')
349
292
  let locatorMap: Array<{ name: string; path: string }> = JSON.parse(fileContent)
350
293
 
351
- // Filter out the entries to be removed
352
294
  const originalLength = locatorMap.length
353
295
  locatorMap = locatorMap.filter(entry => !locatorGroupNames.includes(entry.name))
354
296
 
355
- // Check if any entries were actually removed
356
- const removedCount = originalLength - locatorMap.length
357
- if (removedCount === 0) {
358
- console.log('No matching locator group entries found in locator map')
297
+ if (originalLength === locatorMap.length) {
359
298
  return true
360
299
  }
361
300
 
362
- // Write the updated locator map back to file
363
301
  await fs.writeFile(locatorMapPath, JSON.stringify(locatorMap, null, 2))
364
- console.log(`Removed ${removedCount} locator group entry(ies) from locator map`)
365
302
  return true
366
303
  } catch (error) {
367
304
  console.error('Error removing locator map entries:', error)
@@ -0,0 +1,236 @@
1
+ import { execa, type Options as ExecaOptions } from 'execa'
2
+ import type { ChildProcess } from 'child_process'
3
+ import { EventEmitter } from 'events'
4
+
5
+ export interface SpawnerOptions extends ExecaOptions {
6
+ streamLogs?: boolean
7
+ prefixLogs?: boolean
8
+ logPrefix?: string
9
+ captureOutput?: boolean
10
+ }
11
+
12
+ export interface SpawnedProcess {
13
+ process: ChildProcess
14
+ pid: number | undefined
15
+ name: string
16
+ output: {
17
+ stdout: string[]
18
+ stderr: string[]
19
+ }
20
+ isRunning: boolean
21
+ exitCode: number | null
22
+ startTime: Date
23
+ endTime: Date | null
24
+ }
25
+
26
+ export class TaskSpawner extends EventEmitter {
27
+ private processes: Map<string, SpawnedProcess> = new Map()
28
+ private processCounter = 0
29
+ private outputBuffers: Map<string, { stdout: string; stderr: string }> = new Map()
30
+
31
+ async spawn(command: string, args: string[] = [], options: SpawnerOptions = {}): Promise<SpawnedProcess> {
32
+ const { streamLogs = true, prefixLogs = true, logPrefix, captureOutput = false, ...spawnOptions } = options
33
+
34
+ const processName = logPrefix || `${command}_${++this.processCounter}`
35
+ const spawnedProcess: SpawnedProcess = {
36
+ process: null as unknown as ChildProcess,
37
+ pid: undefined,
38
+ name: processName,
39
+ output: {
40
+ stdout: [],
41
+ stderr: [],
42
+ },
43
+ isRunning: false,
44
+ exitCode: null,
45
+ startTime: new Date(),
46
+ endTime: null,
47
+ }
48
+
49
+ const stdioConfig = captureOutput ? 'pipe' : streamLogs ? 'inherit' : 'pipe'
50
+ const childProcess = execa(command, args, {
51
+ stdio: stdioConfig,
52
+ ...spawnOptions,
53
+ })
54
+
55
+ spawnedProcess.process = childProcess
56
+ spawnedProcess.pid = childProcess.pid
57
+ spawnedProcess.isRunning = true
58
+
59
+ this.processes.set(processName, spawnedProcess)
60
+ this.outputBuffers.set(processName, { stdout: '', stderr: '' })
61
+
62
+ this.setupProcessListeners(spawnedProcess, {
63
+ streamLogs,
64
+ prefixLogs,
65
+ captureOutput,
66
+ stdioConfig,
67
+ })
68
+
69
+ this.emit('spawn', spawnedProcess)
70
+ return spawnedProcess
71
+ }
72
+
73
+ killProcess(processName: string, signal: NodeJS.Signals = 'SIGTERM'): boolean {
74
+ const spawnedProcess = this.processes.get(processName)
75
+ if (!spawnedProcess || !spawnedProcess.isRunning) {
76
+ return false
77
+ }
78
+
79
+ spawnedProcess.process.kill(signal)
80
+ return true
81
+ }
82
+
83
+ async waitForProcess(processName: string): Promise<number | null> {
84
+ const spawnedProcess = this.processes.get(processName)
85
+ if (!spawnedProcess) {
86
+ throw new Error(`Process '${processName}' not found`)
87
+ }
88
+
89
+ return new Promise(resolve => {
90
+ if (!spawnedProcess.isRunning) {
91
+ resolve(spawnedProcess.exitCode)
92
+ return
93
+ }
94
+
95
+ spawnedProcess.process.on('exit', (code: number | null) => resolve(code))
96
+ })
97
+ }
98
+
99
+ getProcess(processName: string): SpawnedProcess | undefined {
100
+ return this.processes.get(processName)
101
+ }
102
+
103
+ private processBufferedOutput(
104
+ processName: string,
105
+ stream: 'stdout' | 'stderr',
106
+ streamLogs: boolean,
107
+ prefixLogs: boolean,
108
+ captureOutput: boolean,
109
+ spawnedProcess: SpawnedProcess,
110
+ ): void {
111
+ const buffer = this.outputBuffers.get(processName)
112
+ if (!buffer) {
113
+ return
114
+ }
115
+
116
+ const lines = buffer[stream].split('\n')
117
+ buffer[stream] = lines.pop() || ''
118
+
119
+ for (const line of lines) {
120
+ if (captureOutput) {
121
+ spawnedProcess.output[stream].push(`${line}\n`)
122
+ }
123
+
124
+ if (streamLogs) {
125
+ const prefix = prefixLogs ? `[${processName}] ` : ''
126
+ if (stream === 'stdout') {
127
+ console.log(`${prefix}${line}`)
128
+ } else {
129
+ console.error(`${prefix}${line}`)
130
+ }
131
+ }
132
+
133
+ this.emit(stream, { processName, data: `${line}\n` })
134
+ }
135
+ }
136
+
137
+ private setupProcessListeners(
138
+ spawnedProcess: SpawnedProcess,
139
+ options: {
140
+ streamLogs: boolean
141
+ prefixLogs: boolean
142
+ captureOutput: boolean
143
+ stdioConfig: string | string[]
144
+ },
145
+ ): void {
146
+ const { streamLogs, prefixLogs, captureOutput, stdioConfig } = options
147
+ const { process: childProcess, name } = spawnedProcess
148
+
149
+ if (stdioConfig === 'pipe') {
150
+ childProcess.stdout?.on('data', (data: Buffer) => {
151
+ const buffer = this.outputBuffers.get(name)
152
+ if (!buffer) {
153
+ return
154
+ }
155
+
156
+ buffer.stdout += data.toString()
157
+ this.processBufferedOutput(name, 'stdout', streamLogs, prefixLogs, captureOutput, spawnedProcess)
158
+ })
159
+
160
+ childProcess.stderr?.on('data', (data: Buffer) => {
161
+ const buffer = this.outputBuffers.get(name)
162
+ if (!buffer) {
163
+ return
164
+ }
165
+
166
+ buffer.stderr += data.toString()
167
+ this.processBufferedOutput(name, 'stderr', streamLogs, prefixLogs, captureOutput, spawnedProcess)
168
+ })
169
+ }
170
+
171
+ childProcess.on('exit', (code: number | null) => {
172
+ if (stdioConfig === 'pipe') {
173
+ const buffer = this.outputBuffers.get(name)
174
+ if (buffer) {
175
+ if (buffer.stdout) {
176
+ if (captureOutput) {
177
+ spawnedProcess.output.stdout.push(buffer.stdout)
178
+ }
179
+ if (streamLogs) {
180
+ const prefix = prefixLogs ? `[${name}] ` : ''
181
+ console.log(`${prefix}${buffer.stdout}`)
182
+ }
183
+ this.emit('stdout', { processName: name, data: buffer.stdout })
184
+ }
185
+
186
+ if (buffer.stderr) {
187
+ if (captureOutput) {
188
+ spawnedProcess.output.stderr.push(buffer.stderr)
189
+ }
190
+ if (streamLogs) {
191
+ const prefix = prefixLogs ? `[${name}] ` : ''
192
+ console.error(`${prefix}${buffer.stderr}`)
193
+ }
194
+ this.emit('stderr', { processName: name, data: buffer.stderr })
195
+ }
196
+
197
+ this.outputBuffers.delete(name)
198
+ }
199
+ }
200
+
201
+ spawnedProcess.isRunning = false
202
+ spawnedProcess.exitCode = code
203
+ spawnedProcess.endTime = new Date()
204
+ this.emit('exit', { processName: name, code })
205
+ })
206
+
207
+ childProcess.on('error', (error: Error) => {
208
+ spawnedProcess.isRunning = false
209
+ spawnedProcess.endTime = new Date()
210
+
211
+ if (streamLogs) {
212
+ const prefix = prefixLogs ? `[${name}] ` : ''
213
+ console.error(`${prefix}ERROR: ${error.message}`)
214
+ }
215
+
216
+ this.emit('error', { processName: name, error })
217
+ })
218
+ }
219
+ }
220
+
221
+ const globalForTaskSpawner = global as unknown as {
222
+ taskSpawner: TaskSpawner | undefined
223
+ }
224
+
225
+ export const taskSpawner = globalForTaskSpawner.taskSpawner ?? new TaskSpawner()
226
+
227
+ if (!globalForTaskSpawner.taskSpawner) {
228
+ globalForTaskSpawner.taskSpawner = taskSpawner
229
+ }
230
+
231
+ export const spawnTask = (command: string, args: string[] = [], options: SpawnerOptions = {}) =>
232
+ taskSpawner.spawn(command, args, options)
233
+
234
+ export const killTask = (processName: string, signal?: NodeJS.Signals) => taskSpawner.killProcess(processName, signal)
235
+
236
+ export const waitForTask = (processName: string) => taskSpawner.waitForProcess(processName)
@@ -0,0 +1,7 @@
1
+ export declare function shouldExcludeTemplatePath(relativePath: string): boolean;
2
+ export declare function shouldBackfillLegacyEnvironmentConfig(targetHasEnvironmentsFile: boolean, legacyEnvironmentsDirExists: boolean): boolean;
3
+ export declare function getAutomationFeaturesDir(baseDir: string): string;
4
+ export declare function getAutomationLocatorsDir(baseDir: string): string;
5
+ export declare function getAutomationLocatorMapPath(baseDir: string): string;
6
+ export declare function extractModulePathFromAutomationFile(filePath: string, baseDir: string, automationSubdir: 'features' | 'locators'): string;
7
+ //# sourceMappingURL=template-sync-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template-sync-utils.d.ts","sourceRoot":"","sources":["template-sync-utils.ts"],"names":[],"mappings":"AAUA,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAgBvE;AAED,wBAAgB,qCAAqC,CACnD,yBAAyB,EAAE,OAAO,EAClC,2BAA2B,EAAE,OAAO,GACnC,OAAO,CAET;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEhE;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,wBAAgB,mCAAmC,CACjD,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,UAAU,GAAG,UAAU,GACxC,MAAM,CAWR"}
@@ -0,0 +1,47 @@
1
+ import { join, relative } from 'path';
2
+ const EXCLUDED_DIRS = new Set(['node_modules', '.next', '.git', 'dist']);
3
+ const EXCLUDED_EXTENSIONS = new Set(['.db', '.sqlite', '.sqlite3', '.tsbuildinfo']);
4
+ const EXCLUDED_PATH_PREFIXES = ['automation/reports/'];
5
+ function toPosixPath(value) {
6
+ return value.replace(/\\/g, '/');
7
+ }
8
+ export function shouldExcludeTemplatePath(relativePath) {
9
+ const normalizedPath = toPosixPath(relativePath);
10
+ const parts = normalizedPath.split('/');
11
+ if (parts.some(part => EXCLUDED_DIRS.has(part)))
12
+ return true;
13
+ if (EXCLUDED_PATH_PREFIXES.some(prefix => normalizedPath.startsWith(prefix)))
14
+ return true;
15
+ const ext = normalizedPath.endsWith('.sqlite3')
16
+ ? '.sqlite3'
17
+ : normalizedPath.endsWith('.sqlite')
18
+ ? '.sqlite'
19
+ : normalizedPath.endsWith('.tsbuildinfo')
20
+ ? '.tsbuildinfo'
21
+ : normalizedPath.slice(normalizedPath.lastIndexOf('.'));
22
+ return EXCLUDED_EXTENSIONS.has(ext);
23
+ }
24
+ export function shouldBackfillLegacyEnvironmentConfig(targetHasEnvironmentsFile, legacyEnvironmentsDirExists) {
25
+ return !targetHasEnvironmentsFile && legacyEnvironmentsDirExists;
26
+ }
27
+ export function getAutomationFeaturesDir(baseDir) {
28
+ return join(baseDir, 'automation', 'features');
29
+ }
30
+ export function getAutomationLocatorsDir(baseDir) {
31
+ return join(baseDir, 'automation', 'locators');
32
+ }
33
+ export function getAutomationLocatorMapPath(baseDir) {
34
+ return join(baseDir, 'automation', 'mapping', 'locator-map.json');
35
+ }
36
+ export function extractModulePathFromAutomationFile(filePath, baseDir, automationSubdir) {
37
+ const automationBaseDir = automationSubdir === 'features' ? getAutomationFeaturesDir(baseDir) : getAutomationLocatorsDir(baseDir);
38
+ const normalizedBaseDir = toPosixPath(automationBaseDir).replace(/\/$/, '');
39
+ const normalizedFilePath = toPosixPath(filePath);
40
+ const relativePath = normalizedFilePath.startsWith(`${normalizedBaseDir}/`)
41
+ ? normalizedFilePath.slice(normalizedBaseDir.length + 1)
42
+ : toPosixPath(relative(automationBaseDir, filePath));
43
+ const pathParts = relativePath.split('/').filter(part => part && part !== '');
44
+ const moduleParts = pathParts.slice(0, -1);
45
+ return moduleParts.length > 0 ? `/${moduleParts.join('/')}` : '/';
46
+ }
47
+ //# sourceMappingURL=template-sync-utils.js.map