@@ -36,7 +36,47 @@ global.Synth = jest.fn().mockImplementation(() => ({
3636 stopSound : jest . fn ( ) ,
3737 disposeAllInstruments : jest . fn ( ) ,
3838 changeInTemperament : false ,
39- recorder : null
39+ recorder : null ,
40+ transport : {
41+ get isAvailable ( ) {
42+ return typeof global . Tone !== "undefined" && ! ! global . Tone . Transport ;
43+ } ,
44+ start ( ) {
45+ if ( this . isAvailable ) global . Tone . Transport . start ( ) ;
46+ } ,
47+ stop ( ) {
48+ if ( this . isAvailable ) global . Tone . Transport . stop ( ) ;
49+ } ,
50+ cancel ( ) {
51+ if ( this . isAvailable && typeof global . Tone . Transport . cancel === "function" ) {
52+ global . Tone . Transport . cancel ( ) ;
53+ }
54+ } ,
55+ clear ( id ) {
56+ if ( this . isAvailable && typeof global . Tone . Transport . clear === "function" ) {
57+ global . Tone . Transport . clear ( id ) ;
58+ }
59+ } ,
60+ schedule ( callback , time ) {
61+ if ( this . isAvailable && typeof global . Tone . Transport . schedule === "function" ) {
62+ return global . Tone . Transport . schedule ( callback , time ) ;
63+ }
64+ return null ;
65+ } ,
66+ get seconds ( ) {
67+ if ( this . isAvailable ) return global . Tone . Transport . seconds ;
68+ return 0 ;
69+ } ,
70+ set seconds ( v ) {
71+ if ( this . isAvailable ) global . Tone . Transport . seconds = v ;
72+ } ,
73+ getSecondsAtTime ( time ) {
74+ if ( this . isAvailable && typeof global . Tone . Transport . getSecondsAtTime === "function" ) {
75+ return global . Tone . Transport . getSecondsAtTime ( time ) ;
76+ }
77+ return this . seconds ;
78+ }
79+ }
4080} ) ) ;
4181global . Singer = {
4282 setSynthVolume : jest . fn ( ) ,
@@ -435,14 +475,28 @@ describe("Logo Class", () => {
435475 } ) ;
436476
437477 describe ( "doStopTurtles" , ( ) => {
438- test ( "sets stopTurtle to true" , ( ) => {
439- logo . sounds = [ ] ;
440- logo . synth = {
478+ function mockSynth ( ) {
479+ return {
441480 stop : jest . fn ( ) ,
442481 stopSound : jest . fn ( ) ,
443482 disposeAllInstruments : jest . fn ( ) ,
444- recorder : null
483+ recorder : null ,
484+ transport : {
485+ get isAvailable ( ) {
486+ return false ;
487+ } ,
488+ cancel : jest . fn ( ) ,
489+ get seconds ( ) {
490+ return 0 ;
491+ } ,
492+ set seconds ( v ) { }
493+ }
445494 } ;
495+ }
496+
497+ test ( "sets stopTurtle to true" , ( ) => {
498+ logo . sounds = [ ] ;
499+ logo . synth = mockSynth ( ) ;
446500
447501 logo . doStopTurtles ( ) ;
448502
@@ -451,12 +505,7 @@ describe("Logo Class", () => {
451505
452506 test ( "marks all turtles as stopped" , ( ) => {
453507 logo . sounds = [ ] ;
454- logo . synth = {
455- stop : jest . fn ( ) ,
456- stopSound : jest . fn ( ) ,
457- disposeAllInstruments : jest . fn ( ) ,
458- recorder : null
459- } ;
508+ logo . synth = mockSynth ( ) ;
460509
461510 logo . doStopTurtles ( ) ;
462511
@@ -466,12 +515,7 @@ describe("Logo Class", () => {
466515 test ( "stops all sounds" , ( ) => {
467516 const mockSound = { stop : jest . fn ( ) } ;
468517 logo . sounds = [ mockSound ] ;
469- logo . synth = {
470- stop : jest . fn ( ) ,
471- stopSound : jest . fn ( ) ,
472- disposeAllInstruments : jest . fn ( ) ,
473- recorder : null
474- } ;
518+ logo . synth = mockSynth ( ) ;
475519
476520 logo . doStopTurtles ( ) ;
477521
@@ -481,12 +525,7 @@ describe("Logo Class", () => {
481525
482526 test ( "clears step queue" , ( ) => {
483527 logo . sounds = [ ] ;
484- logo . synth = {
485- stop : jest . fn ( ) ,
486- stopSound : jest . fn ( ) ,
487- disposeAllInstruments : jest . fn ( ) ,
488- recorder : null
489- } ;
528+ logo . synth = mockSynth ( ) ;
490529 logo . stepQueue = { 0 : [ 1 , 2 , 3 ] } ;
491530
492531 logo . doStopTurtles ( ) ;
@@ -496,12 +535,7 @@ describe("Logo Class", () => {
496535
497536 test ( "executes ONSTOP plugin hooks" , ( ) => {
498537 logo . sounds = [ ] ;
499- logo . synth = {
500- stop : jest . fn ( ) ,
501- stopSound : jest . fn ( ) ,
502- disposeAllInstruments : jest . fn ( ) ,
503- recorder : null
504- } ;
538+ logo . synth = mockSynth ( ) ;
505539 logo . evalOnStopList = {
506540 firstHook : "code-first" ,
507541 secondHook : "code-second"
@@ -719,6 +753,8 @@ describe("Logo comprehensive method coverage", () => {
719753 x : 0 ,
720754 y : 0 ,
721755 companionTurtle : null ,
756+ _transportTime : null ,
757+ _transportEventId : null ,
722758 doShowImage : jest . fn ( ) ,
723759 doShowURL : jest . fn ( ) ,
724760 doShowText : jest . fn ( )
@@ -915,6 +951,80 @@ describe("Logo comprehensive method coverage", () => {
915951 expect ( logo . runFromBlockNow ) . toHaveBeenCalledWith ( logo , 0 , 3 , 1 , "x" ) ;
916952 } ) ;
917953
954+ test ( "runFromBlock uses Transport schedule for non-zero delay when available" , ( ) => {
955+ const originalTone = global . Tone ;
956+ const getSecondsAtTimeMock = jest . fn ( t => t + 1 ) ;
957+ let scheduledCallback = null ;
958+ const scheduleSpy = jest . fn ( ( fn , time ) => {
959+ scheduledCallback = fn ;
960+ return "evt-1" ;
961+ } ) ;
962+ global . Tone = {
963+ ...originalTone ,
964+ Transport : {
965+ start : jest . fn ( ) ,
966+ stop : jest . fn ( ) ,
967+ schedule : scheduleSpy ,
968+ cancel : jest . fn ( ) ,
969+ getSecondsAtTime : getSecondsAtTimeMock ,
970+ get seconds ( ) {
971+ return 0 ;
972+ } ,
973+ set seconds ( v ) { }
974+ }
975+ } ;
976+
977+ logo . runFromBlockNow = jest . fn ( ) ;
978+ logo . turtleDelay = 0 ;
979+ logo . stopTurtle = false ;
980+ turtle0 . waitTime = 200 ;
981+ turtle0 . _transportTime = 10 ;
982+ turtle0 . delayParameters = null ;
983+ turtle0 . _transportEventId = null ;
984+
985+ logo . runFromBlock ( logo , 0 , 3 , 1 , "x" ) ;
986+
987+ expect ( scheduleSpy ) . toHaveBeenCalledWith ( expect . any ( Function ) , 10 + 200 / 1000 ) ;
988+ expect ( logo . runFromBlockNow ) . not . toHaveBeenCalled ( ) ;
989+ expect ( turtle0 . _transportEventId ) . toBe ( "evt-1" ) ;
990+
991+ // Invoke the scheduled callback and verify state updates
992+ scheduledCallback ( 5.5 ) ;
993+ expect ( getSecondsAtTimeMock ) . toHaveBeenCalledWith ( 5.5 ) ;
994+ expect ( turtle0 . _transportTime ) . toBe ( 6.5 ) ;
995+ expect ( turtle0 . _transportEventId ) . toBeNull ( ) ;
996+ expect ( turtle0 . delayParameters ) . toBeNull ( ) ;
997+ expect ( logo . runFromBlockNow ) . toHaveBeenCalledWith ( logo , 0 , 3 , 1 , "x" ) ;
998+
999+ global . Tone = originalTone ;
1000+ turtle0 . _transportTime = null ;
1001+ turtle0 . _transportEventId = null ;
1002+ turtle0 . delayParameters = null ;
1003+ } ) ;
1004+
1005+ test ( "runFromBlock falls back to setTimeout when Transport is unavailable" , ( ) => {
1006+ const originalTone = global . Tone ;
1007+ global . Tone = { ...originalTone , Transport : undefined } ;
1008+ timeoutSpy = jest . spyOn ( global , "setTimeout" ) . mockImplementation ( fn => {
1009+ fn ( ) ;
1010+ return 5 ;
1011+ } ) ;
1012+
1013+ logo . runFromBlockNow = jest . fn ( ) ;
1014+ logo . turtleDelay = 0 ;
1015+ logo . stopTurtle = false ;
1016+ turtle0 . waitTime = 200 ;
1017+
1018+ logo . runFromBlock ( logo , 0 , 3 , 1 , "x" ) ;
1019+
1020+ expect ( timeoutSpy ) . toHaveBeenCalled ( ) ;
1021+ expect ( logo . runFromBlockNow ) . toHaveBeenCalledWith ( logo , 0 , 3 , 1 , "x" ) ;
1022+
1023+ global . Tone = originalTone ;
1024+ timeoutSpy . mockRestore ( ) ;
1025+ timeoutSpy = null ;
1026+ } ) ;
1027+
9181028 test ( "runFromBlockNow executes flow block and queues next block" , ( ) => {
9191029 logo . runFromBlock = jest . fn ( ) ;
9201030 logo . blockList = [
@@ -1184,13 +1294,59 @@ describe("Logo comprehensive method coverage", () => {
11841294 stop : jest . fn ( ) ,
11851295 stopSound : jest . fn ( ) ,
11861296 disposeAllInstruments : jest . fn ( ) ,
1187- recorder : { state : "recording" , stop : jest . fn ( ) }
1297+ recorder : { state : "recording" , stop : jest . fn ( ) } ,
1298+ transport : {
1299+ get isAvailable ( ) {
1300+ return typeof global . Tone !== "undefined" && ! ! global . Tone . Transport ;
1301+ } ,
1302+ cancel ( ) {
1303+ if ( this . isAvailable && typeof global . Tone . Transport . cancel === "function" ) {
1304+ global . Tone . Transport . cancel ( ) ;
1305+ }
1306+ } ,
1307+ get seconds ( ) {
1308+ if ( this . isAvailable ) return global . Tone . Transport . seconds ;
1309+ return 0 ;
1310+ } ,
1311+ set seconds ( v ) {
1312+ if ( this . isAvailable ) global . Tone . Transport . seconds = v ;
1313+ }
1314+ }
11881315 } ;
11891316 logo . _restoreConnections = jest . fn ( ) ;
11901317 mockActivity . showBlocksAfterRun = true ;
11911318
1319+ const originalTone = global . Tone ;
1320+ const cancelSpy = jest . fn ( ) ;
1321+ let transportSeconds = 42 ;
1322+ global . Tone = {
1323+ ...originalTone ,
1324+ Transport : {
1325+ start : jest . fn ( ) ,
1326+ stop : jest . fn ( ) ,
1327+ cancel : cancelSpy ,
1328+ getSecondsAtTime : jest . fn ( ( ) => 0 ) ,
1329+ get seconds ( ) {
1330+ return transportSeconds ;
1331+ } ,
1332+ set seconds ( v ) {
1333+ transportSeconds = v ;
1334+ }
1335+ }
1336+ } ;
1337+ turtle0 . _transportTime = 15 ;
1338+ turtle1 . _transportTime = 25 ;
1339+ turtle0 . _transportEventId = "evt-1" ;
1340+ turtle1 . _transportEventId = "evt-2" ;
1341+
11921342 logo . doStopTurtles ( ) ;
11931343
1344+ expect ( cancelSpy ) . toHaveBeenCalled ( ) ;
1345+ expect ( turtle0 . _transportTime ) . toBeNull ( ) ;
1346+ expect ( turtle1 . _transportTime ) . toBeNull ( ) ;
1347+ expect ( turtle0 . _transportEventId ) . toBeNull ( ) ;
1348+ expect ( turtle1 . _transportEventId ) . toBeNull ( ) ;
1349+ expect ( transportSeconds ) . toBe ( 0 ) ;
11941350 expect ( turtle0 . singer . killAllVoices ) . toHaveBeenCalled ( ) ;
11951351 expect ( logo . synth . stopSound ) . toHaveBeenCalledWith ( "0" , "flute" ) ;
11961352 expect ( logo . synth . stopSound ) . toHaveBeenCalledWith ( "1" , "piano" ) ;
@@ -1199,6 +1355,10 @@ describe("Logo comprehensive method coverage", () => {
11991355 expect ( doStopVideoCam ) . toHaveBeenCalledWith ( "cam-1" , logo . setCameraID ) ;
12001356 expect ( mockActivity . blocks . showBlocks ) . toHaveBeenCalled ( ) ;
12011357 expect ( document . getElementById ) . toHaveBeenCalledWith ( "stop" ) ;
1358+
1359+ turtle0 . _transportTime = null ;
1360+ turtle1 . _transportTime = null ;
1361+ global . Tone = originalTone ;
12021362 clearIntervalSpy . mockRestore ( ) ;
12031363 } ) ;
12041364
@@ -1285,6 +1445,80 @@ describe("Logo comprehensive method coverage", () => {
12851445 clearTimeoutSpy . mockRestore ( ) ;
12861446 } ) ;
12871447
1448+ test ( "clearTurtleRun cancels Transport-scheduled event and resumes execution" , ( ) => {
1449+ const clearSpy = jest . fn ( ) ;
1450+ const originalTone = global . Tone ;
1451+ global . Tone = {
1452+ ...originalTone ,
1453+ Transport : {
1454+ start : jest . fn ( ) ,
1455+ stop : jest . fn ( ) ,
1456+ cancel : jest . fn ( ) ,
1457+ clear : clearSpy ,
1458+ getSecondsAtTime : jest . fn ( ( ) => 42 ) ,
1459+ get seconds ( ) {
1460+ return 42 ;
1461+ } ,
1462+ set seconds ( v ) { }
1463+ }
1464+ } ;
1465+ turtle0 . delayTimeout = null ;
1466+ turtle0 . _transportEventId = "evt-3" ;
1467+ turtle0 . delayParameters = { blk : 7 , flow : 0 , arg : null } ;
1468+ turtle0 . _transportTime = 50 ;
1469+ logo . runFromBlockNow = jest . fn ( ) ;
1470+
1471+ logo . clearTurtleRun ( 0 ) ;
1472+
1473+ expect ( clearSpy ) . toHaveBeenCalledWith ( "evt-3" ) ;
1474+ expect ( turtle0 . _transportEventId ) . toBeNull ( ) ;
1475+ expect ( turtle0 . _transportTime ) . toBe ( 42 ) ;
1476+ expect ( logo . runFromBlockNow ) . toHaveBeenCalledWith ( logo , 0 , 7 , 0 , null ) ;
1477+ expect ( turtle0 . delayParameters ) . toBeNull ( ) ;
1478+
1479+ global . Tone = originalTone ;
1480+ } ) ;
1481+
1482+ test ( "clearTurtleRun is no-op when no pending delay or Transport event" , ( ) => {
1483+ turtle0 . delayTimeout = null ;
1484+ turtle0 . _transportEventId = null ;
1485+ logo . runFromBlockNow = jest . fn ( ) ;
1486+
1487+ logo . clearTurtleRun ( 0 ) ;
1488+
1489+ expect ( logo . runFromBlockNow ) . not . toHaveBeenCalled ( ) ;
1490+ } ) ;
1491+
1492+ test ( "runFromBlock lazily initializes _transportTime when null" , ( ) => {
1493+ const originalTone = global . Tone ;
1494+ global . Tone = {
1495+ ...originalTone ,
1496+ Transport : {
1497+ start : jest . fn ( ) ,
1498+ stop : jest . fn ( ) ,
1499+ schedule : jest . fn ( ( ) => "evt-1" ) ,
1500+ cancel : jest . fn ( ) ,
1501+ getSecondsAtTime : jest . fn ( ( ) => 0 ) ,
1502+ get seconds ( ) {
1503+ return 5 ;
1504+ } ,
1505+ set seconds ( v ) { }
1506+ }
1507+ } ;
1508+ logo . runFromBlockNow = jest . fn ( ) ;
1509+ logo . turtleDelay = 0 ;
1510+ logo . stopTurtle = false ;
1511+ turtle0 . waitTime = 200 ;
1512+ turtle0 . _transportTime = null ;
1513+
1514+ logo . runFromBlock ( logo , 0 , 3 , 1 , "x" ) ;
1515+
1516+ expect ( turtle0 . _transportTime ) . toBe ( 5 ) ;
1517+ expect ( logo . runFromBlockNow ) . not . toHaveBeenCalled ( ) ;
1518+
1519+ global . Tone = originalTone ;
1520+ } ) ;
1521+
12881522 test ( "setDispatchBlock adds clamp signal for normal and backward traversal" , ( ) => {
12891523 turtle0 . endOfClampSignals = { } ;
12901524 logo . blockList = [
0 commit comments