From f0b5a571551557e8451e661ca46f3034178eb6ce Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Tue, 26 Jan 2021 14:44:23 -0700 Subject: [PATCH 01/29] feat: add playwright --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fdb7c563..e4538ec2 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,4 @@ node-* /lib/coder-cloud-agent .home coverage -**/.DS_Store \ No newline at end of file +**/.DS_Store From c2f1a2dace23a6b7f84fe441f16e748b61f214ff Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 28 Jan 2021 11:48:57 -0700 Subject: [PATCH 02/29] feat: add test for login page --- test/goHome.test.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++ test/login.test.ts | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 test/goHome.test.ts create mode 100644 test/login.test.ts diff --git a/test/goHome.test.ts b/test/goHome.test.ts new file mode 100644 index 00000000..b3faf825 --- /dev/null +++ b/test/goHome.test.ts @@ -0,0 +1,50 @@ +import { chromium, Page, Browser, BrowserContext } from "playwright" + +// NOTE: this is hard-coded and passed as an environment variable +// See the test job in ci.yml +const PASSWORD = "e45432jklfdsab" + +describe("login", () => { + let browser: Browser + let page: Page + let context: BrowserContext + + beforeAll(async () => { + browser = await chromium.launch({ headless: false }) + context = await browser.newContext() + }) + + afterAll(async () => { + await browser.close() + }) + + beforeEach(async () => { + page = await context.newPage() + }) + + afterEach(async () => { + await page.close() + // Remove password from local storage + await context.clearCookies() + }) + + it("should see a 'Go Home' button in the Application Menu that goes to coder.com", async () => { + await page.goto("http://localhost:8080") + // Type in password + await page.fill(".password", PASSWORD) + // Click the submit button and login + await page.click(".submit") + // Click the Applicaiton menu + await page.click(".menubar-menu-button[title='Application Menu']") + // See the Go Home button + const goHomeButton = ".home-bar[aria-label='Home'] li" + expect(await page.isVisible(goHomeButton)) + // Hover over element without clicking + await page.hover(goHomeButton) + // Click the top left corner of the element + await page.click(goHomeButton) + // Note: we have to click on
  • in the Go Home button for it to work + // Land on coder.com + // expect(await page.url()).toBe("https://coder.com/") + }) +}) diff --git a/test/login.test.ts b/test/login.test.ts new file mode 100644 index 00000000..622adddb --- /dev/null +++ b/test/login.test.ts @@ -0,0 +1,41 @@ +import { chromium, Page, Browser, BrowserContext } from "playwright" + +// NOTE: this is hard-coded and passed as an environment variable +// See the test job in ci.yml +const PASSWORD = "e45432jklfdsab" + +describe("login", () => { + let browser: Browser + let page: Page + let context: BrowserContext + + beforeAll(async () => { + browser = await chromium.launch() + context = await browser.newContext() + }) + + afterAll(async () => { + await browser.close() + }) + + beforeEach(async () => { + page = await context.newPage() + }) + + afterEach(async () => { + await page.close() + // Remove password from local storage + await context.clearCookies() + }) + + it("should be able to login with the password from config.yml", async () => { + await page.goto("http://localhost:8080") + // Type in password + await page.fill(".password", PASSWORD) + // Click the submit button and login + await page.click(".submit") + // See the editor + const codeServerEditor = await page.isVisible(".monaco-workbench") + expect(codeServerEditor).toBeTruthy() + }) +}) From d7e41a3187b8f6c8aa58411fbfbd33df4b11b59f Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 28 Jan 2021 16:23:26 -0700 Subject: [PATCH 03/29] fix: increase test timeout to 30000 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index decec942..e739a5f9 100644 --- a/package.json +++ b/package.json @@ -145,6 +145,7 @@ }, "modulePathIgnorePatterns": [ "/release" - ] + ], + "testTimeout": 30000 } } From 3033c8f9a23383ad701d3252a0d02315cadc2ef1 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 28 Jan 2021 16:23:55 -0700 Subject: [PATCH 04/29] feat: add test to visit go home in app menu --- .github/workflows/ci.yaml | 5 ++++- ci/dev/test.sh | 3 ++- test/goHome.test.ts | 47 ++++++++++++++++++++++++++------------- test/login.test.ts | 8 ++----- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a232b5bc..cfa94aaa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -24,6 +24,9 @@ jobs: test: needs: linux-amd64 runs-on: ubuntu-latest + env: + PASSWORD: e45432jklfdsab + CODE_SERVER_ADDRESS: http://localhost:8080 steps: - uses: actions/checkout@v1 - name: Download release packages @@ -37,7 +40,7 @@ jobs: - uses: microsoft/playwright-github-action@v1 - name: Install dependencies and run tests run: | - node ./release-packages/code-server*-linux-amd64 & + node ./release-packages/code-server*-linux-amd64 --home $CODE_SERVER_ADDRESS/healthz & yarn --frozen-lockfile yarn test diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 851aa0d3..c54974ff 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -9,7 +9,8 @@ main() { # information. We must also run it from the root otherwise coverage will not # include our source files. cd "$OLDPWD" - CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" + # We use the same environment variables set in ci.yml in the test job + CS_DISABLE_PLUGINS=true PASSWORD=e45432jklfdsab CODE_SERVER_ADDRESS=http://localhost:8080 ./test/node_modules/.bin/jest "$@" } main "$@" diff --git a/test/goHome.test.ts b/test/goHome.test.ts index b3faf825..b7993dd8 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -1,21 +1,18 @@ import { chromium, Page, Browser, BrowserContext } from "playwright" -// NOTE: this is hard-coded and passed as an environment variable -// See the test job in ci.yml -const PASSWORD = "e45432jklfdsab" - describe("login", () => { let browser: Browser let page: Page let context: BrowserContext beforeAll(async () => { - browser = await chromium.launch({ headless: false }) + browser = await chromium.launch() context = await browser.newContext() }) afterAll(async () => { await browser.close() + await context.close() }) beforeEach(async () => { @@ -29,22 +26,40 @@ describe("login", () => { }) it("should see a 'Go Home' button in the Application Menu that goes to coder.com", async () => { - await page.goto("http://localhost:8080") + const GO_HOME_URL = `${process.env.CODE_SERVER_ADDRESS}/healthz` + let requestedGoHomeUrl = false + page.on("request", (request) => { + // This ensures that we did make a request to the GO_HOME_URL + // Most reliable way to test button + // because we don't care if the request has a response + // only that it was made + if (request.url() === GO_HOME_URL) { + requestedGoHomeUrl = true + } + }) + // waitUntil: "networkidle" + // In case the page takes a long time to load + await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "networkidle" }) // Type in password - await page.fill(".password", PASSWORD) + await page.fill(".password", process.env.PASSWORD || "password") // Click the submit button and login await page.click(".submit") - // Click the Applicaiton menu + // Click the Application menu await page.click(".menubar-menu-button[title='Application Menu']") // See the Go Home button - const goHomeButton = ".home-bar[aria-label='Home'] li" + const goHomeButton = "a.action-menu-item span[aria-label='Go Home']" expect(await page.isVisible(goHomeButton)) - // Hover over element without clicking - await page.hover(goHomeButton) - // Click the top left corner of the element - await page.click(goHomeButton) - // Note: we have to click on
  • in the Go Home button for it to work - // Land on coder.com - // expect(await page.url()).toBe("https://coder.com/") + // Click it and navigate to coder.com + // NOTE: ran into issues of it failing intermittently + // without having button: "middle" + await page.click(goHomeButton, { button: "middle" }) + + // If there are unsaved changes it will show a dialog + // asking if you're sure you want to leave + page.on("dialog", (dialog) => dialog.accept()) + + // We make sure to wait on a request to the GO_HOME_URL + await page.waitForRequest(GO_HOME_URL) + expect(requestedGoHomeUrl).toBeTruthy() }) }) diff --git a/test/login.test.ts b/test/login.test.ts index 622adddb..460dbc30 100644 --- a/test/login.test.ts +++ b/test/login.test.ts @@ -1,9 +1,5 @@ import { chromium, Page, Browser, BrowserContext } from "playwright" -// NOTE: this is hard-coded and passed as an environment variable -// See the test job in ci.yml -const PASSWORD = "e45432jklfdsab" - describe("login", () => { let browser: Browser let page: Page @@ -29,9 +25,9 @@ describe("login", () => { }) it("should be able to login with the password from config.yml", async () => { - await page.goto("http://localhost:8080") + await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080") // Type in password - await page.fill(".password", PASSWORD) + await page.fill(".password", process.env.PASSWORD || "password") // Click the submit button and login await page.click(".submit") // See the editor From 34c6ec4c071a8a1470684fd32dd88d4e7ca584f4 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Mon, 1 Feb 2021 14:38:53 -0700 Subject: [PATCH 05/29] feat: add globalSetup for testing --- package.json | 3 ++- test/globalSetup.ts | 25 +++++++++++++++++++++++++ test/goHome.test.ts | 36 ++++++++++++++++++++---------------- 3 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 test/globalSetup.ts diff --git a/package.json b/package.json index e739a5f9..28ff7f66 100644 --- a/package.json +++ b/package.json @@ -146,6 +146,7 @@ "modulePathIgnorePatterns": [ "/release" ], - "testTimeout": 30000 + "testTimeout": 30000, + "globalSetup": "/test/globalSetup.ts" } } diff --git a/test/globalSetup.ts b/test/globalSetup.ts new file mode 100644 index 00000000..4904c0c3 --- /dev/null +++ b/test/globalSetup.ts @@ -0,0 +1,25 @@ +// This setup runs before our e2e tests +// so that it authenticates us into code-server +// ensuring that we're logged in before we run any tests +import { chromium, Page, Browser, BrowserContext } from "playwright" + +module.exports = async () => { + const browser: Browser = await chromium.launch() + const context: BrowserContext = await browser.newContext() + const page: Page = await context.newPage() + + await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "domcontentloaded" }) + // Type in password + await page.fill(".password", process.env.PASSWORD || "password") + // Click the submit button and login + await page.click(".submit") + + // Save storage state and store as an env variable + // More info: https://playwright.dev/docs/auth?_highlight=authe#reuse-authentication-state + const storage = await context.storageState() + process.env.STORAGE = JSON.stringify(storage) + + await page.close() + await browser.close() + await context.close() +} diff --git a/test/goHome.test.ts b/test/goHome.test.ts index b7993dd8..3618254b 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -7,10 +7,15 @@ describe("login", () => { beforeAll(async () => { browser = await chromium.launch() - context = await browser.newContext() + // Create a new context with the saved storage state + const storageState = JSON.parse(process.env.STORAGE || "") + context = await browser.newContext({ storageState }) }) afterAll(async () => { + // Remove password from local storage + await context.clearCookies() + await browser.close() await context.close() }) @@ -19,12 +24,6 @@ describe("login", () => { page = await context.newPage() }) - afterEach(async () => { - await page.close() - // Remove password from local storage - await context.clearCookies() - }) - it("should see a 'Go Home' button in the Application Menu that goes to coder.com", async () => { const GO_HOME_URL = `${process.env.CODE_SERVER_ADDRESS}/healthz` let requestedGoHomeUrl = false @@ -35,15 +34,13 @@ describe("login", () => { // only that it was made if (request.url() === GO_HOME_URL) { requestedGoHomeUrl = true + console.log("woooo =>>>", requestedGoHomeUrl) } }) - // waitUntil: "networkidle" + + // waitUntil: "domcontentloaded" // In case the page takes a long time to load - await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "networkidle" }) - // Type in password - await page.fill(".password", process.env.PASSWORD || "password") - // Click the submit button and login - await page.click(".submit") + await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "domcontentloaded" }) // Click the Application menu await page.click(".menubar-menu-button[title='Application Menu']") // See the Go Home button @@ -56,10 +53,17 @@ describe("login", () => { // If there are unsaved changes it will show a dialog // asking if you're sure you want to leave - page.on("dialog", (dialog) => dialog.accept()) + await page.on("dialog", (dialog) => dialog.accept()) - // We make sure to wait on a request to the GO_HOME_URL - await page.waitForRequest(GO_HOME_URL) + // If it takes longer than 3 seconds to navigate, something is wrong + await page.waitForRequest(GO_HOME_URL, { timeout: 10000 }) expect(requestedGoHomeUrl).toBeTruthy() + + // // Make sure the response for GO_HOME_URL was successful + // const response = await page.waitForResponse( + // (response) => response.url() === GO_HOME_URL && response.status() === 200, + // ) + // We make sure a request was made to the GO_HOME_URL + // expect(response.ok()).toBeTruthy() }) }) From 9eba2bd4fd980a1afd818b7478600a2b36f7497c Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Mon, 1 Feb 2021 14:40:06 -0700 Subject: [PATCH 06/29] fix(ci): update test job to use bin --- .github/workflows/ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index cfa94aaa..39137585 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -40,7 +40,7 @@ jobs: - uses: microsoft/playwright-github-action@v1 - name: Install dependencies and run tests run: | - node ./release-packages/code-server*-linux-amd64 --home $CODE_SERVER_ADDRESS/healthz & + ./release-packages/code-server*-linux-amd64/bin/code-server --home $CODE_SERVER_ADDRESS/healthz & yarn --frozen-lockfile yarn test From 236717ee986acb1e6d46b7786e0772f805bdbc9e Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Mon, 1 Feb 2021 15:26:22 -0700 Subject: [PATCH 07/29] fix: update modulePathIgnorePatterns for jest --- package.json | 8 +++++++- test/globalSetup.ts | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 28ff7f66..c5f32a5f 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,12 @@ "/release" ], "testTimeout": 30000, - "globalSetup": "/test/globalSetup.ts" + "globalSetup": "/test/globalSetup.ts", + "modulePathIgnorePatterns": [ + "/lib/vscode", + "/release-packages", + "/release", + "/release-standalone" + ] } } diff --git a/test/globalSetup.ts b/test/globalSetup.ts index 4904c0c3..daaf7921 100644 --- a/test/globalSetup.ts +++ b/test/globalSetup.ts @@ -4,6 +4,8 @@ import { chromium, Page, Browser, BrowserContext } from "playwright" module.exports = async () => { + console.log("🚨 Running Global Setup for Jest Tests") + console.log(" Please hang tight...") const browser: Browser = await chromium.launch() const context: BrowserContext = await browser.newContext() const page: Page = await context.newPage() @@ -22,4 +24,5 @@ module.exports = async () => { await page.close() await browser.close() await context.close() + console.log("✅ Global Setup for Jest Tests is now complete.") } From ffdbf3a730c339d5db3b05a8c25efd19a98087bd Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Tue, 2 Feb 2021 13:58:51 -0700 Subject: [PATCH 08/29] feat: add test/videos & /screenshots to gitignore --- .gitignore | 2 ++ package.json | 5 ++++- test/goHome.test.ts | 54 +++++++++++++++++++++++++++------------------ test/login.test.ts | 2 +- 4 files changed, 39 insertions(+), 24 deletions(-) diff --git a/.gitignore b/.gitignore index e4538ec2..e49888f4 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ node-* .home coverage **/.DS_Store +test/videos +test/screenshots diff --git a/package.json b/package.json index c5f32a5f..1df87c1a 100644 --- a/package.json +++ b/package.json @@ -152,7 +152,10 @@ "/lib/vscode", "/release-packages", "/release", - "/release-standalone" + "/release-standalone", + "/release-npm-package", + "/release-gcp", + "/release-images" ] } } diff --git a/test/goHome.test.ts b/test/goHome.test.ts index 3618254b..ea42dcbf 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -5,26 +5,29 @@ describe("login", () => { let page: Page let context: BrowserContext - beforeAll(async () => { + beforeAll(async (done) => { browser = await chromium.launch() // Create a new context with the saved storage state const storageState = JSON.parse(process.env.STORAGE || "") - context = await browser.newContext({ storageState }) + context = await browser.newContext({ storageState, recordVideo: { dir: "./test/videos/" } }) + done() }) - afterAll(async () => { + afterAll(async (done) => { // Remove password from local storage await context.clearCookies() await browser.close() await context.close() + done() }) - beforeEach(async () => { + beforeEach(async (done) => { page = await context.newPage() + done() }) - it("should see a 'Go Home' button in the Application Menu that goes to coder.com", async () => { + it("should see a 'Go Home' button in the Application Menu that goes to /healthz", async (done) => { const GO_HOME_URL = `${process.env.CODE_SERVER_ADDRESS}/healthz` let requestedGoHomeUrl = false page.on("request", (request) => { @@ -34,36 +37,43 @@ describe("login", () => { // only that it was made if (request.url() === GO_HOME_URL) { requestedGoHomeUrl = true - console.log("woooo =>>>", requestedGoHomeUrl) + expect(requestedGoHomeUrl).toBeTruthy() + + // This ensures Jest knows we're done here. + done() } }) + // Sometimes a dialog shows up when you navigate + // asking if you're sure you want to leave + // so we listen if it comes, we accept it + page.on("dialog", (dialog) => dialog.accept()) // waitUntil: "domcontentloaded" // In case the page takes a long time to load await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "domcontentloaded" }) + + // For some odd reason, the login method used in globalSetup.ts + // I don't know if it's on playwright clearing our cookies by accident + // or if it's our cookies disappearing. + // This means we need an additional check to make sure we're logged in + // otherwise this test will hang and fail. + const currentPageURL = await page.url() + const isLoginPage = currentPageURL.includes("login") + if (isLoginPage) { + await page.fill(".password", process.env.PASSWORD || "password") + // Click the submit button and login + await page.click(".submit") + } + // Click the Application menu await page.click(".menubar-menu-button[title='Application Menu']") // See the Go Home button const goHomeButton = "a.action-menu-item span[aria-label='Go Home']" expect(await page.isVisible(goHomeButton)) - // Click it and navigate to coder.com + + // Click it and navigate to /healthz // NOTE: ran into issues of it failing intermittently // without having button: "middle" await page.click(goHomeButton, { button: "middle" }) - - // If there are unsaved changes it will show a dialog - // asking if you're sure you want to leave - await page.on("dialog", (dialog) => dialog.accept()) - - // If it takes longer than 3 seconds to navigate, something is wrong - await page.waitForRequest(GO_HOME_URL, { timeout: 10000 }) - expect(requestedGoHomeUrl).toBeTruthy() - - // // Make sure the response for GO_HOME_URL was successful - // const response = await page.waitForResponse( - // (response) => response.url() === GO_HOME_URL && response.status() === 200, - // ) - // We make sure a request was made to the GO_HOME_URL - // expect(response.ok()).toBeTruthy() }) }) diff --git a/test/login.test.ts b/test/login.test.ts index 460dbc30..5358adaa 100644 --- a/test/login.test.ts +++ b/test/login.test.ts @@ -24,7 +24,7 @@ describe("login", () => { await context.clearCookies() }) - it("should be able to login with the password from config.yml", async () => { + it("should be able to login", async () => { await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080") // Type in password await page.fill(".password", process.env.PASSWORD || "password") From 9e3c8bd93d2478d16f5dda59e1a67820646493fe Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Tue, 2 Feb 2021 14:29:02 -0700 Subject: [PATCH 09/29] feat: add step to upload test videos --- .github/workflows/ci.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 39137585..838a0ba6 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -43,6 +43,11 @@ jobs: ./release-packages/code-server*-linux-amd64/bin/code-server --home $CODE_SERVER_ADDRESS/healthz & yarn --frozen-lockfile yarn test + - name: Upload test artifacts + uses: actions/upload-artifact@v2 + with: + name: test-videos + path: ./test/videos release: runs-on: ubuntu-latest From e077f2d97fc988b00650d365cad210bc4d846675 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 4 Feb 2021 11:09:19 -0700 Subject: [PATCH 10/29] refactor: update test script to check env var --- ci/dev/test.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index c54974ff..70434db1 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -10,7 +10,18 @@ main() { # include our source files. cd "$OLDPWD" # We use the same environment variables set in ci.yml in the test job - CS_DISABLE_PLUGINS=true PASSWORD=e45432jklfdsab CODE_SERVER_ADDRESS=http://localhost:8080 ./test/node_modules/.bin/jest "$@" + if [[ -z ${PASSWORD+x} ]] || [[ -z ${CODE_SERVER_ADDRESS+x} ]]; then + echo "The end-to-end testing suites rely on your local environment" + echo -e "\n" + echo "Please set the following environment variables locally:" + echo " \$PASSWORD" + echo " \$CODE_SERVER_ADDRESS" + echo -e "\n" + echo "Please make sure you have code-server running locally." + echo -e "\n" + exit 1 + fi + CS_DISABLE_PLUGINS=true ./test/node_modules/.bin/jest "$@" } main "$@" From b02d2fb3ccabbbb941d06e6606a7abb8113e98a9 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 4 Feb 2021 14:53:54 -0700 Subject: [PATCH 11/29] feat: add cookie utils for e2e tests --- src/common/util.ts | 36 +++++++++++++++++++++++ src/node/routes/login.ts | 2 +- test/util.test.ts | 63 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/common/util.ts b/src/common/util.ts index 87ca6f59..d0e03ab3 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -120,3 +120,39 @@ export function logError(prefix: string, err: any): void { logger.error(`${prefix}: ${err}`) } } + +// Borrowed from playwright +export interface Cookie { + name: string + value: string + domain: string + path: string + /** + * Unix time in seconds. + */ + expires: number + httpOnly: boolean + secure: boolean + sameSite: "Strict" | "Lax" | "None" +} + +/** + * Checks if a cookie exists in array of cookies + */ +export function checkForCookie(cookies: Array, key: string): boolean { + // Check for at least one cookie where the name is equal to key + return cookies.filter((cookie) => cookie.name === key).length > 0 +} + +/** + * Creates a login cookie if one doesn't already exist + */ +export function createCookieIfDoesntExist(cookies: Array, cookieToStore: Cookie): Array { + const cookieName = cookieToStore.name + const doesCookieExist = checkForCookie(cookies, cookieName) + if (!doesCookieExist) { + const updatedCookies = [...cookies, cookieToStore] + return updatedCookies + } + return cookies +} diff --git a/src/node/routes/login.ts b/src/node/routes/login.ts index c3ad12ad..b89470ae 100644 --- a/src/node/routes/login.ts +++ b/src/node/routes/login.ts @@ -7,7 +7,7 @@ import { rootPath } from "../constants" import { authenticated, getCookieDomain, redirect, replaceTemplates } from "../http" import { hash, humanPath } from "../util" -enum Cookie { +export enum Cookie { Key = "key", } diff --git a/test/util.test.ts b/test/util.test.ts index 78985554..91a4315f 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -1,4 +1,5 @@ import { JSDOM } from "jsdom" +import { Cookie } from "playwright" // Note: we need to import logger from the root // because this is the logger used in logError in ../src/common/util import { logger } from "../node_modules/@coder/logger" @@ -8,12 +9,16 @@ import { getFirstString, getOptions, logError, - normalize, plural, resolveBase, split, trimSlashes, + checkForCookie, + createCookieIfDoesntExist, + normalize, } from "../src/common/util" +import { Cookie as CookieEnum } from "../src/node/routes/login" +import { hash } from "../src/node/util" const dom = new JSDOM() global.document = dom.window.document @@ -255,4 +260,60 @@ describe("util", () => { expect(spy).toHaveBeenCalledWith("api: oh no") }) }) + + describe("checkForCookie", () => { + it("should check if the cookie exists and has a value", () => { + const PASSWORD = "123supersecure!" + const fakeCookies: Cookie[] = [ + { + name: CookieEnum.Key, + value: hash(PASSWORD), + domain: "localhost", + secure: false, + sameSite: "Lax", + httpOnly: false, + expires: 18000, + path: "/", + }, + ] + expect(checkForCookie(fakeCookies, CookieEnum.Key)).toBe(true) + }) + it("should return false if there are no cookies", () => { + const fakeCookies: Cookie[] = [] + expect(checkForCookie(fakeCookies, "key")).toBe(false) + }) + }) + + describe("createCookieIfDoesntExist", () => { + it("should create a cookie if it doesn't exist", () => { + const PASSWORD = "123supersecure" + const cookies: Cookie[] = [] + const cookieToStore = { + name: CookieEnum.Key, + value: hash(PASSWORD), + domain: "localhost", + secure: false, + sameSite: "Lax" as const, + httpOnly: false, + expires: 18000, + path: "/", + } + expect(createCookieIfDoesntExist(cookies, cookieToStore)).toStrictEqual([cookieToStore]) + }) + it("should return the same cookies if the cookie already exists", () => { + const PASSWORD = "123supersecure" + const cookieToStore = { + name: CookieEnum.Key, + value: hash(PASSWORD), + domain: "localhost", + secure: false, + sameSite: "Lax" as const, + httpOnly: false, + expires: 18000, + path: "/", + } + const cookies: Cookie[] = [cookieToStore] + expect(createCookieIfDoesntExist(cookies, cookieToStore)).toStrictEqual(cookies) + }) + }) }) From 2dc56ad4d775d6604b0719a000bed543bd1634db Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 4 Feb 2021 14:54:08 -0700 Subject: [PATCH 12/29] refactor: manually add cookie goHome --- test/goHome.test.ts | 59 +++++++++++++++++++++++++++++++++------------ 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/test/goHome.test.ts b/test/goHome.test.ts index ea42dcbf..b8646bc3 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -1,4 +1,6 @@ -import { chromium, Page, Browser, BrowserContext } from "playwright" +import { chromium, Page, Browser, BrowserContext, Cookie } from "playwright" +import { createCookieIfDoesntExist } from "../src/common/util" +import { hash } from "../src/node/util" describe("login", () => { let browser: Browser @@ -9,7 +11,37 @@ describe("login", () => { browser = await chromium.launch() // Create a new context with the saved storage state const storageState = JSON.parse(process.env.STORAGE || "") - context = await browser.newContext({ storageState, recordVideo: { dir: "./test/videos/" } }) + + // + const cookieToStore = { + sameSite: "Lax" as const, + name: "key", + value: hash(process.env.PASSWORD || ""), + domain: "localhost", + path: "/", + expires: -1, + httpOnly: false, + secure: false, + } + + // For some odd reason, the login method used in globalSetup.ts doesn't always work + // I don't know if it's on playwright clearing our cookies by accident + // or if it's our cookies disappearing. + // This means we need an additional check to make sure we're logged in. + // We do this by manually adding the cookie to the browser environment + // if it's not there at the time the test starts + const cookies: Cookie[] = storageState.cookies || [] + // If the cookie exists in cookies then + // this will return the cookies with no changes + // otherwise if it doesn't exist, it will create it + // hence the name maybeUpdatedCookies + const maybeUpdatedCookies = createCookieIfDoesntExist(cookies, cookieToStore) + console.log("here are the cookies", maybeUpdatedCookies) + + context = await browser.newContext({ + storageState: { cookies: maybeUpdatedCookies }, + recordVideo: { dir: "./test/videos/" }, + }) done() }) @@ -27,7 +59,16 @@ describe("login", () => { done() }) + // NOTE: this test will fail if you do not run code-server with --home $CODE_SERVER_ADDRESS/healthz it("should see a 'Go Home' button in the Application Menu that goes to /healthz", async (done) => { + // Ideally, this test should pass and finish before the timeout set in the Jest config + // However, if it doesn't, we don't want a memory leak so we set this backup timeout + // Otherwise Jest may throw this error + // "Jest did not exit one second after the test run has completed. + // This usually means that there are asynchronous operations that weren't stopped in your tests. + // Consider running Jest with `--detectOpenHandles` to troubleshoot this issue." + const backupTimeout = setTimeout(() => done(), 20000) + const GO_HOME_URL = `${process.env.CODE_SERVER_ADDRESS}/healthz` let requestedGoHomeUrl = false page.on("request", (request) => { @@ -38,6 +79,7 @@ describe("login", () => { if (request.url() === GO_HOME_URL) { requestedGoHomeUrl = true expect(requestedGoHomeUrl).toBeTruthy() + clearTimeout(backupTimeout) // This ensures Jest knows we're done here. done() @@ -52,19 +94,6 @@ describe("login", () => { // In case the page takes a long time to load await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "domcontentloaded" }) - // For some odd reason, the login method used in globalSetup.ts - // I don't know if it's on playwright clearing our cookies by accident - // or if it's our cookies disappearing. - // This means we need an additional check to make sure we're logged in - // otherwise this test will hang and fail. - const currentPageURL = await page.url() - const isLoginPage = currentPageURL.includes("login") - if (isLoginPage) { - await page.fill(".password", process.env.PASSWORD || "password") - // Click the submit button and login - await page.click(".submit") - } - // Click the Application menu await page.click(".menubar-menu-button[title='Application Menu']") // See the Go Home button From d0eece3d8ff5b3eff7e86752a73e16d85b52ee50 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 4 Feb 2021 14:54:26 -0700 Subject: [PATCH 13/29] refactor: add note to test.sh about --home --- ci/dev/test.sh | 3 ++- src/common/util.ts | 4 ++-- test/goHome.test.ts | 9 ++++++--- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 70434db1..6520176d 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -17,7 +17,8 @@ main() { echo " \$PASSWORD" echo " \$CODE_SERVER_ADDRESS" echo -e "\n" - echo "Please make sure you have code-server running locally." + echo "Please make sure you have code-server running locally with the flag:" + echo " --home \$CODE_SERVER_ADDRESS/healthz " echo -e "\n" exit 1 fi diff --git a/src/common/util.ts b/src/common/util.ts index d0e03ab3..9b6418da 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -140,8 +140,8 @@ export interface Cookie { * Checks if a cookie exists in array of cookies */ export function checkForCookie(cookies: Array, key: string): boolean { - // Check for at least one cookie where the name is equal to key - return cookies.filter((cookie) => cookie.name === key).length > 0 + // Check for a cookie where the name is equal to key + return Boolean(cookies.find((cookie) => cookie.name === key)) } /** diff --git a/test/goHome.test.ts b/test/goHome.test.ts index b8646bc3..57712e5a 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -12,7 +12,6 @@ describe("login", () => { // Create a new context with the saved storage state const storageState = JSON.parse(process.env.STORAGE || "") - // const cookieToStore = { sameSite: "Lax" as const, name: "key", @@ -61,16 +60,20 @@ describe("login", () => { // NOTE: this test will fail if you do not run code-server with --home $CODE_SERVER_ADDRESS/healthz it("should see a 'Go Home' button in the Application Menu that goes to /healthz", async (done) => { + let requestedGoHomeUrl = false // Ideally, this test should pass and finish before the timeout set in the Jest config // However, if it doesn't, we don't want a memory leak so we set this backup timeout // Otherwise Jest may throw this error // "Jest did not exit one second after the test run has completed. // This usually means that there are asynchronous operations that weren't stopped in your tests. // Consider running Jest with `--detectOpenHandles` to troubleshoot this issue." - const backupTimeout = setTimeout(() => done(), 20000) + const backupTimeout = setTimeout(() => { + // If it's not true by this point then the test should fail + expect(requestedGoHomeUrl).toBeTruthy() + done() + }, 20000) const GO_HOME_URL = `${process.env.CODE_SERVER_ADDRESS}/healthz` - let requestedGoHomeUrl = false page.on("request", (request) => { // This ensures that we did make a request to the GO_HOME_URL // Most reliable way to test button From 06af8b3202de28a318cd806ca9d558af801fa53e Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Wed, 10 Feb 2021 17:00:22 -0700 Subject: [PATCH 14/29] refactor: update goHome location in test --- test/goHome.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/goHome.test.ts b/test/goHome.test.ts index 57712e5a..1c608875 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -35,7 +35,6 @@ describe("login", () => { // otherwise if it doesn't exist, it will create it // hence the name maybeUpdatedCookies const maybeUpdatedCookies = createCookieIfDoesntExist(cookies, cookieToStore) - console.log("here are the cookies", maybeUpdatedCookies) context = await browser.newContext({ storageState: { cookies: maybeUpdatedCookies }, @@ -97,8 +96,8 @@ describe("login", () => { // In case the page takes a long time to load await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "domcontentloaded" }) - // Click the Application menu - await page.click(".menubar-menu-button[title='Application Menu']") + // Click the Home menu + await page.click(".home-bar ul[aria-label='Home'] li") // See the Go Home button const goHomeButton = "a.action-menu-item span[aria-label='Go Home']" expect(await page.isVisible(goHomeButton)) From 38d7718feb32d27c540d6f94ba753cf4a2bcc3eb Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Thu, 11 Feb 2021 11:18:15 -0700 Subject: [PATCH 15/29] refactor: use promises for goHome test --- ci/dev/test.sh | 3 +-- test/goHome.test.ts | 45 +++++++++++++++++++-------------------------- 2 files changed, 20 insertions(+), 28 deletions(-) diff --git a/ci/dev/test.sh b/ci/dev/test.sh index 6520176d..82f6ad36 100755 --- a/ci/dev/test.sh +++ b/ci/dev/test.sh @@ -9,8 +9,7 @@ main() { # information. We must also run it from the root otherwise coverage will not # include our source files. cd "$OLDPWD" - # We use the same environment variables set in ci.yml in the test job - if [[ -z ${PASSWORD+x} ]] || [[ -z ${CODE_SERVER_ADDRESS+x} ]]; then + if [[ -z ${PASSWORD-} ]] || [[ -z ${CODE_SERVER_ADDRESS-} ]]; then echo "The end-to-end testing suites rely on your local environment" echo -e "\n" echo "Please set the following environment variables locally:" diff --git a/test/goHome.test.ts b/test/goHome.test.ts index 1c608875..f688df05 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -2,15 +2,23 @@ import { chromium, Page, Browser, BrowserContext, Cookie } from "playwright" import { createCookieIfDoesntExist } from "../src/common/util" import { hash } from "../src/node/util" -describe("login", () => { +async function setTimeoutPromise(milliseconds: number): Promise { + return new Promise((resolve, _) => { + setTimeout(() => { + resolve() + }, milliseconds) + }) +} + +describe("go home", () => { let browser: Browser let page: Page let context: BrowserContext - beforeAll(async (done) => { + beforeAll(async () => { browser = await chromium.launch() // Create a new context with the saved storage state - const storageState = JSON.parse(process.env.STORAGE || "") + const storageState = JSON.parse(process.env.STORAGE || "{}") const cookieToStore = { sameSite: "Lax" as const, @@ -40,37 +48,23 @@ describe("login", () => { storageState: { cookies: maybeUpdatedCookies }, recordVideo: { dir: "./test/videos/" }, }) - done() }) - afterAll(async (done) => { + afterAll(async () => { // Remove password from local storage await context.clearCookies() await browser.close() await context.close() - done() }) - beforeEach(async (done) => { + beforeEach(async () => { page = await context.newPage() - done() }) // NOTE: this test will fail if you do not run code-server with --home $CODE_SERVER_ADDRESS/healthz - it("should see a 'Go Home' button in the Application Menu that goes to /healthz", async (done) => { + it("should see a 'Go Home' button in the Application Menu that goes to /healthz", async () => { let requestedGoHomeUrl = false - // Ideally, this test should pass and finish before the timeout set in the Jest config - // However, if it doesn't, we don't want a memory leak so we set this backup timeout - // Otherwise Jest may throw this error - // "Jest did not exit one second after the test run has completed. - // This usually means that there are asynchronous operations that weren't stopped in your tests. - // Consider running Jest with `--detectOpenHandles` to troubleshoot this issue." - const backupTimeout = setTimeout(() => { - // If it's not true by this point then the test should fail - expect(requestedGoHomeUrl).toBeTruthy() - done() - }, 20000) const GO_HOME_URL = `${process.env.CODE_SERVER_ADDRESS}/healthz` page.on("request", (request) => { @@ -80,11 +74,6 @@ describe("login", () => { // only that it was made if (request.url() === GO_HOME_URL) { requestedGoHomeUrl = true - expect(requestedGoHomeUrl).toBeTruthy() - clearTimeout(backupTimeout) - - // This ensures Jest knows we're done here. - done() } }) // Sometimes a dialog shows up when you navigate @@ -105,6 +94,10 @@ describe("login", () => { // Click it and navigate to /healthz // NOTE: ran into issues of it failing intermittently // without having button: "middle" - await page.click(goHomeButton, { button: "middle" }) + await Promise.all([ + page.waitForNavigation(), + page.click(goHomeButton, { button: "middle" }) + ]) + expect(page.url()).toBe(GO_HOME_URL) }) }) From 3fa460c24436eff0d1b1b6e6252473327424141b Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Fri, 12 Feb 2021 11:55:16 -0700 Subject: [PATCH 16/29] refactor: create helpers.ts & add Cookie --- src/common/util.ts | 16 +--------------- test/globalSetup.ts | 8 ++++---- test/helpers.ts | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 19 deletions(-) create mode 100644 test/helpers.ts diff --git a/src/common/util.ts b/src/common/util.ts index 9b6418da..e8edc13b 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -1,4 +1,5 @@ import { logger, field } from "@coder/logger" +import { Cookie } from "../../test/helpers" export interface Options { base: string @@ -121,21 +122,6 @@ export function logError(prefix: string, err: any): void { } } -// Borrowed from playwright -export interface Cookie { - name: string - value: string - domain: string - path: string - /** - * Unix time in seconds. - */ - expires: number - httpOnly: boolean - secure: boolean - sameSite: "Strict" | "Lax" | "None" -} - /** * Checks if a cookie exists in array of cookies */ diff --git a/test/globalSetup.ts b/test/globalSetup.ts index daaf7921..dcf5047d 100644 --- a/test/globalSetup.ts +++ b/test/globalSetup.ts @@ -1,14 +1,14 @@ // This setup runs before our e2e tests // so that it authenticates us into code-server // ensuring that we're logged in before we run any tests -import { chromium, Page, Browser, BrowserContext } from "playwright" +import { chromium } from "playwright" module.exports = async () => { console.log("🚨 Running Global Setup for Jest Tests") console.log(" Please hang tight...") - const browser: Browser = await chromium.launch() - const context: BrowserContext = await browser.newContext() - const page: Page = await context.newPage() + const browser = await chromium.launch() + const context = await browser.newContext() + const page = await context.newPage() await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "domcontentloaded" }) // Type in password diff --git a/test/helpers.ts b/test/helpers.ts new file mode 100644 index 00000000..07a85996 --- /dev/null +++ b/test/helpers.ts @@ -0,0 +1,14 @@ +// Borrowed from playwright +export interface Cookie { + name: string + value: string + domain: string + path: string + /** + * Unix time in seconds. + */ + expires: number + httpOnly: boolean + secure: boolean + sameSite: "Strict" | "Lax" | "None" +} From 5857b250797e292190343bae0709d8b36f44948c Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Fri, 12 Feb 2021 11:58:56 -0700 Subject: [PATCH 17/29] chore: add todo regarding storage and cookies e2e --- test/goHome.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/goHome.test.ts b/test/goHome.test.ts index f688df05..86431c56 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -42,6 +42,12 @@ describe("go home", () => { // this will return the cookies with no changes // otherwise if it doesn't exist, it will create it // hence the name maybeUpdatedCookies + // + // TODO(@jsjoeio) + // The playwright storage thing sometimes works and sometimes doesn't. We should investigate this further + // at some point. + // See discussion: https://github.com/cdr/code-server/pull/2648#discussion_r575434946 + const maybeUpdatedCookies = createCookieIfDoesntExist(cookies, cookieToStore) context = await browser.newContext({ From b0fd55463bda2855e3e7f53f7c12b2ecaf71257d Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Fri, 12 Feb 2021 12:08:34 -0700 Subject: [PATCH 18/29] refactor: add constants.ts with PASSWORD, etc --- test/constants.ts | 3 +++ test/e2e.test.ts | 2 +- test/globalSetup.ts | 5 +++-- test/goHome.test.ts | 9 +++++---- test/login.test.ts | 5 +++-- test/util.test.ts | 3 +-- 6 files changed, 16 insertions(+), 11 deletions(-) create mode 100644 test/constants.ts diff --git a/test/constants.ts b/test/constants.ts new file mode 100644 index 00000000..ac2250e1 --- /dev/null +++ b/test/constants.ts @@ -0,0 +1,3 @@ +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 || "" diff --git a/test/e2e.test.ts b/test/e2e.test.ts index b7cf3739..915b111c 100644 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -17,7 +17,7 @@ afterEach(async () => { }) it("should see the login page", async () => { - await page.goto("http://localhost:8080") + await page.goto(process.env) // It should send us to the login page expect(await page.title()).toBe("code-server login") }) diff --git a/test/globalSetup.ts b/test/globalSetup.ts index dcf5047d..8a974688 100644 --- a/test/globalSetup.ts +++ b/test/globalSetup.ts @@ -2,6 +2,7 @@ // so that it authenticates us into code-server // ensuring that we're logged in before we run any tests import { chromium } from "playwright" +import { CODE_SERVER_ADDRESS, PASSWORD } from "./constants" module.exports = async () => { console.log("🚨 Running Global Setup for Jest Tests") @@ -10,9 +11,9 @@ module.exports = async () => { const context = await browser.newContext() const page = await context.newPage() - await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "domcontentloaded" }) + await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "domcontentloaded" }) // Type in password - await page.fill(".password", process.env.PASSWORD || "password") + await page.fill(".password", PASSWORD) // Click the submit button and login await page.click(".submit") diff --git a/test/goHome.test.ts b/test/goHome.test.ts index 86431c56..e9a56dd5 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -1,6 +1,7 @@ import { chromium, Page, Browser, BrowserContext, Cookie } from "playwright" import { createCookieIfDoesntExist } from "../src/common/util" import { hash } from "../src/node/util" +import { CODE_SERVER_ADDRESS, PASSWORD, STORAGE } from "./constants" async function setTimeoutPromise(milliseconds: number): Promise { return new Promise((resolve, _) => { @@ -18,12 +19,12 @@ describe("go home", () => { beforeAll(async () => { browser = await chromium.launch() // Create a new context with the saved storage state - const storageState = JSON.parse(process.env.STORAGE || "{}") + const storageState = JSON.parse(STORAGE) || {} const cookieToStore = { sameSite: "Lax" as const, name: "key", - value: hash(process.env.PASSWORD || ""), + value: hash(PASSWORD), domain: "localhost", path: "/", expires: -1, @@ -72,7 +73,7 @@ describe("go home", () => { it("should see a 'Go Home' button in the Application Menu that goes to /healthz", async () => { let requestedGoHomeUrl = false - const GO_HOME_URL = `${process.env.CODE_SERVER_ADDRESS}/healthz` + const GO_HOME_URL = `${CODE_SERVER_ADDRESS}/healthz` page.on("request", (request) => { // This ensures that we did make a request to the GO_HOME_URL // Most reliable way to test button @@ -89,7 +90,7 @@ describe("go home", () => { // waitUntil: "domcontentloaded" // In case the page takes a long time to load - await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080", { waitUntil: "domcontentloaded" }) + await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "domcontentloaded" }) // Click the Home menu await page.click(".home-bar ul[aria-label='Home'] li") diff --git a/test/login.test.ts b/test/login.test.ts index 5358adaa..b269acbd 100644 --- a/test/login.test.ts +++ b/test/login.test.ts @@ -1,4 +1,5 @@ import { chromium, Page, Browser, BrowserContext } from "playwright" +import { CODE_SERVER_ADDRESS, PASSWORD } from "./constants" describe("login", () => { let browser: Browser @@ -25,9 +26,9 @@ describe("login", () => { }) it("should be able to login", async () => { - await page.goto(process.env.CODE_SERVER_ADDRESS || "http://localhost:8080") + await page.goto(CODE_SERVER_ADDRESS) // Type in password - await page.fill(".password", process.env.PASSWORD || "password") + await page.fill(".password", PASSWORD) // Click the submit button and login await page.click(".submit") // See the editor diff --git a/test/util.test.ts b/test/util.test.ts index 91a4315f..535b60a1 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -19,6 +19,7 @@ import { } from "../src/common/util" import { Cookie as CookieEnum } from "../src/node/routes/login" import { hash } from "../src/node/util" +import { PASSWORD } from "./constants" const dom = new JSDOM() global.document = dom.window.document @@ -263,7 +264,6 @@ describe("util", () => { describe("checkForCookie", () => { it("should check if the cookie exists and has a value", () => { - const PASSWORD = "123supersecure!" const fakeCookies: Cookie[] = [ { name: CookieEnum.Key, @@ -286,7 +286,6 @@ describe("util", () => { describe("createCookieIfDoesntExist", () => { it("should create a cookie if it doesn't exist", () => { - const PASSWORD = "123supersecure" const cookies: Cookie[] = [] const cookieToStore = { name: CookieEnum.Key, From d61bbc4c4fc8131dcfcddf85d24528caa9052daf Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Fri, 12 Feb 2021 12:21:01 -0700 Subject: [PATCH 19/29] refactor(goHome): check url, remove timeout --- test/goHome.test.ts | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/test/goHome.test.ts b/test/goHome.test.ts index e9a56dd5..9d5b8e5e 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -3,14 +3,6 @@ import { createCookieIfDoesntExist } from "../src/common/util" import { hash } from "../src/node/util" import { CODE_SERVER_ADDRESS, PASSWORD, STORAGE } from "./constants" -async function setTimeoutPromise(milliseconds: number): Promise { - return new Promise((resolve, _) => { - setTimeout(() => { - resolve() - }, milliseconds) - }) -} - describe("go home", () => { let browser: Browser let page: Page @@ -71,18 +63,7 @@ describe("go home", () => { // NOTE: this test will fail if you do not run code-server with --home $CODE_SERVER_ADDRESS/healthz it("should see a 'Go Home' button in the Application Menu that goes to /healthz", async () => { - let requestedGoHomeUrl = false - const GO_HOME_URL = `${CODE_SERVER_ADDRESS}/healthz` - page.on("request", (request) => { - // This ensures that we did make a request to the GO_HOME_URL - // Most reliable way to test button - // because we don't care if the request has a response - // only that it was made - if (request.url() === GO_HOME_URL) { - requestedGoHomeUrl = true - } - }) // Sometimes a dialog shows up when you navigate // asking if you're sure you want to leave // so we listen if it comes, we accept it @@ -101,10 +82,7 @@ describe("go home", () => { // Click it and navigate to /healthz // NOTE: ran into issues of it failing intermittently // without having button: "middle" - await Promise.all([ - page.waitForNavigation(), - page.click(goHomeButton, { button: "middle" }) - ]) + await Promise.all([page.waitForNavigation(), page.click(goHomeButton, { button: "middle" })]) expect(page.url()).toBe(GO_HOME_URL) }) }) From 6d4f814f849513e7a65f1d400f72ce7d86b25528 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 12 Feb 2021 13:37:27 -0600 Subject: [PATCH 20/29] Close context before browser This seems to resolve a warning about a process being forcefully exited. --- test/goHome.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/goHome.test.ts b/test/goHome.test.ts index 9d5b8e5e..dde20475 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -53,8 +53,8 @@ describe("go home", () => { // Remove password from local storage await context.clearCookies() - await browser.close() await context.close() + await browser.close() }) beforeEach(async () => { From ef7e7271b65e940fe49868ca306d1a053132b4e2 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 12 Feb 2021 13:41:45 -0600 Subject: [PATCH 21/29] Fix unreadable wtfnode output --- test/wtfnode.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/wtfnode.ts b/test/wtfnode.ts index 2dfce59a..2d31a4e6 100644 --- a/test/wtfnode.ts +++ b/test/wtfnode.ts @@ -1,7 +1,23 @@ +import * as util from "util" import * as wtfnode from "wtfnode" +// Jest seems to hijack console.log in a way that makes the output difficult to +// read. So we'll write directly to process.stderr instead. +const write = (...args: [any, ...any]) => { + if (args.length > 0) { + process.stderr.write(util.format(...args) + "\n") + } +} +wtfnode.setLogger("info", write) +wtfnode.setLogger("warn", write) +wtfnode.setLogger("error", write) + let active = false +/** + * Start logging open handles periodically. This can be used to see what is + * hanging open if anything. + */ export function setup(): void { if (active) { return From 6685b3a4ff534d4f11c2bde9408d984482a766f3 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 12 Feb 2021 13:41:59 -0600 Subject: [PATCH 22/29] Move wtfnode setup to global setup I think Jest provides separate console methods for each test so when the socket tests finish Jest complains that a test keeps trying to output. --- test/globalSetup.ts | 3 +++ test/socket.test.ts | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/globalSetup.ts b/test/globalSetup.ts index 8a974688..a3cd485e 100644 --- a/test/globalSetup.ts +++ b/test/globalSetup.ts @@ -3,6 +3,7 @@ // ensuring that we're logged in before we run any tests import { chromium } from "playwright" import { CODE_SERVER_ADDRESS, PASSWORD } from "./constants" +import * as wtfnode from "./wtfnode" module.exports = async () => { console.log("🚨 Running Global Setup for Jest Tests") @@ -11,6 +12,8 @@ module.exports = async () => { const context = await browser.newContext() const page = await context.newPage() + wtfnode.setup() + await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "domcontentloaded" }) // Type in password await page.fill(".password", PASSWORD) diff --git a/test/socket.test.ts b/test/socket.test.ts index aadf86b4..e614e94d 100644 --- a/test/socket.test.ts +++ b/test/socket.test.ts @@ -6,11 +6,8 @@ import * as tls from "tls" import { Emitter } from "../src/common/emitter" import { SocketProxyProvider } from "../src/node/socket" import { generateCertificate, tmpdir } from "../src/node/util" -import * as wtfnode from "./wtfnode" describe("SocketProxyProvider", () => { - wtfnode.setup() - const provider = new SocketProxyProvider() const onServerError = new Emitter<{ event: string; error: Error }>() From 47a05c998a2a81d7d6ef9ce876dcfd8e0c26a0d8 Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 12 Feb 2021 13:43:11 -0600 Subject: [PATCH 23/29] Gate wtfnode behind WTF_NODE env var After thinking about it some more it's probably mostly only useful to see the output when the tests are hanging. Otherwise there's a lot of noise about Jest child processes and pipes. --- package.json | 3 --- src/common/util.ts | 22 ---------------------- test/e2e.test.ts | 3 ++- test/globalSetup.ts | 8 +++++--- test/goHome.test.ts | 2 +- test/helpers.ts | 21 +++++++++++++++++++++ test/util.test.ts | 3 +-- 7 files changed, 30 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 1df87c1a..7f427a78 100644 --- a/package.json +++ b/package.json @@ -143,9 +143,6 @@ "lines": 40 } }, - "modulePathIgnorePatterns": [ - "/release" - ], "testTimeout": 30000, "globalSetup": "/test/globalSetup.ts", "modulePathIgnorePatterns": [ diff --git a/src/common/util.ts b/src/common/util.ts index e8edc13b..87ca6f59 100644 --- a/src/common/util.ts +++ b/src/common/util.ts @@ -1,5 +1,4 @@ import { logger, field } from "@coder/logger" -import { Cookie } from "../../test/helpers" export interface Options { base: string @@ -121,24 +120,3 @@ export function logError(prefix: string, err: any): void { logger.error(`${prefix}: ${err}`) } } - -/** - * Checks if a cookie exists in array of cookies - */ -export function checkForCookie(cookies: Array, key: string): boolean { - // Check for a cookie where the name is equal to key - return Boolean(cookies.find((cookie) => cookie.name === key)) -} - -/** - * Creates a login cookie if one doesn't already exist - */ -export function createCookieIfDoesntExist(cookies: Array, cookieToStore: Cookie): Array { - const cookieName = cookieToStore.name - const doesCookieExist = checkForCookie(cookies, cookieName) - if (!doesCookieExist) { - const updatedCookies = [...cookies, cookieToStore] - return updatedCookies - } - return cookies -} diff --git a/test/e2e.test.ts b/test/e2e.test.ts index 915b111c..21df386b 100644 --- a/test/e2e.test.ts +++ b/test/e2e.test.ts @@ -1,4 +1,5 @@ import { chromium, Page, Browser } from "playwright" +import { CODE_SERVER_ADDRESS } from "./constants" let browser: Browser let page: Page @@ -17,7 +18,7 @@ afterEach(async () => { }) it("should see the login page", async () => { - await page.goto(process.env) + await page.goto(CODE_SERVER_ADDRESS) // It should send us to the login page expect(await page.title()).toBe("code-server login") }) diff --git a/test/globalSetup.ts b/test/globalSetup.ts index a3cd485e..5ef45faa 100644 --- a/test/globalSetup.ts +++ b/test/globalSetup.ts @@ -6,13 +6,15 @@ import { CODE_SERVER_ADDRESS, PASSWORD } from "./constants" import * as wtfnode from "./wtfnode" module.exports = async () => { - console.log("🚨 Running Global Setup for Jest Tests") - console.log(" Please hang tight...") + console.log("\n🚨 Running Global Setup for Jest Tests") + console.log(" Please hang tight...") const browser = await chromium.launch() const context = await browser.newContext() const page = await context.newPage() - wtfnode.setup() + if (process.env.WTF_NODE) { + wtfnode.setup() + } await page.goto(CODE_SERVER_ADDRESS, { waitUntil: "domcontentloaded" }) // Type in password diff --git a/test/goHome.test.ts b/test/goHome.test.ts index dde20475..31cd773c 100644 --- a/test/goHome.test.ts +++ b/test/goHome.test.ts @@ -1,7 +1,7 @@ import { chromium, Page, Browser, BrowserContext, Cookie } from "playwright" -import { createCookieIfDoesntExist } from "../src/common/util" import { hash } from "../src/node/util" import { CODE_SERVER_ADDRESS, PASSWORD, STORAGE } from "./constants" +import { createCookieIfDoesntExist } from "./helpers" describe("go home", () => { let browser: Browser diff --git a/test/helpers.ts b/test/helpers.ts index 07a85996..193fd0ca 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -12,3 +12,24 @@ export interface Cookie { secure: boolean sameSite: "Strict" | "Lax" | "None" } + +/** + * Checks if a cookie exists in array of cookies + */ +export function checkForCookie(cookies: Array, key: string): boolean { + // Check for a cookie where the name is equal to key + return Boolean(cookies.find((cookie) => cookie.name === key)) +} + +/** + * Creates a login cookie if one doesn't already exist + */ +export function createCookieIfDoesntExist(cookies: Array, cookieToStore: Cookie): Array { + const cookieName = cookieToStore.name + const doesCookieExist = checkForCookie(cookies, cookieName) + if (!doesCookieExist) { + const updatedCookies = [...cookies, cookieToStore] + return updatedCookies + } + return cookies +} diff --git a/test/util.test.ts b/test/util.test.ts index 535b60a1..0de2d7ea 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -13,13 +13,12 @@ import { resolveBase, split, trimSlashes, - checkForCookie, - createCookieIfDoesntExist, normalize, } from "../src/common/util" import { Cookie as CookieEnum } from "../src/node/routes/login" import { hash } from "../src/node/util" import { PASSWORD } from "./constants" +import { checkForCookie, createCookieIfDoesntExist } from "./helpers" const dom = new JSDOM() global.document = dom.window.document From a2b24321c02d8a4ca15231fea4a76f9d193b5889 Mon Sep 17 00:00:00 2001 From: G r e y Date: Thu, 25 Feb 2021 13:20:44 -0500 Subject: [PATCH 24/29] chore: Format docs (#2774) See also: #2771 --- docs/FAQ.md | 2 +- docs/install.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/FAQ.md b/docs/FAQ.md index e0cad695..d36d70af 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -4,7 +4,7 @@ - [Questions?](#questions) - [iPad Status?](#ipad-status) -- [Community Projects (awesome-code-server)](#awesome-code-server) +- [Community projects (awesome-code-server)](#community-projects-awesome-code-server) - [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration) - [Differences compared to VS Code?](#differences-compared-to-vs-code) - [How can I request a missing extension?](#how-can-i-request-a-missing-extension) diff --git a/docs/install.md b/docs/install.md index b764046c..e37389d2 100644 --- a/docs/install.md +++ b/docs/install.md @@ -14,7 +14,7 @@ - [Standalone Releases](#standalone-releases) - [Docker](#docker) - [helm](#helm) -- [App Engines (Azure, Heroku)](#app-engines) +- [App Engines (Azure, Heroku)](#app-engines-azure-heroku) From a5edbcb6b6f8788e367451891b61795e912ac15f Mon Sep 17 00:00:00 2001 From: Asher Date: Thu, 25 Feb 2021 12:48:03 -0600 Subject: [PATCH 25/29] Add reviewer group as a codeowner (#2777) --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 64614c98..b7e6805b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1,3 @@ +* @cdr/code-server-reviewers + ci/helm-chart @Matthew-Beckett @alexgorbatchev From 077af0511e6bd0f3815c3fe428d3e439619d560f Mon Sep 17 00:00:00 2001 From: G r e y Date: Thu, 25 Feb 2021 14:08:54 -0500 Subject: [PATCH 26/29] fix: Responsive Sign In Page (#2770) --- src/browser/pages/login.css | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/browser/pages/login.css b/src/browser/pages/login.css index f0586ee8..026cac97 100644 --- a/src/browser/pages/login.css +++ b/src/browser/pages/login.css @@ -1,4 +1,6 @@ body { + min-height: 568px; + min-width: 320px; overflow: auto; } @@ -15,6 +17,12 @@ body { width: 100%; } +@media (max-width: 600px) { + .login-form > .field { + flex-direction: column; + } +} + .login-form > .error { color: red; margin-top: 16px; @@ -38,6 +46,13 @@ body { margin-left: 20px; } +@media (max-width: 600px) { + .login-form > .field > .submit { + margin-left: 0px; + margin-top: 16px; + } +} + input { -webkit-appearance: none; } From bf4779991e1167040a74cfdd8849c6a041040742 Mon Sep 17 00:00:00 2001 From: G r e y Date: Thu, 25 Feb 2021 14:09:21 -0500 Subject: [PATCH 27/29] docs: Update contributing requirements (#2775) Resolves: #2771 --- docs/CONTRIBUTING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index edaaf767..e77b0e9e 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -35,8 +35,9 @@ There are several differences, however. You must: - Use Node.js version 12.x (or greater) - Have [yarn](https://classic.yarnpkg.com/en/) installed (which is used to install JS packages and run development scripts) - Have [nfpm](https://github.com/goreleaser/nfpm) (which is used to build `.deb` and `.rpm` packages and [jq](https://stedolan.github.io/jq/) (used to build code-server releases) installed +- Have [shfmt](https://pkg.go.dev/mvdan.cc/sh/v3) installed to run `yarn fmt` (requires Go is installed on your system) -The [CI container](../ci/images/debian8/Dockerfile) is a useful reference for all +The [CI container](../ci/images/debian10/Dockerfile) is a useful reference for all of the dependencies code-server uses. ## Development Workflow From ad89ffaa59f52c6f9faab045474ee3e49262bc8d Mon Sep 17 00:00:00 2001 From: G r e y Date: Thu, 25 Feb 2021 13:15:28 -0600 Subject: [PATCH 28/29] docs(contributing): Update links --- docs/CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e77b0e9e..67377ee2 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -121,10 +121,10 @@ node ./release The `code-server` script serves an HTTP API for login and starting a remote VS Code process. -The CLI code is in [./src/node](./src/node) and the HTTP routes are implemented in -[./src/node/app](./src/node/app). +The CLI code is in [src/node](../src/node) and the HTTP routes are implemented in +[src/node/routes](../src/node/routes). -Most of the meaty parts are in the VS Code portion of the codebase under [./lib/vscode](./lib/vscode), which we described next. +Most of the meaty parts are in the VS Code portion of the codebase under [lib/vscode](../lib/vscode), which we described next. ### Modifications to VS Code @@ -134,7 +134,7 @@ and exposed an API to the front-end for file access and all UI needs. Over time, Microsoft added support to VS Code to run it on the web. They have made the front-end open source, but not the server. As such, code-server v2 (and later) uses -the VS Code front-end and implements the server. We do this by using a git subtree to fork and modify VS Code. This code lives under [./lib/vscode](./lib/vscode). +the VS Code front-end and implements the server. We do this by using a git subtree to fork and modify VS Code. This code lives under [lib/vscode](../lib/vscode). Some noteworthy changes in our version of VS Code: From 99af11ecc3b7602e30cdb13bfe05e693a165af38 Mon Sep 17 00:00:00 2001 From: Joe Previte Date: Wed, 24 Feb 2021 12:23:44 -0700 Subject: [PATCH 29/29] docs: add homebrew bump to release steps --- ci/README.md | 5 +++-- docs/FAQ.md | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ci/README.md b/ci/README.md index c063b88c..4ebda4d8 100644 --- a/ci/README.md +++ b/ci/README.md @@ -43,8 +43,9 @@ Make sure you have `$GITHUB_TOKEN` set and [hub](https://github.com/github/hub) 9. Update the AUR package. - Instructions on updating the AUR package are at [cdr/code-server-aur](https://github.com/cdr/code-server-aur). 10. Wait for the npm package to be published. -11. Update the homebrew package. - - Send a pull request to [homebrew-core](https://github.com/Homebrew/homebrew-core) with the URL in the [formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb) updated. +11. Update the [homebrew package](https://github.com/Homebrew/homebrew-core/blob/master/Formula/code-server.rb). + 1. Install [homebrew](https://brew.sh/) + 2. Run `brew bump-formula-pr --version=3.8.1 code-server` and update the version accordingly. This will bump the version and open a PR. Note: this will only work once the version is published on npm. ## Updating Code Coverage in README diff --git a/docs/FAQ.md b/docs/FAQ.md index d36d70af..9b08b58d 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -4,7 +4,7 @@ - [Questions?](#questions) - [iPad Status?](#ipad-status) -- [Community projects (awesome-code-server)](#community-projects-awesome-code-server) +- [Community Projects (awesome-code-server)](#community-projects-awesome-code-server) - [How can I reuse my VS Code configuration?](#how-can-i-reuse-my-vs-code-configuration) - [Differences compared to VS Code?](#differences-compared-to-vs-code) - [How can I request a missing extension?](#how-can-i-request-a-missing-extension) @@ -43,7 +43,7 @@ Please file all questions and support requests at https://github.com/cdr/code-se Please see [./ipad.md](./ipad.md). -## Community projects (awesome-code-server) +## Community Projects (awesome-code-server) Visit the [awesome-code-server](https://github.com/cdr/awesome-code-server) repository to view community projects and guides with code-server! Feel free to add your own!