@elabs-ai/components-editor 4.0.0
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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/chunk-LBC5VJBD.js +3748 -0
- package/dist/chunk-LBC5VJBD.js.map +1 -0
- package/dist/index.css +233 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +199 -0
- package/dist/index.js +362 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/monaco-environment.d.ts +2 -0
- package/dist/lib/monaco-environment.js +44 -0
- package/dist/lib/monaco-environment.js.map +1 -0
- package/dist/markdown/frontmatter.d.ts +11 -0
- package/dist/markdown/frontmatter.js +33 -0
- package/dist/markdown/frontmatter.js.map +1 -0
- package/dist/markdown/index.css +233 -0
- package/dist/markdown/index.css.map +1 -0
- package/dist/markdown/index.d.ts +1486 -0
- package/dist/markdown/index.js +4882 -0
- package/dist/markdown/index.js.map +1 -0
- package/dist/markdown/parse.d.ts +25 -0
- package/dist/markdown/parse.js +14 -0
- package/dist/markdown/parse.js.map +1 -0
- package/dist/markdown-editor-DfBZibAn.d.ts +702 -0
- package/package.json +101 -0
- package/src/ai-objects/ai-objects-integration.test.tsx +128 -0
- package/src/ai-objects/ai-objects.stories.tsx +233 -0
- package/src/ai-objects/ai-objects.test.tsx +205 -0
- package/src/ai-objects/decision-card.tsx +178 -0
- package/src/ai-objects/directives.ts +136 -0
- package/src/ai-objects/entity.tsx +181 -0
- package/src/ai-objects/index.ts +34 -0
- package/src/ai-objects/knowledge-card.tsx +143 -0
- package/src/calc-block/calc-block.stories.tsx +297 -0
- package/src/calc-block/calc-block.test.tsx +164 -0
- package/src/calc-block/calc-block.tsx +398 -0
- package/src/calc-block/calc-editor-monaco.ts +214 -0
- package/src/calc-block/calc-editor-prose.ts +128 -0
- package/src/calc-block/calc-editor.css +80 -0
- package/src/calc-block/calc-editor.stories.tsx +272 -0
- package/src/calc-block/calc-editor.test.ts +228 -0
- package/src/calc-block/calc-editor.ts +270 -0
- package/src/calc-block/calc-inline.stories.tsx +44 -0
- package/src/calc-block/calc-inline.test.tsx +44 -0
- package/src/calc-block/calc-inline.tsx +75 -0
- package/src/calc-block/index.ts +20 -0
- package/src/calc-block/types.ts +164 -0
- package/src/code-editor/code-editor.stories.tsx +218 -0
- package/src/code-editor/code-editor.test.tsx +134 -0
- package/src/code-editor/code-editor.tsx +250 -0
- package/src/code-editor/index.ts +6 -0
- package/src/code-workspace/code-workspace.stories.tsx +51 -0
- package/src/code-workspace/code-workspace.test.tsx +146 -0
- package/src/code-workspace/code-workspace.tsx +253 -0
- package/src/code-workspace/index.ts +6 -0
- package/src/copy-button/copy-button.tsx +51 -0
- package/src/copy-button/index.ts +1 -0
- package/src/css.d.ts +3 -0
- package/src/diff-editor/diff-editor.stories.tsx +44 -0
- package/src/diff-editor/diff-editor.test.tsx +68 -0
- package/src/diff-editor/diff-editor.tsx +151 -0
- package/src/diff-editor/index.ts +1 -0
- package/src/editor-context-menu/editor-context-menu.tsx +120 -0
- package/src/editor-context-menu/index.ts +1 -0
- package/src/editor-toolbar/editor-toolbar.tsx +83 -0
- package/src/editor-toolbar/index.ts +1 -0
- package/src/index.ts +48 -0
- package/src/lib/editor-completions-monaco.test.ts +206 -0
- package/src/lib/editor-completions-monaco.ts +127 -0
- package/src/lib/editor-completions.test.ts +156 -0
- package/src/lib/editor-completions.ts +153 -0
- package/src/lib/editor-content-access-prose.test.ts +411 -0
- package/src/lib/editor-content-access-prose.ts +172 -0
- package/src/lib/editor-content-access.test.ts +182 -0
- package/src/lib/editor-content-access.ts +109 -0
- package/src/lib/languages.ts +32 -0
- package/src/lib/markdown/diff.test.ts +80 -0
- package/src/lib/markdown/diff.ts +194 -0
- package/src/lib/markdown/directives.ts +342 -0
- package/src/lib/markdown/frontmatter.ts +50 -0
- package/src/lib/markdown/markdown-scale.test.ts +65 -0
- package/src/lib/markdown/markdown-scale.ts +62 -0
- package/src/lib/markdown/merge.test.ts +64 -0
- package/src/lib/markdown/merge.ts +158 -0
- package/src/lib/markdown/slugify.test.ts +156 -0
- package/src/lib/markdown/slugify.ts +47 -0
- package/src/lib/monaco-environment.ts +103 -0
- package/src/lib/monaco-theme-bridge.ts +293 -0
- package/src/lib/use-data-theme.ts +61 -0
- package/src/markdown/frontmatter.ts +14 -0
- package/src/markdown/index.ts +323 -0
- package/src/markdown/parse.test.ts +60 -0
- package/src/markdown/parse.ts +38 -0
- package/src/markdown-academic/citations.test.ts +77 -0
- package/src/markdown-academic/citations.tsx +442 -0
- package/src/markdown-academic/footnotes.tsx +235 -0
- package/src/markdown-academic/index.ts +32 -0
- package/src/markdown-academic/math.tsx +163 -0
- package/src/markdown-academic/toc.tsx +88 -0
- package/src/markdown-editor/completions/completions-menu.tsx +96 -0
- package/src/markdown-editor/completions/completions-prose.test.ts +356 -0
- package/src/markdown-editor/completions/completions-prose.ts +313 -0
- package/src/markdown-editor/completions/completions-widget.tsx +75 -0
- package/src/markdown-editor/completions/index.ts +37 -0
- package/src/markdown-editor/directive-nodes.ts +151 -0
- package/src/markdown-editor/directive-views.test.tsx +189 -0
- package/src/markdown-editor/directive-views.tsx +795 -0
- package/src/markdown-editor/exit-keymap.test.ts +88 -0
- package/src/markdown-editor/exit-keymap.ts +87 -0
- package/src/markdown-editor/index.ts +6 -0
- package/src/markdown-editor/markdown-editor.css +250 -0
- package/src/markdown-editor/markdown-editor.directives.test.tsx +105 -0
- package/src/markdown-editor/markdown-editor.fill.test.ts +34 -0
- package/src/markdown-editor/markdown-editor.paste-embed.test.tsx +297 -0
- package/src/markdown-editor/markdown-editor.stories.tsx +560 -0
- package/src/markdown-editor/markdown-editor.strictmode.test.tsx +38 -0
- package/src/markdown-editor/markdown-editor.table.test.tsx +287 -0
- package/src/markdown-editor/markdown-editor.tsx +560 -0
- package/src/markdown-editor/milkdown-react/editor.tsx +33 -0
- package/src/markdown-editor/milkdown-react/index.ts +8 -0
- package/src/markdown-editor/milkdown-react/types.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-editor.ts +27 -0
- package/src/markdown-editor/milkdown-react/use-get-editor.ts +56 -0
- package/src/markdown-editor/milkdown-react/use-instance.ts +23 -0
- package/src/markdown-editor/paste-embed.ts +355 -0
- package/src/markdown-editor/slash/brand-slash-commands.test.ts +194 -0
- package/src/markdown-editor/slash/brand-slash-commands.ts +324 -0
- package/src/markdown-editor/slash/brand-slash-plugin.test.ts +254 -0
- package/src/markdown-editor/slash/brand-slash-plugin.ts +385 -0
- package/src/markdown-editor/slash/index.ts +107 -0
- package/src/markdown-editor/slash/insert-directive.test.ts +263 -0
- package/src/markdown-editor/slash/insert-directive.ts +345 -0
- package/src/markdown-editor/slash/monaco-slash-menu.test.tsx +198 -0
- package/src/markdown-editor/slash/monaco-slash-menu.tsx +356 -0
- package/src/markdown-editor/slash/shortcut-monaco.ts +69 -0
- package/src/markdown-editor/slash/shortcut.test.ts +157 -0
- package/src/markdown-editor/slash/shortcut.ts +57 -0
- package/src/markdown-editor/slash/slash-menu.stories.tsx +139 -0
- package/src/markdown-editor/slash/slash-menu.tsx +115 -0
- package/src/markdown-editor/slash/slash-scroll.test.tsx +52 -0
- package/src/markdown-editor/slash/slash-widget.tsx +97 -0
- package/src/markdown-editor/slash/source-slash-trigger.test.ts +42 -0
- package/src/markdown-editor/slash/source-slash-trigger.ts +46 -0
- package/src/markdown-editor/table-view.tsx +249 -0
- package/src/markdown-iteration/directive.tsx +92 -0
- package/src/markdown-iteration/edit-context.ts +56 -0
- package/src/markdown-iteration/index.ts +24 -0
- package/src/markdown-iteration/iteration-block.test.tsx +138 -0
- package/src/markdown-iteration/iteration-builder-dialog.stories.tsx +293 -0
- package/src/markdown-iteration/iteration-builder-dialog.tsx +312 -0
- package/src/markdown-iteration/iteration-builder.test.ts +252 -0
- package/src/markdown-iteration/iteration-builder.ts +303 -0
- package/src/markdown-iteration/iteration.test.ts +56 -0
- package/src/markdown-iteration/iteration.tsx +333 -0
- package/src/markdown-iteration/template-dialog.stories.tsx +63 -0
- package/src/markdown-iteration/template-dialog.test.tsx +104 -0
- package/src/markdown-iteration/template-dialog.tsx +116 -0
- package/src/markdown-outline/document-outline.stories.tsx +76 -0
- package/src/markdown-outline/document-outline.tsx +83 -0
- package/src/markdown-outline/index.ts +2 -0
- package/src/markdown-outline/markdown-outline.test.ts +49 -0
- package/src/markdown-outline/markdown-outline.ts +60 -0
- package/src/markdown-preview/code-fence.tsx +193 -0
- package/src/markdown-preview/index.ts +8 -0
- package/src/markdown-preview/markdown-preview-academic.stories.tsx +203 -0
- package/src/markdown-preview/markdown-preview-academic.test.tsx +257 -0
- package/src/markdown-preview/markdown-preview-iteration-calc.test.tsx +71 -0
- package/src/markdown-preview/markdown-preview-iteration-nested.test.tsx +58 -0
- package/src/markdown-preview/markdown-preview-iteration.stories.tsx +187 -0
- package/src/markdown-preview/markdown-preview-iteration.test.tsx +90 -0
- package/src/markdown-preview/markdown-preview-linking.test.tsx +61 -0
- package/src/markdown-preview/markdown-preview-transclusion.test.tsx +33 -0
- package/src/markdown-preview/markdown-preview.stories.tsx +108 -0
- package/src/markdown-preview/markdown-preview.test.tsx +411 -0
- package/src/markdown-preview/markdown-preview.tsx +1541 -0
- package/src/markdown-toolbar/index.ts +8 -0
- package/src/markdown-toolbar/markdown-commands.test.ts +84 -0
- package/src/markdown-toolbar/markdown-commands.ts +104 -0
- package/src/markdown-toolbar/markdown-toolbar.stories.tsx +56 -0
- package/src/markdown-toolbar/markdown-toolbar.tsx +263 -0
- package/src/markdown-workspace/focus-writing.test.ts +52 -0
- package/src/markdown-workspace/focus-writing.ts +38 -0
- package/src/markdown-workspace/index.ts +6 -0
- package/src/markdown-workspace/markdown-workspace.stories.tsx +688 -0
- package/src/markdown-workspace/markdown-workspace.test.tsx +326 -0
- package/src/markdown-workspace/markdown-workspace.tsx +787 -0
- package/src/mermaid-diagram/index.ts +1 -0
- package/src/mermaid-diagram/mermaid-diagram.stories.tsx +42 -0
- package/src/mermaid-diagram/mermaid-diagram.test.tsx +86 -0
- package/src/mermaid-diagram/mermaid-diagram.tsx +318 -0
- package/src/mermaid-diagram/mermaid-viewer.test.tsx +89 -0
- package/src/mermaid-diagram/mermaid-viewer.tsx +367 -0
- package/src/mermaid-diagram/real-parse.test.ts +42 -0
- package/src/mermaid-diagram/remediate.test.ts +60 -0
- package/src/mermaid-diagram/remediate.ts +124 -0
- package/src/mermaid-workspace/index.ts +1 -0
- package/src/mermaid-workspace/mermaid-workspace.stories.tsx +28 -0
- package/src/mermaid-workspace/mermaid-workspace.tsx +71 -0
- package/src/metric-block/index.ts +1 -0
- package/src/metric-block/metric-block.stories.tsx +40 -0
- package/src/metric-block/metric-block.tsx +14 -0
- package/src/monaco-workers.d.ts +10 -0
- package/src/prose/index.ts +14 -0
- package/src/prose/prose.stories.tsx +44 -0
- package/src/prose/prose.test.tsx +44 -0
- package/src/prose/prose.tsx +21 -0
- package/src/timeline/index.ts +18 -0
package/package.json
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elabs-ai/components-editor",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/mreimitz/elabs-components.git",
|
|
8
|
+
"directory": "packages/editor"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"sideEffects": [
|
|
16
|
+
"./src/lib/monaco-environment.ts",
|
|
17
|
+
"./dist/lib/monaco-environment.js"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"default": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./markdown": {
|
|
25
|
+
"types": "./dist/markdown/index.d.ts",
|
|
26
|
+
"default": "./dist/markdown/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./markdown/frontmatter": {
|
|
29
|
+
"types": "./dist/markdown/frontmatter.d.ts",
|
|
30
|
+
"default": "./dist/markdown/frontmatter.js"
|
|
31
|
+
},
|
|
32
|
+
"./markdown/parse": {
|
|
33
|
+
"types": "./dist/markdown/parse.d.ts",
|
|
34
|
+
"default": "./dist/markdown/parse.js"
|
|
35
|
+
},
|
|
36
|
+
"./monaco-environment": {
|
|
37
|
+
"types": "./dist/lib/monaco-environment.d.ts",
|
|
38
|
+
"default": "./dist/lib/monaco-environment.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public",
|
|
43
|
+
"registry": "https://registry.npmjs.org/"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@milkdown/kit": "^7.21.2",
|
|
47
|
+
"@prosemirror-adapter/react": "^0.5.0",
|
|
48
|
+
"@streamdown/code": "^1.1.0",
|
|
49
|
+
"class-variance-authority": "^0.7.1",
|
|
50
|
+
"js-yaml": "^4.1.0",
|
|
51
|
+
"katex": "^0.16.11",
|
|
52
|
+
"lucide-react": "^0.577.0",
|
|
53
|
+
"mermaid": "^11.15.0",
|
|
54
|
+
"remark-directive": "^4.0.0",
|
|
55
|
+
"remark-frontmatter": "^5.0.0",
|
|
56
|
+
"remark-gfm": "^4.0.1",
|
|
57
|
+
"remark-math": "^6.0.0",
|
|
58
|
+
"remark-parse": "^11.0.0",
|
|
59
|
+
"shiki": "^3.22.0",
|
|
60
|
+
"streamdown": "^2.4.0",
|
|
61
|
+
"unified": "^11.0.0",
|
|
62
|
+
"unist-util-visit": "^5.0.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"monaco-editor": "^0.55.1",
|
|
66
|
+
"react": "^18.2.0 || ^19.0.0",
|
|
67
|
+
"react-dom": "^18.2.0 || ^19.0.0",
|
|
68
|
+
"@elabs-ai/components-tokens": "4.0.0",
|
|
69
|
+
"@elabs-ai/components-ui": "4.0.0"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
73
|
+
"@testing-library/react": "^16.1.0",
|
|
74
|
+
"@testing-library/user-event": "^14.5.2",
|
|
75
|
+
"@types/js-yaml": "^4.0.9",
|
|
76
|
+
"@types/mdast": "^4.0.4",
|
|
77
|
+
"@types/react": "^19.0.2",
|
|
78
|
+
"@types/react-dom": "^19.0.2",
|
|
79
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
80
|
+
"eslint": "^9.17.0",
|
|
81
|
+
"jsdom": "^25.0.1",
|
|
82
|
+
"monaco-editor": "^0.55.1",
|
|
83
|
+
"react": "^19.0.0",
|
|
84
|
+
"react-dom": "^19.0.0",
|
|
85
|
+
"tsup": "^8.3.5",
|
|
86
|
+
"typescript": "^5.7.3",
|
|
87
|
+
"vitest": "^3.0.2",
|
|
88
|
+
"@elabs-ai/components-typescript-config": "0.1.0",
|
|
89
|
+
"@elabs-ai/components-tokens": "4.0.0",
|
|
90
|
+
"@elabs-ai/components-ui": "4.0.0",
|
|
91
|
+
"@elabs-ai/components-eslint-config": "0.1.0"
|
|
92
|
+
},
|
|
93
|
+
"scripts": {
|
|
94
|
+
"build": "rm -rf dist && tsup && node ../../scripts/link-dist-css.mjs",
|
|
95
|
+
"typecheck": "tsc --noEmit",
|
|
96
|
+
"lint": "eslint .",
|
|
97
|
+
"test": "vitest run",
|
|
98
|
+
"test:watch": "vitest",
|
|
99
|
+
"clean": "rm -rf dist .turbo coverage"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests: ai-object directives wired into MarkdownPreview via
|
|
3
|
+
* `extensions.directives`. Verifies the full parse → render pipeline for all
|
|
4
|
+
* three directive types without forking the engine.
|
|
5
|
+
*/
|
|
6
|
+
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
7
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
8
|
+
|
|
9
|
+
import { MarkdownPreview } from "../markdown-preview/markdown-preview";
|
|
10
|
+
import { aiObjectDirectives } from "./directives";
|
|
11
|
+
import type { KnowledgeSourceResolver } from "./knowledge-card";
|
|
12
|
+
|
|
13
|
+
afterEach(cleanup);
|
|
14
|
+
|
|
15
|
+
const resolve: KnowledgeSourceResolver = (path) => {
|
|
16
|
+
if (path === "notes/a.md") return { href: "/notes/a", title: "Note A" };
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const extensions = { directives: aiObjectDirectives({ resolveKnowledge: resolve }) };
|
|
21
|
+
|
|
22
|
+
describe("MarkdownPreview + aiObjectDirectives — :::decision", () => {
|
|
23
|
+
it("renders the status badge and rationale body", async () => {
|
|
24
|
+
render(
|
|
25
|
+
<MarkdownPreview extensions={extensions}>
|
|
26
|
+
{`:::decision{status=accepted date=2026-06-15}\nWe chose PostgreSQL.\n:::`}
|
|
27
|
+
</MarkdownPreview>,
|
|
28
|
+
);
|
|
29
|
+
await waitFor(() => expect(screen.getByText("Accepted")).toBeInTheDocument());
|
|
30
|
+
expect(screen.getByText("We chose PostgreSQL.")).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText("2026-06-15")).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders alternatives from the attribute", async () => {
|
|
35
|
+
render(
|
|
36
|
+
<MarkdownPreview extensions={extensions}>
|
|
37
|
+
{`:::decision{status=accepted alternatives="Redis, SQLite"}\nBody.\n:::`}
|
|
38
|
+
</MarkdownPreview>,
|
|
39
|
+
);
|
|
40
|
+
await waitFor(() => expect(screen.getByText("Accepted")).toBeInTheDocument());
|
|
41
|
+
expect(screen.getByText("Redis")).toBeInTheDocument();
|
|
42
|
+
expect(screen.getByText("SQLite")).toBeInTheDocument();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("degrades an unknown status to 'proposed' without errors", async () => {
|
|
46
|
+
render(
|
|
47
|
+
<MarkdownPreview extensions={extensions}>
|
|
48
|
+
{`:::decision{status=unknown}\nBody.\n:::`}
|
|
49
|
+
</MarkdownPreview>,
|
|
50
|
+
);
|
|
51
|
+
await waitFor(() => expect(screen.getByText("Proposed")).toBeInTheDocument());
|
|
52
|
+
expect(screen.queryByText(/Unknown block/)).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("MarkdownPreview + aiObjectDirectives — :::entity (block)", () => {
|
|
57
|
+
it("renders the entity name and kind label", async () => {
|
|
58
|
+
render(
|
|
59
|
+
<MarkdownPreview extensions={extensions}>
|
|
60
|
+
{`:::entity{kind=org name="Acme Corp"}\nPrimary vendor.\n:::`}
|
|
61
|
+
</MarkdownPreview>,
|
|
62
|
+
);
|
|
63
|
+
await waitFor(() => expect(screen.getByText("Acme Corp")).toBeInTheDocument());
|
|
64
|
+
expect(screen.getByText("Organisation")).toBeInTheDocument();
|
|
65
|
+
expect(screen.getByText("Primary vendor.")).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("MarkdownPreview + aiObjectDirectives — :entity[…] (inline chip)", () => {
|
|
70
|
+
it("renders the chip inside the paragraph text flow", async () => {
|
|
71
|
+
render(
|
|
72
|
+
<MarkdownPreview extensions={extensions}>
|
|
73
|
+
{`Owner :entity[Acme]{kind=org} ships it.`}
|
|
74
|
+
</MarkdownPreview>,
|
|
75
|
+
);
|
|
76
|
+
await waitFor(() => expect(screen.getByText("Acme")).toBeInTheDocument());
|
|
77
|
+
// The chip must live inside a <p> — no block wrapper around it.
|
|
78
|
+
const chip = screen.getByRole("mark", { name: /Acme \(Organisation\)/i });
|
|
79
|
+
expect(chip).toBeInTheDocument();
|
|
80
|
+
expect(chip.closest("p")).not.toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("renders an inline chip for an unknown kind without errors", async () => {
|
|
84
|
+
render(
|
|
85
|
+
<MarkdownPreview extensions={extensions}>
|
|
86
|
+
{`See :entity[Widget]{kind=thing} for details.`}
|
|
87
|
+
</MarkdownPreview>,
|
|
88
|
+
);
|
|
89
|
+
await waitFor(() => expect(screen.getByText("Widget")).toBeInTheDocument());
|
|
90
|
+
expect(screen.queryByText(/Unknown block/)).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("MarkdownPreview + aiObjectDirectives — :::knowledge", () => {
|
|
95
|
+
it("renders the fact body and a resolved source link", async () => {
|
|
96
|
+
render(
|
|
97
|
+
<MarkdownPreview extensions={extensions}>
|
|
98
|
+
{`:::knowledge{sources="notes/a.md"}\nPostgreSQL reduced latency by 38%.\n:::`}
|
|
99
|
+
</MarkdownPreview>,
|
|
100
|
+
);
|
|
101
|
+
await waitFor(() =>
|
|
102
|
+
expect(screen.getByText("PostgreSQL reduced latency by 38%.")).toBeInTheDocument(),
|
|
103
|
+
);
|
|
104
|
+
expect(screen.getByRole("link", { name: /Source: Note A/i })).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("degrades an unresolved source to a plain label (no broken link)", async () => {
|
|
108
|
+
render(
|
|
109
|
+
<MarkdownPreview extensions={extensions}>
|
|
110
|
+
{`:::knowledge{sources="notes/missing.md"}\nfact\n:::`}
|
|
111
|
+
</MarkdownPreview>,
|
|
112
|
+
);
|
|
113
|
+
await waitFor(() => expect(screen.getByText("fact")).toBeInTheDocument());
|
|
114
|
+
expect(screen.queryByRole("link")).toBeNull();
|
|
115
|
+
expect(screen.getByText("notes/missing.md")).toBeInTheDocument();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("renders mixed resolved/unresolved sources without errors", async () => {
|
|
119
|
+
render(
|
|
120
|
+
<MarkdownPreview extensions={extensions}>
|
|
121
|
+
{`:::knowledge{sources="notes/a.md, notes/missing.md"}\nfact\n:::`}
|
|
122
|
+
</MarkdownPreview>,
|
|
123
|
+
);
|
|
124
|
+
await waitFor(() => expect(screen.getByText("fact")).toBeInTheDocument());
|
|
125
|
+
expect(screen.getByRole("link", { name: /Note A/i })).toBeInTheDocument();
|
|
126
|
+
expect(screen.getByText("notes/missing.md")).toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
2
|
+
|
|
3
|
+
import { DecisionCard } from "./decision-card";
|
|
4
|
+
import { EntityCard, EntityChip } from "./entity";
|
|
5
|
+
import { KnowledgeCard, type KnowledgeSourceResolver } from "./knowledge-card";
|
|
6
|
+
|
|
7
|
+
/* ------------------------------------------------------------------ */
|
|
8
|
+
/* DecisionCard */
|
|
9
|
+
/* ------------------------------------------------------------------ */
|
|
10
|
+
|
|
11
|
+
const decisionMeta = {
|
|
12
|
+
title: "Editor/AiObjects/DecisionCard",
|
|
13
|
+
component: DecisionCard,
|
|
14
|
+
tags: ["autodocs"],
|
|
15
|
+
parameters: { layout: "padded" },
|
|
16
|
+
} satisfies Meta<typeof DecisionCard>;
|
|
17
|
+
|
|
18
|
+
export default decisionMeta;
|
|
19
|
+
type DecisionStory = StoryObj<typeof decisionMeta>;
|
|
20
|
+
|
|
21
|
+
export const Accepted: DecisionStory = {
|
|
22
|
+
args: {
|
|
23
|
+
status: "accepted",
|
|
24
|
+
date: "2026-06-15",
|
|
25
|
+
alternatives: "Redis cache, in-memory map, SQLite",
|
|
26
|
+
children: (
|
|
27
|
+
<p>
|
|
28
|
+
PostgreSQL was chosen because it already runs in production, supports JSONB for flexible
|
|
29
|
+
schemas, and the team has operational expertise with it. Introducing a second data store for
|
|
30
|
+
caching would add operational burden that outweighs the latency gain at current scale.
|
|
31
|
+
</p>
|
|
32
|
+
),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Proposed: DecisionStory = {
|
|
37
|
+
args: {
|
|
38
|
+
status: "proposed",
|
|
39
|
+
date: "2026-07-01",
|
|
40
|
+
children: <p>Migrate the report pipeline to streaming to reduce time-to-first-byte by ~40%.</p>,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const Rejected: DecisionStory = {
|
|
45
|
+
args: {
|
|
46
|
+
status: "rejected",
|
|
47
|
+
date: "2026-05-10",
|
|
48
|
+
alternatives: "Micro-frontend shell, Module federation",
|
|
49
|
+
children: <p>Full micro-frontend split was rejected due to excessive coordination overhead.</p>,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const Superseded: DecisionStory = {
|
|
54
|
+
args: {
|
|
55
|
+
status: "superseded",
|
|
56
|
+
date: "2025-11-01",
|
|
57
|
+
children: (
|
|
58
|
+
<p>
|
|
59
|
+
Original caching strategy (Redis) was superseded by the PostgreSQL materialized-view
|
|
60
|
+
approach in ADR-0014.
|
|
61
|
+
</p>
|
|
62
|
+
),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/** No rationale body — header-only layout. */
|
|
67
|
+
export const NoBody: DecisionStory = {
|
|
68
|
+
args: {
|
|
69
|
+
status: "accepted",
|
|
70
|
+
date: "2026-06-01",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** Unrecognised status degrades to "proposed". */
|
|
75
|
+
export const UnknownStatus: DecisionStory = {
|
|
76
|
+
args: {
|
|
77
|
+
status: "in-review",
|
|
78
|
+
date: "2026-06-15",
|
|
79
|
+
children: <p>Unknown status values fall back to the "proposed" state.</p>,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Long alternatives list — chips wrap. */
|
|
84
|
+
export const ManyAlternatives: DecisionStory = {
|
|
85
|
+
args: {
|
|
86
|
+
status: "accepted",
|
|
87
|
+
date: "2026-06-15",
|
|
88
|
+
alternatives: "Option A, Option B, Option C, Option D, Option E, Option F, Option G, Option H",
|
|
89
|
+
children: <p>Chose the simplest path that could possibly work.</p>,
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/* ------------------------------------------------------------------ */
|
|
94
|
+
/* EntityChip (inline) */
|
|
95
|
+
/* ------------------------------------------------------------------ */
|
|
96
|
+
|
|
97
|
+
export const EntityChipDefault: StoryObj = {
|
|
98
|
+
name: "EntityChip / Default",
|
|
99
|
+
render: () => (
|
|
100
|
+
<p className="max-w-prose text-body text-foreground">
|
|
101
|
+
The contract was signed between <EntityChip kind="org">Acme Corp</EntityChip> and{" "}
|
|
102
|
+
<EntityChip kind="person">Jane Smith</EntityChip> at the{" "}
|
|
103
|
+
<EntityChip kind="place">London office</EntityChip>. The{" "}
|
|
104
|
+
<EntityChip kind="product">DataBridge SDK</EntityChip> was the primary deliverable, and the{" "}
|
|
105
|
+
<EntityChip kind="concept">data sovereignty</EntityChip> requirement shaped every clause.
|
|
106
|
+
</p>
|
|
107
|
+
),
|
|
108
|
+
parameters: { layout: "padded" },
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const EntityChipKinds: StoryObj = {
|
|
112
|
+
name: "EntityChip / All kinds",
|
|
113
|
+
render: () => (
|
|
114
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
115
|
+
<EntityChip kind="org">Acme Corp</EntityChip>
|
|
116
|
+
<EntityChip kind="person">Jane Smith</EntityChip>
|
|
117
|
+
<EntityChip kind="place">London</EntityChip>
|
|
118
|
+
<EntityChip kind="product">DataBridge</EntityChip>
|
|
119
|
+
<EntityChip kind="concept">Data Sovereignty</EntityChip>
|
|
120
|
+
<EntityChip>Unknown kind</EntityChip>
|
|
121
|
+
</div>
|
|
122
|
+
),
|
|
123
|
+
parameters: { layout: "padded" },
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/* ------------------------------------------------------------------ */
|
|
127
|
+
/* EntityCard (block) */
|
|
128
|
+
/* ------------------------------------------------------------------ */
|
|
129
|
+
|
|
130
|
+
export const EntityCardOrg: StoryObj = {
|
|
131
|
+
name: "EntityCard / Organisation",
|
|
132
|
+
render: () => (
|
|
133
|
+
<EntityCard kind="org" name="Acme Corp">
|
|
134
|
+
<p>
|
|
135
|
+
Acme Corp is the primary vendor for the data integration layer. Founded 2014, HQ Berlin.
|
|
136
|
+
</p>
|
|
137
|
+
</EntityCard>
|
|
138
|
+
),
|
|
139
|
+
parameters: { layout: "padded" },
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const EntityCardPerson: StoryObj = {
|
|
143
|
+
name: "EntityCard / Person",
|
|
144
|
+
render: () => (
|
|
145
|
+
<EntityCard kind="person" name="Jane Smith">
|
|
146
|
+
<p>Jane Smith is the lead architect on the platform migration project.</p>
|
|
147
|
+
</EntityCard>
|
|
148
|
+
),
|
|
149
|
+
parameters: { layout: "padded" },
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
export const EntityCardNoBody: StoryObj = {
|
|
153
|
+
name: "EntityCard / No body",
|
|
154
|
+
render: () => <EntityCard kind="product" name="DataBridge SDK" />,
|
|
155
|
+
parameters: { layout: "padded" },
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/* ------------------------------------------------------------------ */
|
|
159
|
+
/* KnowledgeCard */
|
|
160
|
+
/* ------------------------------------------------------------------ */
|
|
161
|
+
|
|
162
|
+
const resolve: KnowledgeSourceResolver = (path) => {
|
|
163
|
+
const map: Record<string, { href: string; title: string }> = {
|
|
164
|
+
"notes/adr-0012.md": { href: "#adr-0012", title: "ADR-0012: Caching strategy" },
|
|
165
|
+
"notes/perf-report.md": { href: "#perf-report", title: "Q1 Performance Report" },
|
|
166
|
+
};
|
|
167
|
+
return map[path] ?? null;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const KnowledgeCardResolved: StoryObj = {
|
|
171
|
+
name: "KnowledgeCard / Resolved sources",
|
|
172
|
+
render: () => (
|
|
173
|
+
<KnowledgeCard sources="notes/adr-0012.md, notes/perf-report.md" resolve={resolve}>
|
|
174
|
+
<p>
|
|
175
|
+
The PostgreSQL materialized-view approach reduces p95 query latency by 38% compared to the
|
|
176
|
+
Redis cache measured in Q1 2026.
|
|
177
|
+
</p>
|
|
178
|
+
</KnowledgeCard>
|
|
179
|
+
),
|
|
180
|
+
parameters: { layout: "padded" },
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const KnowledgeCardPartialResolve: StoryObj = {
|
|
184
|
+
name: "KnowledgeCard / Partial resolve (one stale link)",
|
|
185
|
+
render: () => (
|
|
186
|
+
<KnowledgeCard
|
|
187
|
+
sources="notes/adr-0012.md, notes/deleted-note.md, notes/perf-report.md"
|
|
188
|
+
resolve={resolve}
|
|
189
|
+
>
|
|
190
|
+
<p>
|
|
191
|
+
Stale links degrade to plain labels — the card never shows a broken anchor or throws an
|
|
192
|
+
error.
|
|
193
|
+
</p>
|
|
194
|
+
</KnowledgeCard>
|
|
195
|
+
),
|
|
196
|
+
parameters: { layout: "padded" },
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const KnowledgeCardNoResolve: StoryObj = {
|
|
200
|
+
name: "KnowledgeCard / No resolver (all labels)",
|
|
201
|
+
render: () => (
|
|
202
|
+
<KnowledgeCard sources="notes/a.md, notes/b.md">
|
|
203
|
+
<p>Without a resolver every source path renders as a plain label.</p>
|
|
204
|
+
</KnowledgeCard>
|
|
205
|
+
),
|
|
206
|
+
parameters: { layout: "padded" },
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export const KnowledgeCardNoSources: StoryObj = {
|
|
210
|
+
name: "KnowledgeCard / No sources",
|
|
211
|
+
render: () => (
|
|
212
|
+
<KnowledgeCard>
|
|
213
|
+
<p>A knowledge card with no sources attribute — the footer is omitted entirely.</p>
|
|
214
|
+
</KnowledgeCard>
|
|
215
|
+
),
|
|
216
|
+
parameters: { layout: "padded" },
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const KnowledgeCardLongFact: StoryObj = {
|
|
220
|
+
name: "KnowledgeCard / Long fact body",
|
|
221
|
+
render: () => (
|
|
222
|
+
<KnowledgeCard sources="notes/adr-0012.md" resolve={resolve}>
|
|
223
|
+
<p>
|
|
224
|
+
The decision to adopt PostgreSQL materialized views was driven by three factors: the
|
|
225
|
+
operational simplicity of a single data store, the 38% latency reduction observed in Q1
|
|
226
|
+
benchmarks, and the team’s existing expertise with PostgreSQL query planning. The
|
|
227
|
+
Redis alternative was estimated to add 1.5 FTE of operational overhead annually, which the
|
|
228
|
+
engineering leadership deemed unacceptable given current headcount constraints.
|
|
229
|
+
</p>
|
|
230
|
+
</KnowledgeCard>
|
|
231
|
+
),
|
|
232
|
+
parameters: { layout: "padded" },
|
|
233
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { render, screen } from "@testing-library/react";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { DecisionCard } from "./decision-card";
|
|
5
|
+
import { EntityCard, EntityChip } from "./entity";
|
|
6
|
+
import { KnowledgeCard, type KnowledgeSourceResolver } from "./knowledge-card";
|
|
7
|
+
|
|
8
|
+
/* ------------------------------------------------------------------ */
|
|
9
|
+
/* DecisionCard */
|
|
10
|
+
/* ------------------------------------------------------------------ */
|
|
11
|
+
|
|
12
|
+
describe("DecisionCard", () => {
|
|
13
|
+
it("renders the status badge and date", () => {
|
|
14
|
+
render(
|
|
15
|
+
<DecisionCard status="accepted" date="2026-06-15">
|
|
16
|
+
<p>We chose PostgreSQL.</p>
|
|
17
|
+
</DecisionCard>,
|
|
18
|
+
);
|
|
19
|
+
expect(screen.getByText("Accepted")).toBeInTheDocument();
|
|
20
|
+
expect(screen.getByText("2026-06-15")).toBeInTheDocument();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders the rationale body (children)", () => {
|
|
24
|
+
render(
|
|
25
|
+
<DecisionCard status="accepted">
|
|
26
|
+
<p>We chose PostgreSQL.</p>
|
|
27
|
+
</DecisionCard>,
|
|
28
|
+
);
|
|
29
|
+
expect(screen.getByText("We chose PostgreSQL.")).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders the alternatives section from the comma-separated attribute", () => {
|
|
33
|
+
render(
|
|
34
|
+
<DecisionCard status="accepted" alternatives="Redis, SQLite, DynamoDB">
|
|
35
|
+
<p>body</p>
|
|
36
|
+
</DecisionCard>,
|
|
37
|
+
);
|
|
38
|
+
expect(screen.getByText("Redis")).toBeInTheDocument();
|
|
39
|
+
expect(screen.getByText("SQLite")).toBeInTheDocument();
|
|
40
|
+
expect(screen.getByText("DynamoDB")).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("omits the alternatives section when not provided", () => {
|
|
44
|
+
render(<DecisionCard status="accepted">body</DecisionCard>);
|
|
45
|
+
expect(screen.queryByText("Alternatives considered")).toBeNull();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("degrades an unrecognised status to 'proposed'", () => {
|
|
49
|
+
render(<DecisionCard status="in-review">body</DecisionCard>);
|
|
50
|
+
expect(screen.getByText("Proposed")).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("has an accessible section label derived from the status", () => {
|
|
54
|
+
render(<DecisionCard status="rejected">body</DecisionCard>);
|
|
55
|
+
expect(screen.getByRole("region", { name: /Decision: Rejected/i })).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("renders without children (header-only layout)", () => {
|
|
59
|
+
render(<DecisionCard status="accepted" date="2026-06-01" />);
|
|
60
|
+
expect(screen.getByText("Accepted")).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
/* ------------------------------------------------------------------ */
|
|
65
|
+
/* EntityChip */
|
|
66
|
+
/* ------------------------------------------------------------------ */
|
|
67
|
+
|
|
68
|
+
describe("EntityChip", () => {
|
|
69
|
+
it("renders the label text", () => {
|
|
70
|
+
render(<EntityChip kind="org">Acme Corp</EntityChip>);
|
|
71
|
+
expect(screen.getByText("Acme Corp")).toBeInTheDocument();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("carries an aria-label that names the entity and its kind", () => {
|
|
75
|
+
render(<EntityChip kind="org">Acme Corp</EntityChip>);
|
|
76
|
+
expect(screen.getByRole("mark", { name: /Acme Corp \(Organisation\)/i })).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("is a <span> (inline element — valid inside <p>)", () => {
|
|
80
|
+
const { container } = render(
|
|
81
|
+
<p>
|
|
82
|
+
Text <EntityChip kind="person">Jane</EntityChip> here
|
|
83
|
+
</p>,
|
|
84
|
+
);
|
|
85
|
+
const chip = container.querySelector("span[role='mark']");
|
|
86
|
+
expect(chip).not.toBeNull();
|
|
87
|
+
expect(chip?.closest("p")).not.toBeNull();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("renders all five known kinds without errors", () => {
|
|
91
|
+
const kinds = ["org", "person", "place", "product", "concept"] as const;
|
|
92
|
+
for (const kind of kinds) {
|
|
93
|
+
render(<EntityChip kind={kind}>{kind}</EntityChip>);
|
|
94
|
+
expect(screen.getAllByText(kind).length).toBeGreaterThan(0);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("gracefully renders an unknown kind without throwing", () => {
|
|
99
|
+
render(<EntityChip kind="unknown-kind">X</EntityChip>);
|
|
100
|
+
expect(screen.getByText("X")).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/* ------------------------------------------------------------------ */
|
|
105
|
+
/* EntityCard */
|
|
106
|
+
/* ------------------------------------------------------------------ */
|
|
107
|
+
|
|
108
|
+
describe("EntityCard", () => {
|
|
109
|
+
it("renders the entity name and kind label", () => {
|
|
110
|
+
render(<EntityCard kind="org" name="Acme Corp" />);
|
|
111
|
+
expect(screen.getByText("Acme Corp")).toBeInTheDocument();
|
|
112
|
+
expect(screen.getByText("Organisation")).toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("renders the description body", () => {
|
|
116
|
+
render(
|
|
117
|
+
<EntityCard kind="person" name="Jane">
|
|
118
|
+
<p>Lead architect.</p>
|
|
119
|
+
</EntityCard>,
|
|
120
|
+
);
|
|
121
|
+
expect(screen.getByText("Lead architect.")).toBeInTheDocument();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("has an accessible section label naming kind and entity", () => {
|
|
125
|
+
render(<EntityCard kind="org" name="Acme Corp" />);
|
|
126
|
+
expect(screen.getByRole("region", { name: /Organisation: Acme Corp/i })).toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("renders without a name (kind-only card)", () => {
|
|
130
|
+
render(<EntityCard kind="concept" />);
|
|
131
|
+
expect(screen.getByText("Concept")).toBeInTheDocument();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
/* ------------------------------------------------------------------ */
|
|
136
|
+
/* KnowledgeCard */
|
|
137
|
+
/* ------------------------------------------------------------------ */
|
|
138
|
+
|
|
139
|
+
describe("KnowledgeCard", () => {
|
|
140
|
+
const resolve: KnowledgeSourceResolver = (path) => {
|
|
141
|
+
if (path === "notes/a.md") return { href: "/notes/a", title: "Note A" };
|
|
142
|
+
return null;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
it("renders the fact body", () => {
|
|
146
|
+
render(
|
|
147
|
+
<KnowledgeCard>
|
|
148
|
+
<p>PostgreSQL reduced p95 latency by 38%.</p>
|
|
149
|
+
</KnowledgeCard>,
|
|
150
|
+
);
|
|
151
|
+
expect(screen.getByText("PostgreSQL reduced p95 latency by 38%.")).toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("renders a resolved source as a link with its title", () => {
|
|
155
|
+
render(
|
|
156
|
+
<KnowledgeCard sources="notes/a.md" resolve={resolve}>
|
|
157
|
+
<p>fact</p>
|
|
158
|
+
</KnowledgeCard>,
|
|
159
|
+
);
|
|
160
|
+
const link = screen.getByRole("link", { name: /Source: Note A/i });
|
|
161
|
+
expect(link).toBeInTheDocument();
|
|
162
|
+
expect(link).toHaveAttribute("href", "/notes/a");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it("renders an unresolved source as a plain label (no link)", () => {
|
|
166
|
+
render(
|
|
167
|
+
<KnowledgeCard sources="notes/missing.md" resolve={resolve}>
|
|
168
|
+
<p>fact</p>
|
|
169
|
+
</KnowledgeCard>,
|
|
170
|
+
);
|
|
171
|
+
expect(screen.queryByRole("link")).toBeNull();
|
|
172
|
+
expect(screen.getByText("notes/missing.md")).toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("renders mixed resolved/unresolved sources without errors", () => {
|
|
176
|
+
render(
|
|
177
|
+
<KnowledgeCard sources="notes/a.md, notes/missing.md" resolve={resolve}>
|
|
178
|
+
<p>fact</p>
|
|
179
|
+
</KnowledgeCard>,
|
|
180
|
+
);
|
|
181
|
+
expect(screen.getByRole("link", { name: /Note A/i })).toBeInTheDocument();
|
|
182
|
+
expect(screen.getByText("notes/missing.md")).toBeInTheDocument();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("omits the sources section when no sources attribute is provided", () => {
|
|
186
|
+
render(<KnowledgeCard>fact</KnowledgeCard>);
|
|
187
|
+
expect(screen.queryByText("Sources")).toBeNull();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("works without a resolver (all sources as plain labels)", () => {
|
|
191
|
+
render(
|
|
192
|
+
<KnowledgeCard sources="notes/a.md, notes/b.md">
|
|
193
|
+
<p>fact</p>
|
|
194
|
+
</KnowledgeCard>,
|
|
195
|
+
);
|
|
196
|
+
expect(screen.queryByRole("link")).toBeNull();
|
|
197
|
+
expect(screen.getByText("notes/a.md")).toBeInTheDocument();
|
|
198
|
+
expect(screen.getByText("notes/b.md")).toBeInTheDocument();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("has an accessible section label", () => {
|
|
202
|
+
render(<KnowledgeCard>fact</KnowledgeCard>);
|
|
203
|
+
expect(screen.getByRole("region", { name: /Knowledge fact/i })).toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
});
|