const test = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { buildFontFamilyFormats, buildFontStylesheet, collectFontLibrarySyncOperations } = require('../src/web/lib/media/font-library'); test('buildFontFamilyFormats quotes multi-word font families for TinyMCE previews', () => { const formats = buildFontFamilyFormats([ { family: 'Old London', enabled: true }, { family: 'My Custom Font', enabled: true } ]); assert.match(formats, /Old London='Old London'/); assert.match(formats, /My Custom Font='My Custom Font'/); assert.match(formats, /Times New Roman='Times New Roman',Times,serif/); assert.match(formats, /Lucida Sans Unicode='Lucida Sans Unicode','Lucida Grande',sans-serif/); }); test('buildFontStylesheet keeps disabled fonts available for rendering', () => { const stylesheet = buildFontStylesheet([ { family: 'Enabled Font', fileName: 'enabled-font.ttf', format: 'truetype', enabled: true }, { family: 'Disabled Font', fileName: 'disabled-font.ttf', format: 'truetype', enabled: false } ]); assert.match(stylesheet, /Enabled Font/); assert.match(stylesheet, /Disabled Font/); assert.match(stylesheet, /\/media\/assets\/fonts\/enabled-font.ttf/); }); test('legacy fonts and weather icons migrate into shared assets without losing metadata', async () => { const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-assets-')); try { const fonts = path.join(root, 'fonts'); const weather = path.join(root, 'weather-icons', 'abc'); await fs.promises.mkdir(fonts, { recursive: true }); await fs.promises.mkdir(weather, { recursive: true }); await fs.promises.writeFile(path.join(fonts, 'custom.ttf'), 'font-data'); await fs.promises.writeFile(path.join(fonts, 'fonts.json'), JSON.stringify([{ id: 'custom', family: 'Custom', fileName: 'custom.ttf', enabled: false }])); await fs.promises.writeFile(path.join(fonts, 'fonts.css'), "src:url('/media/fonts/custom.ttf')"); await fs.promises.writeFile(path.join(weather, 'clear-123abc.png'), 'icon-data'); await fs.promises.writeFile(path.join(root, 'weather-icons', 'sets.json'), JSON.stringify([{ id: 'abc', name: 'Custom', icons: { clear: '/media/weather-icons/abc/clear-123abc.png' } }])); const library = require('../src/web/lib/media/font-library'); await library.ensureFontLibrary(path.join(root, 'uploads')); const loaded = library.loadFontLibrary(root); assert.equal(loaded.fonts[0].id, 'custom'); assert.equal(loaded.fonts[0].enabled, false); assert.equal(loaded.manifestPath, path.join(root, 'assets', 'fonts.json')); assert.equal(loaded.stylesheetPath, path.join(root, 'assets', 'fonts.css')); assert.match(loaded.stylesheetHref, /^\/media\/assets\/fonts.css/); assert.equal(fs.readFileSync(path.join(root, 'assets', 'fonts', 'custom.ttf'), 'utf8'), 'font-data'); assert.match(fs.readFileSync(loaded.stylesheetPath, 'utf8'), /\/media\/assets\/fonts\/custom.ttf/); const weatherLibrary = require('../src/web/lib/media/weather-icon-library'); assert.equal(weatherLibrary.readWeatherIconSets(root)[0].icons.clear, '/media/assets/weather-icons/abc/clear-123abc.png'); assert.equal(weatherLibrary.getWeatherManifestPath(root), path.join(root, 'assets', 'weather.json')); assert.equal(fs.existsSync(path.join(root, 'assets', 'weather-icons', 'sets.json')), false); assert.equal(fs.readFileSync(path.join(root, 'assets', 'weather-icons', 'abc', 'clear-123abc.png'), 'utf8'), 'icon-data'); assert.equal(fs.existsSync(fonts), false); assert.equal(fs.existsSync(path.join(root, 'weather-icons')), false); await library.ensureFontLibrary(root); assert.equal(library.loadFontLibrary(root).fonts.length, 1); } finally { await fs.promises.rm(root, { recursive: true, force: true }); } }); test('shared assets migration tolerates simultaneous service startup', async () => { const root = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-assets-race-')); try { const fonts = path.join(root, 'fonts'); await fs.promises.mkdir(fonts); await fs.promises.writeFile(path.join(fonts, 'fonts.json'), '[]'); await fs.promises.writeFile(path.join(fonts, 'fonts.css'), "src:url('/media/fonts/custom.ttf')"); await Promise.all(Array.from({ length: 100 }, function (_unused, index) { return fs.promises.writeFile(path.join(fonts, 'font-' + index + '.ttf'), 'font-data'); })); const run = require('node:util').promisify(require('node:child_process').execFile); const modulePath = require.resolve('../src/web/lib/media/assets-storage'); await Promise.all(Array.from({ length: 4 }, function () { return run(process.execPath, ['-e', 'require(' + JSON.stringify(modulePath) + ').migrateLegacyAssets(' + JSON.stringify(root) + ')']); })); assert.equal(fs.existsSync(fonts), false); assert.equal(fs.readdirSync(path.join(root, 'assets', 'fonts')).length, 100); assert.equal(JSON.parse(fs.readFileSync(path.join(root, 'assets', 'fonts.json'))).length, 0); } finally { await fs.promises.rm(root, { recursive: true, force: true }); } }); test('collectFontLibrarySyncOperations keeps disabled fonts as puts', async () => { const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'pulse-font-library-')); const fontDir = path.join(tempRoot, 'fonts'); await fs.promises.mkdir(fontDir, { recursive: true }); await fs.promises.writeFile(path.join(fontDir, 'fonts.json'), JSON.stringify([ { family: 'Enabled Font', fileName: 'enabled-font.ttf', enabled: true }, { family: 'Disabled Font', fileName: 'disabled-font.ttf', enabled: false } ], null, 2)); const operations = collectFontLibrarySyncOperations(tempRoot); const fontOps = operations.filter(function (operation) { return String(operation.uploadPath || '').indexOf('/media/assets/fonts/') === 0 && /\.ttf$/.test(String(operation.uploadPath || '')); }); assert.deepEqual(fontOps.map(function (operation) { return operation.type; }), ['put', 'put']); });