refactor: migrate test suite from Jest to Jasmine and add zone.js polyfills
This commit is contained in:
parent
6c4febe205
commit
600238de44
@ -58,6 +58,10 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
"karmaConfig": "karma.conf.cjs",
|
"karmaConfig": "karma.conf.cjs",
|
||||||
|
"polyfills": [
|
||||||
|
"zone.js",
|
||||||
|
"zone.js/testing"
|
||||||
|
],
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.spec.ts"
|
"src/**/*.spec.ts"
|
||||||
],
|
],
|
||||||
|
10
package-lock.json
generated
10
package-lock.json
generated
@ -62,7 +62,8 @@
|
|||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0",
|
||||||
|
"zone.js": "^0.15.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@algolia/abtesting": {
|
"node_modules/@algolia/abtesting": {
|
||||||
@ -18132,6 +18133,13 @@
|
|||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zone.js": {
|
||||||
|
"version": "0.15.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.1.tgz",
|
||||||
|
"integrity": "sha512-XE96n56IQpJM7NAoXswY3XRLcWFW83xe0BiAOeMD7K5k5xecOeul3Qcpx6GqEeeHNkW5DWL5zOyTbEfB4eti8w==",
|
||||||
|
"devOptional": true,
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,8 @@
|
|||||||
"postcss": "^8.4.49",
|
"postcss": "^8.4.49",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "~5.8.2",
|
"typescript": "~5.8.2",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0",
|
||||||
|
"zone.js": "^0.15.1"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@angular/core": "20.3.2",
|
"@angular/core": "20.3.2",
|
||||||
|
@ -17,24 +17,44 @@ describe('sendBatch', () => {
|
|||||||
data: {},
|
data: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let originalFetch: typeof fetch | undefined;
|
||||||
|
let fetchSpy: (jasmine.Spy & typeof fetch) | undefined;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
originalFetch = globalThis.fetch;
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Mock fetch
|
fetchSpy = jasmine.createSpy('fetch') as jasmine.Spy & typeof fetch;
|
||||||
global.fetch = jest.fn();
|
(globalThis as { fetch: typeof fetch }).fetch = fetchSpy;
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.restoreAllMocks();
|
fetchSpy = undefined;
|
||||||
|
if (originalFetch) {
|
||||||
|
(globalThis as { fetch: typeof fetch }).fetch = originalFetch;
|
||||||
|
} else {
|
||||||
|
delete (globalThis as { fetch?: typeof fetch }).fetch;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
if (originalFetch) {
|
||||||
|
(globalThis as { fetch: typeof fetch }).fetch = originalFetch;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send single record', async () => {
|
it('should send single record', async () => {
|
||||||
(global.fetch as jest.Mock).mockResolvedValue({
|
fetchSpy!.and.returnValue(
|
||||||
ok: true,
|
Promise.resolve({
|
||||||
status: 200,
|
ok: true,
|
||||||
});
|
status: 200,
|
||||||
|
} as Response)
|
||||||
|
);
|
||||||
|
|
||||||
await sendBatch([mockRecord], '/api/log');
|
await sendBatch([mockRecord], '/api/log');
|
||||||
|
|
||||||
expect(global.fetch).toHaveBeenCalledWith('/api/log', {
|
expect(fetchSpy).toHaveBeenCalledWith('/api/log', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -44,15 +64,17 @@ describe('sendBatch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should send multiple records as array', async () => {
|
it('should send multiple records as array', async () => {
|
||||||
(global.fetch as jest.Mock).mockResolvedValue({
|
fetchSpy.and.returnValue(
|
||||||
ok: true,
|
Promise.resolve({
|
||||||
status: 200,
|
ok: true,
|
||||||
});
|
status: 200,
|
||||||
|
} as Response)
|
||||||
|
);
|
||||||
|
|
||||||
const records = [mockRecord, { ...mockRecord, event: 'NAVIGATE' as const }];
|
const records = [mockRecord, { ...mockRecord, event: 'NAVIGATE' as const }];
|
||||||
await sendBatch(records, '/api/log');
|
await sendBatch(records, '/api/log');
|
||||||
|
|
||||||
expect(global.fetch).toHaveBeenCalledWith('/api/log', {
|
expect(fetchSpy).toHaveBeenCalledWith('/api/log', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
@ -62,26 +84,38 @@ describe('sendBatch', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should throw on HTTP error', async () => {
|
it('should throw on HTTP error', async () => {
|
||||||
(global.fetch as jest.Mock).mockResolvedValue({
|
fetchSpy!.and.returnValue(
|
||||||
ok: false,
|
Promise.resolve({
|
||||||
status: 500,
|
ok: false,
|
||||||
statusText: 'Internal Server Error',
|
status: 500,
|
||||||
});
|
statusText: 'Internal Server Error',
|
||||||
|
} as Response)
|
||||||
await expect(sendBatch([mockRecord], '/api/log')).rejects.toThrow(
|
|
||||||
'HTTP 500: Internal Server Error'
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendBatch([mockRecord], '/api/log');
|
||||||
|
fail('Expected sendBatch to throw on HTTP error');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toEqual(jasmine.any(Error));
|
||||||
|
expect((error as Error).message).toBe('HTTP 500: Internal Server Error');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw on network error', async () => {
|
it('should throw on network error', async () => {
|
||||||
(global.fetch as jest.Mock).mockRejectedValue(new Error('Network error'));
|
fetchSpy!.and.returnValue(Promise.reject(new Error('Network error')));
|
||||||
|
|
||||||
await expect(sendBatch([mockRecord], '/api/log')).rejects.toThrow('Network error');
|
try {
|
||||||
|
await sendBatch([mockRecord], '/api/log');
|
||||||
|
fail('Expected sendBatch to throw on network error');
|
||||||
|
} catch (error) {
|
||||||
|
expect(error).toEqual(jasmine.any(Error));
|
||||||
|
expect((error as Error).message).toBe('Network error');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not send empty batch', async () => {
|
it('should not send empty batch', async () => {
|
||||||
await sendBatch([], '/api/log');
|
await sendBatch([], '/api/log');
|
||||||
|
|
||||||
expect(global.fetch).not.toHaveBeenCalled();
|
expect(fetchSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,17 +1,45 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { PLATFORM_ID } from '@angular/core';
|
||||||
import { LogService } from './log.service';
|
import { LogService } from './log.service';
|
||||||
import { environment } from './environment';
|
import { environment } from './environment';
|
||||||
|
|
||||||
describe('LogService', () => {
|
describe('LogService', () => {
|
||||||
let service: LogService;
|
let service: LogService;
|
||||||
|
let originalOnLine: boolean | undefined;
|
||||||
|
let originalRequestIdle: (typeof window)[keyof typeof window] | undefined;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({});
|
// Clear storages BEFORE creating the service instance
|
||||||
service = TestBed.inject(LogService);
|
|
||||||
|
|
||||||
// Clear localStorage and sessionStorage
|
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
|
|
||||||
|
// Make sure env won't trigger immediate flushes during assertions
|
||||||
|
environment.logging.enabled = true;
|
||||||
|
environment.logging.batchSize = 9999; // avoid immediate batch flush
|
||||||
|
environment.logging.debounceMs = 60 * 60 * 1000; // 1h to avoid timer firing
|
||||||
|
|
||||||
|
// Stub navigator.onLine and requestIdleCallback for deterministic behavior
|
||||||
|
originalOnLine = navigator.onLine;
|
||||||
|
Object.defineProperty(navigator, 'onLine', { value: true, configurable: true });
|
||||||
|
originalRequestIdle = (window as any).requestIdleCallback;
|
||||||
|
(window as any).requestIdleCallback = (cb: Function) => cb();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [{ provide: PLATFORM_ID, useValue: 'browser' }],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(LogService);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore stubs
|
||||||
|
if (originalOnLine !== undefined) {
|
||||||
|
Object.defineProperty(navigator, 'onLine', { value: originalOnLine, configurable: true });
|
||||||
|
}
|
||||||
|
if (originalRequestIdle) {
|
||||||
|
(window as any).requestIdleCallback = originalRequestIdle;
|
||||||
|
} else {
|
||||||
|
delete (window as any).requestIdleCallback;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be created', () => {
|
it('should be created', () => {
|
||||||
@ -61,12 +89,13 @@ describe('LogService', () => {
|
|||||||
|
|
||||||
it('should not log when disabled', () => {
|
it('should not log when disabled', () => {
|
||||||
const originalEnabled = environment.logging.enabled;
|
const originalEnabled = environment.logging.enabled;
|
||||||
|
environment.logging.enabled = false;
|
||||||
|
|
||||||
service.log('APP_START');
|
service.log('APP_START');
|
||||||
|
|
||||||
const queue = (service as any).queue;
|
const queue = (service as any).queue;
|
||||||
expect(queue.length).toBe(0);
|
expect(queue.length).toBe(0);
|
||||||
|
|
||||||
environment.logging.enabled = originalEnabled;
|
environment.logging.enabled = originalEnabled;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -95,10 +124,9 @@ describe('LogService', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
localStorage.setItem('obsiviewer.log.queue', JSON.stringify(mockQueue));
|
localStorage.setItem('obsiviewer.log.queue', JSON.stringify(mockQueue));
|
||||||
|
// Trigger loading using the existing instance
|
||||||
// Create new service instance
|
(service as any).loadQueueFromStorage();
|
||||||
const newService = TestBed.inject(LogService);
|
const queue = (service as any).queue;
|
||||||
const queue = (newService as any).queue;
|
|
||||||
|
|
||||||
expect(queue.length).toBe(1);
|
expect(queue.length).toBe(1);
|
||||||
expect(queue[0].event).toBe('APP_START');
|
expect(queue[0].event).toBe('APP_START');
|
||||||
|
13
src/test.ts
Normal file
13
src/test.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import 'zone.js';
|
||||||
|
import 'zone.js/testing';
|
||||||
|
import { getTestBed } from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting,
|
||||||
|
} from '@angular/platform-browser-dynamic/testing';
|
||||||
|
|
||||||
|
// Initialize the Angular testing environment.
|
||||||
|
getTestBed().initTestEnvironment(
|
||||||
|
BrowserDynamicTestingModule,
|
||||||
|
platformBrowserDynamicTesting(),
|
||||||
|
);
|
@ -38,8 +38,9 @@
|
|||||||
"./src/**/*.d.ts"
|
"./src/**/*.d.ts"
|
||||||
],
|
],
|
||||||
"exclude": [
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
"node_modules"
|
"./src/**/*.spec.ts",
|
||||||
|
"./src/**/*.spec.tsx"
|
||||||
],
|
],
|
||||||
"angularCompilerOptions": {
|
"angularCompilerOptions": {
|
||||||
"disableTypeScriptVersionCheck": true
|
"disableTypeScriptVersionCheck": true
|
||||||
|
Loading…
x
Reference in New Issue
Block a user