What documentation says
The difference between toBe() and toEqual() assertions is not very intuitive, so let's quickly glance at what Playwright documentation says:
toBe() - Compares value with expected by calling Object.is. This method compares objects by reference instead of their contents, similarly to the strict equality operator ===
toEqual() - Compares contents of the value with contents of expected, performing "deep equality" check. For objects, this method recursively checks equality of all fields, rather than comparing objects by reference as performed by expect(value).toBe(). For primitive values, this method is equivalent to expect(value).toBe().
Unfortunately, not very clear. Let's dive deeper using examples
Assertion of primitives
Primitive values are the simplest elements available in a programming language. A primitive is the smallest unit of processing available to a programmer of a given machine. In JavaScript, those values are: number, string, bigint, boolean, undefined, symbol, and null.
So let's write a simple code using primitives and make an assertion:
test('assert primitive', () => { const sizeA = 5 const sizeB = 5 expect(sizeA).toEqual(sizeB) expect(sizeA).toBe(sizeB)})
In this example, the test will pass. toBe() and toEqual() work the same, because 5 is a primitive value. So when you assert primitives, it does not matter which assertion to use.
Assertion of objects
Now let's look at the second example, and instead of asserting the primitives, we assert two simple objects, which look the same:
test('assert objects', () => { const houseA = {size: 5} const houseB = {size: 5} expect(houseA).toEqual(houseB) //THIS ONE PASS expect(houseA).toBe(houseB) //THIS ONE FAILS})
In this example, we have two constants, houseA and houseB, which are assigned the same values as the objects.
toEqual() assertion passes because it performs "deep equality" of the object content. In other words, it compares the exact values of two objects. Since the values are the same, the assertion passes.
toBe() assertion fails because it does not look at the content, it looks at the reference to the object! And if a reference is different, the assertion will fail. In our example, we have two independent objects, assigned to individual constants. houseA refers to one object, houseB refers to another object. They refer to different objects, so not the same.
And just to make it even clearer, here is an example:
test('assert objects', () => { const houseA = {size: 5} const houseB = houseA expect(houseA).toEqual(houseB) expect(houseA).toBe(houseB)})
In this example, houseB constant refers to houseA, and houseA refers to the object. So houseA and houseB refer to the same object, so both assertions toBe() and toEqual() will pass as well.
Summary
In most cases, toEqual() is all you need to perform assertions of deep data equality. But if you need to dig deeper and validate that not just the content, but the reference to the object is the same, then the toBe() assertion has to be used. For primitive data types, it does not matter which assertion you use; it will work the same.
Playwright best practice is to use Locator Assertions instead of utilizing toBe() or toEqual() which are related to Generic Assertions. Check more about the difference between the assertions in this blog post.
In my Playwright UI Testing Mastery program, I have a separate lecture that shows in detail the two types of Playwright assertions: Generic assertions and Locator assertions. How and when to use them correctly.

