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.
- package/package.json +1 -1
- package/templates/blank/.env.example +2 -2
- package/templates/blank/.prettierrc +13 -13
- package/templates/blank/cucumber.mjs +12 -1
- package/templates/blank/e2e/helpers/test-data.ts +10 -0
- package/templates/blank/next-env.d.ts +6 -6
- package/templates/blank/package-lock.json +2 -2
- package/templates/blank/package.json +1 -1
- package/templates/blank/packages/cucumber-runtime/package.json +2 -1
- package/templates/blank/packages/cucumber-runtime/src/paths.ts +22 -5
- package/templates/blank/packages/locator-picker-companion/package.json +2 -1
- package/templates/blank/prisma/dev.db +0 -0
- package/templates/blank/prisma/migrations/20251104113456_add_type_for_template_step_groups/migration.sql +16 -16
- package/templates/blank/prisma/migrations/20251104170946_add_tags_to_test_suite_and_test_case/migration.sql +27 -27
- package/templates/blank/prisma/migrations/20251112190024_add_cascade_delete_to_test_run_test_case/migration.sql +17 -17
- package/templates/blank/prisma/migrations/20251113181100_add_test_run_log/migration.sql +12 -12
- package/templates/blank/prisma/migrations/20251119191838_add_tag_type/migration.sql +28 -28
- package/templates/blank/prisma/migrations/20251121164059_add_conflict_resolution/migration.sql +12 -12
- package/templates/blank/prisma/migrations/20251223183400_add_report_model_to_db_schema/migration.sql +10 -10
- package/templates/blank/prisma/migrations/20251223183637_add_report_test_case_entity_for_storing_test_results_for_individual_test_cases/migration.sql +10 -10
- package/templates/blank/prisma/migrations/20251224083549_add_comprehensive_report_storage/migration.sql +108 -108
- package/templates/blank/prisma/migrations/20251229194422_migrate_duration_to_string/migration.sql +55 -55
- package/templates/blank/prisma/migrations/20251230124637_add_unique_constraint_to_test_run_name/migration.sql +27 -27
- package/templates/blank/prisma/migrations/20260115094436_add_dashboard_metrics/migration.sql +59 -59
- package/templates/blank/prisma/migrations/20260127172022_add_cascade_delete_to_step_parameters/migration.sql +34 -34
- package/templates/blank/prisma/migrations/20260313093000_add_report_step_screenshot_path/migration.sql +1 -1
- package/templates/blank/scripts/setup-env.ts +0 -0
- package/templates/blank/scripts/sync-test-cases.ts +60 -10
- package/templates/blank/src/components/diagram/flow-diagram-node-search.tsx +9 -2
- package/templates/blank/src/components/diagram/flow-diagram-toolbar.tsx +37 -3
- package/templates/blank/src/components/diagram/flow-diagram.test.tsx +225 -0
- package/templates/blank/src/components/diagram/use-flow-diagram-search.ts +2 -0
- package/templates/blank/src/components/diagram/use-flow-diagram.ts +93 -0
- package/templates/blank/src/lib/appraise-test-case-metadata.test.ts +78 -0
- package/templates/blank/src/lib/appraise-test-case-metadata.ts +220 -0
- package/templates/blank/src/lib/automation/automation-path-roots.test.ts +14 -0
- package/templates/blank/src/lib/automation/automation-path-roots.ts +10 -2
- package/templates/blank/src/lib/database-sync.ts +166 -15
- package/templates/blank/src/lib/executor/local-executor-adapter.ts +6 -2
- package/templates/blank/src/lib/feature-file-generator.ts +54 -10
- package/templates/blank/src/lib/gherkin-parser.test.ts +52 -0
- package/templates/blank/src/lib/gherkin-parser.ts +39 -1
- package/templates/blank/src/lib/sync/projected-feature-utils.ts +5 -1
- package/templates/blank/src/lib/sync/sync-pending-counts.test.ts +115 -0
- package/templates/blank/src/lib/sync/sync-pending-counts.ts +108 -13
- package/templates/blank/src/services/test-run/test-run-service.test.ts +10 -0
- package/templates/blank/src/services/test-run/test-run-service.ts +41 -1
- package/templates/starter/.env.example +2 -2
- package/templates/starter/.prettierrc +13 -13
- package/templates/starter/cucumber.mjs +12 -1
- package/templates/starter/e2e/helpers/test-data.ts +10 -0
- package/templates/starter/next-env.d.ts +6 -6
- package/templates/starter/package-lock.json +2 -2
- package/templates/starter/package.json +1 -1
- package/templates/starter/packages/cucumber-runtime/package.json +2 -1
- package/templates/starter/packages/cucumber-runtime/src/paths.ts +22 -5
- package/templates/starter/packages/locator-picker-companion/package.json +2 -1
- package/templates/starter/prisma/dev.db +0 -0
- package/templates/starter/prisma/migrations/20251104113456_add_type_for_template_step_groups/migration.sql +16 -16
- package/templates/starter/prisma/migrations/20251104170946_add_tags_to_test_suite_and_test_case/migration.sql +27 -27
- package/templates/starter/prisma/migrations/20251112190024_add_cascade_delete_to_test_run_test_case/migration.sql +17 -17
- package/templates/starter/prisma/migrations/20251113181100_add_test_run_log/migration.sql +12 -12
- package/templates/starter/prisma/migrations/20251119191838_add_tag_type/migration.sql +28 -28
- package/templates/starter/prisma/migrations/20251121164059_add_conflict_resolution/migration.sql +12 -12
- package/templates/starter/prisma/migrations/20251223183400_add_report_model_to_db_schema/migration.sql +10 -10
- package/templates/starter/prisma/migrations/20251223183637_add_report_test_case_entity_for_storing_test_results_for_individual_test_cases/migration.sql +10 -10
- package/templates/starter/prisma/migrations/20251224083549_add_comprehensive_report_storage/migration.sql +108 -108
- package/templates/starter/prisma/migrations/20251229194422_migrate_duration_to_string/migration.sql +55 -55
- package/templates/starter/prisma/migrations/20251230124637_add_unique_constraint_to_test_run_name/migration.sql +27 -27
- package/templates/starter/prisma/migrations/20260115094436_add_dashboard_metrics/migration.sql +59 -59
- package/templates/starter/prisma/migrations/20260127172022_add_cascade_delete_to_step_parameters/migration.sql +34 -34
- package/templates/starter/prisma/migrations/20260313093000_add_report_step_screenshot_path/migration.sql +1 -1
- package/templates/starter/scripts/setup-env.ts +0 -0
- package/templates/starter/scripts/sync-test-cases.ts +60 -10
- package/templates/starter/src/components/diagram/flow-diagram-node-search.tsx +9 -2
- package/templates/starter/src/components/diagram/flow-diagram-toolbar.tsx +37 -3
- package/templates/starter/src/components/diagram/flow-diagram.test.tsx +225 -0
- package/templates/starter/src/components/diagram/use-flow-diagram-search.ts +2 -0
- package/templates/starter/src/components/diagram/use-flow-diagram.ts +93 -0
- package/templates/starter/src/lib/appraise-test-case-metadata.test.ts +78 -0
- package/templates/starter/src/lib/appraise-test-case-metadata.ts +220 -0
- package/templates/starter/src/lib/automation/automation-path-roots.test.ts +14 -0
- package/templates/starter/src/lib/automation/automation-path-roots.ts +10 -2
- package/templates/starter/src/lib/database-sync.ts +166 -15
- package/templates/starter/src/lib/executor/local-executor-adapter.ts +6 -2
- package/templates/starter/src/lib/feature-file-generator.ts +54 -10
- package/templates/starter/src/lib/gherkin-parser.test.ts +52 -0
- package/templates/starter/src/lib/gherkin-parser.ts +39 -1
- package/templates/starter/src/lib/sync/projected-feature-utils.ts +5 -1
- package/templates/starter/src/lib/sync/sync-pending-counts.test.ts +115 -0
- package/templates/starter/src/lib/sync/sync-pending-counts.ts +108 -13
- package/templates/starter/src/services/test-run/test-run-service.test.ts +10 -0
- package/templates/starter/src/services/test-run/test-run-service.ts +41 -1
- package/dist/cli.e2e.test.d.ts +0 -2
- package/dist/cli.e2e.test.d.ts.map +0 -1
- package/dist/cli.e2e.test.js +0 -75
- package/dist/cli.e2e.test.js.map +0 -1
- package/dist/config.test.d.ts +0 -2
- package/dist/config.test.d.ts.map +0 -1
- package/dist/config.test.js +0 -65
- package/dist/config.test.js.map +0 -1
- package/dist/copy-template.test.d.ts +0 -2
- package/dist/copy-template.test.d.ts.map +0 -1
- package/dist/copy-template.test.js +0 -71
- package/dist/copy-template.test.js.map +0 -1
- package/dist/download-repo.test.d.ts +0 -2
- package/dist/download-repo.test.d.ts.map +0 -1
- package/dist/download-repo.test.js +0 -16
- package/dist/download-repo.test.js.map +0 -1
- package/dist/install.test.d.ts +0 -2
- package/dist/install.test.d.ts.map +0 -1
- package/dist/install.test.js +0 -120
- package/dist/install.test.js.map +0 -1
- package/dist/prompts.test.d.ts +0 -2
- package/dist/prompts.test.d.ts.map +0 -1
- package/dist/prompts.test.js +0 -58
- package/dist/prompts.test.js.map +0 -1
- package/templates/default/next-env.d.ts +0 -6
- package/templates/default/packages/locator-picker-companion/dist/cli.d.ts +0 -1
- package/templates/default/packages/locator-picker-companion/dist/cli.js +0 -336
- package/templates/default/packages/locator-picker-companion/dist/index.d.ts +0 -3
- package/templates/default/packages/locator-picker-companion/dist/index.js +0 -3
- package/templates/default/packages/locator-picker-companion/dist/injected-picker-script.d.ts +0 -1
- package/templates/default/packages/locator-picker-companion/dist/injected-picker-script.js +0 -660
- package/templates/default/packages/locator-picker-companion/dist/launcher.d.ts +0 -14
- package/templates/default/packages/locator-picker-companion/dist/launcher.js +0 -58
- package/templates/default/packages/locator-picker-companion/dist/selector-generator.d.ts +0 -6
- package/templates/default/packages/locator-picker-companion/dist/selector-generator.js +0 -261
- package/templates/default/packages/locator-picker-companion/dist/session-file.d.ts +0 -30
- package/templates/default/packages/locator-picker-companion/dist/session-file.js +0 -162
- package/templates/default/packages/locator-picker-companion/dist/types.d.ts +0 -31
- package/templates/default/packages/locator-picker-companion/dist/types.js +0 -1
- 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
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
500
|
+
const existingTestCase = findExistingScenarioTestCase(scenario, testSuite.testCases)
|
|
370
501
|
|
|
371
|
-
if (
|
|
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
|
|
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.
|
|
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 {
|
|
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:
|
|
46
|
+
REPORT_FORMAT: buildJsonReportFormat(reportPath),
|
|
43
47
|
TEST_RUN_ID: testRunId,
|
|
44
48
|
}
|
|
45
49
|
|