diff --git a/apps/test-suite/TestModules.ts b/apps/test-suite/TestModules.ts index 8730775b4e1697..e0342e8f031e4d 100644 --- a/apps/test-suite/TestModules.ts +++ b/apps/test-suite/TestModules.ts @@ -40,6 +40,17 @@ export function getTestModules() { const modules: Module[] = [ // Sanity require('./tests/Basic'), + require('./tests/JSDestructuring'), + require('./tests/JSAsync'), + require('./tests/JSAsyncGenerator'), + require('./tests/JSBlockScoping'), + require('./tests/JSPrivateMethods'), + require('./tests/JSPrivateProperties'), + require('./tests/JSReactJSX'), + require('./tests/JSNamedGroupsRegexes'), + require('./tests/JSUnicodeRegexes'), + require('./tests/JSNullishCoalescing'), + require('./tests/JSOptionalChaining'), ]; // Expo core modules should run everywhere diff --git a/apps/test-suite/tests/JSAsync.js b/apps/test-suite/tests/JSAsync.js new file mode 100644 index 00000000000000..7453bf22107d2a --- /dev/null +++ b/apps/test-suite/tests/JSAsync.js @@ -0,0 +1,789 @@ +/* eslint-disable */ +'use strict'; + +// Comprehensive runtime compliance tests for async/await. +// Validates that async functions work correctly at runtime in the Hermes engine. +// Cases sourced from @babel/plugin-transform-async-to-generator exec.js fixtures +// and supplemented with edge cases and stack trace diagnostics. +// Async generators, for-await-of, and for-of are in JSAsyncGenerator.js. + +export const name = 'JS Async'; + +export function test({ describe, it, xit, expect }) { + describe('JS Async', () => { + describe('basic async/await', () => { + it('async function returns a promise', () => { + async function foo() { + return 42; + } + const result = foo(); + expect(result instanceof Promise).toBe(true); + }); + + it('await resolves promise value', async () => { + async function foo() { + return await Promise.resolve(42); + } + expect(await foo()).toBe(42); + }); + + it('async function returning non-promise wraps in promise', async () => { + async function foo() { + return 'hello'; + } + expect(await foo()).toBe('hello'); + }); + + it('await non-promise value passes through', async () => { + async function foo() { + const x = await 42; + return x; + } + expect(await foo()).toBe(42); + }); + + it('sequential awaits', async () => { + async function foo() { + const a = await Promise.resolve(1); + const b = await Promise.resolve(2); + const c = await Promise.resolve(3); + return a + b + c; + } + expect(await foo()).toBe(6); + }); + + it('await in expression position', async () => { + async function foo() { + return (await Promise.resolve(10)) + (await Promise.resolve(32)); + } + expect(await foo()).toBe(42); + }); + }); + + describe('error handling', () => { + it('rejected promise throws on await', async () => { + async function foo() { + try { + await Promise.reject(new Error('fail')); + return 'no error'; + } catch (e) { + return e.message; + } + } + expect(await foo()).toBe('fail'); + }); + + it('throw in async function rejects the promise', async () => { + async function foo() { + throw new Error('oops'); + } + try { + await foo(); + expect(true).toBe(false); // should not reach + } catch (e) { + expect(e.message).toBe('oops'); + } + }); + + it('try-catch-finally with await', async () => { + const log = []; + async function foo() { + try { + log.push('try'); + await Promise.reject(new Error('err')); + } catch (e) { + log.push('catch'); + } finally { + log.push('finally'); + } + } + await foo(); + expect(log).toEqual(['try', 'catch', 'finally']); + }); + + it('nested try-catch with async', async () => { + async function inner() { + throw new Error('inner'); + } + async function outer() { + try { + await inner(); + } catch (e) { + return 'caught: ' + e.message; + } + } + expect(await outer()).toBe('caught: inner'); + }); + + it('error after await does not lose context', async () => { + async function foo() { + await Promise.resolve(); + throw new Error('after await'); + } + try { + await foo(); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('after await'); + } + }); + }); + + describe('async arrow functions', () => { + it('basic async arrow', async () => { + const foo = async () => 42; + expect(await foo()).toBe(42); + }); + + it('async arrow with await', async () => { + const foo = async (x) => { + const result = await Promise.resolve(x * 2); + return result; + }; + expect(await foo(21)).toBe(42); + }); + + it('async arrow preserves this from enclosing scope', async () => { + const obj = { + val: 42, + getVal: function () { + const fn = async () => { + return await Promise.resolve(this.val); + }; + return fn(); + }, + }; + expect(await obj.getVal()).toBe(42); + }); + + // From: regression/fn-name/exec.js + it('async arrow preserves name property', () => { + var concat = async (...args) => { + var x = args[0]; + var y = args[1]; + }; + expect(concat.name).toBe('concat'); + }); + + // From: regression/test262-fn-length/exec.js + it('async function preserves length property', () => { + var a = async (b) => {}; + expect(a.length).toBe(1); + }); + }); + + describe('async methods', () => { + it('async class method', async () => { + class Foo { + async bar() { + return await Promise.resolve(42); + } + } + expect(await new Foo().bar()).toBe(42); + }); + + it('async static method', async () => { + class Foo { + static async bar() { + return await Promise.resolve(42); + } + } + expect(await Foo.bar()).toBe(42); + }); + + it('async object method', async () => { + const obj = { + async foo() { + return await Promise.resolve(42); + }, + }; + expect(await obj.foo()).toBe(42); + }); + + it('async method preserves this', async () => { + class Foo { + constructor(val) { + this.val = val; + } + async getVal() { + return await Promise.resolve(this.val); + } + } + expect(await new Foo(42).getVal()).toBe(42); + }); + }); + + describe('complex parameters', () => { + // From: async-to-generator/async-complex-params/exec.js + it('async method with destructured parameter that throws', async () => { + let caught = false; + class Foo { + async bar(a, { b }) {} + } + try { + await new Foo().bar(); + } catch (e) { + caught = true; + } + expect(caught).toBe(true); + }); + + it('async method with destructured parameter', async () => { + class Foo { + async bar(a, { b }) { + return [a, b]; + } + } + expect(await new Foo().bar(1, { b: 2 })).toEqual([1, 2]); + }); + + it('async method with default parameter', async () => { + async function foo( + a, + b = (() => { + throw new Error('required'); + })() + ) { + return [a, b]; + } + expect(await foo(1, 2)).toEqual([1, 2]); + }); + + // From: regression/4943/exec.js + it('async function with mandatory default throws on missing param', async () => { + function mandatory(name) { + throw new Error('Missing: ' + name); + } + async function foo({ a, b = mandatory('b') } = {}) { + return b; + } + try { + await foo(); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Missing: b'); + } + }); + + it('async method with rest parameter', async () => { + class Foo { + async bar(a, ...b) { + return [a, b]; + } + } + expect(await new Foo().bar(1, 2, 3)).toEqual([1, [2, 3]]); + }); + + it('async method with super access', async () => { + class Base { + get val() { + return 'base'; + } + } + class Child extends Base { + async foo(a, { b }) { + return [a, b, super.val]; + } + } + expect(await new Child().foo(1, { b: 2 })).toEqual([1, 2, 'base']); + }); + }); + + describe('execution order', () => { + it('code after await runs asynchronously', async () => { + const log = []; + async function foo() { + log.push(1); + await null; + log.push(2); + } + const p = foo(); + log.push(3); + await p; + expect(log).toEqual([1, 3, 2]); + }); + + it('multiple async functions interleave correctly', async () => { + const log = []; + async function a() { + log.push('a1'); + await null; + log.push('a2'); + } + async function b() { + log.push('b1'); + await null; + log.push('b2'); + } + await Promise.all([a(), b()]); + expect(log[0]).toBe('a1'); + expect(log[1]).toBe('b1'); + // a2 and b2 ordering depends on microtask queue + expect(log.indexOf('a2') >= 0).toBe(true); + expect(log.indexOf('b2') >= 0).toBe(true); + }); + + // From: async-to-generator/double-await/exec.js + it('double await adds extra microtask delay', async () => { + const log = []; + + const p1 = (async function () { + log.push(1); + await await null; + log.push(2); + })(); + + const p2 = (async function () { + log.push(3); + await null; + log.push(4); + })(); + + log.push(5); + + await Promise.all([p1, p2]); + // 4 should come before 2 due to double-await extra tick + expect(log.indexOf(4) < log.indexOf(2)).toBe(true); + expect(log[0]).toBe(1); + expect(log[1]).toBe(3); + expect(log[2]).toBe(5); + }); + }); + + describe('closures and loops', () => { + // From: regression/15978/exec.js + it('async closure in for-of captures correct value', async () => { + let items = [1, 2, 3, 4]; + let output = []; + for (const item of items) { + await (async (x) => { + output.push(item); + })(); + } + expect(output).toEqual([1, 2, 3, 4]); + }); + + it('await in for loop with closure', async () => { + const results = []; + for (let i = 0; i < 3; i++) { + const val = await Promise.resolve(i * 10); + results.push(() => val); + } + expect(results[0]()).toBe(0); + expect(results[1]()).toBe(10); + expect(results[2]()).toBe(20); + }); + + it('await in while loop', async () => { + let sum = 0; + let i = 0; + while (i < 5) { + sum += await Promise.resolve(i); + i++; + } + expect(sum).toBe(10); + }); + + // From: regression/8783/exec.js + it('recursive async function (polling pattern)', async () => { + const log = []; + async function collect(count) { + log.push(await Promise.resolve(count)); + if (count < 3) { + await collect(count + 1); + } + } + await collect(0); + expect(log).toEqual([0, 1, 2, 3]); + }); + }); + + describe('hoisting and declarations', () => { + // From: regression/T6882/exec.js + it('async function declaration is hoisted', async () => { + // Call before declaration + const result = await foo(); + expect(result).toBe(42); + + async function foo() { + return 42; + } + }); + + it('async function expression is not hoisted', () => { + expect(() => { + bar(); + }).toThrow(); + + var bar = async function () { + return 42; + }; + }); + }); + + describe('parallel patterns', () => { + it('Promise.all with multiple async calls', async () => { + async function double(x) { + return await Promise.resolve(x * 2); + } + const results = await Promise.all([double(1), double(2), double(3)]); + expect(results).toEqual([2, 4, 6]); + }); + + it('Promise.race with async functions', async () => { + async function fast() { + return 'fast'; + } + async function slow() { + await new Promise((r) => setTimeout(r, 100)); + return 'slow'; + } + const result = await Promise.race([fast(), slow()]); + expect(result).toBe('fast'); + }); + + it('Promise.allSettled with mixed results', async () => { + async function succeed() { + return 'ok'; + } + async function fail() { + throw new Error('fail'); + } + const results = await Promise.allSettled([succeed(), fail()]); + expect(results[0].status).toBe('fulfilled'); + expect(results[0].value).toBe('ok'); + expect(results[1].status).toBe('rejected'); + expect(results[1].reason.message).toBe('fail'); + }); + }); + + describe('edge cases', () => { + it('async IIFE', async () => { + const result = await (async () => { + return await Promise.resolve(42); + })(); + expect(result).toBe(42); + }); + + it('nested async functions', async () => { + async function outer() { + async function inner() { + return await Promise.resolve(21); + } + const val = await inner(); + return val * 2; + } + expect(await outer()).toBe(42); + }); + + it('async function returning another async function', async () => { + async function factory() { + return async (x) => x * 2; + } + const fn = await factory(); + expect(await fn(21)).toBe(42); + }); + + it('await in conditional expression', async () => { + async function foo(flag) { + return flag ? await Promise.resolve('yes') : await Promise.resolve('no'); + } + expect(await foo(true)).toBe('yes'); + expect(await foo(false)).toBe('no'); + }); + + it('await in template literal', async () => { + async function foo() { + return `result: ${await Promise.resolve(42)}`; + } + expect(await foo()).toBe('result: 42'); + }); + + it('await in array literal', async () => { + async function foo() { + return [await Promise.resolve(1), await Promise.resolve(2), await Promise.resolve(3)]; + } + expect(await foo()).toEqual([1, 2, 3]); + }); + + it('await in object literal', async () => { + async function foo() { + return { + a: await Promise.resolve(1), + b: await Promise.resolve(2), + }; + } + expect(await foo()).toEqual({ a: 1, b: 2 }); + }); + + it('await in destructuring', async () => { + async function foo() { + const { a, b } = await Promise.resolve({ a: 1, b: 2 }); + return a + b; + } + expect(await foo()).toBe(3); + }); + + it('await in switch statement', async () => { + async function foo(key) { + switch (await Promise.resolve(key)) { + case 'a': + return 1; + case 'b': + return 2; + default: + return 0; + } + } + expect(await foo('a')).toBe(1); + expect(await foo('b')).toBe(2); + expect(await foo('c')).toBe(0); + }); + + it('async function with no await still returns promise', () => { + async function foo() { + return 42; + } + const result = foo(); + expect(result instanceof Promise).toBe(true); + }); + + it('returning a promise from async function does not double-wrap', async () => { + async function foo() { + return Promise.resolve(42); + } + const result = await foo(); + expect(result).toBe(42); + }); + + it('returning a thenable from async function', async () => { + async function foo() { + return { + then(resolve) { + resolve(42); + }, + }; + } + expect(await foo()).toBe(42); + }); + }); + + describe('stack traces', () => { + it('error stack includes async function name', async () => { + async function myAsyncFunction() { + throw new Error('trace test'); + } + try { + await myAsyncFunction(); + } catch (e) { + const stack = e.stack || ''; + // The async function name should appear somewhere in the stack + expect(stack.indexOf('myAsyncFunction') >= 0).toBe(true); + } + }); + + it('error stack includes caller chain through await', async () => { + async function innerFunc() { + throw new Error('deep trace'); + } + async function middleFunc() { + return await innerFunc(); + } + async function outerFunc() { + return await middleFunc(); + } + try { + await outerFunc(); + } catch (e) { + const stack = e.stack || ''; + // At minimum, the function that threw should be in the stack + expect(stack.indexOf('innerFunc') >= 0).toBe(true); + } + }); + + // TODO(@kitten): transform-async-to-generator loses function names after await + xit('error created inside async function has meaningful stack', async () => { + async function createError() { + await Promise.resolve(); + return new Error('for inspection'); + } + const err = await createError(); + const stack = err.stack || ''; + // The error should have a stack that includes the function name + expect(stack.indexOf('createError') >= 0).toBe(true); + }); + + it('console-style stack preservation through async chain', async () => { + // Tests that the originating call site is preserved when errors + // propagate through multiple await layers + const errors = []; + async function level3() { + throw new Error('level3 error'); + } + async function level2() { + await level3(); + } + async function level1() { + try { + await level2(); + } catch (e) { + errors.push(e); + } + } + await level1(); + expect(errors.length).toBe(1); + expect(errors[0].message).toBe('level3 error'); + const stack = errors[0].stack || ''; + expect(stack.indexOf('level3') >= 0).toBe(true); + }); + + it('deep async chain preserves multiple function names in stack', async () => { + async function database() { + throw new Error('connection refused'); + } + async function repository() { + return await database(); + } + async function service() { + return await repository(); + } + async function controller() { + return await service(); + } + async function router() { + return await controller(); + } + try { + await router(); + } catch (e) { + const stack = e.stack || ''; + // The throw site should always be present + expect(stack.indexOf('database') >= 0).toBe(true); + // Intermediate callers should appear in the async stack + expect(stack.indexOf('repository') >= 0).toBe(true); + expect(stack.indexOf('service') >= 0).toBe(true); + expect(stack.indexOf('controller') >= 0).toBe(true); + expect(stack.indexOf('router') >= 0).toBe(true); + } + }); + + it('error thrown synchronously before any await has function name', async () => { + async function failImmediately() { + throw new Error('sync throw'); + } + try { + await failImmediately(); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('failImmediately') >= 0).toBe(true); + } + }); + + it('error from rejected promise includes thrower name', async () => { + async function rejecter() { + return Promise.reject(new Error('rejected')); + } + async function caller() { + return await rejecter(); + } + try { + await caller(); + } catch (e) { + expect(e.message).toBe('rejected'); + const stack = e.stack || ''; + expect(stack.indexOf('rejecter') >= 0).toBe(true); + } + }); + + it('async arrow function name in stack', async () => { + const myArrowAsync = async () => { + throw new Error('arrow error'); + }; + try { + await myArrowAsync(); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('myArrowAsync') >= 0).toBe(true); + } + }); + + it('async method name in stack', async () => { + class MyService { + async fetchData() { + throw new Error('service error'); + } + } + try { + await new MyService().fetchData(); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('fetchData') >= 0).toBe(true); + } + }); + + it('nested async class methods preserve inner name', async () => { + class Repo { + async query() { + throw new Error('db error'); + } + } + class Service { + async handle() { + return await new Repo().query(); + } + } + try { + await new Service().handle(); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('query') >= 0).toBe(true); + } + }); + + // TODO(@kitten): transform-async-to-generator loses function names after await + xit('error in Promise.all includes thrower name', async () => { + async function badTask() { + await Promise.resolve(); + throw new Error('task failed'); + } + try { + await Promise.all([Promise.resolve(1), badTask()]); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('badTask') >= 0).toBe(true); + } + }); + + it('re-thrown error preserves original stack', async () => { + async function original() { + throw new Error('original error'); + } + async function wrapper() { + try { + await original(); + } catch (e) { + throw e; // re-throw + } + } + try { + await wrapper(); + } catch (e) { + const stack = e.stack || ''; + // The original throw site should still be in the stack + expect(stack.indexOf('original') >= 0).toBe(true); + } + }); + + // TODO(@kitten): transform-async-to-generator loses function names after await + xit('error constructed after await preserves function name', async () => { + async function buildError() { + await Promise.resolve(); + return new Error('constructed'); + } + const err = await buildError(); + const stack = err.stack || ''; + expect(stack.indexOf('buildError') >= 0).toBe(true); + }); + }); + }); +} diff --git a/apps/test-suite/tests/JSAsyncGenerator.js b/apps/test-suite/tests/JSAsyncGenerator.js new file mode 100644 index 00000000000000..34af607d86caf7 --- /dev/null +++ b/apps/test-suite/tests/JSAsyncGenerator.js @@ -0,0 +1,1141 @@ +/* eslint-disable */ +'use strict'; + +// Comprehensive runtime compliance tests for async generators, for-await-of, +// for-of iteration, and sync generators. +// Validates that these features work correctly at runtime in the Hermes engine. +// Cases sourced from @babel/plugin-transform-async-generator-functions, +// @babel/plugin-transform-for-of, and @babel/plugin-transform-regenerator +// exec.js fixtures, supplemented with edge cases. + +export const name = 'JS Async Generator'; + +export function test({ describe, it, xit, expect }) { + describe('JS Async Generator', () => { + describe('sync generators', () => { + it('basic generator yields values', () => { + function* gen() { + yield 1; + yield 2; + yield 3; + } + const g = gen(); + expect(g.next()).toEqual({ value: 1, done: false }); + expect(g.next()).toEqual({ value: 2, done: false }); + expect(g.next()).toEqual({ value: 3, done: false }); + expect(g.next()).toEqual({ value: undefined, done: true }); + }); + + it('generator with return value', () => { + function* gen() { + yield 1; + return 'done'; + } + const g = gen(); + expect(g.next()).toEqual({ value: 1, done: false }); + expect(g.next()).toEqual({ value: 'done', done: true }); + expect(g.next()).toEqual({ value: undefined, done: true }); + }); + + // From: regression/17358/exec.js + it('generator with try-catch and no throw', () => { + function* foobar() { + try { + yield 1; + } catch { + return false; + } + } + const gen = foobar(); + expect(gen.next().value).toBe(1); + expect(gen.next().value).toBeUndefined(); + }); + + // From: regression/17365/exec.js + it('generator with try-finally and early return', () => { + function* gen() { + try { + return; + } finally { + // noop + } + } + expect(gen().next()).toEqual({ done: true, value: undefined }); + }); + + // From: regression/break-default-in-end/exec.js + it('generator with switch-default-break', () => { + function* gen(type) { + switch (type) { + default: + break; + } + } + expect(gen(1).next().done).toBe(true); + + function* gen2(type) { + switch (type) { + case 0: + throw 'unreachable'; + default: + break; + } + } + expect(gen2(1).next().done).toBe(true); + }); + + // From: integration/default-parameters/exec.js + it('generator with default parameters', () => { + function* foo(bar = 'bar') { + return bar; + } + expect(foo().next().value).toBe('bar'); + expect(foo('foo').next().value).toBe('foo'); + }); + + // From: integration/destructuring-parameters/exec.js + it('generator with destructured parameters', () => { + function* foo({ bar }) { + return bar; + } + expect(foo({ bar: 'bar' }).next().value).toBe('bar'); + }); + + // From: integration/rest-parameters/exec.js + it('generator with rest parameters', () => { + function* foo(...items) { + return items; + } + expect(foo(1, 2, 3).next().value).toEqual([1, 2, 3]); + }); + + it('generator with throw()', () => { + function* gen() { + try { + yield 1; + yield 2; + } catch (e) { + yield 'caught: ' + e.message; + } + } + const g = gen(); + expect(g.next()).toEqual({ value: 1, done: false }); + expect(g.throw(new Error('test'))).toEqual({ value: 'caught: test', done: false }); + }); + + it('generator with return()', () => { + function* gen() { + try { + yield 1; + yield 2; + } finally { + yield 'cleanup'; + } + } + const g = gen(); + expect(g.next()).toEqual({ value: 1, done: false }); + expect(g.return('early')).toEqual({ value: 'cleanup', done: false }); + expect(g.next()).toEqual({ value: 'early', done: true }); + }); + + it('yield* delegation to another generator', () => { + function* inner() { + yield 'a'; + yield 'b'; + } + function* outer() { + yield 1; + yield* inner(); + yield 2; + } + expect([...outer()]).toEqual([1, 'a', 'b', 2]); + }); + + it('yield* delegation to array', () => { + function* gen() { + yield* [10, 20, 30]; + } + expect([...gen()]).toEqual([10, 20, 30]); + }); + + // From: regression/star-rhs-iter-rtrn-no-rtrn/exec.js + it('yield* with iterable that has no return method', () => { + function* inner() { + yield 1; + yield 2; + } + function* outer() { + const result = yield* inner(); + yield result; + } + const g = outer(); + expect(g.next()).toEqual({ value: 1, done: false }); + expect(g.next()).toEqual({ value: 2, done: false }); + expect(g.next()).toEqual({ value: undefined, done: false }); + }); + + it('generator used in spread', () => { + function* gen() { + yield 1; + yield 2; + yield 3; + } + expect([...gen()]).toEqual([1, 2, 3]); + }); + + it('generator used in destructuring', () => { + function* gen() { + yield 'a'; + yield 'b'; + yield 'c'; + } + const [x, y, z] = gen(); + expect(x).toBe('a'); + expect(y).toBe('b'); + expect(z).toBe('c'); + }); + + it('generator with complex control flow', () => { + function* gen(n) { + for (let i = 0; i < n; i++) { + if (i % 2 === 0) { + yield i; + } + } + } + expect([...gen(6)]).toEqual([0, 2, 4]); + }); + + it('nested generators with yield*', () => { + function* a() { + yield 1; + } + function* b() { + yield* a(); + yield 2; + } + function* c() { + yield* b(); + yield 3; + } + expect([...c()]).toEqual([1, 2, 3]); + }); + + it('generator passing values via next()', () => { + function* gen() { + const a = yield 'first'; + const b = yield 'second'; + return a + b; + } + const g = gen(); + expect(g.next().value).toBe('first'); + expect(g.next(10).value).toBe('second'); + expect(g.next(32).value).toBe(42); + }); + + it('infinite generator with break', () => { + function* counter() { + let i = 0; + while (true) { + yield i++; + } + } + const results = []; + for (const n of counter()) { + results.push(n); + if (n >= 4) break; + } + expect(results).toEqual([0, 1, 2, 3, 4]); + }); + }); + + describe('for-of iteration', () => { + // From: loose-exec/array.js + it('for-of with array', () => { + var arr = [1, 2, 3]; + var res = []; + for (const x of arr) res.push(x); + expect(res).toEqual([1, 2, 3]); + }); + + // From: loose-exec/array-break.js + it('for-of with array and break', () => { + var arr = [1, 2, 3]; + var res = []; + for (const x of arr) { + if (x === 2) break; + res.push(x); + } + expect(res).toEqual([1]); + }); + + // From: loose-exec/array-continue.js + it('for-of with array and continue', () => { + var arr = [1, 2, 3]; + var res = []; + for (const x of arr) { + if (x === 2) continue; + res.push(x); + } + expect(res).toEqual([1, 3]); + }); + + // From: loose-exec/generator.js + it('for-of with generator', () => { + function* f() { + yield 1; + yield 2; + yield 3; + } + var res = []; + for (const x of f()) res.push(x); + expect(res).toEqual([1, 2, 3]); + }); + + // From: loose-exec/generator-break.js + it('for-of with generator and break', () => { + function* f() { + yield 1; + yield 2; + yield 3; + } + var res = []; + for (const x of f()) { + if (x === 2) break; + res.push(x); + } + expect(res).toEqual([1]); + }); + + // From: loose-exec/generator-continue.js + it('for-of with generator and continue', () => { + function* f() { + yield 1; + yield 2; + yield 3; + } + var res = []; + for (const x of f()) { + if (x === 2) continue; + res.push(x); + } + expect(res).toEqual([1, 3]); + }); + + it('for-of with string', () => { + var res = []; + for (const ch of 'abc') res.push(ch); + expect(res).toEqual(['a', 'b', 'c']); + }); + + it('for-of with Set', () => { + var res = []; + for (const x of new Set([1, 2, 3])) res.push(x); + expect(res).toEqual([1, 2, 3]); + }); + + it('for-of with Map', () => { + var res = []; + for (const [k, v] of new Map([['a', 1], ['b', 2]])) { + res.push(k + ':' + v); + } + expect(res).toEqual(['a:1', 'b:2']); + }); + + it('for-of with custom iterable', () => { + const iterable = { + [Symbol.iterator]() { + let i = 0; + return { + next() { + if (i < 3) return { value: i++, done: false }; + return { value: undefined, done: true }; + }, + }; + }, + }; + var res = []; + for (const x of iterable) res.push(x); + expect(res).toEqual([0, 1, 2]); + }); + + // From: spec-exec/throw-iterator-handling.js + // TODO(@kitten): loose mode in Babel doesn't support this + xit('for-of calls return() on iterator when break occurs', () => { + let returnCalled = false; + const iterable = { + [Symbol.iterator]() { + let i = 0; + return { + next() { + return { value: i++, done: false }; + }, + return() { + returnCalled = true; + return { value: undefined, done: true }; + }, + }; + }, + }; + for (const x of iterable) { + if (x === 2) break; + } + expect(returnCalled).toBe(true); + }); + + // TODO(@kitten): loose mode in Babel doesn't support this + xit('for-of calls return() on iterator when exception is thrown', () => { + let returnCalled = false; + const iterable = { + [Symbol.iterator]() { + let i = 0; + return { + next() { + return { value: i++, done: false }; + }, + return() { + returnCalled = true; + return { value: undefined, done: true }; + }, + }; + }, + }; + try { + for (const x of iterable) { + if (x === 1) throw new Error('bail'); + } + } catch (e) {} + expect(returnCalled).toBe(true); + }); + + it('for-of with let creates per-iteration binding', () => { + const funcs = []; + for (let x of [0, 1, 2]) { + funcs.push(() => x); + } + expect(funcs[0]()).toBe(0); + expect(funcs[1]()).toBe(1); + expect(funcs[2]()).toBe(2); + }); + + it('for-of with destructuring', () => { + const pairs = [[1, 'a'], [2, 'b'], [3, 'c']]; + var nums = []; + var strs = []; + for (const [n, s] of pairs) { + nums.push(n); + strs.push(s); + } + expect(nums).toEqual([1, 2, 3]); + expect(strs).toEqual(['a', 'b', 'c']); + }); + + it('for-of with arguments object', () => { + function getArgs() { + return arguments; + } + var args = getArgs(1, 2, 3); + var res = []; + for (const x of args) res.push(x); + expect(res).toEqual([1, 2, 3]); + }); + }); + + describe('async generators', () => { + // From: async-generators/declaration-exec/exec.js + it('basic async generator yields resolved values', async () => { + async function* gen() { + yield await Promise.resolve('foo'); + yield await Promise.resolve('bar'); + yield await Promise.resolve('baz'); + } + const g = gen(); + expect(await g.next()).toEqual({ value: 'foo', done: false }); + expect(await g.next()).toEqual({ value: 'bar', done: false }); + expect(await g.next()).toEqual({ value: 'baz', done: false }); + expect(await g.next()).toEqual({ value: undefined, done: true }); + }); + + // From: async-generators/yield-exec/exec.js + it('async generator execution order with delays', async () => { + const log = []; + async function* gen() { + log.push('a'); + yield Promise.resolve(); + log.push('b'); + yield Promise.resolve(); + log.push('c'); + } + const g = gen(); + await g.next(); + await g.next(); + await g.next(); + expect(log).toEqual(['a', 'b', 'c']); + }); + + it('async generator with return value', async () => { + async function* gen() { + yield 1; + yield 2; + return 'done'; + } + const g = gen(); + expect(await g.next()).toEqual({ value: 1, done: false }); + expect(await g.next()).toEqual({ value: 2, done: false }); + expect(await g.next()).toEqual({ value: 'done', done: true }); + expect(await g.next()).toEqual({ value: undefined, done: true }); + }); + + it('async generator with throw()', async () => { + async function* gen() { + try { + yield 1; + yield 2; + } catch (e) { + yield 'caught: ' + e.message; + } + } + const g = gen(); + expect(await g.next()).toEqual({ value: 1, done: false }); + const result = await g.throw(new Error('test')); + expect(result).toEqual({ value: 'caught: test', done: false }); + }); + + it('async generator with return() for cleanup', async () => { + const log = []; + async function* gen() { + try { + log.push('start'); + yield 1; + log.push('after yield 1'); + yield 2; + } finally { + log.push('cleanup'); + } + } + const g = gen(); + await g.next(); + await g.return(); + expect(log).toEqual(['start', 'cleanup']); + }); + + it('async generator passing values via next()', async () => { + async function* gen() { + const a = yield 'first'; + const b = yield 'second'; + return a + b; + } + const g = gen(); + expect((await g.next()).value).toBe('first'); + expect((await g.next(10)).value).toBe('second'); + expect((await g.next(32)).value).toBe(42); + }); + + it('async generator with try-finally', async () => { + const log = []; + async function* gen() { + try { + yield 1; + yield 2; + } finally { + log.push('finally'); + } + } + const g = gen(); + await g.next(); + await g.next(); + await g.next(); + expect(log).toEqual(['finally']); + }); + + it('async generator with nested await', async () => { + async function fetchVal(x) { + return Promise.resolve(x * 2); + } + async function* gen() { + yield await fetchVal(1); + yield await fetchVal(2); + yield await fetchVal(3); + } + const results = []; + const g = gen(); + let r = await g.next(); + while (!r.done) { + results.push(r.value); + r = await g.next(); + } + expect(results).toEqual([2, 4, 6]); + }); + + it('async generator expression', async () => { + const gen = async function* () { + yield 1; + yield 2; + }; + const g = gen(); + expect(await g.next()).toEqual({ value: 1, done: false }); + expect(await g.next()).toEqual({ value: 2, done: false }); + expect(await g.next()).toEqual({ value: undefined, done: true }); + }); + + it('async generator as class method', async () => { + class Foo { + async *items() { + yield 'a'; + yield 'b'; + } + } + const results = []; + for await (const val of new Foo().items()) { + results.push(val); + } + expect(results).toEqual(['a', 'b']); + }); + }); + + describe('for-await-of', () => { + // From: for-await/async-generator-exec/exec.js + it('for-await-of with async generator (cumulative sum)', async () => { + async function* genAnswers() { + var stream = [ + Promise.resolve(4), + Promise.resolve(9), + Promise.resolve(12), + ]; + var total = 0; + for await (let val of stream) { + total += await val; + yield total; + } + } + const results = []; + for await (const val of genAnswers()) { + results.push(val); + } + expect(results).toEqual([4, 13, 25]); + }); + + it('for-await-of with array of promises', async () => { + const promises = [ + Promise.resolve(1), + Promise.resolve(2), + Promise.resolve(3), + ]; + const results = []; + for await (const val of promises) { + results.push(val); + } + expect(results).toEqual([1, 2, 3]); + }); + + it('for-await-of with custom async iterable', async () => { + const iterable = { + [Symbol.asyncIterator]() { + let i = 0; + return { + next() { + if (i < 3) return Promise.resolve({ value: i++, done: false }); + return Promise.resolve({ value: undefined, done: true }); + }, + }; + }, + }; + const results = []; + for await (const val of iterable) { + results.push(val); + } + expect(results).toEqual([0, 1, 2]); + }); + + it('for-await-of with break', async () => { + async function* gen() { + yield 1; + yield 2; + yield 3; + yield 4; + yield 5; + } + const results = []; + for await (const val of gen()) { + results.push(val); + if (val === 3) break; + } + expect(results).toEqual([1, 2, 3]); + }); + + it('for-await-of with error in iterable', async () => { + async function* gen() { + yield 1; + throw new Error('gen error'); + } + const results = []; + try { + for await (const val of gen()) { + results.push(val); + } + } catch (e) { + results.push('error: ' + e.message); + } + expect(results).toEqual([1, 'error: gen error']); + }); + + // From: for-await/step-value-not-accessed-when-done/exec.js + it('for-await-of does not access value when done', async () => { + let gotValue = false; + const iterable = { + [Symbol.asyncIterator]() { + return { + next: () => + Promise.resolve({ + get value() { + gotValue = true; + }, + done: true, + }), + }; + }, + }; + for await (let value of iterable) { + } + expect(gotValue).toBe(false); + }); + + // From: for-await/lhs-member-expression/exec.js + it('for-await-of with member expression as LHS', async () => { + let obj = {}; + for await (obj.x of [1, 2]) { + } + expect(obj.x).toBe(2); + }); + + // From: for-await/re-declare-var-in-init-body/exec.js + it('for-await-of allows re-declaration of let in body', async () => { + // Should not throw a SyntaxError + for await (let x of []) { + let x; + } + expect(true).toBe(true); + }); + + it('for-await-of with destructuring', async () => { + async function* gen() { + yield { a: 1, b: 2 }; + yield { a: 3, b: 4 }; + } + const results = []; + for await (const { a, b } of gen()) { + results.push(a + b); + } + expect(results).toEqual([3, 7]); + }); + + it('for-await-of with sync iterable (fallback)', async () => { + // for-await-of should work with sync iterables too + const results = []; + for await (const val of [10, 20, 30]) { + results.push(val); + } + expect(results).toEqual([10, 20, 30]); + }); + }); + + describe('async generator yield* delegation', () => { + // From: yield-star/return-method/exec.js + it('yield* delegation with early return()', async () => { + const log = []; + async function* inner() { + log.push(1); + yield 'a'; + log.push(2); + yield 'b'; + log.push(3); + } + async function* outer() { + log.push(4); + yield* inner(); + log.push(5); + } + const iter = outer(); + const res = await iter.next(); + expect(res).toEqual({ value: 'a', done: false }); + expect(log).toEqual([4, 1]); + + await iter.return(); + expect(log).toEqual([4, 1]); + }); + + // From: yield-star/throw-method-with-catch/exec.js + it('yield* delegation with throw() caught by inner', async () => { + const log = []; + async function* inner() { + try { + log.push(1); + yield 'a'; + log.push(2); + } catch (e) { + log.push(3); + yield 'caught'; + log.push(4); + } + } + async function* outer() { + log.push(5); + yield* inner(); + log.push(6); + } + const iter = outer(); + await iter.next(); + expect(log).toEqual([5, 1]); + + const res = await iter.throw(new Error('TEST')); + expect(res).toEqual({ value: 'caught', done: false }); + expect(log).toEqual([5, 1, 3]); + + await iter.next(); + expect(log).toEqual([5, 1, 3, 4, 6]); + }); + + // From: yield-star/create-async-from-sync-iterator/exec.js + it('yield* from sync iterable in async generator', async () => { + async function* fn() { + yield* [Promise.resolve('ok')]; + } + const result = await fn().next(); + expect(result.done).toBe(false); + // CreateAsyncFromSyncIterator awaits the yielded promise, + // so the value is the resolved string, not the Promise itself. + expect(result.value).toBe('ok'); + }); + + // From: yield-star/issue-9905/exec.js + it('yield* early return does not execute rest of inner', async () => { + const log = []; + async function* func1() { + log.push(1); + yield 'a'; + log.push(2); + } + async function* func2() { + yield* func1(); + log.push(3); + } + const iter = func2(); + await iter.next(); + await iter.return(); + expect(log).toEqual([1]); + }); + + it('yield* delegation with async inner values', async () => { + async function* inner() { + yield await Promise.resolve(10); + yield await Promise.resolve(20); + } + async function* outer() { + yield 1; + yield* inner(); + yield 2; + } + const results = []; + for await (const val of outer()) { + results.push(val); + } + expect(results).toEqual([1, 10, 20, 2]); + }); + + it('yield* nested three levels deep', async () => { + async function* a() { + yield 'a'; + } + async function* b() { + yield* a(); + yield 'b'; + } + async function* c() { + yield* b(); + yield 'c'; + } + const results = []; + for await (const val of c()) { + results.push(val); + } + expect(results).toEqual(['a', 'b', 'c']); + }); + }); + + describe('edge cases', () => { + it('generator with labeled loop and break', () => { + function* gen() { + outer: for (let i = 0; i < 3; i++) { + for (let j = 0; j < 3; j++) { + if (j === 1) continue outer; + yield [i, j]; + } + } + } + expect([...gen()]).toEqual([[0, 0], [1, 0], [2, 0]]); + }); + + it('async generator with for-of inside', async () => { + async function* gen() { + for (const x of [1, 2, 3]) { + yield await Promise.resolve(x * 10); + } + } + const results = []; + for await (const val of gen()) { + results.push(val); + } + expect(results).toEqual([10, 20, 30]); + }); + + it('async generator with for-await-of inside', async () => { + async function* gen() { + for await (const x of [Promise.resolve(1), Promise.resolve(2)]) { + yield x * 10; + } + } + const results = []; + for await (const val of gen()) { + results.push(val); + } + expect(results).toEqual([10, 20]); + }); + + it('for-of with empty iterable', () => { + var count = 0; + for (const x of []) { + count++; + } + expect(count).toBe(0); + }); + + it('for-await-of with empty async iterable', async () => { + async function* empty() {} + var count = 0; + for await (const x of empty()) { + count++; + } + expect(count).toBe(0); + }); + + it('generator function length property', () => { + function* gen(a, b, c) {} + expect(gen.length).toBe(3); + }); + + it('generator is iterable (Symbol.iterator)', () => { + function* gen() { + yield 1; + } + const g = gen(); + expect(g[Symbol.iterator]() === g).toBe(true); + }); + + it('async generator is async iterable (Symbol.asyncIterator)', () => { + async function* gen() { + yield 1; + } + const g = gen(); + expect(g[Symbol.asyncIterator]() === g).toBe(true); + }); + + it('for-of with TypedArray', () => { + var res = []; + for (const x of new Uint8Array([10, 20, 30])) { + res.push(x); + } + expect(res).toEqual([10, 20, 30]); + }); + + it('async generator with Promise.all inside', async () => { + async function* gen() { + const [a, b, c] = await Promise.all([ + Promise.resolve(1), + Promise.resolve(2), + Promise.resolve(3), + ]); + yield a; + yield b; + yield c; + } + const results = []; + for await (const val of gen()) { + results.push(val); + } + expect(results).toEqual([1, 2, 3]); + }); + }); + + describe('stack traces', () => { + it('error thrown inside sync generator includes function name', () => { + function* failingGen() { + throw new Error('gen error'); + } + const g = failingGen(); + try { + g.next(); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('failingGen') >= 0).toBe(true); + } + }); + + it('generator throw() preserves caller stack', () => { + function* myGen() { + try { + yield 1; + } catch (e) { + throw e; + } + } + const g = myGen(); + g.next(); + try { + g.throw(new Error('injected')); + } catch (e) { + expect(e.message).toBe('injected'); + } + }); + + // TODO(@kitten): transform-async-generator-functions loses function names + xit('error thrown inside async generator includes function name', async () => { + async function* failingAsyncGen() { + throw new Error('async gen error'); + } + const g = failingAsyncGen(); + try { + await g.next(); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('failingAsyncGen') >= 0).toBe(true); + } + }); + + // TODO(@kitten): transform-async-generator-functions loses function names + xit('error after await inside async generator includes function name', async () => { + async function* genAfterAwait() { + yield 'sentinel'; + await Promise.resolve(); + throw new Error('after await'); + } + const g = genAfterAwait(); + await g.next(); // yields 'sentinel' + try { + await g.next(); // resumes, awaits, then throws + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('genAfterAwait') >= 0).toBe(true); + } + }); + + // TODO(@kitten): transform-async-generator-functions loses function names + xit('error propagates through for-await-of with function name', async () => { + async function* explodingGen() { + yield 1; + throw new Error('mid-iteration'); + } + try { + for await (const _val of explodingGen()) { + // consume + } + } catch (e) { + expect(e.message).toBe('mid-iteration'); + const stack = e.stack || ''; + expect(stack.indexOf('explodingGen') >= 0).toBe(true); + } + }); + + it('error in yield* delegation includes inner generator name', () => { + function* inner() { + yield 1; + throw new Error('inner fail'); + } + function* outer() { + yield* inner(); + } + const g = outer(); + g.next(); // yields 1 + try { + g.next(); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('inner') >= 0).toBe(true); + } + }); + + // TODO(@kitten): transform-async-generator-functions loses function names + xit('async generator method name in stack', async () => { + class DataStream { + async *fetchChunks() { + throw new Error('stream error'); + } + } + const s = new DataStream(); + const g = s.fetchChunks(); + try { + await g.next(); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('fetchChunks') >= 0).toBe(true); + } + }); + + // TODO(@kitten): transform-async-generator-functions loses function names + xit('nested async generators preserve inner name', async () => { + async function* innerGen() { + await Promise.resolve(); + throw new Error('inner async fail'); + } + async function* outerGen() { + for await (const val of innerGen()) { + yield val; + } + } + try { + for await (const _val of outerGen()) { + // consume + } + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('innerGen') >= 0).toBe(true); + } + }); + + it('for-of error in loop body has correct stack', () => { + function thrower() { + throw new Error('loop body error'); + } + try { + for (const x of [1, 2, 3]) { + if (x === 2) thrower(); + } + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('thrower') >= 0).toBe(true); + } + }); + + // TODO(@kitten): transform-async-generator-functions loses function names + xit('re-thrown error from async generator preserves original stack', async () => { + async function* originalGen() { + throw new Error('original gen error'); + } + async function consumer() { + try { + for await (const _val of originalGen()) { + // consume + } + } catch (e) { + throw e; + } + } + try { + await consumer(); + } catch (e) { + const stack = e.stack || ''; + expect(stack.indexOf('originalGen') >= 0).toBe(true); + } + }); + }); + }); +} diff --git a/apps/test-suite/tests/JSBlockScoping.js b/apps/test-suite/tests/JSBlockScoping.js new file mode 100644 index 00000000000000..4233b11973b373 --- /dev/null +++ b/apps/test-suite/tests/JSBlockScoping.js @@ -0,0 +1,809 @@ +/* eslint-disable */ +'use strict'; + +// Comprehensive runtime compliance tests for ES6 block scoping (let/const). +// Validates that let/const work correctly at runtime in the Hermes engine. +// Cases sourced from @babel/plugin-transform-block-scoping exec.js fixtures, +// Hermes issues #575 and #1599, and supplemental edge cases. + +export const name = 'JS Block Scoping'; + +export function test({ describe, it, expect }) { + describe('JS Block Scoping', () => { + describe('basic block scoping', () => { + // From: exec/block-scoped.js + it('nested let declarations create independent scopes', () => { + let x = 1; + { + let x = 2; + expect(x).toBe(2); + { + let x = 3; + expect(x).toBe(3); + x++; + expect(x).toBe(4); + } + } + expect(x).toBe(1); + }); + + // From: exec/block-scoped-2.js + it('closures in nested blocks capture correct binding', () => { + expect( + (() => { + let sum = 0; + let a = 0; + { + let a = 10; + for (let i = 0; i < a; i++) { + let a = 1; + sum += (() => a)(); + } + } + return sum; + })() + ).toBe(10); + }); + + // From: exec/duplicate-function-scope.js + it('inner scope shadows outer function scope', () => { + function test() { + let value = 'outer'; + return (function () { + let value = 'inner'; + return value; + })(); + } + expect(test()).toBe('inner'); + }); + + // From: exec/collision-for.js + it('let in loop body shadows outer let', () => { + let x = 0; + for (;;) { + let x = 1; + expect(x).toBe(1); + break; + } + expect(x).toBe(0); + }); + + it('const in block is not visible outside', () => { + { + const a = 1; + expect(a).toBe(1); + } + expect(typeof a).toBe('undefined'); + }); + + it('let in block is not visible outside', () => { + { + let b = 2; + expect(b).toBe(2); + } + expect(typeof b).toBe('undefined'); + }); + + it('if-block scoping', () => { + let x = 'outer'; + if (true) { + let x = 'inner'; + expect(x).toBe('inner'); + } + expect(x).toBe('outer'); + }); + }); + + describe('for loop scoping', () => { + // From: exec/for-loop-head.js + it('let in for-loop head does not leak', () => { + expect( + function () { + let a = 1; + for (let a = 0; a < 8; a++) {} + return a; + }() + ).toBe(1); + }); + + // From: exec/multiple.js + it('multiple let declarations in for-loop head do not leak', () => { + for (let i = 0, x = 2; i < 5; i++); + expect(typeof i).toBe('undefined'); + expect(typeof x).toBe('undefined'); + }); + + // From: exec/for-continuation.js + it('closures capture per-iteration let binding in for loop', () => { + var fns = []; + for (let i = 0; i < 10; i++) { + fns.push(function () { + return i; + }); + i += 1; + } + expect(fns[0]()).toBe(1); + expect(fns[1]()).toBe(3); + expect(fns[2]()).toBe(5); + expect(fns[3]()).toBe(7); + expect(fns[4]()).toBe(9); + }); + + // From: exec/for-continuation-outer-reference.js + it('closures with splice and index mutation in for loop', () => { + let data = [true, false, false, true, false]; + for (let index = 0; index < data.length; index++) { + let item = data[index]; + if (!item) { + data.splice(index, 1); + index--; + continue; + } + let fn = function () { + item; + }; + } + expect(data.every((item) => item)).toBe(true); + }); + + it('each for-loop iteration gets its own let binding', () => { + var funcs = []; + for (let i = 0; i < 5; i++) { + funcs.push(() => i); + } + expect(funcs[0]()).toBe(0); + expect(funcs[1]()).toBe(1); + expect(funcs[2]()).toBe(2); + expect(funcs[3]()).toBe(3); + expect(funcs[4]()).toBe(4); + }); + + it('const in for-of creates per-iteration binding', () => { + var funcs = []; + for (const val of [10, 20, 30]) { + funcs.push(() => val); + } + expect(funcs[0]()).toBe(10); + expect(funcs[1]()).toBe(20); + expect(funcs[2]()).toBe(30); + }); + + // Hermes issue #575: for-in with closure capturing loop variable + it('const in for-in creates per-iteration binding (hermes#575)', () => { + const target = { + a: 'a', + b: 'b', + c: 'c', + }; + const copy = {}; + let keys = ''; + for (const prop in target) { + keys += ' ' + prop; + Object.defineProperty(copy, prop, { + get: () => target[prop], + enumerable: true, + }); + } + expect(copy.a).toBe('a'); + expect(copy.b).toBe('b'); + expect(copy.c).toBe('c'); + }); + + it('let in for-in creates per-iteration binding', () => { + const obj = { x: 1, y: 2, z: 3 }; + var funcs = []; + for (let key in obj) { + funcs.push(() => key); + } + var results = funcs.map((f) => f()); + expect(results).toContain('x'); + expect(results).toContain('y'); + expect(results).toContain('z'); + }); + + // From: exec/destructuring-defaults.js + it('destructuring with defaults in for-of', () => { + var fields = [{ name: 'title' }, { name: 'content' }]; + for (let { name, value = 'Default value' } of fields) { + expect(value).toBe('Default value'); + } + }); + }); + + describe('labels and control flow', () => { + // From: exec/label.js + it('labeled break with closures in for-in', () => { + var heh = []; + var nums = [1, 2, 3]; + + loop1: for (let i in nums) { + let num = nums[i]; + heh.push((x) => x * num); + if (num >= 2) { + break loop1; + } + } + expect(heh.length).toBe(2); + expect(heh[0](2)).toBe(2); + expect(heh[1](4)).toBe(8); + }); + + // From: exec/nested-labels.js + it('nested loops with labeled continue and closures', () => { + var stack = []; + loop1: for (let j = 0; j < 10; j++) { + for (let i = 0; i < 10; i++) { + stack.push(() => [i, j]); + continue loop1; + } + } + expect(stack[0]()).toEqual([0, 0]); + expect(stack[1]()).toEqual([0, 1]); + expect(stack[4]()).toEqual([0, 4]); + expect(stack[9]()).toEqual([0, 9]); + }); + + // From: exec/nested-labels-2.js + it('nested loops with inner break and closures', () => { + var stack = []; + for (let j = 0; j < 10; j++) { + for (let i = 0; i < 10; i++) { + stack.push(() => [i, j]); + break; + } + } + expect(stack[0]()).toEqual([0, 0]); + expect(stack[1]()).toEqual([0, 1]); + expect(stack[9]()).toEqual([0, 9]); + }); + + // From: exec/nested-labels-3.js + it('triple-nested loops with labeled continue and closures', () => { + var stack = []; + loop1: for (let j = 0; j < 10; j++) { + loop2: for (let i = 0; i < 10; i++) { + for (let x = 0; x < 10; x++) { + stack.push(() => [j, i, x]); + continue loop2; + } + } + } + expect(stack[0]()).toEqual([0, 0, 0]); + expect(stack[1]()).toEqual([0, 1, 0]); + expect(stack[9]()).toEqual([0, 9, 0]); + }); + + // From: exec/nested-labels-4.js + it('labeled break exits all nested loops', () => { + var stack = []; + loop1: for (let j = 0; j < 10; j++) { + for (let i = 0; i < 10; i++) { + stack.push(() => [i, j]); + break loop1; + } + } + expect(stack.length).toBe(1); + expect(stack[0]()).toEqual([0, 0]); + }); + }); + + describe('switch statements', () => { + // From: exec/switch-break.js + it('const in switch case block is accessible', () => { + if (true) { + const x = 1; + switch (x) { + case 1: { + expect(x).toBe(1); + break; + } + } + } + }); + + // From: exec/switch-labeled-break.js + it('labeled break inside switch exits outer loop', () => { + var i; + the_loop: for (i = 0; i < 10; i++) { + switch (i) { + case 3: { + break the_loop; + } + } + const z = 3; + (() => z)(); + } + expect(i).toBe(3); + }); + + // From: general/block-inside-switch-inside-loop/exec.js + it('switch break does not exit for loop', () => { + var i; + for (i = 0; i < 10; i++) { + switch (i) { + case 1: { + break; + } + } + const z = 3; + (() => z)(); + } + expect(i).toBe(10); + }); + + it('continue in switch case continues loop', () => { + var j = 0; + var i; + for (i = 0; i < 10; i++) { + switch (i) { + case 0: { + continue; + } + } + j++; + const z = 3; + (() => z)(); + } + expect(j).toBe(9); + }); + + it('nested loop inside switch with break', () => { + var j = 0; + var i; + for (i = 0; i < 10; i++) { + switch (i) { + case 0: { + for (var k = 0; k < 10; k++) { + const z = 3; + (() => z)(); + j++; + break; + } + break; + } + } + const z = 3; + (() => z)(); + } + expect(j).toBe(1); + }); + + it('let in switch cases are block-scoped', () => { + let result; + switch (1) { + case 1: { + let x = 'one'; + result = x; + break; + } + case 2: { + let x = 'two'; + result = x; + break; + } + } + expect(result).toBe('one'); + }); + }); + + describe('temporal dead zone', () => { + // From: tdz/simple-reference/exec.js + // Hermes does not enforce TDZ — accessing let before declaration does not throw. + xit('reference before let declaration throws', () => { + expect(() => { + i; + let i; + }).toThrow(); + }); + + // From: tdz/self-reference/exec.js + // Hermes does not enforce TDZ — self-referencing let initializer does not throw. + xit('let x = x throws ReferenceError', () => { + expect(() => { + let x = x; + }).toThrow(); + }); + + // From: tdz/const-readonly/exec.js + it('const reassignment throws TypeError', () => { + expect(() => { + const x = 0; + x = 1; + }).toThrow(); + }); + + it('assignment before const declaration throws', () => { + expect(() => { + x = 1; + const x = 0; + }).toThrow(); + }); + + it('closure called before const declaration throws', () => { + expect(() => { + (() => { + x = 1; + })(); + const x = 0; + }).toThrow(); + }); + + it('deferred closure called after const declaration throws TypeError', () => { + expect(() => { + const call = (() => { + return () => { + x = 1; + }; + })(); + const x = 0; + call(); + }).toThrow(); + }); + + // Hermes does not enforce TDZ — let is accessible (as undefined) before initialization. + xit('let is undefined before initialization in same scope', () => { + expect(() => { + expect(y).toBeUndefined(); + let y = 1; + }).toThrow(); + }); + + it('typeof on TDZ variable does not throw', () => { + // typeof is special - it doesn't throw even in TDZ in some engines + // but per spec it should throw for let/const. We just check it doesn't crash. + let didThrow = false; + try { + typeof tdz_var; + let tdz_var = 1; + } catch (e) { + didThrow = true; + } + // Either behavior is acceptable for this edge case + expect(typeof didThrow).toBe('boolean'); + }); + + // Hermes does not enforce TDZ — inner block let does not shadow outer in TDZ. + xit('TDZ applies per-block', () => { + let x = 'outer'; + { + expect(() => { + return x; + let x = 'inner'; + }).toThrow(); + } + }); + + it('function hoisting is not affected by TDZ', () => { + expect(() => { + foo(); + function foo() {} + }).not.toThrow(); + }); + }); + + describe('closures in loops', () => { + it('var in for-loop shares binding (baseline)', () => { + var funcs = []; + for (var i = 0; i < 5; i++) { + funcs.push(() => i); + } + // All closures see the same i = 5 + expect(funcs[0]()).toBe(5); + expect(funcs[4]()).toBe(5); + }); + + it('let in for-loop gives per-iteration binding', () => { + var funcs = []; + for (let i = 0; i < 5; i++) { + funcs.push(() => i); + } + expect(funcs[0]()).toBe(0); + expect(funcs[1]()).toBe(1); + expect(funcs[2]()).toBe(2); + expect(funcs[3]()).toBe(3); + expect(funcs[4]()).toBe(4); + }); + + it('let in for-loop with setTimeout pattern', () => { + var results = []; + var callbacks = []; + for (let i = 0; i < 3; i++) { + callbacks.push(() => { + results.push(i); + }); + } + callbacks.forEach((cb) => cb()); + expect(results).toEqual([0, 1, 2]); + }); + + it('const in for-of with async-like closures', () => { + var tasks = []; + for (const item of ['a', 'b', 'c']) { + tasks.push(() => item); + } + expect(tasks.map((t) => t())).toEqual(['a', 'b', 'c']); + }); + + it('let in for-loop with nested closures', () => { + var outer = []; + for (let i = 0; i < 3; i++) { + var inner = []; + for (let j = 0; j < 3; j++) { + inner.push(() => [i, j]); + } + outer.push(inner); + } + expect(outer[0][0]()).toEqual([0, 0]); + expect(outer[0][2]()).toEqual([0, 2]); + expect(outer[2][1]()).toEqual([2, 1]); + }); + + it('let in while loop with closure', () => { + var funcs = []; + let i = 0; + while (i < 3) { + let captured = i; + funcs.push(() => captured); + i++; + } + expect(funcs[0]()).toBe(0); + expect(funcs[1]()).toBe(1); + expect(funcs[2]()).toBe(2); + }); + + it('let in for-in with closure captures each key', () => { + var obj = { a: 1, b: 2, c: 3 }; + var funcs = []; + for (let k in obj) { + funcs.push(() => k); + } + var keys = funcs.map((f) => f()).sort(); + expect(keys).toEqual(['a', 'b', 'c']); + }); + }); + + describe('Hermes issues', () => { + // Hermes issue #575: for-in closure captures last value instead of per-iteration + it('for-in closure with Object.defineProperty (hermes#575)', () => { + const target = { x: 10, y: 20, z: 30 }; + const proxy = {}; + for (const key in target) { + Object.defineProperty(proxy, key, { + get: () => target[key], + enumerable: true, + }); + } + expect(proxy.x).toBe(10); + expect(proxy.y).toBe(20); + expect(proxy.z).toBe(30); + }); + + // Hermes issue #1599: variable shadowing in arrow function + it('block-scoped shadow does not affect outer closure (hermes#1599)', () => { + function repro(t) { + const lambda = () => { + // Under the Hermes bug, t would be undefined here due to + // the const t declaration in the if-block below + const result = t === 42; + if (result) { + const t = 69; + void t; // use it to prevent removal + } + return result; + }; + return lambda(); + } + expect(repro(42)).toBe(true); + }); + + // Hermes issue #1599 variant: deeper nesting + it('deeply nested shadow does not affect outer reference', () => { + function test(value) { + const check = () => { + const captured = value; + if (true) { + if (true) { + const value = 'shadow'; + void value; + } + } + return captured; + }; + return check(); + } + expect(test('original')).toBe('original'); + }); + + // Hermes issue #1599 variant: multiple shadows + it('multiple shadows in different blocks', () => { + function test(x) { + const fn = () => x; + { + const x = 'a'; + void x; + } + { + const x = 'b'; + void x; + } + return fn(); + } + expect(test(42)).toBe(42); + }); + }); + + describe('assignment and mutation', () => { + it('let allows reassignment', () => { + let x = 1; + x = 2; + expect(x).toBe(2); + }); + + it('const does not allow reassignment', () => { + expect(() => { + const x = 1; + x = 2; + }).toThrow(); + }); + + it('const object properties can be mutated', () => { + const obj = { a: 1 }; + obj.a = 2; + expect(obj.a).toBe(2); + }); + + it('const array elements can be mutated', () => { + const arr = [1, 2, 3]; + arr[0] = 10; + expect(arr[0]).toBe(10); + }); + + // From: exec/constant-violation.js + it('const += throws TypeError', () => { + expect(() => { + const a = 1; + a += 2; + }).toThrow(); + }); + + it('const++ throws TypeError', () => { + expect(() => { + const a = 1; + a++; + }).toThrow(); + }); + }); + + describe('edge cases', () => { + it('let in try-catch blocks', () => { + let x = 'outer'; + try { + let x = 'try'; + expect(x).toBe('try'); + throw new Error('test'); + } catch (e) { + let x = 'catch'; + expect(x).toBe('catch'); + } finally { + let x = 'finally'; + expect(x).toBe('finally'); + } + expect(x).toBe('outer'); + }); + + it('let in labeled block', () => { + let x = 'outer'; + block: { + let x = 'inner'; + expect(x).toBe('inner'); + break block; + } + expect(x).toBe('outer'); + }); + + it('const declaration in case block does not leak to other cases', () => { + function test(val) { + switch (val) { + case 1: { + const x = 'one'; + return x; + } + case 2: { + const x = 'two'; + return x; + } + default: { + const x = 'default'; + return x; + } + } + } + expect(test(1)).toBe('one'); + expect(test(2)).toBe('two'); + expect(test(3)).toBe('default'); + }); + + it('block scoping with comma operator', () => { + let x; + { + let a = 1, + b = 2; + x = a + b; + } + expect(x).toBe(3); + expect(typeof a).toBe('undefined'); + expect(typeof b).toBe('undefined'); + }); + + it('for loop with let and complex update expression', () => { + var results = []; + for (let i = 0; i < 6; i += 2) { + results.push(() => i); + } + expect(results[0]()).toBe(0); + expect(results[1]()).toBe(2); + expect(results[2]()).toBe(4); + }); + + it('immediately invoked closure in for loop captures correct value', () => { + var results = []; + for (let i = 0; i < 3; i++) { + results.push( + ((val) => () => val)(i) + ); + } + expect(results[0]()).toBe(0); + expect(results[1]()).toBe(1); + expect(results[2]()).toBe(2); + }); + + it('let in arrow function parameter default does not leak', () => { + let x = 'outer'; + const fn = (val = (() => { let x = 'default'; return x; })()) => val; + expect(fn()).toBe('default'); + expect(x).toBe('outer'); + }); + + it('nested for loops with same variable name', () => { + var outer = []; + var inner = []; + for (let i = 0; i < 3; i++) { + outer.push(() => i); + for (let i = 10; i < 13; i++) { + inner.push(() => i); + } + } + expect(outer[0]()).toBe(0); + expect(outer[1]()).toBe(1); + expect(outer[2]()).toBe(2); + expect(inner[0]()).toBe(10); + expect(inner[1]()).toBe(11); + expect(inner[2]()).toBe(12); + }); + + it('for-of with let and break', () => { + var funcs = []; + for (const x of [1, 2, 3, 4, 5]) { + funcs.push(() => x); + if (x === 3) break; + } + expect(funcs.length).toBe(3); + expect(funcs[0]()).toBe(1); + expect(funcs[1]()).toBe(2); + expect(funcs[2]()).toBe(3); + }); + + it('closure captures let after mutation within same iteration', () => { + var funcs = []; + for (let i = 0; i < 3; i++) { + let val = i; + val *= 10; + funcs.push(() => val); + } + expect(funcs[0]()).toBe(0); + expect(funcs[1]()).toBe(10); + expect(funcs[2]()).toBe(20); + }); + }); + }); +} diff --git a/apps/test-suite/tests/JSDestructuring.js b/apps/test-suite/tests/JSDestructuring.js new file mode 100644 index 00000000000000..75de98c47725b6 --- /dev/null +++ b/apps/test-suite/tests/JSDestructuring.js @@ -0,0 +1,1062 @@ +/* eslint-disable */ +'use strict'; + +// Comprehensive runtime compliance tests for ES6+ destructuring. +// Validates that destructuring works correctly at runtime in the Hermes engine. +// Cases sourced from @babel/plugin-transform-destructuring exec.js fixtures +// and supplemented with additional coverage for all destructuring forms. + +export const name = 'JS Destructuring'; + +export function test({ describe, it, expect }) { + describe('JS Destructuring', () => { + describe('object patterns', () => { + it('extracts basic properties', () => { + const { a, b } = { a: 1, b: 2, c: 3 }; + expect(a).toBe(1); + expect(b).toBe(2); + }); + + it('renames properties', () => { + const { a: x, b: y } = { a: 1, b: 2 }; + expect(x).toBe(1); + expect(y).toBe(2); + }); + + it('applies default values for missing properties', () => { + const { a = 10, b = 20 } = { a: 1 }; + expect(a).toBe(1); + expect(b).toBe(20); + }); + + it('applies default values only for undefined, not null', () => { + const { a = 10, b = 20 } = { a: null, b: undefined }; + expect(a).toBe(null); + expect(b).toBe(20); + }); + + it('renames with default values', () => { + const { a: x = 10, b: y = 20 } = { a: 1 }; + expect(x).toBe(1); + expect(y).toBe(20); + }); + + it('extracts computed property keys', () => { + const key = 'hello'; + const { [key]: val } = { hello: 42 }; + expect(val).toBe(42); + }); + + it('extracts computed property keys with side effects', () => { + let counter = 0; + const keyFn = () => 'k' + counter++; + const obj = { k0: 'a', k1: 'b' }; + const { [keyFn()]: first, [keyFn()]: second } = obj; + expect(first).toBe('a'); + expect(second).toBe('b'); + expect(counter).toBe(2); + }); + + it('collects rest properties', () => { + const { a, ...rest } = { a: 1, b: 2, c: 3 }; + expect(a).toBe(1); + expect(rest).toEqual({ b: 2, c: 3 }); + }); + + it('rest does not include extracted keys', () => { + const { x, y, ...rest } = { x: 1, y: 2, z: 3, w: 4 }; + expect(x).toBe(1); + expect(y).toBe(2); + expect(rest).toEqual({ z: 3, w: 4 }); + expect(rest.x).toBeUndefined(); + expect(rest.y).toBeUndefined(); + }); + + it('rest produces an empty object when all keys extracted', () => { + const { a, b, ...rest } = { a: 1, b: 2 }; + expect(a).toBe(1); + expect(b).toBe(2); + expect(rest).toEqual({}); + }); + + it('handles nested object patterns', () => { + const { + a: { b }, + } = { a: { b: 42 } }; + expect(b).toBe(42); + }); + + it('handles nested objects with defaults', () => { + const { + a: { b = 99 }, + } = { a: {} }; + expect(b).toBe(99); + }); + + it('handles shorthand for prototype properties', () => { + const { toString } = {}; + expect(typeof toString).toBe('function'); + }); + + it('handles string keys with special characters', () => { + const { 'foo-bar': val } = { 'foo-bar': 42 }; + expect(val).toBe(42); + }); + + it('handles numeric keys', () => { + const { 0: first, 1: second } = { 0: 'a', 1: 'b' }; + expect(first).toBe('a'); + expect(second).toBe('b'); + }); + + it('extracts from nested defaults at multiple levels', () => { + const { + a: { b: { c = 3 } = {} } = {}, + } = {}; + expect(c).toBe(3); + }); + + it('does not copy prototype properties into rest', () => { + function Parent() {} + Parent.prototype.inherited = true; + const obj = Object.create(Parent.prototype); + obj.own = 1; + const { own, ...rest } = obj; + expect(own).toBe(1); + expect(rest.inherited).toBeUndefined(); + }); + + // From: destructuring/number-key-with-object-rest-spread/exec.js + it('number key with object rest spread', () => { + const foo = { + 1: 'a', + 2: 'b', + 3: 'c', + }; + const { [1]: bar, ...rest } = foo; + expect(bar).toBe('a'); + expect(rest).toEqual({ 2: 'b', 3: 'c' }); + }); + + // From: destructuring/function-key-with-object-rest-spread/exec.js + it('function key with object rest spread', () => { + const { [(() => 1)()]: a, ...rest } = { 1: 'a' }; + expect(a).toBe('a'); + expect(rest).toEqual({}); + }); + + // From: destructuring/object-rest-impure-computed-keys/exec.js + it('object rest with impure computed keys', () => { + var key, x, y, z; + + // impure key + key = 1; + ({ [key++]: y, ...x } = { 1: 1, a: 1 }); + expect(x).toEqual({ a: 1 }); + expect(key).toBe(2); + expect(y).toBe(1); + + // takes care of the order + key = 1; + ({ [++key]: y, [++key]: z, ...x } = { 2: 2, 3: 3 }); + expect(y).toBe(2); + expect(z).toBe(3); + + // pure computed property should remain as-is + key = 2; + ({ [key]: y, z, ...x } = { 2: 'two', z: 'zee' }); + expect(y).toBe('two'); + expect(x).toEqual({}); + expect(z).toBe('zee'); + + // rhs evaluated before lhs + var order = []; + function left() { + order.push('left'); + return 0; + } + function right() { + order.push('right'); + return {}; + } + ({ [left()]: y, ...x } = right()); + expect(order).toEqual(['right', 'left']); + }); + + // From: destructuring/empty-object-pattern/exec.js + it('empty object pattern throws on null', () => { + expect(function () { + var {} = null; + }).toThrow(); + }); + }); + + describe('array patterns', () => { + it('extracts basic elements', () => { + const [a, b, c] = [1, 2, 3]; + expect(a).toBe(1); + expect(b).toBe(2); + expect(c).toBe(3); + }); + + it('skips elements with elision', () => { + const [, second, , fourth] = [1, 2, 3, 4]; + expect(second).toBe(2); + expect(fourth).toBe(4); + }); + + it('applies default values for missing elements', () => { + const [a = 10, b = 20, c = 30] = [1]; + expect(a).toBe(1); + expect(b).toBe(20); + expect(c).toBe(30); + }); + + it('applies default values only for undefined, not null or 0', () => { + const [a = 10, b = 20, c = 30] = [null, 0, undefined]; + expect(a).toBe(null); + expect(b).toBe(0); + expect(c).toBe(30); + }); + + it('collects rest elements', () => { + const [head, ...tail] = [1, 2, 3, 4]; + expect(head).toBe(1); + expect(tail).toEqual([2, 3, 4]); + }); + + it('rest produces empty array when no remaining elements', () => { + const [a, ...rest] = [1]; + expect(a).toBe(1); + expect(rest).toEqual([]); + }); + + it('handles nested array patterns', () => { + const [a, [b, c]] = [1, [2, 3]]; + expect(a).toBe(1); + expect(b).toBe(2); + expect(c).toBe(3); + }); + + it('destructures string as iterable', () => { + const [a, b, c] = 'xyz'; + expect(a).toBe('x'); + expect(b).toBe('y'); + expect(c).toBe('z'); + }); + + it('destructures string with rest', () => { + const [first, ...rest] = 'hello'; + expect(first).toBe('h'); + expect(rest).toEqual(['e', 'l', 'l', 'o']); + }); + + it('handles holes in source array', () => { + // eslint-disable-next-line no-sparse-arrays + const [a, b, c] = [1, , 3]; + expect(a).toBe(1); + expect(b).toBeUndefined(); + expect(c).toBe(3); + }); + + it('handles holes with defaults', () => { + // eslint-disable-next-line no-sparse-arrays + const [a = 10, b = 20, c = 30] = [, , 3]; + expect(a).toBe(10); + expect(b).toBe(20); + expect(c).toBe(3); + }); + + it('destructures Set iterable', () => { + const [a, b, c] = new Set([1, 2, 3]); + expect(a).toBe(1); + expect(b).toBe(2); + expect(c).toBe(3); + }); + + it('destructures Map iterable', () => { + const map = new Map(); + map.set('x', 1); + map.set('y', 2); + const [[k1, v1], [k2, v2]] = map; + expect(k1).toBe('x'); + expect(v1).toBe(1); + expect(k2).toBe('y'); + expect(v2).toBe(2); + }); + + it('destructures generator iterable', () => { + function* gen() { + yield 10; + yield 20; + yield 30; + } + const [a, b] = gen(); + expect(a).toBe(10); + expect(b).toBe(20); + }); + + it('rest with generator consumes remaining yields', () => { + function* gen() { + yield 1; + yield 2; + yield 3; + } + const [first, ...rest] = gen(); + expect(first).toBe(1); + expect(rest).toEqual([2, 3]); + }); + + it('destructures custom iterable', () => { + const iterable = { + [Symbol.iterator]() { + let i = 0; + return { + next() { + return i < 3 ? { value: i++, done: false } : { done: true }; + }, + }; + }, + }; + const [a, b, c] = iterable; + expect(a).toBe(0); + expect(b).toBe(1); + expect(c).toBe(2); + }); + + // From: destructuring/empty-string/exec.js + it('destructures empty string', () => { + let [a] = ''; + expect(a).toBe(undefined); + }); + + // From: destructuring/spread-generator/exec.js + it('spread from generator into rest', () => { + function* f() { + for (var i = 0; i < 3; i++) { + yield i; + } + } + var [...xs] = f(); + expect(xs).toEqual([0, 1, 2]); + }); + + // From: destructuring/inherited-array/exec.js + it('rest from extended array produces plain Array', () => { + class ExtendedArray extends Array { + constructor(...args) { + super(...args); + } + } + let extArr = new ExtendedArray(1, 2, 3, 4, 5); + let [first, second, ...rest] = extArr; + expect(first).toBe(1); + expect(second).toBe(2); + expect(rest).toEqual([3, 4, 5]); + expect(Object.getPrototypeOf(rest).constructor.name).toBe('Array'); + }); + + // From: destructuring/issue-5090/exec.js + it('array rest in params does not mutate original (issue-5090)', () => { + const assign = function ([...arr], index, value) { + arr[index] = value; + return arr; + }; + const arr = [1, 2, 3]; + const result = assign(arr, 1, 42); + expect(arr).toEqual([1, 2, 3]); + expect(result).toEqual([1, 42, 3]); + }); + + // From: destructuring/next-eval-once/exec.js + it('iterator .next getter is evaluated only once per destructuring', () => { + let gets = 0; + let it = { + [Symbol.iterator]: () => ({ + get next() { + gets++; + let i = 0; + return () => ({ done: false, value: i++ }); + }, + }), + }; + + // eslint-disable-next-line no-empty-pattern + let [] = it; + expect(gets).toBe(1); + + let [a] = it; + expect(gets).toBe(2); + expect(a).toBe(0); + + let [b, c] = it; + expect(gets).toBe(3); + expect(b).toBe(0); + expect(c).toBe(1); + }); + }); + + describe('mixed patterns', () => { + it('array pattern inside object', () => { + const { + items: [x, y], + } = { items: [1, 2] }; + expect(x).toBe(1); + expect(y).toBe(2); + }); + + it('object pattern inside array', () => { + const [{ a, b }] = [{ a: 1, b: 2 }]; + expect(a).toBe(1); + expect(b).toBe(2); + }); + + it('deeply nested mixed patterns', () => { + const data = { + users: [{ name: 'Alice', scores: [10, 20, 30] }], + }; + const { + users: [ + { + name, + scores: [first, , third], + }, + ], + } = data; + expect(name).toBe('Alice'); + expect(first).toBe(10); + expect(third).toBe(30); + }); + + it('object rest with nested array', () => { + const { + data: [head, ...tail], + ...meta + } = { + data: [1, 2, 3], + type: 'test', + version: 2, + }; + expect(head).toBe(1); + expect(tail).toEqual([2, 3]); + expect(meta).toEqual({ type: 'test', version: 2 }); + }); + }); + + describe('function parameters', () => { + it('object destructuring in function params', () => { + function greet({ name, greeting }) { + return greeting + ' ' + name; + } + expect(greet({ name: 'world', greeting: 'Hello' })).toBe('Hello world'); + }); + + it('object destructuring with defaults in params', () => { + function point({ x = 0, y = 0 }) { + return [x, y]; + } + expect(point({})).toEqual([0, 0]); + expect(point({ x: 5 })).toEqual([5, 0]); + expect(point({ x: 3, y: 4 })).toEqual([3, 4]); + }); + + it('array destructuring in function params', () => { + function first([a]) { + return a; + } + expect(first([42, 99])).toBe(42); + }); + + it('arrow function with object destructuring', () => { + const sum = ({ a, b }) => a + b; + expect(sum({ a: 3, b: 4 })).toBe(7); + }); + + it('arrow function with array destructuring', () => { + const head = ([first]) => first; + expect(head([10, 20, 30])).toBe(10); + }); + + it('nested destructuring in arrow params', () => { + const fn = ({ + data: { id, value = 0 }, + }) => id + value; + expect(fn({ data: { id: 10 } })).toBe(10); + expect(fn({ data: { id: 10, value: 5 } })).toBe(15); + }); + + it('rest in function params', () => { + function f({ required, ...options }) { + return { required, keys: Object.keys(options) }; + } + const result = f({ required: true, a: 1, b: 2 }); + expect(result.required).toBe(true); + expect(result.keys.sort()).toEqual(['a', 'b']); + }); + + it('default parameter value for entire destructured param', () => { + function f({ x, y } = { x: 0, y: 0 }) { + return x + y; + } + expect(f()).toBe(0); + expect(f({ x: 1, y: 2 })).toBe(3); + }); + + it('mixed defaults: param-level and property-level', () => { + function f({ x = 1, y = 2 } = {}) { + return x + y; + } + expect(f()).toBe(3); + expect(f({})).toBe(3); + expect(f({ x: 10 })).toBe(12); + }); + + // From: destructuring/default-precedence/exec.js + it('default value references previous params', () => { + var f0 = function (a, b = a, c = b) { + return [a, b, c]; + }; + expect(f0(1)).toEqual([1, 1, 1]); + + var f1 = function ({ a }, b = a, c = b) { + return [a, b, c]; + }; + expect(f1({ a: 1 })).toEqual([1, 1, 1]); + + var f2 = function ({ a }, b = a, c = a) { + return [a, b, c]; + }; + expect(f2({ a: 1 })).toEqual([1, 1, 1]); + }); + }); + + describe('loops', () => { + it('for-of with object destructuring', () => { + const items = [ + { id: 1, value: 'a' }, + { id: 2, value: 'b' }, + ]; + const ids = []; + for (const { id } of items) { + ids.push(id); + } + expect(ids).toEqual([1, 2]); + }); + + it('for-of with array destructuring', () => { + const pairs = [ + [1, 'a'], + [2, 'b'], + ]; + const keys = []; + const values = []; + for (const [k, v] of pairs) { + keys.push(k); + values.push(v); + } + expect(keys).toEqual([1, 2]); + expect(values).toEqual(['a', 'b']); + }); + + it('for-of with nested destructuring', () => { + const data = [ + { user: { name: 'Alice' }, scores: [10] }, + { user: { name: 'Bob' }, scores: [20] }, + ]; + const results = []; + for (const { + user: { name }, + scores: [score], + } of data) { + results.push({ name, score }); + } + expect(results).toEqual([ + { name: 'Alice', score: 10 }, + { name: 'Bob', score: 20 }, + ]); + }); + + it('for-of with Map entries', () => { + const map = new Map([ + ['x', 1], + ['y', 2], + ]); + const result = {}; + for (const [key, val] of map) { + result[key] = val; + } + expect(result).toEqual({ x: 1, y: 2 }); + }); + + // NOTE(@kitten): Broken test case + // From: destructuring/for-of-shadowed-block-scoped/exec.js + // @babel/plugin-transform-block-scoping bug: when it renames the inner + // `const a` to `_a` to avoid shadowing, it incorrectly also renames the + // computed key `[a]` in the for-of destructuring pattern — even though + // that `[a]` should reference the *outer* `a`. The renamed `_a` is + // hoisted as `undefined`, so `O[undefined]` yields `undefined`. + // When transform-destructuring ran first it resolved the computed key + // before block-scoping touched the inner declaration, masking the bug. + xit('for-of with computed key referencing outer scope despite inner shadow', () => { + var O = { a: 'a' }; + const a = 'a'; + var ran = false; + for (const { [a]: _ } of [O]) { + const a = 'A'; // shadows outer `a`, but computed key already evaluated + expect(_).toBe('a'); + ran = true; + } + expect(ran).toBe(true); + }); + }); + + describe('assignment expressions', () => { + it('object destructuring assignment', () => { + let a, b; + ({ a, b } = { a: 1, b: 2 }); + expect(a).toBe(1); + expect(b).toBe(2); + }); + + it('array destructuring assignment', () => { + let x, y; + [x, y] = [1, 2]; + expect(x).toBe(1); + expect(y).toBe(2); + }); + + it('swap via array destructuring', () => { + let a = 1, + b = 2; + [a, b] = [b, a]; + expect(a).toBe(2); + expect(b).toBe(1); + }); + + it('nested assignment', () => { + let name, score; + ({ + user: { name }, + scores: [score], + } = { user: { name: 'test' }, scores: [100] }); + expect(name).toBe('test'); + expect(score).toBe(100); + }); + + it('assignment with rest', () => { + let first, rest; + [first, ...rest] = [1, 2, 3, 4]; + expect(first).toBe(1); + expect(rest).toEqual([2, 3, 4]); + }); + + // From: destructuring/chained/exec.js + it('chained destructuring assignment', () => { + var a, b, c, d; + ({ a, b } = { c, d } = { a: 1, b: 2, c: 3, d: 4 }); + expect(a).toBe(1); + expect(b).toBe(2); + expect(c).toBe(3); + expect(d).toBe(4); + }); + }); + + describe('catch clause', () => { + it('destructures error object in catch', () => { + let msg, code; + try { + throw { message: 'fail', code: 42 }; + } catch ({ message, code: c }) { + msg = message; + code = c; + } + expect(msg).toBe('fail'); + expect(code).toBe(42); + }); + + it('destructures Error instance properties', () => { + let msg; + try { + throw new TypeError('bad type'); + } catch ({ message }) { + msg = message; + } + expect(msg).toBe('bad type'); + }); + }); + + describe('declaration variants', () => { + it('const with object destructuring', () => { + const { x, y } = { x: 1, y: 2 }; + expect(x).toBe(1); + expect(y).toBe(2); + }); + + it('let with array destructuring', () => { + let [a, b] = [1, 2]; + expect(a).toBe(1); + expect(b).toBe(2); + // let allows reassignment + [a, b] = [3, 4]; + expect(a).toBe(3); + expect(b).toBe(4); + }); + + it('var with multiple destructuring patterns', () => { + var { a } = { a: 1 }, + [b] = [2]; + expect(a).toBe(1); + expect(b).toBe(2); + }); + + // From: destructuring/const/exec.js + it('const with nested defaults from function return', () => { + const getState = () => ({}); + const { + data: { courses: oldCourses = [] } = {}, + } = getState(); + expect(oldCourses).toEqual([]); + }); + }); + + // From: destructuring/check-iterator-return/exec.js + describe('iterator protocol', () => { + it('empty pattern calls iterator.return when return() returns object', () => { + var returnCalled = false; + // eslint-disable-next-line no-empty-pattern + var [] = { + [Symbol.iterator]: () => { + return { + return: () => { + returnCalled = true; + return {}; + }, + }; + }, + }; + expect(returnCalled).toBe(true); + }); + + it('empty pattern throws if iterator.return() returns non-object', () => { + expect(() => { + // eslint-disable-next-line no-empty-pattern + var [] = { + [Symbol.iterator]: () => { + return { + return: () => {}, + }; + }, + }; + }).toThrow(); + }); + + it('single-element pattern throws if iterator.return() returns non-object', () => { + expect(() => { + var [x] = { + [Symbol.iterator]: () => { + return { + next: () => ({ done: false, value: 1 }), + return: () => {}, + }; + }, + }; + }).toThrow(); + }); + + it('single-element pattern does not throw if iterator.return() returns object', () => { + expect(() => { + var [x] = { + [Symbol.iterator]: () => { + return { + next: () => ({ done: false, value: 1 }), + return: () => ({}), + }; + }, + }; + }).not.toThrow(); + }); + }); + + // From: destructuring/init-hole/exec.js + describe('holes and sparse arrays', () => { + it('default with explicit hole (let)', () => { + // eslint-disable-next-line no-sparse-arrays + let [x = 23] = [,]; + expect(x).toBe(23); + }); + + it('default with hole and trailing value (const)', () => { + // eslint-disable-next-line no-sparse-arrays + const [y = 24, z] = [, 42]; + expect(y).toBe(24); + expect(z).toBe(42); + }); + + it('default interleaves with generator evaluation order', () => { + function* foo() { + yield 1; + yield 2; + } + let bar = foo(); + // The default for `a` is bar.next().value, but it only runs because + // position 0 is a hole. By the time it runs, bar has already yielded 1 + // (for position 1), so the default produces 2. + // eslint-disable-next-line no-sparse-arrays + const [a = bar.next().value, b] = [, bar.next().value]; + expect(a).toBe(2); + expect(b).toBe(1); + }); + + it('assignment pattern with hole', () => { + var c; + // eslint-disable-next-line no-sparse-arrays + const arr = ([c = 42] = [,]); + expect(c).toBe(42); + // eslint-disable-next-line no-sparse-arrays + expect(arr).toEqual([,]); + }); + + it('rest from sparse array fills holes with undefined', () => { + // eslint-disable-next-line no-sparse-arrays + const [...d] = [,]; + expect(d).toEqual([undefined]); + }); + + it('rest with object pattern from sparse array', () => { + // eslint-disable-next-line no-sparse-arrays + const [...{ 0: e }] = [,]; + expect(e).toEqual(undefined); + }); + + it('single element from sparse array is undefined', () => { + // eslint-disable-next-line no-sparse-arrays + const [f] = [,]; + expect(f).toEqual(undefined); + // eslint-disable-next-line no-sparse-arrays + let [g] = [,]; + expect(g).toEqual(undefined); + }); + + it('object pattern from hole throws', () => { + var thrown; + try { + thrown = false; + // eslint-disable-next-line no-sparse-arrays + [{}] = [,]; + } catch (e) { + thrown = true; + } + expect(thrown).toBe(true); + }); + + it('array pattern from hole throws', () => { + var thrown; + try { + thrown = false; + // eslint-disable-next-line no-sparse-arrays + [[]] = [,]; + } catch (e) { + thrown = true; + } + expect(thrown).toBe(true); + }); + }); + + // From: destructuring/empty-array-pattern/exec.js + describe('empty array pattern', () => { + it('throws on null', () => { + expect(() => { + // eslint-disable-next-line no-empty-pattern + var [] = null; + }).toThrow(); + }); + + it('throws on number', () => { + expect(() => { + // eslint-disable-next-line no-empty-pattern + var [] = 42; + }).toThrow(); + }); + + it('throws on plain object', () => { + expect(() => { + // eslint-disable-next-line no-empty-pattern + var [] = {}; + }).toThrow(); + }); + + it('throws if Symbol.iterator returns non-object', () => { + expect(() => { + // eslint-disable-next-line no-empty-pattern + var [] = { [Symbol.iterator]: () => {} }; + }).toThrow(); + }); + + it('does not throw on valid iterables', () => { + expect(() => { + // eslint-disable-next-line no-empty-pattern + var [] = []; + // eslint-disable-next-line no-empty-pattern + var [] = [0, 1, 2]; + // eslint-disable-next-line no-empty-pattern + var [] = 'foo'; + // eslint-disable-next-line no-empty-pattern + var [] = (function* () { + throw new Error('Should not throw'); + })(); + // eslint-disable-next-line no-empty-pattern + var [] = { [Symbol.iterator]: () => ({}) }; + }).not.toThrow(); + }); + + it('calls iterator.return on empty pattern', () => { + var returnCalled = false; + // eslint-disable-next-line no-empty-pattern + var [] = { + [Symbol.iterator]: () => { + return { + return: () => { + returnCalled = true; + return {}; + }, + }; + }, + }; + expect(returnCalled).toBe(true); + }); + }); + + // From: destructuring/non-iterable/exec.js + describe('non-iterable errors', () => { + it('throws on undefined', () => { + var foo; + expect(() => { + [foo] = undefined; + }).toThrow(); + }); + + it('throws on plain object', () => { + var foo; + expect(() => { + [foo] = {}; + }).toThrow(); + }); + }); + + describe('edge cases', () => { + it('empty object pattern does not throw on object', () => { + expect(() => { + // eslint-disable-next-line no-empty-pattern + const {} = { a: 1 }; + }).not.toThrow(); + }); + + it('empty array pattern does not throw on array', () => { + expect(() => { + // eslint-disable-next-line no-empty-pattern + const [] = [1, 2]; + }).not.toThrow(); + }); + + it('destructuring null throws TypeError', () => { + expect(() => { + const { a } = null; + }).toThrow(); + }); + + it('destructuring undefined throws TypeError', () => { + expect(() => { + const { a } = undefined; + }).toThrow(); + }); + + it('array destructuring from non-iterable throws', () => { + expect(() => { + const [a] = 42; + }).toThrow(); + }); + + it('default value expression is lazily evaluated', () => { + let evaluated = false; + const { a = (evaluated = true) } = { a: 1 }; + expect(a).toBe(1); + expect(evaluated).toBe(false); + }); + + it('default value expression evaluated when value is undefined', () => { + let count = 0; + const { a = ++count, b = ++count } = { a: undefined }; + expect(a).toBe(1); + expect(b).toBe(2); + expect(count).toBe(2); + }); + + it('array default value expression is lazily evaluated', () => { + let evaluated = false; + const [a = (evaluated = true)] = [42]; + expect(a).toBe(42); + expect(evaluated).toBe(false); + }); + + it('evaluation order matches spec for object destructuring', () => { + const order = []; + const obj = { + get a() { + order.push('get a'); + return 1; + }, + get b() { + order.push('get b'); + return 2; + }, + }; + const { a, b } = obj; + expect(a).toBe(1); + expect(b).toBe(2); + expect(order).toEqual(['get a', 'get b']); + }); + + it('rest creates a shallow copy', () => { + const inner = { value: 1 }; + const { a, ...rest } = { a: 1, b: inner }; + expect(rest.b).toBe(inner); + inner.value = 2; + expect(rest.b.value).toBe(2); + }); + + it('rest with symbol keys are included', () => { + const sym = Symbol('test'); + const obj = { a: 1, [sym]: 2 }; + const { a, ...rest } = obj; + expect(a).toBe(1); + expect(rest[sym]).toBe(2); + }); + + it('destructuring assignment returns the right-hand value', () => { + let a, b; + const result = ([a, b] = [1, 2]); + expect(result).toEqual([1, 2]); + expect(a).toBe(1); + expect(b).toBe(2); + }); + + it('array rest with object pattern', () => { + const [first, ...{ length }] = [1, 2, 3]; + expect(first).toBe(1); + expect(length).toBe(2); + }); + + it('handles large number of properties', () => { + const obj = {}; + for (let i = 0; i < 100; i++) { + obj['k' + i] = i; + } + const { k0, k50, k99, ...rest } = obj; + expect(k0).toBe(0); + expect(k50).toBe(50); + expect(k99).toBe(99); + expect(Object.keys(rest).length).toBe(97); + }); + }); + }); +} diff --git a/apps/test-suite/tests/JSNamedGroupsRegexes.js b/apps/test-suite/tests/JSNamedGroupsRegexes.js new file mode 100644 index 00000000000000..414dcc0ebcc13b --- /dev/null +++ b/apps/test-suite/tests/JSNamedGroupsRegexes.js @@ -0,0 +1,492 @@ +/* eslint-disable */ +'use strict'; + +// Comprehensive runtime compliance tests for named capturing groups in regexes. +// Validates that (?...) syntax works correctly at runtime in the Hermes engine. +// Cases sourced from @babel/plugin-transform-named-capturing-groups-regex +// and supplemented with edge cases. + +export const name = 'JS Named Groups Regexes'; + +export function test({ describe, it, xit, expect }) { + describe('JS Named Groups Regexes', () => { + describe('basic named groups', () => { + it('exec() returns groups object', () => { + const re = /(?\d{4})-(?\d{2})-(?\d{2})/; + const match = re.exec('2025-05-01'); + expect(match).not.toBe(null); + expect(match.groups.year).toBe('2025'); + expect(match.groups.month).toBe('05'); + expect(match.groups.day).toBe('01'); + }); + + it('match() returns groups object', () => { + const re = /(?hello) (?world)/; + const match = 'hello world'.match(re); + expect(match.groups.greeting).toBe('hello'); + expect(match.groups.target).toBe('world'); + }); + + it('single named group', () => { + const re = /(?\w+)/; + const match = re.exec('foobar'); + expect(match.groups.name).toBe('foobar'); + }); + + it('named group with no match returns null', () => { + const re = /(?\d{4})/; + const match = re.exec('no digits here'); + expect(match).toBe(null); + }); + + it('groups is a null-prototype object', () => { + const re = /(?x)/; + const match = re.exec('x'); + expect(match.groups.a).toBe('x'); + // groups should not inherit from Object.prototype + expect(match.groups.hasOwnProperty).toBe(undefined); + }); + + it('numbered capture still works alongside named', () => { + const re = /(?\w+) (\w+)/; + const match = re.exec('hello world'); + expect(match[1]).toBe('hello'); + expect(match[2]).toBe('world'); + expect(match.groups.first).toBe('hello'); + }); + }); + + describe('multiple named groups', () => { + it('three groups', () => { + const re = /(?\d+)\.(?\d+)\.(?\d+)/; + const match = re.exec('255.128.0'); + expect(match.groups.r).toBe('255'); + expect(match.groups.g).toBe('128'); + expect(match.groups.b).toBe('0'); + }); + + it('groups with different character classes', () => { + const re = /(?[a-z]+)(?\d+)(?.*)/; + const match = re.exec('abc123!@#'); + expect(match.groups.letters).toBe('abc'); + expect(match.groups.digits).toBe('123'); + expect(match.groups.rest).toBe('!@#'); + }); + + it('five groups', () => { + const re = /(?\w+):\/\/(?[^/:]+)(?::(?\d+))?(?\/[^?]*)?(?:\?(?.*))?/; + const match = re.exec('https://example.com:8080/path?q=1'); + expect(match.groups.proto).toBe('https'); + expect(match.groups.host).toBe('example.com'); + expect(match.groups.port).toBe('8080'); + expect(match.groups.path).toBe('/path'); + expect(match.groups.query).toBe('q=1'); + }); + }); + + describe('backreferences', () => { + it('\\k matches same text as named group', () => { + const re = /(?['"]).*?\k/; + expect(re.test("'hello'")).toBe(true); + expect(re.test('"hello"')).toBe(true); + expect(re.test("'hello\"")).toBe(false); + }); + + it('\\k with exec', () => { + const re = /^(?\w+).*\k$/; + const match = re.exec('div content div'); + expect(match).not.toBe(null); + expect(match.groups.tag).toBe('div'); + }); + + it('\\k fails when group content differs', () => { + const re = /^(?\w+) \k$/; + expect(re.test('abc abc')).toBe(true); + expect(re.test('abc def')).toBe(false); + }); + + it('multiple backreferences', () => { + const re = /(?\w)(?\w) \k\k/; + const match = re.exec('ab ab'); + expect(match).not.toBe(null); + expect(match.groups.a).toBe('a'); + expect(match.groups.b).toBe('b'); + }); + }); + + describe('string replace with named groups', () => { + it('$ in replacement string', () => { + const re = /(?\w+) (?\w+)/; + const result = 'hello world'.replace(re, '$ $'); + expect(result).toBe('world hello'); + }); + + it('$ with date reformatting', () => { + const re = /(?\d{2})\/(?\d{2})\/(?\d{4})/; + const result = '05/01/2025'.replace(re, '$-$-$'); + expect(result).toBe('2025-05-01'); + }); + + it('$ with global flag replaces all', () => { + const re = /(?[aeiou])/g; + const result = 'hello'.replace(re, '[$]'); + expect(result).toBe('h[e]ll[o]'); + }); + + it('$ produces empty string', () => { + const re = /(?x)/; + const result = 'x'.replace(re, '$$'); + // $ doesn't match a named group, should produce empty string + expect(result).toBe('x'); + }); + + it('replace function receives groups as last argument', () => { + const re = /(?\w+) (?\w+)/; + let capturedGroups = null; + 'hello world'.replace(re, function () { + capturedGroups = arguments[arguments.length - 1]; + }); + expect(capturedGroups).not.toBe(null); + expect(capturedGroups.first).toBe('hello'); + expect(capturedGroups.second).toBe('world'); + }); + + it('replace function return value is used', () => { + const re = /(?\d+)/g; + const result = 'a1b2c3'.replace(re, function (match, n, offset, str, groups) { + return String(Number(groups.n) * 2); + }); + expect(result).toBe('a2b4c6'); + }); + + it('numbered references $1 $2 still work', () => { + const re = /(?\w+) (?\w+)/; + const result = 'hello world'.replace(re, '$2 $1'); + expect(result).toBe('world hello'); + }); + }); + + describe('test() method', () => { + it('test() works with named groups', () => { + const re = /(?\d{4})/; + expect(re.test('2025')).toBe(true); + expect(re.test('nope')).toBe(false); + }); + + it('test() with backreference', () => { + const re = /(?.)\k/; + expect(re.test('aa')).toBe(true); + expect(re.test('ab')).toBe(false); + }); + + it('test() with global flag advances lastIndex', () => { + const re = /(?\d)/g; + const str = 'a1b2c3'; + expect(re.test(str)).toBe(true); + expect(re.lastIndex).toBe(2); + expect(re.test(str)).toBe(true); + expect(re.lastIndex).toBe(4); + }); + }); + + describe('flags', () => { + it('case-insensitive flag', () => { + const re = /(?[a-z]+)/i; + const match = re.exec('HELLO'); + expect(match.groups.word).toBe('HELLO'); + }); + + it('global flag with exec iteration', () => { + const re = /(?\d+)/g; + const str = 'a1b22c333'; + const results = []; + let m; + while ((m = re.exec(str)) !== null) { + results.push(m.groups.num); + } + expect(results).toEqual(['1', '22', '333']); + }); + + it('multiline flag', () => { + const re = /^(?\w+)/gm; + const str = 'hello\nworld\nfoo'; + const results = []; + let m; + while ((m = re.exec(str)) !== null) { + results.push(m.groups.first); + } + expect(results).toEqual(['hello', 'world', 'foo']); + }); + + it('dotAll flag', () => { + const re = /(?.*)/s; + const match = re.exec('line1\nline2'); + expect(match.groups.all).toBe('line1\nline2'); + }); + + it('unicode flag', () => { + const re = /(?.)/u; + const match = re.exec('\u{1F600}'); + expect(match.groups.emoji).toBe('\u{1F600}'); + }); + + it('sticky flag', () => { + const re = /(?\w)/y; + re.lastIndex = 2; + const match = re.exec('abcdef'); + expect(match.groups.ch).toBe('c'); + }); + }); + + describe('matchAll', () => { + // TODO(@kitten): _wrapRegExp helper does not override [Symbol.matchAll]. + // matchAll creates a regex copy via `new this.constructor(R, flags)` which + // drops the group mapping (3rd arg), so buildGroups() fails on the copy. + xit('matchAll returns groups on each match', () => { + const re = /(?\d+)/g; + const matches = [...'a1b22c333'.matchAll(re)]; + expect(matches.length).toBe(3); + expect(matches[0].groups.n).toBe('1'); + expect(matches[1].groups.n).toBe('22'); + expect(matches[2].groups.n).toBe('333'); + }); + + // TODO(@kitten): Same _wrapRegExp matchAll limitation as above. + xit('matchAll with multiple named groups', () => { + const re = /(?\w+)=(?\w+)/g; + const matches = [...'a=1&b=2&c=3'.matchAll(re)]; + expect(matches.length).toBe(3); + expect(matches[0].groups.key).toBe('a'); + expect(matches[0].groups.val).toBe('1'); + expect(matches[2].groups.key).toBe('c'); + expect(matches[2].groups.val).toBe('3'); + }); + }); + + describe('optional and alternation groups', () => { + it('optional named group returns undefined when unmatched', () => { + const re = /(?\w+)(?:\.(?\w+))?/; + const match = re.exec('readme'); + expect(match.groups.required).toBe('readme'); + expect(match.groups.ext).toBe(undefined); + }); + + it('optional named group returns value when matched', () => { + const re = /(?\w+)(?:\.(?\w+))?/; + const match = re.exec('readme.md'); + expect(match.groups.required).toBe('readme'); + expect(match.groups.ext).toBe('md'); + }); + + it('alternation with named groups', () => { + const re = /(?#[0-9a-f]{6})|(?rgb\(\d+,\s*\d+,\s*\d+\))/i; + const hexMatch = re.exec('#ff0000'); + expect(hexMatch.groups.hex).toBe('#ff0000'); + expect(hexMatch.groups.rgb).toBe(undefined); + + const rgbMatch = re.exec('rgb(255, 0, 0)'); + expect(rgbMatch.groups.rgb).toBe('rgb(255, 0, 0)'); + expect(rgbMatch.groups.hex).toBe(undefined); + }); + + it('named group in one branch of alternation', () => { + const re = /(?:(?foo)|bar)/; + const m1 = re.exec('foo'); + expect(m1.groups.a).toBe('foo'); + const m2 = re.exec('bar'); + expect(m2.groups.a).toBe(undefined); + }); + }); + + describe('mixed named and unnamed groups', () => { + it('unnamed groups do not appear in groups object', () => { + const re = /(\d+)-(?\w+)/; + const match = re.exec('123-abc'); + expect(match[1]).toBe('123'); + expect(match[2]).toBe('abc'); + expect(match.groups.named).toBe('abc'); + expect(Object.keys(match.groups).length).toBe(1); + }); + + it('non-capturing groups are transparent', () => { + const re = /(?:prefix_)(?\w+)/; + const match = re.exec('prefix_hello'); + expect(match.groups.value).toBe('hello'); + expect(match[1]).toBe('hello'); + }); + + it('nested named group inside unnamed group', () => { + const re = /((?\d+))/; + const match = re.exec('42'); + expect(match[1]).toBe('42'); + expect(match.groups.inner).toBe('42'); + }); + }); + + describe('string search and split', () => { + it('search() works with named groups', () => { + const re = /(?\bhello\b)/; + const idx = 'say hello there'.search(re); + expect(idx).toBe(4); + }); + + it('split() with named capturing group retains captures', () => { + const re = /(?[,;])/; + const parts = 'a,b;c'.split(re); + // split includes capturing group matches + expect(parts).toEqual(['a', ',', 'b', ';', 'c']); + }); + }); + + describe('destructuring groups', () => { + it('destructure groups from exec result', () => { + const re = /(?\d{4})-(?\d{2})-(?\d{2})/; + const { groups: { year, month, day } } = re.exec('2025-05-01'); + expect(year).toBe('2025'); + expect(month).toBe('05'); + expect(day).toBe('01'); + }); + + it('destructure with default for optional group', () => { + const re = /(?[^:]+)(?::(?\d+))?/; + const { groups: { host, port = '80' } } = re.exec('example.com'); + expect(host).toBe('example.com'); + expect(port).toBe('80'); + }); + }); + + describe('edge cases', () => { + it('empty string match', () => { + const re = /(?)/; + const match = re.exec('anything'); + expect(match.groups.empty).toBe(''); + }); + + it('group name with underscores and digits', () => { + const re = /(?\w+)/; + const match = re.exec('hello'); + expect(match.groups.my_group_1).toBe('hello'); + }); + + it('group name with dollar sign', () => { + const re = /(?<$val>\d+)/; + const match = re.exec('42'); + expect(match.groups.$val).toBe('42'); + }); + + it('very long match in named group', () => { + const long = 'a'.repeat(10000); + const re = /(?.+)/s; + const match = re.exec(long); + expect(match.groups.all.length).toBe(10000); + }); + + it('named group with quantifier', () => { + const re = /(?\d+)/; + const match = re.exec('abc123def'); + expect(match.groups.digits).toBe('123'); + }); + + it('named group captures last iteration of quantified group', () => { + const re = /(?:(?\w))+/; + const match = re.exec('abcd'); + // A repeated capturing group only keeps the last match + expect(match.groups.ch).toBe('d'); + }); + + it('lookahead does not interfere with named groups', () => { + const re = /(?\w+)(?=\s)/; + const match = re.exec('hello world'); + expect(match.groups.word).toBe('hello'); + expect(match[0]).toBe('hello'); + }); + + it('lookbehind does not interfere with named groups', () => { + const re = /(?<=\s)(?\w+)/; + const match = re.exec('hello world'); + expect(match.groups.word).toBe('world'); + }); + + it('regex constructed with new RegExp also supports named groups', () => { + const re = new RegExp('(?\\d+)'); + const match = re.exec('42'); + expect(match.groups.n).toBe('42'); + }); + + // TODO(@kitten): _wrapRegExp [Symbol.replace] processes $ before + // the native replace handles $$ escaping. It converts $$ → $$1, which + // native replace then interprets as literal "$" + "1" → "$1". + // Native named groups correctly treat $$ as literal $ first, leaving "" + // as literal text → "$". + xit('named group in replace with special characters in replacement', () => { + const re = /(?\d+)/; + const result = '42'.replace(re, '$$'); + // $$ is a literal $, so result should be literal "$" + expect(result).toBe('$'); + }); + + it('incomplete $< in replacement is literal', () => { + const re = /(?x)/; + // $< without closing > is treated as literal by the wrapRegExp helper + const result = 'x'.replace(re, '$ { + it('parse semver version string', () => { + const re = /^(?\d+)\.(?\d+)\.(?\d+)(?:-(?
[a-z0-9.]+))?(?:\+(?[a-z0-9.]+))?$/i;
+        const m1 = re.exec('1.2.3');
+        expect(m1.groups.major).toBe('1');
+        expect(m1.groups.minor).toBe('2');
+        expect(m1.groups.patch).toBe('3');
+        expect(m1.groups.pre).toBe(undefined);
+
+        const m2 = re.exec('1.0.0-beta.1+build.42');
+        expect(m2.groups.pre).toBe('beta.1');
+        expect(m2.groups.build).toBe('build.42');
+      });
+
+      it('parse email-like address', () => {
+        const re = /(?[^@]+)@(?[^.]+)\.(?\w+)/;
+        const match = re.exec('user@example.com');
+        expect(match.groups.user).toBe('user');
+        expect(match.groups.domain).toBe('example');
+        expect(match.groups.tld).toBe('com');
+      });
+
+      it('extract key-value pairs from query string', () => {
+        const re = /(?[^=&]+)=(?[^&]*)/g;
+        const str = 'foo=bar&baz=qux&n=42';
+        const pairs = {};
+        let m;
+        while ((m = re.exec(str)) !== null) {
+          pairs[m.groups.key] = m.groups.value;
+        }
+        expect(pairs).toEqual({ foo: 'bar', baz: 'qux', n: '42' });
+      });
+
+      it('reformat dates with replace', () => {
+        const re = /(?\d{4})-(?\d{2})-(?\d{2})/g;
+        const result = 'From 2025-05-01 to 2025-12-31'.replace(re, '$/$/$');
+        expect(result).toBe('From 01/05/2025 to 31/12/2025');
+      });
+
+      it('CSS hex color extraction', () => {
+        const re = /#(?[0-9a-f]{2})(?[0-9a-f]{2})(?[0-9a-f]{2})/i;
+        const match = re.exec('#FF8800');
+        expect(match.groups.r).toBe('FF');
+        expect(match.groups.g).toBe('88');
+        expect(match.groups.b).toBe('00');
+      });
+
+      it('log line parsing', () => {
+        const re = /\[(?\w+)\] (?\d{4}-\d{2}-\d{2}T[\d:]+) (?.*)/;
+        const match = re.exec('[ERROR] 2025-05-01T12:00:00 Something failed');
+        expect(match.groups.level).toBe('ERROR');
+        expect(match.groups.ts).toBe('2025-05-01T12:00:00');
+        expect(match.groups.msg).toBe('Something failed');
+      });
+    });
+  });
+}
diff --git a/apps/test-suite/tests/JSNullishCoalescing.js b/apps/test-suite/tests/JSNullishCoalescing.js
new file mode 100644
index 00000000000000..42406282ef6935
--- /dev/null
+++ b/apps/test-suite/tests/JSNullishCoalescing.js
@@ -0,0 +1,657 @@
+/* eslint-disable */
+'use strict';
+
+// Comprehensive runtime compliance tests for the nullish coalescing operator (??).
+// Validates that ?? works correctly at runtime in the Hermes engine.
+//
+// @babel/plugin-transform-nullish-coalescing-operator rewrites `a ?? b` to:
+//   Spec mode:  (_a = a) !== null && _a !== void 0 ? _a : b
+//   Loose mode: (_a = a) != null ? _a : b
+//
+// The key semantics: ?? only falls through on null/undefined, NOT on other
+// falsy values (0, "", false, NaN). This is the critical difference from ||.
+//
+// Cases sourced from @babel/plugin-transform-nullish-coalescing-operator
+// and supplemented with edge cases.
+
+export const name = 'JS Nullish Coalescing';
+
+export function test({ describe, it, xit, expect }) {
+  describe('JS Nullish Coalescing', () => {
+    describe('basic semantics', () => {
+      it('returns left side when not null or undefined', () => {
+        expect('hello' ?? 'default').toBe('hello');
+        expect(42 ?? 0).toBe(42);
+        expect(true ?? false).toBe(true);
+      });
+
+      it('returns right side when left is null', () => {
+        expect(null ?? 'default').toBe('default');
+      });
+
+      it('returns right side when left is undefined', () => {
+        expect(undefined ?? 'default').toBe('default');
+      });
+
+      it('returns right side when left is void 0', () => {
+        expect(void 0 ?? 'default').toBe('default');
+      });
+
+      it('preserves falsy non-nullish values', () => {
+        expect(0 ?? 'default').toBe(0);
+        expect('' ?? 'default').toBe('');
+        expect(false ?? 'default').toBe(false);
+        expect(NaN ?? 'default').not.toBe('default');
+      });
+
+      it('returns -0 without falling through', () => {
+        const result = -0 ?? 'default';
+        expect(result).toBe(-0);
+        expect(1 / result).toBe(-Infinity);
+      });
+    });
+
+    describe('difference from || operator', () => {
+      it('?? keeps 0, || does not', () => {
+        expect(0 ?? 'fallback').toBe(0);
+        expect(0 || 'fallback').toBe('fallback');
+      });
+
+      it('?? keeps empty string, || does not', () => {
+        expect('' ?? 'fallback').toBe('');
+        expect('' || 'fallback').toBe('fallback');
+      });
+
+      it('?? keeps false, || does not', () => {
+        expect(false ?? 'fallback').toBe(false);
+        expect(false || 'fallback').toBe('fallback');
+      });
+
+      it('?? keeps NaN, || does not', () => {
+        const qqResult = NaN ?? 'fallback';
+        const orResult = NaN || 'fallback';
+        expect(typeof qqResult).toBe('number');
+        expect(orResult).toBe('fallback');
+      });
+
+      it('both fall through on null', () => {
+        expect(null ?? 'fallback').toBe('fallback');
+        expect(null || 'fallback').toBe('fallback');
+      });
+
+      it('both fall through on undefined', () => {
+        expect(undefined ?? 'fallback').toBe('fallback');
+        expect(undefined || 'fallback').toBe('fallback');
+      });
+    });
+
+    describe('chaining', () => {
+      it('multiple ?? operators', () => {
+        expect(null ?? undefined ?? 'found').toBe('found');
+      });
+
+      it('first non-nullish wins in chain', () => {
+        expect(null ?? 0 ?? 'fallback').toBe(0);
+      });
+
+      it('all null/undefined falls through to last', () => {
+        expect(null ?? undefined ?? null ?? 'end').toBe('end');
+      });
+
+      it('first value returned if not nullish', () => {
+        expect('first' ?? 'second' ?? 'third').toBe('first');
+      });
+
+      it('chain with false', () => {
+        expect(null ?? false ?? 'fallback').toBe(false);
+      });
+    });
+
+    describe('with member expressions', () => {
+      it('property access on object', () => {
+        const obj = { foo: 'bar' };
+        expect(obj.foo ?? 'default').toBe('bar');
+      });
+
+      it('missing property returns default', () => {
+        const obj = {};
+        expect(obj.foo ?? 'default').toBe('default');
+      });
+
+      it('null property value returns default', () => {
+        const obj = { foo: null };
+        expect(obj.foo ?? 'default').toBe('default');
+      });
+
+      it('falsy property value is preserved', () => {
+        const obj = { count: 0, name: '', active: false };
+        expect(obj.count ?? 99).toBe(0);
+        expect(obj.name ?? 'anonymous').toBe('');
+        expect(obj.active ?? true).toBe(false);
+      });
+
+      it('nested property access', () => {
+        const obj = { a: { b: { c: null } } };
+        expect(obj.a.b.c ?? 'default').toBe('default');
+      });
+
+      it('computed property access', () => {
+        const obj = { key: 'value' };
+        const key = 'key';
+        expect(obj[key] ?? 'default').toBe('value');
+        expect(obj['missing'] ?? 'default').toBe('default');
+      });
+
+      it('array element access', () => {
+        const arr = [1, null, undefined, 0];
+        expect(arr[0] ?? 'default').toBe(1);
+        expect(arr[1] ?? 'default').toBe('default');
+        expect(arr[2] ?? 'default').toBe('default');
+        expect(arr[3] ?? 'default').toBe(0);
+        expect(arr[99] ?? 'default').toBe('default');
+      });
+    });
+
+    describe('with function calls', () => {
+      it('function returning null', () => {
+        const fn = () => null;
+        expect(fn() ?? 'default').toBe('default');
+      });
+
+      it('function returning undefined', () => {
+        const fn = () => undefined;
+        expect(fn() ?? 'default').toBe('default');
+      });
+
+      it('function returning value', () => {
+        const fn = () => 42;
+        expect(fn() ?? 'default').toBe(42);
+      });
+
+      it('function returning falsy non-nullish', () => {
+        expect((() => 0)() ?? 'default').toBe(0);
+        expect((() => '')() ?? 'default').toBe('');
+        expect((() => false)() ?? 'default').toBe(false);
+      });
+
+      it('left side evaluated only once', () => {
+        let count = 0;
+        const fn = () => { count++; return null; };
+        fn() ?? 'default';
+        expect(count).toBe(1);
+      });
+
+      it('right side not evaluated when left is non-nullish', () => {
+        let evaluated = false;
+        const right = () => { evaluated = true; return 'right'; };
+        'left' ?? right();
+        expect(evaluated).toBe(false);
+      });
+
+      it('right side evaluated when left is nullish', () => {
+        let evaluated = false;
+        const right = () => { evaluated = true; return 'right'; };
+        const result = null ?? right();
+        expect(evaluated).toBe(true);
+        expect(result).toBe('right');
+      });
+    });
+
+    describe('short-circuit evaluation', () => {
+      it('does not evaluate right side when left is 0', () => {
+        let sideEffect = false;
+        const result = 0 ?? (sideEffect = true, 'fallback');
+        expect(result).toBe(0);
+        expect(sideEffect).toBe(false);
+      });
+
+      it('does not evaluate right side when left is empty string', () => {
+        let sideEffect = false;
+        const result = '' ?? (sideEffect = true, 'fallback');
+        expect(result).toBe('');
+        expect(sideEffect).toBe(false);
+      });
+
+      it('does not evaluate right side when left is false', () => {
+        let sideEffect = false;
+        const result = false ?? (sideEffect = true, 'fallback');
+        expect(result).toBe(false);
+        expect(sideEffect).toBe(false);
+      });
+
+      it('evaluates right side when left is null', () => {
+        let sideEffect = false;
+        const result = null ?? (sideEffect = true, 'fallback');
+        expect(result).toBe('fallback');
+        expect(sideEffect).toBe(true);
+      });
+
+      it('evaluates right side when left is undefined', () => {
+        let sideEffect = false;
+        const result = undefined ?? (sideEffect = true, 'fallback');
+        expect(result).toBe('fallback');
+        expect(sideEffect).toBe(true);
+      });
+
+      it('complex left side evaluated once with side effects', () => {
+        const calls = [];
+        const obj = {
+          get prop() {
+            calls.push('get');
+            return null;
+          },
+        };
+        obj.prop ?? 'default';
+        expect(calls.length).toBe(1);
+      });
+    });
+
+    describe('with assignment', () => {
+      it('assign result to variable', () => {
+        const x = null ?? 42;
+        expect(x).toBe(42);
+      });
+
+      it('assign with non-nullish left', () => {
+        const x = 0 ?? 42;
+        expect(x).toBe(0);
+      });
+
+      it('nullish coalescing assignment (??=)', () => {
+        let a = null;
+        a ??= 'default';
+        expect(a).toBe('default');
+      });
+
+      it('??= does not assign when non-nullish', () => {
+        let a = 0;
+        a ??= 99;
+        expect(a).toBe(0);
+      });
+
+      it('??= with undefined', () => {
+        let a = undefined;
+        a ??= 'filled';
+        expect(a).toBe('filled');
+      });
+
+      it('??= with false', () => {
+        let a = false;
+        a ??= true;
+        expect(a).toBe(false);
+      });
+
+      it('??= with empty string', () => {
+        let a = '';
+        a ??= 'nonempty';
+        expect(a).toBe('');
+      });
+
+      it('??= on object property', () => {
+        const obj = { a: null, b: 0 };
+        obj.a ??= 'filled';
+        obj.b ??= 99;
+        expect(obj.a).toBe('filled');
+        expect(obj.b).toBe(0);
+      });
+    });
+
+    describe('with various types', () => {
+      it('object value preserved', () => {
+        const obj = { key: 'value' };
+        expect((obj ?? {}).key).toBe('value');
+      });
+
+      it('null falls through to object default', () => {
+        const result = null ?? { key: 'default' };
+        expect(result.key).toBe('default');
+      });
+
+      it('array value preserved', () => {
+        const arr = [1, 2, 3];
+        const result = arr ?? [];
+        expect(result.length).toBe(3);
+      });
+
+      it('function value preserved', () => {
+        const fn = () => 42;
+        const result = fn ?? (() => 0);
+        expect(result()).toBe(42);
+      });
+
+      it('symbol value preserved', () => {
+        const sym = Symbol('test');
+        expect((sym ?? 'fallback')).toBe(sym);
+      });
+
+      it('bigint value preserved', () => {
+        const big = BigInt(0);
+        expect((big ?? 'fallback')).toBe(big);
+      });
+
+      it('regex value preserved', () => {
+        const re = /test/;
+        expect((re ?? 'fallback')).toBe(re);
+      });
+
+      it('Date value preserved', () => {
+        const date = new Date(0);
+        expect((date ?? 'fallback')).toBe(date);
+      });
+    });
+
+    describe('with optional chaining', () => {
+      it('?. producing undefined triggers ??', () => {
+        const obj = {};
+        expect(obj.foo?.bar ?? 'default').toBe('default');
+      });
+
+      it('?. on null triggers ??', () => {
+        const obj = { foo: null };
+        expect(obj.foo?.bar ?? 'default').toBe('default');
+      });
+
+      it('?. producing value does not trigger ??', () => {
+        const obj = { foo: { bar: 'value' } };
+        expect(obj.foo?.bar ?? 'default').toBe('value');
+      });
+
+      it('?. producing 0 does not trigger ??', () => {
+        const obj = { foo: { bar: 0 } };
+        expect(obj.foo?.bar ?? 99).toBe(0);
+      });
+
+      it('deep optional chain with ??', () => {
+        const obj = { a: { b: { c: { d: null } } } };
+        expect(obj.a?.b?.c?.d ?? 'default').toBe('default');
+        expect(obj.a?.b?.c?.e ?? 'default').toBe('default');
+        expect(obj.x?.y?.z ?? 'default').toBe('default');
+      });
+
+      it('optional method call with ??', () => {
+        const obj = { fn: () => null };
+        expect(obj.fn?.() ?? 'default').toBe('default');
+        expect(obj.missing?.() ?? 'default').toBe('default');
+      });
+    });
+
+    describe('in function parameters', () => {
+      it('default parameter with ??', () => {
+        function greet(name) {
+          const displayName = name ?? 'stranger';
+          return 'Hello, ' + displayName;
+        }
+        expect(greet('Alice')).toBe('Hello, Alice');
+        expect(greet(null)).toBe('Hello, stranger');
+        expect(greet(undefined)).toBe('Hello, stranger');
+        expect(greet('')).toBe('Hello, ');
+      });
+
+      it('multiple parameters with ??', () => {
+        function config(host, port, secure) {
+          return {
+            host: host ?? 'localhost',
+            port: port ?? 3000,
+            secure: secure ?? false,
+          };
+        }
+        const c = config(null, 0, undefined);
+        expect(c.host).toBe('localhost');
+        expect(c.port).toBe(0);
+        expect(c.secure).toBe(false);
+      });
+
+      it('arrow function with ??', () => {
+        const getVal = (x) => x ?? 'default';
+        expect(getVal(null)).toBe('default');
+        expect(getVal(0)).toBe(0);
+      });
+    });
+
+    describe('in expressions', () => {
+      it('ternary on left side', () => {
+        const x = true ? null : 'value';
+        expect(x ?? 'default').toBe('default');
+      });
+
+      it('arithmetic on right side', () => {
+        const x = null ?? 2 + 3;
+        expect(x).toBe(5);
+      });
+
+      it('template literal on right side', () => {
+        const name = null ?? `world`;
+        expect(name).toBe('world');
+      });
+
+      it('nested ?? in right side', () => {
+        const a = null;
+        const b = undefined;
+        const c = 'found';
+        expect(a ?? (b ?? c)).toBe('found');
+      });
+
+      it('in return statement', () => {
+        function getValue(obj) {
+          return obj.value ?? 'empty';
+        }
+        expect(getValue({ value: null })).toBe('empty');
+        expect(getValue({ value: 0 })).toBe(0);
+      });
+
+      it('in throw expression right side', () => {
+        const val = null;
+        try {
+          const result = val ?? (() => { throw new Error('nullish'); })();
+          // Should have thrown
+          expect(true).toBe(false);
+        } catch (e) {
+          expect(e.message).toBe('nullish');
+        }
+      });
+    });
+
+    describe('in destructuring', () => {
+      it('with destructured property', () => {
+        const { a } = { a: null };
+        expect(a ?? 'default').toBe('default');
+      });
+
+      it('with default destructuring vs ??', () => {
+        // Default destructuring only triggers on undefined, not null
+        const { a = 'destructure-default' } = { a: null };
+        // a is null because destructuring default only applies on undefined
+        expect(a).toBe(null);
+        expect(a ?? 'nullish-default').toBe('nullish-default');
+      });
+
+      it('with array destructuring', () => {
+        const [a, b] = [null, undefined];
+        expect(a ?? 'first').toBe('first');
+        expect(b ?? 'second').toBe('second');
+      });
+    });
+
+    describe('in loops and conditionals', () => {
+      it('in if condition', () => {
+        const x = null;
+        const val = x ?? 0;
+        if (val === 0) {
+          expect(true).toBe(true);
+        } else {
+          expect(true).toBe(false);
+        }
+      });
+
+      it('in for loop initializer', () => {
+        const values = [];
+        for (let i = null ?? 0; i < 3; i++) {
+          values.push(i);
+        }
+        expect(values).toEqual([0, 1, 2]);
+      });
+
+      it('in while condition', () => {
+        let arr = [1, 2, null, 3];
+        let results = [];
+        let idx = 0;
+        while (idx < arr.length) {
+          results.push(arr[idx] ?? 'nil');
+          idx++;
+        }
+        expect(results).toEqual([1, 2, 'nil', 3]);
+      });
+    });
+
+    describe('class contexts', () => {
+      it('in class method', () => {
+        class Config {
+          constructor(value) {
+            this.value = value ?? 'default';
+          }
+          getValue() {
+            return this.value ?? 'none';
+          }
+        }
+        expect(new Config(null).value).toBe('default');
+        expect(new Config(0).value).toBe(0);
+        expect(new Config(null).getValue()).toBe('default');
+      });
+
+      it('in static method', () => {
+        class Utils {
+          static getDefault(val) {
+            return val ?? 'default';
+          }
+        }
+        expect(Utils.getDefault(null)).toBe('default');
+        expect(Utils.getDefault('')).toBe('');
+      });
+
+      it('in getter', () => {
+        class Box {
+          #value;
+          constructor(v) { this.#value = v; }
+          get content() { return this.#value ?? 'empty'; }
+        }
+        expect(new Box(null).content).toBe('empty');
+        expect(new Box(0).content).toBe(0);
+        expect(new Box(false).content).toBe(false);
+      });
+    });
+
+    describe('edge cases', () => {
+      it('both sides are null', () => {
+        expect(null ?? null).toBe(null);
+      });
+
+      it('both sides are undefined', () => {
+        expect(undefined ?? undefined).toBe(undefined);
+      });
+
+      it('right side is null', () => {
+        expect(undefined ?? null).toBe(null);
+      });
+
+      it('left side is an expression returning null', () => {
+        expect((1 > 2 ? 'yes' : null) ?? 'default').toBe('default');
+      });
+
+      it('works with comma operator', () => {
+        let x;
+        const result = (x = null, x) ?? 'default';
+        expect(result).toBe('default');
+      });
+
+      it('typeof does not throw for undeclared variable in right side', () => {
+        const result = 'value' ?? undeclaredVar;
+        expect(result).toBe('value');
+      });
+
+      it('deeply nested nullish coalescing', () => {
+        const a = null ?? (null ?? (null ?? (null ?? 'deep')));
+        expect(a).toBe('deep');
+      });
+
+      it('with Map.get()', () => {
+        const map = new Map();
+        map.set('key', 0);
+        expect(map.get('key') ?? 'default').toBe(0);
+        expect(map.get('missing') ?? 'default').toBe('default');
+      });
+
+      it('with WeakRef deref', () => {
+        let obj = { name: 'test' };
+        const ref = new WeakRef(obj);
+        expect(ref.deref() ?? 'collected').toBe(obj);
+      });
+
+      it('preserves object identity', () => {
+        const obj = {};
+        const result = obj ?? {};
+        expect(result).toBe(obj);
+      });
+    });
+
+    describe('practical patterns', () => {
+      it('config merging with ??', () => {
+        const defaults = { host: 'localhost', port: 3000, debug: false };
+        const userConfig = { host: null, port: 8080, debug: undefined };
+        const config = {
+          host: userConfig.host ?? defaults.host,
+          port: userConfig.port ?? defaults.port,
+          debug: userConfig.debug ?? defaults.debug,
+        };
+        expect(config.host).toBe('localhost');
+        expect(config.port).toBe(8080);
+        expect(config.debug).toBe(false);
+      });
+
+      it('safe access with fallback', () => {
+        const response = { data: { users: null } };
+        const users = response.data?.users ?? [];
+        expect(users).toEqual([]);
+      });
+
+      it('counting with zero preservation', () => {
+        function getCount(obj) {
+          return obj.count ?? -1;
+        }
+        expect(getCount({ count: 0 })).toBe(0);
+        expect(getCount({ count: 10 })).toBe(10);
+        expect(getCount({})).toBe(-1);
+      });
+
+      it('DOM-style attribute defaults', () => {
+        const attrs = { tabIndex: 0, disabled: false, id: '' };
+        expect(attrs.tabIndex ?? -1).toBe(0);
+        expect(attrs.disabled ?? false).toBe(false);
+        expect(attrs.id ?? 'auto').toBe('');
+        expect(attrs.className ?? 'default').toBe('default');
+      });
+
+      it('nested defaults', () => {
+        function getTheme(prefs) {
+          return {
+            color: prefs?.theme?.color ?? prefs?.color ?? 'blue',
+            size: prefs?.theme?.size ?? 16,
+          };
+        }
+        expect(getTheme(null).color).toBe('blue');
+        expect(getTheme({ color: 'red' }).color).toBe('red');
+        expect(getTheme({ theme: { color: 'green' } }).color).toBe('green');
+        expect(getTheme({}).size).toBe(16);
+      });
+
+      it('error message fallback', () => {
+        function getErrorMessage(error) {
+          return error?.message ?? error?.code ?? 'Unknown error';
+        }
+        expect(getErrorMessage({ message: 'Not found' })).toBe('Not found');
+        expect(getErrorMessage({ code: 404 })).toBe(404);
+        expect(getErrorMessage({})).toBe('Unknown error');
+        expect(getErrorMessage(null)).toBe('Unknown error');
+      });
+    });
+  });
+}
diff --git a/apps/test-suite/tests/JSOptionalChaining.js b/apps/test-suite/tests/JSOptionalChaining.js
new file mode 100644
index 00000000000000..fb9444edb98aab
--- /dev/null
+++ b/apps/test-suite/tests/JSOptionalChaining.js
@@ -0,0 +1,774 @@
+/* eslint-disable */
+'use strict';
+
+// Comprehensive runtime compliance tests for optional chaining (?.).
+// Validates that ?. works correctly at runtime in the Hermes engine.
+//
+// @babel/plugin-transform-optional-chaining rewrites:
+//   a?.b       → a === null || a === void 0 ? undefined : a.b
+//   a?.[b]     → a === null || a === void 0 ? undefined : a[b]
+//   a?.()      → a === null || a === void 0 ? undefined : a()
+//   a?.b()     → a === null || a === void 0 ? undefined : a.b.call(a)
+//   delete a?.b → a === null || a === void 0 ? true : delete a.b
+//
+// In loose mode (used by Expo): uses a == null instead of strict checks.
+//
+// Cases sourced from @babel/plugin-transform-optional-chaining
+// and supplemented with edge cases.
+
+export const name = 'JS Optional Chaining';
+
+export function test({ describe, it, xit, expect }) {
+  describe('JS Optional Chaining', () => {
+    describe('member access (?.)', () => {
+      it('accesses property on object', () => {
+        const obj = { a: 1 };
+        expect(obj?.a).toBe(1);
+      });
+
+      it('returns undefined for null base', () => {
+        const obj = null;
+        expect(obj?.a).toBe(undefined);
+      });
+
+      it('returns undefined for undefined base', () => {
+        const obj = undefined;
+        expect(obj?.a).toBe(undefined);
+      });
+
+      it('accesses nested property', () => {
+        const obj = { a: { b: { c: 42 } } };
+        expect(obj?.a?.b?.c).toBe(42);
+      });
+
+      it('short-circuits at first null', () => {
+        const obj = { a: null };
+        expect(obj?.a?.b?.c).toBe(undefined);
+      });
+
+      it('short-circuits at first undefined property', () => {
+        const obj = { a: {} };
+        expect(obj?.a?.b?.c).toBe(undefined);
+      });
+
+      it('property exists but is falsy', () => {
+        const obj = { val: 0 };
+        expect(obj?.val).toBe(0);
+      });
+
+      it('preserves all falsy non-nullish values', () => {
+        expect(({ v: 0 })?.v).toBe(0);
+        expect(({ v: '' })?.v).toBe('');
+        expect(({ v: false })?.v).toBe(false);
+        expect(({ v: NaN })?.v).not.toBe(NaN); // NaN !== NaN
+        expect(typeof ({ v: NaN })?.v).toBe('number');
+      });
+
+      it('works on non-plain objects', () => {
+        expect('hello'?.length).toBe(5);
+        expect([1, 2, 3]?.length).toBe(3);
+        expect((42)?.toFixed(2)).toBe('42.00');
+      });
+    });
+
+    describe('computed member access (?.[])', () => {
+      it('accesses computed property', () => {
+        const obj = { foo: 'bar' };
+        const key = 'foo';
+        expect(obj?.[key]).toBe('bar');
+      });
+
+      it('returns undefined for null base', () => {
+        const obj = null;
+        expect(obj?.['foo']).toBe(undefined);
+      });
+
+      it('returns undefined for undefined base', () => {
+        const obj = undefined;
+        expect(obj?.['foo']).toBe(undefined);
+      });
+
+      it('accesses array element', () => {
+        const arr = [10, 20, 30];
+        expect(arr?.[1]).toBe(20);
+      });
+
+      it('returns undefined for null array', () => {
+        const arr = null;
+        expect(arr?.[0]).toBe(undefined);
+      });
+
+      it('accesses with symbol key', () => {
+        const sym = Symbol('key');
+        const obj = { [sym]: 'value' };
+        expect(obj?.[sym]).toBe('value');
+      });
+
+      it('computed key expression is not evaluated when base is nullish', () => {
+        let evaluated = false;
+        const key = () => { evaluated = true; return 'foo'; };
+        const obj = null;
+        obj?.[key()];
+        expect(evaluated).toBe(false);
+      });
+
+      it('computed key expression evaluated once when base is non-nullish', () => {
+        let count = 0;
+        const key = () => { count++; return 'foo'; };
+        const obj = { foo: 'bar' };
+        expect(obj?.[key()]).toBe('bar');
+        expect(count).toBe(1);
+      });
+    });
+
+    describe('optional call (?.())', () => {
+      it('calls function', () => {
+        const fn = () => 42;
+        expect(fn?.()).toBe(42);
+      });
+
+      it('returns undefined for null function', () => {
+        const fn = null;
+        expect(fn?.()).toBe(undefined);
+      });
+
+      it('returns undefined for undefined function', () => {
+        const fn = undefined;
+        expect(fn?.()).toBe(undefined);
+      });
+
+      it('passes arguments', () => {
+        const fn = (a, b) => a + b;
+        expect(fn?.(2, 3)).toBe(5);
+      });
+
+      it('arguments not evaluated when function is nullish', () => {
+        let evaluated = false;
+        const fn = null;
+        fn?.((evaluated = true));
+        expect(evaluated).toBe(false);
+      });
+
+      it('works with function stored in variable', () => {
+        const obj = { fn: (x) => x * 2 };
+        expect(obj.fn?.(5)).toBe(10);
+      });
+
+      it('returns undefined for missing method', () => {
+        const obj = {};
+        expect(obj.fn?.()).toBe(undefined);
+      });
+    });
+
+    describe('method calls (?.method())', () => {
+      it('calls method on object', () => {
+        const obj = {
+          greet() { return 'hello'; },
+        };
+        expect(obj?.greet()).toBe('hello');
+      });
+
+      it('preserves this context', () => {
+        const obj = {
+          name: 'test',
+          getName() { return this.name; },
+        };
+        expect(obj?.getName()).toBe('test');
+      });
+
+      it('returns undefined when object is null', () => {
+        const obj = null;
+        expect(obj?.greet()).toBe(undefined);
+      });
+
+      it('returns undefined when method is undefined', () => {
+        const obj = {};
+        expect(obj?.greet?.()).toBe(undefined);
+      });
+
+      it('chained method calls', () => {
+        const obj = {
+          inner: {
+            getValue() { return 42; },
+          },
+        };
+        expect(obj?.inner?.getValue()).toBe(42);
+      });
+
+      it('method on prototype chain', () => {
+        class Base {
+          hello() { return 'base'; }
+        }
+        class Child extends Base {}
+        const c = new Child();
+        expect(c?.hello()).toBe('base');
+      });
+
+      it('this context preserved in nested access', () => {
+        const obj = {
+          x: 10,
+          inner: {
+            x: 20,
+            getX() { return this.x; },
+          },
+        };
+        expect(obj?.inner?.getX()).toBe(20);
+      });
+    });
+
+    describe('short-circuit evaluation', () => {
+      it('does not access further properties after null', () => {
+        let accessed = false;
+        const handler = {
+          get() { accessed = true; return {}; },
+        };
+        const obj = null;
+        obj?.foo;
+        expect(accessed).toBe(false);
+      });
+
+      it('does not call methods after null', () => {
+        let called = false;
+        const obj = null;
+        obj?.method?.((called = true));
+        expect(called).toBe(false);
+      });
+
+      it('entire chain short-circuits', () => {
+        let sideEffect = false;
+        const obj = null;
+        obj?.a.b.c.d.e?.f?.(() => { sideEffect = true; });
+        expect(sideEffect).toBe(false);
+      });
+
+      it('short-circuits computed access after null', () => {
+        let keyEvaluated = false;
+        const obj = null;
+        obj?.[(() => { keyEvaluated = true; return 'key'; })()];
+        expect(keyEvaluated).toBe(false);
+      });
+
+      it('side effects in base are evaluated once', () => {
+        let count = 0;
+        const getObj = () => { count++; return { a: 1 }; };
+        expect(getObj()?.a).toBe(1);
+        expect(count).toBe(1);
+      });
+
+      it('non-optional part after ?. still evaluates if base is present', () => {
+        let count = 0;
+        const obj = { get a() { count++; return { b: 42 }; } };
+        expect(obj?.a.b).toBe(42);
+        expect(count).toBe(1);
+      });
+    });
+
+    describe('delete with optional chaining', () => {
+      it('deletes existing property', () => {
+        const obj = { a: 1 };
+        expect(delete obj?.a).toBe(true);
+        expect(obj.a).toBe(undefined);
+      });
+
+      // TODO(@kitten): Hermes returns undefined for `delete nullObj?.prop` instead
+      // of the spec-mandated true. The spec says the optional chain short-circuits
+      // to undefined (not a Reference), and delete on a non-Reference returns true.
+      // Babel's spec-mode transform handles this correctly (`? true : delete obj.a`),
+      // but Hermes' native implementation does not.
+      xit('returns true for null base', () => {
+        const obj = null;
+        expect(delete obj?.a).toBe(true);
+      });
+
+      // TODO(@kitten): Same Hermes delete-on-nullish limitation as above.
+      xit('returns true for undefined base', () => {
+        const obj = undefined;
+        expect(delete obj?.a).toBe(true);
+      });
+
+      it('deletes nested property', () => {
+        const obj = { a: { b: 1 } };
+        expect(delete obj?.a?.b).toBe(true);
+        expect(obj.a.b).toBe(undefined);
+      });
+
+      // TODO(@kitten): Same Hermes delete-on-nullish limitation as above.
+      xit('returns true for null in nested delete', () => {
+        const obj = { a: null };
+        expect(delete obj?.a?.b).toBe(true);
+      });
+
+      it('deletes computed property', () => {
+        const obj = { foo: 'bar' };
+        const key = 'foo';
+        expect(delete obj?.[key]).toBe(true);
+        expect(obj.foo).toBe(undefined);
+      });
+    });
+
+    describe('in boolean context', () => {
+      it('if statement with optional chaining', () => {
+        const obj = null;
+        let branch = 'none';
+        if (obj?.truthy) {
+          branch = 'if';
+        } else {
+          branch = 'else';
+        }
+        expect(branch).toBe('else');
+      });
+
+      it('if with non-null object and truthy value', () => {
+        const obj = { flag: true };
+        let branch = 'none';
+        if (obj?.flag) {
+          branch = 'if';
+        }
+        expect(branch).toBe('if');
+      });
+
+      it('if with non-null object and falsy value', () => {
+        const obj = { flag: 0 };
+        let branch = 'none';
+        if (obj?.flag) {
+          branch = 'if';
+        } else {
+          branch = 'else';
+        }
+        expect(branch).toBe('else');
+      });
+
+      it('logical NOT with optional chaining', () => {
+        const obj = null;
+        expect(!obj?.foo).toBe(true);
+      });
+
+      it('logical NOT with existing value', () => {
+        const obj = { foo: 'bar' };
+        expect(!obj?.foo).toBe(false);
+      });
+
+      it('double NOT coerces to boolean', () => {
+        expect(!!null?.foo).toBe(false);
+        expect(!!({ foo: 1 })?.foo).toBe(true);
+        expect(!!({ foo: 0 })?.foo).toBe(false);
+      });
+
+      it('ternary with optional chaining', () => {
+        const obj = null;
+        expect(obj?.val ? 'yes' : 'no').toBe('no');
+      });
+
+      it('while loop with optional chaining', () => {
+        const items = [{ next: { next: { next: null } } }];
+        let node = items[0];
+        let count = 0;
+        while (node?.next) {
+          node = node.next;
+          count++;
+        }
+        expect(count).toBe(2);
+      });
+    });
+
+    describe('with logical operators', () => {
+      it('?. with && operator', () => {
+        const obj = null;
+        expect(obj?.a && 'yes').toBe(undefined);
+      });
+
+      it('?. with || operator', () => {
+        const obj = null;
+        expect(obj?.a || 'fallback').toBe('fallback');
+      });
+
+      it('?. with ?? operator', () => {
+        const obj = null;
+        expect(obj?.a ?? 'fallback').toBe('fallback');
+      });
+
+      it('?. returning falsy with ??', () => {
+        const obj = { a: 0 };
+        expect(obj?.a ?? 'fallback').toBe(0);
+      });
+
+      it('?. returning falsy with ||', () => {
+        const obj = { a: 0 };
+        expect(obj?.a || 'fallback').toBe('fallback');
+      });
+
+      it('chained logical with multiple ?.', () => {
+        const a = { x: 1 };
+        const b = null;
+        expect(a?.x && b?.y).toBe(undefined);
+      });
+    });
+
+    describe('mixed chains', () => {
+      it('optional then regular access', () => {
+        const obj = { a: { b: 42 } };
+        expect(obj?.a.b).toBe(42);
+      });
+
+      it('optional then regular access on null short-circuits all', () => {
+        const obj = null;
+        // The entire chain short-circuits — .b is not attempted
+        expect(obj?.a.b).toBe(undefined);
+      });
+
+      it('regular then optional access', () => {
+        const obj = { a: null };
+        expect(obj.a?.b).toBe(undefined);
+      });
+
+      it('mixed optional and regular deep chain', () => {
+        const obj = { a: { b: { c: { d: 'deep' } } } };
+        expect(obj?.a.b?.c.d).toBe('deep');
+      });
+
+      it('optional member then optional call', () => {
+        const obj = { fn: () => 42 };
+        expect(obj?.fn?.()).toBe(42);
+      });
+
+      it('optional member with missing function', () => {
+        const obj = {};
+        expect(obj?.fn?.()).toBe(undefined);
+      });
+
+      it('optional computed then optional member', () => {
+        const obj = { items: [{ name: 'first' }] };
+        expect(obj?.items?.[0]?.name).toBe('first');
+        expect(obj?.items?.[99]?.name).toBe(undefined);
+      });
+    });
+
+    describe('with classes', () => {
+      it('optional access on class instance', () => {
+        class User {
+          constructor(name) { this.name = name; }
+          greet() { return 'Hi, ' + this.name; }
+        }
+        const user = new User('Alice');
+        expect(user?.name).toBe('Alice');
+        expect(user?.greet()).toBe('Hi, Alice');
+      });
+
+      it('optional access on null instance', () => {
+        const user = null;
+        expect(user?.name).toBe(undefined);
+        expect(user?.greet?.()).toBe(undefined);
+      });
+
+      it('optional access on inherited method', () => {
+        class Animal {
+          speak() { return 'generic'; }
+        }
+        class Dog extends Animal {
+          speak() { return 'woof'; }
+        }
+        const d = new Dog();
+        expect(d?.speak()).toBe('woof');
+        const n = null;
+        expect(n?.speak?.()).toBe(undefined);
+      });
+
+      it('optional access on static method', () => {
+        class Utils {
+          static parse(s) { return parseInt(s, 10); }
+        }
+        expect(Utils?.parse('42')).toBe(42);
+        const Cls = null;
+        expect(Cls?.parse?.('42')).toBe(undefined);
+      });
+
+      it('optional access on getter', () => {
+        class Box {
+          #val;
+          constructor(v) { this.#val = v; }
+          get value() { return this.#val; }
+        }
+        const box = new Box(42);
+        expect(box?.value).toBe(42);
+        const empty = null;
+        expect(empty?.value).toBe(undefined);
+      });
+    });
+
+    describe('with various types as base', () => {
+      it('number base', () => {
+        const n = 42;
+        expect(n?.toFixed(2)).toBe('42.00');
+        expect(n?.toString()).toBe('42');
+      });
+
+      it('string base', () => {
+        const s = 'hello';
+        expect(s?.length).toBe(5);
+        expect(s?.toUpperCase()).toBe('HELLO');
+        expect(s?.[0]).toBe('h');
+      });
+
+      it('boolean base', () => {
+        const b = true;
+        expect(b?.toString()).toBe('true');
+      });
+
+      it('symbol base', () => {
+        const s = Symbol('test');
+        expect(s?.toString()).toBe('Symbol(test)');
+        expect(s?.description).toBe('test');
+      });
+
+      it('bigint base', () => {
+        const b = BigInt(42);
+        expect(b?.toString()).toBe('42');
+      });
+
+      it('array base', () => {
+        const arr = [1, 2, 3];
+        expect(arr?.length).toBe(3);
+        expect(arr?.map(x => x * 2)).toEqual([2, 4, 6]);
+        expect(arr?.[2]).toBe(3);
+      });
+
+      it('function base', () => {
+        const fn = (x) => x * 2;
+        expect(fn?.name).toBe('fn');
+        expect(fn?.length).toBe(1);
+        expect(fn?.(5)).toBe(10);
+      });
+
+      it('regex base', () => {
+        const re = /test/i;
+        expect(re?.test('TEST')).toBe(true);
+        expect(re?.flags).toBe('i');
+      });
+    });
+
+    describe('with destructuring', () => {
+      it('result of optional chain in destructuring default', () => {
+        const obj = null;
+        const { a = obj?.b ?? 'default' } = {};
+        expect(a).toBe('default');
+      });
+
+      it('optional chain inside destructured value', () => {
+        const data = { user: { address: { city: 'NYC' } } };
+        const city = data?.user?.address?.city;
+        expect(city).toBe('NYC');
+      });
+
+      it('optional chain result destructured', () => {
+        const obj = { coords: { x: 10, y: 20 } };
+        const { x, y } = obj?.coords ?? {};
+        expect(x).toBe(10);
+        expect(y).toBe(20);
+      });
+
+      it('null base with destructure fallback', () => {
+        const obj = null;
+        const { x = 0, y = 0 } = obj?.coords ?? {};
+        expect(x).toBe(0);
+        expect(y).toBe(0);
+      });
+    });
+
+    describe('with async/await', () => {
+      it('optional chain on async function result', async () => {
+        const fetchData = async () => ({ value: 42 });
+        const result = await fetchData();
+        expect(result?.value).toBe(42);
+      });
+
+      it('optional chain on null async result', async () => {
+        const fetchData = async () => null;
+        const result = await fetchData();
+        expect(result?.value).toBe(undefined);
+      });
+
+      it('optional call on async method', async () => {
+        const obj = {
+          async getData() { return 'data'; },
+        };
+        const result = await obj?.getData();
+        expect(result).toBe('data');
+      });
+
+      it('optional call on null async method', async () => {
+        const obj = {};
+        const result = await obj?.getData?.();
+        expect(result).toBe(undefined);
+      });
+    });
+
+    describe('with Map, Set, and WeakMap', () => {
+      it('optional method call on Map', () => {
+        const map = new Map([['key', 'value']]);
+        expect(map?.get('key')).toBe('value');
+        expect(map?.get('missing')).toBe(undefined);
+      });
+
+      it('optional on null Map', () => {
+        const map = null;
+        expect(map?.get?.('key')).toBe(undefined);
+      });
+
+      it('optional on Set', () => {
+        const set = new Set([1, 2, 3]);
+        expect(set?.has(2)).toBe(true);
+        expect(set?.has(4)).toBe(false);
+        expect(set?.size).toBe(3);
+      });
+
+      it('optional on WeakMap', () => {
+        const wm = new WeakMap();
+        const key = {};
+        wm.set(key, 'val');
+        expect(wm?.get(key)).toBe('val');
+      });
+    });
+
+    describe('edge cases', () => {
+      it('optional chaining on result of typeof', () => {
+        // typeof always returns a string
+        const result = (typeof undefined)?.length;
+        expect(result).toBe(9); // "undefined".length
+      });
+
+      it('deeply nested with all nullish', () => {
+        const obj = null;
+        expect(obj?.a?.b?.c?.d?.e?.f?.g?.h).toBe(undefined);
+      });
+
+      it('optional chaining in template literal', () => {
+        const obj = null;
+        const result = `value: ${obj?.foo ?? 'none'}`;
+        expect(result).toBe('value: none');
+      });
+
+      it('optional chaining with spread', () => {
+        const obj = { arr: [1, 2, 3] };
+        const result = [...(obj?.arr ?? [])];
+        expect(result).toEqual([1, 2, 3]);
+
+        const nul = null;
+        const result2 = [...(nul?.arr ?? [])];
+        expect(result2).toEqual([]);
+      });
+
+      it('chained optional calls', () => {
+        const fn = () => () => () => 42;
+        expect(fn?.()?.()?.()).toBe(42);
+      });
+
+      it('chained optional calls with null', () => {
+        const fn = () => () => null;
+        expect(fn?.()?.()?.()).toBe(undefined);
+      });
+
+      it('optional on Map.prototype.get result', () => {
+        const map = new Map();
+        map.set('user', { name: 'Alice' });
+        expect(map.get('user')?.name).toBe('Alice');
+        expect(map.get('missing')?.name).toBe(undefined);
+      });
+
+      it('optional chaining preserves object identity', () => {
+        const inner = { value: 42 };
+        const obj = { inner };
+        expect(obj?.inner).toBe(inner);
+      });
+
+      it('parenthesized optional chain', () => {
+        const obj = { a: { b: 42 } };
+        expect((obj?.a)?.b).toBe(42);
+        const nul = null;
+        expect((nul?.a)?.b).toBe(undefined);
+      });
+
+      it('optional chaining with comma operator', () => {
+        const obj = { a: 1 };
+        const result = (0, obj)?.a;
+        expect(result).toBe(1);
+      });
+    });
+
+    describe('practical patterns', () => {
+      it('safe nested config access', () => {
+        const config = {
+          database: {
+            connection: {
+              host: 'localhost',
+              port: 5432,
+            },
+          },
+        };
+        expect(config?.database?.connection?.host).toBe('localhost');
+        expect(config?.database?.connection?.ssl).toBe(undefined);
+        expect(config?.cache?.redis?.host).toBe(undefined);
+      });
+
+      it('safe event handler invocation', () => {
+        let called = false;
+        const props = {
+          onPress: () => { called = true; },
+        };
+        props?.onPress?.();
+        expect(called).toBe(true);
+
+        const emptyProps = {};
+        emptyProps?.onPress?.();
+        // No error thrown
+        expect(true).toBe(true);
+      });
+
+      it('safe array element access', () => {
+        const matrix = [[1, 2], [3, 4]];
+        expect(matrix?.[0]?.[1]).toBe(2);
+        expect(matrix?.[5]?.[0]).toBe(undefined);
+      });
+
+      it('optional chaining in reduce', () => {
+        const items = [
+          { category: { name: 'A' }, value: 1 },
+          { category: null, value: 2 },
+          { category: { name: 'B' }, value: 3 },
+        ];
+        const categories = items.map(item => item.category?.name ?? 'Unknown');
+        expect(categories).toEqual(['A', 'Unknown', 'B']);
+      });
+
+      it('JSON response navigation', () => {
+        const response = {
+          data: {
+            users: [
+              { profile: { avatar: { url: 'https://example.com/photo.jpg' } } },
+              { profile: { avatar: null } },
+              { profile: null },
+            ],
+          },
+        };
+        expect(response.data?.users?.[0]?.profile?.avatar?.url).toBe('https://example.com/photo.jpg');
+        expect(response.data?.users?.[1]?.profile?.avatar?.url).toBe(undefined);
+        expect(response.data?.users?.[2]?.profile?.avatar?.url).toBe(undefined);
+        expect(response.data?.users?.[99]?.profile?.avatar?.url).toBe(undefined);
+      });
+
+      it('safe method chaining pattern', () => {
+        class Builder {
+          #parts = [];
+          add(part) { this.#parts.push(part); return this; }
+          build() { return this.#parts.join(' '); }
+        }
+        const builder = new Builder();
+        const result = builder?.add('hello')?.add('world')?.build();
+        expect(result).toBe('hello world');
+
+        const nullBuilder = null;
+        const nullResult = nullBuilder?.add('hello')?.add('world')?.build();
+        expect(nullResult).toBe(undefined);
+      });
+    });
+  });
+}
diff --git a/apps/test-suite/tests/JSPrivateMethods.js b/apps/test-suite/tests/JSPrivateMethods.js
new file mode 100644
index 00000000000000..e3712e06da4e2a
--- /dev/null
+++ b/apps/test-suite/tests/JSPrivateMethods.js
@@ -0,0 +1,1203 @@
+/* eslint-disable */
+'use strict';
+
+// Comprehensive runtime compliance tests for private class methods.
+// Validates that private methods, accessors, and static private methods
+// work correctly at runtime in the Hermes engine.
+// Cases sourced from @babel/plugin-transform-private-methods exec.js fixtures
+// and supplemented with additional edge cases.
+
+export const name = 'JS Private Methods';
+
+export function test({ describe, it, xit, expect }) {
+  describe('JS Private Methods', () => {
+    describe('private instance methods', () => {
+      // From: private-method/assignment/exec.js
+      it('private method can be called from constructor', () => {
+        class Foo {
+          #bar() {
+            return 42;
+          }
+          constructor() {
+            this.result = this.#bar();
+          }
+        }
+        const foo = new Foo();
+        expect(foo.result).toBe(42);
+      });
+
+      // From: private-method/context/exec.js
+      it('private method preserves this context', () => {
+        class Foo {
+          #name = 'foo';
+          #bar() {
+            return this.#name;
+          }
+          test() {
+            return this.#bar();
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test()).toBe('foo');
+      });
+
+      it('private method called with different this via call/apply', () => {
+        class Foo {
+          x = 1;
+          #bar() {
+            return this.x;
+          }
+          getBar() {
+            return this.#bar;
+          }
+        }
+        const foo = new Foo();
+        const bar = foo.getBar();
+        // Private method extracted — calling with original receiver
+        expect(bar.call(foo)).toBe(1);
+      });
+
+      // From: private-method/exfiltrated/exec.js
+      it('exfiltrated private method has stable identity', () => {
+        class Foo {
+          #bar() {
+            return 'bar';
+          }
+          getBar() {
+            return this.#bar;
+          }
+        }
+        const foo = new Foo();
+        expect(foo.getBar()).toBe(foo.getBar());
+      });
+
+      // From: private-method/before-fields/exec.js
+      it('private method available during field initialization', () => {
+        class Foo {
+          #bar() {
+            return 'bar';
+          }
+          foo = this.#bar();
+        }
+        const foo = new Foo();
+        expect(foo.foo).toBe('bar');
+      });
+
+      // From: private-method/scopable/exec.js
+      it('multiple private methods in same class', () => {
+        class Foo {
+          #bar() {
+            return 'bar';
+          }
+          #baz() {
+            return 'baz';
+          }
+          test() {
+            return this.#bar() + this.#baz();
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test()).toBe('barbaz');
+      });
+
+      // From: private-method/read-only/exec.js
+      it('private method is read-only (assignment throws)', () => {
+        expect(() => {
+          class Foo {
+            #bar() {}
+            constructor() {
+              this.#bar = 1;
+            }
+          }
+          new Foo();
+        }).toThrow();
+      });
+
+      // From: private-method/reassignment/exec.js
+      it('private method compound assignment throws', () => {
+        expect(() => {
+          class Foo {
+            #bar() {}
+            constructor() {
+              this.#bar += 1;
+            }
+          }
+          new Foo();
+        }).toThrow();
+      });
+
+      it('private method increment throws', () => {
+        expect(() => {
+          class Foo {
+            #bar() {}
+            constructor() {
+              this.#bar++;
+            }
+          }
+          new Foo();
+        }).toThrow();
+      });
+
+      // From: private-method/class-binding/exec.js
+      it('class name binding preserved inside private method', () => {
+        class Foo {
+          #bar() {
+            return Foo;
+          }
+          test() {
+            return this.#bar();
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test()).toBe(Foo);
+      });
+
+      // From: private-method/super/exec.js
+      it('super method access from private method', () => {
+        class Base {
+          greet() {
+            return 'hello';
+          }
+        }
+        class Derived extends Base {
+          #bar() {
+            return super.greet();
+          }
+          test() {
+            return this.#bar();
+          }
+        }
+        const d = new Derived();
+        expect(d.test()).toBe('hello');
+      });
+
+      // From: private-method/tagged-template/exec.js
+      // Hermes v1 does not support private methods as tagged template literals.
+      xit('private method as tagged template', () => {
+        class Foo {
+          #tag(strings, ...values) {
+            return { strings: Array.from(strings), values };
+          }
+          test() {
+            return this.#tag`hello ${'world'}`;
+          }
+        }
+        const foo = new Foo();
+        const result = foo.test();
+        expect(result.strings).toEqual(['hello ', '']);
+        expect(result.values).toEqual(['world']);
+      });
+
+      it('private method with parameters', () => {
+        class Calc {
+          #add(a, b) {
+            return a + b;
+          }
+          sum(a, b) {
+            return this.#add(a, b);
+          }
+        }
+        const calc = new Calc();
+        expect(calc.sum(3, 4)).toBe(7);
+      });
+
+      it('private method with default parameters', () => {
+        class Foo {
+          #greet(name = 'world') {
+            return `hello ${name}`;
+          }
+          test(n) {
+            return this.#greet(n);
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test()).toBe('hello world');
+        expect(foo.test('bar')).toBe('hello bar');
+      });
+
+      it('private method with rest parameters', () => {
+        class Foo {
+          #sum(...args) {
+            return args.reduce((a, b) => a + b, 0);
+          }
+          test(...args) {
+            return this.#sum(...args);
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test(1, 2, 3)).toBe(6);
+      });
+
+      it('private method with destructured parameters', () => {
+        class Foo {
+          #extract({ x, y }) {
+            return x + y;
+          }
+          test(obj) {
+            return this.#extract(obj);
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test({ x: 10, y: 20 })).toBe(30);
+      });
+
+      it('private method not accessible from outside', () => {
+        class Foo {
+          #secret() {
+            return 42;
+          }
+        }
+        const foo = new Foo();
+        expect(foo['#secret']).toBeUndefined();
+        expect(typeof foo['#secret']).toBe('undefined');
+      });
+
+      it('private method not accessible from subclass', () => {
+        class Base {
+          #secret() {
+            return 42;
+          }
+          callSecret() {
+            return this.#secret();
+          }
+        }
+        class Child extends Base {}
+        const child = new Child();
+        // Can only call through inherited public method
+        expect(child.callSecret()).toBe(42);
+      });
+
+      it('private method calling another private method', () => {
+        class Foo {
+          #a() {
+            return 1;
+          }
+          #b() {
+            return this.#a() + 2;
+          }
+          test() {
+            return this.#b();
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test()).toBe(3);
+      });
+
+      it('private method returning this for chaining', () => {
+        class Builder {
+          value = 0;
+          #add(n) {
+            this.value += n;
+            return this;
+          }
+          #mul(n) {
+            this.value *= n;
+            return this;
+          }
+          build(a, b) {
+            return this.#add(a).#mul(b).value;
+          }
+        }
+        const b = new Builder();
+        expect(b.build(3, 4)).toBe(12);
+      });
+    });
+
+    describe('async private methods', () => {
+      // From: private-method/async/exec.js
+      it('async private method returns promise', async () => {
+        class Foo {
+          async #bar() {
+            return 42;
+          }
+          async test() {
+            return await this.#bar();
+          }
+        }
+        const foo = new Foo();
+        const result = await foo.test();
+        expect(result).toBe(42);
+      });
+
+      it('async private method with await', async () => {
+        class Foo {
+          async #fetch(val) {
+            const result = await Promise.resolve(val * 2);
+            return result;
+          }
+          async test() {
+            return await this.#fetch(21);
+          }
+        }
+        const foo = new Foo();
+        expect(await foo.test()).toBe(42);
+      });
+
+      it('async private method error propagation', async () => {
+        class Foo {
+          async #fail() {
+            throw new Error('private error');
+          }
+          async test() {
+            try {
+              await this.#fail();
+              return 'no error';
+            } catch (e) {
+              return e.message;
+            }
+          }
+        }
+        const foo = new Foo();
+        expect(await foo.test()).toBe('private error');
+      });
+    });
+
+    describe('generator private methods', () => {
+      // From: private-method/generator/exec.js
+      it('private generator method yields values', () => {
+        class Foo {
+          *#gen() {
+            yield 1;
+            yield 2;
+            yield 3;
+          }
+          test() {
+            return [...this.#gen()];
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test()).toEqual([1, 2, 3]);
+      });
+
+      it('private async generator method', async () => {
+        class Foo {
+          async *#gen() {
+            yield await Promise.resolve(1);
+            yield await Promise.resolve(2);
+            yield await Promise.resolve(3);
+          }
+          async test() {
+            const results = [];
+            for await (const val of this.#gen()) {
+              results.push(val);
+            }
+            return results;
+          }
+        }
+        const foo = new Foo();
+        expect(await foo.test()).toEqual([1, 2, 3]);
+      });
+
+      it('private generator with return', () => {
+        class Foo {
+          *#gen() {
+            yield 'a';
+            return 'done';
+          }
+          test() {
+            const gen = this.#gen();
+            const first = gen.next();
+            const second = gen.next();
+            return { first, second };
+          }
+        }
+        const foo = new Foo();
+        const result = foo.test();
+        expect(result.first.value).toBe('a');
+        expect(result.first.done).toBe(false);
+        expect(result.second.value).toBe('done');
+        expect(result.second.done).toBe(true);
+      });
+    });
+
+    describe('private accessors', () => {
+      it('private getter', () => {
+        class Foo {
+          #x = 42;
+          get #value() {
+            return this.#x;
+          }
+          test() {
+            return this.#value;
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test()).toBe(42);
+      });
+
+      it('private setter', () => {
+        class Foo {
+          #x = 0;
+          set #value(v) {
+            this.#x = v;
+          }
+          getValue() {
+            return this.#x;
+          }
+          test(v) {
+            this.#value = v;
+          }
+        }
+        const foo = new Foo();
+        foo.test(42);
+        expect(foo.getValue()).toBe(42);
+      });
+
+      it('private getter and setter pair', () => {
+        class Foo {
+          #x = 0;
+          get #value() {
+            return this.#x;
+          }
+          set #value(v) {
+            this.#x = v * 2;
+          }
+          test() {
+            this.#value = 21;
+            return this.#value;
+          }
+        }
+        const foo = new Foo();
+        expect(foo.test()).toBe(42);
+      });
+
+      it('getter-only private accessor throws on set', () => {
+        expect(() => {
+          class Foo {
+            get #bar() {
+              return 1;
+            }
+            constructor() {
+              this.#bar = 2;
+            }
+          }
+          new Foo();
+        }).toThrow();
+      });
+
+      // In loose mode, setter-only accessor does not throw on get — returns undefined instead.
+      xit('setter-only private accessor throws on get', () => {
+        expect(() => {
+          class Foo {
+            set #bar(v) {}
+            constructor() {
+              const x = this.#bar;
+            }
+          }
+          new Foo();
+        }).toThrow();
+      });
+
+      it('private accessor available during field initialization', () => {
+        class Foo {
+          get #value() {
+            return 42;
+          }
+          field = this.#value;
+        }
+        const foo = new Foo();
+        expect(foo.field).toBe(42);
+      });
+
+      it('private getter with computed value', () => {
+        class Rect {
+          #w;
+          #h;
+          constructor(w, h) {
+            this.#w = w;
+            this.#h = h;
+          }
+          get #area() {
+            return this.#w * this.#h;
+          }
+          getArea() {
+            return this.#area;
+          }
+        }
+        const r = new Rect(3, 4);
+        expect(r.getArea()).toBe(12);
+      });
+
+      it('private accessor increment/decrement via getter+setter', () => {
+        class Counter {
+          #x = 0;
+          get #count() {
+            return this.#x;
+          }
+          set #count(v) {
+            this.#x = v;
+          }
+          inc() {
+            this.#count++;
+            return this.#count;
+          }
+          dec() {
+            this.#count--;
+            return this.#count;
+          }
+        }
+        const c = new Counter();
+        expect(c.inc()).toBe(1);
+        expect(c.inc()).toBe(2);
+        expect(c.dec()).toBe(1);
+      });
+    });
+
+    describe('private static methods', () => {
+      // From: private-static-method/basic/exec.js
+      it('basic static private method', () => {
+        class Foo {
+          static #bar() {
+            return 'bar';
+          }
+          static test() {
+            return Foo.#bar();
+          }
+        }
+        expect(Foo.test()).toBe('bar');
+      });
+
+      // From: private-static-method/class-check/exec.js
+      it('static private method with class passed as argument', () => {
+        class Foo {
+          static #bar() {
+            return 'bar';
+          }
+          static extract(klass) {
+            return klass.#bar();
+          }
+        }
+        expect(Foo.extract(Foo)).toBe('bar');
+      });
+
+      // From: private-static-method/exfiltrated/exec.js
+      it('exfiltrated static private method has stable identity', () => {
+        class Foo {
+          static #bar() {
+            return 'bar';
+          }
+          static getBar() {
+            return Foo.#bar;
+          }
+        }
+        expect(Foo.getBar()).toBe(Foo.getBar());
+      });
+
+      // From: private-static-method/scopable/exec.js
+      it('multiple static private methods', () => {
+        class Foo {
+          static #bar() {
+            return 'bar';
+          }
+          static #baz() {
+            return 'baz';
+          }
+          static test() {
+            return Foo.#bar() + Foo.#baz();
+          }
+        }
+        expect(Foo.test()).toBe('barbaz');
+      });
+
+      // From: private-static-method/read-only/exec.js
+      it('static private method is read-only', () => {
+        expect(() => {
+          class Foo {
+            static #bar() {}
+            static {
+              Foo.#bar = 1;
+            }
+          }
+        }).toThrow();
+      });
+
+      // From: private-static-method/reassignment/exec.js
+      it('static private method compound assignment throws', () => {
+        expect(() => {
+          class Foo {
+            static #bar() {}
+            static {
+              Foo.#bar += 1;
+            }
+          }
+        }).toThrow();
+      });
+
+      // From: private-static-method/super/exec.js
+      it('super access from static private method', () => {
+        class Base {
+          static greet() {
+            return 'hello';
+          }
+        }
+        class Derived extends Base {
+          static #bar() {
+            return super.greet();
+          }
+          static test() {
+            return Derived.#bar();
+          }
+        }
+        expect(Derived.test()).toBe('hello');
+      });
+
+      // From: private-static-method/this/exec.js
+      it('this in static private method refers to class', () => {
+        class Foo {
+          static val = 42;
+          static #bar() {
+            return this.val;
+          }
+          static test() {
+            return this.#bar();
+          }
+        }
+        expect(Foo.test()).toBe(42);
+      });
+
+      // From: private-static-method/tagged-template/exec.js
+      // Hermes v1 does not support private methods as tagged template literals.
+      xit('static private method as tagged template', () => {
+        class Foo {
+          static #tag(strings, ...values) {
+            return { strings: Array.from(strings), values };
+          }
+          static test() {
+            return Foo.#tag`hi ${'there'}`;
+          }
+        }
+        const result = Foo.test();
+        expect(result.strings).toEqual(['hi ', '']);
+        expect(result.values).toEqual(['there']);
+      });
+
+      // From: private-static-method/generator/exec.js
+      it('static private generator method', () => {
+        class Foo {
+          static *#gen() {
+            yield 10;
+            yield 20;
+          }
+          static test() {
+            return [...Foo.#gen()];
+          }
+        }
+        expect(Foo.test()).toEqual([10, 20]);
+      });
+
+      it('static private async method', async () => {
+        class Foo {
+          static async #compute(x) {
+            return await Promise.resolve(x * 2);
+          }
+          static async test() {
+            return await Foo.#compute(21);
+          }
+        }
+        expect(await Foo.test()).toBe(42);
+      });
+    });
+
+    describe('private static accessors', () => {
+      it('static private getter', () => {
+        class Foo {
+          static #x = 42;
+          static get #value() {
+            return Foo.#x;
+          }
+          static test() {
+            return Foo.#value;
+          }
+        }
+        expect(Foo.test()).toBe(42);
+      });
+
+      it('static private setter', () => {
+        class Foo {
+          static #x = 0;
+          static set #value(v) {
+            Foo.#x = v;
+          }
+          static get() {
+            return Foo.#x;
+          }
+          static test(v) {
+            Foo.#value = v;
+          }
+        }
+        Foo.test(42);
+        expect(Foo.get()).toBe(42);
+      });
+
+      it('static private getter and setter pair', () => {
+        class Foo {
+          static #x = 0;
+          static get #value() {
+            return Foo.#x;
+          }
+          static set #value(v) {
+            Foo.#x = v * 2;
+          }
+          static test() {
+            Foo.#value = 21;
+            return Foo.#value;
+          }
+        }
+        expect(Foo.test()).toBe(42);
+      });
+
+      it('static getter-only throws on set', () => {
+        expect(() => {
+          class Foo {
+            static get #bar() {
+              return 1;
+            }
+            static {
+              Foo.#bar = 2;
+            }
+          }
+        }).toThrow();
+      });
+    });
+
+    describe('inheritance and isolation', () => {
+      it('private methods are per-class (not inherited)', () => {
+        class Base {
+          #greet() {
+            return 'base';
+          }
+          test() {
+            return this.#greet();
+          }
+        }
+        class Child extends Base {
+          // Child does NOT have access to Base's #greet
+        }
+        const child = new Child();
+        // inherited public method still works because it calls #greet on Base
+        expect(child.test()).toBe('base');
+      });
+
+      it('subclass can define same-named private method', () => {
+        class Base {
+          #method() {
+            return 'base';
+          }
+          baseTest() {
+            return this.#method();
+          }
+        }
+        class Child extends Base {
+          #method() {
+            return 'child';
+          }
+          childTest() {
+            return this.#method();
+          }
+        }
+        const c = new Child();
+        expect(c.baseTest()).toBe('base');
+        expect(c.childTest()).toBe('child');
+      });
+
+      it('private method access on wrong class instance throws', () => {
+        class A {
+          #foo() {
+            return 'a';
+          }
+          test(obj) {
+            return obj.#foo();
+          }
+        }
+        class B {}
+        const a = new A();
+        const b = new B();
+        expect(() => a.test(b)).toThrow();
+      });
+
+      it('different classes with same private method name are independent', () => {
+        class A {
+          #x() {
+            return 'A';
+          }
+          test() {
+            return this.#x();
+          }
+        }
+        class B {
+          #x() {
+            return 'B';
+          }
+          test() {
+            return this.#x();
+          }
+        }
+        expect(new A().test()).toBe('A');
+        expect(new B().test()).toBe('B');
+      });
+    });
+
+    describe('private fields interaction', () => {
+      it('private method accessing private field', () => {
+        class Foo {
+          #x = 10;
+          #double() {
+            return this.#x * 2;
+          }
+          test() {
+            return this.#double();
+          }
+        }
+        expect(new Foo().test()).toBe(20);
+      });
+
+      it('private method mutating private field', () => {
+        class Counter {
+          #count = 0;
+          #increment() {
+            this.#count++;
+          }
+          inc() {
+            this.#increment();
+            return this.#count;
+          }
+        }
+        const c = new Counter();
+        expect(c.inc()).toBe(1);
+        expect(c.inc()).toBe(2);
+      });
+
+      it('private setter validating before setting private field', () => {
+        class Temp {
+          #celsius = 0;
+          set #temperature(v) {
+            if (v < -273.15) throw new Error('below absolute zero');
+            this.#celsius = v;
+          }
+          get #temperature() {
+            return this.#celsius;
+          }
+          setTemp(v) {
+            this.#temperature = v;
+          }
+          getTemp() {
+            return this.#temperature;
+          }
+        }
+        const t = new Temp();
+        t.setTemp(100);
+        expect(t.getTemp()).toBe(100);
+        expect(() => t.setTemp(-300)).toThrow();
+        // Value unchanged after rejected set
+        expect(t.getTemp()).toBe(100);
+      });
+    });
+
+    describe('edge cases', () => {
+      it('private method in expression class', () => {
+        const Foo = class {
+          #bar() {
+            return 'bar';
+          }
+          test() {
+            return this.#bar();
+          }
+        };
+        expect(new Foo().test()).toBe('bar');
+      });
+
+      it('private method in nested class', () => {
+        class Outer {
+          test() {
+            class Inner {
+              #foo() {
+                return 'inner';
+              }
+              test() {
+                return this.#foo();
+              }
+            }
+            return new Inner().test();
+          }
+        }
+        expect(new Outer().test()).toBe('inner');
+      });
+
+      it('private method with symbol property on instance', () => {
+        const sym = Symbol('test');
+        class Foo {
+          [sym] = 'symbol-val';
+          #bar() {
+            return this[sym];
+          }
+          test() {
+            return this.#bar();
+          }
+        }
+        expect(new Foo().test()).toBe('symbol-val');
+      });
+
+      it('private method with computed public method', () => {
+        const key = 'myMethod';
+        class Foo {
+          #priv() {
+            return 'private';
+          }
+          [key]() {
+            return this.#priv();
+          }
+        }
+        expect(new Foo().myMethod()).toBe('private');
+      });
+
+      it('multiple instances have independent private state but shared method identity', () => {
+        class Foo {
+          #val;
+          constructor(val) {
+            this.#val = val;
+          }
+          #getVal() {
+            return this.#val;
+          }
+          test() {
+            return this.#getVal();
+          }
+          getMethod() {
+            return this.#getVal;
+          }
+        }
+        const a = new Foo(1);
+        const b = new Foo(2);
+        expect(a.test()).toBe(1);
+        expect(b.test()).toBe(2);
+        // Private methods share identity across instances
+        expect(a.getMethod()).toBe(b.getMethod());
+      });
+
+      it('private method in class with constructor returning different object', () => {
+        class Foo {
+          #x = 10;
+          #bar() {
+            return this.#x;
+          }
+          test() {
+            return this.#bar();
+          }
+        }
+        // Normal instance works
+        expect(new Foo().test()).toBe(10);
+      });
+
+      it('private static method not accessible on instances', () => {
+        class Foo {
+          static #bar() {
+            return 42;
+          }
+          static test() {
+            return Foo.#bar();
+          }
+          instanceTest() {
+            // Can't access static private from instance
+            return Foo.test();
+          }
+        }
+        expect(new Foo().instanceTest()).toBe(42);
+      });
+
+      // In loose mode, private methods are stored as regular properties with
+      // mangled names (e.g. __private_..._secret), so they appear in
+      // getOwnPropertyNames. With native private methods this would pass.
+      xit('toString does not reveal private method names', () => {
+        class Foo {
+          #secret() {
+            return 42;
+          }
+          test() {
+            return this.#secret();
+          }
+        }
+        const foo = new Foo();
+        const keys = Object.getOwnPropertyNames(foo);
+        const hasPrivate = keys.some((k) => k.includes('secret'));
+        expect(hasPrivate).toBe(false);
+      });
+
+      it('for-in does not enumerate private methods', () => {
+        class Foo {
+          #secret() {}
+          pub = 1;
+        }
+        const foo = new Foo();
+        const keys = [];
+        for (const k in foo) {
+          keys.push(k);
+        }
+        // In loose mode, the transformed key is non-enumerable.
+        // With native private methods, the key wouldn't exist at all.
+        // Either way, no key named '#secret' is enumerable.
+        expect(keys.indexOf('#secret')).toBe(-1);
+        expect(keys.indexOf('pub') >= 0).toBe(true);
+      });
+
+      it('JSON.stringify only includes public properties', () => {
+        class Foo {
+          #method() {
+            return 42;
+          }
+          x = 1;
+        }
+        const foo = new Foo();
+        const parsed = JSON.parse(JSON.stringify(foo));
+        expect(parsed.x).toBe(1);
+        // Private method storage (loose or native) should not appear
+        var keyCount = Object.keys(parsed).length;
+        expect(keyCount).toBe(1);
+      });
+
+      it('Object.keys does not include private methods', () => {
+        class Foo {
+          #method() {}
+          x = 1;
+        }
+        const keys = Object.keys(new Foo());
+        expect(keys.indexOf('x') >= 0).toBe(true);
+        expect(keys.length).toBe(1);
+      });
+
+      it('typeof private method is function', () => {
+        class Foo {
+          #bar() {}
+          test() {
+            return typeof this.#bar;
+          }
+        }
+        expect(new Foo().test()).toBe('function');
+      });
+
+      it('constructor returning different object loses private brand', () => {
+        class Foo {
+          #x = 10;
+          #bar() {
+            return this.#x;
+          }
+          constructor(override) {
+            if (override) return override;
+          }
+          test() {
+            return this.#bar();
+          }
+        }
+        // Normal construction installs brand
+        expect(new Foo().test()).toBe(10);
+        // Overridden constructor returns a plain object without the brand
+        const plain = {};
+        const result = new Foo(plain);
+        expect(result).toBe(plain);
+        expect(result.test).toBeUndefined();
+      });
+
+      it('private method as detached callback loses this', () => {
+        class Foo {
+          #val = 42;
+          #getVal() {
+            return this.#val;
+          }
+          getCallback() {
+            // Wrapping in arrow preserves this
+            return () => this.#getVal();
+          }
+          getDetached() {
+            return this.#getVal;
+          }
+        }
+        const foo = new Foo();
+        // Arrow-wrapped callback works
+        expect(foo.getCallback()()).toBe(42);
+        // Detached loses this context — calling without a receiver
+        const detached = foo.getDetached();
+        expect(() => detached()).toThrow();
+      });
+
+      it('private method with closure over constructor parameter', () => {
+        class Foo {
+          #fn;
+          constructor(multiplier) {
+            this.#fn = (x) => x * multiplier;
+          }
+          test(x) {
+            return this.#fn(x);
+          }
+        }
+        expect(new Foo(3).test(7)).toBe(21);
+      });
+    });
+
+    describe('static blocks', () => {
+      it('static block can access private static method', () => {
+        let captured;
+        class Foo {
+          static #bar() {
+            return 'from static block';
+          }
+          static {
+            captured = Foo.#bar();
+          }
+        }
+        expect(captured).toBe('from static block');
+      });
+
+      it('static block can access private static accessor', () => {
+        let captured;
+        class Foo {
+          static #x = 42;
+          static get #value() {
+            return Foo.#x;
+          }
+          static {
+            captured = Foo.#value;
+          }
+        }
+        expect(captured).toBe(42);
+      });
+    });
+
+    describe('private-in checks (ergonomic brand checks)', () => {
+      it('#field in obj for instance with private method', () => {
+        class Foo {
+          #bar() {}
+          static check(obj) {
+            return #bar in obj;
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(true);
+        expect(Foo.check({})).toBe(false);
+        // `in` operator throws TypeError for non-object right-hand side
+        expect(() => Foo.check(42)).toThrow();
+      });
+
+      it('#field in obj for instance with private field', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(true);
+        expect(Foo.check({})).toBe(false);
+      });
+
+      it('#field in obj with inheritance', () => {
+        class Base {
+          #secret() {}
+          static check(obj) {
+            return #secret in obj;
+          }
+        }
+        class Child extends Base {}
+        expect(Base.check(new Base())).toBe(true);
+        expect(Base.check(new Child())).toBe(true);
+        expect(Base.check({})).toBe(false);
+      });
+    });
+  });
+}
diff --git a/apps/test-suite/tests/JSPrivateProperties.js b/apps/test-suite/tests/JSPrivateProperties.js
new file mode 100644
index 00000000000000..0666ff47c45ac0
--- /dev/null
+++ b/apps/test-suite/tests/JSPrivateProperties.js
@@ -0,0 +1,1350 @@
+/* eslint-disable */
+'use strict';
+
+// Comprehensive runtime compliance tests for private class properties (fields).
+// Validates that private field declaration, access, mutation, and the `#prop in obj`
+// brand check syntax work correctly at runtime in the Hermes engine.
+// Cases sourced from @babel/plugin-transform-class-properties and
+// @babel/plugin-transform-private-property-in-object exec.js fixtures,
+// supplemented with additional edge cases.
+
+export const name = 'JS Private Properties';
+
+export function test({ describe, it, xit, expect }) {
+  describe('JS Private Properties', () => {
+    describe('private field declaration and access', () => {
+      it('basic private field with initializer', () => {
+        class Foo {
+          #x = 42;
+          get() {
+            return this.#x;
+          }
+        }
+        expect(new Foo().get()).toBe(42);
+      });
+
+      it('private field without initializer is undefined', () => {
+        class Foo {
+          #x;
+          get() {
+            return this.#x;
+          }
+        }
+        expect(new Foo().get()).toBeUndefined();
+      });
+
+      it('private field initialized from constructor parameter', () => {
+        class Foo {
+          #x;
+          constructor(x) {
+            this.#x = x;
+          }
+          get() {
+            return this.#x;
+          }
+        }
+        expect(new Foo(42).get()).toBe(42);
+      });
+
+      it('private field read and write', () => {
+        class Foo {
+          #x = 0;
+          inc() {
+            this.#x++;
+          }
+          get() {
+            return this.#x;
+          }
+        }
+        const foo = new Foo();
+        expect(foo.get()).toBe(0);
+        foo.inc();
+        expect(foo.get()).toBe(1);
+        foo.inc();
+        expect(foo.get()).toBe(2);
+      });
+
+      it('multiple private fields', () => {
+        class Point {
+          #x;
+          #y;
+          constructor(x, y) {
+            this.#x = x;
+            this.#y = y;
+          }
+          toString() {
+            return `(${this.#x}, ${this.#y})`;
+          }
+        }
+        expect(new Point(1, 2).toString()).toBe('(1, 2)');
+      });
+
+      it('private field with various value types', () => {
+        class Store {
+          #num = 42;
+          #str = 'hello';
+          #bool = true;
+          #nil = null;
+          #undef = undefined;
+          #arr = [1, 2, 3];
+          #obj = { a: 1 };
+          #fn = (x) => x * 2;
+          values() {
+            return {
+              num: this.#num,
+              str: this.#str,
+              bool: this.#bool,
+              nil: this.#nil,
+              undef: this.#undef,
+              arr: this.#arr,
+              obj: this.#obj,
+              fn: this.#fn(5),
+            };
+          }
+        }
+        const v = new Store().values();
+        expect(v.num).toBe(42);
+        expect(v.str).toBe('hello');
+        expect(v.bool).toBe(true);
+        expect(v.nil).toBe(null);
+        expect(v.undef).toBeUndefined();
+        expect(v.arr).toEqual([1, 2, 3]);
+        expect(v.obj).toEqual({ a: 1 });
+        expect(v.fn).toBe(10);
+      });
+
+      it('private field with computed initializer', () => {
+        let counter = 0;
+        class Foo {
+          #x = ++counter;
+          get() {
+            return this.#x;
+          }
+        }
+        const a = new Foo();
+        const b = new Foo();
+        expect(a.get()).toBe(1);
+        expect(b.get()).toBe(2);
+      });
+
+      it('private field initializer runs per instance', () => {
+        class Foo {
+          #arr = [];
+          push(val) {
+            this.#arr.push(val);
+          }
+          get() {
+            return this.#arr;
+          }
+        }
+        const a = new Foo();
+        const b = new Foo();
+        a.push(1);
+        b.push(2);
+        expect(a.get()).toEqual([1]);
+        expect(b.get()).toEqual([2]);
+      });
+
+      it('private field not visible via Object.keys', () => {
+        class Foo {
+          #x = 1;
+          y = 2;
+        }
+        const keys = Object.keys(new Foo());
+        expect(keys.length).toBe(1);
+        expect(keys[0]).toBe('y');
+      });
+
+      it('private field not visible via for-in', () => {
+        class Foo {
+          #x = 1;
+          y = 2;
+        }
+        const keys = [];
+        for (const k in new Foo()) {
+          keys.push(k);
+        }
+        expect(keys.indexOf('y') >= 0).toBe(true);
+        expect(keys.indexOf('#x')).toBe(-1);
+      });
+
+      it('private field not visible in JSON.stringify', () => {
+        class Foo {
+          #secret = 'hidden';
+          pub = 'visible';
+        }
+        const json = JSON.parse(JSON.stringify(new Foo()));
+        expect(json.pub).toBe('visible');
+        expect(Object.keys(json).length).toBe(1);
+      });
+
+      it('private field access from outside throws', () => {
+        class Foo {
+          #x = 1;
+        }
+        const foo = new Foo();
+        // Accessing via bracket notation with the string '#x' just returns undefined
+        // (it's a different property name, not the private field)
+        expect(foo['#x']).toBeUndefined();
+      });
+    });
+
+    describe('private field mutation', () => {
+      it('compound assignment operators', () => {
+        class Foo {
+          #x = 10;
+          addAssign(n) {
+            this.#x += n;
+          }
+          subAssign(n) {
+            this.#x -= n;
+          }
+          mulAssign(n) {
+            this.#x *= n;
+          }
+          get() {
+            return this.#x;
+          }
+        }
+        const foo = new Foo();
+        foo.addAssign(5);
+        expect(foo.get()).toBe(15);
+        foo.subAssign(3);
+        expect(foo.get()).toBe(12);
+        foo.mulAssign(2);
+        expect(foo.get()).toBe(24);
+      });
+
+      it('prefix and postfix increment/decrement', () => {
+        class Counter {
+          #x = 0;
+          preInc() {
+            return ++this.#x;
+          }
+          postInc() {
+            return this.#x++;
+          }
+          preDec() {
+            return --this.#x;
+          }
+          postDec() {
+            return this.#x--;
+          }
+          get() {
+            return this.#x;
+          }
+        }
+        const c = new Counter();
+        expect(c.preInc()).toBe(1);
+        expect(c.get()).toBe(1);
+        expect(c.postInc()).toBe(1);
+        expect(c.get()).toBe(2);
+        expect(c.preDec()).toBe(1);
+        expect(c.get()).toBe(1);
+        expect(c.postDec()).toBe(1);
+        expect(c.get()).toBe(0);
+      });
+
+      it('logical assignment operators', () => {
+        class Foo {
+          #a = null;
+          #b = 0;
+          #c = '';
+          nullishAssign(val) {
+            this.#a ??= val;
+          }
+          orAssign(val) {
+            this.#b ||= val;
+          }
+          andAssign(val) {
+            this.#c &&= val;
+          }
+          values() {
+            return [this.#a, this.#b, this.#c];
+          }
+        }
+        const foo = new Foo();
+        foo.nullishAssign(42);
+        foo.orAssign(7);
+        foo.andAssign('replaced');
+        expect(foo.values()).toEqual([42, 7, '']);
+        // #a is no longer null, so ??= is a no-op
+        foo.nullishAssign(100);
+        expect(foo.values()[0]).toBe(42);
+      });
+
+      it('destructuring assignment to private field', () => {
+        class Foo {
+          #x = 0;
+          #y = 0;
+          assign(obj) {
+            ({ x: this.#x, y: this.#y } = obj);
+          }
+          get() {
+            return [this.#x, this.#y];
+          }
+        }
+        const foo = new Foo();
+        foo.assign({ x: 10, y: 20 });
+        expect(foo.get()).toEqual([10, 20]);
+      });
+    });
+
+    describe('static private fields', () => {
+      it('basic static private field', () => {
+        class Foo {
+          static #x = 42;
+          static get() {
+            return Foo.#x;
+          }
+        }
+        expect(Foo.get()).toBe(42);
+      });
+
+      it('static private field mutation', () => {
+        class Counter {
+          static #count = 0;
+          static inc() {
+            Counter.#count++;
+          }
+          static get() {
+            return Counter.#count;
+          }
+        }
+        expect(Counter.get()).toBe(0);
+        Counter.inc();
+        expect(Counter.get()).toBe(1);
+        Counter.inc();
+        expect(Counter.get()).toBe(2);
+      });
+
+      it('static private field accessed via this in static method', () => {
+        class Foo {
+          static #x = 99;
+          static get() {
+            return this.#x;
+          }
+        }
+        expect(Foo.get()).toBe(99);
+      });
+
+      it('static private field not accessible from instance', () => {
+        class Foo {
+          static #x = 42;
+          tryAccess() {
+            // Can't do this.#x for static from instance — use class name
+            return Foo.#x;
+          }
+        }
+        expect(new Foo().tryAccess()).toBe(42);
+      });
+
+      it('static private field with computed initializer', () => {
+        class Foo {
+          static #x = [1, 2, 3].reduce((a, b) => a + b, 0);
+          static get() {
+            return Foo.#x;
+          }
+        }
+        expect(Foo.get()).toBe(6);
+      });
+    });
+
+    describe('private field inheritance', () => {
+      it('subclass does not inherit parent private field', () => {
+        class Base {
+          #x = 1;
+          getBase() {
+            return this.#x;
+          }
+        }
+        class Child extends Base {
+          getChild() {
+            // Cannot access this.#x — it belongs to Base
+            return typeof this['#x'];
+          }
+        }
+        const child = new Child();
+        expect(child.getBase()).toBe(1);
+        expect(child.getChild()).toBe('undefined');
+      });
+
+      it('subclass can have same-named private field', () => {
+        class Base {
+          #x = 'base';
+          getBaseX() {
+            return this.#x;
+          }
+        }
+        class Child extends Base {
+          #x = 'child';
+          getChildX() {
+            return this.#x;
+          }
+        }
+        const child = new Child();
+        expect(child.getBaseX()).toBe('base');
+        expect(child.getChildX()).toBe('child');
+      });
+
+      it('subclass constructor can set parent private field via inherited method', () => {
+        class Base {
+          #x;
+          setX(val) {
+            this.#x = val;
+          }
+          getX() {
+            return this.#x;
+          }
+        }
+        class Child extends Base {
+          constructor(val) {
+            super();
+            this.setX(val);
+          }
+        }
+        expect(new Child(42).getX()).toBe(42);
+      });
+
+      it('private field per-instance with subclass', () => {
+        class Base {
+          #items = [];
+          push(val) {
+            this.#items.push(val);
+          }
+          get() {
+            return this.#items;
+          }
+        }
+        class Child extends Base {}
+        const a = new Child();
+        const b = new Child();
+        a.push(1);
+        b.push(2);
+        expect(a.get()).toEqual([1]);
+        expect(b.get()).toEqual([2]);
+      });
+    });
+
+    describe('private field interactions', () => {
+      it('private field with public field', () => {
+        class Foo {
+          #priv = 'private';
+          pub = 'public';
+          get() {
+            return this.#priv + ' ' + this.pub;
+          }
+        }
+        expect(new Foo().get()).toBe('private public');
+      });
+
+      it('private field initialization order', () => {
+        const order = [];
+        class Foo {
+          #a = (order.push('a'), 1);
+          b = (order.push('b'), 2);
+          #c = (order.push('c'), 3);
+          d = (order.push('d'), 4);
+          getOrder() {
+            return order;
+          }
+        }
+        new Foo();
+        expect(order).toEqual(['a', 'b', 'c', 'd']);
+      });
+
+      it('private field with constructor', () => {
+        class Foo {
+          #x = 10;
+          constructor() {
+            this.#x += 5;
+          }
+          get() {
+            return this.#x;
+          }
+        }
+        expect(new Foo().get()).toBe(15);
+      });
+
+      it('private field with super constructor', () => {
+        class Base {
+          constructor(val) {
+            this.baseVal = val;
+          }
+        }
+        class Child extends Base {
+          #x;
+          constructor(a, b) {
+            super(a);
+            this.#x = b;
+          }
+          get() {
+            return this.baseVal + this.#x;
+          }
+        }
+        expect(new Child(10, 32).get()).toBe(42);
+      });
+
+      it('private field used in method with destructuring', () => {
+        class Config {
+          #defaults = { color: 'blue', size: 10 };
+          merge(overrides) {
+            return { ...this.#defaults, ...overrides };
+          }
+        }
+        const c = new Config();
+        expect(c.merge({ size: 20 })).toEqual({ color: 'blue', size: 20 });
+        expect(c.merge({})).toEqual({ color: 'blue', size: 10 });
+      });
+
+      it('private field used as Map/Set key', () => {
+        class Registry {
+          #map = new Map();
+          set(key, val) {
+            this.#map.set(key, val);
+          }
+          get(key) {
+            return this.#map.get(key);
+          }
+          size() {
+            return this.#map.size;
+          }
+        }
+        const r = new Registry();
+        r.set('a', 1);
+        r.set('b', 2);
+        expect(r.get('a')).toBe(1);
+        expect(r.size()).toBe(2);
+      });
+
+      it('private field accessed in arrow function preserves this', () => {
+        class Foo {
+          #x = 42;
+          getGetter() {
+            return () => this.#x;
+          }
+        }
+        const foo = new Foo();
+        const getter = foo.getGetter();
+        expect(getter()).toBe(42);
+      });
+
+      it('private field accessed in callback', () => {
+        class Multiplier {
+          #factor;
+          constructor(factor) {
+            this.#factor = factor;
+          }
+          applyAll(arr) {
+            return arr.map((x) => x * this.#factor);
+          }
+        }
+        expect(new Multiplier(3).applyAll([1, 2, 3])).toEqual([3, 6, 9]);
+      });
+    });
+
+    describe('private instance field brand checks', () => {
+      it('basic #field in obj returns true for own instance', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(true);
+      });
+
+      it('#field in obj returns false for plain object', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check({})).toBe(false);
+      });
+
+      it('#field in obj returns false for instance of different class', () => {
+        class A {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        class B {
+          #x = 2;
+        }
+        expect(A.check(new B())).toBe(false);
+      });
+
+      it('multiple private field checks on same object', () => {
+        class Foo {
+          #a = 1;
+          #b = 2;
+          static checkA(obj) {
+            return #a in obj;
+          }
+          static checkB(obj) {
+            return #b in obj;
+          }
+        }
+        const foo = new Foo();
+        expect(Foo.checkA(foo)).toBe(true);
+        expect(Foo.checkB(foo)).toBe(true);
+        expect(Foo.checkA({})).toBe(false);
+        expect(Foo.checkB({})).toBe(false);
+      });
+
+      it('#field in obj with uninitialized field (no default)', () => {
+        class Foo {
+          #x;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(true);
+      });
+
+      it('instance method can check its own brand', () => {
+        class Foo {
+          #x = 1;
+          isFoo(obj) {
+            return #x in obj;
+          }
+        }
+        const foo = new Foo();
+        expect(foo.isFoo(foo)).toBe(true);
+        expect(foo.isFoo({})).toBe(false);
+      });
+    });
+
+    describe('private instance method brand checks', () => {
+      // From: assumption-privateFieldsAsProperties/method/exec.js
+      it('#method in obj returns true for instance', () => {
+        class Foo {
+          #foo() {}
+          #foo2() {}
+          test(other) {
+            return #foo in other;
+          }
+          test2(other) {
+            return #foo2 in other;
+          }
+        }
+        const cl = new Foo();
+        expect(cl.test({})).toBe(false);
+        expect(cl.test(cl)).toBe(true);
+        expect(cl.test2({})).toBe(false);
+        expect(cl.test2(cl)).toBe(true);
+      });
+
+      it('#method in obj for different instances of same class', () => {
+        class Foo {
+          #bar() {}
+          static check(obj) {
+            return #bar in obj;
+          }
+        }
+        const a = new Foo();
+        const b = new Foo();
+        expect(Foo.check(a)).toBe(true);
+        expect(Foo.check(b)).toBe(true);
+      });
+    });
+
+    describe('private accessor brand checks', () => {
+      it('#getter in obj returns true for instance', () => {
+        class Foo {
+          get #value() {
+            return 42;
+          }
+          static check(obj) {
+            return #value in obj;
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(true);
+        expect(Foo.check({})).toBe(false);
+      });
+
+      it('#setter in obj returns true for instance', () => {
+        class Foo {
+          set #value(v) {}
+          static check(obj) {
+            return #value in obj;
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(true);
+        expect(Foo.check({})).toBe(false);
+      });
+
+      it('#accessor (getter+setter) in obj returns true', () => {
+        class Foo {
+          #x = 0;
+          get #value() {
+            return this.#x;
+          }
+          set #value(v) {
+            this.#x = v;
+          }
+          static check(obj) {
+            return #value in obj;
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(true);
+        expect(Foo.check({})).toBe(false);
+      });
+
+      it('brand check does not trigger getter side effects', () => {
+        let getterCalled = false;
+        class Foo {
+          get #value() {
+            getterCalled = true;
+            return 42;
+          }
+          static check(obj) {
+            return #value in obj;
+          }
+        }
+        const foo = new Foo();
+        getterCalled = false;
+        Foo.check(foo);
+        expect(getterCalled).toBe(false);
+      });
+    });
+
+    describe('private static field brand checks', () => {
+      it('#staticField in Class returns true', () => {
+        class Foo {
+          static #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check(Foo)).toBe(true);
+      });
+
+      it('#staticField in instance returns false', () => {
+        class Foo {
+          static #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(false);
+      });
+
+      it('#staticField in plain object returns false', () => {
+        class Foo {
+          static #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check({})).toBe(false);
+      });
+    });
+
+    describe('private static method brand checks', () => {
+      it('#staticMethod in Class returns true', () => {
+        class Foo {
+          static #bar() {}
+          static check(obj) {
+            return #bar in obj;
+          }
+        }
+        expect(Foo.check(Foo)).toBe(true);
+      });
+
+      it('#staticMethod in instance returns false', () => {
+        class Foo {
+          static #bar() {}
+          static check(obj) {
+            return #bar in obj;
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(false);
+      });
+    });
+
+    describe('private static accessor brand checks', () => {
+      it('static #getter in Class returns true', () => {
+        class Foo {
+          static get #value() {
+            return 1;
+          }
+          static check(obj) {
+            return #value in obj;
+          }
+        }
+        expect(Foo.check(Foo)).toBe(true);
+        expect(Foo.check({})).toBe(false);
+      });
+    });
+
+    describe('RHS not an object', () => {
+      // From: private/rhs-not-object/exec.js
+      it('throws TypeError for number', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(() => Foo.check(42)).toThrow();
+      });
+
+      it('throws TypeError for string', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(() => Foo.check('hello')).toThrow();
+      });
+
+      it('throws TypeError for boolean', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(() => Foo.check(true)).toThrow();
+      });
+
+      it('throws TypeError for undefined', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(() => Foo.check(undefined)).toThrow();
+      });
+
+      it('throws TypeError for null', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(() => Foo.check(null)).toThrow();
+      });
+
+      it('throws TypeError for symbol', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(() => Foo.check(Symbol('test'))).toThrow();
+      });
+
+      it('throws for static private field with non-object RHS', () => {
+        class Foo {
+          static #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(() => Foo.check(42)).toThrow();
+        expect(() => Foo.check('str')).toThrow();
+        expect(() => Foo.check(null)).toThrow();
+      });
+
+      it('throws for private method with non-object RHS', () => {
+        class Foo {
+          #m() {}
+          static check(obj) {
+            return #m in obj;
+          }
+        }
+        expect(() => Foo.check(42)).toThrow();
+        expect(() => Foo.check(undefined)).toThrow();
+      });
+    });
+
+    describe('class name shadowing', () => {
+      // From: private/static-shadow/exec.js
+      it('static private field check works when class name is shadowed', () => {
+        class Test {
+          static #x = 1;
+          method(other) {
+            const Test = 2;
+            const func = () => {
+              const Test = 3;
+              return #x in other && Test;
+            };
+            return func() + Test;
+          }
+        }
+        const t = new Test();
+        expect(t.method(Test)).toBe(5);
+      });
+
+      it('instance private field check works when class name is shadowed', () => {
+        class Foo {
+          #x = 1;
+          check(obj) {
+            const Foo = 'shadowed';
+            return #x in obj;
+          }
+        }
+        const foo = new Foo();
+        expect(foo.check(foo)).toBe(true);
+        expect(foo.check({})).toBe(false);
+      });
+    });
+
+    describe('inheritance', () => {
+      it('#field in subclass instance returns true', () => {
+        class Base {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        class Child extends Base {}
+        expect(Base.check(new Base())).toBe(true);
+        expect(Base.check(new Child())).toBe(true);
+      });
+
+      it('subclass private field is independent from parent', () => {
+        class Base {
+          #x = 1;
+          static checkBase(obj) {
+            return #x in obj;
+          }
+        }
+        class Child extends Base {
+          #y = 2;
+          static checkChild(obj) {
+            return #y in obj;
+          }
+        }
+        const child = new Child();
+        expect(Base.checkBase(child)).toBe(true);
+        expect(Child.checkChild(child)).toBe(true);
+        expect(Child.checkChild(new Base())).toBe(false);
+      });
+
+      it('#staticField in subclass returns false', () => {
+        class Base {
+          static #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        class Child extends Base {}
+        expect(Base.check(Base)).toBe(true);
+        // Static private fields are NOT inherited
+        expect(Base.check(Child)).toBe(false);
+      });
+
+      it('private method brand check on subclass instance', () => {
+        class Base {
+          #secret() {}
+          static check(obj) {
+            return #secret in obj;
+          }
+        }
+        class Child extends Base {}
+        expect(Base.check(new Child())).toBe(true);
+        expect(Base.check({})).toBe(false);
+      });
+    });
+
+    describe('usage in expressions', () => {
+      it('negation: !(#field in obj)', () => {
+        class Foo {
+          #x = 1;
+          static notFoo(obj) {
+            return !(#x in obj);
+          }
+        }
+        expect(Foo.notFoo(new Foo())).toBe(false);
+        expect(Foo.notFoo({})).toBe(true);
+      });
+
+      it('ternary: #field in obj ? a : b', () => {
+        class Foo {
+          #x = 1;
+          static label(obj) {
+            return #x in obj ? 'foo' : 'not foo';
+          }
+        }
+        expect(Foo.label(new Foo())).toBe('foo');
+        expect(Foo.label({})).toBe('not foo');
+      });
+
+      it('logical AND: #field in obj && value', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj && 'yes';
+          }
+        }
+        expect(Foo.check(new Foo())).toBe('yes');
+        expect(Foo.check({})).toBe(false);
+      });
+
+      it('logical OR: #field in obj || fallback', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj || 'fallback';
+          }
+        }
+        expect(Foo.check(new Foo())).toBe(true);
+        expect(Foo.check({})).toBe('fallback');
+      });
+
+      it('if/else with brand check', () => {
+        class Foo {
+          #x = 1;
+          static describe(obj) {
+            if (#x in obj) {
+              return 'is Foo';
+            } else {
+              return 'not Foo';
+            }
+          }
+        }
+        expect(Foo.describe(new Foo())).toBe('is Foo');
+        expect(Foo.describe({})).toBe('not Foo');
+      });
+
+      it('brand check in while loop', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        const items = [{}, new Foo(), {}, new Foo(), {}];
+        var count = 0;
+        for (var i = 0; i < items.length; i++) {
+          if (Foo.check(items[i])) count++;
+        }
+        expect(count).toBe(2);
+      });
+
+      it('brand check with nullish coalescing', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return (#x in obj) ?? 'never';
+          }
+        }
+        // #x in obj always returns boolean, never nullish
+        expect(Foo.check(new Foo())).toBe(true);
+        expect(Foo.check({})).toBe(false);
+      });
+    });
+
+    describe('practical patterns', () => {
+      it('static factory with brand check (instanceof alternative)', () => {
+        class MyClass {
+          #brand;
+          constructor() {
+            this.#brand = undefined;
+          }
+          static isMyClass(obj) {
+            try {
+              return #brand in obj;
+            } catch {
+              return false;
+            }
+          }
+        }
+        expect(MyClass.isMyClass(new MyClass())).toBe(true);
+        expect(MyClass.isMyClass({})).toBe(false);
+        expect(MyClass.isMyClass(null)).toBe(false);
+        expect(MyClass.isMyClass(42)).toBe(false);
+      });
+
+      it('safe unwrap pattern', () => {
+        class Wrapper {
+          #value;
+          constructor(value) {
+            this.#value = value;
+          }
+          static unwrap(obj) {
+            if (typeof obj === 'object' && obj !== null && #value in obj) {
+              return obj.getValue();
+            }
+            return obj;
+          }
+          getValue() {
+            return this.#value;
+          }
+        }
+        expect(Wrapper.unwrap(new Wrapper(42))).toBe(42);
+        expect(Wrapper.unwrap('plain')).toBe('plain');
+        expect(Wrapper.unwrap(null)).toBe(null);
+      });
+
+      it('type narrowing with multiple private brands', () => {
+        class Dog {
+          #bark = true;
+          static isDog(obj) {
+            return #bark in obj;
+          }
+        }
+        class Cat {
+          #meow = true;
+          static isCat(obj) {
+            return #meow in obj;
+          }
+        }
+        const dog = new Dog();
+        const cat = new Cat();
+        expect(Dog.isDog(dog)).toBe(true);
+        expect(Cat.isCat(dog)).toBe(false);
+        expect(Dog.isDog(cat)).toBe(false);
+        expect(Cat.isCat(cat)).toBe(true);
+      });
+
+      it('filtering an array by brand', () => {
+        class Special {
+          #mark = true;
+          static is(obj) {
+            return typeof obj === 'object' && obj !== null && #mark in obj;
+          }
+        }
+        const items = [new Special(), {}, new Special(), 42, null, new Special()];
+        var count = 0;
+        for (var i = 0; i < items.length; i++) {
+          if (Special.is(items[i])) count++;
+        }
+        expect(count).toBe(3);
+      });
+    });
+
+    describe('cross-class isolation', () => {
+      it('same field name in different classes are independent brands', () => {
+        class A {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        class B {
+          #x = 2;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        const a = new A();
+        const b = new B();
+        expect(A.check(a)).toBe(true);
+        expect(A.check(b)).toBe(false);
+        expect(B.check(b)).toBe(true);
+        expect(B.check(a)).toBe(false);
+      });
+
+      it('nested class brand checks are independent', () => {
+        class Outer {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+          makeInner() {
+            return new (class Inner {
+              #x = 2;
+              static check(obj) {
+                return #x in obj;
+              }
+            })();
+          }
+        }
+        const outer = new Outer();
+        expect(Outer.check(outer)).toBe(true);
+      });
+    });
+
+    describe('various object types as RHS', () => {
+      it('works with arrays', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check([])).toBe(false);
+      });
+
+      it('works with functions', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check(function () {})).toBe(false);
+      });
+
+      it('works with class itself as object', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        // Instance brand is not on the class constructor
+        expect(Foo.check(Foo)).toBe(false);
+      });
+
+      it('works with Object.create(null)', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check(Object.create(null))).toBe(false);
+      });
+
+      it('works with Date, RegExp, Map objects', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(Foo.check(new Date())).toBe(false);
+        expect(Foo.check(/test/)).toBe(false);
+        expect(Foo.check(new Map())).toBe(false);
+      });
+    });
+
+    describe('edge cases', () => {
+      it('brand check in constructor', () => {
+        class Foo {
+          #x = 1;
+          hasBrand;
+          constructor() {
+            this.hasBrand = #x in this;
+          }
+        }
+        expect(new Foo().hasBrand).toBe(true);
+      });
+
+      it('brand check during field initialization', () => {
+        class Foo {
+          #x = 1;
+          hasBrand = #x in this;
+        }
+        expect(new Foo().hasBrand).toBe(true);
+      });
+
+      it('brand check in static block', () => {
+        let result;
+        class Foo {
+          static #x = 1;
+          static {
+            result = #x in Foo;
+          }
+        }
+        expect(result).toBe(true);
+      });
+
+      it('brand check result is always a boolean', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        expect(typeof Foo.check(new Foo())).toBe('boolean');
+        expect(typeof Foo.check({})).toBe('boolean');
+        expect(Foo.check(new Foo()) === true).toBe(true);
+        expect(Foo.check({}) === false).toBe(true);
+      });
+
+      it('brand check on Proxy-wrapped object', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        const foo = new Foo();
+        const proxy = new Proxy(foo, {});
+        // Proxy does not forward private field brand — should be false
+        // (or true if the engine sees through the proxy — implementation-defined)
+        var result = Foo.check(proxy);
+        expect(typeof result).toBe('boolean');
+      });
+
+      it('multiple brand checks in single expression', () => {
+        class A {
+          #a = 1;
+          static check(obj) {
+            return #a in obj;
+          }
+        }
+        class B {
+          #b = 2;
+          static check(obj) {
+            return #b in obj;
+          }
+        }
+        class AB extends A {
+          #b_too = 3;
+        }
+        const ab = new AB();
+        // Has A's brand (through inheritance) but not B's
+        expect(A.check(ab) && !B.check(ab)).toBe(true);
+      });
+
+      it('brand check after Object.freeze', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        const foo = Object.freeze(new Foo());
+        expect(Foo.check(foo)).toBe(true);
+      });
+
+      it('brand check after Object.seal', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        const foo = Object.seal(new Foo());
+        expect(Foo.check(foo)).toBe(true);
+      });
+
+      it('brand check with Object.preventExtensions', () => {
+        class Foo {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        }
+        const foo = new Foo();
+        Object.preventExtensions(foo);
+        expect(Foo.check(foo)).toBe(true);
+      });
+
+      it('expression class with brand check', () => {
+        const Foo = class {
+          #x = 1;
+          static check(obj) {
+            return #x in obj;
+          }
+        };
+        expect(Foo.check(new Foo())).toBe(true);
+        expect(Foo.check({})).toBe(false);
+      });
+    });
+  });
+}
diff --git a/apps/test-suite/tests/JSReactJSX.js b/apps/test-suite/tests/JSReactJSX.js
new file mode 100644
index 00000000000000..07c6450cfdffe1
--- /dev/null
+++ b/apps/test-suite/tests/JSReactJSX.js
@@ -0,0 +1,361 @@
+/* eslint-disable */
+'use strict';
+
+// Tests for React JSX development annotations (__source and __self).
+//
+// The React Native babel preset uses plugin-transform-react-jsx with
+// { runtime: 'automatic' } (development: false), generating jsx()/jsxs()
+// calls. The jsx-source and jsx-self plugins add __source/__self as JSX
+// attributes, but the automatic transform extracts and drops them since
+// development is false. They are completely dead code.
+//
+// In React 19, jsx()/jsxs()/jsxDEV() all capture their own stack trace via
+// Error("react-stack-top-frame") and store it as _debugStack on the element.
+// There is no _source or _self property — React 19 removed them entirely.
+// The _debugStack Error uses Hermes native stack traces for source location.
+//
+// These tests validate:
+// 1. React 19 elements have _debugStack, not _source/_self
+// 2. _debugStack uses Hermes native stack traces for source location
+// 3. __source/__self do not leak into props
+// 4. Component identification works without __source
+
+export const name = 'JS React JSX';
+
+export function test({ describe, it, xit, expect }) {
+  const React = require('react');
+
+  function Dummy() {
+    return null;
+  }
+
+  describe('JS React JSX', () => {
+    describe('React 19 element shape', () => {
+      it('JSX element does not have _source property', () => {
+        const el = ;
+        // React 19 removed _source — it is not defined on elements
+        expect('_source' in el).toBe(false);
+      });
+
+      it('JSX element does not have _self property', () => {
+        const el = ;
+        // React 19 removed _self — it is not defined on elements
+        expect('_self' in el).toBe(false);
+      });
+
+      it('JSX element has _debugStack', () => {
+        const el = ;
+        // React 19 captures the creation-site stack via Error()
+        expect('_debugStack' in el).toBe(true);
+        expect(el._debugStack != null).toBe(true);
+      });
+
+      it('_debugStack is an Error object', () => {
+        const el = ;
+        expect(el._debugStack instanceof Error).toBe(true);
+      });
+
+      it('_debugStack has message "react-stack-top-frame"', () => {
+        const el = ;
+        expect(el._debugStack.message).toBe('react-stack-top-frame');
+      });
+
+      it('_debugStack has a stack trace string', () => {
+        const el = ;
+        const stack = el._debugStack.stack;
+        expect(typeof stack).toBe('string');
+        expect(stack.length > 0).toBe(true);
+      });
+
+      it('JSX element has _debugTask', () => {
+        const el = ;
+        // _debugTask is set for async debugging (null if console.createTask unavailable)
+        expect('_debugTask' in el).toBe(true);
+      });
+
+      it('createElement also produces _debugStack', () => {
+        const el = React.createElement(Dummy, null);
+        // createElement in React 19 also captures Error("react-stack-top-frame")
+        expect(el._debugStack instanceof Error).toBe(true);
+      });
+
+      it('createElement element does not have _source', () => {
+        const el = React.createElement(Dummy, null);
+        expect('_source' in el).toBe(false);
+      });
+
+      it('createElement element does not have _self', () => {
+        const el = React.createElement(Dummy, null);
+        expect('_self' in el).toBe(false);
+      });
+    });
+
+    describe('_debugStack source location', () => {
+      it('_debugStack.stack has source location frames', () => {
+        const el = ;
+        const stack = el._debugStack.stack || '';
+        // Raw Hermes stacks reference the bundle, not original source files.
+        // Source filenames are available after symbolication (DevTools, error overlay).
+        expect(/:\d+:\d+/.test(stack)).toBe(true);
+      });
+
+      it('_debugStack.stack contains line:col numbers', () => {
+        const el = ;
+        const stack = el._debugStack.stack || '';
+        expect(/:\d+:\d+/.test(stack)).toBe(true);
+      });
+
+      it('elements on different lines have different _debugStack', () => {
+        const el1 = ;
+
+        const el2 = ;
+        // Created on different lines, so the captured stacks differ
+        expect(el1._debugStack.stack).not.toBe(el2._debugStack.stack);
+      });
+
+      it('_debugStack captures the calling function name', () => {
+        function createWidget() {
+          return ;
+        }
+        const el = createWidget();
+        const stack = el._debugStack.stack || '';
+        expect(stack.indexOf('createWidget') >= 0).toBe(true);
+      });
+
+      it('_debugStack captures class method name', () => {
+        class WidgetFactory {
+          build() {
+            return ;
+          }
+        }
+        const el = new WidgetFactory().build();
+        const stack = el._debugStack.stack || '';
+        expect(stack.indexOf('build') >= 0).toBe(true);
+      });
+
+      it('_debugStack captures arrow function name', () => {
+        const makeElement = () => ;
+        const el = makeElement();
+        const stack = el._debugStack.stack || '';
+        expect(stack.indexOf('makeElement') >= 0).toBe(true);
+      });
+
+      it('children have independent _debugStack', () => {
+        function Parent({ children }) {
+          return null;
+        }
+        function Child() {
+          return null;
+        }
+        const parent = (
+          
+            
+          
+        );
+        const child = parent.props.children;
+        expect(parent._debugStack.stack).not.toBe(child._debugStack.stack);
+      });
+
+      it('createElement _debugStack has source location', () => {
+        const el = React.createElement(Dummy, null);
+        const stack = el._debugStack.stack || '';
+        expect(/:\d+:\d+/.test(stack)).toBe(true);
+      });
+    });
+
+    describe('_debugStack vs __source equivalence', () => {
+      // __source used to provide { fileName, lineNumber, columnNumber }.
+      // _debugStack.stack provides the same info (and more: full call chain)
+      // via Hermes native stack traces.
+
+      it('_debugStack.stack has source location (replaces __source.fileName/lineNumber/columnNumber)', () => {
+        const el = ;
+        const stack = el._debugStack.stack || '';
+        // __source provided { fileName, lineNumber, columnNumber } statically.
+        // _debugStack captures the same via Error().stack — file/line/col are
+        // available after symbolication (which DevTools and error overlay perform).
+        expect(/:\d+:\d+/.test(stack)).toBe(true);
+      });
+
+      it('_debugStack provides full call chain (more than __source)', () => {
+        function outerFactory() {
+          function innerFactory() {
+            return ;
+          }
+          return innerFactory();
+        }
+        const el = outerFactory();
+        const stack = el._debugStack.stack || '';
+        // __source only had the immediate creation site.
+        // _debugStack has the full call chain — both functions visible.
+        expect(stack.indexOf('innerFactory') >= 0).toBe(true);
+        expect(stack.indexOf('outerFactory') >= 0).toBe(true);
+      });
+
+      it('manual Error().stack matches _debugStack format', () => {
+        const el = ;
+        const err = new Error('manual');
+        // Both use the same Hermes stack trace mechanism
+        expect(/:\d+:\d+/.test(el._debugStack.stack || '')).toBe(true);
+        expect(/:\d+:\d+/.test(err.stack || '')).toBe(true);
+      });
+    });
+
+    describe('__source and __self prop stripping', () => {
+      it('__source does not leak into JSX element props', () => {
+        const el = ;
+        expect(el.props.__source).toBe(undefined);
+      });
+
+      it('__self does not leak into JSX element props', () => {
+        const el = ;
+        expect(el.props.__self).toBe(undefined);
+      });
+
+      it('createElement strips __source from props', () => {
+        const el = React.createElement(Dummy, {
+          __source: { fileName: 'fake.js', lineNumber: 1, columnNumber: 0 },
+          label: 'hello',
+        });
+        // React 19 strips __source from props (line 961-962 of react.development.js)
+        expect(el.props.__source).toBe(undefined);
+        expect(el.props.label).toBe('hello');
+      });
+
+      it('createElement strips __self from props', () => {
+        const el = React.createElement(Dummy, {
+          __self: {},
+          label: 'hello',
+        });
+        // React 19 strips __self from props (line 961 of react.development.js)
+        expect(el.props.__self).toBe(undefined);
+        expect(el.props.label).toBe('hello');
+      });
+
+      it('regular props are preserved alongside stripped __source/__self', () => {
+        const el = React.createElement(Dummy, {
+          __source: { fileName: 'x.js', lineNumber: 1, columnNumber: 0 },
+          __self: {},
+          foo: 1,
+          bar: 'two',
+          baz: true,
+        });
+        expect(el.props.foo).toBe(1);
+        expect(el.props.bar).toBe('two');
+        expect(el.props.baz).toBe(true);
+        expect(el.props.__source).toBe(undefined);
+        expect(el.props.__self).toBe(undefined);
+      });
+    });
+
+    describe('Hermes native source info', () => {
+      it('Error().stack has source location', () => {
+        const err = new Error('test');
+        const stack = err.stack || '';
+        // Raw Hermes stacks have line:col; original filenames after symbolication
+        expect(/:\d+:\d+/.test(stack)).toBe(true);
+      });
+
+      it('Error().stack has line:col numbers', () => {
+        const err = new Error('test');
+        expect(/:\d+:\d+/.test(err.stack || '')).toBe(true);
+      });
+
+      it('Error().stack preserves function name', () => {
+        function renderProfile() {
+          return new Error('trace');
+        }
+        const err = renderProfile();
+        expect((err.stack || '').indexOf('renderProfile') >= 0).toBe(true);
+      });
+
+      it('Error().stack preserves class method name', () => {
+        class Renderer {
+          render() {
+            return new Error('trace');
+          }
+        }
+        const err = new Renderer().render();
+        expect((err.stack || '').indexOf('render') >= 0).toBe(true);
+      });
+
+      it('Error().stack preserves arrow function name', () => {
+        const buildView = () => new Error('trace');
+        const err = buildView();
+        expect((err.stack || '').indexOf('buildView') >= 0).toBe(true);
+      });
+
+      it('nested call chain is preserved in Error().stack', () => {
+        function database() {
+          return new Error('query failed');
+        }
+        function repository() {
+          return database();
+        }
+        function service() {
+          return repository();
+        }
+        const err = service();
+        const stack = err.stack || '';
+        expect(stack.indexOf('database') >= 0).toBe(true);
+        expect(stack.indexOf('repository') >= 0).toBe(true);
+        expect(stack.indexOf('service') >= 0).toBe(true);
+      });
+    });
+
+    describe('component identification without __source', () => {
+      it('named function component has type.name', () => {
+        function ProfileCard() {
+          return null;
+        }
+        const el = ;
+        expect(el.type.name).toBe('ProfileCard');
+      });
+
+      it('displayName is available on type', () => {
+        function Comp() {
+          return null;
+        }
+        Comp.displayName = 'FancyButton';
+        const el = ;
+        expect(el.type.displayName).toBe('FancyButton');
+      });
+
+      it('type.name works with createElement', () => {
+        function UserAvatar() {
+          return null;
+        }
+        const el = React.createElement(UserAvatar, null);
+        expect(el.type.name).toBe('UserAvatar');
+      });
+
+      it('forwardRef render function name is accessible', () => {
+        const FancyInput = React.forwardRef(function FancyInput() {
+          return null;
+        });
+        expect(FancyInput.render.name).toBe('FancyInput');
+      });
+
+      it('memo preserves inner component name', () => {
+        function ExpensiveList() {
+          return null;
+        }
+        const Memoized = React.memo(ExpensiveList);
+        expect(Memoized.type.name).toBe('ExpensiveList');
+      });
+
+      it('anonymous component has empty name', () => {
+        const el = React.createElement(function () {
+          return null;
+        }, null);
+        expect(el.type.name).toBe('');
+      });
+
+      it('arrow component assigned to variable has name', () => {
+        const MyArrow = () => null;
+        const el = ;
+        expect(el.type.name).toBe('MyArrow');
+      });
+    });
+  });
+}
diff --git a/apps/test-suite/tests/JSUnicodeRegexes.js b/apps/test-suite/tests/JSUnicodeRegexes.js
new file mode 100644
index 00000000000000..eb8c71fd391986
--- /dev/null
+++ b/apps/test-suite/tests/JSUnicodeRegexes.js
@@ -0,0 +1,772 @@
+/* eslint-disable */
+'use strict';
+
+// Comprehensive runtime compliance tests for the Unicode flag (/u) in regexes.
+// Validates that /u flag semantics work correctly at runtime in the Hermes engine.
+//
+// @babel/plugin-transform-unicode-regex rewrites /pattern/u regexes by:
+// 1. Expanding astral plane characters (U+10000+) to surrogate pair sequences
+// 2. Expanding \u{XXXXX} code point escapes to surrogate pairs or BMP chars
+// 3. Making . match full code points (including astral) instead of code units
+// 4. Expanding character classes to handle surrogate pairs correctly
+// 5. Adjusting case-insensitive matching for Unicode case folding rules
+// 6. Removing the /u flag after rewriting
+//
+// Cases sourced from regexpu-core transformations and @babel/plugin-transform-unicode-regex,
+// supplemented with edge cases.
+
+export const name = 'JS Unicode Regexes';
+
+export function test({ describe, it, xit, expect }) {
+  describe('JS Unicode Regexes', () => {
+    describe('basic /u flag semantics', () => {
+      it('/u flag regex matches ASCII text', () => {
+        const re = /hello/u;
+        expect(re.test('hello')).toBe(true);
+        expect(re.test('world')).toBe(false);
+      });
+
+      it('/u flag regex works with exec()', () => {
+        const re = /(\w+)/u;
+        const match = re.exec('hello');
+        expect(match[0]).toBe('hello');
+        expect(match[1]).toBe('hello');
+      });
+
+      it('/u flag regex works with match()', () => {
+        const match = 'hello world'.match(/(\w+)/u);
+        expect(match[0]).toBe('hello');
+        expect(match[1]).toBe('hello');
+      });
+
+      it('/u flag regex works with global search', () => {
+        const re = /\w+/gu;
+        const matches = 'hello world'.match(re);
+        expect(matches.length).toBe(2);
+        expect(matches[0]).toBe('hello');
+        expect(matches[1]).toBe('world');
+      });
+
+      it('/u flag regex works with test()', () => {
+        expect(/^\d+$/u.test('12345')).toBe(true);
+        expect(/^\d+$/u.test('123a5')).toBe(false);
+      });
+
+      it('/u flag regex preserves lastIndex with global flag', () => {
+        const re = /\d+/gu;
+        const str = 'a1b22c333';
+        expect(re.test(str)).toBe(true);
+        expect(re.lastIndex).toBe(2);
+        expect(re.test(str)).toBe(true);
+        expect(re.lastIndex).toBe(5);
+      });
+    });
+
+    describe('Unicode code point escapes (\\u{XXXXX})', () => {
+      it('\\u{} matches BMP character', () => {
+        const re = /\u{0041}/u;
+        expect(re.test('A')).toBe(true);
+        expect(re.test('B')).toBe(false);
+      });
+
+      it('\\u{} matches multi-digit BMP code point', () => {
+        const re = /\u{00E9}/u;
+        expect(re.test('\u00E9')).toBe(true); // é
+        expect(re.test('e')).toBe(false);
+      });
+
+      it('\\u{} matches astral plane character', () => {
+        const re = /\u{1F600}/u;
+        expect(re.test('\u{1F600}')).toBe(true); // 😀
+        expect(re.test('A')).toBe(false);
+      });
+
+      it('\\u{} matches musical symbol (astral plane)', () => {
+        // U+1D306 = TETRAGRAM FOR CENTRE (𝌆)
+        const re = /\u{1D306}/u;
+        expect(re.test('\u{1D306}')).toBe(true);
+        expect(re.test('A')).toBe(false);
+      });
+
+      it('\\u{} in character class', () => {
+        const re = /[\u{1F600}\u{1F601}\u{1F602}]/u;
+        expect(re.test('\u{1F600}')).toBe(true); // 😀
+        expect(re.test('\u{1F601}')).toBe(true); // 😁
+        expect(re.test('\u{1F602}')).toBe(true); // 😂
+        expect(re.test('\u{1F603}')).toBe(false);
+      });
+
+      it('\\u{} range in character class', () => {
+        const re = /[\u{1F600}-\u{1F602}]/u;
+        expect(re.test('\u{1F600}')).toBe(true);
+        expect(re.test('\u{1F601}')).toBe(true);
+        expect(re.test('\u{1F602}')).toBe(true);
+        expect(re.test('\u{1F603}')).toBe(false);
+        expect(re.test('\u{1F5FF}')).toBe(false);
+      });
+
+      it('\\u{} with zero-padded code point', () => {
+        const re = /\u{00000041}/u;
+        expect(re.test('A')).toBe(true);
+      });
+
+      it('\\u{} maximum valid code point', () => {
+        const re = /\u{10FFFF}/u;
+        expect(re.test('\u{10FFFF}')).toBe(true);
+      });
+    });
+
+    describe('astral plane character matching', () => {
+      it('astral character literal in pattern', () => {
+        const re = /𝌆/u;
+        expect(re.test('𝌆')).toBe(true);
+        expect(re.test('A')).toBe(false);
+      });
+
+      it('emoji literal in pattern', () => {
+        const re = /😀/u;
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('😁')).toBe(false);
+      });
+
+      it('astral character is one code point, not two code units', () => {
+        // Without /u, emoji is two code units (surrogate pair) and . matches one unit
+        // With /u, emoji is one code point and . matches the full character
+        const re = /^.$/u;
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('A')).toBe(true);
+      });
+
+      it('without /u, astral character is two code units', () => {
+        // Without /u, . matches one code unit, so emoji (2 units) needs ..
+        const re = /^.$/;
+        expect(re.test('😀')).toBe(false); // Two code units, . only matches one
+        const re2 = /^..$/;
+        expect(re2.test('😀')).toBe(true); // Two code units matched by two dots
+      });
+
+      it('string of astral characters matches with /u', () => {
+        const re = /^.{3}$/u;
+        expect(re.test('😀😁😂')).toBe(true);
+      });
+
+      it('astral characters in character class range', () => {
+        // U+1D306 to U+1D308
+        const re = /[\u{1D306}-\u{1D308}]/u;
+        expect(re.test('\u{1D306}')).toBe(true);
+        expect(re.test('\u{1D307}')).toBe(true);
+        expect(re.test('\u{1D308}')).toBe(true);
+        expect(re.test('\u{1D305}')).toBe(false);
+        expect(re.test('\u{1D309}')).toBe(false);
+      });
+
+      it('mixed BMP and astral in character class', () => {
+        const re = /[a-z\u{1F600}-\u{1F602}]/u;
+        expect(re.test('a')).toBe(true);
+        expect(re.test('z')).toBe(true);
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('😂')).toBe(true);
+        expect(re.test('1')).toBe(false);
+      });
+
+      it('exec returns full code point, not surrogate halves', () => {
+        const re = /(.)/u;
+        const match = re.exec('😀');
+        expect(match[0]).toBe('😀');
+        expect(match[1]).toBe('😀');
+        expect(match[0].length).toBe(2); // Still 2 UTF-16 code units
+      });
+
+      it('quantifier applies to full code point', () => {
+        const re = /^(.)\1$/u;
+        expect(re.test('😀😀')).toBe(true);
+        expect(re.test('😀😁')).toBe(false);
+      });
+    });
+
+    describe('dot matching with /u', () => {
+      it('dot matches BMP character with /u', () => {
+        const re = /^.$/u;
+        expect(re.test('A')).toBe(true);
+        expect(re.test('\u00E9')).toBe(true); // é
+        expect(re.test('\u4E16')).toBe(true); // 世
+      });
+
+      it('dot matches astral plane character with /u', () => {
+        const re = /^.$/u;
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('𝌆')).toBe(true);
+        expect(re.test('\u{1F4A9}')).toBe(true); // 💩
+      });
+
+      it('dot does not match newline with /u', () => {
+        const re = /^.$/u;
+        expect(re.test('\n')).toBe(false);
+        expect(re.test('\r')).toBe(false);
+        expect(re.test('\u2028')).toBe(false); // Line separator
+        expect(re.test('\u2029')).toBe(false); // Paragraph separator
+      });
+
+      it('dot matches newline with /su', () => {
+        const re = /^.$/su;
+        expect(re.test('\n')).toBe(true);
+        expect(re.test('\r')).toBe(true);
+        expect(re.test('\u2028')).toBe(true);
+        expect(re.test('\u2029')).toBe(true);
+      });
+
+      it('dot with /su matches astral characters', () => {
+        const re = /^.$/su;
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('A')).toBe(true);
+      });
+
+      it('multiple dots match multiple code points', () => {
+        const re = /^..$/u;
+        expect(re.test('AB')).toBe(true);
+        expect(re.test('😀😁')).toBe(true);
+        expect(re.test('A😀')).toBe(true);
+        expect(re.test('ABC')).toBe(false);
+      });
+
+      it('dot with quantifier counts code points', () => {
+        const re = /^.{2}$/u;
+        expect(re.test('AB')).toBe(true);
+        expect(re.test('😀😁')).toBe(true);
+        expect(re.test('😀')).toBe(false);
+      });
+    });
+
+    describe('character class escapes with /u', () => {
+      it('\\d matches digits with /u', () => {
+        const re = /^\d+$/u;
+        expect(re.test('12345')).toBe(true);
+        expect(re.test('abc')).toBe(false);
+      });
+
+      it('\\D matches non-digits including astral with /u', () => {
+        const re = /^\D$/u;
+        expect(re.test('A')).toBe(true);
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('5')).toBe(false);
+      });
+
+      it('\\w matches word characters with /u', () => {
+        const re = /^\w+$/u;
+        expect(re.test('hello_123')).toBe(true);
+        expect(re.test('hello world')).toBe(false);
+      });
+
+      it('\\W matches non-word characters including astral with /u', () => {
+        const re = /^\W$/u;
+        expect(re.test('!')).toBe(true);
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('a')).toBe(false);
+      });
+
+      it('\\s matches whitespace with /u', () => {
+        const re = /^\s+$/u;
+        expect(re.test(' \t\n')).toBe(true);
+        expect(re.test('\u00A0')).toBe(true); // non-breaking space
+        expect(re.test('a')).toBe(false);
+      });
+
+      it('\\S matches non-whitespace including astral with /u', () => {
+        const re = /^\S$/u;
+        expect(re.test('A')).toBe(true);
+        expect(re.test('😀')).toBe(true);
+        expect(re.test(' ')).toBe(false);
+      });
+
+      it('\\b word boundary works with /u', () => {
+        const re = /\bhello\b/u;
+        expect(re.test('hello world')).toBe(true);
+        expect(re.test('helloworld')).toBe(false);
+      });
+    });
+
+    describe('negated character classes with /u', () => {
+      it('negated class [^...] with BMP characters', () => {
+        const re = /^[^abc]$/u;
+        expect(re.test('d')).toBe(true);
+        expect(re.test('a')).toBe(false);
+      });
+
+      it('negated class matches astral characters', () => {
+        const re = /^[^abc]$/u;
+        expect(re.test('😀')).toBe(true);
+      });
+
+      it('negated class with astral character', () => {
+        const re = /^[^\u{1F600}]$/u;
+        expect(re.test('A')).toBe(true);
+        expect(re.test('😁')).toBe(true);
+        expect(re.test('😀')).toBe(false);
+      });
+
+      it('negated class excludes full code point', () => {
+        // Without /u, [^\u{1F600}] would exclude the individual surrogate halves
+        // With /u, it excludes the whole code point
+        const re = /^[^\u{1F600}]+$/u;
+        expect(re.test('hello')).toBe(true);
+        expect(re.test('😁😂')).toBe(true);
+        expect(re.test('😀')).toBe(false);
+      });
+    });
+
+    describe('case-insensitive with /u (ui flags)', () => {
+      it('basic ASCII case insensitive', () => {
+        const re = /^[a-z]+$/ui;
+        expect(re.test('hello')).toBe(true);
+        expect(re.test('HELLO')).toBe(true);
+        expect(re.test('Hello')).toBe(true);
+      });
+
+      it('Unicode case equivalence: K (U+212A) matches k', () => {
+        // U+212A is KELVIN SIGN, which case-folds to 'k'
+        const re = /k/ui;
+        expect(re.test('k')).toBe(true);
+        expect(re.test('K')).toBe(true);
+        expect(re.test('\u212A')).toBe(true); // Kelvin sign
+      });
+
+      it('Unicode case equivalence: long s (U+017F) matches s', () => {
+        // U+017F is LATIN SMALL LETTER LONG S, which case-folds to 's'
+        const re = /s/ui;
+        expect(re.test('s')).toBe(true);
+        expect(re.test('S')).toBe(true);
+        expect(re.test('\u017F')).toBe(true); // ſ (long s)
+      });
+
+      it('case insensitive match for accented characters', () => {
+        const re = /é/ui;
+        expect(re.test('\u00E9')).toBe(true); // é
+        expect(re.test('\u00C9')).toBe(true); // É
+      });
+
+      it('case insensitive with character class range', () => {
+        const re = /^[a-z]$/ui;
+        expect(re.test('A')).toBe(true);
+        expect(re.test('z')).toBe(true);
+        expect(re.test('Z')).toBe(true);
+        // Long s and Kelvin sign are case equivalents of s and k
+        expect(re.test('\u017F')).toBe(true); // ſ
+        expect(re.test('\u212A')).toBe(true); // K (Kelvin)
+      });
+
+      it('case insensitive with astral characters', () => {
+        // Most astral characters don't have case variants
+        // But the flag combination should still work
+        const re = /\u{1F600}/ui;
+        expect(re.test('\u{1F600}')).toBe(true);
+      });
+
+      it('Greek case insensitive', () => {
+        const re = /α/ui;
+        expect(re.test('α')).toBe(true); // lowercase alpha
+        expect(re.test('Α')).toBe(true); // uppercase Alpha
+      });
+
+      it('Cyrillic case insensitive', () => {
+        const re = /б/ui;
+        expect(re.test('б')).toBe(true); // lowercase
+        expect(re.test('Б')).toBe(true); // uppercase
+      });
+    });
+
+    describe('surrogate pair handling', () => {
+      it('/u treats surrogate pair as single code point', () => {
+        // 😀 is U+1F600, stored as surrogate pair \uD83D\uDE00
+        const str = '\uD83D\uDE00';
+        const re = /^.$/u;
+        expect(re.test(str)).toBe(true); // One code point
+      });
+
+      it('without /u, surrogate pair is two code units', () => {
+        const str = '\uD83D\uDE00';
+        const re = /^..$/;
+        expect(re.test(str)).toBe(true); // Two code units
+      });
+
+      it('lone high surrogate does not match . with /u', () => {
+        // A lone high surrogate is not a valid code point sequence
+        const re = /^.$/u;
+        expect(re.test('\uD83D')).toBe(true);
+      });
+
+      it('astral range boundary correctness', () => {
+        // U+10000 is the first astral code point
+        const re = /\u{10000}/u;
+        expect(re.test('\u{10000}')).toBe(true);
+        expect(re.test('\uFFFF')).toBe(false);
+      });
+
+      it('consecutive astral characters', () => {
+        const re = /^(.)(.)$/u;
+        const match = re.exec('😀😁');
+        expect(match[1]).toBe('😀');
+        expect(match[2]).toBe('😁');
+      });
+    });
+
+    describe('quantifiers with /u', () => {
+      it('+ quantifier applies to full code point', () => {
+        const re = /^a+$/u;
+        expect(re.test('aaa')).toBe(true);
+        expect(re.test('')).toBe(false);
+      });
+
+      it('* quantifier with astral characters', () => {
+        const re = /^😀*$/u;
+        expect(re.test('')).toBe(true);
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('😀😀😀')).toBe(true);
+        expect(re.test('😀a')).toBe(false);
+      });
+
+      it('{n} quantifier counts code points', () => {
+        const re = /^.{3}$/u;
+        expect(re.test('abc')).toBe(true);
+        expect(re.test('😀😁😂')).toBe(true);
+        expect(re.test('a😀b')).toBe(true);
+        expect(re.test('abcd')).toBe(false);
+      });
+
+      it('{n,m} range quantifier with code points', () => {
+        const re = /^.{2,4}$/u;
+        expect(re.test('ab')).toBe(true);
+        expect(re.test('😀😁')).toBe(true);
+        expect(re.test('😀😁😂😃')).toBe(true);
+        expect(re.test('a')).toBe(false);
+        expect(re.test('abcde')).toBe(false);
+      });
+
+      it('? quantifier on astral character', () => {
+        const re = /^a😀?b$/u;
+        expect(re.test('ab')).toBe(true);
+        expect(re.test('a😀b')).toBe(true);
+        expect(re.test('a😀😀b')).toBe(false);
+      });
+    });
+
+    describe('capturing groups with /u', () => {
+      it('capturing group captures full code point', () => {
+        const re = /^(.)$/u;
+        const match = re.exec('😀');
+        expect(match[1]).toBe('😀');
+      });
+
+      it('multiple groups with astral characters', () => {
+        const re = /^(.)(.)(.)$/u;
+        const match = re.exec('a😀b');
+        expect(match[1]).toBe('a');
+        expect(match[2]).toBe('😀');
+        expect(match[3]).toBe('b');
+      });
+
+      it('backreference matches same code point', () => {
+        const re = /^(.)\1$/u;
+        expect(re.test('aa')).toBe(true);
+        expect(re.test('😀😀')).toBe(true);
+        expect(re.test('😀😁')).toBe(false);
+      });
+
+      it('named group with /u', () => {
+        const re = /(?.)/u;
+        const match = re.exec('😀');
+        expect(match.groups.emoji).toBe('😀');
+      });
+
+      it('non-capturing group with /u', () => {
+        const re = /^(?:.)$/u;
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('A')).toBe(true);
+      });
+    });
+
+    describe('anchors and boundaries with /u', () => {
+      it('^ and $ work with astral characters', () => {
+        const re = /^😀$/u;
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('😀😀')).toBe(false);
+      });
+
+      it('multiline anchors with /u', () => {
+        const re = /^.$/gmu;
+        const matches = 'a\n😀\nb'.match(re);
+        expect(matches.length).toBe(3);
+        expect(matches[0]).toBe('a');
+        expect(matches[1]).toBe('😀');
+        expect(matches[2]).toBe('b');
+      });
+
+      it('\\b boundary with /u before astral character', () => {
+        const re = /\bword/u;
+        expect(re.test('word')).toBe(true);
+        expect(re.test('a word')).toBe(true);
+      });
+    });
+
+    describe('alternation with /u', () => {
+      it('alternation between BMP and astral', () => {
+        const re = /^(?:A|😀)$/u;
+        expect(re.test('A')).toBe(true);
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('B')).toBe(false);
+      });
+
+      it('alternation between astral characters', () => {
+        const re = /^(?:😀|😁|😂)$/u;
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('😁')).toBe(true);
+        expect(re.test('😂')).toBe(true);
+        expect(re.test('😃')).toBe(false);
+      });
+    });
+
+    describe('lookahead and lookbehind with /u', () => {
+      it('positive lookahead with astral', () => {
+        const re = /A(?=😀)/u;
+        expect(re.test('A😀')).toBe(true);
+        expect(re.test('AB')).toBe(false);
+      });
+
+      it('negative lookahead with astral', () => {
+        const re = /A(?!😀)/u;
+        expect(re.test('AB')).toBe(true);
+        expect(re.test('A😀')).toBe(false);
+      });
+
+      it('positive lookbehind with astral', () => {
+        const re = /(?<=😀)B/u;
+        expect(re.test('😀B')).toBe(true);
+        expect(re.test('AB')).toBe(false);
+      });
+
+      it('negative lookbehind with astral', () => {
+        const re = /(? {
+      it('replace astral character', () => {
+        const result = '😀'.replace(/😀/u, 'smile');
+        expect(result).toBe('smile');
+      });
+
+      it('global replace of astral characters', () => {
+        const result = 'a😀b😀c'.replace(/😀/gu, 'X');
+        expect(result).toBe('aXbXc');
+      });
+
+      it('replace with capture group on astral', () => {
+        const result = 'a😀b'.replace(/(😀)/u, '[$1]');
+        expect(result).toBe('a[😀]b');
+      });
+
+      it('split on astral character', () => {
+        const parts = 'a😀b😀c'.split(/😀/u);
+        expect(parts).toEqual(['a', 'b', 'c']);
+      });
+
+      it('replace dot-matched astral character', () => {
+        const result = 'a😀b'.replace(/(.)/gu, '[$1]');
+        expect(result).toBe('[a][😀][b]');
+      });
+
+      it('search for astral character', () => {
+        const idx = 'hello😀world'.search(/😀/u);
+        expect(idx).toBe(5);
+      });
+    });
+
+    describe('combined flags', () => {
+      it('/gu global unicode', () => {
+        const re = /./gu;
+        const matches = '😀😁😂'.match(re);
+        expect(matches.length).toBe(3);
+        expect(matches[0]).toBe('😀');
+        expect(matches[1]).toBe('😁');
+        expect(matches[2]).toBe('😂');
+      });
+
+      it('/giu global case-insensitive unicode', () => {
+        const re = /[a-z]/giu;
+        const matches = 'Hello'.match(re);
+        expect(matches.length).toBe(5);
+      });
+
+      it('/mu multiline unicode', () => {
+        const re = /^.$/mu;
+        expect(re.test('😀')).toBe(true);
+      });
+
+      it('/siu dotAll case-insensitive unicode', () => {
+        const re = /^.$/siu;
+        expect(re.test('\n')).toBe(true);
+        expect(re.test('😀')).toBe(true);
+      });
+
+      it('/yu sticky unicode', () => {
+        const re = /./yu;
+        re.lastIndex = 0;
+        const match = re.exec('😀A');
+        expect(match[0]).toBe('😀');
+        expect(re.lastIndex).toBe(2); // 2 code units for the surrogate pair
+      });
+    });
+
+    describe('escapes with /u', () => {
+      it('\\t \\n \\r work with /u', () => {
+        expect(/^\t$/u.test('\t')).toBe(true);
+        expect(/^\n$/u.test('\n')).toBe(true);
+        expect(/^\r$/u.test('\r')).toBe(true);
+      });
+
+      it('\\0 matches null character', () => {
+        expect(/^\0$/u.test('\0')).toBe(true);
+      });
+
+      it('hex escape \\xNN works with /u', () => {
+        expect(/^\x41$/u.test('A')).toBe(true);
+        expect(/^\xFF$/u.test('\xFF')).toBe(true);
+      });
+
+      it('BMP unicode escape \\uNNNN works with /u', () => {
+        expect(/^\u0041$/u.test('A')).toBe(true);
+        expect(/^\u00E9$/u.test('é')).toBe(true);
+      });
+
+      it('escaped special characters with /u', () => {
+        expect(/^\.$/u.test('.')).toBe(true);
+        expect(/^\*$/u.test('*')).toBe(true);
+        expect(/^\($/u.test('(')).toBe(true);
+        expect(/^\)$/u.test(')')).toBe(true);
+        expect(/^\[$/u.test('[')).toBe(true);
+        expect(/^\]$/u.test(']')).toBe(true);
+      });
+    });
+
+    describe('edge cases', () => {
+      it('empty regex with /u', () => {
+        const re = /(?:)/u;
+        expect(re.test('')).toBe(true);
+        expect(re.test('a')).toBe(true);
+      });
+
+      it('empty character class complement matches astral', () => {
+        // [^] without /u matches any character including newlines
+        // With /u we use [\s\S] to match any character
+        const re = /^[\s\S]$/u;
+        expect(re.test('😀')).toBe(true);
+        expect(re.test('\n')).toBe(true);
+      });
+
+      it('consecutive astral characters in class', () => {
+        const re = /^[😀😁😂]+$/u;
+        expect(re.test('😀😁😂😀')).toBe(true);
+        expect(re.test('😃')).toBe(false);
+      });
+
+      it('astral character at string boundaries', () => {
+        const re = /^😀/u;
+        expect(re.test('😀hello')).toBe(true);
+        const re2 = /😀$/u;
+        expect(re2.test('hello😀')).toBe(true);
+      });
+
+      it('very long string with astral characters', () => {
+        const str = '😀'.repeat(1000);
+        const re = /^(😀)+$/u;
+        expect(re.test(str)).toBe(true);
+      });
+
+      it('mixed BMP and astral string', () => {
+        const re = /^[a-z😀]+$/u;
+        expect(re.test('hello😀world')).toBe(true);
+        expect(re.test('hello😀world!')).toBe(false);
+      });
+
+      it('new RegExp with /u flag', () => {
+        const re = new RegExp('.', 'u');
+        const match = re.exec('😀');
+        expect(match[0]).toBe('😀');
+      });
+
+      it('new RegExp with \\u{} escape and /u flag', () => {
+        const re = new RegExp('\\u{1F600}', 'u');
+        expect(re.test('😀')).toBe(true);
+      });
+
+      it('lastIndex advances by code units, not code points', () => {
+        const re = /./gu;
+        const str = '😀a';
+        const m1 = re.exec(str);
+        expect(m1[0]).toBe('😀');
+        expect(re.lastIndex).toBe(2); // 2 code units
+        const m2 = re.exec(str);
+        expect(m2[0]).toBe('a');
+        expect(re.lastIndex).toBe(3); // 3 code units total
+      });
+    });
+
+    describe('practical patterns', () => {
+      it('match any emoji in range', () => {
+        const re = /[\u{1F600}-\u{1F64F}]/u;
+        expect(re.test('😀')).toBe(true); // U+1F600
+        expect(re.test('🙏')).toBe(true); // U+1F64F
+        expect(re.test('A')).toBe(false);
+      });
+
+      it('extract emoji from text', () => {
+        const re = /[\u{1F600}-\u{1F64F}]/gu;
+        const matches = 'Hello 😀 World 😂!'.match(re);
+        expect(matches.length).toBe(2);
+        expect(matches[0]).toBe('😀');
+        expect(matches[1]).toBe('😂');
+      });
+
+      it('count code points in string', () => {
+        const re = /./gsu;
+        const str = 'a😀b😁c';
+        const count = str.match(re).length;
+        expect(count).toBe(5); // 5 code points
+        expect(str.length).toBe(7); // 7 code units
+      });
+
+      it('validate hex color with /u', () => {
+        const re = /^#[0-9a-f]{6}$/ui;
+        expect(re.test('#FF8800')).toBe(true);
+        expect(re.test('#ff8800')).toBe(true);
+        expect(re.test('#GGGGGG')).toBe(false);
+      });
+
+      it('match CJK characters', () => {
+        const re = /^[\u4E00-\u9FFF]+$/u;
+        expect(re.test('你好世界')).toBe(true);
+        expect(re.test('hello')).toBe(false);
+      });
+
+      it('match mathematical symbols (astral plane)', () => {
+        // U+1D400-U+1D7FF: Mathematical Alphanumeric Symbols
+        const re = /[\u{1D400}-\u{1D7FF}]/u;
+        expect(re.test('\u{1D400}')).toBe(true); // 𝐀 (Math bold A)
+        expect(re.test('\u{1D7FF}')).toBe(true); // 𝟿 (Math monospace 9)
+        expect(re.test('A')).toBe(false);
+      });
+
+      it('split text preserving emoji', () => {
+        const re = /\s+/u;
+        const parts = 'hello 😀 world'.split(re);
+        expect(parts).toEqual(['hello', '😀', 'world']);
+      });
+
+      it('replace each code point individually', () => {
+        const result = 'a😀b'.replace(/./gu, (ch) => `[${ch}]`);
+        expect(result).toBe('[a][😀][b]');
+      });
+    });
+  });
+}
diff --git a/docs/common/client-redirects.test.ts b/docs/common/client-redirects.test.ts
index 18dbdf5e639453..babedc383c5192 100644
--- a/docs/common/client-redirects.test.ts
+++ b/docs/common/client-redirects.test.ts
@@ -69,3 +69,25 @@ test('removes null from end of paths', () => {
 test('redirect SDK permissions to the permission guide', () => {
   expect(getRedirectPath('/versions/latest/sdk/permissions/')).toEqual('/guides/permissions/');
 });
+
+test('rewrites /eas/build/** prefix to /build/**', () => {
+  expect(getRedirectPath('/eas/build/introduction/')).toEqual('/build/introduction/');
+});
+
+test('rewrites /eas/update/** prefix to /eas-update/**', () => {
+  expect(getRedirectPath('/eas/update/introduction/')).toEqual('/eas-update/introduction/');
+});
+
+test('rewrites /eas/submit/** prefix to /submit/**', () => {
+  expect(getRedirectPath('/eas/submit/android/')).toEqual('/submit/android/');
+});
+
+test('rewrites /eas/insights/** prefix to /eas-insights/**', () => {
+  expect(getRedirectPath('/eas/insights/introduction/')).toEqual('/eas-insights/introduction/');
+});
+
+test('does not rewrite /eas/** paths that are canonical (workflows, hosting, metadata, ai)', () => {
+  expect(getRedirectPath('/eas/workflows/get-started/')).toEqual('/eas/workflows/get-started/');
+  expect(getRedirectPath('/eas/hosting/introduction/')).toEqual('/eas/hosting/introduction/');
+  expect(getRedirectPath('/eas/metadata/')).toEqual('/eas/metadata/');
+});
diff --git a/docs/common/client-redirects.ts b/docs/common/client-redirects.ts
index e08d11d7e52040..90ed5a0a4ed56b 100644
--- a/docs/common/client-redirects.ts
+++ b/docs/common/client-redirects.ts
@@ -36,6 +36,16 @@ export function getRedirectPath(redirectPath: string): string {
     }
   }
 
+  // Catch path-prefix renames that the static `_redirects` rules also handle
+  // at the Cloudflare edge. This branch backstops in-app Next.js client
+  // navigation (e.g. an MDX link to a stale path), which never round-trips
+  // through the edge and would otherwise hit the 404 page.
+  for (const { pattern, replacement } of WILDCARD_RENAMES) {
+    if (pattern.test(redirectPath)) {
+      return redirectPath.replace(pattern, replacement);
+    }
+  }
+
   // Check if the version is documented, replace it with latest if not
   if (!isVersionDocumented(redirectPath)) {
     redirectPath = replaceVersionWithLatest(redirectPath);
@@ -135,6 +145,13 @@ function endsInNull(path: string) {
   return path.endsWith('/null');
 }
 
+const WILDCARD_RENAMES: { pattern: RegExp; replacement: string }[] = [
+  { pattern: /^\/eas\/build\/(.*)$/, replacement: '/build/$1' },
+  { pattern: /^\/eas\/submit\/(.*)$/, replacement: '/submit/$1' },
+  { pattern: /^\/eas\/update\/(.*)$/, replacement: '/eas-update/$1' },
+  { pattern: /^\/eas\/insights\/(.*)$/, replacement: '/eas-insights/$1' },
+];
+
 // Simple remapping of renamed pages, similar to public/_redirects but in some cases,
 // for reasons I'm not totally clear on, those redirects do not work
 const RENAMED_PAGES: Record = {
diff --git a/docs/public/_redirects b/docs/public/_redirects
index 15de81ea092ed1..b2de63fa422686 100644
--- a/docs/public/_redirects
+++ b/docs/public/_redirects
@@ -148,7 +148,6 @@
 
 # Redirects based on Sentry reports
 /push-notifications /push-notifications/overview 301
-/eas/submit /submit/introduction 301
 /development/tools/expo-dev-client /develop/development-builds/introduction 301
 /develop/user-interface/custom-fonts /develop/user-interface/fonts 301
 /workflow/snack /more/glossary-of-terms 301
@@ -368,6 +367,16 @@
 /versions/unversioned/sdk/ui/jetpack-compose/textinput /versions/unversioned/sdk/ui/jetpack-compose/textfield 301
 /versions/v55.0.0/sdk/ui/jetpack-compose/textinput /versions/v55.0.0/sdk/ui/jetpack-compose/textfield 301
 
+# EAS Build, Submit, Update, and Insights predate the /eas/* URL convention;
+/eas/build/* /build/:splat 301
+/eas/build /build/introduction 301
+/eas/submit/* /submit/:splat 301
+/eas/submit /submit/introduction 301
+/eas/update/* /eas-update/:splat 301
+/eas/update /eas-update/introduction 301
+/eas/insights/* /eas-insights/:splat 301
+/eas/insights /eas-insights/introduction 301
+
 # Serve markdown at the sibling /.md path that AI agents typically request,
 # rewriting to the canonical //index.md the build script writes.
 # The first two rules are pass-throughs that preserve canonical URLs (root /index.md