1use std::cell::RefCell;
22use std::sync::Arc;
23
24use rquickjs::class::{Class, Trace};
25use rquickjs::function::{Args, Constructor, Func, Opt, Rest};
26use rquickjs::{
27 ArrayBuffer, AsyncContext, CatchResultExt, Ctx, Function, JsLifetime, Object, Persistent, TypedArray, Value,
28 async_with,
29};
30
31use crate::bindings::convert::{serde_from_js, serde_to_js};
32use crate::bindings::{install_browser_context_on, install_browser_on, install_page_on, install_request_on};
33use crate::engine::caught_to_script_error;
34use crate::error::ScriptError;
35
36const SKIP_SENTINEL: &str = "__ferri_skip__";
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum StepKind {
44 Given,
45 When,
46 Then,
47 Step,
48}
49
50impl StepKind {
51 #[must_use]
52 pub fn as_str(self) -> &'static str {
53 match self {
54 Self::Given => "Given",
55 Self::When => "When",
56 Self::Then => "Then",
57 Self::Step => "Step",
58 }
59 }
60}
61
62struct StepReg {
63 kind: StepKind,
64 pattern: String,
65 is_regex: bool,
66 func: Persistent<Function<'static>>,
67 timeout_ms: Option<u64>,
70}
71
72struct HookReg {
73 kind: String,
74 tags: Option<String>,
75 func: Persistent<Function<'static>>,
76 timeout_ms: Option<u64>,
78}
79
80struct ParamTypeReg {
81 name: String,
82 regexp: String,
83 transformer: Option<Persistent<Function<'static>>>,
87}
88
89struct ToolReg {
93 name: String,
94 description: Option<String>,
95 input_schema: Option<serde_json::Value>,
96 expose_as_tool: bool,
97 allowed_commands: std::collections::BTreeMap<String, crate::command_spec::CommandSpec>,
98 allowed_net: Vec<String>,
99 timeout_ms: Option<u64>,
103 handler: Persistent<Function<'static>>,
104}
105
106#[derive(Debug, Clone)]
111pub struct ScriptAttachment {
112 pub media_type: String,
113 pub bytes: Vec<u8>,
114}
115
116#[derive(Debug, Clone, Default)]
122pub struct HookArg {
123 pub name: String,
124 pub tags: Vec<String>,
125 pub status: String,
128 pub message: Option<String>,
129}
130
131#[derive(Default)]
139struct ExtensionRegistry {
140 steps: Vec<StepReg>,
141 hooks: Vec<HookReg>,
142 param_types: Vec<ParamTypeReg>,
143 tools: Vec<ToolReg>,
144 attachments: Vec<ScriptAttachment>,
147 default_timeout_ms: u64,
148 def_fn_wrapper: Option<Persistent<Function<'static>>>,
151 world_ctor: Option<Persistent<Constructor<'static>>>,
152 current_world: Option<Persistent<Object<'static>>>,
153}
154
155fn timeout_from_opts(args: &[Value<'_>]) -> Option<u64> {
159 args.iter().find_map(|v| {
160 let o = v.as_object()?;
161 if v.as_function().is_some() {
162 return None;
163 }
164 o.get::<_, f64>("timeout").ok().map(|ms| ms.max(0.0) as u64)
165 })
166}
167
168struct BddUserData(RefCell<ExtensionRegistry>);
171
172#[allow(unsafe_code)]
176unsafe impl JsLifetime<'_> for BddUserData {
177 type Changed<'to> = BddUserData;
178}
179
180fn with_registry<R>(ctx: &Ctx<'_>, f: impl FnOnce(&mut ExtensionRegistry) -> R) -> Result<R, ScriptError> {
181 let ud = ctx
182 .userdata::<BddUserData>()
183 .ok_or_else(|| ScriptError::internal("bdd registry not installed".to_string()))?;
184 let mut reg = ud.0.borrow_mut();
185 Ok(f(&mut reg))
186}
187
188#[derive(Trace, JsLifetime)]
190#[rquickjs::class(rename = "DataTable")]
191pub struct DataTableJs {
192 #[qjs(skip_trace)]
193 rows: Vec<Vec<String>>,
194}
195
196#[rquickjs::methods]
197impl DataTableJs {
198 #[qjs(rename = "raw")]
200 pub fn raw(&self) -> Vec<Vec<String>> {
201 self.rows.clone()
202 }
203
204 #[qjs(rename = "rows")]
206 pub fn data_rows(&self) -> Vec<Vec<String>> {
207 self.rows.iter().skip(1).cloned().collect()
208 }
209
210 #[qjs(rename = "hashes")]
212 pub fn hashes<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
213 let header = self.rows.first().cloned().unwrap_or_default();
214 let out: Vec<serde_json::Map<String, serde_json::Value>> = self
215 .rows
216 .iter()
217 .skip(1)
218 .map(|row| {
219 header
220 .iter()
221 .zip(row.iter())
222 .map(|(k, v)| (k.clone(), serde_json::Value::String(v.clone())))
223 .collect()
224 })
225 .collect();
226 serde_to_js(&ctx, &out)
227 }
228
229 #[qjs(rename = "rowsHash")]
231 pub fn rows_hash<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Value<'js>> {
232 let map: serde_json::Map<String, serde_json::Value> = self
233 .rows
234 .iter()
235 .filter(|r| r.len() >= 2)
236 .map(|r| (r[0].clone(), serde_json::Value::String(r[1].clone())))
237 .collect();
238 serde_to_js(&ctx, &map)
239 }
240
241 #[qjs(rename = "transpose")]
243 pub fn transpose<'js>(&self, ctx: Ctx<'js>) -> rquickjs::Result<Class<'js, DataTableJs>> {
244 let cols = self.rows.iter().map(Vec::len).max().unwrap_or(0);
245 let rows = (0..cols)
246 .map(|c| {
247 self
248 .rows
249 .iter()
250 .map(|r| r.get(c).cloned().unwrap_or_default())
251 .collect()
252 })
253 .collect();
254 Class::instance(ctx, DataTableJs { rows })
255 }
256}
257
258fn as_function<'js>(v: &Value<'js>) -> Option<Function<'js>> {
259 v.as_function().cloned()
260}
261
262fn pattern_of(a: &Value<'_>) -> Result<(String, bool), ScriptError> {
263 if let Some(s) = a.as_string() {
264 return Ok((s.to_string().map_err(|e| ScriptError::internal(e.to_string()))?, false));
265 }
266 if let Some(o) = a.as_object() {
267 if let Ok(src) = o.get::<_, String>("source") {
268 return Ok((src, true));
269 }
270 }
271 Err(ScriptError::internal(
272 "step pattern must be a string or RegExp".to_string(),
273 ))
274}
275
276fn rq(e: &ScriptError) -> rquickjs::Error {
277 rquickjs::Error::new_from_js_message("bdd", "Error", e.message.clone())
278}
279
280fn ctx_of<'js>(args: &[Value<'js>]) -> Result<Ctx<'js>, rquickjs::Error> {
281 args
282 .first()
283 .map(|v| v.ctx().clone())
284 .ok_or_else(|| rq(&ScriptError::internal("missing arguments".to_string())))
285}
286
287fn register_step(kind: StepKind, args: &[Value<'_>]) -> rquickjs::Result<()> {
288 let ctx = ctx_of(args)?;
289 let pattern = args
290 .first()
291 .ok_or_else(|| rq(&ScriptError::internal("step pattern missing".to_string())))?;
292 let (pat, is_regex) = pattern_of(pattern).map_err(|e| rq(&e))?;
293 let func = args
296 .iter()
297 .skip(1)
298 .rev()
299 .find_map(as_function)
300 .ok_or_else(|| rq(&ScriptError::internal(format!("step `{pat}` has no function body"))))?;
301 let timeout_ms = timeout_from_opts(&args[1..]);
302 let saved = Persistent::save(&ctx, func);
303 with_registry(&ctx, |reg| {
304 reg.steps.push(StepReg {
305 kind,
306 pattern: pat,
307 is_regex,
308 func: saved,
309 timeout_ms,
310 });
311 })
312 .map_err(|e| rq(&e))
313}
314
315fn register_hook(kind: &str, args: &[Value<'_>]) -> rquickjs::Result<()> {
316 let ctx = ctx_of(args)?;
317 let first = args
318 .first()
319 .ok_or_else(|| rq(&ScriptError::internal(format!("{kind} hook missing"))))?;
320 let (tags, func) = if let Some(f) = as_function(first) {
321 (None, f)
322 } else {
323 let tags = if let Some(s) = first.as_string() {
324 Some(s.to_string().map_err(|e| rq(&ScriptError::internal(e.to_string())))?)
325 } else if let Some(o) = first.as_object() {
326 o.get::<_, String>("tags").ok()
327 } else {
328 None
329 };
330 let f = args
331 .iter()
332 .skip(1)
333 .find_map(as_function)
334 .ok_or_else(|| rq(&ScriptError::internal(format!("{kind} hook has no function body"))))?;
335 (tags, f)
336 };
337 let timeout_ms = timeout_from_opts(args);
338 let saved = Persistent::save(&ctx, func);
339 with_registry(&ctx, |reg| {
340 reg.hooks.push(HookReg {
341 kind: kind.to_string(),
342 tags,
343 func: saved,
344 timeout_ms,
345 });
346 })
347 .map_err(|e| rq(&e))
348}
349
350fn value_bytes(v: &Value<'_>) -> Option<Vec<u8>> {
351 if let Ok(ta) = TypedArray::<u8>::from_value(v.clone())
352 && let Some(b) = ta.as_bytes()
353 {
354 return Some(b.to_vec());
355 }
356 if let Some(obj) = v.as_object()
357 && let Some(buf) = ArrayBuffer::from_object(obj.clone())
358 && let Some(b) = buf.as_bytes()
359 {
360 return Some(b.to_vec());
361 }
362 None
363}
364
365fn register_attachment(args: &[Value<'_>], is_log: bool) -> rquickjs::Result<()> {
373 let ctx = ctx_of(args)?;
374 let media_arg = args.get(1).and_then(Value::as_string).and_then(|s| s.to_string().ok());
375
376 let (bytes, media): (Vec<u8>, String) = if is_log {
377 let text = args
378 .iter()
379 .map(|v| {
380 v.as_string().and_then(|s| s.to_string().ok()).unwrap_or_else(|| {
381 serde_from_js::<serde_json::Value>(&ctx, v.clone())
382 .map(|j| j.to_string())
383 .unwrap_or_default()
384 })
385 })
386 .collect::<Vec<_>>()
387 .join(" ");
388 (text.into_bytes(), "text/x.cucumber.log+plain".to_string())
389 } else {
390 let data = args
391 .first()
392 .cloned()
393 .unwrap_or_else(|| Value::new_undefined(ctx.clone()));
394 if let Some(s) = data.as_string() {
395 let s = s.to_string().map_err(|e| rq(&ScriptError::internal(e.to_string())))?;
396 (s.into_bytes(), media_arg.unwrap_or_else(|| "text/plain".to_string()))
397 } else if let Some(b) = value_bytes(&data) {
398 (b, media_arg.unwrap_or_else(|| "application/octet-stream".to_string()))
399 } else {
400 let j: serde_json::Value = serde_from_js(&ctx, data).map_err(|e| rq(&ScriptError::internal(e.to_string())))?;
401 (
402 serde_json::to_vec(&j).unwrap_or_default(),
403 media_arg.unwrap_or_else(|| "application/json".to_string()),
404 )
405 }
406 };
407
408 with_registry(&ctx, |reg| {
409 reg.attachments.push(ScriptAttachment {
410 media_type: media,
411 bytes,
412 });
413 })
414 .map_err(|e| rq(&e))
415}
416
417pub async fn drain_attachments(actx: &AsyncContext) -> Result<Vec<ScriptAttachment>, ScriptError> {
421 async_with!(actx => |ctx| {
422 with_registry(&ctx, |reg| std::mem::take(&mut reg.attachments))
423 })
424 .await
425}
426
427fn register_tool<'js>(ctx: &Ctx<'js>, m: &Object<'js>, handler: Function<'js>) -> Result<(), ScriptError> {
431 let name: String = m
432 .get("name")
433 .map_err(|e| ScriptError::internal(format!("tool manifest missing string `name`: {e}")))?;
434 if name.trim().is_empty() {
435 return Err(ScriptError::internal(
436 "defineTool: `name` must be a non-empty string".to_string(),
437 ));
438 }
439 let description = m
440 .get::<_, Value<'_>>("description")
441 .ok()
442 .and_then(|v| v.as_string().and_then(|s| s.to_string().ok()));
443 let input_schema = match m.get::<_, Value<'_>>("inputSchema") {
444 Ok(v) if !v.is_undefined() && !v.is_null() => {
445 Some(serde_from_js::<serde_json::Value>(ctx, v).map_err(|e| ScriptError::internal(e.to_string()))?)
446 },
447 _ => None,
448 };
449 let expose_as_tool = m.get::<_, bool>("exposeAsTool").unwrap_or(false);
450 let timeout_ms = m
451 .get::<_, f64>("timeoutMs")
452 .ok()
453 .map(|ms| ms.max(0.0) as u64)
454 .filter(|&v| v > 0);
455
456 let (allowed_commands, allowed_net) = match m.get::<_, Value<'_>>("allow") {
457 Ok(v) => {
458 if let Some(allow) = v.as_object() {
459 let commands = ["exec", "commands"]
462 .into_iter()
463 .find_map(|k| match allow.get::<_, Value<'_>>(k) {
464 Ok(c) if c.is_object() => serde_from_js(ctx, c).ok(),
465 _ => None,
466 })
467 .unwrap_or_default();
468 let net = match allow.get::<_, Value<'_>>("net") {
469 Ok(n) if !n.is_undefined() && !n.is_null() => serde_from_js(ctx, n).unwrap_or_default(),
470 _ => Vec::new(),
471 };
472 (commands, net)
473 } else {
474 (std::collections::BTreeMap::new(), Vec::new())
475 }
476 },
477 Err(_) => (std::collections::BTreeMap::new(), Vec::new()),
478 };
479
480 let saved = Persistent::save(ctx, handler);
481 with_registry(ctx, |reg| {
482 if reg.tools.iter().any(|t| t.name == name) {
483 return Err(ScriptError::internal(format!(
484 "defineTool: duplicate tool name `{name}` — names must be unique across all loaded extensions"
485 )));
486 }
487 reg.tools.push(ToolReg {
488 name,
489 description,
490 input_schema,
491 expose_as_tool,
492 allowed_commands,
493 allowed_net,
494 timeout_ms,
495 handler: saved,
496 });
497 Ok(())
498 })?
499}
500
501fn register_tool_args(args: &[Value<'_>]) -> rquickjs::Result<()> {
508 let ctx = ctx_of(args)?;
509 let manifest = args.first().and_then(Value::as_object).ok_or_else(|| {
510 rq(&ScriptError::internal(
511 "defineTool: first arg must be a tool/manifest object".to_string(),
512 ))
513 })?;
514 let handler = args
517 .iter()
518 .skip(1)
519 .find_map(as_function)
520 .or_else(|| {
521 manifest
522 .get::<_, Value<'_>>("handler")
523 .ok()
524 .and_then(|v| v.as_function().cloned())
525 })
526 .ok_or_else(|| {
527 rq(&ScriptError::internal(
528 "defineTool: no handler — pass defineTool(tool) with a `handler` method or defineTool(manifest, fn)"
529 .to_string(),
530 ))
531 })?;
532 register_tool(&ctx, manifest, handler).map_err(|e| rq(&e))
533}
534
535pub fn install_bdd(ctx: &Ctx<'_>) -> rquickjs::Result<()> {
539 if ctx.userdata::<BddUserData>().is_some() {
540 return Ok(());
541 }
542 let _ = ctx.store_userdata(BddUserData(RefCell::new(ExtensionRegistry {
543 default_timeout_ms: 5000,
544 ..ExtensionRegistry::default()
545 })));
546
547 let g = ctx.globals();
548 Class::<DataTableJs>::define(&g)?;
549
550 for (name, kind) in [
551 ("Given", StepKind::Given),
552 ("When", StepKind::When),
553 ("Then", StepKind::Then),
554 ("defineStep", StepKind::Step),
555 ("And", StepKind::Step),
556 ("But", StepKind::Step),
557 ] {
558 g.set(
559 name,
560 Func::from(move |args: Rest<Value<'_>>| register_step(kind, &args.0)),
561 )?;
562 }
563
564 for hook in ["Before", "After", "BeforeAll", "AfterAll", "BeforeStep", "AfterStep"] {
565 g.set(
566 hook,
567 Func::from(move |args: Rest<Value<'_>>| register_hook(hook, &args.0)),
568 )?;
569 }
570
571 g.set(
572 "defineParameterType",
573 Func::from(|def: Object<'_>| -> rquickjs::Result<()> {
574 let ctx = def.ctx().clone();
575 let name: String = def.get("name").map_err(|e| rq(&ScriptError::internal(e.to_string())))?;
576 let rx_val: Value<'_> = def
577 .get("regexp")
578 .map_err(|e| rq(&ScriptError::internal(e.to_string())))?;
579 let regexp = if let Some(s) = rx_val.as_string() {
580 s.to_string().map_err(|e| rq(&ScriptError::internal(e.to_string())))?
581 } else if let Some(o) = rx_val.as_object() {
582 o.get::<_, String>("source")
583 .map_err(|e| rq(&ScriptError::internal(e.to_string())))?
584 } else {
585 return Err(rq(&ScriptError::internal(
586 "parameter type regexp must be string or RegExp".to_string(),
587 )));
588 };
589 let transformer = def
590 .get::<_, Value<'_>>("transformer")
591 .ok()
592 .and_then(|v| v.as_function().cloned())
593 .map(|f| Persistent::save(&ctx, f));
594 with_registry(&ctx, |reg| {
595 reg.param_types.push(ParamTypeReg {
596 name,
597 regexp,
598 transformer,
599 });
600 })
601 .map_err(|e| rq(&e))
602 }),
603 )?;
604
605 g.set(
606 "setDefaultTimeout",
607 Func::from(|ctx: Ctx<'_>, ms: f64| -> rquickjs::Result<()> {
608 with_registry(&ctx, |reg| reg.default_timeout_ms = ms.max(0.0) as u64).map_err(|e| rq(&e))
609 }),
610 )?;
611 g.set(
612 "setDefinitionFunctionWrapper",
613 Func::from(|w: Function<'_>| -> rquickjs::Result<()> {
614 let ctx = w.ctx().clone();
615 let saved = Persistent::save(&ctx, w);
616 with_registry(&ctx, |reg| reg.def_fn_wrapper = Some(saved)).map_err(|e| rq(&e))
617 }),
618 )?;
619 g.set(
620 "setWorldConstructor",
621 Func::from(|c: Constructor<'_>| -> rquickjs::Result<()> {
622 let ctx = c.ctx().clone();
623 let saved = Persistent::save(&ctx, c);
624 with_registry(&ctx, |reg| reg.world_ctor = Some(saved)).map_err(|e| rq(&e))
625 }),
626 )?;
627 g.set("setParallelCanAssign", Func::from(|_: Opt<Value<'_>>| {}))?;
635
636 g.set(
639 "defineTool",
640 Func::from(|args: Rest<Value<'_>>| register_tool_args(&args.0)),
641 )?;
642
643 Ok(())
644}
645
646#[derive(Debug, Clone)]
650pub struct CollectedStep {
651 pub kind: String,
652 pub pattern: String,
653 pub is_regex: bool,
654}
655
656#[derive(Debug, Clone)]
657pub struct CollectedHook {
658 pub hook_type: String,
659 pub tags: Option<String>,
660}
661
662#[derive(Debug, Clone)]
663pub struct CollectedParamType {
664 pub name: String,
665 pub regexp: String,
666}
667
668#[derive(Debug, Clone)]
669pub struct CollectedRegistry {
670 pub default_timeout_ms: u64,
671 pub steps: Vec<CollectedStep>,
672 pub hooks: Vec<CollectedHook>,
673 pub param_types: Vec<CollectedParamType>,
674}
675
676pub async fn collect_registry(actx: &AsyncContext) -> Result<CollectedRegistry, ScriptError> {
678 async_with!(actx => |ctx| {
679 with_registry(&ctx, |reg| CollectedRegistry {
680 default_timeout_ms: reg.default_timeout_ms,
681 steps: reg
682 .steps
683 .iter()
684 .map(|s| CollectedStep {
685 kind: s.kind.as_str().to_string(),
686 pattern: s.pattern.clone(),
687 is_regex: s.is_regex,
688 })
689 .collect(),
690 hooks: reg
691 .hooks
692 .iter()
693 .map(|h| CollectedHook {
694 hook_type: h.kind.clone(),
695 tags: h.tags.clone(),
696 })
697 .collect(),
698 param_types: reg
699 .param_types
700 .iter()
701 .map(|p| CollectedParamType {
702 name: p.name.clone(),
703 regexp: p.regexp.clone(),
704 })
705 .collect(),
706 })
707 })
708 .await
709}
710
711#[derive(Debug, Clone, serde::Serialize)]
715pub struct CollectedAllow {
716 pub commands: std::collections::BTreeMap<String, crate::command_spec::CommandSpec>,
717 pub net: Vec<String>,
718}
719
720#[derive(Debug, Clone, serde::Serialize)]
724#[serde(rename_all = "camelCase")]
725pub struct CollectedTool {
726 pub name: String,
727 #[serde(skip_serializing_if = "Option::is_none")]
728 pub description: Option<String>,
729 #[serde(skip_serializing_if = "Option::is_none")]
730 pub input_schema: Option<serde_json::Value>,
731 pub allow: CollectedAllow,
732 pub expose_as_tool: bool,
733 #[serde(skip_serializing_if = "Option::is_none")]
734 pub timeout_ms: Option<u64>,
735}
736
737pub fn tools_snapshot(ctx: &Ctx<'_>) -> Result<Vec<CollectedTool>, ScriptError> {
741 with_registry(ctx, |reg| {
742 reg
743 .tools
744 .iter()
745 .map(|t| CollectedTool {
746 name: t.name.clone(),
747 description: t.description.clone(),
748 input_schema: t.input_schema.clone(),
749 allow: CollectedAllow {
750 commands: t.allowed_commands.clone(),
751 net: t.allowed_net.clone(),
752 },
753 expose_as_tool: t.expose_as_tool,
754 timeout_ms: t.timeout_ms,
755 })
756 .collect()
757 })
758}
759
760pub fn tools_len(ctx: &Ctx<'_>) -> Result<usize, ScriptError> {
763 with_registry(ctx, |reg| reg.tools.len())
764}
765
766pub fn tool_names(ctx: &Ctx<'_>) -> Result<Vec<String>, ScriptError> {
769 with_registry(ctx, |reg| reg.tools.iter().map(|t| t.name.clone()).collect())
770}
771
772pub(crate) struct ToolDispatch<'js> {
776 pub handler: Function<'js>,
777 pub allowed_commands: std::collections::BTreeMap<String, crate::command_spec::CommandSpec>,
778 pub allowed_net: Vec<String>,
779 pub timeout_ms: Option<u64>,
780}
781
782pub(crate) fn tool_dispatch<'js>(ctx: &Ctx<'js>, idx: usize) -> Result<ToolDispatch<'js>, ScriptError> {
783 let (saved, allowed_commands, allowed_net, timeout_ms) = with_registry(ctx, |reg| {
784 reg
785 .tools
786 .get(idx)
787 .map(|t| {
788 (
789 t.handler.clone(),
790 t.allowed_commands.clone(),
791 t.allowed_net.clone(),
792 t.timeout_ms,
793 )
794 })
795 .ok_or_else(|| ScriptError::internal(format!("tool index {idx} out of range")))
796 })??;
797 let handler = saved.restore(ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
798 Ok(ToolDispatch {
799 handler,
800 allowed_commands,
801 allowed_net,
802 timeout_ms,
803 })
804}
805
806#[derive(Clone, Default)]
810pub struct ScenarioWorld {
811 pub page: Option<Arc<ferridriver::Page>>,
812 pub context: Option<Arc<ferridriver::context::ContextRef>>,
813 pub request: Option<Arc<ferridriver::http_client::HttpClient>>,
814 pub browser: Option<Arc<ferridriver::Browser>>,
815 pub parameters: Option<serde_json::Value>,
819}
820
821pub async fn set_scenario_world(actx: &AsyncContext, world: &ScenarioWorld) -> Result<(), ScriptError> {
825 let world = world.clone();
826 let route_ctx = actx.clone();
827 async_with!(actx => |ctx| {
828 let ctor = with_registry(&ctx, |reg| reg.world_ctor.clone())?;
829
830 let params_val: Value<'_> = match &world.parameters {
834 Some(v) if !v.is_null() => serde_to_js(&ctx, v).map_err(|e| ScriptError::internal(e.to_string()))?,
835 _ => Object::new(ctx.clone())
836 .map_err(|e| ScriptError::internal(e.to_string()))?
837 .into_value(),
838 };
839
840 let obj: Object<'_> = if let Some(ctor) = ctor {
841 let ctor = ctor.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
842 let opts = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
843 opts
844 .set("parameters", params_val.clone())
845 .map_err(|e| ScriptError::internal(e.to_string()))?;
846 ctor
847 .construct::<_, Object<'_>>((opts,))
848 .map_err(|e| ScriptError::internal(format!("World constructor: {e}")))?
849 } else {
850 Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?
851 };
852
853 obj
854 .set("parameters", params_val)
855 .map_err(|e| ScriptError::internal(e.to_string()))?;
856 let attach = Function::new(ctx.clone(), |args: Rest<Value<'_>>| register_attachment(&args.0, false))
859 .map_err(|e| ScriptError::internal(e.to_string()))?;
860 let log = Function::new(ctx.clone(), |args: Rest<Value<'_>>| register_attachment(&args.0, true))
861 .map_err(|e| ScriptError::internal(e.to_string()))?;
862 obj.set("attach", attach).map_err(|e| ScriptError::internal(e.to_string()))?;
863 obj.set("log", log).map_err(|e| ScriptError::internal(e.to_string()))?;
864 let skip = Function::new(ctx.clone(), || -> rquickjs::Result<()> {
867 Err(rquickjs::Error::new_from_js_message("World", "Error", SKIP_SENTINEL.to_string()))
868 })
869 .map_err(|e| ScriptError::internal(e.to_string()))?;
870 obj.set("skip", skip).map_err(|e| ScriptError::internal(e.to_string()))?;
871
872 if let Some(page) = world.page {
873 install_page_on(&ctx, &obj, page, route_ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
874 }
875 if let Some(c) = world.context {
876 install_browser_context_on(&ctx, &obj, c).map_err(|e| ScriptError::internal(e.to_string()))?;
877 }
878 if let Some(r) = world.request {
879 install_request_on(&ctx, &obj, r).map_err(|e| ScriptError::internal(e.to_string()))?;
880 }
881 if let Some(b) = world.browser {
882 install_browser_on(&ctx, &obj, b).map_err(|e| ScriptError::internal(e.to_string()))?;
883 }
884
885 let saved = Persistent::save(&ctx, obj);
886 with_registry(&ctx, |reg| reg.current_world = Some(saved))
887 })
888 .await
889}
890
891pub async fn reset_world(actx: &AsyncContext) -> Result<(), ScriptError> {
894 async_with!(actx => |ctx| {
895 with_registry(&ctx, |reg| {
896 reg.current_world = None;
897 reg.attachments.clear();
898 })
899 })
900 .await
901}
902
903#[derive(Debug, Clone)]
908pub enum JsArg {
909 Str(String),
910 Int(i64),
911 Float(f64),
912 Custom {
917 type_name: String,
918 raw: String,
919 },
920}
921
922#[derive(Debug, Clone, Copy, PartialEq, Eq)]
925pub enum StepOutcome {
926 Passed,
927 Pending,
928 Skipped,
929}
930
931pub async fn invoke_step(
935 actx: &AsyncContext,
936 idx: usize,
937 params: &[JsArg],
938 data_table: Option<&[Vec<String>]>,
939 doc_string: Option<&str>,
940 source: &str,
941) -> Result<StepOutcome, ScriptError> {
942 let params = params.to_vec();
943 let data_table = data_table.map(<[Vec<String>]>::to_vec);
944 let doc_string = doc_string.map(str::to_string);
945 let source = source.to_string();
946
947 async_with!(actx => |ctx| {
948 let (func, world, wrapper, timeout_ms) = with_registry(&ctx, |reg| {
949 let step = reg
950 .steps
951 .get(idx)
952 .ok_or_else(|| ScriptError::internal(format!("step index {idx} out of range")))?;
953 let t = step.timeout_ms.or(Some(reg.default_timeout_ms)).filter(|&v| v > 0);
954 Ok::<_, ScriptError>((step.func.clone(), reg.current_world.clone(), reg.def_fn_wrapper.clone(), t))
955 })??;
956
957 let mut func = func.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
958 if let Some(w) = wrapper {
961 let w = w.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
962 func = w
963 .call::<_, Function<'_>>((func.clone(),))
964 .catch(&ctx)
965 .map_err(|e| caught_to_script_error(e, &source))?;
966 }
967 let world_obj = match world {
968 Some(w) => w.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?,
969 None => Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?,
970 };
971
972 let n = 1 + params.len() + usize::from(data_table.is_some()) + usize::from(doc_string.is_some());
982 let mut args = Args::new(ctx.clone(), n);
983 args
984 .this(world_obj.clone())
985 .map_err(|e| ScriptError::internal(e.to_string()))?;
986 args
987 .push_arg(world_obj)
988 .map_err(|e| ScriptError::internal(e.to_string()))?;
989 for p in ¶ms {
990 match p {
991 JsArg::Str(s) => args.push_arg(s.as_str()).map_err(|e| ScriptError::internal(e.to_string()))?,
992 JsArg::Int(i) => args.push_arg(*i).map_err(|e| ScriptError::internal(e.to_string()))?,
993 JsArg::Float(f) => args.push_arg(*f).map_err(|e| ScriptError::internal(e.to_string()))?,
994 JsArg::Custom { type_name, raw } => {
995 let tx = with_registry(&ctx, |reg| {
999 reg
1000 .param_types
1001 .iter()
1002 .find(|pt| &pt.name == type_name)
1003 .and_then(|pt| pt.transformer.clone())
1004 })?;
1005 match tx {
1006 Some(saved) => {
1007 let f = saved.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
1008 let call: rquickjs::Result<rquickjs::promise::MaybePromise<'_>> = f.call((raw.as_str(),));
1009 let mp = call.catch(&ctx).map_err(|e| caught_to_script_error(e, &source))?;
1010 let v: Value<'_> = mp
1011 .into_future::<Value<'_>>()
1012 .await
1013 .catch(&ctx)
1014 .map_err(|e| caught_to_script_error(e, &source))?;
1015 args.push_arg(v).map_err(|e| ScriptError::internal(e.to_string()))?;
1016 },
1017 None => args
1018 .push_arg(raw.as_str())
1019 .map_err(|e| ScriptError::internal(e.to_string()))?,
1020 }
1021 },
1022 }
1023 }
1024 if let Some(rows) = data_table {
1025 let dt = Class::instance(ctx.clone(), DataTableJs { rows }).map_err(|e| ScriptError::internal(e.to_string()))?;
1026 args.push_arg(dt).map_err(|e| ScriptError::internal(e.to_string()))?;
1027 }
1028 if let Some(s) = doc_string {
1029 args.push_arg(s).map_err(|e| ScriptError::internal(e.to_string()))?;
1030 }
1031
1032 let called: rquickjs::Result<rquickjs::promise::MaybePromise<'_>> = args.apply(&func);
1033 let mp = match called.catch(&ctx) {
1034 Ok(v) => v,
1035 Err(e) => return Err(caught_to_script_error(e, &source)),
1036 };
1037 let fut = mp.into_future::<Value<'_>>();
1039 let awaited = match timeout_ms {
1040 Some(t) => match tokio::time::timeout(std::time::Duration::from_millis(t), fut).await {
1041 Ok(r) => r,
1042 Err(_) => return Err(ScriptError::timeout(t, t)),
1043 },
1044 None => fut.await,
1045 };
1046 let resolved: Value<'_> = match awaited.catch(&ctx) {
1047 Ok(v) => v,
1048 Err(e) => {
1049 let se = caught_to_script_error(e, &source);
1050 if se.message.contains(SKIP_SENTINEL) {
1053 return Ok(StepOutcome::Skipped);
1054 }
1055 return Err(se);
1056 },
1057 };
1058 let marker = resolved.as_string().and_then(|s| s.to_string().ok());
1059 Ok(match marker.as_deref() {
1060 Some("pending") => StepOutcome::Pending,
1061 Some("skipped") => StepOutcome::Skipped,
1062 _ => StepOutcome::Passed,
1063 })
1064 })
1065 .await
1066}
1067
1068pub async fn invoke_hook(
1070 actx: &AsyncContext,
1071 idx: usize,
1072 arg: Option<&HookArg>,
1073 source: &str,
1074) -> Result<StepOutcome, ScriptError> {
1075 let source = source.to_string();
1076 let arg = arg.cloned();
1077 async_with!(actx => |ctx| {
1078 let (func, world, timeout_ms) = with_registry(&ctx, |reg| {
1079 let hook = reg
1080 .hooks
1081 .get(idx)
1082 .ok_or_else(|| ScriptError::internal(format!("hook index {idx} out of range")))?;
1083 let t = hook.timeout_ms.or(Some(reg.default_timeout_ms)).filter(|&v| v > 0);
1084 Ok::<_, ScriptError>((hook.func.clone(), reg.current_world.clone(), t))
1085 })??;
1086 let func = func.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?;
1087 let world_obj = match world {
1088 Some(w) => w.restore(&ctx).map_err(|e| ScriptError::internal(e.to_string()))?,
1089 None => Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?,
1090 };
1091 let n_args = 1 + usize::from(arg.is_some());
1095 let mut args = Args::new(ctx.clone(), n_args);
1096 args
1097 .this(world_obj.clone())
1098 .map_err(|e| ScriptError::internal(e.to_string()))?;
1099 args
1100 .push_arg(world_obj)
1101 .map_err(|e| ScriptError::internal(e.to_string()))?;
1102 if let Some(a) = arg {
1103 let param = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
1104 let pickle = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
1105 pickle.set("name", a.name).map_err(|e| ScriptError::internal(e.to_string()))?;
1106 let tags = rquickjs::Array::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
1107 for (i, t) in a.tags.iter().enumerate() {
1108 let to = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
1109 to.set("name", t.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
1110 tags.set(i, to).map_err(|e| ScriptError::internal(e.to_string()))?;
1111 }
1112 pickle.set("tags", tags).map_err(|e| ScriptError::internal(e.to_string()))?;
1113 let result = Object::new(ctx.clone()).map_err(|e| ScriptError::internal(e.to_string()))?;
1114 result.set("status", a.status).map_err(|e| ScriptError::internal(e.to_string()))?;
1115 if let Some(m) = a.message {
1116 result.set("message", m).map_err(|e| ScriptError::internal(e.to_string()))?;
1117 }
1118 param.set("pickle", pickle).map_err(|e| ScriptError::internal(e.to_string()))?;
1119 param.set("result", result).map_err(|e| ScriptError::internal(e.to_string()))?;
1120 args.push_arg(param).map_err(|e| ScriptError::internal(e.to_string()))?;
1121 }
1122 let called: rquickjs::Result<rquickjs::promise::MaybePromise<'_>> = args.apply(&func);
1123 let mp = match called.catch(&ctx) {
1124 Ok(v) => v,
1125 Err(e) => return Err(caught_to_script_error(e, &source)),
1126 };
1127 let fut = mp.into_future::<Value<'_>>();
1128 let awaited = match timeout_ms {
1129 Some(t) => match tokio::time::timeout(std::time::Duration::from_millis(t), fut).await {
1130 Ok(r) => r,
1131 Err(_) => return Err(ScriptError::timeout(t, t)),
1132 },
1133 None => fut.await,
1134 };
1135 if let Err(e) = awaited.catch(&ctx) {
1136 return Err(caught_to_script_error(e, &source));
1137 }
1138 Ok(StepOutcome::Passed)
1139 })
1140 .await
1141}