@@ -14,6 +14,13 @@ import {
1414 extractMediaFilename ,
1515 buildMediaUrl ,
1616 extractBlobUrl ,
17+ SELECTION_CHANGED_MESSAGE_TYPE ,
18+ extractSelectedText ,
19+ serializeSelectedElements ,
20+ buildCellSelectionPayload ,
21+ buildDocumentSelectionPayload ,
22+ buildSelectionMessage ,
23+ createSelectionStream ,
1724 FONT_SPRITE_BASE_WIDTH ,
1825 FONT_SPRITE_ROW_HEIGHT
1926} from '../editors/desktop-stub-utils.js' ;
@@ -394,6 +401,208 @@ describe('extractBlobUrl', () => {
394401 } ) ;
395402} ) ;
396403
404+ describe ( 'selection streaming utilities' , ( ) => {
405+ test ( 'extractSelectedText reads Word and slide selected text safely' , ( ) => {
406+ const api = {
407+ asc_GetSelectedText ( includeHidden ) {
408+ expect ( includeHidden ) . toBe ( false ) ;
409+ return 'selected word text' ;
410+ }
411+ } ;
412+
413+ expect ( extractSelectedText ( api ) ) . toBe ( 'selected word text' ) ;
414+ expect ( extractSelectedText ( { asc_GetSelectedText ( ) { throw new Error ( 'sdk failure' ) ; } } ) ) . toBe ( '' ) ;
415+ expect ( extractSelectedText ( null ) ) . toBe ( '' ) ;
416+ } ) ;
417+
418+ test ( 'buildCellSelectionPayload streams active Excel cell, range, text, and sheet index' , ( ) => {
419+ const api = {
420+ asc_getCellInfo ( ) {
421+ return {
422+ asc_getName ( ) { return 'B2' ; } ,
423+ asc_getText ( ) { return 'Quarterly revenue' ; }
424+ } ;
425+ } ,
426+ asc_getActiveRangeStr ( _unused , activeCell ) {
427+ return activeCell ? 'B2' : 'B2:D4' ;
428+ } ,
429+ asc_getActiveWorksheetIndex ( ) {
430+ return 2 ;
431+ }
432+ } ;
433+
434+ expect ( buildCellSelectionPayload ( api , { } , null ) ) . toEqual ( {
435+ kind : 'cell' ,
436+ cell : 'B2' ,
437+ range : 'B2:D4' ,
438+ activeCell : 'B2' ,
439+ sheetIndex : 2 ,
440+ text : 'Quarterly revenue'
441+ } ) ;
442+ } ) ;
443+
444+ test ( 'buildCellSelectionPayload falls back to the workbook selection model' , ( ) => {
445+ const api = {
446+ wbModel : {
447+ getActiveWs ( ) {
448+ return {
449+ selectionRange : {
450+ getLast ( ) {
451+ return { c1 : 26 , r1 : 9 } ;
452+ }
453+ }
454+ } ;
455+ }
456+ } ,
457+ getActiveWorksheetIndex ( ) {
458+ return 1 ;
459+ }
460+ } ;
461+
462+ expect ( buildCellSelectionPayload ( api , { AscCommonExcel : { } } , null ) ) . toEqual ( {
463+ kind : 'cell' ,
464+ cell : 'AA10' ,
465+ range : 'AA10' ,
466+ activeCell : 'AA10' ,
467+ sheetIndex : 1 ,
468+ text : null
469+ } ) ;
470+ } ) ;
471+
472+ test ( 'serializeSelectedElements keeps image metadata JSON-safe and drops raw SDK objects' , ( ) => {
473+ const rawValue = {
474+ imageUrl : 'media/image1.png' ,
475+ cyclic : null
476+ } ;
477+ rawValue . cyclic = rawValue ;
478+
479+ const result = serializeSelectedElements ( [
480+ {
481+ asc_getType ( ) { return 'Image' ; } ,
482+ asc_getObjectValue ( ) { return rawValue ; } ,
483+ getId ( ) { return 'object-1' ; }
484+ }
485+ ] ) ;
486+
487+ expect ( result ) . toEqual ( [
488+ {
489+ type : 'Image' ,
490+ id : 'object-1' ,
491+ imageUrl : 'media/image1.png' ,
492+ hasImage : true
493+ }
494+ ] ) ;
495+ expect ( result [ 0 ] . value ) . toBeUndefined ( ) ;
496+ expect ( JSON . stringify ( result ) ) . toBe ( '[{"type":"Image","id":"object-1","imageUrl":"media/image1.png","hasImage":true}]' ) ;
497+ } ) ;
498+
499+ test ( 'buildDocumentSelectionPayload prefers selected Word text and includes selected objects' , ( ) => {
500+ const api = {
501+ asc_GetSelectedText ( ) {
502+ return 'hello world' ;
503+ } ,
504+ getSelectedElements ( ) {
505+ return [ { type : 'Paragraph' , value : 'p1' } ] ;
506+ }
507+ } ;
508+
509+ expect ( buildDocumentSelectionPayload ( api ) ) . toEqual ( {
510+ kind : 'text' ,
511+ text : 'hello world' ,
512+ objects : [
513+ {
514+ type : 'Paragraph' ,
515+ value : 'p1' ,
516+ hasImage : false
517+ }
518+ ]
519+ } ) ;
520+ } ) ;
521+
522+ test ( 'buildDocumentSelectionPayload emits image selection when text is empty' , ( ) => {
523+ const api = {
524+ asc_GetSelectedText ( ) {
525+ return '' ;
526+ }
527+ } ;
528+
529+ expect ( buildDocumentSelectionPayload ( api , [ { type : 'Picture' , ImageUrl : 'media/photo.png' } ] ) ) . toEqual ( {
530+ kind : 'image' ,
531+ objects : [
532+ {
533+ type : 'Picture' ,
534+ imageUrl : 'media/photo.png' ,
535+ hasImage : true
536+ }
537+ ]
538+ } ) ;
539+ } ) ;
540+
541+ test ( 'buildSelectionMessage includes file identity so all open files can be merged by the host' , ( ) => {
542+ expect ( buildSelectionMessage ( {
543+ doctype : 'word' ,
544+ filePath : '/tmp/report.docx' ,
545+ filename : 'report.docx'
546+ } , { kind : 'text' , text : 'budget' } , 1234 ) ) . toEqual ( {
547+ type : SELECTION_CHANGED_MESSAGE_TYPE ,
548+ filePath : '/tmp/report.docx' ,
549+ filename : 'report.docx' ,
550+ doctype : 'word' ,
551+ selection : { kind : 'text' , text : 'budget' } ,
552+ timestamp : 1234
553+ } ) ;
554+ } ) ;
555+
556+ test ( 'createSelectionStream posts live selection changes and dedupes identical payloads' , ( ) => {
557+ const posted = [ ] ;
558+ const parentWindow = {
559+ postMessage ( message , targetOrigin ) {
560+ posted . push ( { message, targetOrigin } ) ;
561+ }
562+ } ;
563+ const api = {
564+ asc_getCellInfo ( ) {
565+ return {
566+ asc_getName ( ) { return 'C3' ; } ,
567+ asc_getText ( ) { return '42' ; }
568+ } ;
569+ } ,
570+ asc_getActiveWorksheetIndex ( ) {
571+ return 0 ;
572+ }
573+ } ;
574+ const stream = createSelectionStream ( {
575+ api,
576+ parentWindow,
577+ doctype : 'cell' ,
578+ filePath : '/tmp/book.xlsx' ,
579+ filename : 'book.xlsx' ,
580+ now : ( ) => 999
581+ } ) ;
582+
583+ const first = stream . emitCell ( ) ;
584+ const duplicate = stream . emitCell ( ) ;
585+
586+ expect ( first ) . toEqual ( {
587+ type : SELECTION_CHANGED_MESSAGE_TYPE ,
588+ filePath : '/tmp/book.xlsx' ,
589+ filename : 'book.xlsx' ,
590+ doctype : 'cell' ,
591+ selection : {
592+ kind : 'cell' ,
593+ cell : 'C3' ,
594+ range : 'C3' ,
595+ activeCell : 'C3' ,
596+ sheetIndex : 0 ,
597+ text : '42'
598+ } ,
599+ timestamp : 999
600+ } ) ;
601+ expect ( duplicate ) . toBe ( null ) ;
602+ expect ( posted ) . toEqual ( [ { message : first , targetOrigin : '*' } ] ) ;
603+ } ) ;
604+ } ) ;
605+
397606describe ( 'constants' , ( ) => {
398607 test ( 'FONT_SPRITE_BASE_WIDTH is defined' , ( ) => {
399608 expect ( FONT_SPRITE_BASE_WIDTH ) . toBe ( 300 ) ;
0 commit comments