Skip to content

Commit 82a3be2

Browse files
SavioBS629claude
andcommitted
fix(testmanagement): wrap rich-text fields in <p> so TM renders special chars
TM's v2 API entity-encodes <, > and & in step/result, preconditions and description on write, and the TM UI renders those fields as rich text only when the value is HTML-wrapped — bare text is displayed literally, so a plain ">" surfaced to users as "&gt;" (PMAA-185, FD 1709260, TM-26634). Per the TM team, external API content must be wrapped in <p> tags. Wrap those fields on createTestCase and updateTestCase when the value is not already HTML and contains an encodable character; all other payloads are sent unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 3661686 commit 82a3be2

4 files changed

Lines changed: 109 additions & 3 deletions

File tree

src/tools/testmanagement-utils/create-testcase.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
} from "./TCG-utils/api.js";
1010
import { BrowserStackConfig } from "../../lib/types.js";
1111
import { getTMBaseURL } from "../../lib/tm-base-url.js";
12+
import { wrapRichText, wrapTestCaseSteps } from "./rich-text.js";
1213
import logger from "../../logger.js";
1314

1415
interface TestCaseStep {
@@ -324,6 +325,18 @@ export async function createTestCase(
324325
new Set([...(testCaseParams.tags ?? []), "MCP Generated"]),
325326
);
326327

328+
// Rich-text fields must be HTML-wrapped or the TM UI shows entity-encoded
329+
// text literally (PMAA-185); see rich-text.ts.
330+
testCaseParams.test_case_steps = wrapTestCaseSteps(
331+
testCaseParams.test_case_steps,
332+
);
333+
if (testCaseParams.preconditions !== undefined) {
334+
testCaseParams.preconditions = wrapRichText(testCaseParams.preconditions);
335+
}
336+
if (testCaseParams.description !== undefined) {
337+
testCaseParams.description = wrapRichText(testCaseParams.description);
338+
}
339+
327340
if (
328341
testCaseParams.priority !== undefined ||
329342
testCaseParams.case_type !== undefined
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// TM's v2 API treats step/result, preconditions and description as rich-text
2+
// fields: its write-time sanitizer entity-encodes <, > and & in text nodes
3+
// (preserving allowed tags like <p>), and the TM UI renders the stored value
4+
// as rich text only when it is HTML-wrapped — bare text is displayed
5+
// literally, so a bare ">" surfaces to users as "&gt;" (PMAA-185).
6+
// Per the TM team, external API content must be wrapped in <p> tags to opt
7+
// into rich-text rendering. Quotes are not entity-encoded, so only values
8+
// containing <, > or & need the wrap; everything else is left untouched.
9+
10+
const LOOKS_LIKE_HTML = /^\s*<[a-z][^>]*>/i;
11+
const HAS_ENCODABLE_CHARS = /[<>&]/;
12+
13+
export function wrapRichText(text: string): string {
14+
if (LOOKS_LIKE_HTML.test(text) || !HAS_ENCODABLE_CHARS.test(text)) {
15+
return text;
16+
}
17+
return `<p>${text}</p>`;
18+
}
19+
20+
export function wrapTestCaseSteps<T extends { step: string; result: string }>(
21+
steps: T[],
22+
): T[] {
23+
return steps.map((s) => ({
24+
...s,
25+
step: wrapRichText(s.step),
26+
result: wrapRichText(s.result),
27+
}));
28+
}

src/tools/testmanagement-utils/update-testcase.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import { BrowserStackConfig } from "../../lib/types.js";
1111
import { getTMBaseURL } from "../../lib/tm-base-url.js";
1212
import { getBrowserStackAuth } from "../../lib/get-auth.js";
13+
import { wrapRichText, wrapTestCaseSteps } from "./rich-text.js";
1314
import logger from "../../logger.js";
1415

1516
export interface TestCaseUpdateRequest {
@@ -177,12 +178,14 @@ export async function updateTestCase(
177178
const testCaseBody: Record<string, any> = {};
178179

179180
if (params.name !== undefined) testCaseBody.name = params.name;
181+
// Rich-text fields must be HTML-wrapped or the TM UI shows entity-encoded
182+
// text literally (PMAA-185); see rich-text.ts.
180183
if (params.description !== undefined)
181-
testCaseBody.description = params.description;
184+
testCaseBody.description = wrapRichText(params.description);
182185
if (params.preconditions !== undefined)
183-
testCaseBody.preconditions = params.preconditions;
186+
testCaseBody.preconditions = wrapRichText(params.preconditions);
184187
if (params.test_case_steps !== undefined)
185-
testCaseBody.test_case_steps = params.test_case_steps;
188+
testCaseBody.test_case_steps = wrapTestCaseSteps(params.test_case_steps);
186189
if (params.owner !== undefined) testCaseBody.owner = params.owner;
187190
if (params.status !== undefined) testCaseBody.status = params.status;
188191
if (params.tags !== undefined) testCaseBody.tags = params.tags;

tests/tools/richText.test.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
wrapRichText,
4+
wrapTestCaseSteps,
5+
} from '../../src/tools/testmanagement-utils/rich-text';
6+
7+
describe('wrapRichText', () => {
8+
it('wraps bare text containing > in <p> tags', () => {
9+
expect(wrapRichText('Navigate to Settings > Users')).toBe(
10+
'<p>Navigate to Settings > Users</p>',
11+
);
12+
});
13+
14+
it('wraps bare text containing < in <p> tags', () => {
15+
expect(wrapRichText('if a < b then pass')).toBe(
16+
'<p>if a < b then pass</p>',
17+
);
18+
});
19+
20+
it('wraps bare text containing & in <p> tags', () => {
21+
expect(wrapRichText('Q&A section loads')).toBe('<p>Q&A section loads</p>');
22+
});
23+
24+
it('leaves text without <, > or & untouched', () => {
25+
expect(wrapRichText('Click "Save" then \'Confirm\'')).toBe(
26+
'Click "Save" then \'Confirm\'',
27+
);
28+
});
29+
30+
it('leaves already-HTML content untouched', () => {
31+
expect(wrapRichText('<p>Settings &gt; Users</p>')).toBe(
32+
'<p>Settings &gt; Users</p>',
33+
);
34+
expect(wrapRichText(' <ul><li>A &amp; B</li></ul>')).toBe(
35+
' <ul><li>A &amp; B</li></ul>',
36+
);
37+
});
38+
39+
it('wraps text starting with < that is not a tag', () => {
40+
expect(wrapRichText('< 5 items are shown')).toBe(
41+
'<p>< 5 items are shown</p>',
42+
);
43+
});
44+
});
45+
46+
describe('wrapTestCaseSteps', () => {
47+
it('wraps step and result independently and preserves other keys', () => {
48+
expect(
49+
wrapTestCaseSteps([
50+
{ step: 'Go to A > B', result: 'B page loads' },
51+
{ step: 'Click Save', result: 'Count > 0' },
52+
]),
53+
).toEqual([
54+
{ step: '<p>Go to A > B</p>', result: 'B page loads' },
55+
{ step: 'Click Save', result: '<p>Count > 0</p>' },
56+
]);
57+
});
58+
59+
it('returns an empty array unchanged', () => {
60+
expect(wrapTestCaseSteps([])).toEqual([]);
61+
});
62+
});

0 commit comments

Comments
 (0)