527 lines
23 KiB
Rust
527 lines
23 KiB
Rust
//! End-to-end scripts against a headless session: build with the object model, read back through
|
|
//! the engine.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use effectcraft_engine::Session;
|
|
use effectcraft_engine::keyframe::Value as KV;
|
|
use effectcraft_engine::project::{Comp, ItemId, Layer};
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::{Outcome, run_code};
|
|
|
|
fn session() -> Session {
|
|
let mut s = Session { expr: Some(Arc::new(effectcraft_expr::Expressions)), expr_check: Some(effectcraft_expr::check_syntax), ..Default::default() };
|
|
crate::install(&mut s);
|
|
s
|
|
}
|
|
|
|
fn ok(s: &mut Session, code: &str) -> Outcome {
|
|
let o = run_code(s, code, "test.jsx");
|
|
assert!(o.error.is_none(), "script failed: {:?}\noutput: {:?}", o.error, o.output);
|
|
o
|
|
}
|
|
|
|
fn comp_named<'a>(s: &'a Session, name: &str) -> (ItemId, &'a Comp) {
|
|
let it = s.project.items.values().find(|i| i.name == name && i.as_comp().is_some()).unwrap_or_else(|| panic!("no comp {name}"));
|
|
(it.id, it.as_comp().unwrap())
|
|
}
|
|
|
|
fn layer<'a>(c: &'a Comp, name: &str) -> &'a Layer {
|
|
c.layers.iter().find(|l| l.name == name).unwrap_or_else(|| panic!("no layer {name}"))
|
|
}
|
|
|
|
#[test]
|
|
fn builds_a_comp_end_to_end() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
app.beginUndoGroup("Build");
|
|
var comp = app.project.items.addComp("Main", 640, 360, 1, 4, 25);
|
|
var bg = comp.layers.addSolid([0.2, 0.4, 0.6], "BG", 640, 360, 1);
|
|
var txt = comp.layers.addText("Hello");
|
|
txt.name = "Title";
|
|
var pos = txt.property("ADBE Transform Group").property("ADBE Position");
|
|
pos.setValueAtTime(0, [100, 180]);
|
|
pos.setValueAtTime(2, [540, 180]);
|
|
txt.transform.opacity.expression = "50 + 25";
|
|
var blur = bg.Effects.addProperty("ADBE Gaussian Blur 2");
|
|
blur.property("Blurriness").setValue(12);
|
|
var nul = comp.layers.addNull();
|
|
txt.setParentWithJump(nul);
|
|
bg.blendingMode = BlendingMode.MULTIPLY;
|
|
var rq = app.project.renderQueue.items.add(comp);
|
|
rq.outputModule(1).file = new File("/tmp/effectcraft-script-test/out.mp4");
|
|
app.endUndoGroup();
|
|
writeLn("layers: " + comp.numLayers);
|
|
[comp.numLayers, pos.numKeys, pos.keyValue(2)[0], txt.transform.opacity.value, comp.layer(1).name]
|
|
"#,
|
|
);
|
|
assert_eq!(o.output, vec!["layers: 3"]);
|
|
assert_eq!(o.result, json!([3, 2, 540, 75, "Null 1"]));
|
|
let (cid, c) = comp_named(&s, "Main");
|
|
assert_eq!((c.width, c.height), (640, 360));
|
|
assert_eq!(c.duration.seconds(), 4.0);
|
|
// AE adds new layers at the top.
|
|
let names: Vec<&str> = c.layers.iter().map(|l| l.name.as_str()).collect();
|
|
assert_eq!(names, ["Null 1", "Title", "BG"]);
|
|
let title = layer(c, "Title");
|
|
let p = title.props.prop("transform/position").unwrap();
|
|
assert_eq!(p.keys.len(), 2);
|
|
assert_eq!(p.keys[1].value, KV::Vec3([540.0, 180.0, 0.0]));
|
|
assert_eq!(title.props.prop("transform/opacity").unwrap().expr.as_ref().unwrap().text, "50 + 25");
|
|
assert_eq!(title.parent, Some(layer(c, "Null 1").id));
|
|
let bg = layer(c, "BG");
|
|
assert_eq!(bg.blend_mode, effectcraft_engine::color::BlendMode::Multiply);
|
|
let fx = bg.props.group("effects/#1").unwrap();
|
|
assert_eq!(fx.match_id, "ec.blur.gaussian");
|
|
assert_eq!(bg.props.prop("effects/#1/blurriness").unwrap().value, KV::Scalar(12.0));
|
|
assert_eq!(s.project.render_queue.len(), 1);
|
|
assert_eq!(s.project.render_queue[0].comp, cid);
|
|
// One undo step for the whole group.
|
|
assert_eq!(s.history.undo.len(), 1);
|
|
assert_eq!(s.history.undo[0].0, "Build");
|
|
assert!(s.undo());
|
|
assert!(s.project.items.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn fill_parameters_use_documented_match_names() {
|
|
// M13.15: `ADBE Fill-0002` is Fill's Color (All Masks is -0007), not the second row.
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"var c = app.project.items.addComp("F", 100, 100, 1, 1, 12); var l = c.layers.addSolid([1,1,1], "S", 100, 100, 1);
|
|
var fx = l.property("ADBE Effect Parade").addProperty("ADBE Fill");
|
|
[fx.property("ADBE Fill-0002").name, fx.property("ADBE Fill-0007").name, fx.property(2).matchName].join("|")"#,
|
|
);
|
|
assert_eq!(o.result, json!("Color|All Masks|ADBE Fill-0007"));
|
|
}
|
|
|
|
#[test]
|
|
fn undo_steps_without_a_group() {
|
|
let mut s = session();
|
|
ok(&mut s, r#"var c = app.project.items.addComp("A", 100, 100, 1, 1, 24); c.layers.addNull(); c.layers.addNull();"#);
|
|
assert_eq!(s.history.undo.len(), 3);
|
|
// An unbalanced group is closed when the script ends.
|
|
ok(&mut s, r#"app.beginUndoGroup("Two"); var c = app.project.item(1); c.layers.addNull(); c.layers.addNull();"#);
|
|
assert_eq!(s.history.undo.len(), 4);
|
|
assert_eq!(s.history.undo[3].0, "Two");
|
|
// Nested groups fold into the outermost one.
|
|
ok(
|
|
&mut s,
|
|
r#"app.beginUndoGroup("Outer"); app.beginUndoGroup("Inner"); app.project.item(1).layers.addNull(); app.endUndoGroup(); app.project.item(1).layers.addNull(); app.endUndoGroup();"#,
|
|
);
|
|
assert_eq!(s.history.undo.len(), 5);
|
|
assert_eq!(s.history.undo[4].0, "Outer");
|
|
s.undo();
|
|
assert_eq!(s.project.comp(ItemId(1)).unwrap().layers.len(), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn errors_report_line_and_column() {
|
|
let mut s = session();
|
|
let o = run_code(&mut s, "var a = 1;\nvar b = 2;\nfoo.bar();\n", "broken.jsx");
|
|
let e = o.error.expect("error");
|
|
assert_eq!(e.line, Some(3), "{e:?}");
|
|
assert!(e.message.contains("foo"), "{e:?}");
|
|
assert_eq!(e.file, "broken.jsx");
|
|
let o = run_code(&mut s, "var a = 1;\n\nvar = ;\n", "syntax.jsx");
|
|
let e = o.error.expect("syntax error");
|
|
assert_eq!(e.line, Some(3), "{e:?}");
|
|
let o = run_code(&mut s, "writeLn('before');\nthrow new Error('custom failure');\n", "throw.jsx");
|
|
let e = o.error.expect("thrown");
|
|
assert_eq!(e.message, "custom failure");
|
|
assert_eq!(e.line, Some(2), "{e:?}");
|
|
assert_eq!(o.output, vec!["before"]);
|
|
// Errors from the object model point at the user's line.
|
|
let o = run_code(&mut s, "var c = app.project.items.addComp('C', 10, 10, 1, 1, 24);\n\nc.layer(5);\n", "range.jsx");
|
|
let e = o.error.expect("range error");
|
|
assert!(e.message.contains("out of range"), "{e:?}");
|
|
assert_eq!(e.line, Some(3), "{e:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn match_names_and_lookups() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var c = app.project.items.addComp("M", 200, 200, 1, 2, 30);
|
|
var l = c.layers.addSolid([1, 1, 1], "S", 200, 200, 1);
|
|
var tr = l.property("ADBE Transform Group");
|
|
var fx = l.property("ADBE Effect Parade").addProperty("Gaussian Blur");
|
|
var m = l.property("ADBE Mask Parade").addProperty("ADBE Mask Atom");
|
|
var sh = c.layers.addShape();
|
|
var grp = sh.property("ADBE Root Vectors Group").addProperty("ADBE Vector Group");
|
|
var rect = grp.property("ADBE Vectors Group").addProperty("ADBE Vector Shape - Rect");
|
|
var fill = grp.property("Contents").addProperty("ADBE Vector Graphic - Fill");
|
|
fill.property("ADBE Vector Fill Color").setValue([1, 0, 0]);
|
|
rect.property("ADBE Vector Rect Size").setValue([50, 60]);
|
|
[
|
|
l.matchName, tr.matchName, tr.property("ADBE Position").matchName, tr.property("Anchor Point").matchName,
|
|
tr.property(1).name, tr.anchorPoint.matchName, l.transform.position.propertyValueType == PropertyValueType.ThreeD_SPATIAL,
|
|
fx.matchName, fx.property(1).matchName, fx.property("ADBE Gaussian Blur 2-0001").name, m.matchName,
|
|
m.property("ADBE Mask Shape").name, sh.matchName, grp.matchName, rect.matchName, fill.matchName,
|
|
tr.propertyType == PropertyType.NAMED_GROUP, l.Effects.propertyType == PropertyType.INDEXED_GROUP,
|
|
tr.position.parentProperty.matchName, tr.position.propertyDepth, l.property("Nope") === null,
|
|
fx.isEffect, m.isMask, l.Effects.numProperties
|
|
]
|
|
"#,
|
|
);
|
|
assert_eq!(
|
|
o.result,
|
|
json!([
|
|
"ADBE AV Layer",
|
|
"ADBE Transform Group",
|
|
"ADBE Position",
|
|
"ADBE Anchor Point",
|
|
"Anchor Point",
|
|
"ADBE Anchor Point",
|
|
true,
|
|
"ADBE Gaussian Blur 2",
|
|
"ADBE Gaussian Blur 2-0001",
|
|
"Blurriness",
|
|
"ADBE Mask Atom",
|
|
"Mask Path",
|
|
"ADBE Vector Layer",
|
|
"ADBE Vector Group",
|
|
"ADBE Vector Shape - Rect",
|
|
"ADBE Vector Graphic - Fill",
|
|
true,
|
|
true,
|
|
"ADBE Transform Group",
|
|
2,
|
|
true,
|
|
true,
|
|
true,
|
|
1
|
|
])
|
|
);
|
|
let (_, c) = comp_named(&s, "M");
|
|
let sh = c.layers.iter().find(|l| l.props.sub("contents").is_some()).unwrap();
|
|
assert_eq!(sh.props.prop("contents/#1/contents/rect/size").unwrap().value, KV::Vec2([50.0, 60.0]));
|
|
assert_eq!(sh.props.prop("contents/#1/contents/fill/color").unwrap().value, KV::Color([1.0, 0.0, 0.0, 1.0]));
|
|
}
|
|
|
|
#[test]
|
|
fn keyframes_eases_and_interpolation() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var c = app.project.items.addComp("K", 100, 100, 1, 5, 25);
|
|
var l = c.layers.addNull();
|
|
var r = l.transform.rotation;
|
|
r.setValuesAtTimes([0, 1, 2], [0, 90, 180]);
|
|
r.setInterpolationTypeAtKey(2, KeyframeInterpolationType.HOLD, KeyframeInterpolationType.LINEAR);
|
|
r.setTemporalEaseAtKey(3, [new KeyframeEase(0, 75)]);
|
|
var idx = r.addKey(1.5);
|
|
r.removeKey(1);
|
|
var out = [r.numKeys, idx, r.keyTime(1), r.keyInInterpolationType(1) == KeyframeInterpolationType.HOLD,
|
|
r.keyOutInterpolationType(1) == KeyframeInterpolationType.LINEAR, r.keyInTemporalEase(3)[0].influence,
|
|
r.valueAtTime(1, false), r.isTimeVarying, r.nearestKeyIndex(1.9)];
|
|
l.startTime = 1;
|
|
out.push(r.keyTime(1));
|
|
out
|
|
"#,
|
|
);
|
|
let v = o.result.as_array().unwrap();
|
|
assert_eq!(v[0], json!(3));
|
|
assert_eq!(v[1], json!(3));
|
|
assert_eq!(v[2], json!(1));
|
|
assert_eq!(v[3], json!(true));
|
|
assert_eq!(v[4], json!(true));
|
|
assert!((v[5].as_f64().unwrap() - 75.0).abs() < 1e-6, "{v:?}");
|
|
assert_eq!(v[6], json!(90));
|
|
assert_eq!(v[7], json!(true));
|
|
assert_eq!(v[8], json!(3));
|
|
// Moving the layer moves its keys (layer time): key 1 now sits at 2 s comp time.
|
|
assert_eq!(v[9], json!(2));
|
|
}
|
|
|
|
#[test]
|
|
fn spatial_tangents_round_trip() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var c = app.project.items.addComp("T", 100, 100, 1, 2, 25);
|
|
var p = c.layers.addNull().transform.position;
|
|
p.setValuesAtTimes([0, 1], [[0, 0], [100, 0]]);
|
|
p.setSpatialTangentsAtKey(1, [0, 0], [30, 40]);
|
|
[p.keyOutSpatialTangent(1)[0], p.keyOutSpatialTangent(1)[1], p.keyInSpatialTangent(1)[0]]
|
|
"#,
|
|
);
|
|
assert_eq!(o.result, json!([30, 40, 0]));
|
|
}
|
|
|
|
#[test]
|
|
fn text_documents() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var c = app.project.items.addComp("T", 400, 200, 1, 2, 30);
|
|
var t = c.layers.addText("abc");
|
|
var st = t.property("ADBE Text Properties").property("ADBE Text Document");
|
|
var doc = st.value;
|
|
doc.text = "Changed";
|
|
doc.fontSize = 40;
|
|
doc.fillColor = [1, 0, 0];
|
|
doc.justification = ParagraphJustification.CENTER_JUSTIFY;
|
|
doc.applyStroke = true;
|
|
doc.strokeWidth = 3;
|
|
st.setValue(doc);
|
|
var d2 = t.text.sourceText.value;
|
|
[d2.text, d2.fontSize, d2.fillColor, d2.justification == ParagraphJustification.CENTER_JUSTIFY, d2.strokeWidth, t instanceof TextLayer, t.name]
|
|
"#,
|
|
);
|
|
assert_eq!(o.result, json!(["Changed", 40, [1, 0, 0], true, 3, true, "abc"]));
|
|
let (_, c) = comp_named(&s, "T");
|
|
let KV::Text(d) = &c.layers[0].props.prop("text/sourceText").unwrap().value else { panic!() };
|
|
assert_eq!(d.text, "Changed");
|
|
assert_eq!(d.style_at(0).size, 40.0);
|
|
}
|
|
|
|
#[test]
|
|
fn layers_items_and_markers() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var f = app.project.items.addFolder("Comps");
|
|
var c = f.items.addComp("Inner", 100, 100, 1, 3, 30);
|
|
var main = app.project.items.addComp("Outer", 200, 200, 1, 6, 30);
|
|
var nested = main.layers.add(c);
|
|
var a = main.layers.addSolid([1, 0, 0], "A", 50, 50, 1, 2);
|
|
var b = main.layers.addSolid([0, 1, 0], "B", 50, 50, 1);
|
|
b.moveAfter(nested);
|
|
a.enabled = false; a.solo = true; a.locked = true; a.shy = true;
|
|
a.threeDLayer = true;
|
|
var cam = main.layers.addCamera("Cam", [100, 100]);
|
|
var lt = main.layers.addLight("Key", [100, 100]);
|
|
lt.lightType = LightType.SPOT;
|
|
var d = a.duplicate();
|
|
d.name = "A copy";
|
|
var mv = new MarkerValue("hello");
|
|
mv.duration = 0.5;
|
|
main.markerProperty.setValueAtTime(1, mv);
|
|
b.marker.setValueAtTime(0.5, new MarkerValue("layer marker"));
|
|
b.inPoint = 1; b.outPoint = 3;
|
|
var pc = main.layers.precompose([b.index], "Pre", true);
|
|
[c.parentFolder.name, nested.source.name, a.outPoint, a.enabled, a.solo, a.locked, a.shy, a.threeDLayer,
|
|
cam instanceof CameraLayer, lt.lightType == LightType.SPOT, main.markerProperty.numKeys, main.markerProperty.keyValue(1).comment,
|
|
main.markerProperty.keyValue(1).duration, pc.name, pc instanceof CompItem, main.layer("A copy").index, app.project.numItems,
|
|
main.layers.byName("Pre") !== null, f.numItems]
|
|
"#,
|
|
);
|
|
assert_eq!(o.result, json!(["Comps", "Inner", 2, false, true, true, true, true, true, true, 1, "hello", 0.5, "Pre", true, 3, 7, true, 1]));
|
|
let (_, c) = comp_named(&s, "Outer");
|
|
assert_eq!(c.markers.len(), 1);
|
|
assert!(
|
|
c.layers
|
|
.iter()
|
|
.any(|l| l.name == "Key"
|
|
&& matches!(l.source, effectcraft_engine::project::LayerSource::Light { kind: effectcraft_engine::project::LightKind::Spot }))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn security_gate_for_files() {
|
|
let dir = std::env::temp_dir().join(format!("ec-script-gate-{}", std::process::id()));
|
|
let proj = dir.join("proj");
|
|
std::fs::create_dir_all(&proj).unwrap();
|
|
std::fs::write(proj.join("data.txt"), "inside").unwrap();
|
|
std::fs::write(dir.join("secret.txt"), "outside").unwrap();
|
|
let mut s = session();
|
|
s.path = Some(proj.join("p.ecproj").to_string_lossy().to_string());
|
|
let q = |p: &std::path::Path| serde_json::to_string(&p.to_string_lossy()).unwrap();
|
|
// Reading inside the project folder works.
|
|
let o = ok(&mut s, &format!("var f = new File({}); f.open('r'); var t = f.read(); f.close(); t", q(&proj.join("data.txt"))));
|
|
assert_eq!(o.result, json!("inside"));
|
|
// Outside it (also via `..`) is refused.
|
|
for path in [dir.join("secret.txt"), proj.join("../secret.txt")] {
|
|
let o = run_code(&mut s, &format!("var f = new File({}); f.open('r');", q(&path)), "gate.jsx");
|
|
let e = o.error.expect("blocked");
|
|
assert!(e.message.contains("Allow Scripts to Write Files"), "{e:?}");
|
|
}
|
|
// Writing is refused anywhere, the network too.
|
|
let o = run_code(&mut s, &format!("var f = new File({}); f.open('w'); f.write('x'); f.close();", q(&proj.join("out.txt"))), "gate.jsx");
|
|
assert!(o.error.is_some());
|
|
assert!(!proj.join("out.txt").exists());
|
|
assert!(run_code(&mut s, "new Socket()", "net.jsx").error.is_some());
|
|
assert!(run_code(&mut s, "system.callSystem('ls')", "sys.jsx").error.is_some());
|
|
// With the preference on, scripts can write and read anywhere.
|
|
s.prefs.scripting.allow_scripts_write_files = true;
|
|
ok(&mut s, &format!("var f = new File({}); f.open('w'); f.writeln('written'); f.close();", q(&dir.join("w.txt"))));
|
|
assert_eq!(std::fs::read_to_string(dir.join("w.txt")).unwrap(), "written\n");
|
|
let o = ok(&mut s, &format!("var f = new File({}); f.open('r'); f.readln()", q(&dir.join("secret.txt"))));
|
|
assert_eq!(o.result, json!("outside"));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn engine_entry_points() {
|
|
let mut s = session();
|
|
// script.run reports errors in the result, not as a command failure.
|
|
let r = s.execute("script.run", json!({"code": "writeLn('hi'); 1 + 2"})).unwrap();
|
|
assert_eq!(r["ok"], json!(true));
|
|
assert_eq!(r["result"], json!(3));
|
|
assert_eq!(r["output"], json!("hi"));
|
|
let r = s.execute("script.run", json!({"code": "\nnope()"})).unwrap();
|
|
assert_eq!(r["ok"], json!(false));
|
|
assert_eq!(r["error"]["line"], json!(2));
|
|
// Scripts can't run scripts.
|
|
let r = s.execute("script.run", json!({"code": "app.run('script.run', {code: '1'})"})).unwrap();
|
|
assert!(r["error"]["message"].as_str().unwrap().contains("scripts can't run scripts"), "{r}");
|
|
// Compiled .jsxbin scripts (by name or by their header) are unsupported, not a SyntaxError
|
|
// at 1:1 (#176).
|
|
for p in [json!({"code": "@JSXBIN@ES@2.0@MyBbyBn0AB", "name": "compiled.bin"}), json!({"code": "1", "name": "/x/compiled.JSXBIN"})] {
|
|
let e = s.execute("script.run", p.clone()).unwrap_err().to_string();
|
|
assert!(e.contains(".jsxbin scripts are not supported: run the .jsx source"), "{p}: {e}");
|
|
}
|
|
// File ▸ Scripts ▸ Run Script File… runs .jsx as JavaScript.
|
|
let dir = std::env::temp_dir().join(format!("ec-script-file-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
let f = dir.join("make.jsx");
|
|
std::fs::write(&f, "app.project.items.addComp('From File', 320, 240, 1, 2, 24);").unwrap();
|
|
s.execute("file.runScript", json!({"path": f.to_string_lossy()})).unwrap();
|
|
assert!(s.project.items.values().any(|i| i.name == "From File"));
|
|
std::fs::write(&f, "\n\nbroken(;").unwrap();
|
|
let e = s.execute("file.runScript", json!({"path": f.to_string_lossy()})).unwrap_err().to_string();
|
|
assert!(e.contains("make.jsx (line 3)"), "{e}");
|
|
// JSON command scripts still work.
|
|
let j = dir.join("steps.json");
|
|
std::fs::write(&j, r#"[{"command": "comp.new", "params": {"name": "From Steps"}}]"#).unwrap();
|
|
s.execute("file.runScript", json!({"path": j.to_string_lossy()})).unwrap();
|
|
assert!(s.project.items.values().any(|i| i.name == "From Steps"));
|
|
// Menu commands by name.
|
|
let o = ok(&mut s, "app.findMenuCommandId('New Project') > 0 && app.findMenuCommandId('No Such Thing') === 0");
|
|
assert_eq!(o.result, json!(true));
|
|
let o = ok(&mut s, "app.executeCommand(app.findMenuCommandId('New Project')); app.project.numItems");
|
|
assert_eq!(o.result, json!(0));
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
}
|
|
|
|
#[test]
|
|
fn console_keeps_variables() {
|
|
let mut s = session();
|
|
let req = |code: &'static str| effectcraft_engine::ScriptRequest { code, name: "console", console: true };
|
|
let r = crate::run(&mut s, &req("var counter = 41;"));
|
|
assert!(r.error.is_none(), "{r:?}");
|
|
let r = crate::run(&mut s, &req("counter + 1"));
|
|
assert_eq!(r.result, json!(42));
|
|
let _ = Value::Null;
|
|
}
|
|
|
|
#[test]
|
|
fn schedule_task_runs_after_the_script() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
"app.scheduleTask('writeLn(\"later\")', 100, false); var id = app.scheduleTask('writeLn(\"never\")', 10, false); app.cancelTask(id); writeLn('now');",
|
|
);
|
|
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));
|
|
}
|
|
|
|
#[test]
|
|
fn motion_graphics_template_hooks_add_mirrors_and_controllers() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var comp = app.project.items.addComp("Card", 64, 64, 1, 2, 30);
|
|
var bg = comp.layers.addSolid([1, 1, 1], "BG", 64, 64, 1);
|
|
var op = bg.transform.opacity;
|
|
var r = [op.canAddToMotionGraphicsTemplate(comp), op.addToMotionGraphicsTemplateAs(comp, "Fade")];
|
|
// Again: a mirror of the same property (a controller of its own).
|
|
r.push(op.addToMotionGraphicsTemplate(comp));
|
|
r.push(comp.motionGraphicsTemplateControllerCount);
|
|
r.push(comp.getMotionGraphicsTemplateControllerName(1), comp.getMotionGraphicsTemplateControllerName(2));
|
|
comp.setMotionGraphicsControllerName(2, "Fade Again");
|
|
r.push(comp.getMotionGraphicsTemplateControllerName(2));
|
|
r
|
|
"#,
|
|
);
|
|
assert_eq!(o.result, json!([true, true, true, 2, "Fade", "Fade", "Fade Again"]));
|
|
let (_, c) = comp_named(&s, "Card");
|
|
let eg = c.essential.as_ref().unwrap();
|
|
assert!(matches!(eg.controls[1].kind, effectcraft_engine::project::essential::EgKind::Mirror { of } if of == eg.controls[0].id));
|
|
}
|
|
|
|
#[test]
|
|
fn comp_preserves_nested_frame_rate_and_resolution() {
|
|
let mut s = session();
|
|
let o = ok(
|
|
&mut s,
|
|
r#"
|
|
var comp = app.project.items.addComp("Nested", 320, 180, 1, 2, 12);
|
|
var before = [comp.preserveNestedFrameRate, comp.preserveNestedResolution];
|
|
comp.preserveNestedFrameRate = true;
|
|
comp.preserveNestedResolution = true;
|
|
before.concat([comp.preserveNestedFrameRate, comp.preserveNestedResolution]).join(",");
|
|
"#,
|
|
);
|
|
assert_eq!(o.result, json!("false,false,true,true"));
|
|
let (_, c) = comp_named(&s, "Nested");
|
|
assert!(c.preserve_frame_rate && c.preserve_resolution);
|
|
}
|