-
Notifications
You must be signed in to change notification settings - Fork 18
Description
toStrictEqual fails for any comparison involving string values. typeEquality calls getmetatable() on primitives, and Roblox Luau strings have a locked metatable — getmetatable("hello") returns "The metatable is locked" instead of nil. This causes typeEquality to short-circuit with false before Object.is is consulted, so two identical strings are treated as unequal.
These fail in jest-roblox but pass in Jest:
-- Fails: "serializes to the same string"
expect({ value = "hello" }).toStrictEqual({ value = "hello" })
-- Works
expect({ value = "hello" }).toEqual({ value = "hello" })Any toStrictEqual comparison that recurses into string (or other locked-metatable primitive) leaf values will hit this.
Cause
typeEquality in expect/src/utils.lua runs as a custom tester on every recursive pair of values, including leaf primitives:
local function typeEquality(a, b)
if a == nil or b == nil then
return nil
end
if typeof(a) ~= typeof(b) then
return false
end
-- Path A: both no metatable -> pass through
if not getmetatable(a) and not getmetatable(b) then
return nil
end
-- Path B: both have metatables with matching __index -> pass through
if
getmetatable(a)
and getmetatable(b)
and getmetatable(a).__index
and getmetatable(b).__index
and getmetatable(a).__index == getmetatable(b).__index
then
return nil
end
-- Path C: FAIL
return false
endWhen a and b are both strings (e.g. "hello"):
- Path A is skipped:
getmetatable("hello")returns"The metatable is locked"(truthy), sonot getmetatable(a)isfalse. - Path B fails:
getmetatable("hello").__indexindexes a string with"__index", which resolves through the string library tonil. Condition fails. - Path C is reached:
return false. Two identical strings declared unequal.
Custom testers run before Object.is in eq(), so the correct comparison never happens:
for _, value in ipairs(customTesters) do
local customTesterResult = value(a, b)
if customTesterResult ~= nil then
return customTesterResult -- returns false for strings
end
end
-- never reached
if Object.is(a, b) then
return true
endObject.is("hello", "hello") would return true, but it's never called.
Proposed fix
Skip non-table metatables in typeEquality:
local function typeEquality(a, b)
if a == nil or b == nil then
return nil
end
if typeof(a) ~= typeof(b) then
return false
end
local metaA = getmetatable(a)
local metaB = getmetatable(b)
-- Skip non-table metatables (e.g. "The metatable is locked" on strings)
if typeof(metaA) ~= "table" and typeof(metaB) ~= "table" then
return nil
end
if not metaA and not metaB then
return nil
end
if
metaA
and metaB
and typeof(metaA) == "table"
and typeof(metaB) == "table"
and metaA.__index
and metaB.__index
and metaA.__index == metaB.__index
then
return nil
end
return false
endOr just bail early on non-tables:
if typeof(a) ~= "table" then
return nil
endRepro
local JestGlobals = require(Packages.Dev.JestGlobals)
local describe = JestGlobals.describe
local expect = JestGlobals.expect
local it = JestGlobals.it
describe("toStrictEqual string bug", function()
it("should pass for identical strings", function()
expect("hello").toStrictEqual("hello")
end)
it("should pass for tables with string values", function()
expect({ value = "hello" }).toStrictEqual({ value = "hello" })
end)
it("should pass for arrays of objects with strings", function()
expect({
{ name = "alice" },
{ name = "bob" },
}).toStrictEqual({
{ name = "alice" },
{ name = "bob" },
})
end)
end)Expected
All 3 tests pass.
Actual
All 3 tests fail with "serializes to the same string".