testomatio-editor-blocks 0.4.0 → 0.4.6

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.
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
  import {
3
3
  blocksToMarkdown,
4
4
  markdownToBlocks,
5
+ fixMalformedImageBlocks,
5
6
  type CustomEditorBlock,
6
7
  type CustomPartialBlock,
7
8
  } from "./customMarkdownConverter";
@@ -74,6 +75,7 @@ describe("blocksToMarkdown", () => {
74
75
  stepTitle: "Open the Login page.",
75
76
  stepData: "",
76
77
  expectedResult: "The Login page loads successfully.",
78
+ listStyle: "bullet",
77
79
  },
78
80
  content: undefined,
79
81
  children: [],
@@ -85,6 +87,7 @@ describe("blocksToMarkdown", () => {
85
87
  stepTitle: "Enter a valid username.",
86
88
  stepData: "",
87
89
  expectedResult: "The username is accepted.",
90
+ listStyle: "bullet",
88
91
  },
89
92
  content: undefined,
90
93
  children: [],
@@ -181,6 +184,7 @@ describe("blocksToMarkdown", () => {
181
184
  stepTitle: "**Click** the _Login_ button",
182
185
  stepData: "",
183
186
  expectedResult: "**Success** is shown\nSecond line with <u>underline</u>",
187
+ listStyle: "bullet",
184
188
  },
185
189
  content: undefined,
186
190
  children: [],
@@ -205,6 +209,7 @@ describe("blocksToMarkdown", () => {
205
209
  stepTitle: "Navigate to login",
206
210
  stepData: "Open browser\nGo to login page",
207
211
  expectedResult: "Login form visible",
212
+ listStyle: "bullet",
208
213
  },
209
214
  content: undefined,
210
215
  children: [],
@@ -242,6 +247,7 @@ describe("blocksToMarkdown", () => {
242
247
  "![](/attachments/HMhkVtlDrO.png)",
243
248
  ].join("\n"),
244
249
  expectedResult: "The user receives a real-time notification for the order update.",
250
+ listStyle: "bullet",
245
251
  },
246
252
  content: undefined,
247
253
  children: [],
@@ -351,6 +357,7 @@ describe("markdownToBlocks", () => {
351
357
  stepTitle: "Open the Login page.",
352
358
  stepData: "",
353
359
  expectedResult: "The Login page loads successfully.",
360
+ listStyle: "bullet",
354
361
  },
355
362
  children: [],
356
363
  },
@@ -457,12 +464,43 @@ describe("markdownToBlocks", () => {
457
464
  const stepBlocks = blocks.filter((block) => block.type === "testStep");
458
465
 
459
466
  expect(stepBlocks).toEqual([
467
+ {
468
+ type: "testStep",
469
+ props: {
470
+ stepTitle: "The user is logged into the application.",
471
+ stepData: "",
472
+ expectedResult: "",
473
+ listStyle: "bullet",
474
+ },
475
+ children: [],
476
+ },
477
+ {
478
+ type: "testStep",
479
+ props: {
480
+ stepTitle: "The user has the necessary permissions to receive notifications.",
481
+ stepData: "",
482
+ expectedResult: "",
483
+ listStyle: "bullet",
484
+ },
485
+ children: [],
486
+ },
487
+ {
488
+ type: "testStep",
489
+ props: {
490
+ stepTitle: "The application is configured to send real-time notifications.",
491
+ stepData: "",
492
+ expectedResult: "",
493
+ listStyle: "bullet",
494
+ },
495
+ children: [],
496
+ },
460
497
  {
461
498
  type: "testStep",
462
499
  props: {
463
500
  stepTitle: "Step 1: Send a chat message to the user.",
464
501
  stepData: "",
465
502
  expectedResult: "The user receives a real-time notification for the chat message.",
503
+ listStyle: "bullet",
466
504
  },
467
505
  children: [],
468
506
  },
@@ -472,6 +510,7 @@ describe("markdownToBlocks", () => {
472
510
  stepTitle: "Step 2: Update an order status.",
473
511
  stepData: "",
474
512
  expectedResult: "The user receives a real-time notification for the order update.",
513
+ listStyle: "bullet",
475
514
  },
476
515
  children: [],
477
516
  },
@@ -481,6 +520,7 @@ describe("markdownToBlocks", () => {
481
520
  stepTitle: "Step 3: Send a file to the user.",
482
521
  stepData: "",
483
522
  expectedResult: "The user receives a real-time notification for the file received.",
523
+ listStyle: "bullet",
484
524
  },
485
525
  children: [],
486
526
  },
@@ -490,6 +530,27 @@ describe("markdownToBlocks", () => {
490
530
  stepTitle: "Step 4: Verify that the notifications are displayed correctly in the application's notification panel.",
491
531
  stepData: "",
492
532
  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",
533
+ listStyle: "bullet",
534
+ },
535
+ children: [],
536
+ },
537
+ {
538
+ type: "testStep",
539
+ props: {
540
+ stepTitle: "The user has received and viewed the notifications.",
541
+ stepData: "",
542
+ expectedResult: "",
543
+ listStyle: "bullet",
544
+ },
545
+ children: [],
546
+ },
547
+ {
548
+ type: "testStep",
549
+ props: {
550
+ stepTitle: "The application continues to function as expected after receiving and processing the notifications.",
551
+ stepData: "",
552
+ expectedResult: "",
553
+ listStyle: "bullet",
493
554
  },
494
555
  children: [],
495
556
  },
@@ -534,6 +595,7 @@ describe("markdownToBlocks", () => {
534
595
  stepTitle: "Step 2: Update an order status.",
535
596
  stepData: expectedData,
536
597
  expectedResult: "The user receives a real-time notification for the order update.",
598
+ listStyle: "bullet",
537
599
  },
538
600
  children: [],
539
601
  },
@@ -547,6 +609,7 @@ describe("markdownToBlocks", () => {
547
609
  stepTitle: "Step 2: Update an order status.",
548
610
  stepData: expectedData,
549
611
  expectedResult: "The user receives a real-time notification for the order update.",
612
+ listStyle: "bullet",
550
613
  },
551
614
  content: undefined,
552
615
  children: [],
@@ -589,39 +652,33 @@ describe("markdownToBlocks", () => {
589
652
  children: [],
590
653
  },
591
654
  {
592
- type: "bulletListItem",
593
- props: baseProps,
594
- content: [
595
- {
596
- type: "text",
597
- text: "The user is logged into the application.",
598
- styles: {},
599
- },
600
- ],
655
+ type: "testStep",
656
+ props: {
657
+ stepTitle: "The user is logged into the application.",
658
+ stepData: "",
659
+ expectedResult: "",
660
+ listStyle: "bullet",
661
+ },
601
662
  children: [],
602
663
  },
603
664
  {
604
- type: "bulletListItem",
605
- props: baseProps,
606
- content: [
607
- {
608
- type: "text",
609
- text: "The user has the necessary permissions to receive notifications.",
610
- styles: {},
611
- },
612
- ],
665
+ type: "testStep",
666
+ props: {
667
+ stepTitle: "The user has the necessary permissions to receive notifications.",
668
+ stepData: "",
669
+ expectedResult: "",
670
+ listStyle: "bullet",
671
+ },
613
672
  children: [],
614
673
  },
615
674
  {
616
- type: "bulletListItem",
617
- props: baseProps,
618
- content: [
619
- {
620
- type: "text",
621
- text: "The application is configured to send real-time notifications.",
622
- styles: {},
623
- },
624
- ],
675
+ type: "testStep",
676
+ props: {
677
+ stepTitle: "The application is configured to send real-time notifications.",
678
+ stepData: "",
679
+ expectedResult: "",
680
+ listStyle: "bullet",
681
+ },
625
682
  children: [],
626
683
  },
627
684
  ]);
@@ -633,7 +690,7 @@ describe("markdownToBlocks", () => {
633
690
  "",
634
691
  "* Verify that each individual unit test completes in ≤ 50 ms (target) and never exceeds 200 ms (hard limit).",
635
692
  "",
636
- "### Steps",
693
+ "### Instructions",
637
694
  "",
638
695
  "1. Execute the full unit test suite with a timer wrapper.",
639
696
  " * Each individual test case.",
@@ -663,6 +720,7 @@ describe("markdownToBlocks", () => {
663
720
  stepTitle: "Open the form.",
664
721
  stepData: "",
665
722
  expectedResult: "** The form opens.\nFields are empty.",
723
+ listStyle: "bullet",
666
724
  },
667
725
  children: [],
668
726
  },
@@ -684,6 +742,7 @@ describe("markdownToBlocks", () => {
684
742
  stepTitle: "Navigate to login",
685
743
  stepData: "Open browser\nGo to login page",
686
744
  expectedResult: "Login form visible",
745
+ listStyle: "bullet",
687
746
  },
688
747
  children: [],
689
748
  },
@@ -705,6 +764,7 @@ describe("markdownToBlocks", () => {
705
764
  stepTitle: "Prepare test fixtures",
706
765
  stepData: "Collect user accounts from staging.\nReset passwords for all test accounts.",
707
766
  expectedResult: "Test accounts are ready for execution.",
767
+ listStyle: "bullet",
708
768
  },
709
769
  children: [],
710
770
  },
@@ -724,6 +784,7 @@ describe("markdownToBlocks", () => {
724
784
  stepTitle: "Display the generated report.",
725
785
  stepData: "",
726
786
  expectedResult: "![](/attachments/report.png)",
787
+ listStyle: "bullet",
727
788
  },
728
789
  children: [],
729
790
  },
@@ -737,6 +798,7 @@ describe("markdownToBlocks", () => {
737
798
  stepTitle: "Display the generated report.",
738
799
  stepData: "",
739
800
  expectedResult: "![](/attachments/report.png)",
801
+ listStyle: "bullet",
740
802
  },
741
803
  content: undefined,
742
804
  children: [],
@@ -760,6 +822,7 @@ describe("markdownToBlocks", () => {
760
822
  stepTitle: "Should open login screen",
761
823
  stepData: "",
762
824
  expectedResult: "Login should look like this\n![](/login.png)",
825
+ listStyle: "bullet",
763
826
  },
764
827
  children: [],
765
828
  },
@@ -773,6 +836,7 @@ describe("markdownToBlocks", () => {
773
836
  stepTitle: "Should open login screen",
774
837
  stepData: "",
775
838
  expectedResult: "Login should look like this\n![](/login.png)",
839
+ listStyle: "bullet",
776
840
  },
777
841
  content: undefined,
778
842
  children: [],
@@ -809,6 +873,7 @@ describe("markdownToBlocks", () => {
809
873
  stepTitle: "Pass onboarding as mobile user",
810
874
  stepData: "",
811
875
  expectedResult: "",
876
+ listStyle: "bullet",
812
877
  },
813
878
  children: [],
814
879
  },
@@ -819,6 +884,7 @@ describe("markdownToBlocks", () => {
819
884
  "Navigate to More tab -≻ My Profile -≻ Log into the app with user from preconditions",
820
885
  stepData: "",
821
886
  expectedResult: "* Upsell SS screen is displayed",
887
+ listStyle: "bullet",
822
888
  },
823
889
  children: [],
824
890
  },
@@ -828,12 +894,70 @@ describe("markdownToBlocks", () => {
828
894
  stepTitle: "Close SS",
829
895
  stepData: "",
830
896
  expectedResult: "* My Course and More tab are displayed",
897
+ listStyle: "bullet",
831
898
  },
832
899
  children: [],
833
900
  },
834
901
  ]);
835
902
  });
836
903
 
904
+ it("handles multiple steps with expected results without extra asterisks", () => {
905
+ const markdown = [
906
+ "### Preconditions",
907
+ "",
908
+ "User on the Sign In with Email Screen",
909
+ "",
910
+ "### Steps",
911
+ "",
912
+ "* Existing email + invalid password",
913
+ " *Expected*: 'Oops, wrong email or password' is displayed",
914
+ "* Not existing email + valid password",
915
+ " *Expected*: 'Oops, wrong email or password' is displayed",
916
+ ].join("\n");
917
+
918
+ const blocks = markdownToBlocks(markdown);
919
+ const stepBlocks = blocks.filter((block) => block.type === "testStep");
920
+
921
+ expect(stepBlocks).toHaveLength(2);
922
+
923
+ expect(stepBlocks[0]).toEqual({
924
+ type: "testStep",
925
+ props: {
926
+ stepTitle: "Existing email + invalid password",
927
+ stepData: "",
928
+ expectedResult: "'Oops, wrong email or password' is displayed",
929
+ listStyle: "bullet",
930
+ },
931
+ children: [],
932
+ });
933
+
934
+ expect(stepBlocks[1]).toEqual({
935
+ type: "testStep",
936
+ props: {
937
+ stepTitle: "Not existing email + valid password",
938
+ stepData: "",
939
+ expectedResult: "'Oops, wrong email or password' is displayed",
940
+ listStyle: "bullet",
941
+ },
942
+ children: [],
943
+ });
944
+
945
+ // Verify round-trip doesn't add extra asterisks
946
+ const roundTrip = blocksToMarkdown(stepBlocks.map((block, index) => ({
947
+ ...block,
948
+ id: `step${index}`,
949
+ })) as CustomEditorBlock[]);
950
+
951
+ expect(roundTrip).toBe(
952
+ [
953
+ "* Existing email + invalid password",
954
+ " *Expected*: 'Oops, wrong email or password' is displayed",
955
+ "* Not existing email + valid password",
956
+ " *Expected*: 'Oops, wrong email or password' is displayed",
957
+ ].join("\n"),
958
+ );
959
+ });
960
+
837
961
  it("round-trips simple blocks", () => {
838
962
  const blocks: CustomEditorBlock[] = [
839
963
  {
@@ -854,7 +978,55 @@ describe("markdownToBlocks", () => {
854
978
 
855
979
  const markdown = blocksToMarkdown(blocks);
856
980
  const parsed = markdownToBlocks(markdown);
857
- expect(parsed).toEqual(blocks.map(toPartial));
981
+ expect(parsed).toEqual([
982
+ {
983
+ type: "paragraph",
984
+ props: baseProps,
985
+ content: [{ type: "text", text: "Paragraph", styles: {} }],
986
+ children: [],
987
+ },
988
+ {
989
+ type: "testStep",
990
+ props: {
991
+ stepTitle: "Bullet",
992
+ stepData: "",
993
+ expectedResult: "",
994
+ listStyle: "bullet",
995
+ },
996
+ children: [],
997
+ },
998
+ ]);
999
+ });
1000
+
1001
+ it("round-trips bullet steps preserving bullet style", () => {
1002
+ const markdown = [
1003
+ "* Open the page",
1004
+ " *Expected*: Page loads",
1005
+ "* Click button",
1006
+ ].join("\n");
1007
+
1008
+ const blocks = markdownToBlocks(markdown);
1009
+ const output = blocksToMarkdown(blocks as CustomEditorBlock[]);
1010
+
1011
+ expect(output).toContain("* Open the page");
1012
+ expect(output).toContain("* Click button");
1013
+ expect(output).not.toMatch(/^\d+\./m);
1014
+ });
1015
+
1016
+ it("round-trips ordered steps preserving ordered style", () => {
1017
+ const markdown = [
1018
+ "### Steps",
1019
+ "",
1020
+ "1. Open the page",
1021
+ " *Expected*: Page loads",
1022
+ "2. Click button",
1023
+ ].join("\n");
1024
+
1025
+ const blocks = markdownToBlocks(markdown);
1026
+ const output = blocksToMarkdown(blocks as CustomEditorBlock[]);
1027
+
1028
+ expect(output).toContain("1. Open the page");
1029
+ expect(output).toContain("2. Click button");
858
1030
  });
859
1031
 
860
1032
  it("parses markdown tables", () => {
@@ -921,6 +1093,7 @@ describe("markdownToBlocks", () => {
921
1093
  stepTitle: "Step 1: Send a chat message to the user.",
922
1094
  stepData: "",
923
1095
  expectedResult: "The user receives a real-time notification for the chat message.",
1096
+ listStyle: "bullet",
924
1097
  },
925
1098
  children: [],
926
1099
  },
@@ -953,6 +1126,7 @@ describe("markdownToBlocks", () => {
953
1126
  stepTitle: "Swipe Back",
954
1127
  stepData: "",
955
1128
  expectedResult: "",
1129
+ listStyle: "bullet",
956
1130
  },
957
1131
  children: [],
958
1132
  });
@@ -963,6 +1137,7 @@ describe("markdownToBlocks", () => {
963
1137
  stepTitle: "Check UI of Sleep score info screen",
964
1138
  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
1139
  expectedResult: "* - 1st block:\n* - 2nd block:\n* - 3d block:",
1140
+ listStyle: "bullet",
966
1141
  },
967
1142
  children: [],
968
1143
  });
@@ -973,8 +1148,398 @@ describe("markdownToBlocks", () => {
973
1148
  stepTitle: "Tap 'Back' button",
974
1149
  stepData: "",
975
1150
  expectedResult: "",
1151
+ listStyle: "bullet",
1152
+ },
1153
+ children: [],
1154
+ });
1155
+ });
1156
+
1157
+ it("correctly parses images at the end of the document", () => {
1158
+ const markdown = [
1159
+ "#### Steps:",
1160
+ "1. Navigate to the product listing page.",
1161
+ " Expected open",
1162
+ "3. Select a product and click the \"Add to Cart\" button.",
1163
+ " Expected result close",
1164
+ "5. Open the shopping cart page.",
1165
+ " **Expected** edit",
1166
+ "6. Verify that the added item is displayed with the correct name, price, and quantity.",
1167
+ " _Expected_ close",
1168
+ "",
1169
+ "### **Expected Result:** The item appears in the cart with correct details and price calculation.",
1170
+ "",
1171
+ "![logs](/attachments/se2n8jaGon.png)",
1172
+ "",
1173
+ "![](/attachments/p5DgklVeMg.png)",
1174
+ ].join("\n");
1175
+
1176
+ const blocks = markdownToBlocks(markdown);
1177
+
1178
+ // Find the paragraph blocks that contain the images (links)
1179
+ const imageBlocks = blocks.filter(block =>
1180
+ block.type === "paragraph" &&
1181
+ block.content &&
1182
+ Array.isArray(block.content) &&
1183
+ block.content.some((item: any) =>
1184
+ (item.type === "text" && item.text === "!") ||
1185
+ (item.type === "link" && item.href && item.href.includes("/attachments/"))
1186
+ )
1187
+ );
1188
+
1189
+ // Should have two paragraph blocks with images
1190
+ expect(imageBlocks.length).toBe(2);
1191
+
1192
+ // Check that both image links are properly parsed
1193
+ const imageLinks: any[] = [];
1194
+ imageBlocks.forEach(block => {
1195
+ if (block.content && Array.isArray(block.content)) {
1196
+ const link = (block.content as any[]).find(item => item.type === "link");
1197
+ if (link) {
1198
+ imageLinks.push(link);
1199
+ }
1200
+ }
1201
+ });
1202
+
1203
+ expect(imageLinks).toHaveLength(2);
1204
+ expect(imageLinks[0].href).toBe("/attachments/se2n8jaGon.png");
1205
+ expect(imageLinks[0].content).toEqual([{ type: "text", text: "logs", styles: {} }]);
1206
+ expect(imageLinks[1].href).toBe("/attachments/p5DgklVeMg.png");
1207
+ expect(imageLinks[1].content).toEqual([{ type: "text", text: "", styles: {} }]);
1208
+
1209
+ // Test round-trip conversion
1210
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1211
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1212
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1213
+ });
1214
+
1215
+ it("parses numbered lists under a Steps heading as test steps", () => {
1216
+ const markdown = [
1217
+ "#### Steps:",
1218
+ "1. Navigate to the product listing page.",
1219
+ " Expected open",
1220
+ "3. Select a product and click the \"Add to Cart\" button.",
1221
+ " Expected result close",
1222
+ "5. Open the shopping cart page.",
1223
+ " **Expected** edit",
1224
+ "6. Verify that the added item is displayed with the correct name, price, and quantity.",
1225
+ " _Expected_ close",
1226
+ "",
1227
+ "### **Expected Result:** The item appears in the cart with correct details and price calculation.",
1228
+ "",
1229
+ "![logs](/attachments/se2n8jaGon.png)",
1230
+ "",
1231
+ "![](/attachments/p5DgklVeMg.png)",
1232
+ ].join("\n");
1233
+
1234
+ const blocks = markdownToBlocks(markdown);
1235
+ const testSteps = blocks.filter(block => block.type === "testStep");
1236
+
1237
+ // Should have 4 test steps
1238
+ expect(testSteps).toHaveLength(4);
1239
+
1240
+ // Check the first test step
1241
+ expect(testSteps[0]).toEqual({
1242
+ type: "testStep",
1243
+ props: {
1244
+ stepTitle: "Navigate to the product listing page.",
1245
+ stepData: "Expected open",
1246
+ expectedResult: "",
1247
+ listStyle: "ordered",
1248
+ },
1249
+ children: [],
1250
+ });
1251
+
1252
+ // Check the second test step
1253
+ expect(testSteps[1]).toEqual({
1254
+ type: "testStep",
1255
+ props: {
1256
+ stepTitle: "Select a product and click the \"Add to Cart\" button.",
1257
+ stepData: "Expected result close",
1258
+ expectedResult: "",
1259
+ listStyle: "ordered",
976
1260
  },
977
1261
  children: [],
978
1262
  });
1263
+
1264
+ // Check the third test step
1265
+ expect(testSteps[2]).toEqual({
1266
+ type: "testStep",
1267
+ props: {
1268
+ stepTitle: "Open the shopping cart page.",
1269
+ stepData: "**Expected** edit",
1270
+ expectedResult: "",
1271
+ listStyle: "ordered",
1272
+ },
1273
+ children: [],
1274
+ });
1275
+
1276
+ // Check the fourth test step
1277
+ expect(testSteps[3]).toEqual({
1278
+ type: "testStep",
1279
+ props: {
1280
+ stepTitle: "Verify that the added item is displayed with the correct name, price, and quantity.",
1281
+ stepData: "_Expected_ close",
1282
+ expectedResult: "",
1283
+ listStyle: "ordered",
1284
+ },
1285
+ children: [],
1286
+ });
1287
+
1288
+ // Test round-trip conversion — numbered steps preserve their ordered style
1289
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1290
+ expect(roundTripMarkdown).toContain("1. Navigate to the product listing page.");
1291
+ expect(roundTripMarkdown).toContain("2. Select a product and click the \"Add to Cart\" button.");
1292
+ expect(roundTripMarkdown).toContain("3. Open the shopping cart page.");
1293
+ expect(roundTripMarkdown).toContain("4. Verify that the added item is displayed with the correct name, price, and quantity.");
1294
+ // Check that step data is preserved
1295
+ expect(roundTripMarkdown).toContain(" Expected open");
1296
+ expect(roundTripMarkdown).toContain(" Expected result close");
1297
+ expect(roundTripMarkdown).toContain(" **Expected** edit");
1298
+ expect(roundTripMarkdown).toContain(" _Expected_ close");
1299
+ });
1300
+
1301
+ it("handles standalone images without bullet list interference", () => {
1302
+ const markdown = [
1303
+ "### Steps:",
1304
+ "1. Navigate to the product listing page.",
1305
+ " Expected open",
1306
+ "",
1307
+ "### **Expected Result:** The item appears in the cart with correct details and price calculation.",
1308
+ "",
1309
+ "![logs](/attachments/se2n8jaGon.png)",
1310
+ "",
1311
+ "![](/attachments/p5DgklVeMg.png)",
1312
+ ].join("\n");
1313
+
1314
+ const blocks = markdownToBlocks(markdown);
1315
+
1316
+ // Find image paragraphs
1317
+ const imageParagraphs = blocks.filter(block =>
1318
+ block.type === "paragraph" &&
1319
+ block.content &&
1320
+ Array.isArray(block.content) &&
1321
+ block.content.some((item: any) => item.type === "link")
1322
+ );
1323
+
1324
+ // Should have exactly 2 image paragraphs
1325
+ expect(imageParagraphs).toHaveLength(2);
1326
+
1327
+ // First image with alt text
1328
+ expect(imageParagraphs[0].content).toContainEqual({
1329
+ type: "text",
1330
+ text: "!",
1331
+ styles: {}
1332
+ });
1333
+ expect(imageParagraphs[0].content).toContainEqual({
1334
+ type: "link",
1335
+ href: "/attachments/se2n8jaGon.png",
1336
+ content: [{ type: "text", text: "logs", styles: {} }]
1337
+ });
1338
+
1339
+ // Second image without alt text
1340
+ expect(imageParagraphs[1].content).toContainEqual({
1341
+ type: "text",
1342
+ text: "!",
1343
+ styles: {}
1344
+ });
1345
+ expect(imageParagraphs[1].content).toContainEqual({
1346
+ type: "link",
1347
+ href: "/attachments/p5DgklVeMg.png",
1348
+ content: [{ type: "text", text: "", styles: {} }]
1349
+ });
1350
+
1351
+ // No extra empty paragraphs
1352
+ const emptyParagraphs = blocks.filter(block =>
1353
+ block.type === "paragraph" &&
1354
+ (!block.content || block.content.length === 0)
1355
+ );
1356
+ expect(emptyParagraphs).toHaveLength(0);
1357
+
1358
+ // Test round-trip conversion
1359
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1360
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1361
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1362
+ });
1363
+
1364
+ it("handles images with multiple blank lines between them", () => {
1365
+ const markdown = `
1366
+
1367
+ ![logs](/attachments/se2n8jaGon.png)
1368
+
1369
+
1370
+
1371
+ ![](/attachments/p5DgklVeMg.png)
1372
+
1373
+
1374
+
1375
+ `;
1376
+
1377
+ const blocks = markdownToBlocks(markdown);
1378
+
1379
+ // Should have exactly 2 image paragraphs, no empty paragraphs
1380
+ const imageParagraphs = blocks.filter(block =>
1381
+ block.type === "paragraph" &&
1382
+ block.content &&
1383
+ Array.isArray(block.content) &&
1384
+ block.content.some((item: any) => item.type === "link")
1385
+ );
1386
+
1387
+ const emptyParagraphs = blocks.filter(block =>
1388
+ block.type === "paragraph" &&
1389
+ (!block.content || block.content.length === 0)
1390
+ );
1391
+
1392
+ // Check for malformed image blocks (paragraphs with just "!" but no link)
1393
+ const malformedBlocks = blocks.filter(block =>
1394
+ block.type === "paragraph" &&
1395
+ block.content &&
1396
+ Array.isArray(block.content) &&
1397
+ block.content.some((item: any) => item.type === "text" && item.text === "!") &&
1398
+ !block.content.some((item: any) => item.type === "link")
1399
+ );
1400
+
1401
+ expect(imageParagraphs).toHaveLength(2);
1402
+ expect(emptyParagraphs).toHaveLength(0);
1403
+ expect(malformedBlocks).toHaveLength(0);
1404
+
1405
+ // Test round-trip conversion
1406
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1407
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1408
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1409
+ });
1410
+
1411
+ it("reproduces the exact issue from user's example", () => {
1412
+ const markdown = `#### Steps:
1413
+ 1. Navigate to the product listing page.
1414
+ Expected open
1415
+ 3. Select a product and click the "Add to Cart" button.
1416
+ Expected result close
1417
+ 5. Open the shopping cart page.
1418
+ **Expected** edit
1419
+ 6. Verify that the added item is displayed with the correct name, price, and quantity.
1420
+ _Expected_ close
1421
+
1422
+ ### **Expected Result:** The item appears in the cart with correct details and price calculation.
1423
+
1424
+
1425
+
1426
+ ![logs](/attachments/se2n8jaGon.png)
1427
+
1428
+
1429
+
1430
+ ![](/attachments/p5DgklVeMg.png)
1431
+
1432
+
1433
+
1434
+ `;
1435
+
1436
+ const blocks = markdownToBlocks(markdown);
1437
+
1438
+ // Test round-trip conversion
1439
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1440
+
1441
+ // Check that both images are preserved
1442
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1443
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1444
+
1445
+ // Make sure we don't have a standalone "!" without the rest of the image
1446
+ const lines = roundTripMarkdown.split('\n');
1447
+ const exclamationLines = lines.filter(line => line.trim() === '!' || line.trim() === '! ');
1448
+ expect(exclamationLines.length).toBe(0);
1449
+ });
1450
+
1451
+ it("handles empty alt text images correctly", () => {
1452
+ const markdown = "![](/attachments/test.png)";
1453
+ const blocks = markdownToBlocks(markdown);
1454
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1455
+
1456
+ expect(roundTripMarkdown).toContain("![](/attachments/test.png)");
1457
+ });
1458
+
1459
+ it("removes malformed image blocks through post-processing", () => {
1460
+ // Simulate the malformed blocks you're seeing
1461
+ const malformedBlocks: any[] = [
1462
+ {
1463
+ type: "heading",
1464
+ props: { level: 3 },
1465
+ content: [{ type: "text", text: "Expected Result:", styles: {} }],
1466
+ children: []
1467
+ },
1468
+ {
1469
+ type: "paragraph",
1470
+ props: {},
1471
+ content: [
1472
+ { type: "text", text: "!", styles: {} },
1473
+ { type: "link", href: "/attachments/se2n8jaGon.png", content: [{ type: "text", text: "logs", styles: {} }] }
1474
+ ],
1475
+ children: []
1476
+ },
1477
+ {
1478
+ type: "paragraph",
1479
+ props: {},
1480
+ content: [{ type: "text", text: "!", styles: {} }],
1481
+ children: []
1482
+ },
1483
+ {
1484
+ type: "paragraph",
1485
+ props: {},
1486
+ content: [],
1487
+ children: []
1488
+ }
1489
+ ];
1490
+
1491
+ // Apply the fixMalformedImageBlocks function
1492
+ const fixedBlocks = fixMalformedImageBlocks(malformedBlocks);
1493
+
1494
+ // Should have removed the malformed image blocks (both the "!" only block and the empty block)
1495
+ expect(fixedBlocks.length).toBe(2);
1496
+ expect(fixedBlocks[0].type).toBe("heading");
1497
+ expect(fixedBlocks[1].type).toBe("paragraph");
1498
+ expect(fixedBlocks[1].content).toContainEqual(
1499
+ { type: "text", text: "!", styles: {} }
1500
+ );
1501
+ expect(fixedBlocks[1].content).toContainEqual(
1502
+ { type: "link", href: "/attachments/se2n8jaGon.png", content: [{ type: "text", text: "logs", styles: {} }] }
1503
+ );
1504
+ });
1505
+
1506
+ it("reproduces the exact Unsplash URL issue", () => {
1507
+ const markdown = `
1508
+
1509
+
1510
+
1511
+ ### **Expected Result:** The item appears in the cart with correct details and price calculation.
1512
+
1513
+
1514
+
1515
+ ![logs](https://images.unsplash.com/photo-1765873360413-6c79486d1fda?q=80&w=2350&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)
1516
+
1517
+
1518
+
1519
+ ![](https://plus.unsplash.com/premium_photo-1765228499795-e58288bc382b?q=80&w=1925&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)
1520
+
1521
+
1522
+
1523
+ `;
1524
+
1525
+ const blocks = markdownToBlocks(markdown);
1526
+
1527
+ // Should have at least 3 blocks
1528
+ expect(blocks.length).toBeGreaterThanOrEqual(3);
1529
+
1530
+ // Should have at least one paragraph with content (images)
1531
+ const imageBlocks = blocks.filter(b =>
1532
+ b.type === "paragraph" &&
1533
+ b.content &&
1534
+ Array.isArray(b.content) &&
1535
+ b.content.some((item: any) => item.type === "link")
1536
+ );
1537
+ expect(imageBlocks.length).toBeGreaterThan(0);
1538
+
1539
+ // Test round-trip conversion - check that we get the images back
1540
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1541
+
1542
+ // Most importantly: should not have a standalone "!" at the end
1543
+ expect(roundTripMarkdown).not.toMatch(/\n!\s*$/);
979
1544
  });
980
1545
  });