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
1 change: 1 addition & 0 deletions .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
- name: "Move files to where hassfest expects them"
run: |
mkdir -p custom_components/ssh_docker
rm -rf tests
find . -maxdepth 1 -not -name 'custom_components' -not -name '.' -exec mv {} custom_components/ssh_docker/ \;

- name: "Run hassfest"
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/playwright-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Playwright E2E Tests

on:
workflow_dispatch:

jobs:
playwright-e2e:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- uses: actions/checkout@v4

- name: Build Docker images
run: docker compose build

- name: Run Playwright E2E tests
# `docker compose run` starts the declared dependencies (homeassistant,
# docker-host) and then runs the playwright-tests container.
# The exit code of the run command mirrors the test container's exit code.
run: docker compose run --rm playwright-tests

- name: Stop services
if: always()
run: docker compose down -v

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-e2e-results
path: playwright-results/
if-no-files-found: ignore
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ htmlcov
.coverage
custom_components/
.pytest_cache/
playwright-results/
docker-compose.override.yaml
20 changes: 20 additions & 0 deletions docker-compose.override.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# docker-compose.override.yaml.example
#
# Override file for **local development and PyCharm debugging**.
# Copy this file to docker-compose.override.yaml (which is git-ignored) to
# expose the HA and docker-host ports on your local machine so that the
# Playwright tests can be run and debugged directly from your IDE.
#
# Usage:
# cp docker-compose.override.yaml.example docker-compose.override.yaml
# docker compose up -d homeassistant docker-host
# # Then run pytest from PyCharm (see tests/playwright/README.md)

services:
homeassistant:
ports:
- "8123:8123" # http://localhost:8123

docker-host:
ports:
- "2222:22" # SSH reachable at localhost:2222
66 changes: 66 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
services:

# ── Home Assistant ──────────────────────────────────────────────────────────
homeassistant:
image: ghcr.io/home-assistant/home-assistant:stable
container_name: homeassistant_test
volumes:
# Persistent HA config (survives container restarts; start fresh with
# `docker compose down -v`).
- ha_config:/config
# Mount the ssh_docker integration source as a custom component so HA
# loads it on startup without any extra copy step.
- ./:/config/custom_components/ssh_docker:ro
# Mount the ssh_command functional copy (required dependency of ssh_docker).
- ./tests/playwright/ssh_command:/config/custom_components/ssh_command:ro
# Startup wrapper that pre-populates /etc/hosts before launching HA.
# Alpine Linux (musl libc) cannot resolve Docker container hostnames via
# Python's socket module because of iptables/UDP limitations in this
# environment. The wrapper uses busybox nslookup (which works) to add
# entries to /etc/hosts so that all resolver calls succeed via the
# "files" nsswitch path.
- ./tests/playwright/ha-init-wrapper.sh:/ha-init-wrapper.sh:ro
environment:
- TZ=UTC
entrypoint: ["/bin/sh", "/ha-init-wrapper.sh"]
# Clear the external search domain that musl's resolver would try first,
# which causes timeouts in this Azure-hosted environment.
dns_search: "."
restart: unless-stopped

# ── Docker host ─────────────────────────────────────────────────────────────
# Ubuntu 24.04 container running sshd and a mock Docker CLI.
# The SSH Docker integration connects here via SSH and issues docker commands.
# No DinD / privileged mode required — the mock CLI simulates container state
# using plain files.
# Credentials: user=foo password=pass
docker-host:
build:
context: tests/playwright
dockerfile: Dockerfile.dockerhost
container_name: docker_host

# ── Playwright E2E test runner ──────────────────────────────────────────────
# Not started by default (`docker compose up`); invoke explicitly:
# docker compose run --rm playwright-tests
playwright-tests:
build:
context: .
dockerfile: tests/playwright/Dockerfile
environment:
- HOMEASSISTANT_URL=http://homeassistant:8123
- DOCKER_HOST_NAME=docker_host
- SSH_USER=foo
- SSH_PASSWORD=pass
- HA_USERNAME=admin
- HA_PASSWORD=admin
volumes:
# Test results (JUnit XML) written here are available on the host after
# the container exits, e.g. for CI artifact upload.
- ./playwright-results:/app/playwright-results
depends_on:
- homeassistant
- docker-host

volumes:
ha_config:
83 changes: 83 additions & 0 deletions run_playwright_tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
# run_playwright_tests.sh
#
# Runs the Playwright E2E test suite in a fully isolated Docker environment.
# No local Python environment or browser installation is required.
#
# The suite spins up Home Assistant, a mock Docker host, and the Playwright
# test runner via docker compose, then tears everything down on exit.
#
# Usage:
# ./run_playwright_tests.sh

set -euo pipefail

# ── Colour helpers ────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
BOLD='\033[1m'
NC='\033[0m'

info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[PASS]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[FAIL]${NC} $*"; }
header() { echo -e "\n${BOLD}$*${NC}"; }

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.yaml"

# ── Resolve docker compose command ───────────────────────────────────────────
get_compose_cmd() {
if command -v docker &>/dev/null && sudo docker compose version &>/dev/null 2>&1; then
echo "sudo docker compose"
else
error "docker compose is not available. Please install Docker with the Compose plugin."
exit 1
fi
}

# ── Main ──────────────────────────────────────────────────────────────────────
main() {
if [[ $# -gt 0 ]]; then
error "This script takes no arguments."
echo "Usage: $0"
exit 1
fi

if [[ ! -f "$COMPOSE_FILE" ]]; then
error "docker-compose.yaml not found at $COMPOSE_FILE"
exit 1
fi

header "════════════════════════════════════════════════════"
header " Playwright E2E tests (docker compose)"
header "════════════════════════════════════════════════════"

local compose_cmd
compose_cmd="$(get_compose_cmd)"

info "Building Docker images…"
$compose_cmd -f "$COMPOSE_FILE" build

info "Running test container (this may take several minutes on first run)…"
local exit_code=0
$compose_cmd -f "$COMPOSE_FILE" run --rm playwright-tests || exit_code=$?

info "Stopping services…"
$compose_cmd -f "$COMPOSE_FILE" down -v || true

if [[ $exit_code -eq 0 ]]; then
echo ""
success "All Playwright E2E tests passed."
exit 0
else
echo ""
error "Playwright E2E tests failed (exit code ${exit_code})."
exit "${exit_code}"
fi
}

main "$@"
Loading
Loading