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
|
@@ -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) {
|
|
@@ -262,8 +262,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
262
262
|
return lines;
|
|
263
263
|
}
|
|
264
264
|
case "codeBlock": {
|
|
265
|
-
const
|
|
266
|
-
const language = /[\s`]/.test(rawLanguage) ? "" : rawLanguage;
|
|
265
|
+
const language = block.props.language || "";
|
|
267
266
|
const fence = "```" + language;
|
|
268
267
|
const body = inlineContentToPlainText(block.content);
|
|
269
268
|
lines.push(fence);
|
|
@@ -317,19 +316,48 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
317
316
|
}
|
|
318
317
|
return lines;
|
|
319
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
|
+
}
|
|
320
348
|
case "testStep":
|
|
321
349
|
case "snippet": {
|
|
322
350
|
const isSnippet = block.type === "snippet";
|
|
323
|
-
const snippetId = isSnippet ? ((
|
|
351
|
+
const snippetId = isSnippet ? ((_f = block.props.snippetId) !== null && _f !== void 0 ? _f : "").trim() : "";
|
|
324
352
|
const stepTitle = isSnippet
|
|
325
|
-
? ((
|
|
326
|
-
: ((
|
|
353
|
+
? ((_g = block.props.snippetTitle) !== null && _g !== void 0 ? _g : "").trim()
|
|
354
|
+
: ((_h = block.props.stepTitle) !== null && _h !== void 0 ? _h : "").trim();
|
|
327
355
|
const stepData = isSnippet
|
|
328
|
-
? ((
|
|
329
|
-
: ((
|
|
356
|
+
? ((_j = block.props.snippetData) !== null && _j !== void 0 ? _j : "").trim()
|
|
357
|
+
: ((_k = block.props.stepData) !== null && _k !== void 0 ? _k : "").trim();
|
|
330
358
|
const expectedResult = isSnippet
|
|
331
|
-
? ((
|
|
332
|
-
: ((
|
|
359
|
+
? ((_l = block.props.snippetExpectedResult) !== null && _l !== void 0 ? _l : "").trim()
|
|
360
|
+
: ((_m = block.props.expectedResult) !== null && _m !== void 0 ? _m : "").trim();
|
|
333
361
|
if (isSnippet) {
|
|
334
362
|
if (snippetId) {
|
|
335
363
|
lines.push(`<!-- begin snippet #${snippetId} -->`);
|
|
@@ -353,7 +381,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
353
381
|
const normalizedExpectedForCheck = stripExpectedPrefix(expectedResult).trim();
|
|
354
382
|
const hasContent = stepData.length > 0 || normalizedExpectedForCheck.length > 0;
|
|
355
383
|
if (normalizedTitle.length > 0 || hasContent) {
|
|
356
|
-
const listStyle = (
|
|
384
|
+
const listStyle = (_o = block.props.listStyle) !== null && _o !== void 0 ? _o : "bullet";
|
|
357
385
|
const prefix = listStyle === "ordered" ? `${(stepIndex !== null && stepIndex !== void 0 ? stepIndex : 0) + 1}.` : "*";
|
|
358
386
|
lines.push(normalizedTitle.length > 0 ? `${prefix} ${escapeStepContent(normalizedTitle)}` : `${prefix} `);
|
|
359
387
|
}
|
|
@@ -407,7 +435,7 @@ function serializeBlock(block, ctx, orderedIndex, stepIndex) {
|
|
|
407
435
|
return lines;
|
|
408
436
|
}
|
|
409
437
|
const headerRowCount = rows.length
|
|
410
|
-
? Math.min(rows.length, Math.max((
|
|
438
|
+
? Math.min(rows.length, Math.max((_p = tableContent.headerRows) !== null && _p !== void 0 ? _p : 1, 1))
|
|
411
439
|
: 0;
|
|
412
440
|
const columnAlignments = new Array(columnCount).fill("left");
|
|
413
441
|
const getCellAlignment = (cell) => {
|
|
@@ -1088,17 +1116,8 @@ function parseCodeBlock(lines, index) {
|
|
|
1088
1116
|
nextIndex: index + 1,
|
|
1089
1117
|
};
|
|
1090
1118
|
}
|
|
1091
|
-
const
|
|
1092
|
-
let language = "";
|
|
1119
|
+
const language = afterOpening.trim();
|
|
1093
1120
|
const body = [];
|
|
1094
|
-
if (info.length > 0) {
|
|
1095
|
-
if (/[\s`]/.test(info)) {
|
|
1096
|
-
body.push(afterOpening);
|
|
1097
|
-
}
|
|
1098
|
-
else {
|
|
1099
|
-
language = info;
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
1121
|
let next = index + 1;
|
|
1103
1122
|
while (next < lines.length && !lines[next].startsWith("```")) {
|
|
1104
1123
|
body.push(lines[next]);
|
|
@@ -1155,6 +1174,86 @@ function parseParagraph(lines, index) {
|
|
|
1155
1174
|
nextIndex: index + 1,
|
|
1156
1175
|
};
|
|
1157
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
|
+
}
|
|
1158
1257
|
function parseSnippetWrapper(lines, index) {
|
|
1159
1258
|
const trimmed = lines[index].trim();
|
|
1160
1259
|
const startMatch = trimmed.match(/^<!--\s*begin snippet\s*#?\s*([^\s>]+)\s*-->/i);
|
|
@@ -1267,6 +1366,14 @@ export function markdownToBlocks(markdown, _options) {
|
|
|
1267
1366
|
index += 1;
|
|
1268
1367
|
continue;
|
|
1269
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
|
+
}
|
|
1270
1377
|
const snippetWrapper = stepsHeadingLevel !== null
|
|
1271
1378
|
? parseSnippetWrapper(lines, index)
|
|
1272
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";
|