fixtures.ts 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import * as fs from 'fs'
  2. import * as path from 'path'
  3. import { test as base, expect, ConsoleMessage, Locator } from '@playwright/test';
  4. import { ElectronApplication, Page, BrowserContext, _electron as electron } from 'playwright'
  5. import { loadLocalGraph, randomString } from './utils';
  6. let electronApp: ElectronApplication
  7. let context: BrowserContext
  8. let page: Page
  9. let repoName = randomString(10)
  10. let testTmpDir = path.resolve(__dirname, '../tmp')
  11. if (fs.existsSync(testTmpDir)) {
  12. fs.rmdirSync(testTmpDir, { recursive: true })
  13. }
  14. export let graphDir = path.resolve(testTmpDir, "e2e-test", repoName)
  15. // NOTE: This following is a console log watcher for error logs.
  16. // Save and print all logs when error happens.
  17. let logs: string
  18. const consoleLogWatcher = (msg: ConsoleMessage) => {
  19. // console.log(msg.text())
  20. const text = msg.text()
  21. logs += text + '\n'
  22. expect(text, logs).not.toMatch(/^(Failed to|Uncaught)/)
  23. // youtube video
  24. if (!text.match(/^Error with Permissions-Policy header: Unrecognized feature/)) {
  25. expect(text, logs).not.toMatch(/^Error/)
  26. }
  27. // NOTE: React warnings will be logged as error.
  28. // expect(msg.type()).not.toBe('error')
  29. }
  30. base.beforeAll(async () => {
  31. if (electronApp) {
  32. return
  33. }
  34. console.log(`Creating test graph directory: ${graphDir}`)
  35. fs.mkdirSync(graphDir, {
  36. recursive: true,
  37. });
  38. electronApp = await electron.launch({
  39. cwd: "./static",
  40. args: ["electron.js"],
  41. locale: 'en',
  42. })
  43. context = electronApp.context()
  44. await context.tracing.start({ screenshots: true, snapshots: true });
  45. // NOTE: The following ensures App first start with the correct path.
  46. const info = await electronApp.evaluate(async ({ app }) => {
  47. return {
  48. "appPath": app.getAppPath(),
  49. "appData": app.getPath("appData"),
  50. "userData": app.getPath("userData"),
  51. "appName": app.getName(),
  52. }
  53. })
  54. console.log("Test start with:", info)
  55. page = await electronApp.firstWindow()
  56. // Direct Electron console to watcher
  57. page.on('console', consoleLogWatcher)
  58. page.on('crash', () => {
  59. expect(false, "Page must not crash").toBeTruthy()
  60. })
  61. page.on('pageerror', (err) => {
  62. console.log(err)
  63. expect(false, 'Page must not have errors!').toBeTruthy()
  64. })
  65. await page.waitForLoadState('domcontentloaded')
  66. // await page.waitForFunction(() => window.document.title != "Loading")
  67. // NOTE: The following ensures first start.
  68. // await page.waitForSelector('text=This is a demo graph, changes will not be saved until you open a local folder')
  69. await page.waitForSelector(':has-text("Loading")', {
  70. state: "hidden",
  71. timeout: 1000 * 15,
  72. });
  73. page.once('load', async () => {
  74. console.log('Page loaded!')
  75. await page.screenshot({ path: 'startup.png' })
  76. })
  77. await loadLocalGraph(page, graphDir);
  78. })
  79. base.beforeEach(async () => {
  80. // discard any dialog by ESC
  81. if (page) {
  82. await page.keyboard.press('Escape')
  83. await page.keyboard.press('Escape')
  84. }
  85. })
  86. base.afterAll(async () => {
  87. // if (electronApp) {
  88. // await electronApp.close()
  89. //}
  90. })
  91. /**
  92. * Block provides helper functions for Logseq's block testing.
  93. */
  94. interface Block {
  95. /** Must fill some text into a block, use `textarea >> nth=0` as selector. */
  96. mustFill(value: string): Promise<void>;
  97. /**
  98. * Must type input some text into an **empty** block.
  99. * **DO NOT USE** this if there's auto-complete
  100. */
  101. mustType(value: string, options?: { delay?: number }): Promise<void>;
  102. /**
  103. * Press Enter and go to next block, require cursor to be in current block(editing mode).
  104. * When cursor is not at the end of block, trailing text will be moved to the next block.
  105. */
  106. enterNext(): Promise<Locator>;
  107. /** Click `.add-button-link-wrap` and create the next block. */
  108. clickNext(): Promise<Locator>;
  109. /** Indent block, return whether it's success. */
  110. indent(): Promise<boolean>;
  111. /** Unindent block, return whether it's success. */
  112. unindent(): Promise<boolean>;
  113. /** Await for a certain number of blocks, with default timeout. */
  114. waitForBlocks(total: number): Promise<void>;
  115. /** Await for a certain number of selected blocks, with default timeout. */
  116. waitForSelectedBlocks(total: number): Promise<void>;
  117. /** Escape editing mode, modal popup and selection. */
  118. escapeEditing(): Promise<void>;
  119. }
  120. // hijack electron app into the test context
  121. // FIXME: add type to `block`
  122. export const test = base.extend<{ page: Page, block: Block, context: BrowserContext, app: ElectronApplication, graphDir: string }>({
  123. page: async ({ }, use) => {
  124. await use(page);
  125. },
  126. // Timeout is used to avoid global timeout, local timeout will have a meaningful error report.
  127. // 1s timeout is enough for most of the test cases.
  128. // Timeout won't introduce additional sleeps.
  129. block: async ({ page }, use) => {
  130. const block = {
  131. mustFill: async (value: string) => {
  132. const locator: Locator = page.locator('textarea >> nth=0')
  133. await locator.waitFor({ timeout: 1000 })
  134. await locator.fill(value)
  135. await expect(locator).toHaveText(value, { timeout: 1000 })
  136. },
  137. mustType: async (value: string, options?: { delay?: number }) => {
  138. const locator: Locator = page.locator('textarea >> nth=0')
  139. await locator.waitFor({ timeout: 1000 })
  140. const { delay = 100 } = options || {};
  141. await locator.type(value, { delay })
  142. await expect(locator).toHaveText(value, { timeout: 1000 })
  143. },
  144. enterNext: async (): Promise<Locator> => {
  145. let blockCount = await page.locator('.page-blocks-inner .ls-block').count()
  146. await page.press('textarea >> nth=0', 'Enter')
  147. await page.waitForSelector(`.ls-block >> nth=${blockCount} >> textarea`, { state: 'visible', timeout: 1000 })
  148. return page.locator('textarea >> nth=0')
  149. },
  150. clickNext: async (): Promise<Locator> => {
  151. let blockCount = await page.locator('.page-blocks-inner .ls-block').count()
  152. // the next element after all blocks.
  153. await page.click('.add-button-link-wrap')
  154. await page.waitForSelector(`.ls-block >> nth=${blockCount} >> textarea`, { state: 'visible', timeout: 1000 })
  155. return page.locator('textarea >> nth=0')
  156. },
  157. indent: async (): Promise<boolean> => {
  158. const locator = page.locator('textarea >> nth=0')
  159. const before = await locator.boundingBox()
  160. await locator.press('Tab', { delay: 100 })
  161. return (await locator.boundingBox()).x > before.x
  162. },
  163. unindent: async (): Promise<boolean> => {
  164. const locator = page.locator('textarea >> nth=0')
  165. const before = await locator.boundingBox()
  166. await locator.press('Shift+Tab', { delay: 100 })
  167. return (await locator.boundingBox()).x < before.x
  168. },
  169. waitForBlocks: async (total: number): Promise<void> => {
  170. // NOTE: `nth=` counts from 0.
  171. await page.waitForSelector(`.ls-block >> nth=${total - 1}`, { timeout: 1000 })
  172. },
  173. waitForSelectedBlocks: async (total: number): Promise<void> => {
  174. // NOTE: `nth=` counts from 0.
  175. await page.waitForSelector(`.ls-block.selected >> nth=${total - 1}`, { timeout: 1000 })
  176. },
  177. escapeEditing: async (): Promise<void> => {
  178. await page.keyboard.press('Escape')
  179. await page.keyboard.press('Escape')
  180. }
  181. }
  182. use(block)
  183. },
  184. context: async ({ }, use) => {
  185. await use(context);
  186. },
  187. app: async ({ }, use) => {
  188. await use(electronApp);
  189. },
  190. graphDir: async ({ }, use) => {
  191. await use(graphDir);
  192. },
  193. });