Skip to content

Commit d4c440c

Browse files
committed
feat: stream editor selection changes
1 parent b7ab4db commit d4c440c

4 files changed

Lines changed: 651 additions & 1 deletion

File tree

__tests__/desktop-stub-utils.test.js

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
397606
describe('constants', () => {
398607
test('FONT_SPRITE_BASE_WIDTH is defined', () => {
399608
expect(FONT_SPRITE_BASE_WIDTH).toBe(300);

__tests__/offline-loader-config.test.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,18 @@ describe('offline loader editorConfig', () => {
99
const loaderHtml = fs.readFileSync(LOADER_PATH, 'utf8');
1010
expect(loaderHtml).toMatch(/featuresTips\s*:\s*false/);
1111
});
12+
13+
test('streams live editor selections to the host window', () => {
14+
const loaderHtml = fs.readFileSync(LOADER_PATH, 'utf8');
15+
16+
expect(loaderHtml).toContain('createSelectionStream');
17+
expect(loaderHtml).toContain('window.parent');
18+
expect(loaderHtml).toContain('selectionStream.emitCell(cellInfo)');
19+
expect(loaderHtml).toContain("asc_registerCallback('asc_onSelectionChanged'");
20+
expect(loaderHtml).toContain("asc_registerCallback('asc_onSelectionNameChanged'");
21+
expect(loaderHtml).toContain("asc_registerCallback('asc_onSelectionEnd'");
22+
expect(loaderHtml).toContain("asc_registerCallback('asc_onCursorMove'");
23+
expect(loaderHtml).toContain("asc_registerCallback('asc_onFocusObject'");
24+
expect(loaderHtml).toContain('selectionStream.emitDocument(focusedElements)');
25+
});
1226
});

0 commit comments

Comments
 (0)