Merge pull request #3590 from mxschmitt/chore/upgrade-to-latest-playwright
chore: upgrade to Playwright 1.12 with its new test-runner
This commit is contained in:
commit
4bb7a8ddb9
@ -6,7 +6,7 @@ main() {
|
|||||||
cd test
|
cd test
|
||||||
# We set these environment variables because they're used in the e2e tests
|
# We set these environment variables because they're used in the e2e tests
|
||||||
# they don't have to be these values, but these are the defaults
|
# they don't have to be these values, but these are the defaults
|
||||||
PASSWORD=e45432jklfdsab CODE_SERVER_ADDRESS=http://localhost:8080 yarn folio --config=config.ts --reporter=list "$@"
|
PASSWORD=e45432jklfdsab CODE_SERVER_ADDRESS=http://localhost:8080 yarn playwright test "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
@ -165,8 +165,7 @@ export const isHashLegacyMatch = (password: string, hashPassword: string) => {
|
|||||||
return safeCompare(hashedWithLegacy, hashPassword)
|
return safeCompare(hashedWithLegacy, hashPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordMethods = ["SHA256", "ARGON2", "PLAIN_TEXT"] as const
|
export type PasswordMethod = "SHA256" | "ARGON2" | "PLAIN_TEXT"
|
||||||
export type PasswordMethod = typeof passwordMethods[number]
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to determine the password method.
|
* Used to determine the password method.
|
||||||
@ -413,7 +412,7 @@ export const isObject = <T extends object>(obj: T): obj is T => {
|
|||||||
* we don't have to set up a `vs` alias to be able to import with types (since
|
* we don't have to set up a `vs` alias to be able to import with types (since
|
||||||
* the alternative is to directly import from `out`).
|
* the alternative is to directly import from `out`).
|
||||||
*/
|
*/
|
||||||
const enum CharCode {
|
enum CharCode {
|
||||||
Slash = 47,
|
Slash = 47,
|
||||||
A = 65,
|
A = 65,
|
||||||
Z = 90,
|
Z = 90,
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
import {
|
|
||||||
ChromiumEnv,
|
|
||||||
FirefoxEnv,
|
|
||||||
WebKitEnv,
|
|
||||||
test,
|
|
||||||
setConfig,
|
|
||||||
PlaywrightOptions,
|
|
||||||
Config,
|
|
||||||
globalSetup,
|
|
||||||
} from "@playwright/test"
|
|
||||||
import * as argon2 from "argon2"
|
|
||||||
import path from "path"
|
|
||||||
import { PASSWORD } from "./utils/constants"
|
|
||||||
import * as wtfnode from "./utils/wtfnode"
|
|
||||||
|
|
||||||
// Playwright doesn't like that ../src/node/util has an enum in it
|
|
||||||
// so I had to copy hash in separately
|
|
||||||
const hash = async (str: string): Promise<string> => {
|
|
||||||
return await argon2.hash(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookieToStore = {
|
|
||||||
sameSite: "Lax" as const,
|
|
||||||
name: "key",
|
|
||||||
value: "",
|
|
||||||
domain: "localhost",
|
|
||||||
path: "/",
|
|
||||||
expires: -1,
|
|
||||||
httpOnly: false,
|
|
||||||
secure: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
globalSetup(async () => {
|
|
||||||
console.log("\n🚨 Running globalSetup for playwright end-to-end tests")
|
|
||||||
console.log("👋 Please hang tight...")
|
|
||||||
|
|
||||||
if (process.env.WTF_NODE) {
|
|
||||||
wtfnode.setup()
|
|
||||||
}
|
|
||||||
|
|
||||||
cookieToStore.value = await hash(PASSWORD)
|
|
||||||
|
|
||||||
const storage = {
|
|
||||||
cookies: [cookieToStore],
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save storage state and store as an env variable
|
|
||||||
// More info: https://playwright.dev/docs/auth?_highlight=authe#reuse-authentication-state
|
|
||||||
process.env.STORAGE = JSON.stringify(storage)
|
|
||||||
console.log("✅ globalSetup is now complete.")
|
|
||||||
})
|
|
||||||
|
|
||||||
const config: Config = {
|
|
||||||
testDir: path.join(__dirname, "e2e"), // Search for tests in this directory.
|
|
||||||
timeout: 60000, // Each test is given 60 seconds.
|
|
||||||
retries: 3, // Retry failing tests 2 times
|
|
||||||
workers: 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.env.CI) {
|
|
||||||
// In CI, retry failing tests 2 times
|
|
||||||
// in the event of flakiness
|
|
||||||
config.retries = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfig(config)
|
|
||||||
|
|
||||||
const options: PlaywrightOptions = {
|
|
||||||
headless: true, // Run tests in headless browsers.
|
|
||||||
video: "on",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run tests in three browsers.
|
|
||||||
test.runWith(new ChromiumEnv(options), { tag: "chromium" })
|
|
||||||
test.runWith(new FirefoxEnv(options), { tag: "firefox" })
|
|
||||||
test.runWith(new WebKitEnv(options), { tag: "webkit" })
|
|
12
test/e2e/baseFixture.ts
Normal file
12
test/e2e/baseFixture.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { test as base } from "@playwright/test"
|
||||||
|
import { CodeServer } from "./models/CodeServer"
|
||||||
|
|
||||||
|
export const test = base.extend<{ codeServerPage: CodeServer }>({
|
||||||
|
codeServerPage: async ({ page }, use) => {
|
||||||
|
const codeServer = new CodeServer(page)
|
||||||
|
await codeServer.navigate()
|
||||||
|
await use(codeServer)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const expect = test.expect
|
@ -1,22 +1,14 @@
|
|||||||
import { test, expect } from "@playwright/test"
|
import { test, expect } from "./baseFixture"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
|
||||||
|
|
||||||
// This is a "gut-check" test to make sure playwright is working as expected
|
// This is a "gut-check" test to make sure playwright is working as expected
|
||||||
test.describe("browser", () => {
|
test.describe("browser", () => {
|
||||||
let codeServer: CodeServer
|
test("browser should display correct userAgent", async ({ codeServerPage, browserName }) => {
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
codeServer = new CodeServer(page)
|
|
||||||
await codeServer.navigate()
|
|
||||||
})
|
|
||||||
|
|
||||||
test("browser should display correct userAgent", async ({ page, browserName }) => {
|
|
||||||
const displayNames = {
|
const displayNames = {
|
||||||
chromium: "Chrome",
|
chromium: "Chrome",
|
||||||
firefox: "Firefox",
|
firefox: "Firefox",
|
||||||
webkit: "Safari",
|
webkit: "Safari",
|
||||||
}
|
}
|
||||||
const userAgent = await page.evaluate("navigator.userAgent")
|
const userAgent = await codeServerPage.page.evaluate("navigator.userAgent")
|
||||||
|
|
||||||
expect(userAgent).toContain(displayNames[browserName])
|
expect(userAgent).toContain(displayNames[browserName])
|
||||||
})
|
})
|
||||||
|
@ -1,49 +1,32 @@
|
|||||||
import { test, expect } from "@playwright/test"
|
import { CODE_SERVER_ADDRESS, storageState } from "../utils/constants"
|
||||||
import { CODE_SERVER_ADDRESS, STORAGE } from "../utils/constants"
|
import { test, expect } from "./baseFixture"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
|
||||||
|
|
||||||
test.describe("CodeServer", () => {
|
test.describe("CodeServer", () => {
|
||||||
// Create a new context with the saved storage state
|
test.use({
|
||||||
// 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,
|
storageState,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
codeServer = new CodeServer(page)
|
|
||||||
await codeServer.setup()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test(`should navigate to ${CODE_SERVER_ADDRESS}`, options, async ({ page }) => {
|
test(`should navigate to ${CODE_SERVER_ADDRESS}`, async ({ codeServerPage }) => {
|
||||||
// We navigate codeServer before each test
|
// We navigate codeServer before each test
|
||||||
// and we start the test with a storage state
|
// and we start the test with a storage state
|
||||||
// which means we should be logged in
|
// which means we should be logged in
|
||||||
// so it should be on the address
|
// so it should be on the address
|
||||||
const url = page.url()
|
const url = codeServerPage.page.url()
|
||||||
// We use match because there may be a / at the end
|
// 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/
|
// 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)
|
expect(url).toMatch(CODE_SERVER_ADDRESS)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should always see the code-server editor", options, async ({ page }) => {
|
test("should always see the code-server editor", async ({ codeServerPage }) => {
|
||||||
expect(await codeServer.isEditorVisible()).toBe(true)
|
expect(await codeServerPage.isEditorVisible()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should always have a connection", options, async ({ page }) => {
|
test("should always have a connection", async ({ codeServerPage }) => {
|
||||||
expect(await codeServer.isConnected()).toBe(true)
|
expect(await codeServerPage.isConnected()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should show the Integrated Terminal", options, async ({ page }) => {
|
test("should show the Integrated Terminal", async ({ codeServerPage }) => {
|
||||||
await codeServer.focusTerminal()
|
await codeServerPage.focusTerminal()
|
||||||
expect(await page.isVisible("#terminal")).toBe(true)
|
expect(await codeServerPage.page.isVisible("#terminal")).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,30 +1,15 @@
|
|||||||
import { test, expect } from "@playwright/test"
|
import { storageState } from "../utils/constants"
|
||||||
import { STORAGE } from "../utils/constants"
|
import { test, expect } from "./baseFixture"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
|
||||||
|
|
||||||
// This test is to make sure the globalSetup works as expected
|
// This test is to make sure the globalSetup works as expected
|
||||||
// meaning globalSetup ran and stored the storageState in STORAGE
|
// meaning globalSetup ran and stored the storageState
|
||||||
test.describe("globalSetup", () => {
|
test.describe("globalSetup", () => {
|
||||||
// Create a new context with the saved storage state
|
test.use({
|
||||||
// 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,
|
storageState,
|
||||||
}
|
|
||||||
}
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
codeServer = new CodeServer(page)
|
|
||||||
await codeServer.setup()
|
|
||||||
})
|
})
|
||||||
test("should keep us logged in using the storageState", options, async ({ page }) => {
|
|
||||||
|
test("should keep us logged in using the storageState", async ({ codeServerPage }) => {
|
||||||
// Make sure the editor actually loaded
|
// Make sure the editor actually loaded
|
||||||
expect(await codeServer.isEditorVisible()).toBe(true)
|
expect(await codeServerPage.isEditorVisible()).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,76 +1,67 @@
|
|||||||
import { test, expect } from "@playwright/test"
|
|
||||||
import { PASSWORD } from "../utils/constants"
|
import { PASSWORD } from "../utils/constants"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
import { test, expect } from "./baseFixture"
|
||||||
|
|
||||||
test.describe("login", () => {
|
test.describe("login", () => {
|
||||||
// Reset the browser so no cookies are persisted
|
// Reset the browser so no cookies are persisted
|
||||||
// by emptying the storageState
|
// by emptying the storageState
|
||||||
const options = {
|
test.use({
|
||||||
contextOptions: {
|
|
||||||
storageState: {},
|
storageState: {},
|
||||||
},
|
|
||||||
}
|
|
||||||
let codeServer: CodeServer
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
codeServer = new CodeServer(page)
|
|
||||||
await codeServer.navigate()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should see the login page", options, async ({ page }) => {
|
test("should see the login page", async ({ codeServerPage }) => {
|
||||||
// It should send us to the login page
|
// It should send us to the login page
|
||||||
expect(await page.title()).toBe("code-server login")
|
expect(await codeServerPage.page.title()).toBe("code-server login")
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should be able to login", options, async ({ page }) => {
|
test("should be able to login", async ({ codeServerPage }) => {
|
||||||
// Type in password
|
// Type in password
|
||||||
await page.fill(".password", PASSWORD)
|
await codeServerPage.page.fill(".password", PASSWORD)
|
||||||
// Click the submit button and login
|
// Click the submit button and login
|
||||||
await page.click(".submit")
|
await codeServerPage.page.click(".submit")
|
||||||
await page.waitForLoadState("networkidle")
|
await codeServerPage.page.waitForLoadState("networkidle")
|
||||||
// We do this because occassionally code-server doesn't load on Firefox
|
// We do this because occassionally code-server doesn't load on Firefox
|
||||||
// but loads if you reload once or twice
|
// but loads if you reload once or twice
|
||||||
await codeServer.reloadUntilEditorIsReady()
|
await codeServerPage.reloadUntilEditorIsReady()
|
||||||
// Make sure the editor actually loaded
|
// Make sure the editor actually loaded
|
||||||
expect(await codeServer.isEditorVisible()).toBe(true)
|
expect(await codeServerPage.isEditorVisible()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should see an error message for missing password", options, async ({ page }) => {
|
test("should see an error message for missing password", async ({ codeServerPage }) => {
|
||||||
// Skip entering password
|
// Skip entering password
|
||||||
// Click the submit button and login
|
// Click the submit button and login
|
||||||
await page.click(".submit")
|
await codeServerPage.page.click(".submit")
|
||||||
await page.waitForLoadState("networkidle")
|
await codeServerPage.page.waitForLoadState("networkidle")
|
||||||
expect(await page.isVisible("text=Missing password"))
|
expect(await codeServerPage.page.isVisible("text=Missing password"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should see an error message for incorrect password", options, async ({ page }) => {
|
test("should see an error message for incorrect password", async ({ codeServerPage }) => {
|
||||||
// Type in password
|
// Type in password
|
||||||
await page.fill(".password", "password123")
|
await codeServerPage.page.fill(".password", "password123")
|
||||||
// Click the submit button and login
|
// Click the submit button and login
|
||||||
await page.click(".submit")
|
await codeServerPage.page.click(".submit")
|
||||||
await page.waitForLoadState("networkidle")
|
await codeServerPage.page.waitForLoadState("networkidle")
|
||||||
expect(await page.isVisible("text=Incorrect password"))
|
expect(await codeServerPage.page.isVisible("text=Incorrect password"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should hit the rate limiter for too many unsuccessful logins", options, async ({ page }) => {
|
test("should hit the rate limiter for too many unsuccessful logins", async ({ codeServerPage }) => {
|
||||||
// Type in password
|
// Type in password
|
||||||
await page.fill(".password", "password123")
|
await codeServerPage.page.fill(".password", "password123")
|
||||||
// Click the submit button and login
|
// Click the submit button and login
|
||||||
// The current RateLimiter allows 2 logins per minute plus
|
// The current RateLimiter allows 2 logins per minute plus
|
||||||
// 12 logins per hour for a total of 14
|
// 12 logins per hour for a total of 14
|
||||||
// See: src/node/routes/login.ts
|
// See: src/node/routes/login.ts
|
||||||
for (let i = 1; i <= 14; i++) {
|
for (let i = 1; i <= 14; i++) {
|
||||||
await page.click(".submit")
|
await codeServerPage.page.click(".submit")
|
||||||
await page.waitForLoadState("networkidle")
|
await codeServerPage.page.waitForLoadState("networkidle")
|
||||||
// We double-check that the correct error message shows
|
// We double-check that the correct error message shows
|
||||||
// which should be for incorrect password
|
// which should be for incorrect password
|
||||||
expect(await page.isVisible("text=Incorrect password"))
|
expect(await codeServerPage.page.isVisible("text=Incorrect password"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// The 15th should fail for a different reason:
|
// The 15th should fail for a different reason:
|
||||||
// login rate
|
// login rate
|
||||||
await page.click(".submit")
|
await codeServerPage.page.click(".submit")
|
||||||
await page.waitForLoadState("networkidle")
|
await codeServerPage.page.waitForLoadState("networkidle")
|
||||||
expect(await page.isVisible("text=Login rate limited!"))
|
expect(await codeServerPage.page.isVisible("text=Login rate limited!"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -1,53 +1,44 @@
|
|||||||
import { test, expect } from "@playwright/test"
|
|
||||||
import { CODE_SERVER_ADDRESS, PASSWORD } from "../utils/constants"
|
import { CODE_SERVER_ADDRESS, PASSWORD } from "../utils/constants"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
import { test, expect } from "./baseFixture"
|
||||||
|
|
||||||
test.describe("logout", () => {
|
test.describe("logout", () => {
|
||||||
// Reset the browser so no cookies are persisted
|
// Reset the browser so no cookies are persisted
|
||||||
// by emptying the storageState
|
// by emptying the storageState
|
||||||
const options = {
|
test.use({
|
||||||
contextOptions: {
|
|
||||||
storageState: {},
|
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 }) => {
|
test("should be able login and logout", async ({ codeServerPage }) => {
|
||||||
// Type in password
|
// Type in password
|
||||||
await page.fill(".password", PASSWORD)
|
await codeServerPage.page.fill(".password", PASSWORD)
|
||||||
// Click the submit button and login
|
// Click the submit button and login
|
||||||
await page.click(".submit")
|
await codeServerPage.page.click(".submit")
|
||||||
await page.waitForLoadState("networkidle")
|
await codeServerPage.page.waitForLoadState("networkidle")
|
||||||
// We do this because occassionally code-server doesn't load on Firefox
|
// We do this because occassionally code-server doesn't load on Firefox
|
||||||
// but loads if you reload once or twice
|
// but loads if you reload once or twice
|
||||||
await codeServer.reloadUntilEditorIsReady()
|
await codeServerPage.reloadUntilEditorIsReady()
|
||||||
// Make sure the editor actually loaded
|
// Make sure the editor actually loaded
|
||||||
expect(await codeServer.isEditorVisible()).toBe(true)
|
expect(await codeServerPage.isEditorVisible()).toBe(true)
|
||||||
|
|
||||||
// Click the Application menu
|
// Click the Application menu
|
||||||
await page.click("[aria-label='Application Menu']")
|
await codeServerPage.page.click("[aria-label='Application Menu']")
|
||||||
|
|
||||||
// See the Log out button
|
// See the Log out button
|
||||||
const logoutButton = "a.action-menu-item span[aria-label='Log out']"
|
const logoutButton = "a.action-menu-item span[aria-label='Log out']"
|
||||||
expect(await page.isVisible(logoutButton)).toBe(true)
|
expect(await codeServerPage.page.isVisible(logoutButton)).toBe(true)
|
||||||
|
|
||||||
await page.hover(logoutButton)
|
await codeServerPage.page.hover(logoutButton)
|
||||||
// TODO(@jsjoeio)
|
// TODO(@jsjoeio)
|
||||||
// Look into how we're attaching the handlers for the logout feature
|
// Look into how we're attaching the handlers for the logout feature
|
||||||
// We need to see how it's done upstream and add logging to the
|
// We need to see how it's done upstream and add logging to the
|
||||||
// handlers themselves.
|
// handlers themselves.
|
||||||
// They may be attached too slowly, hence why we need this timeout
|
// They may be attached too slowly, hence why we need this timeout
|
||||||
await page.waitForTimeout(2000)
|
await codeServerPage.page.waitForTimeout(2000)
|
||||||
|
|
||||||
// Recommended by Playwright for async navigation
|
// Recommended by Playwright for async navigation
|
||||||
// https://github.com/microsoft/playwright/issues/1987#issuecomment-620182151
|
// https://github.com/microsoft/playwright/issues/1987#issuecomment-620182151
|
||||||
await Promise.all([page.waitForNavigation(), page.click(logoutButton)])
|
await Promise.all([codeServerPage.page.waitForNavigation(), codeServerPage.page.click(logoutButton)])
|
||||||
const currentUrl = page.url()
|
const currentUrl = codeServerPage.page.url()
|
||||||
expect(currentUrl).toBe(`${CODE_SERVER_ADDRESS}/login`)
|
expect(currentUrl).toBe(`${CODE_SERVER_ADDRESS}/login`)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -41,7 +41,7 @@ export class CodeServer {
|
|||||||
// Give it an extra second just in case it's feeling extra slow
|
// Give it an extra second just in case it's feeling extra slow
|
||||||
await this.page.waitForTimeout(1000)
|
await this.page.waitForTimeout(1000)
|
||||||
reloadCount += 1
|
reloadCount += 1
|
||||||
if ((await this.isEditorVisible()) && (await this.isConnected)) {
|
if ((await this.isEditorVisible()) && (await this.isConnected())) {
|
||||||
console.log(` Editor became ready after ${reloadCount} reloads`)
|
console.log(` Editor became ready after ${reloadCount} reloads`)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -54,8 +54,7 @@ export class CodeServer {
|
|||||||
*/
|
*/
|
||||||
async isEditorVisible() {
|
async isEditorVisible() {
|
||||||
// Make sure the editor actually loaded
|
// Make sure the editor actually loaded
|
||||||
// If it's not visible after 5 seconds, something is wrong
|
await this.page.waitForSelector(this.editorSelector)
|
||||||
await this.page.waitForLoadState("networkidle")
|
|
||||||
return await this.page.isVisible(this.editorSelector)
|
return await this.page.isVisible(this.editorSelector)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,46 +1,28 @@
|
|||||||
import { test, expect } from "@playwright/test"
|
import { storageState } from "../utils/constants"
|
||||||
import { STORAGE } from "../utils/constants"
|
import { test, expect } from "./baseFixture"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
|
||||||
|
|
||||||
test.describe("Open Help > About", () => {
|
test.describe("Open Help > About", () => {
|
||||||
// Create a new context with the saved storage state
|
test.use({
|
||||||
// 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,
|
storageState,
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
codeServer = new CodeServer(page)
|
|
||||||
await codeServer.setup()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
test(
|
test("should see a 'Help' then 'About' button in the Application Menu that opens a dialog", async ({
|
||||||
"should see a 'Help' then 'About' button in the Application Menu that opens a dialog",
|
codeServerPage,
|
||||||
options,
|
}) => {
|
||||||
async ({ page }) => {
|
|
||||||
// Open using the manu
|
// Open using the manu
|
||||||
// Click [aria-label="Application Menu"] div[role="none"]
|
// Click [aria-label="Application Menu"] div[role="none"]
|
||||||
await page.click('[aria-label="Application Menu"] div[role="none"]')
|
await codeServerPage.page.click('[aria-label="Application Menu"] div[role="none"]')
|
||||||
|
|
||||||
// Click the Help button
|
// Click the Help button
|
||||||
await page.hover("text=Help")
|
await codeServerPage.page.hover("text=Help")
|
||||||
await page.click("text=Help")
|
await codeServerPage.page.click("text=Help")
|
||||||
|
|
||||||
// Click the About button
|
// Click the About button
|
||||||
await page.hover("text=About")
|
await codeServerPage.page.hover("text=About")
|
||||||
await page.click("text=About")
|
await codeServerPage.page.click("text=About")
|
||||||
|
|
||||||
// Click div[role="dialog"] >> text=code-server
|
// Click div[role="dialog"] >> text=code-server
|
||||||
const element = await page.waitForSelector('div[role="dialog"] >> text=code-server')
|
const element = await codeServerPage.page.waitForSelector('div[role="dialog"] >> text=code-server')
|
||||||
expect(element).not.toBeNull()
|
expect(element).not.toBeNull()
|
||||||
},
|
})
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
@ -1,59 +1,46 @@
|
|||||||
import { expect, test } from "@playwright/test"
|
|
||||||
import * as cp from "child_process"
|
import * as cp from "child_process"
|
||||||
import * as fs from "fs"
|
import * as fs from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import util from "util"
|
import util from "util"
|
||||||
import { STORAGE } from "../utils/constants"
|
import { storageState } from "../utils/constants"
|
||||||
import { tmpdir } from "../utils/helpers"
|
import { tmpdir } from "../utils/helpers"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
import { expect, test } from "./baseFixture"
|
||||||
|
|
||||||
test.describe("Integrated Terminal", () => {
|
test.describe("Integrated Terminal", () => {
|
||||||
// Create a new context with the saved storage state
|
// Create a new context with the saved storage state
|
||||||
// so we don't have to logged in
|
// so we don't have to logged in
|
||||||
const options: any = {}
|
|
||||||
const testFileName = "pipe"
|
const testFileName = "pipe"
|
||||||
const testString = "new string test from e2e test"
|
const testString = "new string test from e2e test"
|
||||||
let codeServer: CodeServer
|
|
||||||
let tmpFolderPath = ""
|
let tmpFolderPath = ""
|
||||||
let tmpFile = ""
|
let tmpFile = ""
|
||||||
|
|
||||||
// TODO@jsjoeio
|
test.use({
|
||||||
// Fix this once https://github.com/microsoft/playwright-test/issues/240
|
|
||||||
// is fixed
|
|
||||||
if (STORAGE) {
|
|
||||||
const storageState = JSON.parse(STORAGE) || {}
|
|
||||||
options.contextOptions = {
|
|
||||||
storageState,
|
storageState,
|
||||||
}
|
})
|
||||||
}
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
tmpFolderPath = await tmpdir("integrated-terminal")
|
tmpFolderPath = await tmpdir("integrated-terminal")
|
||||||
tmpFile = path.join(tmpFolderPath, testFileName)
|
tmpFile = path.join(tmpFolderPath, testFileName)
|
||||||
})
|
})
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
codeServer = new CodeServer(page)
|
|
||||||
await codeServer.setup()
|
|
||||||
})
|
|
||||||
|
|
||||||
test.afterAll(async () => {
|
test.afterAll(async () => {
|
||||||
// Ensure directory was removed
|
// Ensure directory was removed
|
||||||
await fs.promises.rmdir(tmpFolderPath, { recursive: true })
|
await fs.promises.rmdir(tmpFolderPath, { recursive: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should echo a string to a file", options, async ({ page }) => {
|
test("should echo a string to a file", async ({ codeServerPage }) => {
|
||||||
const command = `mkfifo '${tmpFile}' && cat '${tmpFile}'`
|
const command = `mkfifo '${tmpFile}' && cat '${tmpFile}'`
|
||||||
const exec = util.promisify(cp.exec)
|
const exec = util.promisify(cp.exec)
|
||||||
const output = exec(command, { encoding: "utf8" })
|
const output = exec(command, { encoding: "utf8" })
|
||||||
|
|
||||||
// Open terminal and type in value
|
// Open terminal and type in value
|
||||||
await codeServer.focusTerminal()
|
await codeServerPage.focusTerminal()
|
||||||
|
|
||||||
await page.waitForLoadState("load")
|
await codeServerPage.page.waitForLoadState("load")
|
||||||
await page.keyboard.type(`echo ${testString} > ${tmpFile}`)
|
await codeServerPage.page.keyboard.type(`echo ${testString} > ${tmpFile}`)
|
||||||
await page.keyboard.press("Enter")
|
await codeServerPage.page.keyboard.press("Enter")
|
||||||
// It may take a second to process
|
// It may take a second to process
|
||||||
await page.waitForTimeout(1000)
|
await codeServerPage.page.waitForTimeout(1000)
|
||||||
|
|
||||||
const { stdout } = await output
|
const { stdout } = await output
|
||||||
expect(stdout).toMatch(testString)
|
expect(stdout).toMatch(testString)
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"#": "We must put jest in a sub-directory otherwise VS Code somehow picks up the types and generates conflicts with mocha.",
|
"#": "We must put jest in a sub-directory otherwise VS Code somehow picks up the types and generates conflicts with mocha.",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^0.1101.0-alpha2",
|
"@playwright/test": "^1.12.1",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/jsdom": "^16.2.6",
|
"@types/jsdom": "^16.2.6",
|
||||||
"@types/node-fetch": "^2.5.8",
|
"@types/node-fetch": "^2.5.8",
|
||||||
@ -11,11 +11,8 @@
|
|||||||
"jest": "^26.6.3",
|
"jest": "^26.6.3",
|
||||||
"jsdom": "^16.4.0",
|
"jsdom": "^16.4.0",
|
||||||
"node-fetch": "^2.6.1",
|
"node-fetch": "^2.6.1",
|
||||||
"playwright": "^1.11.0-next-alpha-apr-13-2021",
|
"playwright": "^1.12.1",
|
||||||
"supertest": "^6.1.1",
|
"supertest": "^6.1.1",
|
||||||
"ts-jest": "^26.4.4"
|
"ts-jest": "^26.4.4"
|
||||||
},
|
|
||||||
"resolutions": {
|
|
||||||
"@playwright/test/playwright": "^1.11.0-next-alpha-apr-13-2021"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
43
test/playwright.config.ts
Normal file
43
test/playwright.config.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import { PlaywrightTestConfig } from "@playwright/test"
|
||||||
|
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
// Run tests in three browsers.
|
||||||
|
const config: PlaywrightTestConfig = {
|
||||||
|
testDir: path.join(__dirname, "e2e"), // Search for tests in this directory.
|
||||||
|
timeout: 60000, // Each test is given 60 seconds.
|
||||||
|
retries: 3, // Retry failing tests 2 times
|
||||||
|
workers: 1,
|
||||||
|
globalSetup: require.resolve("./utils/globalSetup.ts"),
|
||||||
|
reporter: "list",
|
||||||
|
// Put any shared options on the top level.
|
||||||
|
use: {
|
||||||
|
headless: true, // Run tests in headless browsers.
|
||||||
|
video: "on",
|
||||||
|
},
|
||||||
|
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: "Chromium",
|
||||||
|
use: { browserName: "chromium" },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "Firefox",
|
||||||
|
use: { browserName: "firefox" },
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "WebKit",
|
||||||
|
use: { browserName: "webkit" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.CI) {
|
||||||
|
// In CI, retry failing tests 2 times
|
||||||
|
// in the event of flakiness
|
||||||
|
config.retries = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export default config
|
@ -1,3 +1,3 @@
|
|||||||
export const CODE_SERVER_ADDRESS = process.env.CODE_SERVER_ADDRESS || "http://localhost:8080"
|
export const CODE_SERVER_ADDRESS = process.env.CODE_SERVER_ADDRESS || "http://localhost:8080"
|
||||||
export const PASSWORD = process.env.PASSWORD || "e45432jklfdsab"
|
export const PASSWORD = process.env.PASSWORD || "e45432jklfdsab"
|
||||||
export const STORAGE = process.env.STORAGE || ""
|
export const storageState = JSON.parse(process.env.STORAGE || "{}")
|
||||||
|
@ -6,10 +6,14 @@ import { hash } from "../../src/node/util"
|
|||||||
import { PASSWORD } from "./constants"
|
import { PASSWORD } from "./constants"
|
||||||
import * as wtfnode from "./wtfnode"
|
import * as wtfnode from "./wtfnode"
|
||||||
|
|
||||||
|
export default async function () {
|
||||||
|
console.log("\n🚨 Running Global Setup for Playwright End-to-End Tests")
|
||||||
|
console.log(" Please hang tight...")
|
||||||
|
|
||||||
const cookieToStore = {
|
const cookieToStore = {
|
||||||
sameSite: "Lax" as const,
|
sameSite: "Lax" as const,
|
||||||
name: "key",
|
name: "key",
|
||||||
value: hash(PASSWORD),
|
value: await hash(PASSWORD),
|
||||||
domain: "localhost",
|
domain: "localhost",
|
||||||
path: "/",
|
path: "/",
|
||||||
expires: -1,
|
expires: -1,
|
||||||
@ -17,9 +21,6 @@ const cookieToStore = {
|
|||||||
secure: false,
|
secure: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = async () => {
|
|
||||||
console.log("\n🚨 Running Global Setup for Jest End-to-End Tests")
|
|
||||||
console.log(" Please hang tight...")
|
|
||||||
const browser = await chromium.launch()
|
const browser = await chromium.launch()
|
||||||
const page = await browser.newPage()
|
const page = await browser.newPage()
|
||||||
const storage = await page.context().storageState()
|
const storage = await page.context().storageState()
|
||||||
@ -31,10 +32,9 @@ module.exports = async () => {
|
|||||||
storage.cookies = [cookieToStore]
|
storage.cookies = [cookieToStore]
|
||||||
|
|
||||||
// Save storage state and store as an env variable
|
// Save storage state and store as an env variable
|
||||||
// More info: https://playwright.dev/docs/auth?_highlight=authe#reuse-authentication-state
|
// More info: https://playwright.dev/docs/auth/#reuse-authentication-state
|
||||||
process.env.STORAGE = JSON.stringify(storage)
|
process.env.STORAGE = JSON.stringify(storage)
|
||||||
await page.close()
|
|
||||||
await browser.close()
|
await browser.close()
|
||||||
|
|
||||||
console.log("✅ Global Setup for Jest End-to-End Tests is now complete.")
|
console.log("✅ Global Setup for Playwright End-to-End Tests is now complete.")
|
||||||
}
|
}
|
||||||
|
1054
test/yarn.lock
1054
test/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user