Skip to content

Commit 07d6823

Browse files
authored
Merge pull request #3169 from cdr/jsjoeio/add-terminal-e2e-test
feat(testing): add e2e tests for code-server and terminal
2 parents 85ded73 + 7bfdd13 commit 07d6823

16 files changed

+315
-50
lines changed

src/node/constants.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { logger } from "@coder/logger"
22
import { JSONSchemaForNPMPackageJsonFiles } from "@schemastore/package"
3+
import * as os from "os"
34
import * as path from "path"
45

56
export function getPackageJson(relativePath: string): JSONSchemaForNPMPackageJsonFiles {
@@ -18,3 +19,4 @@ const pkg = getPackageJson("../../package.json")
1819
export const version = pkg.version || "development"
1920
export const commit = pkg.commit || "development"
2021
export const rootPath = path.resolve(__dirname, "../..")
22+
export const tmpdir = path.join(os.tmpdir(), "code-server")

src/node/socket.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import * as path from "path"
44
import * as tls from "tls"
55
import { Emitter } from "../common/emitter"
66
import { generateUuid } from "../common/util"
7-
import { canConnect, tmpdir } from "./util"
7+
import { tmpdir } from "./constants"
8+
import { canConnect } from "./util"
89

910
/**
1011
* Provides a way to proxy a TLS socket. Can be used when you need to pass a

src/node/util.ts

-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ import * as path from "path"
88
import * as util from "util"
99
import xdgBasedir from "xdg-basedir"
1010

11-
export const tmpdir = path.join(os.tmpdir(), "code-server")
12-
1311
interface Paths {
1412
data: string
1513
config: string

test/e2e/browser.test.ts

+19-11
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import { test, expect } from "@playwright/test"
2-
import { CODE_SERVER_ADDRESS } from "../utils/constants"
2+
import { CodeServer } from "./models/CodeServer"
33

44
// This is a "gut-check" test to make sure playwright is working as expected
5-
test("browser should display correct userAgent", async ({ page, browserName }) => {
6-
const displayNames = {
7-
chromium: "Chrome",
8-
firefox: "Firefox",
9-
webkit: "Safari",
10-
}
11-
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
12-
const userAgent = await page.evaluate("navigator.userAgent")
13-
14-
expect(userAgent).toContain(displayNames[browserName])
5+
test.describe("browser", () => {
6+
let codeServer: CodeServer
7+
8+
test.beforeEach(async ({ page }) => {
9+
codeServer = new CodeServer(page)
10+
await codeServer.navigate()
11+
})
12+
13+
test("browser should display correct userAgent", async ({ page, browserName }) => {
14+
const displayNames = {
15+
chromium: "Chrome",
16+
firefox: "Firefox",
17+
webkit: "Safari",
18+
}
19+
const userAgent = await page.evaluate("navigator.userAgent")
20+
21+
expect(userAgent).toContain(displayNames[browserName])
22+
})
1523
})

test/e2e/codeServer.test.ts

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { test, expect } from "@playwright/test"
2+
import { CODE_SERVER_ADDRESS, STORAGE } from "../utils/constants"
3+
import { CodeServer } from "./models/CodeServer"
4+
5+
test.describe("CodeServer", () => {
6+
// Create a new context with the saved storage state
7+
// so we don't have to logged in
8+
const options: any = {}
9+
let codeServer: CodeServer
10+
11+
// TODO@jsjoeio
12+
// Fix this once https://github.com/microsoft/playwright-test/issues/240
13+
// is fixed
14+
if (STORAGE) {
15+
const storageState = JSON.parse(STORAGE) || {}
16+
options.contextOptions = {
17+
storageState,
18+
}
19+
}
20+
21+
test.beforeEach(async ({ page }) => {
22+
codeServer = new CodeServer(page)
23+
await codeServer.setup()
24+
})
25+
26+
test(`should navigate to ${CODE_SERVER_ADDRESS}`, options, async ({ page }) => {
27+
// We navigate codeServer before each test
28+
// and we start the test with a storage state
29+
// which means we should be logged in
30+
// so it should be on the address
31+
const url = page.url()
32+
// We use match because there may be a / at the end
33+
// so we don't want it to fail if we expect http://localhost:8080 to match http://localhost:8080/
34+
expect(url).toMatch(CODE_SERVER_ADDRESS)
35+
})
36+
37+
test("should always see the code-server editor", options, async ({ page }) => {
38+
expect(await codeServer.isEditorVisible()).toBe(true)
39+
})
40+
41+
test("should show the Integrated Terminal", options, async ({ page }) => {
42+
await codeServer.focusTerminal()
43+
expect(await page.isVisible("#terminal")).toBe(true)
44+
})
45+
})

test/e2e/globalSetup.test.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
import { test, expect } from "@playwright/test"
2-
import { CODE_SERVER_ADDRESS, STORAGE } from "../utils/constants"
2+
import { STORAGE } from "../utils/constants"
3+
import { CodeServer } from "./models/CodeServer"
34

45
// This test is to make sure the globalSetup works as expected
56
// meaning globalSetup ran and stored the storageState in STORAGE
67
test.describe("globalSetup", () => {
78
// Create a new context with the saved storage state
89
// so we don't have to logged in
910
const options: any = {}
11+
let codeServer: CodeServer
1012

1113
// TODO@jsjoeio
1214
// Fix this once https://github.com/microsoft/playwright-test/issues/240
@@ -17,9 +19,12 @@ test.describe("globalSetup", () => {
1719
storageState,
1820
}
1921
}
22+
test.beforeEach(async ({ page }) => {
23+
codeServer = new CodeServer(page)
24+
await codeServer.setup()
25+
})
2026
test("should keep us logged in using the storageState", options, async ({ page }) => {
21-
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
2227
// Make sure the editor actually loaded
23-
expect(await page.isVisible("div.monaco-workbench"))
28+
expect(await codeServer.isEditorVisible()).toBe(true)
2429
})
2530
})

test/e2e/login.test.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { test, expect } from "@playwright/test"
2-
import { CODE_SERVER_ADDRESS, PASSWORD } from "../utils/constants"
2+
import { PASSWORD } from "../utils/constants"
3+
import { CodeServer } from "./models/CodeServer"
34

45
test.describe("login", () => {
56
// Reset the browser so no cookies are persisted
@@ -9,26 +10,32 @@ test.describe("login", () => {
910
storageState: {},
1011
},
1112
}
13+
let codeServer: CodeServer
14+
15+
test.beforeEach(async ({ page }) => {
16+
codeServer = new CodeServer(page)
17+
await codeServer.navigate()
18+
})
1219

1320
test("should see the login page", options, async ({ page }) => {
14-
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
1521
// It should send us to the login page
1622
expect(await page.title()).toBe("code-server login")
1723
})
1824

1925
test("should be able to login", options, async ({ page }) => {
20-
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
2126
// Type in password
2227
await page.fill(".password", PASSWORD)
2328
// Click the submit button and login
2429
await page.click(".submit")
2530
await page.waitForLoadState("networkidle")
31+
// We do this because occassionally code-server doesn't load on Firefox
32+
// but loads if you reload once or twice
33+
await codeServer.reloadUntilEditorIsVisible()
2634
// Make sure the editor actually loaded
27-
expect(await page.isVisible("div.monaco-workbench"))
35+
expect(await codeServer.isEditorVisible()).toBe(true)
2836
})
2937

3038
test("should see an error message for missing password", options, async ({ page }) => {
31-
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
3239
// Skip entering password
3340
// Click the submit button and login
3441
await page.click(".submit")
@@ -37,7 +44,6 @@ test.describe("login", () => {
3744
})
3845

3946
test("should see an error message for incorrect password", options, async ({ page }) => {
40-
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
4147
// Type in password
4248
await page.fill(".password", "password123")
4349
// Click the submit button and login
@@ -47,7 +53,6 @@ test.describe("login", () => {
4753
})
4854

4955
test("should hit the rate limiter for too many unsuccessful logins", options, async ({ page }) => {
50-
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
5156
// Type in password
5257
await page.fill(".password", "password123")
5358
// Click the submit button and login

test/e2e/logout.test.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { test, expect } from "@playwright/test"
22
import { CODE_SERVER_ADDRESS, PASSWORD } from "../utils/constants"
3+
import { CodeServer } from "./models/CodeServer"
34

45
test.describe("logout", () => {
56
// Reset the browser so no cookies are persisted
@@ -9,22 +10,31 @@ test.describe("logout", () => {
910
storageState: {},
1011
},
1112
}
13+
let codeServer: CodeServer
14+
15+
test.beforeEach(async ({ page }) => {
16+
codeServer = new CodeServer(page)
17+
await codeServer.navigate()
18+
})
19+
1220
test("should be able login and logout", options, async ({ page }) => {
13-
await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
1421
// Type in password
1522
await page.fill(".password", PASSWORD)
1623
// Click the submit button and login
1724
await page.click(".submit")
1825
await page.waitForLoadState("networkidle")
26+
// We do this because occassionally code-server doesn't load on Firefox
27+
// but loads if you reload once or twice
28+
await codeServer.reloadUntilEditorIsVisible()
1929
// Make sure the editor actually loaded
20-
expect(await page.isVisible("div.monaco-workbench"))
30+
expect(await codeServer.isEditorVisible()).toBe(true)
2131

2232
// Click the Application menu
2333
await page.click("[aria-label='Application Menu']")
2434

2535
// See the Log out button
2636
const logoutButton = "a.action-menu-item span[aria-label='Log out']"
27-
expect(await page.isVisible(logoutButton))
37+
expect(await page.isVisible(logoutButton)).toBe(true)
2838

2939
await page.hover(logoutButton)
3040
// TODO(@jsjoeio)

test/e2e/models/CodeServer.ts

+104
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { Page } from "playwright"
2+
import { CODE_SERVER_ADDRESS } from "../../utils/constants"
3+
// This is a Page Object Model
4+
// We use these to simplify e2e test authoring
5+
// See Playwright docs: https://playwright.dev/docs/pom/
6+
export class CodeServer {
7+
page: Page
8+
9+
constructor(page: Page) {
10+
this.page = page
11+
}
12+
13+
/**
14+
* Navigates to CODE_SERVER_ADDRESS
15+
*/
16+
async navigate() {
17+
await this.page.goto(CODE_SERVER_ADDRESS, { waitUntil: "networkidle" })
18+
}
19+
20+
/**
21+
* Checks if the editor is visible
22+
* and reloads until it is
23+
*/
24+
async reloadUntilEditorIsVisible() {
25+
const editorIsVisible = await this.isEditorVisible()
26+
let reloadCount = 0
27+
28+
// Occassionally code-server timeouts in Firefox
29+
// we're not sure why
30+
// but usually a reload or two fixes it
31+
// TODO@jsjoeio @oxy look into Firefox reconnection/timeout issues
32+
while (!editorIsVisible) {
33+
reloadCount += 1
34+
if (await this.isEditorVisible()) {
35+
console.log(` Editor became visible after ${reloadCount} reloads`)
36+
break
37+
}
38+
// When a reload happens, we want to wait for all resources to be
39+
// loaded completely. Hence why we use that instead of DOMContentLoaded
40+
// Read more: https://thisthat.dev/dom-content-loaded-vs-load/
41+
await this.page.reload({ waitUntil: "load" })
42+
}
43+
}
44+
45+
/**
46+
* Checks if the editor is visible
47+
*/
48+
async isEditorVisible() {
49+
// Make sure the editor actually loaded
50+
// If it's not visible after 5 seconds, something is wrong
51+
await this.page.waitForLoadState("networkidle")
52+
return await this.page.isVisible("div.monaco-workbench", { timeout: 5000 })
53+
}
54+
55+
/**
56+
* Focuses Integrated Terminal
57+
* by going to the Application Menu
58+
* and clicking View > Terminal
59+
*/
60+
async focusTerminal() {
61+
// If the terminal is already visible
62+
// then we can focus it by hitting the keyboard shortcut
63+
const isTerminalVisible = await this.page.isVisible("#terminal")
64+
if (isTerminalVisible) {
65+
await this.page.keyboard.press(`Control+Backquote`)
66+
// Wait for terminal to receive focus
67+
await this.page.waitForSelector("div.terminal.xterm.focus")
68+
// Sometimes the terminal reloads
69+
// which is why we wait for it twice
70+
await this.page.waitForSelector("div.terminal.xterm.focus")
71+
return
72+
}
73+
// Open using the manu
74+
// Click [aria-label="Application Menu"] div[role="none"]
75+
await this.page.click('[aria-label="Application Menu"] div[role="none"]')
76+
77+
// Click text=View
78+
await this.page.hover("text=View")
79+
await this.page.click("text=View")
80+
81+
// Click text=Terminal
82+
await this.page.hover("text=Terminal")
83+
await this.page.click("text=Terminal")
84+
85+
// Wait for terminal to receive focus
86+
// Sometimes the terminal reloads once or twice
87+
// which is why we wait for it to have the focus class
88+
await this.page.waitForSelector("div.terminal.xterm.focus")
89+
// Sometimes the terminal reloads
90+
// which is why we wait for it twice
91+
await this.page.waitForSelector("div.terminal.xterm.focus")
92+
}
93+
94+
/**
95+
* Navigates to CODE_SERVER_ADDRESS
96+
* and reloads until the editor is visible
97+
*
98+
* Helpful for running before tests
99+
*/
100+
async setup() {
101+
await this.navigate()
102+
await this.reloadUntilEditorIsVisible()
103+
}
104+
}

0 commit comments

Comments
 (0)