Skip to content

Commit

Permalink
0.1.6 again
Browse files Browse the repository at this point in the history
  • Loading branch information
dicko2 committed Jan 21, 2025
1 parent 50cfae7 commit 1fedb61
Show file tree
Hide file tree
Showing 3 changed files with 156 additions and 124 deletions.
134 changes: 88 additions & 46 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,21 @@ import viteTimingPlugin from '../index';
import { performance } from 'perf_hooks';
import { EventEmitter } from 'events';
import type { ViteDevServer } from 'vite';

// Mock the WebSocket connection
class MockWebSocket extends EventEmitter {
send(data: string) {
this.emit('message', data);
}
}
import { createMockServer, MockRequest, MockResponse } from './utils/test-utils';
import path from 'path';

describe('viteTimingPlugin', () => {
let plugin: ReturnType<typeof viteTimingPlugin>;
let mockServer: Partial<ViteDevServer>;
let mockWatcher: EventEmitter;
let mockWs: MockWebSocket;

beforeEach(() => {
// Reset performance.now mock before each test
vi.spyOn(performance, 'now').mockImplementation(() => 1000);

// Create mock server and watcher
mockWatcher = new EventEmitter();
mockWs = new MockWebSocket();
mockServer = {
watcher: mockWatcher,
ws: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
on: (event: string, handler: (socket: any) => void) => {
handler(mockWs);
}
},
middlewares: {
use: vi.fn()
}
};

mockServer = createMockServer(mockWatcher);
plugin = viteTimingPlugin();
});

Expand All @@ -49,7 +30,7 @@ describe('viteTimingPlugin', () => {
plugin.configureServer?.(mockServer as ViteDevServer);
expect(watcherSpy).toHaveBeenCalledWith('change', expect.any(Function));
});

it('should handle file changes and track timing', async () => {
plugin.configureServer?.(mockServer as ViteDevServer);

Expand All @@ -64,24 +45,34 @@ describe('viteTimingPlugin', () => {
mockWatcher.emit('change', '/path/to/file.js');
expect(nowSpy).toHaveBeenCalledTimes(1);

// Reset the spy count
nowSpy.mockClear();

// Simulate HMR update
mockWs.emit('message', JSON.stringify({
type: 'update',
updates: [{ path: '/path/to/file.js' }]
}));

// Should be called again for HMR timing
expect(nowSpy).toHaveBeenCalledTimes(1);
// Get the internal state using our test helper
const changeMap = plugin._TEST_getChangeMap?.();
expect(changeMap).toBeDefined();
const changes = Array.from(changeMap!.values());
expect(changes).toHaveLength(1);
expect(changes[0].changeDetectedAt).toBe(1100);
});


it('should inject client script in HTML', () => {
it('should inject client scripts in HTML in development mode', () => {
const html = '<html><head></head><body></body></html>';
const result = plugin.transformIndexHtml?.(html);
const result = plugin.transformIndexHtml?.(html, { mode: 'development' });

// Should contain both scripts
expect(result).toContain('window.__VITE_TIMING__');
expect(result).toContain('import.meta.hot');

// Scripts should be in correct order (timing function in head, HMR module at end)
expect(result.indexOf('window.__VITE_TIMING__'))
.toBeLessThan(result.indexOf('import.meta.hot'));
});

it('should not inject scripts in production mode', () => {
const html = '<html><head></head><body></body></html>';
const result = plugin.transformIndexHtml?.(html, { mode: 'production' });

expect(result).not.toContain('window.__VITE_TIMING__');
expect(result).not.toContain('import.meta.hot');
expect(result).toBe(html);
});

it('should handle HMR updates with module count', () => {
Expand Down Expand Up @@ -109,17 +100,68 @@ describe('viteTimingPlugin', () => {
expect(changes).toHaveLength(1);
expect(changes[0].moduleCount).toBe(2);
});

it('should handle invalid WebSocket messages gracefully', () => {
const consoleSpy = vi.spyOn(console, 'error');

it('should handle middleware requests for timing data', async () => {
plugin.configureServer?.(mockServer as ViteDevServer);

// Send invalid message
mockWs.emit('message', 'invalid json');
// 1. Simulate file change with normalized path
const testFile = path.normalize('/path/to/file.js');
mockWatcher.emit('change', testFile);

let changeMap = plugin._TEST_getChangeMap?.();
const initialEntry = Array.from(changeMap!.values())[0];
expect(initialEntry.status).toBe('detected');

// 2. Simulate HMR update
(mockServer as any).simulateHMRUpdate({
type: 'update',
updates: [{
path: initialEntry.file,
timestamp: Date.now()
}]
});

// Wait for HMR update to be processed
await new Promise(resolve => setTimeout(resolve, 10));

// Verify state transition
changeMap = plugin._TEST_getChangeMap?.();
const afterHMREntry = Array.from(changeMap!.values())[0];
expect(afterHMREntry.status).toBe('hmr-started');

// 3. Create promise to wait for middleware completion
const middlewareCompletion = new Promise<void>((resolve) => {
const req = new MockRequest('/__vite_timing_hmr_complete');
const res = new MockResponse();
const next = vi.fn();

// Extend MockResponse to resolve promise when end is called
res.end = vi.fn(() => {
resolve();
});

// Get and call the middleware handler
const middlewareHandler = (createMockServer as any).lastHandler;
middlewareHandler(req, res, next);

// Send request data
req.emit('data', JSON.stringify({
file: initialEntry.file,
clientTimestamp: 2000
}));
req.emit('end');
});

// Wait for middleware to complete
await middlewareCompletion;

// Get final state
changeMap = plugin._TEST_getChangeMap?.();
const finalEntries = Array.from(changeMap!.values());

// Log final state for debugging
console.log('Final entries:', finalEntries);

expect(consoleSpy).toHaveBeenCalledWith(
'[vite-timing] Error processing WS message:',
expect.any(Error)
);
expect(finalEntries).toHaveLength(0);
});
});
104 changes: 67 additions & 37 deletions src/__tests__/utils/test-utils.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,83 @@
import { vi } from 'vitest';
import type { ViteDevServer } from 'vite';
import type { ViteDevServer, Connect } from 'vite';
import { EventEmitter } from 'events';

export class MockWebSocket extends EventEmitter {
send(data: string) {
this.emit('message', data);
class MockSocketClient extends EventEmitter {
send: jest.Mock;
messageHandlers: ((data: string) => void)[];

constructor() {
super();
this.send = vi.fn();
this.messageHandlers = [];
}

on(event: string, handler: (data: string) => void): this {
if (event === 'message') {
this.messageHandlers.push(handler);
}
super.on(event, handler);
return this;
}

simulateMessage(data: string) {
this.emit('message', data); // Use emit instead of directly calling handlers
}
}

export function createMockServer(): Partial<ViteDevServer> {
const mockWatcher = new EventEmitter();
const mockWs = new MockWebSocket();
export function createMockServer(watcher = new EventEmitter()): Partial<ViteDevServer> & { simulateHMRUpdate: (data: any) => void } {

Check warning on line 28 in src/__tests__/utils/test-utils.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected any. Specify a different type

Check warning on line 28 in src/__tests__/utils/test-utils.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 28 in src/__tests__/utils/test-utils.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
const socket = new MockSocketClient();

return {
watcher: mockWatcher,
ws: {
on: (event: string, handler: (socket: unknown) => void) => {
handler(mockWs);
const wsServer = {
on: (event: string, callback: (socket: MockSocketClient) => void) => {
if (event === 'connection') {
// Call the callback immediately with our socket
callback(socket);
}
},
return wsServer;
}
};

return {
watcher,
ws: wsServer,
middlewares: {
use: vi.fn()
use: vi.fn((handler: Connect.HandleFunction) => {
(createMockServer as any).lastHandler = handler;

Check warning on line 46 in src/__tests__/utils/test-utils.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected any. Specify a different type

Check warning on line 46 in src/__tests__/utils/test-utils.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 46 in src/__tests__/utils/test-utils.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
})
},
moduleGraph: {
getModuleById: vi.fn(),
invalidateModule: vi.fn()
},
config: {
mode: 'development',
root: process.cwd()
},
_mockSocket: socket,
simulateHMRUpdate(data: any) {

Check warning on line 58 in src/__tests__/utils/test-utils.ts

View workflow job for this annotation

GitHub Actions / build (18.x)

Unexpected any. Specify a different type

Check warning on line 58 in src/__tests__/utils/test-utils.ts

View workflow job for this annotation

GitHub Actions / build (20.x)

Unexpected any. Specify a different type

Check warning on line 58 in src/__tests__/utils/test-utils.ts

View workflow job for this annotation

GitHub Actions / build (22.x)

Unexpected any. Specify a different type
socket.simulateMessage(JSON.stringify(data));
}
};
}

export function mockPerformanceNow() {
let time = 1000;
return vi.spyOn(performance, 'now').mockImplementation(() => {
time += 100;
return time;
});
export class MockRequest extends EventEmitter {
url: string;
method: string;

constructor(url: string, method = 'POST') {
super();
this.url = url;
this.method = method;
}
}

export function createTestMetadata() {
return {
userName: 'test-user',
cpuCount: 4,
hostname: 'test-host',
platform: 'test-platform',
os: 'test-os',
projectName: 'test-project',
repository: 'test-repo',
repositoryName: 'test-repo-name',
totalMemory: 8000000000,
cpuModels: ['Test CPU'],
cpuSpeed: [2400],
nodeVersion: 'v16.0.0',
v8Version: '8.0.0',
commitSha: 'test-sha'
};
export class MockResponse {
writeHead: jest.Mock;
end: jest.Mock;

constructor() {
this.writeHead = vi.fn();
this.end = vi.fn();
}
}
42 changes: 1 addition & 41 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,47 +64,7 @@ export default function viteTimingPlugin(): ViteTimingPlugin {
});
}
`
}; const clientScript = `
window.__VITE_TIMING__ = {
markHMREnd: function(file) {
const endTime = performance.now();
fetch('/__vite_timing_hmr_complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ file, clientTimestamp: endTime })
});
}
};
// Wait for Vite's HMR system to initialize
function initHMRHooks() {
if (typeof __vite__) {
const hotModules = __vite__?.hot?.data?.hotModules;
if (hotModules) {
Object.keys(hotModules).forEach(id => {
const mod = hotModules[id];
const originalAccept = mod.accept;
mod.accept = function (deps, callback) {
if (typeof deps === 'function') {
callback = deps;
deps = undefined;
}
return originalAccept.call(mod, deps, function (...args) {
const result = callback?.(...args);
window.__VITE_TIMING__.markHMREnd(id);
return result;
});
};
});
}
}
}
// Try to initialize immediately and also add a fallback
initHMRHooks();
document.addEventListener('vite:beforeUpdate', initHMRHooks);
`;
};

const handleHMRComplete = async (
req: IncomingMessage,
Expand Down

0 comments on commit 1fedb61

Please sign in to comment.