Skip to content

Commit cd16a28

Browse files
authored
RHOAIENG-15152: test(tests/browser): add playwright test for starting code-server (#774)
* RHOAIENG-15152: feat(codeserver/e2e): add playwright test for starting code-server * DO-NOT-COMMIT: trigger GHA * fixup typo in utils.ts * fixup default to not using existing CDP browser instance * fixup remove commented code about using dotenv * fixup print more meaningful exception when misconfigured * fixup forgot to change paths in gha * suppress the npm funding message * Revert "DO-NOT-COMMIT: trigger GHA" This reverts commit 629e732.
1 parent 3ceb400 commit cd16a28

File tree

6 files changed

+489
-12
lines changed

6 files changed

+489
-12
lines changed

.github/workflows/build-notebooks-TEMPLATE.yaml

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ jobs:
134134
if: "${{ fromJson(inputs.github).event_name == 'pull_request' }}"
135135
env:
136136
IMAGE_TAG: "${{ github.sha }}"
137-
IMAGE_REGISTRY: "localhost:5000/workbench-images"
137+
IMAGE_REGISTRY: "ghcr.io/${{ github.repository }}/workbench-images"
138138
CONTAINER_BUILD_CACHE_ARGS: "--cache-from ${{ env.CACHE }}"
139139
# We don't have access to image registry, so disable pushing
140140
PUSH_IMAGES: "no"
@@ -155,7 +155,7 @@ jobs:
155155
TARGET="$FS_SCAN_FOLDER"
156156
TYPE="fs"
157157
else
158-
TARGET="localhost:5000/workbench-images:${{ inputs.target }}-${{ github.sha }}"
158+
TARGET="ghcr.io/${{ github.repository }}/workbench-images:${{ inputs.target }}-${{ github.sha }}"
159159
TYPE="image"
160160
fi
161161
elif [[ "$EVENT_NAME" == "schedule" ]]; then
@@ -215,5 +215,50 @@ jobs:
215215
216216
cat $REPORT_FOLDER/$REPORT_FILE >> $GITHUB_STEP_SUMMARY
217217
218+
# https://playwright.dev/docs/ci
219+
# https://playwright.dev/docs/docker
220+
# we leave little free disk space after we mount LVM for podman storage
221+
# not enough to install playwright; running playwright in podman uses the space we have
222+
- name: Run Playwright tests
223+
if: ${{ fromJson(inputs.github).event_name == 'pull_request' && contains(inputs.target, 'codeserver') }}
224+
# --ipc=host because Microsoft says so in Playwright docs
225+
# --net=host because testcontainers connects to the Reaper container's exposed port
226+
# we need to pass through the relevant environment variables
227+
# DEBUG configures nodejs debuggers, sets different verbosity as needed
228+
# CI=true is set on every CI nowadays
229+
# PODMAN_SOCK should be mounted to /var/run/docker.sock, other likely mounting locations may not exist (mkdir -p)
230+
# TEST_TARGET is the workbench image the test will run
231+
# --volume(s) let us access docker socket and not clobber host's node_modules
232+
run: |
233+
podman run \
234+
--interactive --rm \
235+
--ipc=host \
236+
--net=host \
237+
--env "CI=true" \
238+
--env "NPM_CONFIG_fund=false" \
239+
--env "DEBUG=testcontainers:*" \
240+
--env "PODMAN_SOCK=/var/run/docker.sock" \
241+
--env "TEST_TARGET" \
242+
--volume ${PODMAN_SOCK}:/var/run/docker.sock \
243+
--volume ${PWD}:/mnt \
244+
--volume /mnt/node_modules \
245+
mcr.microsoft.com/playwright:v1.48.1-noble \
246+
/bin/bash <<EOF
247+
set -Eeuxo pipefail
248+
cd /mnt
249+
npm install -g pnpm && pnpm install
250+
pnpm exec playwright test
251+
exit 0
252+
EOF
253+
working-directory: tests/browser
254+
env:
255+
TEST_TARGET: "ghcr.io/${{ github.repository }}/workbench-images:${{ inputs.target }}-${{ github.sha }}"
256+
- uses: actions/upload-artifact@v4
257+
if: ${{ !cancelled() && fromJson(inputs.github).event_name == 'pull_request' && contains(inputs.target, 'codeserver') }}
258+
with:
259+
name: "${{ inputs.target }}_playwright-report"
260+
path: tests/browser/playwright-report/
261+
retention-days: 30
262+
218263
- run: df -h
219264
if: "${{ !cancelled() }}"

tests/browser/playwright.config.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,6 @@
11
import { defineConfig, devices } from '@playwright/test';
22
import * as process from "node:process";
33

4-
/**
5-
* Read environment variables from file.
6-
* https://github.com/motdotla/dotenv
7-
*/
8-
// import dotenv from 'dotenv';
9-
// import path from 'path';
10-
// dotenv.config({ path: path.resolve(__dirname, '.env') });
11-
124
/**
135
* See https://playwright.dev/docs/test-configuration.
146
*/
@@ -69,10 +61,10 @@ function getProjects() {
6961
use: { ...devices['Desktop Chrome'], channel: 'chrome',
7062
headless: false, // the CDP browser configured below is not affected by this
7163
/* custom properties, comment out as needed */
72-
connectCDP: 9222, // false | number: connect to an existing browser running at given port
64+
connectCDP: false, // false | number: connect to an existing browser running at given port (e.g. 9222)
7365
codeServerSource: { // prefers url if specified, otherwise will start the specified docker image
7466
// url: "", // not-present | string
75-
image: "quay.io/modh/codeserver:codeserver-ubi9-python-3.11-2024b-20241018", // string
67+
image: "quay.io/modh/codeserver:codeserver-ubi9-python-3.9-20241114-aed66a4", // string
7668
}
7769
},
7870
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as path from "node:path";
2+
3+
import { test as base, expect, chromium } from '@playwright/test';
4+
5+
import {GenericContainer} from "testcontainers";
6+
import {HttpWaitStrategy} from "testcontainers/build/wait-strategies/http-wait-strategy";
7+
8+
import {CodeServer} from "./models/codeserver"
9+
10+
import {setupTestcontainers} from "./testcontainers";
11+
12+
import * as utils from './utils'
13+
14+
// Declare the types of your fixtures.
15+
type MyFixtures = {
16+
connectCDP: false | number;
17+
codeServerSource: {url?: string, image?: string};
18+
codeServer: CodeServer
19+
};
20+
const test = base.extend<MyFixtures>({
21+
connectCDP: [false, {option: true}],
22+
codeServerSource: [{url:'http://localhost:8787'}, {option: true}],
23+
page: async ({ page, connectCDP }, use) => {
24+
if (!connectCDP) {
25+
await use(page)
26+
} else {
27+
// we close the provided page and send onwards our own
28+
await page.close()
29+
{
30+
const browser = await chromium.connectOverCDP(`http://localhost:${connectCDP}`);
31+
const defaultContext = browser.contexts()[0];
32+
const page = defaultContext.pages()[0];
33+
await use(page)
34+
}
35+
}
36+
},
37+
codeServer: [async ({ page, codeServerSource }, use) => {
38+
if (codeServerSource?.url) {
39+
await use(new CodeServer(page, codeServerSource.url))
40+
} else {
41+
const image = codeServerSource.image ?? (() => {
42+
throw new Error("invalid config: codeserver image not specified")
43+
})()
44+
const container = await new GenericContainer(image)
45+
.withExposedPorts(8787)
46+
.withWaitStrategy(new HttpWaitStrategy('/', 8787, {abortOnContainerExit: true}))
47+
.start();
48+
await use(new CodeServer(page, `http://${container.getHost()}:${container.getMappedPort(8787)}`))
49+
await container.stop()
50+
}
51+
}, {timeout: 10 * 60 * 1000}],
52+
});
53+
54+
test.beforeAll(setupTestcontainers)
55+
56+
test('open codeserver', async ({codeServer, page}) => {
57+
await page.goto(codeServer.url)
58+
59+
await codeServer.isEditorVisible()
60+
})
61+
62+
test('wait for welcome screen to load', async ({codeServer, page}, testInfo) => {
63+
await page.goto(codeServer.url);
64+
65+
await codeServer.isEditorVisible()
66+
page.on("console", console.log)
67+
68+
await codeServer.isEditorVisible()
69+
await utils.waitForStableDOM(page, "div.monaco-workbench", 1000, 10000)
70+
await utils.waitForNextRender(page)
71+
72+
await utils.takeScreenshot(page, testInfo, "welcome.png")
73+
})
74+
75+
test('use the terminal to run command', async ({codeServer, page}, testInfo) => {
76+
await page.goto(codeServer.url);
77+
78+
await test.step("Should always see the code-server editor", async () => {
79+
expect(await codeServer.isEditorVisible()).toBe(true)
80+
})
81+
82+
await test.step("should show the Integrated Terminal", async () => {
83+
await codeServer.focusTerminal()
84+
expect(await page.isVisible("#terminal")).toBe(true)
85+
})
86+
87+
await test.step("should execute Terminal command successfully", async () => {
88+
await page.keyboard.type('echo The answer is $(( 6 * 7 )). > answer.txt', {delay: 100})
89+
await page.keyboard.press('Enter', {delay: 100})
90+
})
91+
92+
await test.step("should open the file", async() => {
93+
const file = path.join('/opt/app-root/src', 'answer.txt')
94+
await codeServer.openFile(file)
95+
await expect(page.getByText("The answer is 42.")).toBeVisible()
96+
})
97+
98+
})

0 commit comments

Comments
 (0)