local x=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 s=x.Error local F=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=F.beforeEach local W=F.expect local M=F.describe local S=F.it local p=F.jest local r local Q local J local B local c local l local O local L local H local I local function V(x)local s=x[1]local F=#x-1 for s=1,F,1 do x[s]=x[s+1]end x[F+1]=nil return s end z(function()p.resetModules()r=require("\046\046\047\117\110\115\116\097\098\108\101\095\109\111\099\107")Q=r.unstable_runWithPriority J=r.unstable_ImmediatePriority B=r.unstable_UserBlockingPriority c=r.unstable_NormalPriority l=r.unstable_scheduleCallback O=r.unstable_cancelCallback L=r.unstable_wrapCallback H=r.unstable_getCurrentPriorityLevel I=r.unstable_shouldYield end)S("\102\108\117\115\104\101\115 \119\111\114\107 \105\110\099\114\101\109\101\110\116\097\108\108\121",function()l(c,function()r.unstable_yieldValue("\065")end)l(c,function()r.unstable_yieldValue("\066")end)l(c,function()r.unstable_yieldValue("\067")end)l(c,function()r.unstable_yieldValue("\068")end);(W(r)).toFlushAndYieldThrough({"\065","\066"});(W(r)).toFlushAndYieldThrough({"\067"});(W(r)).toFlushAndYield({"\068"})end)S("\099\097\110\099\101\108\115 \119\111\114\107",function()l(c,function()r.unstable_yieldValue("\065")end)local x=l(c,function()r.unstable_yieldValue("\066")end)l(c,function()r.unstable_yieldValue("\067")end)O(x);(W(r)).toFlushAndYield({"\065";"\067"})end)S("\101\120\101\099\117\116\101\115 \116\104\101 \104\105\103\104\101\115\116 \112\114\105\111\114\105\116\121 \099\097\108\108\098\097\099\107\115 \102\105\114\115\116",function()l(c,function()r.unstable_yieldValue("\065")end)l(c,function()r.unstable_yieldValue("\066")end);(W(r)).toFlushAndYieldThrough({"\065"})l(B,function()r.unstable_yieldValue("\067")end)l(B,function()r.unstable_yieldValue("\068")end);(W(r)).toFlushAndYield({"\067","\068","\066"})end)S("\101\120\112\105\114\101\115 \119\111\114\107",function()l(c,function(x)r.unstable_advanceTime(100)r.unstable_yieldValue(string.format("\065 \040\100\105\100 \116\105\109\101\111\117\116\058 \037\115\041",tostring(x)))end)l(B,function(x)r.unstable_advanceTime(100)r.unstable_yieldValue(string.format("\066 \040\100\105\100 \116\105\109\101\111\117\116\058 \037\115\041",tostring(x)))end)l(B,function(x)r.unstable_advanceTime(100)r.unstable_yieldValue(string.format("\067 \040\100\105\100 \116\105\109\101\111\117\116\058 \037\115\041",tostring(x)))end)r.unstable_advanceTime(249);(W(r)).toHaveYielded({})l(c,function(x)r.unstable_advanceTime(100)r.unstable_yieldValue(string.format("\068 \040\100\105\100 \116\105\109\101\111\117\116\058 \037\115\041",tostring(x)))end)l(c,function(x)r.unstable_advanceTime(100)r.unstable_yieldValue(string.format("\069 \040\100\105\100 \116\105\109\101\111\117\116\058 \037\115\041",tostring(x)))end)r.unstable_advanceTime(1);(W(r)).toFlushExpired({"\066 \040\100\105\100 \116\105\109\101\111\117\116\058 \116\114\117\101\041","\067 \040\100\105\100 \116\105\109\101\111\117\116\058 \116\114\117\101\041"})r.unstable_advanceTime(4600);(W(r)).toFlushExpired({"\065 \040\100\105\100 \116\105\109\101\111\117\116\058 \116\114\117\101\041"});(W(r)).toFlushAndYield({"\068 \040\100\105\100 \116\105\109\101\111\117\116\058 \102\097\108\115\101\041","\069 \040\100\105\100 \116\105\109\101\111\117\116\058 \116\114\117\101\041"})end)S("\104\097\115 \097 \100\101\102\097\117\108\116 \101\120\112\105\114\097\116\105\111\110 \111\102 ~\053 \115\101\099\111\110\100\115",function()l(c,function()r.unstable_yieldValue("\065")end)r.unstable_advanceTime(4999);(W(r)).toHaveYielded({})r.unstable_advanceTime(1);(W(r)).toFlushExpired({"\065"})end)S("\099\111\110\116\105\110\117\101\115 \119\111\114\107\105\110\103 \111\110 \115\097\109\101 \116\097\115\107 \097\102\116\101\114 \121\105\101\108\100\105\110\103",function()l(c,function()r.unstable_advanceTime(100)r.unstable_yieldValue("\065")end)l(c,function()r.unstable_advanceTime(100)r.unstable_yieldValue("\066")end)local x=false local s={{"\067\049",100};{"\067\050",100};{"\067\051";100}}local function F()while#s>0 do local z,W=unpack(V(s))r.unstable_advanceTime(W)r.unstable_yieldValue(z)if I()then x=true return F end end return nil end l(c,F)l(c,function()r.unstable_advanceTime(100)r.unstable_yieldValue("\068")end)l(c,function()r.unstable_advanceTime(100)r.unstable_yieldValue("\069")end);(W(x)).toBe(false);(W(r)).toFlushAndYieldThrough({"\065";"\066";"\067\049"});(W(x)).toBe(true);(W(r)).toFlushAndYield({"\067\050","\067\051";"\068";"\069"})end)S("\099\111\110\116\105\110\117\097\116\105\111\110 \099\097\108\108\098\097\099\107\115 \105\110\104\101\114\105\116 \116\104\101 \101\120\112\105\114\097\116\105\111\110 \111\102 \116\104\101 \112\114\101\118\105\111\117\115 \099\097\108\108\098\097\099\107",function()local x={{"\065";125},{"\066",124},{"\067";100},{"\068",100}}local function s()while#x>0 do local F,z=unpack(V(x))r.unstable_advanceTime(z)r.unstable_yieldValue(F)if I()then return s end end return nil end l(B,s);(W(r)).toFlushAndYieldThrough({"\065","\066"})r.unstable_advanceTime(1);(W(r)).toFlushExpired({"\067","\068"})end)S("\099\111\110\116\105\110\117\097\116\105\111\110\115 \097\114\101 \105\110\116\101\114\114\117\112\116\101\100 \098\121 \104\105\103\104\101\114 \112\114\105\111\114\105\116\121 \119\111\114\107",function()local x={{"\065";100},{"\066";100},{"\067",100};{"\068",100}}local function s()while#x>0 do local F,z=unpack(V(x))r.unstable_advanceTime(z)r.unstable_yieldValue(F)if#x>0 and I()then return s end end return nil end l(c,s);(W(r)).toFlushAndYieldThrough({"\065"})l(B,function()r.unstable_advanceTime(100)r.unstable_yieldValue("\072\105\103\104 \112\114\105")end);(W(r)).toFlushAndYield({"\072\105\103\104 \112\114\105";"\066";"\067","\068"})end)S("\099\111\110\116\105\110\117\097\116\105\111\110\115 \100\111 \110\111\116 \098\108\111\099\107 \104\105\103\104\101\114 \112\114\105\111\114\105\116\121 \119\111\114\107 \115\099\104\101\100\117\108\101\100 \105\110\115\105\100\101 \097\110 \101\120\101\099\117\116\105\110\103 \099\097\108\108\098\097\099\107",function()local x={{"\065";100},{"\066",100};{"\067",100},{"\068",100}}local function s()while#x>0 do local F=V(x)local z,W=unpack(F)r.unstable_advanceTime(W)r.unstable_yieldValue(z)if z=="\066"then r.unstable_yieldValue("\083\099\104\101\100\117\108\101 \104\105\103\104 \112\114\105")l(B,function()r.unstable_advanceTime(100)r.unstable_yieldValue("\072\105\103\104 \112\114\105")end)end if#x>0 then return s end end return nil end l(c,s);(W(r)).toFlushAndYield({"\065","\066","\083\099\104\101\100\117\108\101 \104\105\103\104 \112\114\105";"\072\105\103\104 \112\114\105";"\067";"\068"})end)S("\099\097\110\099\101\108\108\105\110\103 \097 \099\111\110\116\105\110\117\097\116\105\111\110",function()local x=l(c,function()r.unstable_yieldValue("\089\105\101\108\100")return function()r.unstable_yieldValue("\067\111\110\116\105\110\117\097\116\105\111\110")end end);(W(r)).toFlushAndYieldThrough({"\089\105\101\108\100"})O(x);(W(r)).toFlushWithoutYielding()end)S("\116\111\112-\108\101\118\101\108 \105\109\109\101\100\105\097\116\101 \099\097\108\108\098\097\099\107\115 \102\105\114\101 \105\110 \097 \115\117\098\115\101\113\117\101\110\116 \116\097\115\107",function()l(J,function()r.unstable_yieldValue("\065")end)l(J,function()r.unstable_yieldValue("\066")end)l(J,function()r.unstable_yieldValue("\067")end)l(J,function()r.unstable_yieldValue("\068")end);(W(r)).toHaveYielded({});(W(r)).toFlushExpired({"\065";"\066";"\067";"\068"})end)S("\110\101\115\116\101\100 \105\109\109\101\100\105\097\116\101 \099\097\108\108\098\097\099\107\115 \097\114\101 \097\100\100\101\100 \116\111 \116\104\101 \113\117\101\117\101 \111\102 \105\109\109\101\100\105\097\116\101 \099\097\108\108\098\097\099\107\115",function()l(J,function()r.unstable_yieldValue("\065")end)l(J,function()r.unstable_yieldValue("\066")l(J,function()r.unstable_yieldValue("\067")end)end)l(J,function()r.unstable_yieldValue("\068")end);(W(r)).toHaveYielded({});(W(r)).toFlushExpired({"\065","\066";"\068";"\067"})end)S("\119\114\097\112\112\101\100 \099\097\108\108\098\097\099\107\115 \104\097\118\101 \115\097\109\101 \115\105\103\110\097\116\117\114\101 \097\115 \111\114\105\103\105\110\097\108 \099\097\108\108\098\097\099\107",function()local x=L(function(...)return{args={...}}end)local s=x("\097","\098");(W(#s.args)).toBe(2);(W(s.args)).toEqual({"\097","\098"})end)S("\119\114\097\112\112\101\100 \099\097\108\108\098\097\099\107\115 \105\110\104\101\114\105\116 \116\104\101 \099\117\114\114\101\110\116 \112\114\105\111\114\105\116\121",function()local x=Q(c,function()return L(function()r.unstable_yieldValue(H())end)end)local s=Q(B,function()return L(function()r.unstable_yieldValue(H())end)end)x();(W(r)).toHaveYielded({c})s();(W(r)).toHaveYielded({B})end)S("\119\114\097\112\112\101\100 \099\097\108\108\098\097\099\107\115 \105\110\104\101\114\105\116 \116\104\101 \099\117\114\114\101\110\116 \112\114\105\111\114\105\116\121 \101\118\101\110 \119\104\101\110 \110\101\115\116\101\100",function()local x local s Q(c,function()x=L(function()r.unstable_yieldValue(H())end)s=Q(B,function()return L(function()r.unstable_yieldValue(H())end)end)end)x();(W(r)).toHaveYielded({c})s();(W(r)).toHaveYielded({B})end)S("\105\109\109\101\100\105\097\116\101 \099\097\108\108\098\097\099\107\115 \102\105\114\101 \101\118\101\110 \105\102 \116\104\101\114\101\'\115 \097\110 \101\114\114\111\114",function()l(J,function()r.unstable_yieldValue("\065")error("\079\111\112\115 \065")end)l(J,function()r.unstable_yieldValue("\066")end)l(J,function()r.unstable_yieldValue("\067")error(s.new("\079\111\112\115 \067"))end);(W(function()(W(r)).toFlushExpired()end)).toThrow("\079\111\112\115 \065");(W(r)).toHaveYielded({"\065"});(W(function()(W(r)).toFlushExpired()end)).toThrow("\079\111\112\115 \067");(W(r)).toHaveYielded({"\066","\067"})end)S("\109\117\108\116\105\112\108\101 \105\109\109\101\100\105\097\116\101 \099\097\108\108\098\097\099\107\115 \099\097\110 \116\104\114\111\119 \097\110\100 \116\104\101\114\101 \119\105\108\108 \098\101 \097\110 \101\114\114\111\114 \102\111\114 \101\097\099\104 \111\110\101",function()l(J,function()error("\070\105\114\115\116 \101\114\114\111\114")end)l(J,function()error("\083\101\099\111\110\100 \101\114\114\111\114")end);(W(function()r.unstable_flushAll()end)).toThrow("\070\105\114\115\116 \101\114\114\111\114");(W(function()r.unstable_flushAll()end)).toThrow("\083\101\099\111\110\100 \101\114\114\111\114")end)S("\101\120\112\111\115\101\115 \116\104\101 \099\117\114\114\101\110\116 \112\114\105\111\114\105\116\121 \108\101\118\101\108",function()r.unstable_yieldValue(H())Q(J,function()r.unstable_yieldValue(H())Q(c,function()r.unstable_yieldValue(H())Q(B,function()r.unstable_yieldValue(H())end)end)r.unstable_yieldValue(H())end);(W(r)).toHaveYielded({c;J,c;B;J})end)M("\100\101\108\097\121\101\100 \116\097\115\107\115",function()S("\115\099\104\101\100\117\108\101\115 \097 \100\101\108\097\121\101\100 \116\097\115\107",function()l(c,function()r.unstable_yieldValue("\065")end,{delay=1000});(W(r)).toFlushAndYield({})r.unstable_advanceTime(999);(W(r)).toFlushAndYield({})r.unstable_advanceTime(1);(W(r)).toFlushAndYield({"\065"})end)S("\115\099\104\101\100\117\108\101\115 \109\117\108\116\105\112\108\101 \100\101\108\097\121\101\100 \116\097\115\107\115",function()l(c,function()r.unstable_yieldValue("\067")end,{delay=300})l(c,function()r.unstable_yieldValue("\066")end,{delay=200})l(c,function()r.unstable_yieldValue("\068")end,{delay=400})l(c,function()r.unstable_yieldValue("\065")end,{delay=100});(W(r)).toFlushAndYield({})r.unstable_advanceTime(200);(W(r)).toFlushAndYieldThrough({"\065"});(W(r)).toFlushAndYield({"\066"})r.unstable_advanceTime(200);(W(r)).toFlushAndYield({"\067","\068"})end)S("\105\110\116\101\114\108\101\097\118\101\115 \110\111\114\109\097\108 \116\097\115\107\115 \097\110\100 \100\101\108\097\121\101\100 \116\097\115\107\115",function()l(B,function()r.unstable_yieldValue("\084\105\109\101\114 \050")end,{delay=300})l(B,function()r.unstable_yieldValue("\084\105\109\101\114 \049")end,{delay=100})l(c,function()r.unstable_yieldValue("\065")r.unstable_advanceTime(100)end)l(c,function()r.unstable_yieldValue("\066")r.unstable_advanceTime(100)end)l(c,function()r.unstable_yieldValue("\067")r.unstable_advanceTime(100)end)l(c,function()r.unstable_yieldValue("\068")r.unstable_advanceTime(100)end);(W(r)).toFlushAndYield({"\065","\084\105\109\101\114 \049","\066","\067";"\084\105\109\101\114 \050","\068"})end)S("\105\110\116\101\114\108\101\097\118\101\115 \100\101\108\097\121\101\100 \116\097\115\107\115 \119\105\116\104 \116\105\109\101-\115\108\105\099\101\100 \116\097\115\107\115",function()l(B,function()r.unstable_yieldValue("\084\105\109\101\114 \050")end,{delay=300})l(B,function()r.unstable_yieldValue("\084\105\109\101\114 \049")end,{delay=100})local x={{"\065";100};{"\066",100},{"\067",100};{"\068";100}}local function s()while#x>0 do local F=V(x)local z,W=unpack(F)r.unstable_advanceTime(W)r.unstable_yieldValue(z)if#x>0 then return s end end return nil end l(c,s);(W(r)).toFlushAndYield({"\065","\084\105\109\101\114 \049";"\066";"\067";"\084\105\109\101\114 \050","\068"})end)S("\099\097\110\099\101\108\115 \097 \100\101\108\097\121\101\100 \116\097\115\107",function()local x={delay=100}l(c,function()r.unstable_yieldValue("\065")end,x)local s=l(c,function()r.unstable_yieldValue("\066")end,x)local F=l(c,function()r.unstable_yieldValue("\067")end,x);(W(r)).toFlushAndYield({})O(s)r.unstable_advanceTime(500)O(F);(W(r)).toFlushAndYield({"\065"})end)S("\103\114\097\099\101\102\117\108\108\121 \104\097\110\100\108\101\115 \115\099\104\101\100\117\108\101\100 \116\097\115\107\115 \116\104\097\116 \097\114\101 \110\111\116 \097 \102\117\110\099\116\105\111\110",function()l(J);(W(r)).toFlushWithoutYielding()l(J,{});(W(r)).toFlushWithoutYielding()l(J,42);(W(r)).toFlushWithoutYielding()end)S("\100\101\108\097\121\101\100 \116\097\115\107\115 \115\116\114\105\110\103\105\102\121 \116\104\101\105\114 \101\114\114\111\114",function()l(c,function()r.unstable_yieldValue("\065")error(s.new("\079\111\112\115 \065"))end,{delay=100})r.unstable_advanceTime(100);(W(r)).toFlushAndThrow("\079\111\112\115 \065")end)end)
-- ROBLOX upstream: https://github.com/facebook/react/blob/9abc2785cb070148d64fae81e523246b90b92016/packages/scheduler/src/__tests__/Scheduler-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
]]

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

local Scheduler
local runWithPriority
local ImmediatePriority
local UserBlockingPriority
local NormalPriority
-- deviation: These are only used in a commented-out _G.__DEV__-only test
-- (commented out to silence lints)
-- local LowPriority
-- local IdlePriority
local scheduleCallback
local cancelCallback
local wrapCallback
local getCurrentPriorityLevel
local shouldYield

local function shift(list)
	local first = list[1]
	local newLength = #list - 1

	for i = 1, newLength do
		list[i] = list[i + 1]
	end

	-- We need to explicitly nil out the end of the list
	list[newLength + 1] = nil

	return first
end

beforeEach(function()
	jest.resetModules()
	-- deviation: In react, jest mocks Scheduler -> unstable_mock; since
	-- unstable_mock depends on the real Scheduler, and our mock
	-- functionality isn't smart enough to prevent self-requires, we simply
	-- require the mock entry point directly for use in tests
	Scheduler = require("../unstable_mock")

	runWithPriority = Scheduler.unstable_runWithPriority
	ImmediatePriority = Scheduler.unstable_ImmediatePriority
	UserBlockingPriority = Scheduler.unstable_UserBlockingPriority
	NormalPriority = Scheduler.unstable_NormalPriority
	-- deviation: These are only used in a commented-out _G.__DEV__-only
	-- test (commented out to silence lints)
	-- LowPriority = Scheduler.unstable_LowPriority
	-- IdlePriority = Scheduler.unstable_IdlePriority
	scheduleCallback = Scheduler.unstable_scheduleCallback
	cancelCallback = Scheduler.unstable_cancelCallback
	wrapCallback = Scheduler.unstable_wrapCallback
	getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel
	shouldYield = Scheduler.unstable_shouldYield
end)

it("flushes work incrementally", function()
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("A")
	end)
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("B")
	end)
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("C")
	end)
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("D")
	end)

	jestExpect(Scheduler).toFlushAndYieldThrough({ "A", "B" })
	jestExpect(Scheduler).toFlushAndYieldThrough({ "C" })
	jestExpect(Scheduler).toFlushAndYield({ "D" })
end)

it("cancels work", function()
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("A")
	end)
	local callbackHandleB = scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("B")
	end)
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("C")
	end)

	cancelCallback(callbackHandleB)

	jestExpect(Scheduler).toFlushAndYield({
		"A",
		-- B should have been cancelled
		"C",
	})
end)

it("executes the highest priority callbacks first", function()
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("A")
	end)
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("B")
	end)

	-- Yield before B is flushed
	jestExpect(Scheduler).toFlushAndYieldThrough({ "A" })

	scheduleCallback(UserBlockingPriority, function()
		Scheduler.unstable_yieldValue("C")
	end)
	scheduleCallback(UserBlockingPriority, function()
		Scheduler.unstable_yieldValue("D")
	end)

	-- C and D should come first, because they are higher priority
	jestExpect(Scheduler).toFlushAndYield({ "C", "D", "B" })
end)

it("expires work", function()
	scheduleCallback(NormalPriority, function(didTimeout)
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue(
			string.format("A (did timeout: %s)", tostring(didTimeout))
		)
	end)
	scheduleCallback(UserBlockingPriority, function(didTimeout)
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue(
			string.format("B (did timeout: %s)", tostring(didTimeout))
		)
	end)
	scheduleCallback(UserBlockingPriority, function(didTimeout)
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue(
			string.format("C (did timeout: %s)", tostring(didTimeout))
		)
	end)

	-- Advance time, but not by enough to expire any work
	Scheduler.unstable_advanceTime(249)
	jestExpect(Scheduler).toHaveYielded({})

	-- Schedule a few more callbacks
	scheduleCallback(NormalPriority, function(didTimeout)
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue(
			string.format("D (did timeout: %s)", tostring(didTimeout))
		)
	end)
	scheduleCallback(NormalPriority, function(didTimeout)
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue(
			string.format("E (did timeout: %s)", tostring(didTimeout))
		)
	end)

	-- Advance by just a bit more to expire the user blocking callbacks
	Scheduler.unstable_advanceTime(1)
	jestExpect(Scheduler).toFlushExpired({
		"B (did timeout: true)",
		"C (did timeout: true)",
	})

	-- Expire A
	Scheduler.unstable_advanceTime(4600)
	jestExpect(Scheduler).toFlushExpired({ "A (did timeout: true)" })

	-- Flush the rest without expiring
	jestExpect(Scheduler).toFlushAndYield({
		"D (did timeout: false)",
		"E (did timeout: true)",
	})
end)

it("has a default expiration of ~5 seconds", function()
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("A")
	end)

	Scheduler.unstable_advanceTime(4999)
	jestExpect(Scheduler).toHaveYielded({})

	Scheduler.unstable_advanceTime(1)
	jestExpect(Scheduler).toFlushExpired({ "A" })
end)

it("continues working on same task after yielding", function()
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue("A")
	end)
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue("B")
	end)

	local didYield = false
	local tasks = {
		{ "C1", 100 },
		{ "C2", 100 },
		{ "C3", 100 },
	}
	local function C()
		while #tasks > 0 do
			local label, ms = unpack(shift(tasks))
			Scheduler.unstable_advanceTime(ms)
			Scheduler.unstable_yieldValue(label)
			if shouldYield() then
				didYield = true
				return C
			end
		end

		return nil
	end

	scheduleCallback(NormalPriority, C)

	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue("D")
	end)
	scheduleCallback(NormalPriority, function()
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue("E")
	end)

	-- Flush, then yield while in the middle of C.
	jestExpect(didYield).toBe(false)
	jestExpect(Scheduler).toFlushAndYieldThrough({ "A", "B", "C1" })
	jestExpect(didYield).toBe(true)

	-- When we resume, we should continue working on C.
	jestExpect(Scheduler).toFlushAndYield({ "C2", "C3", "D", "E" })
end)

it("continuation callbacks inherit the expiration of the previous callback", function()
	local tasks = {
		{ "A", 125 },
		{ "B", 124 },
		{ "C", 100 },
		{ "D", 100 },
	}
	local function work()
		while #tasks > 0 do
			local label, ms = unpack(shift(tasks))
			Scheduler.unstable_advanceTime(ms)
			Scheduler.unstable_yieldValue(label)
			if shouldYield() then
				return work
			end
		end

		return nil
	end

	-- Schedule a high priority callback
	scheduleCallback(UserBlockingPriority, work)

	-- Flush until just before the expiration time
	jestExpect(Scheduler).toFlushAndYieldThrough({ "A", "B" })

	-- Advance time by just a bit more. This should expire all the remaining work.
	Scheduler.unstable_advanceTime(1)
	jestExpect(Scheduler).toFlushExpired({ "C", "D" })
end)

it("continuations are interrupted by higher priority work", function()
	local tasks = {
		{ "A", 100 },
		{ "B", 100 },
		{ "C", 100 },
		{ "D", 100 },
	}
	local function work()
		while #tasks > 0 do
			local label, ms = unpack(shift(tasks))
			Scheduler.unstable_advanceTime(ms)
			Scheduler.unstable_yieldValue(label)
			if #tasks > 0 and shouldYield() then
				return work
			end
		end

		return nil
	end
	scheduleCallback(NormalPriority, work)
	jestExpect(Scheduler).toFlushAndYieldThrough({ "A" })

	scheduleCallback(UserBlockingPriority, function()
		Scheduler.unstable_advanceTime(100)
		Scheduler.unstable_yieldValue("High pri")
	end)

	jestExpect(Scheduler).toFlushAndYield({ "High pri", "B", "C", "D" })
end)

it(
	"continuations do not block higher priority work scheduled "
		.. "inside an executing callback",
	function()
		local tasks = {
			{ "A", 100 },
			{ "B", 100 },
			{ "C", 100 },
			{ "D", 100 },
		}
		local function work()
			while #tasks > 0 do
				local task = shift(tasks)
				local label, ms = unpack(task)
				Scheduler.unstable_advanceTime(ms)
				Scheduler.unstable_yieldValue(label)
				if label == "B" then
					-- Schedule high pri work from inside another callback
					Scheduler.unstable_yieldValue("Schedule high pri")
					scheduleCallback(UserBlockingPriority, function()
						Scheduler.unstable_advanceTime(100)
						Scheduler.unstable_yieldValue("High pri")
					end)
				end
				if #tasks > 0 then
					-- Return a continuation
					return work
				end
			end

			return nil
		end
		scheduleCallback(NormalPriority, work)
		jestExpect(Scheduler).toFlushAndYield({
			"A",
			"B",
			"Schedule high pri",
			-- The high pri callback should fire before the continuation of the
			-- lower pri work
			"High pri",
			-- Continue low pri work
			"C",
			"D",
		})
	end
)

it("cancelling a continuation", function()
	local task = scheduleCallback(NormalPriority, function()
		Scheduler.unstable_yieldValue("Yield")
		return function()
			Scheduler.unstable_yieldValue("Continuation")
		end
	end)

	jestExpect(Scheduler).toFlushAndYieldThrough({ "Yield" })
	cancelCallback(task)
	jestExpect(Scheduler).toFlushWithoutYielding()
end)

it("top-level immediate callbacks fire in a subsequent task", function()
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("A")
	end)
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("B")
	end)
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("C")
	end)
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("D")
	end)
	-- Immediate callback hasn't fired, yet.
	jestExpect(Scheduler).toHaveYielded({})
	-- They all flush immediately within the subsequent task.
	jestExpect(Scheduler).toFlushExpired({ "A", "B", "C", "D" })
end)

it("nested immediate callbacks are added to the queue of immediate callbacks", function()
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("A")
	end)
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("B")
		-- This callback should go to the end of the queue
		scheduleCallback(ImmediatePriority, function()
			Scheduler.unstable_yieldValue("C")
		end)
	end)
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("D")
	end)
	jestExpect(Scheduler).toHaveYielded({})
	-- C should flush at the end
	jestExpect(Scheduler).toFlushExpired({ "A", "B", "D", "C" })
end)

it("wrapped callbacks have same signature as original callback", function()
	local wrappedCallback = wrapCallback(function(...)
		return {
			args = { ... },
		}
	end)
	local result = wrappedCallback("a", "b")
	jestExpect(#result.args).toBe(2)
	jestExpect(result.args).toEqual({ "a", "b" })
end)

it("wrapped callbacks inherit the current priority", function()
	local wrappedCallback = runWithPriority(NormalPriority, function()
		return wrapCallback(function()
			Scheduler.unstable_yieldValue(getCurrentPriorityLevel())
		end)
	end)

	local wrappedUserBlockingCallback = runWithPriority(UserBlockingPriority, function()
		return wrapCallback(function()
			Scheduler.unstable_yieldValue(getCurrentPriorityLevel())
		end)
	end)

	wrappedCallback()
	jestExpect(Scheduler).toHaveYielded({ NormalPriority })

	wrappedUserBlockingCallback()
	jestExpect(Scheduler).toHaveYielded({ UserBlockingPriority })
end)

it("wrapped callbacks inherit the current priority even when nested", function()
	local wrappedCallback
	local wrappedUserBlockingCallback

	runWithPriority(NormalPriority, function()
		wrappedCallback = wrapCallback(function()
			Scheduler.unstable_yieldValue(getCurrentPriorityLevel())
		end)
		wrappedUserBlockingCallback = runWithPriority(UserBlockingPriority, function()
			return wrapCallback(function()
				Scheduler.unstable_yieldValue(getCurrentPriorityLevel())
			end)
		end)
	end)

	wrappedCallback()
	jestExpect(Scheduler).toHaveYielded({ NormalPriority })

	wrappedUserBlockingCallback()
	jestExpect(Scheduler).toHaveYielded({ UserBlockingPriority })
end)

it("immediate callbacks fire even if there's an error", function()
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("A")
		error("Oops A")
	end)
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("B")
	end)
	scheduleCallback(ImmediatePriority, function()
		Scheduler.unstable_yieldValue("C")
		error(Error.new("Oops C"))
	end)

	jestExpect(function()
		jestExpect(Scheduler).toFlushExpired()
	end).toThrow("Oops A")
	jestExpect(Scheduler).toHaveYielded({ "A" })

	-- B and C flush in a subsequent event. That way, the second error is not
	-- swallowed.
	jestExpect(function()
		jestExpect(Scheduler).toFlushExpired()
	end).toThrow("Oops C")
	jestExpect(Scheduler).toHaveYielded({ "B", "C" })
end)

it(
	"multiple immediate callbacks can throw and there will be an error for each one",
	function()
		scheduleCallback(ImmediatePriority, function()
			error("First error")
		end)
		scheduleCallback(ImmediatePriority, function()
			error("Second error")
		end)
		jestExpect(function()
			Scheduler.unstable_flushAll()
		end).toThrow("First error")
		-- The next error is thrown in the subsequent event
		jestExpect(function()
			Scheduler.unstable_flushAll()
		end).toThrow("Second error")
	end
)

it("exposes the current priority level", function()
	Scheduler.unstable_yieldValue(getCurrentPriorityLevel())
	runWithPriority(ImmediatePriority, function()
		Scheduler.unstable_yieldValue(getCurrentPriorityLevel())
		runWithPriority(NormalPriority, function()
			Scheduler.unstable_yieldValue(getCurrentPriorityLevel())
			runWithPriority(UserBlockingPriority, function()
				Scheduler.unstable_yieldValue(getCurrentPriorityLevel())
			end)
		end)
		Scheduler.unstable_yieldValue(getCurrentPriorityLevel())
	end)

	jestExpect(Scheduler).toHaveYielded({
		NormalPriority,
		ImmediatePriority,
		NormalPriority,
		UserBlockingPriority,
		ImmediatePriority,
	})
end)

-- if _G.__DEV__ then
-- ROBLOX TODO(align): Re-enable this test if it's useful
--
-- Function names are minified in prod, though you could still infer the
-- priority if you have sourcemaps.
-- TODO: Feature temporarily disabled while we investigate a bug in one of
-- our minifiers.
-- it.skip('adds extra function to the JS stack whose name includes the priority level', function()
-- 	function inferPriorityFromCallstack()
-- 		try {
-- 			throw Error()
-- 		} catch (e) {
-- 			local stack = e.stack
-- 			local lines = stack.split('\n')
-- 			for (local i = lines.length - 1 i >= 0 i--) {
-- 				local line = lines[i]
-- 				local found = line.match(
-- 					/scheduler_flushTaskAtPriority_({A-Za-z]+)/,
-- 				)
-- 				if (found !== null) {
-- 					local priorityStr = found[1]
-- 					switch (priorityStr) {
-- 						case 'Immediate':
-- 							return ImmediatePriority
-- 						case 'UserBlocking':
-- 							return UserBlockingPriority
-- 						case 'Normal':
-- 							return NormalPriority
-- 						case 'Low':
-- 							return LowPriority
-- 						case 'Idle':
-- 							return IdlePriority
-- 					}
-- 				}
-- 			}
-- 			return null
-- 		}
-- 	end

-- 	scheduleCallback(ImmediatePriority, () =>
-- 		Scheduler.unstable_yieldValue(
-- 			'Immediate: ' + inferPriorityFromCallstack(),
-- 		),
-- 	)
-- 	scheduleCallback(UserBlockingPriority, () =>
-- 		Scheduler.unstable_yieldValue(
-- 			'UserBlocking: ' + inferPriorityFromCallstack(),
-- 		),
-- 	)
-- 	scheduleCallback(NormalPriority, () =>
-- 		Scheduler.unstable_yieldValue(
-- 			'Normal: ' + inferPriorityFromCallstack(),
-- 		),
-- 	)
-- 	scheduleCallback(LowPriority, () =>
-- 		Scheduler.unstable_yieldValue('Low: ' + inferPriorityFromCallstack()),
-- 	)
-- 	scheduleCallback(IdlePriority, () =>
-- 		Scheduler.unstable_yieldValue('Idle: ' + inferPriorityFromCallstack()),
-- 	)

-- 	jestExpect(Scheduler).toFlushAndYield({
-- 		'Immediate: ' + ImmediatePriority,
-- 		'UserBlocking: ' + UserBlockingPriority,
-- 		'Normal: ' + NormalPriority,
-- 		'Low: ' + LowPriority,
-- 		'Idle: ' + IdlePriority,
-- 	})
-- end)
-- end

describe("delayed tasks", function()
	it("schedules a delayed task", function()
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("A")
		end, {
			delay = 1000,
		})

		-- Should flush nothing, because delay hasn't elapsed
		jestExpect(Scheduler).toFlushAndYield({})

		-- Advance time until right before the threshold
		Scheduler.unstable_advanceTime(999)
		-- Still nothing
		jestExpect(Scheduler).toFlushAndYield({})

		-- Advance time past the threshold
		Scheduler.unstable_advanceTime(1)

		-- Now it should flush like normal
		jestExpect(Scheduler).toFlushAndYield({ "A" })
	end)

	it("schedules multiple delayed tasks", function()
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("C")
		end, {
			delay = 300,
		})

		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("B")
		end, {
			delay = 200,
		})

		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("D")
		end, {
			delay = 400,
		})

		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("A")
		end, {
			delay = 100,
		})

		-- Should flush nothing, because delay hasn't elapsed
		jestExpect(Scheduler).toFlushAndYield({})

		-- Advance some time.
		Scheduler.unstable_advanceTime(200)
		-- Both A and B are no longer delayed. They can now flush incrementally.
		jestExpect(Scheduler).toFlushAndYieldThrough({ "A" })
		jestExpect(Scheduler).toFlushAndYield({ "B" })

		-- Advance the rest
		Scheduler.unstable_advanceTime(200)
		jestExpect(Scheduler).toFlushAndYield({ "C", "D" })
	end)

	it("interleaves normal tasks and delayed tasks", function()
		-- Schedule some high priority callbacks with a delay. When their delay
		-- elapses, they will be the most important callback in the queue.
		scheduleCallback(UserBlockingPriority, function()
			Scheduler.unstable_yieldValue("Timer 2")
		end, {
			delay = 300,
		})
		scheduleCallback(UserBlockingPriority, function()
			Scheduler.unstable_yieldValue("Timer 1")
		end, {
			delay = 100,
		})

		-- Schedule some tasks at default priority.
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("A")
			Scheduler.unstable_advanceTime(100)
		end)
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("B")
			Scheduler.unstable_advanceTime(100)
		end)
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("C")
			Scheduler.unstable_advanceTime(100)
		end)
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("D")
			Scheduler.unstable_advanceTime(100)
		end)

		-- Flush all the work. The timers should be interleaved with the
		-- other tasks.
		jestExpect(Scheduler).toFlushAndYield({
			"A",
			"Timer 1",
			"B",
			"C",
			"Timer 2",
			"D",
		})
	end)

	it("interleaves delayed tasks with time-sliced tasks", function()
		-- Schedule some high priority callbacks with a delay. When their delay
		-- elapses, they will be the most important callback in the queue.
		scheduleCallback(UserBlockingPriority, function()
			Scheduler.unstable_yieldValue("Timer 2")
		end, {
			delay = 300,
		})
		scheduleCallback(UserBlockingPriority, function()
			Scheduler.unstable_yieldValue("Timer 1")
		end, {
			delay = 100,
		})

		-- Schedule a time-sliced task at default priority.
		local tasks = {
			{ "A", 100 },
			{ "B", 100 },
			{ "C", 100 },
			{ "D", 100 },
		}
		local function work()
			while #tasks > 0 do
				local task = shift(tasks)
				local label, ms = unpack(task)
				Scheduler.unstable_advanceTime(ms)
				Scheduler.unstable_yieldValue(label)
				if #tasks > 0 then
					return work
				end
			end

			return nil
		end
		scheduleCallback(NormalPriority, work)

		-- Flush all the work. The timers should be interleaved with the
		-- other tasks.
		jestExpect(Scheduler).toFlushAndYield({
			"A",
			"Timer 1",
			"B",
			"C",
			"Timer 2",
			"D",
		})
	end)

	it("cancels a delayed task", function()
		-- Schedule several tasks with the same delay
		local options = {
			delay = 100,
		}

		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("A")
		end, options)
		local taskB = scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("B")
		end, options)
		local taskC = scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("C")
		end, options)

		-- Cancel B before its delay has elapsed
		jestExpect(Scheduler).toFlushAndYield({})
		cancelCallback(taskB)

		-- Cancel C after its delay has elapsed
		Scheduler.unstable_advanceTime(500)
		cancelCallback(taskC)

		-- Only A should flush
		jestExpect(Scheduler).toFlushAndYield({ "A" })
	end)

	it("gracefully handles scheduled tasks that are not a function", function()
		scheduleCallback(ImmediatePriority)
		jestExpect(Scheduler).toFlushWithoutYielding()

		scheduleCallback(ImmediatePriority, {})
		jestExpect(Scheduler).toFlushWithoutYielding()

		scheduleCallback(ImmediatePriority, 42)
		jestExpect(Scheduler).toFlushWithoutYielding()
	end)

	it("delayed tasks stringify their error", function()
		scheduleCallback(NormalPriority, function()
			Scheduler.unstable_yieldValue("A")
			error(Error.new("Oops A"))
		end, {
			delay = 100,
		})

		Scheduler.unstable_advanceTime(100)
		jestExpect(Scheduler).toFlushAndThrow("Oops A")
	end)
end)
