whiteboards.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. import { expect } from '@playwright/test'
  2. import { test } from './fixtures'
  3. import { modKey, renamePage } from './utils'
  4. test('enable whiteboards', async ({ page }) => {
  5. if (await page.$('.nav-header .whiteboard') === null) {
  6. await page.click('#head .toolbar-dots-btn')
  7. await page.click('#head .dropdown-wrapper >> text=Settings')
  8. await page.click('.settings-modal a[data-id=features]')
  9. await page.click('text=Whiteboards >> .. >> .ui__toggle')
  10. await page.waitForTimeout(1000)
  11. await page.keyboard.press('Escape')
  12. }
  13. await expect(page.locator('.nav-header .whiteboard')).toBeVisible()
  14. })
  15. test('should display onboarding tour', async ({ page }) => {
  16. // ensure onboarding tour is going to be triggered locally
  17. await page.evaluate(`window.clearWhiteboardStorage()`)
  18. await page.click('.nav-header .whiteboard')
  19. await expect(page.locator('.cp__whiteboard-welcome')).toBeVisible()
  20. await page.click('.cp__whiteboard-welcome button.skip-welcome')
  21. await expect(page.locator('.cp__whiteboard-welcome')).toBeHidden()
  22. })
  23. test('create new whiteboard', async ({ page }) => {
  24. await page.click('#tl-create-whiteboard')
  25. await expect(page.locator('.logseq-tldraw')).toBeVisible()
  26. })
  27. test('can right click title to show context menu', async ({ page }) => {
  28. await page.click('.whiteboard-page-title', {
  29. button: 'right',
  30. })
  31. await expect(page.locator('#custom-context-menu')).toBeVisible()
  32. await page.keyboard.press('Escape')
  33. await expect(page.locator('#custom-context-menu')).toHaveCount(0)
  34. })
  35. test('newly created whiteboard should have a default title', async ({ page }) => {
  36. await expect(page.locator('.whiteboard-page-title .title')).toContainText(
  37. 'Untitled'
  38. )
  39. })
  40. test('set whiteboard title', async ({ page }) => {
  41. const title = 'my-whiteboard'
  42. await page.click('.nav-header .whiteboard')
  43. await page.click('#tl-create-whiteboard')
  44. await page.click('.whiteboard-page-title')
  45. await page.fill('.whiteboard-page-title input', title)
  46. await page.keyboard.press('Enter')
  47. await expect(page.locator('.whiteboard-page-title .title')).toContainText(
  48. title
  49. )
  50. })
  51. test('update whiteboard title', async ({ page }) => {
  52. const title = 'my-whiteboard'
  53. await page.click('.whiteboard-page-title')
  54. await page.fill('.whiteboard-page-title input', title + '-2')
  55. await page.keyboard.press('Enter')
  56. await expect(page.locator('.whiteboard-page-title .title')).toContainText(
  57. title + '-2'
  58. )
  59. })
  60. test('draw a rectangle', async ({ page }) => {
  61. const canvas = await page.waitForSelector('.logseq-tldraw')
  62. const bounds = (await canvas.boundingBox())!
  63. await page.keyboard.type('wr')
  64. await page.mouse.move(bounds.x + 105, bounds.y + 105)
  65. await page.mouse.down()
  66. await page.mouse.move(bounds.x + 150, bounds.y + 150 )
  67. await page.mouse.up()
  68. await page.keyboard.press('Escape')
  69. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
  70. })
  71. test('undo the rectangle action', async ({ page }) => {
  72. await page.keyboard.press(modKey + '+z', { delay: 100 })
  73. await expect(page.locator('.logseq-tldraw .tl-positioned-svg rect')).toHaveCount(0)
  74. })
  75. test('redo the rectangle action', async ({ page }) => {
  76. await page.waitForTimeout(100)
  77. await page.keyboard.press(modKey + '+Shift+z', { delay: 100 })
  78. await page.keyboard.press('Escape')
  79. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
  80. })
  81. test('clone the rectangle', async ({ page }) => {
  82. const canvas = await page.waitForSelector('.logseq-tldraw')
  83. const bounds = (await canvas.boundingBox())!
  84. await page.mouse.move(bounds.x + 400, bounds.y + 400)
  85. await page.mouse.move(bounds.x + 120, bounds.y + 120, {steps: 5})
  86. await page.keyboard.down('Alt')
  87. await page.mouse.down()
  88. await page.mouse.move(bounds.x + 200, bounds.y + 200, {steps: 5})
  89. await page.mouse.up()
  90. await page.keyboard.up('Alt')
  91. await page.waitForTimeout(100)
  92. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
  93. })
  94. test('group the rectangles', async ({ page }) => {
  95. await page.keyboard.press(modKey + '+a')
  96. await page.keyboard.press(modKey + '+g')
  97. await expect(page.locator('.logseq-tldraw .tl-group-container')).toHaveCount(1)
  98. })
  99. test('delete the group', async ({ page }) => {
  100. await page.keyboard.press(modKey + '+a')
  101. await page.keyboard.press('Delete')
  102. await expect(page.locator('.logseq-tldraw .tl-group-container')).toHaveCount(0)
  103. // should also delete the grouped shapes
  104. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(0)
  105. })
  106. test('undo the group deletion', async ({ page }) => {
  107. await page.keyboard.press(modKey + '+z')
  108. await expect(page.locator('.logseq-tldraw .tl-group-container')).toHaveCount(1)
  109. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
  110. })
  111. test('undo the group action', async ({ page }) => {
  112. await page.keyboard.press(modKey + '+z')
  113. await expect(page.locator('.logseq-tldraw .tl-group-container')).toHaveCount(0)
  114. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
  115. })
  116. test('connect rectangles with an arrow', async ({ page }) => {
  117. const canvas = await page.waitForSelector('.logseq-tldraw')
  118. const bounds = (await canvas.boundingBox())!
  119. await page.keyboard.type('wc')
  120. await page.mouse.move(bounds.x + 120, bounds.y + 120)
  121. await page.mouse.down()
  122. await page.mouse.move(bounds.x + 200, bounds.y + 200, {steps: 5}) // will fail without steps
  123. await page.mouse.up()
  124. await page.keyboard.press('Escape')
  125. await expect(page.locator('.logseq-tldraw .tl-line-container')).toHaveCount(1)
  126. })
  127. test('delete the first rectangle', async ({ page }) => {
  128. await page.keyboard.press('Escape', { delay: 100 })
  129. await page.keyboard.press('Escape', { delay: 100 })
  130. await page.click('.logseq-tldraw .tl-box-container:first-of-type')
  131. await page.keyboard.press('Delete')
  132. await page.waitForTimeout(200)
  133. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
  134. await expect(page.locator('.logseq-tldraw .tl-line-container')).toHaveCount(0)
  135. })
  136. test('undo the delete action', async ({ page }) => {
  137. await page.keyboard.press(modKey + '+z')
  138. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
  139. await expect(page.locator('.logseq-tldraw .tl-line-container')).toHaveCount(1)
  140. })
  141. test('convert the first rectangle to ellipse', async ({ page }) => {
  142. const canvas = await page.waitForSelector('.logseq-tldraw')
  143. const bounds = (await canvas.boundingBox())!
  144. await page.keyboard.press('Escape')
  145. await page.mouse.move(bounds.x + 220, bounds.y + 220)
  146. await page.mouse.down()
  147. await page.mouse.up()
  148. await page.mouse.move(bounds.x + 520, bounds.y + 520)
  149. await page.click('.tl-context-bar .tl-geometry-tools-pane-anchor')
  150. await page.click('.tl-context-bar .tl-geometry-toolbar [data-tool=ellipse]')
  151. await expect(page.locator('.logseq-tldraw .tl-ellipse-container')).toHaveCount(1)
  152. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
  153. })
  154. test('change the color of the ellipse', async ({ page }) => {
  155. await page.click('.tl-context-bar .tl-color-bg')
  156. await page.click('.tl-context-bar .tl-color-palette .bg-red-500')
  157. await expect(page.locator('.logseq-tldraw .tl-ellipse-container ellipse:last-of-type')).toHaveAttribute('fill', 'var(--ls-wb-background-color-red)')
  158. })
  159. test('undo the color switch', async ({ page }) => {
  160. await page.keyboard.press(modKey + '+z')
  161. await expect(page.locator('.logseq-tldraw .tl-ellipse-container ellipse:last-of-type')).toHaveAttribute('fill', 'var(--ls-wb-background-color-default)')
  162. })
  163. test('undo the shape conversion', async ({ page }) => {
  164. await page.keyboard.press(modKey + '+z')
  165. await page.waitForTimeout(100)
  166. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
  167. await expect(page.locator('.logseq-tldraw .tl-ellipse-container')).toHaveCount(0)
  168. })
  169. test('locked elements should not be removed', async ({ page }) => {
  170. const canvas = await page.waitForSelector('.logseq-tldraw')
  171. const bounds = (await canvas.boundingBox())!
  172. await page.keyboard.press('Escape')
  173. await page.mouse.move(bounds.x + 220, bounds.y + 220)
  174. await page.mouse.down()
  175. await page.mouse.up()
  176. await page.mouse.move(bounds.x + 520, bounds.y + 520)
  177. await page.keyboard.press(`${modKey}+l`, { delay: 100 })
  178. await page.keyboard.press('Delete', { delay: 100 })
  179. await page.keyboard.press(`${modKey}+Shift+l`, { delay: 100 })
  180. await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
  181. })
  182. test('move arrow to back', async ({ page }) => {
  183. await page.keyboard.press('Escape')
  184. await page.waitForTimeout(1000)
  185. await page.click('.logseq-tldraw .tl-line-container')
  186. await page.keyboard.press('Shift+[')
  187. await expect(page.locator('.logseq-tldraw .tl-canvas .tl-layer > div:first-of-type > div:first-of-type')).toHaveClass('tl-line-container')
  188. })
  189. test('move arrow to front', async ({ page }) => {
  190. await page.keyboard.press('Escape')
  191. await page.waitForTimeout(1000)
  192. await page.click('.logseq-tldraw .tl-line-container')
  193. await page.keyboard.press('Shift+]')
  194. await expect(page.locator('.logseq-tldraw .tl-canvas .tl-layer > div:first-of-type > div:first-of-type')).not.toHaveClass('tl-line-container')
  195. })
  196. test('undo the move action', async ({ page }) => {
  197. await page.keyboard.press(modKey + '+z')
  198. await page.waitForTimeout(100)
  199. await expect(page.locator('.logseq-tldraw .tl-canvas .tl-layer > div:first-of-type > div:first-of-type')).toHaveClass('tl-line-container')
  200. })
  201. test('cleanup the shapes', async ({ page }) => {
  202. await page.keyboard.press(`${modKey}+a`)
  203. await page.keyboard.press('Delete')
  204. await page.waitForTimeout(100)
  205. await expect(page.locator('[data-type=Shape]')).toHaveCount(0)
  206. })
  207. test('create a block', async ({ page }) => {
  208. const canvas = await page.waitForSelector('.logseq-tldraw')
  209. const bounds = (await canvas.boundingBox())!
  210. await page.keyboard.type('ws')
  211. await page.mouse.dblclick(bounds.x + 105, bounds.y + 105)
  212. await page.waitForTimeout(100)
  213. await page.keyboard.type('a')
  214. await page.keyboard.press('Enter')
  215. await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container')).toHaveCount(1)
  216. })
  217. // TODO: Fix the failing test
  218. test.skip('expand the block', async ({ page }) => {
  219. await page.keyboard.press('Escape')
  220. await page.keyboard.press(modKey + '+ArrowDown')
  221. await page.waitForTimeout(100)
  222. await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(1)
  223. })
  224. // TODO: Depends on the previous test
  225. test.skip('undo the expand action', async ({ page }) => {
  226. await page.keyboard.press(modKey + '+z')
  227. await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(0)
  228. })
  229. // TODO: Fix the failing test
  230. test.skip('undo the block action', async ({ page }) => {
  231. await page.keyboard.press(modKey + '+z')
  232. await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container')).toHaveCount(0)
  233. })
  234. test('copy/paste url to create an iFrame shape', async ({ page }) => {
  235. const canvas = await page.waitForSelector('.logseq-tldraw')
  236. const bounds = (await canvas.boundingBox())!
  237. await page.keyboard.type('wt')
  238. await page.mouse.move(bounds.x + 105, bounds.y + 105)
  239. await page.mouse.down()
  240. await page.waitForTimeout(100)
  241. await page.keyboard.type('https://logseq.com')
  242. await page.keyboard.press(modKey + '+a')
  243. await page.keyboard.press(modKey + '+c')
  244. await page.keyboard.press('Escape')
  245. await page.keyboard.press(modKey + '+v')
  246. await expect( page.locator('.logseq-tldraw .tl-iframe-container')).toHaveCount(1)
  247. })
  248. test('copy/paste X status url to create a Post shape', async ({ page }) => {
  249. const canvas = await page.waitForSelector('.logseq-tldraw')
  250. const bounds = (await canvas.boundingBox())!
  251. await page.keyboard.type('wt')
  252. await page.mouse.move(bounds.x + 105, bounds.y + 105)
  253. await page.mouse.down()
  254. await page.waitForTimeout(100)
  255. await page.keyboard.type('https://x.com/logseq/status/1605224589046386689')
  256. await page.keyboard.press(modKey + '+a')
  257. await page.keyboard.press(modKey + '+c')
  258. await page.keyboard.press('Escape')
  259. await page.keyboard.press(modKey + '+v')
  260. await expect( page.locator('.logseq-tldraw .tl-tweet-container')).toHaveCount(1)
  261. })
  262. test('copy/paste twitter status url to create a Tweet shape', async ({ page }) => {
  263. const canvas = await page.waitForSelector('.logseq-tldraw')
  264. const bounds = (await canvas.boundingBox())!
  265. await page.keyboard.type('wt')
  266. await page.mouse.move(bounds.x + 105, bounds.y + 105)
  267. await page.mouse.down()
  268. await page.waitForTimeout(100)
  269. await page.keyboard.type('https://twitter.com/logseq/status/1605224589046386689')
  270. await page.keyboard.press(modKey + '+a')
  271. await page.keyboard.press(modKey + '+c')
  272. await page.keyboard.press('Escape')
  273. await page.keyboard.press(modKey + '+v')
  274. await expect( page.locator('.logseq-tldraw .tl-tweet-container')).toHaveCount(2)
  275. })
  276. test('copy/paste youtube video url to create a Youtube shape', async ({ page }) => {
  277. const canvas = await page.waitForSelector('.logseq-tldraw')
  278. const bounds = (await canvas.boundingBox())!
  279. await page.keyboard.type('wt')
  280. await page.mouse.move(bounds.x + 105, bounds.y + 105)
  281. await page.mouse.down()
  282. await page.waitForTimeout(100)
  283. await page.keyboard.type('https://www.youtube.com/watch?v=hz2BacySDXE')
  284. await page.keyboard.press(modKey + '+a')
  285. await page.keyboard.press(modKey + '+c')
  286. await page.keyboard.press('Escape')
  287. await page.keyboard.press(modKey + '+v')
  288. await expect(page.locator('.logseq-tldraw .tl-youtube-container')).toHaveCount(1)
  289. })
  290. test('zoom in', async ({ page }) => {
  291. await page.keyboard.press('Shift+0') // reset zoom
  292. await page.waitForTimeout(1500) // wait for the zoom animation to finish
  293. await page.keyboard.press('Shift+=')
  294. await page.waitForTimeout(1500) // wait for the zoom animation to finish
  295. await expect(page.locator('#tl-zoom')).toContainText('125%')
  296. })
  297. test('zoom out', async ({ page }) => {
  298. await page.keyboard.press('Shift+0')
  299. await page.waitForTimeout(1500) // wait for the zoom animation to finish
  300. await page.keyboard.press('Shift+-')
  301. await page.waitForTimeout(1500) // wait for the zoom animation to finish
  302. await expect(page.locator('#tl-zoom')).toContainText('80%')
  303. })
  304. test('open context menu', async ({ page }) => {
  305. await page.locator('.logseq-tldraw').click({ button: 'right' })
  306. await expect(page.locator('.tl-context-menu')).toBeVisible()
  307. })
  308. test('close context menu on esc', async ({ page }) => {
  309. await page.keyboard.press('Escape')
  310. await expect(page.locator('.tl-context-menu')).toBeHidden()
  311. })
  312. test('quick add another whiteboard', async ({ page }) => {
  313. // create a new board first
  314. await page.click('.nav-header .whiteboard')
  315. await page.click('#tl-create-whiteboard')
  316. await page.click('.whiteboard-page-title')
  317. await page.fill('.whiteboard-page-title input', 'my-whiteboard-3')
  318. await page.keyboard.press('Enter')
  319. await page.waitForTimeout(300)
  320. const canvas = await page.waitForSelector('.logseq-tldraw')
  321. await canvas.dblclick({
  322. position: {
  323. x: 200,
  324. y: 200,
  325. },
  326. })
  327. const quickAdd$ = page.locator('.tl-quick-search')
  328. await expect(quickAdd$).toBeVisible()
  329. await page.fill('.tl-quick-search input', 'my-whiteboard')
  330. await quickAdd$
  331. .locator('.tl-quick-search-option >> text=my-whiteboard-2')
  332. .first()
  333. .click()
  334. await expect(quickAdd$).toBeHidden()
  335. await expect(
  336. page.locator('.tl-logseq-portal-container >> text=my-whiteboard-2')
  337. ).toBeVisible()
  338. })
  339. test('go to another board and check reference', async ({ page }) => {
  340. await page
  341. .locator('.tl-logseq-portal-container >> text=my-whiteboard-2')
  342. .click()
  343. await expect(page.locator('.whiteboard-page-title .title')).toContainText(
  344. 'my-whiteboard-2'
  345. )
  346. const pageRefCount$ = page.locator('.whiteboard-page-refs-count')
  347. await expect(pageRefCount$.locator('.open-page-ref-link')).toContainText('1')
  348. })
  349. test('Create an embedded whiteboard', async ({ page }) => {
  350. const canvas = await page.waitForSelector('.logseq-tldraw')
  351. await canvas.dblclick({
  352. position: {
  353. x: 110,
  354. y: 110,
  355. },
  356. })
  357. const quickAdd$ = page.locator('.tl-quick-search')
  358. await expect(quickAdd$).toBeVisible()
  359. await page.fill('.tl-quick-search input', 'My embedded whiteboard')
  360. await quickAdd$
  361. .locator('div[data-index="2"] .tl-quick-search-option')
  362. .first()
  363. .click()
  364. await expect(quickAdd$).toBeHidden()
  365. await expect(page.locator('.tl-logseq-portal-header a')).toContainText('My embedded whiteboard')
  366. })
  367. test('New whiteboard should have the correct name', async ({ page }) => {
  368. page.locator('.tl-logseq-portal-header a').click()
  369. await expect(page.locator('.whiteboard-page-title')).toContainText('My embedded whiteboard')
  370. })
  371. test('Create an embedded page', async ({ page }) => {
  372. const canvas = await page.waitForSelector('.logseq-tldraw')
  373. await canvas.dblclick({
  374. position: {
  375. x: 150,
  376. y: 150,
  377. },
  378. })
  379. const quickAdd$ = page.locator('.tl-quick-search')
  380. await expect(quickAdd$).toBeVisible()
  381. await page.fill('.tl-quick-search input', 'My page')
  382. await quickAdd$
  383. .locator('div[data-index="1"] .tl-quick-search-option')
  384. .first()
  385. .click()
  386. await expect(quickAdd$).toBeHidden()
  387. await expect(page.locator('.tl-logseq-portal-header a')).toContainText('My page')
  388. })
  389. test('New page should have the correct name', async ({ page }) => {
  390. page.locator('.tl-logseq-portal-header a').click()
  391. await expect(page.locator('.ls-page-title')).toContainText('My page')
  392. })
  393. test('Renaming a page to an existing whiteboard name should be prohibited', async ({ page }) => {
  394. await renamePage(page, "My embedded whiteboard")
  395. await expect(page.locator('.page-title input')).toHaveValue('My page')
  396. })