549 lines
25 KiB
JavaScript
549 lines
25 KiB
JavaScript
/*
|
|
* Description: Static and helper regression tests for MXPIC EDA frontend/backend integration contracts.
|
|
* Inside functions: N/A - assertion-based test/module script.
|
|
* Developer : Qin Yue @ 2026
|
|
* Organization : OptiHK Limited
|
|
*/
|
|
const assert = require('assert');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const root = path.resolve(__dirname, '..');
|
|
const canvasHtml = fs.readFileSync(path.join(root, 'frontend', 'canvas.html'), 'utf8');
|
|
const canvasHelpers = fs.readFileSync(path.join(root, 'frontend', 'canvas-helpers.js'), 'utf8');
|
|
|
|
assert(
|
|
canvasHtml.includes('Build GDS'),
|
|
'Project Tree header should include a Build GDS button'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Save YAML for all canvases') && canvasHtml.includes('handleSaveProjectLayouts'),
|
|
'Project Tree should include a save button that writes YAML for all canvases'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('/api/build-gds'),
|
|
'Build GDS button should call the backend build-gds API'
|
|
);
|
|
assert(
|
|
canvasHtml.includes(':layout'),
|
|
'Build Layout should open an SVG preview tab named like canvas_1:layout'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('svg_url'),
|
|
'Build Layout should use the backend svg_url response'
|
|
);
|
|
assert(
|
|
<<<<<<< HEAD
|
|
canvasHtml.includes('result.svg_ready && result.svg_url') &&
|
|
canvasHtml.includes('buildLayoutRequestRef') &&
|
|
canvasHtml.includes('buildLayoutBusyRef') &&
|
|
canvasHtml.includes("cache: 'no-store'"),
|
|
'Build Layout should wait for a ready, versioned SVG response and prevent stale duplicate preview updates'
|
|
);
|
|
assert(
|
|
=======
|
|
>>>>>>> jingwen_main
|
|
canvasHtml.includes('result.preview_error') &&
|
|
canvasHtml.includes('Preview skipped: '),
|
|
'Build Layout should log when the backend saves YAML but skips SVG preview because the router stack is unavailable'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('layoutPreview'),
|
|
'canvas pages should support a layoutPreview tab type'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('LayoutSvgPreview'),
|
|
'layout preview tabs should use the auto-scaling SVG viewer'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('layoutScale'),
|
|
'layout SVG preview should expose an editable scale value'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('objectFit: \'contain\''),
|
|
'100% layout preview scale should fit the full SVG within the screen'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('className="build-gds-btn"'),
|
|
'Build GDS should use a dedicated polished button class'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('buildGdsBusy'),
|
|
'Build GDS should expose an in-progress state to prevent duplicate requests'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Build GDS network error'),
|
|
'Build GDS fetch failures should produce a specific network diagnostic'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('className="build-layout-btn"'),
|
|
'Build Layout should use the polished primary action class'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Route Editor'),
|
|
'Selecting an edge should expose a route editor'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('selectedEdge'),
|
|
'canvas should track selected edges separately from selected nodes'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('technologyManifest'),
|
|
'canvas should load the selected technology manifest'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('standard_bend'),
|
|
'route editor should offer standard_bend as a routing type'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('findSameTypeRouteCrossing'),
|
|
'canvas should validate same-xsection route crossings'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('link-mode-tabs') &&
|
|
canvasHtml.includes('link-mode-summary') &&
|
|
canvasHtml.includes('link-mode-menu') &&
|
|
canvasHtml.includes('currentLinkXsection') &&
|
|
canvasHtml.includes('setCurrentLinkXsection') &&
|
|
canvasHtml.includes("['strip', 'rib_low', 'metal_1', 'metal_2']"),
|
|
'canvas should expose a collapsed route-type selector for new links'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('handleBasicConnection') &&
|
|
canvasHtml.includes('onConnect={handleBasicConnection}') &&
|
|
canvasHtml.includes('nodesConnectable={true}') &&
|
|
canvasHtml.includes('connectionMode="loose"') &&
|
|
canvasHtml.includes('const conflict = findSameTypeRouteCrossing(candidate, activePage.edges, nodeMap, technologyManifest);') &&
|
|
canvasHtml.includes('Connection rejected:') &&
|
|
canvasHtml.includes('data: { route }') &&
|
|
canvasHtml.includes('addEdge(candidate, p.edges)'),
|
|
'canvas should use React Flow native pin-to-pin connections and reject same-xsection crossings for new links'
|
|
);
|
|
assert(
|
|
!canvasHtml.includes('linkDraft') &&
|
|
!canvasHtml.includes('routingMode') &&
|
|
!canvasHtml.includes('toggleRoutingMode') &&
|
|
!canvasHtml.includes('handleLinkCanvasMouseDown') &&
|
|
!canvasHtml.includes('handleLinkCanvasMouseMove') &&
|
|
!canvasHtml.includes('finalizeLinkDraft') &&
|
|
!canvasHtml.includes('__link_draft_edge__') &&
|
|
!canvasHtml.includes('Routing mode: click anywhere to start a point route.'),
|
|
'current interactive point-link drawing mode should be removed from the canvas'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('<Handle') &&
|
|
canvasHtml.includes('type="source"') &&
|
|
canvasHtml.includes('type="target"') &&
|
|
!canvasHtml.includes('nodesConnectable={false}'),
|
|
'component and port handles should remain connectable for basic pin linking'
|
|
);
|
|
assert(
|
|
canvasHtml.includes("selectable: true") &&
|
|
canvasHtml.includes("type: 'parallelRoute'") &&
|
|
canvasHtml.includes('hasRoutePoints') &&
|
|
canvasHtml.includes("hasRoutePoints ? 'parallelRoute' : view.type") &&
|
|
canvasHtml.includes('data-route-edge-id') &&
|
|
canvasHtml.includes('handleRouteEdgeMouseDown') &&
|
|
canvasHtml.includes('selectEdgeById') &&
|
|
canvasHtml.includes('vectorEffect="non-scaling-stroke"') &&
|
|
canvasHtml.includes('onEdgeMouseDown={handleReactFlowEdgeMouseDown}'),
|
|
'saved manual routes should remain selectable through a zoom-independent custom SVG hit path and React Flow edge mouse handling'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('panOnDrag={false}'),
|
|
'left mouse drag should not pan the canvas'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('selectionOnDrag={true}') &&
|
|
canvasHtml.includes('selectionMode={FULL_SELECTION_MODE}') &&
|
|
canvasHtml.includes('SelectionMode'),
|
|
'left mouse drag should draw a full-coverage selection area instead of panning'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('const path = points.length >= 2') &&
|
|
canvasHtml.includes('points.slice(1).map(point => `L ${point.x},${point.y}`)') &&
|
|
canvasHtml.includes('position: { x: first.x - 6, y: first.y - 6 }') &&
|
|
canvasHtml.includes('position: { x: last.x - 6, y: last.y - 6 }'),
|
|
'free routes should render all saved point-to-point line segments and keep hidden endpoints aligned'
|
|
);
|
|
assert(
|
|
!canvasHtml.includes('buildOrthogonalPoints') &&
|
|
!canvasHtml.includes('buildManhattanRoutePoints') &&
|
|
!canvasHtml.includes('findNearestPort') &&
|
|
!canvasHtml.includes('linkPreviewSnapPort'),
|
|
'custom link drawing geometry helpers should be deleted from the current canvas code'
|
|
);
|
|
const routePointNodeBlock = canvasHtml.slice(
|
|
canvasHtml.indexOf('const RulerPointNode'),
|
|
canvasHtml.indexOf('const RulerMeasurementNode')
|
|
);
|
|
assert(
|
|
routePointNodeBlock.includes('<Handle') &&
|
|
routePointNodeBlock.includes('type="source"') &&
|
|
routePointNodeBlock.includes('type="target"') &&
|
|
routePointNodeBlock.includes('id="route"'),
|
|
'route point nodes should expose hidden source and target handles so route edges can attach and render'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('build-progress') && canvasHtml.includes('buildProgress'),
|
|
'Build Layout and Build GDS should show progress while running'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('port-name-label'),
|
|
'component nodes should render visible port name labels'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('multiSelectionKeyCode="Shift"'),
|
|
'ReactFlow should allow shift multi-select for links'
|
|
);
|
|
assert(
|
|
canvasHtml.includes("deleteKeyCode={['Backspace', 'Delete']}"),
|
|
'ReactFlow should allow selected links to be deleted with the Delete key'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('selectedEdges'),
|
|
'canvas should track multiple selected links for batch route editing'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('__mixed__'),
|
|
'multi-link route editor should show mixed values as --'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Flip X') && canvasHtml.includes('Mirror Y'),
|
|
'component inspector should expose flip/flop controls'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('onNodeMouseDown') && canvasHtml.includes('spaceRotateNodeIdRef'),
|
|
'holding a component and pressing Space should rotate it by 90 degrees'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('getSpaceRotationTarget') &&
|
|
canvasHtml.includes('selectedSpaceNode') &&
|
|
canvasHtml.includes('node.type !== \'rotatableNode\' && node.type !== \'portNode\' && node.type !== \'anchorNode\'') &&
|
|
canvasHtml.includes('node.type === \'portNode\' || node.data?.elementType === \'port\'') &&
|
|
canvasHtml.includes('angle: normalizeAngle(Number(node.data?.angle || 0) + 90)'),
|
|
'Space rotation should also rotate selected Port and Anchor elements'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('const anchorRotation = data.rotation || 0') &&
|
|
canvasHtml.includes('const anchorVisualRotation = -Number(anchorRotation || 0)') &&
|
|
canvasHtml.includes('transform: `rotate(${anchorVisualRotation}deg)`') &&
|
|
canvasHtml.includes('buildPortHandles(localAnchorHandlePorts, { rotation: 0, boxSize: elementSize') &&
|
|
canvasHtml.includes('anchorDirectionHandles') &&
|
|
canvasHtml.includes('rotation: -Number(anchorRotation || 0)') &&
|
|
canvasHtml.includes('anchorHandleVisualStyle(portHandle') &&
|
|
canvasHtml.includes('anchorPortVisualSide') &&
|
|
canvasHtml.includes('portHandle.name') &&
|
|
canvasHtml.includes('visualSide === \'left\' ? 0 : elementSize.width') &&
|
|
canvasHtml.includes('transform: \'translate(-50%, -50%)\'') &&
|
|
canvasHtml.includes("portHandle.style?.top || '50%'") &&
|
|
canvasHtml.includes('localLeft') &&
|
|
canvasHtml.includes('localTop') &&
|
|
canvasHtml.includes('handlePositionMap[anchorDirectionHandles.get(portHandle.name) || portHandle.position]') &&
|
|
canvasHtml.includes('getAnchorHandleRouteDirection') &&
|
|
canvasHtml.includes('rotation: -Number(node.data?.rotation || 0)') &&
|
|
canvasHtml.includes('directionToReactFlowPosition') &&
|
|
canvasHtml.includes('sourcePosition: directionToReactFlowPosition(sourceDirection)') &&
|
|
canvasHtml.includes('targetPosition: directionToReactFlowPosition(targetDirection)') &&
|
|
!canvasHtml.includes('type: \'parallelRoute\',\n data: {\n ...(edge.data || {}),\n parallelOffset: offset,\n sourceDirection,\n targetDirection') &&
|
|
!canvasHtml.includes('rotatedAnchorHandlePositions'),
|
|
'Anchor port circles should split into side columns and spread across the full anchor body while built-in rectangular links use rotated directions'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Port Number') &&
|
|
canvasHtml.includes('Pitch') &&
|
|
canvasHtml.includes('portNumber') &&
|
|
canvasHtml.includes('pitch') &&
|
|
canvasHtml.includes('DEFAULT_ELEMENT_PITCH') &&
|
|
canvasHtml.includes('buildElementBoxSize') &&
|
|
canvasHtml.includes('height: elementSize.height') &&
|
|
canvasHtml.includes('elementType: \'anchor\'') &&
|
|
canvasHtml.includes('pitch: DEFAULT_ELEMENT_PITCH') &&
|
|
canvasHtml.includes('ports: buildElementPorts'),
|
|
'Port and Anchor inspectors should expose port number and pitch, default to 10 um pitch, and grow in height'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('const componentIndexesByPrefixRef = useRef({});') &&
|
|
canvasHtml.includes('const usedIndexes = componentIndexesByPrefixRef.current[prefix] || new Set();') &&
|
|
canvasHtml.includes('while (usedIndexes.has(nextIndex)) nextIndex += 1;') &&
|
|
canvasHtml.includes('usedIndexes.add(nextIndex);') &&
|
|
canvasHtml.includes('const name = `${prefix}_${nextIndex}`;') &&
|
|
canvasHtml.includes('releaseComponentDisplayNames(selectedNodes);') &&
|
|
canvasHtml.includes('releaseComponentDisplayName(oldDisplayName);') &&
|
|
canvasHtml.includes('reserveComponentDisplayName(newComponentDisplayName);') &&
|
|
!canvasHtml.includes('componentCounterRef.current') &&
|
|
!canvasHtml.includes('componentCountersByPrefixRef') &&
|
|
canvasHtml.includes('COMPONENT_CATEGORY_PREFIX_ABBREVIATIONS') &&
|
|
canvasHtml.includes("directional_coupler: 'DC'") &&
|
|
canvasHtml.includes("multimode_interferometers: 'MMI'") &&
|
|
canvasHtml.includes("photodetectors: 'PD'") &&
|
|
canvasHtml.includes("waveguides: 'WG'") &&
|
|
canvasHtml.includes("transitions: 'TRX'") &&
|
|
canvasHtml.includes("Mach_Zender_modulators: 'MZM'") &&
|
|
canvasHtml.includes("bendings: 'BD'") &&
|
|
canvasHtml.includes("edge_couplers: 'EC'") &&
|
|
canvasHtml.includes("grating_couplers: 'GC'") &&
|
|
canvasHtml.includes("terminations: 'TERM'") &&
|
|
canvasHtml.includes('abbreviate: Boolean(parsedData.category)') &&
|
|
canvasHtml.includes('abbreviate: Boolean(copyCategory)'),
|
|
'new PDK component instances should use their component category abbreviation as the display-name prefix'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('normalizeAngle,') && canvasHtml.includes('normalizeAngle(Number(node.data?.rotation || 0) + 90)'),
|
|
'Space rotation should import normalizeAngle before using it'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('component-floating-label') && canvasHtml.includes('component-visual-body'),
|
|
'component labels should float outside the rotated body'
|
|
);
|
|
assert(
|
|
!canvasHtml.includes('const visualPortHandles = useMemo(') &&
|
|
canvasHtml.includes('buildPortHandles(data.ports, { rotation: data.rotation || 0, flip: Boolean(data.flip), flop: Boolean(data.flop), boxSize: componentSize })') &&
|
|
canvasHtml.includes('const portDirectionMap = useMemo(') &&
|
|
canvasHtml.includes('position: \'absolute\', inset: 0') &&
|
|
canvasHtml.includes('pointerEvents: \'none\'') &&
|
|
canvasHtml.includes('pointerEvents: \'all\'') &&
|
|
canvasHtml.includes('handlePositionMap[portDirectionMap.get(portHandle.name) || portHandle.position]'),
|
|
'component port circles should use transformed pin positions so rendered sides follow pin angles'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('canvasTextVisible') &&
|
|
canvasHtml.includes('toggleCanvasText') &&
|
|
canvasHtml.includes('Text On') &&
|
|
canvasHtml.includes('Text Off') &&
|
|
canvasHtml.includes('canvas-text-hidden') &&
|
|
canvasHtml.includes('.canvas-text-hidden .component-floating-label'),
|
|
'canvas toolbar should toggle instance name and PDK text above components'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('--floating-label-bg') && canvasHtml.includes('--port-label-bg') && canvasHtml.includes('--mini-button-bg'),
|
|
'theme variables should keep labels, port chips, and header buttons readable in light and dark modes'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('className="site-nav-actions"') &&
|
|
canvasHtml.includes('className="canvas-toolbar"') &&
|
|
canvasHtml.includes('grid-snap-label') &&
|
|
canvasHtml.includes('body.light-mode .canvas-toolbar'),
|
|
'dashboard/logout should move to a site-level top-right action group and the canvas toolbar should keep readable grid text in light mode'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('body.light-mode .component-floating-label') &&
|
|
canvasHtml.includes('body.light-mode .port-name-label') &&
|
|
canvasHtml.includes('body.light-mode .mini-btn'),
|
|
'light mode should override dark translucent label and button surfaces'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Canvas Size') && canvasHtml.includes('canvasSize') && canvasHtml.includes('DEFAULT_CANVAS_SIZE'),
|
|
'project tree should expose a canvas size control with a 5000 um default'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('CanvasBoundaryNode') && canvasHtml.includes('canvas-boundary-node'),
|
|
'canvas should render a bold boundary rectangle in flow coordinates'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('renderNodes') && canvasHtml.includes('nodeExtent={canvasNodeExtent}'),
|
|
'ReactFlow should render the boundary node and constrain draggable nodes to the canvas extent'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('minZoom={0.02}') && canvasHtml.includes('defaultViewport={{ x: 80, y: 80, zoom: 0.12 }}'),
|
|
'large 5000 um canvases should zoom out far enough to fit on one screen'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('onWheel={handleWheel}') &&
|
|
canvasHtml.includes('calculateLayoutBounds(activePage)') &&
|
|
canvasHtml.includes('layoutBounds') &&
|
|
canvasHtml.includes('stageWidth') &&
|
|
canvasHtml.includes('stageHeight'),
|
|
'layout preview should mouse-wheel zoom and size 100% from calculated box_size layout bounds'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('reactFlowInstance.fitBounds') &&
|
|
canvasHtml.includes('width: activeCanvasSize.width') &&
|
|
canvasHtml.includes('height: activeCanvasSize.height'),
|
|
'switching canvases should fit the full canvas boundary instead of resetting to 100% zoom'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('boxSize') && canvasHtml.includes('normalizeBoxSize'),
|
|
'component metadata box_size should drive rendered component box dimensions'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('chooseCategoryComponent') && canvasHtml.includes('[id, data.ports, data.componentName, data.boxSize]'),
|
|
'category drops and metadata refreshes should remeasure components with YAML box sizes'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Background color="#334155" gap={10} size={1}'),
|
|
'default grid spacing should be 10 um'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Ruler') &&
|
|
canvasHtml.includes('rulerMode') &&
|
|
canvasHtml.includes('onPaneClick={handleCanvasPaneClick}') &&
|
|
canvasHtml.includes('onNodeClick={handleCanvasNodeClick}') &&
|
|
canvasHtml.includes('onPaneMouseMove={handleCanvasMouseMove}'),
|
|
'canvas should expose a ruler mode controlled from the top toolbar, allow measuring on component bodies, and preview to the mouse'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('mouseCanvasPoint') &&
|
|
canvasHtml.includes('canvasOrigin') &&
|
|
canvasHtml.includes('originPickMode') &&
|
|
canvasHtml.includes('displayMousePoint') &&
|
|
canvasHtml.includes('toggleOriginPickMode') &&
|
|
canvasHtml.includes('onMouseMoveCapture={handleCanvasMouseMove}') &&
|
|
canvasHtml.includes('handleCanvasPaneClick') &&
|
|
canvasHtml.includes('handleCanvasNodeClick') &&
|
|
canvasHtml.includes('origin-select-btn') &&
|
|
canvasHtml.includes('Select canvas origin') &&
|
|
canvasHtml.includes('className="coordinate-readout"') &&
|
|
canvasHtml.includes('className="origin-crosshair"'),
|
|
'canvas should show live mouse coordinates and support one-click origin selection with a crosshair preview'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('createRulerMeasurement') &&
|
|
canvasHtml.includes('rulerPointNode') &&
|
|
canvasHtml.includes('rulerMeasurementNode') &&
|
|
canvasHtml.includes('rulerPreviewPoint') &&
|
|
canvasHtml.includes('strokeDasharray: rulerPreviewMeasurement ? undefined') &&
|
|
canvasHtml.includes('renderEdges'),
|
|
'ruler mode should render temporary point nodes, a live solid preview edge, and a measurement label'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Box Size') &&
|
|
canvasHtml.includes('selectedNodeBoxSize') &&
|
|
canvasHtml.includes('box-size-readout'),
|
|
'component inspector should show the selected component YAML box size'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('coordinate-grid'),
|
|
'selected component coordinates should be displayed horizontally in the right panel'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('selectedPositionNodes.length > 1') &&
|
|
canvasHtml.includes("const MIXED_VALUE = '--'") &&
|
|
canvasHtml.includes('getSharedNumericDisplay') &&
|
|
canvasHtml.includes('clearMixedInput') &&
|
|
canvasHtml.includes('event.currentTarget.value === MIXED_VALUE') &&
|
|
canvasHtml.includes('editingTransformField') &&
|
|
canvasHtml.includes('if (editingTransformField) return;') &&
|
|
canvasHtml.includes('commitTransformInput') &&
|
|
canvasHtml.includes('event.currentTarget.value') &&
|
|
canvasHtml.includes('updatePosition(selectedNode.id, \'x\', val)') &&
|
|
canvasHtml.includes('selectedPositionNodes.forEach(node => {') &&
|
|
canvasHtml.includes('position: { [axis]: val }') &&
|
|
canvasHtml.includes('selectedNodes={selectedNodes}'),
|
|
'multi-selected components should show mixed values as -- and set all selected X/Y values absolutely from the inspector'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('portInfo.description') || canvasHtml.includes('port.description'),
|
|
'port information should append optional human-readable port descriptions'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('BASIC_COMPONENTS') &&
|
|
canvasHelpers.includes('BASIC_COMPONENTS') &&
|
|
canvasHelpers.includes('waveguide') &&
|
|
canvasHelpers.includes('90 bend') &&
|
|
canvasHelpers.includes('180 bend') &&
|
|
canvasHelpers.includes('circle') &&
|
|
canvasHelpers.includes('taper'),
|
|
'component library should expose basic Nazca primitives'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('Cells: cellEntries') &&
|
|
canvasHtml.includes('Basic: basicEntries') &&
|
|
canvasHtml.includes('PDK: library || {}'),
|
|
'component library should keep top-level Cells, Basic, and PDK folders'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('isDirectLeafGrid ? (') &&
|
|
canvasHtml.includes('<div className="category-grid">') &&
|
|
canvasHtml.includes("isUserCell ? 'compact-tree-card' : ''"),
|
|
'Basic, Port, and Anchor entries should render as consistent 2D cards instead of compact list rows'
|
|
);
|
|
assert(
|
|
canvasHtml.includes("key === 'xsection'") &&
|
|
canvasHtml.includes('<select') &&
|
|
canvasHtml.includes('xsections.map(xsection =>') &&
|
|
canvasHtml.includes('updateBasicArgument(key, event.target.value)'),
|
|
'Basic component xsection should be selected from technology manifest xsections instead of free text'
|
|
);
|
|
assert(
|
|
canvasHtml.indexOf('const xsections = Object.keys') <
|
|
canvasHtml.indexOf('if (selectedRouteEdges.length > 0)'),
|
|
'Basic and route property panels should share the same xsection list from RightPanel scope'
|
|
);
|
|
assert(
|
|
canvasHtml.includes("['waveguide', 'taper', '90 bend'].includes(data.componentName)") &&
|
|
canvasHtml.includes('minHeight: visualSize.height') &&
|
|
canvasHtml.includes('isBasicCompactComponent ?'),
|
|
'waveguide, taper, and 90 bend nodes should override the default component min-height and padding on the canvas'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('font-size: 0.3rem;') &&
|
|
canvasHtml.includes('font-size: 0.4rem;') &&
|
|
canvasHtml.includes('font-size: 0.32rem;') &&
|
|
canvasHtml.includes("font: 600 0.5rem/1.35") &&
|
|
canvasHtml.includes('width: 6, height: 6') &&
|
|
canvasHtml.includes("border: '1px solid var(--accent)'") &&
|
|
canvasHtml.includes('width: 5,') &&
|
|
canvasHtml.includes('fontSize: 8'),
|
|
'canvas labels and port circles should render smaller than the previous sizing'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('const portDisplayName = data.portName || data.componentDisplayName || data.label || \'port\';') &&
|
|
canvasHtml.includes('const canvasAngle = -Number(angle || 0);') &&
|
|
canvasHtml.includes('const pinLabelFromPortName =') &&
|
|
canvasHtml.includes('buildPortHandles(localHandlePorts, { rotation: canvasAngle })') &&
|
|
canvasHtml.includes('buildPortHandles(localHandlePorts, { rotation: 0, boxSize: elementSize })') &&
|
|
canvasHtml.includes('style={{ ...baseHandleStyle, ...portHandle.style }}') &&
|
|
canvasHtml.includes('borderRadius: 7') &&
|
|
canvasHtml.includes('boxSizing: \'border-box\'') &&
|
|
canvasHtml.includes('width: elementSize.width, height: elementSize.height, position: \'relative\'') &&
|
|
canvasHtml.includes('className="component-floating-label"') &&
|
|
canvasHtml.includes('className="port-pin-label"') &&
|
|
canvasHtml.includes('pinLabelFromPortName(portHandle.name)') &&
|
|
canvasHtml.includes('pinLabelStyle(portHandle') &&
|
|
canvasHtml.includes('transform: `rotate(${canvasAngle}deg) scaleX(${data.flop ? -1 : 1}) scaleY(${data.flip ? -1 : 1})`') &&
|
|
canvasHtml.includes('{portDisplayName}'),
|
|
'standalone Port nodes should render pin labels at their circles while the port instance name floats outside the rotated body'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('const anchorDisplayName = data.componentDisplayName || data.label || \'anchor\';') &&
|
|
canvasHtml.includes('className="anchor-node-shell"') &&
|
|
canvasHtml.includes('className="anchor-visual-body"') &&
|
|
canvasHtml.includes("const localLeft = visualSide === 'left' ? 0 : elementSize.width") &&
|
|
canvasHtml.includes("transform: 'translate(-50%, -50%)'") &&
|
|
canvasHtml.includes('pinLabelFromPortName(portHandle.name)') &&
|
|
canvasHtml.includes('pinLabelStyle(portHandle') &&
|
|
canvasHtml.includes('className="port-pin-label"') &&
|
|
canvasHtml.includes('{anchorDisplayName}'),
|
|
'standalone Anchor nodes should use the same outside name label and per-pin labels as Port nodes'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('ParallelRouteEdge') &&
|
|
canvasHtml.includes('parallelOffset') &&
|
|
canvasHtml.includes("type: 'parallelRoute'") &&
|
|
canvasHtml.includes('edgeTypes={edgeTypes}'),
|
|
'overlapped links should render with separated parallel edge paths'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('data: { route, points: routePoints }') &&
|
|
canvasHtml.includes('normalizeRoutePoints(link.points'),
|
|
'manual link route points should be stored on edges and restored from saved YAML'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('createComponentSymbolMetrics') &&
|
|
!canvasHtml.includes('Math.min(128') &&
|
|
!canvasHtml.includes('Math.min(64'),
|
|
'component icon/symbol dimensions should scale with component box size without old hard caps'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('/api/library?project=') &&
|
|
canvasHtml.includes('/api/component/${encodeURIComponent(componentName)}?project=') &&
|
|
canvasHtml.includes('/api/component/${encodeURIComponent(compName)}?project='),
|
|
'canvas should pass project context when loading library and component metadata'
|
|
);
|
|
assert(
|
|
canvasHtml.includes('/api/component/${encodeURIComponent(componentData.name)}/image?project=') &&
|
|
canvasHtml.includes('return obj.__path__.split'),
|
|
'component images and saved component paths should use technology-scoped library metadata'
|
|
);
|