Pārlūkot izejas kodu

test(e2e): add test for backspace and cursor pos (#4896)

* test(e2e): add test for backspace and cursor pos
* fix(test): refine, fix wrong helper
Andelf 3 gadi atpakaļ
vecāks
revīzija
115054736d

+ 2 - 5
.github/workflows/build.yml

@@ -168,19 +168,16 @@ jobs:
         env:
           PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true
 
+      # NOTE: require the app to be build in debug mode(compile instead of build).
       - name: Prepare E2E test build
         run: |
-          yarn gulp:build && yarn cljs:release
+          yarn gulp:build && clojure -M:cljs compile app publishing electron
           (cd static && yarn install && yarn rebuild:better-sqlite3)
 
       # Exits with 0 if yarn.lock is up to date or 1 if we forgot to update it
       - name: Ensure static yarn.lock is up to date
         run: git diff --exit-code static/yarn.lock
 
-      # If not building, the `event_` of `goog.event.KeyboardEvent` would be missing
-      - name: Build app
-        run: clojure -M:cljs compile app
-
       - name: Run Playwright test
         run: xvfb-run -- yarn e2e-test
         env:

+ 15 - 41
e2e-tests/basic.spec.ts

@@ -4,13 +4,6 @@ import path from 'path'
 import { test } from './fixtures'
 import { randomString, createRandomPage } from './utils'
 
-test('render app', async ({ page }) => {
-  // NOTE: part of app startup tests is moved to `fixtures.ts`.
-  await page.waitForFunction('window.document.title != "Loading"')
-
-  expect(await page.title()).toMatch(/^Logseq.*?/)
-})
-
 test('toggle sidebar', async ({ page }) => {
   let sidebar = page.locator('#left-sidebar')
 
@@ -52,7 +45,7 @@ test('create page and blocks, save to disk', async ({ page, block, graphDir }) =
   await block.mustFill('this is my first bullet')
   await block.enterNext()
 
-  await block.waitForBlocks(1)
+  await block.waitForBlocks(2)
 
   await block.mustFill('this is my second bullet')
   await block.clickNext()
@@ -110,7 +103,7 @@ test('delete and backspace', async ({ page, block }) => {
 
   // refill
   await block.enterNext()
-  await page.type('textarea >> nth=0', 'test', { delay: 50 })
+  await block.mustType('test')
   await page.keyboard.press('ArrowLeft', { delay: 50 })
   await page.keyboard.press('ArrowLeft', { delay: 50 })
 
@@ -178,7 +171,7 @@ test('template', async ({ page, block }) => {
   expect(await block.unindent()).toBe(true)
   expect(await block.unindent()).toBe(false) // already at the first level
 
-  await block.waitForBlocks(4)
+  await block.waitForBlocks(5)
 
   // NOTE: use delay to type slower, to trigger auto-completion UI.
   await block.mustType('/template')
@@ -194,21 +187,18 @@ test('template', async ({ page, block }) => {
   await block.waitForBlocks(8)
 })
 
-test('auto completion square brackets', async ({ page }) => {
+test('auto completion square brackets', async ({ page, block }) => {
   await createRandomPage(page)
 
   // In this test, `type` is unsed instead of `fill`, to allow for auto-completion.
 
   // [[]]
-  await page.type('textarea >> nth=0', 'This is a [')
-  await expect(page.locator('textarea >> nth=0')).toHaveText('This is a []')
-  await page.waitForTimeout(100)
-  await page.type('textarea >> nth=0', '[')
+  await block.mustType('This is a [', { toBe: 'This is a []'})
+  await block.mustType('[', { toBe: 'This is a [[]]'})
+
   // wait for search popup
   await page.waitForSelector('text="Search for a page"')
 
-  expect(await page.inputValue('textarea >> nth=0')).toBe('This is a [[]]')
-
   // re-enter edit mode
   await page.press('textarea >> nth=0', 'Escape')
   await page.click('.ls-block >> nth=-1')
@@ -236,43 +226,27 @@ test('auto completion and auto pair', async ({ page, block }) => {
   await block.enterNext()
 
   // {{
-  await page.type('textarea >> nth=0', 'type {{')
-  expect(await page.inputValue('textarea >> nth=0')).toBe('type {{}}')
+  await block.mustType('type {{', { toBe: 'type {{}}'})
 
   // ((
   await block.clickNext()
 
-  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 block.clickNext()
-
-  // 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')
+  await block.mustType('type (', { toBe: 'type ()'})
+  await block.mustType('(', { toBe: 'type (())'})
 
   // [[  #3251
   await block.clickNext()
 
-  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 block.mustType('type [', { toBe: 'type []'})
+  await block.mustType('[', { toBe: 'type [[]]'})
+
   await page.press('textarea >> nth=0', 'Escape') // escape any popup from `[[]]`
 
   // ``
   await block.clickNext()
 
-  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('textarea >> nth=0')).toBe('type `code here`')
+  await block.mustType('type `', { toBe: 'type ``'})
+  await block.mustType('code here', { toBe: 'type `code here`'})
 })
 
 test('invalid page props #3944', async ({ page, block }) => {

+ 45 - 7
e2e-tests/editor.spec.ts

@@ -5,10 +5,10 @@ import { dispatch_kb_events } from './util/keyboard-events'
 import * as kb_events from './util/keyboard-events'
 
 test(
-  "press Chinese parenthesis 【 by 2 times #3251 should trigger [[]], " +
+  "Press CJK Left Black Lenticular Bracket `【` by 2 times #3251 should trigger [[]], " +
   "but dont trigger RIME #3440 ",
   // cases should trigger [[]] #3251
-  async ({ page }) => {
+  async ({ page, block }) => {
     for (let [idx, events] of [
       kb_events.win10_pinyin_left_full_square_bracket,
       kb_events.macos_pinyin_left_full_square_bracket
@@ -17,10 +17,10 @@ test(
     ].entries()) {
       await createRandomPage(page)
       let check_text = "#3251 test " + idx
-      await page.fill(':nth-match(textarea, 1)', check_text + "【")
+      await block.mustFill(check_text + "【")
       await dispatch_kb_events(page, ':nth-match(textarea, 1)', events)
       expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(check_text + '【')
-      await page.fill(':nth-match(textarea, 1)', check_text + "【【")
+      await block.mustFill(check_text + "【【")
       await dispatch_kb_events(page, ':nth-match(textarea, 1)', events)
       expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(check_text + '[[]]')
     };
@@ -32,7 +32,7 @@ test(
     ].entries()) {
       await createRandomPage(page)
       let check_text = "#3440 test " + idx
-      await page.fill(':nth-match(textarea, 1)', check_text)
+      await block.mustFill(check_text)
       await dispatch_kb_events(page, ':nth-match(textarea, 1)', events)
       expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(check_text)
       await dispatch_kb_events(page, ':nth-match(textarea, 1)', events)
@@ -45,7 +45,7 @@ test('hashtag and quare brackets in same line #4178', async ({ page }) => {
 
   await page.type('textarea >> nth=0', '#foo bar')
   await enterNextBlock(page)
-  await page.type('textarea >> nth=0', 'bar [[blah]]', { delay: 100})
+  await page.type('textarea >> nth=0', 'bar [[blah]]', { delay: 100 })
 
   for (let i = 0; i < 12; i++) {
     await page.press('textarea >> nth=0', 'ArrowLeft')
@@ -86,9 +86,47 @@ test('disappeared children #4814', async ({ page, block }) => {
   await block.waitForBlocks(7) // 1 + 5 + 1 empty
 
   // Ensures there's no active editor
-  await expect(page.locator('.editor-inner')).toHaveCount(0, {timeout: 500})
+  await expect(page.locator('.editor-inner')).toHaveCount(0, { timeout: 500 })
 })
 
+test.skip('backspace and cursor position #4897', async ({ page, block }) => {
+  await createRandomPage(page)
+
+  // Delete to previous block, and check cursor postion, with markup
+  await block.mustFill('`012345`')
+  await block.enterNext()
+  await block.mustType('`abcdef', { toBe: '`abcdef`' }) // "`" auto-completes
+
+  expect(await block.selectionStart()).toBe(7)
+  expect(await block.selectionEnd()).toBe(7)
+  for (let i = 0; i < 7; i++) {
+    await page.keyboard.press('ArrowLeft')
+  }
+  expect(await block.selectionStart()).toBe(0)
+
+  await page.keyboard.press('Backspace')
+  await block.waitForBlocks(1) // wait for delete and re-render
+  expect(await block.selectionStart()).toBe(8)
+})
+
+test.skip('next block and cursor position', async ({ page, block }) => {
+  await createRandomPage(page)
+
+  // Press Enter and check cursor postion, with markup
+  await block.mustType('abcde`12345', { toBe: 'abcde`12345`' }) // "`" auto-completes
+  for (let i = 0; i < 7; i++) {
+    await page.keyboard.press('ArrowLeft')
+  }
+  expect(await block.selectionStart()).toBe(5) // after letter 'e'
+
+  await block.enterNext()
+  expect(await block.selectionStart()).toBe(0) // should at the beginning of the next block
+
+  const locator = page.locator('textarea >> nth=0')
+  await expect(locator).toHaveText('`12345`', { timeout: 1000 })
+})
+
+
 // FIXME: ClipboardItem is not defined when running with this test
 // test('copy & paste block ref and replace its content', async ({ page }) => {
 //   await createRandomPage(page)

+ 31 - 7
e2e-tests/fixtures.ts

@@ -2,7 +2,7 @@ import * as fs from 'fs'
 import * as path from 'path'
 import { test as base, expect, ConsoleMessage, Locator } from '@playwright/test';
 import { ElectronApplication, Page, BrowserContext, _electron as electron } from 'playwright'
-import { loadLocalGraph, randomString } from './utils';
+import { loadLocalGraph, openLeftSidebar, randomString } from './utils';
 
 let electronApp: ElectronApplication
 let context: BrowserContext
@@ -12,7 +12,7 @@ let repoName = randomString(10)
 let testTmpDir = path.resolve(__dirname, '../tmp')
 
 if (fs.existsSync(testTmpDir)) {
-  fs.rmdirSync(testTmpDir, { recursive: true })
+  fs.rmSync(testTmpDir, { recursive: true })
 }
 
 export let graphDir = path.resolve(testTmpDir, "e2e-test", repoName)
@@ -49,6 +49,7 @@ base.beforeAll(async () => {
     cwd: "./static",
     args: ["electron.js"],
     locale: 'en',
+    timeout: 10_000, // should be enough for the app to start
   })
   context = electronApp.context()
   await context.tracing.start({ screenshots: true, snapshots: true });
@@ -61,6 +62,7 @@ base.beforeAll(async () => {
       "appData": app.getPath("appData"),
       "userData": app.getPath("userData"),
       "appName": app.getName(),
+      "electronVersion": app.getVersion(),
     }
   })
   console.log("Test start with:", info)
@@ -77,7 +79,6 @@ base.beforeAll(async () => {
   })
 
   await page.waitForLoadState('domcontentloaded')
-  // 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')
 
@@ -92,6 +93,11 @@ base.beforeAll(async () => {
   })
 
   await loadLocalGraph(page, graphDir);
+
+  // render app
+  await page.waitForFunction('window.document.title !== "Loading"')
+  expect(await page.title()).toMatch(/^Logseq.*?/)
+  await openLeftSidebar(page)
 })
 
 base.beforeEach(async () => {
@@ -118,7 +124,7 @@ interface Block {
    * Must type input some text into an **empty** block.
    * **DO NOT USE** this if there's auto-complete
    */
-  mustType(value: string, options?: { delay?: number }): Promise<void>;
+  mustType(value: string, options?: { delay?: number, toBe?: string }): Promise<void>;
   /**
    * Press Enter and go to next block, require cursor to be in current block(editing mode).
    * When cursor is not at the end of block, trailing text will be moved to the next block.
@@ -136,6 +142,10 @@ interface Block {
   waitForSelectedBlocks(total: number): Promise<void>;
   /** Escape editing mode, modal popup and selection. */
   escapeEditing(): Promise<void>;
+  /** Find current selectionStart, i.e. text cursor position. */
+  selectionStart(): Promise<number>;
+  /** Find current selectionEnd. */
+  selectionEnd(): Promise<number>;
 }
 
 // hijack electron app into the test context
@@ -156,12 +166,13 @@ export const test = base.extend<{ page: Page, block: Block, context: BrowserCont
         await locator.fill(value)
         await expect(locator).toHaveText(value, { timeout: 1000 })
       },
-      mustType: async (value: string, options?: { delay?: number }) => {
+      mustType: async (value: string, options?: { delay?: number, toBe?: string }) => {
         const locator: Locator = page.locator('textarea >> nth=0')
         await locator.waitFor({ timeout: 1000 })
-        const { delay = 100 } = options || {};
+        const { delay = 50 } = options || {};
+        const { toBe = value } = options || {};
         await locator.type(value, { delay })
-        await expect(locator).toHaveText(value, { timeout: 1000 })
+        await expect(locator).toHaveText(toBe, { timeout: 1000 })
       },
       enterNext: async (): Promise<Locator> => {
         let blockCount = await page.locator('.page-blocks-inner .ls-block').count()
@@ -191,6 +202,7 @@ export const test = base.extend<{ page: Page, block: Block, context: BrowserCont
       waitForBlocks: async (total: number): Promise<void> => {
         // NOTE: `nth=` counts from 0.
         await page.waitForSelector(`.ls-block >> nth=${total - 1}`, { timeout: 1000 })
+        await page.waitForSelector(`.ls-block >> nth=${total}`, { state: 'detached', timeout: 1000 })
       },
       waitForSelectedBlocks: async (total: number): Promise<void> => {
         // NOTE: `nth=` counts from 0.
@@ -199,6 +211,18 @@ export const test = base.extend<{ page: Page, block: Block, context: BrowserCont
       escapeEditing: async (): Promise<void> => {
         await page.keyboard.press('Escape')
         await page.keyboard.press('Escape')
+      },
+      selectionStart: async (): Promise<number> => {
+        return await page.locator('textarea >> nth=0').evaluate(node => {
+          const elem = <HTMLTextAreaElement>node
+          return elem.selectionStart
+        })
+      },
+      selectionEnd: async (): Promise<number> => {
+        return await page.locator('textarea >> nth=0').evaluate(node => {
+          const elem = <HTMLTextAreaElement>node
+          return elem.selectionEnd
+        })
       }
     }
     use(block)

+ 1 - 2
e2e-tests/hotkey.spec.ts

@@ -16,8 +16,7 @@ test('open search dialog', async ({ page }) => {
   await page.waitForSelector('[placeholder="Search or create page"]', { state: 'hidden' })
 })
 
-// See-also: https://github.com/logseq/logseq/issues/3278
-test('insert link', async ({ page }) => {
+test('insert link #3278', async ({ page }) => {
   await createRandomPage(page)
 
   let hotKey = 'Control+l'

+ 4 - 2
e2e-tests/utils.ts

@@ -168,10 +168,12 @@ export async function loadLocalGraph(page: Page, path?: string): Promise<void> {
     }
 
     await page.click('#left-sidebar #repo-switch');
-    await page.waitForSelector('#left-sidebar .dropdown-wrapper >> text="Add new graph"', { state: 'visible' })
+    await page.waitForSelector('#left-sidebar .dropdown-wrapper >> text="Add new graph"',
+      { state: 'visible', timeout: 5000 })
 
     await page.click('text=Add new graph')
-    await page.waitForSelector('strong:has-text("Choose a folder")', { state: 'visible' })
+    await page.waitForSelector('strong:has-text("Choose a folder")',
+      { state: 'visible', timeout: 5000 })
     await page.click('strong:has-text("Choose a folder")')
 
     const skip = page.locator('a:has-text("Skip")')