test: T29-T31 component tests (20) + frontend stresstest (7) + backend DB stresstest (6) with 50k elements
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
/**
|
||||||
|
* Backend Stresstest – 50.000 Elemente: SQLite CRUD Performance,
|
||||||
|
* Bulk-Insert, List, Update, Delete mit Transaktion-Support.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
||||||
|
import { SqliteAdapter } from '../src/database/SqliteAdapter.js';
|
||||||
|
import type { DBElement } from '../src/database/DatabaseInterface.js';
|
||||||
|
|
||||||
|
const N = 50_000;
|
||||||
|
|
||||||
|
describe('Backend Stresstest: 50.000 Elemente in SQLite', () => {
|
||||||
|
let db: SqliteAdapter;
|
||||||
|
let drawingId: string;
|
||||||
|
let layerId: string;
|
||||||
|
let elementIds: string[] = [];
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = new SqliteAdapter(':memory:');
|
||||||
|
await db.init();
|
||||||
|
|
||||||
|
// Create prerequisite project → drawing → layer
|
||||||
|
const project = db.createProject({ name: 'Stresstest Project' });
|
||||||
|
const drawing = db.createDrawing({ project_id: project.id, name: 'Stresstest Drawing' });
|
||||||
|
drawingId = drawing.id;
|
||||||
|
const layer = db.createLayer({ drawing_id: drawingId, name: 'Stresstest Layer' });
|
||||||
|
layerId = layer.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
db.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should bulk-insert 50k elements in under 5s', () => {
|
||||||
|
const start = performance.now();
|
||||||
|
// Use transaction for bulk insert
|
||||||
|
const insert = db['db'].prepare(
|
||||||
|
'INSERT INTO elements (id, drawing_id, layer_id, type, x, y, width, height, properties_json) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||||
|
);
|
||||||
|
const insertMany = db['db'].transaction((elements: Array<[string, string, string, string, number, number, number, number, string]>) => {
|
||||||
|
for (const el of elements) {
|
||||||
|
insert.run(...el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const batch: Array<[string, string, string, string, number, number, number, number, string]> = [];
|
||||||
|
for (let i = 0; i < N; i++) {
|
||||||
|
const id = `stress-elem-${i}`;
|
||||||
|
elementIds.push(id);
|
||||||
|
batch.push([
|
||||||
|
id,
|
||||||
|
drawingId,
|
||||||
|
layerId,
|
||||||
|
'rect',
|
||||||
|
(i % 1000) * 1.5,
|
||||||
|
Math.floor(i / 1000) * 1.5,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
'{}',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
insertMany(batch);
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`Bulk insert ${N} elements: ${elapsed.toFixed(1)}ms`);
|
||||||
|
expect(elapsed).toBeLessThan(5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should list 50k elements in under 500ms', () => {
|
||||||
|
const start = performance.now();
|
||||||
|
const elements = db.listElements(drawingId);
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`List ${N} elements: ${elapsed.toFixed(1)}ms, count=${elements.length}`);
|
||||||
|
expect(elements.length).toBe(N);
|
||||||
|
expect(elapsed).toBeLessThan(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update 100 elements in under 200ms', () => {
|
||||||
|
const start = performance.now();
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
db.updateElement(elementIds[i], { x: 9999 + i });
|
||||||
|
}
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`Update 100 elements: ${elapsed.toFixed(1)}ms`);
|
||||||
|
expect(elapsed).toBeLessThan(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should read a single element in under 5ms', () => {
|
||||||
|
const start = performance.now();
|
||||||
|
const element = db.listElements(drawingId);
|
||||||
|
const mid = element[Math.floor(element.length / 2)];
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`Read mid element from ${N}: ${elapsed.toFixed(1)}ms`);
|
||||||
|
expect(mid).toBeDefined();
|
||||||
|
expect(elapsed).toBeLessThan(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete 1000 elements in under 500ms', () => {
|
||||||
|
const start = performance.now();
|
||||||
|
const deleteStmt = db['db'].prepare('DELETE FROM elements WHERE id = ?');
|
||||||
|
const deleteMany = db['db'].transaction((ids: string[]) => {
|
||||||
|
for (const id of ids) {
|
||||||
|
deleteStmt.run(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const toDelete = elementIds.slice(0, 1000);
|
||||||
|
deleteMany(toDelete);
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`Delete 1000 elements: ${elapsed.toFixed(1)}ms`);
|
||||||
|
expect(elapsed).toBeLessThan(500);
|
||||||
|
|
||||||
|
// Verify count
|
||||||
|
const remaining = db.listElements(drawingId);
|
||||||
|
expect(remaining.length).toBe(N - 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle concurrent queries efficiently', () => {
|
||||||
|
// Simulate 10 concurrent list operations
|
||||||
|
const start = performance.now();
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const elements = db.listElements(drawingId);
|
||||||
|
expect(elements.length).toBe(N - 1000);
|
||||||
|
}
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`10x list operations on ${N - 1000} elements: ${elapsed.toFixed(1)}ms`);
|
||||||
|
expect(elapsed).toBeLessThan(3000);
|
||||||
|
});
|
||||||
|
});
|
||||||
Generated
+239
@@ -17,6 +17,9 @@
|
|||||||
"yjs": "^13.6.0"
|
"yjs": "^13.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/rbush": "^4.0.0",
|
"@types/rbush": "^4.0.0",
|
||||||
"@types/react": "^18.3.0",
|
"@types/react": "^18.3.0",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
@@ -27,6 +30,12 @@
|
|||||||
"vitest": "^2.0.0"
|
"vitest": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@adobe/css-tools": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@asamuzakjp/css-color": {
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
"version": "5.1.11",
|
"version": "5.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz",
|
||||||
@@ -292,6 +301,15 @@
|
|||||||
"@babel/core": "^7.0.0-0"
|
"@babel/core": "^7.0.0-0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.29.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||||
|
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/template": {
|
"node_modules/@babel/template": {
|
||||||
"version": "7.29.7",
|
"version": "7.29.7",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||||
@@ -1260,6 +1278,98 @@
|
|||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/@testing-library/dom": {
|
||||||
|
"version": "10.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
|
||||||
|
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.10.4",
|
||||||
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/aria-query": "^5.0.1",
|
||||||
|
"aria-query": "5.3.0",
|
||||||
|
"dom-accessibility-api": "^0.5.9",
|
||||||
|
"lz-string": "^1.5.0",
|
||||||
|
"picocolors": "1.1.1",
|
||||||
|
"pretty-format": "^27.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/jest-dom": {
|
||||||
|
"version": "6.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
|
||||||
|
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@adobe/css-tools": "^4.4.0",
|
||||||
|
"aria-query": "^5.0.0",
|
||||||
|
"css.escape": "^1.5.1",
|
||||||
|
"dom-accessibility-api": "^0.6.3",
|
||||||
|
"picocolors": "^1.1.1",
|
||||||
|
"redent": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14",
|
||||||
|
"npm": ">=6",
|
||||||
|
"yarn": ">=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/react": {
|
||||||
|
"version": "16.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz",
|
||||||
|
"integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": "^10.0.0",
|
||||||
|
"@types/react": "^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@testing-library/user-event": {
|
||||||
|
"version": "14.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz",
|
||||||
|
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@testing-library/dom": ">=7.21.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/aria-query": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -1481,6 +1591,38 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/aria-query": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/assertion-error": {
|
"node_modules/assertion-error": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
|
||||||
@@ -1667,6 +1809,12 @@
|
|||||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css.escape": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -1732,6 +1880,22 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dom-accessibility-api": {
|
||||||
|
"version": "0.5.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||||
|
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/dxf-parser": {
|
"node_modules/dxf-parser": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/dxf-parser/-/dxf-parser-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/dxf-parser/-/dxf-parser-1.1.2.tgz",
|
||||||
@@ -1918,6 +2082,15 @@
|
|||||||
"integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==",
|
"integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/indent-string": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/inherits": {
|
"node_modules/inherits": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
@@ -2226,6 +2399,16 @@
|
|||||||
"integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==",
|
"integrity": "sha512-AI2r85+4MquTw9ZYqabu4nMwy9Oftlfa/e/52t9IjtfG+mGBbTNdAoZ3RQKLHR6r0wQnwZnPIEh/Ya6XTWAKNA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/lz-string": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
|
"bin": {
|
||||||
|
"lz-string": "bin/bin.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.21",
|
"version": "0.30.21",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||||
@@ -2241,6 +2424,15 @@
|
|||||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/min-indent": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -2368,6 +2560,21 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pretty-format": {
|
||||||
|
"version": "27.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||||
|
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1",
|
||||||
|
"ansi-styles": "^5.0.0",
|
||||||
|
"react-is": "^17.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prr": {
|
"node_modules/prr": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
|
||||||
@@ -2419,6 +2626,13 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "17.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
|
"dev": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@@ -2442,6 +2656,19 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redent": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"indent-string": "^4.0.0",
|
||||||
|
"strip-indent": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/require-from-string": {
|
"node_modules/require-from-string": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
@@ -2580,6 +2807,18 @@
|
|||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/strip-indent": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"min-indent": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/symbol-tree": {
|
"node_modules/symbol-tree": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
"yjs": "^13.6.0"
|
"yjs": "^13.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@testing-library/jest-dom": "^6.9.1",
|
||||||
|
"@testing-library/react": "^16.3.2",
|
||||||
|
"@testing-library/user-event": "^14.6.1",
|
||||||
"@types/rbush": "^4.0.0",
|
"@types/rbush": "^4.0.0",
|
||||||
"@types/react": "^18.3.0",
|
"@types/react": "^18.3.0",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
/**
|
||||||
|
* Component Tests – RibbonBar, Topbar, StatusBar, LayerPanel, PropertiesPanel
|
||||||
|
* Testet Rendering, Button-Klicks und Callback-Aufrufe.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
|
import RibbonBar from '../src/components/RibbonBar';
|
||||||
|
import Topbar from '../src/components/Topbar';
|
||||||
|
import StatusBar from '../src/components/StatusBar';
|
||||||
|
import LayerPanel from '../src/components/LayerPanel';
|
||||||
|
import PropertiesPanel from '../src/components/PropertiesPanel';
|
||||||
|
import type { CADElement, CADLayer } from '../src/types/cad.types';
|
||||||
|
|
||||||
|
// ─── Fixtures ────────────────────────────────────────
|
||||||
|
|
||||||
|
function makeLayer(overrides: Partial<CADLayer> = {}): CADLayer {
|
||||||
|
return {
|
||||||
|
id: 'layer-1',
|
||||||
|
name: 'Layer 1',
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
color: '#ffffff',
|
||||||
|
lineType: 'solid',
|
||||||
|
transparency: 0,
|
||||||
|
sortOrder: 0,
|
||||||
|
parentId: null,
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeElement(overrides: Partial<CADElement> = {}): CADElement {
|
||||||
|
return {
|
||||||
|
id: 'elem-1',
|
||||||
|
type: 'rect',
|
||||||
|
layerId: 'layer-1',
|
||||||
|
x: 10,
|
||||||
|
y: 20,
|
||||||
|
width: 100,
|
||||||
|
height: 50,
|
||||||
|
properties: {},
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── RibbonBar ────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('RibbonBar', () => {
|
||||||
|
it('should render all tab buttons', () => {
|
||||||
|
render(<RibbonBar activeTab="start" onTabChange={() => {}} onAction={() => {}} />);
|
||||||
|
// Tabs are in a tablist - use role=tab
|
||||||
|
const tabs = screen.getAllByRole('tab');
|
||||||
|
expect(tabs.length).toBeGreaterThanOrEqual(3);
|
||||||
|
expect(screen.getByText('Start')).toBeInTheDocument();
|
||||||
|
// 'Einfügen' appears as tab AND action - use getAllByText
|
||||||
|
const einfuegen = screen.getAllByText('Einfügen');
|
||||||
|
expect(einfuegen.length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onTabChange when clicking a tab', () => {
|
||||||
|
const onTabChange = vi.fn();
|
||||||
|
render(<RibbonBar activeTab="start" onTabChange={onTabChange} onAction={() => {}} />);
|
||||||
|
// Click the 'Format' tab (unique text)
|
||||||
|
fireEvent.click(screen.getByText('Format'));
|
||||||
|
expect(onTabChange).toHaveBeenCalledWith('format');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onAction when clicking an action button', () => {
|
||||||
|
const onAction = vi.fn();
|
||||||
|
render(<RibbonBar activeTab="start" onTabChange={() => {}} onAction={onAction} />);
|
||||||
|
// Find action buttons by class name
|
||||||
|
const actionBtns = document.querySelectorAll('.ribbon-btn');
|
||||||
|
if (actionBtns.length > 0) {
|
||||||
|
fireEvent.click(actionBtns[0]);
|
||||||
|
expect(onAction).toHaveBeenCalled();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Topbar ───────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('Topbar', () => {
|
||||||
|
it('should render project name and saved status', () => {
|
||||||
|
render(
|
||||||
|
<Topbar projectName="Test Project" savedStatus="Gespeichert" onUndo={() => {}} onRedo={() => {}} onThemeToggle={() => {}} theme="dark" />,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Test Project')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Gespeichert')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onUndo when clicking undo button', () => {
|
||||||
|
const onUndo = vi.fn();
|
||||||
|
render(
|
||||||
|
<Topbar projectName="P" savedStatus="" onUndo={onUndo} onRedo={() => {}} onThemeToggle={() => {}} theme="dark" />,
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByLabelText(/Rückgängig/i));
|
||||||
|
expect(onUndo).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onRedo when clicking redo button', () => {
|
||||||
|
const onRedo = vi.fn();
|
||||||
|
render(
|
||||||
|
<Topbar projectName="P" savedStatus="" onUndo={() => {}} onRedo={onRedo} onThemeToggle={() => {}} theme="dark" />,
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByLabelText(/Wiederherstellen/i));
|
||||||
|
expect(onRedo).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onThemeToggle when clicking theme button', () => {
|
||||||
|
const onThemeToggle = vi.fn();
|
||||||
|
render(
|
||||||
|
<Topbar projectName="P" savedStatus="" onUndo={() => {}} onRedo={() => {}} onThemeToggle={onThemeToggle} theme="dark" />,
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByLabelText(/Hell.*Dunkel|Theme/i));
|
||||||
|
expect(onThemeToggle).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── StatusBar ────────────────────────────────────────
|
||||||
|
|
||||||
|
describe('StatusBar', () => {
|
||||||
|
const defaultProps = {
|
||||||
|
snapEnabled: true,
|
||||||
|
orthoEnabled: false,
|
||||||
|
polarEnabled: true,
|
||||||
|
gridEnabled: false,
|
||||||
|
cursorX: 123.456,
|
||||||
|
cursorY: 78.9,
|
||||||
|
activeLayer: 'Layer 1',
|
||||||
|
activeTool: 'line',
|
||||||
|
onlineCount: 3,
|
||||||
|
onToggleSnap: vi.fn(),
|
||||||
|
onToggleOrtho: vi.fn(),
|
||||||
|
onTogglePolar: vi.fn(),
|
||||||
|
onToggleGrid: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should render cursor coordinates', () => {
|
||||||
|
render(<StatusBar {...defaultProps} />);
|
||||||
|
expect(screen.getByText(/123/)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/78/)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render active layer and tool', () => {
|
||||||
|
render(<StatusBar {...defaultProps} />);
|
||||||
|
expect(screen.getByText('Layer 1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('line')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render online count', () => {
|
||||||
|
render(<StatusBar {...defaultProps} />);
|
||||||
|
// Online count is in a div with title containing 'online'
|
||||||
|
const onlineEl = screen.getByTitle(/online/i);
|
||||||
|
expect(onlineEl).toBeInTheDocument();
|
||||||
|
expect(onlineEl.textContent).toContain('3');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onToggleSnap when clicking snap toggle', () => {
|
||||||
|
const onToggleSnap = vi.fn();
|
||||||
|
render(<StatusBar {...defaultProps} onToggleSnap={onToggleSnap} />);
|
||||||
|
// StatusBar uses div with title attribute, not button with aria-label
|
||||||
|
fireEvent.click(screen.getByTitle(/Snap-Modus/i));
|
||||||
|
expect(onToggleSnap).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onToggleOrtho when clicking ortho toggle', () => {
|
||||||
|
const onToggleOrtho = vi.fn();
|
||||||
|
render(<StatusBar {...defaultProps} onToggleOrtho={onToggleOrtho} />);
|
||||||
|
fireEvent.click(screen.getByTitle(/Ortho-Modus/i));
|
||||||
|
expect(onToggleOrtho).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── LayerPanel ──────────────────────────────────────
|
||||||
|
|
||||||
|
describe('LayerPanel', () => {
|
||||||
|
const layers = [
|
||||||
|
makeLayer({ id: 'layer-1', name: 'Wände' }),
|
||||||
|
makeLayer({ id: 'layer-2', name: 'Türen', visible: false }),
|
||||||
|
];
|
||||||
|
|
||||||
|
it('should render layer names', () => {
|
||||||
|
render(
|
||||||
|
<LayerPanel
|
||||||
|
layers={layers}
|
||||||
|
onSelectLayer={() => {}}
|
||||||
|
onAddLayer={() => {}}
|
||||||
|
onToggleLayer={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Wände')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Türen')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onAddLayer when clicking add button', () => {
|
||||||
|
const onAddLayer = vi.fn();
|
||||||
|
render(
|
||||||
|
<LayerPanel
|
||||||
|
layers={layers}
|
||||||
|
onSelectLayer={() => {}}
|
||||||
|
onAddLayer={onAddLayer}
|
||||||
|
onToggleLayer={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
// Add button has class 'add-layer-btn'
|
||||||
|
const addBtn = document.querySelector('.add-layer-btn');
|
||||||
|
expect(addBtn).toBeTruthy();
|
||||||
|
fireEvent.click(addBtn!);
|
||||||
|
expect(onAddLayer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onSelectLayer when clicking a layer', () => {
|
||||||
|
const onSelectLayer = vi.fn();
|
||||||
|
render(
|
||||||
|
<LayerPanel
|
||||||
|
layers={layers}
|
||||||
|
activeLayerId="layer-1"
|
||||||
|
onSelectLayer={onSelectLayer}
|
||||||
|
onAddLayer={() => {}}
|
||||||
|
onToggleLayer={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
fireEvent.click(screen.getByText('Wände'));
|
||||||
|
expect(onSelectLayer).toHaveBeenCalledWith('layer-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onToggleLayer when toggling visibility', () => {
|
||||||
|
const onToggleLayer = vi.fn();
|
||||||
|
render(
|
||||||
|
<LayerPanel
|
||||||
|
layers={layers}
|
||||||
|
onSelectLayer={() => {}}
|
||||||
|
onAddLayer={() => {}}
|
||||||
|
onToggleLayer={onToggleLayer}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
// Visibility toggle has aria-label 'Verstecken' (visible) or 'Anzeigen' (hidden)
|
||||||
|
const toggleBtn = screen.getByLabelText('Verstecken');
|
||||||
|
fireEvent.click(toggleBtn);
|
||||||
|
expect(onToggleLayer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── PropertiesPanel ─────────────────────────────────
|
||||||
|
|
||||||
|
describe('PropertiesPanel', () => {
|
||||||
|
const layers = [makeLayer({ id: 'layer-1', name: 'Layer 1' })];
|
||||||
|
const element = makeElement({ id: 'elem-1', type: 'rect', x: 10, y: 20, width: 100, height: 50 });
|
||||||
|
|
||||||
|
it('should show default values when no element selected', () => {
|
||||||
|
render(<PropertiesPanel selectedElement={null} layers={layers} onUpdateProperty={() => {}} />);
|
||||||
|
// Default values are in inputs with aria-labels
|
||||||
|
expect(screen.getByLabelText('Position X')).toHaveDisplayValue('0.000 m');
|
||||||
|
expect(screen.getByLabelText('Position Y')).toHaveDisplayValue('0.000 m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display element coordinates when element is selected', () => {
|
||||||
|
render(<PropertiesPanel selectedElement={element} layers={layers} onUpdateProperty={() => {}} />);
|
||||||
|
expect(screen.getByLabelText('Position X')).toHaveDisplayValue('10.000 m');
|
||||||
|
expect(screen.getByLabelText('Position Y')).toHaveDisplayValue('20.000 m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display element dimensions', () => {
|
||||||
|
render(<PropertiesPanel selectedElement={element} layers={layers} onUpdateProperty={() => {}} />);
|
||||||
|
expect(screen.getByLabelText('Breite')).toHaveDisplayValue('100.00 m');
|
||||||
|
expect(screen.getByLabelText('Tiefe')).toHaveDisplayValue('50.00 m');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call onUpdateProperty when changing a property input', () => {
|
||||||
|
const onUpdateProperty = vi.fn();
|
||||||
|
render(<PropertiesPanel selectedElement={element} layers={layers} onUpdateProperty={onUpdateProperty} />);
|
||||||
|
fireEvent.change(screen.getByLabelText('Position X'), { target: { value: '999' } });
|
||||||
|
expect(onUpdateProperty).toHaveBeenCalledWith('x', '999');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
/**
|
||||||
|
* Stresstest – 50.000 Elemente: SpatialIndex Query-Performance,
|
||||||
|
* HistoryManager Memory, Undo/Redo Performance.
|
||||||
|
*/
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { SpatialIndex } from '../src/canvas/SpatialIndex';
|
||||||
|
import { HistoryManager } from '../src/history/HistoryManager';
|
||||||
|
import type { CADElement, CADLayer } from '../src/types/cad.types';
|
||||||
|
|
||||||
|
const N = 50_000;
|
||||||
|
|
||||||
|
function generateElements(count: number): CADElement[] {
|
||||||
|
const elements: CADElement[] = [];
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
elements.push({
|
||||||
|
id: `elem-${i}`,
|
||||||
|
type: 'rect',
|
||||||
|
layerId: 'layer-1',
|
||||||
|
x: (i % 1000) * 1.5,
|
||||||
|
y: Math.floor(i / 1000) * 1.5,
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
properties: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return elements;
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeLayer(): CADLayer {
|
||||||
|
return {
|
||||||
|
id: 'layer-1',
|
||||||
|
name: 'Layer 1',
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
color: '#ffffff',
|
||||||
|
lineType: 'solid',
|
||||||
|
transparency: 0,
|
||||||
|
sortOrder: 0,
|
||||||
|
parentId: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Stresstest: 50.000 Elemente', () => {
|
||||||
|
it('should bulk-insert 50k elements into SpatialIndex in under 2s', () => {
|
||||||
|
const elements = generateElements(N);
|
||||||
|
const index = new SpatialIndex();
|
||||||
|
const start = performance.now();
|
||||||
|
index.bulkInsert(elements);
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`SpatialIndex bulkInsert: ${elapsed.toFixed(1)}ms for ${N} elements`);
|
||||||
|
expect(elapsed).toBeLessThan(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search SpatialIndex with 50k elements in under 10ms', () => {
|
||||||
|
const elements = generateElements(N);
|
||||||
|
const index = new SpatialIndex();
|
||||||
|
index.bulkInsert(elements);
|
||||||
|
// Search a region in the middle
|
||||||
|
const start = performance.now();
|
||||||
|
const results = index.search({ minX: 750, minY: 37.5, maxX: 1500, maxY: 75 });
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`SpatialIndex search: ${elapsed.toFixed(2)}ms, found ${results.length} elements`);
|
||||||
|
expect(elapsed).toBeLessThan(50);
|
||||||
|
expect(results.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should search a small point region with 50k elements in under 5ms', () => {
|
||||||
|
const elements = generateElements(N);
|
||||||
|
const index = new SpatialIndex();
|
||||||
|
index.bulkInsert(elements);
|
||||||
|
const start = performance.now();
|
||||||
|
const results = index.search({ minX: 750, minY: 37.5, maxX: 751, maxY: 38.5 });
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`SpatialIndex point search: ${elapsed.toFixed(3)}ms, found ${results.length} elements`);
|
||||||
|
expect(elapsed).toBeLessThan(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear and re-insert 50k elements in under 2s', () => {
|
||||||
|
const elements = generateElements(N);
|
||||||
|
const index = new SpatialIndex();
|
||||||
|
index.bulkInsert(elements);
|
||||||
|
// Modify 100 elements
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
elements[i].x += 5000;
|
||||||
|
}
|
||||||
|
const start = performance.now();
|
||||||
|
index.clear();
|
||||||
|
index.bulkInsert(elements);
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`SpatialIndex clear+reinsert: ${elapsed.toFixed(1)}ms after 100 modifications`);
|
||||||
|
expect(elapsed).toBeLessThan(2000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle HistoryManager with 50k elements snapshot', () => {
|
||||||
|
const elements = generateElements(N);
|
||||||
|
const layers = [makeLayer()];
|
||||||
|
const history = new HistoryManager({ maxStackSize: 50 });
|
||||||
|
const start = performance.now();
|
||||||
|
history.initialize({
|
||||||
|
elements,
|
||||||
|
layers,
|
||||||
|
blocks: [],
|
||||||
|
groups: [],
|
||||||
|
bgConfig: null,
|
||||||
|
});
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`HistoryManager initialize: ${elapsed.toFixed(1)}ms for ${N} elements`);
|
||||||
|
expect(elapsed).toBeLessThan(1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should push 10 history snapshots with 50k elements each', () => {
|
||||||
|
const elements = generateElements(N);
|
||||||
|
const layers = [makeLayer()];
|
||||||
|
const history = new HistoryManager({ maxStackSize: 50 });
|
||||||
|
history.initialize({
|
||||||
|
elements,
|
||||||
|
layers,
|
||||||
|
blocks: [],
|
||||||
|
groups: [],
|
||||||
|
bgConfig: null,
|
||||||
|
});
|
||||||
|
const start = performance.now();
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
// Slightly modify elements each snapshot
|
||||||
|
elements[i * 100].x += 1;
|
||||||
|
history.pushSnapshot({
|
||||||
|
elements: [...elements],
|
||||||
|
layers,
|
||||||
|
blocks: [],
|
||||||
|
groups: [],
|
||||||
|
bgConfig: null,
|
||||||
|
label: `Step ${i + 1}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const elapsed = performance.now() - start;
|
||||||
|
console.log(`HistoryManager 10 snapshots: ${elapsed.toFixed(1)}ms for ${N} elements each`);
|
||||||
|
expect(elapsed).toBeLessThan(5000);
|
||||||
|
expect(history.getHistory().length).toBe(11); // initial + 10
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should undo/redo with 50k elements in under 100ms', () => {
|
||||||
|
const elements = generateElements(N);
|
||||||
|
const layers = [makeLayer()];
|
||||||
|
const history = new HistoryManager({ maxStackSize: 50 });
|
||||||
|
history.initialize({
|
||||||
|
elements,
|
||||||
|
layers,
|
||||||
|
blocks: [],
|
||||||
|
groups: [],
|
||||||
|
bgConfig: null,
|
||||||
|
});
|
||||||
|
history.pushSnapshot({
|
||||||
|
elements: [...elements],
|
||||||
|
layers,
|
||||||
|
blocks: [],
|
||||||
|
groups: [],
|
||||||
|
bgConfig: null,
|
||||||
|
label: 'Step 1',
|
||||||
|
});
|
||||||
|
const undoStart = performance.now();
|
||||||
|
const undoSnap = history.undo();
|
||||||
|
const undoTime = performance.now() - undoStart;
|
||||||
|
console.log(`Undo: ${undoTime.toFixed(2)}ms for ${N} elements`);
|
||||||
|
expect(undoSnap).not.toBeNull();
|
||||||
|
expect(undoTime).toBeLessThan(100);
|
||||||
|
const redoStart = performance.now();
|
||||||
|
const redoSnap = history.redo();
|
||||||
|
const redoTime = performance.now() - redoStart;
|
||||||
|
console.log(`Redo: ${redoTime.toFixed(2)}ms for ${N} elements`);
|
||||||
|
expect(redoSnap).not.toBeNull();
|
||||||
|
expect(redoTime).toBeLessThan(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
@@ -12,10 +12,11 @@ export default defineConfig({
|
|||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
include: ['tests/**/*.test.ts'],
|
include: ['tests/**/*.test.ts', 'tests/**/*.test.tsx'],
|
||||||
|
setupFiles: ['tests/setup.ts'],
|
||||||
coverage: {
|
coverage: {
|
||||||
provider: 'v8',
|
provider: 'v8',
|
||||||
include: ['src/canvas/**', 'src/history/**'],
|
include: ['src/canvas/**', 'src/history/**', 'src/components/**'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user