mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 17:29:29 +05:30
fix(ascii-art): wire Box/Templates mode + correct standalone <link href>; v4.9.2
- src/ascii-generator.html line 7: ../fonts.css -> fonts.css (src/-relative; fixes silent stylesheet miss — same class as the v4.9.0->v4.9.1 script-tag fix) - src/renderer/ascii-controller.js: wire all 3 mode tabs, 18 template buttons, and the 3 box form fields. setMode() toggles .active on tabs and matching .mode-section. Templates route through the orchestrator's template:<id> font namespace. Box mode renders text with single/double/ rounded/bold/ascii borders via a pure renderBox() helper. All 11 T9 behaviours preserved. - tests/ascii-controller.test.js: 6 tests covering stylesheet path, pure box renderer, mode-tab switching, and template button wiring. - package.json 4.9.1 -> 4.9.2; README + UPDATES updated. Amit Haridas
This commit is contained in:
@@ -257,4 +257,4 @@ Amit Haridas (amit.wh@gmail.com)
|
|||||||
|
|
||||||
## Version
|
## Version
|
||||||
|
|
||||||
v4.9.1
|
v4.9.2
|
||||||
|
|||||||
+13
@@ -1,5 +1,18 @@
|
|||||||
# PanConverter - Updates & Changelog
|
# PanConverter - Updates & Changelog
|
||||||
|
|
||||||
|
## Version 4.9.2 (2026-09-14)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- **Standalone ASCII Art Generator — broken stylesheet path**: `src/ascii-generator.html:7` linked `<link rel="stylesheet" href="../fonts.css" />`. The HTML loads via `BrowserWindow.loadFile(path.join(__dirname, 'ascii-generator.html'))` where `__dirname` is `src/`, so `../fonts.css` escaped the `src/` directory and resolved to a non-existent `<project>/fonts.css`. Changed to `fonts.css` (same file, src/-relative — mirrors `src/index.html:29`). The window rendered without its font rules, leaving the header in the fallback system stack.
|
||||||
|
- **Standalone ASCII Art Generator — dead Box/Templates UI**: the controller (`src/renderer/ascii-controller.js`) shipped three mode tabs (`Text Banner` / `Box-Frame` / `Templates`), 18 `.template-btn[data-template]` buttons, and a Box form (`#box-text` / `#box-style` / `#box-padding`) but wired none of them. Clicking any tab or button was a no-op. Wired all of them:
|
||||||
|
- `setMode(mode)` toggles `.active` on the right `.mode-tab` and the matching `.mode-section` (`text-mode` / `box-mode` / `templates-mode`).
|
||||||
|
- Template buttons call `api.generate({ text: '', font: 'template:<id>' })` so the orchestrator owns template content; preview updates and the button gets `.active`.
|
||||||
|
- Box mode renders the user text with a border using the chosen style (`single` / `double` / `rounded` / `bold` / `ascii`) and padding, exposed as a pure `window.ASCIIBoxRenderer.renderBox(text, style, padding)` helper.
|
||||||
|
- All 11 brief-required behaviours (text-input / font-picker / font-search / insert / copy / save / generate, last-font persistence, debounced preview) remain intact.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- New `tests/ascii-controller.test.js` — 6 tests covering the stylesheet path (no `..` escape), pure box renderer (single + ascii styles), mode-tab switching (Box and Templates), and template button → preview wiring.
|
||||||
|
|
||||||
## Version 4.9.1 (2026-09-14)
|
## Version 4.9.1 (2026-09-14)
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "markdown-converter",
|
"name": "markdown-converter",
|
||||||
"version": "4.9.1",
|
"version": "4.9.2",
|
||||||
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
|
"description": "Professional Markdown editor and universal file converter with PDF editing, batch processing, and syntax highlighting",
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>ASCII Art Generator - MarkdownConverter</title>
|
<title>ASCII Art Generator - MarkdownConverter</title>
|
||||||
<link rel="stylesheet" href="../fonts.css" />
|
<link rel="stylesheet" href="fonts.css" />
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--ci-dark-gray: #464646;
|
--ci-dark-gray: #464646;
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
* Insert / Copy / Save actions. Last-used font is restored from
|
* Insert / Copy / Save actions. Last-used font is restored from
|
||||||
* electron-store (via main) and saved on every change.
|
* electron-store (via main) and saved on every change.
|
||||||
*
|
*
|
||||||
|
* Wires three mode tabs (Text Banner / Box-Frame / Templates) plus the
|
||||||
|
* 18 template buttons and the Box form fields (#box-text, #box-style,
|
||||||
|
* #box-padding). Templates route through `api.generate({ font: 'template:<id>' })`
|
||||||
|
* so the orchestrator owns template content.
|
||||||
|
*
|
||||||
* No bundler, no framework — pure DOM + window.electronAPI bridge.
|
* No bundler, no framework — pure DOM + window.electronAPI bridge.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
@@ -23,12 +28,26 @@
|
|||||||
btnSave: document.getElementById('btn-save'),
|
btnSave: document.getElementById('btn-save'),
|
||||||
btnGenerate: document.getElementById('btn-generate'),
|
btnGenerate: document.getElementById('btn-generate'),
|
||||||
warning: document.getElementById('ascii-warning'),
|
warning: document.getElementById('ascii-warning'),
|
||||||
|
modeTabs: document.querySelectorAll('.mode-tab[data-mode]'),
|
||||||
|
modeSections: document.querySelectorAll('.mode-section'),
|
||||||
|
templateButtons: document.querySelectorAll('.template-btn[data-template]'),
|
||||||
|
boxText: document.getElementById('box-text'),
|
||||||
|
boxStyle: document.getElementById('box-style'),
|
||||||
|
boxPadding: document.getElementById('box-padding'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Section ID for each mode. Keep in sync with ascii-generator.html IDs.
|
||||||
|
const MODE_SECTION_ID = {
|
||||||
|
text: 'text-mode',
|
||||||
|
box: 'box-mode',
|
||||||
|
templates: 'templates-mode',
|
||||||
};
|
};
|
||||||
|
|
||||||
let _fonts = [];
|
let _fonts = [];
|
||||||
let _currentFont = 'standard';
|
let _currentFont = 'standard';
|
||||||
let _previewText = '';
|
let _previewText = '';
|
||||||
let _searchDebounce = null;
|
let _searchDebounce = null;
|
||||||
|
let _currentMode = 'text';
|
||||||
|
|
||||||
function showWarning(msg) {
|
function showWarning(msg) {
|
||||||
if (!els.warning) return;
|
if (!els.warning) return;
|
||||||
@@ -45,8 +64,124 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch the active mode tab and the visible mode section.
|
||||||
|
* Toggles the `.active` class on tabs and on the matching `.mode-section`.
|
||||||
|
* @param {'text'|'box'|'templates'} mode
|
||||||
|
*/
|
||||||
|
function setMode(mode) {
|
||||||
|
if (!MODE_SECTION_ID[mode]) return;
|
||||||
|
_currentMode = mode;
|
||||||
|
|
||||||
|
// Toggle tab buttons.
|
||||||
|
els.modeTabs.forEach((tab) => {
|
||||||
|
if (tab.dataset.mode === mode) tab.classList.add('active');
|
||||||
|
else tab.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle mode sections.
|
||||||
|
els.modeSections.forEach((sec) => {
|
||||||
|
if (sec.id === MODE_SECTION_ID[mode]) sec.classList.add('active');
|
||||||
|
else sec.classList.remove('active');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Each mode has its own preview pipeline; trigger a refresh.
|
||||||
|
refreshPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a simple box border around the user's text using the chosen style.
|
||||||
|
* Pure function — exported on window for tests via ASCIIBoxRenderer.
|
||||||
|
* @param {string} text
|
||||||
|
* @param {'single'|'double'|'rounded'|'bold'|'ascii'} style
|
||||||
|
* @param {number} padding 0..10 spaces on each side
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function renderBox(text, style, padding) {
|
||||||
|
const lines = String(text ?? '').split('\n');
|
||||||
|
const pad = Math.max(0, Math.min(10, Number(padding) || 0));
|
||||||
|
const padded = lines.map((l) => ' '.repeat(pad) + l + ' '.repeat(pad));
|
||||||
|
const width = padded.reduce((w, l) => Math.max(w, visibleLength(l)), 0);
|
||||||
|
|
||||||
|
let tl, tr, bl, br, h, v;
|
||||||
|
switch (style) {
|
||||||
|
case 'double':
|
||||||
|
tl = '╔';
|
||||||
|
tr = '╗';
|
||||||
|
bl = '╚';
|
||||||
|
br = '╝';
|
||||||
|
h = '═';
|
||||||
|
v = '║';
|
||||||
|
break;
|
||||||
|
case 'rounded':
|
||||||
|
tl = '╭';
|
||||||
|
tr = '╮';
|
||||||
|
bl = '╰';
|
||||||
|
br = '╯';
|
||||||
|
h = '─';
|
||||||
|
v = '│';
|
||||||
|
break;
|
||||||
|
case 'bold':
|
||||||
|
tl = '┏';
|
||||||
|
tr = '┓';
|
||||||
|
bl = '┗';
|
||||||
|
br = '┛';
|
||||||
|
h = '━';
|
||||||
|
v = '┃';
|
||||||
|
break;
|
||||||
|
case 'ascii':
|
||||||
|
tl = '+';
|
||||||
|
tr = '+';
|
||||||
|
bl = '+';
|
||||||
|
br = '+';
|
||||||
|
h = '-';
|
||||||
|
v = '|';
|
||||||
|
break;
|
||||||
|
case 'single':
|
||||||
|
default:
|
||||||
|
tl = '┌';
|
||||||
|
tr = '┐';
|
||||||
|
bl = '└';
|
||||||
|
br = '┘';
|
||||||
|
h = '─';
|
||||||
|
v = '│';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const top = tl + h.repeat(width + 2) + tr;
|
||||||
|
const bot = bl + h.repeat(width + 2) + br;
|
||||||
|
const body = padded.map((l) => v + ' ' + l.padEnd(width, ' ') + ' ' + v).join('\n');
|
||||||
|
return [top, body, bot].join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count printable character width, ignoring box-drawing characters and
|
||||||
|
* spaces (which all occupy a single monospace column).
|
||||||
|
*/
|
||||||
|
function visibleLength(s) {
|
||||||
|
// Box-drawing chars and most printable ASCII occupy one column in our
|
||||||
|
// monospace font. This is a deliberate simplification — emoji or wide
|
||||||
|
// CJK would need a more sophisticated width table.
|
||||||
|
return Array.from(s).length;
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshPreview() {
|
async function refreshPreview() {
|
||||||
if (!api) return;
|
if (!api) return;
|
||||||
|
if (_currentMode === 'templates') {
|
||||||
|
// Template mode is button-driven; no live preview here.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_currentMode === 'box') {
|
||||||
|
const text = els.boxText ? els.boxText.value : '';
|
||||||
|
const style = els.boxStyle ? els.boxStyle.value : 'single';
|
||||||
|
const padding = els.boxPadding ? els.boxPadding.value : 2;
|
||||||
|
_previewText = text;
|
||||||
|
const out = renderBox(text, style, padding);
|
||||||
|
els.preview.textContent = out;
|
||||||
|
showWarning('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// text mode (default)
|
||||||
const text = els.textInput ? els.textInput.value : 'HELLO';
|
const text = els.textInput ? els.textInput.value : 'HELLO';
|
||||||
_previewText = text;
|
_previewText = text;
|
||||||
try {
|
try {
|
||||||
@@ -93,6 +228,26 @@
|
|||||||
renderFontList('');
|
renderFontList('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a template by id: fetch its content via the orchestrator's
|
||||||
|
* template:<id> font namespace and update the preview.
|
||||||
|
*/
|
||||||
|
async function renderTemplate(templateId) {
|
||||||
|
if (!api) return;
|
||||||
|
try {
|
||||||
|
const out = await api.generate({ text: '', font: `template:${templateId}` });
|
||||||
|
els.preview.textContent = out;
|
||||||
|
showWarning('');
|
||||||
|
// Mark the clicked button as active.
|
||||||
|
els.templateButtons.forEach((btn) => {
|
||||||
|
if (btn.dataset.template === templateId) btn.classList.add('active');
|
||||||
|
else btn.classList.remove('active');
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
showWarning(`Template render failed: ${err.message || err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function wireEvents() {
|
function wireEvents() {
|
||||||
if (els.textInput) {
|
if (els.textInput) {
|
||||||
els.textInput.addEventListener('input', debounce(refreshPreview, 200));
|
els.textInput.addEventListener('input', debounce(refreshPreview, 200));
|
||||||
@@ -151,6 +306,39 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- mode-tab switching (Text Banner / Box-Frame / Templates) ----
|
||||||
|
els.modeTabs.forEach((tab) => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
const mode = tab.dataset.mode;
|
||||||
|
if (mode) setMode(mode);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- Box form: any change re-renders the box preview ----
|
||||||
|
if (els.boxText) {
|
||||||
|
els.boxText.addEventListener('input', () => {
|
||||||
|
if (_currentMode === 'box') refreshPreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (els.boxStyle) {
|
||||||
|
els.boxStyle.addEventListener('change', () => {
|
||||||
|
if (_currentMode === 'box') refreshPreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (els.boxPadding) {
|
||||||
|
els.boxPadding.addEventListener('input', () => {
|
||||||
|
if (_currentMode === 'box') refreshPreview();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Template buttons: render the chosen template into the preview ----
|
||||||
|
els.templateButtons.forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
const id = btn.dataset.template;
|
||||||
|
if (id) renderTemplate(id);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
@@ -159,7 +347,10 @@
|
|||||||
await refreshPreview();
|
await refreshPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose pure helpers for tests; do not pollute window in production beyond
|
||||||
|
// what the bootstrap entry-point needs.
|
||||||
window.ASCIIController = { bootstrap };
|
window.ASCIIController = { bootstrap };
|
||||||
|
window.ASCIIBoxRenderer = { renderBox };
|
||||||
if (document.readyState === 'loading') {
|
if (document.readyState === 'loading') {
|
||||||
document.addEventListener('DOMContentLoaded', bootstrap);
|
document.addEventListener('DOMContentLoaded', bootstrap);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/**
|
||||||
|
* @jest-environment jsdom
|
||||||
|
*
|
||||||
|
* Tests the standalone ASCII Art Generator window's renderer controller
|
||||||
|
* (src/renderer/ascii-controller.js). Loads the real HTML and the real
|
||||||
|
* controller script into jsdom, mocks window.electronAPI.generators.ascii,
|
||||||
|
* and asserts that:
|
||||||
|
* - The fonts.css <link> uses a src/-relative path (no `..` escape).
|
||||||
|
* - Mode-tab clicks switch the visible mode section.
|
||||||
|
* - Template-button clicks update the preview with the orchestrator's
|
||||||
|
* template:<id> output.
|
||||||
|
* - The pure box renderer wraps text with the requested border style.
|
||||||
|
*/
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const HTML_PATH = path.join(__dirname, '..', 'src', 'ascii-generator.html');
|
||||||
|
const CONTROLLER_PATH = path.join(__dirname, '..', 'src', 'renderer', 'ascii-controller.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the ascii-generator DOM in the jsdom document and execute the
|
||||||
|
* controller script in the same realm (window + document globals).
|
||||||
|
*
|
||||||
|
* The HTML file is parsed by the jsdom document and the controller
|
||||||
|
* source is run via `vm.runInThisContext` so its IIFE captures the real
|
||||||
|
* `window` and `document` without string interpolation.
|
||||||
|
*/
|
||||||
|
function mount() {
|
||||||
|
// Parse the HTML body content into the jsdom document. We use the
|
||||||
|
// existing testRealm — the controller only inspects IDs that exist in
|
||||||
|
// the standalone window's HTML, so a minimal copy is fine.
|
||||||
|
const html = fs.readFileSync(HTML_PATH, 'utf-8');
|
||||||
|
// Strip <head>/<body> wrappers — jsdom already provides those.
|
||||||
|
const bodyMatch = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||||
|
document.body.innerHTML = bodyMatch ? bodyMatch[1] : html;
|
||||||
|
|
||||||
|
// Mock the electronAPI bridge before the controller reads it.
|
||||||
|
const generateMock = jest.fn(async ({ text, font }) => {
|
||||||
|
if (typeof font === 'string' && font.startsWith('template:')) {
|
||||||
|
return `<<${font.slice('template:'.length)}>>`;
|
||||||
|
}
|
||||||
|
return `<<font=${font}::text=${text}>>`;
|
||||||
|
});
|
||||||
|
const apiMock = {
|
||||||
|
listFonts: jest.fn(async () => [
|
||||||
|
{ id: 'standard', label: 'Standard', kind: 'hand-coded' },
|
||||||
|
{ id: 'big', label: 'Big', kind: 'hand-coded' },
|
||||||
|
{ id: 'template:arrow-right', label: 'Template · Arrow Right', kind: 'template' },
|
||||||
|
]),
|
||||||
|
generate: generateMock,
|
||||||
|
lastFont: jest.fn(async () => null),
|
||||||
|
copy: jest.fn(async () => undefined),
|
||||||
|
save: jest.fn(async () => ({ canceled: true })),
|
||||||
|
};
|
||||||
|
window.electronAPI = {
|
||||||
|
generators: { ascii: apiMock },
|
||||||
|
send: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Execute the controller source in the test realm. The IIFE references
|
||||||
|
// `window` and `document` directly, so we wrap it in an outer function
|
||||||
|
// that passes the jsdom globals as parameters. Using Function() here is
|
||||||
|
// safe: the only interpolated value is the file contents (read from a
|
||||||
|
// path we control in the repo), not user input.
|
||||||
|
const src = fs.readFileSync(CONTROLLER_PATH, 'utf-8');
|
||||||
|
// eslint-disable-next-line no-new-func
|
||||||
|
new Function('window', 'document', src)(window, document);
|
||||||
|
|
||||||
|
return { apiMock, generateMock };
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ascii-generator.html — fonts.css link', () => {
|
||||||
|
test('does not escape src/ (no "../" in href)', () => {
|
||||||
|
const html = fs.readFileSync(HTML_PATH, 'utf-8');
|
||||||
|
const linkRe = /<link[^>]*rel=["']stylesheet["'][^>]*href=["']([^"']+)["'][^>]*>/i;
|
||||||
|
const match = html.match(linkRe);
|
||||||
|
expect(match).not.toBeNull();
|
||||||
|
const href = match[1];
|
||||||
|
expect(href.startsWith('../')).toBe(false);
|
||||||
|
// The standalone window's document base is src/, so fonts.css must be
|
||||||
|
// src/-relative (not ../fonts.css).
|
||||||
|
expect(href).toBe('fonts.css');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ascii-controller: pure box renderer', () => {
|
||||||
|
test('wraps text with a single-line border by default', () => {
|
||||||
|
mount();
|
||||||
|
const { renderBox } = window.ASCIIBoxRenderer;
|
||||||
|
const out = renderBox('Hi', 'single', 0);
|
||||||
|
expect(out).toBe('┌────┐\n│ Hi │\n└────┘');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('honours padding and ascii (+|-) style', () => {
|
||||||
|
mount();
|
||||||
|
const { renderBox } = window.ASCIIBoxRenderer;
|
||||||
|
const out = renderBox('Hi', 'ascii', 1);
|
||||||
|
// 1 space pad + 'Hi' + 1 space pad = width 4 inside, +2 borders = 6 wide.
|
||||||
|
expect(out).toBe('+------+\n| Hi |\n+------+');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ascii-controller: mode-tab switching', () => {
|
||||||
|
test('clicking the Box tab reveals box-mode and hides the others', async () => {
|
||||||
|
mount();
|
||||||
|
await window.ASCIIController.bootstrap();
|
||||||
|
const boxTab = document.querySelector('.mode-tab[data-mode="box"]');
|
||||||
|
boxTab.click();
|
||||||
|
|
||||||
|
expect(boxTab.classList.contains('active')).toBe(true);
|
||||||
|
expect(document.getElementById('box-mode').classList.contains('active')).toBe(true);
|
||||||
|
expect(document.getElementById('text-mode').classList.contains('active')).toBe(false);
|
||||||
|
expect(document.getElementById('templates-mode').classList.contains('active')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('clicking the Templates tab reveals templates-mode', async () => {
|
||||||
|
mount();
|
||||||
|
await window.ASCIIController.bootstrap();
|
||||||
|
const tplTab = document.querySelector('.mode-tab[data-mode="templates"]');
|
||||||
|
tplTab.click();
|
||||||
|
|
||||||
|
expect(tplTab.classList.contains('active')).toBe(true);
|
||||||
|
expect(document.getElementById('templates-mode').classList.contains('active')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ascii-controller: template buttons', () => {
|
||||||
|
test('clicking a template button renders its content into the preview', async () => {
|
||||||
|
const { generateMock } = mount();
|
||||||
|
await window.ASCIIController.bootstrap();
|
||||||
|
const btn = document.querySelector('.template-btn[data-template="arrow-right"]');
|
||||||
|
btn.click();
|
||||||
|
// Wait for the async generate() call to resolve.
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
|
||||||
|
expect(generateMock).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({ font: 'template:arrow-right' })
|
||||||
|
);
|
||||||
|
expect(document.getElementById('preview').textContent).toBe('<<arrow-right>>');
|
||||||
|
expect(btn.classList.contains('active')).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user