task(A4.14): make v2 tool path the default (verified e2e without flag)
This commit is contained in:
@@ -0,0 +1,104 @@
|
|||||||
|
/**
|
||||||
|
* Phase-A-Verifikation (ROADMAP Übergabepunkt):
|
||||||
|
* Beweist mit echtem Chromium bei gesetzttem VITE_TOOL_DISPATCHER=v2:
|
||||||
|
* 1. Ribbon-V2-Sektion rendert die registrierten V2-Werkzeuge
|
||||||
|
* 2. Werkzeugwechsel über den v2tool:-Kanal funktioniert
|
||||||
|
* 3. Zeichnen + Ctrl+Z/Y laufen crashfrei durch den V2-Pfad
|
||||||
|
*
|
||||||
|
* Läuft gegen die bereits gestarteten Dev-Server (reuseExistingServer).
|
||||||
|
*/
|
||||||
|
import { test, expect, chromium } from '@playwright/test';
|
||||||
|
|
||||||
|
const API_URL = process.env.API_URL || 'http://localhost:3001';
|
||||||
|
const BASE_URL = process.env.FRONTEND_URL || 'http://localhost:5173';
|
||||||
|
|
||||||
|
async function api(method: string, endpoint: string, body?: unknown, token?: string) {
|
||||||
|
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
|
||||||
|
if (token) headers.Authorization = `Bearer ${token}`;
|
||||||
|
const res = await fetch(`${API_URL}${endpoint}`, {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
});
|
||||||
|
return { status: res.status, body: await res.json() };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('Phase-A: V2-Pfad end-to-end (Ribbon-Sektion, Toolwechsel, Zeichnen+Undo crashfrei)', async () => {
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1600, height: 1000 } });
|
||||||
|
|
||||||
|
// ── Auth & Navigation (Muster aus diagnose-editor.test.ts) ──
|
||||||
|
const email = `pa-verify-${Date.now()}@example.com`;
|
||||||
|
const reg = await api('POST', '/api/auth/register', { email, password: 'Test1234!', name: 'PA' });
|
||||||
|
expect(reg.status).toBe(201);
|
||||||
|
const token = reg.body.session.token;
|
||||||
|
|
||||||
|
const proj = await api('POST', '/api/projects', { name: `PA-${Date.now()}`, description: 'x' }, token);
|
||||||
|
expect(proj.status).toBe(201);
|
||||||
|
|
||||||
|
await page.goto(`${BASE_URL}/`);
|
||||||
|
await page.evaluate((t) => localStorage.setItem('auth_token', t), token);
|
||||||
|
await page.reload();
|
||||||
|
|
||||||
|
await page.waitForSelector('.dashboard-project-card', { timeout: 15000 });
|
||||||
|
await page.locator('.dashboard-project-card').first().click();
|
||||||
|
await page.waitForSelector('canvas', { timeout: 15000 });
|
||||||
|
await page.waitForTimeout(1200); // Engines mounten lassen
|
||||||
|
|
||||||
|
// ── Fehlerkanäle sammeln — V2-Pfad darf KEINE erzeugen ──
|
||||||
|
const pageErrors: string[] = [];
|
||||||
|
const consoleErrors: string[] = [];
|
||||||
|
page.on('pageerror', (err) => pageErrors.push(err.message));
|
||||||
|
page.on('console', (msg) => {
|
||||||
|
if (msg.type() === 'error') consoleErrors.push(msg.text());
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── 1) Ribbon: Zeichenflächen-Tab öffnen und V2-Sektion prüfen ──
|
||||||
|
await page.locator('.ribbon-tab', { hasText: 'Zeichenfläche' }).first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
const v2Section = page.locator('text=V2:').first();
|
||||||
|
await expect(v2Section).toBeVisible();
|
||||||
|
|
||||||
|
// Registrierte V2-Buttons müssen vorhanden sein (alle Registry-Tools):
|
||||||
|
const v2ButtonGroup = page.locator('[data-v2tool]');
|
||||||
|
const buttonCount = await v2ButtonGroup.count();
|
||||||
|
expect(buttonCount).toBeGreaterThanOrEqual(22);
|
||||||
|
|
||||||
|
// ── 2) Toolwechsel: Linie über V2-Button aktivieren ──
|
||||||
|
await page.locator('[data-v2tool="line"]').first().click();
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// ── 3) Zeichnen auf dem Canvas (Zug in der Mitte) ──
|
||||||
|
const canvas = page.locator('canvas').first();
|
||||||
|
const box = await canvas.boundingBox();
|
||||||
|
expect(box).not.toBeNull();
|
||||||
|
const cx = box!.x + box!.width / 2;
|
||||||
|
const cy = box!.y + box!.height / 2;
|
||||||
|
|
||||||
|
await page.mouse.move(cx - 100, cy - 50);
|
||||||
|
await page.mouse.down();
|
||||||
|
await page.mouse.move(cx + 100, cy + 50, { steps: 8 });
|
||||||
|
await page.mouse.up();
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
|
||||||
|
// ── 4) Undo/Redo Shortcuts crashfrei ──
|
||||||
|
await page.keyboard.press('Control+z');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
await page.keyboard.press('Control+y');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
// Kein Seitenfehler durch den gesamten V2-Ablauf:
|
||||||
|
expect(pageErrors).toEqual([]);
|
||||||
|
// Konsolenfehler nur tolerieren, wenn sie nichts mit dem V2-Pfad zu tun haben:
|
||||||
|
const v2Related = consoleErrors.filter((e) => /dispatcher|v2tool|undo/i.test(e));
|
||||||
|
expect(v2Related).toEqual([]);
|
||||||
|
|
||||||
|
// ── 5) Sichtbelege ──
|
||||||
|
await page.screenshot({ path: 'playwright-tests/screenshots/phase-a-canvas.png', fullPage: false });
|
||||||
|
await page.keyboard.press('Control+z');
|
||||||
|
await page.waitForTimeout(250);
|
||||||
|
await page.screenshot({ path: 'playwright-tests/screenshots/phase-a-after-undo.png', fullPage: false });
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
});
|
||||||
@@ -19,6 +19,7 @@ import MobileDrawers from './components/MobileDrawers';
|
|||||||
import BackgroundImport from './components/BackgroundImport';
|
import BackgroundImport from './components/BackgroundImport';
|
||||||
import HistoryPanel from './components/HistoryPanel';
|
import HistoryPanel from './components/HistoryPanel';
|
||||||
import V2HistoryPanel from './components/V2HistoryPanel';
|
import V2HistoryPanel from './components/V2HistoryPanel';
|
||||||
|
import { V2_TOOLS_ENABLED } from './kernel/featureFlags';
|
||||||
import SettingsModal from './components/SettingsModal';
|
import SettingsModal from './components/SettingsModal';
|
||||||
import InlineTextEditor from './components/InlineTextEditor';
|
import InlineTextEditor from './components/InlineTextEditor';
|
||||||
import { BackgroundService, type BackgroundConfig } from './services/backgroundService';
|
import { BackgroundService, type BackgroundConfig } from './services/backgroundService';
|
||||||
@@ -1574,7 +1575,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
onTabChange={setActiveRibbonTab}
|
onTabChange={setActiveRibbonTab}
|
||||||
onAction={handleRibbonAction}
|
onAction={handleRibbonAction}
|
||||||
onIsV2ToolActive={(toolId: string) =>
|
onIsV2ToolActive={(toolId: string) =>
|
||||||
import.meta.env.VITE_TOOL_DISPATCHER === 'v2' &&
|
V2_TOOLS_ENABLED &&
|
||||||
activeTool === toolId &&
|
activeTool === toolId &&
|
||||||
!!pluginRegistry.getToolV2(toolId)
|
!!pluginRegistry.getToolV2(toolId)
|
||||||
}
|
}
|
||||||
@@ -1743,7 +1744,7 @@ const CADEditor: React.FC<CADEditorProps> = ({ projectId, token, onNavigateBack
|
|||||||
onApply={handleBgApply}
|
onApply={handleBgApply}
|
||||||
backgroundService={bgServiceRef.current}
|
backgroundService={bgServiceRef.current}
|
||||||
/>
|
/>
|
||||||
{historyPanelOpen && (import.meta.env.VITE_TOOL_DISPATCHER === 'v2' ? (
|
{historyPanelOpen && (V2_TOOLS_ENABLED ? (
|
||||||
<V2HistoryPanel
|
<V2HistoryPanel
|
||||||
onClose={() => setHistoryPanelOpen(false)}
|
onClose={() => setHistoryPanelOpen(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { logger } from '../utils/logger';
|
|||||||
import { CADDocument } from '../kernel/document/CADDocument';
|
import { CADDocument } from '../kernel/document/CADDocument';
|
||||||
import { YjsDocument } from '../crdt/YjsDocument';
|
import { YjsDocument } from '../crdt/YjsDocument';
|
||||||
import { setActiveDocument, getActiveDocument } from '../kernel/document/documentService';
|
import { setActiveDocument, getActiveDocument } from '../kernel/document/documentService';
|
||||||
|
import { V2_TOOLS_ENABLED } from '../kernel/featureFlags';
|
||||||
import { pluginRegistry } from '../plugins';
|
import { pluginRegistry } from '../plugins';
|
||||||
|
|
||||||
const CanvasArea: React.FC<CanvasAreaProps> = ({
|
const CanvasArea: React.FC<CanvasAreaProps> = ({
|
||||||
@@ -53,10 +54,9 @@ const CanvasArea: React.FC<CanvasAreaProps> = ({
|
|||||||
canvas, zoomPan, renderEngine, snapEngine, selectionEngine, spatialIndex, layerManager,
|
canvas, zoomPan, renderEngine, snapEngine, selectionEngine, spatialIndex, layerManager,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ── Task A2/A4: V2-Tool-Dispatcher hinter Feature-Flag ──
|
// ── Task A2/A4.14: V2-Pfad ist seit Verifikation STANDARD — Opt-out via
|
||||||
// Flag VITE_TOOL_DISPATCHER=v2: aktiviert den InteractionDispatcher UND
|
// VITE_TOOL_DISPATCHER=legacy. Siehe kernel/featureFlags.ts.
|
||||||
// verdrahtet ihn mit einem lokalen CADDocument + SelectionBridge (Task A4.1).
|
if (V2_TOOLS_ENABLED) {
|
||||||
if (import.meta.env.VITE_TOOL_DISPATCHER === 'v2') {
|
|
||||||
const ydoc = new YjsDocument();
|
const ydoc = new YjsDocument();
|
||||||
ydoc.data.layers.set('layer-0', {
|
ydoc.data.layers.set('layer-0', {
|
||||||
id: 'layer-0', name: 'Standard', visible: true, locked: false,
|
id: 'layer-0', name: 'Standard', visible: true, locked: false,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { RibbonBarProps, RibbonTab } from '../types/ui.types';
|
import type { RibbonBarProps, RibbonTab } from '../types/ui.types';
|
||||||
import { pluginRegistry } from '../plugins';
|
import { pluginRegistry } from '../plugins';
|
||||||
|
import { V2_TOOLS_ENABLED } from '../kernel/featureFlags';
|
||||||
|
|
||||||
const tabs: Array<{ id: RibbonTab; label: string; svg: React.ReactNode }> = [
|
const tabs: Array<{ id: RibbonTab; label: string; svg: React.ReactNode }> = [
|
||||||
{ id: 'start', label: 'Start', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg> },
|
{ id: 'start', label: 'Start', svg: <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg> },
|
||||||
@@ -38,8 +39,8 @@ const RibbonBar: React.FC<RibbonBarProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ribbon-content" id="ribbon-content">
|
<div className="ribbon-content" id="ribbon-content">
|
||||||
{/* ===== V2 TOOLS (Task A4.13, nur bei aktivem Dispatcher-Flag) ===== */}
|
{/* ===== V2 TOOLS (Task A4.13/A4.14: Standard aktiv — Opt-out via VITE_TOOL_DISPATCHER=legacy) ===== */}
|
||||||
{activeTab === 'canvas' && import.meta.env.VITE_TOOL_DISPATCHER === 'v2' && (
|
{activeTab === 'canvas' && V2_TOOLS_ENABLED && (
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center',
|
display: 'flex', flexWrap: 'wrap', gap: '6px', alignItems: 'center',
|
||||||
padding: '8px 4px', width: '100%',
|
padding: '8px 4px', width: '100%',
|
||||||
@@ -51,6 +52,7 @@ const RibbonBar: React.FC<RibbonBarProps> = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={tool.manifest.id}
|
key={tool.manifest.id}
|
||||||
|
data-v2tool={tool.manifest.id}
|
||||||
title={tool.manifest.description ?? tool.manifest.label}
|
title={tool.manifest.description ?? tool.manifest.label}
|
||||||
onClick={() => onAction(`v2tool:${tool.manifest.id}`)}
|
onClick={() => onAction(`v2tool:${tool.manifest.id}`)}
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/**
|
||||||
|
* featureFlags – Zentrale Feature-Schalter (Task A4.14).
|
||||||
|
*
|
||||||
|
* V2_TOOLS_ENABLED: Ab v2.0 ist der Plugin-first-Pfad (InteractionDispatcher,
|
||||||
|
* CADDocument, dynamisches Ribbon, V2HistoryPanel) STANDARDMÄSSIG AKTIV.
|
||||||
|
* Opt-out für den Legacy-Pfad: VITE_TOOL_DISPATCHER=legacy setzen.
|
||||||
|
*
|
||||||
|
* Begründung Historie: Vor A4.14 war V2 opt-in ('v2' explizit nötig), weil
|
||||||
|
* noch keine Browser-Verifikation existierte. Die Playwright-Verifikation
|
||||||
|
* (phase-a-verify.test.ts, grün) hat das Risiko entwertet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Ist der V2-Werkzeugpfad (Dispatcher/CADDocument) aktiv? Default: ja. */
|
||||||
|
export const V2_TOOLS_ENABLED: boolean =
|
||||||
|
import.meta.env.VITE_TOOL_DISPATCHER !== 'legacy';
|
||||||
Reference in New Issue
Block a user