create-appraisejs 0.2.0-alpha.1 → 0.2.0-alpha.2
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/default/.appraise-template-meta.json +2 -2
- package/templates/default/package.json +1 -1
- package/templates/default/prisma/dev.db +0 -0
- package/templates/default/src/lib/gherkin-parser.test.ts +44 -0
- package/templates/default/src/lib/gherkin-parser.ts +253 -259
- package/templates/default/src/lib/sync/sync-pending-counts.test.ts +24 -0
- package/templates/default/src/lib/sync/sync-pending-counts.ts +3 -2
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"preparedAt": "2026-03-
|
|
3
|
-
"inputHash": "
|
|
2
|
+
"preparedAt": "2026-03-23T19:35:53.852Z",
|
|
3
|
+
"inputHash": "165288718820de7247b9067d6c4f1a78057520fb791c4b28fa781f2c30c95ea3",
|
|
4
4
|
"databasePath": "prisma/dev.db"
|
|
5
5
|
}
|
|
Binary file
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { promises as fs } from 'fs'
|
|
4
|
+
import { join } from 'path'
|
|
5
|
+
import { tmpdir } from 'os'
|
|
6
|
+
import { parseFeatureFile } from '@/lib/gherkin-parser'
|
|
7
|
+
|
|
8
|
+
async function withTempFeatureFile(content: string): Promise<string> {
|
|
9
|
+
const dir = await fs.mkdtemp(join(tmpdir(), 'gherkin-parser-'))
|
|
10
|
+
const filePath = join(dir, 'sample.feature')
|
|
11
|
+
await fs.writeFile(filePath, content, 'utf8')
|
|
12
|
+
return filePath
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('uses Feature line text as feature description', async () => {
|
|
16
|
+
const filePath = await withTempFeatureFile(`
|
|
17
|
+
@smoke
|
|
18
|
+
Feature: Login workflow
|
|
19
|
+
|
|
20
|
+
Scenario: logs in
|
|
21
|
+
Given user opens app
|
|
22
|
+
`)
|
|
23
|
+
|
|
24
|
+
const parsed = await parseFeatureFile(filePath)
|
|
25
|
+
|
|
26
|
+
assert.ok(parsed)
|
|
27
|
+
assert.equal(parsed?.featureName, 'Login workflow')
|
|
28
|
+
assert.equal(parsed?.featureDescription, 'Login workflow')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('keeps Feature line as description even when free text follows', async () => {
|
|
32
|
+
const filePath = await withTempFeatureFile(`
|
|
33
|
+
Feature: Checkout flow
|
|
34
|
+
Legacy block text that should not override the description
|
|
35
|
+
|
|
36
|
+
Scenario: buys item
|
|
37
|
+
Given user adds item to cart
|
|
38
|
+
`)
|
|
39
|
+
|
|
40
|
+
const parsed = await parseFeatureFile(filePath)
|
|
41
|
+
|
|
42
|
+
assert.ok(parsed)
|
|
43
|
+
assert.equal(parsed?.featureDescription, 'Checkout flow')
|
|
44
|
+
})
|
|
@@ -1,259 +1,253 @@
|
|
|
1
|
-
import { promises as fs } from 'fs'
|
|
2
|
-
import { join, relative } from 'path'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Represents a parsed feature file with its scenarios and steps
|
|
6
|
-
*/
|
|
7
|
-
export interface ParsedFeature {
|
|
8
|
-
filePath: string
|
|
9
|
-
featureName: string
|
|
10
|
-
featureDescription?: string
|
|
11
|
-
tags: string[]
|
|
12
|
-
scenarios: ParsedScenario[]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Represents a parsed scenario from a feature file
|
|
17
|
-
*/
|
|
18
|
-
export interface ParsedScenario {
|
|
19
|
-
name: string
|
|
20
|
-
description?: string
|
|
21
|
-
tags: string[]
|
|
22
|
-
steps: ParsedStep[]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Represents a parsed step from a feature file
|
|
27
|
-
*/
|
|
28
|
-
export interface ParsedStep {
|
|
29
|
-
keyword: string
|
|
30
|
-
text: string
|
|
31
|
-
order: number
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Parses a Gherkin feature file and extracts scenarios and steps
|
|
36
|
-
* @param filePath - Path to the feature file
|
|
37
|
-
* @returns Promise<ParsedFeature | null> - Parsed feature data or null if parsing fails
|
|
38
|
-
*/
|
|
39
|
-
export async function parseFeatureFile(filePath: string): Promise<ParsedFeature | null> {
|
|
40
|
-
try {
|
|
41
|
-
const content = await fs.readFile(filePath, 'utf-8')
|
|
42
|
-
|
|
43
|
-
// Simple Gherkin parser implementation
|
|
44
|
-
const lines = content.split('\n').map(line => line.trim())
|
|
45
|
-
const scenarios: ParsedScenario[] = []
|
|
46
|
-
|
|
47
|
-
let featureName = ''
|
|
48
|
-
let featureDescription = ''
|
|
49
|
-
const featureTags: string[] = []
|
|
50
|
-
let currentScenario: ParsedScenario | null = null
|
|
51
|
-
let stepOrder = 1
|
|
52
|
-
|
|
53
|
-
// Find feature line and extract tags before it
|
|
54
|
-
let _featureLineIndex = -1
|
|
55
|
-
for (let i = 0; i < lines.length; i++) {
|
|
56
|
-
if (lines[i].startsWith('Feature:')) {
|
|
57
|
-
_featureLineIndex = i
|
|
58
|
-
// Look backwards for tags (skip comments and empty lines)
|
|
59
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
60
|
-
const prevLine = lines[j]
|
|
61
|
-
if (prevLine === '' || prevLine.startsWith('#')) {
|
|
62
|
-
continue
|
|
63
|
-
}
|
|
64
|
-
if (prevLine.startsWith('@')) {
|
|
65
|
-
featureTags.unshift(prevLine) // Add to beginning to maintain order
|
|
66
|
-
} else {
|
|
67
|
-
break // Stop when we hit a non-tag line
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
break
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
for (let i = 0; i < lines.length; i++) {
|
|
75
|
-
const line = lines[i]
|
|
76
|
-
|
|
77
|
-
// Skip comments and empty lines
|
|
78
|
-
if (line.startsWith('#') || line === '') {
|
|
79
|
-
continue
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Parse Feature line
|
|
83
|
-
if (line.startsWith('Feature:')) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
.replace(
|
|
251
|
-
.replace(
|
|
252
|
-
.trim()
|
|
253
|
-
|
|
254
|
-
return cleanName
|
|
255
|
-
.toLowerCase()
|
|
256
|
-
.replace(/[^a-z0-9\s]+/g, '')
|
|
257
|
-
.replace(/\s+/g, ' ')
|
|
258
|
-
.trim()
|
|
259
|
-
}
|
|
1
|
+
import { promises as fs } from 'fs'
|
|
2
|
+
import { join, relative } from 'path'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Represents a parsed feature file with its scenarios and steps
|
|
6
|
+
*/
|
|
7
|
+
export interface ParsedFeature {
|
|
8
|
+
filePath: string
|
|
9
|
+
featureName: string
|
|
10
|
+
featureDescription?: string
|
|
11
|
+
tags: string[]
|
|
12
|
+
scenarios: ParsedScenario[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Represents a parsed scenario from a feature file
|
|
17
|
+
*/
|
|
18
|
+
export interface ParsedScenario {
|
|
19
|
+
name: string
|
|
20
|
+
description?: string
|
|
21
|
+
tags: string[]
|
|
22
|
+
steps: ParsedStep[]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Represents a parsed step from a feature file
|
|
27
|
+
*/
|
|
28
|
+
export interface ParsedStep {
|
|
29
|
+
keyword: string
|
|
30
|
+
text: string
|
|
31
|
+
order: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parses a Gherkin feature file and extracts scenarios and steps
|
|
36
|
+
* @param filePath - Path to the feature file
|
|
37
|
+
* @returns Promise<ParsedFeature | null> - Parsed feature data or null if parsing fails
|
|
38
|
+
*/
|
|
39
|
+
export async function parseFeatureFile(filePath: string): Promise<ParsedFeature | null> {
|
|
40
|
+
try {
|
|
41
|
+
const content = await fs.readFile(filePath, 'utf-8')
|
|
42
|
+
|
|
43
|
+
// Simple Gherkin parser implementation
|
|
44
|
+
const lines = content.split('\n').map(line => line.trim())
|
|
45
|
+
const scenarios: ParsedScenario[] = []
|
|
46
|
+
|
|
47
|
+
let featureName = ''
|
|
48
|
+
let featureDescription = ''
|
|
49
|
+
const featureTags: string[] = []
|
|
50
|
+
let currentScenario: ParsedScenario | null = null
|
|
51
|
+
let stepOrder = 1
|
|
52
|
+
|
|
53
|
+
// Find feature line and extract tags before it
|
|
54
|
+
let _featureLineIndex = -1
|
|
55
|
+
for (let i = 0; i < lines.length; i++) {
|
|
56
|
+
if (lines[i].startsWith('Feature:')) {
|
|
57
|
+
_featureLineIndex = i
|
|
58
|
+
// Look backwards for tags (skip comments and empty lines)
|
|
59
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
60
|
+
const prevLine = lines[j]
|
|
61
|
+
if (prevLine === '' || prevLine.startsWith('#')) {
|
|
62
|
+
continue
|
|
63
|
+
}
|
|
64
|
+
if (prevLine.startsWith('@')) {
|
|
65
|
+
featureTags.unshift(prevLine) // Add to beginning to maintain order
|
|
66
|
+
} else {
|
|
67
|
+
break // Stop when we hit a non-tag line
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < lines.length; i++) {
|
|
75
|
+
const line = lines[i]
|
|
76
|
+
|
|
77
|
+
// Skip comments and empty lines
|
|
78
|
+
if (line.startsWith('#') || line === '') {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Parse Feature line
|
|
83
|
+
if (line.startsWith('Feature:')) {
|
|
84
|
+
const featureLineText = line.replace('Feature:', '').trim()
|
|
85
|
+
featureName = featureLineText
|
|
86
|
+
featureDescription = featureLineText
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Parse Scenario line
|
|
91
|
+
if (line.startsWith('Scenario:')) {
|
|
92
|
+
// Save previous scenario if exists
|
|
93
|
+
if (currentScenario) {
|
|
94
|
+
scenarios.push(currentScenario)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Extract tags before this scenario
|
|
98
|
+
const scenarioTags: string[] = []
|
|
99
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
100
|
+
const prevLine = lines[j]
|
|
101
|
+
if (prevLine === '' || prevLine.startsWith('#')) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
if (prevLine.startsWith('@')) {
|
|
105
|
+
scenarioTags.unshift(prevLine) // Add to beginning to maintain order
|
|
106
|
+
} else {
|
|
107
|
+
break // Stop when we hit a non-tag line
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const scenarioText = line.replace('Scenario:', '').trim()
|
|
112
|
+
const [name, description] =
|
|
113
|
+
scenarioText.split(']').length > 1
|
|
114
|
+
? [scenarioText.split(']')[1].trim(), scenarioText.split(']')[0].replace('[', '').trim()]
|
|
115
|
+
: [scenarioText, '']
|
|
116
|
+
|
|
117
|
+
currentScenario = {
|
|
118
|
+
name: name,
|
|
119
|
+
description: description || undefined,
|
|
120
|
+
tags: scenarioTags,
|
|
121
|
+
steps: [],
|
|
122
|
+
}
|
|
123
|
+
stepOrder = 1
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Parse steps (Given, When, Then, And, But)
|
|
128
|
+
if (
|
|
129
|
+
currentScenario &&
|
|
130
|
+
(line.startsWith('Given ') ||
|
|
131
|
+
line.startsWith('When ') ||
|
|
132
|
+
line.startsWith('Then ') ||
|
|
133
|
+
line.startsWith('And ') ||
|
|
134
|
+
line.startsWith('But '))
|
|
135
|
+
) {
|
|
136
|
+
const keyword = line.split(' ')[0]
|
|
137
|
+
const text = line.substring(keyword.length).trim()
|
|
138
|
+
|
|
139
|
+
currentScenario.steps.push({
|
|
140
|
+
keyword: keyword,
|
|
141
|
+
text: text,
|
|
142
|
+
order: stepOrder++,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Add the last scenario
|
|
148
|
+
if (currentScenario) {
|
|
149
|
+
scenarios.push(currentScenario)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!featureName) {
|
|
153
|
+
console.warn(`No feature found in file: ${filePath}`)
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
filePath,
|
|
159
|
+
featureName,
|
|
160
|
+
featureDescription: featureDescription || undefined,
|
|
161
|
+
tags: featureTags,
|
|
162
|
+
scenarios,
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
console.error(`Error parsing feature file ${filePath}:`, error)
|
|
166
|
+
return null
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Scans a directory for feature files and parses them
|
|
172
|
+
* @param directoryPath - Path to scan for feature files
|
|
173
|
+
* @returns Promise<ParsedFeature[]> - Array of parsed feature files
|
|
174
|
+
*/
|
|
175
|
+
export async function scanFeatureFiles(directoryPath: string): Promise<ParsedFeature[]> {
|
|
176
|
+
const parsedFeatures: ParsedFeature[] = []
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const entries = await fs.readdir(directoryPath, { withFileTypes: true })
|
|
180
|
+
|
|
181
|
+
for (const entry of entries) {
|
|
182
|
+
const fullPath = join(directoryPath, entry.name)
|
|
183
|
+
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
// Recursively scan subdirectories
|
|
186
|
+
const subFeatures = await scanFeatureFiles(fullPath)
|
|
187
|
+
parsedFeatures.push(...subFeatures)
|
|
188
|
+
} else if (entry.isFile() && entry.name.endsWith('.feature')) {
|
|
189
|
+
// Parse feature file
|
|
190
|
+
const parsedFeature = await parseFeatureFile(fullPath)
|
|
191
|
+
if (parsedFeature) {
|
|
192
|
+
parsedFeatures.push(parsedFeature)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error(`Error scanning directory ${directoryPath}:`, error)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return parsedFeatures
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Extracts module path from feature file path
|
|
205
|
+
* Works cross-platform (Windows, Mac, Linux)
|
|
206
|
+
* @param featureFilePath - Full path to the feature file
|
|
207
|
+
* @param featuresBaseDir - Base directory for features
|
|
208
|
+
* @returns string - Module path (e.g., "/module1/submodule")
|
|
209
|
+
*/
|
|
210
|
+
export function extractModulePathFromFilePath(featureFilePath: string, featuresBaseDir: string): string {
|
|
211
|
+
// Use path.relative for cross-platform path handling
|
|
212
|
+
const relativePath = relative(featuresBaseDir, featureFilePath)
|
|
213
|
+
|
|
214
|
+
// Normalize to forward slashes for module path format (database uses /)
|
|
215
|
+
const normalizedPath = relativePath.replace(/\\/g, '/')
|
|
216
|
+
const pathParts = normalizedPath.split('/').filter(part => part && part !== '')
|
|
217
|
+
|
|
218
|
+
// Remove the filename and join the remaining parts
|
|
219
|
+
const moduleParts = pathParts.slice(0, -1)
|
|
220
|
+
return moduleParts.length > 0 ? '/' + moduleParts.join('/') : '/'
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Generates a safe test suite name from feature name
|
|
225
|
+
* @param featureName - Name of the feature
|
|
226
|
+
* @returns string - Safe test suite name
|
|
227
|
+
*/
|
|
228
|
+
export function generateSafeTestSuiteName(featureName: string): string {
|
|
229
|
+
return featureName
|
|
230
|
+
.toLowerCase()
|
|
231
|
+
.replace(/[^a-z0-9\s]+/g, '')
|
|
232
|
+
.replace(/\s+/g, ' ')
|
|
233
|
+
.trim()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Generates a safe test case name from scenario name
|
|
238
|
+
* @param scenarioName - Name of the scenario
|
|
239
|
+
* @returns string - Safe test case name
|
|
240
|
+
*/
|
|
241
|
+
export function generateSafeTestCaseName(scenarioName: string): string {
|
|
242
|
+
// Remove scenario prefix if present and clean up the name
|
|
243
|
+
const cleanName = scenarioName
|
|
244
|
+
.replace(/^Scenario:\s*/i, '')
|
|
245
|
+
.replace(/^\[.*?\]\s*/, '') // Remove [brackets] prefix
|
|
246
|
+
.trim()
|
|
247
|
+
|
|
248
|
+
return cleanName
|
|
249
|
+
.toLowerCase()
|
|
250
|
+
.replace(/[^a-z0-9\s]+/g, '')
|
|
251
|
+
.replace(/\s+/g, ' ')
|
|
252
|
+
.trim()
|
|
253
|
+
}
|
|
@@ -123,6 +123,30 @@ test('matches test suites by generated filesystem key instead of raw DB name', (
|
|
|
123
123
|
assert.equal(count, 0)
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
+
test('matches test suites when DB description is null and feature uses suite name', () => {
|
|
127
|
+
const count = countTestSuiteMismatches(
|
|
128
|
+
[
|
|
129
|
+
{
|
|
130
|
+
name: 'user-login-suite',
|
|
131
|
+
description: 'User Login Suite',
|
|
132
|
+
modulePath: '/auth',
|
|
133
|
+
tags: [],
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
[
|
|
137
|
+
{
|
|
138
|
+
name: 'User Login Suite',
|
|
139
|
+
description: null,
|
|
140
|
+
moduleId: 'module-auth',
|
|
141
|
+
tags: [],
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
new Map([['module-auth', '/auth']]),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
assert.equal(count, 0)
|
|
148
|
+
})
|
|
149
|
+
|
|
126
150
|
test('matches projected test cases against generated feature-file output', () => {
|
|
127
151
|
const count = countTestCaseMismatches(
|
|
128
152
|
[
|
|
@@ -825,7 +825,7 @@ async function buildFilesystemSnapshot(baseDir: string): Promise<FilesystemSnaps
|
|
|
825
825
|
|
|
826
826
|
const testSuites: TestSuiteFromFs[] = parsedFeatures.map(feature => ({
|
|
827
827
|
name: extractTestSuiteNameFromFilename(feature.filePath),
|
|
828
|
-
description: feature.featureDescription ?? null,
|
|
828
|
+
description: feature.featureDescription ?? feature.featureName ?? null,
|
|
829
829
|
modulePath: extractModulePathFromFilePath(feature.filePath, featuresDir),
|
|
830
830
|
tags: extractFeatureLevelTags(feature),
|
|
831
831
|
}))
|
|
@@ -1099,7 +1099,8 @@ export function countTestSuiteMismatches(
|
|
|
1099
1099
|
const fsTagExpressions = suite.tags.map(normalizeTagExpression)
|
|
1100
1100
|
const hasMatch = (dbByKey.get(suiteKey) ?? []).some(existing => {
|
|
1101
1101
|
const dbTagExpressions = existing.tags.map(tag => normalizeTagExpression(tag.tagExpression))
|
|
1102
|
-
|
|
1102
|
+
const expectedDescription = existing.description ?? existing.name
|
|
1103
|
+
return expectedDescription === (suite.description ?? null) && sameStringSet(dbTagExpressions, fsTagExpressions)
|
|
1103
1104
|
})
|
|
1104
1105
|
|
|
1105
1106
|
if (!hasMatch) {
|