Jelajahi Sumber

fix(test): refactor and fix all e2e tests

Andelf 3 tahun lalu
induk
melakukan
f1af5344b2

+ 119 - 136
e2e-tests/basic.spec.ts

@@ -1,9 +1,8 @@
 import { expect } from '@playwright/test'
-import * as fs from 'fs'
-import * as path from 'path'
-import { test, graphDir } from './fixtures'
-import { randomString, createRandomPage, newBlock } from './utils'
-
+import fs from 'fs/promises'
+import path from 'path'
+import { test } from './fixtures'
+import { randomString, createRandomPage, newBlock, enterNextBlock } from './utils'
 
 test('render app', async ({ page }) => {
   // NOTE: part of app startup tests is moved to `fixtures.ts`.
@@ -18,20 +17,20 @@ test('toggle sidebar', async ({ page }) => {
   // Left sidebar is toggled by `is-open` class
   if (/is-open/.test(await sidebar.getAttribute('class'))) {
     await page.click('#left-menu.button')
-    expect(await sidebar.getAttribute('class')).not.toMatch(/is-open/)
+    await expect(sidebar).not.toHaveClass(/is-open/)
   } else {
     await page.click('#left-menu.button')
     await page.waitForTimeout(10)
-    expect(await sidebar.getAttribute('class')).toMatch(/is-open/)
+    await expect(sidebar).toHaveClass(/is-open/)
     await page.click('#left-menu.button')
     await page.waitForTimeout(10)
-    expect(await sidebar.getAttribute('class')).not.toMatch(/is-open/)
+    await expect(sidebar).not.toHaveClass(/is-open/)
   }
 
   await page.click('#left-menu.button')
 
   await page.waitForTimeout(10)
-  expect(await sidebar.getAttribute('class')).toMatch(/is-open/)
+  await expect(sidebar).toHaveClass(/is-open/)
   await page.waitForSelector('#left-sidebar .left-sidebar-inner', { state: 'visible' })
   await page.waitForSelector('#left-sidebar a:has-text("New page")', { state: 'visible' })
 })
@@ -46,90 +45,86 @@ test('search', async ({ page }) => {
   expect(results.length).toBeGreaterThanOrEqual(1)
 })
 
-test('create page and blocks', async ({ page }) => {
+test('create page and blocks, save to disk', async ({ page, graphDir }) => {
   const pageTitle = await createRandomPage(page)
 
   // do editing
-  await page.fill(':nth-match(textarea, 1)', 'this is my first bullet')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-
-  await page.waitForTimeout(10)
+  await page.fill('textarea >> nth=0', 'this is my first bullet')
+  await enterNextBlock(page)
 
-  // first block
-  expect(await page.$$('.block-content')).toHaveLength(1)
+  // wait first block
+  await page.waitForSelector('.ls-block >> nth=0')
 
-  await page.fill(':nth-match(textarea, 1)', 'this is my second bullet')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill('textarea >> nth=0', 'this is my second bullet')
+  await enterNextBlock(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'this is my third bullet')
-  await page.press(':nth-match(textarea, 1)', 'Tab')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill('textarea >> nth=0', 'this is my third bullet')
+  await page.press('textarea >> nth=0', 'Tab')
+  await enterNextBlock(page)
 
   await page.keyboard.type('continue editing test')
   await page.keyboard.press('Shift+Enter')
   await page.keyboard.type('continue')
 
-  await page.keyboard.press('Enter')
+  await enterNextBlock(page)
   await page.keyboard.press('Shift+Tab')
   await page.keyboard.press('Shift+Tab')
   await page.keyboard.type('test ok')
   await page.keyboard.press('Escape')
 
-  // const blocks = await page.$$('.ls-block')
-  // expect(blocks).toHaveLength(5)
+  // NOTE: nth= counts from 0, so here're 5 blocks
+  await page.waitForSelector('.ls-block >> nth=4')
 
-  // // active edit
-  // await page.click('.ls-block >> nth=-1')
-  // await page.press('textarea >> nth=0', 'Enter')
-  // await page.fill('textarea >> nth=0', 'test')
-  // for (let i = 0; i < 5; i++) {
-  //   await page.keyboard.press('Backspace')
-  // }
+  // active edit
+  await page.click('.ls-block >> nth=-1')
+  await enterNextBlock(page)
+  await page.fill('textarea >> nth=0', 'test')
+  for (let i = 0; i < 5; i++) {
+    await page.keyboard.press('Backspace')
+  }
 
-  // await page.keyboard.press('Escape')
-  // await page.waitForTimeout(500)
-  // expect(await page.$$('.ls-block')).toHaveLength(5)
+  await page.keyboard.press('Escape')
+  await page.waitForSelector('.ls-block >> nth=4') // 5 blocks
 
-  await page.waitForTimeout(1000)
+  await page.waitForTimeout(2000) // wait for saving to disk
 
   const contentOnDisk = fs.readFileSync(
     path.join(graphDir, `pages/${pageTitle}.md`),
     'utf8'
   )
-
   expect(contentOnDisk.trim()).toEqual(`
 - this is my first bullet
 - this is my second bullet
-        - this is my third bullet
-        - continue editing test
-          continue
+\t- this is my third bullet
+\t- continue editing test
+\tcontinue
 - test ok`.trim())
 })
 
 test('delete and backspace', async ({ page }) => {
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'test')
+  await page.fill('textarea >> nth=0', 'test')
 
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('test')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('test')
 
   // backspace
   await page.keyboard.press('Backspace')
   await page.keyboard.press('Backspace')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('te')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('te')
 
   // refill
-  await page.fill(':nth-match(textarea, 1)', 'test')
+  await page.fill('textarea >> nth=0', 'test')
   await page.keyboard.press('ArrowLeft')
   await page.keyboard.press('ArrowLeft')
 
   // delete
   await page.keyboard.press('Delete')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('tet')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('tet')
   await page.keyboard.press('Delete')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('te')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('te')
   await page.keyboard.press('Delete')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('te')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('te')
 
   // TODO: test delete & backspace across blocks
 })
@@ -138,28 +133,30 @@ test('delete and backspace', async ({ page }) => {
 test('selection', async ({ page }) => {
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'line 1')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.fill(':nth-match(textarea, 1)', 'line 2')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.press(':nth-match(textarea, 1)', 'Tab')
-  await page.fill(':nth-match(textarea, 1)', 'line 3')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.fill(':nth-match(textarea, 1)', 'line 4')
-  await page.press(':nth-match(textarea, 1)', 'Tab')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.fill(':nth-match(textarea, 1)', 'line 5')
-
+  // add 5 blocks
+  await page.fill('textarea >> nth=0', 'line 1')
+  await enterNextBlock(page)
+  await page.fill('textarea >> nth=0', 'line 2')
+  await enterNextBlock(page)
+  await page.press('textarea >> nth=0', 'Tab')
+  await page.fill('textarea >> nth=0', 'line 3')
+  await enterNextBlock(page)
+  await page.fill('textarea >> nth=0', 'line 4')
+  await page.press('textarea >> nth=0', 'Tab')
+  await enterNextBlock(page)
+  await page.fill('textarea >> nth=0', 'line 5')
+
+  // shift+up select 3 blocks
   await page.keyboard.down('Shift')
   await page.keyboard.press('ArrowUp')
   await page.keyboard.press('ArrowUp')
   await page.keyboard.press('ArrowUp')
   await page.keyboard.up('Shift')
 
-  await page.waitForTimeout(500)
+  await page.waitForSelector('.ls-block.selected >> nth=2') // 3 selected
   await page.keyboard.press('Backspace')
 
-  expect(await page.$$('.ls-block')).toHaveLength(2)
+  await page.waitForSelector('.ls-block >> nth=1') // 2 blocks
 })
 
 test('template', async ({ page }) => {
@@ -167,140 +164,126 @@ test('template', async ({ page }) => {
 
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'template')
-  await page.press(':nth-match(textarea, 1)', 'Shift+Enter')
-  await page.type(':nth-match(textarea, 1)', 'template:: ' + randomTemplate)
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill('textarea >> nth=0', 'template')
+  await page.press('textarea >> nth=0', 'Shift+Enter')
+  await page.type('textarea >> nth=0', 'template:: ' + randomTemplate)
 
-  await page.press(':nth-match(textarea, 1)', 'Tab')
-  await page.fill(':nth-match(textarea, 1)', 'line1')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.fill(':nth-match(textarea, 1)', 'line2')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.press(':nth-match(textarea, 1)', 'Tab')
-  await page.fill(':nth-match(textarea, 1)', 'line3')
+  // Enter twice to exit from property block
+  await page.press('textarea >> nth=0', 'Enter')
+  await enterNextBlock(page)
 
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.press('textarea >> nth=0', 'Tab')
+  await page.fill('textarea >> nth=0', 'line1')
+  await enterNextBlock(page)
+  await page.fill('textarea >> nth=0', 'line2')
+  await enterNextBlock(page)
+  await page.press('textarea >> nth=0', 'Tab')
+  await page.fill('textarea >> nth=0', 'line3')
 
+  await enterNextBlock(page)
+  await page.press('textarea >> nth=0', 'Shift+Tab')
+  await page.press('textarea >> nth=0', 'Shift+Tab')
+  await page.press('textarea >> nth=0', 'Shift+Tab')
 
-  expect(await page.$$('.ls-block')).toHaveLength(5)
+  await page.waitForSelector('.ls-block >> nth=3') // total 4 blocks
 
-  await page.type(':nth-match(textarea, 1)', '/template')
+  // NOTE: use delay to type slower, to trigger auto-completion UI.
+  await page.type('textarea >> nth=0', '/template', { delay: 100 })
 
   await page.click('[title="Insert a created template here"]')
   // type to search template name
-  await page.keyboard.type(randomTemplate.substring(0, 3))
+  await page.keyboard.type(randomTemplate.substring(0, 3), { delay: 100 })
+  await page.waitForTimeout(500) // wait for template search
   await page.click('.absolute >> text=' + randomTemplate)
 
-  await page.waitForTimeout(500)
 
-  expect(await page.$$('.ls-block')).toHaveLength(8)
+  await page.waitForSelector('.ls-block >> nth=7') // 8 blocks
 })
 
 test('auto completion square brackets', async ({ page }) => {
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'Auto-completion test')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-
   // [[]]
-  await page.type(':nth-match(textarea, 1)', 'This is a [')
-  await page.inputValue(':nth-match(textarea, 1)').then(text => {
+  await page.type('textarea >> nth=0', 'This is a [')
+  await page.inputValue('textarea >> nth=0').then(text => {
     expect(text).toBe('This is a []')
   })
-  await page.type(':nth-match(textarea, 1)', '[')
+  await page.waitForTimeout(100)
+  await page.type('textarea >> nth=0', '[')
   // wait for search popup
   await page.waitForSelector('text="Search for a page"')
 
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('This is a [[]]')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('This is a [[]]')
 
   // re-enter edit mode
-  await page.press(':nth-match(textarea, 1)', 'Escape')
+  await page.press('textarea >> nth=0', 'Escape')
   await page.click('.ls-block >> nth=-1')
-  await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
+  await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
 
   // #3253
-  await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
-  await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.press('textarea >> nth=0', 'ArrowLeft')
+  await page.press('textarea >> nth=0', 'ArrowLeft')
+  await page.press('textarea >> nth=0', 'Enter')
   await page.waitForSelector('text="Search for a page"', { state: 'visible' })
 
   // type more `]`s
-  await page.type(':nth-match(textarea, 1)', ']')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('This is a [[]]')
-  await page.type(':nth-match(textarea, 1)', ']')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('This is a [[]]')
-  await page.type(':nth-match(textarea, 1)', ']')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('This is a [[]]]')
+  await page.type('textarea >> nth=0', ']')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('This is a [[]]')
+  await page.type('textarea >> nth=0', ']')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('This is a [[]]')
+  await page.type('textarea >> nth=0', ']')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('This is a [[]]]')
 })
 
 test('auto completion and auto pair', async ({ page }) => {
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'Auto-completion test')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill('textarea >> nth=0', 'Auto-completion test')
+  await enterNextBlock(page)
 
   // {{
-  await page.type(':nth-match(textarea, 1)', 'type {{')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type {{}}')
+  await page.type('textarea >> nth=0', 'type {{')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('type {{}}')
 
   // ((
   await newBlock(page)
 
-  await page.type(':nth-match(textarea, 1)', 'type (')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type ()')
-  await page.type(':nth-match(textarea, 1)', '(')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type (())')
+  await page.type('textarea >> nth=0', 'type (')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('type ()')
+  await page.type('textarea >> nth=0', '(')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('type (())')
 
   // 99  #3444
   // TODO: Test under different keyboard layout when Playwright supports it
   // await newBlock(page)
 
-  // await page.type(':nth-match(textarea, 1)', 'type 9')
-  // expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type 9')
-  // await page.type(':nth-match(textarea, 1)', '9')
-  // expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type 99')
+  // await page.type('textarea >> nth=0', 'type 9')
+  // expect(await page.inputValue('textarea >> nth=0')).toBe('type 9')
+  // await page.type('textarea >> nth=0', '9')
+  // expect(await page.inputValue('textarea >> nth=0')).toBe('type 99')
 
   // [[  #3251
   await newBlock(page)
 
-  await page.type(':nth-match(textarea, 1)', 'type [')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type []')
-  await page.type(':nth-match(textarea, 1)', '[')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type [[]]')
+  await page.type('textarea >> nth=0', 'type [')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('type []')
+  await page.type('textarea >> nth=0', '[')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('type [[]]')
 
   // ``
   await newBlock(page)
 
-  await page.type(':nth-match(textarea, 1)', 'type `')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type ``')
-  await page.type(':nth-match(textarea, 1)', 'code here')
+  await page.type('textarea >> nth=0', 'type `')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('type ``')
+  await page.type('textarea >> nth=0', 'code here')
 
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('type `code here`')
-})
-
-
-// FIXME: Electron with filechooser is not working
-test.skip('open directory', async ({ page }) => {
-  await page.click('#left-sidebar >> text=Journals')
-  await page.waitForSelector('strong:has-text("Choose a folder")')
-  await page.click('strong:has-text("Choose a folder")')
-
-  // await page.waitForEvent('filechooser')
-  await page.keyboard.press('Escape')
-
-  await page.click('#left-sidebar >> text=Journals')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('type `code here`')
 })
 
 test('invalid page props #3944', async ({ page }) => {
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'public:: true\nsize:: 65535')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-
-  await page.waitForTimeout(1000)
+  await page.fill('textarea >> nth=0', 'public:: true\nsize:: 65535')
+  await page.press('textarea >> nth=0', 'Enter')
+  await enterNextBlock(page)
 })

+ 3 - 3
e2e-tests/code-editing.spec.ts

@@ -19,7 +19,7 @@ test('switch code editing mode', async ({ page }) => {
   // NOTE: multiple textarea elements are existed in the editor, be careful to select the right one
 
   // code block with 0 line
-  await page.type(':nth-match(textarea, 1)', '```clojure\n')
+  await page.type('textarea >> nth=0', '```clojure\n')
   // line number: 1
   await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
   expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber').innerText()).toBe('1')
@@ -28,10 +28,10 @@ test('switch code editing mode', async ({ page }) => {
 
   await page.press('.CodeMirror textarea', 'Escape')
   await page.waitForSelector('.CodeMirror pre', { state: 'hidden' })
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('```clojure\n```')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('```clojure\n```')
 
   await page.waitForTimeout(200)
-  await page.press(':nth-match(textarea, 1)', 'Escape')
+  await page.press('textarea >> nth=0', 'Escape')
   await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
 
   // NOTE: must wait here, await loading of CodeMirror editor

+ 28 - 22
e2e-tests/dnd.spec.ts

@@ -1,6 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { createRandomPage } from './utils'
+import { createRandomPage, enterNextBlock } from './utils'
 
 /**
  * Drag and Drop tests.
@@ -11,14 +11,14 @@ import { createRandomPage } from './utils'
 test('drop to left center', async ({ page }) => {
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'block a')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill('textarea >> nth=0', 'block a')
+  await enterNextBlock(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'block b')
-  await page.press(':nth-match(textarea, 1)', 'Escape')
+  await page.fill('textarea >> nth=0', 'block b')
+  await page.press('textarea >> nth=0', 'Escape')
 
   const bullet = page.locator('span.bullet-container >> nth=-1')
-  const where = page.locator('div.ls-block >> nth=0')
+  const where = page.locator('.ls-block >> nth=0')
   await bullet.dragTo(where, {
     targetPosition: {
       x: 30,
@@ -26,22 +26,24 @@ test('drop to left center', async ({ page }) => {
     }
   })
 
-  expect.soft(await page.locator('div.ls-block >> nth=0').innerText()).toBe("block b")
-  expect.soft(await page.locator('div.ls-block >> nth=1').innerText()).toBe("block a")
+  await page.keyboard.press('Escape')
+
+  const pageElem = page.locator('.page-blocks-inner')
+  await expect(pageElem).toHaveText('block b\nblock a', {useInnerText: true})
 })
 
 
 test('drop to upper left', async ({ page }) => {
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'block a')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill('textarea >> nth=0', 'block a')
+  await enterNextBlock(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'block b')
-  await page.press(':nth-match(textarea, 1)', 'Escape')
+  await page.fill('textarea >> nth=0', 'block b')
+  await page.press('textarea >> nth=0', 'Escape')
 
   const bullet = page.locator('span.bullet-container >> nth=-1')
-  const where = page.locator('div.ls-block >> nth=0')
+  const where = page.locator('.ls-block >> nth=0')
   await bullet.dragTo(where, {
     targetPosition: {
       x: 30,
@@ -49,21 +51,23 @@ test('drop to upper left', async ({ page }) => {
     }
   })
 
-  expect.soft(await page.locator('div.ls-block >> nth=0').innerText()).toBe("block b")
-  expect.soft(await page.locator('div.ls-block >> nth=1').innerText()).toBe("block a")
+  await page.keyboard.press('Escape')
+
+  const pageElem = page.locator('.page-blocks-inner')
+  await expect(pageElem).toHaveText('block b\nblock a', {useInnerText: true})
 })
 
 test('drop to bottom left', async ({ page }) => {
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'block a')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await page.fill('textarea >> nth=0', 'block a')
+  await enterNextBlock(page)
 
-  await page.fill(':nth-match(textarea, 1)', 'block b')
-  await page.press(':nth-match(textarea, 1)', 'Escape')
+  await page.fill('textarea >> nth=0', 'block b')
+  await page.press('textarea >> nth=0', 'Escape')
 
   const bullet = page.locator('span.bullet-container >> nth=-1')
-  const where = page.locator('div.ls-block >> nth=0')
+  const where = page.locator('.ls-block >> nth=0')
   await bullet.dragTo(where, {
     targetPosition: {
       x: 30,
@@ -71,6 +75,8 @@ test('drop to bottom left', async ({ page }) => {
     }
   })
 
-  expect.soft(await page.locator('div.ls-block >> nth=0').innerText()).toBe("block a")
-  expect.soft(await page.locator('div.ls-block >> nth=1').innerText()).toBe("block b")
+  await page.keyboard.press('Escape')
+
+  const pageElem = page.locator('.page-blocks-inner')
+  await expect(pageElem).toHaveText('block a\nblock b', {useInnerText: true})
 })

+ 15 - 15
e2e-tests/editor.spec.ts

@@ -1,6 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { createRandomPage } from './utils'
+import { createRandomPage, enterNextBlock, IsMac } from './utils'
 import { dispatch_kb_events } from './util/keyboard-events'
 import * as kb_events from './util/keyboard-events'
 
@@ -43,23 +43,24 @@ test(
 test('hashtag and quare brackets in same line #4178', async ({ page }) => {
   await createRandomPage(page)
 
-  await page.type(':nth-match(textarea, 1)', '#foo bar')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.type(':nth-match(textarea, 1)', 'bar [[blah]]')
+  await page.type('textarea >> nth=0', '#foo bar')
+  await enterNextBlock(page)
+  await page.type('textarea >> nth=0', 'bar [[blah]]', { delay: 100})
+
   for (let i = 0; i < 12; i++) {
-    await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
+    await page.press('textarea >> nth=0', 'ArrowLeft')
   }
-  await page.type(':nth-match(textarea, 1)', ' ')
-  await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
+  await page.type('textarea >> nth=0', ' ')
+  await page.press('textarea >> nth=0', 'ArrowLeft')
 
-  await page.type(':nth-match(textarea, 1)', '#')
+  await page.type('textarea >> nth=0', '#')
   await page.waitForSelector('text="Search for a page"', { state: 'visible' })
 
-  await page.type(':nth-match(textarea, 1)', 'fo')
+  await page.type('textarea >> nth=0', 'fo')
 
   await page.click('.absolute >> text=' + 'foo')
 
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(
+  expect(await page.inputValue('textarea >> nth=0')).toBe(
     '#foo bar [[blah]]'
   )
 })
@@ -68,7 +69,7 @@ test('hashtag and quare brackets in same line #4178', async ({ page }) => {
 // test('copy & paste block ref and replace its content', async ({ page }) => {
 //   await createRandomPage(page)
 
-//   await page.type(':nth-match(textarea, 1)', 'Some random text')
+//   await page.type('textarea >> nth=0', 'Some random text')
 //   if (IsMac) {
 //     await page.keyboard.press('Meta+c')
 //   } else {
@@ -77,7 +78,7 @@ test('hashtag and quare brackets in same line #4178', async ({ page }) => {
 
 //   await page.pause()
 
-//   await page.press(':nth-match(textarea, 1)', 'Enter')
+//   await page.press('textarea >> nth=0', 'Enter')
 //   if (IsMac) {
 //     await page.keyboard.press('Meta+v')
 //   } else {
@@ -95,7 +96,7 @@ test('hashtag and quare brackets in same line #4178', async ({ page }) => {
 
 //   // Move cursor into the block ref
 //   for (let i = 0; i < 4; i++) {
-//     await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
+//     await page.press('textarea >> nth=0', 'ArrowLeft')
 //   }
 
 //   // Trigger replace-block-reference-with-content-at-point
@@ -103,6 +104,5 @@ test('hashtag and quare brackets in same line #4178', async ({ page }) => {
 //     await page.keyboard.press('Meta+Shift+r')
 //   } else {
 //     await page.keyboard.press('Control+Shift+v')
-//   }  
+//   }
 // })
-  

+ 10 - 6
e2e-tests/fixtures.ts

@@ -38,6 +38,7 @@ base.beforeAll(async () => {
     return
   }
 
+  console.log(`Creating test graph directory: ${graphDir}`)
   fs.mkdirSync(graphDir, {
     recursive: true,
   });
@@ -66,15 +67,15 @@ base.beforeAll(async () => {
   // Direct Electron console to watcher
   page.on('console', consoleLogWatcher)
   page.on('crash', () => {
-    expect('page must not crash!').toBe('page crashed')
+    expect(false, "Page must not crash").toBeTruthy()
   })
   page.on('pageerror', (err) => {
-    console.error("[pageerror]", err)
-    expect('page must not have errors!').toBe('page has some error')
+    console.log(err)
+    expect(false, 'Page must not have errors!').toBeTruthy()
   })
 
   await page.waitForLoadState('domcontentloaded')
-  await page.waitForFunction('window.document.title != "Loading"')
+  // await page.waitForFunction(() => window.document.title != "Loading")
   // NOTE: The following ensures first start.
   // await page.waitForSelector('text=This is a demo graph, changes will not be saved until you open a local folder')
 
@@ -106,7 +107,7 @@ base.afterAll(async () => {
 })
 
 // hijack electron app into the test context
-export const test = base.extend<{ page: Page, context: BrowserContext, app: ElectronApplication }>({
+export const test = base.extend<{ page: Page, context: BrowserContext, app: ElectronApplication, graphDir: string }>({
   page: async ({ }, use) => {
     await use(page);
   },
@@ -115,5 +116,8 @@ export const test = base.extend<{ page: Page, context: BrowserContext, app: Elec
   },
   app: async ({ }, use) => {
     await use(electronApp);
-  }
+  },
+  graphDir: async ({ }, use) => {
+    await use(graphDir);
+  },
 });

+ 18 - 18
e2e-tests/hotkey.spec.ts

@@ -8,8 +8,7 @@ test('open search dialog', async ({ page }) => {
   } else if (IsLinux) {
     await page.keyboard.press('Control+k')
   } else {
-    // TODO: test on Windows and other platforms
-    expect(false)
+    expect(false, "TODO: test on Windows and other platforms").toBeTruthy()
   }
 
   await page.waitForSelector('[placeholder="Search or create page"]')
@@ -30,26 +29,27 @@ test('insert link', async ({ page }) => {
 
   // Case 1: empty link
   await lastBlock(page)
-  await page.press(':nth-match(textarea, 1)', hotKey)
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[]()')
-  await page.type(':nth-match(textarea, 1)', 'Logseq Website')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[Logseq Website]()')
+  await page.press('textarea >> nth=0', hotKey)
+  expect(await page.inputValue('textarea >> nth=0')).toBe('[]()')
+  await page.type('textarea >> nth=0', 'Logseq Website')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('[Logseq Website]()')
+  await page.fill('textarea >> nth=0', '[Logseq Website](https://logseq.com)')
 
   // Case 2: link with label
   await newBlock(page)
-  await page.type(':nth-match(textarea, 1)', 'Logseq')
-  await page.press(':nth-match(textarea, 1)', selectAll)
-  await page.press(':nth-match(textarea, 1)', hotKey)
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[Logseq]()')
-  await page.type(':nth-match(textarea, 1)', 'https://logseq.com/')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[Logseq](https://logseq.com/)')
+  await page.type('textarea >> nth=0', 'Logseq')
+  await page.press('textarea >> nth=0', selectAll)
+  await page.press('textarea >> nth=0', hotKey)
+  expect(await page.inputValue('textarea >> nth=0')).toBe('[Logseq]()')
+  await page.type('textarea >> nth=0', 'https://logseq.com/')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('[Logseq](https://logseq.com/)')
 
   // Case 3: link with URL
   await newBlock(page)
-  await page.type(':nth-match(textarea, 1)', 'https://logseq.com/')
-  await page.press(':nth-match(textarea, 1)', selectAll)
-  await page.press(':nth-match(textarea, 1)', hotKey)
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[](https://logseq.com/)')
-  await page.type(':nth-match(textarea, 1)', 'Logseq')
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe('[Logseq](https://logseq.com/)')
+  await page.type('textarea >> nth=0', 'https://logseq.com/')
+  await page.press('textarea >> nth=0', selectAll)
+  await page.press('textarea >> nth=0', hotKey)
+  expect(await page.inputValue('textarea >> nth=0')).toBe('[](https://logseq.com/)')
+  await page.type('textarea >> nth=0', 'Logseq')
+  expect(await page.inputValue('textarea >> nth=0')).toBe('[Logseq](https://logseq.com/)')
 })

+ 5 - 3
e2e-tests/page-rename.spec.ts

@@ -1,6 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { IsMac, createPage, newBlock, newInnerBlock, randomString, lastInnerBlock } from './utils'
+import { IsMac, createPage, newBlock, newInnerBlock, randomString, lastBlock } from './utils'
 
 /***
  * Test rename feature
@@ -18,7 +18,9 @@ async function page_rename_test(page, original_page_name: string, new_page_name:
 
   await createPage(page, original_name)
   await page.click('.page-title .title')
+  await page.waitForSelector('input[type="text"]')
   await page.keyboard.press(selectAll)
+  await page.keyboard.press('Backspace')
   await page.type('.title input', new_name)
   await page.keyboard.press('Enter')
   await page.click('.ui__confirm-modal button')
@@ -26,11 +28,11 @@ async function page_rename_test(page, original_page_name: string, new_page_name:
   expect(await page.innerText('.page-title .title')).toBe(new_name)
 
   // TODO: Test if page is renamed in re-entrance
-  
+
   // TODO: Test if page is hierarchy
 }
 
 test('page rename test', async ({ page }) => {
   await page_rename_test(page, "abcd", "a.b.c.d")
   await page_rename_test(page, "abcd", "a/b/c/d")
-})
+})

+ 29 - 28
e2e-tests/page-search.spec.ts

@@ -1,6 +1,6 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { IsMac, createRandomPage, newBlock, newInnerBlock, randomString, lastInnerBlock, activateNewPage } from './utils'
+import { IsMac, createRandomPage, newBlock, newInnerBlock, randomString, lastBlock, activateNewPage, enterNextBlock } from './utils'
 
 /***
  * Test alias features
@@ -21,15 +21,15 @@ import { IsMac, createRandomPage, newBlock, newInnerBlock, randomString, lastInn
   // diacritic opening test
   await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', '[[Einführung in die Allgemeine Sprachwissenschaft' + rand + ']] diacritic-block-1')
+  await page.fill('textarea >> nth=0', '[[Einführung in die Allgemeine Sprachwissenschaft' + rand + ']] diacritic-block-1')
   await page.keyboard.press(hotkeyOpenLink)
 
   // build target Page with diacritics
   await activateNewPage(page)
-  await page.type(':nth-match(textarea, 1)', 'Diacritic title test content')
+  await page.type('textarea >> nth=0', 'Diacritic title test content')
 
   await page.keyboard.press('Enter')
-  await page.fill(':nth-match(textarea, 1)', '[[Einführung in die Allgemeine Sprachwissenschaft' + rand + ']] diacritic-block-2')
+  await page.fill('textarea >> nth=0', '[[Einführung in die Allgemeine Sprachwissenschaft' + rand + ']] diacritic-block-2')
   await page.keyboard.press(hotkeyBack)
 
   // check if diacritics are indexed
@@ -61,45 +61,46 @@ async function alias_test(page, page_name: string, search_kws: string[]) {
   // shortcut opening test
   let parent_title = await createRandomPage(page)
 
-  await page.fill(':nth-match(textarea, 1)', '[[' + target_name + ']]')
+  await page.fill('textarea >> nth=0', '[[' + target_name + ']]')
   await page.keyboard.press(hotkeyOpenLink)
 
+  await lastBlock(page)
   // build target Page with alias
-  await page.type(':nth-match(textarea, 1)', 'alias:: [[' + alias_name + ']]')
-  await page.press(':nth-match(textarea, 1)', 'Enter') // double Enter for exit property editing
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await page.type(':nth-match(textarea, 1)', alias_test_content_1)
+  await page.fill('textarea >> nth=0', 'alias:: [[' + alias_name + ']]')
+  await page.press('textarea >> nth=0', 'Enter') // double Enter for exit property editing
+  await enterNextBlock(page)
+  await page.type('textarea >> nth=0', alias_test_content_1)
   await page.keyboard.press(hotkeyBack)
 
   // create alias ref in origin Page
   await newBlock(page)
-  await page.type(':nth-match(textarea, 1)', '[[' + alias_name + ']]')
+  await page.fill('textarea >> nth=0', '[[' + alias_name + ']]')
   await page.keyboard.press(hotkeyOpenLink)
 
   // shortcut opening test
-  await lastInnerBlock(page)
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(alias_test_content_1)
+  await lastBlock(page)
+  expect(await page.inputValue('textarea >> nth=0')).toBe(alias_test_content_1)
   await newInnerBlock(page)
-  await page.type(':nth-match(textarea, 1)', alias_test_content_2)
+  await page.type('textarea >> nth=0', alias_test_content_2)
   await page.keyboard.press(hotkeyBack)
 
   // pressing enter opening test
-  await lastInnerBlock(page)
-  await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
-  await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
-  await page.press(':nth-match(textarea, 1)', 'ArrowLeft')
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-  await lastInnerBlock(page)
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(alias_test_content_2)
+  await lastBlock(page)
+  await page.press('textarea >> nth=0', 'ArrowLeft')
+  await page.press('textarea >> nth=0', 'ArrowLeft')
+  await page.press('textarea >> nth=0', 'ArrowLeft')
+  await page.press('textarea >> nth=0', 'Enter')
+  await lastBlock(page)
+  expect(await page.inputValue('textarea >> nth=0')).toBe(alias_test_content_2)
   await newInnerBlock(page)
-  await page.type(':nth-match(textarea, 1)', alias_test_content_3)
+  await page.type('textarea >> nth=0', alias_test_content_3)
   await page.keyboard.press(hotkeyBack)
 
   // clicking opening test
   await page.waitForSelector('.page-blocks-inner .ls-block .page-ref >> nth=-1')
   await page.click('.page-blocks-inner .ls-block .page-ref >> nth=-1')
-  await lastInnerBlock(page)
-  expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(alias_test_content_3)
+  await lastBlock(page)
+  expect(await page.inputValue('textarea >> nth=0')).toBe(alias_test_content_3)
 
   // TODO: test alias from graph clicking
 
@@ -127,8 +128,8 @@ async function alias_test(page, page_name: string, search_kws: string[]) {
     page.keyboard.press("Enter")
     await page.waitForNavigation()
     await page.waitForTimeout(100)
-    await lastInnerBlock(page)
-    expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(alias_test_content_3)
+    await lastBlock(page)
+    expect(await page.inputValue('textarea >> nth=0')).toBe(alias_test_content_3)
 
     // test search clicking (block)
     await page.click('#search-button')
@@ -138,8 +139,8 @@ async function alias_test(page, page_name: string, search_kws: string[]) {
     page.click(":nth-match(.menu-link, 2)")
     await page.waitForNavigation()
     await page.waitForTimeout(500)
-    await lastInnerBlock(page)
-    expect(await page.inputValue(':nth-match(textarea, 1)')).toBe("[[" + alias_name + "]]")
+    await lastBlock(page)
+    expect(await page.inputValue('textarea >> nth=0')).toBe("[[" + alias_name + "]]")
     await page.keyboard.press(hotkeyBack)
   }
 
@@ -148,4 +149,4 @@ async function alias_test(page, page_name: string, search_kws: string[]) {
 
 test('page diacritic alias', async ({ page }) => {
   await alias_test(page, "ü", ["ü", "ü", "Ü"])
-})
+})

+ 4 - 3
e2e-tests/sidebar.spec.ts

@@ -1,12 +1,13 @@
 import { expect } from '@playwright/test'
 import { test } from './fixtures'
-import { createRandomPage, searchAndJumpToPage } from './utils'
+import { createRandomPage, openLeftSidebar, searchAndJumpToPage } from './utils'
 
 /***
  * Test side bar features
  ***/
 
 test('favorite item and recent item test', async ({ page }) => {
+  await openLeftSidebar(page)
   // add page to fav
   const fav_page_name = await createRandomPage(page)
   let favs = await page.$$('.favorite-item a')
@@ -37,10 +38,10 @@ test('favorite item and recent item test', async ({ page }) => {
 
 test('recent is updated #4320', async ({ page }) => {
   const page1 = await createRandomPage(page)
-  await page.fill(':nth-match(textarea, 1)', 'Random Thought')
+  await page.fill('textarea >> nth=0', 'Random Thought')
 
   const page2 = await createRandomPage(page)
-  await page.fill(':nth-match(textarea, 1)', 'Another Random Thought')
+  await page.fill('textarea >> nth=0', 'Another Random Thought')
 
   const firstRecent = page.locator('.nav-content-item.recent li >> nth=0')
   expect(await firstRecent.textContent()).toContain(page2)

+ 34 - 23
e2e-tests/utils.ts

@@ -28,7 +28,7 @@ export async function createRandomPage(page: Page) {
   // Click text=/.*New page: "new page".*/
   await page.click('text=/.*New page: ".*/')
   // wait for textarea of first block
-  await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
+  await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
 
   return randomTitle;
 }
@@ -40,7 +40,7 @@ export async function createPage(page: Page, page_name: string) {// Click #searc
   // Click text=/.*New page: "new page".*/
   await page.click('text=/.*New page: ".*/')
   // wait for textarea of first block
-  await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
+  await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
 
   return page_name;
 }
@@ -59,26 +59,25 @@ export async function searchAndJumpToPage(page: Page, pageTitle: string) {
 * @param page The Playwright Page object.
 * @returns The locator of the last block.
 */
-export async function lastInnerBlock(page: Page): Promise<Locator> {
+export async function lastBlock(page: Page): Promise<Locator> {
   // discard any popups
   await page.keyboard.press('Escape')
   // click last block
-  await page.waitForSelector('.page-blocks-inner .ls-block >> nth=-1')
   await page.click('.page-blocks-inner .ls-block >> nth=-1')
   // wait for textarea
-  await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
-  return page.locator(':nth-match(textarea, 1)')
+  await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
+  return page.locator('textarea >> nth=0')
 }
 
-export async function lastBlock(page: Page): Promise<Locator> {
-  // discard any popups
-  await page.keyboard.press('Escape')
-  // click last block
-  await page.click('.ls-block >> nth=-1')
-  // wait for textarea
-  await page.waitForSelector(':nth-match(textarea, 1)', { state: 'visible' })
-
-  return page.locator(':nth-match(textarea, 1)')
+/**
+ * Press Enter and create the next block.
+ * @param page The Playwright Page object.
+ */
+export async function enterNextBlock(page: Page): Promise<Locator> {
+  let blockCount = await page.locator('.page-blocks-inner .ls-block').count()
+  await page.press('textarea >> nth=0', 'Enter')
+  await page.waitForSelector(`.ls-block >> nth=${blockCount} >> textarea`, { state: 'visible' })
+  return page.locator('textarea >> nth=0')
 }
 
 /**
@@ -87,17 +86,18 @@ export async function lastBlock(page: Page): Promise<Locator> {
 * @returns The locator of the last block
 */
 export async function newInnerBlock(page: Page): Promise<Locator> {
-  await lastInnerBlock(page)
-  await page.press(':nth-match(textarea, 1)', 'Enter')
+  await lastBlock(page)
+  await page.press('textarea >> nth=0', 'Enter')
 
-  return page.locator(':nth-match(textarea, 1)')
+  return page.locator('textarea >> nth=0')
 }
 
 export async function newBlock(page: Page): Promise<Locator> {
-  await lastBlock(page)
-  await page.press(':nth-match(textarea, 1)', 'Enter')
-
-  return page.locator(':nth-match(textarea, 1)')
+  let blockNumber = await page.locator('.ls-block').count()
+  const prev = await lastBlock(page)
+  await page.press('textarea >> nth=0', 'Enter')
+  await page.waitForSelector(`.ls-block >> nth=${blockNumber} >> textarea`, { state: 'visible' })
+  return page.locator('textarea >> nth=0')
 }
 
 export async function escapeToCodeEditor(page: Page): Promise<void> {
@@ -135,6 +135,17 @@ export async function setMockedOpenDirPath(
   )
 }
 
+export async function openLeftSidebar(page: Page): Promise<void> {
+  let sidebar = page.locator('#left-sidebar')
+
+  // Left sidebar is toggled by `is-open` class
+  if (!/is-open/.test(await sidebar.getAttribute('class'))) {
+    await page.click('#left-menu.button')
+    await page.waitForTimeout(10)
+    await expect(sidebar).toHaveClass(/is-open/)
+  }
+}
+
 export async function loadLocalGraph(page: Page, path?: string): Promise<void> {
   await setMockedOpenDirPath(page, path);
 
@@ -147,7 +158,7 @@ export async function loadLocalGraph(page: Page, path?: string): Promise<void> {
     let sidebar = page.locator('#left-sidebar')
     if (!/is-open/.test(await sidebar.getAttribute('class'))) {
       await page.click('#left-menu.button')
-      expect(await sidebar.getAttribute('class')).toMatch(/is-open/)
+      await expect(sidebar).toHaveClass(/is-open/)
     }
 
     await page.click('#left-sidebar #repo-switch');

+ 3 - 2
src/main/frontend/db/model.cljs

@@ -642,7 +642,8 @@
                             limit initial-blocks-length
                             use-cache? true
                             scoped-block-id nil}}]
-   (assert (integer? block-id))
+   (when block-id
+     (assert (integer? block-id) (str "wrong block-id: " block-id))
    (let [entity (db-utils/entity repo-url block-id)
          page? (some? (:block/name entity))
          page-entity (if page? entity (:block/page entity))
@@ -686,7 +687,7 @@
                            blocks (remove (fn [b] (nil? (:block/content b))) blocks)]
                        (map (fn [b] (assoc b :block/page bare-page-map)) blocks)))}
         nil)
-      react))))
+      react)))))
 
 (defn get-page-blocks-no-cache
   ([page]