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.
@@ -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
- for (const wrapper of wrappers) {
120
- const suffix = (_a = wrapper.suffix) !== null && _a !== void 0 ? _a : wrapper.prefix;
121
- result = `${wrapper.prefix}${result}${suffix}`;
122
- }
123
- return result;
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
- if (stepTitle.length > 0) {
313
- const normalizedTitle = stepTitle
314
- .split(/\r?\n/)
315
- .map((segment) => segment.trim())
316
- .filter((segment) => segment.length > 0)
317
- .join(" ");
318
- if (normalizedTitle.length > 0) {
319
- const listStyle = (_m = block.props.listStyle) !== null && _m !== void 0 ? _m : "bullet";
320
- const prefix = listStyle === "ordered" ? `${(stepIndex !== null && stepIndex !== void 0 ? stepIndex : 0) + 1}.` : "*";
321
- lines.push(`${prefix} ${normalizedTitle}`);
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+/.test(trimmed)) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.35",
3
+ "version": "0.4.36",
4
4
  "description": "Custom BlockNote schema, markdown conversion helpers, and UI for Testomatio-style test cases and steps.",
5
5
  "type": "module",
6
6
  "main": "./package/index.js",
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
- const cursorBlock = editor.getTextCursorPosition()?.block;
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 || !canInsertStepOrSnippet(editor, 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
- for (const wrapper of wrappers) {
178
- const suffix = wrapper.suffix ?? wrapper.prefix;
179
- result = `${wrapper.prefix}${result}${suffix}`;
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 result;
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
- if (stepTitle.length > 0) {
396
- const normalizedTitle = stepTitle
397
- .split(/\r?\n/)
398
- .map((segment: string) => segment.trim())
399
- .filter((segment: string) => segment.length > 0)
400
- .join(" ");
401
-
402
- if (normalizedTitle.length > 0) {
403
- const listStyle = (block.props as any).listStyle ?? "bullet";
404
- const prefix = listStyle === "ordered" ? `${(stepIndex ?? 0) + 1}.` : "*";
405
- lines.push(`${prefix} ${normalizedTitle}`);
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+/.test(trimmed)) {
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(trimmed.slice(2)).trim();
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) {