2022-04-02 13:37:38 +00:00
|
|
|
import * as fs from 'fs'
|
|
|
|
import * as path from 'path'
|
2022-04-08 19:18:28 +00:00
|
|
|
import { test as base, expect, ConsoleMessage, Locator } from '@playwright/test';
|
2021-11-29 07:15:45 +00:00
|
|
|
import { ElectronApplication, Page, BrowserContext, _electron as electron } from 'playwright'
|
2022-04-15 11:50:25 +00:00
|
|
|
import { loadLocalGraph, openLeftSidebar, randomString } from './utils';
|
2022-08-15 10:47:26 +00:00
|
|
|
import { autocompleteMenu, LogseqFixtures } from './types';
|
2021-11-29 07:15:45 +00:00
|
|
|
|
|
|
|
let electronApp: ElectronApplication
|
|
|
|
let context: BrowserContext
|
|
|
|
let page: Page
|
2022-01-17 05:30:59 +00:00
|
|
|
|
2022-01-18 02:37:31 +00:00
|
|
|
let repoName = randomString(10)
|
|
|
|
let testTmpDir = path.resolve(__dirname, '../tmp')
|
|
|
|
|
|
|
|
if (fs.existsSync(testTmpDir)) {
|
2022-04-15 11:50:25 +00:00
|
|
|
fs.rmSync(testTmpDir, { recursive: true })
|
2022-01-18 02:37:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export let graphDir = path.resolve(testTmpDir, "e2e-test", repoName)
|
2021-11-29 07:15:45 +00:00
|
|
|
|
2022-03-31 19:41:49 +00:00
|
|
|
// NOTE: This following is a console log watcher for error logs.
|
|
|
|
// Save and print all logs when error happens.
|
|
|
|
let logs: string
|
2021-12-24 06:49:02 +00:00
|
|
|
const consoleLogWatcher = (msg: ConsoleMessage) => {
|
2022-03-31 19:41:49 +00:00
|
|
|
// console.log(msg.text())
|
2022-04-09 03:00:28 +00:00
|
|
|
const text = msg.text()
|
|
|
|
logs += text + '\n'
|
|
|
|
expect(text, logs).not.toMatch(/^(Failed to|Uncaught)/)
|
2022-04-02 13:37:38 +00:00
|
|
|
|
|
|
|
// youtube video
|
2022-08-31 09:38:36 +00:00
|
|
|
// Error with Permissions-Policy header: Origin trial controlled feature not enabled: 'ch-ua-reduced'.
|
|
|
|
if (!text.match(/^Error with Permissions-Policy header:/)) {
|
2022-04-09 03:00:28 +00:00
|
|
|
expect(text, logs).not.toMatch(/^Error/)
|
2022-04-02 13:37:38 +00:00
|
|
|
}
|
|
|
|
|
2021-12-24 06:49:02 +00:00
|
|
|
// NOTE: React warnings will be logged as error.
|
|
|
|
// expect(msg.type()).not.toBe('error')
|
|
|
|
}
|
|
|
|
|
2021-11-29 07:15:45 +00:00
|
|
|
base.beforeAll(async () => {
|
|
|
|
if (electronApp) {
|
2021-12-24 06:49:02 +00:00
|
|
|
return
|
2021-11-29 07:15:45 +00:00
|
|
|
}
|
|
|
|
|
2022-03-31 14:52:42 +00:00
|
|
|
console.log(`Creating test graph directory: ${graphDir}`)
|
2022-01-05 13:05:05 +00:00
|
|
|
fs.mkdirSync(graphDir, {
|
|
|
|
recursive: true,
|
|
|
|
});
|
|
|
|
|
2021-11-29 07:15:45 +00:00
|
|
|
electronApp = await electron.launch({
|
|
|
|
cwd: "./static",
|
|
|
|
args: ["electron.js"],
|
2022-01-05 13:05:05 +00:00
|
|
|
locale: 'en',
|
2022-04-15 11:50:25 +00:00
|
|
|
timeout: 10_000, // should be enough for the app to start
|
2021-11-29 07:15:45 +00:00
|
|
|
})
|
|
|
|
context = electronApp.context()
|
|
|
|
await context.tracing.start({ screenshots: true, snapshots: true });
|
|
|
|
|
|
|
|
// NOTE: The following ensures App first start with the correct path.
|
2022-01-17 05:30:59 +00:00
|
|
|
const info = await electronApp.evaluate(async ({ app }) => {
|
|
|
|
|
|
|
|
return {
|
|
|
|
"appPath": app.getAppPath(),
|
|
|
|
"appData": app.getPath("appData"),
|
|
|
|
"userData": app.getPath("userData"),
|
|
|
|
"appName": app.getName(),
|
2022-04-15 11:50:25 +00:00
|
|
|
"electronVersion": app.getVersion(),
|
2022-01-17 05:30:59 +00:00
|
|
|
}
|
2021-11-29 07:15:45 +00:00
|
|
|
})
|
2022-01-17 05:30:59 +00:00
|
|
|
console.log("Test start with:", info)
|
2021-11-29 07:15:45 +00:00
|
|
|
|
|
|
|
page = await electronApp.firstWindow()
|
2021-12-24 06:49:02 +00:00
|
|
|
// Direct Electron console to watcher
|
|
|
|
page.on('console', consoleLogWatcher)
|
|
|
|
page.on('crash', () => {
|
2022-03-31 14:52:42 +00:00
|
|
|
expect(false, "Page must not crash").toBeTruthy()
|
2021-12-24 06:49:02 +00:00
|
|
|
})
|
|
|
|
page.on('pageerror', (err) => {
|
2022-03-31 14:52:42 +00:00
|
|
|
console.log(err)
|
2022-04-23 00:19:34 +00:00
|
|
|
// expect(false, 'Page must not have errors!').toBeTruthy()
|
2021-12-24 06:49:02 +00:00
|
|
|
})
|
2021-11-29 07:15:45 +00:00
|
|
|
|
|
|
|
await page.waitForLoadState('domcontentloaded')
|
2022-01-17 05:30:59 +00:00
|
|
|
// 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')
|
2021-11-29 07:15:45 +00:00
|
|
|
|
2022-01-17 05:48:32 +00:00
|
|
|
await page.waitForSelector(':has-text("Loading")', {
|
|
|
|
state: "hidden",
|
|
|
|
timeout: 1000 * 15,
|
|
|
|
});
|
|
|
|
|
2021-11-29 07:15:45 +00:00
|
|
|
page.once('load', async () => {
|
|
|
|
console.log('Page loaded!')
|
|
|
|
await page.screenshot({ path: 'startup.png' })
|
|
|
|
})
|
2022-01-05 13:05:05 +00:00
|
|
|
|
|
|
|
await loadLocalGraph(page, graphDir);
|
2022-04-15 11:50:25 +00:00
|
|
|
|
|
|
|
// render app
|
|
|
|
await page.waitForFunction('window.document.title !== "Loading"')
|
|
|
|
expect(await page.title()).toMatch(/^Logseq.*?/)
|
|
|
|
await openLeftSidebar(page)
|
2021-11-29 07:15:45 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
base.beforeEach(async () => {
|
|
|
|
// discard any dialog by ESC
|
|
|
|
if (page) {
|
|
|
|
await page.keyboard.press('Escape')
|
|
|
|
await page.keyboard.press('Escape')
|
2022-08-22 10:52:36 +00:00
|
|
|
|
|
|
|
const rightSidebar = page.locator('.cp__right-sidebar-inner')
|
|
|
|
if (await rightSidebar.isVisible()) {
|
|
|
|
await page.click('button.toggle-right-sidebar', {delay: 100})
|
|
|
|
}
|
2021-11-29 07:15:45 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
base.afterAll(async () => {
|
|
|
|
// if (electronApp) {
|
|
|
|
// await electronApp.close()
|
|
|
|
//}
|
|
|
|
})
|
|
|
|
|
|
|
|
// hijack electron app into the test context
|
2022-04-08 19:18:28 +00:00
|
|
|
// FIXME: add type to `block`
|
2022-04-21 10:15:53 +00:00
|
|
|
export const test = base.extend<LogseqFixtures>({
|
2021-11-29 07:15:45 +00:00
|
|
|
page: async ({ }, use) => {
|
|
|
|
await use(page);
|
|
|
|
},
|
2022-04-08 19:18:28 +00:00
|
|
|
|
|
|
|
// 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 })
|
|
|
|
},
|
2022-04-15 11:50:25 +00:00
|
|
|
mustType: async (value: string, options?: { delay?: number, toBe?: string }) => {
|
2022-04-08 19:18:28 +00:00
|
|
|
const locator: Locator = page.locator('textarea >> nth=0')
|
|
|
|
await locator.waitFor({ timeout: 1000 })
|
2022-04-15 11:50:25 +00:00
|
|
|
const { delay = 50 } = options || {};
|
|
|
|
const { toBe = value } = options || {};
|
2022-04-08 19:18:28 +00:00
|
|
|
await locator.type(value, { delay })
|
2022-04-15 11:50:25 +00:00
|
|
|
await expect(locator).toHaveText(toBe, { timeout: 1000 })
|
2022-04-08 19:18:28 +00:00
|
|
|
},
|
|
|
|
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> => {
|
2022-04-21 01:25:14 +00:00
|
|
|
await page.$eval('.add-button-link-wrap', (element) => {
|
|
|
|
element.scrollIntoView();
|
|
|
|
});
|
2022-04-08 19:18:28 +00:00
|
|
|
let blockCount = await page.locator('.page-blocks-inner .ls-block').count()
|
|
|
|
// the next element after all blocks.
|
2022-04-22 03:02:28 +00:00
|
|
|
await page.click('.add-button-link-wrap', { delay: 100 })
|
2022-04-08 19:18:28 +00:00
|
|
|
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.
|
2022-04-21 01:25:14 +00:00
|
|
|
await page.waitForSelector(`.ls-block >> nth=${total - 1}`, { state: 'attached', timeout: 50000 })
|
|
|
|
await page.waitForSelector(`.ls-block >> nth=${total}`, { state: 'detached', timeout: 50000 })
|
2022-04-08 19:18:28 +00:00
|
|
|
},
|
|
|
|
waitForSelectedBlocks: async (total: number): Promise<void> => {
|
|
|
|
// NOTE: `nth=` counts from 0.
|
|
|
|
await page.waitForSelector(`.ls-block.selected >> nth=${total - 1}`, { timeout: 1000 })
|
|
|
|
},
|
2022-04-09 03:00:28 +00:00
|
|
|
escapeEditing: async (): Promise<void> => {
|
|
|
|
await page.keyboard.press('Escape')
|
|
|
|
await page.keyboard.press('Escape')
|
2022-04-15 11:50:25 +00:00
|
|
|
},
|
2022-04-21 10:15:53 +00:00
|
|
|
activeEditing: async (nth: number): Promise<void> => {
|
|
|
|
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<boolean> => {
|
|
|
|
const locator = page.locator('.ls-block textarea >> nth=0')
|
|
|
|
return await locator.isVisible()
|
|
|
|
},
|
2022-04-15 11:50:25 +00:00
|
|
|
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
|
|
|
|
})
|
2022-04-09 03:00:28 +00:00
|
|
|
}
|
2022-04-08 19:18:28 +00:00
|
|
|
}
|
|
|
|
use(block)
|
|
|
|
},
|
|
|
|
|
2022-08-15 10:47:26 +00:00
|
|
|
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)
|
|
|
|
},
|
|
|
|
|
2021-11-29 07:15:45 +00:00
|
|
|
context: async ({ }, use) => {
|
|
|
|
await use(context);
|
|
|
|
},
|
|
|
|
app: async ({ }, use) => {
|
|
|
|
await use(electronApp);
|
2022-03-31 14:52:42 +00:00
|
|
|
},
|
|
|
|
graphDir: async ({ }, use) => {
|
|
|
|
await use(graphDir);
|
|
|
|
},
|
2021-11-29 07:15:45 +00:00
|
|
|
});
|