diff --git a/modules/signals/rxjs-interop/src/rx-method.ts b/modules/signals/rxjs-interop/src/rx-method.ts index 03f2e32652..079e2878b7 100644 --- a/modules/signals/rxjs-interop/src/rx-method.ts +++ b/modules/signals/rxjs-interop/src/rx-method.ts @@ -12,7 +12,9 @@ import { isObservable, noop, Observable, Subject, Unsubscribable } from 'rxjs'; type RxMethodInput = Input | Observable | Signal; -type RxMethod = ((input: RxMethodInput) => Unsubscribable) & +export type RxMethod = (( + input: RxMethodInput +) => Unsubscribable) & Unsubscribable; export function rxMethod( diff --git a/modules/signals/testing/index.ts b/modules/signals/testing/index.ts new file mode 100644 index 0000000000..0b6112f7a9 --- /dev/null +++ b/modules/signals/testing/index.ts @@ -0,0 +1,24 @@ +/* + * Public API Surface of fake-rx-method + */ + +export { + FakeRxMethod, + FAKE_RX_METHOD, + newFakeRxMethod, + asFakeRxMethod, + getRxMethodFake, +} from './src/fake-rx-method'; + +/* + * Public API Surface of mock-signal-store + */ + +export { + MockSignalStore, + ProvideMockSignalStoreParams, + provideMockSignalStore, + UnwrapProvider, + asMockSignalStore, + asSinonSpy, +} from './src/mock-signal-store'; diff --git a/modules/signals/testing/ng-package.json b/modules/signals/testing/ng-package.json new file mode 100644 index 0000000000..1dc0b0bd36 --- /dev/null +++ b/modules/signals/testing/ng-package.json @@ -0,0 +1,5 @@ +{ + "lib": { + "entryFile": "index.ts" + } +} diff --git a/modules/signals/testing/spec/fake-rx-method.spec.ts b/modules/signals/testing/spec/fake-rx-method.spec.ts new file mode 100644 index 0000000000..02fcfad91a --- /dev/null +++ b/modules/signals/testing/spec/fake-rx-method.spec.ts @@ -0,0 +1,85 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { + FakeRxMethod, + asFakeRxMethod, + getRxMethodFake, + newFakeRxMethod, +} from '../src/fake-rx-method'; +import { Subject } from 'rxjs'; +import { RxMethod } from 'modules/signals/rxjs-interop/src/rx-method'; + +@Component({ + selector: 'app-test', + standalone: true, + changeDetection: ChangeDetectionStrategy.OnPush, + template: ``, +}) +export class TestComponent { + fakeRxMethod = newFakeRxMethod(); +} + +describe('FakeRxMethod', () => { + describe('newFakeRxMethod and getRxMethodFake', () => { + let component: TestComponent; + let fixture: ComponentFixture; + let fakeRxMethod: FakeRxMethod; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [TestComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(TestComponent); + component = fixture.componentInstance; + fakeRxMethod = component.fakeRxMethod; + }); + + it('updates the Sinon fake on imperative calls', () => { + fakeRxMethod(11); + expect(getRxMethodFake(fakeRxMethod).callCount).toBe(1); + expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([11]); + }); + + it('updates the Sinon fake when an observable input emits', () => { + const o = new Subject(); + fakeRxMethod(o); + o.next(11); + expect(getRxMethodFake(fakeRxMethod).callCount).toBe(1); + expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([11]); + o.next(22); + expect(getRxMethodFake(fakeRxMethod).callCount).toBe(2); + expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([22]); + }); + + it('updates the Sinon fake when a signal input emits (1)', () => { + const s = signal(72); + fakeRxMethod(s); + fixture.detectChanges(); + expect(getRxMethodFake(fakeRxMethod).callCount).toBe(1); + expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([72]); + s.set(23); + fixture.detectChanges(); + expect(getRxMethodFake(fakeRxMethod).callCount).toBe(2); + expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([23]); + }); + + it('updates the Sinon fake when a signal input emits (2)', () => { + const s = signal(72); + fakeRxMethod(s); + s.set(23); + fixture.detectChanges(); + expect(getRxMethodFake(fakeRxMethod).callCount).toBe(1); + expect(getRxMethodFake(fakeRxMethod).lastCall.args).toEqual([23]); + }); + }); + + describe('asFakeRxMethod', () => { + it('should return the input wihtout change', () => { + TestBed.runInInjectionContext(() => { + const rxMethod = newFakeRxMethod() as RxMethod; + expect(asFakeRxMethod(rxMethod)).toEqual(rxMethod); + }); + }); + }); +}); diff --git a/modules/signals/testing/spec/mock-signal-store.spec.ts b/modules/signals/testing/spec/mock-signal-store.spec.ts new file mode 100644 index 0000000000..01d52c7c4a --- /dev/null +++ b/modules/signals/testing/spec/mock-signal-store.spec.ts @@ -0,0 +1,276 @@ +import { + signalStore, + withState, + withComputed, + withMethods, + patchState, + getState, +} from '@ngrx/signals'; +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { Injectable, computed, inject } from '@angular/core'; +import { pipe, switchMap, tap, Observable, of, Subject } from 'rxjs'; +import { tapResponse } from '@ngrx/component-store'; +import { HttpErrorResponse } from '@angular/common/http'; +import { TestBed } from '@angular/core/testing'; +import { + MockSignalStore, + UnwrapProvider, + asMockSignalStore, + asSinonSpy, + provideMockSignalStore, +} from '../src/mock-signal-store'; +import { getRxMethodFake } from '../src/fake-rx-method'; +import { fake, replace } from 'sinon'; + +@Injectable() +class SampleService { + getTripleValue(n: number): Observable { + return of(n * 3); + } +} + +const initialState = { + value: 1, + object: { + objectValue: 2, + nestedObject: { + nestedObjectValue: 3, + }, + }, +}; + +const SampleSignalStore = signalStore( + withState(initialState), + withComputed(({ value }) => ({ + doubleNumericValue: computed(() => value() * 2), + tripleNumericValue: computed(() => value() * 3), + })), + withMethods((store) => ({ + setValue(value: number): void { + patchState(store, () => ({ + value, + })); + }, + setNestedObjectValue(nestedObjectValue: number): void { + patchState(store, () => ({ + object: { + ...store.object(), + nestedObject: { + ...store.object.nestedObject(), + nestedObjectValue, + }, + }, + })); + }, + })), + withMethods((store, service = inject(SampleService)) => ({ + rxMethod: rxMethod( + pipe( + tap(() => store.setValue(10)), + switchMap((n) => service.getTripleValue(n)), + tapResponse( + (response) => { + store.setNestedObjectValue(response); + }, + (errorResponse: HttpErrorResponse) => { + store.setNestedObjectValue(0); + } + ) + ) + ), + })) +); + +describe('mockSignalStore', () => { + describe('with default parameters', () => { + let store: UnwrapProvider; + let mockStore: MockSignalStore; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + SampleService, + provideMockSignalStore(SampleSignalStore, { + initialComputedValues: { + doubleNumericValue: 20, + tripleNumericValue: 30, + }, + }), + ], + }); + store = TestBed.inject(SampleSignalStore); + mockStore = asMockSignalStore(store); + }); + + it('should set the original initial values', () => { + expect(store.value()).toBe(initialState.value); + expect(store.object()).toEqual(initialState.object); + }); + + it("should set the computed signal's initial value", () => { + expect(store.doubleNumericValue()).toBe(20); + }); + + it('should mock the computed signal with a writable signal', () => { + expect(store.doubleNumericValue()).toBe(20); + mockStore.doubleNumericValue.set(33); + expect(store.doubleNumericValue()).toBe(33); + }); + + it('should mock the updater with a Sinon fake', () => { + expect(mockStore.setValue.callCount).toBe(0); + store.setValue(11); + expect(mockStore.setValue.callCount).toBe(1); + expect(mockStore.setValue.lastCall.args).toEqual([11]); + }); + + it('should mock the rxMethod with a FakeRxMethod (imperative)', () => { + expect(getRxMethodFake(store.rxMethod).callCount).toBe(0); + store.rxMethod(22); + expect(getRxMethodFake(store.rxMethod).callCount).toBe(1); + expect(getRxMethodFake(store.rxMethod).lastCall.args).toEqual([22]); + }); + + it('should mock the rxMethod with a FakeRxMethod (declarative)', () => { + const o = new Subject(); + store.rxMethod(o); + expect(getRxMethodFake(store.rxMethod).callCount).toBe(0); + o.next(22); + expect(getRxMethodFake(store.rxMethod).callCount).toBe(1); + expect(getRxMethodFake(store.rxMethod).lastCall.args).toEqual([22]); + }); + + it('can alter the DeepSignal with patchState', () => { + patchState(store, { + value: 20, + }); + expect(store.value()).toBe(20); + expect(store.object()).toEqual(initialState.object); + + patchState(store, { + object: { + ...initialState.object, + nestedObject: { + ...initialState.object.nestedObject, + nestedObjectValue: 40, + }, + }, + }); + expect(store.object()).toEqual({ + ...initialState.object, + nestedObject: { + ...initialState.object.nestedObject, + nestedObjectValue: 40, + }, + }); + }); + }); + describe('parameters', () => { + it('should throw an expection, if an inital value is missing for a computed Signal', () => { + expect(() => { + TestBed.configureTestingModule({ + providers: [ + SampleService, + provideMockSignalStore(SampleSignalStore, { + initialComputedValues: { + doubleNumericValue: 20, + }, + }), + ], + }); + const store = TestBed.inject(SampleSignalStore); + const mockStore = asMockSignalStore(store); + }).toThrow(Error('tripleNumericValue should have an initial value')); + }); + + it('should throw an expection, if an inital value is missing for a computed Signal (2)', () => { + expect(() => { + TestBed.configureTestingModule({ + providers: [SampleService, provideMockSignalStore(SampleSignalStore)], + }); + const store = TestBed.inject(SampleSignalStore); + const mockStore = asMockSignalStore(store); + }).toThrowError(); + }); + + it('can keep the original computed signals', () => { + TestBed.configureTestingModule({ + providers: [ + SampleService, + provideMockSignalStore(SampleSignalStore, { + mockComputedSignals: false, + }), + ], + }); + const store = TestBed.inject(SampleSignalStore); + const mockStore = asMockSignalStore(store); + + expect(store.doubleNumericValue()).toBe(2); + expect(store.tripleNumericValue()).toBe(3); + }); + + it('can update the initial value of the store by the initialStatePatch parameter', () => { + TestBed.configureTestingModule({ + providers: [ + SampleService, + provideMockSignalStore(SampleSignalStore, { + initialComputedValues: { + doubleNumericValue: 20, + tripleNumericValue: 30, + }, + initialStatePatch: { + value: 22, + }, + }), + ], + }); + const store = TestBed.inject(SampleSignalStore); + + expect(store.value()).toBe(22); + }); + + it('can update the initial value of the store by the initialStatePatch parameter (nested objects)', () => { + TestBed.configureTestingModule({ + providers: [ + SampleService, + provideMockSignalStore(SampleSignalStore, { + initialComputedValues: { + doubleNumericValue: 20, + tripleNumericValue: 30, + }, + initialStatePatch: { + object: { + ...initialState.object, + nestedObject: { + ...initialState.object.nestedObject, + nestedObjectValue: 40, + }, + }, + }, + }), + ], + }); + const store = TestBed.inject(SampleSignalStore); + + expect(getState(store)).toEqual({ + ...initialState, + object: { + ...initialState.object, + nestedObject: { + ...initialState.object.nestedObject, + nestedObjectValue: 40, + }, + }, + }); + }); + }); + describe('asSinonSpy', () => { + it('should return the input wihtout change', () => { + TestBed.runInInjectionContext(() => { + const o = { fnc: () => 1 }; + replace(o, 'fnc', fake()); + expect(asSinonSpy(o.fnc)).toEqual(o.fnc); + }); + }); + }); +}); diff --git a/modules/signals/testing/src/fake-rx-method.ts b/modules/signals/testing/src/fake-rx-method.ts new file mode 100644 index 0000000000..368dbe396e --- /dev/null +++ b/modules/signals/testing/src/fake-rx-method.ts @@ -0,0 +1,53 @@ +import { rxMethod } from '@ngrx/signals/rxjs-interop'; +import { tap } from 'rxjs'; +import { SinonSpy, fake } from 'sinon'; +import { RxMethod } from 'modules/signals/rxjs-interop/src/rx-method'; + +/** + * FakeRxMethod mock type, it's an extended version of RxMethod, with an additional + * [FAKE_RX_METHOD] property containing a Sinon fake (SinonSpy<[T]>). + */ +export const FAKE_RX_METHOD = Symbol('FAKE_RX_METHOD'); +export type FakeRxMethod = RxMethod & { [FAKE_RX_METHOD]: SinonSpy<[T]> }; + +/** + * Creates a new rxMethod mock. + * The returned function accepts a static value, signal, or observable as an input argument. + * + * The Sinon fake stores the call information, when: + * - the generated function was called with a static value. + * - the generated function was called with a signal argument, and the signal's value changes. + * - the generated function was called with an observable argument, and the observable emits. + * + * @returns {FakeRxMethod} A new rxMethod mock. + */ +export function newFakeRxMethod(): FakeRxMethod { + const f = fake<[T]>(); + const r = rxMethod(tap((x) => f(x))) as FakeRxMethod; + r[FAKE_RX_METHOD] = f; + return r; +} + +/** + * Converts the type of a (mocked) RxMethod into a FakeRxMethod. + * + * @template T - The argument type of the RxMethod. + * @param {RxMethod} rxMethod - The (mocked) RxMethod to be converted. + * @returns {FakeRxMethod} The converted FakeRxMethod. + */ +export function asFakeRxMethod(rxMethod: RxMethod): FakeRxMethod { + return rxMethod as unknown as FakeRxMethod; +} + +/** + * Gets the Sinon fake from a mocked RxMethod. + * + * @template T - The argument type of the RxMethod. + * @param {RxMethod} rxMethod - The (mocked) RxMethod for which to retrieve the Sinon fake. + * @returns {sinon.SinonSpy<[T], unknown>} The Sinon fake capturing calls to the RxMethod. + */ +export function getRxMethodFake( + rxMethod: RxMethod +): SinonSpy<[T], unknown> { + return asFakeRxMethod(rxMethod)[FAKE_RX_METHOD]; +} diff --git a/modules/signals/testing/src/mock-signal-store.ts b/modules/signals/testing/src/mock-signal-store.ts new file mode 100644 index 0000000000..e788ddb30d --- /dev/null +++ b/modules/signals/testing/src/mock-signal-store.ts @@ -0,0 +1,270 @@ +import { + Provider, + ProviderToken, + Signal, + WritableSignal, + isSignal, + untracked, + signal, +} from '@angular/core'; +import { SinonSpy, fake } from 'sinon'; +import { FakeRxMethod, newFakeRxMethod } from './fake-rx-method'; +import { getState, patchState } from '../../src'; +import { StateSignal } from '../../src/state-signal'; +import { RxMethod } from 'modules/signals/rxjs-interop/src/rx-method'; + +/** + * Constructor type. + */ +interface Constructor { + new (...args: never[]): ClassType; +} + +/** + * Function type. + */ +type Method = (...args: T) => unknown; + +/** + * Type for a mocked singalStore: + * - Signals are replaced by WritableSignals. + * - RxMethods are replaced by FakeRxMethods. + * - Functions are replaced by Sinon fakes. + */ +export type MockSignalStore = { + [K in keyof T]: T[K] extends Signal + ? WritableSignal + : T[K] extends RxMethod + ? FakeRxMethod + : T[K] extends Method + ? SinonSpy + : T[K]; +}; + +/** + * Type for the state of the singlaStore. + */ +type InitialState = T extends StateSignal ? U : never; + +/** + * Given a type T, determines the keys of the signal properties. + */ +type SignalKeys = { + // -? makes the key required, opposite of ? + [K in keyof T]-?: T[K] extends Signal ? K : never; +}[keyof T]; + +/** + * Type to extract the wrapped type from a Signal type. + * + * @template T - The original Signal type. + * @returns The unwrapped type if T is a Signal, otherwise, 'never'. + */ +type UnwrapSignal = T extends Signal ? U : never; + +/** + * Parameters for providing a mock signal store. + * + * @template T The type of the original signal store. + * @param initialStatePatch A partial initial state to override the original initial state. + * @param initialComputedValues Initial values for computed signals. + * @param mockComputedSignals Flag to mock computed signals (default is true). + * @param mockMethods Flag to mock methods (default is true). + * @param mockRxMethods Flag to mock RxMethods (default is true). + * @param debug Flag to enable debug mode (default is false). + */ +export type ProvideMockSignalStoreParams = { + initialStatePatch?: Partial>; + initialComputedValues?: Omit< + { + [K in SignalKeys]?: UnwrapSignal; + }, + keyof InitialState + >; + mockComputedSignals?: boolean; + mockMethods?: boolean; + mockRxMethods?: boolean; + debug?: boolean; +}; + +/** + * Provides a mock version of signal store. + * + * @template ClassType The class type that extends StateSignal. + * @param classConstructor The constructor function for the class. + * @param params Optional parameters for providing the mock signal store. + * @returns The provider for the mock signal store. + * + * Usage: + * + * ```typescript + * // component: + * + * export const ArticleListSignalStore = signalStore( + * withState(initialArticleListState), + * withComputed(({ articlesCount, pageSize }) => ({ + * totalPages: computed(() => Math.ceil(articlesCount() / pageSize())), + * })), + * withComputed(({ selectedPage, totalPages }) => ({ + * pagination: computed(() => ({ selectedPage: selectedPage(), totalPages: totalPages() })), + * })), + * // ... + * ); + * + * @Component(...) + * export class ArticleListComponent_SS { + * readonly store = inject(ArticleListSignalStore); + * // ... + * } + * + * // test: + * + * // we have to use UnwrapProvider to get the real type of a SignalStore + * let store: UnwrapProvider; + * let mockStore: MockSignalStore; + * + * await TestBed.configureTestingModule({ + * imports: [ + * ArticleListComponent_SS, + * MockComponent(UiArticleListComponent) + * ] + * }) + * .overrideComponent( + * ArticleListComponent_SS, + * { + * set: { + * providers: [ // override the component level providers + * MockProvider(ArticlesService), // injected in ArticleListSignalStore + * provideMockSignalStore(ArticleListSignalStore, { + * // if mockComputedSignals is enabled (default), + * // you must provide an initial value for each computed signals + * initialComputedValues: { + * totalPages: 0, + * pagination: { selectedPage: 0, totalPages: 0 } + * } + * }) + * ] + * } + * } + * ) + * .compileComponents(); + * + * store = component.store; + * mockStore = asMockSignalStore(store); + * + * ``` + */ + +export function provideMockSignalStore>( + classConstructor: Constructor, + params?: ProvideMockSignalStoreParams +): Provider { + let cachedStore: ClassType | undefined = undefined; + return { + provide: classConstructor, + useFactory: () => { + // use the cached instance of the store to work around Angular + // attaching created items to certain nodes. + if (cachedStore) { + return cachedStore as MockSignalStore; + } + const store = Reflect.construct(classConstructor, []); + cachedStore = store; + + const keys = Object.keys(store) as Array; + + const pluckerSignals = keys.filter( + (k) => isSignal(store[k]) && k in getState(store) + ); + const combinedSignals = keys.filter( + (k) => isSignal(store[k]) && !pluckerSignals.includes(k) + ); + const rxMethods = keys.filter( + (k) => + typeof store[k] === 'function' && + !isSignal(store[k]) && + 'unsubscribe' in (store[k] as object) + ); + const methods = keys.filter( + (k) => + typeof store[k] === 'function' && + !isSignal(store[k]) && + !rxMethods.includes(k) + ); + + if (params?.debug === true) { + console.debug('pluckerSignals', pluckerSignals); + console.debug('combinedSignals', combinedSignals); + console.debug('rxMethods', rxMethods); + console.debug('methods', methods); + } + + if (params?.mockComputedSignals !== false) { + combinedSignals.forEach((k) => { + if ( + params?.initialComputedValues && + k in params.initialComputedValues + ) { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + store[k] = signal(params?.initialComputedValues?.[k]); + } else { + throw new Error(`${String(k)} should have an initial value`); + } + }); + } + + if (params?.mockMethods !== false) { + methods.forEach((k) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + store[k] = fake(); + }); + } + + if (params?.mockRxMethods !== false) { + rxMethods.forEach((k) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + store[k] = newFakeRxMethod(); + }); + } + + if (params?.initialStatePatch) { + untracked(() => { + patchState(store, (s) => ({ ...s, ...params.initialStatePatch })); + }); + } + + if (params?.debug === true) { + console.debug('Mocked store:', store); + } + + return store as MockSignalStore; + }, + }; +} + +/** + * Type to extract the type of a signal store. + * + * The signalStore() function returns a provider for the generated signal store. + */ +export type UnwrapProvider = T extends ProviderToken ? U : never; + +/** + * Converts the type of a (mocked) SignalStore to a MockSignalStore + */ +export function asMockSignalStore(s: T): MockSignalStore { + return s as MockSignalStore; +} + +/** + * Converts the type of a (mocked) function to a Sinon Spy + */ +export function asSinonSpy< + TArgs extends readonly any[] = any[], + TReturnValue = any +>(fn: (...x: TArgs) => TReturnValue): SinonSpy { + return fn as unknown as SinonSpy; +} diff --git a/package.json b/package.json index bf8a5f2722..9c044c66ba 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "@nx/workspace": "16.8.1", "@octokit/rest": "^15.17.0", "@schematics/angular": "17.0.0", + "sinon": "^17.0.1", "@testing-library/cypress": "9.0.0", "@types/fs-extra": "^2.1.0", "@types/glob": "^5.0.33", @@ -123,6 +124,7 @@ "@types/rimraf": "^0.0.28", "@types/semver": "^7.3.9", "@types/shelljs": "^0.8.5", + "@types/sinon": "^17.0.3", "@typescript-eslint/eslint-plugin": "5.62.0", "@typescript-eslint/parser": "5.62.0", "@typescript-eslint/utils": "5.62.0", diff --git a/tsconfig.json b/tsconfig.json index 5180357b28..e755ce0df0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -47,6 +47,7 @@ "@ngrx/signals/entities": ["./modules/signals/entities"], "@ngrx/signals/rxjs-interop": ["./modules/signals/rxjs-interop"], "@ngrx/signals/schematics-core": ["./modules/signals/schematics-core"], + "@ngrx/signals/testing": ["./modules/signals/testing"], "@ngrx/store": ["./modules/store"], "@ngrx/store-devtools": ["./modules/store-devtools"], "@ngrx/store-devtools/schematics-core": [ diff --git a/yarn.lock b/yarn.lock index 96b5c70200..45834f73e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4365,6 +4365,13 @@ dependencies: type-detect "4.0.8" +"@sinonjs/commons@^3.0.0": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-3.0.1.tgz#1029357e44ca901a615585f6d27738dbc89084cd" + integrity sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ== + dependencies: + type-detect "4.0.8" + "@sinonjs/fake-timers@^10.0.2": version "10.0.2" resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" @@ -4372,6 +4379,27 @@ dependencies: "@sinonjs/commons" "^2.0.0" +"@sinonjs/fake-timers@^11.2.2": + version "11.2.2" + resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz#50063cc3574f4a27bd8453180a04171c85cc9699" + integrity sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw== + dependencies: + "@sinonjs/commons" "^3.0.0" + +"@sinonjs/samsam@^8.0.0": + version "8.0.0" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-8.0.0.tgz#0d488c91efb3fa1442e26abea81759dfc8b5ac60" + integrity sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew== + dependencies: + "@sinonjs/commons" "^2.0.0" + lodash.get "^4.4.2" + type-detect "^4.0.8" + +"@sinonjs/text-encoding@^0.7.2": + version "0.7.2" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz#5981a8db18b56ba38ef0efb7d995b12aa7b51918" + integrity sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ== + "@socket.io/component-emitter@~3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" @@ -4804,6 +4832,18 @@ "@types/glob" "*" "@types/node" "*" +"@types/sinon@^17.0.3": + version "17.0.3" + resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-17.0.3.tgz#9aa7e62f0a323b9ead177ed23a36ea757141a5fa" + integrity sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw== + dependencies: + "@types/sinonjs__fake-timers" "*" + +"@types/sinonjs__fake-timers@*": + version "8.1.5" + resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz#5fd3592ff10c1e9695d377020c033116cc2889f2" + integrity sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ== + "@types/sinonjs__fake-timers@8.1.1": version "8.1.1" resolved "https://registry.yarnpkg.com/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz#b49c2c70150141a15e0fa7e79cf1f92a72934ce3" @@ -7969,6 +8009,11 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" + integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== + dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -11997,6 +12042,11 @@ jszip@^3.1.3: readable-stream "~2.3.6" setimmediate "^1.0.5" +just-extend@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" + integrity sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw== + karma-chrome-launcher@3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz#805a586799a4d05f4e54f72a204979f3f3066738" @@ -12420,6 +12470,11 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== + lodash.isfinite@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz#fb89b65a9a80281833f0b7478b3a5104f898ebb3" @@ -13270,6 +13325,17 @@ nice-try@^1.0.4: resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== +nise@^5.1.5: + version "5.1.9" + resolved "https://registry.yarnpkg.com/nise/-/nise-5.1.9.tgz#0cb73b5e4499d738231a473cd89bd8afbb618139" + integrity sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers" "^11.2.2" + "@sinonjs/text-encoding" "^0.7.2" + just-extend "^6.2.0" + path-to-regexp "^6.2.1" + node-abort-controller@^3.0.1: version "3.1.1" resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548" @@ -14204,6 +14270,11 @@ path-to-regexp@0.1.7: resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== +path-to-regexp@^6.2.1: + version "6.2.1" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5" + integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw== + path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" @@ -15932,6 +16003,18 @@ simple-git@^1.85.0: dependencies: debug "^4.0.1" +sinon@^17.0.1: + version "17.0.1" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-17.0.1.tgz#26b8ef719261bf8df43f925924cccc96748e407a" + integrity sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g== + dependencies: + "@sinonjs/commons" "^3.0.0" + "@sinonjs/fake-timers" "^11.2.2" + "@sinonjs/samsam" "^8.0.0" + diff "^5.1.0" + nise "^5.1.5" + supports-color "^7.2.0" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -16646,7 +16729,7 @@ supports-color@^5.3.0: dependencies: has-flag "^3.0.0" -supports-color@^7.1.0: +supports-color@^7.1.0, supports-color@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== @@ -17169,7 +17252,7 @@ type-check@~0.3.2: dependencies: prelude-ls "~1.1.2" -type-detect@4.0.8: +type-detect@4.0.8, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== @@ -18249,4 +18332,4 @@ zone.js@0.14.2: resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.14.2.tgz#91b20b24e8ab9a5a74f319ed5a3000f234ffa3b6" integrity sha512-X4U7J1isDhoOmHmFWiLhloWc2lzMkdnumtfQ1LXzf/IOZp5NQYuMUTaviVzG/q1ugMBIXzin2AqeVJUoSEkNyQ== dependencies: - tslib "^2.3.0" \ No newline at end of file + tslib "^2.3.0"