local d=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 i=d.Object local F=(require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\115\104\097\114\101\100")).ReactFeatureFlags local k local b local a local M=10 local B=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 z=B.expect local H=B.describe local U=B.beforeEach local v=B.it local S=B.xit local w=B.jest H("\082\101\097\099\116\073\110\099\114\101\109\101\110\116\097\108\085\112\100\097\116\101\115",function()local function d(d)return d(F)end U(function()w.resetModules()k=require("\064\112\107\103\047\064\106\115\100\111\116\108\117\097\047\114\101\097\099\116")b=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")a=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")end)local function B(d)return{type="\115\112\097\110",children={};prop=d,hidden=false}end v("\097\112\112\108\105\101\115 \117\112\100\097\116\101\115 \105\110 \111\114\100\101\114 \111\102 \112\114\105\111\114\105\116\121",function()local d local i=k.Component:extend("\070\111\111")function i.init(d)d.state={}end function i.componentDidMount(d)local i=d a.unstable_yieldValue("\099\111\109\109\105\116")b.deferredUpdates(function()i:setState({b="\098"})i:setState({c="\099"})end)d:setState({a="\097"})end function i.render(i)d=i.state return k.createElement("\100\105\118")end b.render(k.createElement(i));(z(a)).toFlushAndYieldThrough({"\099\111\109\109\105\116"});(z(d)).toEqual({a="\097"});(z(a)).toFlushWithoutYielding();(z(d)).toEqual({a="\097";b="\098",c="\099"})end)v("\097\112\112\108\105\101\115 \117\112\100\097\116\101\115 \119\105\116\104 \101\113\117\097\108 \112\114\105\111\114\105\116\121 \105\110 \105\110\115\101\114\116\105\111\110 \111\114\100\101\114",function()local d local i=k.Component:extend("\070\111\111")function i.init(d)d.state={}end function i.componentDidMount(d)d:setState({a="\097"})d:setState({b="\098"})d:setState({c="\099"})end function i.render(i)d=i.state return k.createElement("\100\105\118")end b.render(k.createElement(i));(z(a)).toFlushWithoutYielding();(z(d)).toEqual({a="\097";b="\098",c="\099"})end)v("\111\110\108\121 \100\114\111\112\115 \117\112\100\097\116\101\115 \119\105\116\104 \101\113\117\097\108 \111\114 \108\101\115\115\101\114 \112\114\105\111\114\105\116\121 \119\104\101\110 \114\101\112\108\097\099\101\083\116\097\116\101 \105\115 \099\097\108\108\101\100",function()local d local i=k.Component:extend("\070\111\111")function i.init(d)d.state={}end function i.componentDidMount(d)a.unstable_yieldValue("\099\111\109\112\111\110\101\110\116\068\105\100\077\111\117\110\116")end function i.componentDidUpdate(d)a.unstable_yieldValue("\099\111\109\112\111\110\101\110\116\068\105\100\085\112\100\097\116\101")end function i.render(i)a.unstable_yieldValue("\114\101\110\100\101\114")d=i return k.createElement("\100\105\118")end b.render(k.createElement(i));(z(a)).toFlushAndYield({"\114\101\110\100\101\114";"\099\111\109\112\111\110\101\110\116\068\105\100\077\111\117\110\116"})b.flushSync(function()b.deferredUpdates(function()d:setState({x="\120"})d:setState({y="\121"})end)d:setState({a="\097"})d:setState({b="\098"})b.deferredUpdates(function()d.__updater.enqueueReplaceState(d,{c="\099"})d:setState({d="\100"})end)end);(z(d.state)).toEqual({a="\097",b="\098"});(z(a)).toHaveYielded({"\114\101\110\100\101\114","\099\111\109\112\111\110\101\110\116\068\105\100\085\112\100\097\116\101"});(z(a)).toFlushAndYield({"\114\101\110\100\101\114","\099\111\109\112\111\110\101\110\116\068\105\100\085\112\100\097\116\101"});(z(d.state)).toEqual({c="\099";d="\100"})end)v("\099\097\110 \097\098\111\114\116 \097\110 \117\112\100\097\116\101\044 \115\099\104\101\100\117\108\101 \097\100\100\105\116\105\111\110\097\108 \117\112\100\097\116\101\115\044 \097\110\100 \114\101\115\117\109\101",function()local d local F=k.Component:extend("\070\111\111")function F.init(d)d.state={}end function F.render(F)d=F local b=i.keys(F.state)table.sort(b)return k.createElement("\115\112\097\110",{prop=table.concat(b,"")})end b.render(k.createElement(F));(z(a)).toFlushWithoutYielding()local function M(d)return function()a.unstable_yieldValue(d)return{[d]=d}end end d:setState(M("\097"))d:setState(M("\098"))d:setState(M("\099"));(z(a)).toFlushAndYieldThrough({"\097";"\098","\099"});(z(b.getChildren())).toEqual({B("")})d:setState(M("\100"))b.flushSync(function()d:setState(M("\101"))d:setState(M("\102"))end)d:setState(M("\103"));(z(a)).toHaveYielded({"\101","\102"});(z(b.getChildren())).toEqual({B("\101\102")});(z(a)).toFlushAndYield({"\097";"\098";"\099","\100";"\101","\102","\103"});(z(b.getChildren())).toEqual({B("\097\098\099\100\101\102\103")})end)v("\099\097\110 \097\098\111\114\116 \097\110 \117\112\100\097\116\101\044 \115\099\104\101\100\117\108\101 \097 \114\101\112\108\097\099\101\083\116\097\116\101\044 \097\110\100 \114\101\115\117\109\101",function()local d local F=k.Component:extend("\070\111\111")function F.init(d)d.state={}end function F.render(F)d=F local b=i.keys(F.state)table.sort(b)return k.createElement("\115\112\097\110",{prop=table.concat(b,"")})end b.render(k.createElement(F));(z(a)).toFlushWithoutYielding()local function M(d)return function()a.unstable_yieldValue(d)return{[d]=d}end end d:setState(M("\097"))d:setState(M("\098"))d:setState(M("\099"));(z(a)).toFlushAndYieldThrough({"\097","\098","\099"});(z(b.getChildren())).toEqual({B("")})d:setState(M("\100"))b.flushSync(function()d:setState(M("\101"))d.__updater.enqueueReplaceState(d,M("\102"))end)d:setState(M("\103"));(z(a)).toHaveYielded({"\101";"\102"});(z(b.getChildren())).toEqual({B("\102")});(z(a)).toFlushAndYield({"\097";"\098","\099";"\100","\101","\102";"\103"});(z(b.getChildren())).toEqual({B("\102\103")})end)v("\099\097\110 \097\098\111\114\116 \097\110 \117\112\100\097\116\101\044 \115\099\104\101\100\117\108\101 \097 \114\101\112\108\097\099\101\083\116\097\116\101\044 \097\110\100 \114\101\115\117\109\101 \109\097\110\121 \116\105\109\101\115",function()local d local F=k.Component:extend("\070\111\111")function F.init(d)d.state={}end function F.render(F)d=F local b=i.keys(F.state)table.sort(b)return k.createElement("\115\112\097\110",{prop=table.concat(b,"")})end b.render(k.createElement(F));(z(a)).toFlushWithoutYielding()local function M(d)return function()a.unstable_yieldValue(d)return{[d]=d}end end for i=1,500,1 do d:setState(M("\097"))d:setState(M("\098"))d:setState(M("\099"))end;(z(b.getChildren())).toEqual({B("")})d:setState(M("\100"))b.flushSync(function()d:setState(M("\101"))d.__updater.enqueueReplaceState(d,M("\102"))end)d:setState(M("\103"));(z(a)).toHaveYielded({"\101";"\102"});(z(b.getChildren())).toEqual({B("\102")})b.flushSync(function()d:setState(M("\103"))end);(z(b.getChildren())).toEqual({B("\102\103")})end)v("\112\097\115\115\101\115 \097\099\099\117\109\117\108\097\116\105\111\110 \111\102 \112\114\101\118\105\111\117\115 \117\112\100\097\116\101\115 \116\111 \114\101\112\108\097\099\101\083\116\097\116\101 \117\112\100\097\116\101\114 \102\117\110\099\116\105\111\110",function()local d local i=k.Component:extend("\070\111\111")function i.init(d)d.state={}end function i.render(i)d=i return k.createElement("\115\112\097\110")end b.render(k.createElement(i));(z(a)).toFlushWithoutYielding()d:setState({a="\097"})d:setState({b="\098"})d.__updater.enqueueReplaceState(d,function(d)return{previousState=d}end);(z(a)).toFlushWithoutYielding();(z(d.state.previousState)).toEqual({a="\097",b="\098"})end)v("\100\111\101\115 \110\111\116 \099\097\108\108 \099\097\108\108\098\097\099\107\115 \116\104\097\116 \097\114\101 \115\099\104\101\100\117\108\101\100 \098\121 \097\110\111\116\104\101\114 \099\097\108\108\098\097\099\107 \117\110\116\105\108 \097 \108\097\116\101\114 \099\111\109\109\105\116",function()local d=k.Component:extend("\070\111\111")function d.init(d)d.state={}end function d.componentDidMount(d)local i=d a.unstable_yieldValue("\100\105\100 \109\111\117\110\116")d:setState({a="\097"},function()a.unstable_yieldValue("\099\097\108\108\098\097\099\107 \097")i:setState({b="\098"},function()a.unstable_yieldValue("\099\097\108\108\098\097\099\107 \098")end)end)end function d.render(d)a.unstable_yieldValue("\114\101\110\100\101\114")return k.createElement("\100\105\118")end b.render(k.createElement(d));(z(a)).toFlushAndYield({"\114\101\110\100\101\114","\100\105\100 \109\111\117\110\116","\114\101\110\100\101\114";"\099\097\108\108\098\097\099\107 \097";"\114\101\110\100\101\114","\099\097\108\108\098\097\099\107 \098"})end)v("\103\105\118\101\115 \115\101\116\083\116\097\116\101 \100\117\114\105\110\103 \114\101\099\111\110\099\105\108\105\097\116\105\111\110 \116\104\101 \115\097\109\101 \112\114\105\111\114\105\116\121 \097\115 \119\104\097\116\101\118\101\114 \108\101\118\101\108 \105\115 \099\117\114\114\101\110\116\108\121 \114\101\099\111\110\099\105\108\105\110\103",function()local d local i=k.Component:extend("\070\111\111")function i.init(d)d.state={}end function i.UNSAFE_componentWillReceiveProps(d)a.unstable_yieldValue("\099\111\109\112\111\110\101\110\116\087\105\108\108\082\101\099\101\105\118\101\080\114\111\112\115")d:setState({b="\098"})end function i.render(i)a.unstable_yieldValue("\114\101\110\100\101\114")d=i return k.createElement("\100\105\118")end b.render(k.createElement(i));(z(function()return(z(a)).toFlushAndYield({"\114\101\110\100\101\114"})end)).toErrorDev("\085\115\105\110\103 \085\078\083\065\070\069\095\099\111\109\112\111\110\101\110\116\087\105\108\108\082\101\099\101\105\118\101\080\114\111\112\115 \105\110 \115\116\114\105\099\116 \109\111\100\101 \105\115 \110\111\116 \114\101\099\111\109\109\101\110\100\101\100",{withoutStack=true})b.flushSync(function()d:setState({a="\097"})b.render(k.createElement(i))return"\116\101\115\116"end);(z(d.state)).toEqual({a="\097";b="\098"});(z(a)).toHaveYielded({"\099\111\109\112\111\110\101\110\116\087\105\108\108\082\101\099\101\105\118\101\080\114\111\112\115","\114\101\110\100\101\114"})end)v("\117\112\100\097\116\101\115 \116\114\105\103\103\101\114\101\100 \102\114\111\109 \105\110\115\105\100\101 \097 \099\108\097\115\115 \115\101\116\083\116\097\116\101 \117\112\100\097\116\101\114",function()local i local F=k.Component:extend("\070\111\111")function F.init(d)d.state={}end function F.render(d)a.unstable_yieldValue("\114\101\110\100\101\114")i=d return k.createElement("\100\105\118")end b.render(k.createElement(F));(z(a)).toFlushAndYield({"\114\101\110\100\101\114"})i:setState(function()a.unstable_yieldValue("\115\101\116\083\116\097\116\101 \117\112\100\097\116\101\114")i:setState({b="\098"})return{a="\097"}end);(z(function()return(z(a)).toFlushAndYield(d(function(d)if d.deferRenderPhaseUpdateToNextBatch then return{"\115\101\116\083\116\097\116\101 \117\112\100\097\116\101\114";"\114\101\110\100\101\114";"\114\101\110\100\101\114"}end return{"\115\101\116\083\116\097\116\101 \117\112\100\097\116\101\114";"\114\101\110\100\101\114"}end))end)).toErrorDev("\065\110 \117\112\100\097\116\101 \040\115\101\116\083\116\097\116\101\044 \114\101\112\108\097\099\101\083\116\097\116\101\044 \111\114 \102\111\114\099\101\085\112\100\097\116\101\041 \119\097\115 \115\099\104\101\100\117\108\101\100 \102\114\111\109 \105\110\115\105\100\101 \097\110 \117\112\100\097\116\101 \102\117\110\099\116\105\111\110\046 \085\112\100\097\116\101 \102\117\110\099\116\105\111\110\115 \115\104\111\117\108\100 \098\101 \112\117\114\101\044 \119\105\116\104 \122\101\114\111 \115\105\100\101-\101\102\102\101\099\116\115\046 \067\111\110\115\105\100\101\114 \117\115\105\110\103 \099\111\109\112\111\110\101\110\116\068\105\100\085\112\100\097\116\101 \111\114 \097 \099\097\108\108\098\097\099\107\046");(z(i.state)).toEqual({a="\097";b="\098"})i:setState(function()i:setState({a="\097"})return{b="\098"}end);(z(a)).toFlushAndYield(d(function(d)return(function()if d.deferRenderPhaseUpdateToNextBatch then return{"\114\101\110\100\101\114","\114\101\110\100\101\114"}end return{"\114\101\110\100\101\114"}end)()end))end)v("\103\101\116\068\101\114\105\118\101\100\083\116\097\116\101\070\114\111\109\080\114\111\112\115 \115\104\111\117\108\100 \117\112\100\097\116\101 \098\097\115\101 \115\116\097\116\101 \111\102 \117\112\100\097\116\101\081\117\101\117\101 \040\098\097\115\101\100 \111\110 \112\114\111\100\117\099\116 \098\117\103\041",function()local d local i local F=k.Component:extend("\066\097\114")function F.render(d)i=d return nil end local a=k.Component:extend("\070\111\111")function a.init(d)d.state={value="\105\110\105\116\105\097\108 \115\116\097\116\101"}end function a.getDerivedStateFromProps(d)return{value="\100\101\114\105\118\101\100 \115\116\097\116\101"}end function a.render(i)d=i return k.createElement(k.Fragment,nil,k.createElement("\115\112\097\110",{prop=i.state.value}),k.createElement(F))end b.flushSync(function()b.render(k.createElement(a))end);(z(b.getChildren())).toEqual({B("\100\101\114\105\118\101\100 \115\116\097\116\101")})b.flushSync(function()b.render(k.createElement(a))d:setState({value="\117\112\100\097\116\101 \115\116\097\116\101"},function() end)end);(z(b.getChildren())).toEqual({B("\100\101\114\105\118\101\100 \115\116\097\116\101")})b.flushSync(function()i:setState({})end);(z(b.getChildren())).toEqual({B("\100\101\114\105\118\101\100 \115\116\097\116\101")})end)v("\114\101\103\114\101\115\115\105\111\110\058 \100\111\101\115 \110\111\116 \101\120\112\105\114\101 \115\111\111\110 \100\117\101 \116\111 \108\097\121\111\117\116 \101\102\102\101\099\116\115 \105\110 \116\104\101 \108\097\115\116 \098\097\116\099\104",function()local d=k.useState local i=k.useLayoutEffect local F local function M()local k,b=d(0)F=b a.unstable_yieldValue("\082\101\110\100\101\114\058 "..k)i(function()F(function(d)return d+1 end)a.unstable_yieldValue("\067\111\109\109\105\116\058 "..k)end,{})return nil end b.act(function()b.render(k.createElement(M));(z(a)).toFlushExpired({});(z(a)).toFlushAndYield({"\082\101\110\100\101\114\058 \048";"\067\111\109\109\105\116\058 \048";"\082\101\110\100\101\114\058 \049"})a.unstable_advanceTime(10000)F(2);(z(a)).toFlushExpired({})end)end)v("\114\101\103\114\101\115\115\105\111\110\058 \100\111\101\115 \110\111\116 \101\120\112\105\114\101 \115\111\111\110 \100\117\101 \116\111 \112\114\101\118\105\111\117\115 \102\108\117\115\104\083\121\110\099",function()local function d(d)local i=d.text a.unstable_yieldValue(i)return i end b.flushSync(function()b.render(k.createElement(d,{text="\065"}))end);(z(a)).toHaveYielded({"\065"})a.unstable_advanceTime(10000)b.render(k.createElement(d,{text="\066"}));(z(a)).toFlushExpired({})end)v("\114\101\103\114\101\115\115\105\111\110\058 \100\111\101\115 \110\111\116 \101\120\112\105\114\101 \115\111\111\110 \100\117\101 \116\111 \112\114\101\118\105\111\117\115 \101\120\112\105\114\101\100 \119\111\114\107",function()local function d(d)local i=d.text a.unstable_yieldValue(i)return i end b.render(k.createElement(d,{text="\065"}))a.unstable_advanceTime(10000);(z(a)).toFlushExpired({"\065"})a.unstable_advanceTime(10000)b.render(k.createElement(d,{text="\066"}));(z(a)).toFlushExpired({})end)v("\119\104\101\110 \114\101\098\097\115\105\110\103\044 \100\111\101\115 \110\111\116 \101\120\099\108\117\100\101 \117\112\100\097\116\101\115 \116\104\097\116 \119\101\114\101 \097\108\114\101\097\100\121 \099\111\109\109\105\116\116\101\100\044 \114\101\103\097\114\100\108\101\115\115 \111\102 \112\114\105\111\114\105\116\121",function()local d=k.useState local i=k.useLayoutEffect local F local function B()local k,B=d("")F=function(d)return B(function(i)return i..d end)end i(function()a.unstable_yieldValue("\067\111\109\109\105\116\116\101\100\058 "..k)if k=="\066"then b.unstable_runWithPriority(M,function()return a.unstable_runWithPriority(a.unstable_UserBlockingPriority,function()F("\067")end)end)B(function(d)return d.."\068"end)end end,{k})return k end local H=b.createRoot()b.act(function()H.render(k.createElement(B))end);(z(a)).toHaveYielded({"\067\111\109\109\105\116\116\101\100\058 "});(z(H)).toMatchRenderedOutput("")b.act(function()F("\065")b.unstable_runWithPriority(M,function()return a.unstable_runWithPriority(a.unstable_UserBlockingPriority,function()F("\066")end)end)end);(z(a)).toHaveYielded({"\067\111\109\109\105\116\116\101\100\058 \066","\067\111\109\109\105\116\116\101\100\058 \066\068","\067\111\109\109\105\116\116\101\100\058 \066\067\068","\067\111\109\109\105\116\116\101\100\058 \065\066\067\068"});(z(H)).toMatchRenderedOutput("\065\066\067\068")end)S("\119\104\101\110 \114\101\098\097\115\105\110\103\044 \100\111\101\115 \110\111\116 \101\120\099\108\117\100\101 \117\112\100\097\116\101\115 \116\104\097\116 \119\101\114\101 \097\108\114\101\097\100\121 \099\111\109\109\105\116\116\101\100\044 \114\101\103\097\114\100\108\101\115\115 \111\102 \112\114\105\111\114\105\116\121 \040\099\108\097\115\115\101\115\041",function()local d local i=k.Component:extend("\065\112\112")function i.init(d)d.state={log=""}end function i.pushToLog(i,d)i:setState(function(i)return{log=i.state.log..d}end)end function i.componentDidUpdate(d)a.unstable_yieldValue("\067\111\109\109\105\116\116\101\100\058 "..d.state.log)if d.state.log=="\066"then b.unstable_runWithPriority(M,function()a.unstable_runWithPriority(a.unstable_UserBlockingPriority,function()d:pushToLog("\067")end)end)d:pushToLog("\068")end end function i.render(i)d=i return i.state.log end local F=b.createRoot()local B=k.createElement(i)b.act(function()F.render(B)end);(z(a)).toHaveYielded({});(z(F)).toMatchRenderedOutput("")b.act(function()d:pushToLog("\065")b.unstable_runWithPriority(M,function()a.unstable_runWithPriority(a.unstable_UserBlockingPriority,function()d:pushToLog("\066")end)end)end);(z(a)).toHaveYielded({"\067\111\109\109\105\116\116\101\100\058 \066";"\067\111\109\109\105\116\116\101\100\058 \066\068","\067\111\109\109\105\116\116\101\100\058 \066\067\068";"\067\111\109\109\105\116\116\101\100\058 \065\066\067\068"});(z(F)).toMatchRenderedOutput("\065\066\067\068")end)v("\098\097\115\101 \115\116\097\116\101 \111\102 \117\112\100\097\116\101 \113\117\101\117\101 \105\115 \105\110\105\116\105\097\108\105\122\101\100 \116\111 \105\116\115 \102\105\098\101\114\'\115 \109\101\109\111\105\122\101\100 \115\116\097\116\101",function()local d local i=k.Component:extend("\065\112\112")function i.init(d)d.state={prevProp="\065";count=0}end function i.getDerivedStateFromProps(d,i)if d.prop~=i.prevProp then return{prevProp=d.prop;count=i.count+100}end return nil end function i.render(i)d=i return i.state.count end local F=b.createRoot()b.act(function()F.render(k.createElement(i,{prop="\065"}))end);(z(F)).toMatchRenderedOutput("\048")b.act(function()F.render(k.createElement(i,{prop="\066"}))end);(z(F)).toMatchRenderedOutput("\049\048\048")b.act(function()F.render(k.createElement(i,{prop="\065"}))d:setState(function(d)return{count=d.count+1}end)end);(z(F)).toMatchRenderedOutput("\050\048\049")end)end)
-- ROBLOX upstream: https://github.com/facebook/react/blob/d13f5b9538e48f74f7c571ef3cde652ca887cca0/packages/react-reconciler/src/__tests__/ReactIncrementalUpdates-test.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 LuauPolyfill = require("@pkg/@jsdotlua/luau-polyfill")
local Object = LuauPolyfill.Object
local ReactFeatureFlags = require("@pkg/@jsdotlua/shared").ReactFeatureFlags
local React
local ReactNoop
local Scheduler
local InputContinuousLanePriority = 10
local JestGlobals = require("@pkg/@jsdotlua/jest-globals")
local jestExpect = JestGlobals.expect
local describe = JestGlobals.describe
local beforeEach = JestGlobals.beforeEach
local it = JestGlobals.it
local xit = JestGlobals.xit
local jest = JestGlobals.jest

describe("ReactIncrementalUpdates", function()
	local function gate(fn)
		return fn(ReactFeatureFlags)
	end
	beforeEach(function()
		jest.resetModules()

		React = require("@pkg/@jsdotlua/react")
		ReactNoop = require("@pkg/@jsdotlua/react-noop-renderer")
		Scheduler = require("@pkg/@jsdotlua/scheduler")
	end)

	local function span(prop)
		return {
			type = "span",
			children = {},
			prop = prop,
			hidden = false,
		}
	end

	it("applies updates in order of priority", function()
		local state
		local Foo = React.Component:extend("Foo")
		function Foo:init()
			self.state = {}
		end
		function Foo:componentDidMount()
			local _this = self
			Scheduler.unstable_yieldValue("commit")
			ReactNoop.deferredUpdates(function()
				-- Has low priority
				_this:setState({ b = "b" })
				_this:setState({ c = "c" })
			end)
			-- Has Task priority
			self:setState({ a = "a" })
		end

		function Foo:render()
			state = self.state
			return React.createElement("div")
		end
		ReactNoop.render(React.createElement(Foo))
		jestExpect(Scheduler).toFlushAndYieldThrough({ "commit" })
		jestExpect(state).toEqual({
			a = "a",
		})
		jestExpect(Scheduler).toFlushWithoutYielding()

		jestExpect(state).toEqual({
			a = "a",
			b = "b",
			c = "c",
		})
	end)
	it("applies updates with equal priority in insertion order", function()
		local state
		local Foo = React.Component:extend("Foo")
		function Foo:init()
			self.state = {}
		end
		function Foo:componentDidMount()
			-- All have Task priority
			self:setState({ a = "a" })
			self:setState({ b = "b" })
			self:setState({ c = "c" })
		end

		function Foo:render()
			state = self.state
			return React.createElement("div")
		end
		ReactNoop.render(React.createElement(Foo))
		jestExpect(Scheduler).toFlushWithoutYielding()
		jestExpect(state).toEqual({
			a = "a",
			b = "b",
			c = "c",
		})
	end)
	it(
		"only drops updates with equal or lesser priority when replaceState is called",
		function()
			local instance
			local Foo = React.Component:extend("Foo")
			function Foo:init()
				self.state = {}
			end

			function Foo:componentDidMount()
				Scheduler.unstable_yieldValue("componentDidMount")
			end

			function Foo:componentDidUpdate()
				Scheduler.unstable_yieldValue("componentDidUpdate")
			end

			function Foo:render()
				Scheduler.unstable_yieldValue("render")
				instance = self
				return React.createElement("div")
			end

			ReactNoop.render(React.createElement(Foo))
			jestExpect(Scheduler).toFlushAndYield({
				"render",
				"componentDidMount",
			})
			ReactNoop.flushSync(function()
				ReactNoop.deferredUpdates(function()
					instance:setState({
						x = "x",
					})
					instance:setState({
						y = "y",
					})
				end)
				instance:setState({
					a = "a",
				})
				instance:setState({
					b = "b",
				})
				ReactNoop.deferredUpdates(function()
					instance.__updater.enqueueReplaceState(instance, {
						c = "c",
					})
					instance:setState({
						d = "d",
					})
				end)
			end)
			-- Even though a replaceState has been already scheduled, it hasn't been
			-- flushed yet because it has async priority.

			jestExpect(instance.state).toEqual({
				a = "a",
				b = "b",
			})
			jestExpect(Scheduler).toHaveYielded({
				"render",
				"componentDidUpdate",
			})
			jestExpect(Scheduler).toFlushAndYield({
				"render",
				"componentDidUpdate",
			})
			-- Now the rest of the updates are flushed, including the replaceState.
			jestExpect(instance.state).toEqual({
				c = "c",
				d = "d",
			})
		end
	)
	-- Test fails due to update priority bug
	it("can abort an update, schedule additional updates, and resume", function()
		local instance
		local Foo = React.Component:extend("Foo")
		function Foo:init()
			self.state = {}
		end

		function Foo:render()
			instance = self
			local keylist = Object.keys(self.state)
			table.sort(keylist)
			return React.createElement("span", {
				prop = table.concat(keylist, ""),
			})
		end

		ReactNoop.render(React.createElement(Foo))
		jestExpect(Scheduler).toFlushWithoutYielding()

		local function createUpdate(letter)
			return function()
				Scheduler.unstable_yieldValue(letter)
				return { [letter] = letter }
			end
		end
		-- Schedule some async updates

		instance:setState(createUpdate("a"))
		instance:setState(createUpdate("b"))
		instance:setState(createUpdate("c")) -- // Begin the updates but don't flush them yet
		jestExpect(Scheduler).toFlushAndYieldThrough({
			"a",
			"b",
			"c",
		})
		jestExpect(ReactNoop.getChildren()).toEqual({
			span(""),
		}) -- Schedule some more updates at different priorities
		instance:setState(createUpdate("d"))
		ReactNoop.flushSync(function()
			instance:setState(createUpdate("e"))
			instance:setState(createUpdate("f"))
		end)
		instance:setState(createUpdate("g")) -- The sync updates should have flushed, but not the async ones
		jestExpect(Scheduler).toHaveYielded({
			"e",
			"f",
		})
		jestExpect(ReactNoop.getChildren()).toEqual({
			span("ef"),
		})
		-- Now flush the remaining work. Even though e and f were already processed,
		-- they should be processed again, to ensure that the terminal state
		-- is deterministic.
		jestExpect(Scheduler).toFlushAndYield({
			"a",
			"b",
			"c",
			"d",
			"e",
			"f",
			"g",
		})
		jestExpect(ReactNoop.getChildren()).toEqual({
			span("abcdefg"),
		})
	end)
	-- Test fails due to update priority bug
	it("can abort an update, schedule a replaceState, and resume", function()
		local instance

		local Foo = React.Component:extend("Foo")
		function Foo:init()
			self.state = {}
		end

		function Foo:render()
			instance = self
			local keylist = Object.keys(self.state)
			table.sort(keylist)
			return React.createElement("span", {
				prop = table.concat(keylist, ""),
			})
		end

		ReactNoop.render(React.createElement(Foo))
		jestExpect(Scheduler).toFlushWithoutYielding()

		local function createUpdate(letter)
			return function()
				Scheduler.unstable_yieldValue(letter)
				return { [letter] = letter }
			end
		end

		-- Schedule some async updates
		instance:setState(createUpdate("a"))
		instance:setState(createUpdate("b"))
		instance:setState(createUpdate("c"))

		-- Begin the updates but don't flush them yet
		jestExpect(Scheduler).toFlushAndYieldThrough({
			"a",
			"b",
			"c",
		})
		jestExpect(ReactNoop.getChildren()).toEqual({
			span(""),
		})

		-- Schedule some more updates at different priorities
		instance:setState(createUpdate("d"))
		ReactNoop.flushSync(function()
			instance:setState(createUpdate("e"))
			instance.__updater.enqueueReplaceState(instance, createUpdate("f"))
		end)
		instance:setState(createUpdate("g"))

		-- The sync updates should have flushed, but not the async ones
		jestExpect(Scheduler).toHaveYielded({
			"e",
			"f",
		})
		jestExpect(ReactNoop.getChildren()).toEqual({
			span("f"),
		})
		-- Now flush the remaining work. Even though e and f were already processed,
		-- they should be processed again, to ensure that the terminal state
		-- is deterministic.
		jestExpect(Scheduler).toFlushAndYield({
			"a",
			"b",
			"c",
			"d",
			"e",
			"f",
			"g",
		})
		jestExpect(ReactNoop.getChildren()).toEqual({
			span("fg"),
		})
	end)
	-- ROBLOX deviation START: same as above, but tests > 1000 updates
	it("can abort an update, schedule a replaceState, and resume many times", function()
		local instance

		local Foo = React.Component:extend("Foo")
		function Foo:init()
			self.state = {}
		end

		function Foo:render()
			instance = self
			local keylist = Object.keys(self.state)
			table.sort(keylist)
			return React.createElement("span", {
				prop = table.concat(keylist, ""),
			})
		end

		ReactNoop.render(React.createElement(Foo))
		jestExpect(Scheduler).toFlushWithoutYielding()

		local function createUpdate(letter)
			return function()
				Scheduler.unstable_yieldValue(letter)
				return { [letter] = letter }
			end
		end

		-- Schedule many async updates
		for _ = 1, 500 do
			instance:setState(createUpdate("a"))
			instance:setState(createUpdate("b"))
			instance:setState(createUpdate("c"))
		end
		jestExpect(ReactNoop.getChildren()).toEqual({
			span(""),
		})

		-- Schedule some more updates at different priorities
		instance:setState(createUpdate("d"))
		ReactNoop.flushSync(function()
			instance:setState(createUpdate("e"))
			instance.__updater.enqueueReplaceState(instance, createUpdate("f"))
		end)
		instance:setState(createUpdate("g"))

		-- The sync updates should have flushed, but not the async ones
		jestExpect(Scheduler).toHaveYielded({
			"e",
			"f",
		})
		jestExpect(ReactNoop.getChildren()).toEqual({
			span("f"),
		})
		-- Now flush the remaining work.
		ReactNoop.flushSync(function()
			instance:setState(createUpdate("g"))
		end)
		jestExpect(ReactNoop.getChildren()).toEqual({
			span("fg"),
		})
	end)
	-- ROBLOX deviation END
	it(
		"passes accumulation of previous updates to replaceState updater function",
		function()
			local instance
			local Foo = React.Component:extend("Foo")
			function Foo:init()
				self.state = {}
			end

			function Foo:render()
				instance = self
				return React.createElement("span")
			end

			ReactNoop.render(React.createElement(Foo))
			jestExpect(Scheduler).toFlushWithoutYielding()
			instance:setState({ a = "a" })
			instance:setState({ b = "b" })
			-- No longer a public API, but we can test that it works internally by
			-- reaching into the updater.
			instance.__updater.enqueueReplaceState(instance, function(previousState)
				return { previousState = previousState }
			end)
			jestExpect(Scheduler).toFlushWithoutYielding()
			jestExpect(instance.state.previousState).toEqual({
				a = "a",
				b = "b",
			})
		end
	)
	it(
		"does not call callbacks that are scheduled by another callback until a later commit",
		function()
			local Foo = React.Component:extend("Foo")
			function Foo:init()
				self.state = {}
			end

			function Foo:componentDidMount()
				local _this2 = self
				Scheduler.unstable_yieldValue("did mount")
				self:setState({
					a = "a",
				}, function()
					Scheduler.unstable_yieldValue("callback a")
					_this2:setState({
						b = "b",
					}, function()
						Scheduler.unstable_yieldValue("callback b")
					end)
				end)
			end

			function Foo:render()
				Scheduler.unstable_yieldValue("render")
				return React.createElement("div")
			end

			ReactNoop.render(React.createElement(Foo))
			jestExpect(Scheduler).toFlushAndYield({
				"render",
				"did mount",
				"render",
				"callback a",
				"render",
				"callback b",
			})
		end
	)
	it(
		"gives setState during reconciliation the same priority as whatever level is currently reconciling",
		function()
			local instance

			local Foo = React.Component:extend("Foo")
			function Foo:init()
				self.state = {}
			end

			function Foo:UNSAFE_componentWillReceiveProps()
				Scheduler.unstable_yieldValue("componentWillReceiveProps")
				self:setState({
					b = "b",
				})
			end

			function Foo:render()
				Scheduler.unstable_yieldValue("render")
				instance = self
				return React.createElement("div")
			end

			ReactNoop.render(React.createElement(Foo))
			jestExpect(function()
				return jestExpect(Scheduler).toFlushAndYield({
					"render",
				})
			end).toErrorDev(
				"Using UNSAFE_componentWillReceiveProps in strict mode is not recommended",
				{ withoutStack = true }
			)
			ReactNoop.flushSync(function()
				instance:setState({
					a = "a",
				})
				ReactNoop.render(React.createElement(Foo))
				return "test"
			end)
			jestExpect(instance.state).toEqual({
				a = "a",
				b = "b",
			})
			jestExpect(Scheduler).toHaveYielded({
				"componentWillReceiveProps",
				"render",
			})
		end
	)
	it("updates triggered from inside a class setState updater", function()
		local instance
		local Foo = React.Component:extend("Foo")
		function Foo:init()
			self.state = {}
		end

		function Foo:render()
			Scheduler.unstable_yieldValue("render")
			instance = self
			return React.createElement("div")
		end

		ReactNoop.render(React.createElement(Foo))
		jestExpect(Scheduler).toFlushAndYield({
			-- Initial render
			"render",
		})
		instance:setState(function()
			Scheduler.unstable_yieldValue("setState updater")
			instance:setState({
				b = "b",
			})
			return {
				a = "a",
			}
		end)
		jestExpect(function()
			-- ROBLOX deviation: using local defined gate which references ReactFeatureFlags as
			-- opposed to upstream's gate() which is defined in setupTests in Jest files
			return jestExpect(Scheduler).toFlushAndYield(gate(function(flags)
				if flags.deferRenderPhaseUpdateToNextBatch then
					return {
						"setState updater", -- In the new reconciler, updates inside the render phase are
						-- treated as if they came from an event, so the update gets
						-- shifted to a subsequent render.
						"render",
						"render",
					}
				end

				return {
					"setState updater", -- In the old reconciler, updates in the render phase receive
					-- the currently rendering expiration time, so the update
					-- flushes immediately in the same render.
					"render",
				}
			end))
		end).toErrorDev(
			"An update (setState, replaceState, or forceUpdate) was scheduled "
				.. "from inside an update function. Update functions should be pure, "
				.. "with zero side-effects. Consider using componentDidUpdate or a "
				.. "callback."
		)
		jestExpect(instance.state).toEqual({
			a = "a",
			b = "b",
		}) -- Test deduplication (no additional warnings expected)
		instance:setState(function()
			instance:setState({
				a = "a",
			})
			return {
				b = "b",
			}
		end)
		jestExpect(Scheduler).toFlushAndYield(gate(function(flags)
			return (function()
				if flags.deferRenderPhaseUpdateToNextBatch then
					return { -- In the new reconciler, updates inside the render phase are
						-- treated as if they came from an event, so the update gets shifted
						-- to a subsequent render.
						"render",
						"render",
					}
				end

				return {
					"render",
				}
			end)()
		end))
	end)
	it(
		"getDerivedStateFromProps should update base state of updateQueue (based on product bug)",
		function()
			-- Based on real-world bug.
			local foo
			local bar

			local Bar = React.Component:extend("Bar")
			function Bar:render()
				bar = self
				return nil
			end

			local Foo = React.Component:extend("Foo")
			function Foo:init()
				self.state = { value = "initial state" }
			end
			function Foo:getDerivedStateFromProps()
				return { value = "derived state" }
			end
			function Foo:render()
				foo = self
				return React.createElement(
					React.Fragment,
					nil,
					React.createElement("span", {
						prop = self.state.value,
					}),
					React.createElement(Bar)
				)
			end
			ReactNoop.flushSync(function()
				-- Triggers getDerivedStateFromProps again
				ReactNoop.render(React.createElement(Foo))
				-- The noop callback is needed to trigger the specific internal path that
				-- led to this bug. Removing it causes it to "accidentally" work.
			end)
			jestExpect(ReactNoop.getChildren()).toEqual({
				span("derived state"),
			})
			ReactNoop.flushSync(function()
				ReactNoop.render(React.createElement(Foo))
				foo:setState({
					value = "update state",
				}, function() end)
			end)
			jestExpect(ReactNoop.getChildren()).toEqual({
				span("derived state"),
			})
			ReactNoop.flushSync(function()
				bar:setState({})
			end)
			jestExpect(ReactNoop.getChildren()).toEqual({
				span("derived state"),
			})
		end
	)
	it(
		"regression: does not expire soon due to layout effects in the last batch",
		function()
			local useState = React.useState
			local useLayoutEffect = React.useLayoutEffect

			local setCount
			local function App()
				local count, setCountTemp = useState(0)
				setCount = setCountTemp

				Scheduler.unstable_yieldValue("Render: " .. count)
				useLayoutEffect(function()
					setCount(function(prevCount)
						return prevCount + 1
					end)
					Scheduler.unstable_yieldValue("Commit: " .. count)
				end, {})
				return nil
			end

			ReactNoop.act(function()
				ReactNoop.render(React.createElement(App))
				jestExpect(Scheduler).toFlushExpired({})
				jestExpect(Scheduler).toFlushAndYield({
					"Render: 0",
					"Commit: 0",
					"Render: 1",
				})
				Scheduler.unstable_advanceTime(10000)
				setCount(2)
				jestExpect(Scheduler).toFlushExpired({})
			end)
		end
	)
	it("regression: does not expire soon due to previous flushSync", function()
		local function Text(_ref)
			local text = _ref.text
			Scheduler.unstable_yieldValue(text)
			return text
		end

		ReactNoop.flushSync(function()
			ReactNoop.render(React.createElement(Text, {
				text = "A",
			}))
		end)
		jestExpect(Scheduler).toHaveYielded({
			"A",
		})
		Scheduler.unstable_advanceTime(10000)
		ReactNoop.render(React.createElement(Text, {
			text = "B",
		}))
		jestExpect(Scheduler).toFlushExpired({})
	end)
	it("regression: does not expire soon due to previous expired work", function()
		local function Text(_ref2)
			local text = _ref2.text

			Scheduler.unstable_yieldValue(text)

			return text
		end

		ReactNoop.render(React.createElement(Text, {
			text = "A",
		}))
		Scheduler.unstable_advanceTime(10000)
		jestExpect(Scheduler).toFlushExpired({
			"A",
		})
		Scheduler.unstable_advanceTime(10000)
		ReactNoop.render(React.createElement(Text, {
			text = "B",
		}))
		jestExpect(Scheduler).toFlushExpired({})
	end)

	it(
		"when rebasing, does not exclude updates that were already committed, regardless of priority",
		function()
			local useState = React.useState
			local useLayoutEffect = React.useLayoutEffect
			local pushToLog

			local function App()
				local log, setLog = useState("")
				pushToLog = function(msg)
					return setLog(function(prevLog)
						return prevLog .. msg
					end)
				end

				useLayoutEffect(function()
					Scheduler.unstable_yieldValue("Committed: " .. log)

					if log == "B" then
						-- Right after B commits, schedule additional updates.
						-- TODO: Double wrapping is temporary while we remove Scheduler runWithPriority.
						ReactNoop.unstable_runWithPriority(
							InputContinuousLanePriority,
							function()
								return Scheduler.unstable_runWithPriority(
									Scheduler.unstable_UserBlockingPriority,
									function()
										pushToLog("C")
									end
								)
							end
						)
						setLog(function(prevLog)
							return prevLog .. "D"
						end)
					end
				end, {
					log,
				})

				return log
			end

			local root = ReactNoop.createRoot()
			ReactNoop.act(function()
				root.render(React.createElement(App))
			end)
			jestExpect(Scheduler).toHaveYielded({
				"Committed: ",
			})

			jestExpect(root).toMatchRenderedOutput("")

			ReactNoop.act(function()
				pushToLog("A")

				-- TODO: Double wrapping is temporary while we remove Scheduler runWithPriority.
				ReactNoop.unstable_runWithPriority(InputContinuousLanePriority, function()
					return Scheduler.unstable_runWithPriority(
						Scheduler.unstable_UserBlockingPriority,
						function()
							pushToLog("B")
						end
					)
				end)
			end)
			jestExpect(Scheduler).toHaveYielded({
				-- A and B are pending. B is higher priority, so we'll render that first.
				"Committed: B",
				-- Because A comes first in the queue, we're now in rebase mode. B must
				-- be rebased on top of A. Also, in a layout effect, we received two new
				-- updates: C and D. C is user-blocking and D is synchronous.
				--
				-- First render the synchronous update. What we're testing here is that
				-- B *is not dropped* even though it has lower than sync priority. That's
				-- because we already committed it. However, this render should not
				-- include C, because that update wasn't already committed.
				"Committed: BD",
				"Committed: BCD",
				"Committed: ABCD",
			})

			jestExpect(root).toMatchRenderedOutput("ABCD")
		end
	)
	-- ROBLOX FIXME: fails with incorrect values
	xit(
		"when rebasing, does not exclude updates that were already committed, regardless of priority (classes)",
		function()
			local instance
			local App = React.Component:extend("App")
			function App:init()
				self.state = { log = "" }
			end
			function App:pushToLog(msg)
				self:setState(function(prevState)
					return { log = prevState.state.log .. msg }
				end)
			end

			function App:componentDidUpdate()
				Scheduler.unstable_yieldValue("Committed: " .. self.state.log)
				if self.state.log == "B" then
					-- Right after B commits, schedule additional updates.
					-- TODO: Double wrapping is temporary while we remove Scheduler runWithPriority.
					ReactNoop.unstable_runWithPriority(
						InputContinuousLanePriority,
						function()
							Scheduler.unstable_runWithPriority(
								Scheduler.unstable_UserBlockingPriority,
								function()
									self:pushToLog("C")
								end
							)
						end
					)
					self:pushToLog("D")
				end
			end

			function App:render()
				instance = self
				return self.state.log
			end

			local root = ReactNoop.createRoot()
			local app = React.createElement(App)
			-- ROBLOX FIXME: fails probably due to this not being as Promise as in upstream
			ReactNoop.act(function()
				root.render(app)
			end)
			jestExpect(Scheduler).toHaveYielded({})

			jestExpect(root).toMatchRenderedOutput("")

			ReactNoop.act(function()
				instance:pushToLog("A")
				-- TODO: Double wrapping is temporary while we remove Scheduler runWithPriority.
				ReactNoop.unstable_runWithPriority(InputContinuousLanePriority, function()
					Scheduler.unstable_runWithPriority(
						Scheduler.unstable_UserBlockingPriority,
						function()
							instance:pushToLog("B")
						end
					)
				end)
			end)
			jestExpect(Scheduler).toHaveYielded({
				-- A and B are pending. B is higher priority, so we'll render that first.
				"Committed: B",
				-- Because A comes first in the queue, we're now in rebase mode. B must
				-- be rebased on top of A. Also, in a layout effect, we received two new
				-- updates: C and D. C is user-blocking and D is synchronous.
				--
				-- First render the synchronous update. What we're testing here is that
				-- B *is not dropped* even though it has lower than sync priority. That's
				-- because we already committed it. However, this render should not
				-- include C, because that update wasn't already committed.
				"Committed: BD",
				"Committed: BCD",
				"Committed: ABCD",
			})
			jestExpect(root).toMatchRenderedOutput("ABCD")
		end
	)
	it(
		"base state of update queue is initialized to its fiber's memoized state",
		function()
			local app
			local App = React.Component:extend("App")
			function App:init()
				self.state = {
					prevProp = "A",
					count = 0,
				}
			end

			function App.getDerivedStateFromProps(props, state)
				-- Add 100 whenever the label prop changes. The prev label is stored
				-- in state. If the state is dropped incorrectly, we'll fail to detect
				-- prop changes.
				if props.prop ~= state.prevProp then
					return {
						prevProp = props.prop,
						count = state.count + 100,
					}
				end

				return nil
			end

			function App:render()
				app = self
				return self.state.count
			end

			local root = ReactNoop.createRoot()
			ReactNoop.act(function()
				root.render(React.createElement(App, { prop = "A" }))
			end)

			jestExpect(root).toMatchRenderedOutput("0") -- Changing the prop causes the count to increase by 100

			ReactNoop.act(function()
				root.render(React.createElement(App, { prop = "B" }))
			end)
			jestExpect(root).toMatchRenderedOutput("100")
			-- Now increment the count by 1 with a state update. And, in the same
			-- batch, change the prop back to its original value.

			ReactNoop.act(function()
				root.render(React.createElement(App, { prop = "A" }))
				app:setState(function(state)
					return {
						count = state.count + 1,
					}
				end)
			end) -- There were two total prop changes, plus an increment

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