ProxyTests.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485
  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. private int x = 1;
  215. public int PropertySideEffect => x++;
  216. public string Name => "My Name is Test";
  217. public void SayHello()
  218. {
  219. }
  220. public int Add(int a, int b)
  221. {
  222. return a + b;
  223. }
  224. }
  225. [Fact]
  226. public void ProxyClrPropertyPrimitiveString()
  227. {
  228. _engine.SetValue("testClass", TestClass.Instance);
  229. var result = _engine.Evaluate("""
  230. const handler = {
  231. get(target, property, receiver) {
  232. return Reflect.get(target, property, receiver);
  233. }
  234. };
  235. const p = new Proxy(testClass, handler);
  236. return p.StringValue;
  237. """);
  238. Assert.Equal(TestClass.Instance.StringValue, result.AsString());
  239. }
  240. [Fact]
  241. public void ProxyClrPropertyPrimitiveInt()
  242. {
  243. _engine.SetValue("testClass", TestClass.Instance);
  244. var result = _engine.Evaluate("""
  245. const handler = {
  246. get(target, property, receiver) {
  247. return Reflect.get(target, property, receiver);
  248. }
  249. };
  250. const p = new Proxy(testClass, handler);
  251. return p.IntValue;
  252. """);
  253. Assert.Equal(TestClass.Instance.IntValue, result.AsInteger());
  254. }
  255. [Fact]
  256. public void ProxyClrPropertyObjectWrapper()
  257. {
  258. _engine.SetValue("testClass", TestClass.Instance);
  259. var result = _engine.Evaluate("""
  260. const handler = {
  261. get(target, property, receiver) {
  262. return Reflect.get(target, property, receiver);
  263. }
  264. };
  265. const p = new Proxy(testClass, handler);
  266. return p.ObjectWrapper;
  267. """);
  268. }
  269. private static ErrorPrototype TypeErrorPrototype(Engine engine)
  270. => engine.Realm.Intrinsics.TypeError.PrototypeObject;
  271. private static void AssertJsTypeError(Engine engine, JavaScriptException ex, string msg)
  272. {
  273. Assert.Same(TypeErrorPrototype(engine), ex.Error.AsObject().Prototype);
  274. Assert.Equal(msg, ex.Message);
  275. }
  276. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get#invariants
  277. // The value reported for a property must be the same as
  278. // the value ofthe corresponding target object property,
  279. // if the target object property is
  280. // a non-writable, non-configurable own data property.
  281. [Fact]
  282. public void ProxyHandlerGetInvariantsDataPropertyReturnsDifferentValue()
  283. {
  284. _engine.Execute("""
  285. let o = Object.defineProperty({}, 'value', {
  286. writable: false,
  287. configurable: false,
  288. value: 42,
  289. });
  290. const handler = {
  291. get(target, property, receiver) {
  292. return 32;
  293. }
  294. };
  295. let p = new Proxy(o, handler);
  296. """);
  297. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("p.value"));
  298. 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')");
  299. }
  300. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/get#invariants
  301. // The value reported for a property must be undefined,
  302. // if the corresponding target object property is
  303. // a non-configurable own accessor property
  304. // that has undefined as its [[Get]] attribute.
  305. [Fact]
  306. public void ProxyHandlerGetInvariantsAccessorPropertyWithoutGetButReturnsValue()
  307. {
  308. _engine.Execute("""
  309. let o = Object.defineProperty({}, 'value', {
  310. configurable: false,
  311. set() {},
  312. });
  313. const handler = {
  314. get(target, property, receiver) {
  315. return 32;
  316. }
  317. };
  318. let p = new Proxy(o, handler);
  319. """);
  320. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("p.value"));
  321. 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')");
  322. }
  323. private const string ScriptProxyHandlerSetInvariantsDataPropertyImmutable = """
  324. let o = Object.defineProperty({}, 'value', {
  325. writable: false,
  326. configurable: false,
  327. value: 42,
  328. });
  329. const handler = {
  330. set(target, property, value, receiver) {
  331. return true;
  332. }
  333. };
  334. let p = new Proxy(o, handler);
  335. """;
  336. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set#invariants
  337. // Cannot change the value of a property to be different from
  338. // the value of the corresponding target object property,
  339. // if the corresponding target object property is
  340. // a non-writable, non-configurable data property.
  341. [Fact]
  342. public void ProxyHandlerSetInvariantsDataPropertyImmutableChangeValue()
  343. {
  344. _engine.Execute(ScriptProxyHandlerSetInvariantsDataPropertyImmutable);
  345. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("p.value = 32"));
  346. 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");
  347. }
  348. [Fact]
  349. public void ProxyHandlerSetInvariantsDataPropertyImmutableSetSameValue()
  350. {
  351. _engine.Execute(ScriptProxyHandlerSetInvariantsDataPropertyImmutable);
  352. _engine.Evaluate("p.value = 42");
  353. }
  354. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set#invariants
  355. // Cannot set the value of a property,
  356. // if the corresponding target object property is
  357. // a non-configurable accessor property
  358. // that has undefined as its [[Set]] attribute.
  359. [Fact]
  360. public void ProxyHandlerSetInvariantsAccessorPropertyWithoutSetChange()
  361. {
  362. _engine.Execute("""
  363. let o = Object.defineProperty({}, 'value', {
  364. configurable: false,
  365. get() { return 42; },
  366. });
  367. const handler = {
  368. set(target, property, value, receiver) {
  369. return true;
  370. }
  371. };
  372. let p = new Proxy(o, handler);
  373. """);
  374. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("p.value = 42"));
  375. 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");
  376. }
  377. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/Proxy/set#invariants
  378. // In strict mode, a false return value from the set() handler
  379. // will throw a TypeError exception.
  380. [Fact]
  381. public void ProxyHandlerSetInvariantsReturnsFalseInStrictMode()
  382. {
  383. var ex = Assert.Throws<JavaScriptException>(() => _engine.Evaluate("""
  384. 'use strict';
  385. let p = new Proxy({}, { set: () => false });
  386. p.value = 42;
  387. """));
  388. // V8: "'set' on proxy: trap returned falsish for property 'value'",
  389. AssertJsTypeError(_engine, ex, "Cannot assign to read only property 'value' of [object Object]");
  390. }
  391. [Fact]
  392. public void ProxyHandlerSetInvariantsReturnsFalseInNonStrictMode()
  393. {
  394. _engine.Evaluate("""
  395. // 'use strict';
  396. let p = new Proxy({}, { set: () => false });
  397. p.value = 42;
  398. """);
  399. }
  400. [Fact]
  401. public void ClrPropertySideEffect()
  402. {
  403. _engine.SetValue("testClass", TestClass.Instance);
  404. _engine.Execute("""
  405. const handler = {
  406. get(target, property, receiver) {
  407. return 2;
  408. }
  409. };
  410. const p = new Proxy(testClass, handler);
  411. """);
  412. Assert.Equal(1, TestClass.Instance.PropertySideEffect); // first call to PropertySideEffect
  413. Assert.Equal(2, _engine.Evaluate("p.PropertySideEffect").AsInteger()); // no call to PropertySideEffect
  414. Assert.Equal(2, TestClass.Instance.PropertySideEffect); // second call to PropertySideEffect
  415. }
  416. [Fact]
  417. public void ToObjectReturnsProxiedToObject()
  418. {
  419. _engine
  420. .SetValue("T", new TestClass())
  421. .Execute("""
  422. const handler = {
  423. get(target, property, receiver) {
  424. if (!target[property]) {
  425. return (...args) => "Not available";
  426. }
  427. // return Reflect.get(target, property, receiver);
  428. return Reflect.get(...arguments);
  429. }
  430. };
  431. const p = new Proxy(T, handler);
  432. const name = p.Name; // works
  433. const s = p.GetX(); // works because method does NOT exist on clr object
  434. p.SayHello(); // throws System.Reflection.TargetException: 'Object does not match target type.'
  435. const t = p.Add(5,3); // throws System.Reflection.TargetException: 'Object does not match target type.'
  436. """);
  437. }
  438. }