Skip to content

Commit

Permalink
feat: support Vitest mocking (#686)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tommy228 authored Dec 17, 2024
1 parent b27223f commit 691c476
Show file tree
Hide file tree
Showing 74 changed files with 3,965 additions and 27 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
projects/spectator/test/**/*.ts
projects/spectator/jest/test/**/*.ts
projects/spectator/vitest/test/**/*.ts
projects/spectator/schematics/**/*.*
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,6 @@ testem.log
# System Files
.DS_Store
Thumbs.db

# vitest
vite.config.mts.timestamp-*.mjs
55 changes: 54 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,15 @@ Spectator helps you get rid of all the boilerplate grunt work, leaving you with
- ✅ Easy DOM querying
- ✅ Clean API for triggering keyboard/mouse/touch events
- ✅ Testing `ng-content`
- ✅ Custom Jasmine/Jest Matchers (toHaveClass, toBeDisabled..)
- ✅ Custom Jasmine/Jest/Vitest Matchers (toHaveClass, toBeDisabled..)
- ✅ Routing testing support
- ✅ HTTP testing support
- ✅ Built-in support for entry components
- ✅ Built-in support for component providers
- ✅ Auto-mocking providers
- ✅ Strongly typed
- ✅ Jest Support
- ✅ Vitest Support


## Sponsoring ngneat
Expand Down Expand Up @@ -88,6 +89,7 @@ Become a bronze sponsor and get your logo on our README on GitHub.
- [Mocking OnInit Dependencies](#mocking-oninit-dependencies)
- [Mocking Constructor Dependencies](#mocking-constructor-dependencies)
- [Jest Support](#jest-support)
- [Vitest Support](#vitest-support)
- [Testing with HTTP](#testing-with-http)
- [Global Injections](#global-injections)
- [Component Providers](#component-providers)
Expand Down Expand Up @@ -1193,6 +1195,57 @@ When using the component schematic you can specify the `--jest` flag to have the
}
```

## Vitest Support
Like Jest, Spectator also supports Vitest.

To use Vitest, update your `vite.config.[m]ts` to inline the Spectator package so it gets transformed with Vite before the tests run.

```ts
export default defineConfig(({ mode }) => ({
/* ... */
test: {
/* ... */
// inline @ngneat/spectator
server: {
deps: {
inline: ['@ngneat/spectator']
}
}
},
}));
```

You can then import the functions from `@ngneat/spectator/vitest` instead of `@ngneat/spectator`
and it will use Vitest instead of Jasmine.

```ts
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest';
import { AuthService } from './auth.service';
import { DateService } from './date.service';

describe('AuthService', () => {
let spectator: SpectatorService<AuthService>;
const createService = createServiceFactory({
service: AuthService,
mocks: [DateService]
});

beforeEach(() => spectator = createService());

it('should not be logged in', () => {
const dateService = spectator.inject<DateService>(DateService);
dateService.isExpired.mockReturnValue(true);
expect(spectator.service.isLoggedIn()).toBeFalsy();
});

it('should be logged in', () => {
const dateService = spectator.inject<DateService>(DateService);
dateService.isExpired.mockReturnValue(false);
expect(spectator.service.isLoggedIn()).toBeTruthy();
});
});
```

## Testing with HTTP
Spectator makes testing data services, which use the Angular HTTP module, a lot easier. For example, let's say that you have service with three methods, one performs a GET, one a POST and one performs
concurrent requests:
Expand Down
3 changes: 3 additions & 0 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
"builder": "@angular-builders/jest:run",
"options": {}
},
"test-vitest": {
"builder": "@analogjs/vitest-angular:test"
},
"lint": {
"builder": "@angular-eslint/builder:lint",
"options": {
Expand Down
57 changes: 57 additions & 0 deletions docs/docs/vitest-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
id: vitest-support
title: Vitest Support
---

By default, Spectator uses Jasmine for creating spies. If you are using Vitest as test framework instead, you can let Spectator create Vitest-compatible spies.

## Configuration

Update your `vite.config.[m]ts` to inline the Spectator package so it gets transformed with Vite before the tests run.

```ts
export default defineConfig(({ mode }) => ({
/* ... */
test: {
/* ... */
// inline @ngneat/spectator
server: {
deps: {
inline: ['@ngneat/spectator']
}
}
},
}));
```

## Usage

Import the functions from `@ngneat/spectator/vitest` instead of `@ngneat/spectator` to use Vitest instead of Jasmine.

```ts
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest';
import { AuthService } from './auth.service';
import { DateService } from './date.service';

describe('AuthService', () => {
let spectator: SpectatorService<AuthService>;
const createService = createServiceFactory({
service: AuthService,
mocks: [DateService]
});

beforeEach(() => spectator = createService());

it('should not be logged in', () => {
const dateService = spectator.inject<DateService>(DateService);
dateService.isExpired.mockReturnValue(true);
expect(spectator.service.isLoggedIn()).toBeFalsy();
});

it('should be logged in', () => {
const dateService = spectator.inject<DateService>(DateService);
dateService.isExpired.mockReturnValue(false);
expect(spectator.service.isLoggedIn()).toBeTruthy();
});
});
```
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"build:schematics": "tsc -p projects/spectator/schematics/tsconfig.json",
"test": "ng test",
"test:jest": "ng run spectator:test-jest",
"test:ci": "cross-env NODE_ENV=build yarn test && yarn test:jest --silent",
"test:vitest": "ng run spectator:test-vitest",
"test:ci": "cross-env NODE_ENV=build yarn test && yarn test:jest --silent && yarn test:vitest",
"lint": "ng lint",
"format": "prettier --write \"{projects,src}/**/*.ts\"",
"commit": "git-cz",
Expand All @@ -30,6 +31,8 @@
"release:dry": "cd projects/spectator && standard-version --infile ../../CHANGELOG.md --dry-run"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^1.10.1",
"@analogjs/vitest-angular": "^1.10.1",
"@angular-builders/jest": "^18.0.0",
"@angular-devkit/build-angular": "^19.0.1",
"@angular-devkit/schematics": "^19.0.1",
Expand All @@ -39,6 +42,7 @@
"@angular-eslint/schematics": "18.4.2",
"@angular-eslint/template-parser": "18.4.2",
"@angular/animations": "^19.0.0",
"@angular/build": "^19.0.0",
"@angular/cdk": "^19.0.0",
"@angular/cli": "^19.0.1",
"@angular/common": "^19.0.0",
Expand Down Expand Up @@ -70,6 +74,7 @@
"jasmine-spec-reporter": "7.0.0",
"jest": "29.7.0",
"jest-preset-angular": "14.1.0",
"jsdom": "^25.0.1",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-coverage-istanbul-reporter": "3.0.3",
Expand All @@ -83,6 +88,8 @@
"ts-node": "10.1.0",
"tslib": "^2.6.2",
"typescript": "5.6.3",
"vite-tsconfig-paths": "^5.0.1",
"vitest": "2.1.8",
"zone.js": "0.15.0"
},
"config": {
Expand Down
8 changes: 4 additions & 4 deletions projects/spectator/jest/test/matchers/matchers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,16 @@ describe('Matchers', () => {
});

it('should detect elements whose computed styles are display: none', () => {
window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'display' && 'none' });
window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'display' && 'none' }) as CSSStyleDeclaration;
expect(document.querySelector('#computed-style')).toBeHidden();
window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'display' && 'block' });
window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'display' && 'block' }) as CSSStyleDeclaration;
expect(document.querySelector('#computed-style')).toBeVisible();
});

it('should detect elements whose computed styles are visibility: hidden', () => {
window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'visibility' && 'hidden' });
window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'visibility' && 'hidden' }) as CSSStyleDeclaration;
expect(document.querySelector('#computed-style')).toBeHidden();
window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'visibility' && 'visible' });
window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'visibility' && 'visible' }) as CSSStyleDeclaration;
expect(document.querySelector('#computed-style')).toBeVisible();
});
});
Expand Down
25 changes: 25 additions & 0 deletions projects/spectator/setup-vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import '@analogjs/vitest-angular/setup-zone';

import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { getTestBed } from '@angular/core/testing';
import { defineGlobalsInjections } from '@ngneat/spectator';
import { TranslateService } from './test/translate.service';
import { TranslatePipe } from './test/translate.pipe';
import { vi } from 'vitest';

getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());

defineGlobalsInjections({
providers: [TranslateService],
declarations: [TranslatePipe],
});

beforeEach(() => {
const mockIntersectionObserver = vi.fn();
mockIntersectionObserver.mockReturnValue({
observe: () => null,
unobserve: () => null,
disconnect: () => null,
});
window.IntersectionObserver = mockIntersectionObserver;
});
10 changes: 5 additions & 5 deletions projects/spectator/src/lib/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export function addMatchers(matchers: Record<string, CustomMatcherFactory>): voi
if (typeof jasmine !== 'undefined') {
jasmine.addMatchers(matchers);
} else {
// Jest isn't on the global scope when using ESM so we
// assume that it's Jest if Jasmine is not defined
const jestExpectExtend = {};
// Jest (when using ESM) and Vitest aren't on the global scope so we
// assume that it's Jest or Vitest if Jasmine is not defined
const jestVitestExpectExtend = {};
for (const key of Object.keys(matchers)) {
if (key.startsWith('to')) jestExpectExtend[key] = matchers[key]().compare;
if (key.startsWith('to')) jestVitestExpectExtend[key] = matchers[key]().compare;
}

(expect as any).extend(jestExpectExtend);
(expect as any).extend(jestVitestExpectExtend);
}
}
16 changes: 11 additions & 5 deletions projects/spectator/test/query-root/query-root.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Overlay, OverlayModule } from '@angular/cdk/overlay';
import { Overlay, OverlayModule, OverlayRef } from '@angular/cdk/overlay';
import { ComponentPortal } from '@angular/cdk/portal';
import { Component } from '@angular/core';
import { Component, OnDestroy } from '@angular/core';

@Component({
selector: 'app-query-root',
Expand Down Expand Up @@ -43,13 +43,19 @@ import { Component } from '@angular/core';
</div>
`,
})
export class QueryRootComponent {
export class QueryRootComponent implements OnDestroy {
public constructor(private overlay: Overlay) {}

private overlayRef?: OverlayRef;

public openOverlay(): void {
const componentPortal = new ComponentPortal(QueryRootOverlayComponent);
const overlayRef = this.overlay.create();
overlayRef.attach(componentPortal);
this.overlayRef = this.overlay.create();
this.overlayRef.attach(componentPortal);
}

public ngOnDestroy(): void {
this.overlayRef?.dispose();
}
}

Expand Down
5 changes: 4 additions & 1 deletion projects/spectator/tsconfig.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
"@ngneat/spectator/jest": [
"jest/src/public_api.ts"
],
"@ngneat/spectator/vitest": [
"vitest/src/public_api.ts"
],
"@ngneat/spectator/internals": [
"internals/src/public_api.ts"
]
Expand All @@ -24,6 +27,6 @@
],
"include": [
"test/**/*.spec.ts",
"src/lib/matchers-types.ts"
"src/lib/matchers-types.ts",
]
}
23 changes: 23 additions & 0 deletions projects/spectator/vite.config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// <reference types="vitest" />
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

import angular from '@analogjs/vite-plugin-angular';
import * as path from 'node:path';

export default defineConfig(({ mode }) => ({
plugins: [
angular({tsconfig: path.join(import.meta.dirname, '/vitest/tsconfig.spec.json')}),
tsconfigPaths()
],
test: {
globals: true,
setupFiles: 'setup-vitest.ts',
environment: 'jsdom',
include: ['vitest/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
},
define: {
'import.meta.vitest': mode !== 'production',
},
}));
1 change: 1 addition & 0 deletions projects/spectator/vitest/ng-package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions projects/spectator/vitest/src/lib/dom-selectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { byAltText, byLabel, byPlaceholder, byText, byTextContent, byTitle, byValue, byTestId, byRole } from '@ngneat/spectator';
Loading

0 comments on commit 691c476

Please sign in to comment.