diff --git a/frontend/apps/remark42/app/components/comment/comment.test.tsx b/frontend/apps/remark42/app/components/comment/comment.test.tsx
index 3e52f471..a1dae1d2 100644
--- a/frontend/apps/remark42/app/components/comment/comment.test.tsx
+++ b/frontend/apps/remark42/app/components/comment/comment.test.tsx
@@ -7,6 +7,7 @@ import { render } from 'tests/utils';
import { StaticStore } from 'common/static-store';
import { Comment, CommentProps } from './comment';
+import { CommentForm } from 'components/comment-form';
import { CommentMode } from 'common/types';
function CommentWithIntl(props: CommentProps) {
@@ -255,4 +256,99 @@ describe('', () => {
expect(screen.getByText('Edit')).toBeVisible();
});
});
+
+ // Regression tests for issue #2040.
+ // The edit textarea must reflect `data.orig` byte-for-byte; any transformation
+ // (HTML-entity decoding in particular) corrupts user input on save because
+ // bluemonday then strips now-real tags that the user typed as entities.
+ describe('edit textarea preserves data.orig verbatim', () => {
+ afterEach(() => {
+ CommentForm.textareaCounter = 0;
+ localStorage.clear();
+ });
+
+ const cases: Array<[string, string]> = [
+ ['issue 2040 canonical', '<script>Hacked you!</script>'],
+ ['doubly-escaped entity', '<script>'],
+ ['mixed named entities', 'I love © 2026 & "quotes" 'too''],
+ ['nbsp entity whitespace', ' indented'],
+ ['decimal numeric entities', '<div>hello</div>'],
+ ['hex numeric entities with xss-like content', '<img src=x onerror=alert(1)>'],
+ ['hex numeric emoji entities', '😀 💩 emoji via numeric'],
+ ['obscure named entities', 'Æsop était ici'],
+ ['malformed entity without semicolon', '< without semicolon and & with'],
+ ['entity-shaped junk', '&;<;ZZZ;'],
+ ['recursive-looking numeric entity', '<'],
+ ['programmer content with real < and entities', '5 < 10 && 10 > 5'],
+ ['real < mixed with <', 'a < b and <tag> literal'],
+ ['sixteen back-to-back <', '<<<<<<<<<<<<<<<<'],
+ ['fenced code block with entities', '```\n<pre>code</pre>\n```'],
+ ['link with entity-encoded query ampersands', '[link](https://example.com/?a=1&b=2&c=3)'],
+ ['null/replacement/surrogate hex entities', '�'],
+
+ ['zero-width characters interleaved', 'Hello\u200Bworld\u200Cfoo\u200Dbar'],
+ ['bidi Hebrew + Arabic + ASCII', '\u05E9\u05DC\u05D5\u05DD hello \u0645\u0631\u062D\u0628\u0627'],
+ ['RLO override embedded', 'safe\u202Eevil\u202Cend'],
+ ['decomposed vs precomposed é', 'cafe\u0301 vs caf\u00E9'],
+ ['ZWJ family emoji', 'family: \uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66'],
+ ['supplementary plane surrogate pairs', 'poop: \uD83D\uDCA9 and math: \uD835\uDC00'],
+ ['line and paragraph separators', 'line1\u2028line2\u2029para2'],
+ ['BOM at start middle end', '\uFEFFstart mid\uFEFFdle end\uFEFF'],
+ ['varied unicode whitespace', 'a\u00A0b\u3000c\u2003d\u202Fe'],
+ ['tab LF CR CRLF LFCR', 'tab\there\nnl\rcr\r\ncrlf\n\rlfcr'],
+ ['cyrillic homoglyph a', 'Cyrillic \u0430pple vs Latin apple'],
+ ['full-width angle brackets', 'fullwidth \uFF1Cscript\uFF1E not a tag'],
+ ['bidi mark soup', 'mix: \u202Dltr\u202C \u202Ertl\u202C \u200E\u200F'],
+
+ ['inline code with real script tag', '``'],
+ ['inline code with entity script tag', '`<script>`'],
+ [
+ 'fenced html with iframe and double-escaped',
+ '```html\n\n<b>\n```',
+ ],
+ ['javascript link', '[click](javascript:alert(1))'],
+ ['image with entity alt and title', ''],
+ ['markdown backslash escapes', '\\*not em\\* \\_not em\\_ \\\\ \\< \\& \\`not code\\`'],
+ ['entities inside emphasis markers', '*<em>* _>underscore<_ **&bold&**'],
+ ['headers with entities', '# <h1>\n## &header&'],
+ ['blockquote with entities multiline', '> <foo>\n> "ed&\n>\n> nested <bar/>'],
+ ['raw autolinks', ' see also '],
+ ['raw script tag no markdown', ''],
+ ['iframe object embed chain', '