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
12 changes: 10 additions & 2 deletions lib/core.sh
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ is_ready() {
}

new_session() {
local chromeOptions
local chromeOptions binaryClause
# Always include anti-automation-detection flags
local allArgs=(
"--disable-blink-features=AutomationControlled"
Expand All @@ -73,14 +73,22 @@ new_session() {
)

# Append headless flag when SHELLNIUM_HEADLESS is enabled
if [ "${SHELLNIUM_HEADLESS}" = "true" ] || [ "${SHELLNIUM_HEADLESS}" = "1" ]; then
if [ "${SHELLNIUM_HEADLESS:-}" = "true" ] || [ "${SHELLNIUM_HEADLESS:-}" = "1" ]; then
allArgs+=("--headless=new")
fi
chromeOptions=$(for i in "${allArgs[@]}"; do printf '"%s",' "${i}"; done | sed 's/,$//')

# Point to downloaded chrome-headless-shell binary if set
binaryClause=""
if [ -n "${SHELLNIUM_CHROME_BIN:-}" ]; then
binaryClause=$(printf '"binary": "%s",' "$SHELLNIUM_CHROME_BIN")
fi

_POST -d '{
"desiredCapabilities": {
"browserName":"chrome",
"chromeOptions": {
'"${binaryClause}"'
"args": ['"${chromeOptions}"'],
"excludeSwitches": ["enable-automation", "enable-logging"],
"useAutomationExtension": false
Expand Down
145 changes: 125 additions & 20 deletions lib/setup.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

SHELLNIUM_CACHE_DIR="${SHELLNIUM_CACHE_DIR:-${HOME}/.cache/shellnium}"
SHELLNIUM_PORT="${SHELLNIUM_PORT:-9515}"
SHELLNIUM_CHROME_BIN=""

_get_chrome_version() {
local version_output
Expand Down Expand Up @@ -54,16 +55,109 @@ _get_platform() {
esac
}

_get_chrome_for_testing_json() {
local url="$1"
local json
json=$(curl -sf "$url")
if [ -z "$json" ]; then
printf "\e[35m[ERROR] Failed to fetch Chrome for Testing info.\e[m\n" >&2
return 1
fi
echo "$json"
}

# Download chrome-headless-shell when no system Chrome is found.
# Sets SHELLNIUM_CHROME_BIN to the downloaded binary path.
_download_chrome_headless_shell() {
local platform version cache_dir download_url zip_file json

platform=$(_get_platform) || return 1

# Use specific version if set, otherwise fetch latest stable
if [ -n "${SHELLNIUM_CHROME_VERSION:-}" ]; then
version="$SHELLNIUM_CHROME_VERSION"
else
json=$(_get_chrome_for_testing_json "https://googlechromelabs.github.io/chrome-for-testing/last-known-good-versions-with-downloads.json") || return 1
version=$(echo "$json" | jq -r '.channels.Stable.version' 2>/dev/null)
if [ -z "$version" ] || [ "$version" = "null" ]; then
printf "\e[35m[ERROR] Could not determine latest stable Chrome version.\e[m\n" >&2
return 1
fi
fi

cache_dir="${SHELLNIUM_CACHE_DIR}/chrome-headless-shell-${version}"

# Return cached binary if it exists
if [ -x "${cache_dir}/chrome-headless-shell" ]; then
SHELLNIUM_CHROME_BIN="${cache_dir}/chrome-headless-shell"
echo "$version"
return 0
fi

mkdir -p "$cache_dir"

download_url="https://storage.googleapis.com/chrome-for-testing-public/${version}/${platform}/chrome-headless-shell-${platform}.zip"

printf "Downloading chrome-headless-shell %s (%s) ...\n" "$version" "$platform" >&2

zip_file="${cache_dir}/chrome-headless-shell.zip"
if ! curl -fL --progress-bar -o "$zip_file" "$download_url" 2>&1; then
printf "\e[35m[ERROR] Failed to download chrome-headless-shell from %s\e[m\n" "$download_url" >&2
rm -f "$zip_file"
return 1
fi

if command -v unzip >/dev/null 2>&1; then
unzip -o "$zip_file" -d "$cache_dir" >/dev/null 2>&1
else
printf "\e[35m[ERROR] 'unzip' is required to extract chrome-headless-shell.\e[m\n" >&2
return 1
fi

rm -f "$zip_file"

# Find the binary (may be nested in a subdirectory)
local found
found=$(find "$cache_dir" -name 'chrome-headless-shell' -type f | head -1)
if [ -n "$found" ] && [ "$found" != "${cache_dir}/chrome-headless-shell" ]; then
# Move all files from the nested directory (shared libs may be needed)
local nested_dir
nested_dir=$(dirname "$found")
if [ "$nested_dir" != "$cache_dir" ]; then
mv "$nested_dir"/* "$cache_dir"/ 2>/dev/null
rm -rf "$nested_dir"
fi
fi

chmod +x "${cache_dir}/chrome-headless-shell"

if [ ! -x "${cache_dir}/chrome-headless-shell" ]; then
printf "\e[35m[ERROR] chrome-headless-shell binary not found after extraction.\e[m\n" >&2
return 1
fi

SHELLNIUM_CHROME_BIN="${cache_dir}/chrome-headless-shell"
printf "chrome-headless-shell %s ready.\n" "$version" >&2
echo "$version"
}

# Download ChromeDriver matching the given version (or system Chrome).
# When a version argument is provided, it is used instead of detecting system Chrome.
_download_chromedriver() {
local chrome_version major platform download_url cache_dir zip_file

chrome_version=$(_get_chrome_version)
if [ -z "$chrome_version" ]; then
printf "\e[35m[ERROR] Google Chrome is not installed.\e[m\n" >&2
return 1
# Use provided version or detect from system Chrome
if [ -n "$1" ]; then
chrome_version="$1"
else
chrome_version=$(_get_chrome_version)
if [ -z "$chrome_version" ]; then
printf "\e[35m[ERROR] Google Chrome is not installed.\e[m\n" >&2
return 1
fi
fi

major=$(_get_chrome_major_version)
major=$(echo "$chrome_version" | cut -d. -f1)
platform=$(_get_platform) || return 1
cache_dir="${SHELLNIUM_CACHE_DIR}/chromedriver-${chrome_version}"

Expand All @@ -80,11 +174,7 @@ _download_chromedriver() {
if [ "$major" -ge 115 ]; then
# Chrome for Testing API (Chrome 115+)
local json driver_version
json=$(curl -sf "https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone-with-downloads.json")
if [ -z "$json" ]; then
printf "\e[35m[ERROR] Failed to fetch ChromeDriver version info.\e[m\n" >&2
return 1
fi
json=$(_get_chrome_for_testing_json "https://googlechromelabs.github.io/chrome-for-testing/latest-versions-per-milestone-with-downloads.json") || return 1

download_url=$(echo "$json" | jq -r \
".milestones.\"${major}\".downloads.chromedriver[] | select(.platform==\"${platform}\") | .url" 2>/dev/null)
Expand Down Expand Up @@ -113,7 +203,7 @@ _download_chromedriver() {
fi

zip_file="${cache_dir}/chromedriver.zip"
if ! curl -sfL -o "$zip_file" "$download_url"; then
if ! curl -fL --progress-bar -o "$zip_file" "$download_url" 2>&1; then
printf "\e[35m[ERROR] Failed to download ChromeDriver from %s\e[m\n" "$download_url" >&2
rm -f "$zip_file"
return 1
Expand Down Expand Up @@ -156,7 +246,7 @@ _is_chromedriver_running() {
# Sets SHELLNIUM_DRIVER_URL and SHELLNIUM_CHROMEDRIVER_PID (if we started it).
setup_chromedriver() {
# If user set a custom URL, assume they manage chromedriver themselves
if [ -n "$SHELLNIUM_DRIVER_URL" ]; then
if [ -n "${SHELLNIUM_DRIVER_URL:-}" ]; then
return 0
fi

Expand All @@ -167,21 +257,36 @@ setup_chromedriver() {
return 0
fi

local chrome_version chromedriver_bin

# Check if system Chrome is available
chrome_version=$(_get_chrome_version)

if [ -z "$chrome_version" ]; then
# No system Chrome found — auto-download chrome-headless-shell
printf "No system Chrome found. Auto-downloading chrome-headless-shell ...\n" >&2
# NOTE: $() runs in a subshell, so SHELLNIUM_CHROME_BIN set inside
# _download_chrome_headless_shell does not propagate. Reconstruct it here.
chrome_version=$(_download_chrome_headless_shell) || return 1
SHELLNIUM_CHROME_BIN="${SHELLNIUM_CACHE_DIR}/chrome-headless-shell-${chrome_version}/chrome-headless-shell"
export SHELLNIUM_CHROME_BIN
# Force headless mode since chrome-headless-shell has no GUI
export SHELLNIUM_HEADLESS=true
fi

# Try system chromedriver first, then auto-download
local chromedriver_bin
if command -v chromedriver >/dev/null 2>&1; then
# Verify version matches
local system_major chrome_major
system_major=$(chromedriver --version 2>/dev/null | awk '{print $2}' | cut -d. -f1)
chrome_major=$(_get_chrome_major_version)
chrome_major=$(echo "$chrome_version" | cut -d. -f1)
if [ -n "$chrome_major" ] && [ "$system_major" = "$chrome_major" ]; then
chromedriver_bin="chromedriver"
else
# Version mismatch - download the right one
chromedriver_bin=$(_download_chromedriver) || return 1
chromedriver_bin=$(_download_chromedriver "$chrome_version") || return 1
fi
else
chromedriver_bin=$(_download_chromedriver) || return 1
chromedriver_bin=$(_download_chromedriver "$chrome_version") || return 1
fi

# Start chromedriver in the background
Expand All @@ -204,9 +309,9 @@ setup_chromedriver() {

# Stop chromedriver if we started it.
cleanup_chromedriver() {
if [ -n "$SHELLNIUM_CHROMEDRIVER_PID" ]; then
kill "$SHELLNIUM_CHROMEDRIVER_PID" 2>/dev/null
wait "$SHELLNIUM_CHROMEDRIVER_PID" 2>/dev/null
if [ -n "${SHELLNIUM_CHROMEDRIVER_PID:-}" ]; then
kill "${SHELLNIUM_CHROMEDRIVER_PID}" 2>/dev/null
wait "${SHELLNIUM_CHROMEDRIVER_PID}" 2>/dev/null
unset SHELLNIUM_CHROMEDRIVER_PID
fi
}
34 changes: 34 additions & 0 deletions tests/unit/core.bats
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,40 @@ teardown() {
unset SHELLNIUM_DRIVER_URL
}

# new_session binary clause tests
# =====================

@test "new_session includes binary field when SHELLNIUM_CHROME_BIN is set" {
curl() {
for arg in "$@"; do
echo "$arg"
done >> "$CURL_LOG"
echo '{"sessionId": "test-session"}'
}
export -f curl

SHELLNIUM_CHROME_BIN="/tmp/chrome-headless-shell"
new_session
run cat "$CURL_LOG"
[[ "$output" == *'"binary": "/tmp/chrome-headless-shell"'* ]]
unset SHELLNIUM_CHROME_BIN
}

@test "new_session omits binary field when SHELLNIUM_CHROME_BIN is empty" {
curl() {
for arg in "$@"; do
echo "$arg"
done >> "$CURL_LOG"
echo '{"sessionId": "test-session"}'
}
export -f curl

SHELLNIUM_CHROME_BIN=""
new_session
run cat "$CURL_LOG"
[[ "$output" != *'"binary"'* ]]
}

# =====================
# Actions API tests
# =====================
Expand Down
101 changes: 101 additions & 0 deletions tests/unit/setup.bats
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,104 @@ setup() {
major=$(echo "$version" | cut -d. -f1)
[[ "$major" == "131" ]]
}

# =====================
# _get_chrome_for_testing_json tests
# =====================

@test "_get_chrome_for_testing_json returns JSON on success" {
source "${SHELLNIUM_DIR}/lib/setup.sh"

curl() {
echo '{"channels":{"Stable":{"version":"120.0.6099.109"}}}'
}
export -f curl

run _get_chrome_for_testing_json "https://example.com/test.json"
[ "$status" -eq 0 ]
[[ "$output" == *"120.0.6099.109"* ]]
}

@test "_get_chrome_for_testing_json fails when curl returns empty" {
source "${SHELLNIUM_DIR}/lib/setup.sh"

curl() {
echo ""
}
export -f curl

run _get_chrome_for_testing_json "https://example.com/test.json"
[ "$status" -eq 1 ]
[[ "$output" == *"Failed to fetch"* ]]
}

# =====================
# _download_chrome_headless_shell tests
# =====================

@test "_download_chrome_headless_shell returns cached binary if exists" {
source "${SHELLNIUM_DIR}/lib/setup.sh"

local version="120.0.6099.109"
local test_cache="${BATS_TEST_TMPDIR}/shellnium-cache"
local cache_dir="${test_cache}/chrome-headless-shell-${version}"
mkdir -p "$cache_dir"
touch "${cache_dir}/chrome-headless-shell"
chmod +x "${cache_dir}/chrome-headless-shell"

SHELLNIUM_CACHE_DIR="$test_cache"
SHELLNIUM_CHROME_VERSION="$version"

run _download_chrome_headless_shell
[ "$status" -eq 0 ]
[[ "$output" == "$version" ]]
}

@test "_download_chrome_headless_shell sets SHELLNIUM_CHROME_BIN on cache hit" {
source "${SHELLNIUM_DIR}/lib/setup.sh"

local version="120.0.6099.109"
local test_cache="${BATS_TEST_TMPDIR}/shellnium-cache"
local cache_dir="${test_cache}/chrome-headless-shell-${version}"
mkdir -p "$cache_dir"
touch "${cache_dir}/chrome-headless-shell"
chmod +x "${cache_dir}/chrome-headless-shell"

SHELLNIUM_CACHE_DIR="$test_cache"
SHELLNIUM_CHROME_VERSION="$version"

_download_chrome_headless_shell
[[ "$SHELLNIUM_CHROME_BIN" == "${cache_dir}/chrome-headless-shell" ]]
}

# =====================
# _download_chromedriver version argument tests
# =====================

@test "_download_chromedriver uses provided version instead of detecting system Chrome" {
source "${SHELLNIUM_DIR}/lib/setup.sh"

local test_cache="${BATS_TEST_TMPDIR}/shellnium-cache"
local cache_dir="${test_cache}/chromedriver-120.0.6099.109"
mkdir -p "$cache_dir"
touch "${cache_dir}/chromedriver"
chmod +x "${cache_dir}/chromedriver"

SHELLNIUM_CACHE_DIR="$test_cache"

run _download_chromedriver "120.0.6099.109"
[ "$status" -eq 0 ]
[[ "$output" == "${cache_dir}/chromedriver" ]]
}

# =====================
# setup_chromedriver Chrome auto-download tests
# =====================

@test "setup_chromedriver skips when SHELLNIUM_DRIVER_URL is set" {
source "${SHELLNIUM_DIR}/lib/setup.sh"

SHELLNIUM_DRIVER_URL="http://remote:9515"
run setup_chromedriver
[ "$status" -eq 0 ]
}
Loading