use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
use chromiumoxide::Page;
use chromiumoxide::cdp::browser_protocol::emulation::SetDeviceMetricsOverrideParams;
use chromiumoxide::cdp::browser_protocol::page::{
AddScriptToEvaluateOnNewDocumentParams, CaptureScreenshotParams, Viewport,
};
use chromiumoxide::layout::Point;
use futures::StreamExt;
use devpage_design::compare::{self, Difference};
use crate::adapters::chrome::testing::{self, Browser, eval, until};
use crate::drive::http;
use devpage_core::designs::Designs;
use devpage_core::document::Document;
use devpage_core::folder::ServedFolder;
fn designs(dir: &Path) {
let files = [
(
"button.design",
r##"<Frame props={{ label: "Button", fill: "#2563eb" }} width={200} height={40} fill={fill} radius={8} layout="row" align="center" justify="center">
<Text size={16} weight={600} color="#ffffff">{label}</Text>
</Frame>"##,
),
(
"screen.design",
r##"import Button from "./button.design"
<Frame width={400} height={300} fill="#f8fafc" layout="column" padding={20} gap={20}>
<Button label="Pay" onClick="navigate ./next.design" />
<Button label="Sheet" fill="#16a34a" onClick="overlay ./sheet.design" />
<Frame width={360} height={100} fill="#e2e8f0" radius={12} shadow={{ y: 4, blur: 12, color: "#0f172a33" }} />
</Frame>"##,
),
(
"next.design",
r##"import Button from "./button.design"
<Frame width={400} height={300} fill="#fef3c7" layout="column" padding={20}>
<Button label="Back" onClick="back" />
</Frame>"##,
),
(
"sheet.design",
r##"import Button from "./button.design"
<Frame width={240} height={100} fill="#ffffff" layout="column" padding={20}>
<Button label="Close" fill="#dc2626" onClick="close" />
</Frame>"##,
),
];
for (name, source) in files {
std::fs::write(dir.join(name), source).unwrap();
}
}
fn folder(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("devpage-canvas-{}-{name}", std::process::id()));
std::fs::remove_dir_all(&dir).ok();
std::fs::create_dir_all(&dir).unwrap();
let dir = dir.canonicalize().unwrap();
designs(&dir);
dir
}
async fn serve(dir: &Path, entry: &str) -> String {
let document =
Arc::new(Document::load(&dir.join(entry), &ServedFolder::new(dir).unwrap()).unwrap());
let changes = crate::adapters::changes::Changes::default();
testing::serve(http::router(
http::App::testing(document, changes)
.with_comments_on_disk(devpage_core::comments::Identity::Local("T".into())),
))
.await
}
async fn serve_designs(dir: &Path, entry: &str) -> (String, Designs) {
let document =
Arc::new(Document::load(&dir.join(entry), &ServedFolder::new(dir).unwrap()).unwrap());
let designs = Designs::new(&document.served);
let changes = crate::adapters::changes::Changes::default();
let router = http::router(
http::App::testing(document, changes)
.with_designs(designs.clone())
.with_comments_on_disk(devpage_core::comments::Identity::Local("T".into())),
);
(testing::serve(router).await, designs)
}
async fn on_screen(page: &Page, selector: &str, x: f64, y: f64) -> Point {
let [zoom, vx, vy, left, top]: [f64; 5] = eval(
page,
&format!(
"(() => {{ const c = document.querySelector('{selector}'); const b = c.getBoundingClientRect(); \
return [...c.dataset.view.split(',').map(Number), b.left, b.top]; }})()"
),
)
.await;
Point::new(left + vx + x * zoom, top + vy + y * zoom)
}
async fn screenshot(page: &Page, width: f64, height: f64, margin: f64) -> Vec<u8> {
let at = on_screen(page, "canvas.design", -margin, -margin).await;
page.screenshot(
CaptureScreenshotParams::builder()
.clip(Viewport {
x: at.x,
y: at.y,
width: width + 2.0 * margin,
height: height + 2.0 * margin,
scale: 1.0,
})
.build(),
)
.await
.unwrap()
}
async fn until_pixel(page: &Page, at: Point, color: [u8; 3]) {
let mut last = [0; 3];
for _ in 0..200 {
last = pixel(page, at).await;
if last == color {
return;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
panic!("timed out waiting for {color:?} at {at:?}, last {last:?}");
}
async fn pixel(page: &Page, at: Point) -> [u8; 3] {
let bytes = page
.screenshot(
CaptureScreenshotParams::builder()
.clip(Viewport {
x: at.x.floor(),
y: at.y.floor(),
width: 1.0,
height: 1.0,
scale: 1.0,
})
.build(),
)
.await
.unwrap();
let bytes = serde_json::to_string(&bytes).unwrap();
eval(
page,
&format!(
"(async () => {{
const bitmap = await createImageBitmap(new Blob([new Uint8Array({bytes})], {{ type: 'image/png' }}));
const c = new OffscreenCanvas(1, 1);
const ctx = c.getContext('2d');
ctx.drawImage(bitmap, 0, 0);
return [...ctx.getImageData(0, 0, 1, 1).data.slice(0, 3)];
}})()"
),
)
.await
}
async fn difference(page: &Page, shot: &[u8], served: &[u8], margin: usize) -> Option<Difference> {
let background: [f64; 3] = eval(
page,
"getComputedStyle(document.body).backgroundColor.match(/[\\d.]+/g).slice(0, 3).map(Number)",
)
.await;
let (drawing, width, height) = compare::pixels(served).unwrap();
let (width, height) = (width as usize, height as usize);
let outer = width + 2 * margin;
let page_color = [
background[0] as u8,
background[1] as u8,
background[2] as u8,
255,
];
let mut b = page_color.repeat(outer * (height + 2 * margin));
for y in 0..height {
for x in 0..width {
let px = &drawing[4 * (y * width + x)..][..4];
let alpha = px[3] as f64 / 255.0;
let to = &mut b[4 * ((y + margin) * outer + x + margin)..][..4];
for k in 0..3 {
to[k] = (px[k] as f64 * alpha + background[k] * (1.0 - alpha)).round() as u8;
}
}
}
let (mut a, _, _) = compare::pixels(shot).unwrap();
for px in a.chunks_mut(4) {
px[3] = 255;
}
compare::compare(&a, &b, outer)
}
async fn until_shows_png(
page: &Page,
address: &str,
png: &str,
margin: usize,
) -> Result<(), String> {
let [width, height]: [f64; 2] = eval(
page,
"document.querySelector('canvas.design').dataset.size.split(',').map(Number)",
)
.await;
let served = reqwest::get(format!("{address}{png}"))
.await
.unwrap()
.bytes()
.await
.unwrap();
let started = std::time::Instant::now();
loop {
let shot = screenshot(page, width, height, margin as f64).await;
let d = difference(page, &shot, &served, margin).await;
if d.is_some_and(|d| d.within_tolerance()) {
return Ok(());
}
if started.elapsed() > Duration::from_secs(20) {
let name: String = png
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect();
let at = std::env::temp_dir().join(format!("devpage-screen{name}.png"));
std::fs::write(&at, &shot).ok();
return Err(format!("{png}: {d:?}; the screen is {}", at.display()));
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
async fn the_examples_match_their_pngs(webgpu: bool) {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let address = serve(root, "docs/designs/checkout/checkout.design").await;
let paths = ["button", "checkout", "confirmation", "details"]
.map(|name| format!("/docs/designs/checkout/{name}.design"));
canvases_match_their_pngs(&address, &paths, webgpu).await;
}
async fn canvases_match_their_pngs(address: &str, paths: &[String], webgpu: bool) {
let compared = ["light", "dark"]
.into_iter()
.flat_map(|theme| paths.iter().map(move |path| (theme, path)))
.map(|(theme, path)| matches_its_png(address, theme, path, webgpu));
let failures: Vec<String> = futures::future::join_all(compared)
.await
.into_iter()
.filter_map(Result::err)
.collect();
assert!(failures.is_empty(), "{failures:#?}");
}
async fn matches_its_png(
address: &str,
theme: &str,
path: &str,
webgpu: bool,
) -> Result<(), String> {
let mut browser = testing::browser().await;
let page = canvas_page(&mut browser, webgpu, 1280, 1000).await;
page.goto(format!("{address}{path}?theme={theme}"))
.await
.unwrap();
until(&page, PAINTED).await;
let backend: String = eval(
&page,
"document.querySelector('canvas.design').dataset.backend",
)
.await;
assert_eq!(backend, if webgpu { "gpu" } else { "cpu" });
let zoom: f64 = eval(
&page,
"Number(document.querySelector('canvas.design').dataset.view.split(',')[0])",
)
.await;
assert_eq!(zoom, 1.0, "{path} isn't shown at 100%");
let shown = until_shows_png(&page, address, &format!("{path}.png?theme={theme}"), 24).await;
browser.close().await.ok();
shown.map_err(|why| format!("{backend} {theme}: {why}"))
}
async fn font_files_match_their_pngs(webgpu: bool) {
let dir = folder(if webgpu { "fonts-gpu" } else { "fonts-cpu" });
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
std::fs::create_dir_all(dir.join("fonts/Static")).unwrap();
std::fs::copy(
root.join("docs/designs/checkout/fonts/Figtree/Figtree-Variable.ttf"),
dir.join("fonts/Figtree.ttf"),
)
.unwrap();
for face in ["Regular", "Bold"] {
std::fs::copy(
root.join(format!("design/fonts/Inter-{face}.ttf")),
dir.join(format!("fonts/Static/Inter-{face}.ttf")),
)
.unwrap();
}
std::fs::write(
dir.join("themes.yml"),
"kind: themes\nfonts:\n figtree: ./fonts/Figtree.ttf\n inter: ./fonts/Static\nlight:\n bg: \"#ffffff\"\n text: \"#0f172a\"\ndark:\n bg: \"#0f172a\"\n text: \"#f1f5f9\"\n",
)
.unwrap();
std::fs::write(
dir.join("type.design"),
r#"<Frame width={360} fill={theme.bg} layout="column" padding={16} gap={8}>
<Text font="figtree" size={28} weight={900} color={theme.text}>Heavy headline</Text>
<Text font="figtree" size={20} weight={300} color={theme.text}>Light subtitle</Text>
<Text font="figtree" width="fill" size={15} lineHeight={1.5} color={theme.text}>Washed linen with a relaxed fit, cut wide through the body and finished by hand. Machine wash cold, tumble dry low.</Text>
<Text font="inter" weight={700} color={theme.text}>Static bold face</Text>
<Text color={theme.text}>Inter, embedded</Text>
</Frame>"#,
)
.unwrap();
let address = serve(&dir, "type.design").await;
canvases_match_their_pngs(&address, &["/type.design".to_string()], webgpu).await;
}
#[tokio::test]
#[ignore]
async fn font_files_on_the_gpu_match_the_png() {
font_files_match_their_pngs(true).await;
}
#[tokio::test]
#[ignore]
async fn font_files_on_the_cpu_match_the_png() {
font_files_match_their_pngs(false).await;
}
#[tokio::test]
#[ignore]
async fn the_canvas_on_the_gpu_matches_the_png() {
the_examples_match_their_pngs(true).await;
}
#[tokio::test]
#[ignore]
async fn the_canvas_on_the_cpu_matches_the_png() {
the_examples_match_their_pngs(false).await;
}
#[tokio::test]
#[ignore]
async fn a_click_navigates_back_returns_and_an_overlay_opens_and_closes() {
let dir = folder("click");
let address = serve(&dir, "screen.design").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/screen.design").as_str())
.await
.unwrap();
let painted = "document.querySelector('body > canvas.design')?.dataset.painted === 'true'";
until(&page, painted).await;
let pay = on_screen(&page, "body > canvas.design", 120.0, 40.0).await;
page.move_mouse(pay).await.unwrap();
until(
&page,
"document.querySelector('body > canvas.design').style.cursor === 'pointer'",
)
.await;
let beside = on_screen(&page, "body > canvas.design", 300.0, 40.0).await;
page.click(pay).await.unwrap();
until(&page, "location.pathname === '/next.design'").await;
until_pixel(&page, beside, [0xfe, 0xf3, 0xc7]).await;
let back = on_screen(&page, "body > canvas.design", 120.0, 40.0).await;
page.click(back).await.unwrap();
until(&page, "location.pathname === '/screen.design'").await;
until_pixel(&page, beside, [0xf8, 0xfa, 0xfc]).await;
let sheet = on_screen(&page, "body > canvas.design", 120.0, 100.0).await;
page.click(sheet).await.unwrap();
until(
&page,
"document.querySelector('.design-overlay canvas.design')?.dataset.painted === 'true'",
)
.await;
let close = on_screen(&page, ".design-overlay canvas.design", 120.0, 40.0).await;
page.click(close).await.unwrap();
until(&page, "!document.querySelector('.design-overlay')").await;
assert_eq!(
eval::<String>(&page, "location.pathname").await,
"/screen.design"
);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn an_interaction_inside_an_instance_from_another_folder_opens_its_own_target() {
let dir = folder("nested");
for (name, source) in [
(
"lib/tile.design",
r##"<Frame width={200} height={40} fill="#2563eb" onClick="navigate ./far.design" />"##,
),
(
"lib/far.design",
r##"<Frame width={400} height={300} fill="#fef3c7" />"##,
),
(
"app/home.design",
r##"import Tile from "../lib/tile.design"
<Frame width={400} height={300} fill="#f8fafc" layout="column" padding={20}>
<Tile />
</Frame>"##,
),
] {
let path = dir.join(name);
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
std::fs::write(path, source).unwrap();
}
let address = serve(&dir, "app/home.design").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/app/home.design").as_str())
.await
.unwrap();
let painted = "document.querySelector('body > canvas.design')?.dataset.painted === 'true'";
until(&page, painted).await;
let tile = on_screen(&page, "body > canvas.design", 120.0, 40.0).await;
page.click(tile).await.unwrap();
until(&page, "location.pathname === '/lib/far.design'").await;
until(&page, painted).await;
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn an_embedded_design_draws_and_survives_a_page_edit() {
let dir = folder("embed");
std::fs::write(
dir.join("page.md"),
"# Page\n\nThe screen:\n\n\n",
)
.unwrap();
let address = serve(&dir, "page.md").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/page.md").as_str())
.await
.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
let [width, height]: [f64; 2] = eval(
&page,
"(() => { const b = document.querySelector('canvas.design').getBoundingClientRect(); \
return [b.width, b.height]; })()",
)
.await;
assert_eq!([width, height], [400.0, 300.0]);
page.evaluate("document.querySelector('canvas.design').marked = true")
.await
.unwrap();
let corner = on_screen(&page, "canvas.design", 300.0, 40.0).await;
let grey = pixel(&page, corner).await;
assert_eq!(grey, [0xf8, 0xfa, 0xfc]);
let pay = on_screen(&page, "canvas.design", 120.0, 40.0).await;
page.click(pay).await.unwrap();
until_pixel(&page, corner, [0xfe, 0xf3, 0xc7]).await;
assert_eq!(eval::<String>(&page, "location.pathname").await, "/page.md");
std::fs::write(
dir.join("page.md"),
"# Page\n\nThe screen, edited:\n\n\n",
)
.unwrap();
page.evaluate("load(location.href)").await.unwrap();
until(
&page,
"document.querySelector('article').textContent.includes('edited')",
)
.await;
assert!(
eval::<bool>(
&page,
"document.querySelector('canvas.design').marked === true"
)
.await
);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_doc_prints_its_embedded_design() {
let dir = folder("print");
std::fs::write(
dir.join("doc.md"),
"# Doc\n\n\n",
)
.unwrap();
let document =
Arc::new(Document::load(&dir.join("doc.md"), &ServedFolder::new(&dir).unwrap()).unwrap());
let with = crate::drive::pdf::print(document).await.unwrap();
std::fs::write(dir.join("plain.md"), "# Doc\n\nNo design.\n").unwrap();
let document =
Arc::new(Document::load(&dir.join("plain.md"), &ServedFolder::new(&dir).unwrap()).unwrap());
let without = crate::drive::pdf::print(document).await.unwrap();
let images = |bytes: &[u8]| bytes.windows(13).filter(|w| w == b"/Subtype /Ima").count();
assert!(images(&with) > 0, "no image in the printed doc");
assert_eq!(images(&without), 0);
}
#[tokio::test]
#[ignore]
async fn the_wheel_zooms_about_the_pointer_and_pans() {
let dir = folder("zoom");
let address = serve(&dir, "screen.design").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/screen.design").as_str())
.await
.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
let view = || async {
eval::<String>(
&page,
"document.querySelector('canvas.design').dataset.view",
)
.await
};
let fitted = view().await;
let [zoom, x, y]: [f64; 3] = serde_json::from_str(&format!("[{fitted}]")).unwrap();
assert_eq!(zoom, 1.0);
assert!(x > 0.0 && y > 0.0, "{fitted}");
let pointer = on_screen(&page, "canvas.design", 100.0, 100.0).await;
page.evaluate(format!(
"document.querySelector('canvas.design').dispatchEvent(new WheelEvent('wheel', \
{{ deltaY: -100, metaKey: true, clientX: {}, clientY: {}, bubbles: true, cancelable: true }}))",
pointer.x, pointer.y
))
.await
.unwrap();
until(
&page,
"document.querySelector('canvas.design').dataset.view.split(',')[0] > 2",
)
.await;
let after = on_screen(&page, "canvas.design", 100.0, 100.0).await;
assert!((after.x - pointer.x).abs() < 0.5 && (after.y - pointer.y).abs() < 0.5);
let before: String = view().await;
page.evaluate(
"document.querySelector('canvas.design').dispatchEvent(new WheelEvent('wheel', \
{ deltaX: 40, deltaY: 0, bubbles: true, cancelable: true }))",
)
.await
.unwrap();
until(
&page,
&format!("document.querySelector('canvas.design').dataset.view !== '{before}'"),
)
.await;
let panned = on_screen(&page, "canvas.design", 100.0, 100.0).await;
assert!((panned.x - (after.x - 40.0)).abs() < 0.5);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn an_undone_typo_clears_the_problems() {
let dir = folder("undo");
let (address, designs) = serve_designs(&dir, "screen.design").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/screen.design").as_str())
.await
.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
let file = dir.join("screen.design");
let good = std::fs::read_to_string(&file).unwrap();
std::fs::write(&file, good.replace("<Button label", "<Button lable")).unwrap();
designs.changed(&file);
until(&page, "!!document.querySelector('.design-problems')").await;
std::fs::write(&file, &good).unwrap();
designs.changed(&file);
until(&page, "!document.querySelector('.design-problems')").await;
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_deleted_design_comes_back_when_written_again() {
a_deleted_design_comes_back_on("next.design").await;
}
#[tokio::test]
#[ignore]
async fn a_deleted_embedded_design_comes_back_when_written_again() {
a_deleted_design_comes_back_on("page.md").await;
}
async fn a_deleted_design_comes_back_on(at: &str) {
let dir = folder(&format!("deleted-{}", at.replace('.', "-")));
std::fs::write(
dir.join("page.md"),
"# Page\n\nThe next screen:\n\n\n",
)
.unwrap();
let (address, designs) = serve_designs(&dir, "page.md").await;
let mut browser = testing::browser().await;
let page = browser.new_page("about:blank").await.unwrap();
page.execute(AddScriptToEvaluateOnNewDocumentParams::new(
"window.watchCalls = 0; const f = window.fetch; \
window.fetch = (url, ...rest) => { if (String(url).includes('/Watch')) window.watchCalls++; return f(url, ...rest); };",
))
.await
.unwrap();
page.goto(format!("{address}/{at}")).await.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
let file = dir.join("next.design");
let source = std::fs::read_to_string(&file).unwrap();
std::fs::remove_file(&file).unwrap();
designs.changed(&file);
until(&page, "!!document.querySelector('.design-problems')").await;
let calls: u32 = eval(&page, "window.watchCalls").await;
if at == "page.md" {
page.evaluate("load(location.href)").await.ok();
}
std::fs::write(&file, source).unwrap();
designs.changed(&file);
until(&page, "!document.querySelector('.design-problems')").await;
let later: u32 = eval(&page, "window.watchCalls").await;
assert_eq!(later, calls, "{at}: the page reopened its stream");
page.close().await.ok();
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_canvas_says_so_when_the_engine_does_not_load() {
let dir = folder("no-engine");
let address = serve(&dir, "screen.design").await;
let mut browser = testing::browser().await;
let page = browser.new_page("about:blank").await.unwrap();
page.execute(AddScriptToEvaluateOnNewDocumentParams::new(
"const no = () => Promise.reject(new Error('no WebAssembly here')); \
WebAssembly.instantiate = no; WebAssembly.instantiateStreaming = no;",
))
.await
.unwrap();
page.goto(format!("{address}/screen.design")).await.unwrap();
until(&page, "!!document.querySelector('.design-failed')").await;
let text: String = eval(
&page,
"document.querySelector('.design-failed').textContent",
)
.await;
assert!(text.contains("design engine didn't load"), "{text}");
assert!(text.contains("no WebAssembly here"), "{text}");
browser.close().await.ok();
}
const EMBED_BOX: &str = "(() => { const c = document.querySelector('canvas.design'); \
const s = c.parentElement; const st = getComputedStyle(s); \
const b = c.getBoundingClientRect(); const o = s.getBoundingClientRect(); \
return [b.width, b.height, \
b.left - o.left - parseFloat(st.paddingLeft), \
o.right - parseFloat(st.paddingRight) - b.right]; })()";
async fn embedded(
browser: &mut Browser,
address: &str,
path: &str,
width: i64,
height: i64,
) -> Page {
let page = browser.new_page("about:blank").await.unwrap();
page.execute(SetDeviceMetricsOverrideParams::new(
width, height, 1.0, false,
))
.await
.unwrap();
page.goto(format!("{address}{path}")).await.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
page
}
#[tokio::test]
#[ignore]
async fn an_embedded_design_is_its_own_size_centred_and_only_ever_scaled_down() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let dir = folder("own-size");
std::fs::write(
dir.join("page.md"),
"# Page\n\n\n",
)
.unwrap();
let address = serve(&dir, "page.md").await;
let mut browser = testing::browser().await;
let page = embedded(&mut browser, &address, "/page.md", 1280, 1000).await;
let [w, h, left, right]: [f64; 4] = eval(&page, EMBED_BOX).await;
assert_eq!([w, h], [400.0, 300.0]);
assert!(
left > 0.0 && (left - right).abs() <= 1.0,
"{left} vs {right}"
);
assert!(
eval::<String>(
&page,
"document.querySelector('canvas.design').dataset.view"
)
.await
.starts_with("1,")
);
let page = embedded(&mut browser, &address, "/page.md", 300, 1000).await;
let [w, h, left, right]: [f64; 4] = eval(&page, EMBED_BOX).await;
assert!(
w < 300.0 && left >= 0.0 && right >= -0.5,
"{w} {left} {right}"
);
assert!((h - w * 0.75).abs() <= 1.0, "{h} for {w}");
let page = embedded(&mut browser, &address, "/page.md", 1280, 240).await;
let [w, h, _, _]: [f64; 4] = eval(&page, EMBED_BOX).await;
assert!(h <= 240.0 && (h - w * 0.75).abs() <= 1.0, "{w} × {h}");
let address = serve(root, "docs/boxes/viewer/designs/README.md").await;
let page = embedded(
&mut browser,
&address,
"/docs/boxes/viewer/designs/README.md",
1280,
1000,
)
.await;
let [w, _, left, right]: [f64; 4] = eval(&page, EMBED_BOX).await;
assert_eq!(w, 390.0);
assert!((left - right).abs() <= 1.0, "{left} vs {right}");
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn the_backdrops_link_opens_the_design_it_shows_on_its_canvas_page() {
let dir = folder("open-link");
std::fs::create_dir_all(dir.join("notes")).unwrap();
std::fs::write(
dir.join("notes/page.md"),
"# Page\n\n\n",
)
.unwrap();
let address = serve(&dir, "notes/page.md").await;
let mut browser = testing::browser().await;
let page = embedded(&mut browser, &address, "/notes/page.md", 1280, 1000).await;
let link = "document.querySelector('.design-embed a.design-open')";
let theme: String = eval(&page, "document.documentElement.dataset.theme").await;
assert_eq!(
eval::<String>(&page, &format!("{link}.getAttribute('href')")).await,
format!("/screen.design?theme={theme}")
);
let corner = on_screen(&page, "canvas.design", 300.0, 40.0).await;
let pay = on_screen(&page, "canvas.design", 120.0, 40.0).await;
page.click(pay).await.unwrap();
until_pixel(&page, corner, [0xfe, 0xf3, 0xc7]).await;
assert_eq!(
eval::<String>(&page, &format!("{link}.getAttribute('href')")).await,
format!("/next.design?theme={theme}")
);
page.evaluate(format!("{link}.click()")).await.unwrap();
until(
&page,
"location.pathname === '/next.design' && document.body.classList.contains('design-page')",
)
.await;
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_small_design_is_not_enlarged_on_its_canvas_page() {
let dir = folder("small");
let address = serve(&dir, "button.design").await;
let mut browser = testing::browser().await;
let page = embedded(&mut browser, &address, "/button.design", 1280, 1000).await;
let view: String = eval(
&page,
"document.querySelector('canvas.design').dataset.view",
)
.await;
let [zoom, x, y]: [f64; 3] = view
.split(',')
.map(|n| n.parse().unwrap())
.collect::<Vec<_>>()
.try_into()
.unwrap();
assert_eq!(zoom, 1.0);
assert_eq!(
[x, y],
[
((1280.0 - 200.0) / 2.0_f64).round(),
((1000.0 - 40.0) / 2.0_f64).round()
]
);
browser.close().await.ok();
}
const WATCH_COUNTER: &str = "window.watches = []; const f = window.fetch; \
window.fetch = (url, options = {}) => { \
if (String(url).includes('/Watch')) window.watches.push(options.signal); \
return f(url, options); };";
const OPEN_WATCHES: &str = "window.watches.filter((s) => s && !s.aborted).length";
fn eight(dir: &Path, name: &str) {
let screen = std::fs::read_to_string(dir.join("screen.design")).unwrap();
let mut page = String::from("# Eight\n\n");
for n in 1..=8 {
std::fs::write(dir.join(format!("s{n}.design")), &screen).unwrap();
page.push_str(&format!("\n\n"));
}
std::fs::write(dir.join(name), page).unwrap();
}
#[tokio::test]
#[ignore]
async fn eight_embedded_designs_paint_through_one_stream_and_the_page_still_loads() {
let dir = folder("eight");
eight(&dir, "page.md");
let address = serve(&dir, "page.md").await;
let mut browser = testing::browser().await;
let page = browser.new_page("about:blank").await.unwrap();
page.execute(AddScriptToEvaluateOnNewDocumentParams::new(WATCH_COUNTER))
.await
.unwrap();
page.goto(format!("{address}/page.md")).await.unwrap();
until(
&page,
"[...document.querySelectorAll('canvas.design')].filter((c) => c.dataset.painted === 'true').length === 8",
)
.await;
assert_eq!(eval::<u32>(&page, OPEN_WATCHES).await, 1);
page.evaluate("document.querySelectorAll('canvas.design').forEach((c) => c.marked = true)")
.await
.unwrap();
let mut edited = std::fs::read_to_string(dir.join("page.md")).unwrap();
edited.push_str("Edited.\n");
std::fs::write(dir.join("page.md"), edited).unwrap();
page.evaluate("load(location.href)").await.unwrap();
until(
&page,
"document.querySelector('article').textContent.includes('Edited.')",
)
.await;
assert_eq!(
eval::<u32>(
&page,
"[...document.querySelectorAll('canvas.design')].filter((c) => c.marked).length"
)
.await,
8
);
assert_eq!(eval::<u32>(&page, "window.watches.length").await, 1);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_doc_with_eight_embeds_prints_as_fast_as_one_with_one() {
let dir = folder("print-eight");
eight(&dir, "eight.md");
std::fs::write(
dir.join("one.md"),
"# One\n\n\n",
)
.unwrap();
let time = |name: &'static str| {
let dir = dir.clone();
async move {
let document = Arc::new(
Document::load(&dir.join(name), &ServedFolder::new(&dir).unwrap()).unwrap(),
);
let started = std::time::Instant::now();
crate::drive::pdf::print(document).await.unwrap();
started.elapsed()
}
};
let one = time("one.md").await;
let eight = time("eight.md").await;
assert!(
eight < one + Duration::from_secs(5),
"eight embeds took {eight:?}, one took {one:?}"
);
}
#[tokio::test]
#[ignore]
async fn leaving_the_last_design_in_place_closes_the_stream() {
let dir = folder("leave");
std::fs::write(
dir.join("page.md"),
"# Page\n\n\n\n[Plain](plain.md)\n",
)
.unwrap();
std::fs::write(dir.join("plain.md"), "# Plain\n\nNo design.\n").unwrap();
let address = serve(&dir, "page.md").await;
let mut browser = testing::browser().await;
let page = browser.new_page("about:blank").await.unwrap();
page.execute(AddScriptToEvaluateOnNewDocumentParams::new(WATCH_COUNTER))
.await
.unwrap();
page.goto(format!("{address}/page.md")).await.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
assert_eq!(eval::<u32>(&page, OPEN_WATCHES).await, 1);
page.evaluate("document.body.marked = true").await.unwrap();
page.evaluate("[...document.querySelectorAll('article a')].find((a) => a.textContent === 'Plain').click()")
.await
.unwrap();
until(
&page,
"document.querySelector('article').textContent.includes('No design.')",
)
.await;
assert!(eval::<bool>(&page, "document.body.marked === true").await);
until(&page, &format!("{OPEN_WATCHES} === 0")).await;
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn an_embed_with_problems_survives_a_page_refetch() {
let dir = folder("embed-problems");
std::fs::write(
dir.join("page.md"),
"# Page\n\n\n",
)
.unwrap();
let (address, designs) = serve_designs(&dir, "page.md").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/page.md").as_str())
.await
.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
page.evaluate("document.querySelector('canvas.design').marked = true")
.await
.unwrap();
let screen = std::fs::read_to_string(dir.join("screen.design")).unwrap();
std::fs::write(
dir.join("screen.design"),
screen.replace("<Button label", "<Button lable"),
)
.unwrap();
designs.changed(&dir.join("screen.design"));
until(
&page,
"!!document.querySelector('.design-embed .design-problems')",
)
.await;
page.evaluate("load(location.href)").await.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.marked === true \
&& !!document.querySelector('.design-embed .design-problems')",
)
.await;
browser.close().await.ok();
}
const GLASS: &str = r##"<Frame width={120} height={80} fill="#00000000" />"##;
async fn until_same(page: &Page, inside: Point, outside: Point) -> [u8; 3] {
let (mut a, mut b) = ([0; 3], [1; 3]);
for _ in 0..200 {
a = pixel(page, inside).await;
b = pixel(page, outside).await;
if a == b {
return a;
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
panic!("timed out: inside {a:?}, outside {b:?}");
}
#[tokio::test]
#[ignore]
async fn the_canvas_page_switches_theme_and_paints_behind_the_design_in_it() {
let dir = folder("theme-page");
std::fs::write(dir.join("glass.design"), GLASS).unwrap();
let address = serve(&dir, "glass.design").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/glass.design").as_str())
.await
.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
let inside = on_screen(&page, "canvas.design", 60.0, 40.0).await;
let outside = on_screen(&page, "canvas.design", 60.0, 140.0).await;
let before: String = eval(&page, "document.documentElement.dataset.theme").await;
let first = until_same(&page, inside, outside).await;
page.find_element(".theme-toggle")
.await
.unwrap()
.click()
.await
.unwrap();
let after: String = eval(&page, "document.documentElement.dataset.theme").await;
assert_ne!(before, after);
let second = until_same(&page, inside, outside).await;
assert_ne!(first, second, "the background didn't change with the theme");
page.reload().await.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
assert_eq!(
eval::<String>(&page, "document.documentElement.dataset.theme").await,
after
);
browser.close().await.ok();
}
fn themed(dir: &Path) {
std::fs::write(
dir.join("themes.yml"),
"kind: themes\nlight:\n bg: \"#ff0000\"\ndark:\n bg: \"#0000ff\"\n",
)
.unwrap();
std::fs::write(
dir.join("tile.design"),
"<Frame width={120} height={80} fill={theme.bg} />",
)
.unwrap();
}
const RED: [u8; 3] = [255, 0, 0];
const BLUE: [u8; 3] = [0, 0, 255];
async fn tab_in(browser: &mut Browser, theme: &str) -> Page {
let page = browser.new_page("about:blank").await.unwrap();
page.execute(SetDeviceMetricsOverrideParams::new(1280, 1000, 1.0, false))
.await
.unwrap();
page.execute(AddScriptToEvaluateOnNewDocumentParams::new(format!(
"try {{ if (!sessionStorage.getItem('set')) {{ localStorage.setItem('devpage-theme', '{theme}'); sessionStorage.setItem('set', '1'); }} }} catch {{}}"
)))
.await
.unwrap();
page
}
#[tokio::test]
#[ignore]
async fn an_embed_follows_its_page_and_its_own_switch_turns_only_it() {
let dir = folder("theme-own");
themed(&dir);
std::fs::write(
dir.join("page.md"),
"# Page\n\n\n\n\n\n\n",
)
.unwrap();
let address = serve(&dir, "page.md").await;
let mut browser = testing::browser().await;
let page = tab_in(&mut browser, "dark").await;
page.goto(format!("{address}/page.md")).await.unwrap();
until(
&page,
"[...document.querySelectorAll('canvas.design')].every((c) => c.dataset.painted === 'true')",
)
.await;
let one = on_screen(&page, r#"canvas.design[aria-label="One"]"#, 60.0, 40.0).await;
let two = on_screen(&page, r#"canvas.design[aria-label="Two"]"#, 60.0, 40.0).await;
until_pixel(&page, one, BLUE).await;
until_pixel(&page, two, BLUE).await;
let shown = "[...document.querySelectorAll('.design-theme')].map((b) => !b.hidden)";
assert_eq!(eval::<Vec<bool>>(&page, shown).await, [true, true, false]);
let backdrop = |label: &str| {
format!("document.querySelector(\"canvas.design[aria-label='{label}']\").parentElement")
};
let backdrop_color =
|label: &str| format!("getComputedStyle({}).backgroundColor", backdrop(label));
let dark_backdrop: String = eval(&page, &backdrop_color("Two")).await;
page.evaluate(format!(
"{}.querySelector('.design-theme').click()",
backdrop("One")
))
.await
.unwrap();
until_pixel(&page, one, RED).await;
assert_eq!(pixel(&page, two).await, BLUE);
assert_eq!(
eval::<String>(&page, "document.documentElement.dataset.theme").await,
"dark",
"the page is as it was"
);
assert_ne!(
eval::<String>(&page, &backdrop_color("One")).await,
dark_backdrop
);
let href = |label: &str| {
format!(
"{}.querySelector('a.design-open').getAttribute('href')",
backdrop(label)
)
};
assert_eq!(
eval::<String>(&page, &href("One")).await,
"/tile.design?theme=light"
);
assert_eq!(
eval::<String>(&page, &href("Two")).await,
"/tile.design?theme=dark"
);
for (theme, two_color) in [("light", RED), ("dark", BLUE)] {
page.find_element(".theme-toggle")
.await
.unwrap()
.click()
.await
.unwrap();
assert_eq!(
eval::<String>(&page, "document.documentElement.dataset.theme").await,
theme
);
until_pixel(&page, two, two_color).await;
assert_eq!(pixel(&page, one).await, RED, "after the page went {theme}");
}
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn the_canvas_page_shows_the_theme_its_address_names() {
let dir = folder("theme-address");
themed(&dir);
let address = serve(&dir, "tile.design").await;
let mut browser = testing::browser().await;
let page = tab_in(&mut browser, "light").await;
page.goto(format!("{address}/tile.design?theme=dark"))
.await
.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
assert_eq!(
eval::<String>(&page, "document.documentElement.dataset.theme").await,
"dark"
);
let inside = on_screen(&page, "canvas.design", 60.0, 40.0).await;
until_pixel(&page, inside, BLUE).await;
assert_eq!(
eval::<String>(&page, "localStorage.getItem('devpage-theme')").await,
"light",
"the remembered choice is untouched"
);
page.find_element(".theme-toggle")
.await
.unwrap()
.click()
.await
.unwrap();
until_pixel(&page, inside, RED).await;
assert_eq!(
eval::<String>(&page, "localStorage.getItem('devpage-theme')").await,
"light"
);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn an_embedded_design_paints_again_when_the_page_switches_theme() {
let dir = folder("theme-embed");
std::fs::write(dir.join("glass.design"), GLASS).unwrap();
std::fs::write(dir.join("page.md"), "# Page\n\n\n").unwrap();
let address = serve(&dir, "page.md").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/page.md").as_str())
.await
.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
let inside = on_screen(&page, "canvas.design", 60.0, 40.0).await;
let outside = on_screen(&page, "canvas.design", -120.0, 40.0).await;
let first = until_same(&page, inside, outside).await;
let before: String = eval(&page, "document.documentElement.dataset.theme").await;
page.find_element(".theme-toggle")
.await
.unwrap()
.click()
.await
.unwrap();
let after: String = eval(&page, "document.documentElement.dataset.theme").await;
assert_ne!(before, after);
let second = until_same(&page, inside, outside).await;
assert_ne!(first, second, "the backdrop didn't change with the theme");
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_design_on_a_widget_slide_fits_the_slide_on_screen_and_on_paper() {
let dir = folder("slide");
std::fs::write(
dir.join("tall.design"),
r##"<Frame width={300} height={1400} fill="#2563eb" />"##,
)
.unwrap();
std::fs::create_dir_all(dir.join("widgets")).unwrap();
std::fs::write(
dir.join("widgets/Box.tsx"),
"export default function Box({ children }) { return <div className=\"box\">{children}</div>; }\n",
)
.unwrap();
std::fs::write(dir.join("one.md"), "# One\n").unwrap();
std::fs::write(
dir.join("two.md"),
"# Two\n\n<Box>\n\n\n\n</Box>\n",
)
.unwrap();
std::fs::write(
dir.join("deck.yml"),
"kind: slides\ntitle: Deck\nwidgets: widgets\nslides: [one.md, two.md]\n",
)
.unwrap();
let address = serve(&dir, "deck.yml").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/deck.yml#1").as_str())
.await
.unwrap();
until(
&page,
"!!document.querySelector('.box canvas.design[data-started]')",
)
.await;
page.evaluate("location.hash = '#2'").await.unwrap();
until(
&page,
"(() => { const c = document.querySelector('.box canvas.design'); \
if (c?.dataset.painted !== 'true') return false; const s = c.closest('.slide'); \
const scale = s.getBoundingClientRect().height / s.offsetHeight; \
window.measured = [(c.getBoundingClientRect().bottom - s.getBoundingClientRect().top) / scale, s.offsetHeight]; \
return true; })()",
)
.await;
let [bottom, slide]: [f64; 2] = eval(&page, "window.measured").await;
assert!(
bottom <= slide,
"the design runs past the slide: {bottom} > {slide}"
);
browser.close().await.ok();
let document =
Arc::new(Document::load(&dir.join("deck.yml"), &ServedFolder::new(&dir).unwrap()).unwrap());
let started = std::time::Instant::now();
let bytes = crate::drive::pdf::print(document).await.unwrap();
assert!(
started.elapsed() < Duration::from_secs(15),
"printing waited out its cap: {:?}",
started.elapsed()
);
assert!(
bytes.windows(13).any(|w| w == b"/Subtype /Ima"),
"no image in the printed deck"
);
}
#[tokio::test]
#[ignore]
async fn an_embed_in_a_widget_keeps_where_it_navigated_across_a_page_refetch() {
let dir = folder("widget-refetch");
std::fs::create_dir_all(dir.join("widgets")).unwrap();
std::fs::write(
dir.join("widgets/Box.tsx"),
"export default function Box({ children }) { return <div className=\"box\">{children}</div>; }\n",
)
.unwrap();
let page_source = |text: &str| {
format!("# Doc\n\n{text}\n\n<Box>\n\n\n\n</Box>\n")
};
std::fs::write(dir.join("doc.md"), page_source("Before.")).unwrap();
std::fs::write(
dir.join("doc.yml"),
"kind: doc\ntitle: Doc\nwidgets: widgets\nchapters: [doc.md]\n",
)
.unwrap();
let address = serve(&dir, "doc.yml").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/doc.yml").as_str())
.await
.unwrap();
let painted = "document.querySelector('.box canvas.design')?.dataset.painted === 'true'";
until(&page, painted).await;
let corner = on_screen(&page, ".box canvas.design", 300.0, 40.0).await;
let pay = on_screen(&page, ".box canvas.design", 120.0, 40.0).await;
page.click(pay).await.unwrap();
until_pixel(&page, corner, [0xfe, 0xf3, 0xc7]).await;
std::fs::write(dir.join("doc.md"), page_source("After.")).unwrap();
page.evaluate("load(location.href)").await.unwrap();
until(
&page,
"document.querySelector('article')?.textContent.includes('After.')",
)
.await;
until(&page, painted).await;
let corner = on_screen(&page, ".box canvas.design", 300.0, 40.0).await;
until_pixel(&page, corner, [0xfe, 0xf3, 0xc7]).await;
assert_eq!(
eval::<usize>(
&page,
"document.querySelectorAll('.box a.design-open').length"
)
.await,
1,
"one link to the canvas page"
);
let back = on_screen(&page, ".box canvas.design", 120.0, 40.0).await;
page.click(back).await.unwrap();
until_pixel(&page, corner, [0xf8, 0xfa, 0xfc]).await;
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn the_canvas_pages_switch_leaves_the_problems_bar_whole() {
let dir = folder("corner");
std::fs::write(
dir.join("broken.design"),
r##"<Frame width={200} height={100} fill="#ffffff" lable="typo" />"##,
)
.unwrap();
let address = serve(&dir, "broken.design").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/broken.design").as_str())
.await
.unwrap();
until(&page, "!!document.querySelector('body > .design-problems')").await;
for theme in ["light", "dark"] {
page.evaluate(format!(
"document.documentElement.dataset.theme = '{theme}'"
))
.await
.unwrap();
let overlap: bool = eval(
&page,
"(() => { const a = document.querySelector('body > .design-problems').getBoundingClientRect(); \
const b = document.querySelector('#devpage-corner').getBoundingClientRect(); \
return a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom; })()",
)
.await;
assert!(!overlap, "the switch covers the problems bar in {theme}");
}
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn an_embed_inside_a_widget_on_a_page_in_a_subfolder_finds_its_design() {
let dir = folder("widget-subfolder");
let sub = dir.join("sub");
std::fs::create_dir_all(&sub).unwrap();
designs(&sub);
std::fs::write(
dir.join("devpage.yml"),
"kind: site\ntitle: Site\nroot: README.md\n",
)
.unwrap();
std::fs::write(
dir.join("README.md"),
"# Root\n\n## Topics\n\n- [Sub](sub/page.md)\n",
)
.unwrap();
std::fs::write(
sub.join("page.md"),
"# Sub\n\n\n\n<Callout>\n\n\n\n</Callout>\n\n\
<Tabs>\n<Tab title=\"One\">\n\n\n\n</Tab>\n</Tabs>\n\n\
<Steps>\n\n1. In a step\n\n \n\n2. In a callout in a step\n\n <Callout>\n\n \n\n </Callout>\n\n</Steps>\n",
)
.unwrap();
let address = serve(&dir, "devpage.yml").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/sub/page.md").as_str())
.await
.unwrap();
until(
&page,
"document.querySelectorAll('canvas.design').length === 5 \
&& [...document.querySelectorAll('canvas.design')].every((c) => c.dataset.painted === 'true')",
)
.await;
let links: Vec<String> = eval(
&page,
"[...document.querySelectorAll('a.design-open')].map((a) => new URL(a.href).pathname)",
)
.await;
assert_eq!(links, vec!["/sub/button.design"; 5]);
assert_eq!(
eval::<usize>(
&page,
"document.querySelectorAll('.design-problems').length"
)
.await,
0,
"no embed reports a problem"
);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_design_in_a_tab_opened_later_draws_when_it_appears() {
let dir = folder("later-tab");
designs(&dir);
std::fs::write(
dir.join("page.md"),
"# Tabs\n\n<Tabs>\n<Tab title=\"Words\">\n\nNo design here.\n\n</Tab>\n<Tab title=\"Design\">\n\n\n\n</Tab>\n</Tabs>\n",
)
.unwrap();
let address = serve(&dir, "page.md").await;
let mut browser = testing::browser().await;
let page = browser
.new_page(format!("{address}/page.md").as_str())
.await
.unwrap();
until(&page, "document.documentElement.dataset.ready === 'true'").await;
assert_eq!(
eval::<usize>(&page, "document.querySelectorAll('canvas.design').length").await,
0,
"the second tab isn't on the page yet"
);
let open =
|n: usize| format!("document.querySelectorAll('.tabs-bar button')[{n}].click(); true");
eval::<bool>(&page, &open(1)).await;
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
eval::<bool>(&page, &open(0)).await;
until(&page, "!document.querySelector('canvas.design')").await;
eval::<bool>(&page, &open(1)).await;
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
browser.close().await.ok();
}
async fn a_design_on_a_scaled_slide_is_painted_whole(webgpu: bool) {
let dir = folder(if webgpu {
"scaled-slide-gpu"
} else {
"scaled-slide-cpu"
});
std::fs::write(
dir.join("corners.design"),
r##"<Frame width={800} height={600} fill="#ffffff">
<Rect x={0} y={0} width={200} height={150} fill="#ff0000" />
<Rect x={600} y={0} width={200} height={150} fill="#00ff00" />
<Rect x={0} y={450} width={200} height={150} fill="#0000ff" />
<Rect x={600} y={450} width={200} height={150} fill="#ffff00" onClick="navigate ./next.design" />
</Frame>"##,
)
.unwrap();
std::fs::write(
dir.join("next.design"),
r##"<Frame width={800} height={600} fill="#00ff00" />"##,
)
.unwrap();
std::fs::write(
dir.join("one.md"),
"# Corners\n\n\n",
)
.unwrap();
std::fs::write(
dir.join("deck.yml"),
"kind: slides\ntitle: Deck\nslides: [one.md]\n",
)
.unwrap();
let address = serve(&dir, "deck.yml").await;
let mut browser = testing::browser().await;
for (width, height) in [(1280, 720), (1600, 900)] {
let page = browser.new_page("about:blank").await.unwrap();
page.execute(SetDeviceMetricsOverrideParams::new(
width, height, 1.0, false,
))
.await
.unwrap();
if !webgpu {
page.execute(AddScriptToEvaluateOnNewDocumentParams::new(
"delete Navigator.prototype.gpu;",
))
.await
.unwrap();
}
page.goto(format!("{address}/deck.yml#1")).await.unwrap();
until(
&page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
let [left, top, w, h]: [f64; 4] = eval(
&page,
"(() => { const b = document.querySelector('canvas.design').getBoundingClientRect(); \
return [b.left, b.top, b.width, b.height]; })()",
)
.await;
let at = |fx: f64, fy: f64| Point::new(left + w * fx, top + h * fy);
for (point, color, corner) in [
(at(0.1, 0.1), [255, 0, 0], "top left"),
(at(0.9, 0.1), [0, 255, 0], "top right"),
(at(0.1, 0.9), [0, 0, 255], "bottom left"),
(at(0.9, 0.9), [255, 255, 0], "bottom right"),
(at(0.5, 0.5), [255, 255, 255], "middle"),
] {
eprintln!("{width}×{height}, webgpu {webgpu}: the {corner}");
until_pixel(&page, point, color).await;
}
page.click(at(0.9, 0.9)).await.unwrap();
until_pixel(&page, at(0.1, 0.1), [0, 255, 0]).await;
page.close().await.ok();
}
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_design_on_a_scaled_slide_is_painted_whole_on_the_gpu() {
a_design_on_a_scaled_slide_is_painted_whole(true).await;
}
#[tokio::test]
#[ignore]
async fn a_design_on_a_scaled_slide_is_painted_whole_on_the_cpu() {
a_design_on_a_scaled_slide_is_painted_whole(false).await;
}
fn painted_png(w: u32, h: u32, fill: &str) -> Vec<u8> {
let files: std::collections::HashMap<String, String> = [(
"p.design".to_string(),
format!(r##"<Frame width={{{w}}} height={{{h}}} fill="{fill}" />"##),
)]
.into();
let scene = devpage_design::resolve(&files, "p.design").unwrap();
devpage_design::png::fresh(
&mut devpage_design::Renderer::new(),
&scene,
devpage_design::Theme::Light,
)
.unwrap()
}
async fn gpu_against_png(page: &Page, address: &str, name: &str) -> Result<(), String> {
gpu_against_png_within(page, address, name, 24).await
}
async fn gpu_against_png_within(
page: &Page,
address: &str,
name: &str,
margin: usize,
) -> Result<(), String> {
until(
page,
"document.querySelector('canvas.design')?.dataset.painted === 'true'",
)
.await;
let backend: String = eval(
page,
"document.querySelector('canvas.design').dataset.backend",
)
.await;
assert_eq!(backend, "gpu");
let [zoom, width, height]: [f64; 3] = eval(
page,
"(() => { const c = document.querySelector('canvas.design'); return [Number(c.dataset.view.split(',')[0]), ...c.dataset.size.split(',').map(Number)]; })()",
)
.await;
assert_eq!(zoom, 1.0, "{name} isn't shown at 100%");
assert!(width > 0.0 && height > 0.0, "{name} has no size");
until_shows_png(page, address, &format!("/{name}.design.png"), margin)
.await
.map_err(|why| format!("gpu {why}"))
}
async fn the_gpu_matches_the_png(dir: &Path, names: &[&str]) {
let address = serve(dir, &format!("{}.design", names[0])).await;
let mut browser = testing::browser().await;
let mut failures = Vec::new();
for name in names {
let page = browser.new_page("about:blank").await.unwrap();
page.execute(SetDeviceMetricsOverrideParams::new(1280, 1000, 1.0, false))
.await
.unwrap();
page.goto(format!("{address}/{name}.design")).await.unwrap();
if let Err(e) = gpu_against_png(&page, &address, name).await {
failures.push(e);
}
page.close().await.ok();
}
assert!(failures.is_empty(), "{failures:#?}");
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn images_larger_than_the_gpu_atlas_draw_on_the_gpu_as_in_the_png() {
let dir = folder("big-images");
std::fs::write(
dir.join("wide.png"),
painted_png(
9000,
300,
"linear-gradient(90deg, #ff0000 0%, #00ff00 25%, #0000ff 50%, #ffff00 75%, #ff00ff 100%)",
),
)
.unwrap();
std::fs::write(
dir.join("wide.design"),
r##"<Frame width={980} height={52} fill="#ffffff" padding={10}><Image src="wide.png" width={960} height={32} /></Frame>"##,
)
.unwrap();
let colors = [
"#ef4444", "#f97316", "#eab308", "#22c55e", "#06b6d4", "#3b82f6", "#8b5cf6", "#ec4899",
"#14b8a6", "#84cc16", "#a855f7", "#64748b",
];
let mut rows = [String::new(), String::new()];
for (i, c) in colors.iter().enumerate() {
std::fs::write(
dir.join(format!("big{i}.png")),
painted_png(
2000,
2000,
&format!("linear-gradient({}deg, {c} 0%, #ffffff 100%)", 30 * i),
),
)
.unwrap();
rows[i / 6].push_str(&format!(
r#"<Image src="big{i}.png" width={{90}} height={{90}} />"#
));
}
let many = |rows: &[String; 2]| {
format!(
r##"<Frame layout="column" gap={{6}} padding={{10}} fill="#ffffff">
<Frame layout="row" gap={{6}}>{}</Frame>
<Frame layout="row" gap={{6}}>{}</Frame>
</Frame>"##,
rows[0], rows[1]
)
};
std::fs::write(dir.join("many.design"), many(&rows)).unwrap();
the_gpu_matches_the_png(&dir, &["wide", "many"]).await;
let (address, designs) = serve_designs(&dir, "many.design").await;
let mut browser = testing::browser().await;
let page = browser.new_page("about:blank").await.unwrap();
page.execute(SetDeviceMetricsOverrideParams::new(1280, 1000, 1.0, false))
.await
.unwrap();
page.goto(format!("{address}/many.design")).await.unwrap();
gpu_against_png(&page, &address, "many").await.unwrap();
let mut swapped = String::new();
for (i, color) in colors.iter().take(6).enumerate() {
std::fs::write(
dir.join(format!("new{i}.png")),
painted_png(
2000,
2000,
&format!(
"linear-gradient({}deg, #0f172a 0%, {color} 100%)",
45 + 20 * i
),
),
)
.unwrap();
swapped.push_str(&format!(
r#"<Image src="new{i}.png" width={{90}} height={{90}} />"#
));
}
eval::<bool>(
&page,
"(document.querySelector('canvas.design').dataset.painted = 'false', true)",
)
.await;
std::fs::write(dir.join("many.design"), many(&[rows[0].clone(), swapped])).unwrap();
designs.changed(&dir.join("many.design"));
gpu_against_png(&page, &address, "many").await.unwrap();
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn shadows_in_pieces_draw_on_the_gpu_as_in_the_png() {
let dir = folder("shadow-pieces");
std::fs::write(
dir.join("shadows.design"),
r##"<Frame width={1100} height={760} fill="#f8fafc" layout="none">
<Rect x={40} y={40} width={600} height={400} radius={[2, 60, 12, 120]} fill="#ffffff" shadow={{ y: 6, blur: 4, color: "#0f172a66" }} />
<Ellipse x={700} y={60} width={360} height={200} fill="#ffffff" shadow={{ x: 4, blur: 3, color: "#1e3a8a80" }} />
<Ellipse x={700} y={320} width={360} height={360} fill="#ffffff" shadow={{ blur: 2, spread: 6, color: "#000000" }} />
<Rect x={40} y={500} width={600} height={6} fill="#ffffff" shadow={{ blur: 5, color: "#7c3aed" }} />
<Rect x={40} y={560} width={620} height={160} radius={80} fill="#ffffff" shadow={{ blur: 2, color: "#000000aa" }} />
</Frame>"##,
)
.unwrap();
the_gpu_matches_the_png(&dir, &["shadows"]).await;
}
#[tokio::test]
#[ignore]
async fn a_navigation_from_a_full_atlas_draws_the_next_design_as_its_png() {
let dir = folder("atlas-navigation");
for (name, color) in [
("red", "#dc0000"),
("green", "#00c800"),
("blue", "#0000dc"),
("yellow", "#dcdc00"),
("magenta", "#c800c8"),
] {
std::fs::write(
dir.join(format!("{name}.png")),
painted_png(4096, 4096, color),
)
.unwrap();
}
std::fs::write(
dir.join("a.design"),
r##"<Frame width={400} height={100} fill="#ffffff" layout="row">
<Image src="red.png" width={100} height={100} onClick="navigate ./b.design" />
<Image src="green.png" width={100} height={100} />
<Image src="blue.png" width={100} height={100} />
<Image src="yellow.png" width={100} height={100} />
</Frame>"##,
)
.unwrap();
std::fs::write(
dir.join("b.design"),
r##"<Frame width={400} height={300} fill="#ffffff" layout="row">
<Image src="magenta.png" width={200} height={200} />
</Frame>"##,
)
.unwrap();
std::fs::write(dir.join("page.md"), "# Page\n\n\n").unwrap();
let address = serve(&dir, "a.design").await;
let mut browser = testing::browser().await;
let mut failures = Vec::new();
for (start, margin) in ["a.design", "page.md"].into_iter().zip([24, 0]) {
let page = browser.new_page("about:blank").await.unwrap();
page.execute(SetDeviceMetricsOverrideParams::new(1280, 1000, 1.0, false))
.await
.unwrap();
page.goto(format!("{address}/{start}")).await.unwrap();
if let Err(e) = gpu_against_png_within(&page, &address, "a", margin).await {
failures.push(format!("{start}: {e}"));
}
eval::<bool>(
&page,
"(document.querySelector('canvas.design').dataset.painted = 'false', true)",
)
.await;
let red = on_screen(&page, "canvas.design", 50.0, 50.0).await;
page.click(red).await.unwrap();
until(
&page,
"document.querySelector('canvas.design').dataset.size === '400,300'",
)
.await;
if let Err(e) = gpu_against_png_within(&page, &address, "b", margin).await {
failures.push(format!("{start}: {e}"));
}
page.close().await.ok();
}
assert!(failures.is_empty(), "{failures:#?}");
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_fill_saved_on_the_gpu_repaints_its_tiles_with_their_images() {
let dir = folder("fill-saved-gpu");
for (name, fill) in [
("a", "#dc0000"),
("b", "#00c800"),
("c", "#0000dc"),
("x", "#c800c8"),
("y", "#00c8c8"),
] {
std::fs::write(dir.join(format!("{name}.png")), painted_png(64, 64, fill)).unwrap();
}
let design = |fill: &str| {
let images: String = "abcxy"
.chars()
.map(|c| format!(r#"<Image src="./{c}.png" width={{40}} height={{40}} />"#))
.collect();
format!(
r##"<Frame layout="row" gap={{4}} padding={{8}} fill="#ffffff">{images}<Rect width={{40}} height={{40}} fill="{fill}" /></Frame>"##
)
};
let file = dir.join("fill.design");
std::fs::write(&file, design("#000000")).unwrap();
let (address, designs) = serve_designs(&dir, "fill.design").await;
let mut browser = testing::browser().await;
let page = canvas_page(&mut browser, true, 1280, 1000).await;
page.goto(format!("{address}/fill.design")).await.unwrap();
gpu_against_png(&page, &address, "fill").await.unwrap();
let painted = number(&page, "tiles").await;
eval::<bool>(
&page,
"(document.querySelector('canvas.design').dataset.painted = 'false', true)",
)
.await;
std::fs::write(&file, design("#ffaa00")).unwrap();
designs.changed(&file);
gpu_against_png(&page, &address, "fill").await.unwrap();
assert!(number(&page, "tiles").await > painted, "no tile repainted");
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn an_image_that_breaks_mid_session_draws_as_its_png_once_mended() {
let dir = folder("image-breaks");
for (name, fill) in [("a", "#dc0000"), ("b", "#00c800"), ("c", "#0000dc")] {
std::fs::write(
dir.join(format!("{name}.png")),
painted_png(4096, 4096, fill),
)
.unwrap();
}
let small = painted_png(8, 8, "#c800c8");
std::fs::write(dir.join("x.png"), &small).unwrap();
std::fs::write(dir.join("y.png"), painted_png(8, 8, "#00c8c8")).unwrap();
let design = |fill: &str| {
let images: String = "abcxy"
.chars()
.map(|c| format!(r#"<Image src="./{c}.png" width={{40}} height={{40}} />"#))
.collect();
format!(
r##"<Frame layout="row" gap={{4}} padding={{8}} fill="#ffffff">{images}<Rect width={{40}} height={{40}} fill="{fill}" /></Frame>"##
)
};
let file = dir.join("grows.design");
std::fs::write(&file, design("#000000")).unwrap();
let (address, designs) = serve_designs(&dir, "grows.design").await;
let mut browser = testing::browser().await;
let page = canvas_page(&mut browser, true, 1280, 1000).await;
page.goto(format!("{address}/grows.design")).await.unwrap();
until(&page, PAINTED).await;
let backend: String = eval(
&page,
"document.querySelector('canvas.design').dataset.backend",
)
.await;
assert_eq!(backend, "gpu");
std::fs::write(dir.join("x.png"), painted_png(4096, 4096, "#c800c8")).unwrap();
designs.changed(&dir.join("x.png"));
until(&page, "!!document.querySelector('.design-problems')").await;
let bar: String = eval(
&page,
"document.querySelector('.design-problems').textContent",
)
.await;
assert!(bar.contains("doesn't fit"), "{bar}");
std::fs::write(&file, design("#ffaa00")).unwrap();
designs.changed(&file);
eval::<bool>(
&page,
"(document.querySelector('canvas.design').dataset.painted = 'false', true)",
)
.await;
std::fs::write(dir.join("x.png"), &small).unwrap();
designs.changed(&dir.join("x.png"));
until(&page, "!document.querySelector('.design-problems')").await;
gpu_against_png(&page, &address, "grows").await.unwrap();
browser.close().await.ok();
}
#[derive(Clone, Copy, Debug)]
enum Break {
Decode,
Atlas,
}
async fn a_broken_design_paints_its_last_good_drawing_where_it_was_not_painted(
webgpu: bool,
broken: Break,
) {
let dir = folder(&format!("last-good-{broken:?}-{webgpu}"));
for (name, fill) in [("a", "#cccccc"), ("b", "#bbbbbb"), ("c", "#aaaaaa")] {
std::fs::write(
dir.join(format!("{name}.png")),
painted_png(4096, 4096, fill),
)
.unwrap();
}
std::fs::write(dir.join("x.png"), painted_png(8, 8, "#c800c8")).unwrap();
std::fs::write(dir.join("y.png"), painted_png(8, 8, "#00c8c8")).unwrap();
let design = |fill: &str| {
let images: String = "abcxy"
.chars()
.map(|c| format!(r#"<Image src="./{c}.png" width={{40}} height={{40}} />"#))
.collect();
format!(
r##"<Frame layout="column" gap={{4}} padding={{8}} fill="#ffffff"><Frame layout="row" gap={{4}}>{images}</Frame><Rect width={{40}} height={{2000}} fill="#ffffff" /><Rect width={{200}} height={{200}} fill="{fill}" /></Frame>"##
)
};
let file = dir.join("tall.design");
std::fs::write(&file, design("#dc0000")).unwrap();
let (address, designs) = serve_designs(&dir, "tall.design").await;
let mut browser = testing::browser().await;
let page = canvas_page(&mut browser, webgpu, 1280, 1000).await;
page.goto(format!("{address}/tall.design")).await.unwrap();
until(&page, PAINTED).await;
let backend: String = eval(
&page,
"document.querySelector('canvas.design').dataset.backend",
)
.await;
assert_eq!(backend, if webgpu { "gpu" } else { "cpu" });
let x = dir.join("x.png");
match broken {
Break::Decode => std::fs::write(&x, b"not a png").unwrap(),
Break::Atlas => std::fs::write(&x, painted_png(4096, 4096, "#c800c8")).unwrap(),
}
designs.changed(&x);
until(&page, "!!document.querySelector('.design-problems')").await;
std::fs::write(&file, design("#ffaa00")).unwrap();
designs.changed(&file);
let before = number(&page, "tiles").await;
zoom_to(&page, 1.0, 100.0, 2156.0).await;
until(
&page,
&format!(
"Number(document.querySelector('canvas.design').dataset.tiles) > {before} && {PAINTED}"
),
)
.await;
let bottom = on_screen(&page, "canvas.design", 100.0, 2156.0).await;
until_pixel(&page, bottom, [220, 0, 0]).await;
assert!(
eval::<bool>(&page, "!!document.querySelector('.design-problems')").await,
"the problem still shows"
);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn an_image_that_stops_decoding_paints_the_last_good_drawing_on_the_cpu() {
a_broken_design_paints_its_last_good_drawing_where_it_was_not_painted(false, Break::Decode)
.await;
}
#[tokio::test]
#[ignore]
async fn an_image_past_the_atlas_paints_the_last_good_drawing_on_the_cpu() {
a_broken_design_paints_its_last_good_drawing_where_it_was_not_painted(false, Break::Atlas)
.await;
}
#[tokio::test]
#[ignore]
async fn an_image_that_stops_decoding_paints_the_last_good_drawing_on_the_gpu() {
a_broken_design_paints_its_last_good_drawing_where_it_was_not_painted(true, Break::Decode)
.await;
}
#[tokio::test]
#[ignore]
async fn an_image_past_the_atlas_paints_the_last_good_drawing_on_the_gpu() {
a_broken_design_paints_its_last_good_drawing_where_it_was_not_painted(true, Break::Atlas).await;
}
#[tokio::test]
#[ignore]
async fn a_swap_keeps_a_design_outside_a_widget_and_stops_watching_a_removed_widgets() {
let dir = folder("removed-widget");
std::fs::create_dir_all(dir.join("widgets")).unwrap();
std::fs::write(
dir.join("widgets/Box.tsx"),
"export default function Box({ children }) { return <div className=\"box\">{children}</div>; }\n",
)
.unwrap();
let with_box =
"# Doc\n\n\n\n<Box>\n\n\n\n</Box>\n";
std::fs::write(dir.join("doc.md"), with_box).unwrap();
std::fs::write(
dir.join("doc.yml"),
"kind: doc\ntitle: Doc\nwidgets: widgets\nchapters: [doc.md]\n",
)
.unwrap();
let address = serve(&dir, "doc.yml").await;
let mut browser = testing::browser().await;
let page = browser.new_page("about:blank").await.unwrap();
page.execute(AddScriptToEvaluateOnNewDocumentParams::new(WATCH_COUNTER))
.await
.unwrap();
page.goto(format!("{address}/doc.yml")).await.unwrap();
until(
&page,
"[...document.querySelectorAll('canvas.design')].filter((c) => c.dataset.painted === 'true').length === 2",
)
.await;
page.evaluate("window.outside = [...document.querySelectorAll('canvas.design')].find((c) => !c.closest('.box')); window.before = window.watches.length")
.await
.unwrap();
assert!(eval::<bool>(&page, "!!window.outside && !window.outside.closest('.box')").await);
std::fs::write(
dir.join("doc.md"),
"# Doc\n\n\n\nNo box.\n",
)
.unwrap();
page.evaluate("load(location.href)").await.unwrap();
until(
&page,
"document.querySelector('article').textContent.includes('No box.')",
)
.await;
until(
&page,
&format!("window.watches.length === window.before + 1 && {OPEN_WATCHES} === 1"),
)
.await;
assert!(
eval::<bool>(
&page,
"window.outside === document.querySelector('canvas.design') && !document.querySelector('.box')"
)
.await
);
browser.close().await.ok();
}
fn huge(dir: &Path, edited: Option<usize>) {
let rows: String = (0..1000)
.map(|i| {
let text = if Some(i) == edited {
"Edited".to_string()
} else {
format!("Row {i}")
};
format!(
"<Frame width={{400}} height={{100}} fill=\"#{:06x}\" padding={{20}}><Text size={{24}} color=\"#000000\">{text}</Text></Frame>\n",
0x808080 + (i * 2_654_435) % 0x7f7f7f
)
})
.collect();
std::fs::write(
dir.join("huge.design"),
format!("<Frame layout=\"column\" fill=\"#ffffff\">\n{rows}</Frame>\n"),
)
.unwrap();
}
async fn canvas_page(browser: &mut Browser, webgpu: bool, width: i64, height: i64) -> Page {
let page = browser.new_page("about:blank").await.unwrap();
page.execute(SetDeviceMetricsOverrideParams::new(
width, height, 1.0, false,
))
.await
.unwrap();
if !webgpu {
page.execute(AddScriptToEvaluateOnNewDocumentParams::new(
"delete Navigator.prototype.gpu;",
))
.await
.unwrap();
}
page
}
const PAINTED: &str = "document.querySelector('canvas.design')?.dataset.painted === 'true'";
async fn number(page: &Page, name: &str) -> f64 {
eval::<String>(
page,
&format!("document.querySelector('canvas.design').dataset.{name} ?? '0'"),
)
.await
.parse()
.unwrap()
}
async fn zoom_to(page: &Page, to: f64, x: f64, y: f64) {
let shown = || async {
let view: String = eval(page, "document.querySelector('canvas.design').dataset.view").await;
view.split(',').next().unwrap().parse::<f64>().unwrap()
};
if (shown().await - to).abs() >= 0.001 {
let zoom = number(page, "zoom").await;
let pointer = on_screen(page, "canvas.design", x, y).await;
page.evaluate(format!(
"document.querySelector('canvas.design').dispatchEvent(new WheelEvent('wheel', \
{{ deltaY: {}, metaKey: true, clientX: {}, clientY: {}, bubbles: true, cancelable: true }}))",
-100.0 * (to / zoom).ln(),
pointer.x,
pointer.y
))
.await
.unwrap();
}
until(
page,
&format!(
"(() => {{ const c = document.querySelector('canvas.design'); \
return Math.abs(c.dataset.view.split(',')[0] - {to}) < 0.001 && c.dataset.painted === 'true'; }})()"
),
)
.await;
}
#[tokio::test]
#[ignore]
async fn the_cpu_paints_the_middle_first_a_few_at_a_time() {
let dir = folder("huge-order-cpu");
huge(&dir, None);
let address = serve(&dir, "huge.design").await;
let mut browser = testing::browser().await;
let page = canvas_page(&mut browser, false, 1280, 1000).await;
page.goto(format!("{address}/huge.design")).await.unwrap();
until(&page, PAINTED).await;
zoom_to(&page, 4.0, 200.0, 50.0).await;
let view: String = eval(
&page,
"document.querySelector('canvas.design').dataset.view",
)
.await;
let [k, ox, oy]: [f64; 3] = view
.split(',')
.map(|v| v.parse().unwrap())
.collect::<Vec<_>>()
.try_into()
.unwrap();
let order: String = eval(
&page,
"document.querySelector('canvas.design').dataset.order",
)
.await;
let first = order
.split(' ')
.find_map(|t| t.strip_prefix(&format!("{k}:")))
.unwrap_or_else(|| panic!("no tile at {k} in {order}"));
let (mx, my) = ((640.0 - ox) / 256.0, (500.0 - oy) / 256.0);
assert_eq!(first, format!("{},{}", mx.floor(), my.floor()), "{order}");
let inflight = number(&page, "inflight").await;
assert!(inflight <= 6.0, "{inflight} tiles asked for at once");
browser.close().await.ok();
}
async fn a_huge_design_paints_only_the_tiles_of_its_view(webgpu: bool) {
let dir = folder(if webgpu {
"huge-view-gpu"
} else {
"huge-view-cpu"
});
huge(&dir, None);
let address = serve(&dir, "huge.design").await;
let mut browser = testing::browser().await;
let page = canvas_page(&mut browser, webgpu, 1280, 1000).await;
page.goto(format!("{address}/huge.design")).await.unwrap();
until(&page, PAINTED).await;
let fitted = number(&page, "tiles").await;
zoom_to(&page, 1.0, 200.0, 0.0).await;
let tiles = number(&page, "tiles").await;
eprintln!("webgpu {webgpu}: {fitted} tiles fitted, {tiles} at 100%");
assert!(fitted <= 4.0, "{fitted} tiles painted fitted");
assert!(
tiles - fitted <= 10.0,
"{} tiles painted at 100%",
tiles - fitted
);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_huge_design_paints_only_the_tiles_of_its_view_on_the_gpu() {
a_huge_design_paints_only_the_tiles_of_its_view(true).await;
}
#[tokio::test]
#[ignore]
async fn a_huge_design_paints_only_the_tiles_of_its_view_on_the_cpu() {
a_huge_design_paints_only_the_tiles_of_its_view(false).await;
}
async fn pan_frames(page: &Page, n: usize) -> (Vec<f64>, f64) {
let [tiles, frames]: [serde_json::Value; 2] = eval(
page,
&format!(
"(async () => {{
const c = document.querySelector('canvas.design');
const frame = () => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)));
await frame();
const before = Number(c.dataset.tiles);
const times = [];
for (let i = 0; i < {n}; i++) {{
c.dispatchEvent(new WheelEvent('wheel', {{ deltaX: i % 2 ? -30 : 30, deltaY: 0, bubbles: true, cancelable: true }}));
await frame();
times.push(Number(c.dataset.frame));
}}
return [Number(c.dataset.tiles) - before, times];
}})()"
),
)
.await;
let frames = frames
.as_array()
.unwrap()
.iter()
.map(|v| v.as_f64().unwrap())
.collect();
(frames, tiles.as_f64().unwrap())
}
fn median(mut v: Vec<f64>) -> f64 {
v.sort_by(f64::total_cmp);
v[v.len() / 2]
}
async fn a_pan_costs_the_same_on_a_huge_design_as_on_a_small_one(webgpu: bool) {
let dir = folder(if webgpu { "pan-gpu" } else { "pan-cpu" });
huge(&dir, None);
let address = serve(&dir, "huge.design").await;
let mut browser = testing::browser().await;
let mut medians = Vec::new();
for name in ["screen", "huge"] {
let page = canvas_page(&mut browser, webgpu, 1280, 1000).await;
page.goto(format!("{address}/{name}.design")).await.unwrap();
until(&page, PAINTED).await;
if name == "huge" {
zoom_to(&page, 1.0, 200.0, 0.0).await;
}
let (frames, painted) = pan_frames(&page, 60).await;
let m = median(frames.clone());
eprintln!(
"webgpu {webgpu}, {name}: median frame {m:.3} ms, max {:.3} ms, {painted} tiles painted",
frames.iter().copied().fold(0.0, f64::max)
);
assert_eq!(painted, 0.0, "{name}: a pan painted tiles");
medians.push(m);
page.close().await.ok();
}
assert!(
medians[1] <= medians[0] * 2.0 + 0.5,
"a pan on the huge design took {} ms a frame, on the small one {}",
medians[1],
medians[0]
);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn a_pan_costs_the_same_on_a_huge_design_as_on_a_small_one_on_the_gpu() {
a_pan_costs_the_same_on_a_huge_design_as_on_a_small_one(true).await;
}
#[tokio::test]
#[ignore]
async fn a_pan_costs_the_same_on_a_huge_design_as_on_a_small_one_on_the_cpu() {
a_pan_costs_the_same_on_a_huge_design_as_on_a_small_one(false).await;
}
async fn zoomed_against_the_engine(page: &Page, width: usize, height: usize) -> Vec<u8> {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let scene = Designs::new(&ServedFolder::new(root).unwrap())
.resolve("docs/designs/checkout/checkout.design")
.unwrap();
let mut renderer = devpage_design::Renderer::new();
let mut design = devpage_design::Design::new(scene);
let list = design
.whole(&mut renderer, devpage_design::Theme::Light)
.unwrap();
let [k, ox, oy]: [f64; 3] = serde_json::from_str(&format!(
"[{}]",
eval::<String>(page, "document.querySelector('canvas.design').dataset.view").await
))
.unwrap();
let area = list.area;
let side = width.max(height);
let rect = devpage_design::tiles::square(
(area.x as f64 - ox / k, area.y as f64 - oy / k),
devpage_design::tiles::TILE as f64 * k / side as f64,
0,
0,
);
let pixmap = devpage_design::cpu::paint_area(list, rect, k);
assert_eq!(pixmap.width() as usize, side);
let mut expected: Vec<u8> = pixmap
.data_as_u8_slice()
.chunks(4 * side)
.take(height)
.flat_map(|row| row[..4 * width].to_vec())
.collect();
devpage_design::cpu::unpremultiply(&mut expected);
let background: [f64; 3] = eval(
page,
"getComputedStyle(document.body).backgroundColor.match(/[\\d.]+/g).slice(0, 3).map(Number)",
)
.await;
for px in expected.chunks_mut(4) {
let alpha = px[3] as f64 / 255.0;
for c in 0..3 {
px[c] = (px[c] as f64 * alpha + background[c] * (1.0 - alpha)).round() as u8;
}
px[3] = 255;
}
page.evaluate(
"for (const e of document.querySelectorAll('.theme-toggle')) e.style.visibility = 'hidden'",
)
.await
.unwrap();
let started = std::time::Instant::now();
loop {
let seen = shot_of(page, width, height).await;
let d = compare::compare(&seen, &expected, width).unwrap();
if d.within_tolerance() {
return seen;
}
assert!(
started.elapsed() < Duration::from_secs(20),
"at {k}×: {d:?}"
);
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
async fn shot_of(page: &Page, width: usize, height: usize) -> Vec<u8> {
let shot = page
.screenshot(
CaptureScreenshotParams::builder()
.clip(Viewport {
x: 0.0,
y: 0.0,
width: width as f64,
height: height as f64,
scale: 1.0,
})
.build(),
)
.await
.unwrap();
let (mut seen, w, h) = compare::pixels(&shot).unwrap();
assert_eq!((w as usize, h as usize), (width, height));
for px in seen.chunks_mut(4) {
px[3] = 255;
}
seen
}
#[tokio::test]
#[ignore]
async fn the_gpu_and_the_cpu_agree_zoomed_in_and_are_sharp() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let address = serve(root, "docs/designs/checkout/checkout.design").await;
let mut browser = testing::browser().await;
let mut shots = Vec::new();
for webgpu in [true, false] {
let page = canvas_page(&mut browser, webgpu, 800, 600).await;
page.goto(format!(
"{address}/docs/designs/checkout/checkout.design?theme=light"
))
.await
.unwrap();
until(&page, PAINTED).await;
zoom_to(&page, 4.0, 100.0, 100.0).await;
shots.push(zoomed_against_the_engine(&page, 800, 600).await);
page.close().await.ok();
}
let d = compare::compare(&shots[0], &shots[1], 800).unwrap();
assert!(d.within_tolerance(), "the GPU and the CPU: {d:?}");
browser.close().await.ok();
}
async fn an_edit_repaints_only_the_tiles_around_it(webgpu: bool) {
let dir = folder(if webgpu { "edit-gpu" } else { "edit-cpu" });
huge(&dir, None);
let (address, designs) = serve_designs(&dir, "huge.design").await;
let mut browser = testing::browser().await;
let page = canvas_page(&mut browser, webgpu, 1280, 1000).await;
page.goto(format!("{address}/huge.design")).await.unwrap();
until(&page, PAINTED).await;
zoom_to(&page, 1.0, 200.0, 0.0).await;
let before = number(&page, "tiles").await;
huge(&dir, Some(1));
designs.changed(&dir.join("huge.design"));
until(
&page,
&format!(
"Number(document.querySelector('canvas.design').dataset.tiles) > {before} && {PAINTED}"
),
)
.await;
let after = number(&page, "tiles").await;
huge(&dir, Some(900));
designs.changed(&dir.join("huge.design"));
until(
&page,
&format!(
"Number(document.querySelector('canvas.design').dataset.tiles) > {after} && {PAINTED}"
),
)
.await;
let last = number(&page, "tiles").await;
eprintln!(
"webgpu {webgpu}: {before} tiles, {after} after an edit in view, {last} after one out of it"
);
assert_eq!(after - before, 1.0);
assert_eq!(last - after, 1.0);
browser.close().await.ok();
}
#[tokio::test]
#[ignore]
async fn an_edit_repaints_only_the_tiles_around_it_on_the_gpu() {
an_edit_repaints_only_the_tiles_around_it(true).await;
}
#[tokio::test]
#[ignore]
async fn an_edit_repaints_only_the_tiles_around_it_on_the_cpu() {
an_edit_repaints_only_the_tiles_around_it(false).await;
}
#[tokio::test]
#[ignore]
async fn a_cpu_canvas_paints_when_its_scene_arrives_before_the_worker_has_loaded() {
use chromiumoxide::cdp::browser_protocol::fetch::{
ContinueRequestParams, EnableParams, EventRequestPaused, RequestPattern,
};
use chromiumoxide::cdp::browser_protocol::network::SetCacheDisabledParams;
let dir = folder("worker-late");
let address = serve(&dir, "screen.design").await;
let mut browser = testing::browser().await;
let page = canvas_page(&mut browser, false, 800, 600).await;
page.execute(SetCacheDisabledParams::new(true))
.await
.unwrap();
let mut paused = page.event_listener::<EventRequestPaused>().await.unwrap();
page.execute(
EnableParams::builder()
.pattern(RequestPattern {
url_pattern: Some("*/__devpage/design*".into()),
resource_type: None,
request_stage: None,
})
.build(),
)
.await
.unwrap();
let held = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let early = Arc::new(std::sync::atomic::AtomicBool::new(false));
let holder = {
let (page, held, early) = (page.clone(), held.clone(), early.clone());
tokio::spawn(async move {
let mut worker = false;
while let Some(event) = paused.next().await {
let url = event.request.url.clone();
let id = event.request_id.clone();
let hold = worker && url.contains("design-engine");
worker |= url.contains("design-worker");
let page = page.clone();
let (held, early) = (held.clone(), early.clone());
tokio::spawn(async move {
if hold {
held.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
until(
&page,
"!!document.querySelector('canvas.design')?.dataset.size",
)
.await;
if eval::<bool>(&page, PAINTED).await {
early.store(true, std::sync::atomic::Ordering::Relaxed);
}
}
page.execute(ContinueRequestParams::new(id)).await.ok();
});
}
})
};
page.goto(format!("{address}/screen.design")).await.unwrap();
until(&page, PAINTED).await;
let backend: String = eval(
&page,
"document.querySelector('canvas.design').dataset.backend",
)
.await;
assert_eq!(backend, "cpu");
assert!(
held.load(std::sync::atomic::Ordering::Relaxed) > 0,
"the worker's requests for the engine were never seen"
);
assert!(
!early.load(std::sync::atomic::Ordering::Relaxed),
"painted before the worker had its engine"
);
let at = on_screen(&page, "canvas.design", 390.0, 290.0).await;
assert_eq!(pixel(&page, at).await, [0xf8, 0xfa, 0xfc]);
holder.abort();
browser.close().await.ok();
}