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.
@@ -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";
@@ -489,7 +490,7 @@ describe("markdownToBlocks", () => {
489
490
  props: {
490
491
  stepTitle: "Step 4: Verify that the notifications are displayed correctly in the application's notification panel.",
491
492
  stepData: "",
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).",
493
+ 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",
493
494
  },
494
495
  children: [],
495
496
  },
@@ -633,7 +634,7 @@ describe("markdownToBlocks", () => {
633
634
  "",
634
635
  "* Verify that each individual unit test completes in ≤ 50 ms (target) and never exceeds 200 ms (hard limit).",
635
636
  "",
636
- "### Steps",
637
+ "### Instructions",
637
638
  "",
638
639
  "1. Execute the full unit test suite with a timer wrapper.",
639
640
  " * Each individual test case.",
@@ -662,7 +663,7 @@ describe("markdownToBlocks", () => {
662
663
  props: {
663
664
  stepTitle: "Open the form.",
664
665
  stepData: "",
665
- expectedResult: "Fields are empty.",
666
+ expectedResult: "** The form opens.\nFields are empty.",
666
667
  },
667
668
  children: [],
668
669
  },
@@ -788,6 +789,107 @@ describe("markdownToBlocks", () => {
788
789
  );
789
790
  });
790
791
 
792
+ it("parses steps under a Steps heading even when expected results are missing", () => {
793
+ const markdown = [
794
+ "### Steps",
795
+ "",
796
+ "* Pass onboarding as mobile user",
797
+ "* Navigate to More tab -≻ My Profile -≻ Log into the app with user from preconditions",
798
+ " *Expected:* Upsell SS screen is displayed",
799
+ "* Close SS",
800
+ " *Expected:* My Course and More tab are displayed",
801
+ ].join("\n");
802
+
803
+ const blocks = markdownToBlocks(markdown);
804
+ const stepBlocks = blocks.filter((block) => block.type === "testStep");
805
+
806
+ expect(stepBlocks).toEqual([
807
+ {
808
+ type: "testStep",
809
+ props: {
810
+ stepTitle: "Pass onboarding as mobile user",
811
+ stepData: "",
812
+ expectedResult: "",
813
+ },
814
+ children: [],
815
+ },
816
+ {
817
+ type: "testStep",
818
+ props: {
819
+ stepTitle:
820
+ "Navigate to More tab -≻ My Profile -≻ Log into the app with user from preconditions",
821
+ stepData: "",
822
+ expectedResult: "* Upsell SS screen is displayed",
823
+ },
824
+ children: [],
825
+ },
826
+ {
827
+ type: "testStep",
828
+ props: {
829
+ stepTitle: "Close SS",
830
+ stepData: "",
831
+ expectedResult: "* My Course and More tab are displayed",
832
+ },
833
+ children: [],
834
+ },
835
+ ]);
836
+ });
837
+
838
+ it("handles multiple steps with expected results without extra asterisks", () => {
839
+ const markdown = [
840
+ "### Preconditions",
841
+ "",
842
+ "User on the Sign In with Email Screen",
843
+ "",
844
+ "### Steps",
845
+ "",
846
+ "* Existing email + invalid password",
847
+ " *Expected*: 'Oops, wrong email or password' is displayed",
848
+ "* Not existing email + valid password",
849
+ " *Expected*: 'Oops, wrong email or password' is displayed",
850
+ ].join("\n");
851
+
852
+ const blocks = markdownToBlocks(markdown);
853
+ const stepBlocks = blocks.filter((block) => block.type === "testStep");
854
+
855
+ expect(stepBlocks).toHaveLength(2);
856
+
857
+ expect(stepBlocks[0]).toEqual({
858
+ type: "testStep",
859
+ props: {
860
+ stepTitle: "Existing email + invalid password",
861
+ stepData: "",
862
+ expectedResult: "'Oops, wrong email or password' is displayed",
863
+ },
864
+ children: [],
865
+ });
866
+
867
+ expect(stepBlocks[1]).toEqual({
868
+ type: "testStep",
869
+ props: {
870
+ stepTitle: "Not existing email + valid password",
871
+ stepData: "",
872
+ expectedResult: "'Oops, wrong email or password' is displayed",
873
+ },
874
+ children: [],
875
+ });
876
+
877
+ // Verify round-trip doesn't add extra asterisks
878
+ const roundTrip = blocksToMarkdown(stepBlocks.map((block, index) => ({
879
+ ...block,
880
+ id: `step${index}`,
881
+ })) as CustomEditorBlock[]);
882
+
883
+ expect(roundTrip).toBe(
884
+ [
885
+ "* Existing email + invalid password",
886
+ " *Expected*: 'Oops, wrong email or password' is displayed",
887
+ "* Not existing email + valid password",
888
+ " *Expected*: 'Oops, wrong email or password' is displayed",
889
+ ].join("\n"),
890
+ );
891
+ });
892
+
791
893
  it("round-trips simple blocks", () => {
792
894
  const blocks: CustomEditorBlock[] = [
793
895
  {
@@ -880,4 +982,441 @@ describe("markdownToBlocks", () => {
880
982
  },
881
983
  ]);
882
984
  });
985
+
986
+ it("parses multiple Expected blocks within a single test step", () => {
987
+ const markdown = [
988
+ "### Steps",
989
+ "",
990
+ "* Swipe Back",
991
+ "* Check UI of Sleep score info screen",
992
+ " - Back button",
993
+ " Header: Sleep Score Info",
994
+ " 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.",
995
+ " *Expected:* - 1st block:",
996
+ " *Expected:* - 2nd block:",
997
+ " *Expected:* - 3d block:",
998
+ "* Tap 'Back' button",
999
+ ].join("\n");
1000
+
1001
+ const blocks = markdownToBlocks(markdown);
1002
+ const stepBlocks = blocks.filter((block) => block.type === "testStep");
1003
+
1004
+ expect(stepBlocks).toHaveLength(3);
1005
+
1006
+ expect(stepBlocks[0]).toEqual({
1007
+ type: "testStep",
1008
+ props: {
1009
+ stepTitle: "Swipe Back",
1010
+ stepData: "",
1011
+ expectedResult: "",
1012
+ },
1013
+ children: [],
1014
+ });
1015
+
1016
+ expect(stepBlocks[1]).toEqual({
1017
+ type: "testStep",
1018
+ props: {
1019
+ stepTitle: "Check UI of Sleep score info screen",
1020
+ 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.",
1021
+ expectedResult: "* - 1st block:\n* - 2nd block:\n* - 3d block:",
1022
+ },
1023
+ children: [],
1024
+ });
1025
+
1026
+ expect(stepBlocks[2]).toEqual({
1027
+ type: "testStep",
1028
+ props: {
1029
+ stepTitle: "Tap 'Back' button",
1030
+ stepData: "",
1031
+ expectedResult: "",
1032
+ },
1033
+ children: [],
1034
+ });
1035
+ });
1036
+
1037
+ it("correctly parses images at the end of the document", () => {
1038
+ const markdown = [
1039
+ "#### Steps:",
1040
+ "1. Navigate to the product listing page.",
1041
+ " Expected open",
1042
+ "3. Select a product and click the \"Add to Cart\" button.",
1043
+ " Expected result close",
1044
+ "5. Open the shopping cart page.",
1045
+ " **Expected** edit",
1046
+ "6. Verify that the added item is displayed with the correct name, price, and quantity.",
1047
+ " _Expected_ close",
1048
+ "",
1049
+ "### **Expected Result:** The item appears in the cart with correct details and price calculation.",
1050
+ "",
1051
+ "![logs](/attachments/se2n8jaGon.png)",
1052
+ "",
1053
+ "![](/attachments/p5DgklVeMg.png)",
1054
+ ].join("\n");
1055
+
1056
+ const blocks = markdownToBlocks(markdown);
1057
+
1058
+ // Find the paragraph blocks that contain the images (links)
1059
+ const imageBlocks = blocks.filter(block =>
1060
+ block.type === "paragraph" &&
1061
+ block.content &&
1062
+ Array.isArray(block.content) &&
1063
+ block.content.some((item: any) =>
1064
+ (item.type === "text" && item.text === "!") ||
1065
+ (item.type === "link" && item.href && item.href.includes("/attachments/"))
1066
+ )
1067
+ );
1068
+
1069
+ // Should have two paragraph blocks with images
1070
+ expect(imageBlocks.length).toBe(2);
1071
+
1072
+ // Check that both image links are properly parsed
1073
+ const imageLinks: any[] = [];
1074
+ imageBlocks.forEach(block => {
1075
+ if (block.content && Array.isArray(block.content)) {
1076
+ const link = (block.content as any[]).find(item => item.type === "link");
1077
+ if (link) {
1078
+ imageLinks.push(link);
1079
+ }
1080
+ }
1081
+ });
1082
+
1083
+ expect(imageLinks).toHaveLength(2);
1084
+ expect(imageLinks[0].href).toBe("/attachments/se2n8jaGon.png");
1085
+ expect(imageLinks[0].content).toEqual([{ type: "text", text: "logs", styles: {} }]);
1086
+ expect(imageLinks[1].href).toBe("/attachments/p5DgklVeMg.png");
1087
+ expect(imageLinks[1].content).toEqual([{ type: "text", text: "", styles: {} }]);
1088
+
1089
+ // Test round-trip conversion
1090
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1091
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1092
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1093
+ });
1094
+
1095
+ it("parses numbered lists under a Steps heading as test steps", () => {
1096
+ const markdown = [
1097
+ "#### Steps:",
1098
+ "1. Navigate to the product listing page.",
1099
+ " Expected open",
1100
+ "3. Select a product and click the \"Add to Cart\" button.",
1101
+ " Expected result close",
1102
+ "5. Open the shopping cart page.",
1103
+ " **Expected** edit",
1104
+ "6. Verify that the added item is displayed with the correct name, price, and quantity.",
1105
+ " _Expected_ close",
1106
+ "",
1107
+ "### **Expected Result:** The item appears in the cart with correct details and price calculation.",
1108
+ "",
1109
+ "![logs](/attachments/se2n8jaGon.png)",
1110
+ "",
1111
+ "![](/attachments/p5DgklVeMg.png)",
1112
+ ].join("\n");
1113
+
1114
+ const blocks = markdownToBlocks(markdown);
1115
+ const testSteps = blocks.filter(block => block.type === "testStep");
1116
+
1117
+ // Should have 4 test steps
1118
+ expect(testSteps).toHaveLength(4);
1119
+
1120
+ // Check the first test step
1121
+ expect(testSteps[0]).toEqual({
1122
+ type: "testStep",
1123
+ props: {
1124
+ stepTitle: "Navigate to the product listing page.",
1125
+ stepData: "Expected open",
1126
+ expectedResult: "",
1127
+ },
1128
+ children: [],
1129
+ });
1130
+
1131
+ // Check the second test step
1132
+ expect(testSteps[1]).toEqual({
1133
+ type: "testStep",
1134
+ props: {
1135
+ stepTitle: "Select a product and click the \"Add to Cart\" button.",
1136
+ stepData: "Expected result close",
1137
+ expectedResult: "",
1138
+ },
1139
+ children: [],
1140
+ });
1141
+
1142
+ // Check the third test step
1143
+ expect(testSteps[2]).toEqual({
1144
+ type: "testStep",
1145
+ props: {
1146
+ stepTitle: "Open the shopping cart page.",
1147
+ stepData: "**Expected** edit",
1148
+ expectedResult: "",
1149
+ },
1150
+ children: [],
1151
+ });
1152
+
1153
+ // Check the fourth test step
1154
+ expect(testSteps[3]).toEqual({
1155
+ type: "testStep",
1156
+ props: {
1157
+ stepTitle: "Verify that the added item is displayed with the correct name, price, and quantity.",
1158
+ stepData: "_Expected_ close",
1159
+ expectedResult: "",
1160
+ },
1161
+ children: [],
1162
+ });
1163
+
1164
+ // Test round-trip conversion
1165
+ // Note: Test steps are always serialized as bullet lists, not numbered lists
1166
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1167
+ expect(roundTripMarkdown).toContain("* Navigate to the product listing page.");
1168
+ expect(roundTripMarkdown).toContain("* Select a product and click the \"Add to Cart\" button.");
1169
+ expect(roundTripMarkdown).toContain("* Open the shopping cart page.");
1170
+ expect(roundTripMarkdown).toContain("* Verify that the added item is displayed with the correct name, price, and quantity.");
1171
+ // Check that step data is preserved
1172
+ expect(roundTripMarkdown).toContain(" Expected open");
1173
+ expect(roundTripMarkdown).toContain(" Expected result close");
1174
+ expect(roundTripMarkdown).toContain(" **Expected** edit");
1175
+ expect(roundTripMarkdown).toContain(" _Expected_ close");
1176
+ });
1177
+
1178
+ it("handles standalone images without bullet list interference", () => {
1179
+ const markdown = [
1180
+ "### Steps:",
1181
+ "1. Navigate to the product listing page.",
1182
+ " Expected open",
1183
+ "",
1184
+ "### **Expected Result:** The item appears in the cart with correct details and price calculation.",
1185
+ "",
1186
+ "![logs](/attachments/se2n8jaGon.png)",
1187
+ "",
1188
+ "![](/attachments/p5DgklVeMg.png)",
1189
+ ].join("\n");
1190
+
1191
+ const blocks = markdownToBlocks(markdown);
1192
+
1193
+ // Find image paragraphs
1194
+ const imageParagraphs = blocks.filter(block =>
1195
+ block.type === "paragraph" &&
1196
+ block.content &&
1197
+ Array.isArray(block.content) &&
1198
+ block.content.some((item: any) => item.type === "link")
1199
+ );
1200
+
1201
+ // Should have exactly 2 image paragraphs
1202
+ expect(imageParagraphs).toHaveLength(2);
1203
+
1204
+ // First image with alt text
1205
+ expect(imageParagraphs[0].content).toContainEqual({
1206
+ type: "text",
1207
+ text: "!",
1208
+ styles: {}
1209
+ });
1210
+ expect(imageParagraphs[0].content).toContainEqual({
1211
+ type: "link",
1212
+ href: "/attachments/se2n8jaGon.png",
1213
+ content: [{ type: "text", text: "logs", styles: {} }]
1214
+ });
1215
+
1216
+ // Second image without alt text
1217
+ expect(imageParagraphs[1].content).toContainEqual({
1218
+ type: "text",
1219
+ text: "!",
1220
+ styles: {}
1221
+ });
1222
+ expect(imageParagraphs[1].content).toContainEqual({
1223
+ type: "link",
1224
+ href: "/attachments/p5DgklVeMg.png",
1225
+ content: [{ type: "text", text: "", styles: {} }]
1226
+ });
1227
+
1228
+ // No extra empty paragraphs
1229
+ const emptyParagraphs = blocks.filter(block =>
1230
+ block.type === "paragraph" &&
1231
+ (!block.content || block.content.length === 0)
1232
+ );
1233
+ expect(emptyParagraphs).toHaveLength(0);
1234
+
1235
+ // Test round-trip conversion
1236
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1237
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1238
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1239
+ });
1240
+
1241
+ it("handles images with multiple blank lines between them", () => {
1242
+ const markdown = `
1243
+
1244
+ ![logs](/attachments/se2n8jaGon.png)
1245
+
1246
+
1247
+
1248
+ ![](/attachments/p5DgklVeMg.png)
1249
+
1250
+
1251
+
1252
+ `;
1253
+
1254
+ const blocks = markdownToBlocks(markdown);
1255
+
1256
+ // Should have exactly 2 image paragraphs, no empty paragraphs
1257
+ const imageParagraphs = blocks.filter(block =>
1258
+ block.type === "paragraph" &&
1259
+ block.content &&
1260
+ Array.isArray(block.content) &&
1261
+ block.content.some((item: any) => item.type === "link")
1262
+ );
1263
+
1264
+ const emptyParagraphs = blocks.filter(block =>
1265
+ block.type === "paragraph" &&
1266
+ (!block.content || block.content.length === 0)
1267
+ );
1268
+
1269
+ // Check for malformed image blocks (paragraphs with just "!" but no link)
1270
+ const malformedBlocks = blocks.filter(block =>
1271
+ block.type === "paragraph" &&
1272
+ block.content &&
1273
+ Array.isArray(block.content) &&
1274
+ block.content.some((item: any) => item.type === "text" && item.text === "!") &&
1275
+ !block.content.some((item: any) => item.type === "link")
1276
+ );
1277
+
1278
+ expect(imageParagraphs).toHaveLength(2);
1279
+ expect(emptyParagraphs).toHaveLength(0);
1280
+ expect(malformedBlocks).toHaveLength(0);
1281
+
1282
+ // Test round-trip conversion
1283
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1284
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1285
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1286
+ });
1287
+
1288
+ it("reproduces the exact issue from user's example", () => {
1289
+ const markdown = `#### Steps:
1290
+ 1. Navigate to the product listing page.
1291
+ Expected open
1292
+ 3. Select a product and click the "Add to Cart" button.
1293
+ Expected result close
1294
+ 5. Open the shopping cart page.
1295
+ **Expected** edit
1296
+ 6. Verify that the added item is displayed with the correct name, price, and quantity.
1297
+ _Expected_ close
1298
+
1299
+ ### **Expected Result:** The item appears in the cart with correct details and price calculation.
1300
+
1301
+
1302
+
1303
+ ![logs](/attachments/se2n8jaGon.png)
1304
+
1305
+
1306
+
1307
+ ![](/attachments/p5DgklVeMg.png)
1308
+
1309
+
1310
+
1311
+ `;
1312
+
1313
+ const blocks = markdownToBlocks(markdown);
1314
+
1315
+ // Test round-trip conversion
1316
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1317
+
1318
+ // Check that both images are preserved
1319
+ expect(roundTripMarkdown).toContain("![logs](/attachments/se2n8jaGon.png)");
1320
+ expect(roundTripMarkdown).toContain("![](/attachments/p5DgklVeMg.png)");
1321
+
1322
+ // Make sure we don't have a standalone "!" without the rest of the image
1323
+ const lines = roundTripMarkdown.split('\n');
1324
+ const exclamationLines = lines.filter(line => line.trim() === '!' || line.trim() === '! ');
1325
+ expect(exclamationLines.length).toBe(0);
1326
+ });
1327
+
1328
+ it("handles empty alt text images correctly", () => {
1329
+ const markdown = "![](/attachments/test.png)";
1330
+ const blocks = markdownToBlocks(markdown);
1331
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1332
+
1333
+ expect(roundTripMarkdown).toContain("![](/attachments/test.png)");
1334
+ });
1335
+
1336
+ it("removes malformed image blocks through post-processing", () => {
1337
+ // Simulate the malformed blocks you're seeing
1338
+ const malformedBlocks: any[] = [
1339
+ {
1340
+ type: "heading",
1341
+ props: { level: 3 },
1342
+ content: [{ type: "text", text: "Expected Result:", styles: {} }],
1343
+ children: []
1344
+ },
1345
+ {
1346
+ type: "paragraph",
1347
+ props: {},
1348
+ content: [
1349
+ { type: "text", text: "!", styles: {} },
1350
+ { type: "link", href: "/attachments/se2n8jaGon.png", content: [{ type: "text", text: "logs", styles: {} }] }
1351
+ ],
1352
+ children: []
1353
+ },
1354
+ {
1355
+ type: "paragraph",
1356
+ props: {},
1357
+ content: [{ type: "text", text: "!", styles: {} }],
1358
+ children: []
1359
+ },
1360
+ {
1361
+ type: "paragraph",
1362
+ props: {},
1363
+ content: [],
1364
+ children: []
1365
+ }
1366
+ ];
1367
+
1368
+ // Apply the fixMalformedImageBlocks function
1369
+ const fixedBlocks = fixMalformedImageBlocks(malformedBlocks);
1370
+
1371
+ // Should have removed the malformed image blocks (both the "!" only block and the empty block)
1372
+ expect(fixedBlocks.length).toBe(2);
1373
+ expect(fixedBlocks[0].type).toBe("heading");
1374
+ expect(fixedBlocks[1].type).toBe("paragraph");
1375
+ expect(fixedBlocks[1].content).toContainEqual(
1376
+ { type: "text", text: "!", styles: {} }
1377
+ );
1378
+ expect(fixedBlocks[1].content).toContainEqual(
1379
+ { type: "link", href: "/attachments/se2n8jaGon.png", content: [{ type: "text", text: "logs", styles: {} }] }
1380
+ );
1381
+ });
1382
+
1383
+ it("reproduces the exact Unsplash URL issue", () => {
1384
+ const markdown = `
1385
+
1386
+
1387
+
1388
+ ### **Expected Result:** The item appears in the cart with correct details and price calculation.
1389
+
1390
+
1391
+
1392
+ ![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)
1393
+
1394
+
1395
+
1396
+ ![](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)
1397
+
1398
+
1399
+
1400
+ `;
1401
+
1402
+ const blocks = markdownToBlocks(markdown);
1403
+
1404
+ // Should have at least 3 blocks
1405
+ expect(blocks.length).toBeGreaterThanOrEqual(3);
1406
+
1407
+ // Should have at least one paragraph with content (images)
1408
+ const imageBlocks = blocks.filter(b =>
1409
+ b.type === "paragraph" &&
1410
+ b.content &&
1411
+ Array.isArray(b.content) &&
1412
+ b.content.some((item: any) => item.type === "link")
1413
+ );
1414
+ expect(imageBlocks.length).toBeGreaterThan(0);
1415
+
1416
+ // Test round-trip conversion - check that we get the images back
1417
+ const roundTripMarkdown = blocksToMarkdown(blocks as CustomEditorBlock[]);
1418
+
1419
+ // Most importantly: should not have a standalone "!" at the end
1420
+ expect(roundTripMarkdown).not.toMatch(/\n!\s*$/);
1421
+ });
883
1422
  });