Decision: Test Strategy

Decision

Three test layers, each with a specific role. All three must pass before a phase is complete.

Layer 1 — Unit tests (Vitest)

One test file per module. Mock all browser APIs. Test the module in isolation.

Required per module:

// 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', () => { ... })
})

Layer 2 — Integration tests (Vitest + jsdom)

Test module combinations and the violation pipeline end-to-end.

Required per phase:

Layer 3 — E2E attacker simulations (Playwright)

Simulate 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

Phase gate model

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.”

Why attacker simulations, not just unit tests

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.

Why phase gates over continuous coverage

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.

Tooling

// 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"
}