fix(ui): keep paper host out of React-managed DOM; fix duplicate stroke-width in node SVGs
Paper.remove() during dispose deleted the React canvas div (StrictMode double mount left a blank canvas). Duplicate XML attributes made 5 symbol data URIs unparseable. Also: README, Playwright smoke test (e2e/smoke.mjs), ES2022 lib. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
2701b4e198
commit
cd937cf4ac
71
README.md
Normal file
71
README.md
Normal file
@ -0,0 +1,71 @@
|
||||
# Pipeline Diagram Editor
|
||||
|
||||
Desktop editor for pipeline / utility network diagrams (water, heating, gas,
|
||||
sewage, power, signal) built with **Tauri 2**, **React + TypeScript + Vite**
|
||||
and **JointJS core** (SVG diagramming, MPL-2.0).
|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **Node palette** (left): equipment grouped by category with SVG previews;
|
||||
drag onto the canvas or double-click to place. Search filter included.
|
||||
- **Ports & relations**: click a node to reveal its ports. Each port carries a
|
||||
relation (water / heat / gas / sewage / power / signal); edges can only be
|
||||
drawn between compatible ports — compatible targets highlight green while
|
||||
dragging. Direction rules forbid out→out / in→in connections.
|
||||
- **Grid routing**: edges are routed orthogonally along the rectangular grid
|
||||
(Manhattan router). A configuration option draws an **arc (jumpover) at
|
||||
line intersections**. Router, grid size, snapping — all configurable.
|
||||
- **Edge styles**: solid/dashed/dotted/dash-dot lines, width, color override,
|
||||
head & tail markers (arrow, circle, diamond, bar, none).
|
||||
- **Property panel** (right): grouped properties of the focused node/edge —
|
||||
General, Presentation (geometry, colors, label), Physics (length, diameter,
|
||||
flow rate, flow type, materials…), Simulation results (read-only). Property
|
||||
types: `string`, `int`, `float`, `enum`, `catalogueItem`, `catalogueItems`,
|
||||
`color`, `listOfValues` — all with validation.
|
||||
- **Catalogues**: pipe diameter series (DN), materials, media, insulation.
|
||||
- **Color schemes**: Classic, Dark, High contrast, Monochrome — recolor ports,
|
||||
edges, node symbols, canvas and grid.
|
||||
- **Editing UX**: multi-select (shift-click / shift-drag rubber band), group
|
||||
dragging, copy/paste/duplicate, delete with edge cleanup, undo/redo,
|
||||
arrow-key nudge, select-all, zoom to fit / reset, pan (drag empty space),
|
||||
wheel zoom, edge vertex editing.
|
||||
- **Mock flow simulation**: sources (supply) and consumers (demand) are
|
||||
balanced across the network; per-edge flow and direction are written into
|
||||
read-only properties, edges animate with moving dashes whose **speed and
|
||||
width are proportional to flow volume**, labels show m³/h. Insufficient
|
||||
supply scales deliveries; unreachable consumers are reported.
|
||||
- **Persistence**: save/load diagram JSON and export SVG via native dialogs
|
||||
(Tauri) with browser-download fallback in web dev mode.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install # frontend deps
|
||||
npm run dev # web dev server on :1420 (browser fallback mode)
|
||||
npm run tauri:dev # full desktop app (requires webkit2gtk etc.)
|
||||
npm test # vitest suite (unit + component + editor integration)
|
||||
npm run build # typecheck + production frontend bundle
|
||||
npm run tauri:build # desktop bundles (deb / AppImage)
|
||||
node e2e/smoke.mjs # Playwright smoke test against the dev server
|
||||
```
|
||||
|
||||
Linux system deps (Debian/Ubuntu): `libwebkit2gtk-4.1-dev librsvg2-dev
|
||||
libayatana-appindicator3-dev libxdo-dev libssl-dev build-essential`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/model pure domain model (properties, catalogues, node types, graph, validation)
|
||||
src/editor JointJS integration (shapes, paper controller, edge styles, color schemes)
|
||||
src/sim mock flow solver + animation
|
||||
src/state app store, history (undo/redo), persistence adapters
|
||||
src/ui React panels (palette, property panel, toolbar, settings, canvas)
|
||||
src-tauri Rust shell (dialog + fs plugins)
|
||||
tests vitest suites; docs/plan/plan.md — implementation plan/checklist
|
||||
```
|
||||
|
||||
The JointJS graph is the runtime source of truth; `EditorController`
|
||||
converts it to the pure `PipelineGraph` document model for the solver,
|
||||
save files and tests.
|
||||
172
e2e/smoke.mjs
Normal file
172
e2e/smoke.mjs
Normal file
@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Playwright smoke test for typical editor scenarios, run against the Vite
|
||||
* dev server: npm run dev → node e2e/smoke.mjs
|
||||
* (starts its own server if :1420 is free)
|
||||
*
|
||||
* Covers: app boot, palette quick-add, node selection + port display,
|
||||
* property editing, port-to-port edge drawing with validation, node drag,
|
||||
* simulation run with animated flow, undo, save config roundtrip absence
|
||||
* of console errors.
|
||||
*/
|
||||
|
||||
import { chromium } from 'playwright';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { setTimeout as sleep } from 'node:timers/promises';
|
||||
|
||||
const URL = 'http://localhost:1420';
|
||||
let devServer = null;
|
||||
|
||||
async function serverUp() {
|
||||
try {
|
||||
const res = await fetch(URL);
|
||||
return res.ok;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function fail(msg) {
|
||||
console.error(`✗ ${msg}`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
|
||||
function ok(msg) {
|
||||
console.log(`✓ ${msg}`);
|
||||
}
|
||||
|
||||
async function ensureServer() {
|
||||
if (await serverUp()) return;
|
||||
devServer = spawn('npm', ['run', 'dev'], { stdio: 'ignore', detached: true });
|
||||
for (let i = 0; i < 60; i += 1) {
|
||||
if (await serverUp()) return;
|
||||
await sleep(500);
|
||||
}
|
||||
throw new Error('dev server did not start');
|
||||
}
|
||||
|
||||
const consoleErrors = [];
|
||||
|
||||
async function main() {
|
||||
await ensureServer();
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage({ viewport: { width: 1500, height: 950 } });
|
||||
page.on('console', (m) => {
|
||||
if (m.type() === 'error') consoleErrors.push(m.text());
|
||||
});
|
||||
page.on('pageerror', (e) => consoleErrors.push(String(e)));
|
||||
|
||||
await page.goto(URL);
|
||||
await page.waitForSelector('[data-testid="palette"]');
|
||||
await page.waitForSelector('[data-testid="canvas"] svg');
|
||||
ok('app boots: palette + canvas rendered');
|
||||
|
||||
// --- add nodes via palette double-click -------------------------------
|
||||
await page.dblclick('[data-testid="palette-item-waterSource"]');
|
||||
await page.dblclick('[data-testid="palette-item-consumer"]');
|
||||
await sleep(300);
|
||||
const nodeCount = await page.locator('.joint-element').count();
|
||||
if (nodeCount === 2) ok('palette quick-add creates nodes');
|
||||
else fail(`expected 2 nodes, got ${nodeCount}`);
|
||||
|
||||
// --- drag the consumer to the right so the diagram is spread out ------
|
||||
const nodes = page.locator('.joint-element');
|
||||
const consumerBox = await nodes.nth(1).boundingBox();
|
||||
await page.mouse.move(consumerBox.x + consumerBox.width / 2, consumerBox.y + consumerBox.height / 2);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(consumerBox.x + 420, consumerBox.y + 60, { steps: 12 });
|
||||
await page.mouse.up();
|
||||
const movedBox = await nodes.nth(1).boundingBox();
|
||||
if (Math.abs(movedBox.x - consumerBox.x) > 300) ok('node dragging works');
|
||||
else fail(`node did not move (dx=${movedBox.x - consumerBox.x})`);
|
||||
|
||||
// --- select source: ports become visible ------------------------------
|
||||
const srcBox = await nodes.nth(0).boundingBox();
|
||||
await page.mouse.click(srcBox.x + srcBox.width / 2, srcBox.y + srcBox.height / 2);
|
||||
await sleep(150);
|
||||
const focused = await page.locator('.joint-element.pf-focused').count();
|
||||
if (focused === 1) ok('clicking a node focuses it (ports shown)');
|
||||
else fail('node focus class missing');
|
||||
|
||||
const panelText = await page.locator('[data-testid="property-panel"]').innerText();
|
||||
if (/water source/i.test(panelText) && /physics/i.test(panelText)) {
|
||||
ok('property panel shows grouped node properties');
|
||||
} else fail(`property panel content unexpected: ${panelText.slice(0, 200).replace(/\n/g, ' | ')}`);
|
||||
|
||||
// --- edit title property ----------------------------------------------
|
||||
await page.fill('[data-testid="prop-title"]', 'Main intake');
|
||||
await page.press('[data-testid="prop-title"]', 'Enter');
|
||||
await sleep(300);
|
||||
const labels = await page.evaluate(() =>
|
||||
[...document.querySelectorAll('.joint-element text')].map((t) => t.textContent),
|
||||
);
|
||||
if (labels.some((l) => l && l.includes('Main intake'))) ok('editing title updates canvas label');
|
||||
else fail(`canvas labels: ${JSON.stringify(labels)}`);
|
||||
|
||||
// --- draw an edge from source out-port to consumer waterIn port --------
|
||||
// Locate the actual port circles: source is focused so its ports are active.
|
||||
const portRects = await page.evaluate(() => {
|
||||
const rect = (el) => {
|
||||
const r = el.getBoundingClientRect();
|
||||
return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
|
||||
};
|
||||
const out = document.querySelector('.joint-element.pf-focused [port="out"]');
|
||||
const elements = [...document.querySelectorAll('.joint-element')];
|
||||
const consumer = elements.find((e) => e.querySelector('[port="waterIn"]'));
|
||||
const waterIn = consumer?.querySelector('[port="waterIn"]');
|
||||
return out && waterIn ? { out: rect(out), waterIn: rect(waterIn) } : null;
|
||||
});
|
||||
let linkCount = 0;
|
||||
if (!portRects) {
|
||||
fail('could not locate port magnets');
|
||||
} else {
|
||||
await page.mouse.move(portRects.out.x, portRects.out.y);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(portRects.waterIn.x, portRects.waterIn.y, { steps: 15 });
|
||||
await sleep(150);
|
||||
await page.mouse.up();
|
||||
await sleep(300);
|
||||
linkCount = await page.locator('.joint-link').count();
|
||||
if (linkCount >= 1) ok('edge drawn between compatible ports');
|
||||
else fail('no edge was created by port drag');
|
||||
}
|
||||
|
||||
// --- simulation ---------------------------------------------------------
|
||||
await page.click('[data-testid="simulate-button"]');
|
||||
await sleep(400);
|
||||
const summary = await page.locator('[data-testid="sim-summary"]').count();
|
||||
if (linkCount >= 1 && summary === 1) {
|
||||
const text = await page.locator('[data-testid="sim-summary"]').innerText();
|
||||
ok(`simulation runs: ${text.trim()}`);
|
||||
const dash = await page.evaluate(() => {
|
||||
const path = document.querySelector('.joint-link path[joint-selector="line"]');
|
||||
return path ? path.getAttribute('stroke-dasharray') : null;
|
||||
});
|
||||
if (dash) ok('flow animation applied to edges (dasharray set)');
|
||||
else fail('no dasharray on edge during simulation');
|
||||
} else if (linkCount === 0) {
|
||||
console.log('… skipping simulation assertions (no edge)');
|
||||
} else fail('simulation summary missing');
|
||||
await page.click('[data-testid="simulate-button"]'); // stop
|
||||
|
||||
// --- undo ---------------------------------------------------------------
|
||||
await page.keyboard.press('Control+z');
|
||||
await sleep(300);
|
||||
ok('undo executes without errors');
|
||||
|
||||
await page.screenshot({ path: 'e2e/smoke.png', fullPage: true });
|
||||
ok('screenshot saved to e2e/smoke.png');
|
||||
|
||||
const realErrors = consoleErrors.filter((e) => !e.includes('favicon'));
|
||||
if (realErrors.length === 0) ok('no console errors');
|
||||
else fail(`console errors:\n${realErrors.join('\n')}`);
|
||||
|
||||
await browser.close();
|
||||
if (devServer) process.kill(-devServer.pid);
|
||||
console.log(process.exitCode ? 'SMOKE TEST FAILED' : 'SMOKE TEST PASSED');
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
if (devServer) process.kill(-devServer.pid);
|
||||
process.exit(1);
|
||||
});
|
||||
BIN
e2e/smoke.png
Normal file
BIN
e2e/smoke.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
48
package-lock.json
generated
48
package-lock.json
generated
@ -23,6 +23,7 @@
|
||||
"@types/react-dom": "^18.3.6",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"playwright": "^1.61.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.1.3"
|
||||
@ -2635,6 +2636,53 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.61.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.61.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
|
||||
@ -30,6 +30,7 @@
|
||||
"@types/react-dom": "^18.3.6",
|
||||
"@vitejs/plugin-react": "^4.4.1",
|
||||
"jsdom": "^26.1.0",
|
||||
"playwright": "^1.61.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.1.3"
|
||||
|
||||
@ -85,6 +85,8 @@ const pipePhysics: PropertyDef[] = [
|
||||
|
||||
const S = 'stroke="currentColor" stroke-width="2.5" fill="none"';
|
||||
const SF = 'stroke="currentColor" stroke-width="2.5" fill="#fff"';
|
||||
/** Stroke with explicit width — S/SF must not be combined with another stroke-width (duplicate XML attributes break SVG data URIs). */
|
||||
const SW = (w: number) => `stroke="currentColor" stroke-width="${w}" fill="none"`;
|
||||
|
||||
function def(
|
||||
partial: Omit<NodeTypeDef, 'properties'> & { extraProps?: PropertyDef[]; titleDefault?: string },
|
||||
@ -134,7 +136,7 @@ export const NODE_TYPES: NodeTypeDef[] = [
|
||||
width: 70,
|
||||
height: 70,
|
||||
simRole: 'junction',
|
||||
svg: `<rect x="10" y="6" width="50" height="58" rx="8" ${SF}/><path d="M35 20 c-6 9-8 13-8 17a8 8 0 0 0 16 0c0-4-2-8-8-17Z" ${S} stroke-width="2"/>`,
|
||||
svg: `<rect x="10" y="6" width="50" height="58" rx="8" ${SF}/><path d="M35 20 c-6 9-8 13-8 17a8 8 0 0 0 16 0c0-4-2-8-8-17Z" ${SW(2)}/>`,
|
||||
ports: [
|
||||
{ id: 'gasIn', relation: 'gas', direction: 'in', x: 0, y: 0.75, label: 'Gas' },
|
||||
{ id: 'heatOut', relation: 'heat', direction: 'out', x: 1, y: 0.25, label: 'Flow' },
|
||||
@ -193,7 +195,7 @@ export const NODE_TYPES: NodeTypeDef[] = [
|
||||
width: 70,
|
||||
height: 60,
|
||||
simRole: 'junction',
|
||||
svg: `<rect x="6" y="10" width="58" height="40" rx="6" ${SF}/><path d="M6 30 h14 l8 -12 l12 24 l12 -24 l8 12 h4" ${S} stroke-width="2"/>`,
|
||||
svg: `<rect x="6" y="10" width="58" height="40" rx="6" ${SF}/><path d="M6 30 h14 l8 -12 l12 24 l12 -24 l8 12 h4" ${SW(2)}/>`,
|
||||
ports: [
|
||||
{ id: 'primaryIn', relation: 'heat', direction: 'in', x: 0, y: 0.25, label: 'Primary in' },
|
||||
{ id: 'primaryOut', relation: 'heat', direction: 'out', x: 0, y: 0.75, label: 'Primary out' },
|
||||
@ -251,7 +253,7 @@ export const NODE_TYPES: NodeTypeDef[] = [
|
||||
width: 60,
|
||||
height: 60,
|
||||
simRole: 'junction',
|
||||
svg: `<path d="M6 30 h48 M30 30 v24" ${S} stroke-width="5"/><circle cx="30" cy="30" r="5" fill="currentColor" stroke="none"/>`,
|
||||
svg: `<path d="M6 30 h48 M30 30 v24" ${SW(5)}/><circle cx="30" cy="30" r="5" fill="currentColor" stroke="none"/>`,
|
||||
ports: [
|
||||
{ id: 'a', relation: 'water', direction: 'any', x: 0, y: 0.5, label: 'A' },
|
||||
{ id: 'b', relation: 'water', direction: 'any', x: 1, y: 0.5, label: 'B' },
|
||||
@ -285,7 +287,7 @@ export const NODE_TYPES: NodeTypeDef[] = [
|
||||
width: 70,
|
||||
height: 80,
|
||||
simRole: 'junction',
|
||||
svg: `<path d="M10 14 a25 10 0 0 1 50 0 v50 a25 10 0 0 1 -50 0 Z" ${SF}/><ellipse cx="35" cy="14" rx="25" ry="10" ${S} stroke-width="2"/><path d="M14 46 h42" ${S} stroke-width="1.5" stroke-dasharray="4 3"/>`,
|
||||
svg: `<path d="M10 14 a25 10 0 0 1 50 0 v50 a25 10 0 0 1 -50 0 Z" ${SF}/><ellipse cx="35" cy="14" rx="25" ry="10" ${SW(2)}/><path d="M14 46 h42" ${SW(1.5)} stroke-dasharray="4 3"/>`,
|
||||
ports: [
|
||||
{ id: 'in', relation: 'water', direction: 'in', x: 0, y: 0.3, label: 'Inlet' },
|
||||
{ id: 'out', relation: 'water', direction: 'out', x: 1, y: 0.8, label: 'Outlet' },
|
||||
@ -326,7 +328,7 @@ export const NODE_TYPES: NodeTypeDef[] = [
|
||||
width: 70,
|
||||
height: 50,
|
||||
simRole: 'consumer',
|
||||
svg: `<rect x="6" y="8" width="58" height="34" rx="4" ${SF}/><path d="M16 8 v34 M26 8 v34 M36 8 v34 M46 8 v34 M56 8 v34" ${S} stroke-width="1.5"/>`,
|
||||
svg: `<rect x="6" y="8" width="58" height="34" rx="4" ${SF}/><path d="M16 8 v34 M26 8 v34 M36 8 v34 M46 8 v34 M56 8 v34" ${SW(1.5)}/>`,
|
||||
ports: [
|
||||
{ id: 'in', relation: 'heat', direction: 'in', x: 0, y: 0.5, label: 'Flow' },
|
||||
{ id: 'out', relation: 'heat', direction: 'out', x: 1, y: 0.5, label: 'Return' },
|
||||
|
||||
@ -19,7 +19,13 @@ export function Canvas({ onReady }: CanvasProps) {
|
||||
useEffect(() => {
|
||||
const host = hostRef.current;
|
||||
if (!host || controllerRef.current) return;
|
||||
const controller = new EditorController(host, getState().config, {
|
||||
// The paper gets its own child div: Paper.remove() (called in dispose)
|
||||
// removes its el from the DOM, and it must never be the React-managed one.
|
||||
const paperHost = document.createElement('div');
|
||||
paperHost.style.width = '100%';
|
||||
paperHost.style.height = '100%';
|
||||
host.appendChild(paperHost);
|
||||
const controller = new EditorController(paperHost, getState().config, {
|
||||
onSelectionChange: (selection) => setState({ selection }),
|
||||
onGraphChange: () => bumpGraphVersion(),
|
||||
onHistoryChange: (canUndo, canRedo) => setState({ canUndo, canRedo }),
|
||||
@ -29,6 +35,7 @@ export function Canvas({ onReady }: CanvasProps) {
|
||||
onReady(controller);
|
||||
return () => {
|
||||
controller.dispose();
|
||||
paperHost.remove();
|
||||
controllerRef.current = null;
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
||||
@ -95,12 +95,12 @@ describe('defaults and grouping', () => {
|
||||
|
||||
it('groups defs in canonical order', () => {
|
||||
const defs: PropertyDef[] = [
|
||||
def({ key: 'a', group: 'physics' }),
|
||||
def({ key: 'b', group: 'general' }),
|
||||
def({ key: 'c', group: 'presentation' }),
|
||||
def({ key: 'd', group: 'physics' }),
|
||||
def({ key: 'a', group: 'physics', type: 'string' }),
|
||||
def({ key: 'b', group: 'general', type: 'string' }),
|
||||
def({ key: 'c', group: 'presentation', type: 'string' }),
|
||||
def({ key: 'd', group: 'physics', type: 'string' }),
|
||||
];
|
||||
const groups = groupDefs(defs.map((d) => ({ ...d, type: 'string' as const })));
|
||||
const groups = groupDefs(defs);
|
||||
expect(groups.map((g) => g.group.id)).toEqual(['general', 'presentation', 'physics']);
|
||||
expect(groups[2].defs.map((d) => d.key)).toEqual(['a', 'd']);
|
||||
});
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2021", "DOM", "DOM.Iterable"],
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
1
tsconfig.tsbuildinfo
Normal file
1
tsconfig.tsbuildinfo
Normal file
@ -0,0 +1 @@
|
||||
{"root":["./src/main.tsx","./src/editor/colorSchemes.ts","./src/editor/edgeStyles.ts","./src/editor/paper.ts","./src/editor/shapes.ts","./src/model/catalog.ts","./src/model/graph.ts","./src/model/nodeTypes.ts","./src/model/properties.ts","./src/model/validation.ts","./src/sim/flow.ts","./src/state/history.ts","./src/state/persistence.ts","./src/state/store.ts","./src/ui/App.tsx","./src/ui/Canvas.tsx","./src/ui/Palette.tsx","./src/ui/PropertyPanel.tsx","./src/ui/SettingsDialog.tsx","./src/ui/Toolbar.tsx","./tests/catalog.test.ts","./tests/components.test.tsx","./tests/edgeStyles.test.ts","./tests/editor.test.ts","./tests/flow.test.ts","./tests/graph.test.ts","./tests/history.test.ts","./tests/nodeTypes.test.ts","./tests/properties.test.ts","./tests/setup.ts","./tests/validation.test.ts"],"version":"5.9.3"}
|
||||
Loading…
x
Reference in New Issue
Block a user