From 2665a4f61b76b8359df6ad8e944f05fdbd409039 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Mon, 19 Apr 2021 15:25:57 -0700 Subject: [PATCH 1/6] feat: add CodeServer page object for e2e tests --- test/e2e/codeServer.test.ts | 46 ++++++++++++++++++++++++++ test/e2e/models/CodeServer.ts | 61 +++++++++++++++++++++++++++++++++++ test/e2e/terminal.test.ts | 60 ++++++++++++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 test/e2e/codeServer.test.ts create mode 100644 test/e2e/models/CodeServer.ts create mode 100644 test/e2e/terminal.test.ts diff --git a/test/e2e/codeServer.test.ts b/test/e2e/codeServer.test.ts new file mode 100644 index 000000000..1982a100b --- /dev/null +++ b/test/e2e/codeServer.test.ts @@ -0,0 +1,46 @@ +import { test, expect } from "@playwright/test" +import { STORAGE } from "../utils/constants" +import { CodeServer } from "./models/CodeServer" + +test.describe("CodeServer", () => { + // Create a new context with the saved storage state + // so we don't have to logged in + const options: any = {} + let codeServer: CodeServer + + // TODO@jsjoeio + // Fix this once https://github.com/microsoft/playwright-test/issues/240 + // is fixed + if (STORAGE) { + const storageState = JSON.parse(STORAGE) || {} + options.contextOptions = { + storageState, + } + } + + test.beforeEach(async ({ page }) => { + codeServer = new CodeServer(page) + await codeServer.navigate() + }) + + test("should open the default folder if not open", options, async ({ page }) => { + await codeServer.openFolder() + + // find workspaceStorage in the Explorer menu, which would be open in the User folder + // which is the default folder that opens + expect(await page.isVisible("text=workspaceStorage")).toBe(true) + }) + + test("should show the Integrated Terminal", options, async ({ page }) => { + await codeServer.viewTerminal() + expect(await page.isVisible("#terminal")).toBe(true) + }) + + test("should open a file with quickOpen", options, async ({ page }) => { + await codeServer.openFolder() + await codeServer.quickOpen("extensions.json") + // If the file is open, we will see an empty array + // assuming no extensions are installed + expect(await page.isVisible("text=[]")) + }) +}) diff --git a/test/e2e/models/CodeServer.ts b/test/e2e/models/CodeServer.ts new file mode 100644 index 000000000..98fce1907 --- /dev/null +++ b/test/e2e/models/CodeServer.ts @@ -0,0 +1,61 @@ +import { Page } from "playwright" +import { CODE_SERVER_ADDRESS } from "../../utils/constants" +// This is a Page Object Model +// We use these to simplify e2e test authoring +// See Playwright docs: https://playwright.dev/docs/pom/ +export class CodeServer { + page: Page + + constructor(page: Page) { + this.page = page + } + async navigate() { + await this.page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) + // Make sure the editor actually loaded + await this.page.isVisible("div.monaco-workbench") + } + /** + * Opens the default folder /User if no arg passed + * @param absolutePath Example: /Users/jp/.local/share/code-server/User/ + * + */ + async openFolder(absolutePath?: string) { + // Check if no folder is opened + const folderIsNotOpen = await this.page.isVisible("text=You have not yet opened") + + if (folderIsNotOpen) { + // Open the default folder + await this.page.keyboard.press("Meta+O") + await this.page.keyboard.press("Enter") + await this.page.waitForLoadState("networkidle") + } + } + + /** + * Toggles the integrated terminal if not already in view + * and focuses it + */ + async viewTerminal() { + // Check if Terminal is already in view + const isTerminalInView = await this.page.isVisible("#terminal") + + if (!isTerminalInView) { + // Open using default keyboard shortcut + await this.focusTerminal() + await this.page.waitForSelector("#terminal") + } + } + + async focusTerminal() { + await this.page.keyboard.press("Control+Backquote") + } + + async quickOpen(input: string) { + await this.page.keyboard.press("Meta+P") + await this.page.waitForSelector('[aria-describedby="quickInput_message"]') + await this.page.keyboard.type(input) + await this.page.waitForTimeout(2000) + await this.page.keyboard.press("Enter") + await this.page.waitForTimeout(2000) + } +} diff --git a/test/e2e/terminal.test.ts b/test/e2e/terminal.test.ts new file mode 100644 index 000000000..a68f01bd1 --- /dev/null +++ b/test/e2e/terminal.test.ts @@ -0,0 +1,60 @@ +import { test, expect } from "@playwright/test" +import { STORAGE } from "../utils/constants" +import { CodeServer } from "./models/CodeServer" + +test.describe("Integrated Terminal", () => { + // Create a new context with the saved storage state + // so we don't have to logged in + const options: any = {} + const testFileName = "hello.txt" + const testString = "new string test from e2e test" + let codeServer: CodeServer + + // TODO@jsjoeio + // Fix this once https://github.com/microsoft/playwright-test/issues/240 + // is fixed + if (STORAGE) { + const storageState = JSON.parse(STORAGE) || {} + options.contextOptions = { + storageState, + } + } + test.beforeEach(async ({ page }) => { + codeServer = new CodeServer(page) + await codeServer.navigate() + }) + + test("should echo a string to a file", options, async ({ page }) => { + // Open the default folder + await codeServer.openFolder() + + // Open terminal and type in value + await codeServer.viewTerminal() + await codeServer.focusTerminal() + + await page.keyboard.type(`echo '${testString}' >> ${testFileName}`) + await page.keyboard.press("Enter") + await page.waitForTimeout(2000) + // It should show up on the left sidebar as a new file + const isFileVisible = await page.isVisible(`text="${testFileName}"`) + expect(isFileVisible).toBe(true) + + if (isFileVisible) { + // Check that the file has the test string in it + await codeServer.quickOpen(testFileName) + expect(await page.isVisible(`text="${testString}"`)).toBe(true) + + // Clean up + // Remove file + await codeServer.focusTerminal() + await page.keyboard.type(`rm ${testFileName}`) + await page.keyboard.press("Enter") + await page.waitForTimeout(2000) + // Close the file from workbench + // otherwise it will still be visible + // and our assertion will fail + await page.keyboard.press(`Meta+W`) + expect(await page.isVisible(`text="${testString}"`)).toBe(false) + } + }) +}) From 2bf0a0e76ed5a3d1ff0a464cad2696f51d147acd Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Tue, 20 Apr 2021 10:48:17 -0700 Subject: [PATCH 2/6] refactor: fix tests to check visibility correctly --- test/e2e/globalSetup.test.ts | 2 +- test/e2e/login.test.ts | 2 +- test/e2e/logout.test.ts | 4 ++-- test/e2e/openHelpAbout.test.ts | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/e2e/globalSetup.test.ts b/test/e2e/globalSetup.test.ts index d0eb8ccc7..28d89bfc3 100644 --- a/test/e2e/globalSetup.test.ts +++ b/test/e2e/globalSetup.test.ts @@ -20,6 +20,6 @@ test.describe("globalSetup", () => { test("should keep us logged in using the storageState", options, async ({ page }) => { await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) // Make sure the editor actually loaded - expect(await page.isVisible("div.monaco-workbench")) + expect(await page.isVisible("div.monaco-workbench")).toBe(true) }) }) diff --git a/test/e2e/login.test.ts b/test/e2e/login.test.ts index 4277e2cd3..e74c81c8d 100644 --- a/test/e2e/login.test.ts +++ b/test/e2e/login.test.ts @@ -24,7 +24,7 @@ test.describe("login", () => { await page.click(".submit") await page.waitForLoadState("networkidle") // Make sure the editor actually loaded - expect(await page.isVisible("div.monaco-workbench")) + expect(await page.isVisible("div.monaco-workbench")).toBe(true) }) test("should see an error message for missing password", options, async ({ page }) => { diff --git a/test/e2e/logout.test.ts b/test/e2e/logout.test.ts index e3ef887a5..aa080f5f1 100644 --- a/test/e2e/logout.test.ts +++ b/test/e2e/logout.test.ts @@ -17,14 +17,14 @@ test.describe("logout", () => { await page.click(".submit") await page.waitForLoadState("networkidle") // Make sure the editor actually loaded - expect(await page.isVisible("div.monaco-workbench")) + expect(await page.isVisible("div.monaco-workbench")).toBe(true) // Click the Application menu await page.click("[aria-label='Application Menu']") // See the Log out button const logoutButton = "a.action-menu-item span[aria-label='Log out']" - expect(await page.isVisible(logoutButton)) + expect(await page.isVisible(logoutButton)).toBe(true) await page.hover(logoutButton) // TODO(@jsjoeio) diff --git a/test/e2e/openHelpAbout.test.ts b/test/e2e/openHelpAbout.test.ts index c1070824f..9de696955 100644 --- a/test/e2e/openHelpAbout.test.ts +++ b/test/e2e/openHelpAbout.test.ts @@ -21,26 +21,26 @@ test.describe("Open Help > About", () => { async ({ page }) => { await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) // Make sure the editor actually loaded - expect(await page.isVisible("div.monaco-workbench")) + expect(await page.isVisible("div.monaco-workbench")).toBe(true) // Click the Application menu await page.click("[aria-label='Application Menu']") // See the Help button const helpButton = "a.action-menu-item span[aria-label='Help']" - expect(await page.isVisible(helpButton)) + expect(await page.isVisible(helpButton)).toBe(true) // Hover the helpButton await page.hover(helpButton) // see the About button and click it const aboutButton = "a.action-menu-item span[aria-label='About']" - expect(await page.isVisible(aboutButton)) + expect(await page.isVisible(aboutButton)).toBe(true) // NOTE: it won't work unless you hover it first await page.hover(aboutButton) await page.click(aboutButton) const codeServerText = "text=code-server" - expect(await page.isVisible(codeServerText)) + expect(await page.isVisible(codeServerText)).toBe(true) }, ) }) From cc99fddf24c7de1bfbb4d41f2e656034f5161ee0 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Tue, 20 Apr 2021 12:41:54 -0700 Subject: [PATCH 3/6] feat: add test for terminal echo to file --- test/e2e/codeServer.test.ts | 29 ++++++----- test/e2e/models/CodeServer.ts | 91 ++++++++++++++++++++--------------- test/e2e/terminal.test.ts | 52 ++++++++++---------- 3 files changed, 93 insertions(+), 79 deletions(-) diff --git a/test/e2e/codeServer.test.ts b/test/e2e/codeServer.test.ts index 1982a100b..d0e6ac929 100644 --- a/test/e2e/codeServer.test.ts +++ b/test/e2e/codeServer.test.ts @@ -1,5 +1,5 @@ import { test, expect } from "@playwright/test" -import { STORAGE } from "../utils/constants" +import { CODE_SERVER_ADDRESS, STORAGE } from "../utils/constants" import { CodeServer } from "./models/CodeServer" test.describe("CodeServer", () => { @@ -23,24 +23,23 @@ test.describe("CodeServer", () => { await codeServer.navigate() }) - test("should open the default folder if not open", options, async ({ page }) => { - await codeServer.openFolder() + test("should navigate to the CODE_SERVER_ADDRESS", options, async ({ page }) => { + // We navigate codeServer before each test + // and we start the test with a storage state + // which means we should be logged in + // so it should be on the address + const url = page.url() + // We use match because there may be a / at the end + // so we don't want it to fail if we expect http://localhost:8080 to match http://localhost:8080/ + expect(url).toMatch(CODE_SERVER_ADDRESS) + }) - // find workspaceStorage in the Explorer menu, which would be open in the User folder - // which is the default folder that opens - expect(await page.isVisible("text=workspaceStorage")).toBe(true) + test("should always see the code-server editor", options, async ({ page }) => { + expect(await codeServer.isEditorVisible()).toBe(true) }) test("should show the Integrated Terminal", options, async ({ page }) => { - await codeServer.viewTerminal() + await codeServer.focusTerminal() expect(await page.isVisible("#terminal")).toBe(true) }) - - test("should open a file with quickOpen", options, async ({ page }) => { - await codeServer.openFolder() - await codeServer.quickOpen("extensions.json") - // If the file is open, we will see an empty array - // assuming no extensions are installed - expect(await page.isVisible("text=[]")) - }) }) diff --git a/test/e2e/models/CodeServer.ts b/test/e2e/models/CodeServer.ts index 98fce1907..7fbf37060 100644 --- a/test/e2e/models/CodeServer.ts +++ b/test/e2e/models/CodeServer.ts @@ -9,53 +9,66 @@ export class CodeServer { constructor(page: Page) { this.page = page } + + /** + * Navigates to CODE_SERVER_ADDRESS + */ async navigate() { await this.page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) + + let editorIsVisible = await this.isEditorVisible() + let reloadCount = 0 + + // Occassionally code-server timeouts in Firefox + // we're not sure why + // but usually a reload or two fixes it + // TODO@jsjoeio @oxy look into Firefox reconnection/timeout issues + // TODO@jsjoeio sometimes it's 2 reloads, othertimes it's 9 + // double-check this logic + while (!editorIsVisible) { + reloadCount += 1 + editorIsVisible = await this.isEditorVisible() + if (editorIsVisible) { + console.log(`Editor became visible after ${reloadCount} reloads`) + break + } + await this.page.reload({ waitUntil: "networkidle" }) + } + } + + /** + * Checks if the editor is visible + */ + async isEditorVisible() { // Make sure the editor actually loaded - await this.page.isVisible("div.monaco-workbench") - } - /** - * Opens the default folder /User if no arg passed - * @param absolutePath Example: /Users/jp/.local/share/code-server/User/ - * - */ - async openFolder(absolutePath?: string) { - // Check if no folder is opened - const folderIsNotOpen = await this.page.isVisible("text=You have not yet opened") - - if (folderIsNotOpen) { - // Open the default folder - await this.page.keyboard.press("Meta+O") - await this.page.keyboard.press("Enter") - await this.page.waitForLoadState("networkidle") - } + // If it's not visible after 2 seconds, something is wrong + await this.page.waitForLoadState("networkidle") + return await this.page.isVisible("div.monaco-workbench", { timeout: 5000 }) } /** - * Toggles the integrated terminal if not already in view - * and focuses it + * Focuses Integrated Terminal + * by going to the Application Menu + * and clicking View > Terminal */ - async viewTerminal() { - // Check if Terminal is already in view - const isTerminalInView = await this.page.isVisible("#terminal") - - if (!isTerminalInView) { - // Open using default keyboard shortcut - await this.focusTerminal() - await this.page.waitForSelector("#terminal") - } - } - async focusTerminal() { - await this.page.keyboard.press("Control+Backquote") - } + // If the terminal is already visible + // then we can focus it by hitting the keyboard shortcut + const isTerminalVisible = await this.page.isVisible("#terminal") + if (isTerminalVisible) { + await this.page.keyboard.press(`Meta+Backquote`) + return + } + // Open using the manu + // Click [aria-label="Application Menu"] div[role="none"] + await this.page.click('[aria-label="Application Menu"] div[role="none"]') - async quickOpen(input: string) { - await this.page.keyboard.press("Meta+P") - await this.page.waitForSelector('[aria-describedby="quickInput_message"]') - await this.page.keyboard.type(input) - await this.page.waitForTimeout(2000) - await this.page.keyboard.press("Enter") - await this.page.waitForTimeout(2000) + // Click text=View + await this.page.hover("text=View") + await this.page.click("text=View") + + // Click text=Terminal + await this.page.hover("text=Terminal") + await this.page.click("text=Terminal") } } diff --git a/test/e2e/terminal.test.ts b/test/e2e/terminal.test.ts index a68f01bd1..9fb31bcfd 100644 --- a/test/e2e/terminal.test.ts +++ b/test/e2e/terminal.test.ts @@ -1,4 +1,8 @@ import { test, expect } from "@playwright/test" +import * as fs from "fs" +import { tmpdir } from "os" +import * as path from "path" + import { STORAGE } from "../utils/constants" import { CodeServer } from "./models/CodeServer" @@ -6,7 +10,7 @@ test.describe("Integrated Terminal", () => { // Create a new context with the saved storage state // so we don't have to logged in const options: any = {} - const testFileName = "hello.txt" + const testFileName = "test.txt" const testString = "new string test from e2e test" let codeServer: CodeServer @@ -25,36 +29,34 @@ test.describe("Integrated Terminal", () => { }) test("should echo a string to a file", options, async ({ page }) => { - // Open the default folder - await codeServer.openFolder() - + const tmpFolderPath = fs.mkdtempSync(path.join(tmpdir(), "code-server-test")) + const tmpFile = `${tmpFolderPath}${path.sep}${testFileName}` // Open terminal and type in value - await codeServer.viewTerminal() await codeServer.focusTerminal() - await page.keyboard.type(`echo '${testString}' >> ${testFileName}`) + // give the terminal a second to load + await page.waitForTimeout(3000) + await page.keyboard.type(`echo '${testString}' > ${tmpFile}`) + // Wait for the typing to finish before hitting enter + await page.waitForTimeout(500) await page.keyboard.press("Enter") await page.waitForTimeout(2000) - // It should show up on the left sidebar as a new file - const isFileVisible = await page.isVisible(`text="${testFileName}"`) - expect(isFileVisible).toBe(true) - if (isFileVisible) { - // Check that the file has the test string in it - await codeServer.quickOpen(testFileName) - expect(await page.isVisible(`text="${testString}"`)).toBe(true) + // .access checks if the file exists without opening it + // it doesn't return anything hence why we expect it to + // resolve to undefined + // If the promise rejects (i.e. the file doesn't exist) + // then the assertion will fail + await expect(fs.promises.access(tmpFile)).resolves.toBeUndefined() - // Clean up - // Remove file - await codeServer.focusTerminal() - await page.keyboard.type(`rm ${testFileName}`) - await page.keyboard.press("Enter") - await page.waitForTimeout(2000) - // Close the file from workbench - // otherwise it will still be visible - // and our assertion will fail - await page.keyboard.press(`Meta+W`) - expect(await page.isVisible(`text="${testString}"`)).toBe(false) - } + await fs.promises.rmdir(tmpFolderPath, { recursive: true }) + // Make sure neither file nor folder exist + // Note: We have to use ts-ignore because of an upstream typing error + // See: https://github.com/microsoft/folio/issues/230#event-4621948411 + /* eslint-disable @typescript-eslint/ban-ts-comment */ + // @ts-ignore + expect(fs.promises.access(tmpFile)).rejects.toThrowError(/no such file or directory/) + // @ts-ignore + expect(fs.promises.access(tmpFolderPath)).rejects.toThrowError(/no such file or directory/) }) }) From cb65590b98404b3e16a68578a9e83063ee7a3cbb Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Wed, 21 Apr 2021 13:22:11 -0700 Subject: [PATCH 4/6] refactor: move tmpdir into src/node/constants --- src/node/constants.ts | 2 ++ src/node/socket.ts | 3 ++- src/node/util.ts | 2 -- test/e2e/models/CodeServer.ts | 14 +++++++------- test/e2e/terminal.test.ts | 4 ++++ test/unit/cli.test.ts | 3 ++- test/unit/socket.test.ts | 3 ++- test/unit/update.test.ts | 2 +- 8 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/node/constants.ts b/src/node/constants.ts index c39beb05e..c198f8fb3 100644 --- a/src/node/constants.ts +++ b/src/node/constants.ts @@ -1,5 +1,6 @@ import { logger } from "@coder/logger" import { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package" +import * as os from "os" import * as path from "path" export function getPackageJson(relativePath: string): JSONSchemaForNPMPackageJsonFiles { @@ -18,3 +19,4 @@ const pkg = getPackageJson("../../package.json") export const version = pkg.version || "development" export const commit = pkg.commit || "development" export const rootPath = path.resolve(__dirname, "../..") +export const tmpdir = path.join(os.tmpdir(), "code-server") diff --git a/src/node/socket.ts b/src/node/socket.ts index 5885f7fde..9c937bbb5 100644 --- a/src/node/socket.ts +++ b/src/node/socket.ts @@ -4,7 +4,8 @@ import * as path from "path" import * as tls from "tls" import { Emitter } from "../common/emitter" import { generateUuid } from "../common/util" -import { canConnect, tmpdir } from "./util" +import { tmpdir } from "./constants" +import { canConnect } from "./util" /** * Provides a way to proxy a TLS socket. Can be used when you need to pass a diff --git a/src/node/util.ts b/src/node/util.ts index e8ec311ed..380e32b9b 100644 --- a/src/node/util.ts +++ b/src/node/util.ts @@ -8,8 +8,6 @@ import * as path from "path" import * as util from "util" import xdgBasedir from "xdg-basedir" -export const tmpdir = path.join(os.tmpdir(), "code-server") - interface Paths { data: string config: string diff --git a/test/e2e/models/CodeServer.ts b/test/e2e/models/CodeServer.ts index 7fbf37060..8e2de3161 100644 --- a/test/e2e/models/CodeServer.ts +++ b/test/e2e/models/CodeServer.ts @@ -16,23 +16,23 @@ export class CodeServer { async navigate() { await this.page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) - let editorIsVisible = await this.isEditorVisible() + const editorIsVisible = await this.isEditorVisible() let reloadCount = 0 // Occassionally code-server timeouts in Firefox // we're not sure why // but usually a reload or two fixes it // TODO@jsjoeio @oxy look into Firefox reconnection/timeout issues - // TODO@jsjoeio sometimes it's 2 reloads, othertimes it's 9 - // double-check this logic while (!editorIsVisible) { reloadCount += 1 - editorIsVisible = await this.isEditorVisible() - if (editorIsVisible) { - console.log(`Editor became visible after ${reloadCount} reloads`) + if (await this.isEditorVisible()) { + console.log(` Editor became visible after ${reloadCount} reloads`) break } - await this.page.reload({ waitUntil: "networkidle" }) + // When a reload happens, we want to wait for all resources to be + // loaded completely. Hence why we use that instead of DOMContentLoaded + // Read more: https://thisthat.dev/dom-content-loaded-vs-load/ + await this.page.reload({ waitUntil: "load" }) } } diff --git a/test/e2e/terminal.test.ts b/test/e2e/terminal.test.ts index 9fb31bcfd..0e1af1e48 100644 --- a/test/e2e/terminal.test.ts +++ b/test/e2e/terminal.test.ts @@ -29,6 +29,10 @@ test.describe("Integrated Terminal", () => { }) test("should echo a string to a file", options, async ({ page }) => { + // NOTE@jsjoeio + // We're not using tmpdir from src/node/constants + // because Playwright doesn't fully support ES modules from + // the erorrs I'm seeing const tmpFolderPath = fs.mkdtempSync(path.join(tmpdir(), "code-server-test")) const tmpFile = `${tmpFolderPath}${path.sep}${testFileName}` // Open terminal and type in value diff --git a/test/unit/cli.test.ts b/test/unit/cli.test.ts index 27498cf00..38d8dc7b9 100644 --- a/test/unit/cli.test.ts +++ b/test/unit/cli.test.ts @@ -4,7 +4,8 @@ import * as net from "net" import * as os from "os" import * as path from "path" import { Args, parse, setDefaults, shouldOpenInExistingInstance } from "../../src/node/cli" -import { paths, tmpdir } from "../../src/node/util" +import { tmpdir } from "../../src/node/constants" +import { paths } from "../../src/node/util" type Mutable = { -readonly [P in keyof T]: T[P] diff --git a/test/unit/socket.test.ts b/test/unit/socket.test.ts index 4fedf5a84..9b8749f53 100644 --- a/test/unit/socket.test.ts +++ b/test/unit/socket.test.ts @@ -4,8 +4,9 @@ import * as net from "net" import * as path from "path" import * as tls from "tls" import { Emitter } from "../../src/common/emitter" +import { tmpdir } from "../../src/node/constants" import { SocketProxyProvider } from "../../src/node/socket" -import { generateCertificate, tmpdir } from "../../src/node/util" +import { generateCertificate } from "../../src/node/util" describe("SocketProxyProvider", () => { const provider = new SocketProxyProvider() diff --git a/test/unit/update.test.ts b/test/unit/update.test.ts index 2f73f80df..394371201 100644 --- a/test/unit/update.test.ts +++ b/test/unit/update.test.ts @@ -1,9 +1,9 @@ import { promises as fs } from "fs" import * as http from "http" import * as path from "path" +import { tmpdir } from "../../src/node/constants" import { SettingsProvider, UpdateSettings } from "../../src/node/settings" import { LatestResponse, UpdateProvider } from "../../src/node/update" -import { tmpdir } from "../../src/node/util" describe.skip("update", () => { let version = "1.0.0" From b0ecff338ff41f17e5bbe2bbece51c386d7d295a Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Wed, 21 Apr 2021 14:31:15 -0700 Subject: [PATCH 5/6] refactor: globalSetup to use CodeServer model --- test/e2e/browser.test.ts | 28 +++++++++++++++--------- test/e2e/codeServer.test.ts | 4 ++-- test/e2e/globalSetup.test.ts | 11 +++++++--- test/e2e/login.test.ts | 19 ++++++++++------ test/e2e/logout.test.ts | 14 ++++++++++-- test/e2e/models/CodeServer.ts | 32 ++++++++++++++++++++++++++- test/e2e/openHelpAbout.test.ts | 40 +++++++++++++++++----------------- test/e2e/terminal.test.ts | 39 +++++++++++++++++++++------------ 8 files changed, 128 insertions(+), 59 deletions(-) diff --git a/test/e2e/browser.test.ts b/test/e2e/browser.test.ts index c5537ba0d..67952123c 100644 --- a/test/e2e/browser.test.ts +++ b/test/e2e/browser.test.ts @@ -1,15 +1,23 @@ import { test, expect } from "@playwright/test" -import { CODE_SERVER_ADDRESS } from "../utils/constants" +import { CodeServer } from "./models/CodeServer" // This is a "gut-check" test to make sure playwright is working as expected -test("browser should display correct userAgent", async ({ page, browserName }) => { - const displayNames = { - chromium: "Chrome", - firefox: "Firefox", - webkit: "Safari", - } - await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) - const userAgent = await page.evaluate("navigator.userAgent") +test.describe("browser", () => { + let codeServer: CodeServer - expect(userAgent).toContain(displayNames[browserName]) + test.beforeEach(async ({ page }) => { + codeServer = new CodeServer(page) + await codeServer.navigate() + }) + + test("browser should display correct userAgent", async ({ page, browserName }) => { + const displayNames = { + chromium: "Chrome", + firefox: "Firefox", + webkit: "Safari", + } + const userAgent = await page.evaluate("navigator.userAgent") + + expect(userAgent).toContain(displayNames[browserName]) + }) }) diff --git a/test/e2e/codeServer.test.ts b/test/e2e/codeServer.test.ts index d0e6ac929..4b20f69fc 100644 --- a/test/e2e/codeServer.test.ts +++ b/test/e2e/codeServer.test.ts @@ -20,10 +20,10 @@ test.describe("CodeServer", () => { test.beforeEach(async ({ page }) => { codeServer = new CodeServer(page) - await codeServer.navigate() + await codeServer.setup() }) - test("should navigate to the CODE_SERVER_ADDRESS", options, async ({ page }) => { + test(`should navigate to ${CODE_SERVER_ADDRESS}`, options, async ({ page }) => { // We navigate codeServer before each test // and we start the test with a storage state // which means we should be logged in diff --git a/test/e2e/globalSetup.test.ts b/test/e2e/globalSetup.test.ts index 28d89bfc3..0a950741f 100644 --- a/test/e2e/globalSetup.test.ts +++ b/test/e2e/globalSetup.test.ts @@ -1,5 +1,6 @@ import { test, expect } from "@playwright/test" -import { CODE_SERVER_ADDRESS, STORAGE } from "../utils/constants" +import { STORAGE } from "../utils/constants" +import { CodeServer } from "./models/CodeServer" // This test is to make sure the globalSetup works as expected // meaning globalSetup ran and stored the storageState in STORAGE @@ -7,6 +8,7 @@ test.describe("globalSetup", () => { // Create a new context with the saved storage state // so we don't have to logged in const options: any = {} + let codeServer: CodeServer // TODO@jsjoeio // Fix this once https://github.com/microsoft/playwright-test/issues/240 @@ -17,9 +19,12 @@ test.describe("globalSetup", () => { storageState, } } + test.beforeEach(async ({ page }) => { + codeServer = new CodeServer(page) + await codeServer.setup() + }) test("should keep us logged in using the storageState", options, async ({ page }) => { - await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) // Make sure the editor actually loaded - expect(await page.isVisible("div.monaco-workbench")).toBe(true) + expect(await codeServer.isEditorVisible()).toBe(true) }) }) diff --git a/test/e2e/login.test.ts b/test/e2e/login.test.ts index e74c81c8d..9e2f3da37 100644 --- a/test/e2e/login.test.ts +++ b/test/e2e/login.test.ts @@ -1,5 +1,6 @@ import { test, expect } from "@playwright/test" -import { CODE_SERVER_ADDRESS, PASSWORD } from "../utils/constants" +import { PASSWORD } from "../utils/constants" +import { CodeServer } from "./models/CodeServer" test.describe("login", () => { // Reset the browser so no cookies are persisted @@ -9,26 +10,32 @@ test.describe("login", () => { storageState: {}, }, } + let codeServer: CodeServer + + test.beforeEach(async ({ page }) => { + codeServer = new CodeServer(page) + await codeServer.navigate() + }) test("should see the login page", options, async ({ page }) => { - await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) // It should send us to the login page expect(await page.title()).toBe("code-server login") }) test("should be able to login", options, async ({ page }) => { - await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) // Type in password await page.fill(".password", PASSWORD) // Click the submit button and login await page.click(".submit") await page.waitForLoadState("networkidle") + // We do this because occassionally code-server doesn't load on Firefox + // but loads if you reload once or twice + await codeServer.reloadUntilEditorIsVisible() // Make sure the editor actually loaded - expect(await page.isVisible("div.monaco-workbench")).toBe(true) + expect(await codeServer.isEditorVisible()).toBe(true) }) test("should see an error message for missing password", options, async ({ page }) => { - await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) // Skip entering password // Click the submit button and login await page.click(".submit") @@ -37,7 +44,6 @@ test.describe("login", () => { }) test("should see an error message for incorrect password", options, async ({ page }) => { - await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) // Type in password await page.fill(".password", "password123") // Click the submit button and login @@ -47,7 +53,6 @@ test.describe("login", () => { }) test("should hit the rate limiter for too many unsuccessful logins", options, async ({ page }) => { - await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) // Type in password await page.fill(".password", "password123") // Click the submit button and login diff --git a/test/e2e/logout.test.ts b/test/e2e/logout.test.ts index aa080f5f1..5e9dc8f9c 100644 --- a/test/e2e/logout.test.ts +++ b/test/e2e/logout.test.ts @@ -1,5 +1,6 @@ import { test, expect } from "@playwright/test" import { CODE_SERVER_ADDRESS, PASSWORD } from "../utils/constants" +import { CodeServer } from "./models/CodeServer" test.describe("logout", () => { // Reset the browser so no cookies are persisted @@ -9,15 +10,24 @@ test.describe("logout", () => { storageState: {}, }, } + let codeServer: CodeServer + + test.beforeEach(async ({ page }) => { + codeServer = new CodeServer(page) + await codeServer.navigate() + }) + test("should be able login and logout", options, async ({ page }) => { - await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) // Type in password await page.fill(".password", PASSWORD) // Click the submit button and login await page.click(".submit") await page.waitForLoadState("networkidle") + // We do this because occassionally code-server doesn't load on Firefox + // but loads if you reload once or twice + await codeServer.reloadUntilEditorIsVisible() // Make sure the editor actually loaded - expect(await page.isVisible("div.monaco-workbench")).toBe(true) + expect(await codeServer.isEditorVisible()).toBe(true) // Click the Application menu await page.click("[aria-label='Application Menu']") diff --git a/test/e2e/models/CodeServer.ts b/test/e2e/models/CodeServer.ts index 8e2de3161..d08e993be 100644 --- a/test/e2e/models/CodeServer.ts +++ b/test/e2e/models/CodeServer.ts @@ -15,7 +15,13 @@ export class CodeServer { */ async navigate() { await this.page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) + } + /** + * Checks if the editor is visible + * and reloads until it is + */ + async reloadUntilEditorIsVisible() { const editorIsVisible = await this.isEditorVisible() let reloadCount = 0 @@ -56,7 +62,12 @@ export class CodeServer { // then we can focus it by hitting the keyboard shortcut const isTerminalVisible = await this.page.isVisible("#terminal") if (isTerminalVisible) { - await this.page.keyboard.press(`Meta+Backquote`) + await this.page.keyboard.press(`Control+Backquote`) + // Wait for terminal to receive focus + await this.page.waitForSelector("div.terminal.xterm.focus") + // Sometimes the terminal reloads + // which is why we wait for it twice + await this.page.waitForSelector("div.terminal.xterm.focus") return } // Open using the manu @@ -70,5 +81,24 @@ export class CodeServer { // Click text=Terminal await this.page.hover("text=Terminal") await this.page.click("text=Terminal") + + // Wait for terminal to receive focus + // Sometimes the terminal reloads once or twice + // which is why we wait for it to have the focus class + await this.page.waitForSelector("div.terminal.xterm.focus") + // Sometimes the terminal reloads + // which is why we wait for it twice + await this.page.waitForSelector("div.terminal.xterm.focus") + } + + /** + * Navigates to CODE_SERVER_ADDRESS + * and reloads until the editor is visible + * + * Helpful for running before tests + */ + async setup() { + await this.navigate() + await this.reloadUntilEditorIsVisible() } } diff --git a/test/e2e/openHelpAbout.test.ts b/test/e2e/openHelpAbout.test.ts index 9de696955..a048ec380 100644 --- a/test/e2e/openHelpAbout.test.ts +++ b/test/e2e/openHelpAbout.test.ts @@ -1,10 +1,12 @@ import { test, expect } from "@playwright/test" -import { CODE_SERVER_ADDRESS, STORAGE } from "../utils/constants" +import { STORAGE } from "../utils/constants" +import { CodeServer } from "./models/CodeServer" test.describe("Open Help > About", () => { // Create a new context with the saved storage state // so we don't have to logged in const options: any = {} + let codeServer: CodeServer // TODO@jsjoeio // Fix this once https://github.com/microsoft/playwright-test/issues/240 // is fixed @@ -15,32 +17,30 @@ test.describe("Open Help > About", () => { } } + test.beforeEach(async ({ page }) => { + codeServer = new CodeServer(page) + await codeServer.setup() + }) + test( "should see a 'Help' then 'About' button in the Application Menu that opens a dialog", options, async ({ page }) => { - await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" }) - // Make sure the editor actually loaded - expect(await page.isVisible("div.monaco-workbench")).toBe(true) + // Open using the manu + // Click [aria-label="Application Menu"] div[role="none"] + await page.click('[aria-label="Application Menu"] div[role="none"]') - // Click the Application menu - await page.click("[aria-label='Application Menu']") - // See the Help button - const helpButton = "a.action-menu-item span[aria-label='Help']" - expect(await page.isVisible(helpButton)).toBe(true) + // Click the Help button + await page.hover("text=Help") + await page.click("text=Help") - // Hover the helpButton - await page.hover(helpButton) + // Click the About button + await page.hover("text=About") + await page.click("text=About") - // see the About button and click it - const aboutButton = "a.action-menu-item span[aria-label='About']" - expect(await page.isVisible(aboutButton)).toBe(true) - // NOTE: it won't work unless you hover it first - await page.hover(aboutButton) - await page.click(aboutButton) - - const codeServerText = "text=code-server" - expect(await page.isVisible(codeServerText)).toBe(true) + // Click div[role="dialog"] >> text=code-server + const element = await page.waitForSelector('div[role="dialog"] >> text=code-server') + expect(element).not.toBeNull() }, ) }) diff --git a/test/e2e/terminal.test.ts b/test/e2e/terminal.test.ts index 0e1af1e48..53f4dcca6 100644 --- a/test/e2e/terminal.test.ts +++ b/test/e2e/terminal.test.ts @@ -2,7 +2,8 @@ import { test, expect } from "@playwright/test" import * as fs from "fs" import { tmpdir } from "os" import * as path from "path" - +import util from "util" +import * as cp from "child_process" import { STORAGE } from "../utils/constants" import { CodeServer } from "./models/CodeServer" @@ -10,9 +11,11 @@ test.describe("Integrated Terminal", () => { // Create a new context with the saved storage state // so we don't have to logged in const options: any = {} - const testFileName = "test.txt" + const testFileName = "pipe" const testString = "new string test from e2e test" let codeServer: CodeServer + let tmpFolderPath: string = "" + let tmpFile: string = "" // TODO@jsjoeio // Fix this once https://github.com/microsoft/playwright-test/issues/240 @@ -25,26 +28,34 @@ test.describe("Integrated Terminal", () => { } test.beforeEach(async ({ page }) => { codeServer = new CodeServer(page) - await codeServer.navigate() - }) - - test("should echo a string to a file", options, async ({ page }) => { + await codeServer.setup() // NOTE@jsjoeio // We're not using tmpdir from src/node/constants // because Playwright doesn't fully support ES modules from // the erorrs I'm seeing - const tmpFolderPath = fs.mkdtempSync(path.join(tmpdir(), "code-server-test")) - const tmpFile = `${tmpFolderPath}${path.sep}${testFileName}` + tmpFolderPath = fs.mkdtempSync(path.join(tmpdir(), "code-server-test")) + tmpFile = path.join(tmpFolderPath, testFileName) + }) + + test.afterEach(async () => { + // Ensure directory was removed + fs.rmdirSync(tmpFolderPath, { recursive: true }) + }) + + test("should echo a string to a file", options, async ({ page }) => { + const command = `mkfifo '${tmpFile}' && cat '${tmpFile}'` + const exec = util.promisify(cp.exec) + const output = exec(command, { encoding: "utf8" }) + // Open terminal and type in value await codeServer.focusTerminal() - // give the terminal a second to load - await page.waitForTimeout(3000) - await page.keyboard.type(`echo '${testString}' > ${tmpFile}`) - // Wait for the typing to finish before hitting enter - await page.waitForTimeout(500) + await page.waitForLoadState("load") + await page.keyboard.type(`echo '${testString}' > '${tmpFile}'`) await page.keyboard.press("Enter") - await page.waitForTimeout(2000) + + const { stdout } = await output + expect(stdout).toMatch(testString) // .access checks if the file exists without opening it // it doesn't return anything hence why we expect it to From 7bfdd13cb308940388cb98ac71729dedb2b67379 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Fri, 23 Apr 2021 14:28:39 -0700 Subject: [PATCH 6/6] refactor: tmpdir and add to test utils --- test/e2e/models/CodeServer.ts | 2 +- test/e2e/terminal.test.ts | 46 +++++++++++------------------------ test/unit/constants.test.ts | 15 ++++++++++++ test/utils/constants.ts | 11 +++++++++ 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/test/e2e/models/CodeServer.ts b/test/e2e/models/CodeServer.ts index d08e993be..7dc2bd9ae 100644 --- a/test/e2e/models/CodeServer.ts +++ b/test/e2e/models/CodeServer.ts @@ -47,7 +47,7 @@ export class CodeServer { */ async isEditorVisible() { // Make sure the editor actually loaded - // If it's not visible after 2 seconds, something is wrong + // If it's not visible after 5 seconds, something is wrong await this.page.waitForLoadState("networkidle") return await this.page.isVisible("div.monaco-workbench", { timeout: 5000 }) } diff --git a/test/e2e/terminal.test.ts b/test/e2e/terminal.test.ts index 53f4dcca6..22a951ca2 100644 --- a/test/e2e/terminal.test.ts +++ b/test/e2e/terminal.test.ts @@ -1,10 +1,10 @@ import { test, expect } from "@playwright/test" +import * as cp from "child_process" import * as fs from "fs" -import { tmpdir } from "os" +// import { tmpdir } from "os" import * as path from "path" import util from "util" -import * as cp from "child_process" -import { STORAGE } from "../utils/constants" +import { STORAGE, tmpdir } from "../utils/constants" import { CodeServer } from "./models/CodeServer" test.describe("Integrated Terminal", () => { @@ -14,8 +14,8 @@ test.describe("Integrated Terminal", () => { const testFileName = "pipe" const testString = "new string test from e2e test" let codeServer: CodeServer - let tmpFolderPath: string = "" - let tmpFile: string = "" + let tmpFolderPath = "" + let tmpFile = "" // TODO@jsjoeio // Fix this once https://github.com/microsoft/playwright-test/issues/240 @@ -26,20 +26,19 @@ test.describe("Integrated Terminal", () => { storageState, } } - test.beforeEach(async ({ page }) => { - codeServer = new CodeServer(page) - await codeServer.setup() - // NOTE@jsjoeio - // We're not using tmpdir from src/node/constants - // because Playwright doesn't fully support ES modules from - // the erorrs I'm seeing - tmpFolderPath = fs.mkdtempSync(path.join(tmpdir(), "code-server-test")) + test.beforeAll(async () => { + tmpFolderPath = await tmpdir("integrated-terminal") tmpFile = path.join(tmpFolderPath, testFileName) }) - test.afterEach(async () => { + test.beforeEach(async ({ page }) => { + codeServer = new CodeServer(page) + await codeServer.setup() + }) + + test.afterAll(async () => { // Ensure directory was removed - fs.rmdirSync(tmpFolderPath, { recursive: true }) + await fs.promises.rmdir(tmpFolderPath, { recursive: true }) }) test("should echo a string to a file", options, async ({ page }) => { @@ -56,22 +55,5 @@ test.describe("Integrated Terminal", () => { const { stdout } = await output expect(stdout).toMatch(testString) - - // .access checks if the file exists without opening it - // it doesn't return anything hence why we expect it to - // resolve to undefined - // If the promise rejects (i.e. the file doesn't exist) - // then the assertion will fail - await expect(fs.promises.access(tmpFile)).resolves.toBeUndefined() - - await fs.promises.rmdir(tmpFolderPath, { recursive: true }) - // Make sure neither file nor folder exist - // Note: We have to use ts-ignore because of an upstream typing error - // See: https://github.com/microsoft/folio/issues/230#event-4621948411 - /* eslint-disable @typescript-eslint/ban-ts-comment */ - // @ts-ignore - expect(fs.promises.access(tmpFile)).rejects.toThrowError(/no such file or directory/) - // @ts-ignore - expect(fs.promises.access(tmpFolderPath)).rejects.toThrowError(/no such file or directory/) }) }) diff --git a/test/unit/constants.test.ts b/test/unit/constants.test.ts index e4b14a6cd..e07338234 100644 --- a/test/unit/constants.test.ts +++ b/test/unit/constants.test.ts @@ -1,4 +1,6 @@ +import * as fs from "fs" import { commit, getPackageJson, version } from "../../src/node/constants" +import { tmpdir } from "../../test/utils/constants" import { loggerModule } from "../utils/helpers" // jest.mock is hoisted above the imports so we must use `require` here. @@ -51,3 +53,16 @@ describe("constants", () => { }) }) }) + +describe("test constants", () => { + describe("tmpdir", () => { + it("should return a temp directory", async () => { + const testName = "temp-dir" + const pathToTempDir = await tmpdir(testName) + + expect(pathToTempDir).toContain(testName) + + await fs.promises.rmdir(pathToTempDir) + }) + }) +}) diff --git a/test/utils/constants.ts b/test/utils/constants.ts index ac2250e1c..a6abd209d 100644 --- a/test/utils/constants.ts +++ b/test/utils/constants.ts @@ -1,3 +1,14 @@ +import * as fs from "fs" +import * as os from "os" +import * as path from "path" + export const CODE_SERVER_ADDRESS = process.env.CODE_SERVER_ADDRESS || "http://localhost:8080" export const PASSWORD = process.env.PASSWORD || "e45432jklfdsab" export const STORAGE = process.env.STORAGE || "" + +export async function tmpdir(testName: string): Promise { + const dir = path.join(os.tmpdir(), "code-server") + await fs.promises.mkdir(dir, { recursive: true }) + + return await fs.promises.mkdtemp(path.join(dir, `test-${testName}-`), { encoding: "utf8" }) +}