{"id":15093130,"url":"https://github.com/angular/catalyst","last_synced_at":"2025-10-06T11:31:20.968Z","repository":{"id":65977153,"uuid":"482115120","full_name":"angular/catalyst","owner":"angular","description":null,"archived":true,"fork":false,"pushed_at":"2022-10-12T20:40:05.000Z","size":56,"stargazers_count":6,"open_issues_count":0,"forks_count":1,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-09-26T11:03:54.504Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":false,"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/angular.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-04-16T00:09:29.000Z","updated_at":"2024-03-20T16:37:26.000Z","dependencies_parsed_at":"2023-02-19T19:31:08.317Z","dependency_job_id":null,"html_url":"https://github.com/angular/catalyst","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/angular%2Fcatalyst","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/angular%2Fcatalyst/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/angular%2Fcatalyst/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/angular%2Fcatalyst/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/angular","download_url":"https://codeload.github.com/angular/catalyst/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":219877267,"owners_count":16554853,"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":[],"created_at":"2024-09-25T11:03:47.179Z","updated_at":"2025-10-06T11:31:15.623Z","avatar_url":"https://github.com/angular.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Catalyst\n\nCatalyst was an experimental opinionated wrapper around the Angular testing API\nwith the aim of reducing number of decisions and boilerplate while writing\ntests.\n\n**This is an experimental API and is not an officially supported product of\nGoogle or Angular. Do not use.**\n\nThis was only an experiment which the Angular team ultimately decided not to\nmove forward with. The code is published here for historical purposes, though it\nlacks some of the infrastructure it relied on to function internally and doesn't\nwork out of the box in this context.\n\nThree challenges to address to get this to work:\n1.  Catalyst relies on `@angular/http` which has been replaced by\n    `@angular/common/http`. Existing `@angular/http` usage would need to be\n    migrated.\n1.  Catalyst used\n    [`goog.module()`](https://google.github.io/closure-library/api/goog.module.html)\n    internally, but Jasmine doesn't support that normally meaning we need to use\n    CommonJS or ESM. Angular v13+ only supports ESM, but this version of Jasmine\n    appears to be too old to use ESM and will likely need to be upgraded.\n1.  Catalyst itself doesn't really require the Angular compiler to be built, but\n    the components it tests do need to be AOT compiled with the Angular CLI or\n    `@angular/compiler-cli`. For real Angular code, this is probably already the\n    case, but this repository does not run the Angular compiler and tests in\n    here likely won't work as is.\n\nCatalyst tests are synchronous (when not using `it.async`) and asserted to be\nso. Catalyst uses a fakeAsync Zone to capture all async tasks within Angular and\nresolve them synchronously. This makes the test easier to write, read and debug.\n\nIt also automatically runs change detection synchronously whenever an event\nhandler is fired. This reduces boilerplate around having to manually trigger\nchange detection in tests.\n\nIf you are using Catalyst, your tests should not use any imports from\n`@angular/core/testing`\n\n## API\n\n-   `describe`, `it`, `beforeEach`, `afterEach`, `beforeAll`, `afterAll`\n    -   Catalyst wrappers to Jasmine that asserts everything is synchronous\n-   `describe.only`, `it.only`\n    -   Convenience shorthand for `fdescribe` and `fit`, respectively\n-   `describe.skip`, `it.skip`\n    -   Convenience shorthand for `xdescribe` and `xit`, respectively\n-   `setupModule`\n    -   To setup the testing module\n-   `bootstrap`, `bootstrapTemplate`\n    -   To synchronously bootstrap from a Component or from a dynamic template\n        string (No more async compileComponents())\n-   `getEl`, `getEls`, `hasEl`\n    -   Locate HTML element(s) using querySelector/querySelectorAll in the DOM\n-   `getDebugEl`, `getDebugEls`, `hasDebugEl`\n    -   Locate DebugElement by Directive or Component type\n-   `tick`\n    -   Advance timer queue by the specified amount and reflect any pending\n        state change to DOM\n-   `describeWithDate`\n    -   Mocks `Date.now()`, `jasmine.clock()` and `goog.now` inside of describe\n        block. `tick` will also move time as seen by all of the above methods.\n        Only works when used instead of the top level `describe` in the file\n        (b/150296894).\n-   `flush`, `markForCheckAndFlush`\n    -   Flush all pending non-periodic timers and promises and reflects any\n        pending state change to DOM\n    -   `markForCheckAndFlush` performs `flush` for components with OnPush\n        change detection strategy. This is only necessary when modifying\n        properties directly on OnPush components within your test.\n        Alternatively, you may consider using bootstrapTemplate(), updating the\n        context, and/or calling flush().\n-   `get`\n    -   Inject an object by token\n-   `trigger`\n    -   Trigger an event on a HTMLElement\n-   `now`\n    -   Run async tasks synchronously and reflects any pending state change to\n        the DOM.\n-   `destroyTestComponent`\n    -   Destroys the root test component and all of its children. This will\n        `ngOnDestroy` to be called in all components.\n\n## Managing State Change\n\nAny DOM events triggered in the test automatically resolve all pending promises\nand run Angular change detection synchronously.\n\nSo if you did something like\n\n```typescript\ngetEl('p').click();\n// DOM updated with state change caused by the click handler even if it's async\n```\n\nIn your test you no longer have to call the right combination of whenStable,\ndetectChanges. It gets done for you automatically and synchronously (using Zone\ninterception of event handler invocation).\n\nIf you don't trigger a state change through an event handler, you can manually\nforce a change detection using `flush`. `flush` recursively flushes all pending\nasync tasks(scheduled by Promise-s and setTimeout-s) and then runs change\ndetection at the end. In that way it flushes all pending tasks and state changes\nand the test is at a stable state at the end of it.\n\n```typescript\ncomponentInstance.myProperty = 10;\nflush();\n// DOM updated with myProperty in component set to 10.\n```\n\nCatalyst doesn't allow any async tasks to be scheduled directly in the test. To\nrun an async task you can wrap it with `now()` to run the task synchronously.\nAny changes to the component state is automatically reflected in the DOM.\n\n```typescript\nnow(() =\u003e componentInstance.asyncTask());\n\n// At this point asyncTask has been run synchronously and any state change will\n// be reflected in DOM.\n```\n\nWhen changing element values, a `trigger` is required to update any ngForm\ncomponent values and validity:\n\n```typescript\nconst input = getEl\u003cHTMLInputElement\u003e('input.template-name-input');\ninput.value = value;     // HTML is updated, ngForm.value is still not updated\ntrigger(input, 'input'); // ngForm.value/ngForm.valid is updated\n```\n\n## Managing Timer Queue\n\nThe event queue can either be moved ahead by a specified time interval using\n`tick(INTERVAL)` or all pending timers can be recursively triggered using\n`flush()`.\n\nThe former is useful for precise control of time in your unit test while the\nlatter can be used to trigger any pending timers in an external component that\nyour component under test is using(and you don’t want your test to be tied to\nexact timeout values).\n\nWhen inside `describeWithDate` block, `tick` and `flush` also advance the mock\nDate through Jasmine's Date mocking facility. `Date.now()` is properly advanced\nas per the elapsed time in the test scheduler of Catalyst. Catalyst\nautomatically installs and uninstalls the Jasmine mock Date before and after\neach test respectively. This is useful for testing code that reschedules tasks\ndepending on the currently elapsed time.\n\nIn addition Catalyst monkey patches the RxJS scheduler to use the Jasmine mock\nDate so that the default async scheduler of RxJS is in sync with the test\nscheduler of Catalyst. This is needed to make RxJS operators like `delay`,\n`debounce` etc. to work as expected with `tick` and `flush`.\n\n## XHR calls / HTTP testing\n\nIf the code under test makes any XHR calls you will need to mock the HTTP\nbackend.\n\n```typescript\nimport {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing';\nimport {NoopAnimationsModule} from '@angular/platform-browser/animations';\n\nlet mockHttp: HttpTestingController;\n\nbeforeEach(() =\u003e {\n  setupModule({\n    imports: [\n      AppModule,\n      HttpClientTestingModule,\n      NoopAnimationsModule,\n    ],\n  });\n\n  // bootstrap your component ...\n\n  mockHttp = get(HttpTestingController);\n});\n\nafterEach(() =\u003e {\n  mockHttp.verify();\n});\n\nit('my test', () =\u003e {\n  // your test ...\n});\n```\n\nNote that you won't be able to use `mockHttp` to respond to any HTTP requests\nuntil the code under test has created the HTTP request. If your component does\nHTTP logic inside its constructor or `ngOnInit`, you can immediately use\n`mockHttp` after `get`, as the constructor and `ngOnInit` immediately run after\nyou call `bootstrap`/`bootstrapTemplate`. A code example follows for all other\nscenarios.\n\n```typescript\nimport {asyncMatchers} from '@angular/catalyst';\nimport {lastValueFrom} from 'rxjs';\nimport {shareReplay} from 'rxjs/operators';\n\n// beforeEach/afterEach from above\n\nbeforeEach(() =\u003e {\n  // another beforeEach to enable toEmit matcher\n  jasmine.addMatchers(asyncMatchers);\n});\n\nit('service calls http', () =\u003e {\n  const output = myService.myObservableMethod('param')\n      .pipe(shareReplay({bufferSize: 1, refCount: true}));\n\n  // Subscribe to the observable but don't await it. It will now be stuck\n  // waiting on an HTTP response. Using `firstValueFrom()` / `lastValueFrom()`\n  // here unsubscribes automatically to avoid leaking test state.\n  lastValueFrom(output);\n\n  const request = mockHttp.expectOne('endpoint');\n  expect(request.request.method).toBe('POST');\n  request.flush('http response');\n\n  expect(output).toEmit('http response');\n});\n```\n\nWARNING: If you find the test doesn't work with `lastValueFrom`, try\n`firstValueFrom`. If that also fails, then instead of `lastValueFrom(output)`,\nyou can use `output.subscribe()` to also trigger the observable, but will then\nneed to manually call `.unsubscribe()` when you are done with the result.\n\nFor more on mocking and testing XHR calls see\nhttps://angular.io/guide/http#testing-http-requests.\n\n## Angular Harnesses\n\nMany Angular Material components provide\n[harnesses](https://material.angular.io/cdk/test-harnesses/overview) to ease\ntesting code that calls them.\n\n## Async Matchers\n\nCatalyst provides jasmine matchers to simplify testing of async code.\n\n```typescript\nimport {beforeAll, it, asyncMatchers} from '@angular/catalyst';\n\nbeforeAll(() =\u003e {\n  jasmine.addMatchers(asyncMatchers);\n});\n\n...\n// For Observables:\nexpect(service.someAsyncCall()).toEmit(expectedResponse);\nexpect(service.someAsyncCallWithError()).toEmitError(expectedErrorResponse);\nexpect(service.someAsyncCallWithMultipleResults()).toEmitSequence([response1, response2]);\n// Same as above without respect to ordering\nexpect(service.someAsyncCallWithMultipleResults())\n    .toEmitSequence(jasmine.arrayWithExactContents([response2, response1]));\n// Only checks partial results\nexpect(service.someAsyncCallWithManyResults()).toEmitSequence(jasmine.arrayContaining([response1]));\nexpect(service.someAsyncCallWithNoValue()).toHaveNeverEmitted();\nexpect(service.someAsyncCallWithNoValue()).toHaveEmitted();\n\n// For Promises:\nexpect(service.someAsyncCall()).toResolveWith(expectedResponse);\nexpect(service.someAsyncCall()).toRejectWith(expectedRejectionValue);\nexpect(service.someAsyncCall()).toRejectWithError(expectedErrorMessageOrRegExp);\n\n```\n\n### Matching hot observables\n\nCatalyst's async matchers essentially subscribe to the observable and collect\nall values that are emitted immediately on subscription. They won't work\nproperly with\n[\"hot\" observables](https://medium.com/@benlesh/hot-vs-cold-observables-f8094ed53339)\n(including Angular's `EventEmitter`). This is because \"hot\" observables emit\nimmediately, not on subscription, so by the time that the async matcher has\nsubscribed in the `expect().toEmit()` line the emissions have already occurred\nand the matcher doesn't see any further emissions.\n\nThe fix is to use a `ReplaySubject` - or, more conveniently, an operator like\n`shareReplay()` that uses a `ReplaySubject` under the hood - to save the\nemissions and then check them later.\n\n```typescript\nimport {firstValueFrom} from 'rxjs';\nimport {shareReplay} from 'rxjs/operators';\n\n...\nconst obs = service.hotObservable.pipe(shareReplay());\n\n// Subscribe to the observable but don't await it. It will now be stuck\n// waiting for emissions. Using `firstValueFrom()` / `lastValueFrom()` here\n// unsubscribes automatically to avoid leaking test state.\nfirstValueFrom(obs);\n\nservice.doThingThatTriggersEmissions();\nexpect(obs).toEmit(expectedEmissions);\n```\n\nWARNING: If you find the test doesn't work with `firstValueFrom`, try\n`lastValueFrom`. If that also fails, then instead of `firstValueFrom(output)`,\nyou can use `output.subscribe()` to also trigger the observable, but will then\nneed to manually call `.unsubscribe()` when you are done with the result.\n\n## Necessarily asynchronous tests\n\nIn some cases, such as screen diffing tests, the test requires asynchronous\nbehavior. In this case `it.async(..., async () =\u003e { ... })` may be called.\n`it.async.skip` and `it.async.only` functions are provided to skip or filter\ntests when dealing with `it.async`. When setting up a `beforeEach` with\nasynchronicity, use `beforeEach.async`.\n\n## Animations\n\nIf the component you are testing uses material components, chances are they use\nanimatons, and the test needs to disable animations. This is also true if you\nuse harnesses, or if you receive the following error message.\n\n```\nFound the synthetic property @transitionMessages. Please include either \"BrowserAnimationsModule\" or \"NoopAnimationsModule\" in your application.\n```\n\nThe easiest way to fix this is to import `NoopAnimationsModule` in your test. It\nmust be the last module imported, so it overrides any other transitively\nimported modules related to animations.\n\n```typescript\nimport {NoopAnimationsModule} from '@angular/platform-browser/animations';\n\nbeforeEach(() =\u003e {\n  setupModule({\n    imports: [\n      AppModule,\n      NoopAnimationsModule,\n    ],\n  });\n\n  // bootstrap your component ...\n});\n\nit('my test', () =\u003e {\n  // ...\n});\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fangular%2Fcatalyst","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fangular%2Fcatalyst","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fangular%2Fcatalyst/lists"}