Repository: maxGraph/maxGraph
Pull request #272 (2023-12-04 00:05:27)

fix(robustness): prevent errors when a plugin is not loaded
Add guards to prevent accessing properties and methods on undefined instances when a plugin is not loaded.
Also fix and improve the JSDoc of the `EdgeHandler` and `Guide` classes.


Patch:
diff --git a/packages/core/jest.config.cjs b/packages/core/jest.config.cjs
index 95a72e799..bf6709141 100644
--- a/packages/core/jest.config.cjs
+++ b/packages/core/jest.config.cjs
@@ -3,4 +3,5 @@ module.exports = {
   // preset: 'ts-jest',
   preset: 'ts-jest/presets/default-esm',
   testEnvironment: 'jsdom', // need to access to the browser objects
+  testMatch: ['**/__tests__/**/?(*.)+(spec|test).ts'],
 };
diff --git a/packages/core/src/editor/Editor.ts b/packages/core/src/editor/Editor.ts
index e2634b6cc..dd3c50402 100644
--- a/packages/core/src/editor/Editor.ts
+++ b/packages/core/src/editor/Editor.ts
@@ -1437,24 +1437,24 @@ export class Editor extends EventSource {
     // event if an insert function is defined
     this.installInsertHandler(graph);
 
-    // Redirects the function for creating the
-    // popupmenu items
+    // Redirects the function for creating the popupmenu items
     const popupMenuHandler = <PopupMenuHandler>graph.getPlugin('PopupMenuHandler');
+    if (popupMenuHandler) {
+      popupMenuHandler.factoryMethod = (menu: any, cell: Cell | null, evt: any): void => {
+        return this.createPopupMenu(menu, cell, evt);
+      };
+    }
 
-    popupMenuHandler.factoryMethod = (menu: any, cell: Cell | null, evt: any): void => {
-      return this.createPopupMenu(menu, cell, evt);
-    };
-
-    // Redirects the function for creating
-    // new connections in the diagram
+    // Redirects the function for creating new connections in the diagram
     const connectionHandler = <ConnectionHandler>graph.getPlugin('ConnectionHandler');
-
-    connectionHandler.factoryMethod = (
-      source: Cell | null,
-      target: Cell | null
-    ): Cell => {
-      return this.createEdge(source, target);
-    };
+    if (connectionHandler) {
+      connectionHandler.factoryMethod = (
+        source: Cell | null,
+        target: Cell | null
+      ): Cell => {
+        return this.createEdge(source, target);
+      };
+    }
 
     // Maintains swimlanes and installs autolayout
     this.createSwimlaneManager(graph);
@@ -2458,13 +2458,13 @@ export class Editor extends EventSource {
     );
 
     if (modename === 'select') {
-      panningHandler.useLeftButtonForPanning = false;
+      panningHandler && (panningHandler.useLeftButtonForPanning = false);
       this.graph.setConnectable(false);
     } else if (modename === 'connect') {
-      panningHandler.useLeftButtonForPanning = false;
+      panningHandler && (panningHandler.useLeftButtonForPanning = false);
       this.graph.setConnectable(true);
     } else if (modename === 'pan') {
-      panningHandler.useLeftButtonForPanning = true;
+      panningHandler && (panningHandler.useLeftButtonForPanning = true);
       this.graph.setConnectable(false);
     }
   }
diff --git a/packages/core/src/util/Constants.ts b/packages/core/src/util/Constants.ts
index 9d1815a35..5cbd19f9e 100644
--- a/packages/core/src/util/Constants.ts
+++ b/packages/core/src/util/Constants.ts
@@ -262,14 +262,14 @@ export const VERTEX_SELECTION_DASHED = true;
 export const EDGE_SELECTION_DASHED = true;
 
 /**
- * Defines the color to be used for the guidelines in mxGraphHandler.
- * Default is #FF0000.
+ * Defines the color to be used for the guidelines in `Guide`.
+ * @default #FF0000.
  */
 export const GUIDE_COLOR = '#FF0000';
 
 /**
- * Defines the strokewidth to be used for the guidelines in mxGraphHandler.
- * Default is 1.
+ * Defines the strokewidth to be used for the guidelines in `Guide`.
+ * @default 1.
  */
 export const GUIDE_STROKEWIDTH = 1;
 
diff --git a/packages/core/src/view/Graph.ts b/packages/core/src/view/Graph.ts
index eda30d651..5aad6fc40 100644
--- a/packages/core/src/view/Graph.ts
+++ b/packages/core/src/view/Graph.ts
@@ -68,21 +68,19 @@ export const defaultPlugins: GraphPluginConstructor[] = [
 ];
 
 /**
- * Extends {@link EventSource} to implement a graph component for
- * the browser. This is the main class of the package. To activate
- * panning and connections use {@link setPanning} and {@link setConnectable}.
- * For rubberband selection you must create a new instance of
- * {@link rubberband}. The following listeners are added to
- * {@link mouseListeners} by default:
+ * Extends {@link EventSource} to implement a graph component for the browser. This is the main class of the package.
+ *
+ * To activate panning and connections use {@link setPanning} and {@link setConnectable}.
+ * For rubberband selection you must create a new instance of {@link rubberband}.
+ *
+ * The following listeners are added to {@link mouseListeners} by default:
  *
  * - tooltipHandler: {@link TooltipHandler} that displays tooltips
  * - panningHandler: {@link PanningHandler} for panning and popup menus
  * - connectionHandler: {@link ConnectionHandler} for creating connections
- * - graphHandler: {@link SelectionHandler} for moving and cloning cells
+ * - selectionHandler: {@link SelectionHandler} for moving and cloning cells
  *
  * These listeners will be called in the above order if they are enabled.
- * @class graph
- * @extends {EventSource}
  */
 class Graph extends EventSource {
   container: HTMLElement;
@@ -723,7 +721,11 @@ class Graph extends EventSource {
           }
         }
       }
-    } else if (this.isAllowAutoPanning() && !panningHandler.isActive()) {
+    } else if (
+      this.isAllowAutoPanning() &&
+      panningHandler &&
+      !panningHandler.isActive()
+    ) {
       panningHandler.getPanningManager().panTo(x + this.getPanDx(), y + this.getPanDy());
     }
   }
diff --git a/packages/core/src/view/GraphView.ts b/packages/core/src/view/GraphView.ts
index 09ea1a5ce..a82c26e5a 100644
--- a/packages/core/src/view/GraphView.ts
+++ b/packages/core/src/view/GraphView.ts
@@ -2165,8 +2165,7 @@ export class GraphView extends EventSource {
     graph.addMouseListener({
       mouseDown: (sender: any, me: InternalMouseEvent) => {
         const popupMenuHandler = graph.getPlugin('PopupMenuHandler') as PopupMenuHandler;
-
-        if (popupMenuHandler) popupMenuHandler.hideMenu();
+        popupMenuHandler?.hideMenu();
       },
       mouseMove: () => {
         return;
diff --git a/packages/core/src/view/cell/CellRenderer.ts b/packages/core/src/view/cell/CellRenderer.ts
index 3fe355051..008938c42 100644
--- a/packages/core/src/view/cell/CellRenderer.ts
+++ b/packages/core/src/view/cell/CellRenderer.ts
@@ -1398,7 +1398,7 @@ class CellRenderer {
         const selectionCellsHandler = graph.getPlugin(
           'SelectionCellsHandler'
         ) as SelectionCellsHandler;
-        selectionCellsHandler.updateHandler(state);
+        selectionCellsHandler?.updateHandler(state);
       }
     } else if (
       !force &&
@@ -1412,7 +1412,7 @@ class CellRenderer {
       const selectionCellsHandler = graph.getPlugin(
         'SelectionCellsHandler'
       ) as SelectionCellsHandler;
-      selectionCellsHandler.updateHandler(state);
+      selectionCellsHandler?.updateHandler(state);
       force = true;
     }
 
diff --git a/packages/core/src/view/handler/CellEditorHandler.ts b/packages/core/src/view/handler/CellEditorHandler.ts
index 620340f9a..5d878fa7c 100644
--- a/packages/core/src/view/handler/CellEditorHandler.ts
+++ b/packages/core/src/view/handler/CellEditorHandler.ts
@@ -712,10 +712,7 @@ class CellEditorHandler implements GraphPlugin {
     }
 
     const tooltipHandler = this.graph.getPlugin('TooltipHandler') as TooltipHandler;
-
-    if (tooltipHandler) {
-      tooltipHandler.hideTooltip();
-    }
+    tooltipHandler?.hideTooltip();
 
     const state = this.graph.getView().getState(cell);
 
diff --git a/packages/core/src/view/handler/ConnectionHandler.ts b/packages/core/src/view/handler/ConnectionHandler.ts
index 5afc879a2..e852cfb43 100644
--- a/packages/core/src/view/handler/ConnectionHandler.ts
+++ b/packages/core/src/view/handler/ConnectionHandler.ts
@@ -184,7 +184,7 @@ type FactoryMethod = (
  * the port IDs, use <Transactions.getCell>.
  *
  * ```javascript
- * graph.getPlugin('ConnectionHandler').addListener(mxEvent.CONNECT, (sender, evt)=>
+ * graph.getPlugin('ConnectionHandler')?.addListener(mxEvent.CONNECT, (sender, evt) =>
  * {
  *   let edge = evt.getProperty('cell');
  *   let source = graph.getDataModel().getTerminal(edge, true);
diff --git a/packages/core/src/view/handler/EdgeHandler.ts b/packages/core/src/view/handler/EdgeHandler.ts
index 5cdbc43fe..1cc4dc1de 100644
--- a/packages/core/src/view/handler/EdgeHandler.ts
+++ b/packages/core/src/view/handler/EdgeHandler.ts
@@ -75,7 +75,7 @@ import { equalPoints } from '../../util/arrayUtils';
 /**
  * Graph event handler that reconnects edges and modifies control points and the edge
  * label location.
- * Uses {@link TerminalMarker} for finding and highlighting new source and target vertices.
+ * Uses {@link CellMarker} for finding and highlighting new source and target vertices.
  * This handler is automatically created in mxGraph.createHandler for each selected edge.
  * **To enable adding/removing control points, the following code can be used**
  * @example
@@ -98,7 +98,7 @@ class EdgeHandler {
   state: CellState;
 
   /**
-   * Holds the {@link TerminalMarker} which is used for highlighting terminals.
+   * Holds the {@link CellMarker} which is used for highlighting terminals.
    */
   marker: CellMarker;
 
@@ -311,13 +311,14 @@ class EdgeHandler {
       }
     }
 
-    const graphHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler;
+    const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler;
 
     // Creates bends for the non-routed absolute points
     // or bends that don't correspond to points
     if (
-      this.graph.getSelectionCount() < graphHandler.maxCells ||
-      graphHandler.maxCells <= 0
+      selectionHandler &&
+      (this.graph.getSelectionCount() < selectionHandler.maxCells ||
+        selectionHandler.maxCells <= 0)
     ) {
       this.bends = this.createBends();
 
@@ -496,21 +497,21 @@ class EdgeHandler {
   }
 
   /**
-   * Returns {@link Constants#EDGE_SELECTION_COLOR}.
+   * Returns {@link EDGE_SELECTION_COLOR}.
    */
   getSelectionColor() {
     return EDGE_SELECTION_COLOR;
   }
 
   /**
-   * Returns {@link Constants#EDGE_SELECTION_STROKEWIDTH}.
+   * Returns {@link EDGE_SELECTION_STROKEWIDTH}.
    */
   getSelectionStrokeWidth() {
     return EDGE_SELECTION_STROKEWIDTH;
   }
 
   /**
-   * Returns {@link Constants#EDGE_SELECTION_DASHED}.
+   * Returns {@link EDGE_SELECTION_DASHED}.
    */
   isSelectionDashed() {
     return EDGE_SELECTION_DASHED;
@@ -525,14 +526,14 @@ class EdgeHandler {
   }
 
   /**
-   * Creates and returns the {@link CellMarker} used in {@link arker}.
+   * Creates and returns the {@link CellMarker} used in {@link marker}.
    */
   getCellAt(x: number, y: number) {
     return !this.outlineConnect ? this.graph.getCellAt(x, y) : null;
   }
 
   /**
-   * Creates and returns the {@link CellMarker} used in {@link arker}.
+   * Creates and returns the {@link CellMarker} used in {@link marker}.
    */
   createMarker() {
     return new EdgeHandlerCellMarker(this.graph, this);
@@ -552,7 +553,7 @@ class EdgeHandler {
 
   /**
    * Creates and returns the bends used for modifying the edge. This is
-   * typically an array of {@link RectangleShapes}.
+   * typically an array of {@link RectangleShape}.
    */
   createBends() {
     const { cell } = this.state;
@@ -593,7 +594,7 @@ class EdgeHandler {
 
   /**
    * Creates and returns the bends used for modifying the edge. This is
-   * typically an array of {@link RectangleShapes}.
+   * typically an array of {@link RectangleShape}.
    */
   // createVirtualBends(): mxRectangleShape[];
   createVirtualBends() {
@@ -1439,7 +1440,7 @@ class EdgeHandler {
 
   /**
    * Handles the event to applying the previewed changes on the edge by
-   * using {@link oveLabel}, <connect> or <changePoints>.
+   * using {@link moveLabel}, <connect> or <changePoints>.
    */
   mouseUp(sender: EventSource, me: InternalMouseEvent) {
     // Workaround for wrong event source in Webkit
diff --git a/packages/core/src/view/handler/KeyHandler.ts b/packages/core/src/view/handler/KeyHandler.ts
index e14939355..46b943fd2 100644
--- a/packages/core/src/view/handler/KeyHandler.ts
+++ b/packages/core/src/view/handler/KeyHandler.ts
@@ -242,15 +242,14 @@ class KeyHandler {
   isGraphEvent(evt: KeyboardEvent) {
     const source = <Element>getSource(evt);
 
-    // Accepts events from the target object or
-    // in-place editing inside graph
-    const cellEditor = this.graph?.getPlugin(
+    // Accepts events from the target object or in-place editing inside graph
+    const cellEditorHandler = this.graph?.getPlugin(
       'CellEditorHandler'
-    ) as CellEditorHandler | null;
+    ) as CellEditorHandler;
     if (
       source === this.target ||
       source.parentNode === this.target ||
-      (cellEditor != null && cellEditor.isEventSource(evt))
+      (cellEditorHandler && cellEditorHandler.isEventSource(evt))
     ) {
       return true;
     }
diff --git a/packages/core/src/view/handler/PopupMenuHandler.ts b/packages/core/src/view/handler/PopupMenuHandler.ts
index 4cbc06524..4a62c5b32 100644
--- a/packages/core/src/view/handler/PopupMenuHandler.ts
+++ b/packages/core/src/view/handler/PopupMenuHandler.ts
@@ -99,11 +99,10 @@ class PopupMenuHandler extends MaxPopupMenu implements GraphPlugin {
    * Initializes the shapes required for this vertex handler.
    */
   init() {
-    // Hides the tooltip if the mouse is over
-    // the context menu
+    // Hides the tooltip if the mouse is over the context menu
     InternalEvent.addGestureListeners(this.div, (evt) => {
       const tooltipHandler = this.graph.getPlugin('TooltipHandler') as TooltipHandler;
-      tooltipHandler.hide();
+      tooltipHandler?.hide();
     });
   }
 
@@ -176,7 +175,7 @@ class PopupMenuHandler extends MaxPopupMenu implements GraphPlugin {
 
       // Hides the tooltip if there is one
       const tooltipHandler = this.graph.getPlugin('TooltipHandler') as TooltipHandler;
-      tooltipHandler.hide();
+      tooltipHandler?.hide();
 
       // Menu is shifted by 1 pixel so that the mouse up event
       // is routed via the underlying shape instead of the DIV
diff --git a/packages/core/src/view/handler/SelectionHandler.ts b/packages/core/src/view/handler/SelectionHandler.ts
index ef6e0d091..22d25a092 100644
--- a/packages/core/src/view/handler/SelectionHandler.ts
+++ b/packages/core/src/view/handler/SelectionHandler.ts
@@ -61,13 +61,9 @@ import type { ColorValue, GraphPlugin } from '../../types';
  * handlers are created using {@link Graph#createHandler} in
  * {@link GraphSelectionModel#cellAdded}.
  *
- * To avoid the container to scroll a moved cell into view, set
- * <scrollAfterMove> to false.
+ * To avoid the container to scroll a moved cell into view, set {@link scrollOnMove} to `false`.
  *
- * Constructor: mxGraphHandler
- *
- * Constructs an event handler that creates handles for the
- * selection cells.
+ * Constructs an event handler that creates handles for the selection cells.
  *
  * @param graph Reference to the enclosing {@link Graph}.
  */
@@ -132,7 +128,7 @@ class SelectionHandler implements GraphPlugin {
 
               // Forces update to ignore last visible state
               this.setHandlesVisibleForCells(
-                selectionCellsHandler.getHandledSelectionCells(),
+                selectionCellsHandler?.getHandledSelectionCells() ?? [],
                 false,
                 true
               );
@@ -269,8 +265,8 @@ class SelectionHandler implements GraphPlugin {
   connectOnDrop = false;
 
   /**
-   * Specifies if the view should be scrolled so that a moved cell is
-   * visible. Default is true.
+   * Specifies if the view should be scrolled so that a moved cell is visible.
+   * @default true
    */
   scrollOnMove = true;
 
@@ -479,11 +475,11 @@ class SelectionHandler implements GraphPlugin {
 
     if (!this.graph.isToggleEvent(me.getEvent()) || !isAltDown(me.getEvent())) {
       while (c) {
-        if (selectionCellsHandler.isHandled(c)) {
-          const cellEditor = this.graph.getPlugin(
+        if (selectionCellsHandler?.isHandled(c)) {
+          const cellEditorHandler = this.graph.getPlugin(
             'CellEditorHandler'
           ) as CellEditorHandler;
-          return cellEditor.getEditingCell() !== c;
+          return cellEditorHandler?.getEditingCell() !== c;
         }
         c = c.getParent();
       }
@@ -497,7 +493,7 @@ class SelectionHandler implements GraphPlugin {
   selectDelayed(me: InternalMouseEvent) {
     const popupMenuHandler = this.graph.getPlugin('PopupMenuHandler') as PopupMenuHandler;
 
-    if (!popupMenuHandler.isPopupTrigger(me)) {
+    if (!popupMenuHandler || !popupMenuHandler.isPopupTrigger(me)) {
       let cell = me.getCell();
       if (cell === null) {
         cell = this.cell;
@@ -1064,7 +1060,7 @@ class SelectionHandler implements GraphPlugin {
         ) as SelectionCellsHandler;
 
         this.setHandlesVisibleForCells(
-          selectionCellsHandler.getHandledSelectionCells(),
+          selectionCellsHandler?.getHandledSelectionCells() ?? [],
           false
         );
         this.updateLivePreview(this.currentDx, this.currentDy);
@@ -1265,11 +1261,8 @@ class SelectionHandler implements GraphPlugin {
     ) as SelectionCellsHandler;
 
     for (let i = 0; i < states.length; i += 1) {
-      const handler = selectionCellsHandler.getHandler(states[i][0].cell);
-
-      if (handler != null) {
-        handler.redraw(true);
-      }
+      const handler = selectionCellsHandler?.getHandler(states[i][0].cell);
+      handler?.redraw(true);
     }
   }
 
@@ -1383,11 +1376,9 @@ class SelectionHandler implements GraphPlugin {
       ) as SelectionCellsHandler;
 
       for (let i = 0; i < cells.length; i += 1) {
-        const handler = selectionCellsHandler.getHandler(cells[i]);
-
-        if (handler != null) {
+        const handler = selectionCellsHandler?.getHandler(cells[i]);
+        if (handler) {
           handler.setHandlesVisible(visible);
-
           if (visible) {
             handler.redraw();
           }
@@ -1438,7 +1429,7 @@ class SelectionHandler implements GraphPlugin {
             'ConnectionHandler'
           ) as ConnectionHandler;
 
-          connectionHandler.connect(this.cell, cell, me.getEvent());
+          connectionHandler?.connect(this.cell, cell, me.getEvent());
         } else {
           const clone =
             graph.isCloneEvent(me.getEvent()) &&
@@ -1493,7 +1484,7 @@ class SelectionHandler implements GraphPlugin {
       ) as SelectionCellsHandler;
 
       this.setHandlesVisibleForCells(
-        selectionCellsHandler.getHandledSelectionCells(),
+        selectionCellsHandler?.getHandledSelectionCells() ?? [],
         true
       );
     }
diff --git a/packages/core/src/view/handler/VertexHandler.ts b/packages/core/src/view/handler/VertexHandler.ts
index 84f7dd546..acf63e94f 100644
--- a/packages/core/src/view/handler/VertexHandler.ts
+++ b/packages/core/src/view/handler/VertexHandler.ts
@@ -233,12 +233,13 @@ class VertexHandler {
       this.selectionBorder.setCursor(CURSOR.MOVABLE_VERTEX);
     }
 
-    const graphHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler;
+    const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler;
 
     // Adds the sizer handles
     if (
-      graphHandler.maxCells <= 0 ||
-      this.graph.getSelectionCount() < graphHandler.maxCells
+      selectionHandler &&
+      (selectionHandler.maxCells <= 0 ||
+        this.graph.getSelectionCount() < selectionHandler.maxCells)
     ) {
       const resizable = this.graph.isCellResizable(this.state.cell);
       this.sizers = [];
@@ -338,14 +339,17 @@ class VertexHandler {
    * Returns true if the rotation handle should be showing.
    */
   isRotationHandleVisible() {
-    const graphHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler;
+    const selectionHandler = this.graph.getPlugin('SelectionHandler') as SelectionHandler;
+    const selectionHandlerCheck = selectionHandler
+      ? selectionHandler.maxCells <= 0 ||
+        this.graph.getSelectionCount() < selectionHandler.maxCells
+      : true;
 
     return (
       this.graph.isEnabled() &&
       this.rotationEnabled &&
       this.graph.isCellRotatable(this.state.cell) &&
-      (graphHandler.maxCells <= 0 ||
-        this.graph.getSelectionCount() < graphHandler.maxCells)
+      selectionHandlerCheck
     );
   }
 
@@ -724,8 +728,7 @@ class VertexHandler {
         ) as SelectionCellsHandler;
 
         for (let i = 0; i < edges.length; i += 1) {
-          const handler = selectionCellsHandler.getHandler(edges[i]);
-
+          const handler = selectionCellsHandler?.getHandler(edges[i]);
           if (handler) {
             this.edgeHandlers.push(handler as EdgeHandler);
           }
@@ -1274,7 +1277,7 @@ class VertexHandler {
   }
 
   /**
-   * Returns the `recursiveResize` status of the given state.   
+   * Returns the `recursiveResize` status of the given state.
    * @param state the given {@link CellState}. This implementation takes the value of this state.
    * @param me the mouse event.
    */
diff --git a/packages/core/src/view/mixins/ConnectionsMixin.ts b/packages/core/src/view/mixins/ConnectionsMixin.ts
index d396934ab..541b508a7 100644
--- a/packages/core/src/view/mixins/ConnectionsMixin.ts
+++ b/packages/core/src/view/mixins/ConnectionsMixin.ts
@@ -746,7 +746,7 @@ const ConnectionsMixin: PartialType = {
    */
   setConnectable(connectable) {
     const connectionHandler = this.getPlugin('ConnectionHandler') as ConnectionHandler;
-    connectionHandler.setEnabled(connectable);
+    connectionHandler?.setEnabled(connectable);
   },
 
   /**
@@ -754,7 +754,7 @@ const ConnectionsMixin: PartialType = {
    */
   isConnectable() {
     const connectionHandler = this.getPlugin('ConnectionHandler') as ConnectionHandler;
-    return connectionHandler.isEnabled();
+    return connectionHandler?.isEnabled() ?? false;
   },
 };
 
diff --git a/packages/core/src/view/mixins/EditingMixin.ts b/packages/core/src/view/mixins/EditingMixin.ts
index f4a132627..d5ce0661b 100644
--- a/packages/core/src/view/mixins/EditingMixin.ts
+++ b/packages/core/src/view/mixins/EditingMixin.ts
@@ -92,8 +92,8 @@ const EditingMixin: PartialType = {
   },
 
   /**
-   * Fires a {@link startEditing} event and invokes {@link CellEditorHandler.startEditing}
-   * on {@link editor}. After editing was started, a {@link editingStarted} event is
+   * Fires a {@link InternalEvent.START_EDITING} event and invokes {@link CellEditorHandler.startEditing}.
+   * After editing was started, a {@link InternalEvent.EDITING_STARTED} event is
    * fired.
    *
    * @param cell {@link mxCell} to start the in-place editor for.
@@ -112,8 +112,10 @@ const EditingMixin: PartialType = {
           new EventObject(InternalEvent.START_EDITING, { cell, event: evt })
         );
 
-        const cellEditor = this.getPlugin('CellEditorHandler') as CellEditorHandler;
-        cellEditor.startEditing(cell, evt);
+        const cellEditorHandler = this.getPlugin(
+          'CellEditorHandler'
+        ) as CellEditorHandler;
+        cellEditorHandler?.startEditing(cell, evt);
 
         this.fireEvent(
           new EventObject(InternalEvent.EDITING_STARTED, { cell, event: evt })
@@ -136,14 +138,14 @@ const EditingMixin: PartialType = {
   },
 
   /**
-   * Stops the current editing  and fires a {@link editingStopped} event.
+   * Stops the current editing  and fires a {@link InternalEvent.EDITING_STOPPED} event.
    *
    * @param cancel Boolean that specifies if the current editing value
    * should be stored.
    */
   stopEditing(cancel = false) {
-    const cellEditor = this.getPlugin('CellEditorHandler') as CellEditorHandler;
-    cellEditor.stopEditing(cancel);
+    const cellEditorHandler = this.getPlugin('CellEditorHandler') as CellEditorHandler;
+    cellEditorHandler?.stopEditing(cancel);
     this.fireEvent(new EventObject(InternalEvent.EDITING_STOPPED, { cancel }));
   },
 
@@ -218,11 +220,11 @@ const EditingMixin: PartialType = {
    * If no cell is specified then this returns true if any
    * cell is currently being edited.
    *
-   * @param cell {@link mxCell} that should be checked.
+   * @param cell {@link Cell} that should be checked.
    */
   isEditing(cell = null) {
-    const cellEditor = this.getPlugin('CellEditorHandler') as CellEditorHandler;
-    const editingCell = cellEditor.getEditingCell();
+    const cellEditorHandler = this.getPlugin('CellEditorHandler') as CellEditorHandler;
+    const editingCell = cellEditorHandler?.getEditingCell();
     return !cell ? !!editingCell : cell === editingCell;
   },
 
diff --git a/packages/core/src/view/mixins/EventsMixin.ts b/packages/core/src/view/mixins/EventsMixin.ts
index 2939203ff..b948a2a5f 100644
--- a/packages/core/src/view/mixins/EventsMixin.ts
+++ b/packages/core/src/view/mixins/EventsMixin.ts
@@ -589,7 +589,7 @@ const EventsMixin: PartialType = {
 
     if (mxe.isConsumed()) {
       // Resets the state of the panning handler
-      panningHandler.panningTrigger = false;
+      panningHandler && (panningHandler.panningTrigger = false);
     }
 
     // Handles the event if it has not been consumed
@@ -597,6 +597,7 @@ const EventsMixin: PartialType = {
       this.isEnabled() &&
       !isConsumed(evt) &&
       !mxe.isConsumed() &&
+      connectionHandler &&
       connectionHandler.isEnabled()
     ) {
       const cell = connectionHandler.marker.getCell(me);
@@ -1057,13 +1058,13 @@ const EventsMixin: PartialType = {
           Math.abs(this.initialTouchY - me.getGraphY()) < this.tolerance;
       }
 
-      const cellEditor = this.getPlugin('CellEditorHandler') as CellEditorHandler;
+      const cellEditorHandler = this.getPlugin('CellEditorHandler') as CellEditorHandler;
 
-      // Stops editing for all events other than from cellEditor
+      // Stops editing for all events other than from cellEditorHandler
       if (
         evtName === InternalEvent.MOUSE_DOWN &&
         this.isEditing() &&
-        !cellEditor.isEventSource(me.getEvent())
+        !cellEditorHandler?.isEventSource(me.getEvent())
       ) {
         this.stopEditing(!this.isInvokesStopCellEditing());
       }
diff --git a/packages/core/src/view/mixins/PanningMixin.ts b/packages/core/src/view/mixins/PanningMixin.ts
index c557daf9b..603e3958f 100644
--- a/packages/core/src/view/mixins/PanningMixin.ts
+++ b/packages/core/src/view/mixins/PanningMixin.ts
@@ -395,15 +395,13 @@ const PanningMixin: PartialType = {
    *****************************************************************************/
 
   /**
-   * Specifies if panning should be enabled. This implementation updates
-   * {@link PanningHandler.panningEnabled} in {@link panningHandler}.
+   * Specifies if panning should be enabled. This implementation updates {@link PanningHandler.panningEnabled}.
    *
    * @param enabled Boolean indicating if panning should be enabled.
    */
   setPanning(enabled) {
     const panningHandler = this.getPlugin('PanningHandler') as PanningHandler;
-
-    if (panningHandler) panningHandler.panningEnabled = enabled;
+    panningHandler && (panningHandler.panningEnabled = enabled);
   },
 };
 
diff --git a/packages/core/src/view/mixins/TooltipMixin.ts b/packages/core/src/view/mixins/TooltipMixin.ts
index 51632f094..c454be95b 100644
--- a/packages/core/src/view/mixins/TooltipMixin.ts
+++ b/packages/core/src/view/mixins/TooltipMixin.ts
@@ -87,7 +87,7 @@ const TooltipMixin: PartialType = {
         'SelectionCellsHandler'
       ) as SelectionCellsHandler;
 
-      const handler = selectionCellsHandler.getHandler(state.cell);
+      const handler = selectionCellsHandler?.getHandler(state.cell);
 
       // @ts-ignore Guarded against undefined error already.
       if (handler && typeof handler.getTooltipForNode === 'function') {
@@ -147,8 +147,7 @@ const TooltipMixin: PartialType = {
    */
   setTooltips(enabled: boolean) {
     const tooltipHandler = this.getPlugin('TooltipHandler') as TooltipHandler;
-
-    tooltipHandler.setEnabled(enabled);
+    tooltipHandler?.setEnabled(enabled);
   },
 };
 
diff --git a/packages/core/src/view/other/DragSource.ts b/packages/core/src/view/other/DragSource.ts
index 2a43d39a8..c57e15c2f 100644
--- a/packages/core/src/view/other/DragSource.ts
+++ b/packages/core/src/view/other/DragSource.ts
@@ -534,8 +534,8 @@ class DragSource {
 
     // Guide is only needed if preview element is used
     if (this.isGuidesEnabled() && this.previewElement) {
-      const graphHandler = graph.getPlugin('SelectionHandler') as SelectionHandler;
-      this.currentGuide = new Guide(graph, graphHandler.getGuideStates());
+      const selectionHandler = graph.getPlugin('SelectionHandler') as SelectionHandler;
+      this.currentGuide = new Guide(graph, selectionHandler?.getGuideStates());
     }
 
     if (this.highlightDropTargets) {
diff --git a/packages/core/src/view/other/Guide.ts b/packages/core/src/view/other/Guide.ts
index 97c39e8ed..b4cd66102 100644
--- a/packages/core/src/view/other/Guide.ts
+++ b/packages/core/src/view/other/Guide.ts
@@ -78,7 +78,7 @@ class Guide {
   tolerance = 2;
 
   /**
-   * Sets the {@link CellStates} that should be used for alignment.
+   * Sets the {@link CellState}s that should be used for alignment.
    */
   setStates(states: CellState[]) {
     this.states = states;
@@ -103,8 +103,8 @@ class Guide {
 
   /**
    * Returns the mxShape to be used for painting the respective guide. This
-   * implementation returns a new, dashed and crisp {@link Polyline} using
-   * {@link Constants#GUIDE_COLOR} and {@link Constants#GUIDE_STROKEWIDTH} as the format.
+   * implementation returns a new, dashed and crisp {@link PolylineShape} using
+   * {@link GUIDE_COLOR} and {@link GUIDE_STROKEWIDTH} as the format.
    *
    * @param horizontal Boolean that specifies which guide should be created.
    */


Test patch:
diff --git a/packages/core/__tests__/view/mixins/ConnectionsMixin.test.ts b/packages/core/__tests__/view/mixins/ConnectionsMixin.test.ts
new file mode 100644
index 000000000..933f83c03
--- /dev/null
+++ b/packages/core/__tests__/view/mixins/ConnectionsMixin.test.ts
@@ -0,0 +1,25 @@
+/*
+Copyright 2023-present The maxGraph project Contributors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { expect, test } from '@jest/globals';
+import { createGraphWithoutPlugins } from './utils';
+
+test('The "ConnectionHandler" plugin is not available', () => {
+  const graph = createGraphWithoutPlugins();
+  graph.setConnectable(true);
+  graph.isConnectable();
+  expect(graph.isConnectable()).toBe(false);
+});
diff --git a/packages/core/__tests__/view/mixins/EdgeMixin.test.ts b/packages/core/__tests__/view/mixins/EdgeMixin.test.ts
index 62cb018de..d4a64e949 100644
--- a/packages/core/__tests__/view/mixins/EdgeMixin.test.ts
+++ b/packages/core/__tests__/view/mixins/EdgeMixin.test.ts
@@ -15,16 +15,12 @@ limitations under the License.
 */
 
 import { describe, expect, test } from '@jest/globals';
-import { Cell, type CellStyle, Geometry, Graph } from '../../../src';
-
-function createGraph(): Graph {
-  // @ts-ignore - no need for a container, we don't check the view here
-  return new Graph(null);
-}
+import { createGraphWithoutContainer } from './utils';
+import { Cell, type CellStyle, Geometry } from '../../../src';
 
 describe('insertEdge', () => {
   test('with several parameters', () => {
-    const graph = createGraph();
+    const graph = createGraphWithoutContainer();
     const source = new Cell();
     const target = new Cell();
 
@@ -55,7 +51,7 @@ describe('insertEdge', () => {
   });
 
   test('with single parameter', () => {
-    const graph = createGraph();
+    const graph = createGraphWithoutContainer();
     const source = new Cell();
     const target = new Cell();
 
@@ -91,7 +87,7 @@ describe('insertEdge', () => {
   });
 
   test('with single parameter and non default parent', () => {
-    const graph = createGraph();
+    const graph = createGraphWithoutContainer();
 
     const parentCell = graph.insertVertex({
       value: 'non default',
diff --git a/packages/core/__tests__/view/mixins/PanningMixin.test.ts b/packages/core/__tests__/view/mixins/PanningMixin.test.ts
new file mode 100644
index 000000000..c8f6aa42a
--- /dev/null
+++ b/packages/core/__tests__/view/mixins/PanningMixin.test.ts
@@ -0,0 +1,23 @@
+/*
+Copyright 2023-present The maxGraph project Contributors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { test } from '@jest/globals';
+import { createGraphWithoutPlugins } from './utils';
+
+test('The "PanningHandler" plugin is not available', () => {
+  const graph = createGraphWithoutPlugins();
+  graph.setPanning(true);
+});
diff --git a/packages/core/__tests__/view/mixins/TooltipMixin.test.ts b/packages/core/__tests__/view/mixins/TooltipMixin.test.ts
new file mode 100644
index 000000000..dc0c2f12f
--- /dev/null
+++ b/packages/core/__tests__/view/mixins/TooltipMixin.test.ts
@@ -0,0 +1,29 @@
+/*
+Copyright 2023-present The maxGraph project Contributors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { test } from '@jest/globals';
+import { createGraphWithoutPlugins } from './utils';
+import { Cell, CellState } from '../../../src';
+
+test('The "TooltipHandler" plugin is not available', () => {
+  const graph = createGraphWithoutPlugins();
+  graph.setTooltips(true);
+});
+
+test('The "SelectionCellsHandler" plugin is not available', () => {
+  const graph = createGraphWithoutPlugins();
+  graph.getTooltip(new CellState(null, new Cell()), null!, 0, 0);
+});
diff --git a/packages/core/__tests__/view/mixins/VertexMixin.test.ts b/packages/core/__tests__/view/mixins/VertexMixin.test.ts
index 4499dc7d8..d657b7e95 100644
--- a/packages/core/__tests__/view/mixins/VertexMixin.test.ts
+++ b/packages/core/__tests__/view/mixins/VertexMixin.test.ts
@@ -15,16 +15,12 @@ limitations under the License.
 */
 
 import { describe, expect, test } from '@jest/globals';
-import { type CellStyle, Geometry, Graph } from '../../../src';
-
-function createGraph(): Graph {
-  // @ts-ignore - no need for a container, we don't check the view here
-  return new Graph(null);
-}
+import { createGraphWithoutContainer } from './utils';
+import { type CellStyle, Geometry } from '../../../src';
 
 describe('insertVertex', () => {
   test('with several parameters', () => {
-    const graph = createGraph();
+    const graph = createGraphWithoutContainer();
     const style: CellStyle = { rounded: true, shape: 'cloud' };
     const cell = graph.insertVertex(null, 'vertex_1', 'a value', 10, 20, 110, 120, style);
     expect(cell.getId()).toBe('vertex_1');
@@ -50,7 +46,7 @@ describe('insertVertex', () => {
   });
 
   test('with single parameter', () => {
-    const graph = createGraph();
+    const graph = createGraphWithoutContainer();
     const style: CellStyle = { align: 'right', fillColor: 'red' };
     const cell = graph.insertVertex({
       value: 'a value',
@@ -82,7 +78,7 @@ describe('insertVertex', () => {
   });
 
   test('with single parameter and non default parent', () => {
-    const graph = createGraph();
+    const graph = createGraphWithoutContainer();
 
     const parentCell = graph.insertVertex({
       value: 'non default',
diff --git a/packages/core/__tests__/view/mixins/utils.ts b/packages/core/__tests__/view/mixins/utils.ts
new file mode 100644
index 000000000..96546ae81
--- /dev/null
+++ b/packages/core/__tests__/view/mixins/utils.ts
@@ -0,0 +1,22 @@
+/*
+Copyright 2023-present The maxGraph project Contributors
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { Graph } from '../../../src';
+
+// no need for a container, we don't check the view here
+export const createGraphWithoutContainer = (): Graph => new Graph(null!);
+
+export const createGraphWithoutPlugins = (): Graph => new Graph(null!, null!, []);