testomatio-editor-blocks 0.4.35 → 0.4.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package/editor/blocks/step.js +0 -29
- package/package/editor/customMarkdownConverter.js +33 -23
- package/package.json +1 -1
- package/src/App.tsx +2 -7
- package/src/editor/blocks/step.tsx +0 -37
- package/src/editor/customMarkdownConverter.test.ts +166 -0
- package/src/editor/customMarkdownConverter.ts +35 -23
|
@@ -193,31 +193,6 @@ export const stepBlock = createReactBlockSpec({
|
|
|
193
193
|
}
|
|
194
194
|
return count;
|
|
195
195
|
}, [block.id, documentVersion, editor.document]);
|
|
196
|
-
// Check if there is a preceding "Steps" heading
|
|
197
|
-
const hasStepsHeading = useMemo(() => {
|
|
198
|
-
const allBlocks = editor.document;
|
|
199
|
-
const blockIndex = allBlocks.findIndex((b) => b.id === block.id);
|
|
200
|
-
if (blockIndex < 0)
|
|
201
|
-
return false;
|
|
202
|
-
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
203
|
-
const b = allBlocks[i];
|
|
204
|
-
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (b.type === "heading") {
|
|
208
|
-
const text = (Array.isArray(b.content) ? b.content : [])
|
|
209
|
-
.filter((n) => n.type === "text")
|
|
210
|
-
.map((n) => { var _a; return (_a = n.text) !== null && _a !== void 0 ? _a : ""; })
|
|
211
|
-
.join("")
|
|
212
|
-
.trim()
|
|
213
|
-
.toLowerCase()
|
|
214
|
-
.replace(/[:\-–—]$/, "");
|
|
215
|
-
return isStepsHeading(text);
|
|
216
|
-
}
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
}, [block.id, documentVersion, editor.document]);
|
|
221
196
|
useEditorChange(() => {
|
|
222
197
|
setDocumentVersion((version) => version + 1);
|
|
223
198
|
}, editor);
|
|
@@ -362,10 +337,6 @@ export const stepBlock = createReactBlockSpec({
|
|
|
362
337
|
}, [expectedHasContent, isExpectedVisible]);
|
|
363
338
|
const canToggleData = !dataHasContent;
|
|
364
339
|
const canToggleExpected = !expectedHasContent;
|
|
365
|
-
// Render as plain text when not under a "Steps" heading
|
|
366
|
-
if (!hasStepsHeading) {
|
|
367
|
-
return (_jsxs("div", { className: "bn-teststep-plain", "data-block-id": block.id, children: [_jsx("span", { children: stepTitle || "(empty step)" }), stepData ? _jsx("span", { className: "bn-teststep-plain__data", children: stepData }) : null, expectedResult ? _jsx("span", { className: "bn-teststep-plain__expected", children: expectedResult }) : null] }));
|
|
368
|
-
}
|
|
369
340
|
if (viewMode === "horizontal") {
|
|
370
341
|
return (_jsx(StepHorizontalView, { blockId: block.id, stepNumber: stepNumber, stepValue: combinedStepValue, expectedResult: expectedResult, onStepChange: handleCombinedStepChange, onExpectedChange: handleExpectedChange, onInsertNextStep: handleInsertNextStep, onFieldFocus: handleFieldFocus, viewToggle: _jsx("button", { type: "button", className: "bn-teststep__view-toggle bn-teststep__view-toggle--horizontal", "data-tooltip": "Switch step view", "aria-label": "Switch step view", onClick: handleToggleView, children: _jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", "aria-hidden": "true", children: [_jsx("mask", { id: "mask-toggle", style: { maskType: "alpha" }, maskUnits: "userSpaceOnUse", x: "0", y: "0", width: "16", height: "16", children: _jsx("rect", { width: "16", height: "16", fill: "#D9D9D9" }) }), _jsx("g", { mask: "url(#mask-toggle)", children: _jsx("path", { d: "M12.6667 2C13.0333 2 13.3472 2.13056 13.6083 2.39167C13.8694 2.65278 14 2.96667 14 3.33333L14 12.6667C14 13.0333 13.8694 13.3472 13.6083 13.6083C13.3472 13.8694 13.0333 14 12.6667 14L10 14C9.63333 14 9.31944 13.8694 9.05833 13.6083C8.79722 13.3472 8.66667 13.0333 8.66667 12.6667L8.66667 3.33333C8.66667 2.96667 8.79722 2.65278 9.05833 2.39167C9.31945 2.13055 9.63333 2 10 2L12.6667 2ZM6 2C6.36667 2 6.68056 2.13055 6.94167 2.39167C7.20278 2.65278 7.33333 2.96667 7.33333 3.33333L7.33333 12.6667C7.33333 13.0333 7.20278 13.3472 6.94167 13.6083C6.68055 13.8694 6.36667 14 6 14L3.33333 14C2.96667 14 2.65278 13.8694 2.39167 13.6083C2.13056 13.3472 2 13.0333 2 12.6667L2 3.33333C2 2.96667 2.13056 2.65278 2.39167 2.39167C2.65278 2.13055 2.96667 2 3.33333 2L6 2ZM3.33333 12.6667L6 12.6667L6 3.33333L3.33333 3.33333L3.33333 12.6667Z", fill: "currentColor" }) })] }) }) }));
|
|
371
342
|
}
|
|
@@ -80,7 +80,6 @@ function unescapeMarkdown(text) {
|
|
|
80
80
|
return stripHtmlWrappers(text).replace(/\\([*_`~\[\]()<>\\])/g, "$1");
|
|
81
81
|
}
|
|
82
82
|
function applyTextStyles(text, styles) {
|
|
83
|
-
var _a;
|
|
84
83
|
if (!styles) {
|
|
85
84
|
return text;
|
|
86
85
|
}
|
|
@@ -116,11 +115,22 @@ function applyTextStyles(text, styles) {
|
|
|
116
115
|
suffix: "</span>",
|
|
117
116
|
});
|
|
118
117
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
118
|
+
// Split on newlines so that style markers wrap each line individually.
|
|
119
|
+
// This prevents <br/> (inserted by table cell formatting) from being
|
|
120
|
+
// trapped inside markers like **bold<br/>text**.
|
|
121
|
+
const segments = result.split("\n");
|
|
122
|
+
const wrapped = segments.map((segment) => {
|
|
123
|
+
var _a;
|
|
124
|
+
if (!segment)
|
|
125
|
+
return segment;
|
|
126
|
+
let s = segment;
|
|
127
|
+
for (const wrapper of wrappers) {
|
|
128
|
+
const suffix = (_a = wrapper.suffix) !== null && _a !== void 0 ? _a : wrapper.prefix;
|
|
129
|
+
s = `${wrapper.prefix}${s}${suffix}`;
|
|
130
|
+
}
|
|
131
|
+
return s;
|
|
132
|
+
});
|
|
133
|
+
return wrapped.join("\n");
|
|
124
134
|
}
|
|
125
135
|
function inlineToMarkdown(content) {
|
|
126
136
|
if (!content || !Array.isArray(content)) {
|
|
@@ -309,17 +319,17 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
309
319
|
}
|
|
310
320
|
return flattenWithBlankLine(lines, true);
|
|
311
321
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
322
|
+
const normalizedTitle = stepTitle
|
|
323
|
+
.split(/\r?\n/)
|
|
324
|
+
.map((segment) => segment.trim())
|
|
325
|
+
.filter((segment) => segment.length > 0)
|
|
326
|
+
.join(" ");
|
|
327
|
+
const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
|
|
328
|
+
const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
|
|
329
|
+
if (normalizedTitle.length > 0 || hasContent) {
|
|
330
|
+
const listStyle = (_m = block.props.listStyle) !== null && _m !== void 0 ? _m : "bullet";
|
|
331
|
+
const prefix = listStyle === "ordered" ? `${(stepIndex !== null && stepIndex !== void 0 ? stepIndex : 0) + 1}.` : "*";
|
|
332
|
+
lines.push(normalizedTitle.length > 0 ? `${prefix} ${normalizedTitle}` : `${prefix} `);
|
|
323
333
|
}
|
|
324
334
|
if (stepData.length > 0) {
|
|
325
335
|
const dataLines = stepData.split(/\r?\n/);
|
|
@@ -606,7 +616,7 @@ function detectListType(trimmed) {
|
|
|
606
616
|
if (/^\d+[.)]\s+/.test(trimmed)) {
|
|
607
617
|
return "numbered";
|
|
608
618
|
}
|
|
609
|
-
if (/^[-*+]\s
|
|
619
|
+
if (/^[-*+](\s+|$)/.test(trimmed)) {
|
|
610
620
|
return "bullet";
|
|
611
621
|
}
|
|
612
622
|
return null;
|
|
@@ -695,7 +705,7 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
|
|
|
695
705
|
}
|
|
696
706
|
else {
|
|
697
707
|
const bulletMatch = trimmed.match(/^[-*+]\s+(.*)$/);
|
|
698
|
-
const text = (_d = bulletMatch === null || bulletMatch === void 0 ? void 0 : bulletMatch[1]) !== null && _d !== void 0 ? _d : trimmed.slice(2);
|
|
708
|
+
const text = (_d = bulletMatch === null || bulletMatch === void 0 ? void 0 : bulletMatch[1]) !== null && _d !== void 0 ? _d : (trimmed.length <= 1 ? "" : trimmed.slice(2));
|
|
699
709
|
items.push({
|
|
700
710
|
type: "bulletListItem",
|
|
701
711
|
props: cloneBaseProps(),
|
|
@@ -721,7 +731,7 @@ function isLikelyStep(lines, index) {
|
|
|
721
731
|
if (hasIndent)
|
|
722
732
|
return true;
|
|
723
733
|
// Stop at new list items, headings, or other block-level elements (only if not indented)
|
|
724
|
-
if (/^[-*+]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed))
|
|
734
|
+
if (/^[-*+](\s|$)/.test(trimmed) || /^\d+[.)]\s/.test(trimmed))
|
|
725
735
|
break;
|
|
726
736
|
if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("```") || trimmed.startsWith(":::"))
|
|
727
737
|
break;
|
|
@@ -736,7 +746,7 @@ function isLikelyStep(lines, index) {
|
|
|
736
746
|
function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
737
747
|
const current = lines[index];
|
|
738
748
|
const trimmed = current.trim();
|
|
739
|
-
const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ");
|
|
749
|
+
const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ") || trimmed === "*" || trimmed === "-";
|
|
740
750
|
const isNumbered = /^\d+[.)]\s+/.test(trimmed);
|
|
741
751
|
if (!isBullet && !isNumbered) {
|
|
742
752
|
return null;
|
|
@@ -749,7 +759,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
749
759
|
}
|
|
750
760
|
let rawTitle;
|
|
751
761
|
if (isBullet) {
|
|
752
|
-
rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
|
|
762
|
+
rawTitle = unescapeMarkdown((trimmed.startsWith("* ") || trimmed.startsWith("- ")) ? trimmed.slice(2) : trimmed.slice(1)).trim();
|
|
753
763
|
}
|
|
754
764
|
else {
|
|
755
765
|
// For numbered lists, remove the number and delimiter
|
|
@@ -793,7 +803,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
|
|
|
793
803
|
continue;
|
|
794
804
|
}
|
|
795
805
|
const isNumberedStep = NUMBERED_STEP_REGEX.test(rawTrimmed);
|
|
796
|
-
const isNewStep = (!hasIndent && (rawTrimmed.startsWith("* ") || rawTrimmed.startsWith("- "))) ||
|
|
806
|
+
const isNewStep = (!hasIndent && (rawTrimmed.startsWith("* ") || rawTrimmed.startsWith("- ") || rawTrimmed === "*" || rawTrimmed === "-")) ||
|
|
797
807
|
(!hasIndent && isNumberedStep);
|
|
798
808
|
if (isNewStep) {
|
|
799
809
|
break;
|
package/package.json
CHANGED
package/src/App.tsx
CHANGED
|
@@ -22,7 +22,6 @@ import { customSchema, type CustomEditor } from "./editor/customSchema";
|
|
|
22
22
|
import { setStepsFetcher, type StepJsonApiDocument } from "./editor/stepAutocomplete";
|
|
23
23
|
import { setSnippetFetcher, type SnippetJsonApiDocument } from "./editor/snippetAutocomplete";
|
|
24
24
|
import { setImageUploadHandler } from "./editor/stepImageUpload";
|
|
25
|
-
import { canInsertStepOrSnippet } from "./editor/blocks/step";
|
|
26
25
|
import "./App.css";
|
|
27
26
|
|
|
28
27
|
const focusStepField = (
|
|
@@ -335,11 +334,7 @@ function CustomSlashMenu() {
|
|
|
335
334
|
},
|
|
336
335
|
};
|
|
337
336
|
|
|
338
|
-
|
|
339
|
-
const canInsert = cursorBlock ? canInsertStepOrSnippet(editor, cursorBlock.id) : false;
|
|
340
|
-
const customItems = canInsert ? [stepItem, snippetItem] : [];
|
|
341
|
-
|
|
342
|
-
return filterSuggestionItems([...defaultItems, ...customItems], query);
|
|
337
|
+
return filterSuggestionItems([...defaultItems, stepItem, snippetItem], query);
|
|
343
338
|
};
|
|
344
339
|
|
|
345
340
|
return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
|
|
@@ -489,7 +484,7 @@ function App() {
|
|
|
489
484
|
const fallbackBlock = documentBlocks[documentBlocks.length - 1];
|
|
490
485
|
const referenceId = selectedBlock?.id ?? fallbackBlock?.id;
|
|
491
486
|
|
|
492
|
-
if (!referenceId
|
|
487
|
+
if (!referenceId) {
|
|
493
488
|
return;
|
|
494
489
|
}
|
|
495
490
|
|
|
@@ -212,32 +212,6 @@ export const stepBlock = createReactBlockSpec(
|
|
|
212
212
|
return count;
|
|
213
213
|
}, [block.id, documentVersion, editor.document]);
|
|
214
214
|
|
|
215
|
-
// Check if there is a preceding "Steps" heading
|
|
216
|
-
const hasStepsHeading = useMemo(() => {
|
|
217
|
-
const allBlocks = editor.document;
|
|
218
|
-
const blockIndex = allBlocks.findIndex((b) => b.id === block.id);
|
|
219
|
-
if (blockIndex < 0) return false;
|
|
220
|
-
|
|
221
|
-
for (let i = blockIndex - 1; i >= 0; i--) {
|
|
222
|
-
const b = allBlocks[i];
|
|
223
|
-
if (b.type === "testStep" || b.type === "snippet" || isEmptyParagraph(b)) {
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (b.type === "heading") {
|
|
227
|
-
const text = (Array.isArray(b.content) ? b.content : [])
|
|
228
|
-
.filter((n: any) => n.type === "text")
|
|
229
|
-
.map((n: any) => n.text ?? "")
|
|
230
|
-
.join("")
|
|
231
|
-
.trim()
|
|
232
|
-
.toLowerCase()
|
|
233
|
-
.replace(/[:\-–—]$/, "");
|
|
234
|
-
return isStepsHeading(text);
|
|
235
|
-
}
|
|
236
|
-
return false;
|
|
237
|
-
}
|
|
238
|
-
return false;
|
|
239
|
-
}, [block.id, documentVersion, editor.document]);
|
|
240
|
-
|
|
241
215
|
useEditorChange(() => {
|
|
242
216
|
setDocumentVersion((version) => version + 1);
|
|
243
217
|
}, editor);
|
|
@@ -417,17 +391,6 @@ export const stepBlock = createReactBlockSpec(
|
|
|
417
391
|
const canToggleData = !dataHasContent;
|
|
418
392
|
const canToggleExpected = !expectedHasContent;
|
|
419
393
|
|
|
420
|
-
// Render as plain text when not under a "Steps" heading
|
|
421
|
-
if (!hasStepsHeading) {
|
|
422
|
-
return (
|
|
423
|
-
<div className="bn-teststep-plain" data-block-id={block.id}>
|
|
424
|
-
<span>{stepTitle || "(empty step)"}</span>
|
|
425
|
-
{stepData ? <span className="bn-teststep-plain__data">{stepData}</span> : null}
|
|
426
|
-
{expectedResult ? <span className="bn-teststep-plain__expected">{expectedResult}</span> : null}
|
|
427
|
-
</div>
|
|
428
|
-
);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
394
|
if (viewMode === "horizontal") {
|
|
432
395
|
return (
|
|
433
396
|
<StepHorizontalView
|
|
@@ -105,6 +105,67 @@ describe("blocksToMarkdown", () => {
|
|
|
105
105
|
);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
it("serializes a step with empty title but with stepData", () => {
|
|
109
|
+
const blocks: CustomEditorBlock[] = [
|
|
110
|
+
{
|
|
111
|
+
id: "s1",
|
|
112
|
+
type: "testStep",
|
|
113
|
+
props: {
|
|
114
|
+
stepTitle: "",
|
|
115
|
+
stepData: "Navigate to the page",
|
|
116
|
+
expectedResult: "",
|
|
117
|
+
listStyle: "bullet",
|
|
118
|
+
},
|
|
119
|
+
content: undefined,
|
|
120
|
+
children: [],
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
expect(blocksToMarkdown(blocks)).toBe(
|
|
125
|
+
["* ", " Navigate to the page"].join("\n"),
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("serializes a step with empty title but with expectedResult", () => {
|
|
130
|
+
const blocks: CustomEditorBlock[] = [
|
|
131
|
+
{
|
|
132
|
+
id: "s1",
|
|
133
|
+
type: "testStep",
|
|
134
|
+
props: {
|
|
135
|
+
stepTitle: "",
|
|
136
|
+
stepData: "",
|
|
137
|
+
expectedResult: "Login form visible",
|
|
138
|
+
listStyle: "bullet",
|
|
139
|
+
},
|
|
140
|
+
content: undefined,
|
|
141
|
+
children: [],
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
expect(blocksToMarkdown(blocks)).toBe(
|
|
146
|
+
["* ", " *Expected*: Login form visible"].join("\n"),
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it("drops completely empty step (no title, no data, no expected)", () => {
|
|
151
|
+
const blocks: CustomEditorBlock[] = [
|
|
152
|
+
{
|
|
153
|
+
id: "s1",
|
|
154
|
+
type: "testStep",
|
|
155
|
+
props: {
|
|
156
|
+
stepTitle: "",
|
|
157
|
+
stepData: "",
|
|
158
|
+
expectedResult: "",
|
|
159
|
+
listStyle: "bullet",
|
|
160
|
+
},
|
|
161
|
+
content: undefined,
|
|
162
|
+
children: [],
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
expect(blocksToMarkdown(blocks)).toBe("");
|
|
167
|
+
});
|
|
168
|
+
|
|
108
169
|
it("serializes a snippet block with prefixed title", () => {
|
|
109
170
|
const blocks: CustomEditorBlock[] = [
|
|
110
171
|
{
|
|
@@ -382,6 +443,62 @@ describe("blocksToMarkdown", () => {
|
|
|
382
443
|
);
|
|
383
444
|
});
|
|
384
445
|
|
|
446
|
+
it("serializes table cells with styled text and newlines without trapping <br/> inside markers", () => {
|
|
447
|
+
const blocks: CustomEditorBlock[] = [
|
|
448
|
+
{
|
|
449
|
+
id: "tbl3",
|
|
450
|
+
type: "table",
|
|
451
|
+
props: { textColor: "default" },
|
|
452
|
+
content: {
|
|
453
|
+
type: "tableContent",
|
|
454
|
+
columnWidths: [undefined, undefined],
|
|
455
|
+
headerRows: 1,
|
|
456
|
+
rows: [
|
|
457
|
+
{
|
|
458
|
+
cells: [
|
|
459
|
+
{
|
|
460
|
+
type: "tableCell",
|
|
461
|
+
props: cellProps,
|
|
462
|
+
content: [{ type: "text", text: "Col A", styles: {} }],
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
type: "tableCell",
|
|
466
|
+
props: cellProps,
|
|
467
|
+
content: [{ type: "text", text: "Col B", styles: {} }],
|
|
468
|
+
},
|
|
469
|
+
],
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
cells: [
|
|
473
|
+
{
|
|
474
|
+
type: "tableCell",
|
|
475
|
+
props: cellProps,
|
|
476
|
+
content: [{ type: "text", text: "ok", styles: {} }],
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
type: "tableCell",
|
|
480
|
+
props: cellProps,
|
|
481
|
+
content: [
|
|
482
|
+
{ type: "text", text: "opened\nnewline", styles: { bold: true } },
|
|
483
|
+
],
|
|
484
|
+
},
|
|
485
|
+
],
|
|
486
|
+
},
|
|
487
|
+
],
|
|
488
|
+
},
|
|
489
|
+
children: [],
|
|
490
|
+
},
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
expect(blocksToMarkdown(blocks)).toBe(
|
|
494
|
+
[
|
|
495
|
+
"| Col A | Col B |",
|
|
496
|
+
"| --- | --- |",
|
|
497
|
+
"| ok | **opened**<br/>**newline** |",
|
|
498
|
+
].join("\n"),
|
|
499
|
+
);
|
|
500
|
+
});
|
|
501
|
+
|
|
385
502
|
it("parses a test step with inline image in the title, moving the image to step data", () => {
|
|
386
503
|
const markdown = [
|
|
387
504
|
"## Steps",
|
|
@@ -419,6 +536,55 @@ describe("markdownToBlocks", () => {
|
|
|
419
536
|
]);
|
|
420
537
|
});
|
|
421
538
|
|
|
539
|
+
it("parses a step with empty title but with step data", () => {
|
|
540
|
+
const markdown = ["* ", " Navigate to the page"].join("\n");
|
|
541
|
+
|
|
542
|
+
expect(markdownToBlocks(markdown)).toEqual([
|
|
543
|
+
{
|
|
544
|
+
type: "testStep",
|
|
545
|
+
props: {
|
|
546
|
+
stepTitle: "",
|
|
547
|
+
stepData: "Navigate to the page",
|
|
548
|
+
expectedResult: "",
|
|
549
|
+
listStyle: "bullet",
|
|
550
|
+
},
|
|
551
|
+
children: [],
|
|
552
|
+
},
|
|
553
|
+
]);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
it("round-trips a title-less step with data", () => {
|
|
557
|
+
const blocks: CustomEditorBlock[] = [
|
|
558
|
+
{
|
|
559
|
+
id: "s1",
|
|
560
|
+
type: "testStep",
|
|
561
|
+
props: {
|
|
562
|
+
stepTitle: "",
|
|
563
|
+
stepData: "Open the browser",
|
|
564
|
+
expectedResult: "Page loads",
|
|
565
|
+
listStyle: "bullet",
|
|
566
|
+
},
|
|
567
|
+
content: undefined,
|
|
568
|
+
children: [],
|
|
569
|
+
},
|
|
570
|
+
];
|
|
571
|
+
|
|
572
|
+
const md = blocksToMarkdown(blocks);
|
|
573
|
+
const parsed = markdownToBlocks(md);
|
|
574
|
+
expect(parsed).toEqual([
|
|
575
|
+
{
|
|
576
|
+
type: "testStep",
|
|
577
|
+
props: {
|
|
578
|
+
stepTitle: "",
|
|
579
|
+
stepData: "Open the browser",
|
|
580
|
+
expectedResult: "Page loads",
|
|
581
|
+
listStyle: "bullet",
|
|
582
|
+
},
|
|
583
|
+
children: [],
|
|
584
|
+
},
|
|
585
|
+
]);
|
|
586
|
+
});
|
|
587
|
+
|
|
422
588
|
it("parses snippet markdown into snippet blocks", () => {
|
|
423
589
|
const markdown = [
|
|
424
590
|
"<!-- begin snippet #501 -->",
|
|
@@ -174,12 +174,21 @@ function applyTextStyles(text: string, styles: EditorStyles | undefined): string
|
|
|
174
174
|
});
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
177
|
+
// Split on newlines so that style markers wrap each line individually.
|
|
178
|
+
// This prevents <br/> (inserted by table cell formatting) from being
|
|
179
|
+
// trapped inside markers like **bold<br/>text**.
|
|
180
|
+
const segments = result.split("\n");
|
|
181
|
+
const wrapped = segments.map((segment) => {
|
|
182
|
+
if (!segment) return segment;
|
|
183
|
+
let s = segment;
|
|
184
|
+
for (const wrapper of wrappers) {
|
|
185
|
+
const suffix = wrapper.suffix ?? wrapper.prefix;
|
|
186
|
+
s = `${wrapper.prefix}${s}${suffix}`;
|
|
187
|
+
}
|
|
188
|
+
return s;
|
|
189
|
+
});
|
|
181
190
|
|
|
182
|
-
return
|
|
191
|
+
return wrapped.join("\n");
|
|
183
192
|
}
|
|
184
193
|
|
|
185
194
|
function inlineToMarkdown(content: CustomEditorBlock["content"]): string {
|
|
@@ -392,18 +401,19 @@ function serializeBlock(
|
|
|
392
401
|
return flattenWithBlankLine(lines, true);
|
|
393
402
|
}
|
|
394
403
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
404
|
+
const normalizedTitle = stepTitle
|
|
405
|
+
.split(/\r?\n/)
|
|
406
|
+
.map((segment: string) => segment.trim())
|
|
407
|
+
.filter((segment: string) => segment.length > 0)
|
|
408
|
+
.join(" ");
|
|
409
|
+
|
|
410
|
+
const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
|
|
411
|
+
const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
|
|
412
|
+
|
|
413
|
+
if (normalizedTitle.length > 0 || hasContent) {
|
|
414
|
+
const listStyle = (block.props as any).listStyle ?? "bullet";
|
|
415
|
+
const prefix = listStyle === "ordered" ? `${(stepIndex ?? 0) + 1}.` : "*";
|
|
416
|
+
lines.push(normalizedTitle.length > 0 ? `${prefix} ${normalizedTitle}` : `${prefix} `);
|
|
407
417
|
}
|
|
408
418
|
|
|
409
419
|
if (stepData.length > 0) {
|
|
@@ -734,7 +744,7 @@ function detectListType(trimmed: string): "bullet" | "numbered" | "check" | null
|
|
|
734
744
|
if (/^\d+[.)]\s+/.test(trimmed)) {
|
|
735
745
|
return "numbered";
|
|
736
746
|
}
|
|
737
|
-
if (/^[-*+]\s
|
|
747
|
+
if (/^[-*+](\s+|$)/.test(trimmed)) {
|
|
738
748
|
return "bullet";
|
|
739
749
|
}
|
|
740
750
|
return null;
|
|
@@ -850,7 +860,7 @@ function parseList(
|
|
|
850
860
|
});
|
|
851
861
|
} else {
|
|
852
862
|
const bulletMatch = trimmed.match(/^[-*+]\s+(.*)$/);
|
|
853
|
-
const text = bulletMatch?.[1] ?? trimmed.slice(2);
|
|
863
|
+
const text = bulletMatch?.[1] ?? (trimmed.length <= 1 ? "" : trimmed.slice(2));
|
|
854
864
|
items.push({
|
|
855
865
|
type: "bulletListItem",
|
|
856
866
|
props: cloneBaseProps(),
|
|
@@ -880,7 +890,7 @@ function isLikelyStep(lines: string[], index: number): boolean {
|
|
|
880
890
|
if (hasIndent) return true;
|
|
881
891
|
|
|
882
892
|
// Stop at new list items, headings, or other block-level elements (only if not indented)
|
|
883
|
-
if (/^[-*+]\s/.test(trimmed) || /^\d+[.)]\s/.test(trimmed)) break;
|
|
893
|
+
if (/^[-*+](\s|$)/.test(trimmed) || /^\d+[.)]\s/.test(trimmed)) break;
|
|
884
894
|
if (trimmed.startsWith("#") || trimmed.startsWith(">") || trimmed.startsWith("|") || trimmed.startsWith("```") || trimmed.startsWith(":::")) break;
|
|
885
895
|
|
|
886
896
|
// Check for expected result markers
|
|
@@ -899,7 +909,7 @@ function parseTestStep(
|
|
|
899
909
|
): { block: CustomPartialBlock; nextIndex: number } | null {
|
|
900
910
|
const current = lines[index];
|
|
901
911
|
const trimmed = current.trim();
|
|
902
|
-
const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ");
|
|
912
|
+
const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ") || trimmed === "*" || trimmed === "-";
|
|
903
913
|
const isNumbered = /^\d+[.)]\s+/.test(trimmed);
|
|
904
914
|
|
|
905
915
|
if (!isBullet && !isNumbered) {
|
|
@@ -915,7 +925,9 @@ function parseTestStep(
|
|
|
915
925
|
|
|
916
926
|
let rawTitle: string;
|
|
917
927
|
if (isBullet) {
|
|
918
|
-
rawTitle = unescapeMarkdown(
|
|
928
|
+
rawTitle = unescapeMarkdown(
|
|
929
|
+
(trimmed.startsWith("* ") || trimmed.startsWith("- ")) ? trimmed.slice(2) : trimmed.slice(1)
|
|
930
|
+
).trim();
|
|
919
931
|
} else {
|
|
920
932
|
// For numbered lists, remove the number and delimiter
|
|
921
933
|
rawTitle = unescapeMarkdown(trimmed.replace(/^\d+[.)]\s+/, "")).trim();
|
|
@@ -963,7 +975,7 @@ function parseTestStep(
|
|
|
963
975
|
}
|
|
964
976
|
const isNumberedStep = NUMBERED_STEP_REGEX.test(rawTrimmed);
|
|
965
977
|
const isNewStep =
|
|
966
|
-
(!hasIndent && (rawTrimmed.startsWith("* ") || rawTrimmed.startsWith("- "))) ||
|
|
978
|
+
(!hasIndent && (rawTrimmed.startsWith("* ") || rawTrimmed.startsWith("- ") || rawTrimmed === "*" || rawTrimmed === "-")) ||
|
|
967
979
|
(!hasIndent && isNumberedStep);
|
|
968
980
|
|
|
969
981
|
if (isNewStep) {
|