testomatio-editor-blocks 0.4.61 → 0.4.63

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.
@@ -93,13 +93,8 @@ export function canInsertStepOrSnippet(editor, referenceBlockId) {
93
93
  * Returns the inserted step's block ID (for focusing), or null.
94
94
  */
95
95
  export function addStepsBlock(editor) {
96
- var _a, _b, _c, _d;
96
+ var _a, _b, _c, _d, _e, _f;
97
97
  const allBlocks = editor.document;
98
- const emptyStep = {
99
- type: "testStep",
100
- props: { stepTitle: "", stepData: "", expectedResult: "" },
101
- children: [],
102
- };
103
98
  let stepsHeadingIndex = -1;
104
99
  for (let i = 0; i < allBlocks.length; i++) {
105
100
  const b = allBlocks[i];
@@ -129,8 +124,17 @@ export function addStepsBlock(editor) {
129
124
  continue;
130
125
  break;
131
126
  }
132
- const inserted = editor.insertBlocks([emptyStep], allBlocks[lastIndex].id, "after");
133
- return (_b = (_a = inserted === null || inserted === void 0 ? void 0 : inserted[0]) === null || _a === void 0 ? void 0 : _a.id) !== null && _b !== void 0 ? _b : null;
127
+ const previousStep = allBlocks[lastIndex];
128
+ const inheritedListStyle = (previousStep === null || previousStep === void 0 ? void 0 : previousStep.type) === "testStep"
129
+ ? ((_b = (_a = previousStep.props) === null || _a === void 0 ? void 0 : _a.listStyle) !== null && _b !== void 0 ? _b : "bullet")
130
+ : "bullet";
131
+ const emptyStep = {
132
+ type: "testStep",
133
+ props: { stepTitle: "", stepData: "", expectedResult: "", listStyle: inheritedListStyle },
134
+ children: [],
135
+ };
136
+ const inserted = editor.insertBlocks([emptyStep], previousStep.id, "after");
137
+ return (_d = (_c = inserted === null || inserted === void 0 ? void 0 : inserted[0]) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
134
138
  }
135
139
  const lastBlock = allBlocks[allBlocks.length - 1];
136
140
  const stepsHeading = {
@@ -139,8 +143,13 @@ export function addStepsBlock(editor) {
139
143
  content: [{ type: "text", text: "Steps" }],
140
144
  children: [],
141
145
  };
146
+ const emptyStep = {
147
+ type: "testStep",
148
+ props: { stepTitle: "", stepData: "", expectedResult: "" },
149
+ children: [],
150
+ };
142
151
  const inserted = editor.insertBlocks([stepsHeading, emptyStep], lastBlock.id, "after");
143
- return (_d = (_c = inserted === null || inserted === void 0 ? void 0 : inserted[1]) === null || _c === void 0 ? void 0 : _c.id) !== null && _d !== void 0 ? _d : null;
152
+ return (_f = (_e = inserted === null || inserted === void 0 ? void 0 : inserted[1]) === null || _e === void 0 ? void 0 : _e.id) !== null && _f !== void 0 ? _f : null;
144
153
  }
145
154
  /**
146
155
  * Programmatically add an empty snippet block to the editor.
@@ -346,12 +355,14 @@ export const stepBlock = createReactBlockSpec({
346
355
  });
347
356
  }, [editor, block.id, expectedResult]);
348
357
  const handleInsertNextStep = useCallback(() => {
358
+ var _a;
349
359
  const allBlocks = editor.document;
350
360
  const idx = allBlocks.findIndex((b) => b.id === block.id);
351
361
  const next = idx >= 0 ? allBlocks[idx + 1] : null;
352
362
  if (next && isEmptyParagraph(next)) {
353
363
  editor.removeBlocks([next.id]);
354
364
  }
365
+ const currentListStyle = (_a = block.props.listStyle) !== null && _a !== void 0 ? _a : "bullet";
355
366
  editor.insertBlocks([
356
367
  {
357
368
  type: "testStep",
@@ -359,11 +370,12 @@ export const stepBlock = createReactBlockSpec({
359
370
  stepTitle: "",
360
371
  stepData: "",
361
372
  expectedResult: "",
373
+ listStyle: currentListStyle,
362
374
  },
363
375
  children: [],
364
376
  },
365
377
  ], block.id, "after");
366
- }, [editor, block.id]);
378
+ }, [editor, block.id, block.props]);
367
379
  const handleFieldFocus = useCallback(() => {
368
380
  var _a, _b, _c;
369
381
  const selection = editor.getSelection();
@@ -698,6 +698,11 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
698
698
  var _a, _b, _c, _d;
699
699
  const items = [];
700
700
  let index = startIndex;
701
+ // The minimum leading-space count for items at this list level. Initialized to
702
+ // the parent's expected indent, but updated to the first item's actual indent
703
+ // so a uniformly indented list stays flat instead of nesting under itself.
704
+ let baseIndent = indentLevel * 2;
705
+ let firstItemSeen = false;
701
706
  while (index < lines.length) {
702
707
  const rawLine = lines[index];
703
708
  const trimmed = rawLine.trim();
@@ -715,7 +720,7 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
715
720
  }
716
721
  const nextLine = lines[lookahead];
717
722
  const nextIndent = countIndent(nextLine);
718
- if (nextIndent < indentLevel * 2) {
723
+ if (nextIndent < baseIndent) {
719
724
  break;
720
725
  }
721
726
  const nextType = detectListType(nextLine.trim());
@@ -726,13 +731,15 @@ function parseList(lines, startIndex, listType, indentLevel, allowEmptySteps = f
726
731
  continue;
727
732
  }
728
733
  let indent = countIndent(rawLine);
729
- if (indent < indentLevel * 2) {
734
+ if (indent < baseIndent) {
730
735
  break;
731
736
  }
732
- // Check if this line should be parsed as nested content
733
- // Only go deeper if indent is at least 2 more than the next level's expected indent
734
- const nextLevelExpectedIndent = (indentLevel + 1) * 2;
735
- if (indent >= nextLevelExpectedIndent && items.length > 0) {
737
+ if (!firstItemSeen) {
738
+ baseIndent = indent;
739
+ firstItemSeen = true;
740
+ }
741
+ // Only go deeper if indent is at least 2 more than this list's base indent
742
+ if (indent >= baseIndent + 2 && items.length > 0) {
736
743
  const lastItem = items.at(-1);
737
744
  if (!lastItem) {
738
745
  break;
@@ -880,11 +887,18 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
880
887
  rawTrimmed.startsWith("|")) {
881
888
  break;
882
889
  }
883
- // Check for expected result labels with different formatting
884
- const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
885
- const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
886
- rawTrimmed.match(/^\*expected\*:\s*(.*)$/i) ||
887
- rawTrimmed.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
890
+ // Check for expected result labels with different formatting.
891
+ // Strip an optional leading bullet marker so patterns like
892
+ // "* *Expected*: ..." (a sub-bullet with an italic Expected label) are
893
+ // recognized via the same matchers as the bare label forms.
894
+ const bulletPrefixMatch = rawTrimmed.match(/^[*-]\s+/);
895
+ const lineForExpectedCheck = bulletPrefixMatch
896
+ ? rawTrimmed.slice(bulletPrefixMatch[0].length)
897
+ : rawTrimmed;
898
+ const expectedMatch = lineForExpectedCheck.match(EXPECTED_LABEL_REGEX);
899
+ const expectedStarMatch = lineForExpectedCheck.match(/^\*expected\s*\*:\s*(.*)$/i) ||
900
+ lineForExpectedCheck.match(/^\*expected\*:\s*(.*)$/i) ||
901
+ lineForExpectedCheck.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
888
902
  if (expectedMatch || expectedStarMatch) {
889
903
  inExpectedResult = true;
890
904
  // Prefer the star match (more specific about formatting) to avoid leaking markers
@@ -893,7 +907,7 @@ function parseTestStep(lines, index, allowEmpty = false, snippetId) {
893
907
  content = (expectedStarMatch[1] || '').trim();
894
908
  }
895
909
  else {
896
- content = rawTrimmed.slice(expectedMatch[0].length).trim();
910
+ content = lineForExpectedCheck.slice(expectedMatch[0].length).trim();
897
911
  }
898
912
  // Add the content (if any) from this line
899
913
  if (content) {
@@ -1057,7 +1071,23 @@ function parseCodeBlock(lines, index) {
1057
1071
  if (!trimmed.startsWith("```")) {
1058
1072
  return null;
1059
1073
  }
1060
- const language = trimmed.slice(3).trim();
1074
+ const afterOpening = trimmed.slice(3);
1075
+ const closeMatch = afterOpening.match(/```\s*$/);
1076
+ if (closeMatch) {
1077
+ const content = afterOpening.slice(0, afterOpening.length - closeMatch[0].length);
1078
+ return {
1079
+ block: {
1080
+ type: "codeBlock",
1081
+ props: { language: "" },
1082
+ content: content.length
1083
+ ? [{ type: "text", text: content, styles: {} }]
1084
+ : undefined,
1085
+ children: [],
1086
+ },
1087
+ nextIndex: index + 1,
1088
+ };
1089
+ }
1090
+ const language = afterOpening.trim();
1061
1091
  const body = [];
1062
1092
  let next = index + 1;
1063
1093
  while (next < lines.length && !lines[next].startsWith("```")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testomatio-editor-blocks",
3
- "version": "0.4.61",
3
+ "version": "0.4.63",
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",
@@ -108,11 +108,6 @@ export function addStepsBlock(editor: {
108
108
  insertBlocks: (blocks: any[], referenceId: string, placement: "before" | "after") => any[];
109
109
  }): string | null {
110
110
  const allBlocks = editor.document;
111
- const emptyStep = {
112
- type: "testStep" as const,
113
- props: { stepTitle: "", stepData: "", expectedResult: "" },
114
- children: [],
115
- };
116
111
 
117
112
  let stepsHeadingIndex = -1;
118
113
  for (let i = 0; i < allBlocks.length; i++) {
@@ -142,7 +137,17 @@ export function addStepsBlock(editor: {
142
137
  if (isEmptyParagraph(b)) continue;
143
138
  break;
144
139
  }
145
- const inserted = editor.insertBlocks([emptyStep], allBlocks[lastIndex].id, "after");
140
+ const previousStep = allBlocks[lastIndex];
141
+ const inheritedListStyle =
142
+ previousStep?.type === "testStep"
143
+ ? ((previousStep.props as any)?.listStyle ?? "bullet")
144
+ : "bullet";
145
+ const emptyStep = {
146
+ type: "testStep" as const,
147
+ props: { stepTitle: "", stepData: "", expectedResult: "", listStyle: inheritedListStyle },
148
+ children: [],
149
+ };
150
+ const inserted = editor.insertBlocks([emptyStep], previousStep.id, "after");
146
151
  return inserted?.[0]?.id ?? null;
147
152
  }
148
153
 
@@ -153,6 +158,11 @@ export function addStepsBlock(editor: {
153
158
  content: [{ type: "text" as const, text: "Steps" }],
154
159
  children: [],
155
160
  };
161
+ const emptyStep = {
162
+ type: "testStep" as const,
163
+ props: { stepTitle: "", stepData: "", expectedResult: "" },
164
+ children: [],
165
+ };
156
166
  const inserted = editor.insertBlocks([stepsHeading, emptyStep], lastBlock.id, "after");
157
167
  return inserted?.[1]?.id ?? null;
158
168
  }
@@ -399,6 +409,7 @@ export const stepBlock = createReactBlockSpec(
399
409
  if (next && isEmptyParagraph(next)) {
400
410
  editor.removeBlocks([next.id]);
401
411
  }
412
+ const currentListStyle = (block.props as any).listStyle ?? "bullet";
402
413
  editor.insertBlocks(
403
414
  [
404
415
  {
@@ -407,6 +418,7 @@ export const stepBlock = createReactBlockSpec(
407
418
  stepTitle: "",
408
419
  stepData: "",
409
420
  expectedResult: "",
421
+ listStyle: currentListStyle,
410
422
  },
411
423
  children: [],
412
424
  },
@@ -414,7 +426,7 @@ export const stepBlock = createReactBlockSpec(
414
426
  block.id,
415
427
  "after",
416
428
  );
417
- }, [editor, block.id]);
429
+ }, [editor, block.id, block.props]);
418
430
 
419
431
  const handleFieldFocus = useCallback(() => {
420
432
  const selection = editor.getSelection();
@@ -849,6 +849,86 @@ describe("markdownToBlocks", () => {
849
849
  ]);
850
850
  });
851
851
 
852
+ it("parses sub-bulleted *Expected*: lines as the step's expected result", () => {
853
+ const markdown = [
854
+ "### Steps",
855
+ "",
856
+ "* Navigate to the “Transfer Funds” page.",
857
+ " * *Expected*: The Transfer Funds page loads, showing fields for source account, destination account, and amount.",
858
+ "",
859
+ "* Select the user’s own source account from the dropdown list.",
860
+ " * *Expected*: The selected account is displayed and its current balance is shown.",
861
+ ].join("\n");
862
+
863
+ const blocks = markdownToBlocks(markdown);
864
+ const stepBlocks = blocks.filter((b) => b.type === "testStep");
865
+ expect(stepBlocks).toEqual([
866
+ {
867
+ type: "testStep",
868
+ props: {
869
+ stepTitle: "Navigate to the “Transfer Funds” page.",
870
+ stepData: "",
871
+ expectedResult:
872
+ "The Transfer Funds page loads, showing fields for source account, destination account, and amount.\n",
873
+ listStyle: "bullet",
874
+ },
875
+ children: [],
876
+ },
877
+ {
878
+ type: "testStep",
879
+ props: {
880
+ stepTitle: "Select the user’s own source account from the dropdown list.",
881
+ stepData: "",
882
+ expectedResult:
883
+ "The selected account is displayed and its current balance is shown.",
884
+ listStyle: "bullet",
885
+ },
886
+ children: [],
887
+ },
888
+ ]);
889
+ });
890
+
891
+ it("parses sub-bulleted **Expected**: (bold) lines as the step's expected result", () => {
892
+ const markdown = [
893
+ "### Steps",
894
+ "",
895
+ "* Open the Login page.",
896
+ " * **Expected**: The Login page loads successfully.",
897
+ ].join("\n");
898
+
899
+ const blocks = markdownToBlocks(markdown);
900
+ const stepBlocks = blocks.filter((b) => b.type === "testStep");
901
+ expect(stepBlocks).toEqual([
902
+ {
903
+ type: "testStep",
904
+ props: {
905
+ stepTitle: "Open the Login page.",
906
+ stepData: "",
907
+ expectedResult: "The Login page loads successfully.",
908
+ listStyle: "bullet",
909
+ },
910
+ children: [],
911
+ },
912
+ ]);
913
+ });
914
+
915
+ it("round-trips sub-bulleted *Expected*: lines through the canonical serialized form", () => {
916
+ const markdown = [
917
+ "### Steps",
918
+ "",
919
+ "* Navigate to the “Transfer Funds” page.",
920
+ " * *Expected*: The Transfer Funds page loads.",
921
+ ].join("\n");
922
+
923
+ const firstPass = markdownToBlocks(markdown);
924
+ const serialized = blocksToMarkdown(firstPass as CustomEditorBlock[]);
925
+ const secondPass = markdownToBlocks(serialized);
926
+
927
+ const firstSteps = firstPass.filter((b) => b.type === "testStep");
928
+ const secondSteps = secondPass.filter((b) => b.type === "testStep");
929
+ expect(secondSteps).toEqual(firstSteps);
930
+ });
931
+
852
932
  it("parses a step with empty title but with step data", () => {
853
933
  const markdown = ["### Steps", "", "* ", " Navigate to the page"].join("\n");
854
934
 
@@ -1332,6 +1412,25 @@ describe("markdownToBlocks", () => {
1332
1412
  expect(nestedChildren.some((child) => child.type === "bulletListItem")).toBe(true);
1333
1413
  });
1334
1414
 
1415
+ it("parses a uniformly indented list as a flat top-level list", () => {
1416
+ const markdown = [
1417
+ "# Requirements",
1418
+ "",
1419
+ " * User has an active account on the platform.",
1420
+ " * User has sufficient funds in the source account.",
1421
+ " * QR code contains valid transfer details and is scannable.",
1422
+ " * The device has camera access and QR scanning capability.",
1423
+ " * The user is authenticated and authorized to perform transfers.",
1424
+ ].join("\n");
1425
+
1426
+ const blocks = markdownToBlocks(markdown);
1427
+ const bulletItems = blocks.filter((b) => b.type === "bulletListItem");
1428
+ expect(bulletItems).toHaveLength(5);
1429
+ for (const item of bulletItems) {
1430
+ expect(item.children ?? []).toEqual([]);
1431
+ }
1432
+ });
1433
+
1335
1434
  it("does not freeze on indented list items without a parent", () => {
1336
1435
  const markdown = [
1337
1436
  "### Requirements",
@@ -1597,6 +1696,36 @@ describe("markdownToBlocks", () => {
1597
1696
  expect(stepBlocks[1].props).toMatchObject({ stepTitle: "Second step", listStyle: "ordered" });
1598
1697
  });
1599
1698
 
1699
+ it("serializes a new ordered step appended to a numbered list as N.", () => {
1700
+ const orderedSteps: CustomEditorBlock[] = [
1701
+ {
1702
+ id: "s1",
1703
+ type: "testStep",
1704
+ props: { stepTitle: "First step", stepData: "", expectedResult: "", listStyle: "ordered" },
1705
+ content: undefined as any,
1706
+ children: [],
1707
+ } as any,
1708
+ {
1709
+ id: "s2",
1710
+ type: "testStep",
1711
+ props: { stepTitle: "Second step", stepData: "", expectedResult: "", listStyle: "ordered" },
1712
+ content: undefined as any,
1713
+ children: [],
1714
+ } as any,
1715
+ {
1716
+ id: "s3",
1717
+ type: "testStep",
1718
+ props: { stepTitle: "Newly added step", stepData: "", expectedResult: "", listStyle: "ordered" },
1719
+ content: undefined as any,
1720
+ children: [],
1721
+ } as any,
1722
+ ];
1723
+
1724
+ expect(blocksToMarkdown(orderedSteps)).toBe(
1725
+ ["1. First step", "2. Second step", "3. Newly added step"].join("\n"),
1726
+ );
1727
+ });
1728
+
1600
1729
  it("parses steps under an h4 'step' heading (lowercase)", () => {
1601
1730
  const markdown = ["#### step", "", "* Do something"].join("\n");
1602
1731
  const blocks = markdownToBlocks(markdown);
@@ -2503,6 +2632,59 @@ describe("markdownToBlocks", () => {
2503
2632
  // Most importantly: should not have a standalone "!" at the end
2504
2633
  expect(roundTripMarkdown).not.toMatch(/\n!\s*$/);
2505
2634
  });
2635
+
2636
+ it("parses a single-line fenced code block", () => {
2637
+ const markdown = "```{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two```";
2638
+ const blocks = markdownToBlocks(markdown);
2639
+ expect(blocks).toEqual([
2640
+ {
2641
+ type: "codeBlock",
2642
+ props: { language: "" },
2643
+ content: [
2644
+ {
2645
+ type: "text",
2646
+ text: "{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two",
2647
+ styles: {},
2648
+ },
2649
+ ],
2650
+ children: [],
2651
+ },
2652
+ ]);
2653
+ });
2654
+
2655
+ it("parses an empty single-line fence without swallowing following lines", () => {
2656
+ const markdown = ["``````", "next line"].join("\n");
2657
+ const blocks = markdownToBlocks(markdown);
2658
+ expect(blocks).toHaveLength(2);
2659
+ expect(blocks[0]).toEqual({
2660
+ type: "codeBlock",
2661
+ props: { language: "" },
2662
+ content: undefined,
2663
+ children: [],
2664
+ });
2665
+ expect(blocks[1].type).toBe("paragraph");
2666
+ });
2667
+
2668
+ it("normalizes a single-line fenced code block to multi-line on round-trip", () => {
2669
+ const markdown = "```hello world```";
2670
+ const blocks = markdownToBlocks(markdown);
2671
+ expect(blocksToMarkdown(blocks as CustomEditorBlock[])).toBe(
2672
+ ["```", "hello world", "```"].join("\n"),
2673
+ );
2674
+ });
2675
+
2676
+ it("still treats an opening fence with a language identifier as multi-line", () => {
2677
+ const markdown = ["```js", "const x = 1;", "```"].join("\n");
2678
+ const blocks = markdownToBlocks(markdown);
2679
+ expect(blocks).toEqual([
2680
+ {
2681
+ type: "codeBlock",
2682
+ props: { language: "js" },
2683
+ content: [{ type: "text", text: "const x = 1;", styles: {} }],
2684
+ children: [],
2685
+ },
2686
+ ]);
2687
+ });
2506
2688
  });
2507
2689
 
2508
2690
  describe("file block serialization", () => {
@@ -845,6 +845,11 @@ function parseList(
845
845
  ): ListParseResult {
846
846
  const items: CustomPartialBlock[] = [];
847
847
  let index = startIndex;
848
+ // The minimum leading-space count for items at this list level. Initialized to
849
+ // the parent's expected indent, but updated to the first item's actual indent
850
+ // so a uniformly indented list stays flat instead of nesting under itself.
851
+ let baseIndent = indentLevel * 2;
852
+ let firstItemSeen = false;
848
853
 
849
854
  while (index < lines.length) {
850
855
  const rawLine = lines[index];
@@ -864,7 +869,7 @@ function parseList(
864
869
  }
865
870
  const nextLine = lines[lookahead];
866
871
  const nextIndent = countIndent(nextLine);
867
- if (nextIndent < indentLevel * 2) {
872
+ if (nextIndent < baseIndent) {
868
873
  break;
869
874
  }
870
875
  const nextType = detectListType(nextLine.trim());
@@ -877,14 +882,17 @@ function parseList(
877
882
 
878
883
  let indent = countIndent(rawLine);
879
884
 
880
- if (indent < indentLevel * 2) {
885
+ if (indent < baseIndent) {
881
886
  break;
882
887
  }
883
888
 
884
- // Check if this line should be parsed as nested content
885
- // Only go deeper if indent is at least 2 more than the next level's expected indent
886
- const nextLevelExpectedIndent = (indentLevel + 1) * 2;
887
- if (indent >= nextLevelExpectedIndent && items.length > 0) {
889
+ if (!firstItemSeen) {
890
+ baseIndent = indent;
891
+ firstItemSeen = true;
892
+ }
893
+
894
+ // Only go deeper if indent is at least 2 more than this list's base indent
895
+ if (indent >= baseIndent + 2 && items.length > 0) {
888
896
  const lastItem = items.at(-1);
889
897
  if (!lastItem) {
890
898
  break;
@@ -1063,11 +1071,18 @@ function parseTestStep(
1063
1071
  break;
1064
1072
  }
1065
1073
 
1066
- // Check for expected result labels with different formatting
1067
- const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
1068
- const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
1069
- rawTrimmed.match(/^\*expected\*:\s*(.*)$/i) ||
1070
- rawTrimmed.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
1074
+ // Check for expected result labels with different formatting.
1075
+ // Strip an optional leading bullet marker so patterns like
1076
+ // "* *Expected*: ..." (a sub-bullet with an italic Expected label) are
1077
+ // recognized via the same matchers as the bare label forms.
1078
+ const bulletPrefixMatch = rawTrimmed.match(/^[*-]\s+/);
1079
+ const lineForExpectedCheck = bulletPrefixMatch
1080
+ ? rawTrimmed.slice(bulletPrefixMatch[0].length)
1081
+ : rawTrimmed;
1082
+ const expectedMatch = lineForExpectedCheck.match(EXPECTED_LABEL_REGEX);
1083
+ const expectedStarMatch = lineForExpectedCheck.match(/^\*expected\s*\*:\s*(.*)$/i) ||
1084
+ lineForExpectedCheck.match(/^\*expected\*:\s*(.*)$/i) ||
1085
+ lineForExpectedCheck.match(/^\*{1,2}expected\s*:\*{1,2}\s*(.*)$/i);
1071
1086
 
1072
1087
  if (expectedMatch || expectedStarMatch) {
1073
1088
  inExpectedResult = true;
@@ -1076,7 +1091,7 @@ function parseTestStep(
1076
1091
  if (expectedStarMatch) {
1077
1092
  content = (expectedStarMatch[1] || '').trim();
1078
1093
  } else {
1079
- content = rawTrimmed.slice(expectedMatch![0].length).trim();
1094
+ content = lineForExpectedCheck.slice(expectedMatch![0].length).trim();
1080
1095
  }
1081
1096
 
1082
1097
  // Add the content (if any) from this line
@@ -1258,7 +1273,24 @@ function parseCodeBlock(lines: string[], index: number): { block: CustomPartialB
1258
1273
  return null;
1259
1274
  }
1260
1275
 
1261
- const language = trimmed.slice(3).trim();
1276
+ const afterOpening = trimmed.slice(3);
1277
+ const closeMatch = afterOpening.match(/```\s*$/);
1278
+ if (closeMatch) {
1279
+ const content = afterOpening.slice(0, afterOpening.length - closeMatch[0].length);
1280
+ return {
1281
+ block: {
1282
+ type: "codeBlock",
1283
+ props: { language: "" },
1284
+ content: content.length
1285
+ ? [{ type: "text", text: content, styles: {} }]
1286
+ : undefined,
1287
+ children: [],
1288
+ },
1289
+ nextIndex: index + 1,
1290
+ };
1291
+ }
1292
+
1293
+ const language = afterOpening.trim();
1262
1294
  const body: string[] = [];
1263
1295
  let next = index + 1;
1264
1296
  while (next < lines.length && !lines[next].startsWith("```") ) {