summaryrefslogtreecommitdiff
path: root/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
blob: 3caf03dabba2ee9f71bbb1852b091987d633ab68 (plain)
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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
import { attributeDefinition } from './renderers/mock_data';

describe('rich_content_editor/services/html_to_markdown_renderer', () => {
  let baseRenderer;
  let htmlToMarkdownRenderer;
  let fakeNode;

  beforeEach(() => {
    baseRenderer = {
      trim: jest.fn((input) => `trimmed ${input}`),
      getSpaceCollapsedText: jest.fn((input) => `space collapsed ${input}`),
      getSpaceControlled: jest.fn((input) => `space controlled ${input}`),
      convert: jest.fn(),
    };

    fakeNode = { nodeValue: 'mock_node', dataset: {} };
  });

  afterEach(() => {
    htmlToMarkdownRenderer = null;
  });

  describe('TEXT_NODE visitor', () => {
    it('composes getSpaceControlled, getSpaceCollapsedText, and trim services', () => {
      htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);

      expect(htmlToMarkdownRenderer.TEXT_NODE(fakeNode)).toBe(
        `space controlled trimmed space collapsed ${fakeNode.nodeValue}`,
      );
    });
  });

  describe('LI OL, LI UL visitor', () => {
    const oneLevelNestedList = '\n    * List item 1\n    * List item 2';
    const twoLevelNestedList = '\n  * List item 1\n    * List item 2';
    const spaceInContentList = '\n  * List    item 1\n  * List item 2';

    it.each`
      list                  | indentSpaces | result
      ${oneLevelNestedList} | ${2}         | ${'\n  * List item 1\n  * List item 2'}
      ${oneLevelNestedList} | ${3}         | ${'\n   * List item 1\n   * List item 2'}
      ${oneLevelNestedList} | ${6}         | ${'\n      * List item 1\n      * List item 2'}
      ${twoLevelNestedList} | ${4}         | ${'\n    * List item 1\n        * List item 2'}
      ${spaceInContentList} | ${1}         | ${'\n * List    item 1\n * List item 2'}
    `('changes the list indentation to $indentSpaces spaces', ({ list, indentSpaces, result }) => {
      htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
        subListIndentSpaces: indentSpaces,
      });

      baseRenderer.convert.mockReturnValueOnce(list);

      expect(htmlToMarkdownRenderer['LI OL, LI UL'](fakeNode, list)).toBe(result);
      expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, list);
    });
  });

  describe('UL LI visitor', () => {
    it.each`
      listItem           | unorderedListBulletChar | result             | bulletChar
      ${'* list item'}   | ${undefined}            | ${'- list item'}   | ${'default'}
      ${'  - list item'} | ${'*'}                  | ${'  * list item'} | ${'*'}
      ${'  * list item'} | ${'-'}                  | ${'  - list item'} | ${'-'}
    `(
      'uses $bulletChar bullet char in unordered list items when $unorderedListBulletChar is set in config',
      ({ listItem, unorderedListBulletChar, result }) => {
        htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
          unorderedListBulletChar,
        });
        baseRenderer.convert.mockReturnValueOnce(listItem);

        expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result);
        expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, listItem);
      },
    );

    it('detects attribute definitions and attaches them to the list item', () => {
      const listItem = '- list item';
      const result = `${listItem}\n${attributeDefinition}\n`;

      fakeNode.dataset.attributeDefinition = attributeDefinition;
      htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
      baseRenderer.convert.mockReturnValueOnce(`${listItem}\n`);

      expect(htmlToMarkdownRenderer['UL LI'](fakeNode, listItem)).toBe(result);
    });
  });

  describe('OL LI visitor', () => {
    it.each`
      listItem              | result              | incrementListMarker | action
      ${'2. list item'}     | ${'1. list item'}   | ${false}            | ${'increments'}
      ${'  3. list item'}   | ${'  1. list item'} | ${false}            | ${'increments'}
      ${'  123. list item'} | ${'  1. list item'} | ${false}            | ${'increments'}
      ${'3. list item'}     | ${'3. list item'}   | ${true}             | ${'does not increment'}
    `(
      '$action a list item counter when incrementListMaker is $incrementListMarker',
      ({ listItem, result, incrementListMarker }) => {
        const subContent = null;

        htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
          incrementListMarker,
        });
        baseRenderer.convert.mockReturnValueOnce(listItem);

        expect(htmlToMarkdownRenderer['OL LI'](fakeNode, subContent)).toBe(result);
        expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, subContent);
      },
    );
  });

  describe('STRONG, B visitor', () => {
    it.each`
      input                | strongCharacter | result
      ${'**strong text**'} | ${'_'}          | ${'__strong text__'}
      ${'__strong text__'} | ${'*'}          | ${'**strong text**'}
    `(
      'converts $input to $result when strong character is $strongCharacter',
      ({ input, strongCharacter, result }) => {
        htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
          strong: strongCharacter,
        });

        baseRenderer.convert.mockReturnValueOnce(input);

        expect(htmlToMarkdownRenderer['STRONG, B'](fakeNode, input)).toBe(result);
        expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input);
      },
    );
  });

  describe('EM, I visitor', () => {
    it.each`
      input              | emphasisCharacter | result
      ${'*strong text*'} | ${'_'}            | ${'_strong text_'}
      ${'_strong text_'} | ${'*'}            | ${'*strong text*'}
    `(
      'converts $input to $result when emphasis character is $emphasisCharacter',
      ({ input, emphasisCharacter, result }) => {
        htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
          emphasis: emphasisCharacter,
        });

        baseRenderer.convert.mockReturnValueOnce(input);

        expect(htmlToMarkdownRenderer['EM, I'](fakeNode, input)).toBe(result);
        expect(baseRenderer.convert).toHaveBeenCalledWith(fakeNode, input);
      },
    );
  });

  describe('H1, H2, H3, H4, H5, H6 visitor', () => {
    it('detects attribute definitions and attaches them to the heading', () => {
      const heading = 'heading text';
      const result = `${heading.trimRight()}\n${attributeDefinition}\n\n`;

      fakeNode.dataset.attributeDefinition = attributeDefinition;
      htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
      baseRenderer.convert.mockReturnValueOnce(`${heading}\n\n`);

      expect(htmlToMarkdownRenderer['H1, H2, H3, H4, H5, H6'](fakeNode, heading)).toBe(result);
    });
  });

  describe('PRE CODE', () => {
    let node;
    const subContent = 'sub content';
    const originalConverterResult = 'base result';

    beforeEach(() => {
      node = document.createElement('PRE');

      node.innerText = 'reference definition content';
      node.dataset.sseReferenceDefinition = true;

      baseRenderer.convert.mockReturnValueOnce(originalConverterResult);
      htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
    });

    it('returns raw text when pre node has sse-reference-definitions class', () => {
      expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(
        `\n\n${node.innerText}\n\n`,
      );
    });

    it('returns base result when pre node does not have sse-reference-definitions class', () => {
      delete node.dataset.sseReferenceDefinition;

      expect(htmlToMarkdownRenderer['PRE CODE'](node, subContent)).toBe(originalConverterResult);
    });
  });

  describe('IMG', () => {
    const originalSrc = 'path/to/image.png';
    const alt = 'alt text';
    let node;

    beforeEach(() => {
      node = document.createElement('img');
      node.alt = alt;
      node.src = originalSrc;
    });

    it('returns an image with its original src of the `original-src` attribute is preset', () => {
      node.dataset.originalSrc = originalSrc;
      node.src = 'modified/path/to/image.png';

      htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);

      expect(htmlToMarkdownRenderer.IMG(node)).toBe(`![${alt}](${originalSrc})`);
    });

    it('fallback to `src` if no `original-src` is specified on the image', () => {
      htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer);
      expect(htmlToMarkdownRenderer.IMG(node)).toBe(`![${alt}](${originalSrc})`);
    });
  });
});