testomatio-editor-blocks 0.4.64 → 0.4.66
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.
- package/package/editor/blocks/testMeta.d.ts +37 -0
- package/package/editor/blocks/testMeta.js +111 -0
- package/package/editor/customMarkdownConverter.js +129 -22
- package/package/editor/customSchema.d.ts +32 -0
- package/package/editor/customSchema.js +2 -0
- package/package/editor/testMetaFields.d.ts +17 -0
- package/package/editor/testMetaFields.js +33 -0
- package/package/index.d.ts +2 -0
- package/package/index.js +2 -0
- package/package/styles.css +201 -0
- package/package.json +1 -1
- package/src/App.tsx +21 -2
- package/src/editor/blocks/testMeta.tsx +242 -0
- package/src/editor/customMarkdownConverter.test.ts +135 -84
- package/src/editor/customMarkdownConverter.ts +127 -11
- package/src/editor/customSchema.tsx +2 -0
- package/src/editor/styles.css +201 -0
- package/src/editor/testMetaFields.ts +53 -0
- package/src/index.ts +7 -0
|
@@ -2685,90 +2685,6 @@ describe("markdownToBlocks", () => {
|
|
|
2685
2685
|
},
|
|
2686
2686
|
]);
|
|
2687
2687
|
});
|
|
2688
|
-
|
|
2689
|
-
it("preserves opening-fence content when it is not a clean language identifier", () => {
|
|
2690
|
-
const markdown = [
|
|
2691
|
-
"```curl `http://localhost:3000/projects/classic-project/test/e1c1b38c/edit` \\",
|
|
2692
|
-
"{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two",
|
|
2693
|
-
"```",
|
|
2694
|
-
].join("\n");
|
|
2695
|
-
const blocks = markdownToBlocks(markdown);
|
|
2696
|
-
expect(blocks).toEqual([
|
|
2697
|
-
{
|
|
2698
|
-
type: "codeBlock",
|
|
2699
|
-
props: { language: "" },
|
|
2700
|
-
content: [
|
|
2701
|
-
{
|
|
2702
|
-
type: "text",
|
|
2703
|
-
text: [
|
|
2704
|
-
"curl `http://localhost:3000/projects/classic-project/test/e1c1b38c/edit` \\",
|
|
2705
|
-
"{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two",
|
|
2706
|
-
].join("\n"),
|
|
2707
|
-
styles: {},
|
|
2708
|
-
},
|
|
2709
|
-
],
|
|
2710
|
-
children: [],
|
|
2711
|
-
},
|
|
2712
|
-
]);
|
|
2713
|
-
});
|
|
2714
|
-
|
|
2715
|
-
it("round-trips an opening-fence-with-content code block to stable markdown", () => {
|
|
2716
|
-
const markdown = [
|
|
2717
|
-
"```curl `http://localhost:3000/projects/classic-project/test/e1c1b38c/edit` \\",
|
|
2718
|
-
"{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two",
|
|
2719
|
-
"```",
|
|
2720
|
-
].join("\n");
|
|
2721
|
-
const blocks = markdownToBlocks(markdown);
|
|
2722
|
-
const serialized = blocksToMarkdown(blocks as CustomEditorBlock[]);
|
|
2723
|
-
expect(serialized).toBe(
|
|
2724
|
-
[
|
|
2725
|
-
"```",
|
|
2726
|
-
"curl `http://localhost:3000/projects/classic-project/test/e1c1b38c/edit` \\",
|
|
2727
|
-
"{{baseURL}}/endpoint?query_param_one=value_one&query_param_two=value_two",
|
|
2728
|
-
"```",
|
|
2729
|
-
].join("\n"),
|
|
2730
|
-
);
|
|
2731
|
-
expect(markdownToBlocks(serialized)).toEqual(blocks);
|
|
2732
|
-
});
|
|
2733
|
-
|
|
2734
|
-
it("preserves hyphenated language identifiers like shell-session", () => {
|
|
2735
|
-
const markdown = ["```shell-session", "$ ls", "```"].join("\n");
|
|
2736
|
-
const blocks = markdownToBlocks(markdown);
|
|
2737
|
-
expect(blocks).toEqual([
|
|
2738
|
-
{
|
|
2739
|
-
type: "codeBlock",
|
|
2740
|
-
props: { language: "shell-session" },
|
|
2741
|
-
content: [{ type: "text", text: "$ ls", styles: {} }],
|
|
2742
|
-
children: [],
|
|
2743
|
-
},
|
|
2744
|
-
]);
|
|
2745
|
-
});
|
|
2746
|
-
|
|
2747
|
-
it("preserves digit-prefixed language identifiers like 1c-enterprise", () => {
|
|
2748
|
-
const markdown = ["```1c-enterprise", "code", "```"].join("\n");
|
|
2749
|
-
const blocks = markdownToBlocks(markdown);
|
|
2750
|
-
expect(blocks).toEqual([
|
|
2751
|
-
{
|
|
2752
|
-
type: "codeBlock",
|
|
2753
|
-
props: { language: "1c-enterprise" },
|
|
2754
|
-
content: [{ type: "text", text: "code", styles: {} }],
|
|
2755
|
-
children: [],
|
|
2756
|
-
},
|
|
2757
|
-
]);
|
|
2758
|
-
});
|
|
2759
|
-
|
|
2760
|
-
it("sanitizes a malformed in-memory language prop on serialize", () => {
|
|
2761
|
-
const blocks: CustomEditorBlock[] = [
|
|
2762
|
-
{
|
|
2763
|
-
id: "1",
|
|
2764
|
-
type: "codeBlock",
|
|
2765
|
-
props: { ...baseProps, language: "curl http://x" } as any,
|
|
2766
|
-
content: [{ type: "text", text: "body", styles: {} }] as any,
|
|
2767
|
-
children: [],
|
|
2768
|
-
},
|
|
2769
|
-
];
|
|
2770
|
-
expect(blocksToMarkdown(blocks)).toBe(["```", "body", "```"].join("\n"));
|
|
2771
|
-
});
|
|
2772
2688
|
});
|
|
2773
2689
|
|
|
2774
2690
|
describe("file block serialization", () => {
|
|
@@ -3137,3 +3053,138 @@ describe("blank line <-> empty paragraph mapping", () => {
|
|
|
3137
3053
|
expect(blocksToMarkdown(blocks as CustomEditorBlock[])).toBe(markdown);
|
|
3138
3054
|
});
|
|
3139
3055
|
});
|
|
3056
|
+
|
|
3057
|
+
describe("test/suite metadata comments", () => {
|
|
3058
|
+
it("parses a one-liner test comment into a testMeta block", () => {
|
|
3059
|
+
const blocks = markdownToBlocks("<!-- test id: @T12345678 -->");
|
|
3060
|
+
expect(blocks).toEqual([
|
|
3061
|
+
{
|
|
3062
|
+
type: "testMeta",
|
|
3063
|
+
props: {
|
|
3064
|
+
metaKind: "test",
|
|
3065
|
+
metaFields: JSON.stringify([{ key: "id", value: "@T12345678" }]),
|
|
3066
|
+
metaInline: true,
|
|
3067
|
+
},
|
|
3068
|
+
children: [],
|
|
3069
|
+
},
|
|
3070
|
+
]);
|
|
3071
|
+
});
|
|
3072
|
+
|
|
3073
|
+
it("round-trips a one-liner test comment", () => {
|
|
3074
|
+
const markdown = "<!-- test id: @T12345678 -->";
|
|
3075
|
+
const blocks = markdownToBlocks(markdown);
|
|
3076
|
+
expect(blocksToMarkdown(blocks as CustomEditorBlock[])).toBe(markdown);
|
|
3077
|
+
});
|
|
3078
|
+
|
|
3079
|
+
it("parses a multi-line suite block with ordered fields", () => {
|
|
3080
|
+
const markdown = [
|
|
3081
|
+
"<!-- suite",
|
|
3082
|
+
"id: @S12345678",
|
|
3083
|
+
"emoji: 🔐",
|
|
3084
|
+
"tags: smoke, regression",
|
|
3085
|
+
"assignee: qa@example.com",
|
|
3086
|
+
"-->",
|
|
3087
|
+
].join("\n");
|
|
3088
|
+
const blocks = markdownToBlocks(markdown);
|
|
3089
|
+
expect(blocks).toEqual([
|
|
3090
|
+
{
|
|
3091
|
+
type: "testMeta",
|
|
3092
|
+
props: {
|
|
3093
|
+
metaKind: "suite",
|
|
3094
|
+
metaFields: JSON.stringify([
|
|
3095
|
+
{ key: "id", value: "@S12345678" },
|
|
3096
|
+
{ key: "emoji", value: "🔐" },
|
|
3097
|
+
{ key: "tags", value: "smoke, regression" },
|
|
3098
|
+
{ key: "assignee", value: "qa@example.com" },
|
|
3099
|
+
]),
|
|
3100
|
+
metaInline: false,
|
|
3101
|
+
},
|
|
3102
|
+
children: [],
|
|
3103
|
+
},
|
|
3104
|
+
]);
|
|
3105
|
+
});
|
|
3106
|
+
|
|
3107
|
+
it("round-trips a multi-line suite block", () => {
|
|
3108
|
+
const markdown = [
|
|
3109
|
+
"<!-- suite",
|
|
3110
|
+
"id: @S12345678",
|
|
3111
|
+
"emoji: 🔐",
|
|
3112
|
+
"tags: smoke, regression",
|
|
3113
|
+
"assignee: qa@example.com",
|
|
3114
|
+
"-->",
|
|
3115
|
+
].join("\n");
|
|
3116
|
+
const blocks = markdownToBlocks(markdown);
|
|
3117
|
+
expect(blocksToMarkdown(blocks as CustomEditorBlock[])).toBe(markdown);
|
|
3118
|
+
});
|
|
3119
|
+
|
|
3120
|
+
it("ignores lines without a colon inside a metadata block", () => {
|
|
3121
|
+
const markdown = [
|
|
3122
|
+
"<!-- test",
|
|
3123
|
+
"id: @T12345678",
|
|
3124
|
+
"this line is ignored",
|
|
3125
|
+
"priority: high",
|
|
3126
|
+
"-->",
|
|
3127
|
+
].join("\n");
|
|
3128
|
+
const blocks = markdownToBlocks(markdown);
|
|
3129
|
+
expect((blocks[0].props as any).metaFields).toBe(
|
|
3130
|
+
JSON.stringify([
|
|
3131
|
+
{ key: "id", value: "@T12345678" },
|
|
3132
|
+
{ key: "priority", value: "high" },
|
|
3133
|
+
]),
|
|
3134
|
+
);
|
|
3135
|
+
});
|
|
3136
|
+
|
|
3137
|
+
it("serializes a one-liner that gained extra fields as a block", () => {
|
|
3138
|
+
const blocks: CustomEditorBlock[] = [
|
|
3139
|
+
{
|
|
3140
|
+
id: "m1",
|
|
3141
|
+
type: "testMeta",
|
|
3142
|
+
props: {
|
|
3143
|
+
metaKind: "test",
|
|
3144
|
+
metaFields: JSON.stringify([
|
|
3145
|
+
{ key: "id", value: "@T12345678" },
|
|
3146
|
+
{ key: "priority", value: "high" },
|
|
3147
|
+
]),
|
|
3148
|
+
metaInline: true,
|
|
3149
|
+
} as any,
|
|
3150
|
+
content: undefined as any,
|
|
3151
|
+
children: [],
|
|
3152
|
+
},
|
|
3153
|
+
];
|
|
3154
|
+
expect(blocksToMarkdown(blocks)).toBe(
|
|
3155
|
+
["<!-- test", "id: @T12345678", "priority: high", "-->"].join("\n"),
|
|
3156
|
+
);
|
|
3157
|
+
});
|
|
3158
|
+
|
|
3159
|
+
it("skips fields with empty values when serializing", () => {
|
|
3160
|
+
const blocks: CustomEditorBlock[] = [
|
|
3161
|
+
{
|
|
3162
|
+
id: "m2",
|
|
3163
|
+
type: "testMeta",
|
|
3164
|
+
props: {
|
|
3165
|
+
metaKind: "test",
|
|
3166
|
+
metaFields: JSON.stringify([
|
|
3167
|
+
{ key: "id", value: "@T12345678" },
|
|
3168
|
+
{ key: "priority", value: "high" },
|
|
3169
|
+
{ key: "tags", value: "" },
|
|
3170
|
+
{ key: "", value: "orphan" },
|
|
3171
|
+
]),
|
|
3172
|
+
metaInline: false,
|
|
3173
|
+
} as any,
|
|
3174
|
+
content: undefined as any,
|
|
3175
|
+
children: [],
|
|
3176
|
+
},
|
|
3177
|
+
];
|
|
3178
|
+
expect(blocksToMarkdown(blocks)).toBe(
|
|
3179
|
+
["<!-- test", "id: @T12345678", "priority: high", "-->"].join("\n"),
|
|
3180
|
+
);
|
|
3181
|
+
});
|
|
3182
|
+
|
|
3183
|
+
it("leaves a generic HTML comment as paragraph text", () => {
|
|
3184
|
+
const blocks = markdownToBlocks("<!-- ai/agent generated description -->");
|
|
3185
|
+
expect(blocks[0].type).toBe("paragraph");
|
|
3186
|
+
expect(blocksToMarkdown(blocks as CustomEditorBlock[])).toBe(
|
|
3187
|
+
"<!-- ai/agent generated description -->",
|
|
3188
|
+
);
|
|
3189
|
+
});
|
|
3190
|
+
});
|
|
@@ -334,8 +334,7 @@ function serializeBlock(
|
|
|
334
334
|
return lines;
|
|
335
335
|
}
|
|
336
336
|
case "codeBlock": {
|
|
337
|
-
const
|
|
338
|
-
const language = /[\s`]/.test(rawLanguage) ? "" : rawLanguage;
|
|
337
|
+
const language = (block.props as any).language || "";
|
|
339
338
|
const fence = "```" + language;
|
|
340
339
|
const body = inlineContentToPlainText(block.content);
|
|
341
340
|
lines.push(fence);
|
|
@@ -390,6 +389,36 @@ function serializeBlock(
|
|
|
390
389
|
}
|
|
391
390
|
return lines;
|
|
392
391
|
}
|
|
392
|
+
case "testMeta": {
|
|
393
|
+
const kind = (block.props as any).metaKind === "suite" ? "suite" : "test";
|
|
394
|
+
const inline = Boolean((block.props as any).metaInline);
|
|
395
|
+
let fields: { key: string; value: string }[] = [];
|
|
396
|
+
try {
|
|
397
|
+
const parsed = JSON.parse(((block.props as any).metaFields ?? "[]") as string);
|
|
398
|
+
if (Array.isArray(parsed)) {
|
|
399
|
+
fields = parsed
|
|
400
|
+
.filter((f) => f && typeof f === "object" && typeof f.key === "string")
|
|
401
|
+
.map((f) => ({ key: f.key.trim(), value: typeof f.value === "string" ? f.value.trim() : "" }))
|
|
402
|
+
// Skip incomplete fields: both a key and a value are required.
|
|
403
|
+
.filter((f) => f.key.length > 0 && f.value.length > 0);
|
|
404
|
+
}
|
|
405
|
+
} catch {
|
|
406
|
+
fields = [];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Preserve the one-liner form only when it still fits on a single line
|
|
410
|
+
// (a one-liner holds at most one `key: value` pair).
|
|
411
|
+
if (inline && fields.length <= 1) {
|
|
412
|
+
const field = fields[0];
|
|
413
|
+
lines.push(field ? `<!-- ${kind} ${field.key}: ${field.value} -->` : `<!-- ${kind} -->`);
|
|
414
|
+
return lines;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
lines.push(`<!-- ${kind}`);
|
|
418
|
+
fields.forEach((field) => lines.push(`${field.key}: ${field.value}`));
|
|
419
|
+
lines.push("-->");
|
|
420
|
+
return lines;
|
|
421
|
+
}
|
|
393
422
|
case "testStep":
|
|
394
423
|
case "snippet": {
|
|
395
424
|
const isSnippet = block.type === "snippet";
|
|
@@ -1291,16 +1320,8 @@ function parseCodeBlock(lines: string[], index: number): { block: CustomPartialB
|
|
|
1291
1320
|
};
|
|
1292
1321
|
}
|
|
1293
1322
|
|
|
1294
|
-
const
|
|
1295
|
-
let language = "";
|
|
1323
|
+
const language = afterOpening.trim();
|
|
1296
1324
|
const body: string[] = [];
|
|
1297
|
-
if (info.length > 0) {
|
|
1298
|
-
if (/[\s`]/.test(info)) {
|
|
1299
|
-
body.push(afterOpening);
|
|
1300
|
-
} else {
|
|
1301
|
-
language = info;
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
1325
|
let next = index + 1;
|
|
1305
1326
|
while (next < lines.length && !lines[next].startsWith("```") ) {
|
|
1306
1327
|
body.push(lines[next]);
|
|
@@ -1364,6 +1385,92 @@ function parseParagraph(lines: string[], index: number): { block: CustomPartialB
|
|
|
1364
1385
|
};
|
|
1365
1386
|
}
|
|
1366
1387
|
|
|
1388
|
+
const META_COMMENT_OPEN_REGEX = /^<!--\s*(test|suite)(?=\s|-->|$)/i;
|
|
1389
|
+
|
|
1390
|
+
function metaFieldsFromBody(bodyLines: string[]): { key: string; value: string }[] {
|
|
1391
|
+
const fields: { key: string; value: string }[] = [];
|
|
1392
|
+
for (const raw of bodyLines) {
|
|
1393
|
+
const line = raw.trim();
|
|
1394
|
+
if (!line) continue;
|
|
1395
|
+
const colon = line.indexOf(":");
|
|
1396
|
+
// "Each line is `key: value`; lines without `:` are ignored."
|
|
1397
|
+
if (colon === -1) continue;
|
|
1398
|
+
const key = line.slice(0, colon).trim();
|
|
1399
|
+
const value = line.slice(colon + 1).trim();
|
|
1400
|
+
if (!key) continue;
|
|
1401
|
+
fields.push({ key, value });
|
|
1402
|
+
}
|
|
1403
|
+
return fields;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
function parseMetaComment(
|
|
1407
|
+
lines: string[],
|
|
1408
|
+
index: number,
|
|
1409
|
+
): { block: CustomPartialBlock; nextIndex: number } | null {
|
|
1410
|
+
const first = lines[index];
|
|
1411
|
+
const openMatch = first.match(META_COMMENT_OPEN_REGEX);
|
|
1412
|
+
if (!openMatch) {
|
|
1413
|
+
return null;
|
|
1414
|
+
}
|
|
1415
|
+
const kind = openMatch[1].toLowerCase();
|
|
1416
|
+
|
|
1417
|
+
let bodyLines: string[] = [];
|
|
1418
|
+
let inline = false;
|
|
1419
|
+
let nextIndex: number;
|
|
1420
|
+
|
|
1421
|
+
// One-liner: opening and closing markers on the same line.
|
|
1422
|
+
const oneLine = first.match(/^<!--\s*(?:test|suite)\b\s*([\s\S]*?)\s*-->\s*$/i);
|
|
1423
|
+
if (oneLine) {
|
|
1424
|
+
inline = true;
|
|
1425
|
+
if (oneLine[1].trim()) {
|
|
1426
|
+
bodyLines = [oneLine[1].trim()];
|
|
1427
|
+
}
|
|
1428
|
+
nextIndex = index + 1;
|
|
1429
|
+
} else {
|
|
1430
|
+
// Block form: keyword line, fields on their own lines, closing `-->`.
|
|
1431
|
+
const afterKeyword = first.replace(/^<!--\s*(?:test|suite)\b/i, "").trim();
|
|
1432
|
+
if (afterKeyword) {
|
|
1433
|
+
bodyLines.push(afterKeyword);
|
|
1434
|
+
}
|
|
1435
|
+
let next = index + 1;
|
|
1436
|
+
let closed = false;
|
|
1437
|
+
while (next < lines.length) {
|
|
1438
|
+
const current = lines[next];
|
|
1439
|
+
if (/-->\s*$/.test(current)) {
|
|
1440
|
+
const beforeClose = current.replace(/-->\s*$/, "").trim();
|
|
1441
|
+
if (beforeClose) {
|
|
1442
|
+
bodyLines.push(beforeClose);
|
|
1443
|
+
}
|
|
1444
|
+
next += 1;
|
|
1445
|
+
closed = true;
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
bodyLines.push(current);
|
|
1449
|
+
next += 1;
|
|
1450
|
+
}
|
|
1451
|
+
if (!closed) {
|
|
1452
|
+
// Unterminated comment — let normal parsing handle these lines.
|
|
1453
|
+
return null;
|
|
1454
|
+
}
|
|
1455
|
+
nextIndex = next;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
const fields = metaFieldsFromBody(bodyLines);
|
|
1459
|
+
|
|
1460
|
+
return {
|
|
1461
|
+
block: {
|
|
1462
|
+
type: "testMeta",
|
|
1463
|
+
props: {
|
|
1464
|
+
metaKind: kind,
|
|
1465
|
+
metaFields: JSON.stringify(fields),
|
|
1466
|
+
metaInline: inline,
|
|
1467
|
+
},
|
|
1468
|
+
children: [],
|
|
1469
|
+
} as CustomPartialBlock,
|
|
1470
|
+
nextIndex,
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1367
1474
|
function parseSnippetWrapper(
|
|
1368
1475
|
lines: string[],
|
|
1369
1476
|
index: number,
|
|
@@ -1506,6 +1613,15 @@ export function markdownToBlocks(markdown: string, _options?: MarkdownToBlocksOp
|
|
|
1506
1613
|
continue;
|
|
1507
1614
|
}
|
|
1508
1615
|
|
|
1616
|
+
// Test/suite metadata comments can appear anywhere (typically at the top of
|
|
1617
|
+
// a document or right after a heading), so this runs ungated.
|
|
1618
|
+
const metaComment = parseMetaComment(lines, index);
|
|
1619
|
+
if (metaComment) {
|
|
1620
|
+
blocks.push(metaComment.block);
|
|
1621
|
+
index = metaComment.nextIndex;
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1509
1625
|
const snippetWrapper = stepsHeadingLevel !== null
|
|
1510
1626
|
? parseSnippetWrapper(lines, index)
|
|
1511
1627
|
: null;
|
|
@@ -2,6 +2,7 @@ import { defaultBlockSpecs } from "@blocknote/core";
|
|
|
2
2
|
import { BlockNoteSchema } from "@blocknote/core";
|
|
3
3
|
import { stepBlock } from "./blocks/step";
|
|
4
4
|
import { snippetBlock } from "./blocks/snippet";
|
|
5
|
+
import { testMetaBlock } from "./blocks/testMeta";
|
|
5
6
|
import { fileBlock } from "./blocks/fileBlock";
|
|
6
7
|
import { htmlToMarkdown, markdownToHtml } from "./blocks/markdown";
|
|
7
8
|
|
|
@@ -11,6 +12,7 @@ export const customSchema = BlockNoteSchema.create({
|
|
|
11
12
|
file: fileBlock,
|
|
12
13
|
testStep: stepBlock,
|
|
13
14
|
snippet: snippetBlock,
|
|
15
|
+
testMeta: testMetaBlock,
|
|
14
16
|
},
|
|
15
17
|
});
|
|
16
18
|
|
package/src/editor/styles.css
CHANGED
|
@@ -595,6 +595,207 @@ html.dark .bn-step-editor .overtype-wrapper .overtype-preview a.step-preview-lin
|
|
|
595
595
|
flex-shrink: 0;
|
|
596
596
|
}
|
|
597
597
|
|
|
598
|
+
/* ============================================
|
|
599
|
+
HEADING SIZES INSIDE THE EDITOR
|
|
600
|
+
Override BlockNote's default heading scale (3em / 2em / 1.3em) with a
|
|
601
|
+
compact 22px → 14px range. BlockNote derives heading font-size from the
|
|
602
|
+
`--level` custom property, so we just redefine it per level.
|
|
603
|
+
============================================ */
|
|
604
|
+
.testomatio-editor [data-content-type="heading"] {
|
|
605
|
+
--level: 22px; /* h1 (level 1 has no [data-level] rule of its own) */
|
|
606
|
+
}
|
|
607
|
+
.testomatio-editor [data-content-type="heading"][data-level="2"] {
|
|
608
|
+
--level: 20px;
|
|
609
|
+
}
|
|
610
|
+
.testomatio-editor [data-content-type="heading"][data-level="3"] {
|
|
611
|
+
--level: 18px;
|
|
612
|
+
}
|
|
613
|
+
.testomatio-editor [data-content-type="heading"][data-level="4"] {
|
|
614
|
+
--level: 16px;
|
|
615
|
+
}
|
|
616
|
+
.testomatio-editor [data-content-type="heading"][data-level="5"] {
|
|
617
|
+
--level: 14px;
|
|
618
|
+
}
|
|
619
|
+
.testomatio-editor [data-content-type="heading"][data-level="6"] {
|
|
620
|
+
--level: 14px;
|
|
621
|
+
}
|
|
622
|
+
/* Keep size stable during BlockNote's heading-transition animation. */
|
|
623
|
+
.testomatio-editor [data-prev-level="1"] {
|
|
624
|
+
--prev-level: 22px;
|
|
625
|
+
}
|
|
626
|
+
.testomatio-editor [data-prev-level="2"] {
|
|
627
|
+
--prev-level: 20px;
|
|
628
|
+
}
|
|
629
|
+
.testomatio-editor [data-prev-level="3"] {
|
|
630
|
+
--prev-level: 18px;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
/* ============================================
|
|
634
|
+
TEST / SUITE METADATA BLOCK
|
|
635
|
+
Dimmed card for `<!-- test ... -->` / `<!-- suite ... -->` comments.
|
|
636
|
+
============================================ */
|
|
637
|
+
.bn-testmeta {
|
|
638
|
+
display: flex;
|
|
639
|
+
flex-direction: column;
|
|
640
|
+
gap: 4px;
|
|
641
|
+
width: 100%;
|
|
642
|
+
box-sizing: border-box;
|
|
643
|
+
padding: 6px 10px;
|
|
644
|
+
background: var(--bg-muted);
|
|
645
|
+
/*border: 1px solid var(--border-default);*/
|
|
646
|
+
/* Stronger top edge signals that the test case begins below this line. */
|
|
647
|
+
border-top: 3px solid var(--color-slate-400);
|
|
648
|
+
/*border-radius: 8px;*/
|
|
649
|
+
margin-top: 2rem;
|
|
650
|
+
opacity: 0.5;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/* Header line: `TEST @T1233456 ............ [+]` — label, id, and add button
|
|
654
|
+
always share one row. */
|
|
655
|
+
.bn-testmeta__header {
|
|
656
|
+
display: flex;
|
|
657
|
+
align-items: center;
|
|
658
|
+
gap: 8px;
|
|
659
|
+
margin-left: 8px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.bn-testmeta__header .bn-testmeta__add-wrap {
|
|
663
|
+
margin-left: auto;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.bn-testmeta__label {
|
|
667
|
+
font-size: 11px;
|
|
668
|
+
font-weight: 600;
|
|
669
|
+
letter-spacing: 0.04em;
|
|
670
|
+
text-transform: uppercase;
|
|
671
|
+
color: var(--text-muted);
|
|
672
|
+
flex-shrink: 0;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
.bn-testmeta__id {
|
|
676
|
+
font-size: 13px;
|
|
677
|
+
font-weight: 600;
|
|
678
|
+
color: var(--text-primary);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.bn-testmeta__rows {
|
|
682
|
+
display: flex;
|
|
683
|
+
flex-direction: column;
|
|
684
|
+
gap: 2px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.bn-testmeta__row {
|
|
688
|
+
display: grid;
|
|
689
|
+
grid-template-columns: 140px minmax(0, 1fr) 24px;
|
|
690
|
+
align-items: center;
|
|
691
|
+
gap: 8px;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
.bn-testmeta__key {
|
|
695
|
+
min-width: 0;
|
|
696
|
+
font-size: 13px;
|
|
697
|
+
color: var(--text-muted);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/* Defined values blend into the block like normal text, and only reveal the
|
|
701
|
+
input affordance on hover/focus ("activate on click"). */
|
|
702
|
+
.bn-testmeta__key--input,
|
|
703
|
+
.bn-testmeta__value {
|
|
704
|
+
width: 100%;
|
|
705
|
+
height: 26px;
|
|
706
|
+
padding: 0 8px;
|
|
707
|
+
box-sizing: border-box;
|
|
708
|
+
font-family: inherit;
|
|
709
|
+
font-size: 13px;
|
|
710
|
+
color: var(--text-primary);
|
|
711
|
+
background: transparent;
|
|
712
|
+
border: 1px solid transparent;
|
|
713
|
+
border-radius: 6px;
|
|
714
|
+
cursor: text;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
.bn-testmeta__key--input:hover,
|
|
718
|
+
.bn-testmeta__value:hover {
|
|
719
|
+
border-color: var(--border-light);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.bn-testmeta__key--input:focus,
|
|
723
|
+
.bn-testmeta__value:focus {
|
|
724
|
+
outline: none;
|
|
725
|
+
background: var(--bg-white);
|
|
726
|
+
border-color: var(--step-input-border-focus);
|
|
727
|
+
box-shadow: 0 0 0 2px var(--step-input-shadow);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
.bn-testmeta__remove,
|
|
731
|
+
.bn-testmeta__add {
|
|
732
|
+
width: 24px;
|
|
733
|
+
height: 24px;
|
|
734
|
+
display: inline-flex;
|
|
735
|
+
align-items: center;
|
|
736
|
+
justify-content: center;
|
|
737
|
+
font-size: 18px;
|
|
738
|
+
line-height: 1;
|
|
739
|
+
color: var(--text-muted);
|
|
740
|
+
background: transparent;
|
|
741
|
+
border: none;
|
|
742
|
+
border-radius: 6px;
|
|
743
|
+
cursor: pointer;
|
|
744
|
+
padding: 0;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.bn-testmeta__remove:hover,
|
|
748
|
+
.bn-testmeta__add:hover {
|
|
749
|
+
background: var(--step-bg-button-hover);
|
|
750
|
+
color: var(--text-primary);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
.bn-testmeta__add-wrap {
|
|
754
|
+
position: relative;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
.bn-testmeta__menu {
|
|
758
|
+
position: absolute;
|
|
759
|
+
top: calc(100% + 4px);
|
|
760
|
+
right: 0;
|
|
761
|
+
z-index: 100;
|
|
762
|
+
min-width: 160px;
|
|
763
|
+
max-height: 240px;
|
|
764
|
+
overflow-y: auto;
|
|
765
|
+
display: flex;
|
|
766
|
+
flex-direction: column;
|
|
767
|
+
padding: 4px;
|
|
768
|
+
background: var(--bg-white-opaque);
|
|
769
|
+
border: 1px solid var(--border-default);
|
|
770
|
+
border-radius: 8px;
|
|
771
|
+
box-shadow: 0 8px 24px var(--shadow-medium);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
.bn-testmeta__menu-item {
|
|
775
|
+
display: block;
|
|
776
|
+
width: 100%;
|
|
777
|
+
padding: 6px 8px;
|
|
778
|
+
text-align: left;
|
|
779
|
+
font-family: inherit;
|
|
780
|
+
font-size: 13px;
|
|
781
|
+
color: var(--text-primary);
|
|
782
|
+
background: transparent;
|
|
783
|
+
border: none;
|
|
784
|
+
border-radius: 6px;
|
|
785
|
+
cursor: pointer;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
.bn-testmeta__menu-item:hover {
|
|
789
|
+
background: var(--bg-muted);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.bn-testmeta__menu-item--custom {
|
|
793
|
+
margin-top: 2px;
|
|
794
|
+
border-top: 1px solid var(--border-light);
|
|
795
|
+
border-radius: 0 0 6px 6px;
|
|
796
|
+
color: var(--text-muted);
|
|
797
|
+
}
|
|
798
|
+
|
|
598
799
|
.bn-snippet-dropdown {
|
|
599
800
|
position: relative;
|
|
600
801
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export type MetaFieldSuggestion = {
|
|
2
|
+
/** The field key that gets inserted, e.g. "priority". */
|
|
3
|
+
key: string;
|
|
4
|
+
/** Optional display label; defaults to `key`. */
|
|
5
|
+
label?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Either a flat list (applied to both test and suite blocks) or per-kind lists.
|
|
10
|
+
* Configure from the host app via `setMetaFieldSuggestions` so embedders can
|
|
11
|
+
* plug in their own set of suggested metadata fields.
|
|
12
|
+
*/
|
|
13
|
+
export type MetaFieldSuggestionsConfig =
|
|
14
|
+
| MetaFieldSuggestion[]
|
|
15
|
+
| { test?: MetaFieldSuggestion[]; suite?: MetaFieldSuggestion[] };
|
|
16
|
+
|
|
17
|
+
// Defaults follow the classical Testomatio markdown format. `id` is intentionally
|
|
18
|
+
// omitted: it is a read-only, system-assigned field, not something users add.
|
|
19
|
+
const DEFAULT_TEST_FIELDS: MetaFieldSuggestion[] = [
|
|
20
|
+
{ key: "priority" },
|
|
21
|
+
{ key: "type" },
|
|
22
|
+
{ key: "tags" },
|
|
23
|
+
{ key: "labels" },
|
|
24
|
+
{ key: "assignee" },
|
|
25
|
+
{ key: "creator" },
|
|
26
|
+
{ key: "shared" },
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
const DEFAULT_SUITE_FIELDS: MetaFieldSuggestion[] = [
|
|
30
|
+
{ key: "emoji" },
|
|
31
|
+
{ key: "tags" },
|
|
32
|
+
{ key: "labels" },
|
|
33
|
+
{ key: "assignee" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
let configured: MetaFieldSuggestionsConfig | null = null;
|
|
37
|
+
|
|
38
|
+
export function setMetaFieldSuggestions(config: MetaFieldSuggestionsConfig | null) {
|
|
39
|
+
configured = config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getMetaFieldSuggestions(kind: "test" | "suite"): MetaFieldSuggestion[] {
|
|
43
|
+
if (configured) {
|
|
44
|
+
if (Array.isArray(configured)) {
|
|
45
|
+
return configured;
|
|
46
|
+
}
|
|
47
|
+
const list = kind === "suite" ? configured.suite : configured.test;
|
|
48
|
+
if (list) {
|
|
49
|
+
return list;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return kind === "suite" ? DEFAULT_SUITE_FIELDS : DEFAULT_TEST_FIELDS;
|
|
53
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,13 @@ export {
|
|
|
6
6
|
} from "./editor/customSchema";
|
|
7
7
|
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
8
8
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
9
|
+
export { testMetaBlock } from "./editor/blocks/testMeta";
|
|
10
|
+
export {
|
|
11
|
+
setMetaFieldSuggestions,
|
|
12
|
+
getMetaFieldSuggestions,
|
|
13
|
+
type MetaFieldSuggestion,
|
|
14
|
+
type MetaFieldSuggestionsConfig,
|
|
15
|
+
} from "./editor/testMetaFields";
|
|
9
16
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
10
17
|
|
|
11
18
|
export {
|