1414
1515import { test , expect , Page , ConsoleMessage } from '@playwright/test' ;
1616
17+ // Emscripten injects `Module` into the page's global scope at runtime.
18+ // Declare it here so TypeScript doesn't report ts(2304) errors.
19+ // Overloads narrow the return type based on the `returnType` string literal.
20+ declare const Module : {
21+ ccall ( ident : string , returnType : 'number' , argTypes : string [ ] , args : ( number | string | boolean ) [ ] ) : number ;
22+ ccall ( ident : string , returnType : 'boolean' , argTypes : string [ ] , args : ( number | string | boolean ) [ ] ) : boolean ;
23+ ccall ( ident : string , returnType : 'string' , argTypes : string [ ] , args : ( number | string | boolean ) [ ] ) : string ;
24+ ccall ( ident : string , returnType : null , argTypes : string [ ] , args : ( number | string | boolean ) [ ] ) : void ;
25+ } ;
26+
27+
1728// ---------------------------------------------------------------------------
1829// Helpers
1930// ---------------------------------------------------------------------------
@@ -465,4 +476,139 @@ test.describe('Web MIDI Support', () => {
465476 timeout : 5000
466477 } ) ;
467478 } ) ;
479+ } ) ;
480+ test . describe ( 'Modular Graph Canvas Interactions' , ( ) => {
481+ async function waitForRuntime ( page : Page ) {
482+ page . on ( 'console' , msg => console . log ( 'BROWSER LOG:' , msg . text ( ) ) ) ;
483+ page . on ( 'pageerror' , err => console . error ( 'BROWSER ERROR:' , err . message ) ) ;
484+ await page . goto ( '/' ) ;
485+ await page . waitForSelector ( '#loading.hidden' , { timeout : 60_000 } ) ;
486+ const overlay = page . locator ( '#audio-unlock' ) ;
487+ if ( await overlay . isVisible ( ) ) await overlay . click ( ) ;
488+ await page . waitForTimeout ( 500 ) ;
489+ }
490+
491+ test ( 'canvas pan via right-click drag shifts scrolling' , async ( { page } ) => {
492+ await waitForRuntime ( page ) ;
493+
494+ const before = await page . evaluate ( ( ) => ( {
495+ x : Module . ccall ( 'get_canvas_scroll_x' , 'number' , [ ] , [ ] ) ,
496+ y : Module . ccall ( 'get_canvas_scroll_y' , 'number' , [ ] , [ ] ) ,
497+ } ) ) ;
498+
499+ const canvas = page . locator ( '#canvas' ) ;
500+ const box = await canvas . boundingBox ( ) ;
501+ if ( ! box ) throw new Error ( 'canvas not visible' ) ;
502+
503+ const cx = box . x + box . width / 2 ;
504+ const cy = box . y + box . height / 2 ;
505+ await page . mouse . click ( cx , cy , { button : 'right' } ) ;
506+ await page . mouse . move ( cx , cy ) ;
507+ await page . mouse . down ( { button : 'right' } ) ;
508+ await page . mouse . move ( cx + 80 , cy + 60 , { steps : 10 } ) ;
509+ await page . mouse . up ( { button : 'right' } ) ;
510+
511+ await page . waitForTimeout ( 200 ) ;
512+
513+ const after = await page . evaluate ( ( ) => ( {
514+ x : Module . ccall ( 'get_canvas_scroll_x' , 'number' , [ ] , [ ] ) ,
515+ y : Module . ccall ( 'get_canvas_scroll_y' , 'number' , [ ] , [ ] ) ,
516+ } ) ) ;
517+
518+ expect ( after . x ) . not . toBeCloseTo ( before . x , 0 ) ;
519+ expect ( after . y ) . not . toBeCloseTo ( before . y , 0 ) ;
520+ } ) ;
521+
522+ test ( 'two-finger touch gesture pans and zooms the canvas' , async ( { page } ) => {
523+ await waitForRuntime ( page ) ;
524+
525+ const before = await page . evaluate ( ( ) => ( {
526+ zoom : Module . ccall ( 'get_canvas_zoom' , 'number' , [ ] , [ ] ) ,
527+ sx : Module . ccall ( 'get_canvas_scroll_x' , 'number' , [ ] , [ ] ) ,
528+ } ) ) ;
529+
530+ await page . evaluate ( ( ) => {
531+ Module . ccall ( 'on_canvas_touch_gesture' , null , [ 'number' , 'number' , 'number' , 'number' , 'number' ] , [ 30 , 20 , 0.15 , 640 , 360 ] ) ;
532+ } ) ;
533+
534+ const after = await page . evaluate ( ( ) => ( {
535+ zoom : Module . ccall ( 'get_canvas_zoom' , 'number' , [ ] , [ ] ) ,
536+ sx : Module . ccall ( 'get_canvas_scroll_x' , 'number' , [ ] , [ ] ) ,
537+ } ) ) ;
538+
539+ expect ( after . zoom ) . toBeGreaterThan ( before . zoom ) ;
540+ expect ( after . sx ) . not . toBeCloseTo ( before . sx , 0 ) ;
541+ } ) ;
542+
543+ test ( 'adding a Splitter node increases the node count' , async ( { page } ) => {
544+ await waitForRuntime ( page ) ;
545+
546+ const countBefore : number = await page . evaluate ( ( ) =>
547+ Module . ccall ( 'get_node_count' , 'number' , [ ] , [ ] )
548+ ) ;
549+
550+ await page . evaluate ( ( ) =>
551+ Module . ccall ( 'trigger_add_splitter_node' , 'number' , [ ] , [ ] )
552+ ) ;
553+ await page . waitForTimeout ( 200 ) ;
554+
555+ const countAfter : number = await page . evaluate ( ( ) =>
556+ Module . ccall ( 'get_node_count' , 'number' , [ ] , [ ] )
557+ ) ;
558+
559+ expect ( countAfter ) . toBe ( countBefore + 1 ) ;
560+
561+ const hasSplitter : boolean = await page . evaluate ( ( ) =>
562+ Module . ccall ( 'has_node_of_type' , 'boolean' , [ 'number' ] , [ 1 ] )
563+ ) ;
564+ expect ( hasSplitter ) . toBe ( true ) ;
565+ } ) ;
566+
567+ test ( 'drawing a cable between two nodes increases link count' , async ( { page } ) => {
568+ await waitForRuntime ( page ) ;
569+
570+ const linksBefore : number = await page . evaluate ( ( ) =>
571+ Module . ccall ( 'get_link_count' , 'number' , [ ] , [ ] )
572+ ) ;
573+
574+ await page . evaluate ( ( ) => {
575+ Module . ccall ( 'trigger_add_splitter_node' , 'number' , [ ] , [ ] ) ;
576+ } ) ;
577+ await page . waitForTimeout ( 100 ) ;
578+
579+ const result : number = await page . evaluate ( ( ) => {
580+ const srcPin = Module . ccall ( 'get_node_output_pin_by_index' , 'number' , [ 'number' , 'number' ] , [ 2 , 0 ] ) ;
581+ const dstPin = Module . ccall ( 'get_node_input_pin_by_index' , 'number' , [ 'number' , 'number' ] , [ 3 , 0 ] ) ;
582+ return Module . ccall ( 'trigger_add_link' , 'number' , [ 'number' , 'number' ] , [ srcPin , dstPin ] ) ;
583+ } ) ;
584+
585+ const linksAfter : number = await page . evaluate ( ( ) =>
586+ Module . ccall ( 'get_link_count' , 'number' , [ ] , [ ] )
587+ ) ;
588+
589+ expect ( linksAfter ) . toBeGreaterThan ( linksBefore ) ;
590+ } ) ;
591+
592+ test ( 'deleting a node decreases the node count' , async ( { page } ) => {
593+ await waitForRuntime ( page ) ;
594+
595+ await page . evaluate ( ( ) =>
596+ Module . ccall ( 'trigger_add_splitter_node' , 'number' , [ ] , [ ] )
597+ ) ;
598+ await page . waitForTimeout ( 100 ) ;
599+
600+ const countBefore : number = await page . evaluate ( ( ) =>
601+ Module . ccall ( 'get_node_count' , 'number' , [ ] , [ ] )
602+ ) ;
603+
604+ const deleted : boolean = await page . evaluate ( ( ) =>
605+ Module . ccall ( 'trigger_delete_last_node' , 'boolean' , [ ] , [ ] )
606+ ) ;
607+ expect ( deleted ) . toBe ( true ) ;
608+
609+ const countAfter : number = await page . evaluate ( ( ) =>
610+ Module . ccall ( 'get_node_count' , 'number' , [ ] , [ ] )
611+ ) ;
612+ expect ( countAfter ) . toBe ( countBefore - 1 ) ;
613+ } ) ;
468614} ) ;
0 commit comments