Files
wekan/tests/copyCodeBlock.test.cjs
T
Lauri Ojansivu 27201f642f Copy code block: reliable per-viewer button, copies raw text, translated (#5149).
The copy-code-block feature existed but the reporter saw it 'work very sporadically (only a handful of items have it)' and lost after editing. Root causes: it ran ONCE in Template.editor.onRendered with a global document.querySelectorAll('.viewer > pre'), so it decorated only whichever viewers were on the page at that instant and never re-added the button after an edit re-rendered the viewer; and it copied the HTML-escaped innerHTML of the <pre>'s first child node (often the wrong content or undefined) via the deprecated execCommand. Now the decoration lives in Template.viewer.onRendered, so EVERY viewer (card description, checklist item, comment) gets a copy button and it is restored on every re-render; it is de-duplicated (skips a <pre> that already has its button) so re-renders never stack buttons; it copies the RAW code text (pre.textContent) via navigator.clipboard.writeText with an execCommand fallback; and the button title is translated (the existing 'copy-text-to-clipboard' i18n key) instead of a hard-coded English string.

Tests: tests/copyCodeBlock.test.cjs (5 checks) guards that the decoration is in Template.viewer.onRendered scoped to the viewer's <pre>, copies textContent (not innerHTML), de-dups, is translated, and uses the async Clipboard API with a fallback.

Thanks to C0rn3j and xet7 !

Fixes #5149,
2026-07-22 13:27:50 +03:00

68 lines
3.1 KiB
JavaScript

'use strict';
// Regression guard for #5149 ("Copy code block to clipboard"). The feature adds a
// copy button to each code block in the read-only card viewer. It previously ran
// once in Template.editor.onRendered with a global `document.querySelectorAll`,
// so it decorated only whichever viewers were on the page at that instant ("works
// sporadically"), lost the button after editing, and copied the HTML-escaped
// innerHTML of childNodes[0] (wrong/undefined content). It now decorates in
// Template.viewer.onRendered — per viewer, on every re-render, de-duplicated,
// copying the raw text. This is a source guard (the code is Blaze/DOM-coupled).
// Run: node tests/copyCodeBlock.test.cjs
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const src = fs.readFileSync(
path.join(__dirname, '..', 'client', 'components', 'main', 'editor.js'),
'utf8',
);
let passed = 0;
function check(name, fn) { fn(); passed += 1; console.log(' ok -', name); }
check('#5149: the copy-code decoration runs in Template.viewer.onRendered (per viewer)', () => {
assert.ok(/Template\.viewer\.onRendered\(/.test(src),
'must decorate in Template.viewer.onRendered, not a one-time global query');
const start = src.indexOf('Template.viewer.onRendered(');
const body = src.slice(start, start + 900);
assert.ok(/this\.\$\('pre'\)/.test(body),
"must scope to the viewer's own <pre> blocks (this.$('pre'))");
});
check('#5149: it copies the RAW text (pre.textContent), not innerHTML', () => {
assert.ok(/copyCodeBlockText\(pre\.textContent\)/.test(src),
'must copy pre.textContent');
assert.ok(!/textArea\.value = target\.innerHTML/.test(src),
'the old innerHTML-of-childNodes[0] copy must be gone');
assert.ok(!/querySelectorAll\("\.viewer > pre"\)/.test(src),
'the old global one-time query must be gone');
});
check('#5149: buttons are de-duplicated (no stacking on re-render)', () => {
const start = src.indexOf('Template.viewer.onRendered(');
const body = src.slice(start, start + 900);
assert.ok(/js-copy-code/.test(body), 'the button carries a js-copy-code marker class');
assert.ok(/previousElementSibling[\s\S]*js-copy-code[\s\S]*return/.test(body),
'must skip a <pre> that already has its copy button');
});
check('#5149: the button title is translated (not a hard-coded English string)', () => {
const start = src.indexOf('Template.viewer.onRendered(');
const body = src.slice(start, start + 900);
assert.ok(/TAPi18n\.__\('copy-text-to-clipboard'\)/.test(body),
"must use the i18n key 'copy-text-to-clipboard'");
const en = JSON.parse(fs.readFileSync(
path.join(__dirname, '..', 'imports', 'i18n', 'data', 'en.i18n.json'), 'utf8'));
assert.ok(en['copy-text-to-clipboard'], 'the i18n key must exist in en.i18n.json');
});
check('#5149: uses the async Clipboard API with an execCommand fallback', () => {
assert.ok(/navigator\.clipboard && navigator\.clipboard\.writeText/.test(src),
'must prefer navigator.clipboard.writeText');
assert.ok(/fallbackCopyText/.test(src), 'must have an execCommand fallback');
});
console.log(`\ncopyCodeBlock: ${passed} checks passed`);