Repository: nodejs/node
Pull request: test_runner: fix line feed escaping in JUnit

Fixes #59593 (a part related to the XML `failure:message` attribute value)

## Cause

In `escapeAttribute` line feed characters were simply removed, so multi-line messages were distorted in the report, like:

```text
The value must be 01 !== 0
```

However, the original message is:

```text
The value must be 0

1 !== 0

```

## Solution motivation

https://www.w3.org/TR/WD-xml-970807#sec3.3.3

> Character references and references to internal text entities must be expanded

Thus, the character reference `&#10;` must be expanded by the XML report processor/application to the line feed character.

## Changes

I used the decimal code `&#10;` as a decimal one is already used in `escapeComment`. 

`/(&)(?!#\d{1,7};)/g` in `escapeContent` prevents escaping `&` in decimal character references.

Updated the test snapshot.

### Effect

```diff
-		<failure type="testCodeFailure" message="Expected values to be strictly equal:true !== false">
+		<failure type="testCodeFailure" message="Expected values to be strictly equal:&#10;&#10;true !== false">
```

## Testing

<img width="983" height="139" alt="image" src="https://github.com/user-attachments/assets/06244199-217d-49a3-b4fa-0ad1927ec81c" />

## Additional details

I haven't removed `testcase:failure` as it's submitted via #59685


Changed files:
filename: lib/internal/test_runner/reporter/junit.js
patch: @@ -19,11 +19,11 @@ const inspectOptions = { __proto__: null, colors: false, breakLength: Infinity }
 const HOSTNAME = hostname();
 
 function escapeAttribute(s = '') {
-  return escapeContent(RegExpPrototypeSymbolReplace(/"/g, RegExpPrototypeSymbolReplace(/\n/g, s, ''), '&quot;'));
+  return escapeContent(RegExpPrototypeSymbolReplace(/"/g, RegExpPrototypeSymbolReplace(/\n/g, s, '&#10;'), '&quot;'));
 }
 
 function escapeContent(s = '') {
-  return RegExpPrototypeSymbolReplace(/</g, RegExpPrototypeSymbolReplace(/&/g, s, '&amp;'), '&lt;');
+  return RegExpPrototypeSymbolReplace(/</g, RegExpPrototypeSymbolReplace(/(&)(?!#\d{1,7};)/g, s, '&amp;'), '&lt;');
 }
 
 function escapeComment(s = '') {
@@ -137,7 +137,7 @@ module.exports = async function* junitReporter(source) {
               __proto__: null,
               nesting: event.data.nesting + 1,
               tag: 'failure',
-              attrs: { __proto__: null, type: error?.failureType || error?.code, message: error?.message ?? '' },
+              attrs: { __proto__: null, type: error?.failureType || error?.code, message: error?.message.trim() ?? '' },
               children: [inspectWithNoCustomRetry(error, inspectOptions)],
             });
             currentTest.failures = 1;

filename: test/fixtures/test-runner/output/junit_reporter.snapshot
patch: @@ -101,8 +101,8 @@
 }
 		</failure>
 	</testcase>
-	<testcase name="async assertion fail" time="*" classname="test" file="*" failure="Expected values to be strictly equal:true !== false">
-		<failure type="testCodeFailure" message="Expected values to be strictly equal:true !== false">
+	<testcase name="async assertion fail" time="*" classname="test" file="*" failure="Expected values to be strictly equal:&#10;&#10;true !== false&#10;">
+		<failure type="testCodeFailure" message="Expected values to be strictly equal:&#10;&#10;true !== false">
 [Error [ERR_TEST_FAILURE]: Expected values to be strictly equal:
 
 true !== false
@@ -316,8 +316,8 @@ Error [ERR_TEST_FAILURE]: thrown from callback async throw
 [Error [ERR_TEST_FAILURE]: customized] { code: 'ERR_TEST_FAILURE', failureType: 'testCodeFailure', cause: customized }
 		</failure>
 	</testcase>
-	<testcase name="custom inspect symbol that throws fail" time="*" classname="test" file="*" failure="{  foo: 1,  Symbol(nodejs.util.inspect.custom): [Function: [nodejs.util.inspect.custom]]}">
-		<failure type="testCodeFailure" message="{  foo: 1,  Symbol(nodejs.util.inspect.custom): [Function: [nodejs.util.inspect.custom]]}">
+	<testcase name="custom inspect symbol that throws fail" time="*" classname="test" file="*" failure="{&#10;  foo: 1,&#10;  Symbol(nodejs.util.inspect.custom): [Function: [nodejs.util.inspect.custom]]&#10;}">
+		<failure type="testCodeFailure" message="{&#10;  foo: 1,&#10;  Symbol(nodejs.util.inspect.custom): [Function: [nodejs.util.inspect.custom]]&#10;}">
 [Error [ERR_TEST_FAILURE]: {
   foo: 1,
   Symbol(nodejs.util.inspect.custom): [Function: [nodejs.util.inspect.custom]]
@@ -413,8 +413,8 @@ Error [ERR_TEST_FAILURE]: bar
 }
 		</failure>
 	</testcase>
-	<testcase name="assertion errors display actual and expected properly" time="*" classname="test" file="*" failure="Expected values to be loosely deep-equal:{  bar: 1,  baz: {    date: 1970-01-01T00:00:00.000Z,    null: null,    number: 1,    string: 'Hello',    undefined: undefined  },  boo: [    1  ],  foo: 1}should loosely deep-equal{  baz: {    date: 1970-01-01T00:00:00.000Z,    null: null,    number: 1,    string: 'Hello',    undefined: undefined  },  boo: [    1  ],  circular: &lt;ref *1> {    bar: 2,    c: [Circular *1]  }}">
-		<failure type="testCodeFailure" message="Expected values to be loosely deep-equal:{  bar: 1,  baz: {    date: 1970-01-01T00:00:00.000Z,    null: null,    number: 1,    string: 'Hello',    undefined: undefined  },  boo: [    1  ],  foo: 1}should loosely deep-equal{  baz: {    date: 1970-01-01T00:00:00.000Z,    null: null,    number: 1,    string: 'Hello',    undefined: undefined  },  boo: [    1  ],  circular: &lt;ref *1> {    bar: 2,    c: [Circular *1]  }}">
+	<testcase name="assertion errors display actual and expected properly" time="*" classname="test" file="*" failure="Expected values to be loosely deep-equal:&#10;&#10;{&#10;  bar: 1,&#10;  baz: {&#10;    date: 1970-01-01T00:00:00.000Z,&#10;    null: null,&#10;    number: 1,&#10;    string: 'Hello',&#10;    undefined: undefined&#10;  },&#10;  boo: [&#10;    1&#10;  ],&#10;  foo: 1&#10;}&#10;&#10;should loosely deep-equal&#10;&#10;{&#10;  baz: {&#10;    date: 1970-01-01T00:00:00.000Z,&#10;    null: null,&#10;    number: 1,&#10;    string: 'Hello',&#10;    undefined: undefined&#10;  },&#10;  boo: [&#10;    1&#10;  ],&#10;  circular: &lt;ref *1> {&#10;    bar: 2,&#10;    c: [Circular *1]&#10;  }&#10;}">
+		<failure type="testCodeFailure" message="Expected values to be loosely deep-equal:&#10;&#10;{&#10;  bar: 1,&#10;  baz: {&#10;    date: 1970-01-01T00:00:00.000Z,&#10;    null: null,&#10;    number: 1,&#10;    string: 'Hello',&#10;    undefined: undefined&#10;  },&#10;  boo: [&#10;    1&#10;  ],&#10;  foo: 1&#10;}&#10;&#10;should loosely deep-equal&#10;&#10;{&#10;  baz: {&#10;    date: 1970-01-01T00:00:00.000Z,&#10;    null: null,&#10;    number: 1,&#10;    string: 'Hello',&#10;    undefined: undefined&#10;  },&#10;  boo: [&#10;    1&#10;  ],&#10;  circular: &lt;ref *1> {&#10;    bar: 2,&#10;    c: [Circular *1]&#10;  }&#10;}">
 [Error [ERR_TEST_FAILURE]: Expected values to be loosely deep-equal:
 
 {