Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support Vitest mocking #686

Merged
merged 1 commit into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading