134 lines
4.9 KiB
TypeScript
134 lines
4.9 KiB
TypeScript
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import { MarkdownExporter } from '../../../src/plugins/exporters/MarkdownExporter';
|
|
import { IAnomaly, IAction } from '../../../src/core/interfaces';
|
|
|
|
function makeAnomaly(overrides: Partial<IAnomaly> = {}): IAnomaly {
|
|
const action: IAction = {
|
|
id: 'act-1',
|
|
type: 'navigate',
|
|
url: 'http://localhost:3000/register',
|
|
timestamp: 1700000000000,
|
|
seed: 42,
|
|
stateId: 's1',
|
|
};
|
|
return {
|
|
id: 'anom-md-001',
|
|
type: 'http_error',
|
|
severity: 'high',
|
|
observationId: 'obs-1',
|
|
actionTrace: [action],
|
|
description: 'Form submission returns HTTP 500 on empty email field',
|
|
evidence: {
|
|
screenshotPath: 'anom-md-001/screenshot.png',
|
|
domSnapshotPath: 'anom-md-001/dom.html',
|
|
httpLog: [{ url: '/api/register', status: 500, method: 'POST', durationMs: 234 }],
|
|
rawErrors: ['POST /api/register → 500 (234ms)'],
|
|
},
|
|
timestamp: 1700000000000,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('MarkdownExporter', () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'abe-md-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('creates report.md in the output directory', async () => {
|
|
const exporter = new MarkdownExporter();
|
|
const outputDir = path.join(tmpDir, 'anom-md-001');
|
|
const filePath = await exporter.export(makeAnomaly(), outputDir);
|
|
expect(fs.existsSync(filePath)).toBe(true);
|
|
expect(filePath.endsWith('report.md')).toBe(true);
|
|
});
|
|
|
|
it('includes anomaly type and date in title', async () => {
|
|
const exporter = new MarkdownExporter();
|
|
const outputDir = path.join(tmpDir, 'title-test');
|
|
const filePath = await exporter.export(makeAnomaly(), outputDir);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
expect(content).toContain('# Bug Report');
|
|
expect(content).toContain('http_error');
|
|
});
|
|
|
|
it('includes severity section', async () => {
|
|
const exporter = new MarkdownExporter();
|
|
const outputDir = path.join(tmpDir, 'severity-test');
|
|
const filePath = await exporter.export(makeAnomaly(), outputDir);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
expect(content).toContain('## Severity');
|
|
expect(content).toContain('high');
|
|
});
|
|
|
|
it('includes reproduction steps with navigate action', async () => {
|
|
const exporter = new MarkdownExporter();
|
|
const outputDir = path.join(tmpDir, 'steps-test');
|
|
const filePath = await exporter.export(makeAnomaly(), outputDir);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
expect(content).toContain('## Reproduction Steps');
|
|
expect(content).toContain('Navigate to');
|
|
expect(content).toContain('http://localhost:3000/register');
|
|
});
|
|
|
|
it('includes seed and replay command', async () => {
|
|
const exporter = new MarkdownExporter();
|
|
const outputDir = path.join(tmpDir, 'seed-test');
|
|
const filePath = await exporter.export(makeAnomaly(), outputDir);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
expect(content).toContain('Seed used');
|
|
expect(content).toContain('42');
|
|
expect(content).toContain('Replay command');
|
|
expect(content).toContain('npm run replay');
|
|
});
|
|
|
|
it('includes evidence section with screenshot and dom paths', async () => {
|
|
const exporter = new MarkdownExporter();
|
|
const outputDir = path.join(tmpDir, 'evidence-test');
|
|
const filePath = await exporter.export(makeAnomaly(), outputDir);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
expect(content).toContain('## Evidence');
|
|
expect(content).toContain('screenshot.png');
|
|
expect(content).toContain('dom.html');
|
|
});
|
|
|
|
it('includes HTTP table when responses present', async () => {
|
|
const exporter = new MarkdownExporter();
|
|
const outputDir = path.join(tmpDir, 'http-test');
|
|
const filePath = await exporter.export(makeAnomaly(), outputDir);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
expect(content).toContain('500');
|
|
expect(content).toContain('/api/register');
|
|
expect(content).toContain('POST');
|
|
});
|
|
|
|
it('includes raw errors section', async () => {
|
|
const exporter = new MarkdownExporter();
|
|
const outputDir = path.join(tmpDir, 'errors-test');
|
|
const filePath = await exporter.export(makeAnomaly(), outputDir);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
expect(content).toContain('## Raw Errors');
|
|
expect(content).toContain('POST /api/register');
|
|
});
|
|
|
|
it('has format = markdown', () => {
|
|
expect(new MarkdownExporter().format).toBe('markdown');
|
|
});
|
|
|
|
it('handles anomaly with no action trace', async () => {
|
|
const exporter = new MarkdownExporter();
|
|
const outputDir = path.join(tmpDir, 'no-trace-test');
|
|
const anomaly = makeAnomaly({ actionTrace: [] });
|
|
const filePath = await exporter.export(anomaly, outputDir);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
expect(content).toContain('No steps recorded');
|
|
});
|
|
});
|