Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ All notable changes to ReproCase are documented here.
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
versioning follows [Semantic Versioning](https://semver.org/).

## [0.7.1] — 2026-11-12

Special-audit follow-up to v0.7.0: one accuracy fix found by the A/B/C reverse audit.

### Fixed

- **Client-initiated fetch aborts are no longer reported** (accuracy / false positives):
SPAs cancel in-flight API requests constantly (stale queries, route changes, timeouts) via
AbortController. The `net::ERR_ABORTED` failure of a cancelled fetch/XHR is application
behaviour, not a page fault. Real network failures on fetch/XHR (connection refused, DNS,
reset, destroyed sockets) are still reported, and aborted subresources (images, scripts)
keep their existing coverage.

### Also

- Corrected the verified/suspected wording in issues.md (suspected covers both
not-yet-proven-reproducible and reproducible-but-informational console errors).
- CI: Chromium install step retries once on CDN connection drops.

### Verification

186/186 tests (25 files) incl. two new regression tests: client abort not reported +
socket-destroyed fetch still reported; real-site audit scan 9→8 findings with all planted
scenarios intact; npm audit 0.

## [0.7.0] — 2026-11-08

Real-world reliability & adoption release. Validated against an independent matrix of
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "reprocase",
"version": "0.7.0",
"version": "0.7.1",
"description": "Automated website fault detection & reproduction. Scans a site in a real browser, verifies each fault independently, and emits reproducible Playwright regression tests plus human- and CI-friendly reports.",
"type": "module",
"license": "MIT",
Expand Down
10 changes: 9 additions & 1 deletion src/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,10 +403,18 @@ export class Scanner {
const u = req.url();
if (FAVICON_RE.test(new URL(u, page.url()).pathname)) return;
if (!this.isFirstParty(u)) return;
// Client-initiated cancellation of an API request (AbortController,
// stale-query cancel, route change) is application behaviour, not a page
// fault: SPAs abort in-flight fetches constantly. Only the abort error
// code is filtered — real network failures on fetch/XHR (connection
// refused, DNS, reset) are still reported (v0.7.1 accuracy fix).
const rt = req.resourceType();
const err = req.failure()?.errorText ?? '';
if ((rt === 'xhr' || rt === 'fetch') && err.includes('ERR_ABORTED')) return;
void obs({
type: 'resource-load-failure',
url: page.url(),
evidence: { resourceUrl: u, failureText: req.failure()?.errorText ?? 'request failed' },
evidence: { resourceUrl: u, failureText: err || 'request failed' },
steps: [...stepsBox.steps]
});
});
Expand Down
78 changes: 78 additions & 0 deletions tests/integration/api-noise.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,82 @@ describe('API business statuses are not resource faults (v0.7.0 accuracy fix)',
},
120_000
);

it(
'does not report a client-initiated fetch abort (stale-request cancel), but keeps real network failures',
async () => {
// Custom server (not the static fixture): needs a slow endpoint so the
// page's AbortController reliably cancels the request mid-flight, and a
// socket-killing endpoint to prove real network failures stay reported.
const http = await import('node:http');
const srv = http.createServer((req, res) => {
if ((req.url || '').startsWith('/api/slow')) {
setTimeout(() => {
res.writeHead(200, { 'content-type': 'application/json' });
res.end('{"status":"slow-ok"}');
}, 1500);
return;
}
if ((req.url || '').startsWith('/api/kill-socket')) {
// Destroy the connection without a response: a real network failure
// (net::ERR_EMPTY_RESPONSE on the client) that must stay reported.
req.socket.destroy();
return;
}
if ((req.url || '').startsWith('/refused')) {
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end(
`<!doctype html><html><body><p id="r">pending</p><script>
fetch('/api/kill-socket').then(r => r.json()).catch(e => {
document.getElementById('r').textContent = 'network failed as designed';
});
</script></body></html>`
);
return;
}
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
res.end(
`<!doctype html><html><body><p id="out">pending</p><a href="/refused">next</a><script>
const c = new AbortController();
fetch('/api/slow', { signal: c.signal })
.then(r => r.json())
.then(() => { document.getElementById('out').textContent = 'unexpected'; })
.catch(e => { document.getElementById('out').textContent = 'aborted by design'; });
setTimeout(() => c.abort(), 120);
</script></body></html>`
);
});
await new Promise<void>((resolve) => srv.listen(0, '127.0.0.1', resolve));
const address = srv.address();
const baseUrl = `http://127.0.0.1:${typeof address === 'object' && address ? address.port : 0}/`;

try {
const out = fs.mkdtempSync(path.join(os.tmpdir(), 'reprocase-abort-'));
const { config } = resolveConfig({
entry: baseUrl,
maxPages: 4,
runTimeoutMs: 120_000,
verification: { attempts: 2 }
});
const { result } = await new Scanner(config, out).run();

// Client-cancelled fetches are application behaviour, not page faults.
const abortNoise = result.findings.filter(
(f) => f.type === 'resource-load-failure' && (f.evidence.failureText || '').includes('ABORT')
);
expect(abortNoise, 'client-initiated fetch abort must not be reported as a fault').toHaveLength(0);

// A real network-level failure on a fetch (server destroyed the socket)
// must STILL be reported — the filter only hides intentional cancels.
const realFailure = result.findings.find(
(f) => f.type === 'resource-load-failure' && (f.evidence.resourceUrl || '').includes('/api/kill-socket')
);
expect(realFailure, 'real network failure on fetch must stay reported').toBeTruthy();
expect(realFailure!.evidence.failureText).not.toContain('ABORT');
} finally {
await new Promise<void>((resolve) => srv.close(() => resolve()));
}
},
120_000
);
});
Loading