testomatio-editor-blocks 0.2.3 → 0.4.0

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.
@@ -0,0 +1,44 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ type Options = {
4
+ textarea: HTMLTextAreaElement | null;
5
+ multiline?: boolean;
6
+ minRows?: number;
7
+ maxRows?: number;
8
+ };
9
+
10
+ export function useAutoResize({ textarea, multiline = false, minRows = 2, maxRows = 12 }: Options) {
11
+ const frameRef = useRef<number>(0);
12
+
13
+ useEffect(() => {
14
+ if (!textarea || !multiline) {
15
+ return;
16
+ }
17
+
18
+ const resize = () => {
19
+ textarea.style.height = "auto";
20
+ const lineHeight = parseFloat(getComputedStyle(textarea).lineHeight || "16");
21
+ const minHeight = lineHeight * minRows;
22
+ const maxHeight = lineHeight * maxRows;
23
+
24
+ textarea.style.height = `${Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight)}px`;
25
+ };
26
+
27
+ const observer = new MutationObserver(resize);
28
+ observer.observe(textarea, { childList: true, characterData: true, subtree: true });
29
+
30
+ const handleInput = () => {
31
+ cancelAnimationFrame(frameRef.current ?? 0);
32
+ frameRef.current = requestAnimationFrame(resize);
33
+ };
34
+
35
+ textarea.addEventListener("input", handleInput);
36
+ resize();
37
+
38
+ return () => {
39
+ observer.disconnect();
40
+ textarea.removeEventListener("input", handleInput);
41
+ cancelAnimationFrame(frameRef.current ?? 0);
42
+ };
43
+ }, [textarea, multiline, minRows, maxRows]);
44
+ }
@@ -157,6 +157,21 @@ describe("blocksToMarkdown", () => {
157
157
  );
158
158
  });
159
159
 
160
+ it("cleans escaped formatting markers when toggling styles repeatedly", () => {
161
+ const blocks: CustomEditorBlock[] = [
162
+ {
163
+ id: "esc1",
164
+ type: "paragraph",
165
+ props: baseProps,
166
+ content: [{ type: "text", text: "text", styles: { bold: true, italic: true } }],
167
+ children: [],
168
+ },
169
+ ];
170
+
171
+ const markdown = blocksToMarkdown(blocks);
172
+ expect(markdown).toBe("***text***");
173
+ });
174
+
160
175
  it("keeps inline formatting inside step fields", () => {
161
176
  const blocks: CustomEditorBlock[] = [
162
177
  {
@@ -474,7 +489,7 @@ describe("markdownToBlocks", () => {
474
489
  props: {
475
490
  stepTitle: "Step 4: Verify that the notifications are displayed correctly in the application's notification panel.",
476
491
  stepData: "",
477
- expectedResult: "All notifications (chat message, order update, file received) are listed in the notification panel with the correct information (e.g., timestamp, message content).",
492
+ expectedResult: "All notifications (chat message, order update, file received) are listed in the notification panel with the correct information (e.g., timestamp, message content).\n",
478
493
  },
479
494
  children: [],
480
495
  },
@@ -647,7 +662,7 @@ describe("markdownToBlocks", () => {
647
662
  props: {
648
663
  stepTitle: "Open the form.",
649
664
  stepData: "",
650
- expectedResult: "Fields are empty.",
665
+ expectedResult: "** The form opens.\nFields are empty.",
651
666
  },
652
667
  children: [],
653
668
  },
@@ -773,6 +788,52 @@ describe("markdownToBlocks", () => {
773
788
  );
774
789
  });
775
790
 
791
+ it("parses steps under a Steps heading even when expected results are missing", () => {
792
+ const markdown = [
793
+ "### Steps",
794
+ "",
795
+ "* Pass onboarding as mobile user",
796
+ "* Navigate to More tab -≻ My Profile -≻ Log into the app with user from preconditions",
797
+ " *Expected:* Upsell SS screen is displayed",
798
+ "* Close SS",
799
+ " *Expected:* My Course and More tab are displayed",
800
+ ].join("\n");
801
+
802
+ const blocks = markdownToBlocks(markdown);
803
+ const stepBlocks = blocks.filter((block) => block.type === "testStep");
804
+
805
+ expect(stepBlocks).toEqual([
806
+ {
807
+ type: "testStep",
808
+ props: {
809
+ stepTitle: "Pass onboarding as mobile user",
810
+ stepData: "",
811
+ expectedResult: "",
812
+ },
813
+ children: [],
814
+ },
815
+ {
816
+ type: "testStep",
817
+ props: {
818
+ stepTitle:
819
+ "Navigate to More tab -≻ My Profile -≻ Log into the app with user from preconditions",
820
+ stepData: "",
821
+ expectedResult: "* Upsell SS screen is displayed",
822
+ },
823
+ children: [],
824
+ },
825
+ {
826
+ type: "testStep",
827
+ props: {
828
+ stepTitle: "Close SS",
829
+ stepData: "",
830
+ expectedResult: "* My Course and More tab are displayed",
831
+ },
832
+ children: [],
833
+ },
834
+ ]);
835
+ });
836
+
776
837
  it("round-trips simple blocks", () => {
777
838
  const blocks: CustomEditorBlock[] = [
778
839
  {
@@ -865,4 +926,55 @@ describe("markdownToBlocks", () => {
865
926
  },
866
927
  ]);
867
928
  });
929
+
930
+ it("parses multiple Expected blocks within a single test step", () => {
931
+ const markdown = [
932
+ "### Steps",
933
+ "",
934
+ "* Swipe Back",
935
+ "* Check UI of Sleep score info screen",
936
+ " - Back button",
937
+ " Header: Sleep Score Info",
938
+ " Text: Ever wonder if 6, 8, or 9 hours of sleep are enough? Sleep score takes the guesswork out of your ZZZ's and shows you how well you slept last night based on duration, efficiency, and consistency.",
939
+ " *Expected:* - 1st block:",
940
+ " *Expected:* - 2nd block:",
941
+ " *Expected:* - 3d block:",
942
+ "* Tap 'Back' button",
943
+ ].join("\n");
944
+
945
+ const blocks = markdownToBlocks(markdown);
946
+ const stepBlocks = blocks.filter((block) => block.type === "testStep");
947
+
948
+ expect(stepBlocks).toHaveLength(3);
949
+
950
+ expect(stepBlocks[0]).toEqual({
951
+ type: "testStep",
952
+ props: {
953
+ stepTitle: "Swipe Back",
954
+ stepData: "",
955
+ expectedResult: "",
956
+ },
957
+ children: [],
958
+ });
959
+
960
+ expect(stepBlocks[1]).toEqual({
961
+ type: "testStep",
962
+ props: {
963
+ stepTitle: "Check UI of Sleep score info screen",
964
+ stepData: "- Back button\nHeader: Sleep Score Info\nText: Ever wonder if 6, 8, or 9 hours of sleep are enough? Sleep score takes the guesswork out of your ZZZ's and shows you how well you slept last night based on duration, efficiency, and consistency.",
965
+ expectedResult: "* - 1st block:\n* - 2nd block:\n* - 3d block:",
966
+ },
967
+ children: [],
968
+ });
969
+
970
+ expect(stepBlocks[2]).toEqual({
971
+ type: "testStep",
972
+ props: {
973
+ stepTitle: "Tap 'Back' button",
974
+ stepData: "",
975
+ expectedResult: "",
976
+ },
977
+ children: [],
978
+ });
979
+ });
868
980
  });
@@ -206,6 +206,26 @@ function inlineToMarkdown(content: CustomEditorBlock["content"]): string {
206
206
  .join("");
207
207
  }
208
208
 
209
+ function inlineContentToPlainText(content: CustomEditorBlock["content"]): string {
210
+ if (!Array.isArray(content)) {
211
+ return "";
212
+ }
213
+
214
+ return (content as any[])
215
+ .map((node: any) => {
216
+ if (node && typeof node === "object") {
217
+ if (typeof node.text === "string") {
218
+ return node.text;
219
+ }
220
+ if (Array.isArray(node.content)) {
221
+ return inlineContentToPlainText(node.content);
222
+ }
223
+ }
224
+ return "";
225
+ })
226
+ .join("");
227
+ }
228
+
209
229
  function serializeChildren(block: CustomEditorBlock, ctx: MarkdownContext): string[] {
210
230
  if (!block.children?.length) {
211
231
  return [];
@@ -667,6 +687,7 @@ function parseList(
667
687
  startIndex: number,
668
688
  listType: "bullet" | "numbered" | "check",
669
689
  indentLevel: number,
690
+ allowEmptySteps = false,
670
691
  ): ListParseResult {
671
692
  const items: CustomPartialBlock[] = [];
672
693
  let index = startIndex;
@@ -681,15 +702,15 @@ function parseList(
681
702
  }
682
703
 
683
704
  let indent = countIndent(rawLine);
684
- const baseIndent = indentLevel * 2;
685
- if (indent > baseIndent && indent <= baseIndent + 1) {
686
- indent = baseIndent;
687
- }
705
+
688
706
  if (indent < indentLevel * 2) {
689
707
  break;
690
708
  }
691
709
 
692
- if (indent > indentLevel * 2) {
710
+ // Check if this line should be parsed as nested content
711
+ // Only go deeper if indent is at least 2 more than the next level's expected indent
712
+ const nextLevelExpectedIndent = (indentLevel + 1) * 2;
713
+ if (indent >= nextLevelExpectedIndent) {
693
714
  const lastItem = items.at(-1);
694
715
  if (!lastItem) {
695
716
  break;
@@ -698,7 +719,18 @@ function parseList(
698
719
  if (!nestedType) {
699
720
  break;
700
721
  }
701
- const nested = parseList(lines, index, nestedType, indentLevel + 1);
722
+ const nested = parseList(
723
+ lines,
724
+ index,
725
+ nestedType,
726
+ indentLevel + 1,
727
+ allowEmptySteps,
728
+ );
729
+ // If nested parsing made no progress, skip this line to avoid infinite loop
730
+ if (nested.nextIndex === index) {
731
+ index += 1;
732
+ continue;
733
+ }
702
734
  lastItem.children = [...(lastItem.children ?? []), ...nested.items];
703
735
  index = nested.nextIndex;
704
736
  continue;
@@ -709,6 +741,17 @@ function parseList(
709
741
  break;
710
742
  }
711
743
 
744
+ // Only try to parse as testStep for top-level bullet items (indentLevel === 0)
745
+ // Nested bullets within numbered lists should remain as regular bulletListItem
746
+ if (listType === "bullet" && indentLevel === 0) {
747
+ const nextStep = parseTestStep(lines, index, allowEmptySteps);
748
+ if (nextStep) {
749
+ items.push(nextStep.block);
750
+ index = nextStep.nextIndex;
751
+ continue;
752
+ }
753
+ }
754
+
712
755
  if (listType === "check") {
713
756
  const checkMatch = trimmed.match(/^[-*+]\s+\[([xX\s])\]\s+(.*)$/);
714
757
  const checked = (checkMatch?.[1] ?? "").toLowerCase() === "x";
@@ -749,6 +792,7 @@ function parseList(
749
792
  function parseTestStep(
750
793
  lines: string[],
751
794
  index: number,
795
+ allowEmpty = false,
752
796
  snippetId?: string,
753
797
  ): { block: CustomPartialBlock; nextIndex: number } | null {
754
798
  const current = lines[index];
@@ -781,6 +825,7 @@ function parseTestStep(
781
825
  let expectedResult = "";
782
826
  let next = index + 1;
783
827
  let inExpectedResult = false;
828
+ let foundFirstExpected = false;
784
829
 
785
830
  while (next < lines.length) {
786
831
  const line = lines[next];
@@ -788,8 +833,12 @@ function parseTestStep(
788
833
  const rawTrimmed = line.trim();
789
834
 
790
835
  if (!rawTrimmed) {
791
- if (stepDataLines.length > 0) {
792
- stepDataLines.push("");
836
+ if (stepDataLines.length > 0 || inExpectedResult) {
837
+ if (inExpectedResult) {
838
+ expectedResult += "\n";
839
+ } else {
840
+ stepDataLines.push("");
841
+ }
793
842
  }
794
843
  next += 1;
795
844
  continue;
@@ -812,21 +861,68 @@ function parseTestStep(
812
861
  break;
813
862
  }
814
863
 
815
- if (rawTrimmed.match(EXPECTED_LABEL_REGEX)) {
864
+ // Check for expected result labels with different formatting
865
+ const expectedMatch = rawTrimmed.match(EXPECTED_LABEL_REGEX);
866
+ const expectedStarMatch = rawTrimmed.match(/^\*expected\s*\*:\s*(.*)$/i) ||
867
+ rawTrimmed.match(/^\*expected\*:\s*(.*)$/i);
868
+
869
+ if (expectedMatch || expectedStarMatch) {
870
+ foundFirstExpected = true;
816
871
  inExpectedResult = true;
817
- const withoutLabel = stripExpectedPrefix(rawTrimmed);
818
- expectedResult = unescapeMarkdown(withoutLabel);
872
+ const label = expectedMatch ? expectedMatch[0] : (expectedStarMatch ? expectedStarMatch[0] : '');
873
+ let content = rawTrimmed.slice(label.length).trim();
874
+
875
+ // Add the content (if any) from this line
876
+ if (content) {
877
+ const expectedContent = unescapeMarkdown(content);
878
+ if (expectedResult.length > 0) {
879
+ expectedResult += "\n" + expectedContent;
880
+ } else {
881
+ expectedResult = expectedContent;
882
+ }
883
+ }
884
+ next += 1;
885
+ continue;
886
+ }
887
+
888
+ // Check for lines that start with * and contain Expected (but don't match the above patterns)
889
+ if (rawTrimmed.match(/^\*[^*]*expected/i)) {
890
+ foundFirstExpected = true;
891
+ inExpectedResult = true;
892
+ // Remove the leading * and trim
893
+ let content = rawTrimmed.slice(1).trim();
894
+ // Remove any "Expected:" prefix
895
+ content = content.replace(/^expected\s*:?\s*/i, '').trim();
896
+
897
+ const expectedContent = unescapeMarkdown(content);
898
+ if (expectedResult.length > 0) {
899
+ expectedResult += "\n" + expectedContent;
900
+ } else {
901
+ expectedResult = expectedContent;
902
+ }
819
903
  next += 1;
820
904
  continue;
821
905
  }
822
906
 
823
907
  if (rawTrimmed.startsWith("```")) {
824
- stepDataLines.push(unescapeMarkdown(rawTrimmed));
908
+ if (inExpectedResult) {
909
+ if (expectedResult.length > 0) {
910
+ expectedResult += "\n" + unescapeMarkdown(rawTrimmed);
911
+ } else {
912
+ expectedResult = unescapeMarkdown(rawTrimmed);
913
+ }
914
+ } else {
915
+ stepDataLines.push(unescapeMarkdown(rawTrimmed));
916
+ }
825
917
  next += 1;
826
918
  while (next < lines.length) {
827
919
  const fenceLine = lines[next];
828
920
  const fenceTrimmed = fenceLine.trim();
829
- stepDataLines.push(unescapeMarkdown(fenceTrimmed));
921
+ if (inExpectedResult) {
922
+ expectedResult += "\n" + unescapeMarkdown(fenceTrimmed);
923
+ } else {
924
+ stepDataLines.push(unescapeMarkdown(fenceTrimmed));
925
+ }
830
926
  next += 1;
831
927
  if (fenceTrimmed.startsWith("```")) {
832
928
  break;
@@ -836,8 +932,24 @@ function parseTestStep(
836
932
  }
837
933
 
838
934
  if (inExpectedResult) {
839
- const withoutLabel = stripExpectedPrefix(rawTrimmed);
840
- expectedResult += "\n" + unescapeMarkdown(withoutLabel);
935
+ // After finding the first expected result, indented lines are part of it
936
+ if (hasIndent) {
937
+ const expectedContent = unescapeMarkdown(rawTrimmed);
938
+ if (expectedResult.length > 0) {
939
+ expectedResult += "\n" + expectedContent;
940
+ } else {
941
+ expectedResult = expectedContent;
942
+ }
943
+ } else if (foundFirstExpected && rawTrimmed.startsWith("*") && !rawTrimmed.startsWith("* ")) {
944
+ // Non-indented lines starting with single * (not list item) are likely more expected results
945
+ // Remove the leading * and treat the rest as content
946
+ const expectedContent = unescapeMarkdown(rawTrimmed.slice(1).trim());
947
+ if (expectedResult.length > 0) {
948
+ expectedResult += "\n" + expectedContent;
949
+ } else {
950
+ expectedResult = expectedContent;
951
+ }
952
+ }
841
953
  next += 1;
842
954
  continue;
843
955
  }
@@ -849,6 +961,14 @@ function parseTestStep(
849
961
  continue;
850
962
  }
851
963
 
964
+ // If we have indent and the line doesn't match other patterns, treat it as step data
965
+ if (hasIndent) {
966
+ const content = unescapeMarkdown(rawTrimmed);
967
+ stepDataLines.push(content);
968
+ next += 1;
969
+ continue;
970
+ }
971
+
852
972
  break;
853
973
  }
854
974
 
@@ -857,7 +977,12 @@ function parseTestStep(
857
977
  .join("\n")
858
978
  .trim();
859
979
 
860
- if (!isLikelyStep && !expectedResult && stepDataLines.length === 0) {
980
+ if (
981
+ !isLikelyStep &&
982
+ !expectedResult &&
983
+ stepDataLines.length === 0 &&
984
+ !(allowEmpty && titleWithPlaceholders.length > 0)
985
+ ) {
861
986
  return null;
862
987
  }
863
988
 
@@ -1095,6 +1220,7 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1095
1220
  const lines = normalized.split("\n");
1096
1221
  const blocks: CustomPartialBlock[] = [];
1097
1222
  let index = 0;
1223
+ let stepsHeadingLevel: number | null = null;
1098
1224
 
1099
1225
  while (index < lines.length) {
1100
1226
  const line = lines[index];
@@ -1110,7 +1236,7 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1110
1236
  continue;
1111
1237
  }
1112
1238
 
1113
- const stepLikeBlock = parseTestStep(lines, index);
1239
+ const stepLikeBlock = parseTestStep(lines, index, stepsHeadingLevel !== null);
1114
1240
  if (stepLikeBlock) {
1115
1241
  blocks.push(stepLikeBlock.block);
1116
1242
  index = stepLikeBlock.nextIndex;
@@ -1126,7 +1252,22 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1126
1252
 
1127
1253
  const heading = parseHeading(lines, index);
1128
1254
  if (heading) {
1129
- blocks.push(heading.block);
1255
+ const headingBlock = heading.block;
1256
+ const headingLevel = (headingBlock.props as any)?.level ?? 3;
1257
+ const headingText = inlineContentToPlainText(headingBlock.content as any);
1258
+ const normalizedHeading = headingText.trim().toLowerCase();
1259
+
1260
+ if (normalizedHeading === "steps") {
1261
+ stepsHeadingLevel = headingLevel;
1262
+ } else if (
1263
+ stepsHeadingLevel !== null &&
1264
+ headingLevel <= stepsHeadingLevel &&
1265
+ normalizedHeading.length > 0
1266
+ ) {
1267
+ stepsHeadingLevel = null;
1268
+ }
1269
+
1270
+ blocks.push(headingBlock);
1130
1271
  index = heading.nextIndex;
1131
1272
  continue;
1132
1273
  }
@@ -1147,7 +1288,13 @@ export function markdownToBlocks(markdown: string): CustomPartialBlock[] {
1147
1288
 
1148
1289
  const listType = detectListType(line.trim());
1149
1290
  if (listType) {
1150
- const { items, nextIndex } = parseList(lines, index, listType, 0);
1291
+ const { items, nextIndex } = parseList(
1292
+ lines,
1293
+ index,
1294
+ listType,
1295
+ 0,
1296
+ stepsHeadingLevel !== null,
1297
+ );
1151
1298
  blocks.push(...items);
1152
1299
  index = nextIndex;
1153
1300
  continue;
@@ -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
+ });