fix: ensure website build doesn't rely on new mobile components

this commit also removes js e2e-tests
This commit is contained in:
Tienson Qin
2025-07-02 22:38:32 +08:00
parent 8a219cacc1
commit 453ee3b3b4
52 changed files with 35 additions and 10301 deletions

View File

@@ -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

View File

@@ -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

3
.gitignore vendored
View File

@@ -60,8 +60,7 @@ startup.png
android/app/src/main/assets/capacitor.config.json
*.sublime-*
/public/static
/public/
/public
.yarn/
.yarnrc.yml

View File

@@ -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

View File

@@ -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)}

View File

@@ -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([]);
})

View File

@@ -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')
})

View File

@@ -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);
})

View File

@@ -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<iostream>')
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<iostream>\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'
)
})

View File

@@ -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)
})

View File

@@ -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})
})

View File

@@ -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: '<u>',
// postfix: '</u>',
// 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)
})

View File

@@ -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<LogseqFixtures>({
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<Locator> => {
let blockCount = await page.locator('.page-blocks-inner .ls-block').count()
await page.press('textarea >> nth=0', 'Enter')
await page.waitForSelector(`.ls-block >> nth=${blockCount} >> textarea`, { state: 'visible', timeout: 1000 })
return page.locator('textarea >> nth=0')
},
clickNext: async (): Promise<Locator> => {
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<boolean> => {
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<boolean> => {
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<void> => {
// 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<void> => {
// NOTE: `nth=` counts from 0.
await page.waitForSelector(`.ls-block.selected >> nth=${total - 1}`, { timeout: 1000 })
},
escapeEditing: async (): Promise<void> => {
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<void> => {
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<boolean> => {
const locator = page.locator('.ls-block textarea >> nth=0')
return await locator.isVisible()
},
selectionStart: async (): Promise<number> => {
return await page.locator('textarea >> nth=0').evaluate(node => {
const elem = <HTMLTextAreaElement>node
return elem.selectionStart
})
},
selectionEnd: async (): Promise<number> => {
return await page.locator('textarea >> nth=0').evaluate(node => {
const elem = <HTMLTextAreaElement>node
return elem.selectionEnd
})
}
}
use(block)
},
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() });
})
}

View File

@@ -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')
})

View File

@@ -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);
}
});

View File

@@ -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('<h1>foo</h1>')
})
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('<h2>foo</h2>')
})
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('<h1>foo</h1>')
})
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('<h2>bar</h2>')
})
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('<h1>bar</h1>')
})

View File

@@ -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)
})

View File

@@ -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/)')
})

View File

@@ -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()
})

View File

@@ -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)
}
})

View File

@@ -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)
})

View File

@@ -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, "ü", ["ü", "ü", "Ü"])
})

View File

@@ -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')
})

View File

@@ -1,15 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="./lsplugin.user.js"></script>
</head>
<body>
<div id="root"></div>
<script src="./index.js"></script>
</body>
</html>

View File

@@ -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: `<li>${input}</li>`,
})
}
// log pane
logseq.provideUI({
key: 'logseq-e2e-tests',
template: `<div id="a-plugin-for-e2e-tests">
<h2>Plugin e2e tests ...</h2>
<ul></ul>
</div>`,
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)

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +0,0 @@
{
"name": "e2e-plugin",
"description": "A plugin for e2e tests",
"main": "./index.html",
"logseq": {
"id": "a-logseq-plugin-for-e2e-tests"
}
}

View File

@@ -1,110 +0,0 @@
import { expect } from '@playwright/test'
import { test } from './fixtures'
import { callPageAPI } from './utils'
/**
* load local tests plugin
*/
export async function loadLocalE2eTestsPlugin(page) {
const pid = 'a-logseq-plugin-for-e2e-tests'
const hasLoaded = await page.evaluate(async ([pid]) => {
// @ts-ignore
const p = window.LSPluginCore.registeredPlugins.get(pid)
// @ts-ignore
await window.LSPluginCore.enable(pid)
return p != null
}, [pid])
if (hasLoaded) return true
await callPageAPI(page, 'set_state_from_store',
'ui/developer-mode?', true)
await page.keyboard.press('t+p')
await page.locator('text=Load unpacked plugin')
await callPageAPI(page, 'set_state_from_store',
'plugin/selected-unpacked-pkg', `${__dirname}/plugin`)
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
}
test.skip('enabled plugin system default', async ({ page }) => {
const callAPI = callPageAPI.bind(null, page)
const pluginEnabled = await callAPI('get_state_from_store', 'plugin/enabled')
await expect(pluginEnabled).toBe(true)
expect(await page.evaluate(`typeof logseq.api.get_current_graph`))
.toBe('function')
const currentGraph = await callAPI('get_current_graph')
expect(Object.keys(currentGraph)).toEqual(['url', 'name', 'path'])
})
test.skip('play a plugin<logseq-journals-calendar> from the Marketplace', async ({ page }) => {
await page.keyboard.press('t+p')
const searchInput = page.locator('.search-ctls .form-input')
await searchInput.type('journals')
const pluginCards = page.locator('.cp__plugins-item-card')
if (await pluginCards.count()) {
await pluginCards.locator('.ctl .ls-icon-settings').hover()
await page.locator('text=Uninstall').click()
const confirmYes = page.locator('button').locator('text=Yes')
await confirmYes.click()
}
// install a plugin from Marketplace
await page.locator('button').locator('text=Marketplace').click()
await page.locator('text=Journals calendar')
await page.locator('.cp__plugins-item-card').first().locator('text=Install').click()
// wait for the plugin installed
await page.locator('.cp__plugins-item-card').first().locator('text=Installed')
await page.locator('a.ui__modal-close').click()
// toolbar plugins manager
const pluginFlag = page.locator('.toolbar-plugins-manager-trigger')
await expect(pluginFlag).toBeVisible()
await pluginFlag.click()
await expect(pluginFlag.locator('text=Plugins')).toBeVisible()
await expect(pluginFlag.locator('text=Settings')).toBeVisible()
await page.locator('text=goto-today').click()
await page.locator('body').click()
const goToToday = page.locator('#logseq-journals-calendar--goto-today').locator('a.button')
await expect(goToToday).toBeVisible()
await goToToday.click()
// TODO: debug
await expect(page.locator('body[data-page="page"]')).toBeVisible()
})
test(`play a plugin from local`, async ({ page }) => {
const callAPI = callPageAPI.bind(null, page)
const _pLoaded = await loadLocalE2eTestsPlugin(page)
const loc = page.locator('#a-plugin-for-e2e-tests')
await loc.waitFor({ state: 'visible' })
await callAPI(`push_state`, 'page', {name: 'contents'})
const b = await callAPI(`append_block_in_page`, 'Contents', 'target e2e block')
expect(typeof b?.uuid).toBe('string')
await expect(page.locator('text=[DB] hook: changed')).toBeVisible()
// 65a0beee-7e01-4e72-8d38-089d923a63de
await callAPI(`insert_block`, b.uuid,
'new custom uuid block', { customUUID: '65a0beee-7e01-4e72-8d38-089d923a63de' })
await expect(page.locator('text=[DB] hook: block changed')).toBeVisible()
// await page.waitForSelector('#test-pause')
})

View File

@@ -1,180 +0,0 @@
import { expect } from '@playwright/test'
import { test } from './fixtures'
import {
createRandomPage, randomInt, IsMac, randomString,
} from './utils'
/**
* Randomized test for single page editing. Block-wise.
*
* For now, only check total number of blocks.
*/
interface RandomTestStep {
/// target block
target: number;
/// action
op: string;
text: string;
/// expected total block number
expectedBlocks: number;
}
// TODO: add better frequency support
const availableOps = [
"insertByEnter",
"insertAtLast",
// "backspace", // FIXME: cannot backspace to delete block if has children, and prev is a parent, so skip
// "delete", // FIXME: cannot delete to delete block if next is outdented
"edit",
"moveUp",
"moveDown",
"indent",
"unindent",
"indent",
"unindent",
"indent",
"indent",
// TODO: selection
]
const generateRandomTest = (size: number): RandomTestStep[] => {
let blockCount = 1; // default block
let steps: RandomTestStep[] = []
for (let i = 0; i < size; i++) {
let op = availableOps[Math.floor(Math.random() * availableOps.length)];
// freq adjust
if (Math.random() > 0.9) {
op = "insertByEnter"
}
let loc = Math.floor(Math.random() * blockCount)
let text = randomString(randomInt(2, 3))
if (op === "insertByEnter" || op === "insertAtLast") {
blockCount++
} else if (op === "backspace") {
if (blockCount == 1) {
continue
}
blockCount--
text = null
} else if (op === "delete") {
if (blockCount == 1) {
continue
}
// cannot delete last block
if (loc === blockCount - 1) {
continue
}
blockCount--
text = null
} else if (op === "moveUp" || op === "moveDown") {
// no op
text = null
} else if (op === "indent" || op === "unindent") {
// no op
text = null
} else if (op === "edit") {
// no ap
} else {
throw new Error("unexpected op");
}
if (blockCount < 1) {
blockCount = 1
}
let step: RandomTestStep = {
target: loc,
op,
text,
expectedBlocks: blockCount,
}
steps.push(step)
}
return steps
}
// TODO: Fix test that intermittently started failing after https://github.com/logseq/logseq/pull/6945
test.skip('Random editor operations', async ({ page, block }) => {
const steps = generateRandomTest(20)
await createRandomPage(page)
await block.mustType("randomized test!")
for (let i = 0; i < steps.length; i++) {
let step = steps[i]
const { target, op, expectedBlocks, text } = step;
console.log(step)
if (op === "insertByEnter") {
await block.activeEditing(target)
let charCount = (await page.inputValue('textarea >> nth=0')).length
// FIXME: CHECK expect(await block.selectionStart()).toBe(charCount)
await page.keyboard.press('Enter', { delay: 50 })
// FIXME: CHECK await block.waitForBlocks(expectedBlocks)
// FIXME: use await block.mustType(text)
await block.mustFill(text)
} else if (op === "insertAtLast") {
await block.clickNext()
await block.mustType(text)
} else if (op === "backspace") {
await block.activeEditing(target)
const charCount = (await page.inputValue('textarea >> nth=0')).length
for (let i = 0; i < charCount + 1; i++) {
await page.keyboard.press('Backspace', { delay: 50 })
}
} else if (op === "delete") {
// move text-cursor to beginning
// then press delete
// then move text-cursor to the end
await block.activeEditing(target)
let charCount = (await page.inputValue('textarea >> nth=0')).length
for (let i = 0; i < charCount; i++) {
await page.keyboard.press('ArrowLeft', { delay: 50 })
}
expect.soft(await block.selectionStart()).toBe(0)
for (let i = 0; i < charCount + 1; i++) {
await page.keyboard.press('Delete', { delay: 50 })
}
await block.waitForBlocks(expectedBlocks)
charCount = (await page.inputValue('textarea >> nth=0')).length
for (let i = 0; i < charCount; i++) {
await page.keyboard.press('ArrowRight', { delay: 50 })
}
} else if (op === "edit") {
await block.activeEditing(target)
await block.mustFill('') // clear old text
await block.mustType(text)
} else if (op === "moveUp") {
await block.activeEditing(target)
if (IsMac) {
await page.keyboard.press('Meta+Shift+ArrowUp')
} else {
await page.keyboard.press('Alt+Shift+ArrowUp')
}
} else if (op === "moveDown") {
await block.activeEditing(target)
if (IsMac) {
await page.keyboard.press('Meta+Shift+ArrowDown')
} else {
await page.keyboard.press('Alt+Shift+ArrowDown')
}
} else if (op === "indent") {
await block.activeEditing(target)
await page.keyboard.press('Tab', { delay: 50 })
} else if (op === "unindent") {
await block.activeEditing(target)
await page.keyboard.press('Shift+Tab', { delay: 50 })
} else {
throw new Error("unexpected op");
}
// FIXME: CHECK await block.waitForBlocks(expectedBlocks)
await page.waitForTimeout(50)
}
})

View File

@@ -1,48 +0,0 @@
import { expect } from '@playwright/test'
import { test } from './fixtures'
import { createRandomPage } from './utils'
test('custom html should not spawn any dialogs', async ({ page, block }) => {
page.on('dialog', async dialog => {
expect(false).toBeTruthy()
await dialog.dismiss()
})
await createRandomPage(page)
await page.keyboard.type('<iframe src="javascript:confirm(1);" />', { delay: 5 })
await block.enterNext()
await page.keyboard.type('<button id="test-xss-button" onclick="confirm(1)">Click me!</button>', { delay: 5 })
await block.enterNext()
await page.keyboard.type('<details open id="test-xss-toggle" ontoggle="confirm(1)">test</details>', { delay: 5 })
await block.enterNext()
await page.click('#test-xss-toggle')
await page.click('#test-xss-button')
expect(true).toBeTruthy()
})
test('custom hiccup should not spawn any dialogs', async ({ page, block }) => {
page.on('dialog', async dialog => {
expect(false).toBeTruthy()
await dialog.dismiss()
})
await createRandomPage(page)
await page.keyboard.type('[:iframe {:src "javascript:confirm(1);"}]', { delay: 5 })
await block.enterNext()
expect(true).toBeTruthy()
})
test('"is" attribute should be allowed for plugin purposes', async ({ page, block }) => {
await createRandomPage(page)
await page.keyboard.type('[:div {:is "custom-element" :id "custom-element-id"}]', { delay: 5 })
await block.enterNext()
await expect(page.locator('#custom-element-id')).toHaveAttribute('is', 'custom-element');
})

View File

@@ -1,66 +0,0 @@
import { expect } from '@playwright/test'
import { test } from './fixtures'
import { createPage, createRandomPage, openLeftSidebar, randomString, searchAndJumpToPage } from './utils'
/***
* Test side bar features
***/
test('favorite item and recent item test', async ({ page }) => {
await openLeftSidebar(page)
// add page to fav
const fav_page_name = await createRandomPage(page)
let favs = await page.$$('.favorite-item a')
let previous_fav_count = favs.length
await page.click('.ui__dropdown-trigger .toolbar-dots-btn')
await page.locator("text=Add to Favorites").click()
// click from another page
const another_page_name = await createRandomPage(page)
expect(await page.innerText(':nth-match(.favorite-item a, 1)')).toBe(fav_page_name)
await page.waitForTimeout(500);
await page.click(":nth-match(.favorite-item, 1)")
await page.waitForTimeout(500);
expect(await page.innerText('.page-title .title')).toBe(fav_page_name)
expect(await page.innerText(':nth-match(.recent-item a, 1)')).toBe(fav_page_name)
expect(await page.innerText(':nth-match(.recent-item a, 2)')).toBe(another_page_name)
// remove fav
await page.click('.ui__dropdown-trigger .toolbar-dots-btn')
await page.locator("text=Unfavorite page").click()
await expect(page.locator('.favorite-item a')).toHaveCount(previous_fav_count)
// click from fav page
await page.click(':nth-match(.recent-item a, 2)')
await expect(page.locator('.page-title .title')).toHaveText(another_page_name)
})
test('recent is updated #4320', async ({ page }) => {
const page1 = await createRandomPage(page)
await page.fill('textarea >> nth=0', 'Random Thought')
const page2 = await createRandomPage(page)
await page.fill('textarea >> nth=0', 'Another Random Thought')
const firstRecent = page.locator('.nav-content-item.recent li >> nth=0')
expect(await firstRecent.textContent()).toContain(page2)
const secondRecent = page.locator('.nav-content-item.recent li >> nth=1')
expect(await secondRecent.textContent()).toContain(page1)
// then jump back
await searchAndJumpToPage(page, page1)
await page.waitForTimeout(500)
expect(await firstRecent.textContent()).toContain(page1)
expect(await secondRecent.textContent()).toContain(page2)
})
test('recent file name is displayed correctly #6297', async ({ page }) => {
const pageName = randomString(5) + "_@#$%^&*()_" + randomString(5)
await createPage(page, pageName)
await page.fill('textarea >> nth=0', 'Random Content')
const firstRecent = page.locator('.nav-content-item.recent li >> nth=0')
expect(await firstRecent.textContent()).toContain(pageName)
})

View File

@@ -1,55 +0,0 @@
import { BrowserContext, ElectronApplication, Locator, Page } from '@playwright/test';
/**
* Block provides helper functions for Logseq's block testing.
*/
export interface Block {
/** Must fill some text into a block, use `textarea >> nth=0` as selector. */
mustFill(value: string): Promise<void>;
/**
* Must type input some text into an **empty** block.
* **DO NOT USE** this if there's auto-complete
*/
mustType(value: string, options?: { delay?: number, toBe?: string }): Promise<void>;
/**
* Press Enter and go to next block, require cursor to be in current block(editing mode).
* When cursor is not at the end of block, trailing text will be moved to the next block.
*/
enterNext(): Promise<Locator>;
/** Click `.add-button-link-wrap` and create the next block. */
clickNext(): Promise<Locator>;
/** Indent block, return whether it's success. */
indent(): Promise<boolean>;
/** Unindent block, return whether it's success. */
unindent(): Promise<boolean>;
/** Await for a certain number of blocks, with default timeout. */
waitForBlocks(total: number): Promise<void>;
/** Await for a certain number of selected blocks, with default timeout. */
waitForSelectedBlocks(total: number): Promise<void>;
/** Escape editing mode, modal popup and selection. */
escapeEditing(): Promise<void>;
/** Active block editing, by click */
activeEditing(nth: number): Promise<void>;
/** Is editing block now? */
isEditing(): Promise<boolean>;
/** Find current selectionStart, i.e. text cursor position. */
selectionStart(): Promise<number>;
/** Find current selectionEnd. */
selectionEnd(): Promise<number>;
}
export interface autocompleteMenu {
// Expect or wait for autocomplete menu to be or become visible
expectVisible(modalName?: string): Promise<void>
// Expect or wait for autocomplete menu to be or become hidden
expectHidden(modalName?: string): Promise<void>
}
export interface LogseqFixtures {
page: Page;
block: Block;
autocompleteMenu: autocompleteMenu;
context: BrowserContext;
app: ElectronApplication;
graphDir: string;
}

View File

@@ -1,42 +0,0 @@
// This file is used to store basic functions that are used in other utils
// Should have no dependency on other utils
import * as process from 'process'
export const IsMac = process.platform === 'darwin'
export const IsLinux = process.platform === 'linux'
export const IsWindows = process.platform === 'win32'
export const IsCI = process.env.CI === 'true'
export const modKey = IsMac ? 'Meta' : 'Control'
export function randomString(length: number) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
export function randomLowerString(length: number) {
const characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
export function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1) + min)
}
export function randomBoolean(): boolean {
return Math.random() < 0.5;
}

View File

@@ -1,64 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<script>
'use strict';
const keys = [
// https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent
// without deprecated / non-standard
"altKey", "code", "ctrlKey", "isComposing", "key", "locale", "location", "metaKey",
"repeat", "shiftKey"
]
let output_list = [];
let last_timestamp = Date.now();
function select_keys(obj, keys) {
let new_obj = {}
for (let k in event)
if (keys.indexOf(k) != -1)
new_obj[k] = event[k];
return new_obj
}
let key_handler_builder = (event_type) => (event) => {
if (event["target"].id != "input")
return;
let cur_timestamp = Date.now();
let output = {
"event_type": event_type,
"event": select_keys(event, keys),
"latency": cur_timestamp - last_timestamp // Time to wait before firing event
}
last_timestamp = cur_timestamp;
output_list.push(output);
let to_print = JSON.stringify(
output_list,
undefined,
2);
document.getElementById("outputs").innerText = to_print;
}
document.addEventListener('keydown', key_handler_builder('keydown'), false);
document.addEventListener('keyup', key_handler_builder('keyup'), false);
document.addEventListener('keypress', key_handler_builder('keypress'), false);
document.addEventListener('compositionstart', key_handler_builder('compositionstart'), false);
document.addEventListener('compositionend', key_handler_builder('compositionend'), false);
document.addEventListener('compositionupdate', key_handler_builder('compositionupdate'), false);
window.onload = (e) => {
document.getElementById("input").focus();
}
</script>
</head>
<body>
<input id="input" />
<h2>Key Down</h2>
<p id="outputs" style="white-space: pre;" />
</body>
</html>

View File

@@ -1,466 +0,0 @@
/***
* Author: Junyi Du <junyi@logseq.com>
* References:
* https://stackoverflow.com/questions/8892238/detect-keyboard-layout-with-javascript
* ***/
import { Page } from '@playwright/test'
interface RecordedEvent {
event_type: string;
event: any; // KeyboardEvent is too heavy
latency: number;
}
export let dispatch_kb_events = async function (page: Page, selector: string, keyboard_events: RecordedEvent[] ){
for (let kbev of keyboard_events){
let { event_type, event, latency } = kbev
await page.waitForTimeout(latency)
await page.dispatchEvent(selector, event_type, event)
}
}
export let macos_pinyin_left_full_square_bracket: RecordedEvent[] = [
{
"event_type": "keydown",
"event": {
"key": "【",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 0
},
{
"event_type": "keypress",
"event": {
"key": "【",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 1
},
{
"event_type": "keyup",
"event": {
"key": "【",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 17
}
]
export let win10_pinyin_left_full_square_bracket: RecordedEvent[] = [
{
"event_type": "keydown",
"event": {
"key": "Process",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 0
},
{
"event_type": "compositionstart",
"event": {},
"latency": 4
},
{
"event_type": "compositionupdate",
"event": {},
"latency": 0
},
{
"event_type": "compositionupdate",
"event": {},
"latency": 12
},
{
"event_type": "compositionend",
"event": {},
"latency": 1
},
{
"event_type": "keyup",
"event": {
"key": "Process",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 61
},
{
"event_type": "keyup",
"event": {
"key": "[",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 1
}
]
export let win10_legacy_pinyin_left_full_square_bracket: RecordedEvent[] = [
{
"event_type": "keydown",
"event": {
"key": "Process",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 0
},
{
"event_type": "compositionstart",
"event": {},
"latency": 1
},
{
"event_type": "compositionupdate",
"event": {},
"latency": 0
},
{
"event_type": "compositionupdate",
"event": {},
"latency": 0
},
{
"event_type": "compositionend",
"event": {},
"latency": 1
},
{
"event_type": "keyup",
"event": {
"key": "Process",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 93
}
]
export let macos_pinyin_selecting_candidate_double_left_square_bracket: RecordedEvent[] = [
{
"event_type": "keydown",
"event": {
"key": "b",
"code": "KeyB",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 0
},
{
"event_type": "compositionstart",
"event": {},
"latency": 1
},
{
"event_type": "compositionupdate",
"event": {},
"latency": 0
},
{
"event_type": "keyup",
"event": {
"key": "b",
"code": "KeyB",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 48
},
{
"event_type": "keydown",
"event": {
"key": "】",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 200
},
{
"event_type": "keyup",
"event": {
"key": "】",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 59
},
{
"event_type": "keydown",
"event": {
"key": "】",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 289
},
{
"event_type": "keyup",
"event": {
"key": "】",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 73
},
{
"event_type": "keydown",
"event": {
"key": "【",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 443
},
{
"event_type": "keyup",
"event": {
"key": "【",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 79
},
{
"event_type": "keydown",
"event": {
"key": "【",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 155
},
{
"event_type": "keyup",
"event": {
"key": "【",
"code": "BracketLeft",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 44
},
{
"event_type": "compositionend",
"event": {},
"latency": 200
}
]
export let win10_RIME_selecting_candidate_double_left_square_bracket: RecordedEvent[] = [
{
"event_type": "keydown",
"event": {
"key": "Process",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": false
},
"latency": 0
},
{
"event_type": "compositionstart",
"event": {},
"latency": 0
},
{
"event_type": "compositionupdate",
"event": {},
"latency": 0
},
{
"event_type": "keyup",
"event": {
"key": "Process",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 79
},
{
"event_type": "keyup",
"event": {
"key": "]",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 3
},
{
"event_type": "keydown",
"event": {
"key": "Process",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 200
},
{
"event_type": "keyup",
"event": {
"key": "Process",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 96
},
{
"event_type": "keyup",
"event": {
"key": "]",
"code": "BracketRight",
"location": 0,
"ctrlKey": false,
"shiftKey": false,
"altKey": false,
"metaKey": false,
"repeat": false,
"isComposing": true
},
"latency": 3
},
{
"event_type": "compositionend",
"event": {},
"latency": 200
}
]

View File

@@ -1,14 +0,0 @@
import { Page } from '@playwright/test'
export async function activateNewPage(page: Page) {
await page.click('.ls-block >> nth=0')
await page.waitForTimeout(500)
}
export async function renamePage(page: Page, new_name: string) {
await page.click('.ls-page-title .page-title')
await page.waitForSelector('input[type="text"]')
await page.fill('input[type="text"]', '')
await page.type('.title input', new_name)
await page.keyboard.press('Enter')
}

View File

@@ -1,64 +0,0 @@
import { Page, Locator, ElementHandle } from '@playwright/test'
import { randomString } from './basic'
export async function closeSearchBox(page: Page): Promise<void> {
await page.keyboard.press("Escape", { delay: 50 }) // escape (potential) search box typing
await page.waitForTimeout(500)
await page.keyboard.press("Escape", { delay: 50 }) // escape modal
}
export async function createRandomPage(page: Page) {
const randomTitle = randomString(20)
await closeSearchBox(page)
// Click #search-button
await page.click('#search-button')
// Fill [placeholder="What are you looking for?"]
await page.fill('[placeholder="What are you looking for?"]', randomTitle)
await page.keyboard.press('Enter', { delay: 50 })
// Wait for h1 to be from our new page
await page.waitForSelector(`h1 >> text="${randomTitle}"`, { state: 'visible' })
// wait for textarea of first block
await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
return randomTitle;
}
export async function createPage(page: Page, page_name: string) {// Click #search-button
await closeSearchBox(page)
await page.click('#search-button')
// Fill [placeholder="What are you looking for?"]
await page.fill('[placeholder="What are you looking for?"]', page_name)
await page.locator('text="Create page"').waitFor({ state: 'visible' })
await page.keyboard.press('Enter', { delay: 100 })
// wait for textarea of first block
await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
return page_name;
}
export async function searchAndJumpToPage(page: Page, pageTitle: string) {
await closeSearchBox(page)
await page.click('#search-button')
await page.type('[placeholder="What are you looking for?"]', pageTitle)
await page.waitForTimeout(200)
await page.keyboard.press('Enter', { delay: 50 })
return pageTitle;
}
/**
* type a search query into the search box
* stop at the point where search box shows up
*
* @param page the pw page object
* @param query the search query to type into the search box
* @returns the HTML element for the search results ui
*/
export async function searchPage(page: Page, query: string): Promise<ElementHandle<SVGElement | HTMLElement>[]> {
await closeSearchBox(page)
await page.click('#search-button')
await page.waitForSelector('[placeholder="What are you looking for?"]')
await page.fill('[placeholder="What are you looking for?"]', query)
await page.waitForTimeout(2000) // wait longer for search contents to render
return page.$$('.search-results>div');
}

View File

@@ -1,310 +0,0 @@
import { Page, Locator } from 'playwright'
import { expect, ConsoleMessage } from '@playwright/test'
import * as pathlib from 'path'
import { modKey } from './util/basic'
import { Block } from './types'
// TODO: The file should be a facade of utils in the /util folder
// No more additional functions should be added to this file
// Move the functions to the corresponding files in the /util folder
// Criteria: If the same selector is shared in multiple functions, they should be in the same file
export * from './util/basic'
export * from './util/search-modal'
export * from './util/page'
/**
* Locate the last block in the inner editor
* @param page The Playwright Page object.
* @returns The locator of the last block.
*/
export async function lastBlock(page: Page): Promise<Locator> {
// discard any popups
await page.keyboard.press('Escape')
// click last block
if (await page.locator('text="Click here to edit..."').isVisible()) {
await page.click('text="Click here to edit..."')
} else {
await page.click('.page-blocks-inner .ls-block >> nth=-1')
}
// wait for textarea
await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
await page.waitForTimeout(100)
return page.locator('textarea >> nth=0')
}
/**
* Move the cursor to the beginning of the current editor
* @param page The Playwright Page object.
*/
export async function moveCursorToBeginning(page: Page): Promise<Locator> {
await page.press('textarea >> nth=0', modKey + '+a') // select all
await page.press('textarea >> nth=0', 'ArrowLeft')
return page.locator('textarea >> nth=0')
}
/**
* Move the cursor to the end of the current editor
* @param page The Playwright Page object.
*/
export async function moveCursorToEnd(page: Page): Promise<Locator> {
await page.press('textarea >> nth=0', modKey + '+a') // select all
await page.press('textarea >> nth=0', 'ArrowRight')
return page.locator('textarea >> nth=0')
}
/**
* Press Enter and create the next block.
* @param page The Playwright Page object.
*/
export async function enterNextBlock(page: Page): Promise<Locator> {
// Move cursor to the end of the editor
await page.press('textarea >> nth=0', modKey + '+a') // select all
await page.press('textarea >> nth=0', 'ArrowRight')
let blockCount = await page.locator('.page-blocks-inner .ls-block').count()
await page.press('textarea >> nth=0', 'Enter')
await page.waitForTimeout(10)
await page.waitForSelector(`.ls-block >> nth=${blockCount} >> textarea`, { state: 'visible' })
return page.locator('textarea >> nth=0')
}
/**
* Create and locate a new block at the end of the inner editor
* @param page The Playwright Page object
* @returns The locator of the last block
*/
export async function newInnerBlock(page: Page): Promise<Locator> {
await lastBlock(page)
await page.press('textarea >> nth=0', 'Enter')
return page.locator('textarea >> nth=0')
}
export async function escapeToCodeEditor(page: Page): Promise<void> {
await page.press('.block-editor textarea', 'Escape')
await page.waitForSelector('.CodeMirror pre', { state: 'visible' })
await page.waitForTimeout(300)
await page.click('.CodeMirror pre')
await page.waitForTimeout(300)
await page.waitForSelector('.CodeMirror textarea', { state: 'visible' })
}
export async function escapeToBlockEditor(page: Page): Promise<void> {
await page.waitForTimeout(300)
await page.click('.CodeMirror pre')
await page.waitForTimeout(300)
await page.press('.CodeMirror textarea', 'Escape')
await page.waitForTimeout(300)
}
export async function setMockedOpenDirPath(
page: Page,
path?: string
): Promise<void> {
// set next open directory
await page.evaluate(
([path]) => {
Object.assign(window, {
__MOCKED_OPEN_DIR_PATH__: path,
})
},
[path]
)
}
export async function openLeftSidebar(page: Page): Promise<void> {
let sidebar = page.locator('#left-sidebar')
// Left sidebar is toggled by `is-open` class
if (!/is-open/.test(await sidebar.getAttribute('class') || '')) {
await page.click('#left-menu.button')
await page.waitForTimeout(10)
await expect(sidebar).toHaveClass(/is-open/)
}
}
export async function loadLocalGraph(page: Page, path: string): Promise<void> {
await setMockedOpenDirPath(page, path);
const sidebar = page.locator('#left-sidebar')
if (!/is-open/.test(await sidebar.getAttribute('class') || '')) {
await page.click('#left-menu.button')
await expect(sidebar).toHaveClass(/is-open/)
}
await page.click('#left-sidebar .cp__graphs-selector > a')
await page.waitForTimeout(300)
await page.waitForSelector('.cp__repos-quick-actions >> text="Add new graph"',
{ state: 'attached', timeout: 5000 })
await page.click('text=Add new graph')
await setMockedOpenDirPath(page, ''); // reset it
await page.waitForSelector(':has-text("Parsing files")', {
state: 'hidden',
timeout: 1000 * 60 * 5,
})
const title = await page.title()
if (title === "Import data into Logseq" || title === "Add another repo") {
await page.click('a.button >> text=Skip')
}
await page.waitForFunction('window.document.title === "Logseq"')
await page.waitForTimeout(500)
// If there is an error notification from a previous test graph being deleted,
// close it first so it doesn't cover up the UI
let n = await page.locator('.notification-close-button').count()
if (n > 1) {
await page.locator('button >> text="Clear all"').click()
} else if (n == 1) {
await page.locator('.notification-close-button').click()
}
await expect(page.locator('.notification-close-button').first()).not.toBeVisible({ timeout: 2000 })
console.log('Graph loaded for ' + path)
}
export async function editNthBlock(page: Page, n) {
await page.click(`.ls-block .block-content >> nth=${n}`)
}
export async function editFirstBlock(page: Page) {
await editNthBlock(page, 0)
}
/**
* Wait for a console message with a given prefix to appear, and return the full text of the message
* Or reject after a timeout
*
* @param page
* @param prefix - the prefix to look for
* @param timeout - the timeout in ms
* @returns the full text of the console message
*/
export async function captureConsoleWithPrefix(page: Page, prefix: string, timeout: number = 3000): Promise<string> {
return new Promise((resolve, reject) => {
let console_handler = (msg: ConsoleMessage) => {
let text = msg.text()
if (text.startsWith(prefix)) {
page.removeListener('console', console_handler)
resolve(text.substring(prefix.length))
}
}
page.on('console', console_handler)
setTimeout(reject.bind("timeout"), timeout)
})
}
export async function queryPermission(page: Page, permission: PermissionName): Promise<boolean> {
// Check if WebAPI clipboard supported
return await page.evaluate(async (eval_permission: PermissionName): Promise<boolean> => {
if (typeof navigator.permissions == "undefined")
return Promise.resolve(false);
return navigator.permissions.query({
name: eval_permission
}).then((result: PermissionStatus): boolean => {
return (result.state == "granted" || result.state == "prompt")
})
}, permission)
}
export async function doesClipboardItemExists(page: Page): Promise<boolean> {
// Check if WebAPI clipboard supported
return await page.evaluate((): boolean => {
return typeof ClipboardItem !== "undefined"
})
}
export async function getIsWebAPIClipboardSupported(page: Page): Promise<boolean> {
// @ts-ignore "clipboard-write" is not included in TS's type definition for permissionName
return await queryPermission(page, "clipboard-write") && await doesClipboardItemExists(page)
}
export async function navigateToStartOfBlock(page: Page, block: Block) {
const selectionStart = await block.selectionStart()
for (let i = 0; i < selectionStart; i++) {
await page.keyboard.press('ArrowLeft')
}
}
/**
* Repeats a key press a certain number of times.
* @param {Page} page - The Page object.
* @param {string} key - The key to press.
* @param {number} times - The number of times to press the key.
* @return {Promise<void>} - Promise which resolves when the key press repetition is done.
*/
export async function repeatKeyPress(page: Page, key: string, times: number): Promise<void> {
for (let i = 0; i < times; i++) {
await page.keyboard.press(key);
}
}
/**
* Moves the cursor a certain number of characters to the right (positive value) or left (negative value).
* @param {Page} page - The Page object.
* @param {number} shift - The number of characters to move the cursor. Positive moves to the right, negative to the left.
* @return {Promise<void>} - Promise which resolves when the cursor has moved.
*/
export async function moveCursor(page: Page, shift: number): Promise<void> {
const direction = shift < 0 ? 'ArrowLeft' : 'ArrowRight';
const absShift = Math.abs(shift);
await repeatKeyPress(page, direction, absShift);
}
/**
* Selects a certain length of text in a textarea to the right of the cursor.
* @param {Page} page - The Page object.
* @param {number} length - The number of characters to select.
* @return {Promise<void>} - Promise which resolves when the text selection is done.
*/
export async function selectCharacters(page: Page, length: number): Promise<void> {
await page.keyboard.down('Shift');
await repeatKeyPress(page, 'ArrowRight', length);
await page.keyboard.up('Shift');
}
/**
* Retrieves the selected text in a textarea.
* @param {Page} page - The page object.
* @return {Promise<string | null>} - Promise which resolves to the selected text or null.
*/
export async function getSelection(page: Page): Promise<string | null> {
const selection = await page.evaluate(() => {
const textarea = document.querySelector('textarea')
return textarea?.value.substring(textarea.selectionStart, textarea.selectionEnd) || null
})
return selection
}
/**
* Retrieves the current cursor position in a textarea.
* @param {Page} page - The page object.
* @return {Promise<number | null>} - Promise which resolves to the cursor position or null.
*/
export async function getCursorPos(page: Page): Promise<number | null> {
const cursorPosition = await page.evaluate(() => {
const textarea = document.querySelector('textarea');
return textarea ? textarea.selectionStart : null;
});
return cursorPosition;
}
/**
* @param page
* @param method
* @param args
*/
export async function callPageAPI(page, method, ...args) {
return await page.evaluate(([method, args]) => {
// @ts-ignore
return window.logseq.api[method]?.(...args)
}, [method, args])
}

View File

@@ -1,526 +0,0 @@
import { expect } from '@playwright/test'
import { test } from './fixtures'
import { modKey, renamePage } from './utils'
test('enable whiteboards', async ({ page }) => {
if (await page.$('.nav-header .whiteboard') === null) {
await page.click('#head .toolbar-dots-btn')
await page.click('#head .dropdown-wrapper >> text=Settings')
await page.click('.settings-modal a[data-id=features]')
await page.click('text=Whiteboards >> .. >> .ui__toggle')
await page.waitForTimeout(1000)
await page.keyboard.press('Escape')
}
await expect(page.locator('.nav-header .whiteboard')).toBeVisible()
})
test('should display onboarding tour', async ({ page }) => {
// ensure onboarding tour is going to be triggered locally
await page.evaluate(`window.clearWhiteboardStorage()`)
await page.click('.nav-header .whiteboard')
await expect(page.locator('.cp__whiteboard-welcome')).toBeVisible()
await page.click('.cp__whiteboard-welcome button.skip-welcome')
await expect(page.locator('.cp__whiteboard-welcome')).toBeHidden()
})
test('create new whiteboard', async ({ page }) => {
await page.click('#tl-create-whiteboard')
await expect(page.locator('.logseq-tldraw')).toBeVisible()
})
test('can right click title to show context menu', async ({ page }) => {
await page.click('.whiteboard-page-title', {
button: 'right',
})
await expect(page.locator('#custom-context-menu')).toBeVisible()
await page.keyboard.press('Escape')
await expect(page.locator('#custom-context-menu')).toHaveCount(0)
})
test('newly created whiteboard should have a default title', async ({ page }) => {
await expect(page.locator('.whiteboard-page-title .title')).toContainText(
'Untitled'
)
})
test('set whiteboard title', async ({ page }) => {
const title = 'my-whiteboard'
await page.click('.nav-header .whiteboard')
await page.click('#tl-create-whiteboard')
await page.click('.whiteboard-page-title')
await page.fill('.whiteboard-page-title input', title)
await page.keyboard.press('Enter')
await expect(page.locator('.whiteboard-page-title .title')).toContainText(
title
)
})
test('update whiteboard title', async ({ page }) => {
const title = 'my-whiteboard'
await page.click('.whiteboard-page-title')
await page.fill('.whiteboard-page-title input', title + '-2')
await page.keyboard.press('Enter')
await expect(page.locator('.whiteboard-page-title .title')).toContainText(
title + '-2'
)
})
test('draw a rectangle', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.keyboard.type('wr')
await page.mouse.move(bounds.x + 105, bounds.y + 105)
await page.mouse.down()
await page.mouse.move(bounds.x + 150, bounds.y + 150 )
await page.mouse.up()
await page.keyboard.press('Escape')
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
})
test('undo the rectangle action', async ({ page }) => {
await page.keyboard.press(modKey + '+z', { delay: 100 })
await expect(page.locator('.logseq-tldraw .tl-positioned-svg rect')).toHaveCount(0)
})
test('redo the rectangle action', async ({ page }) => {
await page.waitForTimeout(100)
await page.keyboard.press(modKey + '+Shift+z', { delay: 100 })
await page.keyboard.press('Escape')
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
})
test('clone the rectangle', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.mouse.move(bounds.x + 400, bounds.y + 400)
await page.mouse.move(bounds.x + 120, bounds.y + 120, {steps: 5})
await page.keyboard.down('Alt')
await page.mouse.down()
await page.mouse.move(bounds.x + 200, bounds.y + 200, {steps: 5})
await page.mouse.up()
await page.keyboard.up('Alt')
await page.waitForTimeout(100)
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
})
test('group the rectangles', async ({ page }) => {
await page.keyboard.press(modKey + '+a')
await page.keyboard.press(modKey + '+g')
await expect(page.locator('.logseq-tldraw .tl-group-container')).toHaveCount(1)
})
test('delete the group', async ({ page }) => {
await page.keyboard.press(modKey + '+a')
await page.keyboard.press('Delete')
await expect(page.locator('.logseq-tldraw .tl-group-container')).toHaveCount(0)
// should also delete the grouped shapes
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(0)
})
test('undo the group deletion', async ({ page }) => {
await page.keyboard.press(modKey + '+z')
await expect(page.locator('.logseq-tldraw .tl-group-container')).toHaveCount(1)
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
})
test('undo the group action', async ({ page }) => {
await page.keyboard.press(modKey + '+z')
await expect(page.locator('.logseq-tldraw .tl-group-container')).toHaveCount(0)
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
})
test('connect rectangles with an arrow', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.keyboard.type('wc')
await page.mouse.move(bounds.x + 120, bounds.y + 120)
await page.mouse.down()
await page.mouse.move(bounds.x + 200, bounds.y + 200, {steps: 5}) // will fail without steps
await page.mouse.up()
await page.keyboard.press('Escape')
await expect(page.locator('.logseq-tldraw .tl-line-container')).toHaveCount(1)
})
test('delete the first rectangle', async ({ page }) => {
await page.keyboard.press('Escape', { delay: 100 })
await page.keyboard.press('Escape', { delay: 100 })
await page.click('.logseq-tldraw .tl-box-container:first-of-type')
await page.keyboard.press('Delete')
await page.waitForTimeout(200)
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
await expect(page.locator('.logseq-tldraw .tl-line-container')).toHaveCount(0)
})
test('undo the delete action', async ({ page }) => {
await page.keyboard.press(modKey + '+z')
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
await expect(page.locator('.logseq-tldraw .tl-line-container')).toHaveCount(1)
})
test('convert the first rectangle to ellipse', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.keyboard.press('Escape')
await page.mouse.move(bounds.x + 220, bounds.y + 220)
await page.mouse.down()
await page.mouse.up()
await page.mouse.move(bounds.x + 520, bounds.y + 520)
await page.click('.tl-context-bar .tl-geometry-tools-pane-anchor')
await page.click('.tl-context-bar .tl-geometry-toolbar [data-tool=ellipse]')
await expect(page.locator('.logseq-tldraw .tl-ellipse-container')).toHaveCount(1)
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(1)
})
test('change the color of the ellipse', async ({ page }) => {
await page.click('.tl-context-bar .tl-color-bg')
await page.click('.tl-context-bar .tl-color-palette .bg-red-500')
await expect(page.locator('.logseq-tldraw .tl-ellipse-container ellipse:last-of-type')).toHaveAttribute('fill', 'var(--ls-wb-background-color-red)')
})
test('undo the color switch', async ({ page }) => {
await page.keyboard.press(modKey + '+z')
await expect(page.locator('.logseq-tldraw .tl-ellipse-container ellipse:last-of-type')).toHaveAttribute('fill', 'var(--ls-wb-background-color-default)')
})
test('undo the shape conversion', async ({ page }) => {
await page.keyboard.press(modKey + '+z')
await page.waitForTimeout(100)
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
await expect(page.locator('.logseq-tldraw .tl-ellipse-container')).toHaveCount(0)
})
test('locked elements should not be removed', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.keyboard.press('Escape')
await page.mouse.move(bounds.x + 220, bounds.y + 220)
await page.mouse.down()
await page.mouse.up()
await page.mouse.move(bounds.x + 520, bounds.y + 520)
await page.keyboard.press(`${modKey}+l`, { delay: 100 })
await page.keyboard.press('Delete', { delay: 100 })
await page.keyboard.press(`${modKey}+Shift+l`, { delay: 100 })
await expect(page.locator('.logseq-tldraw .tl-box-container')).toHaveCount(2)
})
test('move arrow to back', async ({ page }) => {
await page.keyboard.press('Escape')
await page.waitForTimeout(1000)
await page.click('.logseq-tldraw .tl-line-container')
await page.keyboard.press('Shift+[')
await expect(page.locator('.logseq-tldraw .tl-canvas .tl-layer > div:first-of-type > div:first-of-type')).toHaveClass('tl-line-container')
})
test('move arrow to front', async ({ page }) => {
await page.keyboard.press('Escape')
await page.waitForTimeout(1000)
await page.click('.logseq-tldraw .tl-line-container')
await page.keyboard.press('Shift+]')
await expect(page.locator('.logseq-tldraw .tl-canvas .tl-layer > div:first-of-type > div:first-of-type')).not.toHaveClass('tl-line-container')
})
test('undo the move action', async ({ page }) => {
await page.keyboard.press(modKey + '+z')
await page.waitForTimeout(100)
await expect(page.locator('.logseq-tldraw .tl-canvas .tl-layer > div:first-of-type > div:first-of-type')).toHaveClass('tl-line-container')
})
test('cleanup the shapes', async ({ page }) => {
await page.keyboard.press(`${modKey}+a`)
await page.keyboard.press('Delete')
await page.waitForTimeout(100)
await expect(page.locator('[data-type=Shape]')).toHaveCount(0)
})
test('create a block', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.keyboard.type('ws')
await page.mouse.dblclick(bounds.x + 105, bounds.y + 105)
await page.waitForTimeout(100)
await page.keyboard.type('a')
await page.keyboard.press('Enter')
await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container')).toHaveCount(1)
})
// TODO: Fix the failing test
test.skip('expand the block', async ({ page }) => {
await page.keyboard.press('Escape')
await page.keyboard.press(modKey + '+ArrowDown')
await page.waitForTimeout(100)
await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(1)
})
// TODO: Depends on the previous test
test.skip('undo the expand action', async ({ page }) => {
await page.keyboard.press(modKey + '+z')
await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container .tl-logseq-portal-header')).toHaveCount(0)
})
// TODO: Fix the failing test
test.skip('undo the block action', async ({ page }) => {
await page.keyboard.press(modKey + '+z')
await expect(page.locator('.logseq-tldraw .tl-logseq-portal-container')).toHaveCount(0)
})
test('copy/paste url to create an iFrame shape', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.keyboard.type('wt')
await page.mouse.move(bounds.x + 105, bounds.y + 105)
await page.mouse.down()
await page.waitForTimeout(100)
await page.keyboard.type('https://logseq.com')
await page.keyboard.press(modKey + '+a')
await page.keyboard.press(modKey + '+c')
await page.keyboard.press('Escape')
await page.keyboard.press(modKey + '+v')
await expect( page.locator('.logseq-tldraw .tl-iframe-container')).toHaveCount(1)
})
test('copy/paste X status url to create a Post shape', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.keyboard.type('wt')
await page.mouse.move(bounds.x + 105, bounds.y + 105)
await page.mouse.down()
await page.waitForTimeout(100)
await page.keyboard.type('https://x.com/logseq/status/1605224589046386689')
await page.keyboard.press(modKey + '+a')
await page.keyboard.press(modKey + '+c')
await page.keyboard.press('Escape')
await page.keyboard.press(modKey + '+v')
await expect( page.locator('.logseq-tldraw .tl-tweet-container')).toHaveCount(1)
})
test('copy/paste twitter status url to create a Tweet shape', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.keyboard.type('wt')
await page.mouse.move(bounds.x + 105, bounds.y + 105)
await page.mouse.down()
await page.waitForTimeout(100)
await page.keyboard.type('https://twitter.com/logseq/status/1605224589046386689')
await page.keyboard.press(modKey + '+a')
await page.keyboard.press(modKey + '+c')
await page.keyboard.press('Escape')
await page.keyboard.press(modKey + '+v')
await expect( page.locator('.logseq-tldraw .tl-tweet-container')).toHaveCount(2)
})
test('copy/paste youtube video url to create a Youtube shape', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
const bounds = (await canvas.boundingBox())!
await page.keyboard.type('wt')
await page.mouse.move(bounds.x + 105, bounds.y + 105)
await page.mouse.down()
await page.waitForTimeout(100)
await page.keyboard.type('https://www.youtube.com/watch?v=hz2BacySDXE')
await page.keyboard.press(modKey + '+a')
await page.keyboard.press(modKey + '+c')
await page.keyboard.press('Escape')
await page.keyboard.press(modKey + '+v')
await expect(page.locator('.logseq-tldraw .tl-youtube-container')).toHaveCount(1)
})
test('zoom in', async ({ page }) => {
await page.keyboard.press('Shift+0') // reset zoom
await page.waitForTimeout(1500) // wait for the zoom animation to finish
await page.keyboard.press('Shift+=')
await page.waitForTimeout(1500) // wait for the zoom animation to finish
await expect(page.locator('#tl-zoom')).toContainText('125%')
})
test('zoom out', async ({ page }) => {
await page.keyboard.press('Shift+0')
await page.waitForTimeout(1500) // wait for the zoom animation to finish
await page.keyboard.press('Shift+-')
await page.waitForTimeout(1500) // wait for the zoom animation to finish
await expect(page.locator('#tl-zoom')).toContainText('80%')
})
test('open context menu', async ({ page }) => {
await page.locator('.logseq-tldraw').click({ button: 'right' })
await expect(page.locator('.tl-context-menu')).toBeVisible()
})
test('close context menu on esc', async ({ page }) => {
await page.keyboard.press('Escape')
await expect(page.locator('.tl-context-menu')).toBeHidden()
})
test('quick add another whiteboard', async ({ page }) => {
// create a new board first
await page.click('.nav-header .whiteboard')
await page.click('#tl-create-whiteboard')
await page.click('.whiteboard-page-title')
await page.fill('.whiteboard-page-title input', 'my-whiteboard-3')
await page.keyboard.press('Enter')
await page.waitForTimeout(300)
const canvas = await page.waitForSelector('.logseq-tldraw')
await canvas.dblclick({
position: {
x: 200,
y: 200,
},
})
const quickAdd$ = page.locator('.tl-quick-search')
await expect(quickAdd$).toBeVisible()
await page.fill('.tl-quick-search input', 'my-whiteboard')
await quickAdd$
.locator('.tl-quick-search-option >> text=my-whiteboard-2')
.first()
.click()
await expect(quickAdd$).toBeHidden()
await expect(
page.locator('.tl-logseq-portal-container >> text=my-whiteboard-2')
).toBeVisible()
})
test('go to another board and check reference', async ({ page }) => {
await page
.locator('.tl-logseq-portal-container >> text=my-whiteboard-2')
.click()
await expect(page.locator('.whiteboard-page-title .title')).toContainText(
'my-whiteboard-2'
)
const pageRefCount$ = page.locator('.whiteboard-page-refs-count')
await expect(pageRefCount$.locator('.open-page-ref-link')).toContainText('1')
})
test('Create an embedded whiteboard', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
await canvas.dblclick({
position: {
x: 110,
y: 110,
},
})
const quickAdd$ = page.locator('.tl-quick-search')
await expect(quickAdd$).toBeVisible()
await page.fill('.tl-quick-search input', 'My embedded whiteboard')
await quickAdd$
.locator('div[data-index="2"] .tl-quick-search-option')
.first()
.click()
await expect(quickAdd$).toBeHidden()
await expect(page.locator('.tl-logseq-portal-header a')).toContainText('My embedded whiteboard')
})
test('New whiteboard should have the correct name', async ({ page }) => {
page.locator('.tl-logseq-portal-header a').click()
await expect(page.locator('.whiteboard-page-title')).toContainText('My embedded whiteboard')
})
test('Create an embedded page', async ({ page }) => {
const canvas = await page.waitForSelector('.logseq-tldraw')
await canvas.dblclick({
position: {
x: 150,
y: 150,
},
})
const quickAdd$ = page.locator('.tl-quick-search')
await expect(quickAdd$).toBeVisible()
await page.fill('.tl-quick-search input', 'My page')
await quickAdd$
.locator('div[data-index="1"] .tl-quick-search-option')
.first()
.click()
await expect(quickAdd$).toBeHidden()
await expect(page.locator('.tl-logseq-portal-header a')).toContainText('My page')
})
test('New page should have the correct name', async ({ page }) => {
page.locator('.tl-logseq-portal-header a').click()
await expect(page.locator('.ls-page-title')).toContainText('My page')
})
test('Renaming a page to an existing whiteboard name should be prohibited', async ({ page }) => {
await renamePage(page, "My embedded whiteboard")
await expect(page.locator('.page-title input')).toHaveValue('My page')
})

View File

@@ -1,34 +0,0 @@
import { expect } from '@playwright/test'
import { test } from './fixtures'
import { IsMac } from './utils';
if (!IsMac) {
test('window should not be maximized on first launch', async ({ page, app }) => {
await expect(page.locator('.window-controls .maximize-toggle.maximize')).toHaveCount(1)
})
test('window should be maximized and icon should change on maximize-toggle click', async ({ page }) => {
await page.click('.window-controls .maximize-toggle.maximize')
await expect(page.locator('.window-controls .maximize-toggle.restore')).toHaveCount(1)
})
test('window should be restored and icon should change on maximize-toggle click', async ({ page }) => {
await page.click('.window-controls .maximize-toggle.restore')
await expect(page.locator('.window-controls .maximize-toggle.maximize')).toHaveCount(1)
})
test('window controls should be hidden on fullscreen mode', async ({ page }) => {
// Keyboard press F11 won't work, probably because it's a chromium shortcut (not a document event)
await page.evaluate(`window.document.body.requestFullscreen()`)
await expect(page.locator('.window-controls .maximize-toggle')).toHaveCount(0)
})
test('window controls should be visible when we exit fullscreen mode', async ({ page }) => {
await page.click('.window-controls .fullscreen-toggle')
await expect(page.locator('.window-controls')).toHaveCount(1)
})
}

View File

@@ -1,28 +0,0 @@
import { PlaywrightTestConfig } from '@playwright/test'
const config: PlaywrightTestConfig = {
// The directory where the tests are located
// The order of the tests is determined by the file names alphabetically.
testDir: './e2e-tests',
// The number of retries before marking a test as failed.
maxFailures: 1,
// The number of Logseq instances to run in parallel.
// NOTE: must be 1 for now, otherwise tests will fail.
workers: 1,
// 'github' for GitHub Actions CI to generate annotations, plus a concise 'dot'.
// default 'list' when running locally.
reporter: process.env.CI ? 'github' : 'list',
// Fail the build on CI if test.only is present.
forbidOnly: !!process.env.CI,
use: {
// SCapture screenshot after each test failure.
screenshot: 'only-on-failure',
},
}
export default config

View File

@@ -1,8 +1,10 @@
(ns capacitor.components.app
(:require ["../externals.js"]
[capacitor.components.editor-toolbar :as editor-toolbar]
[capacitor.components.modal :as modal]
[capacitor.components.popup :as popup]
[capacitor.components.search :as search]
[capacitor.components.selection-toolbar :as selection-toolbar]
[capacitor.components.settings :as settings]
[capacitor.components.ui :as ui-component]
[capacitor.ionic :as ion]
@@ -17,8 +19,6 @@
[frontend.handler.page :as page-handler]
[frontend.handler.repo :as repo-handler]
[frontend.handler.user :as user-handler]
[frontend.mobile.action-bar :as action-bar]
[frontend.mobile.mobile-bar :as mobile-bar]
[frontend.mobile.util :as mobile-util]
[frontend.rum :as frum]
[frontend.state :as fstate]
@@ -202,7 +202,7 @@
{:id "app-ion-tabs"
:onIonTabsDidChange (fn [^js e]
(state/set-tab! (.-tab (.-detail e))
(.-target e)))}
(.-target e)))}
(ion/tab
{:tab "home"}
(ion/content
@@ -237,6 +237,6 @@
(tabs current-repo)
(when-not open?
[:<>
(mobile-bar/mobile-bar)
(editor-toolbar/mobile-bar)
(when show-action-bar?
(action-bar/action-bar))]))))
(selection-toolbar/action-bar))]))))

View File

@@ -1,11 +1,11 @@
(ns frontend.mobile.mobile-bar
(ns capacitor.components.editor-toolbar
(:require [dommy.core :as dom]
[frontend.commands :as commands]
[frontend.date :as date]
[frontend.handler.editor :as editor-handler]
[frontend.handler.page :as page-handler]
[frontend.mobile.camera :as mobile-camera]
[frontend.mobile.core :as mobile]
[capacitor.init :as init]
[frontend.mobile.haptics :as haptics]
[frontend.state :as state]
[frontend.ui :as ui]
@@ -125,4 +125,4 @@
[:div.toolbar-hide-keyboard
(command #(do
(state/clear-edit!)
(mobile/keyboard-hide)) {:icon "keyboard-show"})]])))
(init/keyboard-hide)) {:icon "keyboard-show"})]])))

View File

@@ -1,15 +1,15 @@
(ns capacitor.components.modal
(:require ["../externals.js"]
[capacitor.components.editor-toolbar :as mobile-bar]
[capacitor.components.selection-toolbar :as selection-toolbar]
[capacitor.components.ui :as ui]
[capacitor.init :as init]
[capacitor.ionic :as ion]
[capacitor.state :as state]
[frontend.components.page :as page]
[frontend.db :as db]
[frontend.handler.notification :as notification]
[frontend.handler.page :as page-handler]
[frontend.mobile.action-bar :as action-bar]
[frontend.mobile.core :as mobile]
[frontend.mobile.mobile-bar :as mobile-bar]
[frontend.state :as fstate]
[frontend.ui :as frontend-ui]
[rum.core :as rum]))
@@ -21,7 +21,7 @@
close! #(swap! state/*modal-data assoc :open? false)]
(when open?
(fstate/clear-edit!)
(mobile/keyboard-hide))
(init/keyboard-hide))
(ion/modal
{:isOpen (boolean open?)
:presenting-element presenting-element
@@ -77,4 +77,4 @@
(page/page-cp (db/entity [:block/uuid (:block/uuid block)])))
(mobile-bar/mobile-bar)
(when show-action-bar?
(action-bar/action-bar)))))))
(selection-toolbar/action-bar)))))))

View File

@@ -1,5 +1,5 @@
(ns frontend.mobile.action-bar
"Block Action bar, activated when swipe on a block"
(ns capacitor.components.selection-toolbar
"Selection action bar, activated when swipe on a block"
(:require [frontend.db :as db]
[frontend.handler.editor :as editor-handler]
[frontend.state :as state]

View File

@@ -1,13 +1,13 @@
(ns capacitor.core
(:require ["react-dom/client" :as rdc]
[capacitor.components.app :as app]
[capacitor.init :as init]
[capacitor.state :as state]
[frontend.background-tasks]
[frontend.components.page :as page]
[frontend.handler :as fhandler]
[frontend.handler.db-based.rtc-background-tasks]
[frontend.handler.route :as route-handler]
[frontend.mobile.core :as mobile]
[frontend.util :as util]
[reitit.frontend :as rf]
[reitit.frontend.easy :as rfe]))
@@ -51,7 +51,7 @@
;; so it is available even in :advanced release builds
(prn "[capacitor-new] init!")
(set-router!)
(mobile/init!)
(init/init!)
(fhandler/start! render!))
(defn ^:export stop! []

View File

@@ -0,0 +1,9 @@
(ns capacitor.events
(:require [capacitor.init :as init]
[frontend.handler.events :as events]
[promesa.core :as p]))
(defmethod events/handle :mobile/post-init [_]
(p/do!
(p/delay 1000)
(init/mobile-post-init)))

View File

@@ -1,4 +1,4 @@
(ns frontend.mobile.core
(ns capacitor.init
"Main ns for handling mobile start"
(:require ["@capacitor/app" :refer [^js App]]
["@capacitor/keyboard" :refer [^js Keyboard]]
@@ -20,7 +20,7 @@
(def *last-shared-url (atom nil))
(def *last-shared-seconds (atom 0))
(defn mobile-postinit
(defn mobile-post-init
"postinit logic of mobile platforms: handle deeplink and intent"
[]
(when (mobile-util/native-ios?)
@@ -44,6 +44,7 @@
(and (js/document.querySelector ".pswp"))
(some-> js/window.photoLightbox (.destroy))
;; TODO: move ui-related code to mobile events
(not-empty (cc-ui/get-modal))
(cc-ui/close-modal!)

View File

@@ -33,7 +33,6 @@
[frontend.handler.user :as user-handler]
[frontend.handler.whiteboard :as whiteboard-handler]
[frontend.mixins :as mixins]
[frontend.mobile.action-bar :as action-bar]
[frontend.mobile.footer :as footer]
[frontend.mobile.util :as mobile-util]
[frontend.modules.shortcut.data-helper :as shortcut-dh]
@@ -611,7 +610,7 @@
(when-let [el (gdom/getElement "main-content-container")]
(dnd/unsubscribe! el :upload-files))
state)}
[{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
[{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-recording-bar?]}]
(let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
(not config/publishing?)
@@ -629,9 +628,6 @@
{:tabIndex "-1"
:data-is-margin-less-pages margin-less-pages?}
(when show-action-bar?
(action-bar/action-bar))
[:div.cp__sidebar-main-content
{:data-is-margin-less-pages margin-less-pages?
:data-is-full-width (or margin-less-pages?

View File

@@ -38,7 +38,6 @@
[frontend.handler.search :as search-handler]
[frontend.handler.shell :as shell-handler]
[frontend.handler.ui :as ui-handler]
[frontend.mobile.core :as mobile]
[frontend.mobile.util :as mobile-util]
[frontend.modules.instrumentation.posthog :as posthog]
[frontend.modules.outliner.pipeline :as pipeline]
@@ -185,9 +184,8 @@
(p/do!
(state/pub-event! [:graph/sync-context])
;; re-render-root is async and delegated to rum, so we need to wait for main ui to refresh
(when (mobile-util/native-ios?)
(js/setTimeout #(mobile/mobile-postinit) 1000))
;; FIXME: an ugly implementation for redirecting to page on new window is restored
(state/pub-event! [:mobile/post-init])
;; FIXME: an ugly implementation for redirecting to page on new window is restored
(repo-handler/graph-ready! repo)
(if db-based?
(export/auto-db-backup! repo {:backup-now? true})

File diff suppressed because it is too large Load Diff

View File

@@ -18,4 +18,4 @@ fom = "fom"
tne = "tne"
Damon = "Damon"
[files]
extend-exclude = ["resources/*", "src/resources/*", "scripts/resources/*", "e2e-tests/plugin/lsplugin.user.js", "src/test/fixtures/*"]
extend-exclude = ["resources/*", "src/resources/*", "scripts/resources/*", "src/test/fixtures/*"]