File size: 6,650 Bytes
7dc28be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
import { describe, it, expect } from 'vitest';
import { buildModifyTextRequests } from './modifyText.js';

describe('buildModifyTextRequests', () => {
  describe('insert mode', () => {
    it('should produce a single insertText request with no delete', () => {
      const requests = buildModifyTextRequests({ startIndex: 5, text: 'hello' });

      expect(requests).toHaveLength(1);
      expect(requests[0]).toHaveProperty('insertText');
      expect(requests[0]).not.toHaveProperty('deleteContentRange');
      expect(requests[0].insertText!.location!.index).toBe(5);
      expect(requests[0].insertText!.text).toBe('hello');
    });
  });

  describe('replace mode', () => {
    it('should produce deleteContentRange then insertText in that order', () => {
      const requests = buildModifyTextRequests({ startIndex: 5, endIndex: 10, text: 'hi' });

      expect(requests).toHaveLength(2);

      // First request must be delete (not insert — wrong order causes index shift)
      expect(requests[0]).toHaveProperty('deleteContentRange');
      expect(requests[0].deleteContentRange!.range!.startIndex).toBe(5);
      expect(requests[0].deleteContentRange!.range!.endIndex).toBe(10);

      // Second request is insert at the same startIndex
      expect(requests[1]).toHaveProperty('insertText');
      expect(requests[1].insertText!.location!.index).toBe(5);
      expect(requests[1].insertText!.text).toBe('hi');
    });
  });

  describe('format-only mode', () => {
    it('should produce a single updateTextStyle using the original range', () => {
      const requests = buildModifyTextRequests({
        startIndex: 5,
        endIndex: 10,
        style: { bold: true },
      });

      expect(requests).toHaveLength(1);
      expect(requests[0]).not.toHaveProperty('deleteContentRange');
      expect(requests[0]).not.toHaveProperty('insertText');
      expect(requests[0]).toHaveProperty('updateTextStyle');
      expect(requests[0].updateTextStyle!.range!.startIndex).toBe(5);
      expect(requests[0].updateTextStyle!.range!.endIndex).toBe(10);
      expect(requests[0].updateTextStyle!.textStyle!.bold).toBe(true);
      expect(requests[0].updateTextStyle!.fields).toBe('bold');
    });
  });

  describe('replace + format mode', () => {
    it('should adjust the format range to match the newly inserted text length', () => {
      const requests = buildModifyTextRequests({
        startIndex: 5,
        endIndex: 10,
        text: 'hi',
        style: { bold: true },
      });

      expect(requests).toHaveLength(3);

      // Delete original range
      expect(requests[0]).toHaveProperty('deleteContentRange');
      expect(requests[0].deleteContentRange!.range!.startIndex).toBe(5);
      expect(requests[0].deleteContentRange!.range!.endIndex).toBe(10);

      // Insert replacement text
      expect(requests[1]).toHaveProperty('insertText');
      expect(requests[1].insertText!.location!.index).toBe(5);
      expect(requests[1].insertText!.text).toBe('hi');

      // Format the NEW range [5, 7), NOT the original [5, 10)
      expect(requests[2]).toHaveProperty('updateTextStyle');
      expect(requests[2].updateTextStyle!.range!.startIndex).toBe(5);
      expect(requests[2].updateTextStyle!.range!.endIndex).toBe(7); // 5 + "hi".length
      expect(requests[2].updateTextStyle!.textStyle!.bold).toBe(true);
    });
  });

  describe('insert + format mode', () => {
    it('should insert text and format the inserted range', () => {
      const requests = buildModifyTextRequests({
        startIndex: 5,
        text: 'hi',
        style: { italic: true },
      });

      expect(requests).toHaveLength(2);

      // No delete — this is insert-only
      expect(requests[0]).toHaveProperty('insertText');
      expect(requests[0]).not.toHaveProperty('deleteContentRange');
      expect(requests[0].insertText!.location!.index).toBe(5);
      expect(requests[0].insertText!.text).toBe('hi');

      // Format the inserted range [5, 7)
      expect(requests[1]).toHaveProperty('updateTextStyle');
      expect(requests[1].updateTextStyle!.range!.startIndex).toBe(5);
      expect(requests[1].updateTextStyle!.range!.endIndex).toBe(7); // 5 + "hi".length
      expect(requests[1].updateTextStyle!.textStyle!.italic).toBe(true);
      expect(requests[1].updateTextStyle!.fields).toBe('italic');
    });
  });

  describe('tabId propagation', () => {
    it('should include tabId on all request locations/ranges when provided', () => {
      const requests = buildModifyTextRequests({
        startIndex: 5,
        endIndex: 10,
        text: 'hi',
        style: { bold: true },
        tabId: 't1',
      });

      expect(requests).toHaveLength(3);
      expect((requests[0].deleteContentRange!.range as any).tabId).toBe('t1');
      expect((requests[1].insertText!.location as any).tabId).toBe('t1');
      expect(requests[2].updateTextStyle!.range!.tabId).toBe('t1');
    });

    it('should not include tabId when not provided', () => {
      const requests = buildModifyTextRequests({
        startIndex: 5,
        endIndex: 10,
        text: 'hi',
        style: { bold: true },
      });

      expect(requests).toHaveLength(3);
      expect((requests[0].deleteContentRange!.range as any).tabId).toBeUndefined();
      expect((requests[1].insertText!.location as any).tabId).toBeUndefined();
      expect(requests[2].updateTextStyle!.range!.tabId).toBeUndefined();
    });
  });

  describe('edge cases', () => {
    it('should return empty array when neither text nor style is provided', () => {
      const requests = buildModifyTextRequests({ startIndex: 5, endIndex: 10 });
      expect(requests).toHaveLength(0);
    });

    it('should not generate delete when endIndex is provided but text is not', () => {
      const requests = buildModifyTextRequests({
        startIndex: 5,
        endIndex: 10,
        style: { underline: true },
      });

      expect(requests).toHaveLength(1);
      expect(requests[0]).toHaveProperty('updateTextStyle');
      expect(requests[0]).not.toHaveProperty('deleteContentRange');
    });

    it('should handle multiple style fields correctly', () => {
      const requests = buildModifyTextRequests({
        startIndex: 1,
        endIndex: 5,
        style: { bold: true, italic: true, fontSize: 14 },
      });

      expect(requests).toHaveLength(1);
      const style = requests[0].updateTextStyle!;
      expect(style.textStyle!.bold).toBe(true);
      expect(style.textStyle!.italic).toBe(true);
      expect(style.textStyle!.fontSize).toEqual({ magnitude: 14, unit: 'PT' });
      expect(style.fields).toBe('bold,italic,fontSize');
    });
  });
});