testomatio-editor-blocks 0.3.0 → 0.4.1

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.
@@ -61,7 +61,7 @@ const headingPrefixes: Record<number, string> = {
61
61
  const SPECIAL_CHAR_REGEX = /([*_`~\[\]()<>\\])/g;
62
62
  const HTML_SPAN_REGEX = /<\/?span[^>]*>/g;
63
63
  const HTML_UNDERLINE_REGEX = /<\/?u>/g;
64
- const EXPECTED_LABEL_REGEX = /^(?:[*_`]*\s*)?(expected(?:\s+result)?)\s*(?:[*_`]*\s*)?(?:\s*[:\-–—]?\s*)/i;
64
+ const EXPECTED_LABEL_REGEX = /^(?:[*_`]*\s*)?(expected(?:\s+result)?)\s*(?:[*_`]*\s*)?\s*[:\-–—]\s*/i;
65
65
  // Matches any non-empty line that falls between the step title and the expected result line.
66
66
  const STEP_DATA_LINE_REGEX =
67
67
  /^(?!\s*(?:[*_`]*\s*)?(?:expected(?:\s+result)?)\b).+/i;
@@ -185,22 +185,62 @@ function inlineToMarkdown(content: CustomEditorBlock["content"]): string {
185
185
  return "";
186
186
  }
187
187
 
188
- return (content as EditorInline[])
189
- .map((item) => {
190
- if (isStyledTextInlineContent(item)) {
191
- return applyTextStyles(escapeMarkdown(item.text), item.styles);
192
- }
188
+ const result: string[] = [];
189
+ let i = 0;
193
190
 
194
- if (isLinkInlineContent(item)) {
195
- const inner = inlineToMarkdown(item.content);
196
- const safeHref = escapeMarkdown(item.href);
197
- return `[${inner}](${safeHref})`;
191
+ while (i < content.length) {
192
+ const item = content[i];
193
+
194
+ if (isStyledTextInlineContent(item)) {
195
+ // Check if this is a "!" followed by a link (image syntax)
196
+ if (item.text === "!" && i + 1 < content.length && isLinkInlineContent(content[i + 1])) {
197
+ const link = content[i + 1] as any;
198
+ const inner = inlineToMarkdown(link.content);
199
+ const safeHref = escapeMarkdown(link.href);
200
+ result.push(`![${inner}](${safeHref})`);
201
+ i += 2;
202
+ continue;
198
203
  }
204
+ result.push(applyTextStyles(escapeMarkdown(item.text), item.styles));
205
+ i += 1;
206
+ continue;
207
+ }
199
208
 
200
- if (Array.isArray((item as any).content)) {
201
- return inlineToMarkdown((item as any).content);
202
- }
209
+ if (isLinkInlineContent(item)) {
210
+ const inner = inlineToMarkdown(item.content);
211
+ const safeHref = escapeMarkdown(item.href);
212
+ result.push(`[${inner}](${safeHref})`);
213
+ i += 1;
214
+ continue;
215
+ }
216
+
217
+ if (Array.isArray((item as any).content)) {
218
+ result.push(inlineToMarkdown((item as any).content));
219
+ i += 1;
220
+ continue;
221
+ }
222
+
223
+ i += 1;
224
+ }
203
225
 
226
+ return result.join("");
227
+ }
228
+
229
+ function inlineContentToPlainText(content: CustomEditorBlock["content"]): string {
230
+ if (!Array.isArray(content)) {
231
+ return "";
232
+ }
233
+
234
+ return (content as any[])
235
+ .map((node: any) => {
236
+ if (node && typeof node === "object") {
237
+ if (typeof node.text === "string") {
238
+ return node.text;
239
+ }
240
+ if (Array.isArray(node.content)) {
241
+ return inlineContentToPlainText(node.content);
242
+ }
243
+ }
204
244
  return "";
205
245
  })
206
246
  .join("");
@@ -593,10 +633,13 @@ function parseInlineMarkdown(text: string): EditorInline[] {
593
633
  pushPlain();
594
634
  const label = cleaned.slice(i + 1, endLabel);
595
635
  const href = cleaned.slice(startLink + 1, endLink);
636
+ const parsedLabel = parseInlineMarkdown(label);
637
+ // Ensure link content is never undefined - if empty, add empty text
638
+ const linkContent = parsedLabel.length > 0 ? parsedLabel : [{ type: "text", text: "", styles: {} }];
596
639
  result.push({
597
640
  type: "link",
598
641
  href: unescapeMarkdown(href),
599
- content: parseInlineMarkdown(label),
642
+ content: linkContent,
600
643
  } as any);
601
644
  i = endLink + 1;
602
645
  continue;
@@ -667,6 +710,7 @@ function parseList(
667
710
  startIndex: number,
668
711
  listType: "bullet" | "numbered" | "check",
669
712
  indentLevel: number,
713
+ allowEmptySteps = false,
670
714
  ): ListParseResult {
671
715
  const items: CustomPartialBlock[] = [];
672
716
  let index = startIndex;
@@ -681,15 +725,15 @@ function parseList(
681
725
  }
682
726
 
683
727
  let indent = countIndent(rawLine);
684
- const baseIndent = indentLevel * 2;
685
- if (indent > baseIndent && indent <= baseIndent + 1) {
686
- indent = baseIndent;
687
- }
728
+
688
729
  if (indent < indentLevel * 2) {
689
730
  break;
690
731
  }
691
732
 
692
- if (indent > indentLevel * 2) {
733
+ // Check if this line should be parsed as nested content
734
+ // Only go deeper if indent is at least 2 more than the next level's expected indent
735
+ const nextLevelExpectedIndent = (indentLevel + 1) * 2;
736
+ if (indent >= nextLevelExpectedIndent) {
693
737
  const lastItem = items.at(-1);
694
738
  if (!lastItem) {
695
739
  break;
@@ -698,7 +742,18 @@ function parseList(
698
742
  if (!nestedType) {
699
743
  break;
700
744
  }
701
- const nested = parseList(lines, index, nestedType, indentLevel + 1);
745
+ const nested = parseList(
746
+ lines,
747
+ index,
748
+ nestedType,
749
+ indentLevel + 1,
750
+ allowEmptySteps,
751
+ );
752
+ // If nested parsing made no progress, skip this line to avoid infinite loop
753
+ if (nested.nextIndex === index) {
754
+ index += 1;
755
+ continue;
756
+ }
702
757
  lastItem.children = [...(lastItem.children ?? []), ...nested.items];
703
758
  index = nested.nextIndex;
704
759
  continue;
@@ -709,6 +764,27 @@ function parseList(
709
764
  break;
710
765
  }
711
766
 
767
+ // Only try to parse as testStep for top-level items (indentLevel === 0)
768
+ // when we're under a Steps heading AND the list type is bullet
769
+ // Numbered lists under Steps heading are only parsed as test steps if they look like test steps
770
+ if (indentLevel === 0 && allowEmptySteps) {
771
+ // For bullet lists, always try to parse as test steps
772
+ // For numbered lists, only try if they have step-like characteristics
773
+ const looksLikeTestStep = listType === "bullet" ||
774
+ (listType === "numbered" && (
775
+ isLikelyStep(lines, index)
776
+ ));
777
+
778
+ if (looksLikeTestStep) {
779
+ const nextStep = parseTestStep(lines, index, allowEmptySteps);
780
+ if (nextStep) {
781
+ items.push(nextStep.block);
782
+ index = nextStep.nextIndex;
783
+ continue;
784
+ }
785
+ }
786
+ }
787
+
712
788
  if (listType === "check") {
713
789
  const checkMatch = trimmed.match(/^[-*+]\s+\[([xX\s])\]\s+(.*)$/);
714
790
  const checked = (checkMatch?.[1] ?? "").toLowerCase() === "x";
@@ -746,18 +822,56 @@ function parseList(
746
822
  return { items, nextIndex: index };
747
823
  }
748
824
 
825
+ function isLikelyStep(lines: string[], index: number): boolean {
826
+ // Look ahead to see if there's indented content or expected result
827
+ if (index + 1 >= lines.length) return false;
828
+
829
+ const nextLine = lines[index + 1];
830
+ const hasIndent = /^\s{2,}/.test(nextLine);
831
+
832
+ // Check if the next line contains expected result markers
833
+ const nextTrimmed = nextLine.trim();
834
+ const hasExpectedResult = EXPECTED_LABEL_REGEX.test(nextTrimmed);
835
+
836
+ // Only consider it a test step if:
837
+ // 1. It has an expected result, OR
838
+ // 2. The next line is indented but doesn't start with a numbered or bullet list
839
+ if (hasExpectedResult) return true;
840
+ if (hasIndent && !/^\d+[.)]/.test(nextTrimmed) && !/^[-*+]/.test(nextTrimmed)) return true;
841
+
842
+ return false;
843
+ }
844
+
749
845
  function parseTestStep(
750
846
  lines: string[],
751
847
  index: number,
848
+ allowEmpty = false,
752
849
  snippetId?: string,
753
850
  ): { block: CustomPartialBlock; nextIndex: number } | null {
754
851
  const current = lines[index];
755
852
  const trimmed = current.trim();
756
- if (!trimmed.startsWith("* ") && !trimmed.startsWith("- ")) {
853
+ const isBullet = trimmed.startsWith("* ") || trimmed.startsWith("- ");
854
+ const isNumbered = /^\d+[.)]\s+/.test(trimmed);
855
+
856
+ if (!isBullet && !isNumbered) {
757
857
  return null;
758
858
  }
759
859
 
760
- let rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
860
+ // For numbered lists, only parse as test steps if called from parseList with allowEmpty=true
861
+ // The first call to parseTestStep from markdownToBlocks uses allowEmpty=stepsHeadingLevel !== null
862
+ // which should be false unless we're under a Steps heading
863
+ if (isNumbered && !allowEmpty) {
864
+ return null;
865
+ }
866
+
867
+ let rawTitle: string;
868
+ if (isBullet) {
869
+ rawTitle = unescapeMarkdown(trimmed.slice(2)).trim();
870
+ } else {
871
+ // For numbered lists, remove the number and delimiter
872
+ rawTitle = unescapeMarkdown(trimmed.replace(/^\d+[.)]\s+/, "")).trim();
873
+ }
874
+
761
875
  let blockType: "testStep" | "snippet" = "testStep";
762
876
  const snippetMatch = rawTitle.match(/^snippet\s*[:\-–—]?\s*(.*)$/i);
763
877
  if (snippetMatch) {
@@ -788,8 +902,12 @@ function parseTestStep(
788
902
  const rawTrimmed = line.trim();
789
903
 
790
904
  if (!rawTrimmed) {
791
- if (stepDataLines.length > 0) {
792
- stepDataLines.push("");
905
+ if (stepDataLines.length > 0 || inExpectedResult) {
906
+ if (inExpectedResult) {
907
+ expectedResult += "\n";
908
+ } else {
909
+ stepDataLines.push("");
910
+ }
793
911
  }
794
912
  next += 1;
795
913
  continue;
@@ -812,21 +930,66 @@ function parseTestStep(
812
930
  break;
813
931
  }
814
932
 
815
- if (rawTrimmed.match(EXPECTED_LABEL_REGEX)) {
933
+ // Check for expected result labels with different formatting
934
+ const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
935
+ const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
936
+ rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
937
+
938
+ if (expectedMatch || expectedStarMatch) {
816
939
  inExpectedResult = true;
817
- const withoutLabel = stripExpectedPrefix(rawTrimmed);
818
- expectedResult = unescapeMarkdown(withoutLabel);
940
+ const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
941
+ let content = rawTrimmed.slice(label.length).trim();
942
+
943
+ // Add the content (if any) from this line
944
+ if (content) {
945
+ const expectedContent = unescapeMarkdown(content);
946
+ if (expectedResult.length > 0) {
947
+ expectedResult += "\n" + expectedContent;
948
+ } else {
949
+ expectedResult = expectedContent;
950
+ }
951
+ }
952
+ next += 1;
953
+ continue;
954
+ }
955
+
956
+ // Check for lines that start with * and contain Expected (but don't match the above patterns)
957
+ if (rawTrimmed.match(/^\*[^*]*expected/i)) {
958
+ inExpectedResult = true;
959
+ // Remove the leading * and trim
960
+ let content = rawTrimmed.slice(1).trim();
961
+ // Remove any "Expected:" prefix
962
+ content = content.replace(/^expected\s*:?\s*/i, '').trim();
963
+
964
+ const expectedContent = unescapeMarkdown(content);
965
+ if (expectedResult.length > 0) {
966
+ expectedResult += "\n" + expectedContent;
967
+ } else {
968
+ expectedResult = expectedContent;
969
+ }
819
970
  next += 1;
820
971
  continue;
821
972
  }
822
973
 
823
974
  if (rawTrimmed.startsWith("```")) {
824
- stepDataLines.push(unescapeMarkdown(rawTrimmed));
975
+ if (inExpectedResult) {
976
+ if (expectedResult.length > 0) {
977
+ expectedResult += "\n" + unescapeMarkdown(rawTrimmed);
978
+ } else {
979
+ expectedResult = unescapeMarkdown(rawTrimmed);
980
+ }
981
+ } else {
982
+ stepDataLines.push(unescapeMarkdown(rawTrimmed));
983
+ }
825
984
  next += 1;
826
985
  while (next < lines.length) {
827
986
  const fenceLine = lines[next];
828
987
  const fenceTrimmed = fenceLine.trim();
829
- stepDataLines.push(unescapeMarkdown(fenceTrimmed));
988
+ if (inExpectedResult) {
989
+ expectedResult += "\n" + unescapeMarkdown(fenceTrimmed);
990
+ } else {
991
+ stepDataLines.push(unescapeMarkdown(fenceTrimmed));
992
+ }
830
993
  next += 1;
831
994
  if (fenceTrimmed.startsWith("```")) {
832
995
  break;
@@ -836,8 +999,15 @@ function parseTestStep(
836
999
  }
837
1000
 
838
1001
  if (inExpectedResult) {
839
- const withoutLabel = stripExpectedPrefix(rawTrimmed);
840
- expectedResult += "\n" + unescapeMarkdown(withoutLabel);
1002
+ // After finding the first expected result, indented lines are part of it
1003
+ if (hasIndent) {
1004
+ const expectedContent = unescapeMarkdown(rawTrimmed);
1005
+ if (expectedResult.length > 0) {
1006
+ expectedResult += "\n" + expectedContent;
1007
+ } else {
1008
+ expectedResult = expectedContent;
1009
+ }
1010
+ }
841
1011
  next += 1;
842
1012
  continue;
843
1013
  }
@@ -849,6 +1019,14 @@ function parseTestStep(
849
1019
  continue;
850
1020
  }
851
1021
 
1022
+ // If we have indent and the line doesn't match other patterns, treat it as step data
1023
+ if (hasIndent) {
1024
+ const content = unescapeMarkdown(rawTrimmed);
1025
+ stepDataLines.push(content);
1026
+ next += 1;
1027
+ continue;
1028
+ }
1029
+
852
1030
  break;
853
1031
  }
854
1032
 
@@ -857,7 +1035,12 @@ function parseTestStep(
857
1035
  .join("\n")
858
1036
  .trim();
859
1037
 
860
- if (!isLikelyStep && !expectedResult && stepDataLines.length === 0) {
1038
+ if (
1039
+ !isLikelyStep &&
1040
+ !expectedResult &&
1041
+ stepDataLines.length === 0 &&
1042
+ !(allowEmpty && titleWithPlaceholders.length > 0)
1043
+ ) {
861
1044
  return null;
862
1045
  }
863
1046
 
@@ -1090,11 +1273,81 @@ function parseSnippetWrapper(
1090
1273
  };
1091
1274
  }
1092
1275
 
1276
+ // Post-process blocks to fix malformed image blocks
1277
+ export function fixMalformedImageBlocks(blocks: CustomPartialBlock[]): CustomPartialBlock[] {
1278
+ const result: CustomPartialBlock[] = [];
1279
+ let i = 0;
1280
+
1281
+ while (i < blocks.length) {
1282
+ const current = blocks[i];
1283
+ const next = blocks[i + 1];
1284
+
1285
+ // Skip empty paragraphs
1286
+ if (
1287
+ current.type === "paragraph" &&
1288
+ (!current.content || !Array.isArray(current.content) || current.content.length === 0)
1289
+ ) {
1290
+ i += 1;
1291
+ continue;
1292
+ }
1293
+
1294
+ // Check if current is a paragraph with just "!" - this is definitely a malformed image
1295
+ if (
1296
+ current.type === "paragraph" &&
1297
+ current.content &&
1298
+ Array.isArray(current.content) &&
1299
+ current.content.length === 1 &&
1300
+ (current.content[0] as any)?.type === "text" &&
1301
+ (current.content[0] as any)?.text === "!"
1302
+ ) {
1303
+ // This is a malformed image block, skip it entirely
1304
+ // The full image was likely parsed as ![](...) but got corrupted
1305
+ i += 1;
1306
+ continue;
1307
+ }
1308
+
1309
+ // Check if current is a paragraph with just "!" and next is an empty paragraph
1310
+ if (
1311
+ current.type === "paragraph" &&
1312
+ next?.type === "paragraph" &&
1313
+ current.content &&
1314
+ Array.isArray(current.content) &&
1315
+ current.content.length === 1 &&
1316
+ (current.content[0] as any)?.type === "text" &&
1317
+ (current.content[0] as any)?.text === "!" &&
1318
+ (!next.content || !Array.isArray(next.content) || next.content.length === 0)
1319
+ ) {
1320
+ // This looks like a malformed image, skip both blocks
1321
+ i += 2;
1322
+ continue;
1323
+ }
1324
+
1325
+ // Check if current has "!" but no link
1326
+ if (
1327
+ current.type === "paragraph" &&
1328
+ current.content &&
1329
+ Array.isArray(current.content) &&
1330
+ current.content.some((item: any) => item.type === "text" && item.text === "!") &&
1331
+ !current.content.some((item: any) => item.type === "link")
1332
+ ) {
1333
+ // Skip malformed image block
1334
+ i += 1;
1335
+ continue;
1336
+ }
1337
+
1338
+ result.push(current);
1339
+ i += 1;
1340
+ }
1341
+
1342
+ return result;
1343
+ }
1344
+
1093
1345
  export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1094
1346
  const normalized = markdown.replace(/\r\n/g, "\n");
1095
1347
  const lines = normalized.split("\n");
1096
1348
  const blocks: CustomPartialBlock[] = [];
1097
1349
  let index = 0;
1350
+ let stepsHeadingLevel: number | null = null;
1098
1351
 
1099
1352
  while (index < lines.length) {
1100
1353
  const line = lines[index];
@@ -1110,7 +1363,7 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1110
1363
  continue;
1111
1364
  }
1112
1365
 
1113
- const stepLikeBlock = parseTestStep(lines, index);
1366
+ const stepLikeBlock = parseTestStep(lines, index, stepsHeadingLevel !== null);
1114
1367
  if (stepLikeBlock) {
1115
1368
  blocks.push(stepLikeBlock.block);
1116
1369
  index = stepLikeBlock.nextIndex;
@@ -1126,7 +1379,22 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1126
1379
 
1127
1380
  const heading = parseHeading(lines, index);
1128
1381
  if (heading) {
1129
- blocks.push(heading.block);
1382
+ const headingBlock = heading.block;
1383
+ const headingLevel = (headingBlock.props as any)?.level ?? 3;
1384
+ const headingText = inlineContentToPlainText(headingBlock.content as any);
1385
+ const normalizedHeading = headingText.trim().toLowerCase();
1386
+
1387
+ if (normalizedHeading.replace(/[:\-–—]$/, "") === "steps") {
1388
+ stepsHeadingLevel = headingLevel;
1389
+ } else if (
1390
+ stepsHeadingLevel !== null &&
1391
+ headingLevel <= stepsHeadingLevel &&
1392
+ normalizedHeading.length > 0
1393
+ ) {
1394
+ stepsHeadingLevel = null;
1395
+ }
1396
+
1397
+ blocks.push(headingBlock);
1130
1398
  index = heading.nextIndex;
1131
1399
  continue;
1132
1400
  }
@@ -1147,7 +1415,13 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1147
1415
 
1148
1416
  const listType = detectListType(line.trim());
1149
1417
  if (listType) {
1150
- const { items, nextIndex } = parseList(lines, index, listType, 0);
1418
+ const { items, nextIndex } = parseList(
1419
+ lines,
1420
+ index,
1421
+ listType,
1422
+ 0,
1423
+ stepsHeadingLevel !== null,
1424
+ );
1151
1425
  blocks.push(...items);
1152
1426
  index = nextIndex;
1153
1427
  continue;
@@ -1158,7 +1432,7 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1158
1432
  index = paragraph.nextIndex;
1159
1433
  }
1160
1434
 
1161
- return blocks;
1435
+ return fixMalformedImageBlocks(blocks);
1162
1436
  }
1163
1437
 
1164
1438
  function splitTableRow(line: string): string[] {
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { __markdownTestUtils } from "./customSchema";
3
+
4
+ describe("customSchema markdown helpers", () => {
5
+ it("renders markdown images as <img> tags and round-trips back to markdown", () => {
6
+ const markdown = "![](/attachments/example.png)";
7
+ const html = __markdownTestUtils.markdownToHtml(markdown);
8
+
9
+ expect(html).toContain('<img src="/attachments/example.png" alt="" class="bn-inline-image" contenteditable="false" draggable="false" />');
10
+ expect(__markdownTestUtils.htmlToMarkdown(html)).toBe(markdown);
11
+ });
12
+
13
+ it("handles images mixed with surrounding text", () => {
14
+ const markdown = [
15
+ "Success screenshot:",
16
+ "![](/attachments/success.png)",
17
+ "Please archive it.",
18
+ ].join("\n");
19
+
20
+ const html = __markdownTestUtils.markdownToHtml(markdown);
21
+ expect(html).toContain('<img src="/attachments/success.png" alt="" class="bn-inline-image" contenteditable="false" draggable="false" />');
22
+ expect(__markdownTestUtils.htmlToMarkdown(html)).toBe(markdown);
23
+ });
24
+
25
+ it("renders expected-result style markdown with an image", () => {
26
+ const markdown = [
27
+ "Login should look like this",
28
+ "![](/login.png)",
29
+ ].join("\n");
30
+
31
+ const html = __markdownTestUtils.markdownToHtml(markdown);
32
+ expect(html).toContain('<img src="/login.png" alt="" class="bn-inline-image" contenteditable="false" draggable="false" />');
33
+ expect(__markdownTestUtils.htmlToMarkdown(html)).toBe(markdown);
34
+ });
35
+ });
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { markdownToBlocks } from "./customMarkdownConverter";
3
+
4
+ const baseProps = {
5
+ textAlignment: "left" as const,
6
+ textColor: "default" as const,
7
+ backgroundColor: "default" as const,
8
+ };
9
+
10
+ describe("markdownToBlocks", () => {
11
+ it("parses test steps and test cases", () => {
12
+ const markdown = [
13
+ "* Open the Login page.",
14
+ " *Expected*: The Login page loads successfully.",
15
+ ].join("\n");
16
+
17
+ expect(markdownToBlocks(markdown)).toEqual([
18
+ {
19
+ type: "testStep",
20
+ props: {
21
+ stepTitle: "Open the Login page.",
22
+ stepData: "",
23
+ expectedResult: "The Login page loads successfully.",
24
+ },
25
+ children: [],
26
+ },
27
+ ]);
28
+ });
29
+
30
+ it("parses snippet markdown into snippet blocks", () => {
31
+ const markdown = [
32
+ "<!-- begin snippet #501 -->",
33
+ "Run the seeder",
34
+ "<!-- end snippet #501 -->",
35
+ ].join("\n");
36
+
37
+ expect(markdownToBlocks(markdown)).toEqual([
38
+ {
39
+ type: "snippet",
40
+ props: {
41
+ snippetId: "501",
42
+ snippetTitle: "",
43
+ snippetData: "Run the seeder",
44
+ snippetExpectedResult: "",
45
+ },
46
+ children: [],
47
+ },
48
+ ]);
49
+ });
50
+
51
+ it("parses snippet bodies and ignores nested snippet markers", () => {
52
+ const markdown = [
53
+ "<!-- begin snippet #888 -->",
54
+ "Prep DB",
55
+ "<!-- begin snippet #ignored -->",
56
+ "Do not keep this marker",
57
+ "<!-- end snippet #ignored -->",
58
+ "<!-- end snippet #888 -->",
59
+ ].join("\n");
60
+
61
+ expect(markdownToBlocks(markdown)).toEqual([
62
+ {
63
+ type: "snippet",
64
+ props: {
65
+ snippetId: "888",
66
+ snippetTitle: "",
67
+ snippetData: "Prep DB\nDo not keep this marker",
68
+ snippetExpectedResult: "",
69
+ },
70
+ children: [],
71
+ },
72
+ ]);
73
+ });
74
+
75
+ // Adding just a few more critical tests to keep the file small and focused
76
+ it("parses step lists with inline expected results label", () => {
77
+ const markdown = [
78
+ "## Test Description: Real-time notifications (chat, order updates, file received)",
79
+ "",
80
+ "This test case verifies the functionality of real-time notifications for chat messages, order updates, and file receipts within the application.",
81
+ "",
82
+ "### Preconditions",
83
+ "",
84
+ "### Steps",
85
+ "",
86
+ "* Step 1: Send a chat message to the user.",
87
+ "**Expected**: The user receives a real-time notification for the chat message.",
88
+ "* Step 2: Update an order status.",
89
+ "**Expected**: The user receives a real-time notification for the order update.",
90
+ ].join("\n");
91
+
92
+ const blocks = markdownToBlocks(markdown);
93
+ const stepBlocks = blocks.filter((block) => block.type === "testStep");
94
+
95
+ expect(stepBlocks).toHaveLength(2);
96
+ expect(stepBlocks[0]).toEqual({
97
+ type: "testStep",
98
+ props: {
99
+ stepTitle: "Step 1: Send a chat message to the user.",
100
+ stepData: "",
101
+ expectedResult: "The user receives a real-time notification for the chat message.",
102
+ },
103
+ children: [],
104
+ });
105
+ });
106
+
107
+ it("round-trips simple blocks", () => {
108
+ const markdown = "Simple paragraph text";
109
+ const blocks = markdownToBlocks(markdown);
110
+
111
+ expect(blocks).toHaveLength(1);
112
+ expect(blocks[0]).toEqual({
113
+ type: "paragraph",
114
+ props: baseProps,
115
+ content: [{ type: "text", text: "Simple paragraph text", styles: {} }],
116
+ children: [],
117
+ });
118
+ });
119
+ });