-
Notifications
You must be signed in to change notification settings - Fork 2
Add a Playwright end-to-end harness with a containerised gateway #91
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
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
47b3998
test(e2e): add a Playwright harness and cover the Scripts tab
bburda d031d22
ci: run the Playwright suite against a containerised gateway
bburda dfb572c
fix(e2e): harden gateway startup wait, config and cleanup, pin image …
bburda b1b3cbc
fix(e2e): keep the gateway loopback-only and harden test isolation
bburda 9771202
ci: run pull_request checks regardless of target branch
bburda ebc9c5b
fix(e2e): accept the delete confirmation dialog in scripts specs
bburda 1058fe0
fix: reach every recording a fault kept, not just the newest
mfaferek93 0071145
test(e2e): prove each recording downloads its own bytes
mfaferek93 4a0dbb7
test(e2e): skip the rosbag specs when their stack is absent
mfaferek93 17c91a6
fix(ui): address multi-rosbag review round
mfaferek93 d39dc06
fix(e2e): source in statements, not in the backgrounded chain
mfaferek93 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,3 +25,8 @@ dist-ssr | |
|
|
||
| # Serena | ||
| .serena/ | ||
|
|
||
| # Playwright | ||
| playwright-report/ | ||
| test-results/ | ||
| e2e/.auth/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| // Copyright 2026 bburda | ||
| // | ||
| // Licensed under the Apache License, Version 2.0 (the "License"); | ||
| // you may not use this file except in compliance with the License. | ||
| // You may obtain a copy of the License at | ||
| // | ||
| // http://www.apache.org/licenses/LICENSE-2.0 | ||
| // | ||
| // Unless required by applicable law or agreed to in writing, software | ||
| // distributed under the License is distributed on an "AS IS" BASIS, | ||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| // See the License for the specific language governing permissions and | ||
| // limitations under the License. | ||
|
|
||
| import { expect, type Dialog, type Page } from '@playwright/test'; | ||
|
|
||
| /** | ||
| * Clicks the (already-visible, uniquely-matched) Delete button for | ||
| * `scriptName` and accepts the native `confirm()` dialog that ScriptRow's | ||
| * handleDelete requires before it calls deleteScript. | ||
| * | ||
| * The listener is registered before the click and accepts inline as soon as | ||
| * the dialog opens, rather than after awaiting the click: `window.confirm` | ||
| * blocks the page's JS (and, with it, the click action itself) until the | ||
| * dialog is resolved, so anything that awaits the click before calling | ||
| * `dialog.accept()` would deadlock - the click can never settle first. | ||
| * | ||
| * Asserts the dialog actually appeared, with the expected message, instead | ||
| * of accepting whatever dialog (if any) shows up - a bare accept-everything | ||
| * handler would still pass the day someone removes the confirmation guard by | ||
| * accident. | ||
| */ | ||
| export async function clickDeleteAndConfirm(page: Page, scriptName: string): Promise<void> { | ||
| let seenDialog: Dialog | undefined; | ||
| page.once('dialog', async (dialog) => { | ||
| seenDialog = dialog; | ||
| await dialog.accept(); | ||
| }); | ||
|
|
||
| await page.getByRole('button', { name: 'Delete' }).click(); | ||
|
|
||
| expect(seenDialog?.type()).toBe('confirm'); | ||
| expect(seenDialog?.message()).toBe(`Delete script "${scriptName}"? This cannot be undone.`); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| # Stack for the rosbag-history specs: a gateway AND a fault manager, so a fault | ||
| # can actually own black-box recordings. docker-compose.yml next to this file | ||
| # runs a manifest-only gateway with no fault manager at all, which cannot | ||
| # produce a single bag; the two scenarios are kept apart rather than merged so | ||
| # neither has to carry the other's configuration. | ||
|
|
||
| # Its own project: both compose files live in e2e/, so without this they share | ||
| # the default project name "e2e" and their `gateway` services replace each other | ||
| # instead of running side by side. | ||
| name: e2e-rosbag | ||
|
|
||
| services: | ||
| gateway: | ||
| # Overridable because the recording-id contract these specs assert on | ||
| # (ros2_medkit#620) is newer than any published tag: point this at a | ||
| # locally built image to run them before that lands. Once it is | ||
| # published, pin a digest here the way docker-compose.yml does. | ||
| image: ${E2E_ROSBAG_GATEWAY_IMAGE:-ghcr.io/selfpatch/ros2_medkit-jazzy:latest} | ||
| ports: | ||
| # Loopback only, and on its own port so this stack can run alongside | ||
| # the scripts one without either stealing the other's. | ||
| - '127.0.0.1:${E2E_ROSBAG_GATEWAY_PORT:-8081}:8080' | ||
| volumes: | ||
| - ./gateway/rosbag-params.yaml:/e2e/params.yaml:ro | ||
| - ./gateway/seed_recordings.py:/e2e/seed_recordings.py:ro | ||
| - e2e-bags:/e2e-bags | ||
| # PID 1 reaps children and forwards signals; without it bash -lc keeps | ||
| # PID 1 for itself and `docker compose down` waits out the whole grace | ||
| # period before SIGKILLing a fault manager mid-write. | ||
| init: true | ||
| # Overriding the entrypoint skips /entrypoint.sh, which is what sources | ||
| # ROS and exports the RMW default - both have to be restored here. | ||
| environment: | ||
| RMW_IMPLEMENTATION: ${RMW_IMPLEMENTATION:-rmw_fastrtps_cpp} | ||
| entrypoint: ['/bin/bash', '-lc'] | ||
| # Fault manager, the seeder and the gateway in one container. Not three | ||
| # services sharing a network: the default DDS transport uses /dev/shm, | ||
| # which is per container, so the seeder's service calls would never | ||
| # complete even though discovery says the service is there. | ||
| # | ||
| # Sourced ONCE in the parent shell, then every process runs as a WATCHED | ||
| # background job: `&` binds looser than `&&`, so the earlier | ||
| # `source && source && fault_manager & gateway` form left the gateway in | ||
| # an unsourced shell ("ros2: command not found", exit 127). `wait -n` | ||
| # returns when the FIRST job dies, so a fault manager that cannot open | ||
| # its DB or a seeder that raises SystemExit takes the container down | ||
| # with its exit code instead of leaving a healthy-looking stack whose | ||
| # specs skip. The trap makes SIGTERM stop the children before the shell | ||
| # exits. | ||
| command: | ||
| - > | ||
| source /opt/ros/jazzy/setup.bash; | ||
| source /home/medkit/ws/install/setup.bash; | ||
| ros2 run ros2_medkit_fault_manager fault_manager_node | ||
| --ros-args --params-file /e2e/params.yaml & | ||
| FM=$!; | ||
| python3 /e2e/seed_recordings.py & | ||
| SEED=$!; | ||
| ros2 run ros2_medkit_gateway gateway_node | ||
| --ros-args --params-file /e2e/params.yaml & | ||
| GW=$!; | ||
| trap 'kill $FM $SEED $GW 2>/dev/null' TERM INT; | ||
| wait -n $FM $SEED $GW; | ||
| exit $? | ||
| depends_on: | ||
| init-bags: | ||
| condition: service_completed_successfully | ||
| # The gateway image runs as uid 999 and a fresh named volume is root-owned, | ||
| # so the fault manager could not write a bag into it. Same one-shot chown | ||
| # the scripts stack does for its upload volume. | ||
| init-bags: | ||
| image: ${E2E_ROSBAG_GATEWAY_IMAGE:-ghcr.io/selfpatch/ros2_medkit-jazzy:latest} | ||
| user: root | ||
| volumes: | ||
| - e2e-bags:/e2e-bags | ||
| entrypoint: ['chown', '-R', '999:999', '/e2e-bags'] | ||
|
|
||
| volumes: | ||
| # Holds the bags AND faults.db, and it outlives `docker compose down`. | ||
| # Re-seed from a clean slate with `down -v` first: on a reused volume the | ||
| # fault is already CONFIRMED, the first confirm captures nothing, and the | ||
| # suite sees three recordings instead of two. | ||
| e2e-bags: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| services: | ||
| # Docker creates a fresh named volume owned by root:root, but the gateway | ||
| # image runs as the unprivileged `medkit` user (uid 999) and cannot create | ||
| # script subdirectories under a root-owned mount. This one-shot service | ||
| # chowns the volume before the gateway starts; it reuses the pinned | ||
| # gateway image (which already has chown) instead of pulling another one. | ||
| init-uploads: | ||
| # ghcr.io/selfpatch/ros2_medkit-jazzy:sha-7939c94 | ||
| image: ghcr.io/selfpatch/ros2_medkit-jazzy@sha256:565db07e1e972b31684bf864fbaad7e8a70aacabf2ef0cd4510fdbd8e3281831 | ||
| user: root | ||
| volumes: | ||
| - e2e-uploads:/e2e-uploads | ||
| entrypoint: ['chown', '-R', '999:999', '/e2e-uploads'] | ||
| gateway: | ||
| # Pinned on purpose: :latest is overwritten on every push to the gateway | ||
| # main branch, which would let unrelated changes turn this repo CI red. | ||
| # Pinned by digest, not by the sha-7939c94 tag alone: tags on this | ||
| # registry are mutable and a re-run of the publishing workflow on the | ||
| # same commit would move one. ghcr.io/selfpatch/ros2_medkit-jazzy:sha-7939c94 | ||
| image: ghcr.io/selfpatch/ros2_medkit-jazzy@sha256:565db07e1e972b31684bf864fbaad7e8a70aacabf2ef0cd4510fdbd8e3281831 | ||
| ports: | ||
| # Bound to loopback only, on purpose: this gateway has uploads enabled | ||
| # and executes uploaded shell scripts without authentication. | ||
| # CONTRIBUTING has developers bring this stack up and leave it running, | ||
| # so publishing it on every interface would let anyone else on the same | ||
| # network or Wi-Fi execute arbitrary shell on this machine for as long | ||
| # as the container is up. Do not drop the `127.0.0.1:` prefix to | ||
| # "simplify" this - that reintroduces the exposure. | ||
| - '127.0.0.1:${E2E_GATEWAY_PORT:-8080}:8080' | ||
| volumes: | ||
| - ./gateway/params.yaml:/e2e/params.yaml:ro | ||
| - ./gateway/manifest.yaml:/e2e/manifest.yaml:ro | ||
| - ./gateway/scripts:/e2e-scripts:ro | ||
| - e2e-uploads:/e2e-uploads | ||
| command: ['--ros-args', '--params-file', '/e2e/params.yaml'] | ||
| depends_on: | ||
| init-uploads: | ||
| condition: service_completed_successfully | ||
| volumes: | ||
| e2e-uploads: |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| #!/usr/bin/env bash | ||
| # Copyright 2026 bburda | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| set -eu | ||
| echo "uploaded script executed" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| manifest_version: '1.0' | ||
| components: | ||
| - id: 'ecu' | ||
| name: 'Test ECU' | ||
| apps: | ||
| - id: 'talker' | ||
| name: 'Talker' | ||
| is_located_on: 'ecu' | ||
| scripts: | ||
| - id: 'hello' | ||
| name: 'Hello' | ||
| description: 'Echoes the parameters it receives on stdin' | ||
| path: '/e2e-scripts/hello.sh' | ||
| format: 'bash' | ||
| timeout_sec: 30 | ||
| entity_filter: | ||
| - 'ecu' | ||
| - 'talker' | ||
| - id: 'failing' | ||
| name: 'Failing' | ||
| description: 'Exits with a non-zero code' | ||
| path: '/e2e-scripts/fail.sh' | ||
| format: 'bash' | ||
| timeout_sec: 30 | ||
| entity_filter: | ||
| - 'ecu' | ||
| - id: 'sleeper' | ||
| name: 'Sleeper' | ||
| description: 'Runs long enough to be stopped' | ||
| path: '/e2e-scripts/sleep.sh' | ||
| format: 'bash' | ||
| # Matches sleep.sh's own sleep duration - see the comment there for why | ||
| # this is kept short rather than generously long. | ||
| timeout_sec: 30 | ||
| entity_filter: | ||
| - 'ecu' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| /**: | ||
| ros__parameters: | ||
| server: | ||
| host: '0.0.0.0' | ||
| port: 8080 | ||
| cors: | ||
| # '*' rather than a hardcoded 'http://localhost:5173': playwright.config.ts | ||
| # and e2e/global-setup.ts both derive the dev server origin from | ||
| # E2E_APP_URL, so overriding that variable (e.g. to dodge a busy port) | ||
| # would otherwise leave the browser talking to an origin this gateway | ||
| # never allowed, failing every request with no | ||
| # Access-Control-Allow-Origin header and no mention of CORS anywhere | ||
| # in the symptom. This is a throwaway local/CI fixture with | ||
| # allow_credentials left at its default false, so a wildcard origin | ||
| # carries none of the risk it would in a real deployment. | ||
| allowed_origins: | ||
| - '*' | ||
| discovery: | ||
| mode: 'manifest_only' | ||
| manifest_path: '/e2e/manifest.yaml' | ||
| scripts: | ||
| scripts_dir: '/e2e-uploads' | ||
| allow_uploads: true |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.