use super::*;
use crate::cascade::{Cascade, FieldRef, FieldWrites, Keystroke, KeystrokeOutcome};
fn field() -> FieldRef {
FieldRef {
name: "Text Box".to_string(),
index: Some(0),
}
}
fn session() -> ScriptCascade {
ScriptCascade::new(&ScriptConfig::frozen_at(GOLDEN_CLOCK_SECS)).expect("a realm builds")
}
fn bounded(limits: Limits) -> ScriptCascade {
ScriptCascade::new(&ScriptConfig {
limits,
..ScriptConfig::frozen_at(GOLDEN_CLOCK_SECS)
})
.expect("a realm builds")
}
fn with_script(trigger: &str, source: &str) -> ScriptCascade {
let mut cascade = session();
let mut actions = FieldActions::default();
match trigger {
"K" => actions.keystroke = Some(source.to_string()),
"V" => actions.validate = Some(source.to_string()),
"C" => actions.calculate = Some(source.to_string()),
_ => actions.format = Some(source.to_string()),
}
cascade.set_field(0, "Text Box", "", actions);
cascade
}
#[test]
fn a_plain_alert_is_prefixed_with_the_literal_alert() {
let mut cascade = session();
assert!(cascade.run("app.alert('hello');", "test"));
assert_eq!(cascade.transcript_text(), "Alert: hello\n");
}
#[test]
fn the_alert_decoration_is_conditional() {
let mut cascade = session();
assert!(cascade.run(
"app.alert('plain');\n\
app.alert('titled', 0, 0, 'Warning');\n\
app.alert('iconed', 3, 0, 'Note');\n\
app.alert('typed', 0, 2, 'Ask');",
"test"
));
assert_eq!(
cascade.transcript_text(),
"Alert: plain\n\
Warning: titled\n\
Note[icon=3,type=0]: iconed\n\
Ask[icon=0,type=2]: typed\n"
);
}
#[test]
fn a_lone_object_argument_supplies_the_four_by_name() {
let mut cascade = session();
assert!(cascade.run(
"app.alert({cMsg: 'named', cTitle: 'T', nIcon: 1, nType: 2});",
"test"
));
assert_eq!(cascade.transcript_text(), "T[icon=1,type=2]: named\n");
}
#[test]
fn an_undefined_named_property_takes_its_default() {
let mut cascade = session();
assert!(cascade.run("app.alert({cMsg: 'm', cTitle: undefined});", "test"));
assert_eq!(cascade.transcript_text(), "Alert: m\n");
}
#[test]
fn an_array_message_is_joined_and_a_lone_object_is_not_a_message() {
let mut cascade = session();
assert!(cascade.run("app.alert([1, 'two', 3]);", "test"));
assert!(cascade.run("app.alert([1, 2, {'color': 'red'}]);", "test"));
assert!(cascade.run("app.alert({'color': 'red'}, 5, 6, 'title');", "test"));
assert_eq!(
cascade.transcript_text(),
"Alert: [1, two, 3]\n\
Alert: [1, 2, [object Object]]\n\
title[icon=5,type=6]: [object Object]\n"
);
let mut lone = session();
assert!(!lone.run("app.alert({});", "test"));
assert!(!lone.run("app.alert({'color': 'red', 'size': 42});", "test"));
}
#[test]
fn a_missing_message_throws_the_parameter_count_error() {
let mut cascade = session();
assert!(!cascade.run("app.alert();", "test"));
let stop = cascade.stops().first().map(|failure| failure.stop.clone());
match stop {
Some(ScriptStop::Threw(message)) => assert!(
message.contains(bind_param_error()),
"expected the parameter-count message, got {message}"
),
other => panic!("expected a throw, got {other:?}"),
}
assert_eq!(cascade.transcript_text(), "");
}
#[test]
fn the_object_form_clears_the_positional_message() {
let mut cascade = session();
assert!(!cascade.run("app.alert({nIcon: 1});", "test"));
}
fn bind_param_error() -> &'static str {
"Incorrect number of parameters passed to function."
}
#[test]
fn every_thrown_error_carries_the_function_name() {
let caught = |source: &str| {
let mut cascade = session();
let script = format!("try {{ {source} }} catch (e) {{ app.alert('' + e); }}");
assert!(
cascade.run(&script, "test"),
"the wrapper itself must not throw"
);
cascade.transcript_text()
};
assert_eq!(
caught("AFDate_Format();"),
format!("Alert: AFDate_Format: {}\n", bind_param_error())
);
assert_eq!(
caught("util.printd();"),
format!("Alert: util.printd: {}\n", bind_param_error())
);
assert_eq!(
caught("util.printf();"),
format!("Alert: util.printf: {}\n", bind_param_error())
);
assert_eq!(
caught("app.alert();"),
format!("Alert: app.alert: {}\n", bind_param_error())
);
assert_eq!(
caught("AFDate_KeystrokeEx();"),
"Alert: AFDate_KeystrokeEx: AFDate_KeystrokeEx's parameter size not correct\n"
);
}
#[test]
fn expect_js_drives_the_binding_the_way_the_fixtures_do() {
const EXPECT_JS: &str = r"
function expect(expression, expected) {
try {
var actual = eval(expression);
if (actual == expected) {
app.alert('PASS: ' + expression + ' = ' + actual);
} else {
app.alert('FAIL: ' + expression + ' = ' + actual + ', expected ' + expected + ' ');
}
} catch (e) {
app.alert('ERROR: ' + e);
}
}
function expectError(expression) {
try {
var actual = eval(expression);
app.alert('FAIL: ' + expression + ' = ' + actual + ', expected to throw');
} catch (e) {
app.alert('PASS: ' + expression + ' threw ' + e);
}
}
";
let mut cascade = session();
assert!(cascade.run(EXPECT_JS, "expect.js"));
assert!(cascade.run(
r#"
expect("app.viewerType", "pdfium");
expect("app.alert('message')", 0);
expectError("app.alert()");
expectError("app.execMenuItem()");
"#,
"test"
));
let expected = concat!(
"Alert: PASS: app.viewerType = pdfium\n",
"Alert: message\n",
"Alert: PASS: app.alert('message') = 0\n",
"Alert: PASS: app.alert() threw app.alert: ",
"Incorrect number of parameters passed to function.\n",
"Alert: PASS: app.execMenuItem() threw app.execMenuItem: ",
"Operation not supported.\n",
);
assert_eq!(cascade.transcript_text(), expected);
}
#[test]
fn console_prints_nothing_because_upstream_prints_nothing() {
let mut cascade = session();
assert!(cascade.run(
"console.println('x'); console.clear(); console.hide(); console.show();",
"test"
));
assert_eq!(cascade.transcript_text(), "");
}
#[test]
fn util_is_bound_to_the_library() {
let mut cascade = session();
assert!(cascade.run(
"app.alert(util.printf('%d apples and %s', 3, 'pears'));\n\
app.alert(util.printx('99999', '123456789'));\n\
app.alert(util.byteToChar(65));",
"test"
));
assert_eq!(
cascade.transcript_text(),
"Alert: 3 apples and pears\nAlert: 12345\nAlert: A\n"
);
}
#[test]
fn the_clock_is_frozen_at_pdfiums_own_seed() {
let mut cascade = session();
assert!(cascade.run("app.alert(Date.now());", "test"));
assert_eq!(cascade.transcript_text(), "Alert: 1399672130000\n");
}
#[test]
fn the_clock_follows_the_configured_seed() {
let mut cascade =
ScriptCascade::new(&ScriptConfig::frozen_at(1_700_000_000)).expect("a realm builds");
assert!(cascade.run("app.alert(Date.now());", "test"));
assert_eq!(cascade.transcript_text(), "Alert: 1700000000000\n");
}
#[test]
fn without_a_seed_the_clock_is_the_hosts_own() {
let now_ms = i64::try_from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("the host clock is after 1970")
.as_millis(),
)
.expect("a millisecond count this century fits an i64");
let mut cascade = ScriptCascade::new(&ScriptConfig::wall_clock()).expect("a realm builds");
assert!(cascade.run("app.alert(Date.now());", "test"));
let reported: i64 = cascade
.transcript_text()
.trim()
.trim_start_matches("Alert: ")
.parse()
.expect("Date.now() is a number");
assert!(
(reported - now_ms).abs() < 10_000,
"the wall clock reported {reported}, which is not within ten seconds of {now_ms}"
);
}
#[test]
fn the_timezone_is_pdfiums_own() {
let mut cascade = session();
assert!(cascade.run("app.alert(new Date().getTimezoneOffset());", "test"));
assert_eq!(cascade.transcript_text(), "Alert: 420\n");
}
#[test]
fn the_af_library_is_reachable_as_bare_globals() {
let mut cascade = session();
assert!(cascade.run(
"app.alert(AFMakeNumber('1234.5'));\n\
app.alert(AFSimple('SUM', 2, 3));\n\
app.alert(AFExtractNums('a12b34').join('|'));\n\
app.alert(AFExtractNums('abc'));",
"test"
));
assert_eq!(
cascade.transcript_text(),
"Alert: 1234.5\nAlert: 5\nAlert: 12|34\nAlert: false\n"
);
let mut comma = session();
assert!(comma.run(
"app.alert(AFMakeNumber('1,234.5'));\n\
app.alert(AFMakeNumber('1,5'));",
"test"
));
assert_eq!(comma.transcript_text(), "Alert: 0\nAlert: 1.5\n");
}
#[test]
fn an_af_alert_is_titled_by_its_caller_and_interleaves() {
let mut cascade = with_script(
"V",
"app.alert('before');\n\
AFRange_Validate(true, 10, false, 0);\n\
app.alert('after');",
);
cascade.validate(&field(), "5");
let text = cascade.transcript_text();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines.first(), Some(&"Alert: before"));
assert_eq!(lines.last(), Some(&"Alert: after"));
assert!(
lines
.iter()
.any(|line| line.starts_with("AFRange_Validate[icon=3,type=0]: ")),
"the AF alert must be titled by its caller, got {lines:?}"
);
}
#[test]
fn a_format_function_writes_the_event_value() {
let mut cascade = with_script(
"F",
"AFNumber_Format(2, 0, 0, 0, '$', true);\n\
app.alert(event.value);",
);
assert_eq!(
cascade.format(&field(), "1234.5"),
Some("$1,234.50".to_string()),
"the formatter's answer is what the appearance draws"
);
assert_eq!(cascade.transcript_text(), "Alert: $1,234.50\n");
}
#[test]
fn the_page_array_is_filled_through_the_prototype_chain() {
let mut cascade = session();
let mut model = crate::script::model::DocumentModel::empty();
model.page_count = 2;
model.fields = vec![crate::script::model::FieldModel {
name: "MyField".to_owned(),
pages: vec![0, 1],
..crate::script::model::FieldModel::default()
}];
cascade.set_document(model);
assert!(cascade.run(
"Object.defineProperty(Array.prototype, 1, {\n\
set: function (v) { app.alert('intercepted ' + v); },\n\
get: function () { return undefined; },\n\
configurable: true });\n\
this.getField('MyField').page;",
"test"
));
assert_eq!(cascade.transcript_text(), "Alert: intercepted 1\n");
}
fn with_words(pages: &[&[&str]]) -> ScriptCascade {
let mut cascade = session();
let mut model = crate::script::model::DocumentModel::empty();
model.page_count = u32::try_from(pages.len()).unwrap_or(0);
model.page_words = pages
.iter()
.map(|page| page.iter().map(|word| (*word).to_owned()).collect())
.collect();
cascade.set_document(model);
cascade
}
#[test]
fn the_word_methods_read_the_installed_list() {
let mut cascade = with_words(&[&["Hello,", " world! "], &["one"]]);
assert!(cascade.run(
"app.alert(this.getPageNumWords(0));\n\
app.alert(this.getPageNthWord(0, 0));\n\
app.alert('[' + this.getPageNthWord(0, 1) + ']');\n\
app.alert('[' + this.getPageNthWord(0, 1, false) + ']');\n\
app.alert(this.getPageNumWords(1));",
"test"
));
assert_eq!(
cascade.transcript_text(),
"Alert: 2\nAlert: Hello,\nAlert: [world!]\nAlert: [ world! ]\nAlert: 1\n"
);
}
#[test]
fn an_out_of_range_page_is_a_value_error_for_both_word_methods() {
let mut cascade = with_words(&[&["a"]]);
assert!(cascade.run(
"function say(f) { try { f(); app.alert('no throw'); } \
catch (e) { app.alert('' + e); } }\n\
say(function () { this.getPageNthWord(-1, 0); });\n\
say(function () { this.getPageNumWords(6); });",
"test"
));
assert_eq!(
cascade.transcript_text(),
"Alert: Document.getPageNthWord: Incorrect parameter value.\n\
Alert: Document.getPageNumWords: Incorrect parameter value.\n"
);
}
#[test]
fn a_word_index_past_the_end_answers_the_last_word() {
let mut cascade = with_words(&[&["first", "last"]]);
assert!(cascade.run("app.alert(this.getPageNthWord(0, 99));", "test"));
assert_eq!(cascade.transcript_text(), "Alert: last\n");
}
#[test]
fn a_document_with_no_installed_words_answers_zero() {
let mut cascade = with_words(&[&[]]);
assert!(cascade.run(
"app.alert(this.getPageNumWords(0));\n\
app.alert('[' + this.getPageNthWord(0, 0) + ']');",
"test"
));
assert_eq!(cascade.transcript_text(), "Alert: 0\nAlert: []\n");
}
fn transcript_of(source: &str) -> String {
let mut cascade = session();
assert!(cascade.run(source, "test"), "{:?}", cascade.stops());
cascade.transcript_text()
}
#[test]
fn color_equality_promotes_to_the_richer_space() {
assert_eq!(
transcript_of(
"app.alert(color.equal(['G', 0.5], ['RGB', 0.5, 0.5, 0.5]));\n\
app.alert(color.equal(['G', 0.5], ['CMYK', 0, 0, 0, 0.5]));\n\
app.alert(color.equal(['RGB', 0.25, 0.25, 0.25], ['G', 0.25]));\n\
app.alert(color.equal(['T'], ['G', 0]));"
),
"Alert: true\nAlert: true\nAlert: true\nAlert: false\n"
);
}
#[test]
fn an_unknown_colour_space_converts_to_transparent() {
assert_eq!(
transcript_of(
"app.alert('' + color.convert(['G', 0.5], 'BOGUS'));\n\
app.alert('' + color.convert(['G', 0.5], 'CMYK'));"
),
"Alert: T\nAlert: CMYK,0,0,0,0.5\n"
);
}
#[test]
fn a_named_colour_is_a_variable_and_not_a_constant() {
assert_eq!(
transcript_of(
"app.alert('' + color.black);\n\
color.black = ['RGB', 1, 0, 0];\n\
app.alert('' + color.black);"
),
"Alert: G,0\nAlert: RGB,1,0,0\n"
);
}
#[test]
fn a_deleted_global_is_a_tombstone_and_can_be_revived() {
assert_eq!(
transcript_of(
"global.x = 1;\n\
delete global.x;\n\
app.alert('' + global.x);\n\
try { global.setPersistent('x', true); }\n\
catch (e) { app.alert('' + e); }\n\
global.x = 2;\n\
app.alert('' + global.x);"
),
"Alert: undefined\n\
Alert: global.setPersistent: Global value not found.\n\
Alert: 2\n"
);
}
#[test]
fn assigning_undefined_to_a_global_deletes_it() {
assert_eq!(
transcript_of(
"global.a = 1;\n\
global.b = undefined;\n\
var seen = [];\n\
for (var name in global) { if (name != 'setPersistent') seen.push(name); }\n\
app.alert(seen.join(','));"
),
"Alert: a\n"
);
}
#[test]
fn globals_enumerate_in_byte_order() {
assert_eq!(
transcript_of(
"global.zeta = 1; global.alpha = 2; global.mid = 3;\n\
var seen = [];\n\
for (var name in global) { if (name != 'setPersistent') seen.push(name); }\n\
app.alert(seen.join(','));"
),
"Alert: alpha,mid,zeta\n"
);
}
#[test]
fn the_constant_namespaces_are_tables_with_undefined_holes() {
assert_eq!(
transcript_of(
"app.alert(border.s);\n\
app.alert('' + border.nonesuch);\n\
app.alert(display.noView);\n\
app.alert(font.ZapfD);\n\
app.alert(scaleHow.anamorphic);\n\
app.alert(zoomtype.fitV);"
),
"Alert: solid\n\
Alert: undefined\n\
Alert: 3\n\
Alert: ZapfDingbats\n\
Alert: 1\n\
Alert: FitVisibleWidth\n"
);
}
#[test]
fn a_static_constructor_refuses_and_a_dynamic_one_constructs() {
assert_eq!(
transcript_of(
"function say(f) { try { f(); app.alert('no throw'); } \
catch (e) { app.alert('' + e); } }\n\
say(function () { app.constructor(); });\n\
say(function () { new app.constructor; });\n\
var t = app.setTimeOut('0', 1);\n\
say(function () { t.constructor(); });\n\
app.alert('' + new t.constructor);"
),
"Alert: illegal constructor\n\
Alert: not a dynamic object\n\
Alert: illegal constructor\n\
Alert: [object Object]\n"
);
}
#[test]
fn the_message_strings_and_pattern_arrays_are_bare_globals() {
assert_eq!(
transcript_of(
"app.alert(IDS_AM + ',' + IDS_PM);\n\
app.alert(IDS_LESS_THAN);\n\
app.alert(RE_ZIP_COMMIT.length + ':' + RE_ZIP_COMMIT[0]);\n\
app.alert(RE_PHONE_COMMIT.length);"
),
"Alert: am,pm\n\
Alert: Invalid value: must be less than or equal to % s.\n\
Alert: 1:\\d{5}\n\
Alert: 4\n"
);
}
fn with_pointer_script(trigger: &str, source: &str) -> ScriptCascade {
let mut cascade = session();
let mut actions = FieldActions::default();
match trigger {
"E" => actions.mouse_enter = Some(source.to_string()),
"X" => actions.mouse_exit = Some(source.to_string()),
"D" => actions.mouse_down = Some(source.to_string()),
"U" => actions.mouse_up = Some(source.to_string()),
"Fo" => actions.focus = Some(source.to_string()),
_ => actions.blur = Some(source.to_string()),
}
cascade.set_field(0, "Text Box", "typed", actions);
cascade
}
#[test]
fn a_read_only_event_property_throws_on_assignment() {
let mut cascade = with_script(
"F",
"try { event.name = 'boo'; app.alert('no throw'); }\n\
catch (e) { app.alert('' + e); }",
);
cascade.format(&field(), "x");
assert_eq!(
cascade.transcript_text(),
"Alert: event.name: Operation not supported.\n"
);
}
#[test]
fn the_rich_event_properties_accept_and_discard() {
let mut cascade = with_script(
"F",
"app.alert('' + event.richValue);\n\
app.alert('' + (event.richValue = 'boo'));\n\
app.alert('' + event.richValue);",
);
cascade.format(&field(), "x");
assert_eq!(
cascade.transcript_text(),
"Alert: undefined\nAlert: boo\nAlert: undefined\n"
);
}
#[test]
fn field_full_throws_outside_a_keystroke() {
let mut cascade = with_script(
"F",
"try { app.alert('' + event.fieldFull); }\n\
catch (e) { app.alert('' + e); }",
);
cascade.format(&field(), "x");
assert_eq!(
cascade.transcript_text(),
"Alert: event.fieldFull: unrecognized event\n"
);
let mut keystroke = with_script("K", "app.alert('' + event.fieldFull);");
keystroke.keystroke_commit(&field(), "x");
assert_eq!(keystroke.transcript_text(), "Alert: false\n");
}
#[test]
fn the_selection_indices_are_live_only_for_a_keystroke() {
let mut cascade = with_script(
"F",
"app.alert('' + event.selStart);\n\
event.selStart = 3;\n\
app.alert('' + event.selStart);",
);
cascade.format(&field(), "x");
assert_eq!(
cascade.transcript_text(),
"Alert: undefined\nAlert: undefined\n"
);
}
#[test]
fn event_value_refuses_three_types_and_stringifies_a_number() {
let mut cascade = with_script(
"F",
"try { event.value = true; } catch (e) { app.alert('' + e); }\n\
try { event.value = null; } catch (e) { app.alert('' + e); }\n\
event.value = 2;\n\
app.alert('' + event.value);",
);
cascade.format(&field(), "x");
assert_eq!(
cascade.transcript_text(),
"Alert: event.value: Set not possible, invalid or unknown.\n\
Alert: event.value: Set not possible, invalid or unknown.\n\
Alert: 2\n"
);
}
#[test]
fn event_rc_resets_false_except_for_the_three_kinds_that_read_it() {
let mut format = with_script("F", "app.alert('' + event.rc);");
format.format(&field(), "x");
assert_eq!(format.transcript_text(), "Alert: false\n");
let mut validate = with_script("V", "app.alert('' + event.rc);");
validate.validate(&field(), "x");
assert_eq!(validate.transcript_text(), "Alert: true\n");
}
#[test]
fn each_pointer_trigger_names_itself() {
use crate::cascade::PointerTrigger;
let cases = [
("E", PointerTrigger::Enter, "Mouse Enter"),
("X", PointerTrigger::Exit, "Mouse Exit"),
("D", PointerTrigger::Down, "Mouse Down"),
("U", PointerTrigger::Up, "Mouse Up"),
("Fo", PointerTrigger::Focus, "Focus"),
("Bl", PointerTrigger::Blur, "Blur"),
];
for (key, trigger, expected) in cases {
let mut cascade = with_pointer_script(key, "app.alert(event.name);");
cascade.pointer(&field(), trigger, crate::Modifiers::NONE);
assert_eq!(
cascade.transcript_text(),
format!("Alert: {expected}\n"),
"trigger {key}"
);
}
}
#[test]
fn only_three_triggers_are_a_user_gesture() {
use crate::cascade::PointerTrigger;
let source = "try { this.submitForm('u'); app.alert('allowed'); }\n\
catch (e) { app.alert('' + e); }";
let mut down = with_pointer_script("D", source);
down.pointer(&field(), PointerTrigger::Down, crate::Modifiers::NONE);
let text = down.transcript_text();
assert!(
text.starts_with("Doc Submit Form: url=u + 85 data bytes:"),
"the gesture must let the submission through, got {text}"
);
assert!(text.ends_with("Alert: allowed\n"), "{text}");
let mut enter = with_pointer_script("E", source);
enter.pointer(&field(), PointerTrigger::Enter, crate::Modifiers::NONE);
assert_eq!(
enter.transcript_text(),
"Alert: Document.submitForm: User gesture required.\n"
);
}
#[test]
fn a_mouse_trigger_has_no_value_and_a_focus_trigger_does() {
use crate::cascade::PointerTrigger;
let source = "try { app.alert('' + event.value); }\n\
catch (e) { app.alert('' + e); }";
let mut down = with_pointer_script("D", source);
down.pointer(&field(), PointerTrigger::Down, crate::Modifiers::NONE);
assert_eq!(
down.transcript_text(),
"Alert: event.value: Object no longer exists.\n"
);
let mut focus = with_pointer_script("Fo", source);
focus.pointer(&field(), PointerTrigger::Focus, crate::Modifiers::NONE);
assert_eq!(focus.transcript_text(), "Alert: typed\n");
}
#[test]
fn the_two_modifier_flags_are_control_and_shift() {
use crate::cascade::PointerTrigger;
let source = "app.alert(event.modifier + ',' + event.shift);";
let cases = [
(crate::Modifiers::NONE, "false,false"),
(crate::Modifiers::CONTROL, "true,false"),
(crate::Modifiers::SHIFT, "false,true"),
(crate::Modifiers::ALT, "false,false"),
];
for (held, expected) in cases {
let mut cascade = with_pointer_script("D", source);
cascade.pointer(&field(), PointerTrigger::Down, held);
assert_eq!(cascade.transcript_text(), format!("Alert: {expected}\n"));
}
}
#[test]
fn a_field_without_the_trigger_runs_nothing() {
use crate::cascade::PointerTrigger;
let mut cascade = with_pointer_script("D", "app.alert('fired');");
cascade.pointer(&field(), PointerTrigger::Up, crate::Modifiers::NONE);
assert_eq!(cascade.transcript_text(), "");
}
#[test]
fn a_page_load_runs_the_formatter_for_its_side_effects() {
let mut cascade = with_script("F", "app.alert('formatted ' + event.value);");
cascade.set_field_value(0, "stored");
assert!(cascade.format_on_load(&field()));
assert_eq!(cascade.transcript_text(), "Alert: formatted stored\n");
}
#[test]
fn a_validate_script_refuses_through_event_rc() {
let mut cascade = with_script("V", "if (event.value == 'bad') event.rc = false;");
assert!(cascade.validate(&field(), "fine"));
assert!(!cascade.validate(&field(), "bad"));
}
#[test]
fn event_rc_is_truthiness_and_not_a_type_check() {
let mut cascade = with_script("V", "event.rc = 'boo';");
assert!(
cascade.validate(&field(), "x"),
"a non-empty string is truthy, and upstream agrees"
);
let mut zero = with_script("V", "event.rc = 0;");
assert!(!zero.validate(&field(), "x"));
}
#[test]
fn a_format_script_produces_a_display_string() {
let mut cascade = with_script("F", "event.value = '$' + event.value + '.00';");
assert_eq!(
cascade.format(&field(), "1234").as_deref(),
Some("$1234.00")
);
}
#[test]
fn a_format_script_that_changes_nothing_answers_none() {
let mut cascade = with_script("F", "var unused = event.value;");
assert_eq!(cascade.format(&field(), "1234"), None);
}
#[test]
fn format_hard_codes_will_commit() {
let mut cascade = with_script("F", "event.value = String(event.willCommit);");
assert_eq!(cascade.format(&field(), "x").as_deref(), Some("true"));
}
#[test]
fn a_keystroke_script_rewrites_the_change() {
let mut cascade = with_script("K", "event.change = event.change.toUpperCase();");
let offered = Keystroke {
change: "a".to_string(),
value: "xy".to_string(),
selection_start: 2,
selection_end: 2,
};
match cascade.keystroke(&field(), offered) {
KeystrokeOutcome::Accept(back) => {
assert_eq!(back.change, "A");
assert_eq!(back.applied(), "xyA");
}
KeystrokeOutcome::Reject => panic!("the script accepted"),
}
}
#[test]
fn a_keystroke_script_may_move_the_selection() {
let mut cascade = with_script("K", "event.selStart = 0; event.selEnd = 2;");
let offered = Keystroke {
change: "Z".to_string(),
value: "abcd".to_string(),
selection_start: 4,
selection_end: 4,
};
match cascade.keystroke(&field(), offered) {
KeystrokeOutcome::Accept(back) => {
assert_eq!(back.selection_start, 0);
assert_eq!(back.selection_end, 2);
assert_eq!(back.applied(), "Zcd");
}
KeystrokeOutcome::Reject => panic!("the script accepted"),
}
}
#[test]
fn no_calculation_order_means_no_calculation() {
let mut cascade = session();
cascade.set_field(
1,
"Total",
"",
FieldActions {
calculate: Some("event.value = 'computed';".to_string()),
..FieldActions::default()
},
);
let mut writes = FieldWrites::with_max_depth(1);
cascade.calculate(&mut writes, &field());
assert!(
writes.is_empty(),
"a field with /AA /C and no /CO entry must not be calculated"
);
}
#[test]
fn a_calculation_sweep_writes_the_fields_the_order_names() {
let mut cascade = session();
cascade.set_field(
1,
"Total",
"",
FieldActions {
calculate: Some("event.value = 'computed';".to_string()),
..FieldActions::default()
},
);
cascade.set_calculation_order(vec![1]);
let mut writes = FieldWrites::with_max_depth(1);
cascade.calculate(&mut writes, &field());
assert_eq!(writes.writes().collect::<Vec<_>>(), vec![(1, "computed")]);
}
#[test]
fn the_three_way_calculation_gate_is_reproduced() {
let unchanged = |source: &str, seed: &str| {
let mut cascade = session();
cascade.set_field(
1,
"Total",
seed,
FieldActions {
calculate: Some(source.to_string()),
..FieldActions::default()
},
);
cascade.set_calculation_order(vec![1]);
let mut writes = FieldWrites::with_max_depth(1);
cascade.calculate(&mut writes, &field());
writes.is_empty()
};
assert!(unchanged("throw new Error('no');", ""));
assert!(unchanged("event.value = 'x'; event.rc = false;", ""));
assert!(unchanged("event.value = 'same';", "same"));
assert!(!unchanged("event.value = 'different';", "same"));
}
#[test]
fn a_runaway_loop_terminates_with_a_diagnostic() {
let mut cascade = bounded(Limits {
max_script_loop_iterations: 100_000,
..Limits::default()
});
cascade.set_field(
0,
"Text Box",
"",
FieldActions {
validate: Some("while (true) {}".to_string()),
..FieldActions::default()
},
);
assert!(
!cascade.validate(&field(), "x"),
"an exhausted script refuses rather than accepts — inventing an \
acceptance is what lets a hostile file walk past a validator"
);
assert!(cascade.last_stop_was_a_limit());
let mut diags = pdfrum_common::Diagnostics::default();
cascade.drain_diagnostics(&mut diags);
assert!(diags.contains(&pdfrum_common::DiagKind::ScriptLimitReached));
}
#[test]
fn a_spent_deadline_refuses_a_script_as_a_limit() {
let mut cascade = bounded(Limits {
deadline: Some(pdfrum_common::Deadline::after(std::time::Duration::ZERO)),
..Limits::default()
});
cascade.set_field(
0,
"Text Box",
"",
FieldActions {
validate: Some("event.rc = true;".to_string()),
..FieldActions::default()
},
);
assert!(!cascade.validate(&field(), "x"));
assert!(cascade.last_stop_was_a_limit());
let mut diags = pdfrum_common::Diagnostics::default();
cascade.drain_diagnostics(&mut diags);
assert!(diags.contains(&pdfrum_common::DiagKind::ScriptLimitReached));
}
#[test]
fn unbounded_recursion_terminates_with_a_diagnostic() {
let mut cascade = bounded(Limits {
max_script_recursion: 64,
..Limits::default()
});
cascade.set_field(
0,
"Text Box",
"",
FieldActions {
validate: Some("function f() { return f(); } f();".to_string()),
..FieldActions::default()
},
);
assert!(!cascade.validate(&field(), "x"));
assert!(cascade.last_stop_was_a_limit());
}
#[test]
fn a_deep_stack_terminates_with_a_diagnostic() {
let mut cascade = bounded(Limits {
max_script_stack: 512,
max_script_recursion: 100_000,
..Limits::default()
});
cascade.set_field(
0,
"Text Box",
"",
FieldActions {
validate: Some("function f(n) { return n ? f(n - 1) : 0; } f(100000);".to_string()),
..FieldActions::default()
},
);
assert!(!cascade.validate(&field(), "x"));
assert!(cascade.last_stop_was_a_limit());
}
#[test]
fn every_hook_refuses_when_its_script_is_stopped() {
let runaway = || Some("while (true) {}".to_string());
let limits = Limits {
max_script_loop_iterations: 10_000,
..Limits::default()
};
let mut keystroke = bounded(limits.clone());
keystroke.set_field(
0,
"Text Box",
"",
FieldActions {
keystroke: runaway(),
..FieldActions::default()
},
);
assert_eq!(
keystroke.keystroke(
&field(),
Keystroke {
change: "a".to_string(),
value: String::new(),
selection_start: 0,
selection_end: 0,
}
),
KeystrokeOutcome::Reject
);
let mut commit = bounded(limits.clone());
commit.set_field(
0,
"Text Box",
"",
FieldActions {
keystroke: runaway(),
..FieldActions::default()
},
);
assert!(!commit.keystroke_commit(&field(), "x"));
let mut validate = bounded(limits.clone());
validate.set_field(
0,
"Text Box",
"",
FieldActions {
validate: runaway(),
..FieldActions::default()
},
);
assert!(!validate.validate(&field(), "x"));
let mut format = bounded(limits);
format.set_field(
0,
"Text Box",
"",
FieldActions {
format: runaway(),
..FieldActions::default()
},
);
assert_eq!(format.format(&field(), "x"), None);
}
#[test]
fn a_calculate_depth_of_one_refuses_nesting() {
let mut cascade = session();
cascade.set_field(
1,
"Total",
"",
FieldActions {
calculate: Some("event.value = 'computed';".to_string()),
..FieldActions::default()
},
);
cascade.set_calculation_order(vec![1]);
let mut writes = FieldWrites::with_max_depth(1);
cascade.calculate(&mut writes, &field());
assert_eq!(writes.writes().count(), 1);
assert!(writes.enter(), "the budget allows one level");
let before = writes.writes().count();
cascade.calculate(&mut writes, &field());
assert_eq!(
writes.writes().count(),
before,
"a nested sweep must write nothing"
);
}
#[test]
fn a_reentrant_script_is_refused() {
let mut cascade = session();
assert!(cascade.run("var x = 1;", "outer"));
assert!(cascade.stops().is_empty());
}
#[test]
fn no_io_is_reachable_from_a_script() {
let mut cascade = session();
assert!(cascade.run(
"var absent = [];\n\
var names = ['require', 'process', 'fetch', 'XMLHttpRequest', 'WebSocket',\n\
'importScripts', 'open', 'read', 'readFile', 'write',\n\
'writeFile', 'load', 'quit', 'system', 'os', 'fs', 'net',\n\
'Net', 'Directory', 'ADBC', 'security', 'dbg',\n\
'localStorage', 'window', 'document',\n\
'WebAssembly', 'gc'];\n\
for (var i = 0; i < names.length; i++) {\n\
if (typeof this[names[i]] !== 'undefined') absent.push(names[i]);\n\
}\n\
app.alert(absent.length ? absent.join(',') : 'none');",
"test"
));
assert_eq!(
cascade.transcript_text(),
"Alert: none\n",
"a script must reach no filesystem, network, process or environment"
);
}
#[test]
fn a_timer_is_recorded_and_never_fired() {
let mut cascade = session();
assert!(cascade.run(
"var t = app.setTimeOut('app.alert(\"fired\")', 1000);\n\
app.alert(typeof t);",
"test"
));
assert_eq!(
cascade.transcript_text(),
"Alert: object\n",
"the timer's own script must not have run"
);
assert_eq!(
cascade.timers(),
vec![("app.alert(\"fired\")".to_string(), 1000)],
"…but what the document wanted is recorded"
);
}
#[test]
fn launch_url_is_a_no_op_because_upstream_is_one() {
let mut cascade = session();
assert!(cascade.run(
"app.launchURL('https://example.com'); app.alert('still here');",
"test"
));
assert_eq!(cascade.transcript_text(), "Alert: still here\n");
}
#[test]
fn the_declined_methods_answer_the_oracles_own_message() {
for name in [
"execMenuItem",
"newDoc",
"openDoc",
"popUpMenu",
"popUpMenuEx",
] {
let mut cascade = session();
let source = format!(
"try {{ app.{name}(); app.alert('no throw'); }} \
catch (e) {{ app.alert('' + e); }}"
);
assert!(cascade.run(&source, "test"));
assert_eq!(
cascade.transcript_text(),
format!("Alert: app.{name}: Operation not supported.\n"),
"app.{name} must answer the oracle's own message, qualified by \
its own name — `JSFormatErrorString`, js_resources.cpp:97-108"
);
}
}
#[test]
fn every_stub_is_inert_rather_than_fatal() {
let mut cascade = session();
assert!(cascade.run(
"app.browseForDoc(); app.execDialog(); app.findComponent();\n\
app.goBack(); app.goForward(); app.newFDF(); app.openFDF();\n\
app.alert('survived');",
"test"
));
assert_eq!(cascade.transcript_text(), "Alert: survived\n");
}
#[test]
fn a_script_that_will_not_parse_is_recorded_rather_than_fatal() {
let mut cascade = session();
assert!(!cascade.run("this is not javascript {{{", "test"));
assert!(matches!(
cascade.stops().first().map(|failure| &failure.stop),
Some(ScriptStop::Threw(_))
));
}
#[test]
fn an_uncaught_throw_yields_one_diagnostic_with_its_whence_and_message() {
let mut cascade = session();
assert!(!cascade.run("app.alert(this.noSuchThing().length);", "/OpenAction"));
let mut diags = pdfrum_common::Diagnostics::default();
let failures = cascade.drain_diagnostics(&mut diags);
assert_eq!(failures.len(), 1, "one throw, one diagnostic");
let failure = failures.first().expect("one failure");
assert_eq!(failure.whence, "/OpenAction", "it says which script");
let ScriptStop::Threw(message) = &failure.stop else {
panic!("an unbound name is a throw, not a limit: {failure:?}");
};
assert!(
message.starts_with("TypeError: not a callable function"),
"it says what the engine said: {message}"
);
assert!(
message.contains(":1:27"),
"and where, which is the line and column upstream's `JS_Error` \
carries and then discards: {message}"
);
assert!(diags.contains(&pdfrum_common::DiagKind::ScriptFailed));
assert_eq!(diags.len(), 1);
let line = failure.line();
assert_eq!(
line, "script /OpenAction: TypeError: not a callable function (unknown at :1:27)",
"the reported line carries the whence, the message and the position"
);
assert!(
!line.contains('\n'),
"and it is one line: boa's stack frames are trimmed off"
);
let mut again = pdfrum_common::Diagnostics::default();
assert!(cascade.drain_diagnostics(&mut again).is_empty());
}
#[test]
fn an_unnamed_script_is_reported_as_the_open_action() {
let mut cascade = session();
assert!(!cascade.run("throw 'boom';", ""));
let failures = cascade.drain_diagnostics(&mut pdfrum_common::Diagnostics::default());
assert_eq!(
failures.first().map(ScriptFailure::line).as_deref(),
Some("script /OpenAction: \"boom\"")
);
}
#[test]
fn a_throw_does_not_stop_the_scripts_after_it() {
let mut cascade = session();
assert!(cascade.run("app.alert('first');", "one"));
assert!(!cascade.run("this.noSuchThing();", "two"));
assert!(cascade.run("app.alert('third');", "three"));
assert!(!cascade.run("throw 'boom';", "four"));
assert!(cascade.run("app.alert('fifth');", "five"));
assert_eq!(
cascade.transcript_text(),
"Alert: first\nAlert: third\nAlert: fifth\n",
"every script after a throw still ran and still spoke"
);
let failures = cascade.drain_diagnostics(&mut pdfrum_common::Diagnostics::default());
let whences: Vec<&str> = failures.iter().map(|f| f.whence.as_str()).collect();
assert_eq!(
whences,
["two", "four"],
"both throws were written down, not only the first"
);
}
#[test]
fn a_throwing_hook_leaves_the_session_usable_for_the_next_one() {
let mut cascade = session();
cascade.set_field(
0,
"Text Box",
"",
FieldActions {
keystroke: Some("this.noSuchThing();".to_string()),
..FieldActions::default()
},
);
cascade.set_field(
1,
"Other Box",
"",
FieldActions {
validate: Some("app.alert('validated'); event.rc = true;".to_string()),
..FieldActions::default()
},
);
let outcome = cascade.keystroke(
&field(),
Keystroke {
change: "a".to_string(),
value: String::new(),
selection_start: 0,
selection_end: 0,
},
);
assert_eq!(
outcome,
KeystrokeOutcome::Reject,
"a script that threw did not say accept"
);
let other = FieldRef {
name: "Other Box".to_string(),
index: Some(1),
};
assert!(
cascade.validate(&other, "x"),
"the next hook in the cascade still runs, and still answers"
);
assert_eq!(cascade.transcript_text(), "Alert: validated\n");
let failures = cascade.drain_diagnostics(&mut pdfrum_common::Diagnostics::default());
assert_eq!(failures.len(), 1, "only the keystroke threw");
assert_eq!(
failures.first().map(|f| f.whence.as_str()),
Some("Text Box")
);
}
#[test]
fn a_field_with_no_script_behaves_exactly_as_without_an_engine() {
let mut cascade = session();
let offered = Keystroke {
change: "a".to_string(),
value: String::new(),
selection_start: 0,
selection_end: 0,
};
assert_eq!(
cascade.keystroke(&field(), offered.clone()),
KeystrokeOutcome::Accept(offered)
);
assert!(cascade.keystroke_commit(&field(), "x"));
assert!(cascade.validate(&field(), "x"));
assert_eq!(cascade.format(&field(), "1234"), None);
assert!(cascade.stops().is_empty());
}
#[test]
fn a_hostile_field_value_cannot_escape_its_string_literal() {
for hostile in [
"'; app.alert('pwned'); '",
"\"; app.alert('pwned'); \"",
"\\\"; app.alert('pwned'); //",
"\n app.alert('pwned'); \n",
"\u{2028} app.alert('pwned');",
"</script>",
] {
let mut cascade = with_script("V", "app.alert(event.value.length);");
assert!(
cascade.validate(&field(), hostile),
"the script itself accepts"
);
let text = cascade.transcript_text();
assert!(
!text.contains("pwned"),
"a field value must never become code: {hostile:?} produced {text:?}"
);
assert_eq!(
text,
format!("Alert: {}\n", hostile.chars().count()),
"the value must arrive whole as well as inert"
);
}
}
fn with_document() -> ScriptCascade {
use crate::script::model::{DocumentModel, FieldModel, FieldModelKind};
let mut cascade = session();
cascade.set_document(DocumentModel {
page_count: 1,
fields: vec![
FieldModel {
name: "MyField".to_string(),
kind: FieldModelKind::Text,
value: "old".to_string(),
..FieldModel::default()
},
FieldModel {
name: "MyField2".to_string(),
kind: FieldModelKind::Text,
..FieldModel::default()
},
],
..DocumentModel::empty()
});
cascade
}
#[test]
fn bug_765384_set_focus_and_border_style_do_not_reenter() {
let mut cascade = with_document();
assert!(
cascade.run(
"this.getField(\"MyField2\").setFocus();\n\
this.getField(\"MyField\").borderStyle=\"dashed\";\n\
this.getField(\"MyField\").setFocus();",
"/OpenAction",
),
"the script must complete: {:?}",
cascade.stops()
);
assert_eq!(
cascade.take_focus_request(),
Some(0),
"the last setFocus wins, and it comes back as a request"
);
assert!(cascade.transcript().is_empty(), "and nothing is alerted");
}
#[test]
fn bug_1477093_a_missing_field_is_undefined_and_the_throw_is_reported() {
let mut cascade = with_document();
assert!(
cascade.run("this.getField('bad_field');", "/OpenAction"),
"asking for a missing field is not itself an error"
);
assert!(cascade.run(
"app.alert(typeof this.getField('bad_field'));",
"/OpenAction"
));
assert_eq!(
crate::script::transcript::render(&cascade.transcript()),
"Alert: undefined\n",
"a missing field is `undefined`, not null and not an error"
);
assert!(!cascade.run("this.getField('bad_field').value = 'Apple';", "run"));
let mut diags = pdfrum_common::Diagnostics::default();
let failures = cascade.drain_diagnostics(&mut diags);
assert_eq!(failures.len(), 1);
let failure = failures.first().expect("one failure");
assert_eq!(failure.whence, "run");
assert!(
matches!(&failure.stop, ScriptStop::Threw(message)
if message.starts_with("TypeError")),
"a property set on `undefined` is a TypeError: {:?}",
failure.stop
);
}
fn one_second() -> std::time::Duration {
std::time::Duration::from_secs(1)
}
#[test]
fn bug_620428_a_cancelled_timer_and_interval_fire_nothing() {
let mut cascade = session();
assert!(cascade.run(
"function fireTimeOut() { app.alert(\"hello world\"); }\n\
function fireInterval() { app.alert(\"goodbye world\"); }\n\
var timer = app.setTimeOut(\"fireTimeOut()\", 3000);\n\
var interval = app.setInterval(\"fireInterval()\", 1000);\n\
app.clearTimeOut(timer);\n\
app.clearInterval(interval);\n\
app.clearTimeOut(timer);\n\
app.clearInterval(interval);\n\
app.alert(\"done\");",
"/OpenAction",
));
assert_eq!(
cascade.timers().len(),
0,
"cancelling twice leaves nothing armed, and the second is not an error"
);
assert_eq!(cascade.advance_time(std::time::Duration::from_secs(5)), 0);
assert_eq!(
crate::script::transcript::render(&cascade.transcript()),
"Alert: done\n"
);
}
#[test]
fn bug_634394_cancelling_inside_a_callback_stops_at_two_alerts() {
let mut cascade = session();
assert!(cascade.run(
"var interval;\n\
function fireTimeOut() { app.alert(\"goodbye world\"); \
app.clearInterval(interval); }\n\
function fireInterval() { app.alert(\"hello world\"); \
app.clearInterval(interval); }\n\
var timer = app.setTimeOut(\"fireTimeOut()\", 3000);\n\
interval = app.setInterval(\"fireInterval()\", 1000);",
"/OpenAction",
));
for _ in 0..5 {
cascade.advance_time(one_second());
}
assert_eq!(
cascade.transcript().len(),
2,
"got {:?}",
crate::script::transcript::render(&cascade.transcript())
);
}
#[test]
fn bug_679649_a_refused_timer_fires_nothing() {
let mut cascade = session();
cascade.fail_next_timer();
assert!(cascade.run(
"function ping() { app.alert(\"ping\"); }\n\
var timer = app.setTimeOut(\"ping()\", 100);\n\
app.clearTimeOut(timer);",
"/OpenAction",
));
assert_eq!(cascade.advance_time(std::time::Duration::from_secs(2)), 0);
assert!(cascade.transcript().is_empty());
}
#[test]
fn an_interval_fires_once_per_step_and_not_once_per_interval() {
let mut cascade = session();
assert!(cascade.run(
"app.setInterval(\"app.alert('tick')\", 1000);",
"/OpenAction",
));
assert_eq!(
cascade.advance_time(std::time::Duration::from_secs(5)),
1,
"one 5-second step fires the 1-second interval once"
);
for _ in 0..3 {
cascade.advance_time(one_second());
}
assert_eq!(
cascade.transcript().len(),
4,
"three more steps, three more firings"
);
}
#[test]
fn a_one_shot_fires_once_and_an_interval_keeps_going() {
let mut cascade = session();
assert!(cascade.run(
"app.setTimeOut(\"app.alert('once')\", 500);\n\
app.setInterval(\"app.alert('again')\", 500);",
"/OpenAction",
));
assert_eq!(cascade.timers().len(), 2);
assert_eq!(cascade.advance_time(one_second()), 2);
assert_eq!(cascade.timers().len(), 1, "the one-shot is gone");
assert_eq!(cascade.advance_time(one_second()), 1);
assert_eq!(
crate::script::transcript::render(&cascade.transcript()),
"Alert: once\nAlert: again\nAlert: again\n"
);
}
#[test]
fn a_zero_timeout_one_shot_arms_and_runs_nothing() {
let mut cascade = session();
assert!(cascade.run("app.setTimeOut(\"app.alert('never')\", 0);", "/OpenAction"));
assert_eq!(cascade.timers().len(), 1, "it is armed");
assert_eq!(cascade.advance_time(one_second()), 0, "and runs nothing");
assert!(cascade.transcript().is_empty());
assert_eq!(cascade.timers().len(), 0, "and is gone afterwards");
}
#[test]
fn a_throwing_timer_script_is_recorded_and_the_next_still_fires() {
let mut cascade = session();
assert!(cascade.run(
"app.setTimeOut(\"nonesuch()\", 100);\n\
app.setTimeOut(\"app.alert('after')\", 200);",
"/OpenAction",
));
assert_eq!(cascade.advance_time(one_second()), 2);
assert_eq!(
crate::script::transcript::render(&cascade.transcript()),
"Alert: after\n"
);
assert_eq!(
cascade.stops().len(),
1,
"the throw is recorded, not swallowed"
);
}
#[test]
fn the_timer_functions_check_their_own_arguments() {
let mut cascade = session();
assert!(cascade.run(
"function say(f) { try { f(); app.alert('no throw'); } \
catch (e) { app.alert('' + e); } }\n\
say(function () { app.setTimeOut(); });\n\
say(function () { app.setTimeOut('a', 1, 2); });\n\
say(function () { app.setTimeOut('', 1); });\n\
say(function () { app.clearTimeOut(); });\n\
say(function () { app.clearTimeOut(42); });",
"/OpenAction",
));
assert_eq!(
crate::script::transcript::render(&cascade.transcript()),
"Alert: app.setTimeOut: Incorrect number of parameters passed to function.\n\
Alert: app.setTimeOut: Incorrect number of parameters passed to function.\n\
Alert: app.setTimeOut: The input value is invalid.\n\
Alert: app.clearTimeOut: Incorrect number of parameters passed to function.\n\
Alert: no throw\n"
);
}
#[test]
fn a_timer_with_no_interval_defaults_to_one_second() {
let mut cascade = session();
assert!(cascade.run("app.setInterval(\"app.alert('t')\");", "/OpenAction"));
assert_eq!(
cascade.advance_time(std::time::Duration::from_millis(999)),
0
);
assert_eq!(cascade.advance_time(std::time::Duration::from_millis(1)), 1);
}
fn with_toggles() -> ScriptCascade {
use crate::script::model::{DocumentModel, FieldModel, FieldModelKind};
let mut cascade = session();
cascade.set_document(DocumentModel {
page_count: 1,
fields: vec![
FieldModel {
name: "MyRadio".to_string(),
kind: FieldModelKind::RadioButton,
value: "Off".to_string(),
export_values: vec!["Yes".to_string()],
checked: vec![false],
..FieldModel::default()
},
FieldModel {
name: "MyCheck".to_string(),
kind: FieldModelKind::CheckBox,
value: "Off".to_string(),
value_as_string: Some("Off".to_string()),
export_values: vec!["Yes".to_string()],
checked: vec![false],
..FieldModel::default()
},
FieldModel {
name: "MyStarred".to_string(),
kind: FieldModelKind::CheckBox,
value: "Off".to_string(),
captions: ["H".to_string(), String::new(), String::new()],
export_values: vec!["Yes".to_string()],
checked: vec![false],
..FieldModel::default()
},
],
..DocumentModel::empty()
});
cascade
}
#[test]
fn field_style_reads_the_caption_then_falls_back_to_the_kind() {
let mut cascade = with_toggles();
assert!(
cascade.run(
"app.alert(this.getField('MyRadio').style);\
app.alert(this.getField('MyCheck').style);\
app.alert(this.getField('MyStarred').style);",
"test",
),
"the script must complete: {:?}",
cascade.stops()
);
let text = cascade.transcript_text();
assert_eq!(
text, "Alert: circle\nAlert: check\nAlert: star\n",
"an empty caption is the kind's default and a present one wins"
);
}
#[test]
fn field_style_on_a_text_field_is_the_wrong_type() {
let mut cascade = with_document();
assert!(
cascade.run(
"try { this.getField('MyField').style; }\
catch (e) { app.alert('' + e); }",
"test",
),
"the script must complete: {:?}",
cascade.stops()
);
assert_eq!(
cascade.transcript_text(),
"Alert: Field.style: Object is of the wrong type.\n"
);
}
#[test]
fn a_toggle_ignores_a_value_that_names_no_control() {
let mut cascade = with_toggles();
assert!(
cascade.run(
"var f = this.getField('MyCheck');\
f.value = 42;\
app.alert('after junk: ' + f.value + '/' + f.valueAsString);\
f.value = 'Yes';\
app.alert('after Yes: ' + f.value + '/' + f.valueAsString);",
"test",
),
"the script must complete: {:?}",
cascade.stops()
);
assert_eq!(
cascade.transcript_text(),
"Alert: after junk: Off/Off\nAlert: after Yes: Yes/Yes\n",
"a value naming no control leaves the group off; one that names a \
control checks it"
);
}
#[test]
fn a_document_delay_clears_what_a_field_delay_parked() {
let mut cascade = with_document();
assert!(
cascade.run(
"var ff = this.getField('MyField');\
ff.delay = true;\
ff.value = 'new value';\
this.delay = true;\
ff.delay = false;\
app.alert(\"field value is '\" + ff.value + \"'\");",
"test",
),
"the script must complete: {:?}",
cascade.stops()
);
assert_eq!(
cascade.transcript_text(),
"Alert: field value is 'old'\n",
"the parked write was discarded, not deferred"
);
}
#[test]
fn dropping_a_field_delay_flushes_what_it_parked() {
let mut cascade = with_document();
assert!(
cascade.run(
"var ff = this.getField('MyField');\
ff.delay = true;\
ff.value = 'new value';\
app.alert(\"while delayed: '\" + ff.value + \"'\");\
ff.delay = false;\
app.alert(\"after flush: '\" + ff.value + \"'\");",
"test",
),
"the script must complete: {:?}",
cascade.stops()
);
assert_eq!(
cascade.transcript_text(),
"Alert: while delayed: 'old'\nAlert: after flush: 'new value'\n"
);
}
#[test]
fn one_fields_flush_leaves_another_fields_parked_write() {
let mut cascade = with_document();
assert!(
cascade.run(
"var a = this.getField('MyField');\
var b = this.getField('MyField2');\
a.delay = true; b.delay = true;\
a.value = 'A'; b.value = 'B';\
a.delay = false;\
app.alert('a=' + a.value + ' b=' + b.value);\
b.delay = false;\
app.alert('a=' + a.value + ' b=' + b.value);",
"test",
),
"the script must complete: {:?}",
cascade.stops()
);
assert_eq!(
cascade.transcript_text(),
"Alert: a=A b=\nAlert: a=A b=B\n",
"dropping a's flag flushes only a's write"
);
}
#[test]
fn a_named_action_is_recorded_as_the_host_request_it_is() {
let mut cascade = session();
cascade.record_named_action("Print");
assert_eq!(cascade.transcript_text(), "Execute named action: Print\n");
}