feat(e2e): add support running behind proxy (#5348)
* docs: update maintaining * chore(e2e): add maxFailures to playwright * fix(ci): skip submodule in e2e job We don't need the submodules for the e2e job. This will speed up the checkout step. * feat(ci): add test-e2e-proxy job This adds a new job to CI to run our tests behind Caddy and simulate code-server running against a reverse-proxy. * refactor: make e2e work with reverse proxy This refactors the e2e test in a couple ways: - remove setting cookie in localStorage (instead we pass --auth none) - refactor address() method to account for reverse proxy logic * Update test/e2e/models/CodeServer.ts * Update test/playwright.config.ts * Update test/utils/constants.ts Co-authored-by: Asher <ash@coder.com> * Update test/utils/helpers.ts Co-authored-by: Asher <ash@coder.com> Co-authored-by: Asher <ash@coder.com>
This commit is contained in:
parent
efb5baec83
commit
f178f0400b
90
.github/workflows/ci.yaml
vendored
90
.github/workflows/ci.yaml
vendored
@ -461,7 +461,6 @@ jobs:
|
|||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
|
||||||
|
|
||||||
- name: Install Node.js v16
|
- name: Install Node.js v16
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
@ -491,7 +490,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
||||||
run: yarn --frozen-lockfile
|
run: SKIP_SUBMODULE_DEPS=1 yarn --frozen-lockfile
|
||||||
|
|
||||||
- name: Install Playwright OS dependencies
|
- name: Install Playwright OS dependencies
|
||||||
run: |
|
run: |
|
||||||
@ -511,6 +510,93 @@ jobs:
|
|||||||
- name: Remove release packages and test artifacts
|
- name: Remove release packages and test artifacts
|
||||||
run: rm -rf ./release-packages ./test/test-results
|
run: rm -rf ./release-packages ./test/test-results
|
||||||
|
|
||||||
|
test-e2e-proxy:
|
||||||
|
name: End-to-end tests behind proxy
|
||||||
|
needs: package-linux-amd64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 25
|
||||||
|
env:
|
||||||
|
# Since we build code-server we might as well run tests from the release
|
||||||
|
# since VS Code will load faster due to the bundling.
|
||||||
|
CODE_SERVER_TEST_ENTRY: "./release-packages/code-server-linux-amd64"
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Install Node.js v16
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
|
||||||
|
- name: Fetch dependencies from cache
|
||||||
|
id: cache-yarn
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: "**/node_modules"
|
||||||
|
key: yarn-build-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
yarn-build-
|
||||||
|
|
||||||
|
- name: Download release packages
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: release-packages
|
||||||
|
path: ./release-packages
|
||||||
|
|
||||||
|
- name: Untar code-server release
|
||||||
|
run: |
|
||||||
|
cd release-packages
|
||||||
|
tar -xzf code-server*-linux-amd64.tar.gz
|
||||||
|
mv code-server*-linux-amd64 code-server-linux-amd64
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.cache-yarn.outputs.cache-hit != 'true'
|
||||||
|
run: SKIP_SUBMODULE_DEPS=1 yarn --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Install Playwright OS dependencies
|
||||||
|
run: |
|
||||||
|
./test/node_modules/.bin/playwright install-deps
|
||||||
|
./test/node_modules/.bin/playwright install
|
||||||
|
|
||||||
|
- name: Cache Caddy
|
||||||
|
uses: actions/cache@v2
|
||||||
|
id: caddy-cache
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/caddy
|
||||||
|
key: cache-caddy-2.5.2
|
||||||
|
|
||||||
|
- name: Install Caddy
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
if: steps.caddy-cache.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
gh release download v2.5.2 --repo caddyserver/caddy --pattern "caddy_2.5.2_linux_amd64.tar.gz"
|
||||||
|
mkdir -p ~/.cache/caddy
|
||||||
|
tar -xzf caddy_2.5.2_linux_amd64.tar.gz --directory ~/.cache/caddy
|
||||||
|
|
||||||
|
- name: Start Caddy
|
||||||
|
run: sudo ~/.cache/caddy/caddy start --config ./ci/Caddyfile
|
||||||
|
|
||||||
|
- name: Run end-to-end tests
|
||||||
|
run: yarn test:e2e:proxy
|
||||||
|
|
||||||
|
- name: Stop Caddy
|
||||||
|
if: always()
|
||||||
|
run: sudo ~/.cache/caddy/caddy stop --config ./ci/Caddyfile
|
||||||
|
|
||||||
|
- name: Upload test artifacts
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: failed-test-videos-proxy
|
||||||
|
path: ./test/test-results
|
||||||
|
|
||||||
|
- name: Remove release packages and test artifacts
|
||||||
|
run: rm -rf ./release-packages ./test/test-results
|
||||||
|
|
||||||
trivy-scan-repo:
|
trivy-scan-repo:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # for actions/checkout to fetch code
|
contents: read # for actions/checkout to fetch code
|
||||||
|
15
ci/Caddyfile
Normal file
15
ci/Caddyfile
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
admin localhost:4444
|
||||||
|
}
|
||||||
|
:8000 {
|
||||||
|
@portLocalhost path_regexp port ^/([0-9]+)\/ide
|
||||||
|
handle @portLocalhost {
|
||||||
|
uri strip_prefix {re.port.1}/ide
|
||||||
|
reverse_proxy localhost:{re.port.1}
|
||||||
|
}
|
||||||
|
|
||||||
|
handle {
|
||||||
|
respond "Bad hostname" 400
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -175,7 +175,7 @@ If you're the current release manager, follow these steps:
|
|||||||
1. Bump chart version in `Chart.yaml`.
|
1. Bump chart version in `Chart.yaml`.
|
||||||
1. Summarize the major changes in the release notes and link to the relevant
|
1. Summarize the major changes in the release notes and link to the relevant
|
||||||
issues.
|
issues.
|
||||||
1. Change the @ to target the version branch. Example: `v3.9.0 @ Target: v3.9.0`
|
1. Change the @ to target the version branch. Example: `v3.9.0 @ Target: release/v3.9.0`
|
||||||
1. Wait for the `npm-package`, `release-packages` and `release-images` artifacts
|
1. Wait for the `npm-package`, `release-packages` and `release-images` artifacts
|
||||||
to build.
|
to build.
|
||||||
1. Run `yarn release:github-assets` to download the `release-packages` artifact.
|
1. Run `yarn release:github-assets` to download the `release-packages` artifact.
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"release:github-assets": "./ci/build/release-github-assets.sh",
|
"release:github-assets": "./ci/build/release-github-assets.sh",
|
||||||
"release:prep": "./ci/build/release-prep.sh",
|
"release:prep": "./ci/build/release-prep.sh",
|
||||||
"test:e2e": "VSCODE_IPC_HOOK_CLI= ./ci/dev/test-e2e.sh",
|
"test:e2e": "VSCODE_IPC_HOOK_CLI= ./ci/dev/test-e2e.sh",
|
||||||
|
"test:e2e:proxy": "USE_PROXY=1 ./ci/dev/test-e2e.sh",
|
||||||
"test:unit": "./ci/dev/test-unit.sh --forceExit --detectOpenHandles",
|
"test:unit": "./ci/dev/test-unit.sh --forceExit --detectOpenHandles",
|
||||||
"test:integration": "./ci/dev/test-integration.sh",
|
"test:integration": "./ci/dev/test-integration.sh",
|
||||||
"test:scripts": "./ci/dev/test-scripts.sh",
|
"test:scripts": "./ci/dev/test-scripts.sh",
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { field, logger } from "@coder/logger"
|
|
||||||
import { test as base } from "@playwright/test"
|
import { test as base } from "@playwright/test"
|
||||||
import { CodeServer, CodeServerPage } from "./models/CodeServer"
|
import { CodeServer, CodeServerPage } from "./models/CodeServer"
|
||||||
|
|
||||||
@ -11,14 +10,13 @@ import { CodeServer, CodeServerPage } from "./models/CodeServer"
|
|||||||
*/
|
*/
|
||||||
export const describe = (
|
export const describe = (
|
||||||
name: string,
|
name: string,
|
||||||
includeCredentials: boolean,
|
|
||||||
codeServerArgs: string[],
|
codeServerArgs: string[],
|
||||||
codeServerEnv: NodeJS.ProcessEnv,
|
codeServerEnv: NodeJS.ProcessEnv,
|
||||||
fn: (codeServer: CodeServer) => void,
|
fn: (codeServer: CodeServer) => void,
|
||||||
) => {
|
) => {
|
||||||
test.describe(name, () => {
|
test.describe(name, () => {
|
||||||
// This will spawn on demand so nothing is necessary on before.
|
// This will spawn on demand so nothing is necessary on before.
|
||||||
const codeServer = new CodeServer(name, codeServerArgs, codeServerEnv)
|
const codeServer = new CodeServer(name, codeServerArgs, codeServerEnv, undefined)
|
||||||
|
|
||||||
// Kill code-server after the suite has ended. This may happen even without
|
// Kill code-server after the suite has ended. This may happen even without
|
||||||
// doing it explicitly but it seems prudent to be sure.
|
// doing it explicitly but it seems prudent to be sure.
|
||||||
@ -26,22 +24,10 @@ export const describe = (
|
|||||||
await codeServer.close()
|
await codeServer.close()
|
||||||
})
|
})
|
||||||
|
|
||||||
const storageState = JSON.parse(process.env.STORAGE || "{}")
|
|
||||||
|
|
||||||
// Sanity check to ensure the cookie is set.
|
|
||||||
const cookies = storageState?.cookies
|
|
||||||
if (includeCredentials && (!cookies || cookies.length !== 1 || !!cookies[0].key)) {
|
|
||||||
logger.error("no cookies", field("storage", JSON.stringify(cookies)))
|
|
||||||
throw new Error("no credentials to include")
|
|
||||||
}
|
|
||||||
|
|
||||||
test.use({
|
test.use({
|
||||||
// Makes `codeServer` and `authenticated` available to the extend call
|
// Makes `codeServer` and `authenticated` available to the extend call
|
||||||
// below.
|
// below.
|
||||||
codeServer,
|
codeServer,
|
||||||
authenticated: includeCredentials,
|
|
||||||
// This provides a cookie that authenticates with code-server.
|
|
||||||
storageState: includeCredentials ? storageState : {},
|
|
||||||
// NOTE@jsjoeio some tests use --cert which uses a self-signed certificate
|
// NOTE@jsjoeio some tests use --cert which uses a self-signed certificate
|
||||||
// without this option, those tests will fail.
|
// without this option, those tests will fail.
|
||||||
ignoreHTTPSErrors: true,
|
ignoreHTTPSErrors: true,
|
||||||
@ -52,7 +38,6 @@ export const describe = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface TestFixtures {
|
interface TestFixtures {
|
||||||
authenticated: boolean
|
|
||||||
codeServer: CodeServer
|
codeServer: CodeServer
|
||||||
codeServerPage: CodeServerPage
|
codeServerPage: CodeServerPage
|
||||||
}
|
}
|
||||||
@ -62,15 +47,14 @@ interface TestFixtures {
|
|||||||
* ready.
|
* ready.
|
||||||
*/
|
*/
|
||||||
export const test = base.extend<TestFixtures>({
|
export const test = base.extend<TestFixtures>({
|
||||||
authenticated: false,
|
|
||||||
codeServer: undefined, // No default; should be provided through `test.use`.
|
codeServer: undefined, // No default; should be provided through `test.use`.
|
||||||
codeServerPage: async ({ authenticated, codeServer, page }, use) => {
|
codeServerPage: async ({ codeServer, page }, use) => {
|
||||||
// It's possible code-server might prevent navigation because of unsaved
|
// It's possible code-server might prevent navigation because of unsaved
|
||||||
// changes (seems to happen based on timing even if no changes have been
|
// changes (seems to happen based on timing even if no changes have been
|
||||||
// made too). In these cases just accept.
|
// made too). In these cases just accept.
|
||||||
page.on("dialog", (d) => d.accept())
|
page.on("dialog", (d) => d.accept())
|
||||||
|
|
||||||
const codeServerPage = new CodeServerPage(codeServer, page, authenticated)
|
const codeServerPage = new CodeServerPage(codeServer, page)
|
||||||
await codeServerPage.navigate()
|
await codeServerPage.navigate()
|
||||||
await use(codeServerPage)
|
await use(codeServerPage)
|
||||||
},
|
},
|
||||||
|
@ -3,10 +3,11 @@ import { promises as fs } from "fs"
|
|||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import * as util from "util"
|
import * as util from "util"
|
||||||
|
import { getMaybeProxiedCodeServer } from "../utils/helpers"
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
import { CodeServer } from "./models/CodeServer"
|
import { CodeServer } from "./models/CodeServer"
|
||||||
|
|
||||||
describe("code-server", true, [], {}, () => {
|
describe("code-server", [], {}, () => {
|
||||||
// TODO@asher: Generalize this? Could be nice if we were to ever need
|
// TODO@asher: Generalize this? Could be nice if we were to ever need
|
||||||
// multiple migration tests in other suites.
|
// multiple migration tests in other suites.
|
||||||
const instances = new Map<string, CodeServer>()
|
const instances = new Map<string, CodeServer>()
|
||||||
@ -48,7 +49,8 @@ describe("code-server", true, [], {}, () => {
|
|||||||
const url = codeServerPage.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(await codeServerPage.address())
|
const address = await getMaybeProxiedCodeServer(codeServerPage)
|
||||||
|
expect(url).toMatch(address)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should always see the code-server editor", async ({ codeServerPage }) => {
|
test("should always see the code-server editor", async ({ codeServerPage }) => {
|
||||||
@ -70,7 +72,9 @@ describe("code-server", true, [], {}, () => {
|
|||||||
test("should migrate state to avoid collisions", async ({ codeServerPage }) => {
|
test("should migrate state to avoid collisions", async ({ codeServerPage }) => {
|
||||||
// This can take a very long time in development because of how long pages
|
// This can take a very long time in development because of how long pages
|
||||||
// take to load and we are doing a lot of that here.
|
// take to load and we are doing a lot of that here.
|
||||||
|
if (process.env.VSCODE_DEV === "1") {
|
||||||
test.slow()
|
test.slow()
|
||||||
|
}
|
||||||
|
|
||||||
const dir = await codeServerPage.workspaceDir
|
const dir = await codeServerPage.workspaceDir
|
||||||
const files = [path.join(dir, "foo"), path.join(dir, "bar")]
|
const files = [path.join(dir, "foo"), path.join(dir, "bar")]
|
||||||
@ -90,6 +94,7 @@ describe("code-server", true, [], {}, () => {
|
|||||||
// domain and can write to the same database.
|
// domain and can write to the same database.
|
||||||
const cs = await spawn("4.0.2", dir)
|
const cs = await spawn("4.0.2", dir)
|
||||||
const address = new URL(await cs.address())
|
const address = new URL(await cs.address())
|
||||||
|
|
||||||
await codeServerPage.navigate("/proxy/" + address.port + "/")
|
await codeServerPage.navigate("/proxy/" + address.port + "/")
|
||||||
await codeServerPage.openFile(files[1])
|
await codeServerPage.openFile(files[1])
|
||||||
expect(await codeServerPage.tabIsVisible(files[0])).toBe(false)
|
expect(await codeServerPage.tabIsVisible(files[0])).toBe(false)
|
||||||
|
@ -3,7 +3,7 @@ import * as path from "path"
|
|||||||
import { clean } from "../utils/helpers"
|
import { clean } from "../utils/helpers"
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
describe("Downloads (enabled)", true, [], {}, async () => {
|
describe("Downloads (enabled)", [], {}, async () => {
|
||||||
const testName = "downloads-enabled"
|
const testName = "downloads-enabled"
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
await clean(testName)
|
await clean(testName)
|
||||||
@ -25,7 +25,7 @@ describe("Downloads (enabled)", true, [], {}, async () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Downloads (disabled)", true, ["--disable-file-downloads"], {}, async () => {
|
describe("Downloads (disabled)", ["--disable-file-downloads"], {}, async () => {
|
||||||
const testName = "downloads-disabled"
|
const testName = "downloads-disabled"
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
await clean(testName)
|
await clean(testName)
|
||||||
|
@ -1,23 +1,36 @@
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { describe, test } from "./baseFixture"
|
import { test as base } from "@playwright/test"
|
||||||
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
import { getMaybeProxiedCodeServer } from "../utils/helpers"
|
||||||
|
|
||||||
function runTestExtensionTests() {
|
function runTestExtensionTests() {
|
||||||
// This will only work if the test extension is loaded into code-server.
|
// This will only work if the test extension is loaded into code-server.
|
||||||
test("should have access to VSCODE_PROXY_URI", async ({ codeServerPage }) => {
|
test("should have access to VSCODE_PROXY_URI", async ({ codeServerPage }) => {
|
||||||
const address = await codeServerPage.address()
|
const address = await getMaybeProxiedCodeServer(codeServerPage)
|
||||||
|
|
||||||
await codeServerPage.executeCommandViaMenus("code-server: Get proxy URI")
|
await codeServerPage.executeCommandViaMenus("code-server: Get proxy URI")
|
||||||
|
|
||||||
await codeServerPage.page.waitForSelector(`text=${address}/proxy/{{port}}`)
|
const text = await codeServerPage.page.locator(".notification-list-item-message").textContent()
|
||||||
|
// Remove end slash in address
|
||||||
|
const normalizedAddress = address.replace(/\/+$/, "")
|
||||||
|
expect(text).toBe(`${normalizedAddress}/proxy/{{port}}`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const flags = ["--extensions-dir", path.join(__dirname, "./extensions")]
|
const flags = ["--extensions-dir", path.join(__dirname, "./extensions")]
|
||||||
|
|
||||||
describe("Extensions", true, flags, {}, () => {
|
describe("Extensions", flags, {}, () => {
|
||||||
runTestExtensionTests()
|
runTestExtensionTests()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("Extensions with --cert", true, [...flags, "--cert"], {}, () => {
|
if (process.env.USE_PROXY !== "1") {
|
||||||
|
describe("Extensions with --cert", [...flags, "--cert"], {}, () => {
|
||||||
runTestExtensionTests()
|
runTestExtensionTests()
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
base.describe("Extensions with --cert", () => {
|
||||||
|
base.skip("skipped because USE_PROXY is set", () => {
|
||||||
|
// Playwright will not show this without a function.
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { test as base } from "@playwright/test"
|
|||||||
import { describe, expect, test } from "./baseFixture"
|
import { describe, expect, test } from "./baseFixture"
|
||||||
|
|
||||||
if (process.env.GITHUB_TOKEN) {
|
if (process.env.GITHUB_TOKEN) {
|
||||||
describe("GitHub token", true, [], {}, () => {
|
describe("GitHub token", [], {}, () => {
|
||||||
test("should be logged in to pull requests extension", async ({ codeServerPage }) => {
|
test("should be logged in to pull requests extension", async ({ codeServerPage }) => {
|
||||||
await codeServerPage.exec("git init")
|
await codeServerPage.exec("git init")
|
||||||
await codeServerPage.exec("git remote add origin https://github.com/coder/code-server")
|
await codeServerPage.exec("git remote add origin https://github.com/coder/code-server")
|
||||||
@ -16,7 +16,7 @@ if (process.env.GITHUB_TOKEN) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("No GitHub token", true, [], { GITHUB_TOKEN: "" }, () => {
|
describe("No GitHub token", [], { GITHUB_TOKEN: "" }, () => {
|
||||||
test("should not be logged in to pull requests extension", async ({ codeServerPage }) => {
|
test("should not be logged in to pull requests extension", async ({ codeServerPage }) => {
|
||||||
await codeServerPage.exec("git init")
|
await codeServerPage.exec("git init")
|
||||||
await codeServerPage.exec("git remote add origin https://github.com/coder/code-server")
|
await codeServerPage.exec("git remote add origin https://github.com/coder/code-server")
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { describe, test, expect } from "./baseFixture"
|
|
||||||
|
|
||||||
// This test is to make sure the globalSetup works as expected
|
|
||||||
// meaning globalSetup ran and stored the storageState
|
|
||||||
describe("globalSetup", true, [], {}, () => {
|
|
||||||
test("should keep us logged in using the storageState", async ({ codeServerPage }) => {
|
|
||||||
// Make sure the editor actually loaded
|
|
||||||
expect(await codeServerPage.isEditorVisible()).toBe(true)
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,7 +1,7 @@
|
|||||||
import { PASSWORD } from "../utils/constants"
|
import { PASSWORD } from "../utils/constants"
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
describe("login", false, [], {}, () => {
|
describe("login", ["--auth", "password"], {}, () => {
|
||||||
test("should see the login page", async ({ codeServerPage }) => {
|
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 codeServerPage.page.title()).toBe("code-server login")
|
expect(await codeServerPage.page.title()).toBe("code-server login")
|
||||||
|
@ -4,10 +4,10 @@ import { promises as fs } from "fs"
|
|||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import { Page } from "playwright"
|
import { Page } from "playwright"
|
||||||
import * as util from "util"
|
import * as util from "util"
|
||||||
import { logError, plural } from "../../../src/common/util"
|
import { logError, normalize, plural } from "../../../src/common/util"
|
||||||
import { onLine } from "../../../src/node/util"
|
import { onLine } from "../../../src/node/util"
|
||||||
import { PASSWORD, workspaceDir } from "../../utils/constants"
|
import { PASSWORD, workspaceDir } from "../../utils/constants"
|
||||||
import { idleTimer, tmpdir } from "../../utils/helpers"
|
import { getMaybeProxiedCodeServer, idleTimer, tmpdir } from "../../utils/helpers"
|
||||||
|
|
||||||
interface CodeServerProcess {
|
interface CodeServerProcess {
|
||||||
process: cp.ChildProcess
|
process: cp.ChildProcess
|
||||||
@ -58,6 +58,7 @@ export class CodeServer {
|
|||||||
this.process = this.spawn()
|
this.process = this.spawn()
|
||||||
}
|
}
|
||||||
const { address } = await this.process
|
const { address } = await this.process
|
||||||
|
|
||||||
return address
|
return address
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,6 +105,8 @@ export class CodeServer {
|
|||||||
this.entry,
|
this.entry,
|
||||||
"--extensions-dir",
|
"--extensions-dir",
|
||||||
path.join(dir, "extensions"),
|
path.join(dir, "extensions"),
|
||||||
|
"--auth",
|
||||||
|
"none",
|
||||||
...this.args,
|
...this.args,
|
||||||
// Using port zero will spawn on a random port.
|
// Using port zero will spawn on a random port.
|
||||||
"--bind-addr",
|
"--bind-addr",
|
||||||
@ -124,6 +127,10 @@ export class CodeServer {
|
|||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
...this.env,
|
...this.env,
|
||||||
|
// Set to empty string to prevent code-server from
|
||||||
|
// using the existing instance when running the e2e tests
|
||||||
|
// from an integrated terminal.
|
||||||
|
VSCODE_IPC_HOOK_CLI: "",
|
||||||
PASSWORD,
|
PASSWORD,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -183,6 +190,13 @@ export class CodeServer {
|
|||||||
proc.kill()
|
proc.kill()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not authentication is enabled.
|
||||||
|
*/
|
||||||
|
authEnabled(): boolean {
|
||||||
|
return this.args.includes("password")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -195,11 +209,7 @@ export class CodeServer {
|
|||||||
export class CodeServerPage {
|
export class CodeServerPage {
|
||||||
private readonly editorSelector = "div.monaco-workbench"
|
private readonly editorSelector = "div.monaco-workbench"
|
||||||
|
|
||||||
constructor(
|
constructor(private readonly codeServer: CodeServer, public readonly page: Page) {
|
||||||
private readonly codeServer: CodeServer,
|
|
||||||
public readonly page: Page,
|
|
||||||
private readonly authenticated: boolean,
|
|
||||||
) {
|
|
||||||
this.page.on("console", (message) => {
|
this.page.on("console", (message) => {
|
||||||
this.codeServer.logger.debug(message.text())
|
this.codeServer.logger.debug(message.text())
|
||||||
})
|
})
|
||||||
@ -224,12 +234,16 @@ export class CodeServerPage {
|
|||||||
* editor to become available.
|
* editor to become available.
|
||||||
*/
|
*/
|
||||||
async navigate(endpoint = "/") {
|
async navigate(endpoint = "/") {
|
||||||
const to = new URL(endpoint, await this.codeServer.address())
|
const address = await getMaybeProxiedCodeServer(this.codeServer)
|
||||||
|
const noramlizedUrl = normalize(address + endpoint, true)
|
||||||
|
const to = new URL(noramlizedUrl)
|
||||||
|
|
||||||
|
this.codeServer.logger.info(`navigating to ${to}`)
|
||||||
await this.page.goto(to.toString(), { waitUntil: "networkidle" })
|
await this.page.goto(to.toString(), { waitUntil: "networkidle" })
|
||||||
|
|
||||||
// Only reload editor if authenticated. Otherwise we'll get stuck
|
// Only reload editor if auth is not enabled. Otherwise we'll get stuck
|
||||||
// reloading the login page.
|
// reloading the login page.
|
||||||
if (this.authenticated) {
|
if (!this.codeServer.authEnabled()) {
|
||||||
await this.reloadUntilEditorIsReady()
|
await this.reloadUntilEditorIsReady()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { version } from "../../src/node/constants"
|
import { version } from "../../src/node/constants"
|
||||||
import { describe, test, expect } from "./baseFixture"
|
import { describe, test, expect } from "./baseFixture"
|
||||||
|
|
||||||
describe("Open Help > About", true, [], {}, () => {
|
describe("Open Help > About", [], {}, () => {
|
||||||
test("should see code-server version in about dialog", async ({ codeServerPage }) => {
|
test("should see code-server version in about dialog", async ({ codeServerPage }) => {
|
||||||
// Open using the menu.
|
// Open using the menu.
|
||||||
await codeServerPage.navigateMenus(["Help", "About"])
|
await codeServerPage.navigateMenus(["Help", "About"])
|
||||||
|
@ -2,10 +2,10 @@ import * as cp from "child_process"
|
|||||||
import { promises as fs } from "fs"
|
import { promises as fs } from "fs"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
import util from "util"
|
import util from "util"
|
||||||
import { clean, tmpdir } from "../utils/helpers"
|
import { clean, getMaybeProxiedCodeServer, tmpdir } from "../utils/helpers"
|
||||||
import { describe, expect, test } from "./baseFixture"
|
import { describe, expect, test } from "./baseFixture"
|
||||||
|
|
||||||
describe("Integrated Terminal", true, [], {}, () => {
|
describe("Integrated Terminal", [], {}, () => {
|
||||||
const testName = "integrated-terminal"
|
const testName = "integrated-terminal"
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
await clean(testName)
|
await clean(testName)
|
||||||
@ -26,7 +26,8 @@ describe("Integrated Terminal", true, [], {}, () => {
|
|||||||
await codeServerPage.page.keyboard.press("Enter")
|
await codeServerPage.page.keyboard.press("Enter")
|
||||||
|
|
||||||
const { stdout } = await output
|
const { stdout } = await output
|
||||||
expect(stdout).toMatch(await codeServerPage.address())
|
const address = await getMaybeProxiedCodeServer(codeServerPage)
|
||||||
|
expect(stdout).toMatch(address)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("should be able to invoke `code-server` to open a file", async ({ codeServerPage }) => {
|
test("should be able to invoke `code-server` to open a file", async ({ codeServerPage }) => {
|
||||||
|
@ -12,6 +12,8 @@ const config: PlaywrightTestConfig = {
|
|||||||
testDir: path.join(__dirname, "e2e"), // Search for tests in this directory.
|
testDir: path.join(__dirname, "e2e"), // Search for tests in this directory.
|
||||||
timeout: 60000, // Each test is given 60 seconds.
|
timeout: 60000, // Each test is given 60 seconds.
|
||||||
retries: process.env.CI ? 2 : 1, // Retry in CI due to flakiness.
|
retries: process.env.CI ? 2 : 1, // Retry in CI due to flakiness.
|
||||||
|
// Limit the number of failures on CI to save resources
|
||||||
|
maxFailures: process.env.CI ? 3 : undefined,
|
||||||
globalSetup: require.resolve("./utils/globalE2eSetup.ts"),
|
globalSetup: require.resolve("./utils/globalE2eSetup.ts"),
|
||||||
reporter: "list",
|
reporter: "list",
|
||||||
// Put any shared options on the top level.
|
// Put any shared options on the top level.
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
export const PASSWORD = "e45432jklfdsab"
|
export const PASSWORD = "e45432jklfdsab"
|
||||||
export const workspaceDir = "workspaces"
|
export const workspaceDir = "workspaces"
|
||||||
|
export const REVERSE_PROXY_BASE_PATH = process.env.CS_TEST_REVERSE_PROXY_BASE_PATH || "ide"
|
||||||
|
export const REVERSE_PROXY_PORT = process.env.CS_TEST_REVERSE_PROXY_PORT || "8000"
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
import { Cookie } from "playwright"
|
import { workspaceDir } from "./constants"
|
||||||
import { CookieKeys } from "../../src/common/http"
|
|
||||||
import { hash } from "../../src/node/util"
|
|
||||||
import { PASSWORD, workspaceDir } from "./constants"
|
|
||||||
import { clean } from "./helpers"
|
import { clean } from "./helpers"
|
||||||
import * as wtfnode from "./wtfnode"
|
import * as wtfnode from "./wtfnode"
|
||||||
|
|
||||||
@ -20,25 +17,5 @@ export default async function () {
|
|||||||
wtfnode.setup()
|
wtfnode.setup()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Replace this with a call to code-server to get the cookie. To avoid
|
|
||||||
// too much overhead we can do an http POST request and avoid spawning a
|
|
||||||
// browser for it.
|
|
||||||
const cookies: Cookie[] = [
|
|
||||||
{
|
|
||||||
domain: "localhost",
|
|
||||||
expires: -1,
|
|
||||||
httpOnly: false,
|
|
||||||
name: CookieKeys.Session,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "Lax",
|
|
||||||
secure: false,
|
|
||||||
value: await hash(PASSWORD),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
// Save storage state and store as an env variable
|
|
||||||
// More info: https://playwright.dev/docs/auth/#reuse-authentication-state
|
|
||||||
process.env.STORAGE = JSON.stringify({ cookies })
|
|
||||||
|
|
||||||
console.log("✅ Global Setup for Playwright End-to-End Tests is now complete.")
|
console.log("✅ Global Setup for Playwright End-to-End Tests is now complete.")
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ import { promises as fs } from "fs"
|
|||||||
import * as net from "net"
|
import * as net from "net"
|
||||||
import * as os from "os"
|
import * as os from "os"
|
||||||
import * as path from "path"
|
import * as path from "path"
|
||||||
|
import { CodeServer, CodeServerPage } from "../e2e/models/CodeServer"
|
||||||
|
import { REVERSE_PROXY_PORT, REVERSE_PROXY_BASE_PATH } from "./constants"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Spy on the logger and console and replace with mock implementations to
|
* Spy on the logger and console and replace with mock implementations to
|
||||||
@ -119,3 +121,18 @@ export function isAddressInfo(address: unknown): address is net.AddressInfo {
|
|||||||
(address as net.AddressInfo).address !== undefined
|
(address as net.AddressInfo).address !== undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If using a proxy, return the address of the proxy.
|
||||||
|
*
|
||||||
|
* Otherwise, return the direct address of code-server.
|
||||||
|
*/
|
||||||
|
export async function getMaybeProxiedCodeServer(codeServer: CodeServerPage | CodeServer): Promise<string> {
|
||||||
|
const address = await codeServer.address()
|
||||||
|
if (process.env.USE_PROXY === "1") {
|
||||||
|
const uri = new URL(address)
|
||||||
|
return `http://${uri.hostname}:${REVERSE_PROXY_PORT}/${uri.port}/${REVERSE_PROXY_BASE_PATH}/`
|
||||||
|
}
|
||||||
|
|
||||||
|
return address
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user