Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,46 @@ jobs:
- name: Run unit tests
working-directory: web-src
run: npm run test:unit-ci

e2e-tests:
name: E2E (Playwright)
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: pip

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: npm
cache-dependency-path: web-src/package-lock.json

- name: Install frontend dependencies
working-directory: web-src
run: npm ci

- name: Install Playwright browser
working-directory: web-src
run: npx playwright install --with-deps chromium

- name: Run e2e tests
working-directory: web-src
# Builds the frontend, then starts the backend with the isolated
# e2e config (tests/e2e/server.sh creates its own venv) and runs
# the Playwright suite against it.
run: npm run test:e2e

- name: Upload Playwright report on failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: web-src/playwright-report/
retention-days: 7
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,9 @@ web-src/geckodriver.log
venv/
/venv2/
e2e_venv/

# e2e (Playwright)
.e2e_venv/
web-src/tests/e2e/.run/
web-src/playwright-report/
web-src/test-results/
98 changes: 98 additions & 0 deletions web-src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion web-src/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"vuex": "^4.1.0"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
"@testing-library/jest-dom": "^6.4.0",
"@vitejs/plugin-vue": "^6.0.7",
"@vue/test-utils": "^2.4.6",
Expand All @@ -38,7 +39,9 @@
"build": "vite build",
"preview": "vite preview",
"test:unit": "vitest",
"test:unit-ci": "vitest run"
"test:unit-ci": "vitest run",
"test:e2e": "npm run build && playwright test",
"test:e2e-ui": "playwright test --ui"
},
"browserslist": [
"> 1%",
Expand Down
32 changes: 32 additions & 0 deletions web-src/playwright.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {defineConfig, devices} from '@playwright/test'

// E2E suite against the real Python backend serving the production build
// (web/). The backend is started automatically with an isolated config
// (tests/e2e/fixtures/conf — never the developer's real conf/), on port 5099.
//
// Run with: npm run test:e2e (builds the frontend first)
export default defineConfig({
testDir: './tests/e2e',
testMatch: '**/*.spec.js',
fullyParallel: false,
workers: 1,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI ? [['github'], ['html', {open: 'never'}]] : 'list',

use: {
baseURL: 'http://localhost:5099',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},

projects: [
{name: 'chromium', use: {...devices['Desktop Chrome']}}
],

webServer: {
command: 'bash tests/e2e/server.sh',
url: 'http://localhost:5099/index.html',
reuseExistingServer: !process.env.CI,
timeout: 120_000
}
})
23 changes: 23 additions & 0 deletions web-src/tests/e2e/admin.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {expect, test} from '@playwright/test'

// Admin app boot: second Vue application (own router + store). Covers the
// admin mount path verified manually after the Vue 3 migration.

test.describe('Admin app', () => {

test('loads with Logs/Scripts tabs and defaults to logs', async ({page}) => {
await page.goto('/admin.html')

await expect(page.locator('.admin-page')).toBeVisible()
await expect(page).toHaveURL(/#\/logs$/)
await expect(page.locator('.tab', {hasText: 'Logs'})).toBeVisible()
await expect(page.locator('.tab', {hasText: 'Scripts'})).toBeVisible()
})

test('scripts tab lists configured scripts with an Add button', async ({page}) => {
await page.goto('/admin.html#/scripts')

await expect(page.locator('.add-script-btn')).toBeVisible()
await expect(page.locator('.collection-item', {hasText: 'E2E Echo'})).toBeVisible()
})
})
70 changes: 70 additions & 0 deletions web-src/tests/e2e/execution.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {expect, test} from '@playwright/test'

// Full execution path in a real browser: XSRF token mode (the _xsrf cookie
// must be readable by JS and echoed as X-XSRFToken — regression of the
// HttpOnly bug), POST /executions/start, websocket streaming, and the
// log panel rendering (regression of the logChunks deep-watch bug).

async function openEchoScript(page) {
await page.goto('/index.html#/' + encodeURIComponent('E2E Echo'))
await expect(page.locator('.script-parameters-panel .parameter')).toHaveCount(2)
}

function paramInput(page, label) {
return page.locator('.script-parameters-panel .parameter')
.filter({has: page.locator('label', {hasText: label})})
.locator('input')
}

test.describe('Script execution', () => {

// NOTE: order matters. Finished executions stay attached server-side
// (/executions/active) for the rest of the suite, so a later page load on
// the same script re-binds the previous execution and its log panel. The
// validation test must therefore run BEFORE any successful execution.

test('execute without required parameter is blocked', async ({page}) => {
await openEchoScript(page)

// Message is required and empty: clicking Execute must show the
// validation panel and must NOT start an execution.
await page.locator('.button-execute').click()

const validationPanel = page.locator('.validation-panel')
await expect(validationPanel).toBeVisible()
await expect(validationPanel).toContainText('Message')

await expect(page.locator('.log-panel .log-content')).toBeHidden()
})

test('executes a script and streams its output to the log panel', async ({page}) => {
await openEchoScript(page)

await paramInput(page, 'Message').fill('hello-e2e')

await page.locator('.button-execute').click()

const log = page.locator('.log-panel .log-content')
await expect(log).toContainText('e2e: started', {timeout: 15_000})
await expect(log).toContainText('--mode alpha --message hello-e2e')
await expect(log).toContainText('e2e: done', {timeout: 15_000})
})

test('changes a combobox value before executing', async ({page}) => {
// Exercises the materialize FormSelect dropdown in a real browser —
// behaviour parity baseline for the upcoming Vuetify migration.
await openEchoScript(page)

const modeParam = page.locator('.script-parameters-panel .parameter')
.filter({has: page.locator('label', {hasText: 'Mode'})})
await modeParam.locator('input.select-dropdown').click()

await page.locator('.dropdown-content li', {hasText: 'beta'}).click()

await paramInput(page, 'Message').fill('combo-test')
await page.locator('.button-execute').click()

const log = page.locator('.log-panel .log-content')
await expect(log).toContainText('--mode beta --message combo-test', {timeout: 15_000})
})
})
6 changes: 6 additions & 0 deletions web-src/tests/e2e/fixtures/conf/conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"port": 5099,
"security": {
"cookie_secure": false
}
}
31 changes: 31 additions & 0 deletions web-src/tests/e2e/fixtures/conf/logging.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"version": 1,
"disable_existing_loggers": false,
"formatters": {
"simple": {
"format": "%(asctime)s [%(name)s.%(levelname)s] %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "DEBUG",
"formatter": "simple",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.FileHandler",
"level": "INFO",
"formatter": "simple"
}
},
"loggers": {
},
"root": {
"level": "DEBUG",
"handlers": [
"console",
"file"
]
}
}
21 changes: 21 additions & 0 deletions web-src/tests/e2e/fixtures/conf/runners/e2e_echo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "E2E Echo",
"group": "E2E Group",
"description": "Fixture script for the Playwright e2e suite",
"script_path": "python3 e2e_echo.py",
"working_directory": "./web-src/tests/e2e/fixtures/scripts",
"parameters": [
{
"name": "Mode",
"param": "--mode",
"type": "list",
"values": ["alpha", "beta", "gamma"],
"default": "alpha"
},
{
"name": "Message",
"param": "--message",
"required": true
}
]
}
Loading
Loading