Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add end-to-end tests with Playwright #516

Merged
merged 57 commits into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
df03498
chore: add a workspace for e2e testing
rjaegers Jul 24, 2024
83ddb8a
chore: add a containerized playwright setup
rjaegers Jul 24, 2024
1e4e4e9
chore: rename .devcontainer.json
rjaegers Jul 24, 2024
aa21602
chore: move devcontainer.json
rjaegers Jul 24, 2024
38655b9
test: depend on 'latest' for now
rjaegers Jul 24, 2024
e12bc90
ci: fix linter findings
rjaegers Jul 24, 2024
a15d621
test: fix incorrect workspaceFolder
rjaegers Jul 24, 2024
31937f7
test: fix workspaceFolder by using ${containerWorkspaceFolder}
rjaegers Jul 24, 2024
da6fb9c
test: different strategy to move workspace folder
rjaegers Jul 24, 2024
929fde7
test: hard-code workspaceFolder
rjaegers Jul 24, 2024
fe4d21f
test: add authentication flow for GitHub
rjaegers Jul 25, 2024
5483f23
refactor: remove unused import
rjaegers Jul 25, 2024
4e0ed9c
chore: add npm test
rjaegers Jul 25, 2024
d406afa
ci: add acceptance tests
rjaegers Jul 25, 2024
727658b
ci: trying to run setup
rjaegers Jul 25, 2024
9f98d3f
test: enable running tests from VS Code extension
rjaegers Jul 25, 2024
f06a1c7
chore: add required extension
rjaegers Jul 25, 2024
79fb4ae
ci: try to dynamically choose the image again
rjaegers Jul 25, 2024
e9381e6
ci: fix starting from incorrect path
rjaegers Jul 25, 2024
9014758
ci: inject env secrets
rjaegers Jul 25, 2024
7f6d90e
ci: create codespace under test
rjaegers Jul 25, 2024
9544055
ci: fix linter findings
rjaegers Jul 25, 2024
91827bb
ci: get codespace going
rjaegers Jul 25, 2024
b4d0099
ci: rtfm
rjaegers Jul 25, 2024
26db80e
test: inspect host environment before starting codespace
rjaegers Jul 26, 2024
e114d9c
ci: communicate image version via codespaces secret
rjaegers Jul 26, 2024
6f6b7bb
ci: test if token already has correct scopes
rjaegers Jul 26, 2024
eaa91ef
ci: able to start codespace, yay
rjaegers Jul 26, 2024
74d80e3
chore: fix linter findings
rjaegers Jul 27, 2024
8aa4cf6
Merge branch 'main' into feature/e2e-tests
ronj Jul 29, 2024
bf30129
Merge branch 'main' into feature/e2e-tests
rjaegers Jul 29, 2024
2413e85
chore: first working test
rjaegers Jul 29, 2024
422f165
chore: fix linter findings
rjaegers Jul 29, 2024
ba0f5c7
ci: add harden-runner action
rjaegers Jul 30, 2024
577ea70
test: use a page object model paradigm
rjaegers Jul 31, 2024
ddd4555
test: mark the beforeEach as slow because of first-time-start
rjaegers Jul 31, 2024
f39b8af
test: make start-up more robust
rjaegers Jul 31, 2024
2fb06dc
test: different timeout strategy
rjaegers Jul 31, 2024
3ca0754
ci: fail on create codespace and clean-up afterwards
rjaegers Jul 31, 2024
ccfa4f1
test: more timeout
rjaegers Jul 31, 2024
c536283
test: give the build more time to finish
rjaegers Jul 31, 2024
d4e8554
test: increase timeout even more
rjaegers Jul 31, 2024
5aa3d89
ci: move acceptance test to the right place
rjaegers Jul 31, 2024
072c5ed
ci: correct issues
rjaegers Jul 31, 2024
b39b2ae
Merge branch 'main' into feature/e2e-tests
rjaegers Aug 1, 2024
1d6209c
test: add more tests and make them robust
rjaegers Aug 1, 2024
807c741
Merge branch 'feature/e2e-tests' of https://github.com/philips-softwa…
rjaegers Aug 1, 2024
55ff007
test: use proper test.describe
rjaegers Aug 1, 2024
ee4036c
test: increase robustness
rjaegers Aug 20, 2024
75020d9
test: enable discovery of code smells
rjaegers Aug 20, 2024
fe07e95
docs: fix badges
rjaegers Aug 20, 2024
531938a
Merge branch 'main' into feature/e2e-tests
rjaegers Aug 21, 2024
eb49578
Merge branch 'main' into feature/e2e-tests
rjaegers Aug 26, 2024
74c379c
chore: reset IMAGE_VERSION after job completion
rjaegers Aug 27, 2024
2a69792
Merge branch 'feature/e2e-tests' of https://github.com/philips-softwa…
rjaegers Aug 27, 2024
47f5a78
chore: update feature desktop-lite
rjaegers Aug 28, 2024
825c320
Merge branch 'main' into feature/e2e-tests
rjaegers Aug 28, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .devcontainer/cpp-test/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"image": "ghcr.io/philips-software/amp-devcontainer-cpp:${localEnv:IMAGE_VERSION}",
"workspaceFolder": "/workspaces/amp-devcontainer/.devcontainer/cpp/e2e/workspace",
"customizations": {
"vscode": {
"settings": {
"cmake.automaticReconfigure": false
}
}
}
}
11 changes: 10 additions & 1 deletion .devcontainer/cpp/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,20 @@
"dockerfile": "Dockerfile",
"context": "../.."
},
"forwardPorts": [6080],
"remoteEnv": {
"CONTAINER_FLAVOR": "cpp"
"CONTAINER_FLAVOR": "cpp",
"NODE_EXTRA_CA_CERTS": "/usr/local/share/ca-certificates/Cisco_Umbrella_Root_CA.crt"
},
"mounts": [
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
],
"features": {
"ghcr.io/devcontainers/features/desktop-lite:1.2.4": {},
"ghcr.io/devcontainers/features/github-cli:1.0.13": {},
"ghcr.io/devcontainers/features/node:1.5.0": {}
},
"postCreateCommand": "npm install && npx playwright install --with-deps",
"customizations": {
"vscode": {
"settings": {
Expand All @@ -23,6 +31,7 @@
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
"[email protected]",
Expand Down
29 changes: 29 additions & 0 deletions .devcontainer/cpp/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig, devices } from '@playwright/test';
import path from 'path';

require('dotenv').config();

export const STORAGE_STATE = path.join(__dirname, 'playwright/.auth/user.json');

export default defineConfig({
testDir: './tests',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: 1,
reporter: 'list',
use: {
trace: 'on-first-retry'
},
projects: [
{ name: 'setup', testMatch: '**/*.setup.ts' },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: STORAGE_STATE
},
dependencies: ['setup']
}
]
});
29 changes: 29 additions & 0 deletions .devcontainer/cpp/e2e/tests/authentication.setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { test as setup } from '@playwright/test';
import * as OTPAuth from 'otpauth';
import { STORAGE_STATE } from '../playwright.config';

setup('authenticate', async ({ page }) => {
await page.goto('https://github.com/login');
await page.getByLabel('Username or email address').fill(process.env.GITHUB_USER!);
await page.getByLabel('Password').fill(process.env.GITHUB_PASSWORD!);
await page.getByRole('button', { name: 'Sign in', exact: true }).click();

let totp = new OTPAuth.TOTP({
issuer: 'GitHub',
label: 'GitHub',
algorithm: 'SHA1',
digits: 6,
period: 30,
secret: process.env.GITHUB_TOTP_SECRET!
});

let code = totp.generate();
await page.getByPlaceholder('XXXXXX').fill(code);

// Wait until the page receives the cookies.
//
// Sometimes login flow sets cookies in the process of several redirects.
// Wait for the final URL to ensure that the cookies are actually set.
await page.waitForURL('https://github.com/');
await page.context().storageState({ path: STORAGE_STATE });
});
103 changes: 103 additions & 0 deletions .devcontainer/cpp/e2e/tests/codespace.pom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { test, expect, type Page, type Locator } from '@playwright/test';

type CommandAndPrompt = {
command: string,
prompt?: string
};

export class CodespacePage {
readonly page: Page;
readonly outputPanel: Locator;
readonly terminal: Locator;

constructor(page: Page) {
this.page = page;
this.outputPanel = page.locator('[id="workbench.panel.output"]');
this.terminal = page.locator('.terminal-widget-container').first();
}

async goto() {
await this.page.goto('https://' + process.env.CODESPACE_NAME + '.github.dev');
}

/**
* Wait for the extensions to be active in the Codespace.
*
* This method is used to verify that the extensions in `extensions` are active in the Codespace.
* Used when waiting for the Codespace to be ready for testing. As the
* extensions are typically activated last, before the Codespace is ready for use.
*
* **Usage**
*
* ```ts
* const codespace = new CodespacePage(page);
* await codespace.areExtensionsActive(['SonarLint', 'CMake', 'Live Share', 'GitHub Pull Requests']);
* ```
*
* @param extensions - The list of extensions to wait for.
*/
async areExtensionsActive(extensions: string[]) {
test.setTimeout(3 * 60 * 1000);

for (const plugin of extensions) {
await expect(this.page.getByRole('tab', { name: plugin }).locator('a')).toBeVisible({ timeout: 5 * 60 * 1000 });
}

await expect(this.page.getByRole('button', { name: 'Activating Extensions...' })).toBeHidden();
}

/**
* Executes the given commands in the terminal.
*
* **Usage**
*
* ```ts
* const codespace = new CodespacePage(page);
* await codespace.executeInTerminal('git clean -fdx');
* ```
*
* @param commands - The commands to execute in the terminal. It can be a single command or an array of commands.
*/
async executeInTerminal(commands: string | string[]) {
await this.page.keyboard.press('Control+Shift+`');
await expect(this.page.locator('.terminal-wrapper.active')).toBeVisible();

for (const command of Array.isArray(commands) ? [...commands + 'exit'] : [commands, 'exit']) {
await this.terminal.pressSequentially(command);
await this.terminal.press('Enter');
}
}

/**
* Executes the given commands in the command palette.
*
* This method waits for `prompt` to appear and then types `command` and presses Enter.
* When no prompt is given the default prompt is used.
*
* @param commands - The commands to execute in the command palette. It can be a single command or an array of commands.
*/
async executeFromCommandPalette(commands: CommandAndPrompt | CommandAndPrompt[]) {
await this.page.keyboard.press('Control+Shift+P');

for (const command of Array.isArray(commands) ? commands : [commands]) {
let prompt = this.page.getByPlaceholder(command.prompt || 'Type the name of a command to run');

await prompt.pressSequentially(command.command);
await prompt.press('Enter');
}
}

/**
* Opens the tab with the given name.
*
* @param name - The name of the tab to open.
*/
async openTabByName(name: string) {
await this.page.getByRole('tab', { name: name }).locator('a').click();
}

async openFileInEditor(name: string) {
await this.page.getByRole('treeitem', { name: name }).locator('a').click();
await expect(this.page.locator('[id="workbench.parts.editor"]')).toContainText(name);
}
}
39 changes: 39 additions & 0 deletions .devcontainer/cpp/e2e/tests/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { test, expect } from '@playwright/test';
import { CodespacePage } from './codespace.pom';

test.beforeEach(async ({ page }) => {
const codespace = new CodespacePage(page);

await codespace.goto();
await codespace.areExtensionsActive(['Testing', 'SonarLint', 'CMake', 'Live Share', 'GitHub Pull Requests']);
await codespace.executeInTerminal('git clean -fdx');
});

test.describe('CMake', () => {
test('should succesfully build without selecting configuration', async ({ page }) => {
const codespace = new CodespacePage(page);

await page.getByRole('button', { name: 'Build the selected target' }).click();
await page.getByLabel('host, Build for host').locator('a').click();
await expect(codespace.outputPanel).toContainText('Build finished with exit code 0', { timeout: 5 * 60 * 1000 });
});

test('should succesfully build after selecting configuration', async ({ page }) => {
const codespace = new CodespacePage(page);

await codespace.openTabByName('CMake');
await expect(page.getByRole('treeitem', { name: 'Change Configure Preset' })).toContainText('[No Configure Preset Selected]');
await expect(page.getByRole('treeitem', { name: 'Change Build Preset' })).toContainText('[No Build Preset Selected]');

await codespace.executeFromCommandPalette([{ command: 'CMake: Select Configure Preset' },
{ command: 'host', prompt: 'Select a configure preset' }]);
await expect(page.getByRole('treeitem', { name: 'Change Configure Preset' })).toContainText('host');

await codespace.executeFromCommandPalette([{ command: 'CMake: Select Build Preset' },
{ command: 'host-Release', prompt: 'Select a build preset' }]);
await expect(page.getByRole('treeitem', { name: 'Change Build Preset' })).toContainText('host-Release');

await page.getByRole('button', { name: 'Build the selected target' }).click();
await expect(codespace.outputPanel).toContainText('Build finished with exit code 0', { timeout: 5 * 60 * 1000 });
});
});
5 changes: 5 additions & 0 deletions .devcontainer/cpp/e2e/workspace/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
cmake_minimum_required(VERSION 3.24)

project(e2e-test VERSION 1.0.0)

add_executable(e2e-test main.cpp)
42 changes: 42 additions & 0 deletions .devcontainer/cpp/e2e/workspace/CMakePresets.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"version": 3,
"configurePresets": [
{
"name": "defaults",
"hidden": true,
"binaryDir": "${sourceDir}/build/${presetName}",
"generator": "Ninja Multi-Config",
"cacheVariables": {
"CMAKE_CONFIGURATION_TYPES": "Debug;Release;RelWithDebInfo;MinSizeRel"
}
},
{
"name": "host",
"displayName": "host",
"description": "Build for host",
"inherits": "defaults"
}
],
"buildPresets": [
{
"name": "host-Debug",
"configuration": "Debug",
"configurePreset": "host"
},
{
"name": "host-Release",
"configuration": "Release",
"configurePreset": "host"
},
{
"name": "host-RelWithDebInfo",
"configuration": "RelWithDebInfo",
"configurePreset": "host"
},
{
"name": "host-MinSizeRel",
"configuration": "MinSizeRel",
"configurePreset": "host"
}
]
}
12 changes: 12 additions & 0 deletions .devcontainer/cpp/e2e/workspace/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#include <iostream>

void SmellyFunction()
{
auto array = new int[10];
}

int main()
{
std::cout << "Hello World!" << std::endl;
return 0;
}
14 changes: 11 additions & 3 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: daily
interval: weekly
groups:
github-actions:
update-types:
Expand All @@ -16,8 +16,16 @@ updates:
- package-ecosystem: docker
directory: .devcontainer
schedule:
interval: daily
interval: weekly
- package-ecosystem: devcontainers
directory: .devcontainer
schedule:
interval: weekly
- package-ecosystem: npm
directory: /
schedule:
interval: weekly
- package-ecosystem: pip
directory: .devcontainer
schedule:
interval: daily
interval: weekly
52 changes: 52 additions & 0 deletions .github/workflows/acceptance-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
name: Acceptance Test

on:
workflow_call:
inputs:
flavor:
required: true
type: string

concurrency:
group: ${{ github.workflow }}

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: step-security/harden-runner@0d381219ddf674d61a7572ddd19d7941e271515c # v9.0.1
with:
egress-policy: audit
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
persist-credentials: false
- uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
# Create a GitHub Codespace and communicate the image version via a Codespace secret (should be a Codespace environment variable).
# This secret is used by devcontainer.json, as such it is a resource that should not be used concurrently.
- run: |
set -Eeuo pipefail
gh secret set -a codespaces IMAGE_VERSION --body "pr-${{ github.event.pull_request.number }}"
echo CODESPACE_NAME="$(gh codespace create -R "${{ github.repository }}" -b "$HEAD_REF" -m basicLinux32gb --devcontainer-path ".devcontainer/${{ inputs.flavor }}-test/devcontainer.json" --idle-timeout 10m --retention-period 1h)" >> "$GITHUB_ENV"
env:
GH_TOKEN: ${{ secrets.TEST_GITHUB_TOKEN }}
HEAD_REF: ${{ github.head_ref }}
- run: cd .devcontainer/${{ inputs.flavor }}/e2e && npm test
env:
GITHUB_USER: ${{ secrets.TEST_GITHUB_USER }}
GITHUB_PASSWORD: ${{ secrets.TEST_GITHUB_PASSWORD }}
GITHUB_TOTP_SECRET: ${{ secrets.TEST_GITHUB_TOTP_SECRET }}
- run: |
set -Eeuo pipefail
gh codespace delete --force --codespace "$CODESPACE_NAME"
gh secret set -a codespaces IMAGE_VERSION --body "latest"
if: always()
env:
GH_TOKEN: ${{ secrets.TEST_GITHUB_TOKEN }}
Loading
Loading