diff --git a/CHANGELOG.md b/CHANGELOG.md index f59033f..93a0d4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,55 @@ 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.0] — 2026-11-08 + +Real-world reliability & adoption release. Validated against an independent matrix of +real applications (HTML5 Boilerplate 9.0.1 multi-page site, create-vite React SPA, +create-vite Vue SPA) with a hand-maintained truth list, and fixed the accuracy defects +that matrix exposed. Performance verified regression-free against v0.6.0 on the same +machine (9 / 42 / 102-page sites). + +### Fixed + +- **Duplicate resource failures merged** (accuracy): a real browser can observe the same + missing resource with different failure shapes (HTTP 404 on one request, `net::ERR_ABORTED` + on a duplicate). Resource-load-failure fingerprints are now keyed by resource URL, so one + root cause produces one finding instead of two. +- **Handled API responses no longer reported** (accuracy / false positives): first-party + `fetch`/XHR responses with 4xx/5xx statuses are application-level business states (REST + lookups legitimately return 404 and pages handle them). They are no longer reported as + resource-load-failure. Network-level failures on those requests are still reported via the + request-failure channel, and broken images/scripts/stylesheets keep their existing coverage. +- **Safe buttons inside forms are now explored** (coverage): previously ALL form-internal + controls were skipped, hiding real interaction faults on form-heavy pages. ` + + + + + `); + const out = await page.evaluate(collectCandidatesInPage, { max: 20, allowSelectors: [], denySelectors: [] }); + await page.close(); + const chosen = out.candidates.map((c) => c.selector); + // Safe non-submitting button inside a form is explorable. + expect(chosen, 'safe type=button inside a form must be a candidate').toContain('#form-safe'); + // Hard-dangerous keyword keeps its protection regardless of location. + expect(chosen).not.toContain('#form-del'); + expect(out.skipped.find((s) => s.selector === '#form-del')?.reason).toBe('forbidden'); + // Submitting controls (explicit submit and default-type button) stay untouched. + expect(chosen).not.toContain('#form-submit'); + expect(chosen).not.toContain('#form-default'); + }, 60_000); + it('skips a soft keyword by default and offers no candidate bypass', async () => { const out = await collect([], []); const chosen = out.candidates.map((c) => c.selector); diff --git a/tests/unit/findings.test.ts b/tests/unit/findings.test.ts index 5d97f5b..fc95616 100644 --- a/tests/unit/findings.test.ts +++ b/tests/unit/findings.test.ts @@ -64,6 +64,18 @@ describe('FindingCollector dedupe', () => { expect(a.finding.repro.observedCount).toBe(2); }); + it('merges resource failures with the same URL but different failure modes (404 vs abort)', () => { + // A real browser can observe the same missing resource twice with different + // failure shapes (HTTP 404 on one request, net::ERR_ABORTED on a duplicate + // request). Same root cause -> one finding, not two. + const c = new FindingCollector(true); + const a = c.observe(obs({ type: 'resource-load-failure', url: 'http://127.0.0.1:4610/p1.html', evidence: { resourceUrl: 'http://127.0.0.1:4610/app.js', statusCode: 404 } })); + const b = c.observe(obs({ type: 'resource-load-failure', url: 'http://127.0.0.1:4610/p2.html', evidence: { resourceUrl: 'http://127.0.0.1:4610/app.js', failureText: 'net::ERR_ABORTED' } })); + expect(b.created, 'same resource URL must dedupe regardless of failure mode').toBe(false); + expect(a.finding.repro.observedCount).toBe(2); + expect(c.list()).toHaveLength(1); + }); + it('assigns stable ids derived from the fingerprint', () => { const c1 = new FindingCollector(true); const c2 = new FindingCollector(true); diff --git a/tests/unit/report.test.ts b/tests/unit/report.test.ts index 14b7a2a..ce6391d 100644 --- a/tests/unit/report.test.ts +++ b/tests/unit/report.test.ts @@ -153,6 +153,13 @@ describe('HTML report', () => { expect(html).toContain(''); }); + it('provides a noscript fallback pointing at result.json/issues.md', () => { + const html = renderHtml(makeResult()); + expect(html).toContain('