testomatio-editor-blocks 0.4.65 → 0.4.67
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/step.d.ts +5 -0
- package/package/editor/blocks/step.js +273 -213
- package/package/editor/blocks/stepHorizontalView.d.ts +1 -0
- package/package/editor/blocks/stepHorizontalView.js +2 -2
- package/package/editor/blocks/testMeta.d.ts +37 -0
- package/package/editor/blocks/testMeta.js +111 -0
- package/package/editor/blocks/useDeferredMount.d.ts +26 -0
- package/package/editor/blocks/useDeferredMount.js +54 -0
- package/package/editor/createMarkdownPasteHandler.js +56 -9
- package/package/editor/customMarkdownConverter.js +127 -10
- 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 +215 -0
- package/package.json +1 -1
- package/src/App.tsx +54 -15
- package/src/editor/blocks/step.tsx +198 -47
- package/src/editor/blocks/stepHorizontalView.tsx +3 -0
- package/src/editor/blocks/stepNumber.test.ts +39 -0
- package/src/editor/blocks/testMeta.tsx +242 -0
- package/src/editor/blocks/useDeferredMount.ts +66 -0
- package/src/editor/createMarkdownPasteHandler.test.ts +126 -0
- package/src/editor/createMarkdownPasteHandler.ts +60 -8
- package/src/editor/customMarkdownConverter.test.ts +135 -0
- package/src/editor/customMarkdownConverter.ts +125 -0
- package/src/editor/customSchema.tsx +2 -0
- package/src/editor/renderingPerf.test.ts +59 -0
- package/src/editor/styles.css +215 -0
- package/src/editor/testMetaFields.ts +53 -0
- package/src/index.ts +7 -0
|
@@ -389,6 +389,36 @@ function serializeBlock(
|
|
|
389
389
|
}
|
|
390
390
|
return lines;
|
|
391
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
|
+
}
|
|
392
422
|
case "testStep":
|
|
393
423
|
case "snippet": {
|
|
394
424
|
const isSnippet = block.type === "snippet";
|
|
@@ -1355,6 +1385,92 @@ function parseParagraph(lines: string[], index: number): { block: CustomPartialB
|
|
|
1355
1385
|
};
|
|
1356
1386
|
}
|
|
1357
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
|
+
|
|
1358
1474
|
function parseSnippetWrapper(
|
|
1359
1475
|
lines: string[],
|
|
1360
1476
|
index: number,
|
|
@@ -1497,6 +1613,15 @@ export function markdownToBlocks(markdown: string, _options?: MarkdownToBlocksOp
|
|
|
1497
1613
|
continue;
|
|
1498
1614
|
}
|
|
1499
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
|
+
|
|
1500
1625
|
const snippetWrapper = stepsHeadingLevel !== null
|
|
1501
1626
|
? parseSnippetWrapper(lines, index)
|
|
1502
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
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { markdownToBlocks } from "./customMarkdownConverter";
|
|
3
|
+
|
|
4
|
+
// Coarse regression guard for parse cost. Parsing was never the bottleneck
|
|
5
|
+
// (~5-15ms for a 1000-block document) — rendering was — but a future change
|
|
6
|
+
// could accidentally make the parser quadratic. These use best-of-N timing
|
|
7
|
+
// (noise only ever adds time, so the minimum approximates true compute cost)
|
|
8
|
+
// with generous bounds, so they only fail on a real algorithmic regression.
|
|
9
|
+
|
|
10
|
+
function generateMarkdown(testCases: number): string {
|
|
11
|
+
const parts: string[] = ["<!-- suite -->", "# Generated Suite", ""];
|
|
12
|
+
for (let t = 0; t < testCases; t++) {
|
|
13
|
+
parts.push("<!-- test\nid: @T" + t + "\n-->");
|
|
14
|
+
parts.push("# Test case number " + t);
|
|
15
|
+
parts.push("## Steps", "");
|
|
16
|
+
for (let s = 0; s < 6; s++) {
|
|
17
|
+
parts.push("* Perform action " + s + " in test " + t);
|
|
18
|
+
parts.push(" *Expected:* Result " + s + " is observed in test " + t);
|
|
19
|
+
}
|
|
20
|
+
parts.push("");
|
|
21
|
+
}
|
|
22
|
+
return parts.join("\n");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function bestOfMs(runs: number, fn: () => void): number {
|
|
26
|
+
// warm up the JIT first
|
|
27
|
+
for (let i = 0; i < 3; i++) fn();
|
|
28
|
+
let min = Infinity;
|
|
29
|
+
for (let i = 0; i < runs; i++) {
|
|
30
|
+
const t0 = performance.now();
|
|
31
|
+
fn();
|
|
32
|
+
min = Math.min(min, performance.now() - t0);
|
|
33
|
+
}
|
|
34
|
+
return min;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("rendering perf — markdown parsing stays fast", () => {
|
|
38
|
+
it("parses a large (1000+ block) document well within budget", () => {
|
|
39
|
+
const md = generateMarkdown(150);
|
|
40
|
+
const blocks = markdownToBlocks(md);
|
|
41
|
+
expect(blocks.length).toBeGreaterThan(500);
|
|
42
|
+
|
|
43
|
+
const ms = bestOfMs(8, () => markdownToBlocks(md));
|
|
44
|
+
// ~20-50x headroom over the real ~5-15ms cost.
|
|
45
|
+
expect(ms).toBeLessThan(250);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("scales sub-quadratically with document size", () => {
|
|
49
|
+
const small = generateMarkdown(100);
|
|
50
|
+
const large = generateMarkdown(400); // 4x the content
|
|
51
|
+
|
|
52
|
+
const tSmall = bestOfMs(8, () => markdownToBlocks(small));
|
|
53
|
+
const tLarge = bestOfMs(8, () => markdownToBlocks(large));
|
|
54
|
+
|
|
55
|
+
// 4x the input: linear parsing ≈ 4x time, quadratic ≈ 16x. Allow a generous
|
|
56
|
+
// 8x (plus a small cushion for tiny-time measurement noise).
|
|
57
|
+
expect(tLarge).toBeLessThan(tSmall * 8 + 5);
|
|
58
|
+
});
|
|
59
|
+
});
|
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
|
}
|
|
@@ -1039,6 +1240,20 @@ html.dark .bn-step-editor .overtype-wrapper .overtype-preview a.step-preview-lin
|
|
|
1039
1240
|
min-height: 4rem;
|
|
1040
1241
|
}
|
|
1041
1242
|
|
|
1243
|
+
/* Static stand-in shown before a step's interactive editor is lazily mounted.
|
|
1244
|
+
Mirrors the OverType inner padding/typography so document height stays stable. */
|
|
1245
|
+
.bn-step-editor--preview {
|
|
1246
|
+
padding: 10px 12px;
|
|
1247
|
+
white-space: pre-wrap;
|
|
1248
|
+
word-break: break-word;
|
|
1249
|
+
color: #262626;
|
|
1250
|
+
cursor: text;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
html.dark .bn-step-editor--preview {
|
|
1254
|
+
color: #e5e5e5;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1042
1257
|
.bn-step-editor.bn-step-editor--focused {
|
|
1043
1258
|
outline: none;
|
|
1044
1259
|
box-shadow: none;
|
|
@@ -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 {
|