M14.4: scripting tests for layer/comp settings, import + save, console snapshot; Project.saveAs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-02 22:18:11 -04:00
co-authored by Claude Opus 5.5
parent 78cd313d3c
commit d880746fec
4 changed files with 110 additions and 0 deletions
+29
View File
@@ -220,6 +220,35 @@ mod tests {
assert!(bytes.starts_with(b"GIF89a"));
}
/// importFile, layers.add(footage) and project save/open from a script.
#[test]
fn script_imports_footage_and_saves() {
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/test-out/host-script-import");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let q = |n: &str| serde_json::to_string(&dir.join(n).to_string_lossy()).unwrap();
let mut s = super::session();
let code = format!(
r#"
var c = app.project.items.addComp("Src", 32, 32, 1, 1, 10);
c.layers.addSolid([0, 1, 0], "G", 32, 32, 1);
app.run("comp.saveFrameAs", {{comp: c.id, path: {png}}});
var still = app.project.importFile(new ImportOptions(new File({png})));
var main = app.project.items.addComp("Main", 64, 64, 1, 1, 10);
var l = main.layers.add(still);
app.project.save(new File({proj}));
[still instanceof FootageItem, still.width, still.mainSource.isStill, l.source.name, app.project.file.name]
"#,
png = q("frame.png"),
proj = q("p.ecproj")
);
let r = s.execute("script.run", json!({"code": code})).unwrap();
assert_eq!(r["ok"], json!(true), "{r}");
assert_eq!(r["result"], json!([true, 32, true, "frame.png", "p.ecproj"]), "{r}");
let r = s.execute("script.run", json!({"code": format!("app.open(new File({})); app.project.numItems", q("p.ecproj"))})).unwrap();
assert_eq!(r["result"], json!(5), "{r}"); // Src, Solids, G, frame.png, Main
}
#[test]
fn expression_syntax_errors_disable_the_expression() {
let mut s = super::session();
+1
View File
@@ -1571,6 +1571,7 @@ Project.prototype = {
else __call("file.save", {});
return true;
},
saveAs: function (file) { return this.save(file); },
saveWithDialog: function () { if (this.file) return this.save(); return false; },
close: function (opt) {
if (opt === CloseOptions.SAVE_CHANGES && this.file) this.save();
+56
View File
@@ -390,3 +390,59 @@ fn schedule_task_runs_after_the_script() {
);
assert_eq!(o.output, vec!["now", "later"]);
}
#[test]
fn layer_switches_timing_and_comp_settings() {
let mut s = session();
let o = ok(
&mut s,
r#"
var c = app.project.items.addComp("S", 320, 240, 1, 5, 30);
var matte = c.layers.addText("M");
var fill = c.layers.addSolid([1, 1, 1], "Fill", 320, 240, 1);
fill.moveToEnd();
fill.setTrackMatte(matte, TrackMatteType.LUMA);
var t1 = fill.trackMatteType == TrackMatteType.LUMA && fill.hasTrackMatte && fill.trackMatteLayer.name == "M";
fill.stretch = 200;
fill.label = 3;
c.width = 400;
c.duration = 8;
c.bgColor = [0.1, 0.2, 0.3];
c.time = 2;
c.workAreaStart = 1;
c.workAreaDuration = 3;
var extra = c.layers.addNull();
extra.remove();
fill.selected = true;
var sel = c.selectedLayers.length;
fill.transform.opacity.expression = "thisLayer.nope(";
var err = fill.transform.opacity.expressionError;
fill.transform.opacity.expressionEnabled = false;
var copy = c.duplicate();
var f = app.project.items.addFolder("Bin");
copy.parentFolder = f;
[t1, fill.stretch, fill.label, c.width, c.duration, c.bgColor[2], c.time, c.workAreaStart, c.workAreaDuration,
c.numLayers, sel, err.length > 0, fill.transform.opacity.expressionEnabled, copy.name, f.numItems, copy.parentFolder.name]
"#,
);
let v = o.result;
assert_eq!(v[0], json!(true), "{v}");
assert_eq!(v[1], json!(200));
assert_eq!(v[2], json!(3));
assert_eq!(v[3], json!(400));
assert_eq!(v[4], json!(8));
assert!((v[5].as_f64().unwrap() - 0.3).abs() < 1e-6);
assert_eq!(v[6], json!(2));
assert_eq!(v[7], json!(1));
assert_eq!(v[8], json!(3));
assert_eq!(v[9], json!(2));
assert_eq!(v[10], json!(1));
assert_eq!(v[11], json!(true));
assert_eq!(v[12], json!(false));
assert_eq!(v[13], json!("S 2"));
assert_eq!(v[14], json!(1));
assert_eq!(v[15], json!("Bin"));
// Removing the copy through the object model.
let o = ok(&mut s, "var n = app.project.numItems; app.project.item(1).remove(); n - app.project.numItems");
assert_eq!(o.result, json!(1));
}
+24
View File
@@ -42,3 +42,27 @@ fn script_console_runs_scripts() {
click(&mut h, "scriptConsole.clear");
assert!(h.state().ui.script_console.log.is_empty());
}
/// Headless look at the panel (wgpu offscreen); run with
/// `SC_SNAPSHOT=/abs/out.png cargo test -p effectcraft-ui-egui --test ui_script_console -- --ignored`.
#[test]
#[ignore]
fn script_console_snapshot() {
let s = effectcraft_host::session();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s));
h.state_mut().show_panel(PanelKind::ScriptConsole);
h.run_steps(2);
for code in ["var c = app.project.items.addComp('Console', 320, 180, 1, 2, 24); writeLn('made ' + c.name); c.id", "c.layers.addText('Hi').name", "nope()"] {
h.state_mut().ui.script_console.input = code.into();
h.run_steps(1);
click(&mut h, "scriptConsole.run");
}
h.state_mut().ui.script_console.input = "app.project.numItems".into();
h.run_steps(3);
let img = h.render().expect("render");
let out = std::env::var("SC_SNAPSHOT").unwrap_or_else(|_| concat!(env!("CARGO_MANIFEST_DIR"), "/../../target/test-out/script_console.png").into());
if let Some(d) = std::path::Path::new(&out).parent() {
std::fs::create_dir_all(d).unwrap();
}
img.save(&out).unwrap();
}