{"id":13475515,"url":"https://github.com/hirezio/observer-spy","last_synced_at":"2025-05-16T03:06:16.397Z","repository":{"id":38173022,"uuid":"258228615","full_name":"hirezio/observer-spy","owner":"hirezio","description":"This library makes RxJS Observables testing easy!","archived":false,"fork":false,"pushed_at":"2023-10-18T01:26:10.000Z","size":3067,"stargazers_count":380,"open_issues_count":3,"forks_count":13,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-08T13:12:38.772Z","etag":null,"topics":["angular","jasmine","jest","marble-tests","microtest","mock","mocks","observables","observer-spies","rxjs","spies","testing","unit-tests"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/hirezio.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2020-04-23T14:15:54.000Z","updated_at":"2025-03-08T19:05:08.000Z","dependencies_parsed_at":"2024-01-16T07:23:49.613Z","dependency_job_id":"ee47e554-d06e-4a96-997f-2bd38228e09d","html_url":"https://github.com/hirezio/observer-spy","commit_stats":{"total_commits":99,"total_committers":10,"mean_commits":9.9,"dds":"0.46464646464646464","last_synced_commit":"f9c9b964216c86cf3a3232093ddd1740f65337aa"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hirezio%2Fobserver-spy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hirezio%2Fobserver-spy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hirezio%2Fobserver-spy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/hirezio%2Fobserver-spy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/hirezio","download_url":"https://codeload.github.com/hirezio/observer-spy/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254459088,"owners_count":22074605,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["angular","jasmine","jest","marble-tests","microtest","mock","mocks","observables","observer-spies","rxjs","spies","testing","unit-tests"],"created_at":"2024-07-31T16:01:21.088Z","updated_at":"2025-05-16T03:06:11.383Z","avatar_url":"https://github.com/hirezio.png","language":"TypeScript","funding_links":[],"categories":["TypeScript","Table of contents","Testing"],"sub_categories":["Third Party Components","Angular-Specific Utilities"],"readme":"# @hirez_io/observer-spy 👀💪\n\nThis library makes RxJS Observables testing easy!\n\n[![npm version](https://img.shields.io/npm/v/@hirez_io/observer-spy.svg?style=flat-square)](https://www.npmjs.org/package/@hirez_io/observer-spy)\n[![npm downloads](https://img.shields.io/npm/dm/@hirez_io/observer-spy.svg?style=flat-square)](http://npm-stat.com/charts.html?package=@hirez_io/observer-spy\u0026from=2017-07-26)\n![Build](https://github.com/hirezio/observer-spy/workflows/Build/badge.svg)\n[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)\n[![codecov](https://img.shields.io/codecov/c/github/hirezio/observer-spy.svg)](https://codecov.io/gh/hirezio/observer-spy) \u003c!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section --\u003e\n[![All Contributors](https://img.shields.io/badge/all_contributors-8-orange.svg?style=flat-square)](#contributors-)\n\u003c!-- ALL-CONTRIBUTORS-BADGE:END --\u003e\n\n\u003cbr/\u003e\n\n\u003cdiv align=\"center\"\u003e\n  \u003ca href=\"https://hirez.io/?utm_source=github\u0026utm_medium=link\u0026utm_campaign=observer-spy\"\u003e\n    \u003cimg src=\"for-readme/test-angular.jpg\"\n      alt=\"TestAngular.com - Free Angular Testing Workshop - The Roadmap to Angular Testing Mastery\"\n      width=\"600\"\n    /\u003e\n  \u003c/a\u003e\n\u003c/div\u003e\n\n\u003cbr/\u003e\n\n# Table of Contents\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n\n\n  - [Installation](#installation)\n  - [The Problem](#the-problem-testing-rxjs-observables-is-hard-)\n  - [The Solution](#the-solution-observer-spies-)\n- [Usage](#usage)\n  - [`subscribeSpyTo(observable)`](#const-observerspy--subscribespytoobservable)\n  - [`onComplete` (using `async` + `await`)](#wait-for-oncomplete-before-expecting-the-result-using-async--await)\n  - [Spying on Errors (`expectErrors`)](#spy-on-errors-with-receivederror-and-geterror)\n  - [`onError` (using `async` + `await`)](#wait-for-onerror-before-expecting-the-result-using-async--await)\n  - [Manually Creating Spies](#manually-using-new-observerspy)\n  - [Auto Unsubscribing](#auto-unsubscribing)\n  - [Testing Sync Logic](#testing-sync-logic)\n\n  - [Testing Async Logic](#testing-async-logic)\n      - [▶ RxJS  + Angular: use `fakeAsync`](#-rxjs---angular-use-fakeasync)\n\n      - [▶ RxJS + Promises: use `async` + `await`](#-rxjs--promises-use-async--await)\n    \n      - [▶ RxJS Timers / Animations: use `fakeTime`](#-rxjs-timers--animations-use-faketime)\n      - [▶ RxJS + _AJAX_ calls:](#-rxjs--ajax-calls)\n  \n\n- [🧠 Wanna become a PRO Observables tester?](#-wanna-become-a-pro-observables-tester)\n- [How to Contribute](#contributing)\n- [Code Of Conduct](#code-of-conduct)\n- [Contributors ✨](#contributors-)\n- [License](#license)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n\u003cbr/\u003e\n\n## Installation\n\n```console\nyarn add -D @hirez_io/observer-spy\n```\n\nor\n\n```console\nnpm install -D @hirez_io/observer-spy\n```\n\n\u003cbr/\u003e\n\n\n## THE PROBLEM: Testing RxJS observables is hard! 😓\n\nEspecially when testing advanced use cases.\n\nUntil this library, the common way to test observables was to use [Marble tests](https://rxjs-dev.firebaseapp.com/guide/testing/internal-marble-tests)\n\n### What are the disadvantages of Marble Tests?\n\nMarble tests are very powerful, but unfortunately for most tests they are conceptually very complicated to learn and to reason about..\n\n\nYou need to learn and understand `cold` and `hot` observables, `schedulers` and to learn a new syntax just to test a simple observable chain.\n\nMore complex observable chains tests get even harder to read and to maintain.\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n## THE SOLUTION: Observer Spies! 👀💪\n\nThe **Observer-Spy** library was created to present a viable alternative to Marble Tests.\n\nAn alternative which we believe is:\n\n* ✅ **Easier** to understand\n\n* ✅ **Reduces** the complexity\n\n* ✅ Makes observables tests **cleaner**\n \n\u003cbr/\u003e\n\n### Here's what people had to say:\n\n![image](https://user-images.githubusercontent.com/1430726/95263162-0cbe8200-0836-11eb-9d78-45a7e64c38f7.png)\n\n---\n\n![image](https://user-images.githubusercontent.com/1430726/95263462-7dfe3500-0836-11eb-8aca-0d9283b2d66b.png)\n\n---\n\n\u003cbr/\u003e\n\n\n## Why Observer-Spy is easier?\n\n### 😮 Marble test: \n\n```js\n\nimport { TestScheduler } from 'rxjs/testing';\n\nlet scheduler: TestScheduler;\n\nbeforeEach(()=\u003e{\n  scheduler = new TestScheduler((actual, expected) =\u003e {\n    expect(actual).toEqual(expected)\n  })\n})\n\nit('should filter even numbers and multiply each number by 10', () =\u003e {\n  \n  scheduler.run(({cold, expectObservable}) =\u003e {\n    const sourceValues = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6, g: 7, h: 8, i: 9, j: 10};\n\n    const source$ = cold('-a-b-c-d-e-f-g-h-i-j|', sourceValues);\n    const expectedOrder = '-a---b---c---d---e--|';\n    const expectedValues = { a: 10, b: 30, c: 50, d: 70, e: 90};\n    \n    const result$ = source$.pipe(\n      filter(n =\u003e n % 2 !== 0),\n      map(x =\u003e x * 10)\n    );\n\n    expectObservable(result$).toBe(expectedOrder, expectedValues);\n  })\n});\n```\n\n### 😎 Observer Spy Test:\n\n```js\n\nimport { subscribeSpyTo } from '@hirez_io/observer-spy';\n\nit('should filter even numbers and multiply each number by 10', () =\u003e {\n  \n    const result$ = of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10).pipe(\n      filter(n =\u003e n % 2 !== 0),\n      map(x =\u003e x * 10)\n    );\n\n    const observerSpy = subscribeSpyTo(result$);\n\n    expect(observerSpy.getValues()).toEqual([10, 30, 50, 70, 90]);\n\n  })\n});\n```\n\n\nYou generally want to test the outcome of your action instead of implementation details [like how many frames were between each value].\n\nFor most production app use cases, if enough (virtual) time passes testing the **received values** or their order should be sufficient.\n\nThis library gives you the tool to investigate your spy about the values it received and their order. \n\n(The idea was inspired by [Reactive Programming with RxJava](https://books.google.co.il/books?id=y4Y1DQAAQBAJ))\n\n\u003cbr/\u003e\n\n# Usage\n\n\u003cbr/\u003e\n\n\n\n## `const observerSpy = subscribeSpyTo(observable)`\n\nIn order to test Observables you can use the `subscribeSpyTo` function: \n\n```js\nimport { subscribeSpyTo } from '@hirez_io/observer-spy';\n\nit('should immediately subscribe and spy on Observable ', () =\u003e {\n  const fakeObservable = of('first', 'second', 'third');\n\n  // get a special observerSpy of type \"SubscriberSpy\" (with an additional \"unsubscribe\" method)\n  // if you're using TypeScript you can declare it with a generic:\n  // const observerSpy: SubscriberSpy\u003cstring\u003e ... \n  const observerSpy = subscribeSpyTo(fakeObservable);\n\n  // You can unsubscribe if you need to:\n  observerSpy.unsubscribe();\n\n\n  // EXPECTATIONS: \n  expect(observerSpy.getFirstValue()).toEqual('first');\n  expect(observerSpy.receivedNext()).toBe(true);\n  expect(observerSpy.getValues()).toEqual(fakeValues);\n  expect(observerSpy.getValuesLength()).toEqual(3);\n  expect(observerSpy.getFirstValue()).toEqual('first');\n  expect(observerSpy.getValueAt(1)).toEqual('second');\n  expect(observerSpy.getLastValue()).toEqual('third');\n  expect(observerSpy.receivedComplete()).toBe(true);\n\n  // --------------------------------------------------------\n\n  // You can also use this shorthand version:\n\n  expect(subscribeSpyTo(fakeObservable).getFirstValue()).toEqual('first');\n\n  // --------------------------------------------------------\n\n});\n```\n\n\u003cbr/\u003e\n\n### Wait for `onComplete` before expecting the result (using `async` + `await`)\n\n```js\nit('should support async await for onComplete()', async () =\u003e {\n  \n  const fakeObservable = of('first', 'second', 'third');\n\n  const observerSpy = subscribeSpyTo(fakeObservable);\n\n// 👇\n  await observerSpy.onComplete(); // \u003c-- the test will pause here until the observable is complete\n\n  expect(observerSpy.receivedComplete()).toBe(true);\n\n  // If you don't want to use async await you could pass a callback:\n  //\n  //   observerSpy.onComplete(() =\u003e {\n  //     expect(observerSpy.receivedComplete()).toBe(true);\n  //   }));\n});\n\n```\n\n\u003cbr/\u003e\n\n### Spy on errors with `receivedError` and `getError`\n\n#### ⚠ You MUST configure `expectErrors` to catch errors!\nThis 👆 is to avoid swallowing unexpected errors ([more details here](https://github.com/hirezio/observer-spy/issues/20))\n\n```js\n\nit('should spy on Observable errors', () =\u003e {\n\n  const fakeObservable = throwError('FAKE ERROR');\n\n  const observerSpy = subscribeSpyTo(fakeObservable, {expectErrors: true});\n\n  expect(observerSpy.receivedError()).toBe(true);\n\n  expect(observerSpy.getError()).toEqual('FAKE ERROR');\n});\n\n```\n\n\u003cbr/\u003e\n\n### Wait for `onError` before expecting the result (using `async` + `await`)\n\n```js\nit('should support async await for onError()', async () =\u003e {\n  \n  const fakeObservable = throwError('FAKE ERROR');\n\n  const observerSpy = subscribeSpyTo(fakeObservable, {expectErrors: true});\n\n// 👇\n  await observerSpy.onError(); // \u003c-- the test will pause here until the observer receive the error\n\n  expect(observerSpy.getError()).toEqual('FAKE ERROR');\n\n});\n\n```\n\n\u003cbr/\u003e\n\n## Manually using `new ObserverSpy()`\n\nYou can create an `ObserverSpy` instance manually:\n\n```js\n// ... other imports\nimport { ObserverSpy } from '@hirez_io/observer-spy';\n\nit('should spy on Observable values', () =\u003e {\n  \n  const fakeValues = ['first', 'second', 'third'];\n  const fakeObservable = of(...fakeValues);\n\n  // BTW, if you're using TypeScript you can declare it with a generic:\n  // const observerSpy: ObserverSpy\u003cstring\u003e = new ObserverSpy();\n  const observerSpy = new ObserverSpy();\n\n  // This type of ObserverSpy doesn't have a built in \"unsubscribe\" method\n  // only the \"SubscriberSpy\" has it, so we need to create a separate \"Subscription\" variable.\n  const subscription = fakeObservable.subscribe(observerSpy);\n\n  // ...DO SOME LOGIC HERE...\n\n  // unsubscribing is optional, it's good for stopping intervals etc\n  subscription.unsubscribe();\n\n  expect(observerSpy.getValuesLength()).toEqual(3);\n\n});\n```\n\nIf you need to spy on errors, make sure to set the `expectErrors` property:\n\n```js\nit('should spy on Observable errors', () =\u003e {\n  \n  const fakeObservable = throwError('OOPS');\n\n  const observerSpy = new ObserverSpy({expectErrors: true}); // \u003c-- IMPORTANT\n  // OR\n  const observerSpy = new ObserverSpy().expectErrors(); // \u003c-- ALTERNATIVE WAY TO SET IT\n\n  // Or even...\n  observerSpy.expectErrors(); // \u003c-- ALTERNATIVE WAY TO SET IT\n  \n  fakeObservable.subscribe(observerSpy);\n\n  expect(observerSpy.receivedError()).toBe(true);\n\n});\n```\n\n\u003cbr/\u003e\n\n\n# Auto Unsubscribing\n\nIn order to save you the trouble of calling `unsubscribe` in each test, you can configure the library to auto unsubscribe from every observer you create with `subscribeSpyTo()`.\n\n\n### ⚠ PAY ATTENTION: \n\n* You should only call `autoUnsubscribe()` once per environment **(not manually after each test!)**. You do it in your testing environment setup files (like `jest.config.js` or `test.ts` in Angular).\n\n* This works **only with subscriptions created** using either `subscribeSpyTo()` or `queueForAutoUnsubscribe()`.\n\n* Currently **it only works** with frameworks like **Jasmine**, **Mocha** and **Jest** (because they have a global `afterEach` function)\n\n\nThis library provide helper functions to help you configure this behavior - \n\n\u003cbr/\u003e\n\n### ⚒ Configuring Jest with `setup-auto-unsubscribe.js`\n\nThis requires Jest to be loaded and then calls `autoUnsubscribe()` which sets up a global / root `afterEach` function that unsubscribes from your observer spies.\n\nAdd this to your jest configuration (i.e `jest.config.js`):\n\n```js\n{\n  setupFilesAfterEnv: ['\u003crootDir\u003e/node_modules/@hirez_io/observer-spy/dist/setup-auto-unsubscribe.js'],\n}\n```\n\n\u003cbr/\u003e\n\n### ⚒ Configuring Angular (Karma + Jasmine) with `autoUnsubscribe`\n\nThis will add a root level `afterEach()` once that auto unsubscribes observer spies. \n\nAdd this to your `test.ts` - \n\n```ts\n// test.ts\n// ~~~~~~~\n\nimport { autoUnsubscribe } from '@hirez_io/observer-spy';\n\nautoUnsubscribe();\n\n```\n\n\u003cbr/\u003e\n\n### ⚒ Manually adding a subscription with `queueForAutoUnsubscribe`\n\nIf you configured your environment to \"autoUnsubscribe\" and want your manually created spies (via `new ObserverSpy()`) to be \"auto unsubscribed\" as well, you can use `queueForAutoUnsubscribe(subscription)`.\n\nIt accepts any `Unsubscribable` object which has an `unsubscribe()` method -\n\n```js\nimport { queueForAutoUnsubscribe } from '@hirez_io/observer-spy';\n\n\nit('should spy on Observable values', () =\u003e {\n  const fakeValues = ['first', 'second', 'third'];\n  const fakeObservable = of(...fakeValues);\n\n  const observerSpy = new ObserverSpy();\n  const subscription = fakeObservable.subscribe(observerSpy)\n  \n  // This will auto unsubscribe this subscription after the test ends\n  // (if you configured \"autoUnsubscribe()\" in your environment)\n  queueForAutoUnsubscribe(subscription);\n\n  // ... rest of the test\n\n});\n```\n\nThis will ensure your manually created spies are auto unsubscribed at the end of each test.\n\u003cbr/\u003e\n\n# Testing Sync Logic\n\n### ▶ Synchronous RxJS\n\nRxJS - without delaying operators or async execution contexts - will run synchronously. This is the simplest use case; where our `it()` does not need any special asynchronous plugins.\n\n```ts\nit('should run synchronously', () =\u003e {\n  const observerSpy = subscribeSpyTo(from(['first', 'second', 'third']));\n  expect(spy.getValuesLength()).toBe(3);\n});\n```\n\n\u003cbr/\u003e\n\n\u003cbr/\u003e\n\n# Testing Async Logic\n\nIf you're **not using Angular** and have RxJS async operators like `delay` or `timeout` \n\nUse `fakeTime` with `flush()` to simulate the passage of time ([detailed explanation](#-rxjs-timers--animations-use-faketime)) - \n\n[![image](https://user-images.githubusercontent.com/210413/85336618-83f92180-b4a4-11ea-800d-6bb275eeda45.png)](#-rxjs-timers--animations-use-faketime)\n\n\n\u003cbr/\u003e\n\n### ▶ RxJS  + Angular: use `fakeAsync`\n\nWith Angular, you can control time in a much more versatile way. \n\nJust use `fakeAsync` (and `tick` if you need it):\n\n```js\n// ... other imports\nimport { subscribeSpyTo } from '@hirez_io/observer-spy';\nimport { fakeAsync, tick } from '@angular/core/testing';\n\nit('should test Angular code with delay', fakeAsync(() =\u003e {\n  \n  const fakeObservable = of('fake value').pipe(delay(1000));\n\n  const observerSpy = subscribeSpyTo(fakeObservable);\n\n  tick(1000);\n\n  expect(observerSpy.getLastValue()).toEqual('fake value');\n}));\n```\n\n\u003cbr/\u003e\n\n### ▶ RxJS + Promises: use `async` + `await`\n\nSince Promise(s) are [MicroTasks](https://javascript.info/microtask-queue), we should consider them to resolve asynchronously.\n\nFor code using _Promise(s)_ **without timeouts or intervals**, just use `async` + `await` with the `onComplete()` method:\n\n\n```js\n// ... other imports\nimport { subscribeSpyTo } from '@hirez_io/observer-spy';\n\nit('should work with promises', async () =\u003e {\n\n  const fakeService = {\n    getData() {\n      return Promise.resolve('fake data');\n    },\n  };\n  const fakeObservable = defer(() =\u003e fakeService.getData());\n\n  const observerSpy = subscribeSpyTo(fakeObservable);\n\n  await observerSpy.onComplete();\n\n  expect(observerSpy.getLastValue()).toEqual('fake data');\n});\n\n```\n\n\u003cbr/\u003e\n\n### ▶ RxJS Timers / Animations: use `fakeTime`\n\nRxJS code that has time-based logic (e.g using timeouts / intervals / animations) will emit asynchronously. \n\n`fakeTime()` is a custom utility function that wraps the test callback which is perfect for most of these use-cases.\n\nIt does the following things:\n\n1. Changes the RxJS `AsyncScheduler` delegate to use `VirtualTimeScheduler` and use \"virtual time\".\n2. Passes a `flush()` function you can call whenever you want to virtually pass time forward.\n3. Works well with `done` if you pass it as the second parameter (instead of the first)\n\nExample:\n\n```js\n// ... other imports\nimport { subscribeSpyTo, fakeTime } from '@hirez_io/observer-spy';\n\nit('should handle delays with a virtual scheduler', fakeTime((flush) =\u003e {\n    const VALUES = ['first', 'second', 'third'];\n\n    const delayedObservable: Observable\u003cstring\u003e = of(...VALUES).pipe(delay(20000));\n\n    const observerSpy = subscribeSpyTo(delayedObservable);\n    \n    flush(); // \u003c-- passes the \"virtual time\" forward\n\n    expect(observerSpy.getValues()).toEqual(VALUES);\n  })\n);\n\n// ===============================================================================\n\nit('should handle done functionality as well', fakeTime((flush, done) =\u003e {\n    const VALUES = ['first', 'second', 'third'];\n\n    const delayedObservable: Observable\u003cstring\u003e = of(...VALUES).pipe(delay(20000));\n\n    const observerSpy = subscribeSpyTo(delayedObservable);\n    flush();\n\n    observerSpy.onComplete(() =\u003e {\n      expect(observerSpy.getValues()).toEqual(VALUES);\n      done();\n    });\n  })\n);\n```\n\n\u003cbr/\u003e\n\n### ▶ RxJS + _AJAX_ calls:\n\nAsynchronous REST calls (using axios, http, fetch, etc.) should not be tested in a unit / micro test... Test those in an integration test! 😜\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n# 🧠 Wanna become a PRO Observables tester?\n\nIn [Angular Class Testing In action](https://hirez.io/?utm_source=github\u0026utm_medium=link\u0026utm_campaign=observer-spy) course Shai Reznik goes over all the differences and show you how to use observer spies to test complex Observable chains with `switchMap`, `interval` etc...\n\n\u003cbr/\u003e\n\u003cbr/\u003e\n\n\n## Contributing\n\nWant to contribute? Yayy! 🎉\n\nPlease read and follow our [Contributing Guidelines](CONTRIBUTING.md) to learn what are the right steps to take before contributing your time, effort and code.\n\nThanks 🙏\n\n\u003cbr/\u003e\n\n## Code Of Conduct\n\nBe kind to each other and please read our [code of conduct](CODE_OF_CONDUCT.md).\n\n\u003cbr/\u003e\n\n## Contributors ✨\n\nThanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --\u003e\n\u003c!-- prettier-ignore-start --\u003e\n\u003c!-- markdownlint-disable --\u003e\n\u003ctable\u003e\n  \u003ctbody\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"http://www.hirez.io/\"\u003e\u003cimg src=\"https://avatars1.githubusercontent.com/u/1430726?v=4?s=100\" width=\"100px;\" alt=\"Shai Reznik\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eShai Reznik\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=shairez\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=shairez\" title=\"Tests\"\u003e⚠️\u003c/a\u003e \u003ca href=\"#infra-shairez\" title=\"Infrastructure (Hosting, Build-Tools, etc)\"\u003e🚇\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=shairez\" title=\"Documentation\"\u003e📖\u003c/a\u003e \u003ca href=\"#maintenance-shairez\" title=\"Maintenance\"\u003e🚧\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/pulls?q=is%3Apr+reviewed-by%3Ashairez\" title=\"Reviewed Pull Requests\"\u003e👀\u003c/a\u003e \u003ca href=\"#ideas-shairez\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"https://www.codamit.dev\"\u003e\u003cimg src=\"https://avatars0.githubusercontent.com/u/8522558?v=4?s=100\" width=\"100px;\" alt=\"Edouard Bozon\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eEdouard Bozon\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=edbzn\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=edbzn\" title=\"Tests\"\u003e⚠️\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=edbzn\" title=\"Documentation\"\u003e📖\u003c/a\u003e \u003ca href=\"#ideas-edbzn\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/burkybang\"\u003e\u003cimg src=\"https://avatars0.githubusercontent.com/u/927886?v=4?s=100\" width=\"100px;\" alt=\"Adam Smith\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eAdam Smith\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=burkybang\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/katharinakoal\"\u003e\u003cimg src=\"https://avatars3.githubusercontent.com/u/17751573?v=4?s=100\" width=\"100px;\" alt=\"Katharina Koal\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eKatharina Koal\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=katharinakoal\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=katharinakoal\" title=\"Tests\"\u003e⚠️\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=katharinakoal\" title=\"Documentation\"\u003e📖\u003c/a\u003e \u003ca href=\"#ideas-katharinakoal\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/issues?q=author%3Akatharinakoal\" title=\"Bug reports\"\u003e🐛\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"http://www.linkedin.com/in/thomasburleson\"\u003e\u003cimg src=\"https://avatars3.githubusercontent.com/u/210413?v=4?s=100\" width=\"100px;\" alt=\"Thomas Burleson\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eThomas Burleson\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=ThomasBurleson\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=ThomasBurleson\" title=\"Tests\"\u003e⚠️\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=ThomasBurleson\" title=\"Documentation\"\u003e📖\u003c/a\u003e \u003ca href=\"#ideas-ThomasBurleson\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"https://www.armanozak.com/\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/15855540?v=4?s=100\" width=\"100px;\" alt=\"Levent Arman Özak\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eLevent Arman Özak\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=armanozak\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/petrzjunior\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/7000918?v=4?s=100\" width=\"100px;\" alt=\"Petr Zahradník\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003ePetr Zahradník\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=petrzjunior\" title=\"Code\"\u003e💻\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=petrzjunior\" title=\"Tests\"\u003e⚠️\u003c/a\u003e \u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=petrzjunior\" title=\"Documentation\"\u003e📖\u003c/a\u003e \u003ca href=\"#ideas-petrzjunior\" title=\"Ideas, Planning, \u0026 Feedback\"\u003e🤔\u003c/a\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n    \u003ctr\u003e\n      \u003ctd align=\"center\"\u003e\u003ca href=\"https://github.com/JasonLandbridge\"\u003e\u003cimg src=\"https://avatars.githubusercontent.com/u/15127381?v=4?s=100\" width=\"100px;\" alt=\"Jason Landbridge\"/\u003e\u003cbr /\u003e\u003csub\u003e\u003cb\u003eJason Landbridge\u003c/b\u003e\u003c/sub\u003e\u003c/a\u003e\u003cbr /\u003e\u003ca href=\"https://github.com/hirezio/observer-spy/commits?author=JasonLandbridge\" title=\"Documentation\"\u003e📖\u003c/a\u003e\u003c/td\u003e\n    \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\u003c!-- markdownlint-restore --\u003e\n\u003c!-- prettier-ignore-end --\u003e\n\n\u003c!-- ALL-CONTRIBUTORS-LIST:END --\u003e\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!\n\n\u003cbr/\u003e\n\n\n## License\n\nMIT\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhirezio%2Fobserver-spy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhirezio%2Fobserver-spy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhirezio%2Fobserver-spy/lists"}