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
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)
# 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ __pycache__/
htmlcov/
.coverage
custom_components/
playwright-results/
48 changes: 48 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
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 integration source as a custom component so HA loads it on
# startup without any extra copy step.
- ./:/config/custom_components/task_tracker: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

# ── 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
- 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

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 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 "$@"
210 changes: 210 additions & 0 deletions run_workflows_locally.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
#!/usr/bin/env bash
# run_workflows_locally.sh
#
# Runs the CI workflows (tests and linting) in this repository locally using
# Docker and act (https://github.com/nektos/act).
#
# Both tools are installed automatically if they are not already present.
#
# Workflows that depend on GitHub infrastructure (hassfest, HACS validation,
# release) are silently skipped as they cannot run meaningfully offline.
#
# Usage:
# ./run_workflows_locally.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}"; }

command_exists() { command -v "$1" &>/dev/null; }

# ── Docker installation ───────────────────────────────────────────────────────
install_docker() {
if command_exists docker; then
info "Docker is already installed: $(sudo docker --version)"
return 0
fi

header "Installing Docker…"
curl -fsSL https://get.docker.com | sudo sh
sudo usermod -aG docker "$USER" || true
warn "Docker installed. You may need to run 'newgrp docker' or re-login for group membership to take effect."
}

# ── act installation ──────────────────────────────────────────────────────────
install_act() {
if command_exists act; then
info "act is already installed: $(act --version)"
return 0
fi

header "Installing act…"
curl -fsSL https://raw.githubusercontent.com/nektos/act/master/install.sh \
| sudo bash -s -- -b /usr/local/bin
}

# ── Docker daemon check ───────────────────────────────────────────────────────
ensure_docker_running() {
if sudo docker info &>/dev/null; then
return 0
fi

warn "Docker daemon is not running – attempting to start it…"
if command_exists systemctl; then
sudo systemctl start docker
else
sudo service docker start
fi
sleep 3

if ! sudo docker info &>/dev/null; then
error "Docker daemon is still not running. Please start Docker manually and re-run this script."
exit 1
fi
}

# ── Workflow runner ───────────────────────────────────────────────────────────

# Ubuntu runner image used by act. The "act-latest" tag is a medium-sized
# image that supports most common Actions without requiring the 20 GB+ full
# image.
ACT_IMAGE="catthehacker/ubuntu:act-latest"

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKFLOWS_DIR="$SCRIPT_DIR/.github/workflows"

# run_workflow <workflow-file> <event>
# Returns 0 on success, 1 on failure.
run_workflow() {
local workflow_file="$1"
local event="$2"
local name
name="$(basename "$workflow_file")"

info "Running [$name] with event '$event'…"

if sudo act "$event" \
-W "$workflow_file" \
-P "ubuntu-latest=$ACT_IMAGE" \
--rm \
2>&1; then
success "$name passed"
return 0
else
error "$name failed"
return 1
fi
}

# ── Playwright E2E tests via docker compose ───────────────────────────────────
# The playwright-tests.yml workflow uses `docker compose run` internally, which
# requires a real Docker daemon. act (Docker-in-Docker) cannot reliably run
# that workflow, so we delegate to the dedicated run_playwright_tests.sh script.
run_playwright_tests() {
local script="$SCRIPT_DIR/run_playwright_tests.sh"

if [[ ! -f "$script" ]]; then
warn "run_playwright_tests.sh not found – skipping Playwright E2E tests."
return 1
fi

if bash "$script"; then
success "playwright-tests.yml passed"
return 0
else
error "playwright-tests.yml failed"
return 1
fi
}

run_all_workflows() {
# Only act-compatible workflows (no Docker-in-Docker requirement).
# Workflows that depend on GitHub infrastructure (hassfest, HACS validation,
# release) are silently omitted.
local workflow_files=(
"test.yml"
"pylint.yml"
"integration-tests.yml"
)
local workflow_events=(
"push"
"push"
"push"
)

local passed=()
local failed=()

local i
for i in "${!workflow_files[@]}"; do
local workflow="${workflow_files[$i]}"
local event="${workflow_events[$i]}"
local workflow_path="$WORKFLOWS_DIR/$workflow"

if [[ ! -f "$workflow_path" ]]; then
warn "Workflow file not found, skipping: $workflow"
continue
fi

if run_workflow "$workflow_path" "$event"; then
passed+=("$workflow")
else
failed+=("$workflow")
fi
done

# ── Playwright E2E tests (docker compose, not act) ────────────────────────
if run_playwright_tests; then
passed+=("playwright-tests.yml")
else
failed+=("playwright-tests.yml")
fi

# ── Summary ───────────────────────────────────────────────────────────────
header "══════════════════════════════════════════════"
header " Results"
header "══════════════════════════════════════════════"

if [[ ${#passed[@]} -gt 0 ]]; then
success "Passed (${#passed[@]}): ${passed[*]}"
fi
if [[ ${#failed[@]} -gt 0 ]]; then
error "Failed (${#failed[@]}): ${failed[*]}"
return 1
fi

echo ""
success "All workflows completed successfully."
}

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

header "════════════════════════════════════════════════════"
header " Running GitHub Actions workflows locally with act"
header "════════════════════════════════════════════════════"

install_docker
install_act
ensure_docker_running
run_all_workflows
}

main "$@"
Loading
Loading