Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 9 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@
"release": "release --deps @uirouter/core @uirouter/rx",
"check-peer-dependencies": "check-peer-dependencies",
"test": "npm run test:zone && npm run test:zoneless",
"test:zone": "jest --rootDir test",
"test:zone:watch": "jest --rootDir test --watchAll",
"test:debug": "node --inspect-brk node_modules/.bin/jest --rootDir test --runInBand",
"test:zone": "vitest run --project zone",
"test:zone:watch": "vitest watch --project zone",
"test:zone:debug": "vitest --project zone --no-file-parallelism --inspect-brk",
"test:downstream": "test_downstream_projects",
"test:zoneless": "jest --rootDir test-zoneless",
"test:zoneless:watch": "jest --rootDir test-zoneless --watchAll",
"test:zoneless": "vitest run --project zoneless",
"test:zoneless:watch": "vitest watch --project zoneless",
"docs": "generate_docs",
"docs:publish": "generate_docs && publish_docs",
"prepublishOnly": "echo && echo DO NOT RUN npm publish. Instead, run npm run release && echo && echo && false",
Expand Down Expand Up @@ -53,22 +53,21 @@
"@uirouter/rx": "^1.0.0"
},
"devDependencies": {
"@analogjs/vite-plugin-angular": "^2.2.0",
"@analogjs/vitest-angular": "^2.2.0",
"@angular-devkit/architect": "^0.2100.0",
"@angular-devkit/build-angular": "^21.0.4",
"@angular/common": "^21.0.3",
"@angular/compiler": "^21.0.3",
"@angular/compiler-cli": "^21.0.3",
"@angular/core": "^21.0.3",
"@angular/platform-browser": "^21.0.3",
"@angular/platform-browser-dynamic": "^21.0.3",
"@types/jest": "^30.0.0",
"@types/node": "^24.10.1",
"@uirouter/core": "^6.1.2",
"@uirouter/publish-scripts": "2.7.0",
"@uirouter/rx": "^1.0.0",
"husky": "^9.0.0",
"jest": "^30.2.0",
"jest-environment-jsdom": "^30.2.0",
"jest-preset-angular": "^16.0.0",
"jsdom": "^27.4.0",
"ng-packagr": "^21.0.0",
"prettier": "^3.4.0",
Expand All @@ -77,14 +76,9 @@
"tslib": "^2.8.1",
"tslint": "^6.1.0",
"typescript": "~5.9.3",
"vitest": "^4.0.8",
"zone.js": "~0.16.0"
},
"jest": {
"preset": "jest-preset-angular",
"setupFilesAfterEnv": [
"./setupJest.ts"
]
},
"checkPeerDependencies": {
"ignore": [
"ajv",
Expand Down
18 changes: 18 additions & 0 deletions test-zoneless/setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import '@analogjs/vitest-angular/setup-testbed';
import { afterEach } from 'vitest';

import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
import { getTestBed, TestBed } from '@angular/core/testing';

getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting(), {
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});

// Global cleanup after each test to ensure test isolation
afterEach(() => {
TestBed.resetTestingModule();
});

// Shared browser mocks for jsdom
import '../test/browser-mocks';
2 changes: 0 additions & 2 deletions test-zoneless/setupJest.ts

This file was deleted.

12 changes: 10 additions & 2 deletions test/jestGlobalMocks.ts → test/browser-mocks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
/**
* Browser mocks for jsdom environment.
* Shared between zone and zoneless test setups.
*/

Object.defineProperty(window, 'CSS', { value: null });

Object.defineProperty(document, 'doctype', {
value: '<!DOCTYPE html>',
});

Object.defineProperty(window, 'getComputedStyle', {
value: () => {
return {
Expand All @@ -10,9 +17,10 @@ Object.defineProperty(window, 'getComputedStyle', {
};
},
});

/**
* ISSUE: https://github.com/angular/material2/issues/7101
* Workaround for JSDOM missing transform property
* Workaround for JSDOM missing transform property.
* @see https://github.com/angular/material2/issues/7101
*/
Object.defineProperty(document.body.style, 'transform', {
value: () => {
Expand Down
3 changes: 2 additions & 1 deletion test/hooks/onBefore.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import { vi, describe, beforeEach, it, expect } from 'vitest';
import { memoryLocationPlugin, UIRouter, StateService } from '@uirouter/core';
import { UIRouterModule } from '../../src';

Expand All @@ -8,7 +9,7 @@ export class AppComponent {}

describe('onBefore hook', () => {
let stateService;
const hookSpy = jest.fn(trans => (stateService = trans.injector().get(StateService)));
const hookSpy = vi.fn(trans => (stateService = trans.injector().get(StateService)));

const setupTests = () => {
const configFn = (router: UIRouter) => {
Expand Down
92 changes: 53 additions & 39 deletions test/loadComponent/loadComponent.spec.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,80 @@
import { memoryLocationPlugin, UIRouter } from "@uirouter/core";
import { UIRouterModule } from "../../src/uiRouterNgModule";
import { inject, TestBed, waitForAsync } from "@angular/core/testing";
import { UIView } from "../../src/directives/uiView";
import { Ng2StateDeclaration } from "../../src/interface";
import { memoryLocationPlugin, UIRouter } from '@uirouter/core';
import { UIRouterModule } from '../../src/uiRouterNgModule';
import { TestBed } from '@angular/core/testing';
import { UIView } from '../../src/directives/uiView';
import { Ng2StateDeclaration } from '../../src/interface';
import { describe, beforeEach, it, expect } from 'vitest';

const fooState = {
name: 'foo',
url: '/foo',
loadComponent: () => import("./foo/foo.component").then(result => result.FooComponent)
loadComponent: () => import('./foo/foo.component').then((result) => result.FooComponent),
};

const barState = {
name: 'bar',
url: '/bar',
loadComponent: () => import("./bar/bar.component").then(result => result.BarComponent)
loadComponent: () => import('./bar/bar.component').then((result) => result.BarComponent),
};

function configFn(router: UIRouter) {
router.plugin(memoryLocationPlugin);
}

describe('lazy loading', () => {

beforeEach(() => {
const routerModule = UIRouterModule.forRoot({ useHash: true, states: [], config: configFn });
TestBed.configureTestingModule({
declarations: [],
imports: [routerModule]
imports: [routerModule],
});
});

it('should lazy load a standalone component', waitForAsync(
inject([UIRouter], ({ stateRegistry, stateService, globals }: UIRouter) => {
stateRegistry.register(fooState);
const fixture = TestBed.createComponent(UIView);
fixture.detectChanges();
const names = stateRegistry.get().map(state => state.name).sort();
expect(names.length).toBe(2);
expect(names).toEqual(['', 'foo']);
it('should lazy load a standalone component', async () => {
const router = TestBed.inject(UIRouter);
const { stateRegistry, stateService, globals } = router;

stateRegistry.register(fooState);
const fixture = TestBed.createComponent(UIView);
fixture.detectChanges();
const names = stateRegistry
.get()
.map((state) => state.name)
.sort();
expect(names.length).toBe(2);
expect(names).toEqual(['', 'foo']);

stateService.go('foo')
.then(() => {
expect(globals.current.name).toBe('foo');
expect((globals.current as Ng2StateDeclaration).component).toBeTruthy();
const innerText = fixture.debugElement.nativeElement.textContent.replace(/\s+/g, ' ').trim();
expect(innerText).toBe('FOO');
});
})
));
await stateService.go('foo');
fixture.detectChanges();

expect(globals.current.name).toBe('foo');
expect((globals.current as Ng2StateDeclaration).component).toBeTruthy();
const innerText = fixture.debugElement.nativeElement.textContent.replace(/\s+/g, ' ').trim();
expect(innerText).toBe('FOO');
});

it('should throw error if component is not standalone', waitForAsync(
inject([UIRouter], ({ stateRegistry, stateService }: UIRouter) => {
stateRegistry.register(barState);
const fixture = TestBed.createComponent(UIView);
fixture.detectChanges();
const names = stateRegistry.get().map(state => state.name).sort();
expect(names.length).toBe(2);
expect(names).toEqual(['', 'bar']);
it('should throw error if component is not standalone', async () => {
const router = TestBed.inject(UIRouter);
const { stateRegistry, stateService } = router;

const success = () => { throw Error('success not expected') };
const error = err => expect(err.detail.message).toBe("Is not a standalone component.");
stateService.go('bar').then(success, error);
})
));
// Suppress default error handler to avoid stderr noise for expected errors
stateService.defaultErrorHandler(() => {});

stateRegistry.register(barState);
const fixture = TestBed.createComponent(UIView);
fixture.detectChanges();
const names = stateRegistry
.get()
.map((state) => state.name)
.sort();
expect(names.length).toBe(2);
expect(names).toEqual(['', 'bar']);

try {
await stateService.go('bar');
throw new Error('success not expected');
} catch (err) {
expect(err).toHaveProperty('detail.message', 'Is not a standalone component.');
}
});
});
31 changes: 18 additions & 13 deletions test/ngModule/deferInitialRender.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import { inject, TestBed } from '@angular/core/testing';
import { UIRouterModule } from '../../src/uiRouterNgModule';
import { memoryLocationPlugin, UIRouter } from '@uirouter/core';
import {
APP_INITIALIZER,
ApplicationInitStatus,
Component,
} from '@angular/core';
import { APP_INITIALIZER, ApplicationInitStatus, Component } from '@angular/core';
import { Ng2StateDeclaration } from '../../src/interface';
import { describe, beforeEach, it, expect } from 'vitest';

const timeout = (delay?: number) => new Promise(resolve => setTimeout(resolve, delay));
const timeout = (delay?: number) => new Promise((resolve) => setTimeout(resolve, delay));
const configFn = (router: UIRouter) => router.plugin(memoryLocationPlugin);

@Component({ selector: 'home', template: 'HOME', standalone: false })
Expand All @@ -19,8 +16,8 @@ export class AppComponent {}

const setupTests = (deferInitialRender: boolean) => {
let resolveData, resolveAppInitializer;
const promise = new Promise<any>(_resolve => (resolveData = _resolve));
const appInitializer = new Promise<any>(_resolve => (resolveAppInitializer = _resolve));
const promise = new Promise<any>((_resolve) => (resolveData = _resolve));
const appInitializer = new Promise<any>((_resolve) => (resolveAppInitializer = _resolve));

const homeState: Ng2StateDeclaration = {
name: 'home',
Expand All @@ -39,9 +36,7 @@ const setupTests = (deferInitialRender: boolean) => {
TestBed.configureTestingModule({
declarations: [HomeComponent, AppComponent],
imports: [routerModule],
providers: [
{ provide: APP_INITIALIZER, useValue: () => appInitializer, multi: true },
],
providers: [{ provide: APP_INITIALIZER, useValue: () => appInitializer, multi: true }],
});

return { resolveData, resolveAppInitializer };
Expand Down Expand Up @@ -94,22 +89,32 @@ describe('deferInitialRender == true', () => {
const { stateService } = router;
const fixture = TestBed.createComponent(AppComponent);

// Before app initializer resolves, status should not be done
expect(status.done).toBe(false);

resolveAppInitializer();

// Start a transition to 'home' - this has a resolve that returns a promise
const goPromise = stateService.go('home');

fixture.detectChanges();
await fixture.whenStable();

// Key assertion: with deferInitialRender=true, the application should NOT
// be marked as done until the initial transition completes.
// The resolve data is still pending at this point.
expect(status.done).toBe(false);

// Wait, then resolve the data that the 'home' state is waiting for
await timeout();
resolveData();

// Wait for the transition to complete
await goPromise;
expect(status.done).toBe(false);

await timeout();
await fixture.whenStable();

// After everything settles, status.done should be true
expect(status.done).toBe(true);
});
});
Loading