1

Using: Node, Prisma and Jest.

I have a value that is acting like a Javascript date in everyway other than the instanceof Date check.

Here is the code that I've used to check if it's a date.

const deletedUser = await app.prisma.user.findUnique({
  where: { id: 1 },
});

console.log(deletedUser);
console.log(typeof deletedUser.deleted_at); // object
console.log(deletedUser.deleted_at.constructor); // [Function: Date]
console.log(deletedUser.deleted_at.constructor.name); // Date
console.log(deletedUser.deleted_at instanceof Date); // false
console.log(deletedUser.deleted_at); // 2022-08-15T21:50:34.344Z
console.log(Object.prototype.toString.call(deletedUser.deleted_at) === '[object Date]'); // true
console.log(Object.prototype.toString.call(deletedUser.deleted_at)); // [object Date]
console.log(new Date(deletedUser.deleted_at)); // 2022-08-15T21:50:34.344Z
console.log(deletedUser.deleted_at.prototype); // undefined
Object.keys(deletedUser.deleted_at).forEach(prop => console.log(prop))
console.log(Object.keys(deletedUser.deleted_at)); // []
console.log(deletedUser.deleted_at.__proto__); // {}
console.log(isNaN(deletedUser.deleted_at)); // false
console.log(deletedUser.deleted_at.valueOf()); // Mon Aug 15 2022 21:56:54 GMT+0000 (Coordinated Universal Time)
console.log(Date(deletedUser.deleted_at.constructor.prototype));

I've looked at the following resources to research this question.

  1. How to check whether an object is a date?
  2. https://jestjs.io/docs/expect#expectobjectcontainingobject
  3. Get the name of an object's type
  4. Detecting an "invalid date" Date instance in JavaScript
  5. Why are myarray instanceof Array and myarray.constructor === Array both false when myarray is in a frame?
  6. http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
  7. https://groups.google.com/g/comp.lang.javascript/c/XTWYCOwC96I/m/70rNoQ3L-xoJ
  8. How to know string value is an instance of Date in Javascript?

Using these resources I used the above checks to inspect whether the deleted_at is a Date and they all pass.

This ans. https://stackoverflow.com/a/643827/9530790 from the first question above mentions that...

you can use the instanceof operator, i.e. But it will return true for invalid dates too, e.g. new Date('random_string') is also instance of Date

date instanceof Date

This will fail if objects are passed across frame boundaries.

A work-around for this is to check the object's class via

Object.prototype.toString.call(date) === '[object Date]'

My understanding is that frames have to do with iframes and separate windows on a browser. I'm using node, so I'm not sure if that has to do with separate frames. Also as mentioned in the ans. if it is separate frames then Object.prototype.toString.call(date) === '[object Date]' should be false but by my case it's true.

In jest when I test like the following...

expect(deletedUser).toMatchObject(
  expect.objectContaining({
    deleted_by_id: expect.any(Number),
    deleted_at: expect.any(Date),
  }),
);

The expect.any(Date) call fails. It's possible that under the hood jest calls instanceOf. I couldn't find this mentioned in the jest docs.

What's interesting to note is that when I set deleted_at before doing the expect call above, lke this deletedUser.deleted_at = new Date` then the test passes. It's just that when it comes out of the database from prisma it fails.

Jest prints out that the test failed with deleted_at looking like this "deleted_at": Date {}. While when I set deleted_at before to new Date then it passes and prints like this "deleted_at": 2022-08-15T21:56:54.402Z. However when it fails and jest prints Date {}, if I log it with console.log it prints the date normaly "deleted_at": 2022-08-15T21:56:54.402Z

I recently changed around the setup of the tests and I imagine that has to do with it. But who is causing this failure? And what is the reason? Who is causing instanceof Date to be false and why?

What I changed is that in my jest.config.js I create a global app to use in the tests and I attach the prisma client so I can get it via app.prisma

const app = require('./tests/app');
const prisma = require('./prisma/client');
const request = require('supertest');
app.prisma = prisma;
app.testRequest = request(app);

module.exports = {
  testEnvironment: 'node',
  globals: {
    app,
  },
};

I did this so I only have to use one app across test suites this greatly speeds up our tests, cutting down the time from about 130s to 40s. I can come up with a workaround to get this test to pass but I'm concerned that this may be indicative of a more significant issue that I've stumbled upon.

I'm wondering if node, prisma or jest is creating a separate context between one Date constructor and another. Similar to passing objects across frame boundaries. However, I can't confirm this and as mentioned above then the Object.prototype.toString.call(date) === '[object Date]' check should be false.

Update

I've noticed that a similar issue is occurring regarding a function that we're attaching to the Array.prototype property. When trying to access it on the test it doesn't exist.

From this behavior, I'm leaning toward the idea that jest is creating some sort of new context before each test, and things that get added in jest.config.js won't necessarily exist during each test.

The jest docs https://jestjs.io/docs/configuration#globals-object mention something about data not persisting between tests when using globals. Even though some things are persisted, it's not clear what is and what isn't and it can't be relied upon.

shmuels
  • 1,039
  • 1
  • 9
  • 22

2 Answers2

1

It's possible that Prisma is using a different date class that behaves identically to the native Date class, like this:

const RealDate = Date;

const fakeDateToRealDate = new WeakMap();

// The name of this function is important since this is the
// constructor and the name can be accessed using `.constructor.name`.
const FakeDate = function Date(...args) {
  fakeDateToRealDate.set(this, new RealDate(...args));
}

// For Object.prototype.toString()
FakeDate.prototype[Symbol.toStringTag] = "Date";

function defineMethod(f) {
  Object.defineProperty(FakeDate.prototype, f.name, {
    configurable: true,
    enumberable: false,
    writable: true,
    value: f
  });
}
defineMethod(function toString(...args) {
  return fakeDateToRealDate.get(this).toString(...args);
});
defineMethod(function getTime(...args) {
  return fakeDateToRealDate.get(this).getTime(...args);
});
defineMethod(function setTime(...args) {
  return fakeDateToRealDate.get(this).setTime(...args);
});
// etc.


const fakeDate = new FakeDate("2022-08-15T21:50:34.344Z");

console.log(typeof fakeDate); // object
console.log(fakeDate.constructor); // [Function: Date]
console.log(fakeDate.constructor.name); // Date
console.log(fakeDate instanceof Date); // false
console.log(fakeDate); // 2022-08-15T21:50:34.344Z
console.log(Object.prototype.toString.call(fakeDate) === '[object Date]'); // true
console.log(Object.prototype.toString.call(fakeDate)); // [object Date]
console.log(new Date(fakeDate)); // 2022-08-15T21:50:34.344Z
console.log(fakeDate.prototype); // undefined
Object.keys(fakeDate).forEach(prop => console.log(prop))
console.log(Object.keys(fakeDate)); // []
console.log(fakeDate.__proto__); // {}
console.log(isNaN(fakeDate)); // false
console.log(fakeDate.valueOf()); // Mon Aug 15 2022 21:56:54 GMT+0000 (Coordinated Universal Time

It's also possible that Prisma is using the vm module, which is similar to code running in a different frame, like this:

const vm = require("vm");

const ctx = {};
vm.createContext(ctx);
const fakeDate = vm.runInContext('new Date("2022-08-15T21:50:34.344Z");', ctx);

console.log(typeof fakeDate); // object
console.log(fakeDate.constructor); // [Function: Date]
console.log(fakeDate.constructor.name); // Date
console.log(fakeDate instanceof Date); // false
console.log(fakeDate); // 2022-08-15T21:50:34.344Z
console.log(Object.prototype.toString.call(fakeDate) === '[object Date]'); // true
console.log(Object.prototype.toString.call(fakeDate)); // [object Date]
console.log(new Date(fakeDate)); // 2022-08-15T21:50:34.344Z
console.log(fakeDate.prototype); // undefined
Object.keys(fakeDate).forEach(prop => console.log(prop))
console.log(Object.keys(fakeDate)); // []
console.log(fakeDate.__proto__); // {}
console.log(isNaN(fakeDate)); // false
console.log(fakeDate.valueOf()); // Mon Aug 15 2022 21:56:54 GMT+0000 (Coordinated Universal Time

Unfortunately I can't confirm exactly what's happening since I didn't find any of those two things in Prisma's code, though it's likely that I searched in the wrong place.

D. Pardal
  • 6,173
  • 1
  • 17
  • 37
1

I don't know app.prisma is coming from, but single context environment might help.

@quramy/jest-prisma explains about similar problem and how to solve it.

musou1500
  • 51
  • 9