@opensyster/diagram-ui 0.0.3-alpha
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/.devcontainer/devcontainer.json +34 -0
- package/.github/workflows/ci.yml +14 -0
- package/.github/workflows/release.yml +20 -0
- package/Makefile +20 -0
- package/README.md +77 -0
- package/bunfig.toml +8 -0
- package/package.json +33 -0
- package/src/__tests__/edgeFactory.test.tsx +387 -0
- package/src/__tests__/integration.test.tsx +143 -0
- package/src/__tests__/nodeFactory.test.tsx +392 -0
- package/src/__tests__/theme.test.tsx +318 -0
- package/src/edges/edgeConfig.ts +136 -0
- package/src/edges/edgeFactory.tsx +140 -0
- package/src/edges/index.ts +9 -0
- package/src/index.ts +37 -0
- package/src/layout/elk-layout.ts +199 -0
- package/src/layout/general-view.ts +194 -0
- package/src/layout/index.ts +10 -0
- package/src/layout/sizing.ts +107 -0
- package/src/layout/strategy.ts +54 -0
- package/src/nodes/SysMLNode.tsx +167 -0
- package/src/nodes/index.ts +7 -0
- package/src/nodes/nodeConfig.ts +196 -0
- package/src/nodes/nodeFactory.tsx +69 -0
- package/src/theme/ThemeContext.tsx +97 -0
- package/src/theme/index.ts +6 -0
- package/src/theme/tokens.ts +471 -0
- package/src/types.ts +132 -0
- package/test-setup.ts +17 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Node.js Development Container",
|
|
3
|
+
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bookworm",
|
|
4
|
+
"features": {
|
|
5
|
+
"ghcr.io/devcontainers/features/github-cli:1": {
|
|
6
|
+
"version": "latest"
|
|
7
|
+
},
|
|
8
|
+
"ghcr.io/devcontainers-extra/features/bun:1": {
|
|
9
|
+
"version": "latest"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"customizations": {
|
|
13
|
+
"vscode": {
|
|
14
|
+
"extensions": [
|
|
15
|
+
"dbaeumer.vscode-eslint",
|
|
16
|
+
"esbenp.prettier-vscode",
|
|
17
|
+
"ms-vscode.vscode-typescript-next"
|
|
18
|
+
],
|
|
19
|
+
"settings": {
|
|
20
|
+
"editor.formatOnSave": true,
|
|
21
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
22
|
+
"[typescript]": {
|
|
23
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
24
|
+
},
|
|
25
|
+
"[javascript]": {
|
|
26
|
+
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"postCreateCommand": "node --version && npm --version && bun --version",
|
|
32
|
+
"forwardPorts": [3000, 5173],
|
|
33
|
+
"remoteUser": "node"
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# CI workflow using reusable syster-pipelines workflow
|
|
2
|
+
# See: https://github.com/jade-codes/syster-pipelines
|
|
3
|
+
|
|
4
|
+
name: CI
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
branches: [main]
|
|
9
|
+
pull_request:
|
|
10
|
+
branches: [main]
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
npm-ci:
|
|
14
|
+
uses: jade-codes/syster-pipelines/.github/workflows/npm-ci.yml@main
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Release workflow using reusable syster-pipelines workflow
|
|
2
|
+
# See: https://github.com/jade-codes/syster-pipelines
|
|
3
|
+
|
|
4
|
+
name: Release
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags:
|
|
9
|
+
- 'v[0-9]+.[0-9]+.[0-9]+'
|
|
10
|
+
- 'v[0-9]+.[0-9]+.[0-9]+-*' # Pre-releases like v0.1.0-alpha
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
contents: write
|
|
14
|
+
id-token: write
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
npm-release:
|
|
18
|
+
uses: jade-codes/syster-pipelines/.github/workflows/npm-release.yml@main
|
|
19
|
+
secrets:
|
|
20
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/Makefile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.PHONY: run-guidelines typecheck test package clean
|
|
2
|
+
|
|
3
|
+
# Run all quality checks
|
|
4
|
+
run-guidelines: typecheck test
|
|
5
|
+
|
|
6
|
+
# Run TypeScript type checking
|
|
7
|
+
typecheck:
|
|
8
|
+
bun run typecheck
|
|
9
|
+
|
|
10
|
+
# Run tests
|
|
11
|
+
test:
|
|
12
|
+
bun test
|
|
13
|
+
|
|
14
|
+
# Package for publishing (placeholder)
|
|
15
|
+
package:
|
|
16
|
+
@echo "Package target not yet implemented"
|
|
17
|
+
|
|
18
|
+
# Clean build artifacts
|
|
19
|
+
clean:
|
|
20
|
+
rm -rf node_modules dist
|
package/README.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# @syster/diagram-ui
|
|
2
|
+
|
|
3
|
+
React UI components for SysML v2 diagrams.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @syster/diagram-ui
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { nodeTypes, edgeTypes } from '@syster/diagram-ui';
|
|
15
|
+
import { ReactFlow } from '@xyflow/react';
|
|
16
|
+
|
|
17
|
+
function DiagramViewer({ nodes, edges }) {
|
|
18
|
+
return (
|
|
19
|
+
<ReactFlow
|
|
20
|
+
nodes={nodes}
|
|
21
|
+
edges={edges}
|
|
22
|
+
nodeTypes={nodeTypes}
|
|
23
|
+
edgeTypes={edgeTypes}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Features
|
|
30
|
+
|
|
31
|
+
- React Flow node components for SysML element types
|
|
32
|
+
- React Flow edge components for SysML relationships
|
|
33
|
+
- Styled for VS Code themes
|
|
34
|
+
- Supports all SysML v2 definition and usage types
|
|
35
|
+
|
|
36
|
+
## Dependencies
|
|
37
|
+
|
|
38
|
+
- `@xyflow/react` - React Flow library
|
|
39
|
+
- `@opensyster/diagram-core` - Core data types
|
|
40
|
+
|
|
41
|
+
## License
|
|
42
|
+
|
|
43
|
+
MIT
|
|
44
|
+
|
|
45
|
+
## Development
|
|
46
|
+
|
|
47
|
+
### DevContainer Setup (Recommended)
|
|
48
|
+
|
|
49
|
+
This project includes a DevContainer configuration for a consistent development environment.
|
|
50
|
+
|
|
51
|
+
**Using VS Code:**
|
|
52
|
+
1. Install the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)
|
|
53
|
+
2. Open this repository in VS Code
|
|
54
|
+
3. Click "Reopen in Container" when prompted (or use Command Palette: "Dev Containers: Reopen in Container")
|
|
55
|
+
|
|
56
|
+
**What's included:**
|
|
57
|
+
- Node.js 20 LTS
|
|
58
|
+
- Bun runtime
|
|
59
|
+
- ESLint, Prettier
|
|
60
|
+
- GitHub CLI
|
|
61
|
+
- All VS Code extensions pre-configured
|
|
62
|
+
|
|
63
|
+
### Manual Setup
|
|
64
|
+
|
|
65
|
+
If not using DevContainer:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Install dependencies
|
|
69
|
+
npm install
|
|
70
|
+
# or
|
|
71
|
+
bun install
|
|
72
|
+
|
|
73
|
+
# Run tests
|
|
74
|
+
npm test
|
|
75
|
+
# or
|
|
76
|
+
bun test
|
|
77
|
+
```
|
package/bunfig.toml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
[test]
|
|
2
|
+
preload = ["./test-setup.ts"]
|
|
3
|
+
coverage = true
|
|
4
|
+
coverageDir = "coverage"
|
|
5
|
+
coverageReporter = ["text", "lcov"]
|
|
6
|
+
coveragePathIgnorePatterns = ["**/test-setup.ts", "**/__mocks__/**"]
|
|
7
|
+
coverageSkipTestFiles = true
|
|
8
|
+
coverageThreshold = { global = { branches = 95, functions = 95, lines = 95, statements = 95 } }
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@opensyster/diagram-ui",
|
|
3
|
+
"version": "0.0.3-alpha",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/jade-codes/syster-diagram-ui"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test",
|
|
13
|
+
"typecheck": "tsc --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@opensyster/diagram-core": "^0.0.3-alpha",
|
|
17
|
+
"@xyflow/react": "^12.10.0",
|
|
18
|
+
"elkjs": "^0.9.3",
|
|
19
|
+
"react": "^18.2.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@testing-library/react": "^14.3.1",
|
|
23
|
+
"@types/bun": "^1.2.14",
|
|
24
|
+
"@types/react": "^18.2.0",
|
|
25
|
+
"happy-dom": "^20.1.0",
|
|
26
|
+
"react-dom": "^18.2.0",
|
|
27
|
+
"typescript": "^5.0.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@xyflow/react": "^12.0.0",
|
|
31
|
+
"react": "^18.2.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import { describe, test, expect, mock, afterEach } from 'bun:test';
|
|
2
|
+
import { render, cleanup } from '@testing-library/react';
|
|
3
|
+
import { EDGE_TYPES } from '@opensyster/diagram-core';
|
|
4
|
+
import { MarkerType, Position } from '@xyflow/react';
|
|
5
|
+
|
|
6
|
+
// Mock @xyflow/react components
|
|
7
|
+
mock.module('@xyflow/react', () => ({
|
|
8
|
+
BaseEdge: ({ id, path, style }: { id: string; path: string; style?: object }) => (
|
|
9
|
+
<path data-testid={`edge-${id}`} d={path} style={style} />
|
|
10
|
+
),
|
|
11
|
+
EdgeLabelRenderer: ({ children }: { children: React.ReactNode }) => (
|
|
12
|
+
<div data-testid="edge-label-renderer">{children}</div>
|
|
13
|
+
),
|
|
14
|
+
getSmoothStepPath: () => ['M0,0 L100,100', 50, 50],
|
|
15
|
+
MarkerType: { Arrow: 'arrow', ArrowClosed: 'arrowclosed' },
|
|
16
|
+
Position: { Top: 'top', Bottom: 'bottom', Left: 'left', Right: 'right' },
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
import { EDGE_CONFIGS, getEdgeConfig, createSysMLEdge, edgeTypes } from '../edges';
|
|
20
|
+
import type { EdgeConfig } from '../edges';
|
|
21
|
+
|
|
22
|
+
// Cleanup after each test to prevent DOM pollution
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
cleanup();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('EDGE_CONFIGS', () => {
|
|
28
|
+
test('has configuration for all EDGE_TYPES', () => {
|
|
29
|
+
const edgeTypeValues = Object.values(EDGE_TYPES);
|
|
30
|
+
|
|
31
|
+
for (const edgeType of edgeTypeValues) {
|
|
32
|
+
expect(EDGE_CONFIGS[edgeType]).toBeDefined();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('specialization edge has correct config', () => {
|
|
37
|
+
const config = EDGE_CONFIGS[EDGE_TYPES.SPECIALIZATION];
|
|
38
|
+
|
|
39
|
+
expect(config.strokeColor).toBe('#475569');
|
|
40
|
+
expect(config.strokeWidth).toBe(2);
|
|
41
|
+
expect(config.markerEnd).toBe(MarkerType.ArrowClosed);
|
|
42
|
+
expect(config.label).toBe('specializes');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('composition edge has correct config', () => {
|
|
46
|
+
const config = EDGE_CONFIGS[EDGE_TYPES.COMPOSITION];
|
|
47
|
+
|
|
48
|
+
expect(config.strokeColor).toBe('#2563eb');
|
|
49
|
+
expect(config.strokeWidth).toBe(2);
|
|
50
|
+
expect(config.markerEnd).toBe(MarkerType.ArrowClosed);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('typing edge is dashed', () => {
|
|
54
|
+
const config = EDGE_CONFIGS[EDGE_TYPES.TYPING];
|
|
55
|
+
|
|
56
|
+
expect(config.strokeDasharray).toBe('5 5');
|
|
57
|
+
expect(config.markerEnd).toBe(MarkerType.Arrow);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('satisfy edge has stereotype label', () => {
|
|
61
|
+
const config = EDGE_CONFIGS[EDGE_TYPES.SATISFY];
|
|
62
|
+
|
|
63
|
+
expect(config.label).toBe('«satisfy»');
|
|
64
|
+
expect(config.strokeColor).toBe('#d97706');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('perform edge has behavioral color', () => {
|
|
68
|
+
const config = EDGE_CONFIGS[EDGE_TYPES.PERFORM];
|
|
69
|
+
|
|
70
|
+
expect(config.strokeColor).toBe('#059669');
|
|
71
|
+
expect(config.label).toBe('«perform»');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('subsetting and cross-subsetting have different labels', () => {
|
|
75
|
+
const subsettingConfig = EDGE_CONFIGS[EDGE_TYPES.SUBSETTING];
|
|
76
|
+
const crossSubsettingConfig = EDGE_CONFIGS[EDGE_TYPES.CROSS_SUBSETTING];
|
|
77
|
+
|
|
78
|
+
expect(subsettingConfig.label).toBe('subsets');
|
|
79
|
+
expect(crossSubsettingConfig.label).toBe('cross-subsets');
|
|
80
|
+
expect(subsettingConfig.label).not.toBe(crossSubsettingConfig.label);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('getEdgeConfig', () => {
|
|
85
|
+
test('returns config for known edge type', () => {
|
|
86
|
+
const config = getEdgeConfig(EDGE_TYPES.SPECIALIZATION);
|
|
87
|
+
|
|
88
|
+
expect(config.strokeColor).toBe('#475569');
|
|
89
|
+
expect(config.label).toBe('specializes');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('returns default config for unknown edge type', () => {
|
|
93
|
+
const config = getEdgeConfig('unknown_edge_type');
|
|
94
|
+
|
|
95
|
+
expect(config.strokeColor).toBe('#475569');
|
|
96
|
+
expect(config.strokeWidth).toBe(2);
|
|
97
|
+
expect(config.markerEnd).toBe(MarkerType.ArrowClosed);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
describe('createSysMLEdge', () => {
|
|
102
|
+
test('creates edge component from config', () => {
|
|
103
|
+
const config: EdgeConfig = {
|
|
104
|
+
strokeColor: '#ff0000',
|
|
105
|
+
strokeWidth: 3,
|
|
106
|
+
markerEnd: MarkerType.Arrow,
|
|
107
|
+
label: 'test',
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const EdgeComponent = createSysMLEdge(config);
|
|
111
|
+
expect(EdgeComponent).toBeDefined();
|
|
112
|
+
expect(typeof EdgeComponent).toBe('function');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('created edge has correct displayName without edgeType', () => {
|
|
116
|
+
const config: EdgeConfig = {
|
|
117
|
+
strokeColor: '#ff0000',
|
|
118
|
+
strokeWidth: 3,
|
|
119
|
+
markerEnd: MarkerType.Arrow,
|
|
120
|
+
label: 'test',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const EdgeComponent = createSysMLEdge(config);
|
|
124
|
+
expect(EdgeComponent.displayName).toBe('SysMLEdge');
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('created edge has unique displayName with edgeType', () => {
|
|
128
|
+
const config: EdgeConfig = {
|
|
129
|
+
strokeColor: '#ff0000',
|
|
130
|
+
strokeWidth: 3,
|
|
131
|
+
markerEnd: MarkerType.Arrow,
|
|
132
|
+
label: 'test',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const EdgeComponent = createSysMLEdge(config, 'SPECIALIZATION');
|
|
136
|
+
expect(EdgeComponent.displayName).toBe('SpecializationEdge');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('created edge has unique displayName for multi-word edge types', () => {
|
|
140
|
+
const config: EdgeConfig = {
|
|
141
|
+
strokeColor: '#ff0000',
|
|
142
|
+
strokeWidth: 3,
|
|
143
|
+
markerEnd: MarkerType.Arrow,
|
|
144
|
+
label: 'test',
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const EdgeComponent = createSysMLEdge(config, 'CROSS_SUBSETTING');
|
|
148
|
+
expect(EdgeComponent.displayName).toBe('CrossSubsettingEdge');
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('created edge renders with correct styles', () => {
|
|
152
|
+
const config: EdgeConfig = {
|
|
153
|
+
strokeColor: '#ff0000',
|
|
154
|
+
strokeWidth: 3,
|
|
155
|
+
markerEnd: MarkerType.Arrow,
|
|
156
|
+
label: 'test-label',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const EdgeComponent = createSysMLEdge(config);
|
|
160
|
+
|
|
161
|
+
const { container, getByText } = render(
|
|
162
|
+
<EdgeComponent
|
|
163
|
+
id="test-edge"
|
|
164
|
+
source="node1"
|
|
165
|
+
target="node2"
|
|
166
|
+
sourceX={0}
|
|
167
|
+
sourceY={0}
|
|
168
|
+
targetX={100}
|
|
169
|
+
targetY={100}
|
|
170
|
+
sourcePosition={Position.Bottom}
|
|
171
|
+
targetPosition={Position.Top}
|
|
172
|
+
/>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Edge path should be rendered
|
|
176
|
+
const path = container.querySelector('[data-testid="edge-test-edge"]');
|
|
177
|
+
expect(path).not.toBeNull();
|
|
178
|
+
|
|
179
|
+
// Label should be rendered
|
|
180
|
+
expect(getByText('test-label')).not.toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test('created edge shows data label over config label', () => {
|
|
184
|
+
const config: EdgeConfig = {
|
|
185
|
+
strokeColor: '#ff0000',
|
|
186
|
+
strokeWidth: 3,
|
|
187
|
+
markerEnd: MarkerType.Arrow,
|
|
188
|
+
label: 'config-label',
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const EdgeComponent = createSysMLEdge(config);
|
|
192
|
+
|
|
193
|
+
const { getByText, queryByText } = render(
|
|
194
|
+
<EdgeComponent
|
|
195
|
+
id="test-edge"
|
|
196
|
+
source="node1"
|
|
197
|
+
target="node2"
|
|
198
|
+
sourceX={0}
|
|
199
|
+
sourceY={0}
|
|
200
|
+
targetX={100}
|
|
201
|
+
targetY={100}
|
|
202
|
+
sourcePosition={Position.Bottom}
|
|
203
|
+
targetPosition={Position.Top}
|
|
204
|
+
data={{ label: 'data-label' }}
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
// Data label should be used instead of config label
|
|
209
|
+
expect(getByText('data-label')).not.toBeNull();
|
|
210
|
+
expect(queryByText('config-label')).toBeNull();
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test('created edge shows multiplicity for composition', () => {
|
|
214
|
+
const config: EdgeConfig = {
|
|
215
|
+
strokeColor: '#2563eb',
|
|
216
|
+
strokeWidth: 2,
|
|
217
|
+
markerEnd: MarkerType.ArrowClosed,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const EdgeComponent = createSysMLEdge(config);
|
|
221
|
+
|
|
222
|
+
const { getByText } = render(
|
|
223
|
+
<EdgeComponent
|
|
224
|
+
id="test-edge"
|
|
225
|
+
source="node1"
|
|
226
|
+
target="node2"
|
|
227
|
+
sourceX={0}
|
|
228
|
+
sourceY={0}
|
|
229
|
+
targetX={100}
|
|
230
|
+
targetY={100}
|
|
231
|
+
sourcePosition={Position.Bottom}
|
|
232
|
+
targetPosition={Position.Top}
|
|
233
|
+
data={{ multiplicity: '4' }}
|
|
234
|
+
/>
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
expect(getByText('[4]')).not.toBeNull();
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('edgeTypes', () => {
|
|
242
|
+
test('has entry for each EDGE_TYPE', () => {
|
|
243
|
+
const edgeTypeValues = Object.values(EDGE_TYPES);
|
|
244
|
+
|
|
245
|
+
for (const edgeType of edgeTypeValues) {
|
|
246
|
+
expect(edgeTypes[edgeType]).toBeDefined();
|
|
247
|
+
expect(typeof edgeTypes[edgeType]).toBe('function');
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test('specialization edge renders correctly', () => {
|
|
252
|
+
const SpecializationEdge = edgeTypes[EDGE_TYPES.SPECIALIZATION];
|
|
253
|
+
|
|
254
|
+
const { container, getByText } = render(
|
|
255
|
+
<SpecializationEdge
|
|
256
|
+
id="spec-edge"
|
|
257
|
+
source="child"
|
|
258
|
+
target="parent"
|
|
259
|
+
sourceX={0}
|
|
260
|
+
sourceY={0}
|
|
261
|
+
targetX={100}
|
|
262
|
+
targetY={100}
|
|
263
|
+
sourcePosition={Position.Bottom}
|
|
264
|
+
targetPosition={Position.Top}
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const path = container.querySelector('[data-testid="edge-spec-edge"]');
|
|
269
|
+
expect(path).not.toBeNull();
|
|
270
|
+
expect(getByText('specializes')).not.toBeNull();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test('composition edge renders correctly', () => {
|
|
274
|
+
const CompositionEdge = edgeTypes[EDGE_TYPES.COMPOSITION];
|
|
275
|
+
|
|
276
|
+
const { container } = render(
|
|
277
|
+
<CompositionEdge
|
|
278
|
+
id="comp-edge"
|
|
279
|
+
source="container"
|
|
280
|
+
target="contained"
|
|
281
|
+
sourceX={0}
|
|
282
|
+
sourceY={0}
|
|
283
|
+
targetX={100}
|
|
284
|
+
targetY={100}
|
|
285
|
+
sourcePosition={Position.Bottom}
|
|
286
|
+
targetPosition={Position.Top}
|
|
287
|
+
data={{ multiplicity: '*' }}
|
|
288
|
+
/>
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const path = container.querySelector('[data-testid="edge-comp-edge"]');
|
|
292
|
+
expect(path).not.toBeNull();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
test('typing edge renders with dashed style', () => {
|
|
296
|
+
const TypingEdge = edgeTypes[EDGE_TYPES.TYPING];
|
|
297
|
+
|
|
298
|
+
const { container, getByText } = render(
|
|
299
|
+
<TypingEdge
|
|
300
|
+
id="typing-edge"
|
|
301
|
+
source="usage"
|
|
302
|
+
target="def"
|
|
303
|
+
sourceX={0}
|
|
304
|
+
sourceY={0}
|
|
305
|
+
targetX={100}
|
|
306
|
+
targetY={100}
|
|
307
|
+
sourcePosition={Position.Bottom}
|
|
308
|
+
targetPosition={Position.Top}
|
|
309
|
+
/>
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
const path = container.querySelector('[data-testid="edge-typing-edge"]');
|
|
313
|
+
expect(path).not.toBeNull();
|
|
314
|
+
expect(getByText(':')).not.toBeNull();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test('satisfy edge renders with stereotype label', () => {
|
|
318
|
+
const SatisfyEdge = edgeTypes[EDGE_TYPES.SATISFY];
|
|
319
|
+
|
|
320
|
+
const { getByText } = render(
|
|
321
|
+
<SatisfyEdge
|
|
322
|
+
id="satisfy-edge"
|
|
323
|
+
source="implementation"
|
|
324
|
+
target="requirement"
|
|
325
|
+
sourceX={0}
|
|
326
|
+
sourceY={0}
|
|
327
|
+
targetX={100}
|
|
328
|
+
targetY={100}
|
|
329
|
+
sourcePosition={Position.Bottom}
|
|
330
|
+
targetPosition={Position.Top}
|
|
331
|
+
/>
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
expect(getByText('«satisfy»')).not.toBeNull();
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test('selected edge has highlight color', () => {
|
|
338
|
+
const SpecEdge = edgeTypes[EDGE_TYPES.SPECIALIZATION];
|
|
339
|
+
|
|
340
|
+
const { container } = render(
|
|
341
|
+
<SpecEdge
|
|
342
|
+
id="selected-edge"
|
|
343
|
+
source="child"
|
|
344
|
+
target="parent"
|
|
345
|
+
sourceX={0}
|
|
346
|
+
sourceY={0}
|
|
347
|
+
targetX={100}
|
|
348
|
+
targetY={100}
|
|
349
|
+
sourcePosition={Position.Bottom}
|
|
350
|
+
targetPosition={Position.Top}
|
|
351
|
+
selected={true}
|
|
352
|
+
/>
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
const path = container.querySelector('[data-testid="edge-selected-edge"]');
|
|
356
|
+
expect(path).not.toBeNull();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test('edge without label or multiplicity renders path only', () => {
|
|
360
|
+
const config: EdgeConfig = {
|
|
361
|
+
strokeColor: '#000000',
|
|
362
|
+
strokeWidth: 1,
|
|
363
|
+
markerEnd: MarkerType.Arrow,
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const EdgeComponent = createSysMLEdge(config);
|
|
367
|
+
|
|
368
|
+
const { container, queryByTestId } = render(
|
|
369
|
+
<EdgeComponent
|
|
370
|
+
id="no-label-edge"
|
|
371
|
+
source="a"
|
|
372
|
+
target="b"
|
|
373
|
+
sourceX={0}
|
|
374
|
+
sourceY={0}
|
|
375
|
+
targetX={100}
|
|
376
|
+
targetY={100}
|
|
377
|
+
sourcePosition={Position.Bottom}
|
|
378
|
+
targetPosition={Position.Top}
|
|
379
|
+
/>
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const path = container.querySelector('[data-testid="edge-no-label-edge"]');
|
|
383
|
+
expect(path).not.toBeNull();
|
|
384
|
+
// No label renderer should be present
|
|
385
|
+
expect(queryByTestId('edge-label-renderer')).toBeNull();
|
|
386
|
+
});
|
|
387
|
+
});
|