Skip to content

Commit

Permalink
foobar
Browse files Browse the repository at this point in the history
  • Loading branch information
cjihrig committed Apr 16, 2022
1 parent adaf602 commit e484301
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 0 deletions.
171 changes: 171 additions & 0 deletions lib/internal/test_runner/mock.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
'use strict';

function noop() {}

class MockFunctionContext {
#_calls;
#_mocks;
#_implementation;
#_restore;
#_tracker;

constructor(tracker, implementation, restore) {
this.#_calls = [];
this.#_mocks = new Map();
this.#_implementation = implementation;
this.#_restore = restore;
this.#_tracker = tracker;
}

get calls() {
return this.#_calls.slice(0);
}

mockImplementation(fn) {
this.#_implementation = fn;
}

mockImplementationOnce(fn, onCall) {
const nextCall = this.#_calls.length;
const call = onCall ?? nextCall;

if (call < nextCall) {
// The call number has already passed.
return;
}

this.#_mocks.set(call, fn);
}

nextMock() {
const nextCall = this.#_calls.length;
const mock = this.#_mocks.get(nextCall);

this.#_mocks.delete(nextCall);
return mock;
}

get implementation() {
return this.#_implementation;
}

trackCall(call) {
this.#_calls.push(call);
}

restore() {
if (this.#_restore === null) {
// This is a bare function mock.
return;
}

// This is an object method mock.
const { descriptor, object, methodName } = this.#_restore;

Object.defineProperty(object, methodName, descriptor);
}
}

const { nextMock, trackCall } = MockFunctionContext.prototype;
delete MockFunctionContext.prototype.trackCall;
delete MockFunctionContext.prototype.nextMock;

class MockTracker {
constructor() {

}

fn(implementation) {
if (implementation === undefined) {
implementation = noop;
}

// TODO(cjihrig): Assert types

const ctx = new MockFunctionContext(this, implementation, null);

return this.#setupMock(ctx, implementation);
}

method(object, methodName, implementation, options) {
// TODO(cjihrig): Assert inputs.
// TODO(cjihrig): Assert this is really a method.
// TODO(cjihrig): Throw if this is already a mock.

const descriptor = Object.getOwnPropertyDescriptor(object, methodName);
let original;

if (options?.getter) {
original = descriptor.get;
} else if (options?.setter) {
original = descriptor.set;
} else {
original = descriptor.value;
}

const restore = { descriptor, object, methodName };
const ctx = new MockFunctionContext(this, implementation, restore);
const mock = this.#setupMock(ctx, original);
const mockDescriptor = {
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
};

if (options?.getter) {
mockDescriptor.get = mock;
mockDescriptor.set = descriptor.set;
} else if (options?.setter) {
mockDescriptor.get = descriptor.get;
mockDescriptor.set = mock;
} else {
mockDescriptor.writable = descriptor.writable;
mockDescriptor.value = mock;
}

Object.defineProperty(object, methodName, mockDescriptor);

return mock;
}

#setupMock(ctx, fnToMatch) {
const mock = function(...args) {
const onceMock = nextMock.call(ctx);
const fn = onceMock ?? ctx.implementation;
const result = new.target ? new fn(...args) : fn.call(this, ...args);

trackCall.call(ctx, {
arguments: args,
result,
stack: new Error(),
target: new.target,
this: this,
});

return result;
};

Object.defineProperty(mock, 'mock', {
get() {
return ctx;
}
});

Object.defineProperty(mock, 'name', {
value: fnToMatch.name,
writable: false,
enumerable: false,
configurable: true,
});

Object.defineProperty(mock, 'length', {
value: fnToMatch.length,
writable: false,
enumerable: false,
configurable: true,
});

return mock;
}
}

module.exports = { MockTracker };
2 changes: 2 additions & 0 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const {
kIsNodeError,
} = require('internal/errors');
const { getOptionValue } = require('internal/options');
const { MockTracker } = require('internal/test_runner/mock');
const { TapStream } = require('internal/test_runner/tap_stream');
const { createDeferredPromise } = require('internal/util');
const { isPromise } = require('internal/util/types');
Expand All @@ -39,6 +40,7 @@ class TestContext {

constructor(test) {
this.#test = test;
this.mock = new MockTracker();
}

diagnostic(message) {
Expand Down

0 comments on commit e484301

Please sign in to comment.