first commit
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
// Extract step definitions from feature files and generate a data file with source code
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
interface StepDefinition {
|
||||
pattern: string;
|
||||
keyword: 'Given' | 'When' | 'Then';
|
||||
file: string;
|
||||
sourceCode: string;
|
||||
lineNumber: number;
|
||||
}
|
||||
|
||||
const stepFiles = [
|
||||
'features/step_definitions/navigation.steps.ts',
|
||||
'features/step_definitions/form.steps.ts',
|
||||
'features/step_definitions/screen.steps.ts',
|
||||
];
|
||||
|
||||
function extractStepDefinitions(): StepDefinition[] {
|
||||
const definitions: StepDefinition[] = [];
|
||||
|
||||
for (const filePath of stepFiles) {
|
||||
const fullPath = path.join(process.cwd(), filePath);
|
||||
if (!fs.existsSync(fullPath)) continue;
|
||||
|
||||
const content = fs.readFileSync(fullPath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
const fileName = path.basename(filePath);
|
||||
|
||||
// Find step definitions line by line
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Match Given/When/Then at the start of a line
|
||||
const match = line.match(/^(Given|When|Then)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
||||
if (match) {
|
||||
const keyword = match[1] as 'Given' | 'When' | 'Then';
|
||||
const pattern = match[2];
|
||||
|
||||
// Extract the full function body
|
||||
const sourceCode = extractFunctionBody(lines, i);
|
||||
|
||||
definitions.push({
|
||||
pattern,
|
||||
keyword,
|
||||
file: fileName,
|
||||
sourceCode,
|
||||
lineNumber: i + 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return definitions;
|
||||
}
|
||||
|
||||
function extractFunctionBody(lines: string[], startLine: number): string {
|
||||
// Look for the closing }); which marks the end of a step definition
|
||||
for (let i = startLine; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line === '});' || line.endsWith('});')) {
|
||||
const extracted = lines.slice(startLine, i + 1);
|
||||
return extracted.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return just the start line if we couldn't find the end
|
||||
return lines[startLine] || '';
|
||||
}
|
||||
|
||||
async function generateStepDefinitionsFile() {
|
||||
const definitions = extractStepDefinitions();
|
||||
|
||||
const findFunctionCode = `export function findStepDefinition(stepText: string): StepDefinitionInfo | null {
|
||||
for (const def of stepDefinitions) {
|
||||
// Convert Cucumber expression to regex
|
||||
// {string} -> "[^"]+"
|
||||
// {int} -> \\\\d+
|
||||
const regexPattern = def.pattern
|
||||
.replace(/\\{string\\}/g, '"[^"]+"')
|
||||
.replace(/\\{int\\}/g, '\\\\d+');
|
||||
|
||||
try {
|
||||
const regex = new RegExp(regexPattern);
|
||||
if (regex.test(stepText)) {
|
||||
return def;
|
||||
}
|
||||
} catch {
|
||||
// If pattern fails, try simple includes
|
||||
const simplified = def.pattern.replace(/\\{string\\}/g, '').replace(/\\{int\\}/g, '').trim();
|
||||
if (stepText.includes(simplified)) {
|
||||
return def;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}`;
|
||||
|
||||
const output = `// Auto-generated by scripts/extract-step-definitions.ts
|
||||
// Do not edit manually - run "bun run steps:extract" to regenerate
|
||||
|
||||
export interface StepDefinitionInfo {
|
||||
pattern: string;
|
||||
keyword: 'Given' | 'When' | 'Then';
|
||||
file: string;
|
||||
sourceCode: string;
|
||||
lineNumber: number;
|
||||
}
|
||||
|
||||
export const stepDefinitions: StepDefinitionInfo[] = ${JSON.stringify(definitions, null, 2)};
|
||||
|
||||
${findFunctionCode}
|
||||
`;
|
||||
|
||||
await Bun.write('src/data/stepDefinitions.ts', output);
|
||||
console.log(`Generated ${definitions.length} step definitions`);
|
||||
}
|
||||
|
||||
generateStepDefinitionsFile().catch(console.error);
|
||||
@@ -0,0 +1,195 @@
|
||||
import { Glob } from 'bun';
|
||||
import type { ParsedFeature, ParsedScenario, ParsedStep } from '../src/types/gherkin';
|
||||
|
||||
async function parseFeatures(): Promise<ParsedFeature[]> {
|
||||
const glob = new Glob('features/**/*.feature');
|
||||
const features: ParsedFeature[] = [];
|
||||
|
||||
for await (const filePath of glob.scan('.')) {
|
||||
const content = await Bun.file(filePath).text();
|
||||
const parsed = parseGherkinContent(content, filePath);
|
||||
if (parsed) {
|
||||
features.push(parsed);
|
||||
}
|
||||
}
|
||||
|
||||
features.sort((a, b) => {
|
||||
if (a.priority !== b.priority) return a.priority - b.priority;
|
||||
return a.category.localeCompare(b.category);
|
||||
});
|
||||
|
||||
const output = `// Auto-generated by scripts/parse-features.ts
|
||||
// Do not edit manually
|
||||
import type { ParsedFeature } from '../types/gherkin';
|
||||
|
||||
export const parsedFeatures: ParsedFeature[] = ${JSON.stringify(features, null, 2)};
|
||||
|
||||
export function getFeatureById(id: string): ParsedFeature | undefined {
|
||||
return parsedFeatures.find(f => f.id === id);
|
||||
}
|
||||
|
||||
export function getFeaturesByCategory(category: string): ParsedFeature[] {
|
||||
return parsedFeatures.filter(f => f.category === category);
|
||||
}
|
||||
|
||||
export function getFeaturesByPriority(priority: number): ParsedFeature[] {
|
||||
return parsedFeatures.filter(f => f.priority === priority);
|
||||
}
|
||||
|
||||
export function getAllCategories(): string[] {
|
||||
return [...new Set(parsedFeatures.map(f => f.category))];
|
||||
}
|
||||
|
||||
export function getAllPriorities(): number[] {
|
||||
return [...new Set(parsedFeatures.map(f => f.priority))].sort((a, b) => a - b);
|
||||
}
|
||||
`;
|
||||
|
||||
await Bun.write('src/data/features.ts', output);
|
||||
console.log(`Parsed ${features.length} feature files`);
|
||||
return features;
|
||||
}
|
||||
|
||||
function parseGherkinContent(content: string, filePath: string): ParsedFeature | null {
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Extract tags from the beginning
|
||||
const tagLines: string[] = [];
|
||||
let contentStartIndex = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith('#')) {
|
||||
contentStartIndex = i + 1;
|
||||
continue;
|
||||
}
|
||||
if (line.startsWith('@')) {
|
||||
tagLines.push(line);
|
||||
contentStartIndex = i + 1;
|
||||
} else if (line !== '') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const tagMatches = tagLines.join(' ').match(/@[\w-]+/g) || [];
|
||||
|
||||
// Extract category and priority from tags
|
||||
const categoryTag = tagMatches.find(t =>
|
||||
['@WORKSHOP', '@EVENT', '@USER', '@MEETING', '@NOTIF'].includes(t)
|
||||
);
|
||||
const category = categoryTag ? categoryTag.slice(1) : 'UNKNOWN';
|
||||
|
||||
const priorityTag = tagMatches.find(t => t.startsWith('@priority-'));
|
||||
const priority = priorityTag ? parseInt(priorityTag.replace('@priority-', '')) : 3;
|
||||
|
||||
// Extract feature name
|
||||
const featureMatch = content.match(/Fonctionnalité:\s*(.+?)(?:\n|$)/);
|
||||
const name = featureMatch?.[1]?.trim() || 'Unknown Feature';
|
||||
|
||||
// Extract US ID from name
|
||||
const idMatch = name.match(/US-(\d+)/i);
|
||||
const id = idMatch ? `us-${idMatch[1]}` : filePath.replace(/.*\//, '').replace('.feature', '');
|
||||
|
||||
// Extract description (lines after Feature until Contexte or Scénario)
|
||||
const descLines: string[] = [];
|
||||
let inDescription = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.startsWith('Fonctionnalité:')) {
|
||||
inDescription = true;
|
||||
continue;
|
||||
}
|
||||
if (inDescription) {
|
||||
if (trimmed.startsWith('Contexte:') || trimmed.startsWith('Scénario:') || trimmed.startsWith('Plan du Scénario:')) {
|
||||
break;
|
||||
}
|
||||
if (trimmed && !trimmed.startsWith('@') && !trimmed.startsWith('#')) {
|
||||
descLines.push(trimmed);
|
||||
}
|
||||
}
|
||||
}
|
||||
const description = descLines.join(' ').trim();
|
||||
|
||||
// Parse scenarios
|
||||
const scenarios: ParsedScenario[] = [];
|
||||
let currentScenario: ParsedScenario | null = null;
|
||||
let inBackground = false;
|
||||
const background: ParsedStep[] = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
if (trimmed.startsWith('Contexte:')) {
|
||||
inBackground = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('Scénario:') || trimmed.startsWith('Plan du Scénario:')) {
|
||||
inBackground = false;
|
||||
if (currentScenario) {
|
||||
scenarios.push(currentScenario);
|
||||
}
|
||||
const scenarioName = trimmed.replace(/^(Scénario:|Plan du Scénario:)\s*/, '').trim();
|
||||
currentScenario = {
|
||||
name: scenarioName,
|
||||
tags: [],
|
||||
steps: [],
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse steps
|
||||
const stepKeywords = ['Étant donné', 'Etant donné', 'Quand', 'Lorsque', 'Alors', 'Et', 'Mais'];
|
||||
for (const keyword of stepKeywords) {
|
||||
if (trimmed.startsWith(keyword)) {
|
||||
const step: ParsedStep = {
|
||||
keyword,
|
||||
text: trimmed.slice(keyword.length).trim(),
|
||||
};
|
||||
|
||||
if (inBackground) {
|
||||
background.push(step);
|
||||
} else if (currentScenario) {
|
||||
currentScenario.steps.push(step);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last scenario
|
||||
if (currentScenario) {
|
||||
scenarios.push(currentScenario);
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
tags: tagMatches,
|
||||
category,
|
||||
priority,
|
||||
background: background.length > 0 ? background : undefined,
|
||||
scenarios,
|
||||
filePath,
|
||||
rawContent: content,
|
||||
};
|
||||
}
|
||||
|
||||
// Run parser
|
||||
parseFeatures().then(features => {
|
||||
console.log('Features by category:');
|
||||
const byCategory = features.reduce((acc, f) => {
|
||||
acc[f.category] = (acc[f.category] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
console.log(byCategory);
|
||||
|
||||
console.log('\nFeatures by priority:');
|
||||
const byPriority = features.reduce((acc, f) => {
|
||||
acc[`P${f.priority}`] = (acc[`P${f.priority}`] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
console.log(byPriority);
|
||||
}).catch(console.error);
|
||||
@@ -0,0 +1,153 @@
|
||||
import type { FeatureTestStatus, ScenarioTestResult } from '../src/types/gherkin';
|
||||
|
||||
interface CucumberScenario {
|
||||
id: string;
|
||||
name: string;
|
||||
steps: Array<{
|
||||
result: {
|
||||
status: 'passed' | 'failed' | 'skipped' | 'pending' | 'undefined';
|
||||
duration?: number;
|
||||
error_message?: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
interface CucumberFeature {
|
||||
id: string;
|
||||
uri: string;
|
||||
name: string;
|
||||
elements: CucumberScenario[];
|
||||
}
|
||||
|
||||
export async function parseTestResults(): Promise<Map<string, FeatureTestStatus>> {
|
||||
const reportPath = 'reports/cucumber-report.json';
|
||||
const file = Bun.file(reportPath);
|
||||
|
||||
if (!await file.exists()) {
|
||||
console.log('No test report found at', reportPath);
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const content = await file.text();
|
||||
const features: CucumberFeature[] = JSON.parse(content);
|
||||
const results = new Map<string, FeatureTestStatus>();
|
||||
|
||||
for (const feature of features) {
|
||||
// Extract feature ID from URI (e.g., features/user/us-9-visualiser-photo.feature -> us-9)
|
||||
const match = feature.uri.match(/us-(\d+)/i);
|
||||
const featureId = match ? `us-${match[1]}` : feature.id;
|
||||
|
||||
const scenarios = feature.elements.filter(el => el.name); // Filter out Background
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
let skipped = 0;
|
||||
const scenarioResults: ScenarioTestResult[] = [];
|
||||
|
||||
for (const scenario of scenarios) {
|
||||
const { status: scenarioStatus, errorMessage } = getScenarioStatusAndError(scenario);
|
||||
if (scenarioStatus === 'passed') passed++;
|
||||
else if (scenarioStatus === 'failed') failed++;
|
||||
else skipped++;
|
||||
|
||||
scenarioResults.push({
|
||||
name: scenario.name,
|
||||
status: scenarioStatus,
|
||||
errorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
results.set(featureId, {
|
||||
featureId,
|
||||
totalScenarios: scenarios.length,
|
||||
passed,
|
||||
failed,
|
||||
skipped,
|
||||
lastRun: new Date(),
|
||||
scenarios: scenarioResults,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
function getScenarioStatusAndError(scenario: CucumberScenario): { status: 'passed' | 'failed' | 'skipped'; errorMessage?: string } {
|
||||
for (const step of scenario.steps) {
|
||||
if (step.result.status === 'failed') {
|
||||
return { status: 'failed', errorMessage: step.result.error_message };
|
||||
}
|
||||
if (step.result.status === 'skipped' || step.result.status === 'pending' || step.result.status === 'undefined') {
|
||||
return { status: 'skipped' };
|
||||
}
|
||||
}
|
||||
return { status: 'passed' };
|
||||
}
|
||||
|
||||
// Generate TypeScript file with test results
|
||||
async function generateTestResultsFile() {
|
||||
const results = await parseTestResults();
|
||||
|
||||
const resultsArray = Array.from(results.entries()).map(([id, status]) => ({
|
||||
...status,
|
||||
lastRun: status.lastRun?.toISOString(),
|
||||
}));
|
||||
|
||||
const output = `// Auto-generated by scripts/parse-test-results.ts
|
||||
// Do not edit manually - run "bun run test:results" to regenerate
|
||||
import type { FeatureTestStatus, ScenarioTestResult } from '../types/gherkin';
|
||||
|
||||
interface RawFeatureTestStatus {
|
||||
featureId: string;
|
||||
totalScenarios: number;
|
||||
passed: number;
|
||||
failed: number;
|
||||
skipped: number;
|
||||
lastRun?: string;
|
||||
scenarios?: ScenarioTestResult[];
|
||||
}
|
||||
|
||||
const rawResults: RawFeatureTestStatus[] = ${JSON.stringify(resultsArray, null, 2)};
|
||||
|
||||
export const testResults: Map<string, FeatureTestStatus> = new Map(
|
||||
rawResults.map(r => [r.featureId, { ...r, lastRun: r.lastRun ? new Date(r.lastRun) : undefined }])
|
||||
);
|
||||
|
||||
export function getTestStatus(featureId: string): FeatureTestStatus | undefined {
|
||||
return testResults.get(featureId);
|
||||
}
|
||||
|
||||
export function getScenarioResults(featureId: string): ScenarioTestResult[] {
|
||||
return testResults.get(featureId)?.scenarios ?? [];
|
||||
}
|
||||
|
||||
export function getAllTestResults(): FeatureTestStatus[] {
|
||||
return Array.from(testResults.values());
|
||||
}
|
||||
|
||||
export function getTestSummary() {
|
||||
const results = getAllTestResults();
|
||||
const firstResult = results[0];
|
||||
return {
|
||||
totalFeatures: results.length,
|
||||
totalScenarios: results.reduce((acc, r) => acc + r.totalScenarios, 0),
|
||||
passed: results.reduce((acc, r) => acc + r.passed, 0),
|
||||
failed: results.reduce((acc, r) => acc + r.failed, 0),
|
||||
skipped: results.reduce((acc, r) => acc + r.skipped, 0),
|
||||
lastRun: firstResult?.lastRun,
|
||||
};
|
||||
}
|
||||
`;
|
||||
|
||||
await Bun.write('src/data/testResults.ts', output);
|
||||
console.log(`Generated test results for ${results.size} features`);
|
||||
|
||||
// Print summary
|
||||
let totalPassed = 0, totalFailed = 0, totalSkipped = 0;
|
||||
results.forEach(r => {
|
||||
totalPassed += r.passed;
|
||||
totalFailed += r.failed;
|
||||
totalSkipped += r.skipped;
|
||||
});
|
||||
console.log(`Summary: ${totalPassed} passed, ${totalFailed} failed, ${totalSkipped} skipped`);
|
||||
}
|
||||
|
||||
generateTestResultsFile().catch(console.error);
|
||||
Reference in New Issue
Block a user