using Jint.Runtime.Debugger; namespace Jint.Tests.Runtime.Debugger; public class CallStackTests { [Fact] public void IncludesFunctionNames() { var script = @" function foo() { debugger; } function bar() { foo(); } bar()"; TestHelpers.TestAtBreak(script, info => { Assert.Collection(info.CallStack, frame => Assert.Equal("foo", frame.FunctionName), frame => Assert.Equal("bar", frame.FunctionName), frame => Assert.Equal("(anonymous)", frame.FunctionName) ); }); } [Fact] public void IncludesLocations() { var script = @" function foo() { debugger; } function bar() { foo(); } bar()"; TestHelpers.TestAtBreak(script, info => { // The line numbers here may mislead - the positions are, as would be expected, // at the position before the currently executing line, not the line after. // Remember that Esprima (and hence Jint) line numbers are 1-based, not 0-based. Assert.Collection(info.CallStack, // "debugger;" frame => Assert.Equal(Position.From(4, 0), frame.Location.Start), // "foo();" frame => Assert.Equal(Position.From(9, 0), frame.Location.Start), // "bar();" frame => Assert.Equal(Position.From(12, 0), frame.Location.Start) ); }); } [Fact] public void IncludesFunctionLocations() { var script = @" function foo() { debugger; } function bar() { foo(); } bar()"; TestHelpers.TestAtBreak(script, info => { // Remember that Esprima (and hence Jint) line numbers are 1-based, not 0-based. Assert.Collection(info.CallStack, // function foo() frame => Assert.Equal(Position.From(2, 0), frame.FunctionLocation?.Start), // function bar() frame => Assert.Equal(Position.From(7, 0), frame.FunctionLocation?.Start), // global - no function location frame => Assert.Equal(null, frame.FunctionLocation?.Start) ); }); } [Fact] public void HasReturnValue() { string script = @" function foo() { return 'result'; } foo();"; var engine = new Engine(options => options.DebugMode().InitialStepMode(StepMode.Into)); bool atReturn = false; bool didCheckReturn = false; engine.Debugger.Step += (sender, info) => { if (atReturn) { Assert.NotNull(info.CurrentCallFrame.ReturnValue); Assert.Equal("result", info.CurrentCallFrame.ReturnValue.AsString()); didCheckReturn = true; atReturn = false; } if (info.CurrentNode is ReturnStatement) { // Step one further, and we should have the return value atReturn = true; } return StepMode.Into; }; engine.Execute(script); Assert.True(didCheckReturn); } [Fact] public void HasThis() { string script = @" function Thing(name) { this.name = name; } Thing.prototype.test = function() { debugger; } var car = new Thing('car'); car.test(); "; TestHelpers.TestAtBreak(script, (engine, info) => { Assert.Collection(info.CallStack, frame => Assert.Equal(engine.Realm.GlobalObject.Get("car"), frame.This), frame => Assert.Equal(engine.Realm.GlobalObject, frame.This) ); }); } [Fact] public void NamesRegularFunction() { string script = @" function regularFunction() { debugger; } regularFunction();"; TestHelpers.TestAtBreak(script, info => { Assert.Equal("regularFunction", info.CurrentCallFrame.FunctionName); }); } [Fact] public void NamesFunctionExpression() { string script = @" const functionExpression = function() { debugger; } functionExpression()"; TestHelpers.TestAtBreak(script, info => { Assert.Equal("functionExpression", info.CurrentCallFrame.FunctionName); }); } [Fact] public void NamesNamedFunctionExpression() { string script = @" const functionExpression = function namedFunction() { debugger; } functionExpression()"; TestHelpers.TestAtBreak(script, info => { Assert.Equal("namedFunction", info.CurrentCallFrame.FunctionName); }); } [Fact] public void NamesArrowFunction() { string script = @" const arrowFunction = () => { debugger; } arrowFunction()"; TestHelpers.TestAtBreak(script, info => { Assert.Equal("arrowFunction", info.CurrentCallFrame.FunctionName); }); } [Fact] public void NamesNewFunction() { string script = @" const newFunction = new Function('debugger;'); newFunction()"; TestHelpers.TestAtBreak(script, info => { // Ideally, this should be "(anonymous)", but FunctionConstructor sets the "anonymous" name. Assert.Equal("anonymous", info.CurrentCallFrame.FunctionName); }); } [Fact] public void NamesMemberFunction() { string script = @" const obj = { memberFunction() { debugger; } }; obj.memberFunction()"; TestHelpers.TestAtBreak(script, info => { Assert.Equal("memberFunction", info.CurrentCallFrame.FunctionName); }); } [Fact] public void NamesAnonymousFunction() { string script = @" (function() { debugger; }());"; TestHelpers.TestAtBreak(script, info => { Assert.Equal("(anonymous)", info.CurrentCallFrame.FunctionName); }); } [Fact] public void NamesGetAccessor() { string script = @" const obj = { get accessor() { debugger; return 'test'; } }; const x = obj.accessor;"; TestHelpers.TestAtBreak(script, info => { Assert.Equal("get accessor", info.CurrentCallFrame.FunctionName); }); } [Fact] public void NamesSetAccessor() { string script = @" const obj = { set accessor(value) { debugger; this.value = value; } }; obj.accessor = 42;"; TestHelpers.TestAtBreak(script, info => { Assert.Equal("set accessor", info.CurrentCallFrame.FunctionName); }); } }