Three test layers, each with a specific role. All three must pass before a phase is complete.
One test file per module. Mock all browser APIs. Test the module in isolation.
Required per module:
report() called with right argssupported(): returns false when required API is absent// Example pattern
describe('devtools-detect', () => {
it('does not report on normal window dimensions', () => { ... })
it('reports when outer/inner size delta exceeds threshold', () => { ... })
it('stop() clears the resize observer', () => { ... })
it('supported() returns false when ResizeObserver is absent', () => { ... })
})
Test module combinations and the violation pipeline end-to-end.
Required per phase:
degrade mode: app still partially renders, WebShield.isDegraded() returns trueSimulate real attacker scenarios against a running app. These are the phase exit gate.
Scenarios (one spec file each):
| Spec | What it does |
|---|---|
wrong-domain.spec.ts |
Serve app from localhost:9999, check violation triggers |
devtools-open.spec.ts |
Open DevTools programmatically via CDP, check response |
headless-detect.spec.ts |
Run in headless Chromium, verify anti-headless fires |
iframe-embed.spec.ts |
Embed app in foreign-origin iframe, check anti-iframe fires |
script-inject.spec.ts |
Inject <script> tag into <head>, check dom-tamper fires |
decoy-access.spec.ts |
Access window.__lessons from injected script, check trap fires |
dom-modify.spec.ts |
Remove shield <script> from DOM, check dom-tamper fires |
Each phase exits on a specific test count — not on “feature works.”
| Phase | Unit | Integration | E2E |
|---|---|---|---|
| 1 | 24 | 8 | 0 |
| 2 | +36 | +12 | 2 |
| 3 | +42 | +15 | 3 |
| 4 | +36 | +12 | 3 |
| 5 | +48 | +16 | 2 |
| 6 | +40 | +14 | 3 |
| 7 | +30 | +10 | 2 |
| 8 | full suite green |
Rule: Never start Phase N+1 until Phase N gate passes. No exceptions for “it probably works.”
Unit tests prove the module logic is correct in isolation. They do not prove the module fires correctly in a real browser against a real attack. A devtools detector that passes unit tests with mocked APIs might fail against actual DevTools — different timing, different event ordering.
The Playwright scenarios are the only tests that prove the protection works end-to-end. A phase without e2e specs is not complete regardless of unit test count.
Coverage metrics measure lines executed, not scenarios covered. A 95% coverage score on protection code is meaningless if the attack scenario that bypasses it was never tested. Phase gates force explicit scenario definition before implementation, not after.
// package.json scripts
{
"test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test:e2e": "playwright test tests/e2e",
"test:phase": "npm run test:unit && npm run test:integration && npm run test:e2e",
"test": "npm run test:phase"
}