local e local k local p local f local S local A local M=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\108\117\097\117-\112\111\108\121\102\105\108\108")local O=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\112\114\111\109\105\115\101")local X=M.Array local o=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\106\101\115\116-\103\108\111\098\097\108\115")local L=o.expect local r=o.jest local q=o.beforeEach local K=o.it local h=o.xit local a=o.describe local function F()r.resetModules()r.useFakeTimers()local M=(require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\115\104\097\114\101\100")).ReactFeatureFlags M.enableSchedulerTracing=true M.enableProfilerTimer=true e=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\114\101\097\099\116")k=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\114\101\097\099\116-\110\111\111\112-\114\101\110\100\101\114\101\114")p=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\115\099\104\101\100\117\108\101\114")f=k.act S=e.createMutableSource A=e.useMutableSource end a("\117\115\101\077\117\116\097\098\108\101\083\111\117\114\099\101",function()local o=function(e)return e.value end local J=function(e,k)if k~=nil then return e.subscribe(k)end return e.subscribe(function() end)end local function I(e,k)local p={}local f={}local S=1 local A=e local M=k local O=function(e,k)if X.indexOf(e,k)<1 then table.insert(e,k)end return function()local p=X.indexOf(e,k)if p>=1 then X.splice(e,p,1)end end end local o={__index=function(e,k)if k=="\108\105\115\116\101\110\101\114\067\111\117\110\116\065"then return#p elseif k=="\108\105\115\116\101\110\101\114\067\111\117\110\116\066"then return#f elseif k=="\118\097\108\117\101\065"then return A elseif k=="\118\097\108\117\101\066"then return M elseif k=="\118\101\114\115\105\111\110"then return S end return end,__newindex=function(e,k,O)if k=="\118\097\108\117\101\065"then S+=1 A=O X.map(p,function(e)return e()end)elseif k=="\118\097\108\117\101\066"then S+=1 M=O X.map(f,function(e)return e()end)end end}local L={subscribeA=function(e)return O(p,e)end;subscribeB=function(e)return O(f,e)end}return setmetatable(L,o)end local function E(e)local k={}local p=1 local f=e local S={__index=function(e,S)if S=="\118\097\108\117\101"then return f elseif S=="\118\101\114\115\105\111\110"then return p elseif S=="\108\105\115\116\101\110\101\114\067\111\117\110\116"then return#k end return end,__newindex=function(e,S,A)if S=="\118\097\108\117\101"then p+=1 f=A X.map(k,function(e)return e()end)end end}local A={subscribe=function(e)if X.indexOf(k,e)<1 then table.insert(k,e)end return function()local p=X.indexOf(k,e)if p>=1 then X.splice(k,p,1)end end end}return setmetatable(A,S)end local function y(k)local f,S,M,O=k.getSnapshot,k.label,k.mutableSource,k.subscribe local X=A(M,f,O)p.unstable_yieldValue(string.format("\037\115\058\037\115",S,X))return e.createElement("\100\105\118",nil,string.format("\037\115\058\037\115",S,X))end q(function()F()end)K("\115\104\111\117\108\100 \115\117\098\115\099\114\105\098\101 \116\111 \097 \115\111\117\114\099\101 \097\110\100 \115\099\104\101\100\117\108\101 \117\112\100\097\116\101\115 \119\104\101\110 \105\116 \099\104\097\110\103\101\115",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)f(function()k.renderToRootWithID(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097",getSnapshot=o;mutableSource=M,subscribe=J}),e.createElement(y,{label="\098";getSnapshot=o;mutableSource=M;subscribe=J})),"\114\111\111\116",function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYieldThrough({"\097\058\111\110\101";"\098\058\111\110\101","\083\121\110\099 \101\102\102\101\099\116"});(L(A.listenerCount)).toEqual(0)k.flushPassiveEffects();(L(A.listenerCount)).toEqual(2)A.value="\116\119\111";(L(p)).toFlushAndYieldThrough({"\097\058\116\119\111";"\098\058\116\119\111"})k.renderToRootWithID(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=o;mutableSource=M,subscribe=J})),"\114\111\111\116",function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\097\058\116\119\111";"\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects();(L(A.listenerCount)).toEqual(1)k.unmountRootWithID("\114\111\111\116");(L(p)).toFlushAndYield({})k.flushPassiveEffects();(L(A.listenerCount)).toEqual(0)A.value="\116\104\114\101\101";(L(p)).toFlushAndYield({})end)end)K("\115\104\111\117\108\100 \114\101\115\116\097\114\116 \119\111\114\107 \105\102 \097 \110\101\119 \115\111\117\114\099\101 \105\115 \109\117\116\097\116\101\100 \100\117\114\105\110\103 \114\101\110\100\101\114",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097",getSnapshot=o;mutableSource=M,subscribe=J}),e.createElement(y,{label="\098";getSnapshot=o,mutableSource=M;subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYieldThrough({"\097\058\111\110\101"})A.value="\116\119\111";(L(p)).toFlushAndYield({"\097\058\116\119\111";"\098\058\116\119\111","\083\121\110\099 \101\102\102\101\099\116"})end)end)K("\115\104\111\117\108\100 \115\099\104\101\100\117\108\101 \097\110 \117\112\100\097\116\101 \105\102 \097 \110\101\119 \115\111\117\114\099\101 \105\115 \109\117\116\097\116\101\100 \098\101\116\119\101\101\110 \114\101\110\100\101\114 \097\110\100 \099\111\109\109\105\116 \040\115\117\098\115\099\114\105\112\116\105\111\110\041",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097",getSnapshot=o,mutableSource=M,subscribe=J}),e.createElement(y,{label="\098";getSnapshot=o;mutableSource=M,subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYieldThrough({"\097\058\111\110\101","\098\058\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"});(L(A.listenerCount)).toEqual(0)A.value="\116\119\111";(L(p)).toFlushAndYield({"\097\058\116\119\111","\098\058\116\119\111"})end)end)K("\115\104\111\117\108\100 \117\110\115\117\098\115\099\114\105\098\101 \097\110\100 \114\101\115\117\098\115\099\114\105\098\101 \105\102 \097 \110\101\119 \115\111\117\114\099\101 \105\115 \117\115\101\100",function()local A=E("\097-\111\110\101")local M=S(A,function(e)return e.versionA end)local O=E("\098-\111\110\101")local X=S(O,function(e)return e.versionB end)f(function()k.render(e.createElement(y,{label="\111\110\108\121";getSnapshot=o;mutableSource=M;subscribe=J}),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\111\110\108\121\058\097-\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects();(L(A.listenerCount)).toEqual(1)A.value="\097-\116\119\111";(L(p)).toFlushAndYield({"\111\110\108\121\058\097-\116\119\111"})k.render(e.createElement(y,{label="\111\110\108\121";getSnapshot=o;mutableSource=X;subscribe=J}),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\111\110\108\121\058\098-\111\110\101","\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects();(L(A.listenerCount)).toEqual(0);(L(O.listenerCount)).toEqual(1)A.value="\097-\116\104\114\101\101";(L(p)).toFlushAndYield({})O.value="\098-\116\119\111";(L(p)).toFlushAndYield({"\111\110\108\121\058\098-\116\119\111"})end)end)K("\115\104\111\117\108\100 \117\110\115\117\098\115\099\114\105\098\101 \097\110\100 \114\101\115\117\098\115\099\114\105\098\101 \105\102 \097 \110\101\119 \115\117\098\115\099\114\105\098\101 \102\117\110\099\116\105\111\110 \105\115 \112\114\111\118\105\100\101\100",function()local A=E("\097-\111\110\101")local M=S(A,function(e)return e.version end)local O=r.fn()local X=r.fn(function(e)local k=J(e)return function()k()O()end end)local q=r.fn()local K=r.fn(function(e)local k=J(e)return function()k()q()end end)f(function()k.renderToRootWithID(e.createElement(y,{label="\111\110\108\121",getSnapshot=o,mutableSource=M;subscribe=X}),"\114\111\111\116",function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\111\110\108\121\058\097-\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects();(L(A.listenerCount)).toEqual(1);(L(X)).toHaveBeenCalledTimes(1)k.renderToRootWithID(e.createElement(y,{label="\111\110\108\121";getSnapshot=o,mutableSource=M;subscribe=K}),"\114\111\111\116",function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\111\110\108\121\058\097-\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects();(L(A.listenerCount)).toEqual(1);(L(O)).toHaveBeenCalledTimes(1);(L(K)).toHaveBeenCalledTimes(1)k.unmountRootWithID("\114\111\111\116");(L(p)).toFlushAndYield({})k.flushPassiveEffects();(L(A.listenerCount)).toEqual(0);(L(q)).toHaveBeenCalledTimes(1)end)end)K("\115\104\111\117\108\100 \114\101-\117\115\101 \112\114\101\118\105\111\117\115\108\121 \114\101\097\100 \115\110\097\112\115\104\111\116 \118\097\108\117\101 \119\104\101\110 \114\101\097\100\105\110\103 \105\115 \117\110\115\097\102\101",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=o,mutableSource=M;subscribe=J}),e.createElement(y,{label="\098",getSnapshot=o;mutableSource=M;subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\097\058\111\110\101","\098\058\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})A.value="\116\119\111";(L(p)).toFlushAndYieldThrough({"\097\058\116\119\111"})k.flushSync(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097",getSnapshot=o,mutableSource=M,subscribe=J}),e.createElement(y,{label="\098";getSnapshot=o;mutableSource=M;subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end)end);(L(p)).toHaveYielded({"\097\058\111\110\101";"\098\058\111\110\101","\083\121\110\099 \101\102\102\101\099\116"});(L(p)).toFlushAndYield({"\097\058\116\119\111";"\098\058\116\119\111"})end)end)K("\115\104\111\117\108\100 \114\101\097\100 \102\114\111\109 \115\111\117\114\099\101 \111\110 \110\101\119\108\121 \109\111\117\110\116\101\100 \115\117\098\116\114\101\101 \105\102 \110\111 \112\101\110\100\105\110\103 \117\112\100\097\116\101\115 \097\114\101 \115\099\104\101\100\117\108\101\100 \102\111\114 \115\111\117\114\099\101",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097",getSnapshot=o;mutableSource=M;subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\097\058\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=o;mutableSource=M;subscribe=J}),e.createElement(y,{label="\098",getSnapshot=o,mutableSource=M,subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\097\058\111\110\101";"\098\058\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})end)end)K("\115\104\111\117\108\100 \116\104\114\111\119 \097\110\100 \114\101\115\116\097\114\116 \114\101\110\100\101\114 \105\102 \115\111\117\114\099\101 \097\110\100 \115\110\097\112\115\104\111\116 \097\114\101 \117\110\097\118\097\105\108\097\098\108\101 \100\117\114\105\110\103 \097\110 \117\112\100\097\116\101",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=o;mutableSource=M;subscribe=J}),e.createElement(y,{label="\098";getSnapshot=o,mutableSource=M;subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\097\058\111\110\101";"\098\058\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects()p.unstable_runWithPriority(p.unstable_LowPriority,function()A.value="\116\119\111";(L(p)).toFlushAndYieldThrough({"\097\058\116\119\111"})end)local f=function(e)return"\110\101\119\058"..o(e)end p.unstable_runWithPriority(p.unstable_UserBlockingPriority,function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=f;mutableSource=M,subscribe=J}),e.createElement(y,{label="\098",getSnapshot=f,mutableSource=M;subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end)end);(L(p)).toFlushAndYieldThrough({"\097\058\110\101\119\058\116\119\111","\098\058\110\101\119\058\116\119\111";"\083\121\110\099 \101\102\102\101\099\116"})end)end)K("\115\104\111\117\108\100 \116\104\114\111\119 \097\110\100 \114\101\115\116\097\114\116 \114\101\110\100\101\114 \105\102 \115\111\117\114\099\101 \097\110\100 \115\110\097\112\115\104\111\116 \097\114\101 \117\110\097\118\097\105\108\097\098\108\101 \100\117\114\105\110\103 \097 \115\121\110\099 \117\112\100\097\116\101",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=o;mutableSource=M;subscribe=J}),e.createElement(y,{label="\098";getSnapshot=o;mutableSource=M;subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\097\058\111\110\101";"\098\058\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects()p.unstable_runWithPriority(p.unstable_LowPriority,function()A.value="\116\119\111";(L(p)).toFlushAndYieldThrough({"\097\058\116\119\111"})end)local f=function(e)return"\110\101\119\058"..o(e)end k.flushSync(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=f;mutableSource=M;subscribe=J}),e.createElement(y,{label="\098",getSnapshot=f;mutableSource=M;subscribe=J})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end)end);(L(p)).toHaveYielded({"\097\058\110\101\119\058\116\119\111","\098\058\110\101\119\058\116\119\111","\083\121\110\099 \101\102\102\101\099\116"})end)end)K("\115\104\111\117\108\100 \111\110\108\121 \117\112\100\097\116\101 \099\111\109\112\111\110\101\110\116\115 \119\104\111\115\101 \115\117\098\115\099\114\105\112\116\105\111\110\115 \102\105\114\101",function()local A=I("\097\058\111\110\101","\098\058\111\110\101")local M=S(A,function(e)return e.version end)local O=function(e)return e.valueA end local X=function(e,k)return e.subscribeA(k)end local o=function(e)return e.valueB end local r=function(e,k)return e.subscribeB(k)end f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=O;mutableSource=M,subscribe=X}),e.createElement(y,{label="\098",getSnapshot=o,mutableSource=M;subscribe=r})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\097\058\097\058\111\110\101","\098\058\098\058\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})A.valueA="\097\058\116\119\111";(L(p)).toFlushAndYield({"\097\058\097\058\116\119\111"})A.valueB="\098\058\116\119\111";(L(p)).toFlushAndYield({"\098\058\098\058\116\119\111"})end)end)K("\115\104\111\117\108\100 \100\101\116\101\099\116 \116\101\097\114\105\110\103 \105\110 \112\097\114\116 \111\102 \116\104\101 \115\116\111\114\101 \110\111\116 \121\101\116 \115\117\098\115\099\114\105\098\101\100 \116\111",function()local A=I("\097\058\111\110\101","\098\058\111\110\101")local M=S(A,function(e)return e.version end)local O=function(e)return e.valueA end local X=function(e,k)return e.subscribeA(k)end local o=function(e)return e.valueB end local r=function(e,k)return e.subscribeB(k)end f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=O;mutableSource=M;subscribe=X})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\097\058\097\058\111\110\101","\083\121\110\099 \101\102\102\101\099\116"})k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097",getSnapshot=O;mutableSource=M,subscribe=X}),e.createElement(y,{label="\098";getSnapshot=o,mutableSource=M;subscribe=r}),e.createElement(y,{label="\099";getSnapshot=o;mutableSource=M;subscribe=r})),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYieldThrough({"\097\058\097\058\111\110\101","\098\058\098\058\111\110\101"})A.valueB="\098\058\116\119\111";(L(p)).toFlushAndYield({"\097\058\097\058\111\110\101";"\098\058\098\058\116\119\111","\099\058\098\058\116\119\111";"\083\121\110\099 \101\102\102\101\099\116"})end)end)K("\100\111\101\115 \110\111\116 \115\099\104\101\100\117\108\101 \097\110 \117\112\100\097\116\101 \102\111\114 \115\117\098\115\099\114\105\112\116\105\111\110\115 \116\104\097\116 \102\105\114\101 \119\105\116\104 \097\110 \117\110\099\104\097\110\103\101\100 \115\110\097\112\115\104\111\116",function()local A=y local M=E("\111\110\101")local O=S(M,function(e)return e.version end)f(function()k.render(e.createElement(A,{label="\111\110\108\121";getSnapshot=o;mutableSource=O,subscribe=J}),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYieldThrough({"\111\110\108\121\058\111\110\101","\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects();(L(M.listenerCount)).toEqual(1)M.value="\111\110\101";(L(p)).toFlushWithoutYielding()end)end)K("\115\104\111\117\108\100 \116\104\114\111\119 \097\110\100 \114\101\115\116\097\114\116 \105\102 \103\101\116\083\110\097\112\115\104\111\116 \099\104\097\110\103\101\115 \098\101\116\119\101\101\110 \115\099\104\101\100\117\108\101\100 \117\112\100\097\116\101 \097\110\100 \114\101-\114\101\110\100\101\114",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)local O=function(e)return"\110\101\119\058"..o(e)end local X,r local function q()X,r=e.useState(function()return o end)return e.createElement(y,{label="\111\110\108\121";getSnapshot=X,mutableSource=M,subscribe=J})end f(function()k.render(e.createElement(q),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\111\110\108\121\058\111\110\101","\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects()p.unstable_runWithPriority(p.unstable_LowPriority,function()A.value="\116\119\111"end)p.unstable_runWithPriority(p.unstable_UserBlockingPriority,function()r(function()return O end)end);(L(p)).toFlushAndYield({"\111\110\108\121\058\110\101\119\058\116\119\111"})end)end)K("\115\104\111\117\108\100 \114\101\099\111\118\101\114 \102\114\111\109 \097 \109\117\116\097\116\105\111\110 \100\117\114\105\110\103 \121\105\101\108\100 \119\104\101\110 \111\116\104\101\114 \119\111\114\107 \105\115 \115\099\104\101\100\117\108\101\100",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(y,{label="\097";getSnapshot=o;mutableSource=M;subscribe=J}),e.createElement(y,{label="\098";getSnapshot=o,mutableSource=M,subscribe=J})));(L(p)).toFlushAndYieldThrough({"\097\058\111\110\101"})A.value="\116\119\111"k.render(e.createElement("\100\105\118"));(L(p)).toFlushAndYield({})end)end)K("\115\104\111\117\108\100 \110\111\116 \116\104\114\111\119 \105\102 \116\104\101 \110\101\119 \103\101\116\083\110\097\112\115\104\111\116 \114\101\116\117\114\110\115 \116\104\101 \115\097\109\101 \115\110\097\112\115\104\111\116 \118\097\108\117\101",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)local O=r.fn()local X=r.fn()local q,K local function h()q,K=e.useState(function()return o end)return e.createElement(y,{label="\098",getSnapshot=q;mutableSource=M;subscribe=J})end f(function()k.render(e.createElement(e.Fragment,nil,e.createElement(e.Profiler,{id="\097",onRender=O},e.createElement(y,{label="\097";getSnapshot=o,mutableSource=M;subscribe=J})),e.createElement(e.Profiler,{id="\098";onRender=X},e.createElement(h))),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\097\058\111\110\101";"\098\058\111\110\101","\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects();(L(O)).toHaveBeenCalledTimes(1);(L(X)).toHaveBeenCalledTimes(1)K(function()return function(e)return o(e)end end);(L(p)).toFlushAndYield({"\098\058\111\110\101"})k.flushPassiveEffects();(L(O)).toHaveBeenCalledTimes(1);(L(X)).toHaveBeenCalledTimes(2)end)end)K("\115\104\111\117\108\100 \110\111\116 \116\104\114\111\119 \105\102 \103\101\116\083\110\097\112\115\104\111\116 \099\104\097\110\103\101\115 \098\117\116 \116\104\101 \115\111\117\114\099\101 \099\097\110 \098\101 \115\097\102\101\108\121 \114\101\097\100 \102\114\111\109 \097\110\121\119\097\121",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)local O=function(e)return"\110\101\119\058"..o(e)end local X,r local function q()X,r=e.useState(function()return o end)return e.createElement(y,{label="\111\110\108\121",getSnapshot=X,mutableSource=M;subscribe=J})end f(function()k.render(e.createElement(q),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\111\110\108\121\058\111\110\101","\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects()k.batchedUpdates(function()A.value="\116\119\111"r(function()return O end)end);(L(p)).toFlushAndYield({"\111\110\108\121\058\110\101\119\058\116\119\111"})end)end)h("\115\104\111\117\108\100 \115\116\105\108\108 \115\099\104\101\100\117\108\101 \097\110 \117\112\100\097\116\101 \105\102 \097\110 \101\097\103\101\114 \115\101\108\101\099\116\111\114 \116\104\114\111\119\115 \097\102\116\101\114 \097 \109\117\116\097\116\105\111\110",function()local M=E({friends={{id=1,name="\070\111\111"};{id=2;name="\066\097\114"}}})local O=S(M,function(e)return e.version end)local function o(k)local f=k.id local S=e.useCallback(function(e)local k=e.value return(X.find(k.friends,function(e)return e.id==f end)).name end,{f})local M=A(O,S,J)p.unstable_yieldValue(string.format("\037\115\058\037\115",f,M))return e.createElement("\108\105",nil,M)end local function r()local k=e.useCallback(function(e)local k=e.value return X.from(k.friends)end,{})local p=A(O,k,J)return e.createElement("\117\108",nil,X.map(p,function(k)return e.createElement(o,{key=k.id,id=k.id})end))end f(function()k.render(e.createElement(r),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\049\058\070\111\111";"\050\058\066\097\114","\083\121\110\099 \101\102\102\101\099\116"})M.value={friends={{id=1,name="\070\111\111"};{id=3,name="\066\097\122"}}};(L(p)).toFlushAndYield({"\049\058\070\111\111","\051\058\066\097\122"})end)end)K("\115\104\111\117\108\100 \110\111\116 \119\097\114\110 \097\098\111\117\116 \117\112\100\097\116\101\115 \116\104\097\116 \102\105\114\101 \098\101\116\119\101\101\110 \117\110\109\111\117\110\116 \097\110\100 \112\097\115\115\105\118\101 \117\110\115\117\098\115\099\114\105\098\101",function()local A=E("\111\110\101")local M=S(A,function(e)return e.version end)local function O()e.useLayoutEffect(function()return function()p.unstable_yieldValue("\108\097\121\111\117\116 \117\110\109\111\117\110\116")end end)return e.createElement(y,{label="\111\110\108\121";getSnapshot=o;mutableSource=M,subscribe=J})end f(function()k.renderToRootWithID(e.createElement(O),"\114\111\111\116",function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYield({"\111\110\108\121\058\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"})k.flushPassiveEffects()k.unmountRootWithID("\114\111\111\116");(L(p)).toFlushAndYieldThrough({"\108\097\121\111\117\116 \117\110\109\111\117\110\116"})A.value="\116\119\111";(L(p)).toFlushAndYield({})end)end)K("\115\104\111\117\108\100 \115\117\112\112\111\114\116 \105\110\108\105\110\101 \115\101\108\101\099\116\111\114\115 \097\110\100 \117\112\100\097\116\101\115 \116\104\097\116 \097\114\101 \112\114\111\099\101\115\115\101\100 \097\102\116\101\114 \115\101\108\101\099\116\111\114 \099\104\097\110\103\101",function()local p=E({a="\105\110\105\116\105\097\108",b="\105\110\105\116\105\097\108"})local O=S(p,function(e)return e.version end)local X=function()return p.value.a end local o=function()return p.value.b end local function r(e)local k=M.Object.entries(p.value)k.b=e p.value=k end local function q(e)local k=e.getSnapshot local p=A(O,k,J)return p end local K=k.createRoot()f(function()K.render(e.createElement(q,{getSnapshot=X}))end);(L(K)).toMatchRenderedOutput("\105\110\105\116\105\097\108")f(function()r("\085\112\100\097\116\101\100 \066")K.render(e.createElement(q,{getSnapshot=o}))end);(L(K)).toMatchRenderedOutput("\085\112\100\097\116\101\100 \066")f(function()r("\065\110\111\116\104\101\114 \117\112\100\097\116\101")end);(L(K)).toMatchRenderedOutput("\065\110\111\116\104\101\114 \117\112\100\097\116\101")end)K("\115\104\111\117\108\100 \099\108\101\097\114 \116\104\101 \117\112\100\097\116\101 \113\117\101\117\101 \119\104\101\110 \103\101\116\083\110\097\112\115\104\111\116 \099\104\097\110\103\101\115 \119\105\116\104 \112\101\110\100\105\110\103 \108\111\119\101\114 \112\114\105\111\114\105\116\121 \117\112\100\097\116\101\115",function()local p=E({a="\105\110\105\116\105\097\108";b="\105\110\105\116\105\097\108"})local O=S(p,function(e)return e.version end)local X=function()return p.value.a end local o=function()return p.value.b end local function r(e)local k=M.Object.assign({},p.value)k.a=e p.value=k end local function q(e)local k=M.Object.assign({},p.value)k.b=e p.value=k end local function K(e)local k=e.toggle local p=A(O,(function()if k then return o end return X end)(),J)local f=(function()if k then return"\066\058 "end return"\065\058 "end)()..p return f end local h=k.createRoot()f(function()h.render(e.createElement(K,{toggle=false}))end);(L(h)).toMatchRenderedOutput("\065\058 \105\110\105\116\105\097\108")f(function()k.discreteUpdates(function()r("\085\112\100\097\116\101")q("\085\112\100\097\116\101")h.render(e.createElement(K,{toggle=true}))end)r("\079\079\080\083\033 \084\104\105\115 \109\117\116\097\116\105\111\110 \115\104\111\117\108\100 \098\101 \105\103\110\111\114\101\100")end);(L(h)).toMatchRenderedOutput("\066\058 \085\112\100\097\116\101")end)K("\115\104\111\117\108\100 \099\108\101\097\114 \116\104\101 \117\112\100\097\116\101 \113\117\101\117\101 \119\104\101\110 \115\111\117\114\099\101 \099\104\097\110\103\101\115 \119\105\116\104 \112\101\110\100\105\110\103 \108\111\119\101\114 \112\114\105\111\114\105\116\121 \117\112\100\097\116\101\115",function()local p=E("\105\110\105\116\105\097\108")local M=E("\105\110\105\116\105\097\108")local O=S(p,function(e)return e.versionA end)local X=S(M,function(e)return e.versionB end)local function r(e)local k=e.toggle local p=A((function()if k then return X end return O end)(),o,J)local f=(function()if k then return"\066\058 "end return"\065\058 "end)()..p return f end local q=k.createRoot()f(function()q.render(e.createElement(r,{toggle=false}))end);(L(q)).toMatchRenderedOutput("\065\058 \105\110\105\116\105\097\108")f(function()k.discreteUpdates(function()p.value="\085\112\100\097\116\101"M.value="\085\112\100\097\116\101"q.render(e.createElement(r,{toggle=true}))end)p.value="\079\079\080\083\033 \084\104\105\115 \109\117\116\097\116\105\111\110 \115\104\111\117\108\100 \098\101 \105\103\110\111\114\101\100"end);(L(q)).toMatchRenderedOutput("\066\058 \085\112\100\097\116\101")end)K("\115\104\111\117\108\100 \097\108\119\097\121\115 \116\114\101\097\116 \114\101\097\100\105\110\103 \097\115 \112\111\116\101\110\116\105\097\108\108\121 \117\110\115\097\102\101 \119\104\101\110 \103\101\116\083\110\097\112\115\104\111\116 \099\104\097\110\103\101\115 \098\101\116\119\101\101\110 \114\101\110\100\101\114\115",function()local O=E({a="\102\111\111",b="\098\097\114"})local X=S(O,function(e)return e.version end)local o=function()return O.value.a end local r=function()return O.value.b end local function q(e)local k=M.Object.assign({},O.value)k.a=e O.value=k end local function K(k)local f,S=k.getSnapshotFirst,k.getSnapshotSecond local M=A(X,f,J)local O=A(X,S,J)local o=string.format("\120\058 \037\115\044 \121\058 \037\115",M,O)if f==S then if M~=O then o="\079\111\112\115\044 \116\101\097\114\105\110\103\033"end end e.useEffect(function()p.unstable_yieldValue(o)end,{o})return o end local h=k.createRoot()f(function()h.render(e.createElement(K,{getSnapshotFirst=o,getSnapshotSecond=r}))end);(L(p)).toHaveYielded({"\120\058 \102\111\111\044 \121\058 \098\097\114"})f(function()k.discreteUpdates(function()q("\098\097\122")h.render(e.createElement(K,{getSnapshotFirst=o,getSnapshotSecond=o}))end)q("\098\097\114")end);(L(p)).toHaveYielded({"\120\058 \098\097\114\044 \121\058 \098\097\114"})end)h("\103\101\116\083\110\097\112\115\104\111\116 \099\104\097\110\103\101\115 \097\110\100 \116\104\101\110 \115\111\117\114\099\101 \105\115 \109\117\116\097\116\101\100 \105\110 \098\101\116\119\101\101\110 \112\097\105\110\116 \097\110\100 \112\097\115\115\105\118\101 \101\102\102\101\099\116 \112\104\097\115\101",function()local O=E({a="\102\111\111";b="\098\097\114"})local X=S(O,function(e)return e.version end)local function o(e)local k=M.Object.entries(O.value)k.b=e O.value=k end local r=function()return O.value.a end local q=function()return O.value.b end local function K(k)local f=k.getSnapshot local S=A(X,f,J)p.unstable_yieldValue("\082\101\110\100\101\114\058 "..S)e.useEffect(function()p.unstable_yieldValue("\067\111\109\109\105\116\058 "..S)end,{S})return S end local h=k.createRoot()f(function()h.render(e.createElement(K,{getSnapshot=r}))end);(L(p)).toHaveYielded({"\082\101\110\100\101\114\058 \102\111\111";"\067\111\109\109\105\116\058 \102\111\111"})f(function()h.render(e.createElement(K,{getSnapshot=q}));(L(p)).toFlushUntilNextPaint({"\082\101\110\100\101\114\058 \098\097\114"})o("\098\097\122")end);(L(p)).toHaveYielded({"\082\101\110\100\101\114\058 \098\097\114","\067\111\109\109\105\116\058 \098\097\114","\082\101\110\100\101\114\058 \098\097\122";"\067\111\109\109\105\116\058 \098\097\122"});(L(h)).toMatchRenderedOutput("\098\097\122")end)h("\103\101\116\083\110\097\112\115\104\111\116 \099\104\097\110\103\101\115 \097\110\100 \116\104\101\110 \115\111\117\114\099\101 \105\115 \109\117\116\097\116\101\100 \105\110 \098\101\116\119\101\101\110 \112\097\105\110\116 \097\110\100 \112\097\115\115\105\118\101 \101\102\102\101\099\116 \112\104\097\115\101\044 \099\097\115\101 \050",function()local O=E({a="\097\048",b="\098\048"})local X=S(O,function(e)return e.version end)local o=function()return O.value.a end local r=function()return O.value.b end local function q(e)local k=M.Object.entries(O.value)k.A=e O.value=k end local function K(e)local k,p=e.getSnapshotFirst,e.getSnapshotSecond local f=A(X,k,J)local S=A(X,p,J)return string.format("\102\105\114\115\116\058 \037\115\044 \115\101\099\111\110\100\058 \037\115",f,S)end local h=k.createRoot()f(function()h.render(e.createElement(K,{getSnapshotFirst=o;getSnapshotSecond=r}))end);(L(h.getChildrenAsJSX())).toEqual("\102\105\114\115\116\058 \097\048\044 \115\101\099\111\110\100\058 \098\048")f(function()h.render(e.createElement(K,{getSnapshotFirst=o,getSnapshotSecond=r}));(L(p)).toFlushUntilNextPaint({})k.discreteUpdates(function()q("\097\049")end);(L(p)).toFlushUntilNextPaint({});(L(h.getChildrenAsJSX())).toEqual("\102\105\114\115\116\058 \097\049\044 \115\101\099\111\110\100\058 \097\049")end);(L(h.getChildrenAsJSX())).toEqual("\102\105\114\115\116\058 \097\049\044 \115\101\099\111\110\100\058 \097\049")end)h("\105\102 \115\111\117\114\099\101 \105\115 \109\117\116\097\116\101\100 \097\102\116\101\114 \105\110\105\116\105\097\108 \114\101\097\100 \098\117\116 \098\101\102\111\114\101 \115\117\098\115\099\114\105\112\116\105\111\110 \105\115 \115\101\116 \117\112\044 \115\104\111\117\108\100 \115\116\105\108\108 \101\110\116\097\110\103\108\101 \097\108\108 \112\101\110\100\105\110\103 \109\117\116\097\116\105\111\110\115 \101\118\101\110 \105\102 \115\110\097\112\115\104\111\116 \111\102 \110\101\119 \115\117\098\115\099\114\105\112\116\105\111\110 \104\097\112\112\101\110\115 \116\111 \109\097\116\099\104",function()local O=E({a="\097\048",b="\098\048"})local X=S(O,function(e)return e.version end)local o=function()return O.value.a end local r=function()return O.value.b end local function q(e)local k=M.Object.assign({},O.value)k.a=e O.value=k end local function K(e)local k=M.Object.assign({},O.value)k.b=e O.value=k end local function h(e)local k=e.getSnapshot local f=A(X,k,J)p.unstable_yieldValue(f)return f end local function a(e)local k=e.text p.unstable_yieldValue(k)return k end local F=k.createRoot()f(function()F.render(e.createElement(h,{getSnapshot=o}))end);(L(p)).toHaveYielded({"\097\048"});(L(F)).toMatchRenderedOutput("\097\048")f(function()F.render(e.createElement(e.Fragment,nil,e.createElement(h,{getSnapshotFirst=o}),e.createElement(h,{getSnapshotFirst=r}),e.createElement(a,{text="\099"})));(L(p)).toFlushAndYieldThrough({"\097\048","\098\048"})q("\097\049")K("\098\049")p.unstable_runWithPriority(p.unstable_IdlePriority,function()q("\097\048")K("\098\048")end);(L(p)).toFlushUntilNextPaint({"\099"});(L(p)).toFlushUntilNextPaint({"\097\048"});(L(F)).toMatchRenderedOutput("\097\048\098\048\099");(L(p)).toFlushAndYield({});(L(F)).toMatchRenderedOutput("\097\048\098\048\099")end)end)K("\119\097\114\110\115 \097\098\111\117\116 \102\117\110\099\116\105\111\110\115 \098\101\105\110\103 \117\115\101\100 \097\115 \115\110\097\112\115\104\111\116 \118\097\108\117\101\115",function()local M=E(function()return"\097"end)local O=S(M,function(e)return e.version end)local X=function()return M.value end local function o()local e=A(O,X,J)local k=e()p.unstable_yieldValue(k)return k end local r=k.createRoot()f(function()r.render(e.createElement(e.Fragment,nil,e.createElement(o)));(L(function()(L(p)).toFlushAndYield({"\097"})end)).toErrorDev("\077\117\116\097\098\108\101 \115\111\117\114\099\101 \115\104\111\117\108\100 \110\111\116 \114\101\116\117\114\110 \097 \102\117\110\099\116\105\111\110 \097\115 \116\104\101 \115\110\097\112\115\104\111\116 \118\097\108\117\101\046")end);(L(r)).toMatchRenderedOutput("\097")end)K("\103\101\116\083\110\097\112\115\104\111\116 \099\104\097\110\103\101\115 \097\110\100 \116\104\101\110 \115\111\117\114\099\101 \105\115 \109\117\116\097\116\101\100 \100\117\114\105\110\103 \105\110\116\101\114\108\101\097\118\101\100 \101\118\101\110\116",function()local M=e.useEffect local X=I("\049","\050")local o=S(X,function(e)return e.version end)local r=function(e)return e.valueA end local q=function(e,k)return e.subscribeA(k)end local K={r,q}local h=function(e)return e.valueB end local a=function(e,k)return e.subscribeB(k)end local F={h,a}local function J(e)local k,f,S=e.parentConfig,e.childConfig,e.parentValue local O,X=f[1],f[2]local L=A(o,O,X)p.unstable_yieldValue("\067\104\105\108\100\058 "..L)local r=string.format("\037\115\044 \037\115",S,L)if k==f then if S~=L then r="\079\111\112\115\044 \116\101\097\114\105\110\103\033"end end M(function()p.unstable_yieldValue("\067\111\109\109\105\116\058 "..r)end,{r})return r end local function E(k)local f,S=k.parentConfig,k.childConfig local M,O=f[1],f[2]local X=A(o,M,O)p.unstable_yieldValue("\080\097\114\101\110\116\058 "..X)return e.createElement(J,{parentConfig=f,childConfig=S,parentValue=X})end local y=k.createRoot();(O.try(function()f(function()y.render(e.createElement(E,{parentConfig=K,childConfig=F}))end)end)):await();(L(p)).toHaveYielded({"\080\097\114\101\110\116\058 \049","\067\104\105\108\100\058 \050","\067\111\109\109\105\116\058 \049\044 \050"})f(function()y.render(e.createElement(E,{parentConfig=F;childConfig=F}));(L(p)).toFlushAndYieldThrough({"\080\097\114\101\110\116\058 \050"})p.unstable_runWithPriority(p.unstable_IdlePriority,function()X.valueB="\051"end);(L(p)).toFlushAndYieldThrough({"\067\104\105\108\100\058 \050";"\067\111\109\109\105\116\058 \050\044 \050"});(L(p)).toFlushAndYieldThrough({"\080\097\114\101\110\116\058 \051"});(L(p)).toFlushAndYield({"\067\104\105\108\100\058 \051","\067\111\109\109\105\116\058 \051\044 \051"})end)end)K("\115\104\111\117\108\100 \110\111\116 \116\101\097\114 \119\105\116\104 \110\101\119\108\121 \109\111\117\110\116\101\100 \099\111\109\112\111\110\101\110\116 \119\104\101\110 \117\112\100\097\116\101\115 \119\101\114\101 \115\099\104\101\100\117\108\101\100 \097\116 \097 \108\111\119\101\114 \112\114\105\111\114\105\116\121",function()local M=E("\111\110\101")local O=S(M,function(e)return e.version end)local X=nil local r=nil local q=function()if r~=nil then(L(X)).toEqual(r)end end local function K()local k=A(O,o,J)p.unstable_yieldValue(string.format("\097\058\037\115",k))e.useEffect(function()X=k end,{k})return e.createElement("\100\105\118",nil,string.format("\097\058\037\115",k))end local function h()local k=A(O,o,J)p.unstable_yieldValue(string.format("\098\058\037\115",k))e.useEffect(function()r=k end,{k})return e.createElement("\100\105\118",nil,string.format("\098\058\037\115",k))end f(function()k.render(e.createElement(e.Profiler,{id="\114\111\111\116",onRender=q},e.createElement(K)),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end)end);(L(p)).toHaveYielded({"\097\058\111\110\101";"\083\121\110\099 \101\102\102\101\099\116"});(L(M.listenerCount)).toEqual(1)f(function()k.render(e.createElement(e.Profiler,{id="\114\111\111\116",onRender=q},e.createElement(K),e.createElement(h)),function()return p.unstable_yieldValue("\083\121\110\099 \101\102\102\101\099\116")end);(L(p)).toFlushAndYieldThrough({"\097\058\111\110\101";"\098\058\111\110\101","\083\121\110\099 \101\102\102\101\099\116"});(L(M.listenerCount)).toEqual(1)p.unstable_runWithPriority(p.unstable_IdlePriority,function()M.value="\116\119\111"end);(L(p)).toFlushAndYield({"\097\058\116\119\111","\098\058\116\119\111"});(L(M.listenerCount)).toEqual(2)end)end)if _G.__DEV__ then a("\100\101\118 \119\097\114\110\105\110\103\115",function()K("\115\104\111\117\108\100 \119\097\114\110 \105\102 \116\104\101 \115\117\098\115\099\114\105\098\101 \102\117\110\099\116\105\111\110 \100\111\101\115 \110\111\116 \114\101\116\117\114\110 \097\110 \117\110\115\117\098\115\099\114\105\098\101 \102\117\110\099\116\105\111\110",function()local p=E("\111\110\101")local A=S(p,function(e)return e.version end)local M=function() end;(L(function()f(function()k.render(e.createElement(y,{label="\111\110\108\121",getSnapshot=o,mutableSource=A;subscribe=M}))end)end)).toErrorDev("\077\117\116\097\098\108\101 \115\111\117\114\099\101 \115\117\098\115\099\114\105\098\101 \102\117\110\099\116\105\111\110 \109\117\115\116 \114\101\116\117\114\110 \097\110 \117\110\115\117\098\115\099\114\105\098\101 \102\117\110\099\116\105\111\110\046")end)end)end end)
-- Upstream: https://github.com/facebook/react/blob/d13f5b9538e48f74f7c571ef3cde652ca887cca0/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
--  * Copyright (c) Facebook, Inc. and its affiliates.
--  *
--  * This source code is licensed under the MIT license found in the
--  * LICENSE file in the root directory of this source tree.
--  *
--  * @emails react-core
--  * @jest-environment node
--  */

local React
local ReactNoop
local Scheduler
local act
local createMutableSource
local useMutableSource

local LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill")
local Promise = require("@pkg/@jsdotlua/promise")
local Array = LuauPolyfill.Array

local JestGlobals = require("@pkg/@jsdotlua/jest-globals")
local jestExpect = JestGlobals.expect
local jest = JestGlobals.jest
local beforeEach = JestGlobals.beforeEach
local it = JestGlobals.it
local xit = JestGlobals.xit
local describe = JestGlobals.describe

local function loadModules()
	jest.resetModules()
	jest.useFakeTimers()
	local ReactFeatureFlags = require("@pkg/@jsdotlua/shared").ReactFeatureFlags
	ReactFeatureFlags.enableSchedulerTracing = true
	ReactFeatureFlags.enableProfilerTimer = true
	React = require("@pkg/@jsdotlua/react")
	ReactNoop = require("@pkg/@jsdotlua/react-noop-renderer")
	Scheduler = require("@pkg/@jsdotlua/scheduler")
	act = ReactNoop.act
	createMutableSource = React.createMutableSource
	useMutableSource = React.useMutableSource
end

describe("useMutableSource", function()
	local defaultGetSnapshot = function(source)
		return source.value
	end
	local defaultSubscribe = function(source, callback)
		if callback ~= nil then
			return source.subscribe(callback)
		end
		-- ROBLOX deviation: Lua can't insert nils into the callbacks array, so insert a no-op instread
		return source.subscribe(function() end)
	end

	local function createComplexSource(initialValueA, initialValueB)
		local callbacksA = {}
		local callbacksB = {}
		local revision = 1
		local valueA = initialValueA
		local valueB = initialValueB
		local subscribeHelper = function(callbacks, callback)
			if Array.indexOf(callbacks, callback) < 1 then
				table.insert(callbacks, callback)
			end

			return function()
				local index = Array.indexOf(callbacks, callback)

				if index >= 1 then
					Array.splice(callbacks, index, 1)
				end
			end
		end

		local metatable = {
			__index = function(table, key)
				if key == "listenerCountA" then
					return #callbacksA
				elseif key == "listenerCountB" then
					return #callbacksB
				elseif key == "valueA" then
					return valueA
				elseif key == "valueB" then
					return valueB
				elseif key == "version" then
					return revision
				end
				return
			end,
			__newindex = function(table, key, newValue)
				if key == "valueA" then
					revision += 1
					valueA = newValue

					Array.map(callbacksA, function(callback)
						return callback()
					end)
				elseif key == "valueB" then
					revision += 1
					valueB = newValue

					Array.map(callbacksB, function(callback)
						return callback()
					end)
				end
			end,
		}

		local tableToReturn = {
			subscribeA = function(callback)
				return subscribeHelper(callbacksA, callback)
			end,
			subscribeB = function(callback)
				return subscribeHelper(callbacksB, callback)
			end,
		}
		return setmetatable(tableToReturn, metatable)
	end
	local function createSource(initialValue)
		local callbacks = {}
		local revision = 1
		local value = initialValue

		local metatable = {
			__index = function(table, key)
				if key == "value" then
					return value
				elseif key == "version" then
					return revision
				elseif key == "listenerCount" then
					return #callbacks
				end
				return
			end,
			__newindex = function(table, key, newValue)
				if key == "value" then
					revision += 1
					value = newValue

					-- deviation: forEach() translated using Array.map
					Array.map(callbacks, function(callback)
						return callback()
					end)
				end
			end,
		}
		local tableToReturn = {
			subscribe = function(callback)
				if Array.indexOf(callbacks, callback) < 1 then
					table.insert(callbacks, callback)
				end

				return function()
					local index = Array.indexOf(callbacks, callback)

					if index >= 1 then
						Array.splice(callbacks, index, 1)
					end
				end
			end,
		}
		return setmetatable(tableToReturn, metatable)
	end
	local function Component(props)
		local getSnapshot, label, mutableSource, subscribe =
			props.getSnapshot, props.label, props.mutableSource, props.subscribe
		local snapshot = useMutableSource(mutableSource, getSnapshot, subscribe)
		Scheduler.unstable_yieldValue(string.format("%s:%s", label, snapshot))

		return React.createElement("div", nil, string.format("%s:%s", label, snapshot))
	end

	beforeEach(function()
		loadModules()
	end)
	-- @gate experimental
	it("should subscribe to a source and schedule updates when it changes", function()
		local source = createSource("one")
		local mutableSource = createMutableSource(source, function(param)
			return param.version
		end)

		act(function()
			ReactNoop.renderToRootWithID(
				React.createElement(
					React.Fragment,
					nil,
					React.createElement(Component, {
						label = "a",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = defaultSubscribe,
					}),
					React.createElement(Component, {
						label = "b",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = defaultSubscribe,
					})
				),
				"root",
				function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end
			)
			jestExpect(Scheduler).toFlushAndYieldThrough({
				"a:one",
				"b:one",
				"Sync effect",
			})
			-- Subscriptions should be passive
			jestExpect(source.listenerCount).toEqual(0)
			ReactNoop.flushPassiveEffects()
			jestExpect(source.listenerCount).toEqual(2)

			-- Changing values should schedule an update with React
			source.value = "two"

			jestExpect(Scheduler).toFlushAndYieldThrough({
				"a:two",
				"b:two",
			})

			-- Umounting a component should remove its subscription.
			ReactNoop.renderToRootWithID(
				React.createElement(
					React.Fragment,
					nil,
					React.createElement(Component, {
						label = "a",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = defaultSubscribe,
					})
				),
				"root",
				function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end
			)
			jestExpect(Scheduler).toFlushAndYield({
				"a:two",
				"Sync effect",
			})
			ReactNoop.flushPassiveEffects()
			jestExpect(source.listenerCount).toEqual(1)

			-- Umounting a root should remove the remaining event listeners
			ReactNoop.unmountRootWithID("root")
			jestExpect(Scheduler).toFlushAndYield({})
			ReactNoop.flushPassiveEffects()
			jestExpect(source.listenerCount).toEqual(0)

			-- Changes to source should not trigger an updates or warnings.
			source.value = "three"

			jestExpect(Scheduler).toFlushAndYield({})
		end)
	end)
	-- @gate experimental
	it("should restart work if a new source is mutated during render", function()
		local source = createSource("one")
		local mutableSource = createMutableSource(source, function(param)
			return param.version
		end)

		act(function()
			ReactNoop.render(
				React.createElement(
					React.Fragment,
					nil,
					React.createElement(Component, {
						label = "a",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = defaultSubscribe,
					}),
					React.createElement(Component, {
						label = "b",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = defaultSubscribe,
					})
				),
				function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end
			)

			-- Do enough work to read from one component
			jestExpect(Scheduler).toFlushAndYieldThrough({
				"a:one",
			})
			-- Mutate source before continuing work
			source.value = "two"

			-- Render work should restart and the updated value should be used
			jestExpect(Scheduler).toFlushAndYield({
				"a:two",
				"b:two",
				"Sync effect",
			})
		end)
	end)
	-- @gate experimental
	it(
		"should schedule an update if a new source is mutated between render and commit (subscription)",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			act(function()
				ReactNoop.render(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(Component, {
							label = "a",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						}),
						React.createElement(Component, {
							label = "b",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						})
					),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)

				-- Finish rendering
				jestExpect(Scheduler).toFlushAndYieldThrough({
					"a:one",
					"b:one",
					"Sync effect",
				})

				-- Mutate source before subscriptions are attached
				jestExpect(source.listenerCount).toEqual(0)

				source.value = "two"

				-- Mutation should be detected, and a new render should be scheduled
				jestExpect(Scheduler).toFlushAndYield({
					"a:two",
					"b:two",
				})
			end)
		end
	)
	-- @gate experimental
	it("should unsubscribe and resubscribe if a new source is used", function()
		local sourceA = createSource("a-one")
		local mutableSourceA = createMutableSource(sourceA, function(param)
			return param.versionA
		end)
		local sourceB = createSource("b-one")
		local mutableSourceB = createMutableSource(sourceB, function(param)
			return param.versionB
		end)

		act(function()
			ReactNoop.render(
				React.createElement(Component, {
					label = "only",
					getSnapshot = defaultGetSnapshot,
					mutableSource = mutableSourceA,
					subscribe = defaultSubscribe,
				}),
				function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end
			)
			jestExpect(Scheduler).toFlushAndYield({
				"only:a-one",
				"Sync effect",
			})
			ReactNoop.flushPassiveEffects()
			jestExpect(sourceA.listenerCount).toEqual(1)

			-- Changing values should schedule an update with React
			sourceA.value = "a-two"

			jestExpect(Scheduler).toFlushAndYield({
				"only:a-two",
			})

			-- If we re-render with a new source, the old one should be unsubscribed.
			ReactNoop.render(
				React.createElement(Component, {
					label = "only",
					getSnapshot = defaultGetSnapshot,
					mutableSource = mutableSourceB,
					subscribe = defaultSubscribe,
				}),
				function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end
			)
			jestExpect(Scheduler).toFlushAndYield({
				"only:b-one",
				"Sync effect",
			})
			ReactNoop.flushPassiveEffects()
			jestExpect(sourceA.listenerCount).toEqual(0)
			jestExpect(sourceB.listenerCount).toEqual(1)

			-- Changing to original source should not schedule updates with React
			sourceA.value = "a-three"

			jestExpect(Scheduler).toFlushAndYield({})

			-- Changing new source value should schedule an update with React
			sourceB.value = "b-two"

			jestExpect(Scheduler).toFlushAndYield({
				"only:b-two",
			})
		end)
	end)

	it(
		"should unsubscribe and resubscribe if a new subscribe function is provided",
		function()
			local source = createSource("a-one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			local unsubscribeA = jest.fn()
			local subscribeA = jest.fn(function(s)
				local unsubscribe = defaultSubscribe(s)

				return function()
					unsubscribe()
					unsubscribeA()
				end
			end)

			local unsubscribeB = jest.fn()
			local subscribeB = jest.fn(function(s)
				local unsubscribe = defaultSubscribe(s)

				return function()
					unsubscribe()
					unsubscribeB()
				end
			end)

			act(function()
				ReactNoop.renderToRootWithID(
					React.createElement(Component, {
						label = "only",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = subscribeA,
					}),
					"root",
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYield({
					"only:a-one",
					"Sync effect",
				})
				ReactNoop.flushPassiveEffects()
				jestExpect(source.listenerCount).toEqual(1)
				jestExpect(subscribeA).toHaveBeenCalledTimes(1)
				ReactNoop.renderToRootWithID(
					React.createElement(Component, {
						label = "only",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = subscribeB,
					}),
					"root",
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYield({
					"only:a-one",
					"Sync effect",
				})
				ReactNoop.flushPassiveEffects()
				jestExpect(source.listenerCount).toEqual(1)
				jestExpect(unsubscribeA).toHaveBeenCalledTimes(1)
				jestExpect(subscribeB).toHaveBeenCalledTimes(1)
				ReactNoop.unmountRootWithID("root")
				jestExpect(Scheduler).toFlushAndYield({})
				ReactNoop.flushPassiveEffects()
				jestExpect(source.listenerCount).toEqual(0)
				jestExpect(unsubscribeB).toHaveBeenCalledTimes(1)
			end)
		end
	)
	it("should re-use previously read snapshot value when reading is unsafe", function()
		local source = createSource("one")
		local mutableSource = createMutableSource(source, function(param)
			return param.version
		end)

		act(function()
			ReactNoop.render(
				React.createElement(
					React.Fragment,
					nil,
					React.createElement(Component, {
						label = "a",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = defaultSubscribe,
					}),
					React.createElement(Component, {
						label = "b",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = defaultSubscribe,
					})
				),
				function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end
			)
			jestExpect(Scheduler).toFlushAndYield({
				"a:one",
				"b:one",
				"Sync effect",
			})

			-- Changing values should schedule an update with React.
			-- Start working on this update but don't finish it.
			source.value = "two"

			jestExpect(Scheduler).toFlushAndYieldThrough({
				"a:two",
			})

			-- Re-renders that occur before the update is processed
			-- should reuse snapshot so long as the config has not changed
			ReactNoop.flushSync(function()
				ReactNoop.render(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(Component, {
							label = "a",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						}),
						React.createElement(Component, {
							label = "b",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						})
					),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
			end)
			jestExpect(Scheduler).toHaveYielded({
				"a:one",
				"b:one",
				"Sync effect",
			})
			jestExpect(Scheduler).toFlushAndYield({
				"a:two",
				"b:two",
			})
		end)
	end)
	-- @gate experimental
	it(
		"should read from source on newly mounted subtree if no pending updates are scheduled for source",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			act(function()
				ReactNoop.render(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(Component, {
							label = "a",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						})
					),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYield({
					"a:one",
					"Sync effect",
				})
				ReactNoop.render(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(Component, {
							label = "a",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						}),
						React.createElement(Component, {
							label = "b",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						})
					),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYield({
					"a:one",
					"b:one",
					"Sync effect",
				})
			end)
		end
	)
	-- @gate experimental
	it(
		"should throw and restart render if source and snapshot are unavailable during an update",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			act(function()
				ReactNoop.render(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(Component, {
							label = "a",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						}),
						React.createElement(Component, {
							label = "b",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						})
					),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYield({
					"a:one",
					"b:one",
					"Sync effect",
				})
				ReactNoop.flushPassiveEffects()
				-- Changing values should schedule an update with React.
				-- Start working on this update but don't finish it.
				Scheduler.unstable_runWithPriority(
					Scheduler.unstable_LowPriority,
					function()
						source.value = "two"

						jestExpect(Scheduler).toFlushAndYieldThrough({
							"a:two",
						})
					end
				)

				local newGetSnapshot = function(s)
					return "new:" .. defaultGetSnapshot(s)
				end

				-- Force a higher priority render with a new config.
				-- This should signal that the snapshot is not safe and trigger a full re-render.
				Scheduler.unstable_runWithPriority(
					Scheduler.unstable_UserBlockingPriority,
					function()
						ReactNoop.render(
							React.createElement(
								React.Fragment,
								nil,
								React.createElement(Component, {
									label = "a",
									getSnapshot = newGetSnapshot,
									mutableSource = mutableSource,
									subscribe = defaultSubscribe,
								}),
								React.createElement(Component, {
									label = "b",
									getSnapshot = newGetSnapshot,
									mutableSource = mutableSource,
									subscribe = defaultSubscribe,
								})
							),
							function()
								return Scheduler.unstable_yieldValue("Sync effect")
							end
						)
					end
				)
				jestExpect(Scheduler).toFlushAndYieldThrough({
					"a:new:two",
					"b:new:two",
					"Sync effect",
				})
			end)
		end
	)
	-- @gate experimental
	it(
		"should throw and restart render if source and snapshot are unavailable during a sync update",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			act(function()
				ReactNoop.render(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(Component, {
							label = "a",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						}),
						React.createElement(Component, {
							label = "b",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						})
					),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYield({
					"a:one",
					"b:one",
					"Sync effect",
				})
				ReactNoop.flushPassiveEffects()
				-- Changing values should schedule an update with React.
				-- Start working on this update but don't finish it.
				Scheduler.unstable_runWithPriority(
					Scheduler.unstable_LowPriority,
					function()
						source.value = "two"

						jestExpect(Scheduler).toFlushAndYieldThrough({
							"a:two",
						})
					end
				)

				local newGetSnapshot = function(s)
					return "new:" .. defaultGetSnapshot(s)
				end
				-- Force a higher priority render with a new config.
				-- This should signal that the snapshot is not safe and trigger a full re-render.
				ReactNoop.flushSync(function()
					ReactNoop.render(
						React.createElement(
							React.Fragment,
							nil,
							React.createElement(Component, {
								label = "a",
								getSnapshot = newGetSnapshot,
								mutableSource = mutableSource,
								subscribe = defaultSubscribe,
							}),
							React.createElement(Component, {
								label = "b",
								getSnapshot = newGetSnapshot,
								mutableSource = mutableSource,
								subscribe = defaultSubscribe,
							})
						),
						function()
							return Scheduler.unstable_yieldValue("Sync effect")
						end
					)
				end)
				jestExpect(Scheduler).toHaveYielded({
					"a:new:two",
					"b:new:two",
					"Sync effect",
				})
			end)
		end
	)
	-- @gate experimental
	it("should only update components whose subscriptions fire", function()
		local source = createComplexSource("a:one", "b:one")
		local mutableSource = createMutableSource(source, function(param)
			return param.version
		end)
		-- Subscribe to part of the store.
		local getSnapshotA = function(s)
			return s.valueA
		end
		local subscribeA = function(s, callback)
			return s.subscribeA(callback)
		end
		local getSnapshotB = function(s)
			return s.valueB
		end
		local subscribeB = function(s, callback)
			return s.subscribeB(callback)
		end

		act(function()
			ReactNoop.render(
				React.createElement(
					React.Fragment,
					nil,
					React.createElement(Component, {
						label = "a",
						getSnapshot = getSnapshotA,
						mutableSource = mutableSource,
						subscribe = subscribeA,
					}),
					React.createElement(Component, {
						label = "b",
						getSnapshot = getSnapshotB,
						mutableSource = mutableSource,
						subscribe = subscribeB,
					})
				),
				function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end
			)
			jestExpect(Scheduler).toFlushAndYield({
				"a:a:one",
				"b:b:one",
				"Sync effect",
			})

			-- Changes to part of the store (e.g. A) should not render other parts.
			source.valueA = "a:two"

			jestExpect(Scheduler).toFlushAndYield({
				"a:a:two",
			})

			source.valueB = "b:two"

			jestExpect(Scheduler).toFlushAndYield({
				"b:b:two",
			})
		end)
	end)

	-- @gate experimental
	it("should detect tearing in part of the store not yet subscribed to", function()
		local source = createComplexSource("a:one", "b:one")
		local mutableSource = createMutableSource(source, function(param)
			return param.version
		end)

		-- Subscribe to part of the store.
		local getSnapshotA = function(s)
			return s.valueA
		end
		local subscribeA = function(s, callback)
			return s.subscribeA(callback)
		end
		local getSnapshotB = function(s)
			return s.valueB
		end
		local subscribeB = function(s, callback)
			return s.subscribeB(callback)
		end

		act(function()
			-- Because the store has not changed yet, there are no pending updates,
			-- so it is considered safe to read from when we start this render.
			ReactNoop.render(
				React.createElement(
					React.Fragment,
					nil,
					React.createElement(Component, {
						label = "a",
						getSnapshot = getSnapshotA,
						mutableSource = mutableSource,
						subscribe = subscribeA,
					})
				),
				function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end
			)
			jestExpect(Scheduler).toFlushAndYield({
				"a:a:one",
				"Sync effect",
			})
			ReactNoop.render(
				React.createElement(
					React.Fragment,
					nil,
					React.createElement(Component, {
						label = "a",
						getSnapshot = getSnapshotA,
						mutableSource = mutableSource,
						subscribe = subscribeA,
					}),
					React.createElement(Component, {
						label = "b",
						getSnapshot = getSnapshotB,
						mutableSource = mutableSource,
						subscribe = subscribeB,
					}),
					React.createElement(Component, {
						label = "c",
						getSnapshot = getSnapshotB,
						mutableSource = mutableSource,
						subscribe = subscribeB,
					})
				),
				function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end
			)
			jestExpect(Scheduler).toFlushAndYieldThrough({
				"a:a:one",
				"b:b:one",
			})

			-- Mutating the source should trigger a tear detection on the next read,
			-- which should throw and re-render the entire tree.
			source.valueB = "b:two"

			jestExpect(Scheduler).toFlushAndYield({
				"a:a:one",
				"b:b:two",
				"c:b:two",
				"Sync effect",
			})
		end)
	end)
	-- ROBLOX TODO: jest.fn
	-- @gate experimental
	it(
		"does not schedule an update for subscriptions that fire with an unchanged snapshot",
		function()
			-- ROBLOX TODO: jest.fn, temporarily uses Component in place of jest:fn(Component)
			local MockComponent = Component
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			act(function()
				ReactNoop.render(
					React.createElement(MockComponent, {
						label = "only",
						getSnapshot = defaultGetSnapshot,
						mutableSource = mutableSource,
						subscribe = defaultSubscribe,
					}),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYieldThrough({
					"only:one",
					"Sync effect",
				})
				ReactNoop.flushPassiveEffects()
				jestExpect(source.listenerCount).toEqual(1)

				-- Notify subscribe function but don't change the value
				source.value = "one"

				jestExpect(Scheduler).toFlushWithoutYielding()
			end)
		end
	)
	-- @gate experimental
	it(
		"should throw and restart if getSnapshot changes between scheduled update and re-render",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)
			local newGetSnapshot = function(s)
				return "new:" .. defaultGetSnapshot(s)
			end
			local getSnapshot, updateGetSnapshot

			local function WrapperWithState()
				-- ROBLOX deviation: useState has 2 returns, not tuple
				getSnapshot, updateGetSnapshot = React.useState(function()
					return defaultGetSnapshot
				end)

				return React.createElement(Component, {
					label = "only",
					getSnapshot = getSnapshot,
					mutableSource = mutableSource,
					subscribe = defaultSubscribe,
				})
			end

			act(function()
				ReactNoop.render(React.createElement(WrapperWithState), function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end)
				jestExpect(Scheduler).toFlushAndYield({
					"only:one",
					"Sync effect",
				})
				ReactNoop.flushPassiveEffects()
				Scheduler.unstable_runWithPriority(
					Scheduler.unstable_LowPriority,
					function()
						-- Change the source (and schedule an update).
						source.value = "two"
					end
				)
				-- Schedule a higher priority update that changes getSnapshot.
				Scheduler.unstable_runWithPriority(
					Scheduler.unstable_UserBlockingPriority,
					function()
						updateGetSnapshot(function()
							return newGetSnapshot
						end)
					end
				)
				jestExpect(Scheduler).toFlushAndYield({
					"only:new:two",
				})
			end)
		end
	)
	-- @gate experimental
	it(
		"should recover from a mutation during yield when other work is scheduled",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			act(function()
				-- Start a render that uses the mutable source.
				ReactNoop.render(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(Component, {
							label = "a",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						}),
						React.createElement(Component, {
							label = "b",
							getSnapshot = defaultGetSnapshot,
							mutableSource = mutableSource,
							subscribe = defaultSubscribe,
						})
					)
				)
				jestExpect(Scheduler).toFlushAndYieldThrough({
					"a:one",
				})

				-- Mutate source
				source.value = "two"

				-- Now render something different.
				ReactNoop.render(React.createElement("div"))
				jestExpect(Scheduler).toFlushAndYield({})
			end)
		end
	)
	-- @gate experimental
	it(
		"should not throw if the new getSnapshot returns the same snapshot value",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)
			local onRenderA = jest.fn()
			local onRenderB = jest.fn()
			local getSnapshot, updateGetSnapshot

			local function WrapperWithState()
				getSnapshot, updateGetSnapshot = React.useState(function()
					return defaultGetSnapshot
				end)

				return React.createElement(Component, {
					label = "b",
					getSnapshot = getSnapshot,
					mutableSource = mutableSource,
					subscribe = defaultSubscribe,
				})
			end

			act(function()
				ReactNoop.render(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(
							React.Profiler,
							{
								id = "a",
								onRender = onRenderA,
							},
							React.createElement(Component, {
								label = "a",
								getSnapshot = defaultGetSnapshot,
								mutableSource = mutableSource,
								subscribe = defaultSubscribe,
							})
						),
						React.createElement(React.Profiler, {
							id = "b",
							onRender = onRenderB,
						}, React.createElement(WrapperWithState))
					),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYield({
					"a:one",
					"b:one",
					"Sync effect",
				})
				ReactNoop.flushPassiveEffects()
				jestExpect(onRenderA).toHaveBeenCalledTimes(1)
				jestExpect(onRenderB).toHaveBeenCalledTimes(1)

				-- If B's getSnapshot function updates, but the snapshot it returns is the same,
				-- only B should re-render (to update its state).
				updateGetSnapshot(function()
					return function(s)
						return defaultGetSnapshot(s)
					end
				end)
				jestExpect(Scheduler).toFlushAndYield({
					"b:one",
				})
				ReactNoop.flushPassiveEffects()
				jestExpect(onRenderA).toHaveBeenCalledTimes(1)
				jestExpect(onRenderB).toHaveBeenCalledTimes(2)
			end)
		end
	)

	-- @gate experimental
	it(
		"should not throw if getSnapshot changes but the source can be safely read from anyway",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)
			local newGetSnapshot = function(s)
				return "new:" .. defaultGetSnapshot(s)
			end
			local getSnapshot, updateGetSnapshot

			local function WrapperWithState()
				-- ROBLOX deviation: useState has 2 returns
				getSnapshot, updateGetSnapshot = React.useState(function()
					return defaultGetSnapshot
				end)

				return React.createElement(Component, {
					label = "only",
					getSnapshot = getSnapshot,
					mutableSource = mutableSource,
					subscribe = defaultSubscribe,
				})
			end

			act(function()
				ReactNoop.render(React.createElement(WrapperWithState), function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end)
				jestExpect(Scheduler).toFlushAndYield({
					"only:one",
					"Sync effect",
				})
				ReactNoop.flushPassiveEffects()

				-- Change the source (and schedule an update)
				-- but also change the snapshot function too.
				ReactNoop.batchedUpdates(function()
					source.value = "two"
					updateGetSnapshot(function()
						return newGetSnapshot
					end)
				end)
				jestExpect(Scheduler).toFlushAndYield({
					"only:new:two",
				})
			end)
		end
	)

	-- ROBLOX FIXME: fails with Error: Should have a queue
	-- @gate experimental
	xit(
		"should still schedule an update if an eager selector throws after a mutation",
		function()
			local source = createSource({
				friends = {
					{
						id = 1,
						name = "Foo",
					},
					{
						id = 2,
						name = "Bar",
					},
				},
			})
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			local function Friend(props)
				local id = props.id
				local getSnapshot = React.useCallback(function(props_)
					local value = props_.value
					-- This selector is intentionally written in a way that will throw
					-- if no matching friend exists in the store.
					return Array.find(value.friends, function(friend)
						return friend.id == id
					end).name
				end, { id })
				local name =
					useMutableSource(mutableSource, getSnapshot, defaultSubscribe)

				Scheduler.unstable_yieldValue(string.format("%s:%s", id, name))

				return React.createElement("li", nil, name)
			end

			local function FriendsList()
				local getSnapshot = React.useCallback(function(props)
					local value = props.value

					return Array.from(value.friends)
				end, {})
				local friends =
					useMutableSource(mutableSource, getSnapshot, defaultSubscribe)

				return React.createElement(
					"ul",
					nil,
					Array.map(friends, function(friend)
						return React.createElement(Friend, {
							key = friend.id,
							id = friend.id,
						})
					end)
				)
			end

			act(function()
				ReactNoop.render(React.createElement(FriendsList), function()
					return Scheduler.unstable_yieldValue("Sync effect")
				end)
				jestExpect(Scheduler).toFlushAndYield({
					"1:Foo",
					"2:Bar",
					"Sync effect",
				})

				-- This mutation will cause the "Bar" component to throw,
				-- since its value will no longer be a part of the store.
				-- Mutable source should still schedule an update though,
				-- which should unmount "Bar" and mount "Baz".
				source.value = {
					friends = {
						{
							id = 1,
							name = "Foo",
						},
						{
							id = 3,
							name = "Baz",
						},
					},
				}

				jestExpect(Scheduler).toFlushAndYield({
					"1:Foo",
					"3:Baz",
				})
			end)
		end
	)

	-- @gate experimental
	it(
		"should not warn about updates that fire between unmount and passive unsubscribe",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			local function Wrapper()
				React.useLayoutEffect(function()
					return function()
						Scheduler.unstable_yieldValue("layout unmount")
					end
				end)

				return React.createElement(Component, {
					label = "only",
					getSnapshot = defaultGetSnapshot,
					mutableSource = mutableSource,
					subscribe = defaultSubscribe,
				})
			end

			act(function()
				ReactNoop.renderToRootWithID(
					React.createElement(Wrapper),
					"root",
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYield({
					"only:one",
					"Sync effect",
				})
				ReactNoop.flushPassiveEffects()

				-- Unmounting a root should remove the remaining event listeners in a passive effect
				ReactNoop.unmountRootWithID("root")
				jestExpect(Scheduler).toFlushAndYieldThrough({
					"layout unmount",
				})

				-- Changes to source should not cause a warning,
				-- even though the unsubscribe hasn't run yet (since it's a pending passive effect).
				source.value = "two"

				jestExpect(Scheduler).toFlushAndYield({})
			end)
		end
	)

	-- @gate experimental
	it(
		"should support inline selectors and updates that are processed after selector change",
		function()
			local source = createSource({
				a = "initial",
				b = "initial",
			})
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)
			local getSnapshotA = function()
				return source.value.a
			end
			local getSnapshotB = function()
				return source.value.b
			end

			local function mutateB(newB)
				-- ROBLOX deviation: no table spread available, so we duplicate then add an entry for 'b'
				local tempTable = LuauPolyfill.Object.entries(source.value)
				tempTable.b = newB
				source.value = tempTable
			end
			local function App(props)
				local getSnapshot = props.getSnapshot
				local state =
					useMutableSource(mutableSource, getSnapshot, defaultSubscribe)

				return state
			end

			local root = ReactNoop.createRoot()

			act(function()
				root.render(React.createElement(App, { getSnapshot = getSnapshotA }))
			end)

			jestExpect(root).toMatchRenderedOutput("initial")

			act(function()
				mutateB("Updated B")
				root.render(React.createElement(App, { getSnapshot = getSnapshotB }))
			end)

			jestExpect(root).toMatchRenderedOutput("Updated B")

			act(function()
				mutateB("Another update")
			end)

			jestExpect(root).toMatchRenderedOutput("Another update")
		end
	)

	-- @gate experimental
	it(
		"should clear the update queue when getSnapshot changes with pending lower priority updates",
		function()
			local source = createSource({
				a = "initial",
				b = "initial",
			})
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)
			local getSnapshotA = function()
				return source.value.a
			end
			local getSnapshotB = function()
				return source.value.b
			end

			local function mutateA(newA)
				-- ROBLOX deviation: no table spread available, so we duplicate then add an entry for 'a'
				local tempTable = LuauPolyfill.Object.assign({}, source.value)
				tempTable.a = newA
				source.value = tempTable
			end
			local function mutateB(newB)
				-- ROBLOX deviation: no table spread available, so we duplicate then add an entry for 'b'
				local tempTable = LuauPolyfill.Object.assign({}, source.value)
				tempTable.b = newB
				source.value = tempTable
			end
			local function App(props)
				local toggle = props.toggle
				local state = useMutableSource(
					mutableSource,
					(function()
						if toggle then
							return getSnapshotB
						end

						return getSnapshotA
					end)(),
					defaultSubscribe
				)
				local result = (function()
					if toggle then
						return "B: "
					end

					return "A: "
				end)() .. state

				return result
			end

			local root = ReactNoop.createRoot()
			act(function()
				root.render(React.createElement(App, { toggle = false }))
			end)

			jestExpect(root).toMatchRenderedOutput("A: initial")

			act(function()
				ReactNoop.discreteUpdates(function()
					-- Update both A and B to the same value
					mutateA("Update")
					mutateB("Update") -- Toggle to B in the same batch

					root.render(React.createElement(App, { toggle = true }))
				end)
				-- Mutate A at lower priority. This should never be rendered, because
				-- by the time we get to the lower priority, we've already switched
				-- to B.

				mutateA("OOPS! This mutation should be ignored")
			end)

			jestExpect(root).toMatchRenderedOutput("B: Update")
		end
	)

	-- @gate experimental
	it(
		"should clear the update queue when source changes with pending lower priority updates",
		function()
			local sourceA = createSource("initial")
			local sourceB = createSource("initial")
			local mutableSourceA = createMutableSource(sourceA, function(param)
				return param.versionA
			end)
			local mutableSourceB = createMutableSource(sourceB, function(param)
				return param.versionB
			end)

			local function App(props)
				local toggle = props.toggle
				local state = useMutableSource(
					(function()
						if toggle then
							return mutableSourceB
						end

						return mutableSourceA
					end)(),
					defaultGetSnapshot,
					defaultSubscribe
				)
				local result = (function()
					if toggle then
						return "B: "
					end

					return "A: "
				end)() .. state

				return result
			end

			local root = ReactNoop.createRoot()
			act(function()
				root.render(React.createElement(App, { toggle = false }))
			end)

			jestExpect(root).toMatchRenderedOutput("A: initial")

			act(function()
				ReactNoop.discreteUpdates(function()
					-- Update both A and B to the same value
					sourceA.value = "Update"
					sourceB.value = "Update"
					-- Toggle to B in the same batch
					root.render(React.createElement(App, { toggle = true }))
				end)
				-- Mutate A at lower priority. This should never be rendered, because
				-- by the time we get to the lower priority, we've already switched
				-- to B.

				sourceA.value = "OOPS! This mutation should be ignored"
			end)

			jestExpect(root).toMatchRenderedOutput("B: Update")
		end
	)

	-- @gate experimental
	it(
		"should always treat reading as potentially unsafe when getSnapshot changes between renders",
		function()
			local source = createSource({
				a = "foo",
				b = "bar",
			})
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)
			local getSnapshotA = function()
				return source.value.a
			end
			local getSnapshotB = function()
				return source.value.b
			end

			local function mutateA(newA)
				-- ROBLOX deviation: no table spread available, so we duplicate then add an entry for 'a'
				local tempTable = LuauPolyfill.Object.assign({}, source.value)
				tempTable.a = newA
				source.value = tempTable
			end
			local function App(props)
				local getSnapshotFirst, getSnapshotSecond =
					props.getSnapshotFirst, props.getSnapshotSecond
				local first =
					useMutableSource(mutableSource, getSnapshotFirst, defaultSubscribe)
				local second =
					useMutableSource(mutableSource, getSnapshotSecond, defaultSubscribe)
				local result = string.format("x: %s, y: %s", first, second)

				if getSnapshotFirst == getSnapshotSecond then
					-- When both getSnapshot functions are equal,
					-- the two values must be consistent.
					if first ~= second then
						result = "Oops, tearing!"
					end
				end

				React.useEffect(function()
					Scheduler.unstable_yieldValue(result)
				end, { result })

				return result
			end

			local root = ReactNoop.createRoot()
			act(function()
				root.render(React.createElement(App, {
					getSnapshotFirst = getSnapshotA,
					getSnapshotSecond = getSnapshotB,
				}))
			end)

			-- x and y start out reading from different parts of the store.
			jestExpect(Scheduler).toHaveYielded({
				"x: foo, y: bar",
			})

			act(function()
				ReactNoop.discreteUpdates(function()
					-- At high priority, toggle y so that it reads from A instead of B.
					-- Simultaneously, mutate A.
					mutateA("baz")
					root.render(React.createElement(App, {
						getSnapshotFirst = getSnapshotA,
						getSnapshotSecond = getSnapshotA,
					}))
					-- If this update were processed before the next mutation,
					-- it would be expected to yield "baz" and "baz".
				end)

				-- At lower priority, mutate A again.
				-- This happens to match the initial value of B.
				mutateA("bar")

				-- When this update is processed,
				-- it is expected to yield "bar" and "bar".
			end)

			-- Check that we didn't commit any inconsistent states.
			-- The actual sequence of work will be:
			-- 1. React renders the high-pri update, sees a new getSnapshot, detects the source has been further mutated, and throws
			-- 2. React re-renders with all pending updates, including the second mutation, and renders "bar" and "bar".
			jestExpect(Scheduler).toHaveYielded({
				"x: bar, y: bar",
			})
		end
	)

	-- ROBLOX TODO: unimplemented toFlushUntilNextPaint
	-- @gate experimental
	xit(
		"getSnapshot changes and then source is mutated in between paint and passive effect phase",
		function()
			local source = createSource({
				a = "foo",
				b = "bar",
			})
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			local function mutateB(newB)
				-- ROBLOX deviation: no table spread available, so we duplicate then add an entry for 'b'
				local tempTable = LuauPolyfill.Object.entries(source.value)
				tempTable.b = newB
				source.value = tempTable
			end

			local getSnapshotA = function()
				return source.value.a
			end
			local getSnapshotB = function()
				return source.value.b
			end

			local function App(props)
				local getSnapshot = props.getSnapshot
				local value =
					useMutableSource(mutableSource, getSnapshot, defaultSubscribe)

				Scheduler.unstable_yieldValue("Render: " .. value)
				React.useEffect(function()
					Scheduler.unstable_yieldValue("Commit: " .. value)
				end, { value })

				return value
			end

			local root = ReactNoop.createRoot()
			act(function()
				root.render(React.createElement(App, {
					getSnapshot = getSnapshotA,
				}))
			end)

			jestExpect(Scheduler).toHaveYielded({
				"Render: foo",
				"Commit: foo",
			})

			act(function()
				-- Switch getSnapshot to read from B instead
				root.render(React.createElement(App, {
					getSnapshot = getSnapshotB,
				}))
				-- Render and finish the tree, but yield right after paint, before
				-- the passive effects have fired.
				jestExpect(Scheduler).toFlushUntilNextPaint({ "Render: bar" })
				-- Then mutate B.
				mutateB("baz")
			end)

			jestExpect(Scheduler).toHaveYielded({
				"Render: bar",
				-- Fires the effect from the previous render
				"Commit: bar",
				-- During that effect, it should detect that the snapshot has changed
				-- and re-render.
				"Render: baz",
				"Commit: baz",
			})
			jestExpect(root).toMatchRenderedOutput("baz")
		end
	)

	-- ROBLOX TODO: unimplemented toFlushUntilNextPaint() and getChildrenAsJSX()
	-- @gate experimental
	xit(
		"getSnapshot changes and then source is mutated in between paint and passive effect phase, case 2",
		function()
			local source = createSource({
				a = "a0",
				b = "b0",
			})
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)
			local getSnapshotA = function()
				return source.value.a
			end
			local getSnapshotB = function()
				return source.value.b
			end

			local function mutateA(newA)
				-- ROBLOX deviation: no table spread available, so we duplicate then add an entry for 'a'
				local tempTable = LuauPolyfill.Object.entries(source.value)
				tempTable.A = newA
				source.value = tempTable
			end
			local function App(props)
				local getSnapshotFirst, getSnapshotSecond =
					props.getSnapshotFirst, props.getSnapshotSecond
				local first =
					useMutableSource(mutableSource, getSnapshotFirst, defaultSubscribe)
				local second =
					useMutableSource(mutableSource, getSnapshotSecond, defaultSubscribe)

				return string.format("first: %s, second: %s", first, second)
			end

			local root = ReactNoop.createRoot()
			act(function()
				root.render(React.createElement(App, {
					getSnapshotFirst = getSnapshotA,
					getSnapshotSecond = getSnapshotB,
				}))
			end)
			jestExpect(root.getChildrenAsJSX()).toEqual("first: a0, second: b0")

			act(function()
				-- Switch the second getSnapshot to also read from A
				root.render(React.createElement(App, {
					getSnapshotFirst = getSnapshotA,
					getSnapshotSecond = getSnapshotB,
				}))
				-- Render and finish the tree, but yield right after paint, before
				-- the passive effects have fired.
				jestExpect(Scheduler).toFlushUntilNextPaint({})

				-- Now mutate A. Both hooks should update.
				-- This is at high priority so that it doesn't get batched with default
				-- priority updates that might fire during the passive effect
				ReactNoop.discreteUpdates(function()
					mutateA("a1")
				end)
				jestExpect(Scheduler).toFlushUntilNextPaint({})

				jestExpect(root.getChildrenAsJSX()).toEqual("first: a1, second: a1")
			end)
			jestExpect(root.getChildrenAsJSX()).toEqual("first: a1, second: a1")
		end
	)

	-- ROBLOX TODO: unimplemented toFlushUntilNextPaint()
	-- @gate experimental
	xit(
		"if source is mutated after initial read but before subscription is set "
			.. "up, should still entangle all pending mutations even if snapshot of "
			.. "new subscription happens to match",
		function()
			local source = createSource({
				a = "a0",
				b = "b0",
			})
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)
			local getSnapshotA = function()
				return source.value.a
			end
			local getSnapshotB = function()
				return source.value.b
			end

			local function mutateA(newA)
				-- ROBLOX deviation: no table spread available, so we duplicate then add an entry for 'a'
				local tempTable = LuauPolyfill.Object.assign({}, source.value)
				tempTable.a = newA
				source.value = tempTable
			end
			local function mutateB(newB)
				-- ROBLOX deviation: no table spread available, so we duplicate then add an entry for 'b'
				local tempTable = LuauPolyfill.Object.assign({}, source.value)
				tempTable.b = newB
				source.value = tempTable
			end
			local function Read(props)
				local getSnapshot = props.getSnapshot
				local value =
					useMutableSource(mutableSource, getSnapshot, defaultSubscribe)

				Scheduler.unstable_yieldValue(value)

				return value
			end
			local function Text(props)
				local text = props.text

				Scheduler.unstable_yieldValue(text)

				return text
			end

			local root = ReactNoop.createRoot()
			act(function()
				root.render(React.createElement(Read, {
					getSnapshot = getSnapshotA,
				}))
			end)

			jestExpect(Scheduler).toHaveYielded({
				"a0",
			})
			jestExpect(root).toMatchRenderedOutput("a0")
			act(function()
				root.render(
					React.createElement(
						React.Fragment,
						nil,
						React.createElement(Read, {
							getSnapshotFirst = getSnapshotA,
						}),
						React.createElement(Read, {
							getSnapshotFirst = getSnapshotB,
						}),
						React.createElement(Text, {
							text = "c",
						})
					)
				)
				jestExpect(Scheduler).toFlushAndYieldThrough({ "a0", "b0" })
				-- Mutate in an event. This schedules a subscription update on a, which
				-- already mounted, but not b, which hasn't subscribed yet.
				mutateA("a1")
				mutateB("b1")

				-- Mutate again at lower priority. This will schedule another subscription
				-- update on a, but not b. When b mounts and subscriptions, the value it
				-- read during render will happen to match the latest value. But it should
				-- still entangle the updates to prevent the previous update (a1) from
				-- rendering by itself.
				Scheduler.unstable_runWithPriority(
					Scheduler.unstable_IdlePriority,
					function()
						mutateA("a0")
						mutateB("b0")
					end
				)
				-- Finish the current render
				jestExpect(Scheduler).toFlushUntilNextPaint({ "c" })
				-- a0 will re-render because of the mutation update. But it should show
				-- the latest value, not the intermediate one, to avoid tearing with b.
				jestExpect(Scheduler).toFlushUntilNextPaint({ "a0" })
				jestExpect(root).toMatchRenderedOutput("a0b0c")
				-- We should be done.
				jestExpect(Scheduler).toFlushAndYield({})
				jestExpect(root).toMatchRenderedOutput("a0b0c")
			end)
		end
	)

	-- @gate experimental
	it("warns about functions being used as snapshot values", function()
		local source = createSource(function()
			return "a"
		end)
		local mutableSource = createMutableSource(source, function(param)
			return param.version
		end)
		local getSnapshot = function()
			return source.value
		end

		local function Read()
			local fn = useMutableSource(mutableSource, getSnapshot, defaultSubscribe)
			local value = fn()
			Scheduler.unstable_yieldValue(value)
			return value
		end

		local root = ReactNoop.createRoot()
		act(function()
			root.render(
				React.createElement(React.Fragment, nil, React.createElement(Read))
			)
			jestExpect(function()
				jestExpect(Scheduler).toFlushAndYield({ "a" })
			end).toErrorDev(
				"Mutable source should not return a function as the snapshot value."
			)
		end)

		jestExpect(root).toMatchRenderedOutput("a")
	end)

	-- @gate experimental
	it(
		"getSnapshot changes and then source is mutated during interleaved event",
		function()
			local useEffect = React.useEffect

			local source = createComplexSource("1", "2")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)

			-- Subscribe to part of the store.
			local getSnapshotA = function(s)
				return s.valueA
			end
			local subscribeA = function(s, callback)
				return s.subscribeA(callback)
			end
			local configA = { getSnapshotA, subscribeA }
			local getSnapshotB = function(s)
				return s.valueB
			end
			local subscribeB = function(s, callback)
				return s.subscribeB(callback)
			end
			local configB = { getSnapshotB, subscribeB }

			local function Child(props)
				local parentConfig, childConfig, parentValue =
					props.parentConfig, props.childConfig, props.parentValue
				local getSnapshot, subscribe = childConfig[1], childConfig[2]
				local childValue = useMutableSource(mutableSource, getSnapshot, subscribe)

				Scheduler.unstable_yieldValue("Child: " .. childValue)

				local result = string.format("%s, %s", parentValue, childValue)

				if parentConfig == childConfig then
					-- When both components read using the same config, the two values
					-- must be consistent.
					if parentValue ~= childValue then
						result = "Oops, tearing!"
					end
				end

				useEffect(function()
					Scheduler.unstable_yieldValue("Commit: " .. result)
				end, { result })

				return result
			end

			local function App(props)
				local parentConfig, childConfig = props.parentConfig, props.childConfig
				local getSnapshot, subscribe = parentConfig[1], parentConfig[2]
				local parentValue =
					useMutableSource(mutableSource, getSnapshot, subscribe)
				Scheduler.unstable_yieldValue("Parent: " .. parentValue)

				return React.createElement(Child, {
					parentConfig = parentConfig,
					childConfig = childConfig,
					parentValue = parentValue,
				})
			end

			local root = ReactNoop.createRoot()
			Promise.try(function()
				act(function()
					root.render(
						React.createElement(
							App,
							{ parentConfig = configA, childConfig = configB }
						)
					)
				end)
			end):await()

			jestExpect(Scheduler).toHaveYielded({
				"Parent: 1",
				"Child: 2",
				"Commit: 1, 2",
			})
			act(function()
				-- Switch the parent and the child to read using the same config
				root.render(
					React.createElement(
						App,
						{ parentConfig = configB, childConfig = configB }
					)
				)
				-- Start rendering the parent, but yield before rendering the child
				jestExpect(Scheduler).toFlushAndYieldThrough({ "Parent: 2" })

				-- Mutate the config. This is at lower priority so that 1) to make sure
				-- it doesn't happen to get batched with the in-progress render, and 2)
				-- so it doesn't interrupt the in-progress render.
				Scheduler.unstable_runWithPriority(
					Scheduler.unstable_IdlePriority,
					function()
						source.valueB = "3"
					end
				)

				jestExpect(Scheduler).toFlushAndYieldThrough({
					-- The partial render completes
					"Child: 2",
					"Commit: 2, 2",
				})

				-- Now there are two pending mutations at different priorities. But they
				-- both read the same version of the mutable source, so we must render
				-- them simultaneously.
				--
				jestExpect(Scheduler).toFlushAndYieldThrough({
					"Parent: 3",
					-- Demonstrates that we can yield here
				})
				jestExpect(Scheduler).toFlushAndYield({
					-- Now finish the rest of the update
					"Child: 3",
					"Commit: 3, 3",
				})
			end)
		end
	)

	-- @gate experimental
	it(
		"should not tear with newly mounted component when updates were scheduled at a lower priority",
		function()
			local source = createSource("one")
			local mutableSource = createMutableSource(source, function(param)
				return param.version
			end)
			local committedA = nil
			local committedB = nil
			local onRender = function()
				if committedB ~= nil then
					jestExpect(committedA).toEqual(committedB)
				end
			end

			local function ComponentA()
				local snapshot =
					useMutableSource(mutableSource, defaultGetSnapshot, defaultSubscribe)

				Scheduler.unstable_yieldValue(string.format("a:%s", snapshot))
				React.useEffect(function()
					committedA = snapshot
				end, { snapshot })

				return React.createElement("div", nil, string.format("a:%s", snapshot))
			end
			local function ComponentB()
				local snapshot =
					useMutableSource(mutableSource, defaultGetSnapshot, defaultSubscribe)

				Scheduler.unstable_yieldValue(string.format("b:%s", snapshot))
				React.useEffect(function()
					committedB = snapshot
				end, { snapshot })

				return React.createElement("div", nil, string.format("b:%s", snapshot))
			end

			-- Mount ComponentA with data version 1
			act(function()
				ReactNoop.render(
					React.createElement(React.Profiler, {
						id = "root",
						onRender = onRender,
					}, React.createElement(ComponentA)),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
			end)
			jestExpect(Scheduler).toHaveYielded({
				"a:one",
				"Sync effect",
			})
			jestExpect(source.listenerCount).toEqual(1)

			-- Mount ComponentB with version 1 (but don't commit it)
			act(function()
				ReactNoop.render(
					React.createElement(React.Profiler, {
						id = "root",
						onRender = onRender,
					}, React.createElement(ComponentA), React.createElement(
						ComponentB
					)),
					function()
						return Scheduler.unstable_yieldValue("Sync effect")
					end
				)
				jestExpect(Scheduler).toFlushAndYieldThrough({
					"a:one",
					"b:one",
					"Sync effect",
				})
				jestExpect(source.listenerCount).toEqual(1)

				-- Mutate -> schedule update for ComponentA
				Scheduler.unstable_runWithPriority(
					Scheduler.unstable_IdlePriority,
					function()
						source.value = "two"
					end
				)

				-- Commit ComponentB -> notice the change and schedule an update for ComponentB
				jestExpect(Scheduler).toFlushAndYield({
					"a:two",
					"b:two",
				})
				jestExpect(source.listenerCount).toEqual(2)
			end)
		end
	)

	if _G.__DEV__ then
		describe("dev warnings", function()
			-- @gate experimental
			it(
				"should warn if the subscribe function does not return an unsubscribe function",
				function()
					local source = createSource("one")
					local mutableSource = createMutableSource(source, function(param)
						return param.version
					end)
					local brokenSubscribe = function() end

					jestExpect(function()
						act(function()
							ReactNoop.render(React.createElement(Component, {
								label = "only",
								getSnapshot = defaultGetSnapshot,
								mutableSource = mutableSource,
								subscribe = brokenSubscribe,
							}))
						end)
					end).toErrorDev(
						"Mutable source subscribe function must return an unsubscribe function."
					)
				end
			)

			-- -- ROBLOX TODO: spyOnDev
			-- -- @gate experimental
			-- xit('should error if multiple renderers of the same type use a mutable source at the same time', function()
			--             --     local source = createSource('one')
			--     local mutableSource = createMutableSource(source, function(param)
			--         return param.version
			--     end)

			--     act(function()
			--         -- Start a render that uses the mutable source.
			--         ReactNoop.render(React.createElement(React.Fragment, nil, React.createElement(Component, {
			--             label = 'a',
			--             getSnapshot = defaultGetSnapshot,
			--             mutableSource = mutableSource,
			--             subscribe = defaultSubscribe,
			--         }), React.createElement(Component, {
			--             label = 'b',
			--             getSnapshot = defaultGetSnapshot,
			--             mutableSource = mutableSource,
			--             subscribe = defaultSubscribe,
			--         })))
			--         jestExpect(Scheduler).toFlushAndYieldThrough({
			--             'a:one',
			--         })

			--         local PrevScheduler = Scheduler
			--         -- Get a new copy of ReactNoop.

			--         loadModules()

			--         spyOnDev(console, 'error')

			--         -- Use the mutablesource again but with a different renderer.
			--         ReactNoop.render(React.createElement(Component, {
			--             label = 'c',
			--             getSnapshot = defaultGetSnapshot,
			--             mutableSource = mutableSource,
			--             subscribe = defaultSubscribe,
			--         }))
			--         jestExpect(Scheduler).toFlushAndYieldThrough({
			--             'c:one',
			--         })
			--         jestExpect(console.error.calls.argsFor(0)[0]).toContain('Detected multiple renderers concurrently rendering the ' + 'same mutable source. This is currently unsupported.')

			--         -- TODO (useMutableSource) Act will automatically flush remaining work from render 1,
			--         -- but at this point something in the hooks dispatcher has been broken by jest.resetModules()
			--         -- Figure out what this is and remove this catch.
			--         jestExpect(function()
			--             return PrevScheduler.unstable_flushAllWithoutAsserting()
			--         end).toThrow('Invalid hook call')
			--     end)
			-- end)

			-- -- ROBLOX TODO: spyOnDev
			-- @gate experimental
			-- xit('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', function(
			-- )
			--         --     local source = createSource('one')
			--     local mutableSource = createMutableSource(source, function(param)
			--         return param.version
			--     end)

			--     act(function()
			--         -- Start a render that uses the mutable source.
			--         ReactNoop.render(React.createElement(React.Fragment, nil, React.createElement(Component, {
			--             label = 'a',
			--             getSnapshot = defaultGetSnapshot,
			--             mutableSource = mutableSource,
			--             subscribe = defaultSubscribe,
			--         }), React.createElement(Component, {
			--             label = 'b',
			--             getSnapshot = defaultGetSnapshot,
			--             mutableSource = mutableSource,
			--             subscribe = defaultSubscribe,
			--         })))
			--         jestExpect(Scheduler).toFlushAndYieldThrough({
			--             'a:one',
			--         })

			--         local PrevScheduler = Scheduler

			--         -- Get a new copy of ReactNoop.
			--         loadModules()

			--         spyOnDev(console, 'error')

			--         -- Mutate before the new render reads from the source.
			--         source.value ='two')

			--         -- Use the mutablesource again but with a different renderer.
			--         ReactNoop.render(React.createElement(Component, {
			--             label = 'c',
			--             getSnapshot = defaultGetSnapshot,
			--             mutableSource = mutableSource,
			--             subscribe = defaultSubscribe,
			--         }))
			--         jestExpect(Scheduler).toFlushAndYieldThrough({
			--             'c:two',
			--         })
			--         jestExpect(console.error.calls.argsFor(0)[0]).toContain('Detected multiple renderers concurrently rendering the ' .. 'same mutable source. This is currently unsupported.')

			--         -- TODO (useMutableSource) Act will automatically flush remaining work from render 1,
			--         -- but at this point something in the hooks dispatcher has been broken by jest.resetModules()
			--         -- Figure out what this is and remove this catch.
			--         jestExpect(function()
			--             return PrevScheduler.unstable_flushAllWithoutAsserting()
			--         end).toThrow('Invalid hook call')
			--     end)
			-- end)
		end)
	end
end)
