const http = require('http'); const fs = require('fs'); const serveThemeCssCommand = require('../serve-theme-css'); jest.mock('fs'); jest.mock('http'); jest.mock('ora', () => jest.fn(() => ({ start: jest.fn().mockReturnThis(), succeed: jest.fn((message) => { // Make ora.succeed call console.log so we can capture it // eslint-disable-next-line no-console console.log(message); return this; }), fail: jest.fn().mockReturnThis(), }))); describe('serveThemeCssCommand', () => { let mockServer; let mockListen; let mockClose; beforeEach(() => { jest.clearAllMocks(); mockListen = jest.fn((port, host, callback) => { if (callback) { callback(); } }); mockClose = jest.fn((callback) => { if (callback) { callback(); } }); mockServer = { listen: mockListen, on: jest.fn(), close: mockClose, }; http.createServer.mockReturnValue(mockServer); fs.existsSync.mockReturnValue(true); fs.statSync.mockReturnValue({ isDirectory: () => false, size: 1024, mtime: { getTime: () => 1234567890 }, }); fs.readFileSync.mockReturnValue(JSON.stringify({ themeUrls: { core: { paths: { default: './core.css', minified: './core.min.css', }, }, defaults: { light: 'light', }, variants: { light: { paths: { default: './light.css', minified: './light.min.css', }, }, }, }, })); }); afterEach(() => { jest.restoreAllMocks(); }); it('should start server with default arguments', async () => { const args = []; const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {}); await serveThemeCssCommand(args); expect(http.createServer).toHaveBeenCalled(); expect(mockListen).toHaveBeenCalledWith(3000, 'localhost', expect.any(Function)); // Wait for the setTimeout to complete and output to be generated await new Promise((resolve) => { setTimeout(resolve, 1100); }); // Verify that the default docs URL is included in the output expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('https://paragon-openedx.netlify.app/'), ); }); it('should exit if build directory does not exist', async () => { const args = ['--build-dir=./nonexistent']; fs.existsSync.mockReturnValue(false); const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); await serveThemeCssCommand(args); expect(mockConsoleError).toHaveBeenCalledWith( expect.stringContaining('Error: Build directory'), ); expect(mockExit).toHaveBeenCalledWith(1); }); it('should exit if theme-urls.json does not exist', async () => { const args = ['--build-dir=./dist']; // Mock that dist exists but theme-urls.json doesn't fs.existsSync.mockImplementation((path) => !path.endsWith('theme-urls.json')); const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); await serveThemeCssCommand(args); // The actual implementation calls console.error twice with separate messages expect(mockConsoleError).toHaveBeenNthCalledWith( 1, expect.stringContaining('Error:'), ); expect(mockConsoleError).toHaveBeenNthCalledWith( 1, expect.stringContaining('does not appear to be a valid Paragon dist directory'), ); expect(mockConsoleError).toHaveBeenNthCalledWith( 2, expect.stringContaining('Missing theme-urls.json file'), ); expect(mockExit).toHaveBeenCalledWith(1); }); it('should exit if theme-urls.json is invalid JSON', async () => { const args = ['--build-dir=./dist']; fs.readFileSync.mockReturnValue('invalid json'); const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); await serveThemeCssCommand(args); expect(mockConsoleError).toHaveBeenCalledWith( expect.stringContaining('Error: Could not read theme-urls.json file.'), ); expect(mockExit).toHaveBeenCalledWith(1); }); it('should handle server errors', async () => { const args = ['--port=3000']; const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); const mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {}); await serveThemeCssCommand(args); // Simulate server error const errorCallback = mockServer.on.mock.calls.find(call => call[0] === 'error')[1]; errorCallback({ code: 'EADDRINUSE' }); expect(mockConsoleError).toHaveBeenCalledWith( expect.stringContaining('Error: Port 3000 is already in use.'), ); expect(mockExit).toHaveBeenCalledWith(1); }); it('should handle graceful shutdown', async () => { const args = []; const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {}); await serveThemeCssCommand(args); // Simulate SIGINT const sigintCallback = process.listeners('SIGINT').pop(); sigintCallback(); expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Shutting down server...'), ); expect(mockClose).toHaveBeenCalled(); expect(mockExit).toHaveBeenCalledWith(0); }); it('should parse command line arguments correctly', async () => { const args = [ '-b', './custom-dist', '-p', '8080', '-h', '0.0.0.0', '--cors=false', ]; await serveThemeCssCommand(args); expect(mockListen).toHaveBeenCalledWith(8080, '0.0.0.0', expect.any(Function)); }); it('should include custom docs URL in output when specified', async () => { const args = [ '--docs-url=https://custom-docs.example.com', '--theme-name=Custom Theme', ]; const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {}); await serveThemeCssCommand(args); expect(http.createServer).toHaveBeenCalled(); expect(mockListen).toHaveBeenCalledWith(3000, 'localhost', expect.any(Function)); // Wait for the setTimeout to complete and output to be generated await new Promise((resolve) => { setTimeout(resolve, 1100); }); // Verify that the custom docs URL is included in the output expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('https://custom-docs.example.com'), ); }); it('should display server URL and available theme files', async () => { const args = []; const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {}); await serveThemeCssCommand(args); expect(http.createServer).toHaveBeenCalled(); expect(mockListen).toHaveBeenCalledWith(3000, 'localhost', expect.any(Function)); // Wait for the setTimeout to complete and output to be generated await new Promise((resolve) => { setTimeout(resolve, 1100); }); // Verify server URL is displayed expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Theme CSS server running at http://localhost:3000'), ); // Verify available theme files section is displayed expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Available theme files:'), ); // Verify core CSS files are listed expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Core CSS:'), ); expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('http://localhost:3000/core.css'), ); // Verify theme variants are listed expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Theme Variants:'), ); expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('light:'), ); // Verify theme configuration is listed expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Theme Configuration:'), ); expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('http://localhost:3000/theme-urls.json'), ); }); it('should display shutdown instructions', async () => { const args = []; const mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {}); await serveThemeCssCommand(args); expect(http.createServer).toHaveBeenCalled(); expect(mockListen).toHaveBeenCalledWith(3000, 'localhost', expect.any(Function)); // Wait for the setTimeout to complete and output to be generated await new Promise((resolve) => { setTimeout(resolve, 1100); }); // Verify shutdown instructions are displayed expect(mockConsoleLog).toHaveBeenCalledWith( expect.stringContaining('Press Ctrl+C to stop the server'), ); }); });