| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340 |
- /**
- * Вспомогательная функция для разбора аргументов createSpy.
- *
- * @private
- */
- function _parseSpyArgs(
- target,
- methodNameOrImplFromSpy,
- customImplForMethodFromSpy,
- ) {
- // объявление переменных для хранения
- // состояния и результатов разбора аргументов
- let originalFn;
- let customImplementation;
- let isMethodSpy = false;
- let objToSpyOn;
- let methodName;
- // определение вероятности того, что
- // создается шпион для отдельной функции
- const isLikelyFunctionSpy =
- typeof target === 'function' && customImplForMethodFromSpy === undefined;
- // определение вероятности того, что
- // создается шпион для метода объекта
- const isLikelyMethodSpy =
- typeof target === 'object' &&
- target !== null &&
- typeof methodNameOrImplFromSpy === 'string';
- // обработка сценария шпионажа
- // за отдельной функцией
- if (isLikelyFunctionSpy) {
- // исходная функция - это первый аргумент
- originalFn = target;
- // проверка наличия второго аргумента, который
- // может быть пользовательской реализацией
- if (methodNameOrImplFromSpy !== undefined) {
- // генерация ошибки, если второй аргумент
- // (пользовательская реализация) не является функцией
- if (typeof methodNameOrImplFromSpy !== 'function') {
- throw new TypeError(
- 'When spying on a function, the second argument (custom ' +
- 'implementation) must be a function if provided.',
- );
- }
- // пользовательская реализация присваивается,
- // если она предоставлена и является функцией
- customImplementation = methodNameOrImplFromSpy;
- }
- // обработка сценария шпионажа
- // за методом объекта
- } else if (isLikelyMethodSpy) {
- // установка параметров для
- // шпионажа за методом
- methodName = methodNameOrImplFromSpy;
- objToSpyOn = target;
- isMethodSpy = true;
- // генерация ошибки, если метод
- // с указанным именем отсутствует на объекте
- if (!(methodName in target)) {
- throw new TypeError(
- `Attempted to spy on a non-existent property: "${methodName}"`,
- );
- }
- // получение свойства объекта,
- // за которым предполагается шпионаж
- const propertyToSpyOn = target[methodName];
- // генерация ошибки, если свойство,
- // за которым шпионят, не является функцией
- if (typeof propertyToSpyOn !== 'function') {
- throw new TypeError(
- `Attempted to spy on "${methodName}" which is not a function. ` +
- `It is a "${typeof propertyToSpyOn}".`,
- );
- }
- // исходная функция - это
- // метод объекта
- originalFn = propertyToSpyOn;
- // проверка наличия третьего аргумента, который может
- // быть пользовательской реализацией для метода
- if (customImplForMethodFromSpy !== undefined) {
- // генерация ошибки, если третья (пользовательская
- // реализация метода) не является функцией
- if (typeof customImplForMethodFromSpy !== 'function') {
- throw new TypeError(
- 'When spying on a method, the third argument (custom ' +
- 'implementation) must be a function if provided.',
- );
- }
- // пользовательская реализация метода присваивается,
- // если она предоставлена и является функцией
- customImplementation = customImplForMethodFromSpy;
- }
- // обработка невалидных
- // комбинаций аргументов
- } else {
- // специальная проверка и генерация ошибки
- // для попытки шпионить за null
- if (
- target === null &&
- methodNameOrImplFromSpy === undefined &&
- customImplForMethodFromSpy === undefined
- ) {
- throw new TypeError('Attempted to spy on null.');
- }
- // генерация ошибки, если target не функция
- // и имя метода не предоставлено
- if (methodNameOrImplFromSpy === undefined && typeof target !== 'function') {
- throw new TypeError(
- `Attempted to spy on a ${typeof target} which is not a function.`,
- );
- }
- // генерация общей ошибки для
- // остальных невалидных сигнатур вызова
- throw new Error(
- 'Invalid arguments. Valid signatures:\n' +
- ' createSpy(function, [customImplementationFunction])\n' +
- ' createSpy(object, methodNameString, [customImplementationFunction])',
- );
- }
- // формирование и возврат объекта
- // с конфигурацией для создания шпиона
- return {
- originalFn,
- // определение функции для выполнения шпионом: либо
- // пользовательская реализация, либо оригинальная функция
- fnToExecute: customImplementation || originalFn,
- isMethodSpy,
- objToSpyOn,
- methodName,
- };
- }
- /**
- * Создает шпиона для функции или метода объекта,
- * с возможностью подмены реализации.
- *
- * Шпионить за отдельной функцией:
- * createSpy(targetFn, [customImplementation])
- *
- * Шпионить за методом объекта:
- * createSpy(targetObject, methodName, [customImplementation])
- *
- * @param target - Функция для шпионажа или объект, на методе которого ставится шпион.
- * @param methodNameOrImpl - Имя метода (строка) если target - объект,
- * или кастомная реализация (функция) если target - функция.
- * @param customImplForMethod - Кастомная реализация (функция) если target - объект и указан methodName.
- * @returns {(function(...[*]): (*|undefined))|*} Шпион-функция.
- */
- export function createSpy(target, methodNameOrImpl, customImplForMethod) {
- // если аргументы не передавались,
- // то определяется функция-пустышка
- if (
- typeof target === 'undefined' &&
- typeof methodNameOrImpl === 'undefined' &&
- typeof customImplForMethod === 'undefined'
- ) {
- target = function () {};
- }
- // получение конфигурации шпиона
- // путем разбора входных аргументов
- const {originalFn, fnToExecute, isMethodSpy, objToSpyOn, methodName} =
- _parseSpyArgs(target, methodNameOrImpl, customImplForMethod);
- // инициализация объекта для хранения
- // информации о вызовах шпиона
- const callLog = {
- count: 0,
- calls: [],
- };
- // определение основной
- // функции-шпиона
- const spy = function (...args) {
- // увеличение счетчика вызовов
- // при каждом запуске шпиона
- callLog.count++;
- // создание объекта для записи
- // деталей текущего вызова
- const callInfo = {
- // сохранение аргументов, с которыми
- // был вызван шпион
- args: [...args],
- // сохранение контекста (this)
- // вызова шпиона
- thisArg: this,
- returnValue: undefined,
- error: undefined,
- };
- // попытка выполнения целевой функции
- // (оригинальной или пользовательской)
- try {
- // выполнение функции и сохранение
- // возвращенного значения
- callInfo.returnValue = fnToExecute.apply(this, args);
- // добавление информации об успешном
- // вызове в лог
- callLog.calls.push(callInfo);
- // возврат результата выполнения
- // целевой функции
- return callInfo.returnValue;
- } catch (e) {
- // обработка ошибки, если выполнение целевой
- // функции привело к исключению
- // сохранение информации
- // о произошедшей ошибке
- callInfo.error = e;
- // добавление информации о вызове
- // с ошибкой в лог
- callLog.calls.push(callInfo);
- // проброс оригинальной
- // ошибки дальше
- throw e;
- }
- };
- // определение свойства `calls` на шпионе,
- // для получения вызовов
- Object.defineProperty(spy, 'calls', {
- get: () => callLog.calls,
- enumerable: true,
- configurable: false,
- });
- // определение свойства `callCount` на шпионе
- // для получения количества вызовов
- Object.defineProperty(spy, 'callCount', {
- get: () => callLog.count,
- enumerable: true,
- configurable: false,
- });
- // определение свойства `called` на шпионе,
- // указывающего, был ли шпион вызван
- Object.defineProperty(spy, 'called', {
- get: () => callLog.count > 0,
- enumerable: true,
- configurable: false,
- });
- // определение метода `getCall` для получения
- // информации о конкретном вызове по его индексу
- spy.getCall = n => {
- // проверка корректности индекса вызова,
- // выбрасывание ошибки при выходе за границы
- if (typeof n !== 'number' || n < 0 || n >= callLog.calls.length) {
- throw new RangeError(
- `Invalid call index ${n}. Spy has ${callLog.calls.length} call(s).`,
- );
- }
- return callLog.calls[n];
- };
- // определение метода `calledWith` для проверки,
- // был ли шпион вызван с определенным набором аргументов
- spy.calledWith = (...expectedArgs) => {
- return callLog.calls.some(
- call =>
- call.args.length === expectedArgs.length &&
- call.args.every((arg, i) => Object.is(arg, expectedArgs[i])),
- );
- };
- // определение метода `nthCalledWith` для проверки
- // аргументов n-го вызова шпиона
- spy.nthCalledWith = (n, ...expectedArgs) => {
- // getCall(n) выбросит ошибку, если индекс n невалиден
- const call = spy.getCall(n);
- return (
- call.args.length === expectedArgs.length &&
- call.args.every((arg, i) => Object.is(arg, expectedArgs[i]))
- );
- };
- // определение метода `nthCallReturned` для проверки
- // значения, возвращенного n-ым вызовом шпиона
- spy.nthCallReturned = (n, expectedReturnValue) => {
- // getCall(n) выбросит ошибку, если индекс n невалиден
- const call = spy.getCall(n);
- // возврат false, если вызов завершился ошибкой
- if (call.error) return false;
- return Object.is(call.returnValue, expectedReturnValue);
- };
- // определение метода `nthCallThrew` для проверки,
- // выбросил ли n-ый вызов шпиона ошибку
- spy.nthCallThrew = (n, expectedError) => {
- // getCall(n) выбросит ошибку, если индекс n невалиден
- const call = spy.getCall(n);
- // возврат false, если вызов не выбросил ошибку
- if (call.error === undefined) return false;
- // если тип ожидаемой ошибки не указан,
- // любая ошибка считается совпадением
- if (expectedError === undefined) return true;
- // проверка строгого равенства
- // ожидаемой ошибки с выброшенной
- if (call.error === expectedError) return true;
- // проверка совпадения ошибки по сообщению,
- // если ожидаемая ошибка - строка
- if (typeof expectedError === 'string') {
- // убедимся, что call.error существует и имеет свойство message
- return (
- call.error &&
- typeof call.error.message === 'string' &&
- call.error.message === expectedError
- );
- }
- // проверка совпадения ошибки по типу (конструктору),
- // если ожидаемая ошибка - функция-конструктор
- if (
- typeof expectedError === 'function' &&
- call.error instanceof expectedError
- ) {
- return true;
- }
- // проверка совпадения ошибки по имени и сообщению,
- // если ожидаемая ошибка - экземпляр Error
- if (expectedError instanceof Error && call.error instanceof Error) {
- return (
- call.error.name === expectedError.name &&
- call.error.message === expectedError.message
- );
- }
- // прямое сравнение объектов ошибок
- // как крайний случай
- return Object.is(call.error, expectedError);
- };
- // определение метода `restore` для восстановления
- // оригинального метода объекта и сброса истории вызовов
- spy.restore = () => {
- // восстановление оригинального метода
- // объекта, если шпионили за ним
- if (isMethodSpy && objToSpyOn) {
- // проверка, что originalFn существует (на всякий случай,
- // хотя по логике _parseSpyArgs он должен быть)
- if (originalFn !== undefined) {
- objToSpyOn[methodName] = originalFn;
- }
- }
- // сброс истории вызовов
- callLog.count = 0;
- callLog.calls = [];
- };
- // если создается шпион для метода объекта,
- // оригинальный метод немедленно заменяется шпионом
- if (isMethodSpy && objToSpyOn) {
- objToSpyOn[methodName] = spy;
- }
- // возврат созданной и настроенной
- // функции-шпиона
- return spy;
- }
|