resumefy
Version:
A simple toolkit to bring your JSON Resume to life
162 lines (161 loc) • 8.26 kB
JavaScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Renderer } from '../render/Renderer.js';
import { log } from './log.js';
import { render } from './render.js';
import fs from 'fs';
vi.mock('ansicolor', () => ({
underline: vi.fn((text) => text),
}));
vi.mock('./log', () => ({
log: {
error: vi.fn(),
dim: vi.fn(),
},
}));
vi.mock('fs', () => ({
watch: vi.fn(),
default: {
watch: vi.fn(),
},
}));
describe('render', () => {
const resumeFile = 'test-resume.json';
const options = {
watch: false,
headless: true,
theme: 'jsonresume-theme-even',
outDir: './output',
port: 3333,
};
const puppeteerOptions = {
timeout: 0,
defaultViewport: null,
headless: options.headless,
pipe: true,
args: ['--no-sandbox'],
};
const launchSpy = vi.spyOn(Renderer, 'launch');
const renderSpy = vi.spyOn(Renderer.prototype, 'render');
const addMenuSpy = vi.spyOn(Renderer.prototype, 'addMenu');
const startFileServerSpy = vi.spyOn(Renderer.prototype, 'startFileServer');
const reloadPreviewSpy = vi.spyOn(Renderer.prototype, 'reloadPreview');
const exitSpy = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
let mockFileChange = async (_) => { };
const fsWatchMock = (_, callback = () => { }) => {
mockFileChange = async (file) => {
await callback('change', file);
// Wait for render to complete
await new Promise(process.nextTick);
};
return {};
};
beforeEach(() => {
vi.mocked(fs.watch).mockImplementation(fsWatchMock);
launchSpy.mockImplementation(() => Promise.resolve(new Renderer(resumeFile, options, {})));
renderSpy.mockImplementation(() => Promise.resolve());
addMenuSpy.mockImplementation(() => Promise.resolve());
startFileServerSpy.mockImplementation(() => { });
reloadPreviewSpy.mockImplementation(() => Promise.resolve());
exitSpy.mockImplementation(() => {
throw new Error('process.exit called');
});
});
afterEach(() => {
vi.clearAllMocks();
});
it('should launch Renderer, call render and addMenu', async () => {
await render(resumeFile, options);
expect(Renderer.launch).toHaveBeenCalledWith(resumeFile, { theme: options.theme, outDir: options.outDir }, puppeteerOptions, { watch: options.watch, headless: options.headless });
expect(Renderer.prototype.render).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.addMenu).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.addMenu).toHaveBeenCalledWith(`http://localhost:${options.port}`);
expect(Renderer.prototype.startFileServer).not.toHaveBeenCalled();
});
it('should log error and exit process if render fails and not in watch mode', async () => {
const error = new Error('Render error');
renderSpy.mockRejectedValueOnce(error);
await expect(() => render(resumeFile, options)).rejects.toThrow('process.exit called');
expect(log.error).toHaveBeenCalledTimes(1);
expect(log.error).toHaveBeenCalledWith(error);
expect(exitSpy).toHaveBeenCalledTimes(1);
expect(exitSpy).toHaveBeenCalledWith(1);
expect(Renderer.prototype.render).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.addMenu).not.toHaveBeenCalled();
expect(Renderer.prototype.startFileServer).not.toHaveBeenCalled();
});
describe('default options', () => {
it('should use default outDir `.` if not passed', async () => {
await render(resumeFile, { theme: options.theme });
expect(Renderer.launch).toHaveBeenCalledWith(resumeFile, { theme: options.theme, outDir: '.' }, puppeteerOptions, { watch: false, headless: true });
});
it('should use default port 8080 if not passed', async () => {
await render(resumeFile, { theme: options.theme });
expect(addMenuSpy).toHaveBeenCalledWith('http://localhost:8080');
});
it('should use inverted watch flag for headless by default', async () => {
await render(resumeFile, { theme: options.theme, watch: true });
expect(Renderer.launch).toHaveBeenCalledWith(resumeFile, { theme: options.theme, outDir: '.' }, { ...puppeteerOptions, headless: false }, { watch: true, headless: false });
});
});
describe('watch mode', () => {
const watchOptions = { ...options, watch: true, headless: false };
it('should start file server and watch for changes', async () => {
await render(resumeFile, watchOptions);
expect(log.dim).toHaveBeenCalledWith('\nWatching test-resume.json for changes...');
expect(Renderer.prototype.startFileServer).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.startFileServer).toHaveBeenCalledWith(options.port);
expect(fs.watch).toHaveBeenCalledWith(resumeFile, expect.any(Function));
});
it('should NOT start file server if is running in headless mode', async () => {
await render(resumeFile, { ...watchOptions, headless: true });
expect(log.dim).toHaveBeenCalledWith('\nWatching test-resume.json for changes...');
expect(Renderer.prototype.startFileServer).not.toHaveBeenCalled();
expect(fs.watch).toHaveBeenCalledWith(resumeFile, expect.any(Function));
});
it('should not exit process if render fails', async () => {
const error = new Error('Render error');
renderSpy.mockRejectedValueOnce(error);
await render(resumeFile, watchOptions);
expect(log.error).toHaveBeenCalledTimes(1);
expect(log.error).toHaveBeenCalledWith(error);
expect(exitSpy).not.toHaveBeenCalled();
expect(Renderer.prototype.render).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.addMenu).not.toHaveBeenCalled();
expect(Renderer.prototype.startFileServer).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.startFileServer).toHaveBeenCalledWith(options.port);
});
it('should reload preview and add menu if resume file changes', async () => {
const date = new Date(2000, 1, 1, 13);
vi.setSystemTime(date);
await render(resumeFile, watchOptions);
expect(Renderer.prototype.addMenu).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.reloadPreview).not.toHaveBeenCalled();
expect(log.dim).toHaveBeenCalledTimes(1);
await mockFileChange(resumeFile);
expect(log.dim).toHaveBeenCalledTimes(2);
expect(log.dim).toHaveBeenCalledWith(`\n[${date.toISOString()}]`, resumeFile, 'changed', '\n');
expect(Renderer.prototype.reloadPreview).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.addMenu).toHaveBeenCalledTimes(2);
});
it('should do nothing if watch callback receives filename = null', async () => {
await render(resumeFile, watchOptions);
expect(Renderer.prototype.addMenu).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.reloadPreview).not.toHaveBeenCalled();
await mockFileChange(null);
expect(log.dim).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.addMenu).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.reloadPreview).not.toHaveBeenCalled();
});
it('should not throw if reloading the preview on file change fails', async () => {
await render(resumeFile, watchOptions);
expect(Renderer.prototype.addMenu).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.reloadPreview).not.toHaveBeenCalled();
reloadPreviewSpy.mockRejectedValueOnce(new Error('Reload preview error'));
await mockFileChange(resumeFile);
expect(Renderer.prototype.reloadPreview).toHaveBeenCalledTimes(1);
expect(Renderer.prototype.addMenu).toHaveBeenCalledTimes(2);
});
});
});