create-appraisejs 0.3.1-alpha.1 → 0.4.0-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 (133) hide show
  1. package/package.json +1 -1
  2. package/templates/blank/.env.example +2 -2
  3. package/templates/blank/.prettierrc +13 -13
  4. package/templates/blank/cucumber.mjs +12 -1
  5. package/templates/blank/e2e/helpers/test-data.ts +10 -0
  6. package/templates/blank/next-env.d.ts +6 -6
  7. package/templates/blank/package-lock.json +2 -2
  8. package/templates/blank/package.json +1 -1
  9. package/templates/blank/packages/cucumber-runtime/package.json +2 -1
  10. package/templates/blank/packages/cucumber-runtime/src/paths.ts +22 -5
  11. package/templates/blank/packages/locator-picker-companion/package.json +2 -1
  12. package/templates/blank/prisma/dev.db +0 -0
  13. package/templates/blank/prisma/migrations/20251104113456_add_type_for_template_step_groups/migration.sql +16 -16
  14. package/templates/blank/prisma/migrations/20251104170946_add_tags_to_test_suite_and_test_case/migration.sql +27 -27
  15. package/templates/blank/prisma/migrations/20251112190024_add_cascade_delete_to_test_run_test_case/migration.sql +17 -17
  16. package/templates/blank/prisma/migrations/20251113181100_add_test_run_log/migration.sql +12 -12
  17. package/templates/blank/prisma/migrations/20251119191838_add_tag_type/migration.sql +28 -28
  18. package/templates/blank/prisma/migrations/20251121164059_add_conflict_resolution/migration.sql +12 -12
  19. package/templates/blank/prisma/migrations/20251223183400_add_report_model_to_db_schema/migration.sql +10 -10
  20. package/templates/blank/prisma/migrations/20251223183637_add_report_test_case_entity_for_storing_test_results_for_individual_test_cases/migration.sql +10 -10
  21. package/templates/blank/prisma/migrations/20251224083549_add_comprehensive_report_storage/migration.sql +108 -108
  22. package/templates/blank/prisma/migrations/20251229194422_migrate_duration_to_string/migration.sql +55 -55
  23. package/templates/blank/prisma/migrations/20251230124637_add_unique_constraint_to_test_run_name/migration.sql +27 -27
  24. package/templates/blank/prisma/migrations/20260115094436_add_dashboard_metrics/migration.sql +59 -59
  25. package/templates/blank/prisma/migrations/20260127172022_add_cascade_delete_to_step_parameters/migration.sql +34 -34
  26. package/templates/blank/prisma/migrations/20260313093000_add_report_step_screenshot_path/migration.sql +1 -1
  27. package/templates/blank/scripts/setup-env.ts +0 -0
  28. package/templates/blank/scripts/sync-test-cases.ts +60 -10
  29. package/templates/blank/src/components/diagram/flow-diagram-node-search.tsx +9 -2
  30. package/templates/blank/src/components/diagram/flow-diagram-toolbar.tsx +37 -3
  31. package/templates/blank/src/components/diagram/flow-diagram.test.tsx +225 -0
  32. package/templates/blank/src/components/diagram/use-flow-diagram-search.ts +2 -0
  33. package/templates/blank/src/components/diagram/use-flow-diagram.ts +93 -0
  34. package/templates/blank/src/lib/appraise-test-case-metadata.test.ts +78 -0
  35. package/templates/blank/src/lib/appraise-test-case-metadata.ts +220 -0
  36. package/templates/blank/src/lib/automation/automation-path-roots.test.ts +14 -0
  37. package/templates/blank/src/lib/automation/automation-path-roots.ts +10 -2
  38. package/templates/blank/src/lib/database-sync.ts +166 -15
  39. package/templates/blank/src/lib/executor/local-executor-adapter.ts +6 -2
  40. package/templates/blank/src/lib/feature-file-generator.ts +54 -10
  41. package/templates/blank/src/lib/gherkin-parser.test.ts +52 -0
  42. package/templates/blank/src/lib/gherkin-parser.ts +39 -1
  43. package/templates/blank/src/lib/sync/projected-feature-utils.ts +5 -1
  44. package/templates/blank/src/lib/sync/sync-pending-counts.test.ts +115 -0
  45. package/templates/blank/src/lib/sync/sync-pending-counts.ts +108 -13
  46. package/templates/blank/src/services/test-run/test-run-service.test.ts +10 -0
  47. package/templates/blank/src/services/test-run/test-run-service.ts +41 -1
  48. package/templates/starter/.env.example +2 -2
  49. package/templates/starter/.prettierrc +13 -13
  50. package/templates/starter/cucumber.mjs +12 -1
  51. package/templates/starter/e2e/helpers/test-data.ts +10 -0
  52. package/templates/starter/next-env.d.ts +6 -6
  53. package/templates/starter/package-lock.json +2 -2
  54. package/templates/starter/package.json +1 -1
  55. package/templates/starter/packages/cucumber-runtime/package.json +2 -1
  56. package/templates/starter/packages/cucumber-runtime/src/paths.ts +22 -5
  57. package/templates/starter/packages/locator-picker-companion/package.json +2 -1
  58. package/templates/starter/prisma/dev.db +0 -0
  59. package/templates/starter/prisma/migrations/20251104113456_add_type_for_template_step_groups/migration.sql +16 -16
  60. package/templates/starter/prisma/migrations/20251104170946_add_tags_to_test_suite_and_test_case/migration.sql +27 -27
  61. package/templates/starter/prisma/migrations/20251112190024_add_cascade_delete_to_test_run_test_case/migration.sql +17 -17
  62. package/templates/starter/prisma/migrations/20251113181100_add_test_run_log/migration.sql +12 -12
  63. package/templates/starter/prisma/migrations/20251119191838_add_tag_type/migration.sql +28 -28
  64. package/templates/starter/prisma/migrations/20251121164059_add_conflict_resolution/migration.sql +12 -12
  65. package/templates/starter/prisma/migrations/20251223183400_add_report_model_to_db_schema/migration.sql +10 -10
  66. package/templates/starter/prisma/migrations/20251223183637_add_report_test_case_entity_for_storing_test_results_for_individual_test_cases/migration.sql +10 -10
  67. package/templates/starter/prisma/migrations/20251224083549_add_comprehensive_report_storage/migration.sql +108 -108
  68. package/templates/starter/prisma/migrations/20251229194422_migrate_duration_to_string/migration.sql +55 -55
  69. package/templates/starter/prisma/migrations/20251230124637_add_unique_constraint_to_test_run_name/migration.sql +27 -27
  70. package/templates/starter/prisma/migrations/20260115094436_add_dashboard_metrics/migration.sql +59 -59
  71. package/templates/starter/prisma/migrations/20260127172022_add_cascade_delete_to_step_parameters/migration.sql +34 -34
  72. package/templates/starter/prisma/migrations/20260313093000_add_report_step_screenshot_path/migration.sql +1 -1
  73. package/templates/starter/scripts/setup-env.ts +0 -0
  74. package/templates/starter/scripts/sync-test-cases.ts +60 -10
  75. package/templates/starter/src/components/diagram/flow-diagram-node-search.tsx +9 -2
  76. package/templates/starter/src/components/diagram/flow-diagram-toolbar.tsx +37 -3
  77. package/templates/starter/src/components/diagram/flow-diagram.test.tsx +225 -0
  78. package/templates/starter/src/components/diagram/use-flow-diagram-search.ts +2 -0
  79. package/templates/starter/src/components/diagram/use-flow-diagram.ts +93 -0
  80. package/templates/starter/src/lib/appraise-test-case-metadata.test.ts +78 -0
  81. package/templates/starter/src/lib/appraise-test-case-metadata.ts +220 -0
  82. package/templates/starter/src/lib/automation/automation-path-roots.test.ts +14 -0
  83. package/templates/starter/src/lib/automation/automation-path-roots.ts +10 -2
  84. package/templates/starter/src/lib/database-sync.ts +166 -15
  85. package/templates/starter/src/lib/executor/local-executor-adapter.ts +6 -2
  86. package/templates/starter/src/lib/feature-file-generator.ts +54 -10
  87. package/templates/starter/src/lib/gherkin-parser.test.ts +52 -0
  88. package/templates/starter/src/lib/gherkin-parser.ts +39 -1
  89. package/templates/starter/src/lib/sync/projected-feature-utils.ts +5 -1
  90. package/templates/starter/src/lib/sync/sync-pending-counts.test.ts +115 -0
  91. package/templates/starter/src/lib/sync/sync-pending-counts.ts +108 -13
  92. package/templates/starter/src/services/test-run/test-run-service.test.ts +10 -0
  93. package/templates/starter/src/services/test-run/test-run-service.ts +41 -1
  94. package/dist/cli.e2e.test.d.ts +0 -2
  95. package/dist/cli.e2e.test.d.ts.map +0 -1
  96. package/dist/cli.e2e.test.js +0 -75
  97. package/dist/cli.e2e.test.js.map +0 -1
  98. package/dist/config.test.d.ts +0 -2
  99. package/dist/config.test.d.ts.map +0 -1
  100. package/dist/config.test.js +0 -65
  101. package/dist/config.test.js.map +0 -1
  102. package/dist/copy-template.test.d.ts +0 -2
  103. package/dist/copy-template.test.d.ts.map +0 -1
  104. package/dist/copy-template.test.js +0 -71
  105. package/dist/copy-template.test.js.map +0 -1
  106. package/dist/download-repo.test.d.ts +0 -2
  107. package/dist/download-repo.test.d.ts.map +0 -1
  108. package/dist/download-repo.test.js +0 -16
  109. package/dist/download-repo.test.js.map +0 -1
  110. package/dist/install.test.d.ts +0 -2
  111. package/dist/install.test.d.ts.map +0 -1
  112. package/dist/install.test.js +0 -120
  113. package/dist/install.test.js.map +0 -1
  114. package/dist/prompts.test.d.ts +0 -2
  115. package/dist/prompts.test.d.ts.map +0 -1
  116. package/dist/prompts.test.js +0 -58
  117. package/dist/prompts.test.js.map +0 -1
  118. package/templates/default/next-env.d.ts +0 -6
  119. package/templates/default/packages/locator-picker-companion/dist/cli.d.ts +0 -1
  120. package/templates/default/packages/locator-picker-companion/dist/cli.js +0 -336
  121. package/templates/default/packages/locator-picker-companion/dist/index.d.ts +0 -3
  122. package/templates/default/packages/locator-picker-companion/dist/index.js +0 -3
  123. package/templates/default/packages/locator-picker-companion/dist/injected-picker-script.d.ts +0 -1
  124. package/templates/default/packages/locator-picker-companion/dist/injected-picker-script.js +0 -660
  125. package/templates/default/packages/locator-picker-companion/dist/launcher.d.ts +0 -14
  126. package/templates/default/packages/locator-picker-companion/dist/launcher.js +0 -58
  127. package/templates/default/packages/locator-picker-companion/dist/selector-generator.d.ts +0 -6
  128. package/templates/default/packages/locator-picker-companion/dist/selector-generator.js +0 -261
  129. package/templates/default/packages/locator-picker-companion/dist/session-file.d.ts +0 -30
  130. package/templates/default/packages/locator-picker-companion/dist/session-file.js +0 -162
  131. package/templates/default/packages/locator-picker-companion/dist/types.d.ts +0 -31
  132. package/templates/default/packages/locator-picker-companion/dist/types.js +0 -1
  133. package/templates/default/prisma/dev.db +0 -0
@@ -0,0 +1,78 @@
1
+ import { promises as fs } from 'fs'
2
+ import { join } from 'path'
3
+ import { tmpdir } from 'os'
4
+ import { expect, test } from 'vitest'
5
+
6
+ import {
7
+ buildAppraiseMetadata,
8
+ getAppraiseMetadataPath,
9
+ readAppraiseMetadataFile,
10
+ } from '@/lib/appraise-test-case-metadata'
11
+
12
+ test('serializes title, stable nodes, and flow blocks by identifier tag', () => {
13
+ const metadata = buildAppraiseMetadata({
14
+ testSuiteName: 'Checkout suite',
15
+ modulePath: '/commerce/checkout',
16
+ testCases: [
17
+ {
18
+ title: 'Buys item',
19
+ description: 'Happy path',
20
+ tags: [{ tagExpression: '@tc_checkout_buy' }, { tagExpression: '@smoke' }],
21
+ steps: [
22
+ { flowNodeId: 'node-open', order: 1, label: 'Open checkout' },
23
+ { flowNodeId: 'node-pay', order: 2, label: 'Pay with card' },
24
+ ],
25
+ flowBlocks: [
26
+ {
27
+ id: 'block-payment',
28
+ name: 'Payment',
29
+ order: 0,
30
+ nodes: [{ flowNodeId: 'node-pay' }, { flowNodeId: 'missing-node' }],
31
+ },
32
+ ],
33
+ },
34
+ ],
35
+ })
36
+
37
+ expect(metadata).toEqual({
38
+ version: 1,
39
+ testSuite: {
40
+ name: 'Checkout suite',
41
+ modulePath: '/commerce/checkout',
42
+ },
43
+ testCases: [
44
+ {
45
+ identifierTag: '@tc_checkout_buy',
46
+ title: 'Buys item',
47
+ description: 'Happy path',
48
+ nodes: [
49
+ { nodeId: 'node-open', order: 1, label: 'Open checkout' },
50
+ { nodeId: 'node-pay', order: 2, label: 'Pay with card' },
51
+ ],
52
+ flowBlocks: [
53
+ {
54
+ id: 'block-payment',
55
+ name: 'Payment',
56
+ order: 0,
57
+ nodeIds: ['node-pay'],
58
+ },
59
+ ],
60
+ },
61
+ ],
62
+ })
63
+ })
64
+
65
+ test('reads missing and malformed sidecars without throwing', async () => {
66
+ const dir = await fs.mkdtemp(join(tmpdir(), 'appraise-metadata-'))
67
+ const featurePath = join(dir, 'sample.feature')
68
+ const metadataPath = getAppraiseMetadataPath(featurePath)
69
+
70
+ await expect(readAppraiseMetadataFile(metadataPath)).resolves.toEqual({ metadata: null, warnings: [] })
71
+
72
+ await fs.writeFile(metadataPath, '{', 'utf8')
73
+
74
+ const result = await readAppraiseMetadataFile(metadataPath)
75
+
76
+ expect(result.metadata).toBeNull()
77
+ expect(result.warnings).toHaveLength(1)
78
+ })
@@ -0,0 +1,220 @@
1
+ import { promises as fs } from 'fs'
2
+
3
+ export const APPRAISE_METADATA_VERSION = 1
4
+ export const APPRAISE_METADATA_EXTENSION = '.appraise.json'
5
+
6
+ export type AppraiseTestCaseMetadataNode = {
7
+ nodeId: string
8
+ order: number
9
+ label: string
10
+ }
11
+
12
+ export type AppraiseTestCaseMetadataFlowBlock = {
13
+ id: string
14
+ name: string
15
+ order: number
16
+ nodeIds: string[]
17
+ }
18
+
19
+ export type AppraiseTestCaseMetadataEntry = {
20
+ identifierTag: string
21
+ title: string
22
+ description: string
23
+ nodes: AppraiseTestCaseMetadataNode[]
24
+ flowBlocks: AppraiseTestCaseMetadataFlowBlock[]
25
+ }
26
+
27
+ export type AppraiseTestCaseMetadata = {
28
+ version: typeof APPRAISE_METADATA_VERSION
29
+ testSuite: {
30
+ name: string
31
+ modulePath: string
32
+ }
33
+ testCases: AppraiseTestCaseMetadataEntry[]
34
+ }
35
+
36
+ export type AppraiseMetadataReadResult =
37
+ | { metadata: AppraiseTestCaseMetadata | null; warnings: string[] }
38
+ | { metadata: null; warnings: string[] }
39
+
40
+ type MetadataInputTestCase = {
41
+ title: string
42
+ description: string
43
+ tags?: Array<{ tagExpression: string }>
44
+ steps?: Array<{
45
+ flowNodeId: string | null
46
+ order: number
47
+ label: string
48
+ }>
49
+ flowBlocks?: Array<{
50
+ id: string
51
+ name: string
52
+ order: number
53
+ nodes: Array<{ flowNodeId: string }>
54
+ }>
55
+ }
56
+
57
+ export function getAppraiseMetadataPath(featureFilePath: string): string {
58
+ return featureFilePath.replace(/\.feature$/, APPRAISE_METADATA_EXTENSION)
59
+ }
60
+
61
+ export function normalizeMetadataTag(tagExpression: string): string {
62
+ return tagExpression.startsWith('@') ? tagExpression : `@${tagExpression}`
63
+ }
64
+
65
+ export function findIdentifierTag(tags: Array<{ tagExpression: string } | string> = []): string | null {
66
+ for (const tag of tags) {
67
+ const tagExpression = typeof tag === 'string' ? tag : tag.tagExpression
68
+ const normalized = normalizeMetadataTag(tagExpression)
69
+ if (normalized.replace(/^@/, '').startsWith('tc_')) {
70
+ return normalized
71
+ }
72
+ }
73
+
74
+ return null
75
+ }
76
+
77
+ export function buildAppraiseMetadata(input: {
78
+ testSuiteName: string
79
+ modulePath: string
80
+ testCases: MetadataInputTestCase[]
81
+ }): AppraiseTestCaseMetadata {
82
+ return {
83
+ version: APPRAISE_METADATA_VERSION,
84
+ testSuite: {
85
+ name: input.testSuiteName,
86
+ modulePath: input.modulePath,
87
+ },
88
+ testCases: input.testCases.flatMap(testCase => {
89
+ const identifierTag = findIdentifierTag(testCase.tags)
90
+ if (!identifierTag) {
91
+ return []
92
+ }
93
+
94
+ const nodes = (testCase.steps ?? [])
95
+ .filter(step => step.flowNodeId)
96
+ .map(step => ({
97
+ nodeId: step.flowNodeId as string,
98
+ order: step.order,
99
+ label: step.label,
100
+ }))
101
+
102
+ const validNodeIds = new Set(nodes.map(node => node.nodeId))
103
+
104
+ return [
105
+ {
106
+ identifierTag,
107
+ title: testCase.title,
108
+ description: testCase.description,
109
+ nodes,
110
+ flowBlocks: (testCase.flowBlocks ?? []).map(block => ({
111
+ id: block.id,
112
+ name: block.name,
113
+ order: block.order,
114
+ nodeIds: block.nodes.map(node => node.flowNodeId).filter(nodeId => validNodeIds.has(nodeId)),
115
+ })),
116
+ },
117
+ ]
118
+ }),
119
+ }
120
+ }
121
+
122
+ function isRecord(value: unknown): value is Record<string, unknown> {
123
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
124
+ }
125
+
126
+ function isString(value: unknown): value is string {
127
+ return typeof value === 'string'
128
+ }
129
+
130
+ function isNumber(value: unknown): value is number {
131
+ return typeof value === 'number' && Number.isFinite(value)
132
+ }
133
+
134
+ function validateMetadata(value: unknown): AppraiseTestCaseMetadata {
135
+ if (!isRecord(value) || value.version !== APPRAISE_METADATA_VERSION) {
136
+ throw new Error(`Unsupported Appraise metadata version`)
137
+ }
138
+
139
+ if (!isRecord(value.testSuite) || !isString(value.testSuite.name) || !isString(value.testSuite.modulePath)) {
140
+ throw new Error('Invalid Appraise metadata testSuite')
141
+ }
142
+
143
+ if (!Array.isArray(value.testCases)) {
144
+ throw new Error('Invalid Appraise metadata testCases')
145
+ }
146
+
147
+ return {
148
+ version: APPRAISE_METADATA_VERSION,
149
+ testSuite: {
150
+ name: value.testSuite.name,
151
+ modulePath: value.testSuite.modulePath,
152
+ },
153
+ testCases: value.testCases.map((entry, index) => {
154
+ if (!isRecord(entry) || !isString(entry.identifierTag) || !isString(entry.title)) {
155
+ throw new Error(`Invalid Appraise metadata testCases[${index}]`)
156
+ }
157
+
158
+ if (!Array.isArray(entry.nodes) || !Array.isArray(entry.flowBlocks)) {
159
+ throw new Error(`Invalid Appraise metadata arrays for ${entry.identifierTag}`)
160
+ }
161
+
162
+ return {
163
+ identifierTag: normalizeMetadataTag(entry.identifierTag),
164
+ title: entry.title,
165
+ description: isString(entry.description) ? entry.description : '',
166
+ nodes: entry.nodes.map((node, nodeIndex) => {
167
+ if (!isRecord(node) || !isString(node.nodeId) || !isNumber(node.order) || !isString(node.label)) {
168
+ throw new Error(`Invalid Appraise metadata node ${nodeIndex} for ${entry.identifierTag}`)
169
+ }
170
+
171
+ return {
172
+ nodeId: node.nodeId,
173
+ order: node.order,
174
+ label: node.label,
175
+ }
176
+ }),
177
+ flowBlocks: entry.flowBlocks.map((block, blockIndex) => {
178
+ if (!isRecord(block) || !isString(block.id) || !isString(block.name) || !isNumber(block.order)) {
179
+ throw new Error(`Invalid Appraise metadata flowBlock ${blockIndex} for ${entry.identifierTag}`)
180
+ }
181
+
182
+ if (!Array.isArray(block.nodeIds) || !block.nodeIds.every(isString)) {
183
+ throw new Error(`Invalid Appraise metadata flowBlock nodeIds for ${entry.identifierTag}`)
184
+ }
185
+
186
+ return {
187
+ id: block.id,
188
+ name: block.name,
189
+ order: block.order,
190
+ nodeIds: block.nodeIds,
191
+ }
192
+ }),
193
+ }
194
+ }),
195
+ }
196
+ }
197
+
198
+ export async function readAppraiseMetadataFile(metadataPath: string): Promise<AppraiseMetadataReadResult> {
199
+ try {
200
+ const content = await fs.readFile(metadataPath, 'utf-8')
201
+ return {
202
+ metadata: validateMetadata(JSON.parse(content)),
203
+ warnings: [],
204
+ }
205
+ } catch (error) {
206
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
207
+ return { metadata: null, warnings: [] }
208
+ }
209
+
210
+ const message = error instanceof Error ? error.message : String(error)
211
+ return {
212
+ metadata: null,
213
+ warnings: [`Unable to read Appraise metadata ${metadataPath}: ${message}`],
214
+ }
215
+ }
216
+ }
217
+
218
+ export function getMetadataByIdentifier(metadata: AppraiseTestCaseMetadata | null) {
219
+ return new Map((metadata?.testCases ?? []).map(testCase => [testCase.identifierTag, testCase]))
220
+ }
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { buildJsonReportFormat } from './automation-path-roots'
3
+
4
+ describe('buildJsonReportFormat', () => {
5
+ it('uses a project-relative path so Windows drive letters stay unambiguous', () => {
6
+ const reportPath = `${process.cwd()}\\automation\\reports\\run-1\\cucumber.json`
7
+ expect(buildJsonReportFormat(reportPath)).toBe('json:automation/reports/run-1/cucumber.json')
8
+ })
9
+
10
+ it('normalizes relative report paths to forward slashes', () => {
11
+ const reportPath = 'automation\\reports\\run-1\\cucumber.json'
12
+ expect(buildJsonReportFormat(reportPath)).toBe('json:automation/reports/run-1/cucumber.json')
13
+ })
14
+ })
@@ -54,6 +54,10 @@ export function getAutomationRunReportPath(runId: string): string {
54
54
  return path.join(getAutomationReportRunDir(runId), 'cucumber.json')
55
55
  }
56
56
 
57
+ export function buildJsonReportFormat(reportPath: string): string {
58
+ return `json:${toProjectRelativePath(reportPath)}`
59
+ }
60
+
57
61
  function getAutomationReportLogsDir(runId: string): string {
58
62
  return path.join(getAutomationReportRunDir(runId), 'logs')
59
63
  }
@@ -63,8 +67,12 @@ export function getAutomationRunLogPath(runId: string): string {
63
67
  }
64
68
 
65
69
  export function toProjectRelativePath(targetPath: string): string {
66
- const normalizedPath = path.isAbsolute(targetPath) ? path.relative(getRepoRoot(), targetPath) : targetPath
67
- return normalizedPath.replace(/\\/g, '/')
70
+ const normalizedTargetPath = targetPath.replace(/\\/g, '/')
71
+ const normalizedRepoRoot = getRepoRoot().replace(/\\/g, '/')
72
+ const normalizedPath = path.isAbsolute(targetPath)
73
+ ? path.posix.relative(normalizedRepoRoot, normalizedTargetPath)
74
+ : normalizedTargetPath
75
+ return normalizedPath
68
76
  }
69
77
 
70
78
  export function resolveStoredPath(storedPath: string): string {
@@ -4,6 +4,40 @@ import { buildModuleHierarchy } from './module-hierarchy-builder'
4
4
  import { Prisma, TemplateStepType, TemplateStepIcon, TestCase, TagType } from '@prisma/client'
5
5
  import { getTagTypeFromExpression } from './tag-identifiers'
6
6
  import { getFeatureModulePath } from './path-helpers/feature-path'
7
+ import { getTestSuiteFilesystemKey } from './sync/projected-feature-utils'
8
+
9
+ function splitTagLine(tagLine: string): string[] {
10
+ return tagLine
11
+ .split(/\s+/)
12
+ .filter(tag => tag.trim().startsWith('@'))
13
+ .map(tag => tag.trim())
14
+ }
15
+
16
+ function flattenFeatureTags(tags: string[]): string[] {
17
+ return tags.flatMap(tag => (tag.startsWith('@') ? splitTagLine(tag) : [tag]))
18
+ }
19
+
20
+ function extractTestSuiteNameFromFilename(filePath: string): string {
21
+ const fileName = filePath.split(/[/\\]/).pop() || ''
22
+ return fileName.replace(/\.feature$/, '')
23
+ }
24
+
25
+ function parseScenarioTitle(
26
+ scenarioName: string,
27
+ scenarioDescription?: string,
28
+ ): { title: string; description: string } {
29
+ if (scenarioDescription) {
30
+ return {
31
+ title: scenarioDescription.trim(),
32
+ description: scenarioName.trim(),
33
+ }
34
+ }
35
+
36
+ return {
37
+ title: scenarioName.trim(),
38
+ description: '',
39
+ }
40
+ }
7
41
 
8
42
  /**
9
43
  * Determines the tag type based on the tag expression pattern
@@ -268,6 +302,7 @@ function determineStepTypeAndIcon(keyword: string): { type: TemplateStepType; ic
268
302
  * Creates or updates a test case step
269
303
  */
270
304
  async function createOrUpdateTestCaseStep(testCaseId: string, step: ParsedStep, templateStepId: string): Promise<void> {
305
+ const metadataNode = step.appraiseNode
271
306
  try {
272
307
  // Check if step already exists
273
308
  const existingStep = await prisma.testCaseStep.findFirst({
@@ -284,7 +319,8 @@ async function createOrUpdateTestCaseStep(testCaseId: string, step: ParsedStep,
284
319
  where: { id: existingStep.id },
285
320
  data: {
286
321
  gherkinStep: `${step.keyword} ${step.text}`,
287
- label: step.text,
322
+ flowNodeId: metadataNode?.nodeId ?? existingStep.flowNodeId,
323
+ label: metadataNode?.label ?? step.text,
288
324
  templateStepId: templateStepId,
289
325
  },
290
326
  })
@@ -295,8 +331,9 @@ async function createOrUpdateTestCaseStep(testCaseId: string, step: ParsedStep,
295
331
  testCaseId: testCaseId,
296
332
  order: step.order,
297
333
  gherkinStep: `${step.keyword} ${step.text}`,
334
+ flowNodeId: metadataNode?.nodeId,
298
335
  icon: determineStepTypeAndIcon(step.keyword).icon,
299
- label: step.text,
336
+ label: metadataNode?.label ?? step.text,
300
337
  templateStepId: templateStepId,
301
338
  },
302
339
  })
@@ -308,14 +345,25 @@ async function createOrUpdateTestCaseStep(testCaseId: string, step: ParsedStep,
308
345
  }
309
346
 
310
347
  type ParsedScenario = ParsedFeature['scenarios'][number]
348
+
349
+ const testSuiteInclude = {
350
+ testCases: {
351
+ include: {
352
+ tags: true,
353
+ steps: true,
354
+ },
355
+ },
356
+ } as const
357
+
311
358
  type ExistingTestSuite = Prisma.TestSuiteGetPayload<{
312
- include: {
313
- testCases: true
314
- }
359
+ include: typeof testSuiteInclude
315
360
  }>
316
361
 
317
362
  async function createScenarioTestCase(scenario: ParsedScenario, testSuiteId: string): Promise<string | null> {
318
- return findOrCreateTestCase(scenario.name, scenario.description || '', testSuiteId, scenario.tags)
363
+ const parsedTitle = parseScenarioTitle(scenario.name, scenario.description)
364
+ const title = scenario.appraiseMetadata?.title ?? parsedTitle.title
365
+ const description = scenario.appraiseMetadata?.description ?? parsedTitle.description
366
+ return findOrCreateTestCase(title, description, testSuiteId, scenario.tags)
319
367
  }
320
368
 
321
369
  async function createScenarioSteps(testCaseId: string, steps: ParsedStep[]): Promise<number> {
@@ -333,15 +381,84 @@ async function createScenarioSteps(testCaseId: string, steps: ParsedStep[]): Pro
333
381
  return createdTemplateSteps
334
382
  }
335
383
 
384
+ function applyScenarioMetadataToSteps(scenario: ParsedScenario): ParsedStep[] {
385
+ const nodesByOrder = new Map((scenario.appraiseMetadata?.nodes ?? []).map(node => [node.order, node]))
386
+ return scenario.steps.map(step => ({
387
+ ...step,
388
+ appraiseNode: nodesByOrder.get(step.order),
389
+ }))
390
+ }
391
+
392
+ async function replaceScenarioFlowBlocks(testCaseId: string, scenario: ParsedScenario): Promise<void> {
393
+ if (!scenario.appraiseMetadata) {
394
+ return
395
+ }
396
+
397
+ const validNodeIds = new Set(scenario.appraiseMetadata.nodes.map(node => node.nodeId))
398
+
399
+ await prisma.testCaseFlowBlock.deleteMany({
400
+ where: { testCaseId },
401
+ })
402
+
403
+ if (scenario.appraiseMetadata.flowBlocks.length === 0) {
404
+ return
405
+ }
406
+
407
+ await prisma.testCase.update({
408
+ where: { id: testCaseId },
409
+ data: {
410
+ flowBlocks: {
411
+ create: scenario.appraiseMetadata.flowBlocks.map(block => ({
412
+ id: block.id,
413
+ name: block.name,
414
+ order: block.order,
415
+ nodes: {
416
+ create: block.nodeIds.filter(nodeId => validNodeIds.has(nodeId)).map(nodeId => ({ flowNodeId: nodeId })),
417
+ },
418
+ })),
419
+ },
420
+ },
421
+ })
422
+ }
423
+
336
424
  async function findExistingTestSuite(feature: ParsedFeature, moduleId: string): Promise<ExistingTestSuite | null> {
425
+ const suiteIdentifierTag = flattenFeatureTags(feature.tags).find(tag => tag.replace(/^@/, '').startsWith('ts_'))
426
+
427
+ if (suiteIdentifierTag) {
428
+ const suiteByTag = await prisma.testSuite.findFirst({
429
+ where: {
430
+ moduleId,
431
+ tags: {
432
+ some: {
433
+ tagExpression: suiteIdentifierTag,
434
+ },
435
+ },
436
+ },
437
+ include: testSuiteInclude,
438
+ })
439
+
440
+ if (suiteByTag) {
441
+ return suiteByTag
442
+ }
443
+ }
444
+
445
+ const filesystemKey = getTestSuiteFilesystemKey(extractTestSuiteNameFromFilename(feature.filePath))
446
+ const suitesInModule = await prisma.testSuite.findMany({
447
+ where: { moduleId },
448
+ include: testSuiteInclude,
449
+ })
450
+ const suiteByFilename = suitesInModule.find(suite => getTestSuiteFilesystemKey(suite.name) === filesystemKey)
451
+
452
+ if (suiteByFilename) {
453
+ return suiteByFilename
454
+ }
455
+
337
456
  return prisma.testSuite.findFirst({
338
457
  where: {
339
458
  name: feature.featureName,
340
- moduleId: moduleId,
341
- },
342
- include: {
343
- testCases: true,
459
+ moduleId,
344
460
  },
461
+ include: testSuiteInclude,
345
462
  })
346
463
  }
347
464
 
@@ -362,13 +479,29 @@ async function connectTagsToTestSuite(testSuiteId: string, tags?: string[]): Pro
362
479
  })
363
480
  }
364
481
 
482
+ function findExistingScenarioTestCase(
483
+ scenario: ParsedScenario,
484
+ testCases: ExistingTestSuite['testCases'],
485
+ ): ExistingTestSuite['testCases'][number] | undefined {
486
+ const identifierTag = flattenFeatureTags(scenario.tags).find(tag => tag.replace(/^@/, '').startsWith('tc_'))
487
+
488
+ if (identifierTag) {
489
+ return testCases.find(testCase => testCase.tags.some(tag => tag.tagExpression === identifierTag))
490
+ }
491
+
492
+ const { title } = parseScenarioTitle(scenario.name, scenario.description)
493
+ return testCases.find(testCase => testCase.title === title)
494
+ }
495
+
365
496
  async function addMissingScenariosToTestSuite(feature: ParsedFeature, testSuite: ExistingTestSuite): Promise<number> {
366
497
  let addedScenarios = 0
367
498
 
368
499
  for (const scenario of feature.scenarios) {
369
- const existingTestCase = testSuite.testCases.find(tc => tc.title === scenario.name)
500
+ const existingTestCase = findExistingScenarioTestCase(scenario, testSuite.testCases)
370
501
 
371
- if (!existingTestCase && (await addScenarioToTestSuite(scenario, testSuite.id))) {
502
+ if (existingTestCase) {
503
+ await updateExistingScenarioMetadata(existingTestCase.id, scenario)
504
+ } else if (await addScenarioToTestSuite(scenario, testSuite.id)) {
372
505
  addedScenarios++
373
506
  }
374
507
  }
@@ -383,10 +516,28 @@ async function addScenarioToTestSuite(scenario: ParsedScenario, testSuiteId: str
383
516
  return false
384
517
  }
385
518
 
386
- await createScenarioSteps(testCaseId, scenario.steps)
519
+ await createScenarioSteps(testCaseId, applyScenarioMetadataToSteps(scenario))
520
+ await replaceScenarioFlowBlocks(testCaseId, scenario)
387
521
  return true
388
522
  }
389
523
 
524
+ async function updateExistingScenarioMetadata(testCaseId: string, scenario: ParsedScenario): Promise<void> {
525
+ if (!scenario.appraiseMetadata) {
526
+ return
527
+ }
528
+
529
+ await prisma.testCase.update({
530
+ where: { id: testCaseId },
531
+ data: {
532
+ title: scenario.appraiseMetadata.title,
533
+ description: scenario.appraiseMetadata.description,
534
+ },
535
+ })
536
+
537
+ await createScenarioSteps(testCaseId, applyScenarioMetadataToSteps(scenario))
538
+ await replaceScenarioFlowBlocks(testCaseId, scenario)
539
+ }
540
+
390
541
  async function createTestSuiteWithScenarios(
391
542
  feature: ParsedFeature,
392
543
  moduleId: string,
@@ -395,8 +546,8 @@ async function createTestSuiteWithScenarios(
395
546
  addedScenarios: number
396
547
  }> {
397
548
  const testSuiteId = await findOrCreateTestSuite(
398
- feature.featureName,
399
- feature.featureDescription,
549
+ extractTestSuiteNameFromFilename(feature.filePath),
550
+ feature.featureDescription || feature.featureName,
400
551
  moduleId,
401
552
  feature.tags,
402
553
  )
@@ -1,6 +1,10 @@
1
1
  import { dirname } from 'path'
2
2
  import { spawnTask, taskSpawner, type SpawnedProcess, waitForTask, killTask } from '@/lib/process/task-spawner'
3
- import { getAutomationRunReportPath, toProjectRelativePath } from '@/lib/automation/automation-path-roots'
3
+ import {
4
+ buildJsonReportFormat,
5
+ getAutomationRunReportPath,
6
+ toProjectRelativePath,
7
+ } from '@/lib/automation/automation-path-roots'
4
8
  import { ensureAutomationWorkspaceReady } from '@/lib/automation/automation-workspace'
5
9
  import type { ExecutorAdapter, TestRunExecutionRequest, TestRunExecutionResult } from './types'
6
10
  import { processManager } from '@/lib/test-run/process-manager'
@@ -39,7 +43,7 @@ class LocalExecutorAdapter implements ExecutorAdapter {
39
43
  HEADLESS: headless.toString(),
40
44
  BROWSER: browserName,
41
45
  REPORT_PATH: reportPath,
42
- REPORT_FORMAT: `json:${reportPath}`,
46
+ REPORT_FORMAT: buildJsonReportFormat(reportPath),
43
47
  TEST_RUN_ID: testRunId,
44
48
  }
45
49