ProxyTests.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610
  1. using Jint.Native.Error;
  2. using Jint.Runtime;
  3. namespace Jint.Tests.Runtime;
  4. public class ProxyTests
  5. {
  6. private readonly Engine _engine;
  7. public ProxyTests()
  8. {
  9. _engine = new Engine()
  10. .SetValue("equal", new Action<object, object>(Assert.Equal));
  11. }
  12. [Fact]
  13. public void ProxyCanBeRevokedWithoutContext()
  14. {
  15. _engine.Execute(@"
  16. var revocable = Proxy.revocable({}, {});
  17. var revoke = revocable.revoke;
  18. revoke.call(null);
  19. ");
  20. }
  21. [Fact]
  22. public void ProxyToStringUseTarget()
  23. {
  24. _engine.Execute(@"
  25. const targetWithToString = {toString: () => 'target'}
  26. ");
  27. Assert.Equal("target", _engine.Evaluate("new Proxy(targetWithToString, {}).toString()").AsString());
  28. Assert.Equal("target", _engine.Evaluate("`${new Proxy(targetWithToString, {})}`").AsString());
  29. }
  30. [Fact]
  31. public void ProxyToStringUseHandler()
  32. {
  33. _engine.Execute(@"
  34. const handler = { get: (target, prop, receiver) => prop === 'toString' ? () => 'handler' : Reflect.get(target, prop, receiver) }
  35. const targetWithToString = {toString: () => 'target'}
  36. ");
  37. Assert.Equal("handler", _engine.Evaluate("new Proxy({}, handler).toString()").AsString());
  38. Assert.Equal("handler", _engine.Evaluate("new Proxy(targetWithToString, handler).toString()").AsString());
  39. Assert.Equal("handler", _engine.Evaluate("`${new Proxy({}, handler)}`").AsString());
  40. Assert.Equal("handler", _engine.Evaluate("`${new Proxy(targetWithToString, handler)}`").AsString());
  41. }
  42. [Fact]
  43. public void ToPropertyDescriptor()
  44. {
  45. const string Script = @"
  46. var get = [];
  47. var p = new Proxy({
  48. enumerable: true, configurable: true, value: true,
  49. writable: true, get: Function(), set: Function()
  50. }, { get: function(o, k) { get.push(k); return o[k]; }});
  51. try {
  52. // This will throw, since it will have true for both ""get"" and ""value"",
  53. // but not before performing a Get on every property.
  54. Object.defineProperty({}, ""foo"", p);
  55. } catch(e) {
  56. return get + '';
  57. }
  58. return 'did not fail as expected'";
  59. Assert.Equal("enumerable,configurable,value,writable,get,set", _engine.Evaluate(Script));
  60. }
  61. [Fact]
  62. public void DefineProperties()
  63. {
  64. const string Script = @"
  65. // Object.defineProperties -> Get -> [[Get]]
  66. var get = [];
  67. var p = new Proxy({foo:{}, bar:{}}, { get: function(o, k) { get.push(k); return o[k]; }});
  68. Object.defineProperties({}, p);
  69. return get + '';";
  70. Assert.Equal("foo,bar", _engine.Evaluate(Script));
  71. }
  72. [Fact]
  73. public void GetHandlerInstancesOfProxies()
  74. {
  75. const string Script = @"
  76. var proxied = { };
  77. var proxy = Object.create(new Proxy(proxied, {
  78. get: function (t, k, r) {
  79. equal(t, proxied); equal('foo', k); equal(proxy, r);
  80. return t === proxied && k === 'foo' && r === proxy && 5;
  81. }
  82. }));
  83. equal(5, proxy.foo);";
  84. _engine.Execute(Script);
  85. }
  86. [Fact]
  87. public void SetHandlerInvariants()
  88. {
  89. const string Script = @"
  90. var passed = false;
  91. var proxied = { };
  92. var proxy = new Proxy(proxied, {
  93. get: function () {
  94. passed = true;
  95. return 4;
  96. }
  97. });
  98. // The value reported for a property must be the same as the value of the corresponding
  99. // target object property if the target object property is a non-writable,
  100. // non-configurable own data property.
  101. Object.defineProperty(proxied, ""foo"", { value: 5, enumerable: true });
  102. try {
  103. proxy.foo;
  104. return false;
  105. }
  106. catch(e) {}
  107. // The value reported for a property must be undefined if the corresponding target
  108. // object property is a non-configurable own accessor property that has undefined
  109. // as its [[Get]] attribute.
  110. Object.defineProperty(proxied, ""bar"",
  111. { set: function(){}, enumerable: true });
  112. try {
  113. proxy.bar;
  114. return false;
  115. }
  116. catch(e) {}
  117. return passed;";
  118. Assert.True(_engine.Evaluate(Script).AsBoolean());
  119. }
  120. [Fact]
  121. public void ApplyHandlerInvariant()
  122. {
  123. const string Script = @"
  124. var passed = false;
  125. new Proxy(function(){}, {
  126. apply: function () { passed = true; }
  127. })();
  128. // A Proxy exotic object only has a [[Call]] internal method if the
  129. // initial value of its [[ProxyTarget]] internal slot is an object
  130. // that has a [[Call]] internal method.
  131. try {
  132. new Proxy({}, {
  133. apply: function () {}
  134. })();
  135. return false;
  136. } catch(e) {}
  137. return passed;";
  138. Assert.True(_engine.Evaluate(Script).AsBoolean());
  139. }
  140. [Fact]
  141. public void ConstructHandlerInvariant()
  142. {
  143. const string Script = @"
  144. var passed = false;
  145. new Proxy({},{});
  146. // A Proxy exotic object only has a [[Construct]] internal method if the
  147. // initial value of its [[ProxyTarget]] internal slot is an object
  148. // that has a [[Construct]] internal method.
  149. try {
  150. new new Proxy({}, {
  151. construct: function (t, args) {
  152. return {};
  153. }
  154. })();
  155. return false;
  156. } catch(e) {}
  157. // The result of [[Construct]] must be an Object.
  158. try {
  159. new new Proxy(function(){}, {
  160. construct: function (t, args) {
  161. passed = true;
  162. return 5;
  163. }
  164. })();
  165. return false;
  166. } catch(e) {}
  167. return passed;";
  168. Assert.True(_engine.Evaluate(Script).AsBoolean());
  169. }
  170. [Fact]
  171. public void ProxyHandlerGetDataPropertyShouldNotUseReferenceEquals()
  172. {
  173. // There are two JsString which should be treat as same value,
  174. // but they are not ReferenceEquals.
  175. _engine.Execute("""
  176. let o = Object.defineProperty({}, 'value', {
  177. configurable: false,
  178. value: 'in',
  179. });
  180. const handler = {
  181. get(target, property, receiver) {
  182. return 'Jint'.substring(1,3);
  183. }
  184. };
  185. let p = new Proxy(o, handler);
  186. let pv = p.value;
  187. """);
  188. }
  189. [Fact]
  190. public void ProxyHandlerGetDataPropertyShouldNotCheckClrType()
  191. {
  192. // There are a JsString and a ConcatenatedString which should be treat as same value,
  193. // but they are different CLR Type.
  194. _engine.Execute("""
  195. let o = Object.defineProperty({}, 'value', {
  196. configurable: false,
  197. value: 'Jint',
  198. });
  199. const handler = {
  200. get(target, property, receiver) {
  201. return 'Ji'.concat('nt');
  202. }
  203. };
  204. let p = new Proxy(o, handler);
  205. let pv = p.value;
  206. """);
  207. }
  208. class TestClass
  209. {
  210. public static readonly TestClass Instance = new TestClass();
  211. public string StringValue => "StringValue";
  212. public int IntValue => 42424242; // avoid small numbers cache
  213. public TestClass ObjectWrapper => Instance;
  214. public List<int> IntList { get; } = [1, 2, 3];
  215. private int x = 1;
  216. public int PropertySideEffect => x++;
  217. public string Name => "My Name is Test";
  218. public void SayHello()
  219. {
  220. }
  221. public int Add(int a, int b)
  222. {
  223. return a + b;
  224. }
  225. }
  226. [Fact]
  227. public void ProxyClrPropertyPrimitiveString()
  228. {
  229. _engine.SetValue("testClass", TestClass.Instance);
  230. var result = _engine.Evaluate("""
  231. const handler = {
  232. get(target, property, receiver) {
  233. return Reflect.get(target, property, receiver);
  234. }
  235. };
  236. const p = new Proxy(testClass, handler);
  237. return p.StringValue;
  238. """);
  239. Assert.Equal(TestClass.Instance.StringValue, result.AsString());
  240. }
  241. [Fact]
  242. public void ProxyClrPropertyPrimitiveInt()
  243. {
  244. _engine.SetValue("testClass", TestClass.Instance);
  245. var result = _engine.Evaluate("""
  246. const handler = {
  247. get(target, property, receiver) {
  248. return Reflect.get(target, property, receiver);
  249. }
  250. };
  251. const p = new Proxy(testClass, handler);
  252. return p.IntValue;
  253. """);
  254. Assert.Equal(TestClass.Instance.IntValue, result.AsInteger());
  255. }
  256. [Fact]
  257. public void ProxyClrPropertyObjectWrapper()
  258. {
  259. _engine.SetValue("testClass", TestClass.Instance);
  260. var result = _engine.Evaluate("""
  261. const handler = {
  262. get(target, property, receiver) {
  263. return Reflect.get(target, property, receiver);
  264. }
  265. };
  266. const p = new Proxy(testClass, handler);
  267. return p.ObjectWrapper;
  268. """);
  269. }
  270. private static ErrorPrototype TypeErrorPrototype(Engine engine)
  271. => engine.Realm.Intrinsics.TypeError.PrototypeObject;
  272. private static void AssertJsTypeError(Engine engine, JavaScriptException ex, string msg)
  273. {
  274. Assert.Same(TypeErrorPrototype(engine), ex.Error.AsObject().Prototype);
  275. Assert.Equal(msg, ex.Message);
  276. }
  277. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get#invariants
  278. // The value reported for a property must be the same as
  279. // the value ofthe corresponding target object property,
  280. // if the target object property is
  281. // a non-writable, non-configurable own data property.
  282. [Fact]
  283. public void ProxyHandlerGetInvariantsDataPropertyReturnsDifferentValue()
  284. {
  285. _engine.Execute("""
  286. let o = Object.defineProperty({}, 'value', {
  287. writable: false,
  288. configurable: false,
  289. value: 42,
  290. });
  291. const handler = {
  292. get(target, property, receiver) {
  293. return 32;
  294. }
  295. };
  296. let p = new Proxy(o, handler);
  297. """);
  298. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("p.value"));
  299. AssertJsTypeError(_engine, ex, "'get' on proxy: property 'value' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '42' but got '32')");
  300. }
  301. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get#invariants
  302. // The value reported for a property must be undefined,
  303. // if the corresponding target object property is
  304. // a non-configurable own accessor property
  305. // that has undefined as its [[Get]] attribute.
  306. [Fact]
  307. public void ProxyHandlerGetInvariantsAccessorPropertyWithoutGetButReturnsValue()
  308. {
  309. _engine.Execute("""
  310. let o = Object.defineProperty({}, 'value', {
  311. configurable: false,
  312. set() {},
  313. });
  314. const handler = {
  315. get(target, property, receiver) {
  316. return 32;
  317. }
  318. };
  319. let p = new Proxy(o, handler);
  320. """);
  321. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("p.value"));
  322. AssertJsTypeError(_engine, ex, "'get' on proxy: property 'value' is a non-configurable accessor property on the proxy target and does not have a getter function, but the trap did not return 'undefined' (got '32')");
  323. }
  324. private const string ScriptProxyHandlerSetInvariantsDataPropertyImmutable = """
  325. let o = Object.defineProperty({}, 'value', {
  326. writable: false,
  327. configurable: false,
  328. value: 42,
  329. });
  330. const handler = {
  331. set(target, property, value, receiver) {
  332. return true;
  333. }
  334. };
  335. let p = new Proxy(o, handler);
  336. """;
  337. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set#invariants
  338. // Cannot change the value of a property to be different from
  339. // the value of the corresponding target object property,
  340. // if the corresponding target object property is
  341. // a non-writable, non-configurable data property.
  342. [Fact]
  343. public void ProxyHandlerSetInvariantsDataPropertyImmutableChangeValue()
  344. {
  345. _engine.Execute(ScriptProxyHandlerSetInvariantsDataPropertyImmutable);
  346. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("p.value = 32"));
  347. AssertJsTypeError(_engine, ex, "'set' on proxy: trap returned truish for property 'value' which exists in the proxy target as a non-configurable and non-writable data property with a different value");
  348. }
  349. [Fact]
  350. public void ProxyHandlerSetInvariantsDataPropertyImmutableSetSameValue()
  351. {
  352. _engine.Execute(ScriptProxyHandlerSetInvariantsDataPropertyImmutable);
  353. _engine.Evaluate("p.value = 42");
  354. }
  355. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set#invariants
  356. // Cannot set the value of a property,
  357. // if the corresponding target object property is
  358. // a non-configurable accessor property
  359. // that has undefined as its [[Set]] attribute.
  360. [Fact]
  361. public void ProxyHandlerSetInvariantsAccessorPropertyWithoutSetChange()
  362. {
  363. _engine.Execute("""
  364. let o = Object.defineProperty({}, 'value', {
  365. configurable: false,
  366. get() { return 42; },
  367. });
  368. const handler = {
  369. set(target, property, value, receiver) {
  370. return true;
  371. }
  372. };
  373. let p = new Proxy(o, handler);
  374. """);
  375. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("p.value = 42"));
  376. AssertJsTypeError(_engine, ex, "'set' on proxy: trap returned truish for property 'value' which exists in the proxy target as a non-configurable and non-writable accessor property without a setter");
  377. }
  378. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set#invariants
  379. // In strict mode, a false return value from the set() handler
  380. // will throw a TypeError exception.
  381. [Fact]
  382. public void ProxyHandlerSetInvariantsReturnsFalseInStrictMode()
  383. {
  384. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("""
  385. 'use strict';
  386. let p = new Proxy({}, { set: () => false });
  387. p.value = 42;
  388. """));
  389. // V8: "'set' on proxy: trap returned falsish for property 'value'",
  390. AssertJsTypeError(_engine, ex, "Cannot assign to read only property 'value' of [object Object]");
  391. }
  392. [Fact]
  393. public void ProxyHandlerSetInvariantsReturnsFalseInNonStrictMode()
  394. {
  395. _engine.Evaluate("""
  396. // 'use strict';
  397. let p = new Proxy({}, { set: () => false });
  398. p.value = 42;
  399. """);
  400. }
  401. [Fact]
  402. public void ClrPropertySideEffect()
  403. {
  404. _engine.SetValue("testClass", TestClass.Instance);
  405. _engine.Execute("""
  406. const handler = {
  407. get(target, property, receiver) {
  408. return 2;
  409. }
  410. };
  411. const p = new Proxy(testClass, handler);
  412. """);
  413. Assert.Equal(1, TestClass.Instance.PropertySideEffect); // first call to PropertySideEffect
  414. Assert.Equal(2, _engine.Evaluate("p.PropertySideEffect").AsInteger()); // no call to PropertySideEffect
  415. Assert.Equal(2, TestClass.Instance.PropertySideEffect); // second call to PropertySideEffect
  416. }
  417. [Fact]
  418. public void ToObjectReturnsProxiedToObject()
  419. {
  420. _engine
  421. .SetValue("T", new TestClass())
  422. .Execute("""
  423. const handler = {
  424. get(target, property, receiver) {
  425. if (!target[property]) {
  426. return (...args) => "Not available";
  427. }
  428. // return Reflect.get(target, property, receiver);
  429. return Reflect.get(...arguments);
  430. }
  431. };
  432. const p = new Proxy(T, handler);
  433. const name = p.Name; // works
  434. const s = p.GetX(); // works because method does NOT exist on clr object
  435. p.SayHello(); // throws System.Reflection.TargetException: 'Object does not match target type.'
  436. const t = p.Add(5,3); // throws System.Reflection.TargetException: 'Object does not match target type.'
  437. """);
  438. }
  439. [Fact]
  440. public void ProxyIterateClrList()
  441. {
  442. var res = _engine
  443. .SetValue("obj", TestClass.Instance)
  444. .Evaluate("""
  445. //const obj = {
  446. // IntList: [1, 2, 3]
  447. //};
  448. const objProxy = new Proxy(obj, {
  449. get(target, prop, receiver) {
  450. const targetValue = Reflect.get(target, prop, receiver);
  451. if (prop == 'IntList') {
  452. return new Proxy(targetValue, {
  453. get(target, prop, receiver) {
  454. return Reflect.get(target, prop, receiver);
  455. }
  456. });
  457. }
  458. return targetValue;
  459. }
  460. });
  461. const arr = []
  462. for (const item of objProxy.IntList)
  463. {
  464. arr.push(item);
  465. }
  466. arr.push(objProxy.IntList.length)
  467. return arr;
  468. """);
  469. Assert.Equal([1, 2, 3, 3], res.AsArray());
  470. }
  471. [Fact]
  472. public void ProxyClrObjectMethod()
  473. {
  474. var res = _engine
  475. .SetValue("T", new TestClass())
  476. .Evaluate("""
  477. const handler = {
  478. get(target, property, receiver) {
  479. if (property == "Add") {
  480. return function(...args) { return 42};
  481. }
  482. return Reflect.get(...arguments);
  483. }
  484. };
  485. const p = new Proxy(T, handler);
  486. p.Add(5,3); // throws 'get' on proxy: property 'Add' is a read-only and non-configurable data property
  487. // on the proxy target but the proxy did not return its actual value
  488. // (expected 'function Jint.Tests.Runtime.ProxyTests+TestClass.Add() { [native code] }' but got 'function () { [native code] }')
  489. """);
  490. Assert.Equal(42, res.AsInteger());
  491. }
  492. [Fact]
  493. public void ProxyClrObjectMethodWithDelegate()
  494. {
  495. var res = _engine
  496. .SetValue("T", new TestClass())
  497. .Evaluate("""
  498. const handler = {
  499. get(target, property, receiver) {
  500. if (property == "Add") {
  501. return (...args) => 42;
  502. }
  503. return Reflect.get(...arguments);
  504. }
  505. };
  506. const p = new Proxy(T, handler);
  507. p.Add(5,3); // throws 'get' on proxy: property 'Add' is a read-only and non-configurable data property
  508. // on the proxy target but the proxy did not return its actual value
  509. // (expected 'function Jint.Tests.Runtime.ProxyTests+TestClass.Add() { [native code] }' but got 'function () { [native code] }')
  510. """);
  511. Assert.Equal(42, res.AsInteger());
  512. }
  513. [Fact]
  514. public void ProxyClrObjectWithTmpObjectMethod()
  515. {
  516. var res = _engine
  517. .SetValue("T", new TestClass())
  518. .Evaluate("""
  519. const handler = {
  520. get(target, property, receiver) {
  521. if (property == "Add") {
  522. return (...args) => target.target[property](...args) + 34;
  523. }
  524. if (typeof target.target[property] === "function")
  525. return (...args) => target.target[property](...args);
  526. return Reflect.get(target.target, property, receiver)
  527. }
  528. };
  529. const tmpObj = { target: T };
  530. const p = new Proxy(tmpObj, handler);
  531. const name = p.Name;
  532. p.SayHello();
  533. const res = p.Add(5,3); // works now
  534. name + " " + res
  535. """);
  536. Assert.Equal("My Name is Test 42", res.AsString());
  537. }
  538. }