testomatio-editor-blocks 0.4.65 → 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 +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 +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 -0
- package/src/editor/customMarkdownConverter.ts +125 -0
- 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
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type MetaField = {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
};
|
|
5
|
+
export declare function serializeMetaFields(fields: MetaField[]): string;
|
|
6
|
+
export declare const testMetaBlock: {
|
|
7
|
+
config: {
|
|
8
|
+
readonly type: "testMeta";
|
|
9
|
+
readonly content: "none";
|
|
10
|
+
readonly propSchema: {
|
|
11
|
+
readonly metaKind: {
|
|
12
|
+
readonly default: "test";
|
|
13
|
+
};
|
|
14
|
+
readonly metaFields: {
|
|
15
|
+
readonly default: "[]";
|
|
16
|
+
};
|
|
17
|
+
readonly metaInline: {
|
|
18
|
+
readonly default: false;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
implementation: import("@blocknote/core").TiptapBlockImplementation<{
|
|
23
|
+
readonly type: "testMeta";
|
|
24
|
+
readonly content: "none";
|
|
25
|
+
readonly propSchema: {
|
|
26
|
+
readonly metaKind: {
|
|
27
|
+
readonly default: "test";
|
|
28
|
+
};
|
|
29
|
+
readonly metaFields: {
|
|
30
|
+
readonly default: "[]";
|
|
31
|
+
};
|
|
32
|
+
readonly metaInline: {
|
|
33
|
+
readonly default: false;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
|
|
37
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createReactBlockSpec } from "@blocknote/react";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
|
+
import { getMetaFieldSuggestions } from "../testMetaFields";
|
|
5
|
+
const ID_KEYS = new Set(["id"]);
|
|
6
|
+
function parseMetaFields(raw) {
|
|
7
|
+
if (typeof raw !== "string" || raw.trim().length === 0) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(raw);
|
|
12
|
+
if (!Array.isArray(parsed)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return parsed
|
|
16
|
+
.filter((item) => item && typeof item === "object")
|
|
17
|
+
.map((item) => ({
|
|
18
|
+
key: typeof item.key === "string" ? item.key : "",
|
|
19
|
+
value: typeof item.value === "string" ? item.value : "",
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function serializeMetaFields(fields) {
|
|
27
|
+
return JSON.stringify(fields);
|
|
28
|
+
}
|
|
29
|
+
function AddFieldMenu({ kind, usedKeys, onPick }) {
|
|
30
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
31
|
+
const containerRef = useRef(null);
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!isOpen)
|
|
34
|
+
return;
|
|
35
|
+
const handleMouseDown = (event) => {
|
|
36
|
+
if (containerRef.current && !containerRef.current.contains(event.target)) {
|
|
37
|
+
setIsOpen(false);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
document.addEventListener("mousedown", handleMouseDown);
|
|
41
|
+
return () => document.removeEventListener("mousedown", handleMouseDown);
|
|
42
|
+
}, [isOpen]);
|
|
43
|
+
const available = useMemo(() => {
|
|
44
|
+
const used = new Set(usedKeys.map((k) => k.trim().toLowerCase()));
|
|
45
|
+
return getMetaFieldSuggestions(kind).filter((s) => !used.has(s.key.trim().toLowerCase()));
|
|
46
|
+
}, [kind, usedKeys]);
|
|
47
|
+
const pick = useCallback((key) => {
|
|
48
|
+
onPick(key);
|
|
49
|
+
setIsOpen(false);
|
|
50
|
+
}, [onPick]);
|
|
51
|
+
return (_jsxs("div", { className: "bn-testmeta__add-wrap", ref: containerRef, children: [_jsx("button", { type: "button", className: "bn-testmeta__add", "aria-label": "Add field", title: "Add field", onClick: () => setIsOpen((prev) => !prev), children: "+" }), isOpen && (_jsxs("div", { className: "bn-testmeta__menu", role: "listbox", children: [available.map((suggestion) => {
|
|
52
|
+
var _a;
|
|
53
|
+
return (_jsx("button", { type: "button", role: "option", className: "bn-testmeta__menu-item", onMouseDown: (event) => {
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
pick(suggestion.key);
|
|
56
|
+
}, children: (_a = suggestion.label) !== null && _a !== void 0 ? _a : suggestion.key }, suggestion.key));
|
|
57
|
+
}), _jsx("button", { type: "button", className: "bn-testmeta__menu-item bn-testmeta__menu-item--custom", onMouseDown: (event) => {
|
|
58
|
+
event.preventDefault();
|
|
59
|
+
pick("");
|
|
60
|
+
}, children: "Custom field\u2026" })] }))] }));
|
|
61
|
+
}
|
|
62
|
+
export const testMetaBlock = createReactBlockSpec({
|
|
63
|
+
type: "testMeta",
|
|
64
|
+
content: "none",
|
|
65
|
+
propSchema: {
|
|
66
|
+
// "test" | "suite" — which keyword the comment opened with.
|
|
67
|
+
metaKind: {
|
|
68
|
+
default: "test",
|
|
69
|
+
},
|
|
70
|
+
// JSON-encoded MetaField[] so insertion order is preserved.
|
|
71
|
+
metaFields: {
|
|
72
|
+
default: "[]",
|
|
73
|
+
},
|
|
74
|
+
// true when the source comment was a one-liner (`<!-- test id: @T.. -->`).
|
|
75
|
+
metaInline: {
|
|
76
|
+
default: false,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
}, {
|
|
80
|
+
render: ({ block, editor }) => {
|
|
81
|
+
const kind = block.props.metaKind === "suite" ? "suite" : "test";
|
|
82
|
+
const fields = useMemo(() => parseMetaFields(block.props.metaFields), [block.props.metaFields]);
|
|
83
|
+
const commitFields = useCallback((next) => {
|
|
84
|
+
editor.updateBlock(block.id, {
|
|
85
|
+
props: { metaFields: serializeMetaFields(next) },
|
|
86
|
+
});
|
|
87
|
+
}, [block.id, editor]);
|
|
88
|
+
const handleValueChange = useCallback((index, value) => {
|
|
89
|
+
const next = fields.map((field, i) => i === index ? { ...field, value } : field);
|
|
90
|
+
commitFields(next);
|
|
91
|
+
}, [fields, commitFields]);
|
|
92
|
+
const handleKeyChange = useCallback((index, key) => {
|
|
93
|
+
const next = fields.map((field, i) => i === index ? { ...field, key } : field);
|
|
94
|
+
commitFields(next);
|
|
95
|
+
}, [fields, commitFields]);
|
|
96
|
+
const handleRemove = useCallback((index) => {
|
|
97
|
+
commitFields(fields.filter((_, i) => i !== index));
|
|
98
|
+
}, [fields, commitFields]);
|
|
99
|
+
const handleAddField = useCallback((key) => {
|
|
100
|
+
commitFields([...fields, { key, value: "" }]);
|
|
101
|
+
}, [fields, commitFields]);
|
|
102
|
+
const usedKeys = useMemo(() => fields.map((f) => f.key), [fields]);
|
|
103
|
+
// The read-only `id` is shown inline on the header line next to the kind
|
|
104
|
+
// label; every other field renders as an editable row below.
|
|
105
|
+
const idField = fields.find((f) => ID_KEYS.has(f.key.trim().toLowerCase()));
|
|
106
|
+
const editableFields = fields
|
|
107
|
+
.map((field, index) => ({ field, index }))
|
|
108
|
+
.filter(({ field }) => !ID_KEYS.has(field.key.trim().toLowerCase()));
|
|
109
|
+
return (_jsxs("div", { className: "bn-testmeta", "data-block-id": block.id, "data-kind": kind, contentEditable: false, suppressContentEditableWarning: true, draggable: false, children: [_jsxs("div", { className: "bn-testmeta__header", children: [_jsx("span", { className: "bn-testmeta__label", children: kind.toUpperCase() }), (idField === null || idField === void 0 ? void 0 : idField.value) && _jsx("span", { className: "bn-testmeta__id", children: idField.value }), _jsx(AddFieldMenu, { kind: kind, usedKeys: usedKeys, onPick: handleAddField })] }), editableFields.length > 0 && (_jsx("div", { className: "bn-testmeta__rows", children: editableFields.map(({ field, index }) => (_jsxs("div", { className: "bn-testmeta__row", children: [_jsx("input", { className: "bn-testmeta__key bn-testmeta__key--input", type: "text", value: field.key, placeholder: "key", spellCheck: false, onChange: (e) => handleKeyChange(index, e.target.value) }), _jsx("input", { className: "bn-testmeta__value", type: "text", value: field.value, placeholder: "value", spellCheck: false, onChange: (e) => handleValueChange(index, e.target.value) }), _jsx("button", { type: "button", className: "bn-testmeta__remove", "aria-label": "Remove field", title: "Remove field", onClick: () => handleRemove(index), children: "\u00D7" })] }, index))) }))] }));
|
|
110
|
+
},
|
|
111
|
+
});
|
|
@@ -224,7 +224,7 @@ function serializeChildren(block, ctx) {
|
|
|
224
224
|
return serializeBlocks(block.children, childCtx);
|
|
225
225
|
}
|
|
226
226
|
function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
227
|
-
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
|
|
227
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
|
|
228
228
|
const lines = [];
|
|
229
229
|
const indent = ctx.listDepth > 0 ? " ".repeat(ctx.listDepth) : "";
|
|
230
230
|
switch (block.type) {
|
|
@@ -316,19 +316,48 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
316
316
|
}
|
|
317
317
|
return lines;
|
|
318
318
|
}
|
|
319
|
+
case "testMeta": {
|
|
320
|
+
const kind = block.props.metaKind === "suite" ? "suite" : "test";
|
|
321
|
+
const inline = Boolean(block.props.metaInline);
|
|
322
|
+
let fields = [];
|
|
323
|
+
try {
|
|
324
|
+
const parsed = JSON.parse(((_e = block.props.metaFields) !== null && _e !== void 0 ? _e : "[]"));
|
|
325
|
+
if (Array.isArray(parsed)) {
|
|
326
|
+
fields = parsed
|
|
327
|
+
.filter((f) => f && typeof f === "object" && typeof f.key === "string")
|
|
328
|
+
.map((f) => ({ key: f.key.trim(), value: typeof f.value === "string" ? f.value.trim() : "" }))
|
|
329
|
+
// Skip incomplete fields: both a key and a value are required.
|
|
330
|
+
.filter((f) => f.key.length > 0 && f.value.length > 0);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
fields = [];
|
|
335
|
+
}
|
|
336
|
+
// Preserve the one-liner form only when it still fits on a single line
|
|
337
|
+
// (a one-liner holds at most one `key: value` pair).
|
|
338
|
+
if (inline && fields.length <= 1) {
|
|
339
|
+
const field = fields[0];
|
|
340
|
+
lines.push(field ? `<!-- ${kind} ${field.key}: ${field.value} -->` : `<!-- ${kind} -->`);
|
|
341
|
+
return lines;
|
|
342
|
+
}
|
|
343
|
+
lines.push(`<!-- ${kind}`);
|
|
344
|
+
fields.forEach((field) => lines.push(`${field.key}: ${field.value}`));
|
|
345
|
+
lines.push("-->");
|
|
346
|
+
return lines;
|
|
347
|
+
}
|
|
319
348
|
case "testStep":
|
|
320
349
|
case "snippet": {
|
|
321
350
|
const isSnippet = block.type === "snippet";
|
|
322
|
-
const snippetId = isSnippet ? ((
|
|
351
|
+
const snippetId = isSnippet ? ((_f = block.props.snippetId) !== null && _f !== void 0 ? _f : "").trim() : "";
|
|
323
352
|
const stepTitle = isSnippet
|
|
324
|
-
? ((
|
|
325
|
-
: ((
|
|
353
|
+
? ((_g = block.props.snippetTitle) !== null && _g !== void 0 ? _g : "").trim()
|
|
354
|
+
: ((_h = block.props.stepTitle) !== null && _h !== void 0 ? _h : "").trim();
|
|
326
355
|
const stepData = isSnippet
|
|
327
|
-
? ((
|
|
328
|
-
: ((
|
|
356
|
+
? ((_j = block.props.snippetData) !== null && _j !== void 0 ? _j : "").trim()
|
|
357
|
+
: ((_k = block.props.stepData) !== null && _k !== void 0 ? _k : "").trim();
|
|
329
358
|
const expectedResult = isSnippet
|
|
330
|
-
? ((
|
|
331
|
-
: ((
|
|
359
|
+
? ((_l = block.props.snippetExpectedResult) !== null && _l !== void 0 ? _l : "").trim()
|
|
360
|
+
: ((_m = block.props.expectedResult) !== null && _m !== void 0 ? _m : "").trim();
|
|
332
361
|
if (isSnippet) {
|
|
333
362
|
if (snippetId) {
|
|
334
363
|
lines.push(`<!-- begin snippet #${snippetId} -->`);
|
|
@@ -352,7 +381,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
352
381
|
const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
|
|
353
382
|
const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
|
|
354
383
|
if (normalizedTitle.length > 0 || hasContent) {
|
|
355
|
-
const listStyle = (
|
|
384
|
+
const listStyle = (_o = block.props.listStyle) !== null && _o !== void 0 ? _o : "bullet";
|
|
356
385
|
const prefix = listStyle === "ordered" ? `${(stepIndex !== null && stepIndex !== void 0 ? stepIndex : 0) + 1}.` : "*";
|
|
357
386
|
lines.push(normalizedTitle.length > 0 ? `${prefix} ${escapeStepContent(normalizedTitle)}` : `${prefix} `);
|
|
358
387
|
}
|
|
@@ -406,7 +435,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
406
435
|
return lines;
|
|
407
436
|
}
|
|
408
437
|
const headerRowCount = rows.length
|
|
409
|
-
? Math.min(rows.length, Math.max((
|
|
438
|
+
? Math.min(rows.length, Math.max((_p = tableContent.headerRows) !== null && _p !== void 0 ? _p : 1, 1))
|
|
410
439
|
: 0;
|
|
411
440
|
const columnAlignments = new Array(columnCount).fill("left");
|
|
412
441
|
const getCellAlignment = (cell) => {
|
|
@@ -1145,6 +1174,86 @@ function parseParagraph(lines, index) {
|
|
|
1145
1174
|
nextIndex: index + 1,
|
|
1146
1175
|
};
|
|
1147
1176
|
}
|
|
1177
|
+
const META_COMMENT_OPEN_REGEX = /^<!--\s*(test|suite)(?=\s|-->|$)/i;
|
|
1178
|
+
function metaFieldsFromBody(bodyLines) {
|
|
1179
|
+
const fields = [];
|
|
1180
|
+
for (const raw of bodyLines) {
|
|
1181
|
+
const line = raw.trim();
|
|
1182
|
+
if (!line)
|
|
1183
|
+
continue;
|
|
1184
|
+
const colon = line.indexOf(":");
|
|
1185
|
+
// "Each line is `key: value`; lines without `:` are ignored."
|
|
1186
|
+
if (colon === -1)
|
|
1187
|
+
continue;
|
|
1188
|
+
const key = line.slice(0, colon).trim();
|
|
1189
|
+
const value = line.slice(colon + 1).trim();
|
|
1190
|
+
if (!key)
|
|
1191
|
+
continue;
|
|
1192
|
+
fields.push({ key, value });
|
|
1193
|
+
}
|
|
1194
|
+
return fields;
|
|
1195
|
+
}
|
|
1196
|
+
function parseMetaComment(lines, index) {
|
|
1197
|
+
const first = lines[index];
|
|
1198
|
+
const openMatch = first.match(META_COMMENT_OPEN_REGEX);
|
|
1199
|
+
if (!openMatch) {
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
const kind = openMatch[1].toLowerCase();
|
|
1203
|
+
let bodyLines = [];
|
|
1204
|
+
let inline = false;
|
|
1205
|
+
let nextIndex;
|
|
1206
|
+
// One-liner: opening and closing markers on the same line.
|
|
1207
|
+
const oneLine = first.match(/^<!--\s*(?:test|suite)\b\s*([\s\S]*?)\s*-->\s*$/i);
|
|
1208
|
+
if (oneLine) {
|
|
1209
|
+
inline = true;
|
|
1210
|
+
if (oneLine[1].trim()) {
|
|
1211
|
+
bodyLines = [oneLine[1].trim()];
|
|
1212
|
+
}
|
|
1213
|
+
nextIndex = index + 1;
|
|
1214
|
+
}
|
|
1215
|
+
else {
|
|
1216
|
+
// Block form: keyword line, fields on their own lines, closing `-->`.
|
|
1217
|
+
const afterKeyword = first.replace(/^<!--\s*(?:test|suite)\b/i, "").trim();
|
|
1218
|
+
if (afterKeyword) {
|
|
1219
|
+
bodyLines.push(afterKeyword);
|
|
1220
|
+
}
|
|
1221
|
+
let next = index + 1;
|
|
1222
|
+
let closed = false;
|
|
1223
|
+
while (next < lines.length) {
|
|
1224
|
+
const current = lines[next];
|
|
1225
|
+
if (/-->\s*$/.test(current)) {
|
|
1226
|
+
const beforeClose = current.replace(/-->\s*$/, "").trim();
|
|
1227
|
+
if (beforeClose) {
|
|
1228
|
+
bodyLines.push(beforeClose);
|
|
1229
|
+
}
|
|
1230
|
+
next += 1;
|
|
1231
|
+
closed = true;
|
|
1232
|
+
break;
|
|
1233
|
+
}
|
|
1234
|
+
bodyLines.push(current);
|
|
1235
|
+
next += 1;
|
|
1236
|
+
}
|
|
1237
|
+
if (!closed) {
|
|
1238
|
+
// Unterminated comment — let normal parsing handle these lines.
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
nextIndex = next;
|
|
1242
|
+
}
|
|
1243
|
+
const fields = metaFieldsFromBody(bodyLines);
|
|
1244
|
+
return {
|
|
1245
|
+
block: {
|
|
1246
|
+
type: "testMeta",
|
|
1247
|
+
props: {
|
|
1248
|
+
metaKind: kind,
|
|
1249
|
+
metaFields: JSON.stringify(fields),
|
|
1250
|
+
metaInline: inline,
|
|
1251
|
+
},
|
|
1252
|
+
children: [],
|
|
1253
|
+
},
|
|
1254
|
+
nextIndex,
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1148
1257
|
function parseSnippetWrapper(lines, index) {
|
|
1149
1258
|
const trimmed = lines[index].trim();
|
|
1150
1259
|
const startMatch = trimmed.match(/^<!--\s*begin snippet\s*#?\s*([^\s>]+)\s*-->/i);
|
|
@@ -1257,6 +1366,14 @@ export function markdownToBlocks(markdown, _options) {
|
|
|
1257
1366
|
index += 1;
|
|
1258
1367
|
continue;
|
|
1259
1368
|
}
|
|
1369
|
+
// Test/suite metadata comments can appear anywhere (typically at the top of
|
|
1370
|
+
// a document or right after a heading), so this runs ungated.
|
|
1371
|
+
const metaComment = parseMetaComment(lines, index);
|
|
1372
|
+
if (metaComment) {
|
|
1373
|
+
blocks.push(metaComment.block);
|
|
1374
|
+
index = metaComment.nextIndex;
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1260
1377
|
const snippetWrapper = stepsHeadingLevel !== null
|
|
1261
1378
|
? parseSnippetWrapper(lines, index)
|
|
1262
1379
|
: null;
|
|
@@ -117,6 +117,38 @@ export declare const customSchema: BlockNoteSchema<import("@blocknote/core").Blo
|
|
|
117
117
|
};
|
|
118
118
|
}, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
|
|
119
119
|
};
|
|
120
|
+
testMeta: {
|
|
121
|
+
config: {
|
|
122
|
+
readonly type: "testMeta";
|
|
123
|
+
readonly content: "none";
|
|
124
|
+
readonly propSchema: {
|
|
125
|
+
readonly metaKind: {
|
|
126
|
+
readonly default: "test";
|
|
127
|
+
};
|
|
128
|
+
readonly metaFields: {
|
|
129
|
+
readonly default: "[]";
|
|
130
|
+
};
|
|
131
|
+
readonly metaInline: {
|
|
132
|
+
readonly default: false;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
implementation: import("@blocknote/core").TiptapBlockImplementation<{
|
|
137
|
+
readonly type: "testMeta";
|
|
138
|
+
readonly content: "none";
|
|
139
|
+
readonly propSchema: {
|
|
140
|
+
readonly metaKind: {
|
|
141
|
+
readonly default: "test";
|
|
142
|
+
};
|
|
143
|
+
readonly metaFields: {
|
|
144
|
+
readonly default: "[]";
|
|
145
|
+
};
|
|
146
|
+
readonly metaInline: {
|
|
147
|
+
readonly default: false;
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
}, any, import("@blocknote/core").InlineContentSchema, import("@blocknote/core").StyleSchema>;
|
|
151
|
+
};
|
|
120
152
|
paragraph: {
|
|
121
153
|
config: {
|
|
122
154
|
type: "paragraph";
|
|
@@ -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
|
export const customSchema = BlockNoteSchema.create({
|
|
@@ -10,6 +11,7 @@ export const customSchema = BlockNoteSchema.create({
|
|
|
10
11
|
file: fileBlock,
|
|
11
12
|
testStep: stepBlock,
|
|
12
13
|
snippet: snippetBlock,
|
|
14
|
+
testMeta: testMetaBlock,
|
|
13
15
|
},
|
|
14
16
|
});
|
|
15
17
|
export const __markdownTestUtils = {
|
|
@@ -0,0 +1,17 @@
|
|
|
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
|
+
* Either a flat list (applied to both test and suite blocks) or per-kind lists.
|
|
9
|
+
* Configure from the host app via `setMetaFieldSuggestions` so embedders can
|
|
10
|
+
* plug in their own set of suggested metadata fields.
|
|
11
|
+
*/
|
|
12
|
+
export type MetaFieldSuggestionsConfig = MetaFieldSuggestion[] | {
|
|
13
|
+
test?: MetaFieldSuggestion[];
|
|
14
|
+
suite?: MetaFieldSuggestion[];
|
|
15
|
+
};
|
|
16
|
+
export declare function setMetaFieldSuggestions(config: MetaFieldSuggestionsConfig | null): void;
|
|
17
|
+
export declare function getMetaFieldSuggestions(kind: "test" | "suite"): MetaFieldSuggestion[];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Defaults follow the classical Testomatio markdown format. `id` is intentionally
|
|
2
|
+
// omitted: it is a read-only, system-assigned field, not something users add.
|
|
3
|
+
const DEFAULT_TEST_FIELDS = [
|
|
4
|
+
{ key: "priority" },
|
|
5
|
+
{ key: "type" },
|
|
6
|
+
{ key: "tags" },
|
|
7
|
+
{ key: "labels" },
|
|
8
|
+
{ key: "assignee" },
|
|
9
|
+
{ key: "creator" },
|
|
10
|
+
{ key: "shared" },
|
|
11
|
+
];
|
|
12
|
+
const DEFAULT_SUITE_FIELDS = [
|
|
13
|
+
{ key: "emoji" },
|
|
14
|
+
{ key: "tags" },
|
|
15
|
+
{ key: "labels" },
|
|
16
|
+
{ key: "assignee" },
|
|
17
|
+
];
|
|
18
|
+
let configured = null;
|
|
19
|
+
export function setMetaFieldSuggestions(config) {
|
|
20
|
+
configured = config;
|
|
21
|
+
}
|
|
22
|
+
export function getMetaFieldSuggestions(kind) {
|
|
23
|
+
if (configured) {
|
|
24
|
+
if (Array.isArray(configured)) {
|
|
25
|
+
return configured;
|
|
26
|
+
}
|
|
27
|
+
const list = kind === "suite" ? configured.suite : configured.test;
|
|
28
|
+
if (list) {
|
|
29
|
+
return list;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return kind === "suite" ? DEFAULT_SUITE_FIELDS : DEFAULT_TEST_FIELDS;
|
|
33
|
+
}
|
package/package/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { customSchema, type CustomSchema, type CustomBlock, type CustomEditor, } from "./editor/customSchema";
|
|
2
2
|
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
3
3
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
4
|
+
export { testMetaBlock } from "./editor/blocks/testMeta";
|
|
5
|
+
export { setMetaFieldSuggestions, getMetaFieldSuggestions, type MetaFieldSuggestion, type MetaFieldSuggestionsConfig, } from "./editor/testMetaFields";
|
|
4
6
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
5
7
|
export { blocksToMarkdown, markdownToBlocks, type CustomEditorBlock, type CustomPartialBlock, type MarkdownToBlocksOptions, } from "./editor/customMarkdownConverter";
|
|
6
8
|
export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, type StepSuggestion, type StepJsonApiDocument, type StepJsonApiResource, } from "./editor/stepAutocomplete";
|
package/package/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { customSchema, } from "./editor/customSchema";
|
|
2
2
|
export { stepBlock, canInsertStepOrSnippet, isStepsHeading, addStepsBlock, addSnippetBlock } from "./editor/blocks/step";
|
|
3
3
|
export { snippetBlock } from "./editor/blocks/snippet";
|
|
4
|
+
export { testMetaBlock } from "./editor/blocks/testMeta";
|
|
5
|
+
export { setMetaFieldSuggestions, getMetaFieldSuggestions, } from "./editor/testMetaFields";
|
|
4
6
|
export { markdownToHtml, htmlToMarkdown } from "./editor/blocks/markdown";
|
|
5
7
|
export { blocksToMarkdown, markdownToBlocks, } from "./editor/customMarkdownConverter";
|
|
6
8
|
export { useStepAutocomplete, parseStepsFromJsonApi, setStepsFetcher, } from "./editor/stepAutocomplete";
|
package/package/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
|
}
|
package/package.json
CHANGED