From fba848ef1637f3ccdcf16d5e493528a5bc05f27f Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Mon, 14 Sep 2026 20:40:55 +0530 Subject: [PATCH] feat(ascii-art): figlet adapter with lazy load, font cache, structured error --- src/main/AsciiArt.figlet-adapter.js | 71 ++++++++++++++++++++ tests/main/ascii-art.figlet-adapter.test.js | 73 +++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 src/main/AsciiArt.figlet-adapter.js create mode 100644 tests/main/ascii-art.figlet-adapter.test.js diff --git a/src/main/AsciiArt.figlet-adapter.js b/src/main/AsciiArt.figlet-adapter.js new file mode 100644 index 0000000..6a86164 --- /dev/null +++ b/src/main/AsciiArt.figlet-adapter.js @@ -0,0 +1,71 @@ +/** + * Figlet adapter — lazy-loads the figlet package and caches the font list. + * + * The figlet npm package ships ~400 fonts. Lazy-loading on first + * `generateFiglet` / `listFigletFonts` call keeps app startup fast. If + * `require('figlet')` itself fails (corrupt install), the adapter reports + * the system as unavailable instead of crashing. + * + * @module AsciiArt.figlet-adapter + */ +'use strict'; + +class AsciiArtFigletError extends Error { + constructor(message, cause) { + super(message); + this.name = 'AsciiArtFigletError'; + this.cause = cause; + } +} + +let _figlet = undefined; // undefined = not yet attempted; null = failed +let _figletFonts = undefined; // undefined = not yet listed + +function loadFiglet() { + if (_figlet !== undefined) return _figlet; + try { + _figlet = require('figlet'); + } catch { + _figlet = null; + } + return _figlet; +} + +function listFigletFonts() { + const f = loadFiglet(); + if (!f) return []; + if (_figletFonts === undefined) { + try { + _figletFonts = f.fontsSync(); + } catch { + _figletFonts = []; + } + } + return _figletFonts.slice(); +} + +function generateFiglet(text, font) { + const f = loadFiglet(); + if (!f) { + throw new AsciiArtFigletError('figlet is not available'); + } + try { + return f.textSync(String(text ?? ''), { font }); + } catch (err) { + throw new AsciiArtFigletError(`figlet render failed for font "${font}": ${err.message}`, err); + } +} + +// Test-only cache reset; not used at runtime. +function _resetCache() { + _figlet = undefined; + _figletFonts = undefined; +} + +module.exports = { + AsciiArtFigletError, + loadFiglet, + listFigletFonts, + generateFiglet, + _resetCache, +}; diff --git a/tests/main/ascii-art.figlet-adapter.test.js b/tests/main/ascii-art.figlet-adapter.test.js new file mode 100644 index 0000000..b8dc7d3 --- /dev/null +++ b/tests/main/ascii-art.figlet-adapter.test.js @@ -0,0 +1,73 @@ +/** + * @jest-environment node + * + * Adapter tests — figlet is mocked so we don't ship 400 KB of fonts into + * the test runner. + */ +jest.mock('figlet', () => { + const actual = jest.requireActual('figlet'); + return { + __esModule: true, + default: actual, + textSync: jest.fn(() => 'MOCKED_FIGLET_OUTPUT'), + fontsSync: jest.fn(() => ['Standard', 'Big', 'Slant']), + }; +}); + +const FigletAdapter = require('../../src/main/AsciiArt.figlet-adapter'); + +describe('AsciiArt.figlet-adapter', () => { + beforeEach(() => { + jest.clearAllMocks(); + FigletAdapter._resetCache(); // test-only reset + }); + + test('loadFiglet returns the figlet module on first call', () => { + const mod = FigletAdapter.loadFiglet(); + expect(mod).not.toBeNull(); + expect(typeof mod.textSync).toBe('function'); + }); + + test('listFigletFonts returns string[] from figlet.fontsSync()', () => { + const fonts = FigletAdapter.listFigletFonts(); + expect(fonts).toEqual(['Standard', 'Big', 'Slant']); + }); + + test('listFigletFonts caches between calls', () => { + FigletAdapter.listFigletFonts(); + FigletAdapter.listFigletFonts(); + const figlet = require('figlet'); + expect(figlet.fontsSync).toHaveBeenCalledTimes(1); + }); + + test('generateFiglet returns rendered string', () => { + const out = FigletAdapter.generateFiglet('HELLO', 'Big'); + expect(out).toBe('MOCKED_FIGLET_OUTPUT'); + const figlet = require('figlet'); + expect(figlet.textSync).toHaveBeenCalledWith('HELLO', { font: 'Big' }); + }); + + test('generateFiglet throws AsciiArtFigletError when figlet throws', () => { + const figlet = require('figlet'); + figlet.textSync.mockImplementationOnce(() => { + throw new Error('unknown font'); + }); + expect(() => FigletAdapter.generateFiglet('X', 'Nope')).toThrow( + FigletAdapter.AsciiArtFigletError + ); + }); + + test('returns null when figlet module fails to load', () => { + // simulate require failure by reloading after breaking the module cache + jest.isolateModules(() => { + jest.doMock('figlet', () => { + throw new Error('not found'); + }); + const Fresh = require('../../src/main/AsciiArt.figlet-adapter'); + Fresh._resetCache(); + expect(Fresh.loadFiglet()).toBeNull(); + expect(Fresh.listFigletFonts()).toEqual([]); + expect(() => Fresh.generateFiglet('X', 'Big')).toThrow(Fresh.AsciiArtFigletError); + }); + }); +});