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