diff --git a/e2e-tests/basic.spec.ts b/e2e-tests/basic.spec.ts
index b123d9822..e95cbd452 100644
--- a/e2e-tests/basic.spec.ts
+++ b/e2e-tests/basic.spec.ts
@@ -149,11 +149,12 @@ test('selection', async ({ page, block }) => {
})
test('template', async ({ page, block }) => {
- const randomTemplate = randomString(10)
+ const randomTemplate = randomString(6)
await createRandomPage(page)
await block.mustFill('template test\ntemplate:: ' + randomTemplate)
+ await page.keyboard.press('Enter')
await block.clickNext()
expect(await block.indent()).toBe(true)
diff --git a/e2e-tests/editor-random.spec.ts b/e2e-tests/editor-random.spec.ts
deleted file mode 100644
index 4d6b61c3d..000000000
--- a/e2e-tests/editor-random.spec.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { expect } from '@playwright/test'
-import { test } from './fixtures'
-import { createRandomPage, enterNextBlock, editFirstBlock, randomInt, IsMac,
- randomInsert, randomEditDelete, randomEditMoveUpDown,
- editRandomBlock, randomSelectBlocks, randomIndentOutdent} from './utils'
-
-test('Random editor operations', async ({page, block}) => {
- var ops = [
- randomInsert,
- randomEditMoveUpDown,
- randomEditDelete,
-
- // Errors:
- // locator.waitFor: Timeout 1000ms exceeded.
- // =========================== logs ===========================
- // waiting for selector "textarea >> nth=0" to be visible
- // selector resolved to hidden
-
- // editRandomBlock,
-
- // randomSelectBlocks,
-
- // randomIndentOutdent,
- ]
-
- await createRandomPage(page)
-
- await block.mustType('Random tests start!')
- await randomInsert(page, block)
-
- for (let i = 0; i < 100; i++) {
- let n = randomInt(0, ops.length - 1)
-
- var f = ops[n]
- if (f.toString() == randomInsert.toString()) {
- await f(page, block)
- } else {
- await f(page)
- }
- }
-})
diff --git a/e2e-tests/editor.spec.ts b/e2e-tests/editor.spec.ts
index dac4be44a..1536ecf7a 100644
--- a/e2e-tests/editor.spec.ts
+++ b/e2e-tests/editor.spec.ts
@@ -1,6 +1,6 @@
import { expect } from '@playwright/test'
import { test } from './fixtures'
-import { createRandomPage, enterNextBlock, editFirstBlock, randomInt, IsMac } from './utils'
+import { createRandomPage, enterNextBlock } from './utils'
import { dispatch_kb_events } from './util/keyboard-events'
import * as kb_events from './util/keyboard-events'
diff --git a/e2e-tests/fixtures.ts b/e2e-tests/fixtures.ts
index f062263a7..0e614eb1d 100644
--- a/e2e-tests/fixtures.ts
+++ b/e2e-tests/fixtures.ts
@@ -3,6 +3,7 @@ import * as path from 'path'
import { test as base, expect, ConsoleMessage, Locator } from '@playwright/test';
import { ElectronApplication, Page, BrowserContext, _electron as electron } from 'playwright'
import { loadLocalGraph, openLeftSidebar, randomString } from './utils';
+import { LogseqFixtures } from './types';
let electronApp: ElectronApplication
let context: BrowserContext
@@ -114,43 +115,9 @@ base.afterAll(async () => {
//}
})
-/**
- * Block provides helper functions for Logseq's block testing.
- */
-interface Block {
- /** Must fill some text into a block, use `textarea >> nth=0` as selector. */
- mustFill(value: string): Promise;
- /**
- * 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;
- /**
- * 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;
- /** Click `.add-button-link-wrap` and create the next block. */
- clickNext(): Promise;
- /** Indent block, return whether it's success. */
- indent(): Promise;
- /** Unindent block, return whether it's success. */
- unindent(): Promise;
- /** Await for a certain number of blocks, with default timeout. */
- waitForBlocks(total: number): Promise;
- /** Await for a certain number of selected blocks, with default timeout. */
- waitForSelectedBlocks(total: number): Promise;
- /** Escape editing mode, modal popup and selection. */
- escapeEditing(): Promise;
- /** Find current selectionStart, i.e. text cursor position. */
- selectionStart(): Promise;
- /** Find current selectionEnd. */
- selectionEnd(): Promise;
-}
-
// hijack electron app into the test context
// FIXME: add type to `block`
-export const test = base.extend<{ page: Page, block: Block, context: BrowserContext, app: ElectronApplication, graphDir: string }>({
+export const test = base.extend({
page: async ({ }, use) => {
await use(page);
},
@@ -215,6 +182,25 @@ export const test = base.extend<{ page: Page, block: Block, context: BrowserCont
await page.keyboard.press('Escape')
await page.keyboard.press('Escape')
},
+ activeEditing: async (nth: number): Promise => {
+ await page.waitForSelector(`.ls-block >> nth=${nth}`, { timeout: 1000 })
+ // scroll, for isVisble test
+ await page.$eval(`.ls-block >> nth=${nth}`, (element) => {
+ element.scrollIntoView();
+ });
+ // when blocks are nested, the first block(the parent) is selected.
+ if (
+ (await page.isVisible(`.ls-block >> nth=${nth} >> .editor-wrapper >> textarea`)) &&
+ !(await page.isVisible(`.ls-block >> nth=${nth} >> .block-children-container >> textarea`))) {
+ return;
+ }
+ await page.click(`.ls-block >> nth=${nth} >> .block-content`, { delay: 10, timeout: 100000 })
+ await page.waitForSelector(`.ls-block >> nth=${nth} >> .editor-wrapper >> textarea`, { timeout: 1000, state: 'visible' })
+ },
+ isEditing: async (): Promise => {
+ const locator = page.locator('.ls-block textarea >> nth=0')
+ return await locator.isVisible()
+ },
selectionStart: async (): Promise => {
return await page.locator('textarea >> nth=0').evaluate(node => {
const elem = node
diff --git a/e2e-tests/random.spec.ts b/e2e-tests/random.spec.ts
new file mode 100644
index 000000000..8b782f0cd
--- /dev/null
+++ b/e2e-tests/random.spec.ts
@@ -0,0 +1,181 @@
+import { expect } from '@playwright/test'
+import { test } from './fixtures'
+import {
+ createRandomPage, randomInt, randomInsert, randomEditDelete, randomEditMoveUpDown, 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
+}
+
+test('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 begining
+ // 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)
+
+ }
+
+})
diff --git a/e2e-tests/types.ts b/e2e-tests/types.ts
new file mode 100644
index 000000000..e801f39b4
--- /dev/null
+++ b/e2e-tests/types.ts
@@ -0,0 +1,48 @@
+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;
+ /**
+ * 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;
+ /**
+ * 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;
+ /** Click `.add-button-link-wrap` and create the next block. */
+ clickNext(): Promise;
+ /** Indent block, return whether it's success. */
+ indent(): Promise;
+ /** Unindent block, return whether it's success. */
+ unindent(): Promise;
+ /** Await for a certain number of blocks, with default timeout. */
+ waitForBlocks(total: number): Promise;
+ /** Await for a certain number of selected blocks, with default timeout. */
+ waitForSelectedBlocks(total: number): Promise;
+ /** Escape editing mode, modal popup and selection. */
+ escapeEditing(): Promise;
+ /** Active block editing, by click */
+ activeEditing(nth: number): Promise;
+ /** Is editing block now? */
+ isEditing(): Promise;
+ /** Find current selectionStart, i.e. text cursor position. */
+ selectionStart(): Promise;
+ /** Find current selectionEnd. */
+ selectionEnd(): Promise;
+}
+
+export interface LogseqFixtures {
+ page: Page;
+ block: Block;
+ context: BrowserContext;
+ app: ElectronApplication;
+ graphDir: string;
+}
+
diff --git a/e2e-tests/utils.ts b/e2e-tests/utils.ts
index eb753ad20..77daf74e4 100644
--- a/e2e-tests/utils.ts
+++ b/e2e-tests/utils.ts
@@ -1,6 +1,7 @@
import { Page, Locator } from 'playwright'
import { expect } from '@playwright/test'
import * as process from 'process'
+import { Block } from './types'
export const IsMac = process.platform === 'darwin'
export const IsLinux = process.platform === 'linux'
@@ -210,98 +211,6 @@ export function randomInt(min: number, max: number): number {
return Math.floor(Math.random() * (max - min + 1) + min)
}
-export function randomBoolean(): bool {
+export function randomBoolean(): boolean {
return Math.random() < 0.5;
}
-
-export async function randomInsert( page, block ) {
- let n = randomInt(0, 100)
- await block.mustFill(n.toString())
-
- // random indent
- if (randomBoolean ()) {
- await block.indent()
- } else {
- await block.unindent()
- }
-
- await page.waitForSelector('textarea >> nth=0', { state: 'visible' })
-
- await page.press('textarea >> nth=0', 'Enter')
-}
-
-export async function randomEditDelete( page: Page ) {
- let n = randomInt(3)
-
- for (let i = 0; i < n; i++) {
- await page.keyboard.press('Backspace')
- }
-}
-
-export async function randomEditMoveUpDown( page: Page ) {
- let n = randomInt(3, 10)
-
- for (let i = 0; i < n; i++) {
- if (randomBoolean ()) {
- await page.keyboard.press('Meta+Shift+ArrowUp')
- } else {
- await page.keyboard.press('Meta+Shift+ArrowDown')
- }
- }
-
- // Leave some time for UI refresh
- await page.waitForTimeout(10)
-}
-
-async function scrollOnElement(page, selector) {
- await page.$eval(selector, (element) => {
- element.scrollIntoView();
- });
-}
-
-export async function editRandomBlock( page: Page ) {
- let blockCount = await page.locator('.page-blocks-inner .ls-block').count()
- let n = randomInt(0, blockCount - 1)
-
- // 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(`.ls-block .block-content >> nth=${n}`)
- }
-
- // wait for textarea
- await page.waitForSelector('textarea >> nth=0', { state: 'visible', timeout: 1000 })
-
- await scrollOnElement(page, 'textarea >> nth=0');
-
- const randomContent = randomString(10)
-
- const locator: Locator = page.locator('textarea >> nth=0')
-
- await locator.type(randomContent)
-
- return locator
-}
-
-export async function randomSelectBlocks( page: Page ) {
- await editRandomBlock(page)
-
- let n = randomInt(1, 10)
-
- for (let i = 0; i < n; i++) {
- await page.keyboard.press('Shift+ArrowUp')
- }
-}
-
-export async function randomIndentOutdent( page: Page ) {
- await randomSelectBlocks(page)
-
- if (randomBoolean ()) {
- await page.keyboard.press('Tab', { delay: 100 })
- } else {
- await page.keyboard.press('Shift+Tab', { delay: 100 })
- }
-}