Bläddra i källkod

test(e2e): fs testings - file create, rename, unlink

Junyi Du 2 år sedan
förälder
incheckning
3bd3991d9a
5 ändrade filer med 237 tillägg och 114 borttagningar
  1. 105 0
      e2e-tests/fs.spec.ts
  2. 12 36
      e2e-tests/page-search.spec.ts
  3. 42 0
      e2e-tests/util/basic.ts
  4. 63 0
      e2e-tests/util/search-modal.ts
  5. 15 78
      e2e-tests/utils.ts

+ 105 - 0
e2e-tests/fs.spec.ts

@@ -0,0 +1,105 @@
+import fsp from 'fs/promises';
+import path from 'path';
+import { expect } from '@playwright/test'
+import { test } from './fixtures';
+import { searchPage, captureConsoleWithPrefix, closeSearchBox } 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:John"},
+    // 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"}
+  ]
+
+  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("New");
+    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("New");
+    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:John", 
+    newPageTitle: "User/John", newFileName: "User___John"},
+    // NameSpace -> Normal
+    {pageTitle: ":#/%23", fileName: ":#___%2523",
+    newPageTitle: ":#%23", newFileName: ":#%2523"}
+  ]
+
+  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("New");
+    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);
+
+    // 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("New");
+    await closeSearchBox(page);
+  }
+})

+ 12 - 36
e2e-tests/page-search.spec.ts

@@ -2,6 +2,7 @@ import { expect, Page } from '@playwright/test'
 import { test } from './fixtures'
 import { Block } from './types'
 import { modKey, createRandomPage, newBlock, newInnerBlock, randomString, lastBlock, enterNextBlock } from './utils'
+import { searchPage, closeSearchBox } from './util/search-modal'
 
 /***
  * Test alias features
@@ -37,18 +38,9 @@ test('Search page and blocks (diacritics)', async ({ page, block }) => {
   await page.keyboard.press(hotkeyBack)
 
   // check if diacritics are indexed
-  await page.click('#search-button')
-  await page.waitForSelector('[placeholder="Search or create page"]')
-  await page.type('[placeholder="Search or create page"]', 'Einführung in die Allgemeine Sprachwissenschaft' + rand, { delay: 10 })
-
-  await page.waitForTimeout(2000) // wait longer for search contents to render
-  // 2 blocks + 1 page + 1 page content
-  const searchResults = page.locator('#ui__ac-inner>div')
-  await expect(searchResults).toHaveCount(5) // 1 page + 2 block + 2 page content
-
-  await page.keyboard.press("Escape") // escape search box typing
-  await page.waitForTimeout(500)
-  await page.keyboard.press("Escape") // escape modal
+  const results = await searchPage(page, 'Einführung in die Allgemeine Sprachwissenschaft' + rand)
+  await expect(results.length).toEqual(5) // 1 page + 2 block + 2 page content
+  await closeSearchBox(page)
 })
 
 test('Search CJK', async ({ page, block }) => {
@@ -67,19 +59,10 @@ test('Search CJK', async ({ page, block }) => {
 
   await page.waitForTimeout(500)
 
-  // check if diacritics are indexed
-  await page.click('#search-button')
-  await page.waitForSelector('[placeholder="Search or create page"]')
-  await page.type('[placeholder="Search or create page"]', '进度', { delay: 10 })
-
-  await page.waitForTimeout(2000) // wait longer for search contents to render
-  // 2 blocks + 1 page + 1 page content
-  const searchResults = page.locator('#ui__ac-inner>div')
-  await expect(searchResults).toHaveCount(4) // 1 new page + 1 page + 1 block + 1 page content
-
-  await page.keyboard.press("Escape") // escape search box typing
-  await page.waitForTimeout(500)
-  await page.keyboard.press("Escape") // escape modal
+  // check if CJK are indexed
+  const results = await searchPage(page, '进度')
+  await expect(results.length).toEqual(4) // 1 page + 1 block + 1 page content
+  await closeSearchBox(page)
 })
 
 async function alias_test(block: Block, page: Page, page_name: string, search_kws: string[]) {
@@ -165,13 +148,8 @@ async function alias_test(block: Block, page: Page, page_name: string, search_kw
   for (let kw of search_kws) {
     let kw_name = kw + ' alias ' + rand
 
-    await page.click('#search-button')
-    await page.waitForSelector('[placeholder="Search or create page"]')
-    await page.type('[placeholder="Search or create page"]', kw_name)
-    await page.waitForTimeout(500)
-
-    const results = await page.$$('#ui__ac-inner>div')
-    expect(results.length).toEqual(5) // page + block + alias property + page content
+    const results = await searchPage(page, kw_name)
+    await expect(results.length).toEqual(5) // page + block + alias property + page content
 
     // test search results
     expect(await results[0].innerText()).toContain("Alias -> " + target_name)
@@ -186,10 +164,8 @@ async function alias_test(block: Block, page: Page, page_name: string, search_kw
     expect(await page.locator('.ls-block span.inline >> nth=2').innerHTML()).toBe(alias_test_content_3)
 
     // test search clicking (block)
-    await page.click('#search-button')
-    await page.waitForSelector('[placeholder="Search or create page"]')
-    await page.type('[placeholder="Search or create page"]', kw_name)
-    await page.waitForTimeout(500)
+    await searchPage(page, kw_name)
+
     page.click(":nth-match(.search-result, 3)")
     await page.waitForNavigation()
     await page.waitForSelector('.selected a.page-ref')

+ 42 - 0
e2e-tests/util/basic.ts

@@ -0,0 +1,42 @@
+// 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;
+}

+ 63 - 0
e2e-tests/util/search-modal.ts

@@ -0,0 +1,63 @@
+import { Page, Locator, ElementHandle } from '@playwright/test'
+import { randomString } from './basic'
+
+export async function createRandomPage(page: Page) {
+    const randomTitle = randomString(20)
+  
+    // Click #search-button
+    await page.click('#search-button')
+    // Fill [placeholder="Search or create page"]
+    await page.fill('[placeholder="Search or create page"]', randomTitle)
+    // Click text=/.*New page: "new page".*/
+    await page.click('text=/.*New page: ".*/')
+    // 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 page.click('#search-button')
+    // Fill [placeholder="Search or create page"]
+    await page.fill('[placeholder="Search or create page"]', page_name)
+    // Click text=/.*New page: "new page".*/
+    await page.click('text=/.*New page: ".*/')
+    // 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 page.click('#search-button')
+    await page.type('[placeholder="Search or create page"]', pageTitle)
+    await page.waitForSelector(`[data-page-ref="${pageTitle}"]`, { state: 'visible' })
+    page.click(`[data-page-ref="${pageTitle}"]`)
+    await page.waitForNavigation()
+    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 page.click('#search-button')
+    await page.waitForSelector('[placeholder="Search or create page"]')
+    await page.type('[placeholder="Search or create page"]', query, { delay: 10 })
+    await page.waitForTimeout(2000) // wait longer for search contents to render
+  
+    return page.$$('#ui__ac-inner>div');
+  }
+  
+  export async function closeSearchBox(page: Page): Promise<void> {
+    await page.keyboard.press("Escape") // escape (potential) search box typing
+    await page.waitForTimeout(500)
+    await page.keyboard.press("Escape") // escape modal
+  }

+ 15 - 78
e2e-tests/utils.ts

@@ -1,77 +1,13 @@
 import { Page, Locator } from 'playwright'
 import { expect, ConsoleMessage } from '@playwright/test'
-import * as process from 'process'
-import { Block } from './types'
 import * as pathlib from 'path'
 
-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 async function createRandomPage(page: Page) {
-  const randomTitle = randomString(20)
-
-  // Click #search-button
-  await page.click('#search-button')
-  // Fill [placeholder="Search or create page"]
-  await page.fill('[placeholder="Search or create page"]', randomTitle)
-  // Click text=/.*New page: "new page".*/
-  await page.click('text=/.*New page: ".*/')
-  // 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 page.click('#search-button')
-  // Fill [placeholder="Search or create page"]
-  await page.fill('[placeholder="Search or create page"]', page_name)
-  // Click text=/.*New page: "new page".*/
-  await page.click('text=/.*New page: ".*/')
-  // 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 page.click('#search-button')
-  await page.type('[placeholder="Search or create page"]', pageTitle)
-  await page.waitForSelector(`[data-page-ref="${pageTitle}"]`, { state: 'visible' })
-  page.click(`[data-page-ref="${pageTitle}"]`)
-  await page.waitForNavigation()
-  return pageTitle;
-}
+// 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'
 
 /**
 * Locate the last block in the inner editor
@@ -234,14 +170,15 @@ export async function editFirstBlock(page: Page) {
   await page.click('.ls-block .block-content >> nth=0')
 }
 
-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;
-}
-
+/**
+ * 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) => {