@@ -11,6 +11,10 @@ global.SpeechSynthesisUtterance = class {
1111 }
1212} ;
1313
14+ function transmit ( speech , str ) {
15+ for ( const ch of str ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
16+ }
17+
1418describe ( "SpeechOutput" , ( ) => {
1519 let speech ;
1620
@@ -23,92 +27,82 @@ describe("SpeechOutput", () => {
2327
2428 it ( "tryReceive always returns -1" , ( ) => {
2529 expect ( speech . tryReceive ( ) ) . toBe ( - 1 ) ;
26- expect ( speech . tryReceive ( true ) ) . toBe ( - 1 ) ;
2730 } ) ;
2831
2932 it ( "speaks buffered text on CR" , ( ) => {
30- for ( const ch of "HELLO" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
33+ transmit ( speech , "HELLO" ) ;
3134 expect ( mockSpeak ) . not . toHaveBeenCalled ( ) ;
32- speech . onTransmit ( 13 ) ; // CR
35+ speech . onTransmit ( 0x0d ) ;
3336 expect ( mockSpeak ) . toHaveBeenCalledOnce ( ) ;
3437 expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( "HELLO" ) ;
3538 } ) ;
3639
37- it ( "does NOT flush on LF (LF is null data per Votrax spec)" , ( ) => {
38- for ( const ch of "WORLD" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
39- speech . onTransmit ( 10 ) ; // LF — null data, must not trigger speech
40+ it ( "multiple CR-terminated lines queue without cancelling each other" , ( ) => {
41+ transmit ( speech , "Welcome to the castle." ) ;
42+ speech . onTransmit ( 0x0d ) ;
43+ transmit ( speech , "There is a sword here." ) ;
44+ speech . onTransmit ( 0x0d ) ;
45+ transmit ( speech , "What now?" ) ;
46+ speech . onTransmit ( 0x0d ) ;
47+
48+ expect ( mockSpeak ) . toHaveBeenCalledTimes ( 3 ) ;
49+ expect ( mockCancel ) . not . toHaveBeenCalled ( ) ;
50+ expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( "Welcome to the castle." ) ;
51+ expect ( mockSpeak . mock . calls [ 1 ] [ 0 ] . text ) . toBe ( "There is a sword here." ) ;
52+ expect ( mockSpeak . mock . calls [ 2 ] [ 0 ] . text ) . toBe ( "What now?" ) ;
53+ } ) ;
54+
55+ it ( "LF is null data — ignored" , ( ) => {
56+ transmit ( speech , "WORLD" ) ;
57+ speech . onTransmit ( 0x0a ) ; // LF — ignored
4058 expect ( mockSpeak ) . not . toHaveBeenCalled ( ) ;
41- speech . onTransmit ( 13 ) ; // CR — the real flush trigger
42- expect ( mockSpeak ) . toHaveBeenCalledOnce ( ) ;
59+ speech . onTransmit ( 0x0d ) ;
4360 expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( "WORLD" ) ;
4461 } ) ;
4562
4663 it ( "does nothing when disabled" , ( ) => {
4764 speech . enabled = false ;
48- for ( const ch of "TEST" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
49- speech . onTransmit ( 13 ) ;
65+ transmit ( speech , "TEST" ) ;
66+ speech . onTransmit ( 0x0d ) ;
5067 expect ( mockSpeak ) . not . toHaveBeenCalled ( ) ;
5168 } ) ;
5269
53- it ( "cancels speech and clears buffer when disabled mid-buffer " , ( ) => {
54- for ( const ch of "PARTIAL" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
70+ it ( "cancels speech and clears buffer when disabled" , ( ) => {
71+ transmit ( speech , "PARTIAL" ) ;
5572 speech . enabled = false ;
5673 expect ( mockCancel ) . toHaveBeenCalled ( ) ;
5774 speech . enabled = true ;
58- speech . onTransmit ( 13 ) ;
59- expect ( mockSpeak ) . not . toHaveBeenCalled ( ) ; // buffer was cleared
75+ speech . onTransmit ( 0x0d ) ;
76+ expect ( mockSpeak ) . not . toHaveBeenCalled ( ) ;
6077 } ) ;
6178
62- it ( "ignores non-printable bytes (< 0x20) other than CR, BS, ESC" , ( ) => {
63- // Per Votrax manual: non-printable bytes that aren't specified commands
64- // are null data and are ignored. This means BBC VDU codes, BEL,
65- // LF, etc. are all silently dropped.
66- speech . onTransmit ( 7 ) ; // BEL
67- speech . onTransmit ( 22 ) ; // VDU 22 (MODE)
68- speech . onTransmit ( 7 ) ; // would-be VDU param byte — treated as null data, not VDU
69- for ( const ch of "DING" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
70- speech . onTransmit ( 13 ) ;
79+ it ( "ignores non-printable bytes other than CR and ESC" , ( ) => {
80+ speech . onTransmit ( 7 ) ; // BEL — null data
81+ speech . onTransmit ( 22 ) ; // VDU 22 — null data
82+ transmit ( speech , "DING" ) ;
83+ speech . onTransmit ( 0x0d ) ;
7184 expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( "DING" ) ;
7285 } ) ;
7386
74- it ( "handles BS (0x08) — deletes last character from buffer" , ( ) => {
75- for ( const ch of "HI!" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
76- speech . onTransmit ( 0x08 ) ; // delete "!"
87+ it ( "BS (0x08) is null data — ignored" , ( ) => {
88+ // The TNT manual lists only CR, LF, and ESC as defined commands.
89+ transmit ( speech , "HI!" ) ;
90+ speech . onTransmit ( 0x08 ) ;
7791 speech . onTransmit ( 0x0d ) ;
78- expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( "HI" ) ;
92+ expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( "HI! " ) ;
7993 } ) ;
8094
81- it ( "handles ESC (0x1B) — next byte is a mode control, not text " , ( ) => {
82- for ( const ch of "TEST" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
95+ it ( "ESC consumes the following byte silently (unit-select, TNT manual) " , ( ) => {
96+ transmit ( speech , "TEST" ) ;
8397 speech . onTransmit ( 0x1b ) ; // ESC
84- speech . onTransmit ( 0x11 ) ; // DC1 = PSEND ON — consumed as mode code
98+ speech . onTransmit ( 0x41 ) ; // unit-select byte — consumed
8599 speech . onTransmit ( 0x0d ) ;
86100 expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( "TEST" ) ;
87101 } ) ;
88102
89- it ( "ignores DEL (127) and high bytes" , ( ) => {
90- speech . onTransmit ( 127 ) ;
91- speech . onTransmit ( 200 ) ;
92- for ( const ch of "HI" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
93- speech . onTransmit ( 13 ) ;
94- expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( "HI" ) ;
95- } ) ;
96-
97- it ( "cancels in-progress speech before starting new utterance" , ( ) => {
98- for ( const ch of "ONE" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
99- speech . onTransmit ( 13 ) ;
100- for ( const ch of "TWO" ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
101- speech . onTransmit ( 13 ) ;
102- expect ( mockCancel ) . toHaveBeenCalledTimes ( 2 ) ;
103- expect ( mockSpeak ) . toHaveBeenCalledTimes ( 2 ) ;
104- } ) ;
105-
106- it ( "auto-speaks when input buffer reaches MAX_BUFFER bytes (buffer-full condition)" , ( ) => {
107- // The Votrax manual says "input buffer full" is a TALK-CLR trigger.
108- // Our MAX_BUFFER is 128 bytes.
109- const longText = "A" . repeat ( MAX_BUFFER ) ;
110- for ( const ch of longText ) speech . onTransmit ( ch . charCodeAt ( 0 ) ) ;
103+ it ( "auto-flushes when buffer reaches MAX_BUFFER bytes" , ( ) => {
104+ transmit ( speech , "A" . repeat ( MAX_BUFFER ) ) ;
111105 expect ( mockSpeak ) . toHaveBeenCalledOnce ( ) ;
112- expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( longText ) ;
106+ expect ( mockSpeak . mock . calls [ 0 ] [ 0 ] . text ) . toBe ( "A" . repeat ( MAX_BUFFER ) ) ;
113107 } ) ;
114108} ) ;
0 commit comments