mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 22:47:36 +00:00
fix: ensure website build doesn't rely on new mobile components
this commit also removes js e2e-tests
This commit is contained in:
2
.github/workflows/clj-e2e.yml
vendored
2
.github/workflows/clj-e2e.yml
vendored
@@ -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
|
||||
|
||||
166
.github/workflows/e2e.yml
vendored
166
.github/workflows/e2e.yml
vendored
@@ -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
3
.gitignore
vendored
@@ -60,8 +60,7 @@ startup.png
|
||||
android/app/src/main/assets/capacitor.config.json
|
||||
|
||||
*.sublime-*
|
||||
/public/static
|
||||
/public/
|
||||
/public
|
||||
.yarn/
|
||||
.yarnrc.yml
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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([]);
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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);
|
||||
})
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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})
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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() });
|
||||
})
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
@@ -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>')
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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/)')
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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, "ü", ["ü", "ü", "Ü"])
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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>
|
||||
@@ -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
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "e2e-plugin",
|
||||
"description": "A plugin for e2e tests",
|
||||
"main": "./index.html",
|
||||
"logseq": {
|
||||
"id": "a-logseq-plugin-for-e2e-tests"
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
@@ -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');
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
@@ -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')
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))]))))
|
||||
|
||||
@@ -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"})]])))
|
||||
@@ -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)))))))
|
||||
|
||||
@@ -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]
|
||||
@@ -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! []
|
||||
|
||||
9
src/main/capacitor/events.cljs
Normal file
9
src/main/capacitor/events.cljs
Normal 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)))
|
||||
@@ -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!)
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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})
|
||||
|
||||
4999
static/yarn.lock
4999
static/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -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/*"]
|
||||
|
||||
Reference in New Issue
Block a user