From b993a2823f76db39e20209c20594226c61754783 Mon Sep 17 00:00:00 2001 From: Tommy228 Date: Thu, 12 Dec 2024 14:49:30 +0100 Subject: [PATCH] feat: support Vitest mocking --- .eslintignore | 1 + .gitignore | 3 + README.md | 55 +- angular.json | 3 + docs/docs/vitest-support.md | 57 ++ package.json | 9 +- .../jest/test/matchers/matchers.spec.ts | 8 +- projects/spectator/setup-vitest.ts | 25 + projects/spectator/src/lib/core.ts | 10 +- .../test/query-root/query-root.component.ts | 16 +- projects/spectator/tsconfig.spec.json | 8 +- projects/spectator/vite.config.mts | 19 + projects/spectator/vitest/ng-package.json | 1 + .../spectator/vitest/src/lib/dom-selectors.ts | 1 + .../vitest/src/lib/matchers-types.ts | 64 +++ projects/spectator/vitest/src/lib/mock.ts | 47 ++ .../vitest/src/lib/spectator-directive.ts | 58 ++ .../vitest/src/lib/spectator-host.ts | 54 ++ .../vitest/src/lib/spectator-http.ts | 39 ++ .../vitest/src/lib/spectator-pipe.ts | 39 ++ .../vitest/src/lib/spectator-routing.ts | 35 ++ .../vitest/src/lib/spectator-service.ts | 33 ++ .../spectator/vitest/src/lib/spectator.ts | 29 + projects/spectator/vitest/src/public_api.ts | 11 + .../async-input/async-input.component.spec.ts | 33 ++ .../vitest/test/async/async.component.spec.ts | 23 + .../vitest/test/auth.service.spec.ts | 30 + .../vitest/test/auto-focus.directive.spec.ts | 82 +++ .../test/button/button.component.spec.ts | 69 +++ .../vitest/test/calc.component.spec.ts | 23 + .../vitest/test/click/click.component.spec.ts | 33 ++ .../consume-dynamic.component.spec.ts | 24 + .../vitest/test/consumer.service.spec.ts | 33 ++ .../spectator/vitest/test/defer-block.spec.ts | 259 +++++++++ .../dom-selectors.component.spec.ts | 173 ++++++ .../vitest/test/fg/fg.component.spec.ts | 31 ++ .../test/focus/test-focus.component.spec.ts | 41 ++ .../form-input/form-input.component.spec.ts | 56 ++ .../form-select/form-select.component.spec.ts | 22 + .../test/form-select/form-select.component.ts | 22 + .../function-output.component.spec.ts | 47 ++ .../vitest/test/hello/hello.component.spec.ts | 40 ++ .../vitest/test/highlight.directive.spec.ts | 48 ++ .../vitest/test/injection-and-mocking.spec.ts | 174 ++++++ .../vitest/test/matchers/matchers.spec.ts | 186 +++++++ projects/spectator/vitest/test/mock.spec.ts | 16 + .../ngonchanges-input.component.spec.ts | 25 + ...no-overwritten-providers.component.spec.ts | 38 ++ .../vitest/test/override-component.spec.ts | 111 ++++ .../vitest/test/override-directive.spec.ts | 66 +++ .../vitest/test/override-module.spec.ts | 79 +++ .../vitest/test/override-pipe.spec.ts | 67 +++ .../override-typesafety.component.spec.ts | 116 ++++ .../query-root/query-root.component.spec.ts | 34 ++ .../vitest/test/set-input-alias-names.spec.ts | 72 +++ .../signal-input.component.spec.ts | 40 ++ .../vitest/test/spy-object/spy-object.spec.ts | 42 ++ .../component/standalone.component.spec.ts | 37 ++ .../directive/standalone.directive.spec.ts | 21 + .../standalone/pipe/standalone.pipe.spec.ts | 37 ++ .../spectator/vitest/test/teardown/error.ts | 3 + .../test/teardown/teardown.component.spec.ts | 89 +++ .../test/teardown/teardown.component.ts | 22 + .../vitest/test/teardown/teardown.service.ts | 6 + .../vitest/test/todos-data.service.spec.ts | 49 ++ .../test/unless/unless.component.spec.ts | 24 + .../view-children.component.spec.ts | 73 +++ .../vitest/test/widget.service.spec.ts | 18 + .../test/widget/widget.component.spec.ts | 25 + .../with-routing/my-page.component.spec.ts | 99 ++++ .../vitest/test/zippy/zippy.component.spec.ts | 161 ++++++ projects/spectator/vitest/tsconfig.spec.json | 17 + tsconfig.json | 7 +- yarn.lock | 523 +++++++++++++++++- 74 files changed, 3963 insertions(+), 28 deletions(-) create mode 100644 docs/docs/vitest-support.md create mode 100644 projects/spectator/setup-vitest.ts create mode 100644 projects/spectator/vite.config.mts create mode 100644 projects/spectator/vitest/ng-package.json create mode 100644 projects/spectator/vitest/src/lib/dom-selectors.ts create mode 100644 projects/spectator/vitest/src/lib/matchers-types.ts create mode 100644 projects/spectator/vitest/src/lib/mock.ts create mode 100644 projects/spectator/vitest/src/lib/spectator-directive.ts create mode 100644 projects/spectator/vitest/src/lib/spectator-host.ts create mode 100644 projects/spectator/vitest/src/lib/spectator-http.ts create mode 100644 projects/spectator/vitest/src/lib/spectator-pipe.ts create mode 100644 projects/spectator/vitest/src/lib/spectator-routing.ts create mode 100644 projects/spectator/vitest/src/lib/spectator-service.ts create mode 100644 projects/spectator/vitest/src/lib/spectator.ts create mode 100644 projects/spectator/vitest/src/public_api.ts create mode 100644 projects/spectator/vitest/test/async-input/async-input.component.spec.ts create mode 100644 projects/spectator/vitest/test/async/async.component.spec.ts create mode 100644 projects/spectator/vitest/test/auth.service.spec.ts create mode 100644 projects/spectator/vitest/test/auto-focus.directive.spec.ts create mode 100644 projects/spectator/vitest/test/button/button.component.spec.ts create mode 100644 projects/spectator/vitest/test/calc.component.spec.ts create mode 100644 projects/spectator/vitest/test/click/click.component.spec.ts create mode 100644 projects/spectator/vitest/test/consum-dynamic/consume-dynamic.component.spec.ts create mode 100644 projects/spectator/vitest/test/consumer.service.spec.ts create mode 100644 projects/spectator/vitest/test/defer-block.spec.ts create mode 100644 projects/spectator/vitest/test/dom-selectors/dom-selectors.component.spec.ts create mode 100644 projects/spectator/vitest/test/fg/fg.component.spec.ts create mode 100644 projects/spectator/vitest/test/focus/test-focus.component.spec.ts create mode 100644 projects/spectator/vitest/test/form-input/form-input.component.spec.ts create mode 100644 projects/spectator/vitest/test/form-select/form-select.component.spec.ts create mode 100644 projects/spectator/vitest/test/form-select/form-select.component.ts create mode 100644 projects/spectator/vitest/test/function-output/function-output.component.spec.ts create mode 100644 projects/spectator/vitest/test/hello/hello.component.spec.ts create mode 100644 projects/spectator/vitest/test/highlight.directive.spec.ts create mode 100644 projects/spectator/vitest/test/injection-and-mocking.spec.ts create mode 100644 projects/spectator/vitest/test/matchers/matchers.spec.ts create mode 100644 projects/spectator/vitest/test/mock.spec.ts create mode 100644 projects/spectator/vitest/test/ngonchanges-input/ngonchanges-input.component.spec.ts create mode 100644 projects/spectator/vitest/test/no-overwritten-providers/no-overwritten-providers.component.spec.ts create mode 100644 projects/spectator/vitest/test/override-component.spec.ts create mode 100644 projects/spectator/vitest/test/override-directive.spec.ts create mode 100644 projects/spectator/vitest/test/override-module.spec.ts create mode 100644 projects/spectator/vitest/test/override-pipe.spec.ts create mode 100644 projects/spectator/vitest/test/override-typesafety.component.spec.ts create mode 100644 projects/spectator/vitest/test/query-root/query-root.component.spec.ts create mode 100644 projects/spectator/vitest/test/set-input-alias-names.spec.ts create mode 100644 projects/spectator/vitest/test/signal-input/signal-input.component.spec.ts create mode 100644 projects/spectator/vitest/test/spy-object/spy-object.spec.ts create mode 100644 projects/spectator/vitest/test/standalone/component/standalone.component.spec.ts create mode 100644 projects/spectator/vitest/test/standalone/directive/standalone.directive.spec.ts create mode 100644 projects/spectator/vitest/test/standalone/pipe/standalone.pipe.spec.ts create mode 100644 projects/spectator/vitest/test/teardown/error.ts create mode 100644 projects/spectator/vitest/test/teardown/teardown.component.spec.ts create mode 100644 projects/spectator/vitest/test/teardown/teardown.component.ts create mode 100644 projects/spectator/vitest/test/teardown/teardown.service.ts create mode 100644 projects/spectator/vitest/test/todos-data.service.spec.ts create mode 100644 projects/spectator/vitest/test/unless/unless.component.spec.ts create mode 100644 projects/spectator/vitest/test/view-children/view-children.component.spec.ts create mode 100644 projects/spectator/vitest/test/widget.service.spec.ts create mode 100644 projects/spectator/vitest/test/widget/widget.component.spec.ts create mode 100644 projects/spectator/vitest/test/with-routing/my-page.component.spec.ts create mode 100644 projects/spectator/vitest/test/zippy/zippy.component.spec.ts create mode 100644 projects/spectator/vitest/tsconfig.spec.json diff --git a/.eslintignore b/.eslintignore index 5afe1ef7..ac3a0982 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ projects/spectator/test/**/*.ts projects/spectator/jest/test/**/*.ts +projects/spectator/vitest/test/**/*.ts projects/spectator/schematics/**/*.* diff --git a/.gitignore b/.gitignore index a6143fee..cc888dd3 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ testem.log # System Files .DS_Store Thumbs.db + +# vitest +vite.config.mts.timestamp-*.mjs diff --git a/README.md b/README.md index 619870eb..88f738a7 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ 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 @@ -27,6 +27,7 @@ Spectator helps you get rid of all the boilerplate grunt work, leaving you with - ✅ Auto-mocking providers - ✅ Strongly typed - ✅ Jest Support +- ✅ Vitest Support ## Sponsoring ngneat @@ -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) @@ -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; + const createService = createServiceFactory({ + service: AuthService, + mocks: [DateService] + }); + + beforeEach(() => spectator = createService()); + + it('should not be logged in', () => { + const dateService = spectator.inject(DateService); + dateService.isExpired.mockReturnValue(true); + expect(spectator.service.isLoggedIn()).toBeFalsy(); + }); + + it('should be logged in', () => { + const dateService = spectator.inject(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: diff --git a/angular.json b/angular.json index 482acf1c..cbf69425 100644 --- a/angular.json +++ b/angular.json @@ -41,6 +41,9 @@ "builder": "@angular-builders/jest:run", "options": {} }, + "test-vitest": { + "builder": "@analogjs/vitest-angular:test" + }, "lint": { "builder": "@angular-eslint/builder:lint", "options": { diff --git a/docs/docs/vitest-support.md b/docs/docs/vitest-support.md new file mode 100644 index 00000000..1bac5965 --- /dev/null +++ b/docs/docs/vitest-support.md @@ -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; + const createService = createServiceFactory({ + service: AuthService, + mocks: [DateService] + }); + + beforeEach(() => spectator = createService()); + + it('should not be logged in', () => { + const dateService = spectator.inject(DateService); + dateService.isExpired.mockReturnValue(true); + expect(spectator.service.isLoggedIn()).toBeFalsy(); + }); + + it('should be logged in', () => { + const dateService = spectator.inject(DateService); + dateService.isExpired.mockReturnValue(false); + expect(spectator.service.isLoggedIn()).toBeTruthy(); + }); +}); +``` diff --git a/package.json b/package.json index 061c9641..7f3ac761 100644 --- a/package.json +++ b/package.json @@ -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 --silent", "lint": "ng lint", "format": "prettier --write \"{projects,src}/**/*.ts\"", "commit": "git-cz", @@ -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", @@ -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", @@ -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", @@ -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": { diff --git a/projects/spectator/jest/test/matchers/matchers.spec.ts b/projects/spectator/jest/test/matchers/matchers.spec.ts index 96aecd58..1ad64e13 100644 --- a/projects/spectator/jest/test/matchers/matchers.spec.ts +++ b/projects/spectator/jest/test/matchers/matchers.spec.ts @@ -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(); }); }); diff --git a/projects/spectator/setup-vitest.ts b/projects/spectator/setup-vitest.ts new file mode 100644 index 00000000..7be7ffdc --- /dev/null +++ b/projects/spectator/setup-vitest.ts @@ -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; +}); diff --git a/projects/spectator/src/lib/core.ts b/projects/spectator/src/lib/core.ts index 80ef6cd1..57e2f80f 100644 --- a/projects/spectator/src/lib/core.ts +++ b/projects/spectator/src/lib/core.ts @@ -8,13 +8,13 @@ export function addMatchers(matchers: Record): 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); } } diff --git a/projects/spectator/test/query-root/query-root.component.ts b/projects/spectator/test/query-root/query-root.component.ts index df13db0d..ae2a45d3 100644 --- a/projects/spectator/test/query-root/query-root.component.ts +++ b/projects/spectator/test/query-root/query-root.component.ts @@ -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', @@ -43,13 +43,19 @@ import { Component } from '@angular/core'; `, }) -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(); } } diff --git a/projects/spectator/tsconfig.spec.json b/projects/spectator/tsconfig.spec.json index d45dea1e..4a341d23 100644 --- a/projects/spectator/tsconfig.spec.json +++ b/projects/spectator/tsconfig.spec.json @@ -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" ] @@ -24,6 +27,7 @@ ], "include": [ "test/**/*.spec.ts", - "src/lib/matchers-types.ts" - ] + "src/lib/matchers-types.ts", + ], + "exclude": [] } diff --git a/projects/spectator/vite.config.mts b/projects/spectator/vite.config.mts new file mode 100644 index 00000000..af421d6e --- /dev/null +++ b/projects/spectator/vite.config.mts @@ -0,0 +1,19 @@ +// +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +import angular from '@analogjs/vite-plugin-angular'; + +export default defineConfig(({ mode }) => ({ + plugins: [angular({tsconfig: __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', + }, +})); diff --git a/projects/spectator/vitest/ng-package.json b/projects/spectator/vitest/ng-package.json new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/projects/spectator/vitest/ng-package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/projects/spectator/vitest/src/lib/dom-selectors.ts b/projects/spectator/vitest/src/lib/dom-selectors.ts new file mode 100644 index 00000000..34f71453 --- /dev/null +++ b/projects/spectator/vitest/src/lib/dom-selectors.ts @@ -0,0 +1 @@ +export { byAltText, byLabel, byPlaceholder, byText, byTextContent, byTitle, byValue, byTestId, byRole } from '@ngneat/spectator'; diff --git a/projects/spectator/vitest/src/lib/matchers-types.ts b/projects/spectator/vitest/src/lib/matchers-types.ts new file mode 100644 index 00000000..650d6e5e --- /dev/null +++ b/projects/spectator/vitest/src/lib/matchers-types.ts @@ -0,0 +1,64 @@ +export * from 'vitest'; + +interface CustomMatchers { + toExist(): R; + + toHaveLength(expected: number): R; + + toHaveId(id: string | number): R; + + toHaveClass(className: string | string[], options?: { strict: boolean }): R; + + toHaveAttribute(attr: string | object, val?: string): R; + + toHaveProperty(prop: string | object, val?: string | boolean): R; + + toContainProperty(prop: string | object, val?: string): R; + + toHaveText(text: string | string[] | ((text: string) => boolean), exact?: boolean): R; + + toContainText(text: string | string[] | ((text: string) => boolean), exact?: boolean): R; + + toHaveExactText(text: string | string[] | ((text: string) => boolean), options?: { trim: boolean }): R; + + toHaveExactTrimmedText(text: string | string[] | ((text: string) => boolean)): R; + + toHaveValue(value: string | string[]): R; + + toContainValue(value: string | string[]): R; + + toHaveStyle(style: { [styleKey: string]: any }): R; + + toHaveData({ data, val }: { data: string; val: string }): R; + + toBeChecked(): R; + + toBeIndeterminate(): R; + + toBeDisabled(): R; + + toBeEmpty(): R; + + toBePartial(partial: object): R; + + toBeHidden(): R; + + toBeSelected(): R; + + toBeVisible(): R; + + toBeFocused(): R; + + toBeMatchedBy(selector: string | Element): R; + + toHaveDescendant(selector: string | Element): R; + + toHaveDescendantWithText({ selector, text }: { selector: string; text: string }): R; + + toHaveSelectedOptions(expected: string | string[] | HTMLOptionElement | HTMLOptionElement[]): R; +} + +declare module 'vitest' { + interface Assertion extends CustomMatchers {} + interface AsymmetricMatchersContaining extends CustomMatchers {} +} diff --git a/projects/spectator/vitest/src/lib/mock.ts b/projects/spectator/vitest/src/lib/mock.ts new file mode 100644 index 00000000..32330f7f --- /dev/null +++ b/projects/spectator/vitest/src/lib/mock.ts @@ -0,0 +1,47 @@ +import { FactoryProvider, AbstractType, Type } from '@angular/core'; +import { installProtoMethods, CompatibleSpy, SpyObject as BaseSpyObject } from '@ngneat/spectator'; +import { Mock, vi } from 'vitest'; + +export type SpyObject = BaseSpyObject & { + [P in keyof T]: T[P] & (T[P] extends (...args: any[]) => infer R ? (R extends (...args: any[]) => any ? Mock : Mock) : T[P]); +}; + +/** + * @publicApi + */ +export function createSpyObject(type: Type | AbstractType, template?: Partial>): SpyObject { + const mock: any = { ...template }; + + installProtoMethods(mock, type.prototype, () => { + const viFn = vi.fn(); + const newSpy: CompatibleSpy = viFn as any; + + newSpy.andCallFake = (fn: Function) => { + viFn.mockImplementation(fn as (...args: any[]) => any); + + return newSpy; + }; + + newSpy.andReturn = (val: any) => { + viFn.mockReturnValue(val); + }; + + newSpy.reset = () => { + viFn.mockReset(); + }; + + return newSpy; + }); + + return mock; +} + +/** + * @publicApi + */ +export function mockProvider(type: Type | AbstractType, properties?: Partial>): FactoryProvider { + return { + provide: type, + useFactory: () => createSpyObject(type, properties), + }; +} diff --git a/projects/spectator/vitest/src/lib/spectator-directive.ts b/projects/spectator/vitest/src/lib/spectator-directive.ts new file mode 100644 index 00000000..6950d4bc --- /dev/null +++ b/projects/spectator/vitest/src/lib/spectator-directive.ts @@ -0,0 +1,58 @@ +import { Type } from '@angular/core'; +import { + createDirectiveFactory as baseCreateDirectiveFactory, + SpectatorDirective as BaseSpectatorDirective, + HostComponent, + isType, + SpectatorDirectiveOptions, + SpectatorDirectiveOverrides, + Token, +} from '@ngneat/spectator'; + +import { mockProvider, SpyObject } from './mock'; + +/** + * @publicApi + */ +export class SpectatorDirective extends BaseSpectatorDirective { + public inject(token: Token, fromComponentInjector: boolean = false): SpyObject { + return super.inject(token, fromComponentInjector) as SpyObject; + } +} + +/** + * @publicApi + */ +export type SpectatorDirectiveFactory = ( + template: string, + overrides?: SpectatorDirectiveOverrides, +) => SpectatorDirective; + +/** + * @publicApi + */ +export type PresetSpectatorDirectiveFactory = ( + template?: string, + overrides?: SpectatorDirectiveOverrides, +) => SpectatorDirective; + +/** + * @publicApi + */ +export function createDirectiveFactory( + options: SpectatorDirectiveOptions & { template: string }, +): PresetSpectatorDirectiveFactory; +/** + * @publicApi + */ +export function createDirectiveFactory( + typeOrOptions: Type | SpectatorDirectiveOptions, +): SpectatorDirectiveFactory; +export function createDirectiveFactory( + typeOrOptions: Type | SpectatorDirectiveOptions, +): SpectatorDirectiveFactory { + return baseCreateDirectiveFactory({ + mockProvider, + ...(isType(typeOrOptions) ? { directive: typeOrOptions } : typeOrOptions), + }) as SpectatorDirectiveFactory; +} diff --git a/projects/spectator/vitest/src/lib/spectator-host.ts b/projects/spectator/vitest/src/lib/spectator-host.ts new file mode 100644 index 00000000..ca9eb371 --- /dev/null +++ b/projects/spectator/vitest/src/lib/spectator-host.ts @@ -0,0 +1,54 @@ +import { Type } from '@angular/core'; +import { + createHostFactory as baseCreateHostFactory, + SpectatorHost as BaseSpectatorHost, + HostComponent, + isType, + SpectatorHostOptions, + SpectatorHostOverrides, + Token, +} from '@ngneat/spectator'; + +import { mockProvider, SpyObject } from './mock'; + +/** + * @publicApi + */ +export class SpectatorHost extends BaseSpectatorHost { + public inject(token: Token, fromComponentInjector: boolean = false): SpyObject { + return super.inject(token, fromComponentInjector) as SpyObject; + } +} + +/** + * @publicApi + */ +export type SpectatorHostFactory = ( + template: string, + overrides?: SpectatorHostOverrides, +) => SpectatorHost; + +/** + * @publicApi + */ +export type PresetSpectatorHostFactory = ( + template?: string, + overrides?: SpectatorHostOverrides, +) => SpectatorHost; + +/** + * @publicApi + */ +export function createHostFactory( + options: SpectatorHostOptions & { template: string }, +): PresetSpectatorHostFactory; +/** + * @publicApi + */ +export function createHostFactory(typeOrOptions: Type | SpectatorHostOptions): SpectatorHostFactory; +export function createHostFactory(typeOrOptions: Type | SpectatorHostOptions): SpectatorHostFactory { + return baseCreateHostFactory({ + mockProvider, + ...(isType(typeOrOptions) ? { component: typeOrOptions } : typeOrOptions), + }) as SpectatorHostFactory; +} diff --git a/projects/spectator/vitest/src/lib/spectator-http.ts b/projects/spectator/vitest/src/lib/spectator-http.ts new file mode 100644 index 00000000..e73b2c3e --- /dev/null +++ b/projects/spectator/vitest/src/lib/spectator-http.ts @@ -0,0 +1,39 @@ +import { Type } from '@angular/core'; +import { + createHttpFactory as baseCreateHttpFactory, + isType, + CreateHttpOverrides, + HttpMethod, + SpectatorHttp as BaseSpectatorHttp, + SpectatorHttpOptions, + Token, +} from '@ngneat/spectator'; + +import { mockProvider, SpyObject } from './mock'; + +/** + * @publicApi + */ +export interface SpectatorHttp extends BaseSpectatorHttp { + inject(token: Token): SpyObject; +} + +/** + * @publicApi + */ +export { HttpMethod }; + +/** + * @pubicApi + */ +export type SpectatorHttpFactory = (overrides?: CreateHttpOverrides) => SpectatorHttp; + +/** + * @publicApi + */ +export function createHttpFactory(typeOrOptions: SpectatorHttpOptions | Type): SpectatorHttpFactory { + return baseCreateHttpFactory({ + mockProvider, + ...(isType(typeOrOptions) ? { service: typeOrOptions } : typeOrOptions), + }) as SpectatorHttpFactory; +} diff --git a/projects/spectator/vitest/src/lib/spectator-pipe.ts b/projects/spectator/vitest/src/lib/spectator-pipe.ts new file mode 100644 index 00000000..3da22582 --- /dev/null +++ b/projects/spectator/vitest/src/lib/spectator-pipe.ts @@ -0,0 +1,39 @@ +import { Type } from '@angular/core'; +import { + createPipeFactory as baseCreatePipeFactory, + isType, + HostComponent, + SpectatorPipe as BaseSpectatorPipe, + SpectatorPipeOptions, + SpectatorPipeOverrides, + Token, +} from '@ngneat/spectator'; + +import { mockProvider, SpyObject } from './mock'; + +/** + * @publicApi + */ +export class SpectatorPipe extends BaseSpectatorPipe { + public inject(token: Token): SpyObject { + return super.inject(token) as SpyObject; + } +} + +/** + * @publicApi + */ +export type SpectatorPipeFactory = ( + templateOrOverrides?: string | SpectatorPipeOverrides, + overrides?: SpectatorPipeOverrides, +) => SpectatorPipe; + +/** + * @publicApi + */ +export function createPipeFactory(typeOrOptions: Type

| SpectatorPipeOptions): SpectatorPipeFactory { + return baseCreatePipeFactory({ + mockProvider, + ...(isType(typeOrOptions) ? { pipe: typeOrOptions } : typeOrOptions), + }) as SpectatorPipeFactory; +} diff --git a/projects/spectator/vitest/src/lib/spectator-routing.ts b/projects/spectator/vitest/src/lib/spectator-routing.ts new file mode 100644 index 00000000..d8bd9019 --- /dev/null +++ b/projects/spectator/vitest/src/lib/spectator-routing.ts @@ -0,0 +1,35 @@ +import { Type } from '@angular/core'; +import { + createRoutingFactory as baseCreateRoutingFactory, + isType, + SpectatorRouting as BaseSpectatorRouting, + SpectatorRoutingOptions, + SpectatorRoutingOverrides, + Token, +} from '@ngneat/spectator'; + +import { mockProvider, SpyObject } from './mock'; + +/** + * @publicApi + */ +export class SpectatorRouting extends BaseSpectatorRouting { + public inject(token: Token, fromComponentInjector: boolean = false): SpyObject { + return super.inject(token, fromComponentInjector) as SpyObject; + } +} + +/** + * @publicApi + */ +export type SpectatorRoutingFactory = (overrides?: SpectatorRoutingOverrides) => SpectatorRouting; + +/** + * @publicApi + */ +export function createRoutingFactory(typeOrOptions: SpectatorRoutingOptions | Type): SpectatorRoutingFactory { + return baseCreateRoutingFactory({ + mockProvider, + ...(isType(typeOrOptions) ? { component: typeOrOptions } : typeOrOptions), + }) as SpectatorRoutingFactory; +} diff --git a/projects/spectator/vitest/src/lib/spectator-service.ts b/projects/spectator/vitest/src/lib/spectator-service.ts new file mode 100644 index 00000000..5f5629eb --- /dev/null +++ b/projects/spectator/vitest/src/lib/spectator-service.ts @@ -0,0 +1,33 @@ +import { Type, InjectionToken, AbstractType } from '@angular/core'; +import { + createServiceFactory as baseCreateServiceFactory, + isType, + SpectatorServiceOverrides, + SpectatorServiceOptions, + SpectatorService as BaseSpectatorService, + Token, +} from '@ngneat/spectator'; + +import { mockProvider, SpyObject } from './mock'; + +/** + * @publicApi + */ +export interface SpectatorService extends BaseSpectatorService { + inject(token: Type | InjectionToken | AbstractType): SpyObject; +} + +/** + * @publicApi + */ +export type SpectatorServiceFactory = (overrides?: SpectatorServiceOverrides) => SpectatorService; + +/** + * @publicApi + */ +export function createServiceFactory(typeOrOptions: SpectatorServiceOptions | Type): SpectatorServiceFactory { + return baseCreateServiceFactory({ + mockProvider, + ...(isType(typeOrOptions) ? { service: typeOrOptions } : typeOrOptions), + }) as SpectatorServiceFactory; +} diff --git a/projects/spectator/vitest/src/lib/spectator.ts b/projects/spectator/vitest/src/lib/spectator.ts new file mode 100644 index 00000000..cbefc4ee --- /dev/null +++ b/projects/spectator/vitest/src/lib/spectator.ts @@ -0,0 +1,29 @@ +import { Type } from '@angular/core'; +import { + createComponentFactory as baseCreateComponentFactory, + isType, + Spectator as BaseSpectator, + SpectatorOptions, + SpectatorOverrides, + Token, +} from '@ngneat/spectator'; + +import { mockProvider, SpyObject } from './mock'; + +/** + * @publicApi + */ +export type SpectatorFactory = (options?: SpectatorOverrides) => Spectator; + +export function createComponentFactory(typeOrOptions: SpectatorOptions | Type): SpectatorFactory { + return baseCreateComponentFactory({ + mockProvider, + ...(isType(typeOrOptions) ? { component: typeOrOptions } : typeOrOptions), + }) as SpectatorFactory; +} + +export class Spectator extends BaseSpectator { + public inject(token: Token, fromComponentInjector: boolean = false): SpyObject { + return super.inject(token, fromComponentInjector) as SpyObject; + } +} diff --git a/projects/spectator/vitest/src/public_api.ts b/projects/spectator/vitest/src/public_api.ts new file mode 100644 index 00000000..4db5a40c --- /dev/null +++ b/projects/spectator/vitest/src/public_api.ts @@ -0,0 +1,11 @@ +/// +/// +export * from './lib/dom-selectors'; +export * from './lib/mock'; +export * from './lib/spectator'; +export * from './lib/spectator-http'; +export * from './lib/spectator-directive'; +export * from './lib/spectator-service'; +export * from './lib/spectator-host'; +export * from './lib/spectator-routing'; +export * from './lib/spectator-pipe'; diff --git a/projects/spectator/vitest/test/async-input/async-input.component.spec.ts b/projects/spectator/vitest/test/async-input/async-input.component.spec.ts new file mode 100644 index 00000000..e0cbf6f0 --- /dev/null +++ b/projects/spectator/vitest/test/async-input/async-input.component.spec.ts @@ -0,0 +1,33 @@ +import { fakeAsync } from '@angular/core/testing'; +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; + +import { AsyncInputComponent } from '../../../test/async-input/async-input.component'; + +describe('ZippyComponent', () => { + let host: SpectatorHost; + + const createHost = createHostFactory(AsyncInputComponent); + + it('should work', () => { + const { component } = createHost(``); + expect(component).toBeDefined(); + }); + + it('should not be visible', () => { + host = createHost(``); + host.setHostInput('widgets', ''); + expect(host.query('div')).not.toExist(); + }); + + it('should be visible', fakeAsync(() => { + host = createHost(``, { + detectChanges: true, + hostProps: { + widgets: '', + }, + }); + host.tick(); + host.detectChanges(); + expect(host.query('div')).toExist(); + })); +}); diff --git a/projects/spectator/vitest/test/async/async.component.spec.ts b/projects/spectator/vitest/test/async/async.component.spec.ts new file mode 100644 index 00000000..c6bbb229 --- /dev/null +++ b/projects/spectator/vitest/test/async/async.component.spec.ts @@ -0,0 +1,23 @@ +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; + +import { AsyncComponent } from '../../../test/async/async.component'; +import { QueryService } from '../../../test/query.service'; + +describe('ZippyComponent', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: AsyncComponent, + mocks: [QueryService], + }); + + it('should work', () => { + const { component } = createHost(``); + expect(component).toBeDefined(); + }); + + it('should be falsy', () => { + host = createHost(``); + expect(host.query('p')).not.toExist(); + }); +}); diff --git a/projects/spectator/vitest/test/auth.service.spec.ts b/projects/spectator/vitest/test/auth.service.spec.ts new file mode 100644 index 00000000..bd2f0ca1 --- /dev/null +++ b/projects/spectator/vitest/test/auth.service.spec.ts @@ -0,0 +1,30 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; + +import { AuthService } from '../../test/auth.service'; +import { DateService } from '../../test/date.service'; + +describe('AuthService', () => { + it('should ', () => { + expect(true).toBeTruthy(); + }); + + let spectator: SpectatorService; + const createService = createServiceFactory({ + service: AuthService, + mocks: [DateService], + }); + + beforeEach(() => (spectator = createService())); + + it('should not be logged in', () => { + const dateService = spectator.inject(DateService); + dateService.isExpired.mockReturnValue(true); + expect(spectator.service.isLoggedIn()).toBeFalsy(); + }); + + it('should be logged in', () => { + const dateService = spectator.inject(DateService); + dateService.isExpired.mockReturnValue(false); + expect(spectator.service.isLoggedIn()).toBeTruthy(); + }); +}); diff --git a/projects/spectator/vitest/test/auto-focus.directive.spec.ts b/projects/spectator/vitest/test/auto-focus.directive.spec.ts new file mode 100644 index 00000000..7b452be4 --- /dev/null +++ b/projects/spectator/vitest/test/auto-focus.directive.spec.ts @@ -0,0 +1,82 @@ +import { Component } from '@angular/core'; +import { createDirectiveFactory, createHostFactory, SpectatorDirective, SpectatorHost } from '@ngneat/spectator/vitest'; + +import { AutoFocusDirective } from '../../test/auto-focus/auto-focus.directive'; + +@Component({ + selector: 'custom-host', + template: '', + standalone: false, +}) +class CustomHostComponent { + public isFocused = false; +} + +describe('DatoAutoFocusDirective', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: AutoFocusDirective, + host: CustomHostComponent, + }); + + it('should be focused', () => { + host = createHost(``); + const instance = host.queryHost(AutoFocusDirective); + expect(host.element).toBeFocused(); + }); + + it('should NOT be focused', () => { + host = createHost(``); + expect(host.element).not.toBeFocused(); + }); + + it('should work with dynamic input', () => { + host = createHost(``); + expect(host.element).not.toBeFocused(); + host.setHostInput({ isFocused: true }); + expect(host.element).toBeFocused(); + }); + + it('should be able to type in input', () => { + host = createHost(``); + + host.typeInElement('foo'); + expect(host.element).toHaveValue('foo'); + }); +}); +describe('DatoAutoFocusDirective (createHostDirectiveFactory)', () => { + let host: SpectatorDirective; + + const createHost = createDirectiveFactory({ + directive: AutoFocusDirective, + host: CustomHostComponent, + }); + + it('should be focused', () => { + host = createHost(``); + const instance1 = host.query(AutoFocusDirective); + const instance2 = host.directive; + expect(instance1).toBe(instance2); + expect(host.element).toBeFocused(); + }); + + it('should NOT be focused', () => { + host = createHost(``); + expect(host.element).not.toBeFocused(); + }); + + it('should work with dynamic input', () => { + host = createHost(``); + expect(host.element).not.toBeFocused(); + host.setHostInput({ isFocused: true }); + expect(host.element).toBeFocused(); + }); + + it('should be able to type in input', () => { + host = createHost(``); + + host.typeInElement('foo'); + expect(host.element).toHaveValue('foo'); + }); +}); diff --git a/projects/spectator/vitest/test/button/button.component.spec.ts b/projects/spectator/vitest/test/button/button.component.spec.ts new file mode 100644 index 00000000..9bfc3651 --- /dev/null +++ b/projects/spectator/vitest/test/button/button.component.spec.ts @@ -0,0 +1,69 @@ +import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/vitest'; +import { of } from 'rxjs'; + +import { ButtonComponent } from '../../../test/button/button.component'; +import { QueryService } from '../../../test/query.service'; + +describe('ButtonComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: ButtonComponent, + componentProviders: [mockProvider(QueryService)], + }); + + beforeEach(() => (spectator = createComponent())); + + it('should set the "success" class by default', () => { + expect(spectator.query('button')).toHaveClass('success'); + }); + + it('should set the class name according to the [className]', () => { + spectator.setInput('className', 'danger'); + expect(spectator.query('button')).toHaveClass('danger'); + expect(spectator.query('button')).not.toHaveClass('success'); + }); + + it('should set the title according to the [title]', () => { + spectator = createComponent({ + props: { title: 'Click' }, + }); + + expect(spectator.query('button')).toHaveText('Click'); + }); + + it('should emit the $event on click', () => { + let output; + spectator.output<{ type: string }>('click').subscribe((result) => (output = result)); + + spectator.component.onClick({ type: 'click' }); + expect(output).toEqual({ type: 'click' }); + }); + + it('should mock the service', () => { + spectator = createComponent({ + detectChanges: false, + }); + spectator.inject(QueryService, true).selectName.mockReturnValue(of('Netanel')); + spectator.detectChanges(); + expect(spectator.query('p')).toHaveText('Netanel'); + }); +}); + +describe('ButtonComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: ButtonComponent, + componentProviders: [mockProvider(QueryService)], + detectChanges: false, + }); + + beforeEach(() => (spectator = createComponent())); + + it('should not run cd by default', () => { + expect(spectator.query('button')).not.toHaveClass('success'); + spectator.detectChanges(); + expect(spectator.query('button')).toHaveClass('success'); + }); +}); diff --git a/projects/spectator/vitest/test/calc.component.spec.ts b/projects/spectator/vitest/test/calc.component.spec.ts new file mode 100644 index 00000000..8ba52c5c --- /dev/null +++ b/projects/spectator/vitest/test/calc.component.spec.ts @@ -0,0 +1,23 @@ +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; + +import { CalcComponent } from '../../test/calc/calc.component'; + +describe('CalcComponent', () => { + let spectator: Spectator; + const createComponent = createComponentFactory(CalcComponent); + + it('should be defined', () => { + spectator = createComponent(); + expect(spectator.component).toBeTruthy(); + }); + + it('should calc the value', () => { + spectator = createComponent(); + const a = spectator.query('.a') as HTMLInputElement; + const b = spectator.query('.b') as HTMLInputElement; + spectator.typeInElement('1', a); + spectator.typeInElement('2', b); + + expect(spectator.query('.result')).toHaveText('12'); + }); +}); diff --git a/projects/spectator/vitest/test/click/click.component.spec.ts b/projects/spectator/vitest/test/click/click.component.spec.ts new file mode 100644 index 00000000..6f45a469 --- /dev/null +++ b/projects/spectator/vitest/test/click/click.component.spec.ts @@ -0,0 +1,33 @@ +import { fakeAsync } from '@angular/core/testing'; +import { byText, createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; +import { ClickComponent } from '../../../test/click/click.component'; + +describe('ClickComponent', () => { + let component: ClickComponent; + let spectator: Spectator; + + const createComponent = createComponentFactory(ClickComponent); + + beforeEach(() => { + spectator = createComponent(); + component = spectator.component; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should changed on click with click query shorthand', fakeAsync(() => { + spectator.click('button'); + spectator.tick(100); + + expect('p').toHaveText('changed'); + })); + + it('should changed on click with click dom selector', fakeAsync(() => { + spectator.click(byText('Change')); + spectator.tick(100); + + expect('p').toHaveText('changed'); + })); +}); diff --git a/projects/spectator/vitest/test/consum-dynamic/consume-dynamic.component.spec.ts b/projects/spectator/vitest/test/consum-dynamic/consume-dynamic.component.spec.ts new file mode 100644 index 00000000..6933ebd4 --- /dev/null +++ b/projects/spectator/vitest/test/consum-dynamic/consume-dynamic.component.spec.ts @@ -0,0 +1,24 @@ +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; + +import { ConsumeDynamicComponent } from '../../../test/consum-dynamic/consume-dynamic.component'; +import { DynamicComponent } from '../../../test/dynamic/dynamic.component'; + +describe('ConsumeDynamicComponent', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + declarations: [DynamicComponent], + entryComponents: [DynamicComponent], + component: ConsumeDynamicComponent, + }); + + it('should work', () => { + host = createHost(``); + expect(host.component).toBeDefined(); + }); + + it('should render the dynamic component', () => { + host = createHost(``); + expect(host.queryHost('.dynamic')).toHaveText('dynamic works!'); + }); +}); diff --git a/projects/spectator/vitest/test/consumer.service.spec.ts b/projects/spectator/vitest/test/consumer.service.spec.ts new file mode 100644 index 00000000..be40f720 --- /dev/null +++ b/projects/spectator/vitest/test/consumer.service.spec.ts @@ -0,0 +1,33 @@ +import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/vitest'; +import { Subject } from 'rxjs'; + +import { ConsumerService } from '../../test/consumer.service'; +import { ProviderService } from '../../test/provider.service'; + +describe('ConsumerService', () => { + const randomNumber = Math.random(); + + let spectator: SpectatorService; + const createService = createServiceFactory({ + service: ConsumerService, + providers: [ + mockProvider(ProviderService, { + obs$: new Subject(), + method: () => randomNumber, + }), + ], + }); + + beforeEach(() => (spectator = createService())); + + it('should consume mocked service with properties', () => { + const provider = spectator.inject(ProviderService); + expect(spectator.service.lastValue).toBeUndefined(); + provider.obs$.next('hey you'); + expect(spectator.service.lastValue).toBe('hey you'); + }); + + it('should consume mocked service methods', () => { + expect(spectator.service.consumeProvider()).toBe(randomNumber); + }); +}); diff --git a/projects/spectator/vitest/test/defer-block.spec.ts b/projects/spectator/vitest/test/defer-block.spec.ts new file mode 100644 index 00000000..225a5dc2 --- /dev/null +++ b/projects/spectator/vitest/test/defer-block.spec.ts @@ -0,0 +1,259 @@ +import { Component } from '@angular/core'; +import { DeferBlockBehavior, fakeAsync } from '@angular/core/testing'; +import { createComponentFactory } from '@ngneat/spectator/vitest'; + +describe('DeferBlock', () => { + describe('Playthrough Behavior', () => { + @Component({ + selector: 'app-root', + template: ` + + + @defer (when isVisible) { +

empty defer block
+ } + `, + standalone: true, + }) + class DummyComponent { + isVisible = false; + } + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Playthrough, + }); + + it('should render the defer block when isVisible is true', fakeAsync(() => { + // Arrange + const spectator = createComponent(); + + const button = spectator.query('[data-test="button--isVisible"]')!; + + // Act + spectator.click(button); + spectator.tick(); + spectator.detectChanges(); + + // Assert + expect(spectator.element.outerHTML).toContain('empty defer block'); + })); + }); + + describe('Manual Behavior', () => { + @Component({ + selector: 'app-root', + template: ` + @defer (on viewport) { +
empty defer block
+ } @placeholder { +
this is the placeholder text
+ } @loading { +
this is the loading text
+ } @error { +
this is the error text
+ } + `, + standalone: false, + }) + class DummyComponent {} + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + it('should render the complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlock().renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('empty defer block'); + }); + + it('should render the placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlock().renderPlaceholder(); + + // Assert + expect(spectator.element.outerHTML).toContain('this is the placeholder text'); + }); + + it('should render the loading state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlock().renderLoading(); + + // Assert + expect(spectator.element.outerHTML).toContain('this is the loading text'); + }); + + it('should render the error state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlock().renderError(); + + // Assert + expect(spectator.element.outerHTML).toContain('this is the error text'); + }); + }); + + describe('Manual Behavior with nested states', () => { + @Component({ + selector: 'app-root', + template: ` + @defer (on viewport) { +
complete state #1
+ + + @defer { +
complete state #1.1
+ + + @defer { +
complete state #1.1.1
+ } @placeholder { +
placeholder state #1.1.1
+ } + + + + @defer { +
complete state #1.1.2
+ } @placeholder { +
placeholder state #1.1.2
+ } + + } @placeholder { +
nested placeholder text
+ } @loading { +
nested loading text
+ } @error { +
nested error text
+ } + + } @placeholder { +
placeholder state #1
+ } @loading { +
loading state #1
+ } @error { +
error state #1
+ } + `, + standalone: false, + }) + class DummyComponent {} + + const createComponent = createComponentFactory({ + component: DummyComponent, + deferBlockBehavior: DeferBlockBehavior.Manual, + }); + + it('should render the first nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlock().renderComplete(); + await parentCompleteState.deferBlock().renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('complete state #1.1'); + }); + + it('should render the first deep nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock().renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('complete state #1.1.1'); + }); + + it('should render the first deep nested placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock().renderPlaceholder(); + + // Assert + expect(spectator.element.outerHTML).toContain('placeholder state #1.1.1'); + }); + + it('should render the second nested complete state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock(1).renderComplete(); + + // Assert + expect(spectator.element.outerHTML).toContain('complete state #1.1.2'); + }); + + it('should render the second nested placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + const parentCompleteState = await spectator.deferBlock().renderComplete(); + const childrenCompleteState = await parentCompleteState.deferBlock().renderComplete(); + await childrenCompleteState.deferBlock(1).renderPlaceholder(); + + // Assert + expect(spectator.element.outerHTML).toContain('placeholder state #1.1.2'); + }); + + it('should render the placeholder state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlock().renderPlaceholder(); + + // Assert + expect(spectator.element.outerHTML).toContain('placeholder state #1'); + }); + + it('should render the loading state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlock().renderLoading(); + + // Assert + expect(spectator.element.outerHTML).toContain('loading state #1'); + }); + + it('should render the error state', async () => { + // Arrange + const spectator = createComponent(); + + // Act + await spectator.deferBlock().renderError(); + + // Assert + expect(spectator.element.outerHTML).toContain('error state #1'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/dom-selectors/dom-selectors.component.spec.ts b/projects/spectator/vitest/test/dom-selectors/dom-selectors.component.spec.ts new file mode 100644 index 00000000..b431996d --- /dev/null +++ b/projects/spectator/vitest/test/dom-selectors/dom-selectors.component.spec.ts @@ -0,0 +1,173 @@ +import { + createComponentFactory, + Spectator, + byAltText, + byLabel, + byPlaceholder, + byText, + byTitle, + byValue, + byTextContent, +} from '@ngneat/spectator/vitest'; + +import { DomSelectorsComponent, DomSelectorsNestedComponent } from '../../../test/dom-selectors/dom-selectors.component'; + +describe('DomSelectorsComponent', () => { + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: DomSelectorsComponent, + imports: [DomSelectorsNestedComponent], + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should allow querying by text', () => { + const element = spectator.query(byText('By text')); + expect(element).toHaveId('by-text-p'); + }); + + it('should allow querying by text and selector', () => { + const element = spectator.query(byText('By text', { selector: '#by-text-p-2' })); + expect(element).toHaveId('by-text-p-2'); + + const elements = spectator.queryAll(byText('By text', { selector: '#by-text-p-2' })); + expect(elements[0]).toHaveId('by-text-p-2'); + expect(elements).toHaveLength(1); + }); + + it('should allow querying by label', () => { + const element = spectator.query(byLabel('By label')); + expect(element).toHaveId('by-label-input'); + }); + + it('should allow querying by placeholder', () => { + const element = spectator.query(byPlaceholder('By placeholder')); + expect(element).toHaveId('by-placeholder-input'); + }); + + it('should allow querying by alt text', () => { + const element = spectator.query(byAltText('By alt text')); + expect(element).toHaveId('by-alt-text-img'); + }); + + it('should allow querying by title', () => { + const element = spectator.query(byTitle('By title')); + expect(element).toHaveId('by-title-a'); + }); + + it('should allow querying by value', () => { + const element = spectator.query(byValue('By value')); + expect(element).toHaveId('by-value-input'); + }); + + describe('parentSelector', () => { + it('should allow querying multiple element by parent selector', () => { + let element = spectator.queryAll(DomSelectorsNestedComponent, { parentSelector: '#nested-components-2' }); + expect(element.length).toBe(2); + element = spectator.queryAll(DomSelectorsNestedComponent, { parentSelector: '#nested-components-1' }); + expect(element.length).toBe(1); + }); + }); + + describe('byTextContent', () => { + describe('with string matcher', () => { + [ + { description: 'by default', opts: {} }, + { description: 'with `exact: true`', opts: { exact: true } }, + ].forEach(({ description, opts }) => { + it(`should exactly match text content ${description}`, () => { + let element = spectator.query(byTextContent('deeply nested', { selector: '#text-content-root', ...opts })); + expect(element).toBeNull(); + element = spectator.query(byTextContent('some deeply nested text', { selector: '#text-content-root', ...opts })); + expect(element).toBeNull(); + element = spectator.query(byTextContent('some deeply NESTED TEXT', { selector: '#text-content-root', ...opts })); + expect(element).toHaveId('text-content-root'); + }); + }); + + it('should partially match text with `exact: false`', () => { + const element = spectator.query(byTextContent('deeply nested', { selector: '#text-content-root', exact: false })); + expect(element).toHaveId('text-content-root'); + }); + + it('should support `trim` option', () => { + let element = spectator.query(byTextContent('TEXT', { selector: '#text-content-span-2', exact: true, trim: false })); + expect(element).toBeNull(); + element = spectator.query(byTextContent(' TEXT ', { selector: '#text-content-span-2', exact: true, trim: false })); + expect(element).toHaveId('text-content-span-2'); + }); + + it('should support `collapseWhitespace` option', () => { + let element = spectator.query( + byTextContent('deeply NESTED', { selector: '#text-content-span-1', exact: true, collapseWhitespace: false }), + ); + expect(element).toBeNull(); + element = spectator.query( + byTextContent('deeply NESTED', { selector: '#text-content-span-1', exact: true, collapseWhitespace: false }), + ); + expect(element).toHaveId('text-content-span-1'); + }); + + it('should support custom normalizer', () => { + const toLowerCase = (text: string) => text.toLowerCase(); + let element = spectator.query( + byTextContent('deeply NESTED', { selector: '#text-content-span-1', exact: true, normalizer: toLowerCase }), + ); + expect(element).toBeNull(); + element = spectator.query( + byTextContent('deeply nested', { selector: '#text-content-span-1', exact: true, normalizer: toLowerCase }), + ); + expect(element).toHaveId('text-content-span-1'); + }); + }); + + describe('with number matcher', () => { + it('should match number content', () => { + let element = spectator.query(byTextContent(8, { selector: '#number-content-root *' })); + expect(element).toHaveId('number-content-only-eight'); + }); + + it('should partially match number with `exact: false`', () => { + let elements = spectator.queryAll(byTextContent(8, { selector: '#number-content-root *', exact: false })); + expect(elements).toHaveLength(2); + expect(elements[0]).toHaveId('number-content-with-eight'); + expect(elements[1]).toHaveId('number-content-only-eight'); + }); + }); + + describe('with RegExp matcher', () => { + it('should match the text', () => { + const element = spectator.query(byTextContent(/^some deeply NESTED TEXT$/, { selector: '#text-content-root' })); + expect(element).toHaveId('text-content-root'); + }); + + it('should support `trim` option', () => { + const element = spectator.query(byTextContent(/^ TEXT $/, { selector: '#text-content-span-2', trim: false })); + expect(element).toHaveId('text-content-span-2'); + }); + + it('should support `collapseWhitespace` option', () => { + const element = spectator.query( + byTextContent(/^deeply\s\sNESTED$/, { selector: '#text-content-span-1', collapseWhitespace: false }), + ); + expect(element).toHaveId('text-content-span-1'); + }); + + it('should support custom normalizer', () => { + const toLowerCase = (text: string) => text.toLowerCase(); + const element = spectator.query(byTextContent(/deeply\s\snested/, { selector: '#text-content-span-1', normalizer: toLowerCase })); + expect(element).toHaveId('text-content-span-1'); + }); + }); + + describe('with function matcher', () => { + it('should match and element for which matcher returns `true`', () => { + const matcher = (text: string) => text === 'TEXT'; + const element = spectator.query(byTextContent(matcher, { selector: '#text-content-root [id^="text-content-span"]' })); + expect(element).toHaveId('text-content-span-2'); + }); + }); + }); +}); diff --git a/projects/spectator/vitest/test/fg/fg.component.spec.ts b/projects/spectator/vitest/test/fg/fg.component.spec.ts new file mode 100644 index 00000000..2dbbddc2 --- /dev/null +++ b/projects/spectator/vitest/test/fg/fg.component.spec.ts @@ -0,0 +1,31 @@ +import { SpectatorHost, createHostFactory } from '@ngneat/spectator/vitest'; +import { Component } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; + +import { FgComponent } from '../../../test/fg/fg.component'; + +@Component({ + selector: 'app-custom-host', + template: '', + standalone: false, +}) +class CustomHostComponent { + public group = new FormGroup({ + name: new FormControl('name'), + }); +} + +describe('With Custom Host Component', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: FgComponent, + imports: [ReactiveFormsModule], + host: CustomHostComponent, + }); + + it('should display the host component title', () => { + host = createHost(``); + expect(host.component).toBeDefined(); + }); +}); diff --git a/projects/spectator/vitest/test/focus/test-focus.component.spec.ts b/projects/spectator/vitest/test/focus/test-focus.component.spec.ts new file mode 100644 index 00000000..5836f4ed --- /dev/null +++ b/projects/spectator/vitest/test/focus/test-focus.component.spec.ts @@ -0,0 +1,41 @@ +import { SpectatorHost, createHostFactory } from '@ngneat/spectator/vitest'; + +import { TestFocusComponent } from '../../../test/focus/test-focus.component'; + +describe('SpectatorHost.focus() in vitest', () => { + const createHost = createHostFactory(TestFocusComponent); + let host: SpectatorHost; + + beforeEach(() => { + host = createHost(''); + }); + + it('sets document.activeElement', () => { + host.focus('#button1'); + + expect(host.query('#button1')).toBeFocused(); + }); + + it('causes blur events', () => { + host.focus(); + host.focus('#button1'); + host.focus('#button2'); + + expect(host.component.focusCount('app-test-focus')).toBe(1); + expect(host.component.blurCount('app-test-focus')).toBe(1); + expect(host.component.focusCount('button1')).toBe(1); + expect(host.component.blurCount('button1')).toBe(1); + expect(host.component.focusCount('button2')).toBe(1); + expect(host.component.blurCount('button2')).toBe(0); + }); + + it('calling focus() multiple times does not cause multiple patches', () => { + host.focus('#button1'); + host.focus(); + host.focus('#button1'); + + expect(host.component.focusCount('app-test-focus')).toBe(1); + expect(host.component.focusCount('button1')).toBe(2); + expect(host.component.blurCount('button1')).toBe(1); + }); +}); diff --git a/projects/spectator/vitest/test/form-input/form-input.component.spec.ts b/projects/spectator/vitest/test/form-input/form-input.component.spec.ts new file mode 100644 index 00000000..305da147 --- /dev/null +++ b/projects/spectator/vitest/test/form-input/form-input.component.spec.ts @@ -0,0 +1,56 @@ +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import { Spectator, SpectatorHost, createComponentFactory, createHostFactory } from '@ngneat/spectator/vitest'; + +import { FormInputComponent } from '../../../test/form-input/form-input.component'; + +describe('FormInputComponent', () => { + let host: SpectatorHost; + const createHost = createHostFactory({ + component: FormInputComponent, + imports: [ReactiveFormsModule], + }); + const group = new FormGroup({ name: new FormControl('') }); + + it('should be defined', () => { + host = createHost(``, { + detectChanges: true, + hostProps: { + subnetControl: group, + enableSubnet: true, + }, + }); + + expect(host.component).toBeDefined(); + expect(host.query('p')).not.toBeNull(); + host.setHostInput('enableSubnet', false); + expect(host.query('p')).toBeNull(); + }); +}); + +describe('FormInputComponent', () => { + let spectator: Spectator; + const group = new FormGroup({ name: new FormControl('') }); + + const inputs = { + subnetControl: group, + enableSubnet: true, + }; + + const createComponent = createComponentFactory({ + component: FormInputComponent, + imports: [ReactiveFormsModule], + }); + + beforeEach(() => { + spectator = createComponent({ + props: inputs, + }); + }); + + it('should work', () => { + expect(spectator.component).toBeDefined(); + expect(spectator.query('p')).not.toBeNull(); + spectator.setInput('enableSubnet', false); + expect(spectator.query('p')).toBeNull(); + }); +}); diff --git a/projects/spectator/vitest/test/form-select/form-select.component.spec.ts b/projects/spectator/vitest/test/form-select/form-select.component.spec.ts new file mode 100644 index 00000000..a57e69b7 --- /dev/null +++ b/projects/spectator/vitest/test/form-select/form-select.component.spec.ts @@ -0,0 +1,22 @@ +import { ReactiveFormsModule } from '@angular/forms'; +import { Spectator, createComponentFactory } from '@ngneat/spectator/vitest'; + +import { FormSelectComponent } from './form-select.component'; + +describe('FormSelectComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: FormSelectComponent, + imports: [ReactiveFormsModule], + }); + + beforeEach(() => (spectator = createComponent())); + + it('should set the correct option on standard select', () => { + // const select = spectator.query('#test-single-select') as HTMLSelectElement; + // spectator.selectOption(select, '1'); + // expect(select).toHaveSelectedOptions('1'); + expect(true).toBeTruthy(); + }); +}); diff --git a/projects/spectator/vitest/test/form-select/form-select.component.ts b/projects/spectator/vitest/test/form-select/form-select.component.ts new file mode 100644 index 00000000..e5504358 --- /dev/null +++ b/projects/spectator/vitest/test/form-select/form-select.component.ts @@ -0,0 +1,22 @@ +import { Component, ChangeDetectionStrategy } from '@angular/core'; + +@Component({ + selector: 'app-form-select', + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, +}) +export class FormSelectComponent { + /** + * Empty method to spy on + */ + public handleChange(): void { + return; + } +} diff --git a/projects/spectator/vitest/test/function-output/function-output.component.spec.ts b/projects/spectator/vitest/test/function-output/function-output.component.spec.ts new file mode 100644 index 00000000..1f84513f --- /dev/null +++ b/projects/spectator/vitest/test/function-output/function-output.component.spec.ts @@ -0,0 +1,47 @@ +import { createComponentFactory, createHostFactory, Spectator, SpectatorHost } from '@ngneat/spectator/vitest'; +import { FunctionOutputComponent } from '../../../test/function-output/function-output.component'; + +describe('FunctionOutputComponent', () => { + describe('with Spectator', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: FunctionOutputComponent, + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should emit the event on button click', () => { + let output; + spectator.output('buttonClick').subscribe((result) => (output = result)); + + spectator.click('button'); + + expect(output).toEqual(true); + }); + }); + + describe('with SpectatorHost', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: FunctionOutputComponent, + template: ``, + }); + + beforeEach(() => { + host = createHost(); + }); + + it('should emit the event on button click', () => { + let output; + host.output('buttonClick').subscribe((result) => (output = result)); + + host.click('button'); + + expect(output).toEqual(true); + }); + }); +}); diff --git a/projects/spectator/vitest/test/hello/hello.component.spec.ts b/projects/spectator/vitest/test/hello/hello.component.spec.ts new file mode 100644 index 00000000..68a85fdd --- /dev/null +++ b/projects/spectator/vitest/test/hello/hello.component.spec.ts @@ -0,0 +1,40 @@ +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; + +import { HelloComponent } from '../../../test/hello/hello.component'; + +describe('HelloComponent', () => { + let host: SpectatorHost; + + const createHost = createHostFactory(HelloComponent); + + it('should display the title', () => { + host = createHost(` `, { + hostProps: { + title: 'some title', + widthRaw: 20, + }, + }); + + expect((host.query('div') as HTMLElement).style.width).toBe('20px'); + + expect('div h1').toHaveText(''); // This should return true, according to the original code + expect('div h1').toHaveText('some title'); + expect('div h1').toHaveText('ome title'); + + expect('div h1').toHaveText('some title', true); + expect('div h1').not.toHaveText('ome title', true); + + expect('div h1').toHaveExactText('some title'); + expect('div h1').not.toHaveExactText('ome title'); + expect('div h1').not.toHaveExactText(''); + + expect('div h2').toHaveText(' some title ', true); + + expect('div h2').toHaveExactText(' some title '); + expect('div h2').toHaveExactText('some title', { trim: true }); + expect('div h2').not.toHaveExactText('ome title', { trim: true }); + + expect('div h2').toHaveExactTrimmedText('some title'); + expect('div h2').not.toHaveExactTrimmedText('ome title'); + }); +}); diff --git a/projects/spectator/vitest/test/highlight.directive.spec.ts b/projects/spectator/vitest/test/highlight.directive.spec.ts new file mode 100644 index 00000000..2db1adaf --- /dev/null +++ b/projects/spectator/vitest/test/highlight.directive.spec.ts @@ -0,0 +1,48 @@ +import { createDirectiveFactory, SpectatorDirective, SpectatorHost } from '@ngneat/spectator'; +import { createHostFactory } from '@ngneat/spectator/vitest'; + +import { HighlightDirective } from '../../test/highlight.directive'; + +describe('HighlightDirective', () => { + let host: SpectatorHost; + + const createHost = createHostFactory(HighlightDirective); + + // calculated styles not supported in JSDOM + it('should change the background color', () => { + host = createHost(`
Testing HighlightDirective
`); + + host.dispatchMouseEvent(host.element, 'mouseover'); + + expect(host.element).toHaveStyle({ + backgroundColor: 'rgba(0,0,0, 0.1)', + }); + + host.dispatchMouseEvent(host.element, 'mouseout'); + expect(host.element).toHaveStyle({ + backgroundColor: '#fff', + }); + }); +}); + +describe('HighlightDirective (createHostDirectiveFactory)', () => { + let host: SpectatorDirective; + + const createHost = createDirectiveFactory(HighlightDirective); + + // calculated styles not supported in JSDOM + it('should change the background color', () => { + host = createHost(`
Testing HighlightDirective
`); + + host.dispatchMouseEvent(host.element, 'mouseover'); + + expect(host.element).toHaveStyle({ + backgroundColor: 'rgba(0,0,0, 0.1)', + }); + + host.dispatchMouseEvent(host.element, 'mouseout'); + expect(host.element).toHaveStyle({ + backgroundColor: '#fff', + }); + }); +}); diff --git a/projects/spectator/vitest/test/injection-and-mocking.spec.ts b/projects/spectator/vitest/test/injection-and-mocking.spec.ts new file mode 100644 index 00000000..4cbdd2ba --- /dev/null +++ b/projects/spectator/vitest/test/injection-and-mocking.spec.ts @@ -0,0 +1,174 @@ +import { + createHostFactory, + createComponentFactory, + Spectator, + SpectatorHost, + SpectatorService, + createServiceFactory, +} from '@ngneat/spectator/vitest'; +import { InjectionToken } from '@angular/core'; + +import { ConsumerService } from '../../test/consumer.service'; +import { AbstractQueryService, QueryService } from '../../test/query.service'; +import { ZippyComponent } from '../../test/zippy/zippy.component'; +import { WidgetService } from '../../test/widget.service'; + +const MY_TOKEN = new InjectionToken('some-token'); + +describe('Injection tokens', () => { + describe('with Spectator', () => { + const createComponent = createComponentFactory({ + component: ZippyComponent, + mocks: [WidgetService], + providers: [ + QueryService, + { + provide: AbstractQueryService, + useExisting: QueryService, + }, + { + provide: MY_TOKEN, + useExisting: QueryService, + }, + ], + }); + + let spectator: Spectator; + + beforeEach(() => (spectator = createComponent())); + + it('should get by concrete class', () => { + const service = spectator.inject(QueryService); + service.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + spectator.inject(WidgetService).get.mockClear(); // should compile and exist + }); + + it('should get by abstract class as token', () => { + const service = spectator.inject(AbstractQueryService); + service.select(); // should compile + + const service2 = spectator.inject(AbstractQueryService); + service2.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + spectator.inject(WidgetService).get.mockClear(); // should compile and exist + }); + + it('should get by injection token', () => { + const service = spectator.inject(MY_TOKEN); + service.select(); // should compile + + const service2 = spectator.inject(MY_TOKEN); + service2.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + spectator.inject(WidgetService).get.mockClear(); // should compile and exist + }); + }); + + describe('with SpectatorHost', () => { + const createHost = createHostFactory({ + component: ZippyComponent, + mocks: [WidgetService], + providers: [ + QueryService, + { + provide: AbstractQueryService, + useExisting: QueryService, + }, + { + provide: MY_TOKEN, + useExisting: QueryService, + }, + ], + }); + + let host: SpectatorHost; + + beforeEach(() => (host = createHost(''))); + + it('should get by concrete class', () => { + const service = host.inject(QueryService); + service.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + host.inject(WidgetService).get.mockClear(); // should compile and exist + }); + + it('should get by abstract class as token', () => { + const service = host.inject(AbstractQueryService); + service.select(); // should compile + + const service2 = host.inject(AbstractQueryService); + service2.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + host.inject(WidgetService).get.mockClear(); // should compile and exist + }); + + it('should get by injection token', () => { + const service = host.inject(MY_TOKEN); + service.select(); // should compile + + const service2 = host.inject(MY_TOKEN); + service2.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + host.inject(WidgetService).get.mockClear(); // should compile and exist + }); + }); + + describe('with Service', () => { + const createService = createServiceFactory({ + service: ConsumerService, + mocks: [WidgetService], + providers: [ + QueryService, + { + provide: AbstractQueryService, + useExisting: QueryService, + }, + { + provide: MY_TOKEN, + useExisting: QueryService, + }, + ], + }); + + let spectator: SpectatorService; + + beforeEach(() => (spectator = createService())); + + it('should get by concrete class', () => { + const service = spectator.inject(QueryService); + service.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + spectator.inject(WidgetService).get.mockClear(); // should compile and exist + }); + + it('should get by abstract class as token', () => { + const service = spectator.inject(AbstractQueryService); + service.select(); // should compile + + const service2 = spectator.inject(AbstractQueryService); + service2.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + spectator.inject(WidgetService).get.mockClear(); // should compile and exist + }); + + it('should get by injection token', () => { + const service = spectator.inject(MY_TOKEN); + service.select(); // should compile + + const service2 = spectator.inject(MY_TOKEN); + service2.selectName(); // should compile + + expect(service).toBeInstanceOf(QueryService); + spectator.inject(WidgetService).get.mockClear(); // should compile and exist + }); + }); +}); diff --git a/projects/spectator/vitest/test/matchers/matchers.spec.ts b/projects/spectator/vitest/test/matchers/matchers.spec.ts new file mode 100644 index 00000000..a2f09025 --- /dev/null +++ b/projects/spectator/vitest/test/matchers/matchers.spec.ts @@ -0,0 +1,186 @@ +import { toBeVisible, toBePartial } from '@ngneat/spectator'; +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; +import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; + +interface Dummy { + lorem: string; + ipsum: string; +} + +@Component({ + template: ` + + + + +
Hidden by parent with display none
+
Hidden by parent with display none
+
Visible
+
Classes
+
+ +
+ `, + standalone: false, +}) +export class MatchersComponent {} + +describe('Matchers', () => { + const createComponent = createComponentFactory({ + component: MatchersComponent, + schemas: [CUSTOM_ELEMENTS_SCHEMA], + }); + let spectator: Spectator; + + beforeEach(() => { + if (!window.customElements.get('custom-element')) { + window.customElements.define( + 'custom-element', + class extends HTMLElement { + connectedCallback() { + if (this.isConnected && !this.shadowRoot) { + const el = document.createElement('div'); + el.id = 'shadow-dom'; + this.attachShadow({ mode: 'open' }).appendChild(el); + } + } + }, + ); + } + spectator = createComponent(); + }); + + describe('toBeVisible', () => { + it('should detect visible element as being visible', () => { + expect('#visible').toBeVisible(); + }); + + it('should detect element hidden by "display:none"-style, as NOT being visible', () => { + expect('#display-none').not.toBeVisible(); + }); + + it('should detect element hidden by "visibility:hidden"-style, as NOT being visible', () => { + expect('#visibility-hidden').not.toBeVisible(); + }); + + it('should detect input (of type hidden), as NOT being visible', () => { + expect('input[type="hidden"]').not.toBeVisible(); + }); + + it('should detect element with hidden-attribute, as NOT being visible', () => { + expect('[hidden]').not.toBeVisible(); + }); + + it('should detect element hidden by parent with "display:none"-style, as NOT being visible', () => { + expect('#parent-display-none').not.toBeVisible(); + }); + + it('should detect element hidden parent with by "visibility:hidden"-style, as NOT being visible', () => { + expect('#parent-visibility-hidden').not.toBeVisible(); + }); + }); + + describe('toBeHidden', () => { + it('should detect visible element as NOT to be hidden', () => { + expect('#visible').not.toBeHidden(); + }); + + it('should detect element hidden by "display:none"-style, as to be hidden', () => { + expect('#display-none').toBeHidden(); + }); + + it('should detect element hidden by "visibility:hidden"-style, as to be hidden', () => { + expect('#visibility-hidden').toBeHidden(); + }); + + it('should detect input (of type hidden), as to be hidden', () => { + expect('input[type="hidden"]').toBeHidden(); + }); + + it('should detect element with hidden-attribute, as to be hidden', () => { + expect('[hidden]').toBeHidden(); + }); + + it('should detect element hidden by parent with "display:none"-style, as to be hidden', () => { + expect('#parent-display-none').toBeHidden(); + }); + + it('should detect element hidden parent with by "visibility:hidden"-style, as being hidden', () => { + expect('#parent-visibility-hidden').toBeHidden(); + }); + + it('should be possible to validate an element that has classes in strict order', () => { + expect('#classes').toHaveClass(['class-a', 'class-b']); + expect('#classes').toHaveClass(['class-a', 'class-b'], { strict: true }); + expect('#classes').not.toHaveClass(['class-b', 'class-a']); + expect('#classes').not.toHaveClass(['class-b', 'class-a'], { strict: true }); + }); + + it('should be possible to validate an element that has classes in any order', () => { + expect('#classes').toHaveClass(['class-a', 'class-b'], { strict: false }); + expect('#classes').toHaveClass(['class-b', 'class-a'], { strict: false }); + }); + + it('should detect elements with hidden parents through shadow DOMs', () => { + expect(document.querySelector('custom-element')?.shadowRoot?.querySelector('#shadow-dom')).toBeHidden(); + }); + + it('should detect elements whose computed styles are display: none', () => { + window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'display' && 'none' }) as CSSStyleDeclaration; + expect(document.querySelector('#computed-style')).toBeHidden(); + 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' }) as CSSStyleDeclaration; + expect(document.querySelector('#computed-style')).toBeHidden(); + window.getComputedStyle = () => ({ getPropertyValue: (style) => style == 'visibility' && 'visible' }) as CSSStyleDeclaration; + expect(document.querySelector('#computed-style')).toBeVisible(); + }); + }); + + describe('toBePartial', () => { + it('should return true when expected is partial of actual', () => { + const actual: Dummy = { lorem: 'first', ipsum: 'second' }; + expect(actual).toBePartial({ lorem: 'first' }); + }); + + it('should return true when expected is same as actual', () => { + const actual: Dummy = { lorem: 'first', ipsum: 'second' }; + expect(actual).toBePartial({ ...actual }); + }); + + it('should return false when expected is not partial of actual', () => { + const actual: Dummy = { lorem: 'first', ipsum: 'second' }; + expect(actual).not.toBePartial({ lorem: 'second' }); + }); + }); + + describe('toHaveStyle', () => { + it('should return true when expected style exists on element', () => { + const element = spectator.query('#styles'); + expect(element).toHaveStyle({ 'background-color': 'indianred' }); + }); + + it('should return true if all styles exist on element', () => { + const element = spectator.query('#styles'); + expect(element).toHaveStyle({ 'background-color': 'indianred', color: 'chocolate' }); + }); + + it('should return true if the CSS variable exist on element', () => { + const element = spectator.query('#styles'); + expect(element).toHaveStyle({ '--primary': 'var(--black)' }); + }); + + it('should return false if style exists on an element but has a different value than expected', () => { + const element = spectator.query('#styles'); + expect(element).not.toHaveStyle({ color: 'blue' }); + }); + + it('should return false when expected style does not exist on element', () => { + const element = spectator.query('#styles'); + expect(element).not.toHaveStyle({ height: '100px' }); + }); + }); +}); diff --git a/projects/spectator/vitest/test/mock.spec.ts b/projects/spectator/vitest/test/mock.spec.ts new file mode 100644 index 00000000..759f29ec --- /dev/null +++ b/projects/spectator/vitest/test/mock.spec.ts @@ -0,0 +1,16 @@ +import { mockProvider } from '@ngneat/spectator/vitest'; + +import { WidgetService } from '../../test/widget.service'; + +describe('mockProvider', () => { + it('should not modify the object passed in 2nd argument when running the mock factory', () => { + const customPropertiesAndMethods: Partial> = { + testingProperty: 'overriden', + }; + const { useFactory: factory } = mockProvider(WidgetService, customPropertiesAndMethods); + factory(); + expect(customPropertiesAndMethods).toEqual({ + testingProperty: 'overriden', + }); + }); +}); diff --git a/projects/spectator/vitest/test/ngonchanges-input/ngonchanges-input.component.spec.ts b/projects/spectator/vitest/test/ngonchanges-input/ngonchanges-input.component.spec.ts new file mode 100644 index 00000000..16847675 --- /dev/null +++ b/projects/spectator/vitest/test/ngonchanges-input/ngonchanges-input.component.spec.ts @@ -0,0 +1,25 @@ +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; +import { NgOnChangesInputComponent } from '../../../test/ngonchanges-input/ngonchanges-input.component'; + +describe('NgOnChangesInputComponent', () => { + describe('with Spectator', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: NgOnChangesInputComponent, + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should re-render when updating fields in ngOnChanges', () => { + expect(spectator.query('button')).toBeDisabled(); + expect(spectator.query('button')).toHaveText('Button disabled'); + + spectator.setInput({ btnDisabled: false }); + expect(spectator.query('button')).not.toBeDisabled(); + expect(spectator.query('button')).toHaveText('Button enabled'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/no-overwritten-providers/no-overwritten-providers.component.spec.ts b/projects/spectator/vitest/test/no-overwritten-providers/no-overwritten-providers.component.spec.ts new file mode 100644 index 00000000..1a6b3586 --- /dev/null +++ b/projects/spectator/vitest/test/no-overwritten-providers/no-overwritten-providers.component.spec.ts @@ -0,0 +1,38 @@ +import { createHostFactory } from '@ngneat/spectator/vitest'; +import { mockProvider } from '@ngneat/spectator'; + +import { ComponentWithoutOverwrittenProvidersComponent } from '../../../test/no-overwritten-providers/no-overwritten-providers.component'; +import { DummyService } from '../../../test/no-overwritten-providers/dummy.service'; + +describe('ComponentWithoutOverwrittenProvidersComponent', () => { + describe('with options', () => { + const createHost = createHostFactory({ + component: ComponentWithoutOverwrittenProvidersComponent, + componentProviders: [mockProvider(DummyService)], + }); + + it('should not overwrite components providers and work using createHostFactory', () => { + const { component } = createHost(` + + + `); + + expect(component).toBeDefined(); + expect(component.dummy).toBeDefined(); + }); + }); + + // describe('with component', () => { + // let createHost = createHostFactory(ComponentWithoutOverwrittenProvidersComponent); + // + // it('should not overwrite component\'s providers and work using createHostFactory', () => { + // const { component } = createHost(` + // + // + // `); + // + // expect(component).toBeDefined(); + // expect(component.dummy).toBeDefined(); + // }); + // }); +}); diff --git a/projects/spectator/vitest/test/override-component.spec.ts b/projects/spectator/vitest/test/override-component.spec.ts new file mode 100644 index 00000000..af13b60a --- /dev/null +++ b/projects/spectator/vitest/test/override-component.spec.ts @@ -0,0 +1,111 @@ +import { createComponentFactory, createHostFactory, Spectator, SpectatorHost } from '@ngneat/spectator'; +import { Component } from '@angular/core'; +import { QueryService } from '../../test/query.service'; +import { overrideComponents } from '../../src/lib/spectator/create-factory'; + +// Created only for testing purpose +@Component({ + selector: `app-standalone-with-dependency`, + template: `
Standalone component with dependency!
`, + standalone: true, +}) +export class StandaloneComponentWithDependency { + constructor(public query: QueryService) {} +} + +@Component({ + selector: `app-standalone-with-import`, + template: `
Standalone component with import!
+ `, + imports: [StandaloneComponentWithDependency], + standalone: true, +}) +export class StandaloneWithImportsComponent {} + +@Component({ + selector: `app-standalone-with-dependency`, + template: `
Standalone component with override dependency!
`, + standalone: true, +}) +export class MockStandaloneComponentWithDependency { + constructor() {} +} + +@Component({ + selector: `app-non-standalone`, + template: `
Non standalone
`, + standalone: false, +}) +export class MockNonStandaloneComponent { + constructor() {} +} + +describe('Override Component', () => { + it('should throw error when override non standalone component', () => { + expect(() => + overrideComponents({ + overrideComponents: [ + [ + MockNonStandaloneComponent, + { + remove: { imports: [] }, + add: { imports: [] }, + }, + ], + ], + } as any), + ).toThrowError('Can not override non standalone component'); + }); + + describe('with Spectator', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: StandaloneWithImportsComponent, + overrideComponents: [ + [ + StandaloneWithImportsComponent, + { + remove: { imports: [StandaloneComponentWithDependency] }, + add: { imports: [MockStandaloneComponentWithDependency] }, + }, + ], + ], + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should render a StandaloneWithImportsComponent', () => { + expect(spectator.query('#standalone')).toContainText('Standalone component with import!'); + }); + }); + + describe('with SpectatorHost', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: StandaloneWithImportsComponent, + template: `
`, + overrideComponents: [ + [ + StandaloneWithImportsComponent, + { + remove: { imports: [StandaloneComponentWithDependency] }, + add: { imports: [MockStandaloneComponentWithDependency] }, + }, + ], + ], + }); + + beforeEach(() => { + host = createHost(); + }); + + it('should render a StandaloneWithImportsComponent', () => { + expect(host.query('#standalone')).toContainText('Standalone component with import!'); + expect(host.query('#standaloneWithDependency')).toContainText('Standalone component with override dependency!'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/override-directive.spec.ts b/projects/spectator/vitest/test/override-directive.spec.ts new file mode 100644 index 00000000..e09ea5f8 --- /dev/null +++ b/projects/spectator/vitest/test/override-directive.spec.ts @@ -0,0 +1,66 @@ +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator'; +import { Directive, Inject, InjectionToken } from '@angular/core'; +import { overrideDirectives } from '../../src/lib/spectator/create-factory'; + +// Created only for testing purpose +export const directiveProviderToken = new InjectionToken('DirectiveProviderToken'); +@Directive({ + selector: `[appStandaloneDirectiveWithDependency]`, + standalone: true, + providers: [{ provide: directiveProviderToken, useValue: 'test' }], +}) +export class StandaloneDirectiveWithDependency { + constructor(@Inject(directiveProviderToken) public provider: string) {} +} + +@Directive({ + selector: `app-non-standalone-directive`, + standalone: false, +}) +export class MockNonStandaloneDirective { + constructor() {} +} + +describe('Override Directive', () => { + it('should throw error when override non standalone directive', () => { + expect(() => + overrideDirectives({ + overrideDirectives: [ + [ + MockNonStandaloneDirective, + { + remove: { imports: [] }, + add: { imports: [] }, + }, + ], + ], + } as any), + ).toThrowError('Can not override non standalone directive'); + }); + + describe('with Spectator', () => { + let spectator: SpectatorDirective; + + const createDirective = createDirectiveFactory({ + directive: StandaloneDirectiveWithDependency, + overrideDirectives: [ + [ + StandaloneDirectiveWithDependency, + { + remove: { providers: [{ provide: directiveProviderToken, useValue: 'test' }] }, + add: { providers: [{ provide: directiveProviderToken, useValue: 'fakeTest' }] }, + }, + ], + ], + template: `
Testing Directive Providers
`, + }); + + beforeEach(() => { + spectator = createDirective(); + }); + + it('should render a StandaloneDirectiveWithDependency', () => { + expect(spectator.directive.provider).toEqual('fakeTest'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/override-module.spec.ts b/projects/spectator/vitest/test/override-module.spec.ts new file mode 100644 index 00000000..d98d2d09 --- /dev/null +++ b/projects/spectator/vitest/test/override-module.spec.ts @@ -0,0 +1,79 @@ +import { Component, Directive, HostBinding, NgModule } from '@angular/core'; +import { Spectator, SpectatorDirective, SpectatorHost } from '@ngneat/spectator'; +import { createComponentFactory, createDirectiveFactory, createHostFactory } from '@ngneat/spectator/vitest'; + +import { AveragePipe } from '../../test/pipe/average.pipe'; + +@Component({ + selector: 'test-comp', + template: '
{{ prop | avg }}
', + standalone: false, +}) +class TestComponent { + public prop = [1, 2, 3]; +} + +@Directive({ + selector: '[someDirective]', + standalone: false, +}) +class SomeDirective { + @HostBinding('class') public someClass = 'someClass'; +} + +@NgModule() +class SomeModule {} + +describe('Override Module With Component Factory', () => { + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: TestComponent, + imports: [SomeModule], + overrideModules: [[SomeModule, { set: { declarations: [AveragePipe], exports: [AveragePipe] } }]], + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should be declared with override modules', () => { + expect(spectator.component).toBeTruthy(); + expect(spectator.query('div')?.textContent).toEqual('2'); + }); +}); + +describe('Override Module With Directive Factory', () => { + let spectator: SpectatorDirective; + const createDirective = createDirectiveFactory({ + directive: SomeDirective, + host: TestComponent, + imports: [SomeModule], + overrideModules: [[SomeModule, { set: { declarations: [AveragePipe], exports: [AveragePipe] } }]], + }); + + beforeEach(() => { + spectator = createDirective(`
{{ prop | avg }}
`); + }); + + it('should be declared with override modules', () => { + expect(spectator.query('div')?.classList).toContain('someClass'); + expect(spectator.query('div')?.textContent).toEqual('2'); + }); +}); + +describe('Override Module With Host Factory', () => { + let spectator: SpectatorHost; + const createHost = createHostFactory({ + component: TestComponent, + imports: [SomeModule], + overrideModules: [[SomeModule, { set: { declarations: [AveragePipe], exports: [AveragePipe] } }]], + }); + + beforeEach(() => { + spectator = createHost(``); + }); + + it('should be declared with override modules', () => { + expect(spectator.query('div')?.textContent).toEqual('2'); + }); +}); diff --git a/projects/spectator/vitest/test/override-pipe.spec.ts b/projects/spectator/vitest/test/override-pipe.spec.ts new file mode 100644 index 00000000..37ae7419 --- /dev/null +++ b/projects/spectator/vitest/test/override-pipe.spec.ts @@ -0,0 +1,67 @@ +import { Pipe, PipeTransform } from '@angular/core'; +import { createPipeFactory, SpectatorPipe } from '@ngneat/spectator'; +import { overridePipes } from '../../src/lib/spectator/create-factory'; + +// Created only for testing purpose +@Pipe({ + name: `standalonePipe`, + standalone: true, + pure: false, +}) +export class StandalonePipe implements PipeTransform { + public transform(value: number[]): number[] { + return value; + } +} + +@Pipe({ + name: `app-non-standalone-pipe`, + standalone: false, +}) +export class MockNonStandalonePipe { + constructor() {} +} + +describe('Override Pipe', () => { + it('should throw error when override non standalone pipe', () => { + expect(() => + overridePipes({ + overridePipes: [ + [ + MockNonStandalonePipe, + { + remove: { imports: [] }, + add: { imports: [] }, + }, + ], + ], + } as any), + ).toThrowError('Can not override non standalone pipe'); + }); + + describe('with Spectator', () => { + let spectator: SpectatorPipe; + + const createPipe = createPipeFactory({ + pipe: StandalonePipe, + overridePipes: [ + [ + StandalonePipe, + { + remove: { pure: false }, + add: { pure: true }, + }, + ], + ], + }); + + beforeEach(() => { + spectator = createPipe(`{{ [1, 2, 3] | standalonePipe }}`); + }); + + it('should render a StandaloneWithImportsComponent', () => { + expect(spectator).toBeTruthy(); + expect(spectator.element).toHaveText('1,2,3'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/override-typesafety.component.spec.ts b/projects/spectator/vitest/test/override-typesafety.component.spec.ts new file mode 100644 index 00000000..4a369522 --- /dev/null +++ b/projects/spectator/vitest/test/override-typesafety.component.spec.ts @@ -0,0 +1,116 @@ +import { createHostFactory, SpectatorHost } from '@ngneat/spectator'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; +import { Component } from '@angular/core'; + +@Component({ + selector: 'my-component', + template: '', + standalone: false, +}) +class MyComponent {} + +describe('Override type-safety', () => { + describe('Default host with type inference and custom properties', () => { + const createHost = createHostFactory({ + component: MyComponent, + }); + + it('should allow accessing the overridden property', () => { + const spectator = createHost('', { + hostProps: { + control: new FormControl(), + x: 'x', + }, + }); + + spectator.hostComponent.control.patchValue('x'); + spectator.hostComponent.x = 'y'; + // spectator.hostComponent.foo = 'y'; // should not compile + }); + }); + + describe('Default host without type inference and custom properties', () => { + let spectator: SpectatorHost; + const createHost = createHostFactory({ + component: MyComponent, + }); + + beforeEach(() => { + spectator = createHost('', { + hostProps: { + control: new FormControl(), + x: 'x', + }, + }); + }); + + it('should allow accessing the overridden property', () => { + spectator.hostComponent.control.patchValue('x'); + // spectator.hostComponent.x = 'y'; // should not compile + // spectator.hostComponent.foo = 'y'; // should not compile + }); + }); + + describe('Custom Host should not allow custom properties', () => { + @Component({ + template: '', + standalone: false, + }) + class CustomHostComponent { + public foo: string = 'bar'; + } + + let spectator: SpectatorHost; + const createHost = createHostFactory({ + component: MyComponent, + host: CustomHostComponent, + imports: [ReactiveFormsModule], + }); + + beforeEach(() => { + spectator = createHost('', { + hostProps: { + // control: new FormControl(), // should not compile + foo: 'x', + }, + }); + + expect(spectator.hostComponent.foo).toBe('x'); + }); + + it('should allow setting the defined properties', () => { + spectator.hostComponent.foo = 'bar'; + // spectator.hostComponent.bar = 'bar'; // should not compile + }); + }); + + describe('Custom Host should not allow custom properties (type inference)', () => { + @Component({ + template: '', + standalone: false, + }) + class CustomHostComponent { + public foo: string = 'bar'; + } + + const createHost = createHostFactory({ + component: MyComponent, + host: CustomHostComponent, + imports: [ReactiveFormsModule], + }); + + it('should allow setting the defined properties', () => { + const spectator = createHost('', { + hostProps: { + // control: new FormControl(), // should not compile + foo: 'x', + }, + }); + + expect(spectator.hostComponent.foo).toBe('x'); + + spectator.hostComponent.foo = 'bar'; + // spectator.hostComponent.bar = 'bar'; // should not compile + }); + }); +}); diff --git a/projects/spectator/vitest/test/query-root/query-root.component.spec.ts b/projects/spectator/vitest/test/query-root/query-root.component.spec.ts new file mode 100644 index 00000000..1873fbc4 --- /dev/null +++ b/projects/spectator/vitest/test/query-root/query-root.component.spec.ts @@ -0,0 +1,34 @@ +import { createComponentFactory, Spectator } from '@ngneat/spectator/vitest'; + +import { QueryRootComponent, QueryRootOverlayComponent } from '../../../test/query-root/query-root.component'; + +describe('QueryRootComponent', () => { + let spectator: Spectator; + const createComponent = createComponentFactory(QueryRootComponent); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should allow querying one by directive', () => { + spectator.component.openOverlay(); + + const component = spectator.query(QueryRootOverlayComponent, { root: true }); + expect(component?.title).toBe('Query Root Overlay'); + }); + + it('should allow querying last by directive', () => { + spectator.component.openOverlay(); + + const component = spectator.queryLast(QueryRootOverlayComponent, { root: true }); + expect(component?.title).toBe('Query Root Overlay'); + }); + + it('should allow querying all by directive', () => { + spectator.component.openOverlay(); + + const components = spectator.queryAll(QueryRootOverlayComponent, { root: true }); + expect(components.length).toBe(1); + expect(components[0].title).toBe('Query Root Overlay'); + }); +}); diff --git a/projects/spectator/vitest/test/set-input-alias-names.spec.ts b/projects/spectator/vitest/test/set-input-alias-names.spec.ts new file mode 100644 index 00000000..f6fbad03 --- /dev/null +++ b/projects/spectator/vitest/test/set-input-alias-names.spec.ts @@ -0,0 +1,72 @@ +import { Component, Input, input } from '@angular/core'; +import { createComponentFactory } from '@ngneat/spectator/vitest'; + +describe('SetInputAliasNames', () => { + describe('input decorators', () => { + @Component({ + selector: 'app-root', + template: ` +
{{ name }}
+
{{ numOfYears }}
+ `, + standalone: true, + }) + class DummyComponent { + @Input('userName') name!: string; + @Input({ alias: 'age' }) numOfYears!: number; + } + + const createComponent = createComponentFactory(DummyComponent); + + it('setInput should respect the alias names', () => { + // Arrange + const spectator = createComponent(); + + const nameElement = spectator.query('[data-test="set-input--name"]')!; + const ageElement = spectator.query('[data-test="set-input--age"]')!; + + // Act + spectator.setInput('userName', 'John'); + spectator.setInput('age', '123'); + + // Assert + expect(nameElement.innerHTML).toBe('John'); + expect(ageElement.innerHTML).toBe('123'); + }); + }); + + describe('signal inputs', () => { + @Component({ + selector: 'app-root', + template: ` +
{{ name() }}
+
{{ numOfYears() }}
+ `, + standalone: true, + }) + class DummyComponent { + name = input.required({ alias: 'userName' }); + numOfYears = input(0, { alias: 'age' }); + } + + const createComponent = createComponentFactory(DummyComponent); + + it('setInput should respect the alias names', () => { + // Arrange + const spectator = createComponent({ + detectChanges: false, + }); + + const nameElement = spectator.query('[data-test="set-input--name"]')!; + const ageElement = spectator.query('[data-test="set-input--age"]')!; + + // Act + spectator.setInput('userName', 'John'); + spectator.setInput('age', '123'); + + // Assert + expect(nameElement.innerHTML).toBe('John'); + expect(ageElement.innerHTML).toBe('123'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/signal-input/signal-input.component.spec.ts b/projects/spectator/vitest/test/signal-input/signal-input.component.spec.ts new file mode 100644 index 00000000..42028200 --- /dev/null +++ b/projects/spectator/vitest/test/signal-input/signal-input.component.spec.ts @@ -0,0 +1,40 @@ +import { createComponentFactory, createHostFactory, Spectator, SpectatorHost } from '@ngneat/spectator/vitest'; +import { SignalInputComponent } from '../../../test/signal-input/signal-input.component'; + +describe('SignalInputComponent', () => { + describe('with Spectator', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: SignalInputComponent, + }); + + beforeEach(() => { + spectator = createComponent({ props: { show: true } }); + }); + + it('should render a SignalInputComponent', () => { + expect(spectator.query('#text')).toContainText('Hello'); + }); + }); + + describe('with SpectatorHost', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: SignalInputComponent, + shallow: true, + template: `
`, + }); + + beforeEach(() => { + host = createHost(); + }); + + it('should render a SignalInputComponent', () => { + expect(host.query('#text')).not.toExist(); + host.setHostInput({ show: true }); + expect(host.query('#text')).toContainText('Hello'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/spy-object/spy-object.spec.ts b/projects/spectator/vitest/test/spy-object/spy-object.spec.ts new file mode 100644 index 00000000..ddb61666 --- /dev/null +++ b/projects/spectator/vitest/test/spy-object/spy-object.spec.ts @@ -0,0 +1,42 @@ +import { createSpyObject } from '@ngneat/spectator/vitest'; +import { vi } from 'vitest'; + +import { Person } from '../../../test/spy-object/person'; + +describe('SpyObject', () => { + it('should mock all public methods', () => { + const person = createSpyObject(Person); + + person.sayHi.andReturn('Bye!'); + }); + + it('should enable spying on properties', () => { + const person = createSpyObject(Person); + person.birthYear = 1990; + vi.spyOn(person, 'age', 'get').mockReturnValue(29); + + expect(person.age).toBe(29); + }); + + it('should enable setting properties by just assigning', () => { + const person = createSpyObject(Person); + person.birthYear = 1990; + (person as any).age = 29; + + expect(person.age).toBe(29); + }); + + it('should allow setting properties', () => { + const person = createSpyObject(Person); + + person.birthYear = 1995; // should compile + }); + + it('should allow setting readonly properties with cast method', () => { + const person = createSpyObject(Person); + + person.castToWritable().name = 'Other name'; // should compile + + expect(person.name).toBe('Other name'); + }); +}); diff --git a/projects/spectator/vitest/test/standalone/component/standalone.component.spec.ts b/projects/spectator/vitest/test/standalone/component/standalone.component.spec.ts new file mode 100644 index 00000000..94438549 --- /dev/null +++ b/projects/spectator/vitest/test/standalone/component/standalone.component.spec.ts @@ -0,0 +1,37 @@ +import { createComponentFactory, createHostFactory, Spectator, SpectatorHost } from '@ngneat/spectator/vitest'; +import { StandaloneComponent } from '../../../../test/standalone/component/standalone.component'; + +describe('StandaloneComponent', () => { + describe('with Spectator', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: StandaloneComponent, + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('should render a StandaloneComponent', () => { + expect(spectator.query('#standalone')).toContainText('This stands alone!'); + }); + }); + + describe('with SpectatorHost', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: StandaloneComponent, + template: `
`, + }); + + beforeEach(() => { + host = createHost(); + }); + + it('should render a StandaloneComponent', () => { + expect(host.query('#standalone')).toContainText('This stands alone!'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/standalone/directive/standalone.directive.spec.ts b/projects/spectator/vitest/test/standalone/directive/standalone.directive.spec.ts new file mode 100644 index 00000000..3fb4d5d9 --- /dev/null +++ b/projects/spectator/vitest/test/standalone/directive/standalone.directive.spec.ts @@ -0,0 +1,21 @@ +import { createDirectiveFactory, SpectatorDirective } from '@ngneat/spectator/vitest'; +import { StandaloneDirective } from '../../../../test/standalone/directive/standalone.directive'; + +describe('StandaloneDirective', () => { + describe('with SpectatorDirective', () => { + let spectator: SpectatorDirective; + + const createDirective = createDirectiveFactory({ + directive: StandaloneDirective, + template: `
This stands alone!
`, + }); + + beforeEach(() => { + spectator = createDirective(); + }); + + it('should render a StandaloneDirective', () => { + expect(spectator.query("[class='btn']")).toContainText('This stands alone!'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/standalone/pipe/standalone.pipe.spec.ts b/projects/spectator/vitest/test/standalone/pipe/standalone.pipe.spec.ts new file mode 100644 index 00000000..bedf2907 --- /dev/null +++ b/projects/spectator/vitest/test/standalone/pipe/standalone.pipe.spec.ts @@ -0,0 +1,37 @@ +import { createPipeFactory, SpectatorPipe } from '@ngneat/spectator/vitest'; +import { StandalonePipe } from '../../../../test/standalone/pipe/standalone.pipe'; + +describe('StandalonePipe', () => { + describe('with SpectatorPipe', () => { + let spectator: SpectatorPipe; + + const createPipe = createPipeFactory({ + pipe: StandalonePipe, + template: `
{{ 'This' | standalone }}
`, + }); + + beforeEach(() => { + spectator = createPipe(); + }); + + it('should render and execute the StandalonePipe', () => { + expect(spectator.element.querySelector('#standalone')).toContainText('This stands alone!'); + }); + }); + + describe('with host inputs', () => { + let spectator: SpectatorPipe; + + const createPipe = createPipeFactory({ + pipe: StandalonePipe, + }); + + beforeEach(() => { + spectator = createPipe(`
{{ thisField | standalone }}
`, { hostProps: { thisField: 'This' } }); + }); + + it('should render and execute the StandalonePipe', () => { + expect(spectator.element.querySelector('#standalone')).toContainText('This stands alone!'); + }); + }); +}); diff --git a/projects/spectator/vitest/test/teardown/error.ts b/projects/spectator/vitest/test/teardown/error.ts new file mode 100644 index 00000000..aadbc832 --- /dev/null +++ b/projects/spectator/vitest/test/teardown/error.ts @@ -0,0 +1,3 @@ +export class TeardownError extends Error { + message = 'The error which is thrown during teardown'; +} diff --git a/projects/spectator/vitest/test/teardown/teardown.component.spec.ts b/projects/spectator/vitest/test/teardown/teardown.component.spec.ts new file mode 100644 index 00000000..a5a04959 --- /dev/null +++ b/projects/spectator/vitest/test/teardown/teardown.component.spec.ts @@ -0,0 +1,89 @@ +import { createComponentFactory } from '@ngneat/spectator/vitest'; +import { TestBed } from '@angular/core/testing'; +import { vi } from 'vitest'; + +import { TeardownComponent } from './teardown.component'; + +describe('TeardownComponent', () => { + describe('destroyAfterEach', () => { + describe('destroyAfterEach equals false', () => { + const createComponent = createComponentFactory({ + component: TeardownComponent, + teardown: { + destroyAfterEach: false, + }, + }); + + it('should not call `ngOnDestroy` on the root provider if `destroyAfterEach` is falsy', () => { + // Arrange + const spectator = createComponent(); + const teardownService = spectator.component.teardownService; + const ngOnDestroySpy = vi.spyOn(teardownService, 'ngOnDestroy'); + // Act + TestBed.resetTestingModule(); + // Assert + expect(ngOnDestroySpy).not.toHaveBeenCalled(); + }); + }); + + describe('destroyAfterEach equals true', () => { + const createComponent = createComponentFactory({ + component: TeardownComponent, + teardown: { + destroyAfterEach: true, + }, + }); + + it('should call `ngOnDestroy` on the root provider if `destroyAfterEach` is truthy', () => { + // Arrange + const spectator = createComponent(); + const teardownService = spectator.component.teardownService; + const ngOnDestroySpy = vi.spyOn(teardownService, 'ngOnDestroy'); + // Act + TestBed.resetTestingModule(); + // Assert + expect(ngOnDestroySpy).toHaveBeenCalled(); + }); + }); + }); + + describe('rethrowErrors', () => { + describe('rethrowErrors equals false', () => { + const createComponent = createComponentFactory({ + component: TeardownComponent, + teardown: { + rethrowErrors: false, + destroyAfterEach: true, + }, + }); + + it('should not rethrow errors after the fixture is destroyed if `teardown.rethrowErrors` is falsy', () => { + // Arrange & act + createComponent({ + props: { rethrowErrors: true }, + }); + // Assert + expect(() => TestBed.resetTestingModule()).not.toThrow(); + }); + }); + + describe('rethrowErrors equals true', () => { + const createComponent = createComponentFactory({ + component: TeardownComponent, + teardown: { + rethrowErrors: true, + destroyAfterEach: true, + }, + }); + + it('should rethrow errors after the fixture is destroyed if `teardown.rethrowErrors` is truthy', () => { + // Arrange & act + createComponent({ + props: { rethrowErrors: true }, + }); + // Assert + expect(() => TestBed.resetTestingModule()).toThrow(/component threw errors during cleanup/); + }); + }); + }); +}); diff --git a/projects/spectator/vitest/test/teardown/teardown.component.ts b/projects/spectator/vitest/test/teardown/teardown.component.ts new file mode 100644 index 00000000..102ee9fa --- /dev/null +++ b/projects/spectator/vitest/test/teardown/teardown.component.ts @@ -0,0 +1,22 @@ +import { Component, Input, OnDestroy } from '@angular/core'; + +import { TeardownError } from './error'; +import { TeardownService } from './teardown.service'; + +@Component({ + selector: 'app-teardown', + template: '', + standalone: false, +}) +export class TeardownComponent implements OnDestroy { + @Input() + rethrowErrors = false; + + constructor(readonly teardownService: TeardownService) {} + + ngOnDestroy(): void { + if (this.rethrowErrors) { + throw new TeardownError(); + } + } +} diff --git a/projects/spectator/vitest/test/teardown/teardown.service.ts b/projects/spectator/vitest/test/teardown/teardown.service.ts new file mode 100644 index 00000000..8df9fa46 --- /dev/null +++ b/projects/spectator/vitest/test/teardown/teardown.service.ts @@ -0,0 +1,6 @@ +import { Injectable, OnDestroy } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class TeardownService implements OnDestroy { + ngOnDestroy(): void {} +} diff --git a/projects/spectator/vitest/test/todos-data.service.spec.ts b/projects/spectator/vitest/test/todos-data.service.spec.ts new file mode 100644 index 00000000..eb34c4e8 --- /dev/null +++ b/projects/spectator/vitest/test/todos-data.service.spec.ts @@ -0,0 +1,49 @@ +import { fakeAsync, tick } from '@angular/core/testing'; +import { createHttpFactory, HttpMethod } from '@ngneat/spectator/vitest'; +import { defer } from 'rxjs'; + +import { TodosDataService, UserService } from '../../test/todos-data.service'; + +describe('HttpClient testing', () => { + const http = createHttpFactory({ + service: TodosDataService, + mocks: [UserService], + }); + + it('can test HttpClient.get', () => { + const spectatorHttp = http(); + + spectatorHttp.service.get().subscribe(); + spectatorHttp.expectOne('url', HttpMethod.GET); + }); + + it('can test HttpClient.post', () => { + const spectatorHttp = http(); + + spectatorHttp.service.post(1).subscribe(); + + const req = spectatorHttp.expectOne('url', HttpMethod.POST); + expect(req.request.body.id).toEqual(1); + }); + + it('should test two requests', () => { + const spectatorHttp = http(); + + spectatorHttp.service.twoRequests().subscribe(); + const req = spectatorHttp.expectOne('one', HttpMethod.POST); + req.flush({}); + spectatorHttp.expectOne('two', HttpMethod.GET); + }); + + it('should work with external service', fakeAsync(() => { + const spectatorHttp = http(); + spectatorHttp.inject(UserService).getUser.mockImplementation(() => { + return defer(() => Promise.resolve({})); + }); + + spectatorHttp.service.requestWithExternalService().subscribe(); + tick(); + + spectatorHttp.expectOne('two', HttpMethod.GET); + })); +}); diff --git a/projects/spectator/vitest/test/unless/unless.component.spec.ts b/projects/spectator/vitest/test/unless/unless.component.spec.ts new file mode 100644 index 00000000..57c5979b --- /dev/null +++ b/projects/spectator/vitest/test/unless/unless.component.spec.ts @@ -0,0 +1,24 @@ +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; + +import { AppUnlessDirective } from '../../../test/unless/unless.component'; + +describe('HelloComponent', () => { + let host: SpectatorHost; + + const createHost = createHostFactory(AppUnlessDirective); + + it('should work', () => { + host = createHost(`
Hello world
`); + expect(host.hostElement).toHaveText('Hello world'); + }); + + it('should work', () => { + host = createHost(`
Hello world
`); + expect(host.hostElement).not.toHaveText('Hello world'); + }); + + it('should use hostElement when using query to find element', () => { + host = createHost(`
Hello world
`); + expect(host.query('div')).not.toHaveText('Hello world'); + }); +}); diff --git a/projects/spectator/vitest/test/view-children/view-children.component.spec.ts b/projects/spectator/vitest/test/view-children/view-children.component.spec.ts new file mode 100644 index 00000000..4edae473 --- /dev/null +++ b/projects/spectator/vitest/test/view-children/view-children.component.spec.ts @@ -0,0 +1,73 @@ +import { ElementRef } from '@angular/core'; +import { createHostFactory, createComponentFactory, Spectator, SpectatorHost } from '@ngneat/spectator/vitest'; + +import { ViewChildrenComponent } from '../../../test/view-children/view-children.component'; +import { ChildServiceService } from '../../../test/child-service.service'; +import { ChildComponent } from '../../../test/child/child.component'; + +describe('ViewChildrenComponent', () => { + let spectator: Spectator; + const createComponent = createComponentFactory({ + component: ViewChildrenComponent, + providers: [ChildServiceService], + declarations: [ChildComponent], + }); + + beforeEach(() => (spectator = createComponent())); + + it('should exist', () => { + expect(spectator.component).toBeDefined(); + }); + + it('should expose the view child', () => { + const serviceFromChild = spectator.query(ChildComponent, { read: ChildServiceService }); + const div = spectator.query('div'); + const component = spectator.query(ChildComponent); + spectator.query(ChildComponent, { + read: ElementRef, + }); + const button = spectator.query('button'); + + expect(serviceFromChild).toBeDefined(); + expect(component).toBeDefined(); + expect(div).toExist(); + }); + + it('should expose the view children', () => { + const serviceFromChild = spectator.queryAll(ChildComponent, { read: ChildServiceService }); + const divs = spectator.queryAll('div'); + const components = spectator.queryAll(ChildComponent); + expect(serviceFromChild.length).toBe(4); + expect(components.length).toBe(4); + expect(divs.length).toBe(2); + + expect(spectator.queryAll(ChildComponent, { read: ChildServiceService })).toEqual(serviceFromChild); + expect(spectator.queryAll('app-child', { read: ChildServiceService })).toEqual(serviceFromChild); + }); +}); + +describe('ContentChild', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: ViewChildrenComponent, + providers: [ChildServiceService], + declarations: [ChildComponent], + }); + + it('should get also the content childs', () => { + host = createHost(` + + + + + `); + + const contentChilds = host.queryAll(ChildComponent); + expect(contentChilds.length).toBe(6); + + const lastContentChild = host.queryLast(ChildComponent); + + expect(host.query('app-child:last-child', { read: ChildComponent })).toBe(lastContentChild); + }); +}); diff --git a/projects/spectator/vitest/test/widget.service.spec.ts b/projects/spectator/vitest/test/widget.service.spec.ts new file mode 100644 index 00000000..436f16e4 --- /dev/null +++ b/projects/spectator/vitest/test/widget.service.spec.ts @@ -0,0 +1,18 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/vitest'; + +import { WidgetDataService } from '../../test/widget-data.service'; +import { WidgetService } from '../../test/widget.service'; + +describe('WidgetService', () => { + let spectator: SpectatorService; + const createService = createServiceFactory({ + service: WidgetService, + mocks: [WidgetDataService], + }); + + beforeEach(() => (spectator = createService())); + + it('should be defined', () => { + expect(spectator.service).toBeDefined(); + }); +}); diff --git a/projects/spectator/vitest/test/widget/widget.component.spec.ts b/projects/spectator/vitest/test/widget/widget.component.spec.ts new file mode 100644 index 00000000..32445bdd --- /dev/null +++ b/projects/spectator/vitest/test/widget/widget.component.spec.ts @@ -0,0 +1,25 @@ +import { createHostFactory, SpectatorHost } from '@ngneat/spectator/vitest'; + +import { WidgetComponent } from '../../../test/widget/widget.component'; +import { WidgetService } from '../../../test/widget.service'; + +describe('WidgetComponent', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: WidgetComponent, + mocks: [WidgetService], + }); + + it('should work', () => { + host = createHost(``); + expect(host.component).toBeDefined(); + }); + + it('should call the service method on button click', () => { + host = createHost(``); + host.click('button'); + const widgetService = host.component.widgetService; + expect(widgetService.get).toHaveBeenCalled(); + }); +}); diff --git a/projects/spectator/vitest/test/with-routing/my-page.component.spec.ts b/projects/spectator/vitest/test/with-routing/my-page.component.spec.ts new file mode 100644 index 00000000..5dbca098 --- /dev/null +++ b/projects/spectator/vitest/test/with-routing/my-page.component.spec.ts @@ -0,0 +1,99 @@ +import { Router, RouterLink } from '@angular/router'; +import { createRoutingFactory } from '@ngneat/spectator/vitest'; + +import { MyPageComponent } from '../../../test/with-routing/my-page.component'; + +describe('MyPageComponent', () => { + describe('simple use', () => { + const createComponent = createRoutingFactory(MyPageComponent); + + it('should create', () => { + const spectator = createComponent(); + + expect(spectator.query('.foo')).toExist(); + }); + }); + + describe('route options', () => { + const createComponent = createRoutingFactory({ + component: MyPageComponent, + data: { title: 'lorem', dynamicTitle: 'ipsum' }, + params: { foo: '1', bar: '2' }, + queryParams: { baz: '3' }, + }); + + it('should create with default options', () => { + const spectator = createComponent(); + + expect(spectator.query('.title')).toHaveText('lorem'); + expect(spectator.query('.dynamic-title')).toHaveText('ipsum'); + + expect(spectator.query('.foo')).toHaveText('1'); + expect(spectator.query('.bar')).toHaveText('2'); + expect(spectator.query('.baz')).toHaveText('3'); + }); + + it('should create with overridden options', () => { + const spectator = createComponent({ + params: { foo: 'A', bar: 'B' }, + }); + + expect(spectator.query('.foo')).toHaveText('A'); + expect(spectator.query('.bar')).toHaveText('B'); + expect(spectator.query('.baz')).toHaveText('3'); + }); + + it('should respond to updates', () => { + const spectator = createComponent({ + params: { foo: 'A', bar: 'B' }, + }); + + expect(spectator.query('.foo')).toHaveText('A'); + expect(spectator.query('.bar')).toHaveText('B'); + expect(spectator.query('.baz')).toHaveText('3'); + + spectator.setRouteParam('bar', 'X'); + + expect(spectator.query('.foo')).toHaveText('A'); + expect(spectator.query('.bar')).toHaveText('X'); + expect(spectator.query('.baz')).toHaveText('3'); + expect(spectator.component.fragment).toBeNull(); + + spectator.setRouteQueryParam('baz', 'Y'); + spectator.setRouteFragment('lorem'); + + expect(spectator.query('.foo')).toHaveText('A'); + expect(spectator.query('.bar')).toHaveText('X'); + expect(spectator.query('.baz')).toHaveText('Y'); + expect(spectator.component.fragment).toBe('lorem'); + }); + + it('should support snapshot data', () => { + const spectator = createComponent(); + + expect(spectator.query('.title')).toHaveText('lorem'); + expect(spectator.query('.dynamic-title')).toHaveText('ipsum'); + + spectator.triggerNavigation({ + data: { title: 'new-title', dynamicTitle: 'new-dynamic-title' }, + }); + + expect(spectator.query('.title')).toHaveText('lorem'); + expect(spectator.query('.dynamic-title')).toHaveText('new-dynamic-title'); + }); + }); + + describe('default router mocking', () => { + const createComponent = createRoutingFactory({ + component: MyPageComponent, + }); + + it('should support mocks', () => { + const spectator = createComponent(); + + spectator.click('.link-2'); + + expect(spectator.inject(Router).navigate).toHaveBeenCalledWith(['bar']); + }); + }); +}); diff --git a/projects/spectator/vitest/test/zippy/zippy.component.spec.ts b/projects/spectator/vitest/test/zippy/zippy.component.spec.ts new file mode 100644 index 00000000..141f8a4d --- /dev/null +++ b/projects/spectator/vitest/test/zippy/zippy.component.spec.ts @@ -0,0 +1,161 @@ +import { Component } from '@angular/core'; +import { fakeAsync } from '@angular/core/testing'; +import { FormControl } from '@angular/forms'; +import { SpectatorHost, createHostFactory } from '@ngneat/spectator/vitest'; + +import { QueryService } from '../../../test/query.service'; +import { ZippyComponent } from '../../../test/zippy/zippy.component'; + +describe('ZippyComponent', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: ZippyComponent, + mocks: [QueryService], + componentProviders: [{ provide: QueryService, useValue: 'componentProviders' }], + }); + + it('should should have a zippy component', () => { + host = createHost(``); + + expect('zippy').toExist(); + expect(host.queryHost('zippy')).toExist(); + expect(host.query('zippy')).not.toExist(); + + expect('.non-existing').not.toExist(); + expect(host.queryHost('.non-existing')).not.toExist(); + expect(host.query('.non-existing')).not.toExist(); + }); + + it('should display the title', () => { + host = createHost(``); + expect(host.query('.zippy__title')).toHaveText('Zippy title'); + }); + + it('should display the title from host property', () => { + host = createHost(``, { + hostProps: { + title: 'ZIPPY2', + control: new FormControl(false), + }, + }); + expect(host.query('.zippy__title')).toHaveText('ZIPPY2'); + }); + + it('should have attribute', () => { + host = createHost(`Zippy content`); + expect(host.query('.zippy')).toHaveAttribute('id'); + }); + + it('should have attribute with value', () => { + host = createHost(`Zippy content`); + const a = document.querySelectorAll('.fiv'); + const b = host.query('.color'); + + expect(host.query('.zippy')).toHaveAttribute('id', 'zippy'); + }); + + it('should be checked', () => { + host = createHost(`Zippy content`); + + expect(host.query('.checkbox')).toHaveProperty('checked', true); + }); + + it('should display the content', () => { + host = createHost(`Zippy content`); + + host.click('.zippy__title'); + + expect(host.query('.zippy__content')).toHaveText('Zippy content'); + }); + + it('should display the "Open" word if closed', () => { + host = createHost(`Zippy content`); + + expect(host.query('.arrow')).toHaveText('Open'); + expect(host.query('.arrow')).not.toHaveText('Close'); + }); + + it('should display the "Close" word if open', () => { + host = createHost(`Zippy content`); + + host.click('.zippy__title'); + + expect(host.query('.arrow')).toHaveText('Close'); + expect(host.query('.arrow')).not.toHaveText('Open'); + }); + + it('should be closed by default', () => { + host = createHost(``); + + expect('.zippy__content').not.toExist(); + }); + + it('should toggle the content when clicked', () => { + host = createHost(``); + + host.click('.zippy__title'); + expect(host.query('.zippy__content')).toExist(); + + host.click('.zippy__title'); + expect('.zippy__content').not.toExist(); + }); + + it('should toggle the content when pressing "Enter"', () => { + host = createHost(``); + host.keyboard.pressEnter('.zippy__title'); + expect(host.query('.zippy__content')).toExist(); + + host.keyboard.pressEnter('.zippy__title'); + expect('.zippy__content').not.toExist(); + }); + + it('should work on the host', () => { + host = createHost(``); + host.keyboard.pressEscape(); + expect(host.query('.zippy__content')).toExist(); + + host.keyboard.pressEscape(); + expect('.zippy__content').not.toExist(); + }); +}); + +@Component({ + selector: 'app-custom-host', + template: '', + standalone: false, +}) +class CustomHostComponent { + public title = 'Custom HostComponent'; + public options = { color: 'blue' }; +} + +describe('With Custom Host Component', () => { + let host: SpectatorHost; + + const createHost = createHostFactory({ + component: ZippyComponent, + componentProviders: [{ provide: QueryService, useValue: 'componentProviders' }], + host: CustomHostComponent, + }); + + it('should display the host component title', () => { + host = createHost(``); + + expect(host.query('.zippy__title')).toHaveText('Custom HostComponent'); + }); + + it('should display the host component title', () => { + host = createHost(``); + + expect(host.query('.color')).toHaveText('blue'); + }); + + it('should work with tick', fakeAsync(() => { + host = createHost(``); + host.component.update(); + expect(host.component.updatedAsync).toBeFalsy(); + host.tick(6000); + expect(host.component.updatedAsync).not.toBeFalsy(); + })); +}); diff --git a/projects/spectator/vitest/tsconfig.spec.json b/projects/spectator/vitest/tsconfig.spec.json new file mode 100644 index 00000000..df9b7606 --- /dev/null +++ b/projects/spectator/vitest/tsconfig.spec.json @@ -0,0 +1,17 @@ +{ + "extends": "../tsconfig.spec.json", + "include": [ + "./test/**/*.spec.ts", + "./src/lib/matchers-types.ts" + ], + "files": [ + "../setup-vitest.ts" + ], + "compilerOptions": { + "types": [ + "node", + "vitest", + "@vitest/globals" + ] + } +} diff --git a/tsconfig.json b/tsconfig.json index e92eb315..47562268 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -28,9 +28,14 @@ "@ngneat/spectator/jest": [ "projects/spectator/jest/src/public_api.ts" ], + "@ngneat/spectator/vitest": [ + "projects/spectator/vitest/src/public_api.ts" + ], "@ngneat/spectator/internals": [ "projects/spectator/internals/src/public_api.ts" - ] + ], + // workaround for: https://github.com/rollup/rollup/issues/5199 + "rollup/parseAst": ["./node_modules/rollup/dist/parseAst"] }, "useDefineForClassFields": false } diff --git a/yarn.lock b/yarn.lock index 9ec1e377..69e8e110 100644 --- a/yarn.lock +++ b/yarn.lock @@ -31,6 +31,19 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" +"@analogjs/vite-plugin-angular@^1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@analogjs/vite-plugin-angular/-/vite-plugin-angular-1.10.1.tgz#d57131bf8540380797d1781a4ab5327c786e8745" + integrity sha512-XqRkN/FOLQO+USKHJePKd7v1QD4pSRPQVQEKOI4sIah53+F+jSsJ5SpJEdEE9W+m0hMKgneA3LG92pmw/+KK2w== + dependencies: + ts-morph "^21.0.0" + vfile "^6.0.3" + +"@analogjs/vitest-angular@^1.10.1": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@analogjs/vitest-angular/-/vitest-angular-1.10.1.tgz#ac752bacfccb6f54c741365163c787fe9c41dea6" + integrity sha512-5hB2CvMWagWtAh0lJMRnm9qFIq1uY3ma8Bo+NTug6nLU2BCp0mUeopTBHUUZHS2EWR5jMJ7+zqEmD0/wi0i7sw== + "@angular-builders/common@2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@angular-builders/common/-/common-2.0.0.tgz#76920a63c50a84669c0a786b6281ae9fbf34560d" @@ -59,6 +72,14 @@ "@angular-devkit/core" "19.0.1" rxjs "7.8.1" +"@angular-devkit/architect@0.1900.5": + version "0.1900.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1900.5.tgz#ae5b5b75b24081016ef3ef63aea01376554fc894" + integrity sha512-JxgoIxwGw3QNj6e70d04g5yJ8ZK0g/my22UK0TlRJRbYcfFQr8pL7u3wq77iNlgeHMDwBskZEf4TEZOVSbm7mw== + dependencies: + "@angular-devkit/core" "19.0.5" + rxjs "7.8.1" + "@angular-devkit/architect@>=0.1800.0 < 0.1900.0": version "0.1800.2" resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1800.2.tgz#c4bc51e654558c7e7d27e0558b671d6731d46ccf" @@ -162,6 +183,18 @@ rxjs "7.8.1" source-map "0.7.4" +"@angular-devkit/core@19.0.5": + version "19.0.5" + resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-19.0.5.tgz#3dcd83cc2e00bb718e7c03af82dda436968776d1" + integrity sha512-njBblpYHmlDI+Jtbub9NEm9RH+SBIFmmsgL9uJB8GxQVSo2qo4+f69nTkijRNN8WNKsSkYoRR9+JSl9QXWbyEA== + dependencies: + ajv "8.17.1" + ajv-formats "3.0.1" + jsonc-parser "3.3.1" + picomatch "4.0.2" + rxjs "7.8.1" + source-map "0.7.4" + "@angular-devkit/schematics@19.0.1", "@angular-devkit/schematics@^19.0.1": version "19.0.1" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-19.0.1.tgz#f6f6e30988c42184cc0ae921ee9747756a723baa" @@ -267,6 +300,39 @@ optionalDependencies: lmdb "3.1.5" +"@angular/build@^19.0.0": + version "19.0.5" + resolved "https://registry.yarnpkg.com/@angular/build/-/build-19.0.5.tgz#53207f0075021b427135af6dd6921577f88c9d05" + integrity sha512-/4msIXebFfDWcsyYGDzcxrhn1G1bWVTVbLYqkDXDVYFTqWRpBA8UtQ6eLM8FrJqrHw9e/1cxkqBNsR0tkDJ9FQ== + dependencies: + "@ampproject/remapping" "2.3.0" + "@angular-devkit/architect" "0.1900.5" + "@babel/core" "7.26.0" + "@babel/helper-annotate-as-pure" "7.25.9" + "@babel/helper-split-export-declaration" "7.24.7" + "@babel/plugin-syntax-import-attributes" "7.26.0" + "@inquirer/confirm" "5.0.2" + "@vitejs/plugin-basic-ssl" "1.1.0" + beasties "0.1.0" + browserslist "^4.23.0" + esbuild "0.24.0" + fast-glob "3.3.2" + https-proxy-agent "7.0.5" + istanbul-lib-instrument "6.0.3" + listr2 "8.2.5" + magic-string "0.30.12" + mrmime "2.0.0" + parse5-html-rewriting-stream "7.0.0" + picomatch "4.0.2" + piscina "4.7.0" + rollup "4.26.0" + sass "1.80.7" + semver "7.6.3" + vite "5.4.11" + watchpack "2.4.2" + optionalDependencies: + lmdb "3.1.5" + "@angular/cdk@^19.0.0": version "19.0.0" resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-19.0.0.tgz#ef1d0baa821d1f56d2f36bb69a76550338197341" @@ -3438,6 +3504,16 @@ resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@ts-morph/common@~0.22.0": + version "0.22.0" + resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.22.0.tgz#8951d451622a26472fbc3a227d6c3a90e687a683" + integrity sha512-HqNBuV/oIlMKdkLshXd1zKBqNQCsuPEsgQOkfFQ/eUKjRlwndXW1AjN9LVkBEIukm00gGXSRmfkl0Wv5VXLnlw== + dependencies: + fast-glob "^3.3.2" + minimatch "^9.0.3" + mkdirp "^3.0.1" + path-browserify "^1.0.1" + "@tsconfig/node10@^1.0.7": version "1.0.8" resolved "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz" @@ -3816,6 +3892,11 @@ resolved "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz" integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw== +"@types/unist@^3.0.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/unist/-/unist-3.0.3.tgz#acaab0f919ce69cce629c2d4ed2eb4adc1b6c20c" + integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q== + "@types/ws@^8.5.10": version "8.5.10" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.10.tgz#4acfb517970853fa6574a3a6886791d04a396787" @@ -3938,6 +4019,65 @@ resolved "https://registry.yarnpkg.com/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz#8b840305a6b48e8764803435ec0c716fa27d3802" integrity sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A== +"@vitest/expect@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.8.tgz#13fad0e8d5a0bf0feb675dcf1d1f1a36a1773bc1" + integrity sha512-8ytZ/fFHq2g4PJVAtDX57mayemKgDR6X3Oa2Foro+EygiOJHUXhCqBAAKQYYajZpFoIfvBCF1j6R6IYRSIUFuw== + dependencies: + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" + chai "^5.1.2" + tinyrainbow "^1.2.0" + +"@vitest/mocker@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/mocker/-/mocker-2.1.8.tgz#51dec42ac244e949d20009249e033e274e323f73" + integrity sha512-7guJ/47I6uqfttp33mgo6ga5Gr1VnL58rcqYKyShoRK9ebu8T5Rs6HN3s1NABiBeVTdWNrwUMcHH54uXZBN4zA== + dependencies: + "@vitest/spy" "2.1.8" + estree-walker "^3.0.3" + magic-string "^0.30.12" + +"@vitest/pretty-format@2.1.8", "@vitest/pretty-format@^2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/pretty-format/-/pretty-format-2.1.8.tgz#88f47726e5d0cf4ba873d50c135b02e4395e2bca" + integrity sha512-9HiSZ9zpqNLKlbIDRWOnAWqgcA7xu+8YxXSekhr0Ykab7PAYFkhkwoqVArPOtJhPmYeE2YHgKZlj3CP36z2AJQ== + dependencies: + tinyrainbow "^1.2.0" + +"@vitest/runner@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-2.1.8.tgz#b0e2dd29ca49c25e9323ea2a45a5125d8729759f" + integrity sha512-17ub8vQstRnRlIU5k50bG+QOMLHRhYPAna5tw8tYbj+jzjcspnwnwtPtiOlkuKC4+ixDPTuLZiqiWWQ2PSXHVg== + dependencies: + "@vitest/utils" "2.1.8" + pathe "^1.1.2" + +"@vitest/snapshot@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-2.1.8.tgz#d5dc204f4b95dc8b5e468b455dfc99000047d2de" + integrity sha512-20T7xRFbmnkfcmgVEz+z3AU/3b0cEzZOt/zmnvZEctg64/QZbSDJEVm9fLnnlSi74KibmRsO9/Qabi+t0vCRPg== + dependencies: + "@vitest/pretty-format" "2.1.8" + magic-string "^0.30.12" + pathe "^1.1.2" + +"@vitest/spy@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-2.1.8.tgz#bc41af3e1e6a41ae3b67e51f09724136b88fa447" + integrity sha512-5swjf2q95gXeYPevtW0BLk6H8+bPlMb4Vw/9Em4hFxDcaOxS+e0LOX4yqNxoHzMR2akEB2xfpnWUzkZokmgWDg== + dependencies: + tinyspy "^3.0.2" + +"@vitest/utils@2.1.8": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-2.1.8.tgz#f8ef85525f3362ebd37fd25d268745108d6ae388" + integrity sha512-dwSoui6djdwbfFmIgbIjX2ZhIoG7Ex/+xpxyiEgIGzjliY8xGkcpITKTlp6B4MgtGkF2ilvm97cPM96XZaAgcA== + dependencies: + "@vitest/pretty-format" "2.1.8" + loupe "^3.1.2" + tinyrainbow "^1.2.0" + "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" @@ -4172,6 +4312,11 @@ agent-base@^7.1.1: dependencies: debug "^4.3.4" +agent-base@^7.1.2: + version "7.1.3" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.3.tgz#29435eb821bc4194633a5b89e5bc4703bafc25a1" + integrity sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw== + aggregate-error@^3.0.0: version "3.1.0" resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" @@ -4427,6 +4572,11 @@ arrify@^1.0.1: resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" integrity "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==" +assertion-error@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-2.0.1.tgz#f641a196b335690b1070bf00b6e7593fec190bf7" + integrity sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== + astral-regex@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz" @@ -4746,6 +4896,11 @@ bytes@3.1.2: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + cacache@^18.0.0: version "18.0.1" resolved "https://registry.yarnpkg.com/cacache/-/cacache-18.0.1.tgz#b026d56ad569e4f73cc07c813b3c66707d0fb142" @@ -4840,6 +4995,17 @@ caniuse-lite@^1.0.30001646, caniuse-lite@^1.0.30001669: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz#0eca437bab7d5f03452ff0ef9de8299be6b08e16" integrity sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ== +chai@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/chai/-/chai-5.1.2.tgz#3afbc340b994ae3610ca519a6c70ace77ad4378d" + integrity sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw== + dependencies: + assertion-error "^2.0.1" + check-error "^2.1.1" + deep-eql "^5.0.1" + loupe "^3.1.0" + pathval "^2.0.0" + chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" @@ -4867,6 +5033,11 @@ chardet@^0.7.0: resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +check-error@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-2.1.1.tgz#87eb876ae71ee388fa0471fe423f494be1d96ccc" + integrity sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw== + chokidar@^3.5.1: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -5045,6 +5216,11 @@ co@^4.6.0: resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" integrity "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==" +code-block-writer@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/code-block-writer/-/code-block-writer-12.0.0.tgz#4dd58946eb4234105aff7f0035977b2afdc2a770" + integrity sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w== + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" @@ -5614,6 +5790,13 @@ cssstyle@^2.3.0: dependencies: cssom "~0.3.6" +cssstyle@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-4.1.0.tgz#161faee382af1bafadb6d3867a92a19bcb4aea70" + integrity sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA== + dependencies: + rrweb-cssom "^0.7.1" + custom-event@~1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz" @@ -5661,6 +5844,14 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" +data-urls@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-5.0.0.tgz#2f76906bce1824429ffecb6920f45a0b30f00dde" + integrity sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== + dependencies: + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + date-format@^4.0.14: version "4.0.14" resolved "https://registry.yarnpkg.com/date-format/-/date-format-4.0.14.tgz#7a8e584434fb169a521c8b7aa481f355810d9400" @@ -5699,6 +5890,13 @@ debug@^4.3.6: dependencies: ms "^2.1.3" +debug@^4.3.7: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz" @@ -5712,7 +5910,7 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" integrity "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==" -decimal.js@^10.4.2: +decimal.js@^10.4.2, decimal.js@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== @@ -5727,6 +5925,11 @@ dedent@^1.0.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.1.tgz#4f3fc94c8b711e9bb2800d185cd6ad20f2a90aff" integrity sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg== +deep-eql@^5.0.1: + version "5.0.2" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-5.0.2.tgz#4b756d8d770a9257300825d52a2c2cff99c3a341" + integrity sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== + deep-is@^0.1.3, deep-is@~0.1.3: version "0.1.3" resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz" @@ -6091,6 +6294,11 @@ es-module-lexer@^1.2.1: resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.2.1.tgz#ba303831f63e6a394983fde2f97ad77b22324527" integrity sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg== +es-module-lexer@^1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + esbuild-wasm@0.24.0: version "0.24.0" resolved "https://registry.yarnpkg.com/esbuild-wasm/-/esbuild-wasm-0.24.0.tgz#99f44feb1dfccd25dbe7de1a26326ea1c7aca0d8" @@ -6362,6 +6570,13 @@ estree-walker@^2.0.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + esutils@^2.0.2: version "2.0.3" resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" @@ -6429,6 +6644,11 @@ expand-tilde@^2.0.0, expand-tilde@^2.0.2: dependencies: homedir-polyfill "^1.0.1" +expect-type@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/expect-type/-/expect-type-1.1.0.tgz#a146e414250d13dfc49eafcfd1344a4060fa4c75" + integrity sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA== + expect@^29.0.0: version "29.4.1" resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.1.tgz#58cfeea9cbf479b64ed081fd1e074ac8beb5a1fe" @@ -7088,6 +7308,11 @@ globby@^14.0.0: slash "^5.1.0" unicorn-magic "^0.1.0" +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -7232,6 +7457,13 @@ html-encoding-sniffer@^3.0.0: dependencies: whatwg-encoding "^2.0.0" +html-encoding-sniffer@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz#696df529a7cfd82446369dc5193e590a3735b448" + integrity sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== + dependencies: + whatwg-encoding "^3.1.1" + html-entities@^2.4.0: version "2.5.2" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" @@ -7305,6 +7537,14 @@ http-proxy-agent@^7.0.0: agent-base "^7.1.0" debug "^4.3.4" +http-proxy-agent@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz#9a8b1f246866c028509486585f62b8f2c18c270e" + integrity sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + http-proxy-middleware@3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz#dc1313c75bd00d81e103823802551ee30130ebd1" @@ -7361,6 +7601,14 @@ https-proxy-agent@^7.0.1: agent-base "^7.0.2" debug "4" +https-proxy-agent@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz#da8dfeac7da130b05c2ba4b59c9b6cd66611a6b9" + integrity sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== + dependencies: + agent-base "^7.1.2" + debug "4" + human-signals@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" @@ -8409,6 +8657,33 @@ jsdom@^20.0.0: ws "^8.11.0" xml-name-validator "^4.0.0" +jsdom@^25.0.1: + version "25.0.1" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-25.0.1.tgz#536ec685c288fc8a5773a65f82d8b44badcc73ef" + integrity sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw== + dependencies: + cssstyle "^4.1.0" + data-urls "^5.0.0" + decimal.js "^10.4.3" + form-data "^4.0.0" + html-encoding-sniffer "^4.0.0" + http-proxy-agent "^7.0.2" + https-proxy-agent "^7.0.5" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.12" + parse5 "^7.1.2" + rrweb-cssom "^0.7.1" + saxes "^6.0.0" + symbol-tree "^3.2.4" + tough-cookie "^5.0.0" + w3c-xmlserializer "^5.0.0" + webidl-conversions "^7.0.0" + whatwg-encoding "^3.1.1" + whatwg-mimetype "^4.0.0" + whatwg-url "^14.0.0" + ws "^8.18.0" + xml-name-validator "^5.0.0" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" @@ -8918,6 +9193,11 @@ longest@^2.0.1: resolved "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz" integrity "sha1-eB4YMpaqlPbU2RbcM10NF676I/g= sha512-Ajzxb8CM6WAnFjgiloPsI3bF+WCxcvhdIG3KNA2KN962+tdBsHcuQ4k4qX/EcS/2CRkcc0iAkR956Nib6aXU/Q==" +loupe@^3.1.0, loupe@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-3.1.2.tgz#c86e0696804a02218f2206124c45d8b15291a240" + integrity sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg== + lru-cache@^10.0.1, "lru-cache@^9.1.1 || ^10.0.0": version "10.1.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.1.0.tgz#2098d41c2dc56500e6c88584aa656c84de7d0484" @@ -8959,6 +9239,13 @@ magic-string@0.30.12: dependencies: "@jridgewell/sourcemap-codec" "^1.5.0" +magic-string@^0.30.12: + version "0.30.15" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.15.tgz#d5474a2c4c5f35f041349edaba8a5cb02733ed3c" + integrity sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + make-dir@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" @@ -9216,6 +9503,13 @@ minimatch@^9.0.0: dependencies: brace-expansion "^2.0.1" +minimatch@^9.0.3, minimatch@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" + integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== + dependencies: + brace-expansion "^2.0.1" + minimatch@^9.0.4: version "9.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" @@ -9223,13 +9517,6 @@ minimatch@^9.0.4: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.5: - version "9.0.5" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5" - integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== - dependencies: - brace-expansion "^2.0.1" - minimist-options@4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz" @@ -9702,6 +9989,11 @@ null-check@^1.0.0: resolved "https://registry.npmjs.org/null-check/-/null-check-1.0.0.tgz" integrity "sha1-l33/1xdgErnsMNKjnbXPcqBDnt0= sha512-j8ZNHg19TyIQOWCGeeQJBuu6xZYIEurf8M1Qsfd8mFrGEfIZytbw18YjKWg+LcO25NowXGZXZpKAx+Ui3TFfDw==" +nwsapi@^2.2.12: + version "2.2.16" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.16.tgz#177760bba02c351df1d2644e220c31dfec8cdb43" + integrity sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ== + nwsapi@^2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.2.tgz#e5418863e7905df67d51ec95938d67bf801f0bb0" @@ -10020,6 +10312,11 @@ parseurl@~1.3.2, parseurl@~1.3.3: resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-exists@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" @@ -10106,6 +10403,16 @@ path-type@^5.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-5.0.0.tgz#14b01ed7aea7ddf9c7c3f46181d4d04f9c785bb8" integrity sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg== +pathe@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pathval@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" + integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== + pegjs@^0.10.0: version "0.10.0" resolved "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz" @@ -10376,6 +10683,11 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== +punycode@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + pure-rand@^6.0.0: version "6.0.1" resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.0.1.tgz#31207dddd15d43f299fdcdb2f572df65030c19af" @@ -10853,6 +11165,11 @@ rollup@^4.20.0, rollup@^4.24.0: "@rollup/rollup-win32-x64-msvc" "4.27.4" fsevents "~2.3.2" +rrweb-cssom@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz#c73451a484b86dd7cfb1e0b2898df4b703183e4b" + integrity sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg== + run-applescript@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.0.0.tgz#e5a553c2bffd620e169d276c1cd8f1b64778fbeb" @@ -11170,6 +11487,11 @@ side-channel@^1.0.6: get-intrinsic "^1.2.4" object-inspect "^1.13.1" +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" @@ -11472,6 +11794,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + standard-version@^9.1.0: version "9.3.0" resolved "https://registry.npmjs.org/standard-version/-/standard-version-9.3.0.tgz" @@ -11503,6 +11830,11 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" integrity "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" +std-env@^3.8.0: + version "3.8.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.8.0.tgz#b56ffc1baf1a29dcc80a3bdf11d7fca7c315e7d5" + integrity sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w== + streamroller@^3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/streamroller/-/streamroller-3.1.5.tgz#1263182329a45def1ffaef58d31b15d13d2ee7ff" @@ -11826,6 +12158,43 @@ thunky@^1.0.2: resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== +tinybench@^2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" + integrity sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== + +tinyexec@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/tinyexec/-/tinyexec-0.3.1.tgz#0ab0daf93b43e2c211212396bdb836b468c97c98" + integrity sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ== + +tinypool@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-1.0.2.tgz#706193cc532f4c100f66aa00b01c42173d9051b2" + integrity sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA== + +tinyrainbow@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-1.2.0.tgz#5c57d2fc0fb3d1afd78465c33ca885d04f02abb5" + integrity sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ== + +tinyspy@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" + integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== + +tldts-core@^6.1.67: + version "6.1.67" + resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-6.1.67.tgz#dd1968bb4f09cfabb6746cd783a143fa13699307" + integrity sha512-12K5O4m3uUW6YM5v45Z7wc6NTSmAYj4Tq3de7eXghZkp879IlfPJrUWeWFwu1FS94U5t2vwETgJ1asu8UGNKVQ== + +tldts@^6.1.32: + version "6.1.67" + resolved "https://registry.yarnpkg.com/tldts/-/tldts-6.1.67.tgz#c1aa1f72d0c93d16b8f0f8c53dedc0e6185c2189" + integrity sha512-714VbegxoZ9WF5/IsVCy9rWXKUpPkJq87ebWLXQzNawce96l5oRrRf2eHzB4pT2g/4HQU1dYbu+sdXClYxlDKQ== + dependencies: + tldts-core "^6.1.67" + tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" @@ -11872,6 +12241,13 @@ tough-cookie@^4.1.2: universalify "^0.2.0" url-parse "^1.5.3" +tough-cookie@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.0.0.tgz#6b6518e2b5c070cf742d872ee0f4f92d69eac1af" + integrity sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q== + dependencies: + tldts "^6.1.32" + tr46@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/tr46/-/tr46-3.0.0.tgz" @@ -11879,6 +12255,13 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" +tr46@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-5.0.0.tgz#3b46d583613ec7283020d79019f1335723801cec" + integrity sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g== + dependencies: + punycode "^2.3.1" + tree-dump@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/tree-dump/-/tree-dump-1.0.1.tgz#b448758da7495580e6b7830d6b7834fca4c45b96" @@ -11918,6 +12301,14 @@ ts-jest@^29.0.0: semver "7.x" yargs-parser "^21.0.1" +ts-morph@^21.0.0: + version "21.0.1" + resolved "https://registry.yarnpkg.com/ts-morph/-/ts-morph-21.0.1.tgz#712302a0f6e9dbf1aa8d9cf33a4386c4b18c2006" + integrity sha512-dbDtVdEAncKctzrVZ+Nr7kHpHkv+0JDJb2MjjpBaj8bFeCkePU9rHfMklmhuLFnpeq/EJZk2IhStY6NzqgjOkg== + dependencies: + "@ts-morph/common" "~0.22.0" + code-block-writer "^12.0.0" + ts-node@10.1.0: version "10.1.0" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.1.0.tgz" @@ -11972,6 +12363,11 @@ ts-node@^10.8.1: v8-compile-cache-lib "^3.0.1" yn "3.1.1" +tsconfck@^3.0.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/tsconfck/-/tsconfck-3.1.4.tgz#de01a15334962e2feb526824339b51be26712229" + integrity sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ== + tsconfig-paths@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.1.1.tgz#7f23094ce897fcf4a93f67c4776e813003e48b75" @@ -12163,6 +12559,13 @@ unique-slug@^5.0.0: dependencies: imurmurhash "^0.1.4" +unist-util-stringify-position@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz#449c6e21a880e0855bf5aabadeb3a740314abac2" + integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ== + dependencies: + "@types/unist" "^3.0.0" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" @@ -12261,7 +12664,43 @@ vary@^1, vary@~1.1.2: resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" -vite@5.4.11: +vfile-message@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.2.tgz#c883c9f677c72c166362fd635f21fc165a7d1181" + integrity sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw== + dependencies: + "@types/unist" "^3.0.0" + unist-util-stringify-position "^4.0.0" + +vfile@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/vfile/-/vfile-6.0.3.tgz#3652ab1c496531852bf55a6bac57af981ebc38ab" + integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q== + dependencies: + "@types/unist" "^3.0.0" + vfile-message "^4.0.0" + +vite-node@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-2.1.8.tgz#9495ca17652f6f7f95ca7c4b568a235e0c8dbac5" + integrity sha512-uPAwSr57kYjAUux+8E2j0q0Fxpn8M9VoyfGiRI8Kfktz9NcYMCenwY5RnZxnF1WTu3TGiYipirIzacLL3VVGFg== + dependencies: + cac "^6.7.14" + debug "^4.3.7" + es-module-lexer "^1.5.4" + pathe "^1.1.2" + vite "^5.0.0" + +vite-tsconfig-paths@^5.0.1: + version "5.1.4" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz#d9a71106a7ff2c1c840c6f1708042f76a9212ed4" + integrity sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + tsconfck "^3.0.3" + +vite@5.4.11, vite@^5.0.0: version "5.4.11" resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.11.tgz#3b415cd4aed781a356c1de5a9ebafb837715f6e5" integrity sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q== @@ -12272,6 +12711,32 @@ vite@5.4.11: optionalDependencies: fsevents "~2.3.3" +vitest@2.1.8: + version "2.1.8" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-2.1.8.tgz#2e6a00bc24833574d535c96d6602fb64163092fa" + integrity sha512-1vBKTZskHw/aosXqQUlVWWlGUxSJR8YtiyZDJAFeW2kPAeX6S3Sool0mjspO+kXLuxVWlEDDowBAeqeAQefqLQ== + dependencies: + "@vitest/expect" "2.1.8" + "@vitest/mocker" "2.1.8" + "@vitest/pretty-format" "^2.1.8" + "@vitest/runner" "2.1.8" + "@vitest/snapshot" "2.1.8" + "@vitest/spy" "2.1.8" + "@vitest/utils" "2.1.8" + chai "^5.1.2" + debug "^4.3.7" + expect-type "^1.1.0" + magic-string "^0.30.12" + pathe "^1.1.2" + std-env "^3.8.0" + tinybench "^2.9.0" + tinyexec "^0.3.1" + tinypool "^1.0.1" + tinyrainbow "^1.2.0" + vite "^5.0.0" + vite-node "2.1.8" + why-is-node-running "^2.3.0" + void-elements@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz" @@ -12284,6 +12749,13 @@ w3c-xmlserializer@^4.0.0: dependencies: xml-name-validator "^4.0.0" +w3c-xmlserializer@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz#f925ba26855158594d907313cedd1476c5967f6c" + integrity sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== + dependencies: + xml-name-validator "^5.0.0" + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" @@ -12448,11 +12920,23 @@ whatwg-encoding@^2.0.0: dependencies: iconv-lite "0.6.3" +whatwg-encoding@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz#d0f4ef769905d426e1688f3e34381a99b60b76e5" + integrity sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== + dependencies: + iconv-lite "0.6.3" + whatwg-mimetype@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz" integrity sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q== +whatwg-mimetype@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz#bc1bf94a985dc50388d54a9258ac405c3ca2fc0a" + integrity sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== + whatwg-url@^11.0.0: version "11.0.0" resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-11.0.0.tgz" @@ -12461,6 +12945,14 @@ whatwg-url@^11.0.0: tr46 "^3.0.0" webidl-conversions "^7.0.0" +whatwg-url@^14.0.0: + version "14.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-14.1.0.tgz#fffebec86cc8e6c2a657e50dc606207b870f0ab3" + integrity sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w== + dependencies: + tr46 "^5.0.0" + webidl-conversions "^7.0.0" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz" @@ -12494,6 +12986,14 @@ which@^5.0.0: dependencies: isexe "^3.1.1" +why-is-node-running@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz#a3f69a97107f494b3cdc3bdddd883a7d65cebf04" + integrity sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + wildcard@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" @@ -12587,6 +13087,11 @@ xml-name-validator@^4.0.0: resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz" integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== +xml-name-validator@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz#82be9b957f7afdacf961e5980f1bf227c0bf7673" + integrity sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz"