From 453ee3b3b4d7ddd83805a4edddbde26ea821758b Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 2 Jul 2025 22:38:32 +0800 Subject: [PATCH] fix: ensure website build doesn't rely on new mobile components this commit also removes js e2e-tests --- .github/workflows/clj-e2e.yml | 2 - .github/workflows/e2e.yml | 166 - .gitignore | 3 +- CODEBASE_OVERVIEW.md | 1 - clj-e2e/bb.edn | 2 +- e2e-tests/accessibility.spec.ts | 23 - e2e-tests/basic.spec.ts | 332 -- e2e-tests/blockref.spec.ts | 79 - e2e-tests/code-editing.spec.ts | 321 -- e2e-tests/context-menu.spec.ts | 60 - e2e-tests/dnd.spec.ts | 82 - e2e-tests/editor.spec.ts | 865 --- e2e-tests/fixtures.ts | 305 - e2e-tests/flashcards.spec.ts | 52 - e2e-tests/fs.spec.ts | 141 - e2e-tests/headings.spec.ts | 80 - e2e-tests/history.spec.ts | 53 - e2e-tests/hotkey.spec.ts | 46 - e2e-tests/logseq-api.spec.ts | 222 - e2e-tests/logseq-url.spec.ts | 55 - e2e-tests/page-rename.spec.ts | 103 - e2e-tests/page-search.spec.ts | 174 - e2e-tests/paste.spec.ts | 15 - e2e-tests/plugin/index.html | 15 - e2e-tests/plugin/index.js | 61 - e2e-tests/plugin/lsplugin.user.js | 2 - e2e-tests/plugin/package.json | 8 - e2e-tests/plugins.spec.ts | 110 - e2e-tests/random.spec.ts | 180 - e2e-tests/sanitization.spec.ts | 48 - e2e-tests/sidebar.spec.ts | 66 - e2e-tests/types.ts | 55 - e2e-tests/util/basic.ts | 42 - e2e-tests/util/keyboard-event-cap.html | 64 - e2e-tests/util/keyboard-events.ts | 466 -- e2e-tests/util/page.ts | 14 - e2e-tests/util/search-modal.ts | 64 - e2e-tests/utils.ts | 310 - e2e-tests/whiteboards.spec.ts | 526 -- e2e-tests/window.spec.ts | 34 - playwright.config.ts | 28 - src/main/capacitor/components/app.cljs | 10 +- .../components/editor_toolbar.cljs} | 6 +- src/main/capacitor/components/modal.cljs | 10 +- .../components/selection_toolbar.cljs} | 4 +- src/main/capacitor/core.cljs | 4 +- src/main/capacitor/events.cljs | 9 + .../mobile/core.cljs => capacitor/init.cljs} | 5 +- src/main/frontend/components/container.cljs | 6 +- src/main/frontend/handler/events.cljs | 6 +- static/yarn.lock | 4999 ----------------- typos.toml | 2 +- 52 files changed, 35 insertions(+), 10301 deletions(-) delete mode 100644 .github/workflows/e2e.yml delete mode 100644 e2e-tests/accessibility.spec.ts delete mode 100644 e2e-tests/basic.spec.ts delete mode 100644 e2e-tests/blockref.spec.ts delete mode 100644 e2e-tests/code-editing.spec.ts delete mode 100644 e2e-tests/context-menu.spec.ts delete mode 100644 e2e-tests/dnd.spec.ts delete mode 100644 e2e-tests/editor.spec.ts delete mode 100644 e2e-tests/fixtures.ts delete mode 100644 e2e-tests/flashcards.spec.ts delete mode 100644 e2e-tests/fs.spec.ts delete mode 100644 e2e-tests/headings.spec.ts delete mode 100644 e2e-tests/history.spec.ts delete mode 100644 e2e-tests/hotkey.spec.ts delete mode 100644 e2e-tests/logseq-api.spec.ts delete mode 100644 e2e-tests/logseq-url.spec.ts delete mode 100644 e2e-tests/page-rename.spec.ts delete mode 100644 e2e-tests/page-search.spec.ts delete mode 100644 e2e-tests/paste.spec.ts delete mode 100644 e2e-tests/plugin/index.html delete mode 100644 e2e-tests/plugin/index.js delete mode 100644 e2e-tests/plugin/lsplugin.user.js delete mode 100644 e2e-tests/plugin/package.json delete mode 100644 e2e-tests/plugins.spec.ts delete mode 100644 e2e-tests/random.spec.ts delete mode 100644 e2e-tests/sanitization.spec.ts delete mode 100644 e2e-tests/sidebar.spec.ts delete mode 100644 e2e-tests/types.ts delete mode 100644 e2e-tests/util/basic.ts delete mode 100644 e2e-tests/util/keyboard-event-cap.html delete mode 100644 e2e-tests/util/keyboard-events.ts delete mode 100644 e2e-tests/util/page.ts delete mode 100644 e2e-tests/util/search-modal.ts delete mode 100644 e2e-tests/utils.ts delete mode 100644 e2e-tests/whiteboards.spec.ts delete mode 100644 e2e-tests/window.spec.ts delete mode 100644 playwright.config.ts rename src/main/{frontend/mobile/mobile_bar.cljs => capacitor/components/editor_toolbar.cljs} (97%) rename src/main/{frontend/mobile/action_bar.cljs => capacitor/components/selection_toolbar.cljs} (94%) create mode 100644 src/main/capacitor/events.cljs rename src/main/{frontend/mobile/core.cljs => capacitor/init.cljs} (97%) delete mode 100644 static/yarn.lock diff --git a/.github/workflows/clj-e2e.yml b/.github/workflows/clj-e2e.yml index 8c2ad31472..4b66342063 100644 --- a/.github/workflows/clj-e2e.yml +++ b/.github/workflows/clj-e2e.yml @@ -79,8 +79,6 @@ jobs: - name: Prepare E2E test build run: | yarn gulp:build && clojure -M:cljs release app --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug - rsync -avz --exclude node_modules --exclude android --exclude ios ./static/ ./public/ - ls -lR ./public - name: Run e2e tests run: cd clj-e2e && timeout 30m bb dev diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index 2bd1d9d83d..0000000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,166 +0,0 @@ -name: E2E - -# Running E2E test multiple times to confirm test stability. -# E2E test could be randomly failed due to the batch update mechanism of React. -# Robust E2E test could help improving dev experience. - -on: - push: - branches: [master] - paths: - - 'e2e-tests/**' - # TODO: Re-enable when ready to enable tests for file graphs - # pull_request: - # branches: [master] - # paths: - # - 'e2e-tests/**' - -env: - CLOJURE_VERSION: '1.11.1.1413' - JAVA_VERSION: '11' - # This is the latest node version we can run. - NODE_VERSION: '20' - BABASHKA_VERSION: '1.0.168' - -jobs: - e2e-test-build: - if: false # disabled - name: Build Test Artifact - runs-on: ubuntu-22.04 - steps: - - name: Checkout - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 - - - name: Set up Node - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'yarn' - cache-dependency-path: | - yarn.lock - static/yarn.lock - - - name: Set up Java - uses: actions/setup-java@v3 - with: - distribution: 'zulu' - java-version: ${{ env.JAVA_VERSION }} - - - name: Set up Clojure - uses: DeLaGuardo/setup-clojure@10.1 - with: - cli: ${{ env.CLOJURE_VERSION }} - - - name: Clojure cache - uses: actions/cache@v3 - id: clojure-deps - with: - path: | - ~/.m2/repository - ~/.gitlibs - key: ${{ runner.os }}-clojure-deps-${{ hashFiles('deps.edn') }} - restore-keys: ${{ runner.os }}-clojure-deps- - - - name: Fetch Clojure deps - if: steps.clojure-deps.outputs.cache-hit != 'true' - run: clojure -A:cljs -P - - - name: Shadow-cljs cache - uses: actions/cache@v3 - with: - path: .shadow-cljs - # ensure update cache every time - key: ${{ runner.os }}-shadow-cljs-${{ github.sha }} - # will match most recent upload - restore-keys: | - ${{ runner.os }}-shadow-cljs- - - - name: Fetch yarn deps - run: yarn install - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true - - # NOTE: require the app to be build with DEV-RELEASE flag - - name: Prepare E2E test build - run: | - yarn gulp:build && clojure -M:cljs release app electron --config-merge "{:closure-defines {frontend.config/DEV-RELEASE true}}" --debug - - # NOTE: should include .shadow-cljs if in dev mode(compile) - - name: Create Archive for build - run: tar czf static.tar.gz static - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: logseq-e2e-artifact - path: static.tar.gz - retention-days: 1 - - e2e-test-run: - needs: [ e2e-test-build ] - name: Test Shard ${{ matrix.shard }} Repeat ${{ matrix.repeat }} - runs-on: ubuntu-22.04 - strategy: - matrix: - repeat: [1, 2] - shard: [1, 2, 3] - - steps: - - name: Repeat message - run: echo ::info title=StartUp::E2E testing shard ${{ matrix.shard}}/3 repeat ${{ matrix.repeat }} - - - name: Checkout - uses: actions/checkout@ac593985615ec2ede58e132d2e21d2b1cbd6127c # v3.3.0 - - - name: Download test build artifact - uses: actions/download-artifact@v4 - with: - name: logseq-e2e-artifact - - - name: Extract test Artifact - run: tar xzf static.tar.gz - - - name: Set up Node - uses: actions/setup-node@v3 - with: - node-version: ${{ env.NODE_VERSION }} - cache: 'yarn' - cache-dependency-path: | - yarn.lock - static/yarn.lock - - - name: Fetch yarn deps for E2E test - run: | - yarn install - (cd static && yarn install && yarn rebuild:all) - env: - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true - - - name: Ensure static yarn.lock is up to date - run: git diff --exit-code static/yarn.lock - - - name: Install Fluxbox - run: sudo apt-get update && sudo apt-get install -y fluxbox - - # Emulate a virtual framebuffer on machines with no display hardware - - name: Run XVFB - run: Xvfb :1 -screen 0 1024x768x24 >/dev/null 2>&1 & - - # Start a lightweight window manager to simulate window actions (maximize,restore etc) - - name: Start Fluxbox - run: DISPLAY=:1.0 fluxbox >/dev/null 2>&1 & - - - name: Run Playwright test - run: DISPLAY=:1.0 npx playwright test --reporter github --shard=${{ matrix.shard }}/3 - env: - LOGSEQ_CI: true - DEBUG: "pw:api" - RELEASE: true # skip dev only test - - - name: Save e2e artifacts - if: ${{ failure() }} - uses: actions/upload-artifact@v4 - with: - name: e2e-repeat-report-${{ matrix.shard}}-${{ matrix.repeat }} - path: e2e-dump/* - retention-days: 1 diff --git a/.gitignore b/.gitignore index fb99ee3334..4ff0bb10b8 100644 --- a/.gitignore +++ b/.gitignore @@ -60,8 +60,7 @@ startup.png android/app/src/main/assets/capacitor.config.json *.sublime-* -/public/static -/public/ +/public .yarn/ .yarnrc.yml diff --git a/CODEBASE_OVERVIEW.md b/CODEBASE_OVERVIEW.md index 2fec58f805..2075c07080 100644 --- a/CODEBASE_OVERVIEW.md +++ b/CODEBASE_OVERVIEW.md @@ -61,7 +61,6 @@ This is overview of this repository's most important directories and files. - `packages/ui/` - The frontend's component system based on shadcn - `packags/tldraw/` - Custom fork of tldraw which powers whiteboards - `scripts` - Dev scripts -- `e2e-tests/` - end to end frontend tests - `clj-e2e/` - end to end clj frontend tests - `android/` - Android app - `ios/` - iOS app diff --git a/clj-e2e/bb.edn b/clj-e2e/bb.edn index 0040204088..e0a40bc49e 100644 --- a/clj-e2e/bb.edn +++ b/clj-e2e/bb.edn @@ -7,7 +7,7 @@ serve {:doc "Serve static assets" :requires ([babashka.http-server :as server]) :task (server/exec (merge {:port 3002 - :dir "../public"} + :dir "../static/"} cli-opts))} prn {:task (clojure "-X clojure.core/prn" cli-opts)} diff --git a/e2e-tests/accessibility.spec.ts b/e2e-tests/accessibility.spec.ts deleted file mode 100644 index aa902a2aad..0000000000 --- a/e2e-tests/accessibility.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { test } from './fixtures' -import { createRandomPage } from './utils' -import { expect } from '@playwright/test' -import AxeBuilder from '@axe-core/playwright' - -// TODO: more configuration is required for this test -test.skip('should not have any automatically detectable accessibility issues', async ({ page }) => { - try { - await page.waitForSelector('.notification-clear', { timeout: 10 }) - page.click('.notification-clear') - } catch (error) { - } - - await createRandomPage(page) - await page.waitForTimeout(2000) - const accessibilityScanResults = await new AxeBuilder({ page }) - .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa']) - .disableRules(['meta-viewport']) - .setLegacyMode() - .analyze() - - expect(accessibilityScanResults.violations).toEqual([]); -}) diff --git a/e2e-tests/basic.spec.ts b/e2e-tests/basic.spec.ts deleted file mode 100644 index fb2675c094..0000000000 --- a/e2e-tests/basic.spec.ts +++ /dev/null @@ -1,332 +0,0 @@ -import { expect } from '@playwright/test' -import fs from 'fs/promises' -import path from 'path' -import { test } from './fixtures' -import { randomString, createRandomPage, modKey } from './utils' - - -test('create page and blocks, save to disk', async ({ page, block, graphDir }) => { - const pageTitle = await createRandomPage(page) - - // do editing - await page.keyboard.type('first bullet') - await block.enterNext() - - await block.waitForBlocks(2) - - await page.keyboard.type('second bullet') - await block.enterNext() - - await page.keyboard.type('third bullet') - expect(await block.indent()).toBe(true) - await block.enterNext() - - await page.keyboard.type('continue editing') - await page.keyboard.press('Shift+Enter') - await page.keyboard.type('second line') - - await block.enterNext() - expect(await block.unindent()).toBe(true) - expect(await block.unindent()).toBe(false) - await page.keyboard.type('test ok') - await page.keyboard.press('Escape') - - await block.waitForBlocks(5) - - // active edit, and create next block - await block.clickNext() - await page.keyboard.type('test') - for (let i = 0; i < 5; i++) { - await page.keyboard.press('Backspace', { delay: 100 }) - } - - await page.keyboard.press('Escape') - await block.waitForBlocks(5) - - await page.waitForTimeout(2000) // wait for saving to disk - const contentOnDisk = await fs.readFile( - path.join(graphDir, `pages/${pageTitle}.md`), - 'utf8' - ) - expect(contentOnDisk.trim()).toEqual('- first bullet\n- second bullet\n\t- third bullet\n\t- continue editing\n\t second line\n- test ok'.trim()) -}) - - -test('delete and backspace', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustFill('test') - - // backspace - await page.keyboard.press('Backspace') - await page.keyboard.press('Backspace') - expect(await page.inputValue('textarea >> nth=0')).toBe('te') - - // refill - await block.enterNext() - await block.mustType('test') - await page.keyboard.press('ArrowLeft', { delay: 50 }) - await page.keyboard.press('ArrowLeft', { delay: 50 }) - - // delete - await page.keyboard.press('Delete', { delay: 50 }) - expect(await page.inputValue('textarea >> nth=0')).toBe('tet') - await page.keyboard.press('Delete', { delay: 50 }) - expect(await page.inputValue('textarea >> nth=0')).toBe('te') - await page.keyboard.press('Delete', { delay: 50 }) - expect(await page.inputValue('textarea >> nth=0')).toBe('te') - -}) - - -test('block selection', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustFill('1') - await block.enterNext() - await block.mustFill('2') - expect(await block.indent()).toBe(true) - await block.enterNext() - await block.mustFill('3') - await block.enterNext() - await block.mustFill('4') - expect(await block.unindent()).toBe(true) - await block.enterNext() - await block.mustFill('5') - expect(await block.indent()).toBe(true) - await block.enterNext() - await block.mustFill('6') - await block.enterNext() - await block.mustFill('7') - expect(await block.unindent()).toBe(true) - await block.enterNext() - await block.mustFill('8') - expect(await block.indent()).toBe(true) - await block.enterNext() - await block.mustFill('9') - expect(await block.unindent()).toBe(true) - - // shift+up/down - await page.keyboard.down('Shift') - - await page.keyboard.press('ArrowUp') - await block.waitForSelectedBlocks(1) - let locator = page.locator('.ls-block >> nth=8') - - await page.keyboard.press('ArrowUp') - await block.waitForSelectedBlocks(2) - - await page.keyboard.press('ArrowUp') - await block.waitForSelectedBlocks(3) - - await page.keyboard.press('ArrowDown') - await block.waitForSelectedBlocks(2) - - await page.keyboard.up('Shift') - - // mod+click select or deselect - await page.keyboard.down(modKey) - await page.click('.ls-block >> nth=7') - await block.waitForSelectedBlocks(1) - - await page.click('.block-main-container >> nth=6') - await block.waitForSelectedBlocks(2) - - // mod+shift+click - await page.click('.ls-block >> nth=4') - await block.waitForSelectedBlocks(3) - - await page.keyboard.down('Shift') - await page.click('.ls-block >> nth=1') - await block.waitForSelectedBlocks(6) - - await page.keyboard.up('Shift') - await page.keyboard.up(modKey) - await page.keyboard.press('Escape') - - // shift+click - await page.keyboard.down('Shift') - await page.click('.block-main-container >> nth=0') - await page.click('.block-main-container >> nth=3') - await block.waitForSelectedBlocks(4) - await page.click('.ls-block >> nth=8') - await block.waitForSelectedBlocks(9) - await page.click('.ls-block >> nth=5') - await block.waitForSelectedBlocks(6) - await page.keyboard.up('Shift') -}) - -test('template', async ({ page, block }) => { - const randomTemplate = randomString(6) - - await createRandomPage(page) - - await block.mustFill('template test\ntemplate:: ') - await page.keyboard.type(randomTemplate, { delay: 100 }) - await page.keyboard.press('Enter') - await page.keyboard.press('Escape') - await block.clickNext() - - expect(await block.indent()).toBe(true) - - await block.mustFill('line1') - await block.enterNext() - await block.mustFill('line2') - await block.enterNext() - - expect(await block.indent()).toBe(true) - await block.mustFill('line3') - await block.enterNext() - - expect(await block.unindent()).toBe(true) - expect(await block.unindent()).toBe(true) - expect(await block.unindent()).toBe(false) // already at the first level - - await block.waitForBlocks(5) - - // See-also: #9354 - await block.enterNext() - await block.mustType('/template') - - await page.click('[title="Insert a created template here"]') - // type to search template name - await page.keyboard.type(randomTemplate.substring(0, 3), { delay: 100 }) - - const popupMenuItem = page.locator('.absolute >> text=' + randomTemplate) - await popupMenuItem.waitFor({ timeout: 2000 }) // wait for template search - await popupMenuItem.click() - - await block.waitForBlocks(9) - - - await block.clickNext() - await block.mustType('/template') - - await page.click('[title="Insert a created template here"]') - // type to search template name - await page.keyboard.type(randomTemplate.substring(0, 3), { delay: 100 }) - - await popupMenuItem.waitFor({ timeout: 2000 }) // wait for template search - await popupMenuItem.click() - - await block.waitForBlocks(13) // 9 + 4 -}) - -test('auto completion square brackets', async ({ page, block }) => { - await createRandomPage(page) - - // In this test, `type` is unused instead of `fill`, to allow for auto-completion. - - // [[]] - 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"') - - // re-enter edit mode - await page.press('textarea >> nth=0', 'Escape') - await page.click('.ls-block >> nth=-1') - await page.waitForSelector('textarea >> nth=0', { state: 'visible' }) - - // #3253 - 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('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, block }) => { - await createRandomPage(page) - - await block.mustFill('Auto-completion test') - await block.enterNext() - - // {{ - await block.mustType('type {{', { toBe: 'type {{}}' }) - await page.waitForTimeout(100); - // (( - await block.clickNext() - - await block.mustType('type (', { toBe: 'type ()' }) - await block.mustType('(', { toBe: 'type (())' }) - - await block.escapeEditing() // escape any popup from `(())` - - // [[ #3251 - await block.clickNext() - - await block.mustType('type [', { toBe: 'type []' }) - await block.mustType('[', { toBe: 'type [[]]' }) - - await block.escapeEditing() // escape any popup from `[[]]` - - // `` - await block.clickNext() - - await block.mustType('type `', { toBe: 'type ``' }) - await block.mustType('code here', { toBe: 'type `code here`' }) -}) - -test('invalid page props #3944', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustFill('public:: true\nsize:: 65535') - await page.press('textarea >> nth=0', 'Enter') - // Force rendering property block - await block.enterNext() -}) - -test('Scheduled date picker should point to the already specified Date #6985', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustFill('testTask \n SCHEDULED: <2000-05-06 Sat>') - await block.enterNext() - await page.waitForTimeout(500) - await block.escapeEditing() - - // Open date picker - await page.click('a.opacity-80') - await page.waitForTimeout(500) - await expect(page.locator('text=May 2000')).toBeVisible() - await expect(page.locator('td:has-text("6").active')).toBeVisible() - - // Close date picker - await page.click('a.opacity-80') - await page.waitForTimeout(500) -}) - -test('Opening a second datepicker should close the first one #7341', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustFill('testTask \n SCHEDULED: <2000-05-06 Sat>') - - await block.enterNext(); - - await block.mustFill('testTask \n SCHEDULED: <2000-06-07 Wed>') - await block.enterNext(); - await page.click('#main-content-container') - // Open date picker - await page.waitForTimeout(500) - await page.click('#main-content-container') - await page.waitForTimeout(500) - await page.click('a:has-text("2000-06-07 Wed").opacity-80') - await page.waitForTimeout(50) - await page.click('a:has-text("2000-05-06 Sat").opacity-80') - await page.waitForTimeout(50) - await expect(page.locator('text=May 2000')).toBeVisible() - await expect(page.locator('td:has-text("6").active')).toBeVisible() - await expect(page.locator('text=June 2000')).not.toBeVisible() - await expect(page.locator('td:has-text("7").active')).not.toBeVisible() - - // Close date picker - await page.click('a:has-text("2000-05-06 Sat").opacity-80') -}) diff --git a/e2e-tests/blockref.spec.ts b/e2e-tests/blockref.spec.ts deleted file mode 100644 index 2ee9c73a09..0000000000 --- a/e2e-tests/blockref.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { createRandomPage, enterNextBlock, modKey, editNthBlock, moveCursorToBeginning, moveCursorToEnd } from './utils' -import { dispatch_kb_events } from './util/keyboard-events' - -// Create a random page with some pre-defined blocks -// - a -// - b -// id:: UUID -// - ((id)) -async function setUpBlocks(page, block) { - await createRandomPage(page) - - await block.mustFill('a') - await block.enterNext() - await block.mustFill('b') - await page.keyboard.press(modKey + '+c', { delay: 100 }) - await page.waitForTimeout(100) - await block.enterNext() - await page.keyboard.press(modKey + '+v', { delay: 100 }) - await page.waitForTimeout(100) -} - -test('backspace at the beginning of a refed block #9406', async ({ page, block }) => { - await setUpBlocks(page, block) - await page.waitForTimeout(100) - await editNthBlock(page, 1) - await page.waitForTimeout(100) - await moveCursorToBeginning(page) - await page.keyboard.press('Backspace', { delay: 100 }) - await expect(page.locator('textarea >> nth=0')).toHaveText("ab") - await expect(await block.selectionStart()).toEqual(1) - await expect(page.locator('.block-ref >> text="ab"')).toHaveCount(1); -}) - -test('delete at the end of a prev block before a refed block #9406', async ({ page, block }) => { - await setUpBlocks(page, block) - await page.waitForTimeout(100) - await editNthBlock(page, 0) - await page.waitForTimeout(100) - await moveCursorToEnd(page) - await page.keyboard.press('Delete', { delay: 100 }) - await page.waitForTimeout(100) - await expect(page.locator('textarea >> nth=0')).toHaveText("ab") - await expect(await block.selectionStart()).toEqual(1) - await expect(page.locator('.block-ref >> text="ab"')).toHaveCount(1); -}) - -test('delete selected blocks, block ref should be replaced by content #9406', async ({ page, block }) => { - await setUpBlocks(page, block) - await editNthBlock(page, 0) - await page.waitForTimeout(100) - await page.keyboard.down('Shift') - await page.keyboard.press('ArrowDown', { delay: 20 }) - await page.keyboard.press('ArrowDown', { delay: 20 }) - await page.keyboard.up('Shift') - await block.waitForSelectedBlocks(2) - await page.keyboard.press('Backspace') - await expect(page.locator('.ls-block')).toHaveCount(1) - await editNthBlock(page, 0) - await expect(page.locator('textarea >> nth=0')).toHaveText("b") -}) - -test('delete and undo #9406', async ({ page, block }) => { - await setUpBlocks(page, block) - await editNthBlock(page, 0) - await page.waitForTimeout(100) - await page.keyboard.down('Shift') - await page.keyboard.press('ArrowDown', { delay: 20 }) - await page.keyboard.press('ArrowDown', { delay: 20 }) - await page.keyboard.up('Shift') - await block.waitForSelectedBlocks(2) - await page.keyboard.press('Backspace', { delay: 100 }) - await expect(page.locator('.ls-block')).toHaveCount(1) - await page.keyboard.press(modKey + '+z', { delay: 100 }) - await page.waitForTimeout(100) - await expect(page.locator('.ls-block')).toHaveCount(3) - await expect(page.locator('.block-ref >> text="b"')).toHaveCount(1); -}) diff --git a/e2e-tests/code-editing.spec.ts b/e2e-tests/code-editing.spec.ts deleted file mode 100644 index c2e34205e0..0000000000 --- a/e2e-tests/code-editing.spec.ts +++ /dev/null @@ -1,321 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { - createRandomPage, - escapeToCodeEditor, - escapeToBlockEditor, - repeatKeyPress, -} from './utils' - -/** - * NOTE: CodeMirror is a complex library that requires a lot of setup to work. - * This test suite is designed to test the basic functionality of the editor. - * It is not intended to test the full functionality of CodeMirror. - * For more information, see: https://codemirror.net/doc/manual.html - */ - -// TODO: Fix test that started intermittently failing some time around -// https://github.com/logseq/logseq/pull/9540 -test.skip('switch code editing mode', async ({ page }) => { - await createRandomPage(page) - - // NOTE: ` will trigger auto-pairing in Logseq - // NOTE: ( will trigger auto-pairing in CodeMirror - // NOTE: waitForTimeout is needed to ensure that the hotkey handler is finished (shift+enter) - // NOTE: waitForTimeout is needed to ensure that the CodeMirror editor is fully loaded and unloaded - // NOTE: multiple textarea elements are existed in the editor, be careful to select the right one - - // code block with 0 line - 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') - // lang label: clojure - expect(await page.innerText('.block-body .extensions__code-lang')).toBe('clojure') - - await page.press('.CodeMirror textarea', 'Escape') - await page.waitForSelector('.CodeMirror pre', { state: 'hidden' }) - expect(await page.inputValue('textarea >> nth=0')).toBe('```clojure\n```') - - await page.waitForTimeout(200) - await page.press('textarea >> nth=0', 'Escape') - await page.waitForSelector('.CodeMirror pre', { state: 'visible' }) - - // NOTE: must wait here, await loading of CodeMirror editor - await page.waitForTimeout(200) - await page.click('.CodeMirror pre') - await page.waitForTimeout(200) - - await page.type('.CodeMirror textarea', '(+ 1 1') - await page.press('.CodeMirror textarea', 'Escape') - await page.waitForSelector('.CodeMirror pre', { state: 'hidden' }) - expect(await page.inputValue('.block-editor textarea')).toBe('```clojure\n(+ 1 1)\n```') - - await page.waitForTimeout(200) // editor unloading - await page.press('.block-editor textarea', 'Escape') - await page.waitForTimeout(200) // editor loading - // click position is estimated to be at the beginning of the first line - await page.click('.CodeMirror pre', { position: { x: 1, y: 5 } }) - await page.waitForTimeout(200) - - await page.type('.CodeMirror textarea', ';; comment\n\n \n') - - await page.press('.CodeMirror textarea', 'Escape') - await page.waitForSelector('.CodeMirror pre', { state: 'hidden' }) - expect(await page.inputValue('.block-editor textarea')).toBe('```clojure\n;; comment\n\n \n(+ 1 1)\n```') -}) - - -test('convert from block content to code', async ({ page }) => { - await createRandomPage(page) - - await page.type('.block-editor textarea', '```') - await page.press('.block-editor textarea', 'Shift+Enter') - await page.waitForTimeout(200) // wait for hotkey handler - await page.press('.block-editor textarea', 'Escape') - await page.waitForSelector('.CodeMirror pre', { state: 'visible' }) - - await page.waitForTimeout(500) - await page.click('.CodeMirror pre') - await page.waitForTimeout(500) - expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber >> nth=-1').innerText()).toBe('1') - - await page.press('.CodeMirror textarea', 'Escape') - await page.waitForTimeout(500) - - expect(await page.inputValue('.block-editor textarea')).toBe('```\n```') - - // reset block, code block with 1 line - await page.fill('.block-editor textarea', '```\n\n```') - await page.waitForTimeout(200) // wait for fill - await escapeToCodeEditor(page) - expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber >> nth=-1').innerText()).toBe('1') - await escapeToBlockEditor(page) - expect(await page.inputValue('.block-editor textarea')).toBe('```\n\n```') - - // reset block, code block with 2 line - await page.fill('.block-editor textarea', '```\n\n\n```') - await page.waitForTimeout(200) - await escapeToCodeEditor(page) - expect(await page.locator('.CodeMirror-gutter-wrapper .CodeMirror-linenumber >> nth=-1').innerText()).toBe('2') - await escapeToBlockEditor(page) - expect(await page.inputValue('.block-editor textarea')).toBe('```\n\n\n```') - - await page.fill('.block-editor textarea', '```\n indented\nsecond line\n\n```') - await page.waitForTimeout(200) - await escapeToCodeEditor(page) - await escapeToBlockEditor(page) - expect(await page.inputValue('.block-editor textarea')).toBe('```\n indented\nsecond line\n\n```') - - await page.fill('.block-editor textarea', '```\n indented\n indented\n```') - await page.waitForTimeout(200) - await escapeToCodeEditor(page) - await escapeToBlockEditor(page) - expect(await page.inputValue('.block-editor textarea')).toBe('```\n indented\n indented\n```') -}) - -test('code block mixed input source', async ({ page }) => { - await createRandomPage(page) - - await page.fill('.block-editor textarea', '```\n ABC\n```') - await page.waitForTimeout(500) // wait for fill - await escapeToCodeEditor(page) - await page.type('.CodeMirror textarea', ' DEF\nGHI') - - await page.waitForTimeout(500) - await page.press('.CodeMirror textarea', 'Escape') - await page.waitForTimeout(500) - // NOTE: auto-indent is on - expect(await page.inputValue('.block-editor textarea')).toBe('```\n ABC DEF\n GHI\n```') -}) - -test('code block with text around', async ({ page }) => { - await createRandomPage(page) - - await page.fill('.block-editor textarea', 'Heading\n```\n```\nFooter') - await page.waitForTimeout(200) - await escapeToCodeEditor(page) - await page.type('.CodeMirror textarea', 'first\n second') - - await page.waitForTimeout(500) - await page.press('.CodeMirror textarea', 'Escape') - await page.waitForTimeout(500) - expect(await page.inputValue('.block-editor textarea')).toBe('Heading\n```\nfirst\n second\n```\nFooter') -}) - -test('multiple code block', async ({ page }) => { - await createRandomPage(page) - - // NOTE: the two code blocks are of the same content - await page.fill('.block-editor textarea', 'δΈ­ζ–‡ Heading\n```clojure\n```\nMiddle πŸš€\n```clojure\n```\nFooter') - await page.waitForTimeout(200) - - await page.press('.block-editor textarea', 'Escape') - await page.waitForSelector('.CodeMirror pre', { state: 'visible' }) - - // first - await page.waitForTimeout(500) - await page.click('.CodeMirror pre >> nth=0') - await page.waitForTimeout(500) - - await page.type('.CodeMirror textarea >> nth=0', ':key-test\n', { strict: true }) - await page.waitForTimeout(500) - - await page.press('.CodeMirror textarea >> nth=0', 'Escape') - await page.waitForTimeout(500) - expect(await page.inputValue('.block-editor textarea')) - .toBe('δΈ­ζ–‡ Heading\n```clojure\n:key-test\n\n```\nMiddle πŸš€\n```clojure\n```\nFooter') - - // second - await page.press('.block-editor textarea', 'Escape') - await page.waitForSelector('.CodeMirror pre', { state: 'visible' }) - - await page.waitForTimeout(500) - await page.click('.CodeMirror >> nth=1 >> pre') - await page.waitForTimeout(500) - - await page.type('.CodeMirror textarea >> nth=1', '\n :key-test ζ—₯本θͺž\n', { strict: true }) - await page.waitForTimeout(500) - - await page.press('.CodeMirror textarea >> nth=1', 'Escape') - await page.waitForTimeout(500) - expect(await page.inputValue('.block-editor textarea')) - .toBe('δΈ­ζ–‡ Heading\n```clojure\n:key-test\n\n```\nMiddle πŸš€\n```clojure\n\n :key-test ζ—₯本θͺž\n\n```\nFooter') -}) - -test('click outside to exit', async ({ page }) => { - await createRandomPage(page) - - await page.fill('.block-editor textarea', 'Header ``Click``\n```\n ABC\n```') - await page.waitForTimeout(200) // wait for fill - await escapeToCodeEditor(page) - await page.type('.CodeMirror textarea', ' DEF\nGHI') - - await page.waitForTimeout(500) - await page.click('text=Click') - await page.waitForTimeout(500) - // NOTE: auto-indent is on - expect(await page.inputValue('.block-editor textarea')).toBe('Header ``Click``\n```\n ABC DEF\n GHI\n```') -}) - -test('click language label to exit #3463', async ({ page, block }) => { - await createRandomPage(page) - - await page.fill('.block-editor textarea', '```cpp\n```') - await page.waitForTimeout(200) - await escapeToCodeEditor(page) - await page.type('.CodeMirror textarea', '#include') - - await page.waitForTimeout(500) - await page.click('text=cpp') // the language label - await page.waitForTimeout(500) - expect(await page.inputValue('.block-editor textarea')).toBe('```cpp\n#include\n```') -}) - -test('multi properties with code', async ({ page }) => { - await createRandomPage(page) - - await page.fill('.block-editor textarea', - 'type:: code\n' + - 'η±»εž‹:: 代码\n' + - '```go\n' + - 'if err != nil {\n' + - '\treturn err\n' + - '}\n' + - '```' - ) - await page.waitForTimeout(200) - await escapeToCodeEditor(page) - - // first character of code - await page.click('.CodeMirror pre', { position: { x: 1, y: 5 } }) - await page.waitForTimeout(500) - await page.type('.CodeMirror textarea', '// Returns nil\n') - - await page.waitForTimeout(500) - await page.press('.CodeMirror textarea', 'Escape') - await page.waitForTimeout(500) - expect(await page.inputValue('.block-editor textarea')).toBe( - 'type:: code\n' + - 'η±»εž‹:: 代码\n' + - '```go\n' + - '// Returns nil\n' + - 'if err != nil {\n' + - '\treturn err\n' + - '}\n' + - '```' - ) -}) - -test('Select codeblock language', async ({ page }) => { - await createRandomPage(page) - - // Open the slash command menu - await page.type('textarea >> nth=0', '/code block', { delay: 20 }) - - expect( - await page.waitForSelector('[data-modal-name="commands"]', { - state: 'visible', - }) - ).toBeTruthy() - - // Select `code block` command and open the language dropdown menu - await page.press('textarea >> nth=0', 'Enter', { delay: 10 }) - // wait for the modal to open - expect( - await page.waitForSelector('[data-modal-name="select-code-block-mode"]', { - state: 'visible', - }) - ).toBeTruthy() - - // Select Clojure from the dropdown menu - await repeatKeyPress(page, 'ArrowDown', 6) - await page.press('textarea >> nth=0', 'Enter', { delay: 10 }) - await page.waitForTimeout(100) - // expect the codeblock to be visible - expect(await page.waitForSelector('.CodeMirror', { state: 'visible' })) - - // Exit codeblock and return to block edit mode - await page.press('.CodeMirror textarea >> nth=0', 'Escape', { delay: 10 }) - - expect(await page.inputValue('.block-editor textarea')).toBe( - '```clojure\n```' - ) -}) - -test('Select codeblock language while surrounded by text', async ({ page }) => { - await createRandomPage(page) - await page.type('textarea >> nth=0', 'ABC XYZ', { delay: 20 }) - await repeatKeyPress(page, 'ArrowLeft', 3) - - // Open the slash command menu - await page.type('textarea >> nth=0', '/code block', { delay: 20 }) - - expect( - await page.waitForSelector('[data-modal-name="commands"]', { - state: 'visible', - }) - ).toBeTruthy() - - // Select `code block` command and open the language dropdown menu - await page.press('textarea >> nth=0', 'Enter', { delay: 10 }) - // wait for the modal to open - expect( - await page.waitForSelector('[data-modal-name="select-code-block-mode"]', { - state: 'visible', - }) - ).toBeTruthy() - - // Select Clojure from the dropdown menu - await repeatKeyPress(page, 'ArrowDown', 6) - await page.press('textarea >> nth=0', 'Enter', { delay: 10 }) - // expect the codeblock to be visible - expect(await page.waitForSelector('.CodeMirror', { state: 'visible' })) - - // Exit codeblock and return to block edit mode - await page.press('.CodeMirror textarea >> nth=0', 'Escape', { delay: 10 }) - - expect(await page.inputValue('.block-editor textarea')).toBe( - 'ABC \n```clojure\n```\nXYZ' - ) -}) diff --git a/e2e-tests/context-menu.spec.ts b/e2e-tests/context-menu.spec.ts deleted file mode 100644 index 70effcdb67..0000000000 --- a/e2e-tests/context-menu.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { createRandomPage } from './utils' - -test('open context menu', async ({ page }) => { - await createRandomPage(page) - - await page.locator('span.bullet-container >> nth=0').click({button: "right"}) - - await expect(page.locator('#custom-context-menu')).toBeVisible() -}) - -test('close context menu on esc', async ({ page }) => { - await createRandomPage(page) - - await page.locator('span.bullet-container >> nth=0').click({button: "right"}) - - await page.keyboard.press('Escape') - - await expect(page.locator('#custom-context-menu')).toHaveCount(0) -}) - -test('close context menu by left clicking on empty space', async ({ page }) => { - await createRandomPage(page) - - await page.locator('span.bullet-container >> nth=0').click({button: "right"}) - - await page.mouse.click(0, 200, {button: "left"}) - - await expect(page.locator('#custom-context-menu')).toHaveCount(0) -}) - -test('close context menu by clicking on a menu item', async ({ page }) => { - await createRandomPage(page) - - await page.locator('span.bullet-container >> nth=0').click({button: "right"}) - - await page.locator('#custom-context-menu .menu-link >> nth=1').click() - - await expect(page.locator('#custom-context-menu')).toHaveCount(0) -}) - -test('close context menu by clicking on a block', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustType('fist Block') - await block.enterNext() - - await page.locator('span.bullet-container >> nth=-1').click({button: "right"}) - - const elementHandle = page.locator('.block-content >> nth=0'); - - const box = await elementHandle.boundingBox(); - expect(box).toBeTruthy() - if (box) { - await page.mouse.click(box.x + box.width - 5, box.y + box.height / 2); - } - - await expect(page.locator('#custom-context-menu')).toHaveCount(0) -}) diff --git a/e2e-tests/dnd.spec.ts b/e2e-tests/dnd.spec.ts deleted file mode 100644 index f17dadab3e..0000000000 --- a/e2e-tests/dnd.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { createRandomPage, enterNextBlock } from './utils' - -/** - * Drag and Drop tests. - * - * NOTE: x = 30 is an estimation of left position of the drop target. - */ - -test('drop to left center', async ({ page }) => { - await createRandomPage(page) - - await page.fill('textarea >> nth=0', 'block a') - await enterNextBlock(page) - - 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('.ls-block >> nth=0') - await bullet.dragTo(where, { - targetPosition: { - x: 30, - y: (await where.boundingBox()).height * 0.5 - } - }) - - 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, block }) => { - await createRandomPage(page) - - await block.mustFill('block a') - await block.enterNext() - - await block.mustFill('block b') - await block.escapeEditing() - - const bullet = page.locator('span.bullet-container >> nth=-1') - const where = page.locator('.ls-block >> nth=0') - await bullet.dragTo(where, { - targetPosition: { - x: 0, - y: 0 - } - }) - - 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('textarea >> nth=0', 'block a') - await enterNextBlock(page) - - 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('.ls-block >> nth=0') - await bullet.dragTo(where, { - targetPosition: { - x: 30, - y: (await where.boundingBox()).height * 0.75 - } - }) - - await page.keyboard.press('Escape') - - const pageElem = page.locator('.page-blocks-inner') - await expect(pageElem).toHaveText('block a\nblock b', {useInnerText: true}) -}) diff --git a/e2e-tests/editor.spec.ts b/e2e-tests/editor.spec.ts deleted file mode 100644 index 828eefbd74..0000000000 --- a/e2e-tests/editor.spec.ts +++ /dev/null @@ -1,865 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { - createRandomPage, - enterNextBlock, - modKey, - repeatKeyPress, - moveCursor, - selectCharacters, - getSelection, - getCursorPos, -} from './utils' -import { dispatch_kb_events } from './util/keyboard-events' -import * as kb_events from './util/keyboard-events' - -test('hashtag and quare brackets in same line #4178', async ({ page }) => { - try { - await page.waitForSelector('.notification-clear', { timeout: 10 }) - page.click('.notification-clear') - } catch (error) { - } - - await createRandomPage(page) - - 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('textarea >> nth=0', 'ArrowLeft') - } - await page.type('textarea >> nth=0', ' ') - await page.press('textarea >> nth=0', 'ArrowLeft') - - await page.type('textarea >> nth=0', '#') - await page.waitForSelector('text="Search for a page"', { state: 'visible' }) - - await page.type('textarea >> nth=0', 'fo') - - await page.click('.absolute >> text=' + 'foo') - - expect(await page.inputValue('textarea >> nth=0')).toBe( - '#foo bar [[blah]]' - ) -}) - -test('hashtag search page auto-complete', async ({ page, block }) => { - await createRandomPage(page) - - await block.activeEditing(0) - - await page.type('textarea >> nth=0', '#', { delay: 100 }) - await page.waitForSelector('text="Search for a page"', { state: 'visible' }) - await page.keyboard.press('Escape', { delay: 50 }) - - await block.mustFill("done") - - await enterNextBlock(page) - await page.type('textarea >> nth=0', 'Some #', { delay: 100 }) - await page.waitForSelector('text="Search for a page"', { state: 'visible' }) - await page.keyboard.press('Escape', { delay: 50 }) - - await block.mustFill("done") -}) - -test('hashtag search #[[ page auto-complete', async ({ page, block }) => { - await createRandomPage(page) - - await block.activeEditing(0) - - await page.type('textarea >> nth=0', '#[[', { delay: 100 }) - await page.waitForSelector('text="Search for a page"', { state: 'visible' }) - await page.keyboard.press('Escape', { delay: 50 }) -}) - -test('disappeared children #4814', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustType('parent') - await block.enterNext() - expect(await block.indent()).toBe(true) - - for (let i = 0; i < 5; i++) { - await block.mustType(i.toString()) - await block.enterNext() - } - - // collapse - await page.click('.block-control >> nth=0') - - // expand - await page.click('.block-control >> nth=0') - - await block.waitForBlocks(7) // 1 + 5 + 1 empty - - // Ensures there's no active editor - await expect(page.locator('.editor-inner')).toHaveCount(0, { timeout: 500 }) -}) - -test('create new page from bracketing text #4971', async ({ page, block }) => { - let title = 'Page not Exists yet' - await createRandomPage(page) - - await block.mustType(`[[${title}]]`) - - await page.keyboard.press(modKey + '+o') - - // Check page title equals to `title` - await page.waitForTimeout(100) - expect(await page.locator('h1.title').innerText()).toContain(title) - - // Check there're linked references - await page.waitForSelector(`.references .ls-block >> nth=1`, { state: 'detached', timeout: 100 }) -}) - -test.skip('backspace and cursor position #4897', async ({ page, block }) => { - await createRandomPage(page) - - // Delete to previous block, and check cursor position, 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 position, 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 }) -}) - -test( - "Press CJK Left Black Lenticular Bracket `【` by 2 times #3251 should trigger [[]], " + - "but dont trigger RIME #3440 ", - // cases should trigger [[]] #3251 - async ({ page, block }) => { - // This test requires dev mode - test.skip(process.env.RELEASE === 'true', 'not available for release version') - - // @ts-ignore - for (let [idx, events] of [ - kb_events.win10_pinyin_left_full_square_bracket, - kb_events.macos_pinyin_left_full_square_bracket - // TODO: support #3741 - // kb_events.win10_legacy_pinyin_left_full_square_bracket, - ].entries()) { - await createRandomPage(page) - let check_text = "#3251 test " + idx - 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 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 + '[[]]') - }; - - // @ts-ignore dont trigger RIME #3440 - for (let [idx, events] of [ - kb_events.macos_pinyin_selecting_candidate_double_left_square_bracket, - kb_events.win10_RIME_selecting_candidate_double_left_square_bracket - ].entries()) { - await createRandomPage(page) - let check_text = "#3440 test " + idx - 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) - expect(await page.inputValue(':nth-match(textarea, 1)')).toBe(check_text) - } - }) - -test('copy & paste block ref and replace its content', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustType('Some random text') - - await page.keyboard.press(modKey + '+c') - await page.waitForTimeout(200) - await page.press('textarea >> nth=0', 'Enter') - await page.waitForTimeout(100) - await block.waitForBlocks(2) - await page.waitForTimeout(100) - await page.keyboard.press(modKey + '+v', { delay: 100 }) - await page.waitForTimeout(100) - await page.keyboard.press('Enter', { delay: 100 }) - - // Check if the newly created block-ref has the same referenced content - await expect(page.locator('.block-ref >> text="Some random text"')).toHaveCount(1); - - // Move cursor into the block ref - for (let i = 0; i < 4; i++) { - await page.press('textarea >> nth=0', 'ArrowLeft', { delay: 10 }) - } - - await expect(page.locator('textarea >> nth=0')).not.toHaveValue('Some random text') - - for (let i = 0; i < 4; i++) { - await page.press('textarea >> nth=0', 'ArrowLeft', { delay: 10 } ) - } - - // Trigger replace-block-reference-with-content-at-point - await page.keyboard.press(modKey + '+Shift+r') - - await expect(page.locator('textarea >> nth=0')).toHaveValue('Some random text') - - await block.escapeEditing() - - await expect(page.locator('.block-ref >> text="Some random text"')).toHaveCount(0); - await expect(page.locator('text="Some random text"')).toHaveCount(2); -}) - -test('copy and paste block after editing new block #5962', async ({ page, block }) => { - await createRandomPage(page) - - // Create a block and copy it in block-select mode - await block.mustType('Block being copied') - await page.keyboard.press('Escape') - await expect(page.locator('.ls-block.selected')).toHaveCount(1) - - await page.keyboard.press(modKey + '+c', { delay: 100 }) - - await page.keyboard.press('Enter') - await expect(page.locator('.ls-block.selected')).toHaveCount(0) - await expect(page.locator('textarea >> nth=0')).toBeVisible() - await page.keyboard.press('Enter') - await block.waitForBlocks(2) - - await page.waitForTimeout(100) - - await block.mustType('Typed block') - - await page.keyboard.press(modKey + '+v') - await expect(page.locator('text="Typed block"')).toHaveCount(1) - await block.waitForBlocks(3) -}) - -test('undo and redo after starting an action should not destroy text #6267', async ({ page, block }) => { - await createRandomPage(page) - - // Get one piece of undo state onto the stack - await block.mustType('text1 ') - await page.waitForTimeout(1000) // auto save - - // Then type more, start an action prompt, and undo - await page.keyboard.type('text2 [[', { delay: 50 }) - - await expect(page.locator(`[data-modal-name="page-search"]`)).toBeVisible() - - await page.waitForTimeout(1000) // auto save - - await page.keyboard.press(modKey + '+z', { delay: 100 }) - - // Should close the action menu when we undo the action prompt - // await expect(page.locator(`[data-modal-name="page-search"]`)).not.toBeVisible() - - // It should undo to the last saved state, and not erase the previous undo action too - await expect(page.locator('text="text1"')).toHaveCount(1) - - // And it should keep what was undone as a redo action - await page.keyboard.press(modKey + '+Shift+z') - await expect(page.locator('text="text1 text2 [[]]"')).toHaveCount(1) -}) - -test('undo after starting an action should close the action menu #6269', async ({ page, block }) => { - for (const [commandTrigger, modalName] of [['/', 'commands'], ['[[', 'page-search']]) { - await createRandomPage(page) - - // Open the action modal - await block.mustType('text1 ') - await page.waitForTimeout(550) - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await page.waitForTimeout(100) // Tolerable delay for the action menu to open - await expect(page.locator(`[data-modal-name="${modalName}"]`)).toBeVisible() - - // Undo, removing "/today", and closing the action modal - await page.keyboard.press(modKey + '+z', { delay: 100 }) - - await expect(page.locator('text="/today"')).toHaveCount(0) - await expect(page.locator(`[data-modal-name="${modalName}"]`)).not.toBeVisible() - } -}) - -test('#6266 moving cursor outside of brackets should close autocomplete menu', async ({ page, block, autocompleteMenu }) => { - for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) { - // First, left arrow - await createRandomPage(page) - - await block.mustFill('t ') - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await page.waitForTimeout(100) // Sometimes it doesn't trigger without this - await autocompleteMenu.expectVisible(modalName) - - await page.keyboard.press('ArrowLeft') - await page.waitForTimeout(100) - await autocompleteMenu.expectHidden(modalName) - - // Then, right arrow - await createRandomPage(page) - - await block.mustFill('t ') - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await autocompleteMenu.expectVisible(modalName) - - await page.waitForTimeout(100) - // Move cursor outside of the space strictly between the double brackets - await page.keyboard.press('ArrowRight') - await page.waitForTimeout(100) - await autocompleteMenu.expectHidden(modalName) - } -}) - -// Old logic would fail this because it didn't do the check if @search-timeout was set -test('#6266 moving cursor outside of parens immediately after searching should still close autocomplete menu', async ({ page, block, autocompleteMenu }) => { - for (const [commandTrigger, modalName] of [['((', 'block-search']]) { - await createRandomPage(page) - - // Open the autocomplete menu - await block.mustFill('t ') - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await page.waitForTimeout(100) - await page.keyboard.type("some block search text") - await page.waitForTimeout(100) // Sometimes it doesn't trigger without this - await autocompleteMenu.expectVisible(modalName) - - // Move cursor outside of the space strictly between the double parens - await page.keyboard.press('ArrowRight') - await page.waitForTimeout(100) - await autocompleteMenu.expectHidden(modalName) - } -}) - -test('pressing up and down should NOT close autocomplete menu', async ({ page, block, autocompleteMenu }) => { - for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) { - await createRandomPage(page) - - // Open the autocomplete menu - await block.mustFill('t ') - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await autocompleteMenu.expectVisible(modalName) - const cursorPos = await block.selectionStart() - - await page.keyboard.press('ArrowUp') - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - await expect(await block.selectionStart()).toEqual(cursorPos) - - await page.keyboard.press('ArrowDown') - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - await expect(await block.selectionStart()).toEqual(cursorPos) - } -}) - -test('moving cursor inside of brackets should NOT close autocomplete menu', async ({ page, block, autocompleteMenu }) => { - for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) { - await createRandomPage(page) - - // Open the autocomplete menu - await block.mustType('test ') - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await page.waitForTimeout(100) - if (commandTrigger === '[[') { - await autocompleteMenu.expectVisible(modalName) - } - - await page.keyboard.type("search", { delay: 20 }) - await autocompleteMenu.expectVisible(modalName) - - // Move cursor, still inside the brackets - await page.keyboard.press('ArrowLeft') - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - } -}) - -test('moving cursor inside of brackets when autocomplete menu is closed should NOT open autocomplete menu', async ({ page, block, autocompleteMenu }) => { - // Note: (( behaves differently and doesn't auto-trigger when typing in it after exiting the search prompt once - for (const [commandTrigger, modalName] of [['[[', 'page-search']]) { - await createRandomPage(page) - - // Open the autocomplete menu - await block.mustFill('') - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await page.waitForTimeout(100) // Sometimes it doesn't trigger without this - await autocompleteMenu.expectVisible(modalName) - - await block.escapeEditing() - await autocompleteMenu.expectHidden(modalName) - - // Move cursor left until it's inside the brackets; shouldn't open autocomplete menu - await page.locator('.block-content').click() - await page.waitForTimeout(100) - await autocompleteMenu.expectHidden(modalName) - - await page.keyboard.press('ArrowLeft', { delay: 50 }) - await autocompleteMenu.expectHidden(modalName) - - await page.keyboard.press('ArrowLeft', { delay: 50 }) - await autocompleteMenu.expectHidden(modalName) - - // Type a letter, this should open the autocomplete menu - await page.keyboard.type('z', { delay: 20 }) - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - } -}) - -test('selecting text inside of brackets should NOT close autocomplete menu', async ({ page, block, autocompleteMenu }) => { - for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) { - await createRandomPage(page) - - // Open the autocomplete menu - await block.mustFill('') - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - - await page.keyboard.type("some page search text", { delay: 10 }) - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - - // Select some text within the brackets - await page.keyboard.press('Shift+ArrowLeft') - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - } -}) - -test('pressing backspace and remaining inside of brackets should NOT close autocomplete menu', async ({ page, block, autocompleteMenu }) => { - for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['((', 'block-search']]) { - await createRandomPage(page) - - // Open the autocomplete menu - await block.mustFill('test ') - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - - await page.keyboard.type("some page search text", { delay: 10 }) - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - - // Delete one character inside the brackets - await page.keyboard.press('Backspace') - await page.waitForTimeout(100) - await autocompleteMenu.expectVisible(modalName) - } -}) - -test('press escape when autocomplete menu is open, should close autocomplete menu only #6270', async ({ page, block }) => { - for (const [commandTrigger, modalName] of [['[[', 'page-search'], ['/', 'commands']]) { - await createRandomPage(page) - - // Open the action modal - await block.mustFill('text ') - await page.waitForTimeout(550) - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await page.waitForTimeout(100) - await expect(page.locator(`[data-modal-name="${modalName}"]`)).toBeVisible() - await page.waitForTimeout(100) - - // Press escape; should close action modal instead of exiting edit mode - await page.keyboard.press('Escape') - await page.waitForTimeout(100) - await expect(page.locator(`[data-modal-name="${modalName}"]`)).not.toBeVisible() - await page.waitForTimeout(1000) - expect(await block.isEditing()).toBe(true) - } -}) - -test('press escape when link/image dialog is open, should restore focus to input', async ({ page, block }) => { - for (const [commandTrigger, modalName] of [['/link', 'commands']]) { - await createRandomPage(page) - - // Open the action modal - await block.mustFill('') - await page.waitForTimeout(550) - await page.keyboard.type(commandTrigger, { delay: 20 }) - - await page.waitForTimeout(100) - await expect(page.locator(`[data-modal-name="${modalName}"]`)).toBeVisible() - await page.waitForTimeout(100) - - // Press enter to open the link dialog - await page.keyboard.press('Enter') - await expect(page.locator(`[data-modal-name="input"]`)).toBeVisible() - - // Press escape; should close link dialog and restore focus to the block textarea - await page.keyboard.press('Escape') - await page.waitForTimeout(100) - await expect(page.locator(`[data-modal-name="input"]`)).not.toBeVisible() - await page.waitForTimeout(1000) - expect(await block.isEditing()).toBe(true) - } -}) - -test('should show text after soft return when node is collapsed #5074', async ({ page, block }) => { - const delay = 300 - await createRandomPage(page) - - await page.type('textarea >> nth=0', 'Before soft return', { delay: 10 }) - await page.keyboard.press('Shift+Enter', { delay: 10 }) - await page.type('textarea >> nth=0', 'After soft return', { delay: 10 }) - - await block.enterNext() - expect(await block.indent()).toBe(true) - await block.mustType('Child text') - - // collapse - await page.click('.block-control >> nth=0') - await block.waitForBlocks(1) - - // select the block that has the soft return - await page.keyboard.press('ArrowDown') - await page.waitForTimeout(delay) - await page.keyboard.press('Enter') - await page.waitForTimeout(delay) - - await expect(page.locator('textarea >> nth=0')).toHaveText('Before soft return\nAfter soft return') - - // zoom into the block - page.click('a.block-control + a') - await page.waitForNavigation() - await page.waitForTimeout(delay * 3) - - // select the block that has the soft return - await page.keyboard.press('ArrowDown') - await page.waitForTimeout(delay) - await page.keyboard.press('Enter') - await page.waitForTimeout(delay) - - await expect(page.locator('textarea >> nth=0')).toHaveText('Before soft return\nAfter soft return') -}) - -test('should not erase typed text when expanding block quickly after typing #3891', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustFill('initial text,') - await page.waitForTimeout(1000) - await page.type('textarea >> nth=0', ' then expand', { delay: 10 }) - // A quick cmd-down must not destroy the typed text - await page.keyboard.press(modKey + '+ArrowDown') - expect(await page.inputValue('textarea >> nth=0')).toBe( - 'initial text, then expand' - ) - - await page.waitForTimeout(1000) - - // First undo should delete the last typed information, not undo a no-op expand action - await page.keyboard.press(modKey + '+z', { delay: 100 }) - expect(await page.inputValue('textarea >> nth=0')).toBe( - 'initial text,' - ) - - await page.keyboard.press(modKey + '+z') - await page.waitForTimeout(100) - expect(await page.inputValue('textarea >> nth=0')).toBe( - '' - ) -}) - -test('should keep correct undo and redo seq after indenting or outdenting the block #7615',async({page,block}) => { - await createRandomPage(page) - - await block.mustFill("foo") - - await page.keyboard.press("Enter") - await expect(page.locator('textarea >> nth=0')).toHaveText("") - await page.waitForTimeout(100) - await block.indent() - await page.waitForTimeout(100) - await block.mustFill("bar") - await expect(page.locator('textarea >> nth=0')).toHaveText("bar") - - // await page.keyboard.press(modKey + '+z') - // // should undo "bar" input - // await expect(page.locator('textarea >> nth=0')).toHaveText("") - - // await page.keyboard.press(modKey + '+Shift+z', { delay: 100 }) - - // // should redo "bar" input - // await expect(page.locator('textarea >> nth=0')).toHaveText("bar") - // await page.keyboard.press("Shift+Tab", { delay: 100 }) - - // await page.keyboard.press("Enter", { delay: 100 }) - // await expect(page.locator('textarea >> nth=0')).toHaveText("") - - // #7615 - await enterNextBlock(page) - await page.keyboard.type("aaa") - await block.indent() - await page.waitForTimeout(550) - await page.keyboard.type(" bbb") - await page.waitForTimeout(550) - await expect(page.locator('textarea >> nth=0')).toHaveText("aaa bbb") - await page.keyboard.press(modKey + '+z') - await page.waitForTimeout(100) - await expect(page.locator('textarea >> nth=0')).toHaveText("aaa") - await page.keyboard.press(modKey + '+Shift+z') - await page.waitForTimeout(100) - await expect(page.locator('textarea >> nth=0')).toHaveText("aaa bbb") -}) - -test.describe('Text Formatting', () => { - const formats = [ - { name: 'bold', prefix: '**', postfix: '**', shortcut: modKey + '+b' }, - { name: 'italic', prefix: '*', postfix: '*', shortcut: modKey + '+i' }, - { - name: 'strikethrough', - prefix: '~~', - postfix: '~~', - shortcut: modKey + '+Shift+s', - }, - // { - // name: 'underline', - // prefix: '', - // postfix: '', - // shortcut: modKey + '+u', - // }, - ] - - for (const format of formats) { - test.describe(`${format.name} formatting`, () => { - test('Applying to an empty selection inserts placeholder formatting and places cursor correctly', async ({ - page, - block, - }) => { - await createRandomPage(page) - - const text = 'Lorem ipsum' - await block.mustFill(text) - - // move the cursor to the end of Lorem - await repeatKeyPress(page, 'ArrowLeft', text.length - 'ipsum'.length) - await page.keyboard.press('Space') - - // Apply formatting - await page.keyboard.press(format.shortcut) - - await expect(page.locator('textarea >> nth=0')).toHaveText( - `Lorem ${format.prefix}${format.postfix} ipsum` - ) - - // Verify cursor position - const cursorPos = await getCursorPos(page) - expect(cursorPos).toBe(' ipsum'.length + format.prefix.length) - }) - - test('Applying to an entire block encloses the block in formatting and places cursor correctly', async ({ - page, - block, - }) => { - await createRandomPage(page) - - const text = 'Lorem ipsum-dolor sit.' - await block.mustFill(text) - - // Select the entire block - await page.keyboard.press(modKey + '+a') - - // Apply formatting - await page.keyboard.press(format.shortcut) - - await expect(page.locator('textarea >> nth=0')).toHaveText( - `${format.prefix}${text}${format.postfix}` - ) - - // Verify cursor position - const cursorPosition = await getCursorPos(page) - expect(cursorPosition).toBe(format.prefix.length + text.length) - }) - - test('Applying and then removing from a word connected with a special character correctly formats and then reverts', async ({ - page, - block, - }) => { - await createRandomPage(page) - - await block.mustFill('Lorem ipsum-dolor sit.') - - // Select 'ipsum' - // Move the cursor to the desired position - await moveCursor(page, -16) - - // Select the desired length of text - await selectCharacters(page, 5) - - // Apply formatting - await page.keyboard.press(format.shortcut) - - // Verify that 'ipsum' is formatted - await expect(page.locator('textarea >> nth=0')).toHaveText( - `Lorem ${format.prefix}ipsum${format.postfix}-dolor sit.` - ) - - // Re-select 'ipsum' - // Move the cursor to the desired position - await moveCursor(page, -5) - - // Select the desired length of text - await selectCharacters(page, 5) - - // Remove formatting - await page.keyboard.press(format.shortcut) - await expect(page.locator('textarea >> nth=0')).toHaveText( - 'Lorem ipsum-dolor sit.' - ) - - // Verify the word 'ipsum' is still selected - const selection = await getSelection(page) - expect(selection).toBe('ipsum') - }) - }) - } -}) - -test.describe('Always auto-pair symbols', () => { - // Define the symbols that should be auto-paired - const autoPairSymbols = [ - { name: 'square brackets', prefix: '[', postfix: ']' }, - { name: 'curly brackets', prefix: '{', postfix: '}' }, - { name: 'parentheses', prefix: '(', postfix: ')' }, - // { name: 'angle brackets', prefix: '<', postfix: '>' }, - { name: 'backtick', prefix: '`', postfix: '`' }, - // { name: 'single quote', prefix: "'", postfix: "'" }, - // { name: 'double quote', prefix: '"', postfix: '"' }, - ] - - for (const symbol of autoPairSymbols) { - test(`${symbol.name} auto-pairing`, async ({ page }) => { - await createRandomPage(page) - - // Type prefix and check that the postfix is automatically added - page.type('textarea >> nth=0', symbol.prefix, { delay: 100 }) - await expect(page.locator('textarea >> nth=0')).toHaveText( - `${symbol.prefix}${symbol.postfix}` - ) - - // Check that the cursor is positioned correctly between the prefix and postfix - const CursorPos = await getCursorPos(page) - expect(CursorPos).toBe(symbol.prefix.length) - }) - } -}) - -test.describe('Auto-pair symbols only with text selection', () => { - const autoPairSymbols = [ - // { name: 'tilde', prefix: '~', postfix: '~' }, - { name: 'asterisk', prefix: '*', postfix: '*' }, - { name: 'underscore', prefix: '_', postfix: '_' }, - { name: 'caret', prefix: '^', postfix: '^' }, - { name: 'equal', prefix: '=', postfix: '=' }, - { name: 'slash', prefix: '/', postfix: '/' }, - { name: 'plus', prefix: '+', postfix: '+' }, - ] - - for (const symbol of autoPairSymbols) { - test(`Only auto-pair ${symbol.name} with text selection`, async ({ - page, - block, - }) => { - await createRandomPage(page) - - // type the symbol - page.type('textarea >> nth=0', symbol.prefix, { delay: 100 }) - - // Verify that there is no auto-pairing - await expect(page.locator('textarea >> nth=0')).toHaveText(symbol.prefix) - - // remove prefix - await page.keyboard.press('Backspace') - - // add text - await block.mustType('Lorem') - // select text - await page.keyboard.press(modKey + '+a') - - // Type the prefix - await page.type('textarea >> nth=0', symbol.prefix, { delay: 100 }) - - // Verify that an additional postfix was automatically added around 'Lorem' - await expect(page.locator('textarea >> nth=0')).toHaveText( - `${symbol.prefix}Lorem${symbol.postfix}` - ) - - // Verify 'Lorem' is selected - const selection = await getSelection(page) - expect(selection).toBe('Lorem') - }) - } -}) - -test('copy blocks should remove all ref-related values', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustFill('test') - await page.keyboard.press(modKey + '+c', { delay: 100 }) - await page.waitForTimeout(100) - await block.clickNext() - await page.keyboard.press(modKey + '+v', { delay: 100 }) - await expect(page.locator('.open-block-ref-link')).toHaveCount(1) - - await page.keyboard.press('ArrowUp', { delay: 10 }) - await page.waitForTimeout(100) - await page.keyboard.press('Escape') - await expect(page.locator('.ls-block.selected')).toHaveCount(1) - await page.keyboard.press(modKey + '+c', { delay: 100 }) - await block.clickNext() - await page.keyboard.press(modKey + '+v', { delay: 100 }) - await block.clickNext() // let 3rd block leave editing state - await expect(page.locator('.open-block-ref-link')).toHaveCount(1) -}) - -test('undo cut block should recover refs', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustFill('test') - await page.keyboard.press(modKey + '+c', { delay: 100 }) - await page.waitForTimeout(100) - await block.clickNext() - await page.keyboard.press(modKey + '+v', { delay: 100 }) - await expect(page.locator('.open-block-ref-link')).toHaveCount(1) - - await page.keyboard.press('ArrowUp', { delay: 10 }) - await page.waitForTimeout(100) - await page.keyboard.press('Escape') - await expect(page.locator('.ls-block.selected')).toHaveCount(1) - await page.keyboard.press(modKey + '+x', { delay: 100 }) - await expect(page.locator('.ls-block')).toHaveCount(1) - await page.keyboard.press(modKey + '+z') - await page.waitForTimeout(100) - await expect(page.locator('.ls-block')).toHaveCount(2) - await expect(page.locator('.open-block-ref-link')).toHaveCount(1) -}) diff --git a/e2e-tests/fixtures.ts b/e2e-tests/fixtures.ts deleted file mode 100644 index e4a14d8d3d..0000000000 --- a/e2e-tests/fixtures.ts +++ /dev/null @@ -1,305 +0,0 @@ -import fs from 'fs' -import path from 'path' -import { test as base, expect, ConsoleMessage, Locator } from '@playwright/test'; -import { ElectronApplication, Page, BrowserContext, _electron as electron } from 'playwright' -import { loadLocalGraph, openLeftSidebar, randomString } from './utils'; -import { autocompleteMenu, LogseqFixtures } from './types'; - -let electronApp: ElectronApplication -let context: BrowserContext -let page: Page - -// For testing special characters in graph name / path -let repoName = "@" + randomString(10) -let testTmpDir = path.resolve(__dirname, '../tmp') - -if (fs.existsSync(testTmpDir)) { - fs.rmSync(testTmpDir, { recursive: true }) -} - -export let graphDir = path.resolve(testTmpDir, "#e2e-test", repoName) - -// NOTE: This following is a console log watcher for error logs. -// Save and print all logs when error happens. -let logs: string = ''; -const consoleLogWatcher = (msg: ConsoleMessage) => { - const text = msg.text(); - - // List of error messages to ignore - const ignoreErrors = [ - /net/, - /^Error with Permissions-Policy header:/ - ]; - - // If the text matches any of the ignoreErrors, return early - if (ignoreErrors.some(error => text.match(error))) { - console.log(`WARN:: ${text}\n`) - return; - } - - logs += text + '\n'; - expect(text, logs).not.toMatch(/^(Failed to|Uncaught|Assert failed)/); - expect(text, logs).not.toMatch(/^Error/); -} - -base.beforeAll(async () => { - if (electronApp) { - return - } - - console.log(`Creating test graph directory: ${graphDir}`) - fs.mkdirSync(graphDir, { - recursive: true, - }); - - electronApp = await electron.launch({ - 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 }); - await context.tracing.startChunk(); - - // NOTE: The following ensures App first start with the correct path. - const info = await electronApp.evaluate(async ({ app }) => { - - return { - "appPath": app.getAppPath(), - "appData": app.getPath("appData"), - "userData": app.getPath("userData"), - "appName": app.getName(), - "electronVersion": app.getVersion(), - } - }) - console.log("Test start with:", info) - - page = await electronApp.firstWindow() - - // inject testing flags - await page.evaluate( - () => { - Object.assign(window, { - __E2E_TESTING__: true, - }) - }, - ) - - // Direct Electron console to watcher - page.on('console', consoleLogWatcher) - page.on('crash', () => { - expect(false, "Page must not crash").toBeTruthy() - }) - page.on('pageerror', (err) => { - console.log(err) - // expect(false, 'Page must not have errors!').toBeTruthy() - }) - - await page.waitForLoadState('domcontentloaded') - // 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') - - await page.waitForSelector(':has-text("Loading")', { - state: "hidden", - timeout: 1000 * 15, - }); - - page.once('load', async () => { - console.log('Page loaded!') - await page.screenshot({ path: 'startup.png' }) - }) - - 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 () => { - // discard any dialog by ESC - if (page) { - await page.keyboard.press('Escape') - await page.keyboard.press('Escape') - - await expect(page.locator('.notification-close-button')).not.toBeVisible() - - if (await page.locator('.notification-clear button').isVisible()) { - await page.locator('.notification-clear button').click() - } - - const rightSidebar = page.locator('.cp__right-sidebar-inner') - if (await rightSidebar.isVisible()) { - await page.click('button.toggle-right-sidebar', {delay: 100}) - } - } -}) - -// hijack electron app into the test context -// FIXME: add type to `block` -export const test = base.extend({ - page: async ({ }, use) => { - await use(page); - }, - - // Timeout is used to avoid global timeout, local timeout will have a meaningful error report. - // 1s timeout is enough for most of the test cases. - // Timeout won't introduce additional sleeps. - block: async ({ page }, use) => { - const block = { - mustFill: async (value: string) => { - const locator: Locator = page.locator('textarea >> nth=0') - await locator.waitFor({ timeout: 1000 }) - await locator.fill(value) - await expect(locator).toHaveText(value, { timeout: 1000 }) - }, - mustType: async (value: string, options?: { delay?: number, toBe?: string }) => { - const locator: Locator = page.locator('textarea >> nth=0') - await locator.waitFor({ timeout: 1000 }) - const { delay = 50 } = options || {}; - const { toBe = value } = options || {}; - await locator.type(value, { delay }) - await expect(locator).toHaveText(toBe, { timeout: 1000 }) - }, - enterNext: async (): Promise => { - 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', timeout: 1000 }) - return page.locator('textarea >> nth=0') - }, - clickNext: async (): Promise => { - await page.$eval('.add-button-link-wrap', (element) => { - element.scrollIntoView(); - }); - let blockCount = await page.locator('.page-blocks-inner .ls-block').count() - // the next element after all blocks. - await page.click('.add-button-link-wrap', { delay: 100 }) - await page.waitForSelector(`.ls-block >> nth=${blockCount} >> textarea`, { state: 'visible', timeout: 1000 }) - return page.locator('textarea >> nth=0') - }, - indent: async (): Promise => { - const locator = page.locator('textarea >> nth=0') - const before = await locator.boundingBox() - await locator.press('Tab', { delay: 100 }) - return (await locator.boundingBox()).x > before.x - }, - unindent: async (): Promise => { - const locator = page.locator('textarea >> nth=0') - const before = await locator.boundingBox() - await locator.press('Shift+Tab', { delay: 100 }) - return (await locator.boundingBox()).x < before.x - }, - waitForBlocks: async (total: number): Promise => { - // NOTE: `nth=` counts from 0. - await page.waitForSelector(`.ls-block >> nth=${total - 1}`, { state: 'attached', timeout: 50000 }) - await page.waitForSelector(`.ls-block >> nth=${total}`, { state: 'detached', timeout: 50000 }) - }, - waitForSelectedBlocks: async (total: number): Promise => { - // NOTE: `nth=` counts from 0. - await page.waitForSelector(`.ls-block.selected >> nth=${total - 1}`, { timeout: 1000 }) - }, - escapeEditing: async (): Promise => { - const blockEdit = page.locator('.ls-block textarea >> nth=0') - while (await blockEdit.isVisible()) { - await page.keyboard.press('Escape') - } - const blockSelect = page.locator('.ls-block.selected') - while (await blockSelect.isVisible()) { - await page.keyboard.press('Escape') - } - }, - activeEditing: async (nth: number): Promise => { - await page.waitForSelector(`.ls-block >> nth=${nth}`, { timeout: 1000 }) - // scroll, for isVisible test - await page.$eval(`.ls-block >> nth=${nth}`, (element) => { - element.scrollIntoView(); - }); - // when blocks are nested, the first block(the parent) is selected. - if ( - (await page.isVisible(`.ls-block >> nth=${nth} >> .editor-wrapper >> textarea`)) && - !(await page.isVisible(`.ls-block >> nth=${nth} >> .block-children-container >> textarea`))) { - return; - } - await page.click(`.ls-block >> nth=${nth} >> .block-content`, { delay: 10, timeout: 100000 }) - await page.waitForSelector(`.ls-block >> nth=${nth} >> .editor-wrapper >> textarea`, { timeout: 1000, state: 'visible' }) - }, - isEditing: async (): Promise => { - const locator = page.locator('.ls-block textarea >> nth=0') - return await locator.isVisible() - }, - selectionStart: async (): Promise => { - return await page.locator('textarea >> nth=0').evaluate(node => { - const elem = node - return elem.selectionStart - }) - }, - selectionEnd: async (): Promise => { - return await page.locator('textarea >> nth=0').evaluate(node => { - const elem = node - return elem.selectionEnd - }) - } - } - use(block) - }, - - autocompleteMenu: async ({ }, use) => { - const autocompleteMenu: autocompleteMenu = { - expectVisible: async (modalName?: string) => { - const modal = page.locator(modalName ? `[data-modal-name="${modalName}"]` : `[data-modal-name]`) - if (await modal.isVisible()) { - await page.waitForTimeout(100) - await expect(modal).toBeVisible() - } else { - await modal.waitFor({ state: 'visible', timeout: 1000 }) - } - }, - expectHidden: async (modalName?: string) => { - const modal = page.locator(modalName ? `[data-modal-name="${modalName}"]` : `[data-modal-name]`) - if (!await modal.isVisible()) { - await page.waitForTimeout(100) - await expect(modal).not.toBeVisible() - } else { - await modal.waitFor({ state: 'hidden', timeout: 1000 }) - } - } - } - await use(autocompleteMenu) - }, - - context: async ({ }, use) => { - await use(context); - }, - app: async ({ }, use) => { - await use(electronApp); - }, - graphDir: async ({ }, use) => { - await use(graphDir); - }, -}); - - -let getTracingFilePath = function(): string { - return `e2e-dump/trace-${Date.now()}.zip.dump` -} - - -test.afterAll(async () => { - await context.tracing.stopChunk({ path: getTracingFilePath() }); -}) - - -/** - * Trace all tests in a file - */ -export let traceAll = function(){ - test.beforeAll(async () => { - await context.tracing.startChunk(); - }) - - test.afterAll(async () => { - await context.tracing.stopChunk({ path: getTracingFilePath() }); - }) -} diff --git a/e2e-tests/flashcards.spec.ts b/e2e-tests/flashcards.spec.ts deleted file mode 100644 index ad08355b64..0000000000 --- a/e2e-tests/flashcards.spec.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { createRandomPage } from './utils' - - -test('flashcard demo', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustFill('Why do you add cards? #card #logseq') - await block.enterNext() - expect(await block.indent()).toBe(true) - await block.mustFill('To augment our minds') - - await block.enterNext() - expect(await block.unindent()).toBe(true) - expect(await block.unindent()).toBe(false) - - await block.mustFill('How do you create clozes? #card #logseq') - await block.enterNext() - expect(await block.indent()).toBe(true) - - await block.mustType('/clo') - const popupMenuItem = page.locator('.absolute >> text=Cloze') - await popupMenuItem.waitFor({ timeout: 1000 }) // wait for electric-input - await popupMenuItem.click({ delay: 10 }) - await page.waitForTimeout(500) - - await page.type('textarea >> nth=0', 'Something') - await page.keyboard.press('ArrowRight') - await page.keyboard.press('ArrowRight') - - await page.type('textarea >> nth=0', ' like this') - - await block.enterNext() - expect(await block.unindent()).toBe(true) - - // navigate to another page, query cards - await createRandomPage(page) - - await block.mustFill('{{cards [[logseq]]}}') - await page.keyboard.press('Enter') - const queryCards = page.locator('text="No matched cards"') - await queryCards.waitFor({ state: 'hidden', timeout: 6000 }) - - const numberLabel = page.locator('.cards-title') - await numberLabel.waitFor({ state: 'visible' }) - expect(await numberLabel.innerText()).toMatch(/\[\[logseq\]\]\s+2\/2/) - - // DO NOT check number label for now - //const cardsNum = page.locator('.flashcards-nav span >> nth=1') - //expect(await cardsNum.innerText()).toBe('2') -}) diff --git a/e2e-tests/fs.spec.ts b/e2e-tests/fs.spec.ts deleted file mode 100644 index 324d114b92..0000000000 --- a/e2e-tests/fs.spec.ts +++ /dev/null @@ -1,141 +0,0 @@ -import fsp from 'fs/promises'; -import path from 'path'; -import { expect } from '@playwright/test' -import { test } from './fixtures'; -import { searchPage, captureConsoleWithPrefix, closeSearchBox, createPage, IsWindows, randomString } from './utils'; - -test('create file on disk then delete', async ({ page, block, graphDir }) => { - // Since have to wait for file watchers - test.slow(); - - // Special page names: namespaced, chars require escaping, chars require unicode normalization, "%" chars, "%" with 2 hexdigests - const testCases = [ - {pageTitle: "User:John", fileName: "User%3AJohn"}, - // invalid url decode escaping as %ff is not parsable but match the common URL encode regex - {pageTitle: "%ff", fileName: "%ff"}, - // valid url decode escaping - {pageTitle: "%23", fileName: "%2523"}, - {pageTitle: "@!%", fileName: "@!%"}, - {pageTitle: "aàÑÒ", fileName: "aàÑÒ"}, - {pageTitle: "%gggg", fileName: "%gggg"} - ] - if (!IsWindows) - testCases.push({pageTitle: "User:Bob", fileName: "User:Bob"}) - - function getFullPath(fileName: string) { - return path.join(graphDir, "pages", `${fileName}.md`); - } - - // Test putting files on disk - for (const {pageTitle, fileName} of testCases) { - // Put the file on disk - const filePath = getFullPath(fileName); - await fsp.writeFile(filePath, `- content for ${pageTitle}`); - await captureConsoleWithPrefix(page, "Parsing finished:", 5000) - - // Check that the page is created - const results = await searchPage(page, pageTitle); - const firstResultRow = await results[0].innerText() - expect(firstResultRow).toContain(pageTitle); - expect(firstResultRow).not.toContain("Create"); - await closeSearchBox(page); - } - - // Test removing files on disk - for (const {pageTitle, fileName} of testCases) { - // Remove the file on disk - const filePath = getFullPath(fileName); - await fsp.unlink(filePath); - await captureConsoleWithPrefix(page, "Delete page:", 5000); - - // Test that the page is deleted - const results = await searchPage(page, pageTitle); - const firstResultRow = await results[0].innerText() - // expect(firstResultRow).toContain("Create"); - await closeSearchBox(page); - } -}); - -test("Rename file on disk", async ({ page, block, graphDir }) => { - // Since have to wait for file watchers - test.slow(); - - const testCases = [ - // Normal -> NameSpace - {pageTitle: "User:John", fileName: "User%3AJohn", - newPageTitle: "User/John", newFileName: "User___John"}, - // NameSpace -> Normal - {pageTitle: "!/%23", fileName: "!___%2523", - newPageTitle: "%23", newFileName: "%2523"} - ] - if (!IsWindows) - testCases.push({pageTitle: "User:Bob", fileName: "User:Bob", - newPageTitle: "User/Bob", newFileName: "User___Bob"}) - - function getFullPath(fileName: string) { - return path.join(graphDir, "pages", `${fileName}.md`); - } - - // Test putting files on disk - for (const {pageTitle, fileName} of testCases) { - // Put the file on disk - const filePath = getFullPath(fileName); - await fsp.writeFile(filePath, `- content for ${pageTitle}`); - await captureConsoleWithPrefix(page, "Parsing finished:", 5000) - - // Check that the page is created - const results = await searchPage(page, pageTitle); - const firstResultRow = await results[0].innerText() - expect(firstResultRow).toContain(pageTitle); - expect(firstResultRow).not.toContain("Create"); - await closeSearchBox(page); - } - - // Test renaming files on disk - for (const {pageTitle, fileName, newPageTitle, newFileName} of testCases) { - // Rename the file on disk - const filePath = getFullPath(fileName); - const newFilePath = getFullPath(newFileName); - await fsp.rename(filePath, newFilePath); - await captureConsoleWithPrefix(page, "Parsing finished:", 5000); - - await page.waitForTimeout(500); - - // Test that the page is renamed - const results = await searchPage(page, newPageTitle); - const firstResultRow = await results[0].innerText() - expect(firstResultRow).toContain(newPageTitle); - expect(firstResultRow).not.toContain(pageTitle); - expect(firstResultRow).not.toContain("Create"); - await closeSearchBox(page); - } -}) - -test('special page names', async ({ page, block, graphDir }) => { - const testCases = [ - {pageTitle: "User:John", fileName: "User%3AJohn"}, - {pageTitle: "_%ff", fileName: "_%25ff"}, - {pageTitle: "@!%", fileName: "@!%"}, - {pageTitle: "aàÑÒ", fileName: "aàÑÒ"}, - {pageTitle: "_%gggg", fileName: "_%gggg"} - ] - - // Test putting files on disk - for (const {pageTitle, fileName} of testCases) { - const prefix = randomString(10) - const fullTitle = `${prefix}${pageTitle}` - // Create page in Logseq - await createPage(page, fullTitle) - const text = `content for ${pageTitle}` - await block.mustFill(text) - await page.keyboard.press("Enter", { delay: 50 }) - await page.keyboard.press("Escape", { delay: 50 }) - - // Wait for the file to be created on disk - await page.waitForTimeout(2500); - // Validate that the file is created on disk with the content - const filePath = path.join(graphDir, "pages", `${prefix}${fileName}.md`); - const fileContent = await fsp.readFile(filePath, "utf8"); - expect(fileContent).toContain(text); - } -}); diff --git a/e2e-tests/headings.spec.ts b/e2e-tests/headings.spec.ts deleted file mode 100644 index 960d8bbc84..0000000000 --- a/e2e-tests/headings.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { createRandomPage, editFirstBlock, newInnerBlock } from './utils' - -test('set heading to 1', async ({ page }) => { - await createRandomPage(page) - - await page.type('textarea >> nth=0', 'foo') - - await page.keyboard.press('Escape', { delay: 50 }) - - await page.locator('span.bullet-container >> nth=0').click({button: "right"}) - - await page.locator('#custom-context-menu .to-heading-button[title="Heading 1"]').click() - - await editFirstBlock(page) - await page.waitForTimeout(500) - - expect(await page.inputValue('textarea >> nth=0')).toBe('# foo') - - await page.keyboard.press('Escape', { delay: 50 }) - - expect(await page.locator('.ls-block .block-content >> nth=0').innerHTML()).toContain('

foo

') -}) - -test('remove heading', async ({ page }) => { - await page.locator('span.bullet-container >> nth=0').click({button: "right"}) - - await page.locator('#custom-context-menu .to-heading-button[title="Remove heading"]').click() - - expect(await page.locator('.ls-block .block-content >> nth=0').innerHTML()).toContain('foo') -}) - -test('set heading to 2', async ({ page }) => { - await page.locator('span.bullet-container >> nth=0').click({button: "right"}) - await page.locator('#custom-context-menu .to-heading-button[title="Heading 2"]').click({ delay: 400}) - - expect(await page.locator('.ls-block .block-content >> nth=0').innerHTML()).toContain('

foo

') -}) - -test('switch to auto heading', async ({ page }) => { - await page.locator('span.bullet-container >> nth=0').click({button: "right"}) - - await page.locator('#custom-context-menu .to-heading-button[title="Auto heading"]').click() - - await editFirstBlock(page) - await page.waitForTimeout(500) - - expect(await page.inputValue('textarea >> nth=0')).toBe('foo') - - await page.keyboard.press('Escape', { delay: 50 }) - - expect(await page.locator('.ls-block .block-content >> nth=0').innerHTML()).toContain('

foo

') -}) - -test('set heading of nested block to auto', async ({ page }) => { - await newInnerBlock(page) - await page.waitForTimeout(500) - - await page.type('textarea >> nth=0', 'bar') - - await page.keyboard.press("Tab", { delay: 100 }) - - await page.keyboard.press('Escape', { delay: 100 }) - - await page.locator('span.bullet-container >> nth=1').click({button: "right"}) - - await page.locator('#custom-context-menu .to-heading-button[title="Auto heading"]').click() - - await page.waitForTimeout(100) - - expect(await page.locator('.ls-block .block-content >> nth=1').innerHTML()).toContain('

bar

') -}) - -test('view nested block on a dedicated page', async ({ page }) => { - await page.locator('span.bullet-container >> nth=1').click() - await page.waitForTimeout(200) - - expect(await page.locator('.ls-block .block-content >> nth=0').innerHTML()).toContain('

bar

') -}) diff --git a/e2e-tests/history.spec.ts b/e2e-tests/history.spec.ts deleted file mode 100644 index e6ebee3cc9..0000000000 --- a/e2e-tests/history.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { createRandomPage, modKey, searchAndJumpToPage, renamePage, randomString } from './utils' - -test('undo/redo on a page should work as expected', async ({ page, block }) => { - const page1 = await createRandomPage(page) - - await block.mustType('text 1') - await page.waitForTimeout(500) // Wait for 500ms autosave period to expire - await expect(page.locator('text="text 1"')).toHaveCount(1) - - await page.keyboard.press(modKey + '+z') - await page.waitForTimeout(100) - await expect(page.locator('text="text 1"')).toHaveCount(0) - - await page.keyboard.press(modKey + '+Shift+z') - await page.waitForTimeout(100) - await expect(page.locator('text="text 1"')).toHaveCount(1) -}) - -test('should navigate to corresponding page on undo', async ({ page, block }) => { - const page1 = await createRandomPage(page) - - await block.mustType('text 1') - await page.waitForTimeout(500) // Wait for 500ms autosave period to expire - - const page2 = await createRandomPage(page) - - await page.keyboard.press(modKey + '+z') - await page.waitForTimeout(100) - expect(await page.innerText('.page-title .title')).toBe(page1) - - await page.keyboard.press(modKey + '+z') - await page.waitForTimeout(100) - await expect(page.locator('text="text 1"')).toHaveCount(0) -}) - - -test('undo/redo of a renamed page should be preserved', async ({ page, block }) => { - const page1 = await createRandomPage(page) - - await block.mustType('text 1') - await page.waitForTimeout(500) // Wait for 500ms autosave period to expire - - await renamePage(page, randomString(10)) - - await page.keyboard.press(modKey + '+z') // undo rename page - await page.waitForTimeout(100) - await page.keyboard.press(modKey + '+z') // undo text edit - await page.waitForTimeout(100) - - await expect(page.locator('text="text 1"')).toHaveCount(0) -}) diff --git a/e2e-tests/hotkey.spec.ts b/e2e-tests/hotkey.spec.ts deleted file mode 100644 index b1a2c1783f..0000000000 --- a/e2e-tests/hotkey.spec.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { createRandomPage, enterNextBlock, lastBlock, modKey, IsLinux, closeSearchBox } from './utils' - -test('open search dialog', async ({ page }) => { - await page.waitForTimeout(200) - await closeSearchBox(page) - await page.keyboard.press(modKey + '+k') - - await page.waitForSelector('[placeholder="What are you looking for?"]', { state: 'visible' }) - await page.keyboard.press('Escape') - await page.waitForSelector('[placeholder="What are you looking for?"]', { state: 'hidden' }) -}) - -test('insert link #3278', async ({ page }) => { - await createRandomPage(page) - - let hotKey = modKey + '+l' - let selectAll = modKey + '+a' - - // Case 1: empty link - await lastBlock(page) - 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 enterNextBlock(page) - 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 enterNextBlock(page) - 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/)') -}) diff --git a/e2e-tests/logseq-api.spec.ts b/e2e-tests/logseq-api.spec.ts deleted file mode 100644 index a438970006..0000000000 --- a/e2e-tests/logseq-api.spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { test } from './fixtures' -import { expect } from '@playwright/test' -import { callPageAPI } from './utils' -import { Page } from 'playwright' - -async function createDBGraph(page: Page) { - await page.locator(`#left-sidebar .cp__graphs-selector > a`).click() - await page.click('text="Create db graph"') - await page.waitForSelector('.new-graph') - const name = `e2e-db-${Date.now()}` - await page.waitForTimeout(100) - await page.keyboard.type(name) - await page.locator('.new-graph > .ui__button').click() - return name -} - -test.skip('test db graph', async ({ page }) => { - const name = await createDBGraph(page) - await page.waitForSelector(`a[title="logseq_db_${name}"]`) - - await page.pause() -}) - -test('(File graph): block related apis', - async ({ page }) => { - const callAPI = callPageAPI.bind(null, page) - - const bPageName = 'block-test-page' - await callAPI('create_page', bPageName, null, { createFirstBlock: false }) - await callAPI('create_page', bPageName, null, { createFirstBlock: false }) - await page.waitForSelector(`body[data-page="${bPageName}"]`) - - let p = await callAPI('get_current_page') - const bp = await callAPI('append_block_in_page', bPageName, 'tests') - - expect(p.name).toBe(bPageName) - - p = await callAPI('get_page', bPageName) - - expect(p.name).toBe(bPageName) - - await callAPI('edit_block', bp.uuid) - - const b = (await callAPI('get_current_block')) - expect(Object.keys(b)).toContain('uuid') - - await page.waitForSelector('.block-editor > textarea') - await page.locator('.block-editor > textarea').fill('') - const content = 'test api' - await page.type('.block-editor > textarea', content) - - const editingContent = await callAPI('get_editing_block_content') - expect(editingContent).toBe(content) - - // create - let b1 = await callAPI('insert_block', b.uuid, content) - b1 = await callAPI('get_block', b1.uuid) - - expect(b1.parent.id).toBe(b.id) - - // update - const content1 = content + '+ update!' - await callAPI('update_block', b1.uuid, content1) - b1 = await callAPI('get_block', b1.uuid) - - expect(b1.content).toBe(content1) - - // remove - await callAPI('remove_block', b1.uuid) - b1 = await callAPI('get_block', b1.uuid) - - expect(b1).toBeNull() - - // traverse - b1 = await callAPI('insert_block', b.uuid, content1, { sibling: true }) - const nb = await callAPI('get_next_sibling_block', b.uuid) - const pb = await callAPI('get_previous_sibling_block', b1.uuid) - - expect(nb.uuid).toBe(b1.uuid) - expect(pb.uuid).toBe(b.uuid) - - // move - await callAPI('move_block', b.uuid, b1.uuid) - const mb = await callAPI('get_next_sibling_block', b1.uuid) - - expect(mb.uuid).toBe(b.uuid) - - // properties - // FIXME: redundant api call - await callAPI('upsert_block_property', b1.uuid, 'a') - await callAPI('upsert_block_property', b1.uuid, 'a', 1) - let prop1 = await callAPI('get_block_property', b1.uuid, 'a') - - expect(prop1).toBe(1) - - await callAPI('upsert_block_property', b1.uuid, 'a', 2) - prop1 = await callAPI('get_block_property', b1.uuid, 'a') - - expect(prop1).toBe(2) - - await callAPI('remove_block_property', b1.uuid, 'a') - prop1 = await callAPI('get_block_property', b1.uuid, 'a') - - expect(prop1).toBeNull() - - await callAPI('upsert_block_property', b1.uuid, 'a', 1) - await callAPI('upsert_block_property', b1.uuid, 'b', 1) - - prop1 = await callAPI('get_block_properties', b1.uuid) - - expect(prop1).toEqual({ a: 1, b: 1 }) - - // await page.pause() - }) - -test('(DB graph): block related apis', - async ({ page }) => { - const name = await createDBGraph(page) - await page.waitForSelector(`a[title="logseq_db_${name}"]`) - - const callAPI = callPageAPI.bind(null, page) - - const bPageName = 'block-test-page' - await callAPI('create_page', bPageName, null, { createFirstBlock: false }) - await callAPI('create_page', bPageName, null, { createFirstBlock: false }) - await page.waitForSelector(`body[data-page="${bPageName}"]`) - - let p = await callAPI('get_current_page') - const bp = await callAPI('append_block_in_page', bPageName, 'tests') - - expect(p.name).toBe(bPageName) - - p = await callAPI('get_page', bPageName) - - expect(p.name).toBe(bPageName) - - await callAPI('edit_block', bp.uuid) - - const b = (await callAPI('get_current_block')) - expect(Object.keys(b)).toContain('uuid') - - await page.waitForSelector('.block-editor > textarea') - await page.locator('.block-editor > textarea').fill('') - const content = 'test api' - await page.type('.block-editor > textarea', content) - - const editingContent = await callAPI('get_editing_block_content') - expect(editingContent).toBe(content) - - // create - let b1 = await callAPI('insert_block', b.uuid, content) - b1 = await callAPI('get_block', b1.uuid) - - expect(b1.parent.id).toBe(b.id) - - // update - const content1 = content + '+ update!' - await callAPI('update_block', b1.uuid, content1) - b1 = await callAPI('get_block', b1.uuid) - - expect(b1.content).toBe(content1) - - // remove - await callAPI('remove_block', b1.uuid) - b1 = await callAPI('get_block', b1.uuid) - - expect(b1).toBeNull() - - // traverse - b1 = await callAPI('insert_block', b.uuid, content1, { sibling: true }) - const nb = await callAPI('get_next_sibling_block', b.uuid) - const pb = await callAPI('get_previous_sibling_block', b1.uuid) - - expect(nb.uuid).toBe(b1.uuid) - expect(pb.uuid).toBe(b.uuid) - - // move - await callAPI('move_block', b.uuid, b1.uuid) - const mb = await callAPI('get_next_sibling_block', b1.uuid) - - expect(mb.uuid).toBe(b.uuid) - - // properties - await callAPI('upsert_block_property', b1.uuid, 'a', 'a') - let prop1 = await callAPI('get_block_property', b1.uuid, 'a') - - expect(prop1.title).toBe('a') - - await callAPI('upsert_block_property', b1.uuid, 'a', 'b') - prop1 = await callAPI('get_block_property', b1.uuid, 'a') - - expect(prop1.title).toBe('b') - - await callAPI('remove_block_property', b1.uuid, 'a') - prop1 = await callAPI('get_block_property', b1.uuid, 'a') - - expect(prop1).toBeNull() - - await callAPI('upsert_block_property', b1.uuid, 'a', 'a') - await callAPI('upsert_block_property', b1.uuid, 'b', 'b') - - prop1 = await callAPI('get_block_properties', b1.uuid) - - expect(prop1).toEqual({ ':plugin.property/a': 'a', ':plugin.property/b': 'b' }) - - // properties entity & schema - await callAPI('upsert_property', 'p1') - prop1 = await callAPI('get_property', 'p1') - - expect(prop1.title).toBe('p1') - expect(prop1.ident).toBe(':plugin.property/p1') - - await callAPI('upsert_property', 'map1', { type: 'map' }) - await callAPI('upsert_block_property', b1.uuid, 'map1', { a: 1 }) - prop1 = await callAPI('get_property', 'map1') - const b1p = await callAPI('get_block_property', b1.uuid, 'map1') - - expect(prop1.type).toBe('map') - expect(b1p).toEqual({a: 1}) - - // await page.pause() - }) diff --git a/e2e-tests/logseq-url.spec.ts b/e2e-tests/logseq-url.spec.ts deleted file mode 100644 index bcc663dd6c..0000000000 --- a/e2e-tests/logseq-url.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { createRandomPage, lastBlock, IsMac, IsLinux } from './utils' - -test("Logseq URLs (same graph)", async ({ page, block }) => { - try { - await page.waitForSelector('.notification-clear', { timeout: 10 }) - page.click('.notification-clear') - } catch (error) { - } - - let paste_key = IsMac ? 'Meta+v' : 'Control+v' - // create a page with identify block - let identify_text = "URL redirect target" - let page_title = await createRandomPage(page) - await block.mustFill(identify_text) - - // paste current page's URL to another page, then redirect through the URL - await page.click('.ui__dropdown-trigger .toolbar-dots-btn') - await page.locator("text=Copy page URL").click() - await createRandomPage(page) - await block.mustFill("") // to enter editing mode - await page.keyboard.press(paste_key) - // paste returns a promise which is async, so we need give it a little bit - // more time - await page.waitForTimeout(100) - let cursor_locator = page.locator('textarea >> nth=0') - expect(await cursor_locator.inputValue()).toContain("page=" + page_title) - await cursor_locator.press("Enter") - if (IsMac) { // FIXME: support Logseq URL on Linux (XDG) - page.locator('a.external-link >> nth=0').click() - await page.waitForNavigation() - await page.waitForTimeout(500) - cursor_locator = await lastBlock(page) - expect(await cursor_locator.inputValue()).toBe(identify_text) - } - - // paste the identify block's URL to another page, then redirect through the URL - await page.click('span.bullet >> nth=0', { button: "right" }) - await page.locator("text=Copy block URL").click() - await createRandomPage(page) - await block.mustFill("") // to enter editing mode - await page.keyboard.press(paste_key) - await page.waitForTimeout(100) - cursor_locator = page.locator('textarea >> nth=0') - expect(await cursor_locator.inputValue()).toContain("block-id=") - await cursor_locator.press("Enter") - if (IsMac) { // FIXME: support Logseq URL on Linux (XDG) - page.locator('a.external-link >> nth=0').click() - await page.waitForNavigation() - await page.waitForTimeout(500) - cursor_locator = await lastBlock(page) - expect(await cursor_locator.inputValue()).toBe(identify_text) - } -}) diff --git a/e2e-tests/page-rename.spec.ts b/e2e-tests/page-rename.spec.ts deleted file mode 100644 index e6512b7d0b..0000000000 --- a/e2e-tests/page-rename.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { expect, Page } from '@playwright/test' -import { test } from './fixtures' -import { closeSearchBox, createPage, randomLowerString, randomString, renamePage, searchPage } from './utils' - -/*** - * Test rename feature - ***/ - -async function page_rename_test(page: Page, original_page_name: string, new_page_name: string) { - const rand = randomString(10) - let original_name = original_page_name + rand - let new_name = new_page_name + rand - - await createPage(page, original_name) - - // Rename page in UI - await renamePage(page, new_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 -} - -async function homepage_rename_test(page: Page, original_page_name: string, new_page_name: string) { - - const rand = randomString(10) - let original_name = original_page_name + rand - let new_name = new_page_name + rand - - await createPage(page, original_name) - - // Toggle settings - await page.click('#main-content-container') - await page.keyboard.press('t') - await page.keyboard.press('s') - - await page.click('a[data-id="features"]') - await page.click('#settings div:nth-child(1) a') - - await page.type('input', original_name) - await page.click('[aria-label="Close"]') - - expect(await page.locator('.home-nav span.flex-1').innerText()).toBe(original_name); - - await renamePage(page, new_name) - - expect(await page.locator('.home-nav span.flex-1').innerText()).toBe(new_name); - - // Reenable journal - await page.click('#main-content-container') - await page.keyboard.press('t') - await page.keyboard.press('s') - await page.click('a[data-id="features"]') - await page.click('#settings div:nth-child(1) a') - await page.click('[aria-label="Close"]') - -} - -test('page rename test', async ({ page }) => { - // TODO: Fix commented out test. Started failing after https://github.com/logseq/logseq/pull/6945 - // await homepage_rename_test(page, "abcd", "a/b/c/d") - await page_rename_test(page, "abcd", "a.b.c.d") - await page_rename_test(page, "abcd", "a/b/c/d") - - // Disabled for now since it's unstable: - // The page name in page search are not updated after changing the capitalization of the page name #9577 - // https://github.com/logseq/logseq/issues/9577 - // Expect the page name to be updated in the search results - // await page_rename_test(page, "DcBA_", "dCBA_") - // const results = await searchPage(page, "DcBA_") - // // search result 0 is the new page & 1 is the new whiteboard - // const resultRow = await results[0].innerText() - // expect(resultRow).toContain("dCBA_"); - // expect(resultRow).not.toContain("DcBA_"); - await closeSearchBox(page) -}) - -// TODO introduce more samples when #4722 is fixed -test('page title property test', async ({ page }) => { - // Edit Title Property and Double Enter (ETPDE) - // exit editing via insert new block - let rand = randomLowerString(10) - let original_name = "etpde old" + rand - let new_name = "etpde new" + rand - await createPage(page, original_name) - // add some spaces to test if it is trimmed - await page.type(':nth-match(textarea, 1)', 'title:: ' + new_name + " ") - await page.press(':nth-match(textarea, 1)', 'Enter') // DWIM property mode creates new line - await page.press(':nth-match(textarea, 1)', 'Enter') - await expect(page.locator('.page-title .title')).toHaveText(new_name) - - // Edit Title Property and Esc (ETPE) - // exit editing via moving out focus - rand = randomLowerString(10) - original_name = "etpe old " + rand - new_name = "etpe new " + rand - await createPage(page, original_name) - await page.type(':nth-match(textarea, 1)', 'title:: ' + new_name) - await page.press(':nth-match(textarea, 1)', 'Escape') - await expect(page.locator('.page-title .title')).toHaveText(new_name) -}) diff --git a/e2e-tests/page-search.spec.ts b/e2e-tests/page-search.spec.ts deleted file mode 100644 index 5ca274c0bf..0000000000 --- a/e2e-tests/page-search.spec.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { expect, Page } from '@playwright/test' -import { test } from './fixtures' -import { Block } from './types' -import { modKey, createRandomPage, newInnerBlock, randomString, lastBlock, enterNextBlock } from './utils' -import { searchPage, closeSearchBox } from './util/search-modal' - -/*** - * Test alias features - * Test search referring features - * Consider diacritics - ***/ - -let hotkeyOpenLink = modKey + '+o' -let hotkeyBack = modKey + '+[' - -test('Search page and blocks (diacritics)', async ({ page, block }) => { - const rand = randomString(20) - - // diacritic opening test - await createRandomPage(page) - - await block.mustType('[[Einführung in die Allgemeine Sprachwissenschaft' + rand + ']] diacritic-block-1', { delay: 10 }) - await page.waitForTimeout(500) - await page.keyboard.press(hotkeyOpenLink, { delay: 10 }) - await page.waitForTimeout(500) - - const pageTitle = page.locator('.page-title').first() - expect(await pageTitle.innerText()).toEqual('Einführung in die Allgemeine Sprachwissenschaft' + rand) - - await page.waitForTimeout(500) - - // build target Page with diacritics - await block.activeEditing(0) - await block.mustType('Diacritic title test content', { delay: 10 }) - - await block.enterNext() - await block.mustType('[[EinfΓΌhrung in die Allgemeine Sprachwissenschaft' + rand + ']] diacritic-block-2', { delay: 10 }) - await page.waitForTimeout(500) - await page.keyboard.press(hotkeyBack) - - // check if diacritics are indexed - const results = await searchPage(page, 'Einführung in die Allgemeine Sprachwissenschaft' + rand) - // await expect(results.length).toEqual(5) // 2 block + 1 current page - await closeSearchBox(page) -}) - -test('Search CJK', async ({ page, block }) => { - const rand = randomString(20) - - // diacritic opening test - await createRandomPage(page) - - await block.mustType('[[今ζ—₯daytime进度村' + rand + ']] diacritic-block-1', { delay: 10 }) - await page.waitForTimeout(500) - await page.keyboard.press(hotkeyOpenLink, { delay: 10 }) - await page.waitForTimeout(500) - - const pageTitle = page.locator('.page-title').first() - expect(await pageTitle.innerText()).toEqual('今ζ—₯daytime进度村' + rand) - - await page.waitForTimeout(500) - - // check if CJK are indexed - const results = await searchPage(page, 'θΏ›εΊ¦') - // await expect(results.length).toEqual(4) // 1 page + 1 block + new whiteboard - await closeSearchBox(page) -}) - -async function alias_test(block: Block, page: Page, page_name: string, search_kws: string[]) { - await createRandomPage(page) - - const rand = randomString(10) - let target_name = page_name + ' target ' + rand - let alias_name = page_name + ' alias ' + rand - let alias_test_content_1 = randomString(20) - let alias_test_content_2 = randomString(20) - let alias_test_content_3 = randomString(20) - - await page.type('textarea >> nth=0', '[[' + target_name) - await page.keyboard.press(hotkeyOpenLink) - - await lastBlock(page) - - // build target Page with alias - // the target page will contains the content in - // alias_test_content_1, - // alias_test_content_2, and - // alias_test_content_3 sequentially, to validate the target page state - await page.type('textarea >> nth=0', 'alias:: [[' + alias_name, { delay: 10 }) - await page.keyboard.press('Enter', { delay: 200 }) // Enter for finishing selection - await page.keyboard.press('Enter', { delay: 200 }) - await page.keyboard.press('Escape') - await page.waitForTimeout(100) - await block.clickNext() - await block.activeEditing(1) - await page.type('textarea >> nth=0', alias_test_content_1) - await lastBlock(page) - page.keyboard.press(hotkeyBack) - - await page.waitForNavigation() - await block.escapeEditing() - // create alias ref in origin Page - await block.activeEditing(0) - await block.enterNext() - await page.type('textarea >> nth=0', '[[' + alias_name, { delay: 20 }) - await page.keyboard.press('Enter') // Enter for finishing selection - await page.waitForTimeout(100) - - page.keyboard.press(hotkeyOpenLink) - await page.waitForNavigation() - await block.escapeEditing() - - // shortcut opening test - await block.activeEditing(1) - expect(await page.inputValue('textarea >> nth=0')).toBe(alias_test_content_1) - - await enterNextBlock(page) - await page.waitForTimeout(100) - await page.type('textarea >> nth=0', alias_test_content_2) - await page.waitForTimeout(100) - page.keyboard.press(hotkeyBack) - - await page.waitForNavigation() - await block.escapeEditing() - // pressing enter on alias opening test - await block.activeEditing(1) - await page.press('textarea >> nth=0', 'ArrowLeft') - await page.press('textarea >> nth=0', 'ArrowLeft') - await page.press('textarea >> nth=0', 'ArrowLeft') - page.press('textarea >> nth=0', 'Enter') - await page.waitForNavigation() - await block.escapeEditing() - await block.activeEditing(2) - expect(await page.inputValue('textarea >> nth=0')).toBe(alias_test_content_2) - await newInnerBlock(page) - await page.waitForTimeout(100) - await page.type('textarea >> nth=0', alias_test_content_3) - await page.waitForTimeout(100) - page.keyboard.press(hotkeyBack) - - await page.waitForNavigation() - await block.escapeEditing() - // clicking alias ref opening test - await block.activeEditing(1) - await block.enterNext() - 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 lastBlock(page) - expect(await page.inputValue('textarea >> nth=0')).toBe(alias_test_content_3) - - // TODO: test alias from graph clicking - - // test alias from search - for (let kw of search_kws) { - let kw_name = kw + ' alias ' + rand - - const results = await searchPage(page, kw_name) - - // test search results - expect(await results[0].innerText()).toContain(alias_name.normalize('NFKC')) - - // test search entering (page) - page.keyboard.press("Enter") - await page.waitForNavigation() - await page.waitForSelector('.ls-block span.inline') - - // test search clicking (block) - await searchPage(page, kw_name) - } -} - -test.skip('page diacritic alias', async ({ block, page }) => { - await alias_test(block, page, "ü", ["ü", "ΓΌ", "Ü"]) -}) diff --git a/e2e-tests/paste.spec.ts b/e2e-tests/paste.spec.ts deleted file mode 100644 index 4859bfeaf2..0000000000 --- a/e2e-tests/paste.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect } from '@playwright/test' -import { test } from './fixtures' -import { createRandomPage, enterNextBlock, lastBlock, modKey } from './utils' -import { dispatch_kb_events } from './util/keyboard-events' -import * as kb_events from './util/keyboard-events' - -test('property text deleted on Ctrl+C when its value mixes [[link]] and other text #9100', async ({ page, block }) => { - await createRandomPage(page) - - await block.mustType('category:: [[A]] and [[B]] test') - - await page.keyboard.press(modKey + '+c', { delay: 10 }) - - await expect(page.locator('textarea >> nth=0')).toHaveValue('category:: [[A]] and [[B]] test') -}) diff --git a/e2e-tests/plugin/index.html b/e2e-tests/plugin/index.html deleted file mode 100644 index 19efb4b1b6..0000000000 --- a/e2e-tests/plugin/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - Document - - - -
- - - \ No newline at end of file diff --git a/e2e-tests/plugin/index.js b/e2e-tests/plugin/index.js deleted file mode 100644 index b2b896bdb3..0000000000 --- a/e2e-tests/plugin/index.js +++ /dev/null @@ -1,61 +0,0 @@ -async function main () { - logseq.UI.showMsg('Hi, e2e tests from a local plugin!') - - // await (new Promise(resolve => setTimeout(resolve, 3000))) - - let msg = 0 - - const logPane = (input) => { - logseq.provideUI({ - key: `log-${++msg}`, - path: `#a-plugin-for-e2e-tests > ul`, - template: `
  • ${input}
  • `, - }) - } - - // log pane - logseq.provideUI({ - key: 'logseq-e2e-tests', - template: `
    -

    Plugin e2e tests ...

    -
      -
      `, - path: 'body', - style: { - width: '300px', - position: 'fixed', - top: '300px', - left: '300px', - zIndex: 99, - }, - }) - - logseq.provideStyle(` - #a-plugin-for-e2e-tests { - padding: 20px; - background-color: red; - color: white; - width: 300px; - } - `) - - let dbChangedDid = false - let blockChangedDid = false - - // hook db change - logseq.DB.onChanged((e) => { - if (dbChangedDid) return - logPane(`[DB] hook: changed`) - dbChangedDid = true - }) - - logseq.DB.onBlockChanged('65a0beee-7e01-4e72-8d38-089d923a63de', - (e) => { - if (blockChangedDid) return - logPane(`[DB] hook: block changed`) - blockChangedDid = true - }) -} - -// bootstrap -logseq.ready(main).catch(null) \ No newline at end of file diff --git a/e2e-tests/plugin/lsplugin.user.js b/e2e-tests/plugin/lsplugin.user.js deleted file mode 100644 index a72c65e74d..0000000000 --- a/e2e-tests/plugin/lsplugin.user.js +++ /dev/null @@ -1,2 +0,0 @@ -/*! For license information please see lsplugin.user.js.LICENSE.txt */ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.LSPluginEntry=t():e.LSPluginEntry=t()}(self,(()=>(()=>{var e={227:(e,t,n)=>{var r=n(155);t.formatArgs=function(t){if(t[0]=(this.useColors?"%c":"")+this.namespace+(this.useColors?" %c":" ")+t[0]+(this.useColors?"%c ":" ")+"+"+e.exports.humanize(this.diff),!this.useColors)return;const n="color: "+this.color;t.splice(1,0,n,"color: inherit");let r=0,o=0;t[0].replace(/%[a-zA-Z%]/g,(e=>{"%%"!==e&&(r++,"%c"===e&&(o=r))})),t.splice(o,0,n)},t.save=function(e){try{e?t.storage.setItem("debug",e):t.storage.removeItem("debug")}catch(e){}},t.load=function(){let e;try{e=t.storage.getItem("debug")}catch(e){}!e&&void 0!==r&&"env"in r&&(e=r.env.DEBUG);return e},t.useColors=function(){if("undefined"!=typeof window&&window.process&&("renderer"===window.process.type||window.process.__nwjs))return!0;if("undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/))return!1;return"undefined"!=typeof document&&document.documentElement&&document.documentElement.style&&document.documentElement.style.WebkitAppearance||"undefined"!=typeof window&&window.console&&(window.console.firebug||window.console.exception&&window.console.table)||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/)&&parseInt(RegExp.$1,10)>=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)},t.storage=function(){try{return localStorage}catch(e){}}(),t.destroy=(()=>{let e=!1;return()=>{e||(e=!0,console.warn("Instance method `debug.destroy()` is deprecated and no longer does anything. It will be removed in the next major version of `debug`."))}})(),t.colors=["#0000CC","#0000FF","#0033CC","#0033FF","#0066CC","#0066FF","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#3300CC","#3300FF","#3333CC","#3333FF","#3366CC","#3366FF","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#6600CC","#6600FF","#6633CC","#6633FF","#66CC00","#66CC33","#9900CC","#9900FF","#9933CC","#9933FF","#99CC00","#99CC33","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC9900","#CC9933","#CCCC00","#CCCC33","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF9900","#FF9933","#FFCC00","#FFCC33"],t.log=console.debug||console.log||(()=>{}),e.exports=n(447)(t);const{formatters:o}=e.exports;o.j=function(e){try{return JSON.stringify(e)}catch(e){return"[UnexpectedJSONParseError]: "+e.message}}},447:(e,t,n)=>{e.exports=function(e){function t(e){let n,o,i,s=null;function a(...e){if(!a.enabled)return;const r=a,o=Number(new Date),i=o-(n||o);r.diff=i,r.prev=n,r.curr=o,n=o,e[0]=t.coerce(e[0]),"string"!=typeof e[0]&&e.unshift("%O");let s=0;e[0]=e[0].replace(/%([a-zA-Z%])/g,((n,o)=>{if("%%"===n)return"%";s++;const i=t.formatters[o];if("function"==typeof i){const t=e[s];n=i.call(r,t),e.splice(s,1),s--}return n})),t.formatArgs.call(r,e);(r.log||t.log).apply(r,e)}return a.namespace=e,a.useColors=t.useColors(),a.color=t.selectColor(e),a.extend=r,a.destroy=t.destroy,Object.defineProperty(a,"enabled",{enumerable:!0,configurable:!1,get:()=>null!==s?s:(o!==t.namespaces&&(o=t.namespaces,i=t.enabled(e)),i),set:e=>{s=e}}),"function"==typeof t.init&&t.init(a),a}function r(e,n){const r=t(this.namespace+(void 0===n?":":n)+e);return r.log=this.log,r}function o(e){return e.toString().substring(2,e.toString().length-2).replace(/\.\*\?$/,"*")}return t.debug=t,t.default=t,t.coerce=function(e){if(e instanceof Error)return e.stack||e.message;return e},t.disable=function(){const e=[...t.names.map(o),...t.skips.map(o).map((e=>"-"+e))].join(",");return t.enable(""),e},t.enable=function(e){let n;t.save(e),t.namespaces=e,t.names=[],t.skips=[];const r=("string"==typeof e?e:"").split(/[\s,]+/),o=r.length;for(n=0;n{t[n]=e[n]})),t.names=[],t.skips=[],t.formatters={},t.selectColor=function(e){let n=0;for(let t=0;t{"use strict";var t=function(e){return function(e){return!!e&&"object"==typeof e}(e)&&!function(e){var t=Object.prototype.toString.call(e);return"[object RegExp]"===t||"[object Date]"===t||function(e){return e.$$typeof===n}(e)}(e)};var n="function"==typeof Symbol&&Symbol.for?Symbol.for("react.element"):60103;function r(e,t){return!1!==t.clone&&t.isMergeableObject(e)?c((n=e,Array.isArray(n)?[]:{}),e,t):e;var n}function o(e,t,n){return e.concat(t).map((function(e){return r(e,n)}))}function i(e){return Object.keys(e).concat(function(e){return Object.getOwnPropertySymbols?Object.getOwnPropertySymbols(e).filter((function(t){return Object.propertyIsEnumerable.call(e,t)})):[]}(e))}function s(e,t){try{return t in e}catch(e){return!1}}function a(e,t,n){var o={};return n.isMergeableObject(e)&&i(e).forEach((function(t){o[t]=r(e[t],n)})),i(t).forEach((function(i){(function(e,t){return s(e,t)&&!(Object.hasOwnProperty.call(e,t)&&Object.propertyIsEnumerable.call(e,t))})(e,i)||(s(e,i)&&n.isMergeableObject(t[i])?o[i]=function(e,t){if(!t.customMerge)return c;var n=t.customMerge(e);return"function"==typeof n?n:c}(i,n)(e[i],t[i],n):o[i]=r(t[i],n))})),o}function c(e,n,i){(i=i||{}).arrayMerge=i.arrayMerge||o,i.isMergeableObject=i.isMergeableObject||t,i.cloneUnlessOtherwiseSpecified=r;var s=Array.isArray(n);return s===Array.isArray(e)?s?i.arrayMerge(e,n,i):a(e,n,i):r(n,i)}c.all=function(e,t){if(!Array.isArray(e))throw new Error("first argument should be an array");return e.reduce((function(e,n){return c(e,n,t)}),{})};var l=c;e.exports=l},856:function(e){e.exports=function(){"use strict";function e(t){return e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},e(t)}function t(e,n){return t=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},t(e,n)}function n(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(e){return!1}}function r(e,o,i){return r=n()?Reflect.construct:function(e,n,r){var o=[null];o.push.apply(o,n);var i=new(Function.bind.apply(e,o));return r&&t(i,r.prototype),i},r.apply(null,arguments)}function o(e){return i(e)||s(e)||a(e)||l()}function i(e){if(Array.isArray(e))return c(e)}function s(e){if("undefined"!=typeof Symbol&&null!=e[Symbol.iterator]||null!=e["@@iterator"])return Array.from(e)}function a(e,t){if(e){if("string"==typeof e)return c(e,t);var n=Object.prototype.toString.call(e).slice(8,-1);return"Object"===n&&e.constructor&&(n=e.constructor.name),"Map"===n||"Set"===n?Array.from(e):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?c(e,t):void 0}}function c(e,t){(null==t||t>e.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?n-1:0),o=1;o/gm),V=g(/^data-[\-\w.\u00B7-\uFFFF]/),K=g(/^aria-[\-\w]+$/),Y=g(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),Q=g(/^(?:\w+script|data):/i),X=g(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),ee=g(/^html$/i),te=function(){return"undefined"==typeof window?null:window},ne=function(t,n){if("object"!==e(t)||"function"!=typeof t.createPolicy)return null;var r=null,o="data-tt-policy-suffix";n.currentScript&&n.currentScript.hasAttribute(o)&&(r=n.currentScript.getAttribute(o));var i="dompurify"+(r?"#"+r:"");try{return t.createPolicy(i,{createHTML:function(e){return e}})}catch(e){return console.warn("TrustedTypes policy "+i+" could not be created."),null}};function re(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:te(),n=function(e){return re(e)};if(n.version="2.3.8",n.removed=[],!t||!t.document||9!==t.document.nodeType)return n.isSupported=!1,n;var r=t.document,i=t.document,s=t.DocumentFragment,a=t.HTMLTemplateElement,c=t.Node,l=t.Element,u=t.NodeFilter,f=t.NamedNodeMap,p=void 0===f?t.NamedNodeMap||t.MozNamedAttrMap:f,h=t.HTMLFormElement,d=t.DOMParser,g=t.trustedTypes,y=l.prototype,v=N(y,"cloneNode"),b=N(y,"nextSibling"),_=N(y,"childNodes"),I=N(y,"parentNode");if("function"==typeof a){var M=i.createElement("template");M.content&&M.content.ownerDocument&&(i=M.content.ownerDocument)}var oe=ne(g,r),ie=oe?oe.createHTML(""):"",se=i,ae=se.implementation,ce=se.createNodeIterator,le=se.createDocumentFragment,ue=se.getElementsByTagName,fe=r.importNode,pe={};try{pe=L(i).documentMode?i.documentMode:{}}catch(e){}var he={};n.isSupported="function"==typeof I&&ae&&void 0!==ae.createHTMLDocument&&9!==pe;var de,me,ge=J,ye=Z,ve=V,be=K,_e=Q,we=X,xe=Y,Ce=null,Se=F({},[].concat(o(P),o(R),o(D),o($),o(H))),Oe=null,je=F({},[].concat(o(B),o(q),o(W),o(G))),Ae=Object.seal(Object.create(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),Ee=null,ke=null,Te=!0,Ie=!0,Me=!1,Fe=!1,Le=!1,Ne=!1,Pe=!1,Re=!1,De=!1,Ue=!1,$e=!0,ze=!0,He=!1,Be={},qe=null,We=F({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]),Ge=null,Je=F({},["audio","video","img","source","image","track"]),Ze=null,Ve=F({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),Ke="http://www.w3.org/1998/Math/MathML",Ye="http://www.w3.org/2000/svg",Qe="http://www.w3.org/1999/xhtml",Xe=Qe,et=!1,tt=["application/xhtml+xml","text/html"],nt="text/html",rt=null,ot=i.createElement("form"),it=function(e){return e instanceof RegExp||e instanceof Function},st=function(t){rt&&rt===t||(t&&"object"===e(t)||(t={}),t=L(t),Ce="ALLOWED_TAGS"in t?F({},t.ALLOWED_TAGS):Se,Oe="ALLOWED_ATTR"in t?F({},t.ALLOWED_ATTR):je,Ze="ADD_URI_SAFE_ATTR"in t?F(L(Ve),t.ADD_URI_SAFE_ATTR):Ve,Ge="ADD_DATA_URI_TAGS"in t?F(L(Je),t.ADD_DATA_URI_TAGS):Je,qe="FORBID_CONTENTS"in t?F({},t.FORBID_CONTENTS):We,Ee="FORBID_TAGS"in t?F({},t.FORBID_TAGS):{},ke="FORBID_ATTR"in t?F({},t.FORBID_ATTR):{},Be="USE_PROFILES"in t&&t.USE_PROFILES,Te=!1!==t.ALLOW_ARIA_ATTR,Ie=!1!==t.ALLOW_DATA_ATTR,Me=t.ALLOW_UNKNOWN_PROTOCOLS||!1,Fe=t.SAFE_FOR_TEMPLATES||!1,Le=t.WHOLE_DOCUMENT||!1,Re=t.RETURN_DOM||!1,De=t.RETURN_DOM_FRAGMENT||!1,Ue=t.RETURN_TRUSTED_TYPE||!1,Pe=t.FORCE_BODY||!1,$e=!1!==t.SANITIZE_DOM,ze=!1!==t.KEEP_CONTENT,He=t.IN_PLACE||!1,xe=t.ALLOWED_URI_REGEXP||xe,Xe=t.NAMESPACE||Qe,t.CUSTOM_ELEMENT_HANDLING&&it(t.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ae.tagNameCheck=t.CUSTOM_ELEMENT_HANDLING.tagNameCheck),t.CUSTOM_ELEMENT_HANDLING&&it(t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ae.attributeNameCheck=t.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),t.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ae.allowCustomizedBuiltInElements=t.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),de=de=-1===tt.indexOf(t.PARSER_MEDIA_TYPE)?nt:t.PARSER_MEDIA_TYPE,me="application/xhtml+xml"===de?function(e){return e}:S,Fe&&(Ie=!1),De&&(Re=!0),Be&&(Ce=F({},o(H)),Oe=[],!0===Be.html&&(F(Ce,P),F(Oe,B)),!0===Be.svg&&(F(Ce,R),F(Oe,q),F(Oe,G)),!0===Be.svgFilters&&(F(Ce,D),F(Oe,q),F(Oe,G)),!0===Be.mathMl&&(F(Ce,$),F(Oe,W),F(Oe,G))),t.ADD_TAGS&&(Ce===Se&&(Ce=L(Ce)),F(Ce,t.ADD_TAGS)),t.ADD_ATTR&&(Oe===je&&(Oe=L(Oe)),F(Oe,t.ADD_ATTR)),t.ADD_URI_SAFE_ATTR&&F(Ze,t.ADD_URI_SAFE_ATTR),t.FORBID_CONTENTS&&(qe===We&&(qe=L(qe)),F(qe,t.FORBID_CONTENTS)),ze&&(Ce["#text"]=!0),Le&&F(Ce,["html","head","body"]),Ce.table&&(F(Ce,["tbody"]),delete Ee.tbody),m&&m(t),rt=t)},at=F({},["mi","mo","mn","ms","mtext"]),ct=F({},["foreignobject","desc","title","annotation-xml"]),lt=F({},["title","style","font","a","script"]),ut=F({},R);F(ut,D),F(ut,U);var ft=F({},$);F(ft,z);var pt=function(e){var t=I(e);t&&t.tagName||(t={namespaceURI:Qe,tagName:"template"});var n=S(e.tagName),r=S(t.tagName);return e.namespaceURI===Ye?t.namespaceURI===Qe?"svg"===n:t.namespaceURI===Ke?"svg"===n&&("annotation-xml"===r||at[r]):Boolean(ut[n]):e.namespaceURI===Ke?t.namespaceURI===Qe?"math"===n:t.namespaceURI===Ye?"math"===n&&ct[r]:Boolean(ft[n]):e.namespaceURI===Qe&&!(t.namespaceURI===Ye&&!ct[r])&&!(t.namespaceURI===Ke&&!at[r])&&!ft[n]&&(lt[n]||!ut[n])},ht=function(e){C(n.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){try{e.outerHTML=ie}catch(t){e.remove()}}},dt=function(e,t){try{C(n.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){C(n.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Oe[e])if(Re||De)try{ht(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},mt=function(e){var t,n;if(Pe)e=""+e;else{var r=O(e,/^[\r\n\t ]+/);n=r&&r[0]}"application/xhtml+xml"===de&&(e=''+e+"");var o=oe?oe.createHTML(e):e;if(Xe===Qe)try{t=(new d).parseFromString(o,de)}catch(e){}if(!t||!t.documentElement){t=ae.createDocument(Xe,"template",null);try{t.documentElement.innerHTML=et?"":o}catch(e){}}var s=t.body||t.documentElement;return e&&n&&s.insertBefore(i.createTextNode(n),s.childNodes[0]||null),Xe===Qe?ue.call(t,Le?"html":"body")[0]:Le?t.documentElement:s},gt=function(e){return ce.call(e.ownerDocument||e,e,u.SHOW_ELEMENT|u.SHOW_COMMENT|u.SHOW_TEXT,null,!1)},yt=function(e){return e instanceof h&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof p)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore)},vt=function(t){return"object"===e(c)?t instanceof c:t&&"object"===e(t)&&"number"==typeof t.nodeType&&"string"==typeof t.nodeName},bt=function(e,t,r){he[e]&&w(he[e],(function(e){e.call(n,t,r,rt)}))},_t=function(e){var t;if(bt("beforeSanitizeElements",e,null),yt(e))return ht(e),!0;if(k(/[\u0080-\uFFFF]/,e.nodeName))return ht(e),!0;var r=me(e.nodeName);if(bt("uponSanitizeElement",e,{tagName:r,allowedTags:Ce}),e.hasChildNodes()&&!vt(e.firstElementChild)&&(!vt(e.content)||!vt(e.content.firstElementChild))&&k(/<[/\w]/g,e.innerHTML)&&k(/<[/\w]/g,e.textContent))return ht(e),!0;if("select"===r&&k(/