mod common;
use common::{Detector, dump};
const SOLID_BANDS: &str = r#"(async () => {
const colours = [[255,0,0],[0,255,0],[0,0,255],[255,255,0],[255,0,255],[0,255,255],
[1,1,1],[254,254,254],[0,0,0],[51,51,51],[102,102,102],[153,153,153],
[204,204,204],[255,255,255]];
const w = 5, h = 5;
const c = document.createElement('canvas');
c.width = w * colours.length; c.height = h;
const ctx = c.getContext('2d');
colours.forEach((m, i) => {
ctx.fillStyle = '#' + m.map((x) => x.toString(16).padStart(2, '0')).join('');
ctx.fillRect(w * i, 0, w * (1 + i), h);
});
const url = c.toDataURL();
const img = document.createElement('img');
await new Promise((r) => { img.onload = () => r(); img.src = url; });
const v = document.createElement('canvas');
v.width = c.width; v.height = c.height;
const vc = v.getContext('2d');
vc.drawImage(img, 0, 0);
const counts = colours.map((m, i) => {
const d = vc.getImageData(w * i, 0, w, h).data;
const px = new Uint32Array(d.buffer);
const want = new Uint32Array(new Uint8Array([...m, 255]).buffer)[0];
let n = 0;
px.forEach((p) => { if (p === want) n++; });
return n;
});
return JSON.stringify({ counts, expected: w * h, allExact: counts.every((n) => n === w * h) });
})()"#;
const BLANK: &str = r#"(() => {
const c = document.createElement('canvas');
c.width = 300; c.height = 150;
const ctx = c.getContext('2d');
// Not Math.max(...data): 180k arguments overflows the call stack.
const maxOf = (d) => { let m = 0; for (let i = 0; i < d.length; i++) if (d[i] > m) m = d[i]; return m; };
const untouched = maxOf(ctx.getImageData(0, 0, 300, 150).data);
ctx.fillStyle = '#123456';
ctx.fillRect(0, 0, 300, 150);
ctx.clearRect(0, 0, 300, 150);
const cleared = maxOf(ctx.getImageData(0, 0, 300, 150).data);
return JSON.stringify({ untouched, cleared });
})()"#;
const TEXT_RENDER: &str = r#"(async () => {
const draw = () => {
const c = document.createElement('canvas');
c.width = 280; c.height = 60;
const ctx = c.getContext('2d');
ctx.fillStyle = '#f0f0f0'; ctx.fillRect(0, 0, 280, 60);
ctx.fillStyle = '#102030';
ctx.font = '28px serif';
ctx.fillText('Cwm fjord bank glyphs vext quiz', 4, 40);
return c;
};
const a = draw(), b = draw();
const directA = a.toDataURL(), directB = b.toDataURL();
const img = document.createElement('img');
await new Promise((r) => { img.onload = () => r(); img.src = directA; });
const v = document.createElement('canvas');
v.width = 280; v.height = 60;
v.getContext('2d').drawImage(img, 0, 0);
const roundTripped = v.toDataURL();
return JSON.stringify({
stableAcrossDraws: directA === directB,
roundTripStable: directA === roundTripped,
hash: directA.slice(-48),
});
})()"#;
#[tokio::test]
#[ignore = "launches a browser"]
async fn solid_fills_survive_a_round_trip() {
let mut d = Detector::open(1, "about:blank").await;
let r = d.eval_async(SOLID_BANDS).await;
dump("adjacent solid fills", &r);
d.close().await;
assert_eq!(
r["allExact"].as_bool(),
Some(true),
"adjacent solid fills lost pixels to the noise: {} (each should be {})",
r["counts"],
r["expected"],
);
}
#[tokio::test]
#[ignore = "launches a browser"]
async fn blank_and_cleared_canvases_read_zero() {
let mut d = Detector::open(1, "about:blank").await;
let r = d.eval(BLANK).await;
dump("blank canvas", &r);
d.close().await;
assert_eq!(r["untouched"].as_i64(), Some(0), "an untouched canvas was not all zero");
assert_eq!(r["cleared"].as_i64(), Some(0), "a cleared canvas was not all zero");
}
#[tokio::test]
#[ignore = "launches a browser"]
async fn text_renders_are_stable() {
let mut d = Detector::open(1, "about:blank").await;
let r = d.eval_async(TEXT_RENDER).await;
dump("antialiased text", &r);
d.close().await;
assert_eq!(
r["stableAcrossDraws"].as_bool(),
Some(true),
"two identical draws produced different bytes — instability is its own tell"
);
assert_eq!(
r["roundTripStable"].as_bool(),
Some(true),
"a PNG round-trip changed the bytes; real hardware round-trips losslessly"
);
}
#[tokio::test]
#[ignore = "launches two browsers"]
async fn noise_is_active_and_seeded_per_identity() {
let mut a = Detector::open(1, "about:blank").await;
let ra = a.eval_async(TEXT_RENDER).await;
a.close().await;
let mut b = Detector::open(4, "about:blank").await;
let rb = b.eval_async(TEXT_RENDER).await;
b.close().await;
println!("preset 1 tail: {}", ra["hash"]);
println!("preset 4 tail: {}", rb["hash"]);
assert_ne!(
ra["hash"], rb["hash"],
"two identities produced an identical canvas — the noise is not being applied, \
or is not seeded from the identity"
);
}