Skip to content

Commit

Permalink
0.1.14
Browse files Browse the repository at this point in the history
  • Loading branch information
dicko2 committed Jan 22, 2025
1 parent d97a98a commit 00f66a8
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 357 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "agoda-devfeedback-vite",
"version": "0.1.13",
"version": "0.1.14",
"description": "Vite plugin for collecting and reporting development feedback metrics",
"type": "module",
"exports": {
Expand Down
286 changes: 133 additions & 153 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,171 +3,151 @@ import viteTimingPlugin from '../index';
import { performance } from 'perf_hooks';
import { EventEmitter } from 'events';
import type { ViteDevServer } from 'vite';
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;

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

// Create mock server and watcher
mockWatcher = new EventEmitter();
mockServer = createMockServer(mockWatcher);
plugin = viteTimingPlugin();
});
class MockRequest extends EventEmitter {
url: string;
method: string;

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

it('should register file watcher on server configure', () => {
const watcherSpy = vi.spyOn(mockWatcher, 'on');
plugin.configureServer?.(mockServer as ViteDevServer);
expect(watcherSpy).toHaveBeenCalledWith('change', expect.any(Function));
});
class MockResponse {
writeHead: jest.Mock;
end: jest.Mock;

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

// Mock performance.now to return increasing values
let timeCounter = 1000;
const nowSpy = vi.spyOn(performance, 'now').mockImplementation(() => {
timeCounter += 100;
return timeCounter;
});

// Simulate file change
mockWatcher.emit('change', '/path/to/file.js');
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);
});
constructor() {
this.writeHead = vi.fn();
this.end = vi.fn();
}
}

it('should inject client scripts in HTML in development mode', () => {
const html = '<html><head></head><body><script type="module" src="/@vite/client"></script></body></html>';
const result = plugin.transformIndexHtml?.(html, { command: 'serve' });

// Should contain timing function
expect(result).toContain('window.__VITE_TIMING__');

// Should contain our virtual HMR module import
expect(result).toContain('/@vite-timing/hmr');

// Scripts should be in correct order
const timingIndex = result.indexOf('window.__VITE_TIMING__');
const hmrModuleIndex = result.indexOf('/@vite-timing/hmr');
expect(timingIndex).toBeLessThan(hmrModuleIndex);

// Should preserve Vite's client script
expect(result).toContain('/@vite/client');
});
describe('viteTimingPlugin', () => {
let plugin: ReturnType<typeof viteTimingPlugin>;
let mockServer: Partial<ViteDevServer>;
let mockWatcher: EventEmitter;

it('should not inject scripts in production mode', () => {
const html = '<html><head></head><body><script type="module" src="/@vite/client"></script></body></html>';
const result = plugin.transformIndexHtml?.(html, { command: 'build' });

expect(result).toBe(html); // Should return unmodified HTML
expect(result).not.toContain('window.__VITE_TIMING__');
expect(result).not.toContain('/@vite-timing/hmr');
});

it('should handle HMR updates with module count', () => {
plugin.configureServer?.(mockServer as ViteDevServer);

// Simulate file change first to create an entry in the change map
mockWatcher.emit('change', '/path/to/file.js');

// Create HMR context
const context = {
file: '/path/to/file.js',
modules: [{ id: 1 }, { id: 2 }],
read: async () => '',
timestamp: Date.now()
};

// Call handleHotUpdate
const result = plugin.handleHotUpdate?.(context);
expect(result).toBeUndefined();

// 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].moduleCount).toBe(2);
});

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

// 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()
}]
});
beforeEach(() => {
// Reset performance.now mock before each test
vi.spyOn(performance, 'now').mockImplementation(() => 1000);

// Create mock server and watcher
mockWatcher = new EventEmitter();
mockServer = {
watcher: mockWatcher,
ws: {
on: (event: string, callback: (socket: any) => void) => {
callback({
on: vi.fn()
});
}
},
middlewares: {
use: vi.fn()
}
};

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

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

// 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();
it('should register file watcher on server configure', () => {
const watcherSpy = vi.spyOn(mockWatcher, 'on');
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);

// Mock performance.now to return increasing values
let timeCounter = 1000;
const nowSpy = vi.spyOn(performance, 'now').mockImplementation(() => {
timeCounter += 100;
return timeCounter;
});

// Simulate file change
mockWatcher.emit('change', '/path/to/file.js');
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);
});

// Extend MockResponse to resolve promise when end is called
res.end = vi.fn(() => {
resolve();
});
it('should inject HMR module in development mode', () => {
const html = '<html><head></head><body></body></html>';
const result = plugin.transformIndexHtml?.(html, { command: 'serve' });

// Should contain our virtual HMR module
expect(result).toContain('/@vite-timing/hmr');
});

// Get and call the middleware handler
const middlewareHandler = (createMockServer as any).lastHandler;
middlewareHandler(req, res, next);
it('should not inject scripts in production mode', () => {
const html = '<html><head></head><body></body></html>';
const result = plugin.transformIndexHtml?.(html, { command: 'build' });

expect(result).toBe(html); // Should return unmodified HTML
expect(result).not.toContain('/@vite-timing/hmr');
});

// Send request data
req.emit('data', JSON.stringify({
file: initialEntry.file,
clientTimestamp: 2000
}));
req.emit('end');
});
it('should provide virtual HMR module', () => {
const id = '/@vite-timing/hmr';
const resolved = plugin.resolveId?.(id);
expect(resolved).toBe(id);

// 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(finalEntries).toHaveLength(0);
const content = plugin.load?.(id);
expect(content).toContain('createHotContext');
expect(content).toContain('vite:afterUpdate');
});
it('should handle middleware requests for timing data', async () => {
plugin.configureServer?.(mockServer as ViteDevServer);

const testFile = 'src/test.js'; // Use forward slashes consistently

// Create a file change entry
mockWatcher.emit('change', testFile);

// Mock request/response
const req = new MockRequest('/__vite_timing_hmr_complete');
const res = new MockResponse();

// Get middleware handler
const middlewareHandler = (mockServer.middlewares?.use as jest.Mock).mock.calls[0][0];

// Call middleware with request
middlewareHandler(req, res, vi.fn());

// Send timing data with same path format
req.emit('data', JSON.stringify({
file: testFile,
clientTimestamp: 2000
}));
req.emit('end');

// Wait for async processing
await new Promise(resolve => setTimeout(resolve, 10));

// Verify response
expect(res.writeHead).toHaveBeenCalledWith(200, {
'Content-Type': 'application/json'
});

const responseData = JSON.parse(res.end.mock.calls[0][0]);
expect(responseData.success).toBe(true);

// Verify entry was cleaned up
const changeMap = plugin._TEST_getChangeMap?.();
expect(Array.from(changeMap!.values())).toHaveLength(0);
});
});
Loading

0 comments on commit 00f66a8

Please sign in to comment.