|
| 1 | +/* eslint-disable testing-library/no-node-access */ |
| 2 | +/* eslint-disable testing-library/no-container */ |
| 3 | +import { promises as fs } from 'fs'; |
| 4 | +import path from 'path'; |
| 5 | + |
| 6 | +// eslint-disable-next-line testing-library/no-manual-cleanup |
| 7 | +import { render, screen, cleanup } from '@testing-library/react'; |
| 8 | +import { globSync } from 'glob'; |
| 9 | + |
| 10 | +import syntaxHighlighter, { uppercase, canonical } from '../src'; |
| 11 | + |
| 12 | +const fixtures = globSync(path.join(__dirname, '/__fixtures__/*')); |
| 13 | + |
| 14 | +test('should highlight a block of code', () => { |
| 15 | + render(syntaxHighlighter('var a = 1;', 'javascript')); |
| 16 | + |
| 17 | + expect(screen.getByTestId('SyntaxHighlighter').outerHTML).toBe( |
| 18 | + '<div class="cm-s-neo" data-testid="SyntaxHighlighter"><span class="cm-keyword">var</span> <span class="cm-def">a</span> <span class="cm-operator">=</span> <span class="cm-number">1</span>;</div>', |
| 19 | + ); |
| 20 | +}); |
| 21 | + |
| 22 | +test('should work when passed a non-string value', () => { |
| 23 | + expect(() => syntaxHighlighter(false, 'text')).not.toThrow(); |
| 24 | +}); |
| 25 | + |
| 26 | +test('should sanitize plain text language', () => { |
| 27 | + render(syntaxHighlighter('& < > " \' /', 'text')); |
| 28 | + expect(screen.getByText('& < > " \' /')).toBeVisible(); |
| 29 | +}); |
| 30 | + |
| 31 | +test('should sanitize mode', () => { |
| 32 | + render(syntaxHighlighter('&', 'json')); |
| 33 | + expect(screen.getByText('&')).toBeVisible(); |
| 34 | + |
| 35 | + render(syntaxHighlighter('<', 'json')); |
| 36 | + expect(screen.getByText('<')).toBeVisible(); |
| 37 | +}); |
| 38 | + |
| 39 | +test('should concat the same style items', () => { |
| 40 | + // This is testing the `accum += text;` line |
| 41 | + render(syntaxHighlighter('====', 'javascript')); |
| 42 | + expect(screen.getByText('====')).toBeVisible(); |
| 43 | +}); |
| 44 | + |
| 45 | +test('should work with modes', () => { |
| 46 | + render(syntaxHighlighter('{ "a": 1 }', 'json')); |
| 47 | + |
| 48 | + expect(screen.getByTestId('SyntaxHighlighter').outerHTML).toBe( |
| 49 | + '<div class="cm-s-neo" data-testid="SyntaxHighlighter">{ <span class="cm-property">"a"</span>: <span class="cm-number">1</span> }</div>', |
| 50 | + ); |
| 51 | +}); |
| 52 | + |
| 53 | +test('should keep trailing json bracket if highlightMode is enabled', () => { |
| 54 | + render(syntaxHighlighter('{ "a": 1 }', 'json', { highlightMode: true })); |
| 55 | + |
| 56 | + expect(screen.getByTestId('CodeMirror').outerHTML).toBe( |
| 57 | + '<div class="CodeMirror" data-testid="CodeMirror"><div class="cm-linerow "><span class="cm-lineNumber">1</span>{ <span class="cm-property">"a"</span>: <span class="cm-number">1</span> }</div></div>', |
| 58 | + ); |
| 59 | +}); |
| 60 | + |
| 61 | +test('should have a dark theme', () => { |
| 62 | + render(syntaxHighlighter('{ "a": 1 }', 'json', { dark: true })); |
| 63 | + expect(screen.getByTestId('SyntaxHighlighter')).toHaveClass('cm-s-material-palenight'); |
| 64 | +}); |
| 65 | + |
| 66 | +describe('variable substitution', () => { |
| 67 | + it('should tokenize variables (double quotes)', () => { |
| 68 | + render(syntaxHighlighter('"<<apiKey>>"', 'json', { tokenizeVariables: true })); |
| 69 | + expect(screen.getByText('APIKEY')).toBeVisible(); |
| 70 | + }); |
| 71 | + |
| 72 | + it('should tokenize variables (single quotes)', () => { |
| 73 | + render(syntaxHighlighter("'<<apiKey>>'", 'json', { tokenizeVariables: true })); |
| 74 | + expect(screen.getByText('APIKEY')).toBeVisible(); |
| 75 | + }); |
| 76 | + |
| 77 | + it('should keep enclosing characters around the variable', () => { |
| 78 | + render(syntaxHighlighter("'<<apiKey>>'", 'json', { tokenizeVariables: true })); |
| 79 | + expect(screen.getByTestId('SyntaxHighlighter')).toHaveTextContent("'APIKEY'"); |
| 80 | + }); |
| 81 | + |
| 82 | + it('should tokenize variables outside of quotes', () => { |
| 83 | + render(syntaxHighlighter('<<apiKey>>', 'json', { tokenizeVariables: true })); |
| 84 | + expect(screen.getByText('APIKEY')).toBeVisible(); |
| 85 | + }); |
| 86 | + |
| 87 | + it('should tokenize variables outside of quotes over multiple lines', () => { |
| 88 | + const codeBlock = ` |
| 89 | + const foo = <<apiKey>>; |
| 90 | + const bar = <<name>>; |
| 91 | +
|
| 92 | + fetch({ foo, bar, baz: <<token>> }); |
| 93 | + `; |
| 94 | + |
| 95 | + render(syntaxHighlighter(codeBlock, 'json', { tokenizeVariables: true })); |
| 96 | + expect(screen.getByTestId('SyntaxHighlighter').textContent).toMatchSnapshot(); |
| 97 | + }); |
| 98 | + |
| 99 | + it('should tokenize multiple variables per line', () => { |
| 100 | + render(syntaxHighlighter('<<apiKey>> <<name>>', 'json', { tokenizeVariables: true })); |
| 101 | + expect(screen.getByTestId('SyntaxHighlighter')).toHaveTextContent('APIKEY NAME'); |
| 102 | + }); |
| 103 | + |
| 104 | + it.each(['\\<<wat>>', '<<wat\\>>', '\\<<wat\\>>'])('should NOT tokenize escaped variables %s', code => { |
| 105 | + render(syntaxHighlighter(code, 'json', { tokenizeVariables: true })); |
| 106 | + expect(screen.getByTestId('SyntaxHighlighter')).toHaveTextContent('<<wat>>'); |
| 107 | + }); |
| 108 | +}); |
| 109 | + |
| 110 | +describe('variable substitution { mdx: true }', () => { |
| 111 | + it('should tokenize variables (double quotes)', () => { |
| 112 | + render(syntaxHighlighter('"{user.apiKey}"', 'json', { tokenizeVariables: true }, { mdx: true })); |
| 113 | + expect(screen.getByText('APIKEY')).toBeVisible(); |
| 114 | + }); |
| 115 | + |
| 116 | + it('should tokenize variables (single quotes)', () => { |
| 117 | + render(syntaxHighlighter("'{user.apiKey}'", 'json', { tokenizeVariables: true }, { mdx: true })); |
| 118 | + expect(screen.getByText('APIKEY')).toBeVisible(); |
| 119 | + }); |
| 120 | + |
| 121 | + it('should keep enclosing characters around the variable', () => { |
| 122 | + render(syntaxHighlighter("'{user.apiKey}'", 'json', { tokenizeVariables: true }, { mdx: true })); |
| 123 | + expect(screen.getByTestId('SyntaxHighlighter')).toHaveTextContent("'APIKEY'"); |
| 124 | + }); |
| 125 | + |
| 126 | + it('should tokenize variables outside of quotes', () => { |
| 127 | + render(syntaxHighlighter('{user.apiKey}', 'json', { tokenizeVariables: true }, { mdx: true })); |
| 128 | + expect(screen.getByText('APIKEY')).toBeVisible(); |
| 129 | + }); |
| 130 | + |
| 131 | + it('should tokenize variables outside of quotes over multiple lines', () => { |
| 132 | + const codeBlock = ` |
| 133 | + const foo = {user.apiKey}; |
| 134 | + const bar = {user.name}; |
| 135 | +
|
| 136 | + fetch({ foo, bar, baz: {user.token} }); |
| 137 | + `; |
| 138 | + |
| 139 | + render(syntaxHighlighter(codeBlock, 'json', { tokenizeVariables: true }, { mdx: true })); |
| 140 | + expect(screen.getByTestId('SyntaxHighlighter').textContent).toMatchInlineSnapshot(` |
| 141 | + " |
| 142 | + const foo = APIKEY; |
| 143 | + const bar = NAME; |
| 144 | +
|
| 145 | + fetch({ foo, bar, baz: TOKEN }); |
| 146 | + " |
| 147 | + `); |
| 148 | + }); |
| 149 | + |
| 150 | + it('should tokenize multiple variables per line', () => { |
| 151 | + render(syntaxHighlighter('{user.apiKey} {user.name}', 'json', { tokenizeVariables: true }, { mdx: true })); |
| 152 | + expect(screen.getByTestId('SyntaxHighlighter')).toHaveTextContent('APIKEY NAME'); |
| 153 | + }); |
| 154 | + |
| 155 | + it('should not tokenize bracket style', () => { |
| 156 | + render(syntaxHighlighter('<<wat>>', 'json', { tokenizeVariables: true }, { mdx: true })); |
| 157 | + expect(screen.getByTestId('SyntaxHighlighter')).toHaveTextContent('<<wat>>'); |
| 158 | + }); |
| 159 | + |
| 160 | + it.each(['\\{user.wat}', '{user.wat\\}', '\\{user.wat\\}'])('should NOT tokenize escaped variables %s', code => { |
| 161 | + render(syntaxHighlighter(code, 'json', { tokenizeVariables: true }, { mdx: true })); |
| 162 | + expect(screen.getByTestId('SyntaxHighlighter')).toHaveTextContent('{user.wat}'); |
| 163 | + }); |
| 164 | +}); |
| 165 | + |
| 166 | +describe('Supported languages', () => { |
| 167 | + const languages = fixtures.map(fixture => { |
| 168 | + return [uppercase(path.basename(fixture)), fixture]; |
| 169 | + }); |
| 170 | + |
| 171 | + describe.each(languages)('%s', (language, fixtureDir) => { |
| 172 | + let testCase; |
| 173 | + |
| 174 | + // eslint-disable-next-line global-require, import/no-dynamic-require |
| 175 | + const instructions = require(path.join(fixtureDir, 'index.js')); |
| 176 | + |
| 177 | + beforeEach(async () => { |
| 178 | + testCase = await fs.readFile(path.join(fixtureDir, `sample.${instructions.mode.primary}`), 'utf8'); |
| 179 | + }); |
| 180 | + |
| 181 | + it('should have a properly formatted instruction set', () => { |
| 182 | + expect(instructions).toBeDefined(); |
| 183 | + expect(instructions).toStrictEqual({ |
| 184 | + language: expect.any(String), |
| 185 | + mode: expect.objectContaining({ |
| 186 | + primary: expect.any(String), |
| 187 | + aliases: expect.any(Object), |
| 188 | + }), |
| 189 | + }); |
| 190 | + }); |
| 191 | + |
| 192 | + it('should syntax highlight an example', () => { |
| 193 | + render(syntaxHighlighter(testCase, instructions.mode.primary)); |
| 194 | + expect(screen.getByTestId('SyntaxHighlighter').outerHTML).toMatchSnapshot(); |
| 195 | + }); |
| 196 | + |
| 197 | + if (Object.keys(instructions.mode.aliases).length > 0) { |
| 198 | + const aliases = Object.keys(instructions.mode.aliases).map(alias => [alias, instructions.mode.aliases[alias]]); |
| 199 | + |
| 200 | + describe('Mode aliases', () => { |
| 201 | + describe.each(aliases)('%s', (alias, aliasName) => { |
| 202 | + it('should support the mode alias', () => { |
| 203 | + render(syntaxHighlighter(testCase, instructions.mode.primary)); |
| 204 | + const highlighted = screen.getByTestId('SyntaxHighlighter').outerHTML; |
| 205 | + cleanup(); |
| 206 | + |
| 207 | + render(syntaxHighlighter(testCase, alias)); |
| 208 | + expect(screen.getByTestId('SyntaxHighlighter').outerHTML).toBe(highlighted); |
| 209 | + }); |
| 210 | + |
| 211 | + it('should uppercase the mode alias', () => { |
| 212 | + expect(uppercase(alias)).toBe(aliasName); |
| 213 | + }); |
| 214 | + |
| 215 | + if ('canonical' in instructions.mode) { |
| 216 | + it('should have a canonical directive set up', () => { |
| 217 | + expect(canonical(alias)).toBe(instructions.mode.canonical); |
| 218 | + }); |
| 219 | + } else { |
| 220 | + it('should have a canonical directive set up off the primary mode', () => { |
| 221 | + expect(canonical(alias)).toBe(instructions.mode.primary); |
| 222 | + }); |
| 223 | + } |
| 224 | + }); |
| 225 | + }); |
| 226 | + } |
| 227 | + |
| 228 | + if (instructions.mode.primary === 'html') { |
| 229 | + it('should highlight handlebars templates', () => { |
| 230 | + const code = '<p>{{firstname}} {{lastname}}</p>'; |
| 231 | + const { container } = render(syntaxHighlighter(code, 'handlebars')); |
| 232 | + |
| 233 | + expect(container.querySelector('.cm-bracket')).toBeVisible(); |
| 234 | + }); |
| 235 | + } else if (instructions.mode.primary === 'php') { |
| 236 | + it('should highlight if missing an opening `<?php` tag', () => { |
| 237 | + const code = 'echo "Hello World";'; |
| 238 | + const { container } = render(syntaxHighlighter(code, 'php')); |
| 239 | + |
| 240 | + expect(container.querySelector('.cm-keyword')).toBeVisible(); |
| 241 | + }); |
| 242 | + } |
| 243 | + }); |
| 244 | +}); |
| 245 | + |
| 246 | +describe('highlight mode', () => { |
| 247 | + const code = `curl --request POST |
| 248 | + --url <<url>> |
| 249 | + --header 'authorization: Bearer 123' |
| 250 | + --header 'content-type: application/json'`; |
| 251 | + |
| 252 | + const defaultRender = () => |
| 253 | + render( |
| 254 | + syntaxHighlighter(code, 'curl', { |
| 255 | + dark: true, |
| 256 | + highlightMode: true, |
| 257 | + tokenizeVariables: true, |
| 258 | + ranges: [ |
| 259 | + [ |
| 260 | + { ch: 0, line: 0 }, |
| 261 | + { ch: 0, line: 1 }, |
| 262 | + ], |
| 263 | + ], |
| 264 | + }), |
| 265 | + ); |
| 266 | + |
| 267 | + it('should return line numbers by default', () => { |
| 268 | + const { container } = defaultRender(); |
| 269 | + expect(container.querySelector('.cm-lineNumber')).toBeVisible(); |
| 270 | + }); |
| 271 | + |
| 272 | + it('should highlight variables', () => { |
| 273 | + defaultRender(); |
| 274 | + expect(screen.getByText('URL')).toBeVisible(); |
| 275 | + }); |
| 276 | + |
| 277 | + it('should highlight based on range input', () => { |
| 278 | + const { container } = defaultRender(); |
| 279 | + expect(container.querySelector('.cm-linerow.cm-highlight')).toHaveTextContent('1curl --request POST'); |
| 280 | + }); |
| 281 | + |
| 282 | + it('should add an overlay to non-highlighted in lines when ranges are applied', () => { |
| 283 | + const { container } = defaultRender(); |
| 284 | + expect(container.querySelectorAll('.cm-linerow.cm-overlay')).toHaveLength(3); |
| 285 | + }); |
| 286 | +}); |
| 287 | + |
| 288 | +describe('runmode', () => { |
| 289 | + const code = |
| 290 | + 'CURL *hnd = curl_easy_init();\n\nurl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "GET");\n\ncurl_easy_setopt(hnd, CURLOPT_URL, "http://httpbin.orgpet/");'; |
| 291 | + |
| 292 | + it('should display the correct number of lines with multiple linebreaks', () => { |
| 293 | + render( |
| 294 | + syntaxHighlighter(code, 'c', { |
| 295 | + dark: true, |
| 296 | + highlightMode: true, |
| 297 | + tokenizeVariables: true, |
| 298 | + ranges: [ |
| 299 | + [ |
| 300 | + { ch: 0, line: 0 }, |
| 301 | + { ch: 0, line: 1 }, |
| 302 | + ], |
| 303 | + ], |
| 304 | + }), |
| 305 | + ); |
| 306 | + |
| 307 | + expect(screen.getAllByText(/\d/)).toHaveLength(5); |
| 308 | + }); |
| 309 | +}); |
| 310 | + |
| 311 | +describe('code folding', () => { |
| 312 | + beforeAll(() => { |
| 313 | + // Prevents tests from throwing `TypeError: range(...).getBoundingClientRect is not a function` |
| 314 | + // Ref: https://github.com/jsdom/jsdom/issues/3002 |
| 315 | + document.createRange = () => { |
| 316 | + const range = new Range(); |
| 317 | + |
| 318 | + range.getBoundingClientRect = jest.fn(); |
| 319 | + |
| 320 | + range.getClientRects = jest.fn(() => ({ |
| 321 | + item: () => null, |
| 322 | + length: 0, |
| 323 | + })); |
| 324 | + |
| 325 | + return range; |
| 326 | + }; |
| 327 | + }); |
| 328 | + |
| 329 | + it('renders folders in the gutter', () => { |
| 330 | + const { container } = render( |
| 331 | + syntaxHighlighter('{ "a": { "b": { "c": 1 } }', 'json', { foldGutter: true, readOnly: true }), |
| 332 | + ); |
| 333 | + |
| 334 | + expect(container.querySelector('.CodeMirror-foldgutter')).toBeVisible(); |
| 335 | + }); |
| 336 | +}); |
0 commit comments