plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
2a4119c
— Peter John
2026-11-02T15:01:16+05:30
Add plum-game/web-test: playwright-rs browser check for web game builds
Generated files not shown: plum-game/web-test/Cargo.lock
plum-game/web-test/Cargo.toml
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "plum-web-test"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[workspace]
|
|
7
|
+
|
|
8
|
+
[lib]
|
|
9
|
+
name = "plum_web_test"
|
|
10
|
+
path = "src/lib.rs"
|
|
11
|
+
|
|
12
|
+
[[bin]]
|
|
13
|
+
name = "plum-web-test"
|
|
14
|
+
path = "src/main.rs"
|
|
15
|
+
|
|
16
|
+
[dependencies]
|
|
17
|
+
playwright-rs = "0.20.1"
|
|
18
|
+
tokio = { version = "1", features = ["full"] }
|
|
19
|
+
image = { version = "0.25", default-features = false, features = ["png"] }
|
plum-game/web-test/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# plum-web-test
|
|
2
|
+
|
|
3
|
+
Browser check for `plum game --web` builds, driven by [playwright-rs](https://crates.io/crates/playwright-rs) 0.20.
|
|
4
|
+
Standalone crate (own `[workspace]`, not part of the root workspace).
|
|
5
|
+
|
|
6
|
+
It builds `plum-cli`, exports the game with `plum game --web`, serves the output on
|
|
7
|
+
127.0.0.1:0, opens it in headless Chromium, clicks the canvas once, then asserts:
|
|
8
|
+
the canvas is not one uniform colour, two screenshots 1 s apart differ (the game
|
|
9
|
+
animates), and there are no page errors or console errors (except the known harmless
|
|
10
|
+
macroquad `register_plugin is not defined`). AudioContext state is reported; a missing
|
|
11
|
+
running context is only a warning unless `--require-audio` is given.
|
|
12
|
+
|
|
13
|
+
## One-time setup
|
|
14
|
+
|
|
15
|
+
Downloads the Chromium build matching the bundled Playwright driver (about 100 MB, to
|
|
16
|
+
`~/Library/Caches/ms-playwright` on macOS; the driver itself is fetched by the crate's build script):
|
|
17
|
+
|
|
18
|
+
cargo run --manifest-path plum-game/web-test/Cargo.toml --example install-browsers
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
cargo run --manifest-path plum-game/web-test/Cargo.toml -- plum-examples/game.plum --screenshot /tmp/orb-run.png
|
|
23
|
+
|
|
24
|
+
Options: `--frames-wait SECS` (default 8), `--screenshot OUT.png`, `--require-audio`.
|
|
25
|
+
Exit code is non-zero with a message on failure.
|
|
26
|
+
|
|
27
|
+
`cargo test --manifest-path plum-game/web-test/Cargo.toml` runs the same check on
|
|
28
|
+
`plum-examples/game.plum`; it skips with a note if Chromium is not installed.
|
plum-game/web-test/examples/install-browsers.rs
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// One-time setup: installs the Chromium build matching the bundled Playwright driver.
|
|
2
|
+
#[tokio::main]
|
|
3
|
+
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
4
|
+
playwright_rs::install_browsers(Some(&["chromium"])).await?;
|
|
5
|
+
Ok(())
|
|
6
|
+
}
|
plum-game/web-test/src/lib.rs
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
//! Browser check for `plum game --web` builds, driven by playwright-rs.
|
|
2
|
+
use playwright_rs::{LaunchOptions, Playwright};
|
|
3
|
+
use std::io::{Read, Write};
|
|
4
|
+
use std::net::{TcpListener, TcpStream};
|
|
5
|
+
use std::path::{Path, PathBuf};
|
|
6
|
+
use std::process::Command;
|
|
7
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
8
|
+
use std::sync::{Arc, Mutex};
|
|
9
|
+
use std::time::Duration;
|
|
10
|
+
|
|
11
|
+
pub struct Options {
|
|
12
|
+
pub frames_wait: f64,
|
|
13
|
+
pub screenshot: Option<PathBuf>,
|
|
14
|
+
pub require_audio: bool,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl Default for Options {
|
|
18
|
+
fn default() -> Self {
|
|
19
|
+
Options { frames_wait: 8.0, screenshot: None, require_audio: false }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Whether `playwright install chromium` has been run (browser cache dir exists).
|
|
24
|
+
pub fn chromium_installed() -> bool {
|
|
25
|
+
let dir = if let Ok(p) = std::env::var("PLAYWRIGHT_BROWSERS_PATH") {
|
|
26
|
+
PathBuf::from(p)
|
|
27
|
+
} else {
|
|
28
|
+
let home = PathBuf::from(std::env::var("HOME").unwrap_or_default());
|
|
29
|
+
if cfg!(target_os = "macos") {
|
|
30
|
+
home.join("Library/Caches/ms-playwright")
|
|
31
|
+
} else if cfg!(target_os = "windows") {
|
|
32
|
+
PathBuf::from(std::env::var("LOCALAPPDATA").unwrap_or_default()).join("ms-playwright")
|
|
33
|
+
} else {
|
|
34
|
+
home.join(".cache/ms-playwright")
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
std::fs::read_dir(dir)
|
|
38
|
+
.map(|rd| rd.flatten().any(|e| e.file_name().to_string_lossy().starts_with("chromium")))
|
|
39
|
+
.unwrap_or(false)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn repo_root() -> PathBuf {
|
|
43
|
+
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().expect("repo root")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/// Builds plum-cli and exports `file` as a web build into `out`.
|
|
47
|
+
pub fn build_web(file: &Path, out: &Path) -> Result<(), String> {
|
|
48
|
+
let root = repo_root();
|
|
49
|
+
let st = Command::new("cargo")
|
|
50
|
+
.args(["build", "-p", "plum-cli"])
|
|
51
|
+
.current_dir(&root)
|
|
52
|
+
.status()
|
|
53
|
+
.map_err(|e| format!("cargo build: {e}"))?;
|
|
54
|
+
if !st.success() {
|
|
55
|
+
return Err("cargo build -p plum-cli failed".into());
|
|
56
|
+
}
|
|
57
|
+
let file = file.canonicalize().map_err(|e| format!("{}: {e}", file.display()))?;
|
|
58
|
+
let o = Command::new(root.join("target/debug/plum"))
|
|
59
|
+
.args(["game", "--web"])
|
|
60
|
+
.arg(&file)
|
|
61
|
+
.arg("-o")
|
|
62
|
+
.arg(out)
|
|
63
|
+
.current_dir(&root)
|
|
64
|
+
.output()
|
|
65
|
+
.map_err(|e| format!("plum game --web: {e}"))?;
|
|
66
|
+
if !o.status.success() {
|
|
67
|
+
return Err(format!(
|
|
68
|
+
"plum game --web failed:\n{}{}",
|
|
69
|
+
String::from_utf8_lossy(&o.stdout),
|
|
70
|
+
String::from_utf8_lossy(&o.stderr)
|
|
71
|
+
));
|
|
72
|
+
}
|
|
73
|
+
Ok(())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
fn mime(path: &Path) -> &'static str {
|
|
77
|
+
match path.extension().and_then(|e| e.to_str()).unwrap_or("") {
|
|
78
|
+
"wasm" => "application/wasm",
|
|
79
|
+
"js" | "mjs" => "text/javascript",
|
|
80
|
+
"html" => "text/html",
|
|
81
|
+
"png" => "image/png",
|
|
82
|
+
"wav" => "audio/wav",
|
|
83
|
+
"ogg" => "audio/ogg",
|
|
84
|
+
"txt" => "text/plain",
|
|
85
|
+
"json" => "application/json",
|
|
86
|
+
_ => "application/octet-stream",
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
fn respond(mut s: TcpStream, root: &Path) {
|
|
91
|
+
let mut buf = [0u8; 8192];
|
|
92
|
+
let n = s.read(&mut buf).unwrap_or(0);
|
|
93
|
+
let req = String::from_utf8_lossy(&buf[..n]);
|
|
94
|
+
let target = req.split_whitespace().nth(1).unwrap_or("/");
|
|
95
|
+
let rel = target.split(['?', '#']).next().unwrap_or("/").trim_start_matches('/');
|
|
96
|
+
let rel = if rel.is_empty() { "index.html" } else { rel };
|
|
97
|
+
let ok = !rel.split('/').any(|c| c == ".." || c.contains('\\'));
|
|
98
|
+
let path = root.join(rel);
|
|
99
|
+
match std::fs::read(&path).ok().filter(|_| ok) {
|
|
100
|
+
Some(body) => {
|
|
101
|
+
let head = format!(
|
|
102
|
+
"HTTP/1.1 200 OK\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
|
|
103
|
+
mime(&path),
|
|
104
|
+
body.len()
|
|
105
|
+
);
|
|
106
|
+
let _ = s.write_all(head.as_bytes());
|
|
107
|
+
let _ = s.write_all(&body);
|
|
108
|
+
}
|
|
109
|
+
None => {
|
|
110
|
+
let _ = s.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n");
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Serves `root` on 127.0.0.1:0; returns the port. Stops when `stop` is set.
|
|
116
|
+
pub fn serve(root: PathBuf, stop: Arc<AtomicBool>) -> u16 {
|
|
117
|
+
let l = TcpListener::bind("127.0.0.1:0").expect("bind");
|
|
118
|
+
let port = l.local_addr().unwrap().port();
|
|
119
|
+
std::thread::spawn(move || {
|
|
120
|
+
for s in l.incoming() {
|
|
121
|
+
if stop.load(Ordering::Relaxed) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
if let Ok(s) = s {
|
|
125
|
+
let root = root.clone();
|
|
126
|
+
std::thread::spawn(move || respond(s, &root));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
port
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
pub struct Shot {
|
|
134
|
+
pub w: u32,
|
|
135
|
+
pub h: u32,
|
|
136
|
+
pub rgba: Vec<u8>,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
pub fn decode(png: &[u8]) -> Result<Shot, String> {
|
|
140
|
+
let img = image::load_from_memory(png).map_err(|e| format!("decode screenshot: {e}"))?.to_rgba8();
|
|
141
|
+
Ok(Shot { w: img.width(), h: img.height(), rgba: img.into_raw() })
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Number of distinct colours (capped at `cap`).
|
|
145
|
+
pub fn distinct_colours(s: &Shot, cap: usize) -> usize {
|
|
146
|
+
let mut set = std::collections::HashSet::new();
|
|
147
|
+
for p in s.rgba.chunks_exact(4) {
|
|
148
|
+
set.insert([p[0], p[1], p[2]]);
|
|
149
|
+
if set.len() >= cap {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
set.len()
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// Fraction-free count of pixels that differ between two same-sized shots.
|
|
157
|
+
pub fn differing_pixels(a: &Shot, b: &Shot) -> usize {
|
|
158
|
+
if (a.w, a.h) != (b.w, b.h) {
|
|
159
|
+
return usize::MAX;
|
|
160
|
+
}
|
|
161
|
+
a.rgba.chunks_exact(4).zip(b.rgba.chunks_exact(4)).filter(|(x, y)| x != y).count()
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const AUDIO_HOOK: &str = r#"(() => {
|
|
165
|
+
window.__plumAudio = [];
|
|
166
|
+
for (const k of ["AudioContext", "webkitAudioContext"]) {
|
|
167
|
+
const Orig = window[k];
|
|
168
|
+
if (!Orig) continue;
|
|
169
|
+
window[k] = function (...a) { const c = new Orig(...a); window.__plumAudio.push(c); return c; };
|
|
170
|
+
window[k].prototype = Orig.prototype;
|
|
171
|
+
}
|
|
172
|
+
})()"#;
|
|
173
|
+
|
|
174
|
+
const AUDIO_PROBE: &str = "() => (window.__plumAudio || []).map(c => c.state)";
|
|
175
|
+
|
|
176
|
+
/// Runs the full check; Ok(report) on success, Err(message) on any failure.
|
|
177
|
+
pub async fn run(file: &Path, opts: &Options) -> Result<String, String> {
|
|
178
|
+
let tmp = std::env::temp_dir().join(format!("plum-web-test-{}", std::process::id()));
|
|
179
|
+
let _ = std::fs::remove_dir_all(&tmp);
|
|
180
|
+
std::fs::create_dir_all(&tmp).map_err(|e| e.to_string())?;
|
|
181
|
+
let res = run_in(file, opts, &tmp).await;
|
|
182
|
+
let _ = std::fs::remove_dir_all(&tmp);
|
|
183
|
+
res
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async fn run_in(file: &Path, opts: &Options, dir: &Path) -> Result<String, String> {
|
|
187
|
+
build_web(file, dir)?;
|
|
188
|
+
let stop = Arc::new(AtomicBool::new(false));
|
|
189
|
+
let port = serve(dir.to_path_buf(), stop.clone());
|
|
190
|
+
let res = check(port, opts).await;
|
|
191
|
+
stop.store(true, Ordering::Relaxed);
|
|
192
|
+
let _ = TcpStream::connect(("127.0.0.1", port)); // unblock accept
|
|
193
|
+
res
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn pe<E: std::fmt::Display>(what: &'static str) -> impl FnOnce(E) -> String {
|
|
197
|
+
move |e| format!("{what}: {e}")
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async fn check(port: u16, opts: &Options) -> Result<String, String> {
|
|
201
|
+
let pw = Playwright::launch().await.map_err(pe("launch playwright"))?;
|
|
202
|
+
let launch = LaunchOptions::new().headless(true).args(
|
|
203
|
+
["--use-angle=swiftshader", "--enable-unsafe-swiftshader", "--ignore-gpu-blocklist", "--autoplay-policy=no-user-gesture-required"]
|
|
204
|
+
.map(String::from)
|
|
205
|
+
.to_vec(),
|
|
206
|
+
);
|
|
207
|
+
let browser = pw.chromium().launch_with_options(launch).await.map_err(pe("launch chromium"))?;
|
|
208
|
+
let out = check_page(&browser, port, opts).await;
|
|
209
|
+
let _ = browser.close().await;
|
|
210
|
+
out
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async fn check_page(browser: &playwright_rs::protocol::Browser, port: u16, opts: &Options) -> Result<String, String> {
|
|
214
|
+
let page = browser.new_page().await.map_err(pe("new page"))?;
|
|
215
|
+
let errors: Arc<Mutex<Vec<String>>> = Arc::default();
|
|
216
|
+
let logs: Arc<Mutex<Vec<String>>> = Arc::default();
|
|
217
|
+
{
|
|
218
|
+
let (errors, logs) = (errors.clone(), logs.clone());
|
|
219
|
+
page.on_console(move |m| {
|
|
220
|
+
let (errors, logs) = (errors.clone(), logs.clone());
|
|
221
|
+
async move {
|
|
222
|
+
let line = format!("console.{}: {}", m.type_(), m.text());
|
|
223
|
+
if m.type_() == "error" && !m.text().contains("register_plugin is not defined") {
|
|
224
|
+
errors.lock().unwrap().push(line.clone());
|
|
225
|
+
}
|
|
226
|
+
logs.lock().unwrap().push(line);
|
|
227
|
+
Ok(())
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
.await
|
|
231
|
+
.map_err(pe("on_console"))?;
|
|
232
|
+
}
|
|
233
|
+
{
|
|
234
|
+
let errors = errors.clone();
|
|
235
|
+
page.on_pageerror(move |m| {
|
|
236
|
+
let errors = errors.clone();
|
|
237
|
+
async move {
|
|
238
|
+
if !m.contains("register_plugin is not defined") {
|
|
239
|
+
errors.lock().unwrap().push(format!("pageerror: {m}"));
|
|
240
|
+
}
|
|
241
|
+
Ok(())
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
.await
|
|
245
|
+
.map_err(pe("on_pageerror"))?;
|
|
246
|
+
}
|
|
247
|
+
page.add_init_script(AUDIO_HOOK).await.map_err(pe("init script"))?;
|
|
248
|
+
page.goto(&format!("http://127.0.0.1:{port}/"), None).await.map_err(pe("goto"))?;
|
|
249
|
+
|
|
250
|
+
// Give the wasm time to load, then provide a user gesture on the canvas.
|
|
251
|
+
tokio::time::sleep(Duration::from_secs_f64((opts.frames_wait / 2.0).min(3.0))).await;
|
|
252
|
+
let size: Vec<f64> = page
|
|
253
|
+
.evaluate("() => { const r = document.getElementById('glcanvas').getBoundingClientRect(); return [r.width, r.height]; }", None::<&()>)
|
|
254
|
+
.await
|
|
255
|
+
.map_err(pe("canvas lookup (is there a #glcanvas?)"))?;
|
|
256
|
+
page.mouse().click(size[0] / 2.0, size[1] / 2.0, None).await.map_err(pe("click"))?;
|
|
257
|
+
tokio::time::sleep(Duration::from_secs_f64((opts.frames_wait - (opts.frames_wait / 2.0).min(3.0)).max(0.0))).await;
|
|
258
|
+
|
|
259
|
+
let png1 = page.screenshot(None).await.map_err(pe("screenshot"))?;
|
|
260
|
+
tokio::time::sleep(Duration::from_secs(1)).await;
|
|
261
|
+
let png2 = page.screenshot(None).await.map_err(pe("screenshot"))?;
|
|
262
|
+
let audio: Vec<String> = page.evaluate(AUDIO_PROBE, None::<&()>).await.unwrap_or_default();
|
|
263
|
+
|
|
264
|
+
if let Some(p) = &opts.screenshot {
|
|
265
|
+
std::fs::write(p, &png2).map_err(|e| format!("write {}: {e}", p.display()))?;
|
|
266
|
+
}
|
|
267
|
+
let (a, b) = (decode(&png1)?, decode(&png2)?);
|
|
268
|
+
let colours = distinct_colours(&b, 1000);
|
|
269
|
+
let diff = differing_pixels(&a, &b);
|
|
270
|
+
let errs = errors.lock().unwrap().clone();
|
|
271
|
+
|
|
272
|
+
let mut report = format!(
|
|
273
|
+
"canvas {}x{}, distinct colours: {colours}{}, pixels changed over 1s: {}\naudio: {}",
|
|
274
|
+
b.w,
|
|
275
|
+
b.h,
|
|
276
|
+
if colours >= 1000 { "+" } else { "" },
|
|
277
|
+
if diff == usize::MAX { "size changed".into() } else { diff.to_string() },
|
|
278
|
+
if audio.is_empty() { "no AudioContext created".to_string() } else { format!("AudioContext states {audio:?}") },
|
|
279
|
+
);
|
|
280
|
+
let mut failures = vec![];
|
|
281
|
+
if colours <= 1 {
|
|
282
|
+
failures.push("canvas is blank (uniformly one colour)".to_string());
|
|
283
|
+
}
|
|
284
|
+
if diff == 0 {
|
|
285
|
+
failures.push("canvas did not change between snapshots 1s apart (game not animating)".to_string());
|
|
286
|
+
}
|
|
287
|
+
failures.extend(errs.iter().cloned());
|
|
288
|
+
let audio_ok = audio.iter().any(|s| s == "running");
|
|
289
|
+
if !audio_ok {
|
|
290
|
+
if opts.require_audio {
|
|
291
|
+
failures.push("--require-audio: no running AudioContext".to_string());
|
|
292
|
+
} else {
|
|
293
|
+
report.push_str("\nWARNING: no running AudioContext (informational)");
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if failures.is_empty() {
|
|
297
|
+
Ok(report)
|
|
298
|
+
} else {
|
|
299
|
+
let logs = logs.lock().unwrap().join("\n");
|
|
300
|
+
Err(format!("{}\n{report}\n--- console ---\n{logs}", failures.join("\n")))
|
|
301
|
+
}
|
|
302
|
+
}
|
plum-game/web-test/src/main.rs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
use plum_web_test::{chromium_installed, run, Options};
|
|
2
|
+
use std::path::PathBuf;
|
|
3
|
+
|
|
4
|
+
const USAGE: &str = "usage: plum-web-test <file.plum> [--frames-wait SECS] [--screenshot OUT.png] [--require-audio]";
|
|
5
|
+
|
|
6
|
+
#[tokio::main]
|
|
7
|
+
async fn main() {
|
|
8
|
+
let mut file: Option<PathBuf> = None;
|
|
9
|
+
let mut opts = Options::default();
|
|
10
|
+
let mut args = std::env::args().skip(1);
|
|
11
|
+
while let Some(a) = args.next() {
|
|
12
|
+
match a.as_str() {
|
|
13
|
+
"--frames-wait" => {
|
|
14
|
+
opts.frames_wait = args.next().and_then(|v| v.parse().ok()).unwrap_or_else(|| fail("--frames-wait needs a number"));
|
|
15
|
+
}
|
|
16
|
+
"--screenshot" => opts.screenshot = Some(args.next().map(PathBuf::from).unwrap_or_else(|| fail("--screenshot needs a path"))),
|
|
17
|
+
"--require-audio" => opts.require_audio = true,
|
|
18
|
+
"-h" | "--help" => {
|
|
19
|
+
println!("{USAGE}");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
s if s.starts_with("--") => fail(&format!("unknown flag {s}\n{USAGE}")),
|
|
23
|
+
_ => file = Some(PathBuf::from(a)),
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
let Some(file) = file else { fail(USAGE) };
|
|
27
|
+
if !chromium_installed() {
|
|
28
|
+
fail("Playwright Chromium is not installed; see plum-game/web-test/README.md (one-time setup)");
|
|
29
|
+
}
|
|
30
|
+
match run(&file, &opts).await {
|
|
31
|
+
Ok(report) => println!("{report}\nPASS"),
|
|
32
|
+
Err(e) => {
|
|
33
|
+
eprintln!("FAIL: {e}");
|
|
34
|
+
std::process::exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn fail(msg: &str) -> ! {
|
|
40
|
+
eprintln!("{msg}");
|
|
41
|
+
std::process::exit(2);
|
|
42
|
+
}
|
plum-game/web-test/tests/web_check.rs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
use plum_web_test::{chromium_installed, repo_root, run, Options};
|
|
2
|
+
|
|
3
|
+
#[test]
|
|
4
|
+
fn orb_run_renders_and_animates() {
|
|
5
|
+
if !chromium_installed() {
|
|
6
|
+
eprintln!("SKIP: Playwright Chromium not installed (see plum-game/web-test/README.md)");
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
10
|
+
let file = repo_root().join("plum-examples/game.plum");
|
|
11
|
+
let report = rt.block_on(run(&file, &Options::default())).unwrap_or_else(|e| panic!("{e}"));
|
|
12
|
+
println!("{report}");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#[test]
|
|
16
|
+
fn image_helpers() {
|
|
17
|
+
use plum_web_test::{differing_pixels, distinct_colours, Shot};
|
|
18
|
+
let a = Shot { w: 2, h: 1, rgba: vec![1, 2, 3, 255, 1, 2, 3, 255] };
|
|
19
|
+
let b = Shot { w: 2, h: 1, rgba: vec![1, 2, 3, 255, 9, 2, 3, 255] };
|
|
20
|
+
assert_eq!(distinct_colours(&a, 10), 1);
|
|
21
|
+
assert_eq!(distinct_colours(&b, 10), 2);
|
|
22
|
+
assert_eq!(differing_pixels(&a, &b), 1);
|
|
23
|
+
}
|