code-editing.spec.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import { expect } from '@playwright/test'
  2. import { test } from './fixtures'
  3. import {
  4. createRandomPage,
  5. escapeToCodeEditor,
  6. escapeToBlockEditor,
  7. repeatKeyPress,
  8. } from './utils'
  9. /**
  10. * NOTE: CodeMirror is a complex library that requires a lot of setup to work.
  11. * This test suite is designed to test the basic functionality of the editor.
  12. * It is not intended to test the full functionality of CodeMirror.
  13. * For more information, see: https://codemirror.net/doc/manual.html
  14. */
  15. // TODO: Fix test that started intermittently failing some time around
  16. // https://github.com/logseq/logseq/pull/9540
  17. test.skip('switch code editing mode', async ({ page }) => {
  18. await createRandomPage(page)
  19. // NOTE: ` will trigger auto-pairing in Logseq
  20. // NOTE: ( will trigger auto-pairing in CodeMirror
  21. // NOTE: waitForTimeout is needed to ensure that the hotkey handler is finished (shift+enter)
  22. // NOTE: waitForTimeout is needed to ensure that the CodeMirror editor is fully loaded and unloaded
  23. // NOTE: multiple textarea elements are existed in the editor, be careful to select the right one
  24. // code block with 0 line
  25. await page.type('textarea >> nth=0', '```clojure\n')
  26. // line number: 1
  27. await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
  28. expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber').innerText()).toBe('1')
  29. // lang label: clojure
  30. expect(await page.innerText('.block-body .extensions__code-lang')).toBe('clojure')
  31. await page.press('.CodeMirror textarea', 'Escape')
  32. await page.waitForSelector('.CodeMirror pre', { state: 'hidden' })
  33. expect(await page.inputValue('textarea >> nth=0')).toBe('```clojure\n```')
  34. await page.waitForTimeout(200)
  35. await page.press('textarea >> nth=0', 'Escape')
  36. await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
  37. // NOTE: must wait here, await loading of CodeMirror editor
  38. await page.waitForTimeout(200)
  39. await page.click('.CodeMirror pre')
  40. await page.waitForTimeout(200)
  41. await page.type('.CodeMirror textarea', '(+ 1 1')
  42. await page.press('.CodeMirror textarea', 'Escape')
  43. await page.waitForSelector('.CodeMirror pre', { state: 'hidden' })
  44. expect(await page.inputValue('.block-editor textarea')).toBe('```clojure\n(+ 1 1)\n```')
  45. await page.waitForTimeout(200) // editor unloading
  46. await page.press('.block-editor textarea', 'Escape')
  47. await page.waitForTimeout(200) // editor loading
  48. // click position is estimated to be at the beginning of the first line
  49. await page.click('.CodeMirror pre', { position: { x: 1, y: 5 } })
  50. await page.waitForTimeout(200)
  51. await page.type('.CodeMirror textarea', ';; comment\n\n \n')
  52. await page.press('.CodeMirror textarea', 'Escape')
  53. await page.waitForSelector('.CodeMirror pre', { state: 'hidden' })
  54. expect(await page.inputValue('.block-editor textarea')).toBe('```clojure\n;; comment\n\n \n(+ 1 1)\n```')
  55. })
  56. test('convert from block content to code', async ({ page }) => {
  57. await createRandomPage(page)
  58. await page.type('.block-editor textarea', '```')
  59. await page.press('.block-editor textarea', 'Shift+Enter')
  60. await page.waitForTimeout(200) // wait for hotkey handler
  61. await page.press('.block-editor textarea', 'Escape')
  62. await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
  63. await page.waitForTimeout(500)
  64. await page.click('.CodeMirror pre')
  65. await page.waitForTimeout(500)
  66. expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber >> nth=-1').innerText()).toBe('1')
  67. await page.press('.CodeMirror textarea', 'Escape')
  68. await page.waitForTimeout(500)
  69. expect(await page.inputValue('.block-editor textarea')).toBe('```\n```')
  70. // reset block, code block with 1 line
  71. await page.fill('.block-editor textarea', '```\n\n```')
  72. await page.waitForTimeout(200) // wait for fill
  73. await escapeToCodeEditor(page)
  74. expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber >> nth=-1').innerText()).toBe('1')
  75. await escapeToBlockEditor(page)
  76. expect(await page.inputValue('.block-editor textarea')).toBe('```\n\n```')
  77. // reset block, code block with 2 line
  78. await page.fill('.block-editor textarea', '```\n\n\n```')
  79. await page.waitForTimeout(200)
  80. await escapeToCodeEditor(page)
  81. expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber >> nth=-1').innerText()).toBe('2')
  82. await escapeToBlockEditor(page)
  83. expect(await page.inputValue('.block-editor textarea')).toBe('```\n\n\n```')
  84. await page.fill('.block-editor textarea', '```\n indented\nsecond line\n\n```')
  85. await page.waitForTimeout(200)
  86. await escapeToCodeEditor(page)
  87. await escapeToBlockEditor(page)
  88. expect(await page.inputValue('.block-editor textarea')).toBe('```\n indented\nsecond line\n\n```')
  89. await page.fill('.block-editor textarea', '```\n indented\n indented\n```')
  90. await page.waitForTimeout(200)
  91. await escapeToCodeEditor(page)
  92. await escapeToBlockEditor(page)
  93. expect(await page.inputValue('.block-editor textarea')).toBe('```\n indented\n indented\n```')
  94. })
  95. test('code block mixed input source', async ({ page }) => {
  96. await createRandomPage(page)
  97. await page.fill('.block-editor textarea', '```\n ABC\n```')
  98. await page.waitForTimeout(500) // wait for fill
  99. await escapeToCodeEditor(page)
  100. await page.type('.CodeMirror textarea', ' DEF\nGHI')
  101. await page.waitForTimeout(500)
  102. await page.press('.CodeMirror textarea', 'Escape')
  103. await page.waitForTimeout(500)
  104. // NOTE: auto-indent is on
  105. expect(await page.inputValue('.block-editor textarea')).toBe('```\n ABC DEF\n GHI\n```')
  106. })
  107. test('code block with text around', async ({ page }) => {
  108. await createRandomPage(page)
  109. await page.fill('.block-editor textarea', 'Heading\n```\n```\nFooter')
  110. await page.waitForTimeout(200)
  111. await escapeToCodeEditor(page)
  112. await page.type('.CodeMirror textarea', 'first\n second')
  113. await page.waitForTimeout(500)
  114. await page.press('.CodeMirror textarea', 'Escape')
  115. await page.waitForTimeout(500)
  116. expect(await page.inputValue('.block-editor textarea')).toBe('Heading\n```\nfirst\n second\n```\nFooter')
  117. })
  118. test('multiple code block', async ({ page }) => {
  119. await createRandomPage(page)
  120. // NOTE: the two code blocks are of the same content
  121. await page.fill('.block-editor textarea', '中文 Heading\n```clojure\n```\nMiddle 🚀\n```clojure\n```\nFooter')
  122. await page.waitForTimeout(200)
  123. await page.press('.block-editor textarea', 'Escape')
  124. await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
  125. // first
  126. await page.waitForTimeout(500)
  127. await page.click('.CodeMirror pre >> nth=0')
  128. await page.waitForTimeout(500)
  129. await page.type('.CodeMirror textarea >> nth=0', ':key-test\n', { strict: true })
  130. await page.waitForTimeout(500)
  131. await page.press('.CodeMirror textarea >> nth=0', 'Escape')
  132. await page.waitForTimeout(500)
  133. expect(await page.inputValue('.block-editor textarea'))
  134. .toBe('中文 Heading\n```clojure\n:key-test\n\n```\nMiddle 🚀\n```clojure\n```\nFooter')
  135. // second
  136. await page.press('.block-editor textarea', 'Escape')
  137. await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
  138. await page.waitForTimeout(500)
  139. await page.click('.CodeMirror >> nth=1 >> pre')
  140. await page.waitForTimeout(500)
  141. await page.type('.CodeMirror textarea >> nth=1', '\n :key-test 日本語\n', { strict: true })
  142. await page.waitForTimeout(500)
  143. await page.press('.CodeMirror textarea >> nth=1', 'Escape')
  144. await page.waitForTimeout(500)
  145. expect(await page.inputValue('.block-editor textarea'))
  146. .toBe('中文 Heading\n```clojure\n:key-test\n\n```\nMiddle 🚀\n```clojure\n\n :key-test 日本語\n\n```\nFooter')
  147. })
  148. test('click outside to exit', async ({ page }) => {
  149. await createRandomPage(page)
  150. await page.fill('.block-editor textarea', 'Header ``Click``\n```\n ABC\n```')
  151. await page.waitForTimeout(200) // wait for fill
  152. await escapeToCodeEditor(page)
  153. await page.type('.CodeMirror textarea', ' DEF\nGHI')
  154. await page.waitForTimeout(500)
  155. await page.click('text=Click')
  156. await page.waitForTimeout(500)
  157. // NOTE: auto-indent is on
  158. expect(await page.inputValue('.block-editor textarea')).toBe('Header ``Click``\n```\n ABC DEF\n GHI\n```')
  159. })
  160. test('click language label to exit #3463', async ({ page, block }) => {
  161. await createRandomPage(page)
  162. await page.fill('.block-editor textarea', '```cpp\n```')
  163. await page.waitForTimeout(200)
  164. await escapeToCodeEditor(page)
  165. await page.type('.CodeMirror textarea', '#include<iostream>')
  166. await page.waitForTimeout(500)
  167. await page.click('text=cpp') // the language label
  168. await page.waitForTimeout(500)
  169. expect(await page.inputValue('.block-editor textarea')).toBe('```cpp\n#include<iostream>\n```')
  170. })
  171. test('multi properties with code', async ({ page }) => {
  172. await createRandomPage(page)
  173. await page.fill('.block-editor textarea',
  174. 'type:: code\n' +
  175. '类型:: 代码\n' +
  176. '```go\n' +
  177. 'if err != nil {\n' +
  178. '\treturn err\n' +
  179. '}\n' +
  180. '```'
  181. )
  182. await page.waitForTimeout(200)
  183. await escapeToCodeEditor(page)
  184. // first character of code
  185. await page.click('.CodeMirror pre', { position: { x: 1, y: 5 } })
  186. await page.waitForTimeout(500)
  187. await page.type('.CodeMirror textarea', '// Returns nil\n')
  188. await page.waitForTimeout(500)
  189. await page.press('.CodeMirror textarea', 'Escape')
  190. await page.waitForTimeout(500)
  191. expect(await page.inputValue('.block-editor textarea')).toBe(
  192. 'type:: code\n' +
  193. '类型:: 代码\n' +
  194. '```go\n' +
  195. '// Returns nil\n' +
  196. 'if err != nil {\n' +
  197. '\treturn err\n' +
  198. '}\n' +
  199. '```'
  200. )
  201. })
  202. test('Select codeblock language', async ({ page }) => {
  203. await createRandomPage(page)
  204. // Open the slash command menu
  205. await page.type('textarea >> nth=0', '/code block', { delay: 20 })
  206. expect(
  207. await page.waitForSelector('[data-modal-name="commands"]', {
  208. state: 'visible',
  209. })
  210. ).toBeTruthy()
  211. // Select `code block` command and open the language dropdown menu
  212. await page.press('textarea >> nth=0', 'Enter', { delay: 10 })
  213. // wait for the modal to open
  214. expect(
  215. await page.waitForSelector('[data-modal-name="select-code-block-mode"]', {
  216. state: 'visible',
  217. })
  218. ).toBeTruthy()
  219. // Select Clojure from the dropdown menu
  220. await repeatKeyPress(page, 'ArrowDown', 6)
  221. await page.press('textarea >> nth=0', 'Enter', { delay: 10 })
  222. await page.waitForTimeout(100)
  223. // expect the codeblock to be visible
  224. expect(await page.waitForSelector('.CodeMirror', { state: 'visible' }))
  225. // Exit codeblock and return to block edit mode
  226. await page.press('.CodeMirror textarea >> nth=0', 'Escape', { delay: 10 })
  227. expect(await page.inputValue('.block-editor textarea')).toBe(
  228. '```clojure\n```'
  229. )
  230. })
  231. test('Select codeblock language while surrounded by text', async ({ page }) => {
  232. await createRandomPage(page)
  233. await page.type('textarea >> nth=0', 'ABC XYZ', { delay: 20 })
  234. await repeatKeyPress(page, 'ArrowLeft', 3)
  235. // Open the slash command menu
  236. await page.type('textarea >> nth=0', '/code block', { delay: 20 })
  237. expect(
  238. await page.waitForSelector('[data-modal-name="commands"]', {
  239. state: 'visible',
  240. })
  241. ).toBeTruthy()
  242. // Select `code block` command and open the language dropdown menu
  243. await page.press('textarea >> nth=0', 'Enter', { delay: 10 })
  244. // wait for the modal to open
  245. expect(
  246. await page.waitForSelector('[data-modal-name="select-code-block-mode"]', {
  247. state: 'visible',
  248. })
  249. ).toBeTruthy()
  250. // Select Clojure from the dropdown menu
  251. await repeatKeyPress(page, 'ArrowDown', 6)
  252. await page.press('textarea >> nth=0', 'Enter', { delay: 10 })
  253. // expect the codeblock to be visible
  254. expect(await page.waitForSelector('.CodeMirror', { state: 'visible' }))
  255. // Exit codeblock and return to block edit mode
  256. await page.press('.CodeMirror textarea >> nth=0', 'Escape', { delay: 10 })
  257. expect(await page.inputValue('.block-editor textarea')).toBe(
  258. 'ABC \n```clojure\n```\nXYZ'
  259. )
  260. })