feat: @wdio/xvfb (#14663)
* feat: `@wdio/xvfb` * feat(local-runner): use `@wdio/xvfb` * chore: update lockfile * chore(wdio-xvfb): update version * chore: add `@wdio/xvfb` to compile script * chore(wdio-xvfb): fix exports * test(wdio-xvfb): fix unit tests * test(wdio-local-runner): add xvfb tests * chore(wdio-xvfb): add logging * chore(wdio-xvfb): process runs independently * feat(wdio-xvfb): skip startup when display in use * test(wdio-xvfb): fix unit tests * refactor(wdio-xvfb): use `xvfb-run` * chore: update lockfile * refactor(wdio-local-runner): use processFactory * refactor(wdio-xvfb): remove command execution * test(e2e): add xvfb E2Es * chore: update lockfile * docs(wdio-xvfb): update LICENSE Co-authored-by: Christian Bromann <git@bromann.dev> * refactor(wdio-xvfb): rework distro detection * docs(wdio-xvfb): update for distros * test(e2e): add new distros, fix perm issues * refactor(wdio-xvfb): use `is-ci` * chore: update lockfile * test(wdio-xvfb): mock `is-ci` * refactor(wdio-local-runner): processFactory tweaks * test(wdio-xvfb): fix `is-ci` mocking * refactor(wdio-xvfb): rework distro detection * refactor(wdio-local-runner): import xvfb directly * fix(e2e): fix log perms issue * fix(e2e): xvfb dockerfile fixes * chore(e2e): exit correctly * fix(wdio-xvfb): update install commands * chore(e2e): ensure clean environments without xvfb * docs(wdio-xvfb): update README * chore(e2e): fix arch chrome install * fix(e2e): fix SUSE perm issue * fix(e2e): fix EACCES * fix(e2e): fix exit issue * chore(e2e): add zypper xvfb fallback * fix(e2e): more aggressive removal of xvfb in clean images * chore(e2e): fix issues * fix(e2e): fix suse xvfb install * chore(wdio-types): add disableAutoXvfb option * feat(wdio-local-runner): skip auto xvfb init * chore(e2e): disable auto xvfb init * fix(wdio-local-runner): check if xvfb-run available * chore(e2e): increase timeout * chore(wdio-xvfb): add logging * chore(wdio-xvfb): add override & force props for tests * test(e2e): update for new manager props * test(wdio-xvfb): update units * test(e2e): add debug * test(wdio-local-runner): make flaky test more robust * chore(e2e): update chromium install for alpine * chore(e2e): add more debug * test(wdio-xvfb): update timeout values * chore(e2e): update alpine repos * test(e2e): add more debug * test(e2e): add more debug * test(e2e): remove cleanup * test(e2e): port fix to existing-xvfb * chore(e2e): standardize on node 22 * chore(e2e): remove debug * chore(e2e): remove redundant test * chore(e2e): add back alpine fix * chore(e2e): update chromeOptions * chore(e2e): display logs on failure * chore(e2e): standardize nomenclature * chore(e2e): delete README * chore: add NL * feat(wdio-xvfb): add void / xbps support * test(e2e): add void E2Es * refactor(e2e): remove rocky tests (covered by fedora) * fix(e2e): fix void docker issues * feat(wdio-xvfb): detect headless mode * feat(wdio-local-runner): lazy init xvfb for headless mode detection * docs(wdio-xvfb): update for package manager-centric approach & headless mode detection * chore(wdio-xvfb): update xbps install command * chore(wdio-xvfb): use types package for caps, remove legacy options support * test(wdio-xvfb): remove tests for unsupported options * chore(wdio-xvfb): add `@wdio/types` * chore: update lockfile * chore(wdio-xvfb): update xbps command * chore(wdio-xvfb): update commands to refresh package repos * test(wdio-xvfb): fix type error * chore(e2e): update base dockerfiles for distro-specific xvfb removal * chore(wdio-xvfb): update zypper command * chore(e2e): update zypper xvfb clean * refactor: move ProcessFactory to `wdio-xvfb` * feat(wdio-xvfb): retries * chore(wdio-types): add xvfb opts * refactor(wdio-local-runner): pass xvfbmanager to processfactory * refactor(e2e): update for `@wdio/xvfb` changes * docs(wdio-xvfb): update README for retries * chore(e2e): remove browserVersion * Update packages/wdio-local-runner/src/index.ts --------- Co-authored-by: Christian Bromann <git@bromann.dev>
This commit is contained in:
parent
504857fa05
commit
d30462eb7f
41 changed files with 3343 additions and 152 deletions
179
.github/workflows/test-xvfb-e2e.yml
vendored
Normal file
179
.github/workflows/test-xvfb-e2e.yml
vendored
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
name: XVFB E2E Tests
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
# Make this a reusable workflow, no value needed
|
||||
# https://docs.github.com/en/actions/using-workflows/reusing-workflows
|
||||
|
||||
jobs:
|
||||
xvfb-e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Ubuntu scenarios
|
||||
- distro: ubuntu
|
||||
version: "24.04"
|
||||
scenario: base
|
||||
dockerfile: ubuntu-base.dockerfile
|
||||
- distro: ubuntu
|
||||
version: "24.04"
|
||||
scenario: with-xvfb
|
||||
dockerfile: ubuntu-with-xvfb.dockerfile
|
||||
|
||||
# Fedora scenarios
|
||||
- distro: fedora
|
||||
version: "40"
|
||||
scenario: base
|
||||
dockerfile: fedora-base.dockerfile
|
||||
- distro: fedora
|
||||
version: "40"
|
||||
scenario: with-xvfb
|
||||
dockerfile: fedora-with-xvfb.dockerfile
|
||||
|
||||
# Alpine scenarios
|
||||
- distro: alpine
|
||||
version: "3.20"
|
||||
scenario: base
|
||||
dockerfile: alpine-base.dockerfile
|
||||
- distro: alpine
|
||||
version: "3.20"
|
||||
scenario: with-xvfb
|
||||
dockerfile: alpine-with-xvfb.dockerfile
|
||||
|
||||
# Debian scenarios
|
||||
- distro: debian
|
||||
version: "12"
|
||||
scenario: base
|
||||
dockerfile: debian-base.dockerfile
|
||||
- distro: debian
|
||||
version: "12"
|
||||
scenario: with-xvfb
|
||||
dockerfile: debian-with-xvfb.dockerfile
|
||||
|
||||
# CentOS Stream scenarios (upstream RHEL)
|
||||
- distro: centos-stream
|
||||
version: "9"
|
||||
scenario: base
|
||||
dockerfile: centos-stream-base.dockerfile
|
||||
- distro: centos-stream
|
||||
version: "9"
|
||||
scenario: with-xvfb
|
||||
dockerfile: centos-stream-with-xvfb.dockerfile
|
||||
|
||||
# Arch Linux scenarios
|
||||
- distro: arch
|
||||
version: "latest"
|
||||
scenario: base
|
||||
dockerfile: arch-base.dockerfile
|
||||
- distro: arch
|
||||
version: "latest"
|
||||
scenario: with-xvfb
|
||||
dockerfile: arch-with-xvfb.dockerfile
|
||||
|
||||
# SUSE scenarios
|
||||
- distro: suse
|
||||
version: "15.6"
|
||||
scenario: base
|
||||
dockerfile: suse-base.dockerfile
|
||||
- distro: suse
|
||||
version: "15.6"
|
||||
scenario: with-xvfb
|
||||
dockerfile: suse-with-xvfb.dockerfile
|
||||
|
||||
# Void Linux scenarios
|
||||
- distro: void
|
||||
version: "latest"
|
||||
scenario: base
|
||||
dockerfile: void-base.dockerfile
|
||||
- distro: void
|
||||
version: "latest"
|
||||
scenario: with-xvfb
|
||||
dockerfile: void-with-xvfb.dockerfile
|
||||
|
||||
name: "XVFB E2E: ${{ matrix.distro }}:${{ matrix.version }} (${{ matrix.scenario }})"
|
||||
|
||||
steps:
|
||||
- name: ⬇️ Checkout Code
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: 🧑🔧 Get Core Dependencies
|
||||
uses: ./.github/workflows/actions/get-core-dependencies
|
||||
|
||||
- name: Download Build Archive
|
||||
uses: ./.github/workflows/actions/download-archive
|
||||
with:
|
||||
name: webdriverio
|
||||
path: .
|
||||
filename: webdriverio-build.zip
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build test container
|
||||
run: |
|
||||
docker build \
|
||||
-f e2e/wdio/xvfb/docker/${{ matrix.dockerfile }} \
|
||||
-t xvfb-test:${{ matrix.distro }}-${{ matrix.scenario }} \
|
||||
e2e/wdio/xvfb/docker/
|
||||
|
||||
- name: Run XVFB E2E tests in container
|
||||
run: |
|
||||
# Copy built packages and test files to container
|
||||
docker run --rm \
|
||||
-v ${{ github.workspace }}:/workspace \
|
||||
-w /workspace \
|
||||
-e CI=true \
|
||||
-e NODE_ENV=test \
|
||||
-e WDIO_LOG_LEVEL=debug \
|
||||
xvfb-test:${{ matrix.distro }}-${{ matrix.scenario }} \
|
||||
bash -c "
|
||||
set -e
|
||||
set -o pipefail
|
||||
|
||||
# Fix permissions for testuser to access workspace
|
||||
sudo chown -R testuser:testuser /workspace/
|
||||
sudo chmod -R 755 /workspace/
|
||||
|
||||
# Install dependencies in container (pnpm already installed in Docker image)
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# Run the appropriate E2E test based on scenario
|
||||
if [ '${{ matrix.scenario }}' = 'base' ]; then
|
||||
cd e2e && pnpm test:xvfb:base
|
||||
else
|
||||
cd e2e && pnpm test:xvfb:existing
|
||||
fi
|
||||
"
|
||||
|
||||
- name: Display logs on failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Test failed, displaying diagnostic logs ==="
|
||||
|
||||
# Show WDIO-generated log files
|
||||
if [ -f e2e/wdio/xvfb/logs/wdio.log ]; then
|
||||
echo "=== WDIO Main Log ==="
|
||||
cat e2e/wdio/xvfb/logs/wdio.log
|
||||
fi
|
||||
|
||||
if [ -f e2e/wdio/xvfb/logs/wdio-0-0.log ]; then
|
||||
echo "=== WDIO Worker Log ==="
|
||||
cat e2e/wdio/xvfb/logs/wdio-0-0.log
|
||||
fi
|
||||
|
||||
if [ -f e2e/wdio/xvfb/logs/wdio-0-0-chromedriver.log ]; then
|
||||
echo "=== Chromedriver Log ==="
|
||||
cat e2e/wdio/xvfb/logs/wdio-0-0-chromedriver.log
|
||||
fi
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: failure()
|
||||
with:
|
||||
name: xvfb-e2e-logs-${{ matrix.distro }}-${{ matrix.version }}-${{ matrix.scenario }}
|
||||
path: |
|
||||
e2e/wdio/xvfb/logs/
|
||||
e2e/wdio/xvfb/screenshots/
|
||||
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
|
|
@ -78,3 +78,8 @@ jobs:
|
|||
name: E2E Tests
|
||||
needs: [smoke_tests, component_tests, unit_tests, interop_tests, typing_tests]
|
||||
uses: ./.github/workflows/test-standalone.yml
|
||||
|
||||
xvfb_tests:
|
||||
name: XVFB E2E Tests
|
||||
needs: [smoke_tests, component_tests, unit_tests, interop_tests, typing_tests]
|
||||
uses: ./.github/workflows/test-xvfb-e2e.yml
|
||||
|
|
|
|||
|
|
@ -33,6 +33,9 @@
|
|||
"test:multiremote": "wdio ./wdio/wdio-multiremote.conf.ts",
|
||||
"test:customReporter": "wdio ./custom-reporter/wdio.conf.js",
|
||||
"test:customService": "wdio ./custom-service/wdio.conf.js",
|
||||
"test:xvfb": "wdio ./wdio/xvfb/wdio.conf.ts",
|
||||
"test:xvfb:base": "wdio ./wdio/xvfb/wdio.conf.ts --spec base-install.e2e.ts",
|
||||
"test:xvfb:existing": "wdio ./wdio/xvfb/wdio.conf.ts --spec existing-xvfb.e2e.ts",
|
||||
"verifyCoverage": "test -d ./browser-runner/coverage"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -62,6 +65,7 @@
|
|||
"@wdio/shared-store-service": "workspace:*",
|
||||
"@wdio/spec-reporter": "workspace:*",
|
||||
"@wdio/types": "workspace:*",
|
||||
"@wdio/xvfb": "workspace:*",
|
||||
"babel-plugin-transform-hook-names": "^1.0.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"expect": "^29.7.0",
|
||||
|
|
|
|||
98
e2e/wdio/xvfb/base-install.e2e.ts
Normal file
98
e2e/wdio/xvfb/base-install.e2e.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { expect } from '@wdio/globals'
|
||||
import { execSync } from 'node:child_process'
|
||||
import { XvfbManager } from '@wdio/xvfb'
|
||||
|
||||
describe('xvfb fresh installation', () => {
|
||||
let xvfbManager: XvfbManager
|
||||
|
||||
beforeEach(() => {
|
||||
xvfbManager = new XvfbManager()
|
||||
})
|
||||
|
||||
it('should detect missing xvfb and install it automatically', async function() {
|
||||
// Set a longer timeout for this test since it involves package installation
|
||||
this.timeout(300000) // 5 minutes
|
||||
|
||||
// Verify we're running on Linux in CI
|
||||
expect(process.platform).toBe('linux')
|
||||
expect(process.env.CI).toBeDefined()
|
||||
|
||||
// Verify xvfb.shouldRun() returns true
|
||||
expect(xvfbManager.shouldRun()).toBe(true)
|
||||
|
||||
// Verify xvfb-run is initially not available
|
||||
let xvfbRunExists = false
|
||||
try {
|
||||
execSync('which xvfb-run', { stdio: 'ignore' })
|
||||
xvfbRunExists = true
|
||||
} catch {
|
||||
// Expected - xvfb-run should not exist initially
|
||||
}
|
||||
expect(xvfbRunExists).toBe(false)
|
||||
|
||||
// Initialize xvfb - this should trigger installation
|
||||
const initResult = await xvfbManager.init()
|
||||
expect(initResult).toBe(true)
|
||||
|
||||
// Verify xvfb-run is now available
|
||||
const xvfbRunPath = execSync('which xvfb-run', { encoding: 'utf8' }).trim()
|
||||
expect(xvfbRunPath).toContain('xvfb-run')
|
||||
expect(xvfbRunPath.length).toBeGreaterThan(0)
|
||||
|
||||
// Verify xvfb-run can execute successfully
|
||||
const testOutput = execSync('xvfb-run --help', { encoding: 'utf8' })
|
||||
expect(testOutput).toContain('xvfb-run')
|
||||
})
|
||||
|
||||
it('should work with WebDriverIO local runner integration', async () => {
|
||||
// Ensure xvfb is initialized
|
||||
await xvfbManager.init()
|
||||
|
||||
// Import ProcessFactory and test its behavior
|
||||
const { ProcessFactory } = await import('@wdio/xvfb')
|
||||
const processFactory = new ProcessFactory(xvfbManager)
|
||||
|
||||
// Create a mock worker process
|
||||
const mockProcess = await processFactory.createWorkerProcess(
|
||||
'/mock/path/run.js',
|
||||
['--test'],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: process.env as Record<string, string>,
|
||||
execArgv: [],
|
||||
stdio: ['inherit', 'pipe', 'pipe', 'ipc']
|
||||
}
|
||||
)
|
||||
|
||||
// Verify the process was created
|
||||
expect(mockProcess).toBeDefined()
|
||||
expect(typeof mockProcess.on).toBe('function')
|
||||
expect(typeof mockProcess.kill).toBe('function')
|
||||
|
||||
// Clean up
|
||||
mockProcess.kill('SIGTERM')
|
||||
})
|
||||
|
||||
it('should handle installation failures gracefully', async () => {
|
||||
// Create a manager with an unsupported package manager, forcing installation
|
||||
const failingManager = new XvfbManager({
|
||||
packageManager: 'unsupported-manager',
|
||||
forceInstall: true
|
||||
})
|
||||
|
||||
// Should throw error for unsupported package manager
|
||||
await expect(failingManager.init()).rejects.toThrow('Unsupported package manager: unsupported-manager')
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Skip process cleanup to prevent SIGTERM
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
})
|
||||
|
||||
after(() => {
|
||||
// Ensure clean exit
|
||||
setTimeout(() => {
|
||||
process.exit(0)
|
||||
}, 1000)
|
||||
})
|
||||
})
|
||||
50
e2e/wdio/xvfb/docker/alpine-base.dockerfile
Normal file
50
e2e/wdio/xvfb/docker/alpine-base.dockerfile
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
FROM alpine:3.20
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Add latest repositories
|
||||
RUN echo @latest https://dl-cdn.alpinelinux.org/alpine/latest-stable/main >> /etc/apk/repositories && \
|
||||
echo @latest https://dl-cdn.alpinelinux.org/alpine/latest-stable/community >> /etc/apk/repositories
|
||||
|
||||
# Update apk index
|
||||
RUN apk update
|
||||
|
||||
# Install basic requirements but explicitly NOT xvfb
|
||||
RUN apk add --no-cache \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs-current \
|
||||
npm \
|
||||
bash \
|
||||
which \
|
||||
unzip \
|
||||
libc6-compat \
|
||||
libstdc++
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN apk add --no-cache chromium@latest
|
||||
|
||||
# Set Chrome binary path for Alpine (uses chromium)
|
||||
ENV CHROME_BIN=/usr/bin/chromium
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN adduser -D -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
# Ensure clean environment by removing any xvfb packages (do this at the very end)
|
||||
RUN apk del xvfb-run || true && \
|
||||
rm -f /usr/bin/xvfb-run /usr/local/bin/xvfb-run
|
||||
|
||||
# Verify xvfb-run is NOT available
|
||||
RUN ! which xvfb-run || exit 1
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
48
e2e/wdio/xvfb/docker/alpine-with-xvfb.dockerfile
Normal file
48
e2e/wdio/xvfb/docker/alpine-with-xvfb.dockerfile
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
FROM alpine:3.20
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Add latest repositories
|
||||
RUN echo @latest https://dl-cdn.alpinelinux.org/alpine/latest-stable/main >> /etc/apk/repositories && \
|
||||
echo @latest https://dl-cdn.alpinelinux.org/alpine/latest-stable/community >> /etc/apk/repositories
|
||||
|
||||
# Update apk index
|
||||
RUN apk update
|
||||
|
||||
# Install requirements including xvfb
|
||||
RUN apk add --no-cache \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs-current \
|
||||
npm \
|
||||
bash \
|
||||
which \
|
||||
xvfb \
|
||||
xvfb-run \
|
||||
unzip \
|
||||
libc6-compat \
|
||||
libstdc++
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chromium for testing
|
||||
RUN apk add --no-cache chromium@latest
|
||||
|
||||
# Set Chrome binary path for Alpine (uses chromium)
|
||||
ENV CHROME_BIN=/usr/bin/chromium
|
||||
|
||||
# Verify xvfb-run is available
|
||||
RUN which xvfb-run
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN adduser -D -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
41
e2e/wdio/xvfb/docker/arch-base.dockerfile
Normal file
41
e2e/wdio/xvfb/docker/arch-base.dockerfile
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
FROM archlinux:latest
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Update system and install basic requirements but explicitly NOT xvfb
|
||||
RUN pacman -Syu --noconfirm && \
|
||||
pacman -S --noconfirm \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs \
|
||||
npm \
|
||||
which && \
|
||||
pacman -Scc --noconfirm
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chrome for testing (Arch doesn't have google-chrome in main repos, use chromium)
|
||||
RUN pacman -S --noconfirm chromium
|
||||
|
||||
# Set Chrome binary path for Arch (uses chromium)
|
||||
ENV CHROME_BIN=/usr/bin/chromium
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
# Ensure clean environment by removing any xvfb packages (do this at the very end)
|
||||
RUN pacman -R --noconfirm xorg-server-xvfb || true && \
|
||||
rm -f /usr/bin/xvfb-run /usr/local/bin/xvfb-run
|
||||
|
||||
# Verify xvfb-run is NOT available
|
||||
RUN ! which xvfb-run || exit 1
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
38
e2e/wdio/xvfb/docker/arch-with-xvfb.dockerfile
Normal file
38
e2e/wdio/xvfb/docker/arch-with-xvfb.dockerfile
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
FROM archlinux:latest
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Update system and install requirements including xvfb
|
||||
RUN pacman -Syu --noconfirm && \
|
||||
pacman -S --noconfirm \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs \
|
||||
npm \
|
||||
which \
|
||||
xorg-server-xvfb && \
|
||||
pacman -Scc --noconfirm
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chrome for testing (Arch doesn't have google-chrome in main repos, use chromium)
|
||||
RUN pacman -S --noconfirm chromium
|
||||
|
||||
# Set Chrome binary path for Arch (uses chromium)
|
||||
ENV CHROME_BIN=/usr/bin/chromium
|
||||
|
||||
# Verify xvfb-run is available
|
||||
RUN which xvfb-run
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
48
e2e/wdio/xvfb/docker/centos-stream-base.dockerfile
Normal file
48
e2e/wdio/xvfb/docker/centos-stream-base.dockerfile
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
FROM quay.io/centos/centos:stream9
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Install basic requirements but explicitly NOT xvfb
|
||||
RUN dnf update -y && \
|
||||
dnf install -y \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
which && \
|
||||
dnf clean all
|
||||
|
||||
# Install Node.js from NodeSource
|
||||
RUN curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - && \
|
||||
dnf install -y nodejs
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN echo '[google-chrome]' > /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'name=google-chrome' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'baseurl=http://dl.google.com/linux/chrome/rpm/stable/x86_64' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'enabled=1' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'gpgcheck=1' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'gpgkey=https://dl.google.com/linux/linux_signing_key.pub' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
dnf install -y google-chrome-stable && \
|
||||
dnf clean all
|
||||
|
||||
# Ensure clean environment by removing any xvfb packages
|
||||
RUN dnf remove -y xorg-x11-server-Xvfb || true && \
|
||||
dnf autoremove -y && \
|
||||
rm -f /usr/bin/xvfb-run /usr/local/bin/xvfb-run && \
|
||||
dnf clean all
|
||||
|
||||
# Verify xvfb-run is NOT available
|
||||
RUN ! which xvfb-run || exit 1
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
43
e2e/wdio/xvfb/docker/centos-stream-with-xvfb.dockerfile
Normal file
43
e2e/wdio/xvfb/docker/centos-stream-with-xvfb.dockerfile
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
FROM quay.io/centos/centos:stream9
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Install requirements including xvfb
|
||||
RUN dnf update -y && \
|
||||
dnf install -y \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
which \
|
||||
xorg-x11-server-Xvfb && \
|
||||
dnf clean all
|
||||
|
||||
# Install Node.js from NodeSource
|
||||
RUN curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - && \
|
||||
dnf install -y nodejs
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN echo '[google-chrome]' > /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'name=google-chrome' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'baseurl=http://dl.google.com/linux/chrome/rpm/stable/x86_64' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'enabled=1' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'gpgcheck=1' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'gpgkey=https://dl.google.com/linux/linux_signing_key.pub' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
dnf install -y google-chrome-stable && \
|
||||
dnf clean all
|
||||
|
||||
# Verify xvfb-run is available
|
||||
RUN which xvfb-run
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
51
e2e/wdio/xvfb/docker/debian-base.dockerfile
Normal file
51
e2e/wdio/xvfb/docker/debian-base.dockerfile
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
FROM debian:12
|
||||
|
||||
# Avoid interactive prompts during installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV CI=true
|
||||
|
||||
# Install basic requirements but explicitly NOT xvfb
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
sudo && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg && \
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y google-chrome-stable && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node.js from NodeSource
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
# Ensure clean environment by removing any xvfb packages (do this at the very end)
|
||||
RUN apt-get update -qq && \
|
||||
apt-get remove -y xvfb || true && \
|
||||
apt-get autoremove -y && \
|
||||
rm -f /usr/bin/xvfb-run /usr/local/bin/xvfb-run && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Verify xvfb-run is NOT available
|
||||
RUN ! which xvfb-run || exit 1
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
44
e2e/wdio/xvfb/docker/debian-with-xvfb.dockerfile
Normal file
44
e2e/wdio/xvfb/docker/debian-with-xvfb.dockerfile
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
FROM debian:12
|
||||
|
||||
# Avoid interactive prompts during installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV CI=true
|
||||
|
||||
# Install requirements including xvfb
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
sudo \
|
||||
xvfb && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg && \
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y google-chrome-stable && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node.js from NodeSource
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Verify xvfb-run is available
|
||||
RUN which xvfb-run
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
47
e2e/wdio/xvfb/docker/fedora-base.dockerfile
Normal file
47
e2e/wdio/xvfb/docker/fedora-base.dockerfile
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
FROM fedora:40
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Install basic requirements but explicitly NOT xvfb
|
||||
RUN dnf update -y && \
|
||||
dnf install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs \
|
||||
npm \
|
||||
which && \
|
||||
dnf clean all
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN echo '[google-chrome]' > /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'name=google-chrome' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'baseurl=http://dl.google.com/linux/chrome/rpm/stable/x86_64' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'enabled=1' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'gpgcheck=1' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'gpgkey=https://dl.google.com/linux/linux_signing_key.pub' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
dnf install -y google-chrome-stable && \
|
||||
dnf clean all
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
# Ensure clean environment by removing any xvfb packages (do this at the very end)
|
||||
RUN dnf remove -y xorg-x11-server-Xvfb || true && \
|
||||
dnf autoremove -y && \
|
||||
rm -f /usr/bin/xvfb-run /usr/local/bin/xvfb-run && \
|
||||
dnf clean all
|
||||
|
||||
# Verify xvfb-run is NOT available
|
||||
RUN ! which xvfb-run || exit 1
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
42
e2e/wdio/xvfb/docker/fedora-with-xvfb.dockerfile
Normal file
42
e2e/wdio/xvfb/docker/fedora-with-xvfb.dockerfile
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
FROM fedora:40
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Install requirements including xvfb
|
||||
RUN dnf update -y && \
|
||||
dnf install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs \
|
||||
npm \
|
||||
which \
|
||||
xorg-x11-server-Xvfb && \
|
||||
dnf clean all
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN echo '[google-chrome]' > /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'name=google-chrome' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'baseurl=http://dl.google.com/linux/chrome/rpm/stable/x86_64' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'enabled=1' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'gpgcheck=1' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
echo 'gpgkey=https://dl.google.com/linux/linux_signing_key.pub' >> /etc/yum.repos.d/google-chrome.repo && \
|
||||
dnf install -y google-chrome-stable && \
|
||||
dnf clean all
|
||||
|
||||
# Verify xvfb-run is available
|
||||
RUN which xvfb-run
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
43
e2e/wdio/xvfb/docker/suse-base.dockerfile
Normal file
43
e2e/wdio/xvfb/docker/suse-base.dockerfile
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
FROM opensuse/leap:15.6
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Install basic requirements but explicitly NOT xvfb
|
||||
RUN zypper refresh && \
|
||||
zypper install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs \
|
||||
npm && \
|
||||
zypper clean -a
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN zypper addrepo -f http://dl.google.com/linux/chrome/rpm/stable/x86_64 google-chrome && \
|
||||
rpm --import https://dl.google.com/linux/linux_signing_key.pub && \
|
||||
zypper refresh && \
|
||||
zypper install -y google-chrome-stable && \
|
||||
zypper clean -a
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN groupadd testuser && \
|
||||
useradd -m -g testuser -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
# Ensure clean environment by removing any xvfb packages (do this at the very end)
|
||||
RUN zypper remove -y xvfb-run || true && \
|
||||
rm -f /usr/bin/xvfb-run /usr/local/bin/xvfb-run && \
|
||||
zypper clean -a
|
||||
|
||||
# Verify xvfb-run is NOT available
|
||||
RUN ! which xvfb-run || exit 1
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
40
e2e/wdio/xvfb/docker/suse-with-xvfb.dockerfile
Normal file
40
e2e/wdio/xvfb/docker/suse-with-xvfb.dockerfile
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
FROM opensuse/leap:15.6
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Install requirements including xvfb
|
||||
RUN zypper refresh && \
|
||||
zypper install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs \
|
||||
npm \
|
||||
xorg-x11-server-Xvfb \
|
||||
xvfb-run && \
|
||||
zypper clean -a
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN zypper addrepo -f http://dl.google.com/linux/chrome/rpm/stable/x86_64 google-chrome && \
|
||||
rpm --import https://dl.google.com/linux/linux_signing_key.pub && \
|
||||
zypper refresh && \
|
||||
zypper install -y google-chrome-stable && \
|
||||
zypper clean -a
|
||||
|
||||
# Verify xvfb-run is available
|
||||
RUN which xvfb-run
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN groupadd testuser && \
|
||||
useradd -m -g testuser -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
51
e2e/wdio/xvfb/docker/ubuntu-base.dockerfile
Normal file
51
e2e/wdio/xvfb/docker/ubuntu-base.dockerfile
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
FROM ubuntu:24.04
|
||||
|
||||
# Avoid interactive prompts during installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV CI=true
|
||||
|
||||
# Install basic requirements but explicitly NOT xvfb
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
sudo && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg && \
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y google-chrome-stable && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node.js from NodeSource
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
# Ensure clean environment by removing any xvfb packages (do this at the very end)
|
||||
RUN apt-get update -qq && \
|
||||
apt-get remove -y xvfb || true && \
|
||||
apt-get autoremove -y && \
|
||||
rm -f /usr/bin/xvfb-run /usr/local/bin/xvfb-run && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Verify xvfb-run is NOT available
|
||||
RUN ! which xvfb-run || exit 1
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
44
e2e/wdio/xvfb/docker/ubuntu-with-xvfb.dockerfile
Normal file
44
e2e/wdio/xvfb/docker/ubuntu-with-xvfb.dockerfile
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
FROM ubuntu:24.04
|
||||
|
||||
# Avoid interactive prompts during installation
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV CI=true
|
||||
|
||||
# Install requirements including xvfb
|
||||
RUN apt-get update -qq && \
|
||||
apt-get install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
sudo \
|
||||
xvfb && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Chrome for testing
|
||||
RUN curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | gpg --dearmor -o /usr/share/keyrings/google-chrome-keyring.gpg && \
|
||||
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome-keyring.gpg] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list && \
|
||||
apt-get update -qq && \
|
||||
apt-get install -y google-chrome-stable && \
|
||||
apt-get clean && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Node.js from NodeSource
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Verify xvfb-run is available
|
||||
RUN which xvfb-run
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
43
e2e/wdio/xvfb/docker/void-base.dockerfile
Normal file
43
e2e/wdio/xvfb/docker/void-base.dockerfile
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
FROM voidlinux/voidlinux:latest
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Fix repository configuration, update XBPS, and install packages in one step
|
||||
RUN echo 'repository=https://repo-default.voidlinux.org/current' > /etc/xbps.d/00-repository-main.conf && \
|
||||
xbps-install -Sy && \
|
||||
xbps-install -yu xbps && \
|
||||
xbps-install -Sy && \
|
||||
xbps-install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs \
|
||||
bash \
|
||||
which \
|
||||
unzip \
|
||||
glibc \
|
||||
chromium
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Set Chrome binary path for Void (uses chromium)
|
||||
ENV CHROME_BIN=/usr/bin/chromium
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
# Ensure clean environment by removing any xvfb packages (do this at the very end)
|
||||
RUN xbps-remove -y xvfb-run || true && \
|
||||
rm -f /usr/bin/xvfb-run /usr/local/bin/xvfb-run
|
||||
|
||||
# Verify xvfb-run is NOT available
|
||||
RUN ! which xvfb-run || exit 1
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
40
e2e/wdio/xvfb/docker/void-with-xvfb.dockerfile
Normal file
40
e2e/wdio/xvfb/docker/void-with-xvfb.dockerfile
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
FROM voidlinux/voidlinux:latest
|
||||
|
||||
# Set environment variables
|
||||
ENV CI=true
|
||||
|
||||
# Fix repository configuration, update XBPS, and install packages in one step
|
||||
RUN echo 'repository=https://repo-default.voidlinux.org/current' > /etc/xbps.d/00-repository-main.conf && \
|
||||
xbps-install -Sy && \
|
||||
xbps-install -yu xbps && \
|
||||
xbps-install -Sy && \
|
||||
xbps-install -y \
|
||||
curl \
|
||||
ca-certificates \
|
||||
sudo \
|
||||
nodejs \
|
||||
bash \
|
||||
which \
|
||||
xvfb-run \
|
||||
unzip \
|
||||
glibc \
|
||||
chromium
|
||||
|
||||
# Install pnpm globally as root
|
||||
RUN npm install -g pnpm
|
||||
|
||||
# Set Chrome binary path for Void (uses chromium)
|
||||
ENV CHROME_BIN=/usr/bin/chromium
|
||||
|
||||
# Verify xvfb-run is available
|
||||
RUN which xvfb-run
|
||||
|
||||
# Create test user with sudo access
|
||||
RUN useradd -m -s /bin/bash testuser && \
|
||||
echo 'testuser ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
|
||||
WORKDIR /app
|
||||
USER testuser
|
||||
|
||||
# Default command
|
||||
CMD ["bash"]
|
||||
111
e2e/wdio/xvfb/existing-xvfb.e2e.ts
Normal file
111
e2e/wdio/xvfb/existing-xvfb.e2e.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { expect } from '@wdio/globals'
|
||||
import { execSync } from 'node:child_process'
|
||||
import { XvfbManager } from '@wdio/xvfb'
|
||||
|
||||
describe('xvfb existing installation', () => {
|
||||
let xvfbManager: XvfbManager
|
||||
|
||||
beforeEach(() => {
|
||||
xvfbManager = new XvfbManager()
|
||||
})
|
||||
|
||||
it('should detect existing xvfb installation', async () => {
|
||||
// Verify we're running on Linux in CI
|
||||
expect(process.platform).toBe('linux')
|
||||
expect(process.env.CI).toBeDefined()
|
||||
|
||||
// Verify xvfb.shouldRun() returns true
|
||||
expect(xvfbManager.shouldRun()).toBe(true)
|
||||
|
||||
// Verify xvfb-run is available (pre-installed in this container)
|
||||
let xvfbRunPath = ''
|
||||
try {
|
||||
xvfbRunPath = execSync('which xvfb-run', { encoding: 'utf8' }).trim()
|
||||
} catch (error) {
|
||||
throw new Error(`xvfb-run should be pre-installed: ${error}`)
|
||||
}
|
||||
expect(xvfbRunPath).toContain('xvfb-run')
|
||||
expect(xvfbRunPath.length).toBeGreaterThan(0)
|
||||
|
||||
// Initialize xvfb - should detect existing installation
|
||||
const initResult = await xvfbManager.init()
|
||||
expect(initResult).toBe(true)
|
||||
|
||||
// Verify xvfb-run still works after init
|
||||
try {
|
||||
const testOutput = execSync('xvfb-run --help', { encoding: 'utf8' })
|
||||
expect(testOutput).toContain('xvfb-run')
|
||||
} catch (error) {
|
||||
throw new Error(`xvfb-run execution failed: ${error}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should integrate correctly with ProcessFactory', async () => {
|
||||
// Initialize xvfb manager
|
||||
await xvfbManager.init()
|
||||
|
||||
// Import and test ProcessFactory integration
|
||||
const { ProcessFactory } = await import('@wdio/xvfb')
|
||||
const processFactory = new ProcessFactory(xvfbManager)
|
||||
|
||||
// Verify that shouldRun() returns true (indicating xvfb should be used)
|
||||
expect(xvfbManager.shouldRun()).toBe(true)
|
||||
|
||||
// Create a test process - this should use the spawn path with xvfb-run
|
||||
const mockProcess = await processFactory.createWorkerProcess(
|
||||
'/mock/path/run.js',
|
||||
['--test'],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: process.env as Record<string, string>,
|
||||
execArgv: [],
|
||||
stdio: ['inherit', 'pipe', 'pipe', 'ipc']
|
||||
}
|
||||
)
|
||||
|
||||
// Verify the mock process was created correctly
|
||||
expect(mockProcess).toBeDefined()
|
||||
expect(typeof mockProcess.on).toBe('function')
|
||||
expect(typeof mockProcess.kill).toBe('function')
|
||||
|
||||
// Clean up
|
||||
mockProcess.kill('SIGTERM')
|
||||
})
|
||||
|
||||
it('should handle xvfb-run command execution', async () => {
|
||||
// Initialize manager with existing xvfb
|
||||
await xvfbManager.init()
|
||||
|
||||
// Test that we can execute a simple command under xvfb-run
|
||||
try {
|
||||
// Execute a basic command that should work under xvfb
|
||||
const result = execSync('xvfb-run --auto-servernum -- echo "test successful"', {
|
||||
encoding: 'utf8',
|
||||
timeout: 10000
|
||||
})
|
||||
expect(result.trim()).toBe('test successful')
|
||||
} catch (error) {
|
||||
throw new Error(`xvfb-run command execution failed: ${error}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('should work on different platforms appropriately', async () => {
|
||||
// Test platform detection logic
|
||||
if (process.platform === 'linux') {
|
||||
// On Linux, should always return true in CI
|
||||
expect(xvfbManager.shouldRun()).toBe(true)
|
||||
} else {
|
||||
// On non-Linux platforms, should return false
|
||||
expect(xvfbManager.shouldRun()).toBe(false)
|
||||
|
||||
// Init should return false on non-Linux
|
||||
const result = await xvfbManager.init()
|
||||
expect(result).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
// Skip process cleanup to prevent SIGTERM
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
})
|
||||
})
|
||||
60
e2e/wdio/xvfb/wdio.conf.ts
Normal file
60
e2e/wdio/xvfb/wdio.conf.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import url from 'node:url'
|
||||
import path from 'node:path'
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url))
|
||||
|
||||
export const config: WebdriverIO.Config = {
|
||||
/**
|
||||
* specify test files
|
||||
*/
|
||||
specs: [
|
||||
path.join(__dirname, '*.e2e.ts')
|
||||
],
|
||||
|
||||
/**
|
||||
* capabilities - run headless since we're testing xvfb functionality
|
||||
*/
|
||||
capabilities: [{
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: [
|
||||
'--headless=new',
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage'
|
||||
],
|
||||
// Handle Alpine Linux with chromium-browser
|
||||
...(process.env.CHROME_BIN && { binary: process.env.CHROME_BIN })
|
||||
}
|
||||
}],
|
||||
|
||||
/**
|
||||
* test configurations
|
||||
*/
|
||||
logLevel: 'info',
|
||||
framework: 'mocha',
|
||||
outputDir: path.join(__dirname, 'logs'),
|
||||
|
||||
/**
|
||||
* Use local runner to test xvfb integration
|
||||
*/
|
||||
runner: 'local',
|
||||
|
||||
/**
|
||||
* Disable automatic xvfb initialization so tests can control it manually
|
||||
*/
|
||||
autoXvfb: false,
|
||||
|
||||
/**
|
||||
* Reporters
|
||||
*/
|
||||
reporters: ['spec'],
|
||||
|
||||
/**
|
||||
* Mocha options
|
||||
*/
|
||||
mochaOpts: {
|
||||
ui: 'bdd',
|
||||
timeout: 300000, // 5 minutes to allow for package installation
|
||||
require: []
|
||||
},
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@
|
|||
"compile": "pnpm -r --filter=@wdio/compiler run build",
|
||||
"compile:all": "run-s compile:all:*",
|
||||
"compile:all:core": "pnpm -r --filter=@wdio/compiler run build -p @wdio/logger -p @wdio/types -p @wdio/globals -p @wdio/protocols -p @wdio/repl",
|
||||
"compile:all:main": "pnpm -r --filter=@wdio/compiler run build -p @wdio/utils -p @wdio/reporter -p @wdio/config -p webdriver -p webdriverio",
|
||||
"compile:all:main": "pnpm -r --filter=@wdio/compiler run build -p @wdio/utils -p @wdio/reporter -p @wdio/config -p @wdio/xvfb -p webdriver -p webdriverio",
|
||||
"compile:all:all": "pnpm -r --filter=@wdio/compiler run build",
|
||||
"generate": "run-s generate:*",
|
||||
"generate:bidi": "tsx ./scripts/bidi/index.ts && pnpm eslint --fix packages/webdriver/src/bidi/",
|
||||
|
|
@ -32,6 +32,7 @@
|
|||
"docs:generate": "run-s clean:website generate:bidi && tsx ./scripts/docs-generation/generateDocs.ts",
|
||||
"docs:build": "cd ./website && pnpm build && cd ..",
|
||||
"docs:deploy": "tsx ./scripts/updateDocs.ts",
|
||||
"eslint:fix": "eslint --fix packages examples scripts tests",
|
||||
"test": "run-s test:eslint test:ejslint test:typings test:unit:run test:smoke test:component",
|
||||
"test:depcheck": "tsx ./scripts/depcheck.ts",
|
||||
"test:eslint": "eslint --cache packages examples scripts tests",
|
||||
|
|
@ -56,6 +57,7 @@
|
|||
"test:e2e:classic": "cd ./e2e && npx wdio run ./wdio/wdio.classic.conf.ts",
|
||||
"test:e2e:cloud": "cd ./e2e && npx wdio run ./wdio/wdio.sauce.conf.ts",
|
||||
"test:e2e:launch": "vitest --config ./e2e/vitest.config.ts --run launch",
|
||||
"test:e2e:xvfb": "cd ./e2e && npx wdio run ./wdio/xvfb/wdio.conf.ts",
|
||||
"test:e2e": "run-s test:e2e:* ",
|
||||
"dev": "pnpm -r --filter=@wdio/compiler run build --watch",
|
||||
"watch:docs": "pnpm docs:generate && cd website && pnpm i && pnpm start",
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
"@wdio/repl": "workspace:*",
|
||||
"@wdio/runner": "workspace:*",
|
||||
"@wdio/types": "workspace:*",
|
||||
"@wdio/xvfb": "workspace:*",
|
||||
"exit-hook": "^4.0.0",
|
||||
"expect-webdriverio": "^5.3.4",
|
||||
"split2": "^4.1.0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import logger from '@wdio/logger'
|
||||
import { WritableStreamBuffer } from 'stream-buffers'
|
||||
import { XvfbManager } from '@wdio/xvfb'
|
||||
import type { Workers } from '@wdio/types'
|
||||
|
||||
import WorkerInstance from './worker.js'
|
||||
|
|
@ -16,25 +17,41 @@ export interface RunArgs extends Workers.WorkerRunPayload {
|
|||
|
||||
export default class LocalRunner {
|
||||
workerPool: Record<string, WorkerInstance> = {}
|
||||
private xvfbInitialized = false
|
||||
private xvfbManager: XvfbManager
|
||||
|
||||
stdout = new WritableStreamBuffer(BUFFER_OPTIONS)
|
||||
stderr = new WritableStreamBuffer(BUFFER_OPTIONS)
|
||||
|
||||
constructor (
|
||||
constructor(
|
||||
private _options: never,
|
||||
protected _config: WebdriverIO.Config
|
||||
) {}
|
||||
protected config: WebdriverIO.Config
|
||||
) {
|
||||
// Initialize XvfbManager
|
||||
this.xvfbManager = new XvfbManager({
|
||||
xvfbMaxRetries: this.config.xvfbMaxRetries,
|
||||
xvfbRetryDelay: this.config.xvfbRetryDelay
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* nothing to initialize when running locally
|
||||
* initialize local runner environment
|
||||
*/
|
||||
initialize () {}
|
||||
async initialize() {
|
||||
// XVFB initialization is handled lazily during first worker creation, to access capabilities for headless detection
|
||||
}
|
||||
|
||||
getWorkerCount () {
|
||||
getWorkerCount() {
|
||||
return Object.keys(this.workerPool).length
|
||||
}
|
||||
|
||||
async run ({ command, args, ...workerOptions }: RunArgs) {
|
||||
async run({ command, args, ...workerOptions }: RunArgs) {
|
||||
// Initialize XVFB lazily on first worker creation
|
||||
if (!this.xvfbInitialized) {
|
||||
await this.initializeXvfb(workerOptions)
|
||||
this.xvfbInitialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* adjust max listeners on stdout/stderr when creating listeners
|
||||
*/
|
||||
|
|
@ -44,23 +61,61 @@ export default class LocalRunner {
|
|||
process.stderr.setMaxListeners(workerCnt + 2)
|
||||
}
|
||||
|
||||
const worker = new WorkerInstance(this._config, workerOptions, this.stdout, this.stderr)
|
||||
const worker = new WorkerInstance(
|
||||
this.config,
|
||||
workerOptions,
|
||||
this.stdout,
|
||||
this.stderr,
|
||||
this.xvfbManager
|
||||
)
|
||||
this.workerPool[workerOptions.cid] = worker
|
||||
worker.postMessage(command, args)
|
||||
await worker.postMessage(command, args)
|
||||
return worker
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize XVFB with capability-aware detection
|
||||
*/
|
||||
private async initializeXvfb(workerOptions: Workers.WorkerRunPayload) {
|
||||
// Skip Xvfb initialization if disabled
|
||||
if (this.config.autoXvfb === false) {
|
||||
log.info('Skipping automatic Xvfb initialization (disabled by config)')
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize Xvfb if needed for headless testing
|
||||
try {
|
||||
const capabilities = workerOptions.caps
|
||||
const xvfbInitialized = await this.xvfbManager.init(capabilities)
|
||||
if (xvfbInitialized) {
|
||||
log.info('Xvfb is ready for use')
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn(
|
||||
'Failed to initialize Xvfb, continuing without virtual display:',
|
||||
error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* shutdown all worker processes
|
||||
*
|
||||
* @return {Promise} resolves when all worker have been shutdown or
|
||||
* a timeout was reached
|
||||
*/
|
||||
shutdown () {
|
||||
async shutdown() {
|
||||
log.info('Shutting down spawned worker')
|
||||
|
||||
for (const [cid, worker] of Object.entries(this.workerPool)) {
|
||||
const { capabilities, server, sessionId, config, isMultiremote, instances } = worker
|
||||
const {
|
||||
capabilities,
|
||||
server,
|
||||
sessionId,
|
||||
config,
|
||||
isMultiremote,
|
||||
instances,
|
||||
} = worker
|
||||
let payload: Partial<Workers.WorkerMessageArgs> = {}
|
||||
|
||||
/**
|
||||
|
|
@ -73,21 +128,22 @@ export default class LocalRunner {
|
|||
capabilities,
|
||||
watch: true,
|
||||
isMultiremote,
|
||||
instances
|
||||
instances,
|
||||
} as unknown as Workers.WorkerMessageArgs
|
||||
} else if (!worker.isBusy) {
|
||||
delete this.workerPool[cid]
|
||||
continue
|
||||
}
|
||||
|
||||
worker.postMessage('endSession', payload)
|
||||
await worker.postMessage('endSession', payload)
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
const shutdownResult = await new Promise<boolean>((resolve) => {
|
||||
const timeout = setTimeout(resolve, SHUTDOWN_TIMEOUT)
|
||||
const interval = setInterval(() => {
|
||||
const busyWorker = Object.entries(this.workerPool)
|
||||
.filter(([, worker]) => worker.isBusy).length
|
||||
const busyWorker = Object.entries(this.workerPool).filter(
|
||||
([, worker]) => worker.isBusy
|
||||
).length
|
||||
|
||||
log.info(`Waiting for ${busyWorker} to shut down gracefully`)
|
||||
if (busyWorker === 0) {
|
||||
|
|
@ -98,5 +154,12 @@ export default class LocalRunner {
|
|||
}
|
||||
}, 250)
|
||||
})
|
||||
|
||||
// Xvfb cleanup is handled automatically by xvfb-run
|
||||
if (this.xvfbManager.shouldRun()) {
|
||||
log.info('Xvfb cleanup handled automatically by xvfb-run')
|
||||
}
|
||||
|
||||
return shutdownResult
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import url from 'node:url'
|
||||
import path from 'node:path'
|
||||
import child from 'node:child_process'
|
||||
import { EventEmitter } from 'node:events'
|
||||
import type { ChildProcess } from 'node:child_process'
|
||||
import type { WritableStreamBuffer } from 'stream-buffers'
|
||||
import { ProcessFactory, type XvfbManager } from '@wdio/xvfb'
|
||||
import type { Workers } from '@wdio/types'
|
||||
import type { ReplConfig } from '@wdio/repl'
|
||||
|
||||
|
|
@ -45,6 +45,7 @@ export default class WorkerInstance extends EventEmitter implements Workers.Work
|
|||
sessionId?: string
|
||||
server?: Record<string, string>
|
||||
logsAggregator: string[] = []
|
||||
#processFactory: ProcessFactory
|
||||
|
||||
instances?: Record<string, { sessionId: string }>
|
||||
isMultiremote?: boolean
|
||||
|
|
@ -65,12 +66,14 @@ export default class WorkerInstance extends EventEmitter implements Workers.Work
|
|||
* @param {string[]} specs list of paths to test files to run in this worker
|
||||
* @param {number} retries number of retries remaining
|
||||
* @param {object} execArgv execution arguments for the test run
|
||||
* @param {XvfbManager} xvfbManager configured XvfbManager instance
|
||||
*/
|
||||
constructor(
|
||||
config: WebdriverIO.Config,
|
||||
{ cid, configFile, caps, specs, execArgv, retries }: Workers.WorkerRunPayload,
|
||||
stdout: WritableStreamBuffer,
|
||||
stderr: WritableStreamBuffer
|
||||
stderr: WritableStreamBuffer,
|
||||
xvfbManager: XvfbManager
|
||||
) {
|
||||
super()
|
||||
this.cid = cid
|
||||
|
|
@ -83,6 +86,7 @@ export default class WorkerInstance extends EventEmitter implements Workers.Work
|
|||
this.retries = retries
|
||||
this.stdout = stdout
|
||||
this.stderr = stderr
|
||||
this.#processFactory = new ProcessFactory(xvfbManager)
|
||||
|
||||
this.isReady = new Promise((resolve) => { this.isReadyResolver = resolve })
|
||||
this.isSetup = new Promise((resolve) => { this.isSetupResolver = resolve })
|
||||
|
|
@ -91,7 +95,7 @@ export default class WorkerInstance extends EventEmitter implements Workers.Work
|
|||
/**
|
||||
* spawns process to kick of wdio-runner
|
||||
*/
|
||||
startProcess() {
|
||||
async startProcess() {
|
||||
const { cid, execArgv } = this
|
||||
const argv = process.argv.slice(2)
|
||||
|
||||
|
|
@ -112,12 +116,18 @@ export default class WorkerInstance extends EventEmitter implements Workers.Work
|
|||
runnerEnv.NODE_OPTIONS = process.env.NODE_OPTIONS + ' ' + (runnerEnv.NODE_OPTIONS || '')
|
||||
|
||||
log.info(`Start worker ${cid} with arg: ${argv.join(' ')}`)
|
||||
const childProcess = this.childProcess = child.fork(path.join(__dirname, 'run.js'), argv, {
|
||||
cwd: process.cwd(),
|
||||
env: runnerEnv,
|
||||
execArgv,
|
||||
stdio: ['inherit', 'pipe', 'pipe', 'ipc']
|
||||
})
|
||||
|
||||
// Use ProcessFactory to create the appropriate process
|
||||
const childProcess = this.childProcess = await this.#processFactory.createWorkerProcess(
|
||||
path.join(__dirname, 'run.js'),
|
||||
argv,
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: runnerEnv,
|
||||
execArgv,
|
||||
stdio: ['inherit', 'pipe', 'pipe', 'ipc']
|
||||
}
|
||||
)
|
||||
|
||||
childProcess.on('message', this._handleMessage.bind(this))
|
||||
childProcess.on('error', this._handleError.bind(this))
|
||||
|
|
@ -226,7 +236,7 @@ export default class WorkerInstance extends EventEmitter implements Workers.Work
|
|||
* @param command method to run in wdio-runner
|
||||
* @param args arguments for functions to call
|
||||
*/
|
||||
postMessage (command: string, args: Workers.WorkerMessageArgs, requiresSetup = false): void {
|
||||
async postMessage (command: string, args: Workers.WorkerMessageArgs, requiresSetup = false): Promise<void> {
|
||||
const { cid, configFile, capabilities, specs, retries, isBusy } = this
|
||||
|
||||
if (isBusy && !ACCEPTABLE_BUSY_COMMANDS.includes(command)) {
|
||||
|
|
@ -238,7 +248,7 @@ export default class WorkerInstance extends EventEmitter implements Workers.Work
|
|||
* closes after running its job
|
||||
*/
|
||||
if (!this.childProcess) {
|
||||
this.childProcess = this.startProcess()
|
||||
this.childProcess = await this.startProcess()
|
||||
}
|
||||
|
||||
const cmd: Workers.WorkerCommand = { cid, command, configFile, args, caps: capabilities, specs, retries }
|
||||
|
|
|
|||
|
|
@ -1,30 +1,46 @@
|
|||
import path from 'node:path'
|
||||
import child from 'node:child_process'
|
||||
import { expect, test, vi } from 'vitest'
|
||||
import { expect, test, vi, beforeEach } from 'vitest'
|
||||
|
||||
import LocalRunner from '../src/index.js'
|
||||
|
||||
const sleep = (ms = 100) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
vi.mock('child_process', () => {
|
||||
vi.mock(
|
||||
'@wdio/logger',
|
||||
() => import(path.join(process.cwd(), '__mocks__', '@wdio/logger'))
|
||||
)
|
||||
|
||||
vi.mock('@wdio/xvfb', () => {
|
||||
const childProcessMock = {
|
||||
on: vi.fn(),
|
||||
send: vi.fn(),
|
||||
kill: vi.fn()
|
||||
kill: vi.fn(),
|
||||
}
|
||||
|
||||
return {
|
||||
default: { fork: vi.fn().mockReturnValue(childProcessMock) }
|
||||
ProcessFactory: vi.fn().mockImplementation(() => ({
|
||||
createWorkerProcess: vi.fn().mockResolvedValue(childProcessMock)
|
||||
})),
|
||||
XvfbManager: vi.fn().mockImplementation(() => ({
|
||||
init: vi.fn().mockResolvedValue(true),
|
||||
shouldRun: vi.fn().mockReturnValue(true)
|
||||
})),
|
||||
default: vi.fn()
|
||||
}
|
||||
})
|
||||
|
||||
test('should fork a new process', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 }
|
||||
} as any)
|
||||
const runner = new LocalRunner(
|
||||
undefined as never,
|
||||
{
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 },
|
||||
} as any
|
||||
)
|
||||
const worker = await runner.run({
|
||||
cid: '0-5',
|
||||
command: 'run',
|
||||
|
|
@ -33,17 +49,12 @@ test('should fork a new process', async () => {
|
|||
caps: {},
|
||||
specs: ['/foo/bar.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0
|
||||
retries: 0,
|
||||
})
|
||||
worker['_handleMessage']({ name: 'ready' } as any)
|
||||
await sleep()
|
||||
|
||||
expect(worker.isBusy).toBe(true)
|
||||
expect(vi.mocked(child.fork).mock.calls[0][0].endsWith('run.js')).toBe(true)
|
||||
|
||||
const { env } = vi.mocked(child.fork).mock.calls[0][2]! as any
|
||||
expect(env.WDIO_LOG_PATH).toMatch(/(\\|\/)foo(\\|\/)bar(\\|\/)wdio-0-5\.log/)
|
||||
expect(env.FORCE_COLOR).toBe(1)
|
||||
expect(worker.childProcess?.on).toHaveBeenCalled()
|
||||
|
||||
expect(worker.childProcess?.send).toHaveBeenCalledWith({
|
||||
|
|
@ -53,17 +64,20 @@ test('should fork a new process', async () => {
|
|||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
retries: 0,
|
||||
specs: ['/foo/bar.test.js']
|
||||
specs: ['/foo/bar.test.js'],
|
||||
})
|
||||
|
||||
worker.postMessage('runAgain', { foo: 'bar' })
|
||||
await worker.postMessage('runAgain', { foo: 'bar' } as any)
|
||||
})
|
||||
|
||||
test('should shut down worker processes', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 }
|
||||
} as any)
|
||||
const runner = new LocalRunner(
|
||||
undefined as never,
|
||||
{
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 },
|
||||
} as any
|
||||
)
|
||||
const worker1 = await runner.run({
|
||||
cid: '0-4',
|
||||
command: 'run',
|
||||
|
|
@ -72,7 +86,7 @@ test('should shut down worker processes', async () => {
|
|||
caps: {},
|
||||
specs: ['/foo/bar2.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0
|
||||
retries: 0,
|
||||
})
|
||||
worker1['_handleMessage']({ name: 'ready' } as any)
|
||||
await sleep()
|
||||
|
|
@ -84,7 +98,7 @@ test('should shut down worker processes', async () => {
|
|||
caps: {},
|
||||
specs: ['/foo/bar.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0
|
||||
retries: 0,
|
||||
})
|
||||
worker2['_handleMessage']({ name: 'ready' } as any)
|
||||
await sleep()
|
||||
|
|
@ -100,29 +114,36 @@ test('should shut down worker processes', async () => {
|
|||
const after = Date.now()
|
||||
|
||||
expect(after - before).toBeGreaterThanOrEqual(750)
|
||||
const call1: any = vi.mocked(worker1.childProcess?.send)!.mock.calls.pop()![0]
|
||||
const call1: any = vi
|
||||
.mocked(worker1.childProcess?.send)!
|
||||
.mock.calls.pop()![0]
|
||||
expect(call1.cid).toBe('0-5')
|
||||
expect(call1.command).toBe('endSession')
|
||||
const call2: any = vi.mocked(worker1.childProcess?.send)!.mock.calls.pop()![0]
|
||||
const call2: any = vi
|
||||
.mocked(worker1.childProcess?.send)!
|
||||
.mock.calls.pop()![0]
|
||||
expect(call2.cid).toBe('0-4')
|
||||
expect(call2.command).toBe('endSession')
|
||||
})
|
||||
|
||||
test('should avoid shutting down if worker is not busy', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 }
|
||||
} as any)
|
||||
const runner = new LocalRunner(
|
||||
undefined as never,
|
||||
{
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 },
|
||||
} as any
|
||||
)
|
||||
|
||||
await runner.run({
|
||||
cid: '0-8',
|
||||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
args: { sessionId: 'abc' },
|
||||
args: { sessionId: 'abc' } as any,
|
||||
caps: {},
|
||||
specs: ['/foo/bar2.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0
|
||||
retries: 0,
|
||||
})
|
||||
runner.workerPool['0-8'].isBusy = false
|
||||
|
||||
|
|
@ -132,21 +153,24 @@ test('should avoid shutting down if worker is not busy', async () => {
|
|||
})
|
||||
|
||||
test('should shut down worker processes in watch mode - regular', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 },
|
||||
watch: true,
|
||||
} as any)
|
||||
const runner = new LocalRunner(
|
||||
undefined as never,
|
||||
{
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 },
|
||||
watch: true,
|
||||
} as any
|
||||
)
|
||||
|
||||
const worker = await runner.run({
|
||||
cid: '0-6',
|
||||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
args: { sessionId: 'abc' },
|
||||
args: { sessionId: 'abc' } as any,
|
||||
caps: {},
|
||||
specs: ['/foo/bar2.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0
|
||||
retries: 0,
|
||||
})
|
||||
worker['_handleMessage']({ name: 'ready' } as any)
|
||||
runner.workerPool['0-6'].sessionId = 'abc'
|
||||
|
|
@ -163,7 +187,9 @@ test('should shut down worker processes in watch mode - regular', async () => {
|
|||
|
||||
expect(after - before).toBeGreaterThanOrEqual(300)
|
||||
|
||||
const call: any = vi.mocked(worker.childProcess?.send)!.mock.calls.pop()![0]
|
||||
const call: any = vi
|
||||
.mocked(worker.childProcess?.send)!
|
||||
.mock.calls.pop()![0]
|
||||
expect(call.cid).toBe('0-6')
|
||||
expect(call.command).toBe('endSession')
|
||||
expect(call.args.watch).toBe(true)
|
||||
|
|
@ -173,11 +199,14 @@ test('should shut down worker processes in watch mode - regular', async () => {
|
|||
})
|
||||
|
||||
test('should shut down worker processes in watch mode - mutliremote', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 },
|
||||
watch: true,
|
||||
} as any)
|
||||
const runner = new LocalRunner(
|
||||
undefined as never,
|
||||
{
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 },
|
||||
watch: true,
|
||||
} as any
|
||||
)
|
||||
|
||||
const worker = await runner.run({
|
||||
cid: '0-7',
|
||||
|
|
@ -187,15 +216,15 @@ test('should shut down worker processes in watch mode - mutliremote', async () =
|
|||
caps: {},
|
||||
specs: ['/foo/bar.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0
|
||||
retries: 0,
|
||||
})
|
||||
worker['_handleMessage']({ name: 'ready' } as any)
|
||||
runner.workerPool['0-7'].isMultiremote = true
|
||||
runner.workerPool['0-7'].instances = { foo: { sessionId: '123' } }
|
||||
runner.workerPool['0-7'].caps = {
|
||||
foo: {
|
||||
capabilities: { browser: 'chrome' }
|
||||
}
|
||||
capabilities: { browser: 'chrome' },
|
||||
},
|
||||
} as any
|
||||
|
||||
setTimeout(() => {
|
||||
|
|
@ -208,7 +237,9 @@ test('should shut down worker processes in watch mode - mutliremote', async () =
|
|||
|
||||
expect(after - before).toBeGreaterThanOrEqual(300)
|
||||
|
||||
const call: any = vi.mocked(worker.childProcess?.send)!.mock.calls.pop()![0]
|
||||
const call: any = vi
|
||||
.mocked(worker.childProcess?.send)!
|
||||
.mock.calls.pop()![0]
|
||||
expect(call.cid).toBe('0-7')
|
||||
expect(call.command).toBe('endSession')
|
||||
expect(call.args.watch).toBe(true)
|
||||
|
|
@ -218,5 +249,172 @@ test('should shut down worker processes in watch mode - mutliremote', async () =
|
|||
|
||||
test('should avoid shutting down if worker is not busy', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {} as any)
|
||||
expect(runner.initialize()).toBe(undefined)
|
||||
expect(await runner.initialize()).toBe(undefined)
|
||||
})
|
||||
|
||||
test('should initialize xvfb lazily during first run when needed', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {} as any)
|
||||
|
||||
// Mock the xvfbManager instance that was created in constructor
|
||||
const mockInit = vi.fn().mockResolvedValue(true)
|
||||
runner['xvfbManager'] = { init: mockInit } as any
|
||||
|
||||
// Initialize should not call xvfb.init
|
||||
await runner.initialize()
|
||||
expect(mockInit).not.toHaveBeenCalled()
|
||||
|
||||
// First run should initialize xvfb
|
||||
await runner.run({
|
||||
cid: '0-1',
|
||||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
args: {},
|
||||
caps: { 'goog:chromeOptions': { args: ['--headless'] } },
|
||||
specs: ['/foo/bar.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0,
|
||||
})
|
||||
|
||||
expect(mockInit).toHaveBeenCalledWith({ 'goog:chromeOptions': { args: ['--headless'] } })
|
||||
})
|
||||
|
||||
test('should not initialize xvfb during run when not needed', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {} as any)
|
||||
|
||||
// Mock the xvfbManager instance that was created in constructor
|
||||
const mockInit = vi.fn().mockResolvedValue(false)
|
||||
runner['xvfbManager'] = { init: mockInit } as any
|
||||
|
||||
await runner.initialize()
|
||||
|
||||
await runner.run({
|
||||
cid: '0-2',
|
||||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
args: {},
|
||||
caps: {},
|
||||
specs: ['/foo/bar.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0,
|
||||
})
|
||||
|
||||
expect(mockInit).toHaveBeenCalled()
|
||||
// Verify that xvfb didn't actually initialize (returned false)
|
||||
const initResult = await mockInit.mock.results[0].value
|
||||
expect(initResult).toBe(false)
|
||||
})
|
||||
|
||||
test('should handle xvfb initialization failure gracefully', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {} as any)
|
||||
|
||||
// Mock the xvfbManager instance that was created in constructor
|
||||
const mockInit = vi.fn().mockResolvedValue(true)
|
||||
runner['xvfbManager'] = { init: mockInit } as any
|
||||
|
||||
// Should not throw during run, just log the error
|
||||
await expect(runner.run({
|
||||
cid: '0-3',
|
||||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
args: {},
|
||||
caps: {},
|
||||
specs: ['/foo/bar.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0,
|
||||
})).resolves.toBeDefined()
|
||||
expect(mockInit).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should only initialize xvfb once across multiple runs', async () => {
|
||||
const runner = new LocalRunner(undefined as never, {} as any)
|
||||
|
||||
// Mock the xvfbManager instance that was created in constructor
|
||||
const mockInit = vi.fn().mockResolvedValue(true)
|
||||
runner['xvfbManager'] = { init: mockInit } as any
|
||||
|
||||
// First run should initialize xvfb
|
||||
await runner.run({
|
||||
cid: '0-4',
|
||||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
args: {},
|
||||
caps: {},
|
||||
specs: ['/foo/bar1.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0,
|
||||
})
|
||||
|
||||
// Second run should not initialize xvfb again
|
||||
await runner.run({
|
||||
cid: '0-5',
|
||||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
args: {},
|
||||
caps: {},
|
||||
specs: ['/foo/bar2.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0,
|
||||
})
|
||||
|
||||
expect(mockInit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('should handle xvfb operations with existing workers', async () => {
|
||||
const runner = new LocalRunner(
|
||||
undefined as never,
|
||||
{
|
||||
outputDir: '/foo/bar',
|
||||
runnerEnv: { FORCE_COLOR: 1 },
|
||||
} as any
|
||||
)
|
||||
|
||||
// Mock the xvfbManager instance that was created in constructor
|
||||
const mockInit = vi.fn().mockResolvedValue(true)
|
||||
const mockShouldRun = vi.fn().mockReturnValue(true)
|
||||
runner['xvfbManager'] = { init: mockInit, shouldRun: mockShouldRun } as any
|
||||
|
||||
// Start a worker (should initialize xvfb)
|
||||
const worker = await runner.run({
|
||||
cid: '0-9',
|
||||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
args: {},
|
||||
caps: {},
|
||||
specs: ['/foo/bar.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0,
|
||||
})
|
||||
worker['_handleMessage']({ name: 'ready' } as any)
|
||||
|
||||
setTimeout(() => {
|
||||
worker.isBusy = false
|
||||
}, 100)
|
||||
|
||||
await runner.shutdown()
|
||||
|
||||
expect(mockInit).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('should skip xvfb initialization when disabled in config', async () => {
|
||||
const runner = new LocalRunner(
|
||||
undefined as never,
|
||||
{ autoXvfb: false } as any
|
||||
)
|
||||
|
||||
// Mock the xvfbManager instance that was created in constructor
|
||||
const mockInit = vi.fn().mockResolvedValue(true)
|
||||
runner['xvfbManager'] = { init: mockInit } as any
|
||||
|
||||
await runner.run({
|
||||
cid: '0-10',
|
||||
command: 'run',
|
||||
configFile: '/path/to/wdio.conf.js',
|
||||
args: {},
|
||||
caps: {},
|
||||
specs: ['/foo/bar.test.js'],
|
||||
execArgv: [],
|
||||
retries: 0,
|
||||
})
|
||||
|
||||
expect(mockInit).not.toHaveBeenCalled()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -105,20 +105,24 @@ test('should call gracefulExit(130) and set sigintWasCalled on SIGINT', () => {
|
|||
|
||||
test('should delay shutdown in exitHook if SIGINT was received', async () => {
|
||||
runner.sigintWasCalled = true
|
||||
const startTime = Date.now()
|
||||
const startTime = process.hrtime.bigint()
|
||||
await exitHookCallback?.()
|
||||
const endTime = Date.now()
|
||||
const endTime = process.hrtime.bigint()
|
||||
// Should wait at least SHUTDOWN_TIMEOUT (10ms in test due to mock)
|
||||
expect(endTime - startTime).toBeGreaterThanOrEqual(10)
|
||||
// Use nanosecond precision and allow for 9ms minimum to account for timing variance
|
||||
const elapsedMs = Number(endTime - startTime) / 1_000_000
|
||||
expect(elapsedMs).toBeGreaterThanOrEqual(9)
|
||||
})
|
||||
|
||||
test('should not delay in exitHook if SIGINT was not received', async () => {
|
||||
runner.sigintWasCalled = false
|
||||
const start = Date.now()
|
||||
const startTime = process.hrtime.bigint()
|
||||
await exitHookCallback?.()
|
||||
const end = Date.now()
|
||||
const endTime = process.hrtime.bigint()
|
||||
// Should not wait (almost immediate) when sigintWasCalled = false
|
||||
expect(end - start).toBeLessThan(10)
|
||||
// Allow up to 5ms for normal async overhead
|
||||
const elapsedMs = Number(endTime - startTime) / 1_000_000
|
||||
expect(elapsedMs).toBeLessThan(5)
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
|
|
|
|||
|
|
@ -19,9 +19,14 @@ const workerConfig = {
|
|||
|
||||
vi.mock('@wdio/logger', () => import(path.join(process.cwd(), '__mocks__', '@wdio/logger')))
|
||||
|
||||
// Mock XvfbManager
|
||||
const mockXvfbManager = {
|
||||
init: vi.fn().mockResolvedValue(true)
|
||||
}
|
||||
|
||||
describe('handleMessage', () => {
|
||||
it('should emit payload with cid', () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
worker.emit = vi.fn()
|
||||
|
||||
worker['_handleMessage']({ foo: 'bar' } as unknown as Workers.WorkerMessage)
|
||||
|
|
@ -32,20 +37,20 @@ describe('handleMessage', () => {
|
|||
})
|
||||
|
||||
it('should un mark worker as busy if command is finished', () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
worker.isBusy = true
|
||||
worker['_handleMessage']({ name: 'finishedCommand' } as unknown as Workers.WorkerMessage)
|
||||
expect(worker.isBusy).toBe(false)
|
||||
})
|
||||
|
||||
it('should mark worker as ready if ready message was received', async () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
worker['_handleMessage']({ name: 'ready' } as unknown as Workers.WorkerMessage)
|
||||
expect(await worker.isReady).toBe(true)
|
||||
})
|
||||
|
||||
it('stores sessionId and connection data to worker instance', () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
worker.emit = vi.fn()
|
||||
const payload = {
|
||||
name: 'sessionStarted',
|
||||
|
|
@ -59,7 +64,7 @@ describe('handleMessage', () => {
|
|||
})
|
||||
|
||||
it('stores instances to worker instance in Multiremote mode', () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
const payload = {
|
||||
name: 'sessionStarted',
|
||||
content: {
|
||||
|
|
@ -75,7 +80,7 @@ describe('handleMessage', () => {
|
|||
|
||||
describe('handleError', () => {
|
||||
it('should emit error', () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
worker.emit = vi.fn()
|
||||
worker['_handleError']({ foo: 'bar' } as unknown as Error)
|
||||
expect(worker.emit).toBeCalledWith('error', {
|
||||
|
|
@ -87,7 +92,7 @@ describe('handleError', () => {
|
|||
|
||||
describe('handleExit', () => {
|
||||
it('should handle it', () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
const childProcess = { kill: vi.fn() }
|
||||
worker.childProcess = childProcess as unknown as ChildProcess
|
||||
worker.isBusy = true
|
||||
|
|
@ -106,25 +111,25 @@ describe('handleExit', () => {
|
|||
})
|
||||
|
||||
describe('postMessage', () => {
|
||||
it('should log if the cid is busy and exit', () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
it('should log if the cid is busy and exit', async () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
const log = logger('webdriver')
|
||||
vi.spyOn(log, 'info').mockImplementation((string) => string)
|
||||
|
||||
worker.isBusy = true
|
||||
worker.postMessage('test-message', {})
|
||||
await worker.postMessage('test-message', {})
|
||||
|
||||
expect(log.info)
|
||||
.toHaveBeenCalledWith('worker with cid 0-3 already busy and can\'t take new commands')
|
||||
})
|
||||
|
||||
it('should create a process if it does not have one', () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
it('should create a process if it does not have one', async () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
worker.isReady = Promise.resolve(true)
|
||||
worker.childProcess = undefined
|
||||
vi.spyOn(worker, 'startProcess').mockImplementation(
|
||||
() => ({ send: vi.fn() }) as unknown as ChildProcess)
|
||||
worker.postMessage('test-message', {})
|
||||
async () => ({ send: vi.fn() }) as unknown as ChildProcess)
|
||||
await worker.postMessage('test-message', {})
|
||||
|
||||
expect(worker.startProcess).toHaveBeenCalled()
|
||||
expect(worker.isBusy).toBeTruthy()
|
||||
|
|
@ -133,9 +138,9 @@ describe('postMessage', () => {
|
|||
})
|
||||
|
||||
it('should wait sending the command until worker is ready', async () => {
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer())
|
||||
const worker = new Worker({} as any, workerConfig, new WritableStreamBuffer(), new WritableStreamBuffer(), mockXvfbManager as any)
|
||||
worker.childProcess = { send: vi.fn() } as any
|
||||
worker.postMessage('test-message', {})
|
||||
await worker.postMessage('test-message', {})
|
||||
expect(worker.childProcess!.send).toBeCalledTimes(0)
|
||||
worker.isReadyResolver(true)
|
||||
await worker.isReady
|
||||
|
|
|
|||
|
|
@ -327,6 +327,23 @@ export interface Testrunner extends Hooks, WebdriverIO, WebdriverIO.HookFunction
|
|||
* Shard tests and execute only the selected shard. Specify in the one-based form like `{ total: 5, current: 2 }`.
|
||||
*/
|
||||
shard?: ShardOptions
|
||||
/**
|
||||
* Enable automatic Xvfb initialization in local runner for headless testing on Linux.
|
||||
* When disabled, tests should manually call xvfb.init() if needed.
|
||||
* @default true
|
||||
*/
|
||||
autoXvfb?: boolean
|
||||
/**
|
||||
* Number of retry attempts for xvfb process failures.
|
||||
* @default 3
|
||||
*/
|
||||
xvfbMaxRetries?: number
|
||||
/**
|
||||
* Base delay between retries in milliseconds for xvfb process failures.
|
||||
* Progressive delay will be: xvfbRetryDelay * attemptNumber
|
||||
* @default 1000
|
||||
*/
|
||||
xvfbRetryDelay?: number
|
||||
// framework options
|
||||
/**
|
||||
* Mocha specific options
|
||||
|
|
|
|||
21
packages/wdio-xvfb/LICENSE
Normal file
21
packages/wdio-xvfb/LICENSE
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2025 OpenJS Foundation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
315
packages/wdio-xvfb/README.md
Normal file
315
packages/wdio-xvfb/README.md
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
# @wdio/xvfb
|
||||
|
||||
A standalone utility to manage Xvfb (X Virtual Framebuffer) for headless testing on Linux systems.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @wdio/xvfb
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Automatic Integration
|
||||
|
||||
**Most users don't need to use this package directly.** It's automatically integrated into:
|
||||
|
||||
- **`@wdio/local-runner`** - Automatically ensures xvfb is available for headless browser testing
|
||||
- **`wdio-electron-service`** - Provides headless Electron testing capabilities
|
||||
|
||||
Simply run your WebDriverIO tests normally, and xvfb will be managed automatically when needed.
|
||||
|
||||
### Manual Usage (Advanced)
|
||||
|
||||
For custom integrations or non-WDIO environments:
|
||||
|
||||
```js
|
||||
import { xvfb } from "@wdio/xvfb";
|
||||
|
||||
// Initialize xvfb-run (only works on Linux in headless environments)
|
||||
const ready = await xvfb.init();
|
||||
|
||||
if (ready) {
|
||||
console.log("xvfb-run is ready for use");
|
||||
// Use xvfb-run to execute commands that need a display
|
||||
// e.g., xvfb-run npm test
|
||||
}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### XvfbManager Class
|
||||
|
||||
#### Constructor Options
|
||||
|
||||
```ts
|
||||
interface XvfbOptions {
|
||||
force?: boolean; // Force Xvfb even on non-Linux systems (for testing)
|
||||
xvfbMaxRetries?: number; // Number of retry attempts for xvfb failures (default: 3)
|
||||
xvfbRetryDelay?: number; // Base delay between retries in milliseconds (default: 1000)
|
||||
}
|
||||
```
|
||||
|
||||
#### Methods
|
||||
|
||||
- **`shouldRun(capabilities?): boolean`** - Check if Xvfb should run on this system
|
||||
- **`init(capabilities?): Promise<boolean>`** - Initialize xvfb-run, returns true if ready, false if not needed
|
||||
- **`executeWithRetry<T>(commandFn, context?): Promise<T>`** - Execute a function with automatic retry on xvfb-related errors
|
||||
|
||||
### Advanced Usage Examples
|
||||
|
||||
#### Force Mode (Testing Only)
|
||||
|
||||
For testing on non-Linux systems:
|
||||
|
||||
```js
|
||||
import { XvfbManager } from "@wdio/xvfb";
|
||||
|
||||
const manager = new XvfbManager({
|
||||
force: true,
|
||||
xvfbMaxRetries: 3,
|
||||
xvfbRetryDelay: 1000
|
||||
});
|
||||
const ready = await manager.init();
|
||||
```
|
||||
|
||||
#### Custom Retry Configuration
|
||||
|
||||
```js
|
||||
import { XvfbManager } from "@wdio/xvfb";
|
||||
|
||||
// High-reliability setup for CI environments
|
||||
const manager = new XvfbManager({
|
||||
xvfbMaxRetries: 10, // More retries for flaky CI
|
||||
xvfbRetryDelay: 500 // Faster retries
|
||||
});
|
||||
|
||||
// Execute operation with automatic retry
|
||||
const result = await manager.executeWithRetry(async () => {
|
||||
// Your operation that might fail due to xvfb issues
|
||||
return await runHeadlessTests();
|
||||
}, 'Running headless tests');
|
||||
```
|
||||
|
||||
## When does it run?
|
||||
|
||||
The utility automatically detects when Xvfb is needed:
|
||||
|
||||
- ✅ Linux systems without a DISPLAY environment variable
|
||||
- ✅ Linux systems in CI environments (uses `is-ci` package for detection)
|
||||
- ✅ Linux systems when headless browser flags are detected:
|
||||
- **Chrome/Chromium**: `--headless`, `--headless=new`, `--headless=old`
|
||||
- **Firefox**: `--headless`, `-headless`
|
||||
- **Edge** (Chromium-based): `--headless`, `--headless=new`, `--headless=old`
|
||||
- ❌ Non-Linux systems (unless `force: true`)
|
||||
- ❌ Linux systems with existing DISPLAY (unless in CI or headless flags detected)
|
||||
|
||||
## Features
|
||||
|
||||
- **Conditional execution**: Only runs on Linux systems in headless environments
|
||||
- **Smart headless detection**: Automatically detects browser headless flags (`--headless`, `--headless=new`, etc.) and forces XVFB usage for consistent behavior
|
||||
- **Automatic retry mechanism**: Built-in retry logic with progressive delays for handling xvfb startup failures
|
||||
- **Configurable retry behavior**: Customizable retry count and delay settings via WDIO configuration
|
||||
- **Universal package manager support**: Automatically detects and uses the system's package manager (`apt`, `dnf`, `yum`, `zypper`, `pacman`, `apk`, `xbps`)
|
||||
- **Cross-distro compatibility**: Works across hundreds of Linux distributions by supporting their underlying package managers
|
||||
- **Command execution**: Execute commands under Xvfb using `xvfb-run`
|
||||
- **Graceful cleanup**: Automatic cleanup when commands complete
|
||||
- **State tracking**: Prevents duplicate starts and handles multiple calls safely
|
||||
- **Flexible logging**: Configurable logger support
|
||||
|
||||
## Retry Mechanism
|
||||
|
||||
The package includes automatic retry functionality for handling xvfb-related failures:
|
||||
|
||||
### Automatic Retry Detection
|
||||
|
||||
The retry mechanism automatically detects xvfb-specific errors:
|
||||
- `xvfb-run: error: Xvfb failed to start`
|
||||
- `Xvfb failed to start`
|
||||
- `xvfb-run: error:`
|
||||
- `X server died`
|
||||
|
||||
### Progressive Delay Strategy
|
||||
|
||||
Retry delays increase progressively to avoid overwhelming the system:
|
||||
- Attempt 1: Immediate execution
|
||||
- Attempt 2: Wait `xvfbRetryDelay × 1` ms (default: 1000ms)
|
||||
- Attempt 3: Wait `xvfbRetryDelay × 2` ms (default: 2000ms)
|
||||
- Attempt N: Wait `xvfbRetryDelay × (N-1)` ms
|
||||
|
||||
### Usage Example
|
||||
|
||||
```js
|
||||
import { XvfbManager } from '@wdio/xvfb';
|
||||
|
||||
const manager = new XvfbManager({
|
||||
xvfbMaxRetries: 5,
|
||||
xvfbRetryDelay: 1500
|
||||
});
|
||||
|
||||
// Automatic retry on xvfb failures
|
||||
const result = await manager.executeWithRetry(async () => {
|
||||
// Your xvfb-dependent operation
|
||||
return await someXvfbDependentTask();
|
||||
}, 'Custom operation name');
|
||||
```
|
||||
|
||||
## Implementation Details
|
||||
|
||||
This package uses `xvfb-run` to manage Xvfb sessions, which:
|
||||
|
||||
- Automatically selects available display numbers with `--auto-servernum`
|
||||
- Configures screen resolution, DPI, and other display settings
|
||||
- Handles process cleanup automatically when commands complete
|
||||
- Supports custom arguments for advanced Xvfb configuration
|
||||
|
||||
## Supported Package Managers
|
||||
|
||||
The utility automatically detects the system's package manager and installs xvfb accordingly:
|
||||
|
||||
| Package Manager | Command | Distributions | Package Name |
|
||||
|----------------|---------|---------------|--------------|
|
||||
| **`apt`** | `apt-get` | Ubuntu, Debian, Pop!_OS, Mint, Elementary, Zorin, etc. | `xvfb` |
|
||||
| **`dnf`** | `dnf` | Fedora, Rocky Linux, AlmaLinux, Nobara, Bazzite, etc. | `xorg-x11-server-Xvfb` |
|
||||
| **`yum`** | `yum` | CentOS, RHEL (legacy) | `xorg-x11-server-Xvfb` |
|
||||
| **`zypper`** | `zypper` | openSUSE, SUSE Linux Enterprise | `xorg-x11-server-Xvfb xvfb-run` |
|
||||
| **`pacman`** | `pacman` | Arch Linux, Manjaro, EndeavourOS, CachyOS, etc. | `xorg-server-xvfb` |
|
||||
| **`apk`** | `apk` | Alpine Linux, PostmarketOS | `xvfb-run` |
|
||||
| **`xbps`** | `xbps-install` | Void Linux | `xvfb` |
|
||||
|
||||
### Future-Proof Design
|
||||
|
||||
This package manager-focused approach means **new Linux distributions automatically work** without code changes, as long as they use one of the supported package managers. For example:
|
||||
- New Ubuntu derivatives automatically use `apt`
|
||||
- New Arch derivatives automatically use `pacman`
|
||||
- New Fedora derivatives automatically use `dnf`
|
||||
|
||||
The 7 supported package managers cover **95%+ of all Linux users** and hundreds of distributions, making this solution both comprehensive and maintainable.
|
||||
|
||||
## Examples
|
||||
|
||||
### Basic Setup for Custom Tools
|
||||
|
||||
```js
|
||||
import { xvfb } from "@wdio/xvfb";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
async function runCustomTests() {
|
||||
const ready = await xvfb.init();
|
||||
|
||||
const command = ready
|
||||
? "xvfb-run your-custom-test-command"
|
||||
: "your-custom-test-command";
|
||||
|
||||
await execAsync(command);
|
||||
}
|
||||
```
|
||||
|
||||
### WebDriverIO Configuration
|
||||
|
||||
For most users, no configuration is needed. WDIO automatically handles xvfb:
|
||||
|
||||
```js
|
||||
// wdio.conf.js - Minimal configuration
|
||||
export const config = {
|
||||
// No xvfb configuration needed - handled automatically
|
||||
capabilities: [{
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: ['--headless', '--no-sandbox'] // Automatically detected - will force XVFB
|
||||
}
|
||||
}],
|
||||
services: [
|
||||
'chromedriver',
|
||||
// xvfb is automatically managed by local-runner
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
#### Advanced WDIO Configuration
|
||||
|
||||
You can customize xvfb behavior and retry settings:
|
||||
|
||||
```js
|
||||
// wdio.conf.js - Custom xvfb configuration
|
||||
export const config = {
|
||||
// Xvfb configuration options (all optional)
|
||||
autoXvfb: true, // Enable automatic xvfb (default: true)
|
||||
xvfbMaxRetries: 5, // Max retry attempts for xvfb failures (default: 3)
|
||||
xvfbRetryDelay: 2000, // Base delay between retries in ms (default: 1000)
|
||||
|
||||
capabilities: [{
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: ['--headless', '--no-sandbox']
|
||||
}
|
||||
}]
|
||||
};
|
||||
```
|
||||
|
||||
**Configuration Options:**
|
||||
|
||||
- **`autoXvfb`** *(boolean, default: true)*: Enable/disable automatic xvfb initialization
|
||||
- **`xvfbMaxRetries`** *(number, default: 3)*: Number of retry attempts when xvfb process fails
|
||||
- **`xvfbRetryDelay`** *(number, default: 1000)*: Base delay between retries in milliseconds. Uses progressive delay (delay × attempt number)
|
||||
|
||||
### Headless Flag Detection
|
||||
|
||||
The utility automatically detects browser headless flags and forces XVFB usage even when a DISPLAY is available:
|
||||
|
||||
```js
|
||||
// These configurations will automatically trigger XVFB on Linux:
|
||||
|
||||
// Chrome/Chromium
|
||||
capabilities: [{
|
||||
'goog:chromeOptions': {
|
||||
args: ['--headless'] // Detected
|
||||
}
|
||||
}]
|
||||
|
||||
// Firefox
|
||||
capabilities: [{
|
||||
'moz:firefoxOptions': {
|
||||
args: ['--headless'] // Detected
|
||||
}
|
||||
}]
|
||||
|
||||
// Edge (Chromium-based)
|
||||
capabilities: [{
|
||||
'ms:edgeOptions': {
|
||||
args: ['--headless=new'] // Detected
|
||||
}
|
||||
}]
|
||||
|
||||
// Legacy formats also supported
|
||||
capabilities: [{
|
||||
chromeOptions: { args: ['--headless'] }, // Legacy Chrome
|
||||
edgeOptions: { args: ['--headless'] } // Legacy Edge
|
||||
}]
|
||||
|
||||
// Multiremote scenarios are also supported
|
||||
capabilities: {
|
||||
browserA: { 'goog:chromeOptions': { args: ['--headless'] }},
|
||||
browserB: { 'ms:edgeOptions': { args: ['--no-sandbox'] }}
|
||||
// XVFB will be used because browserA has headless flag
|
||||
}
|
||||
```
|
||||
|
||||
This ensures consistent headless behavior regardless of host environment setup.
|
||||
|
||||
## Logging
|
||||
|
||||
The utility uses `@wdio/logger` with the namespace `@wdio/xvfb`. Enable debug logging:
|
||||
|
||||
```bash
|
||||
DEBUG=@wdio/xvfb npm run test
|
||||
```
|
||||
|
||||
The logger is automatically created with the namespace `@wdio/xvfb` and cannot be customized in the current interface.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
52
packages/wdio-xvfb/package.json
Normal file
52
packages/wdio-xvfb/package.json
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
{
|
||||
"name": "@wdio/xvfb",
|
||||
"version": "9.18.4",
|
||||
"description": "A standalone utility to manage Xvfb (X Virtual Framebuffer) for headless testing",
|
||||
"author": "WebdriverIO Team",
|
||||
"homepage": "https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-xvfb",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/webdriverio/webdriverio.git",
|
||||
"directory": "packages/wdio-xvfb"
|
||||
},
|
||||
"keywords": [
|
||||
"xvfb",
|
||||
"headless",
|
||||
"display",
|
||||
"linux",
|
||||
"testing",
|
||||
"x11",
|
||||
"virtual-framebuffer",
|
||||
"electron",
|
||||
"browser-testing"
|
||||
],
|
||||
"bugs": {
|
||||
"url": "https://github.com/webdriverio/webdriverio/issues"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "./build/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./build/index.d.ts",
|
||||
"import": "./build/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typeScriptVersion": "3.8.3",
|
||||
"dependencies": {
|
||||
"@wdio/logger": "workspace:*",
|
||||
"is-ci": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wdio/types": "workspace:*",
|
||||
"@types/node": "^20.0.0",
|
||||
"@types/is-ci": "^3.0.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
}
|
||||
}
|
||||
135
packages/wdio-xvfb/src/ProcessFactory.ts
Normal file
135
packages/wdio-xvfb/src/ProcessFactory.ts
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
import { spawn, fork, execSync } from 'node:child_process'
|
||||
import type {
|
||||
ChildProcess,
|
||||
SpawnOptions,
|
||||
ForkOptions,
|
||||
} from 'node:child_process'
|
||||
import logger from '@wdio/logger'
|
||||
import { XvfbManager } from './XvfbManager.js'
|
||||
|
||||
export interface ProcessCreator {
|
||||
createWorkerProcess(
|
||||
scriptPath: string,
|
||||
args: string[],
|
||||
options: ProcessCreationOptions
|
||||
): Promise<ChildProcess>;
|
||||
}
|
||||
|
||||
export interface ProcessCreationOptions {
|
||||
cwd?: string;
|
||||
env?: Record<string, string>;
|
||||
execArgv?: string[];
|
||||
stdio?: ('inherit' | 'pipe' | 'ignore' | 'ipc')[];
|
||||
}
|
||||
|
||||
export class ProcessFactory implements ProcessCreator {
|
||||
#xvfbManager: XvfbManager
|
||||
#log = logger('@wdio/xvfb:ProcessFactory')
|
||||
|
||||
constructor(xvfbManager?: XvfbManager) {
|
||||
this.#xvfbManager = xvfbManager || new XvfbManager()
|
||||
}
|
||||
|
||||
async createWorkerProcess(
|
||||
scriptPath: string,
|
||||
args: string[],
|
||||
options: ProcessCreationOptions
|
||||
): Promise<ChildProcess> {
|
||||
const shouldRun = this.#xvfbManager.shouldRun()
|
||||
const isAvailable = this.#isXvfbRunAvailable()
|
||||
|
||||
this.#log.info(`ProcessFactory: shouldRun=${shouldRun}, isAvailable=${isAvailable}`)
|
||||
|
||||
if (shouldRun && isAvailable) {
|
||||
this.#log.info('Creating worker process with xvfb-run wrapper and retry logic')
|
||||
|
||||
return await this.#xvfbManager.executeWithRetry(
|
||||
() => this.#createXvfbProcess(scriptPath, args, options),
|
||||
'xvfb worker process creation'
|
||||
)
|
||||
}
|
||||
|
||||
this.#log.info('Creating worker process with regular fork')
|
||||
return this.#createRegularProcess(scriptPath, args, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create process with xvfb-run wrapper
|
||||
*/
|
||||
#createXvfbProcess(
|
||||
scriptPath: string,
|
||||
args: string[],
|
||||
options: ProcessCreationOptions
|
||||
): Promise<ChildProcess> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { cwd, env, execArgv = [], stdio } = options
|
||||
const nodeArgs = [...execArgv, scriptPath, ...args]
|
||||
|
||||
const childProcess = spawn(
|
||||
'xvfb-run',
|
||||
['--auto-servernum', '--', 'node', ...nodeArgs],
|
||||
{
|
||||
cwd,
|
||||
env,
|
||||
stdio,
|
||||
} as SpawnOptions
|
||||
)
|
||||
|
||||
// Check for immediate startup failures
|
||||
let resolved = false
|
||||
const startupTimeout = setTimeout(() => {
|
||||
if (!resolved) {
|
||||
resolved = true
|
||||
resolve(childProcess)
|
||||
}
|
||||
}, 100) // Short timeout to detect immediate failures
|
||||
|
||||
childProcess.on('error', (error) => {
|
||||
if (!resolved) {
|
||||
resolved = true
|
||||
clearTimeout(startupTimeout)
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
childProcess.on('exit', (code, signal) => {
|
||||
if (!resolved && code !== 0) {
|
||||
resolved = true
|
||||
clearTimeout(startupTimeout)
|
||||
reject(new Error(`xvfb-run process exited with code ${code} and signal ${signal}`))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create regular process without xvfb
|
||||
*/
|
||||
#createRegularProcess(
|
||||
scriptPath: string,
|
||||
args: string[],
|
||||
options: ProcessCreationOptions
|
||||
): ChildProcess {
|
||||
const { cwd, env, execArgv = [], stdio } = options
|
||||
return fork(scriptPath, args, {
|
||||
cwd,
|
||||
env,
|
||||
execArgv,
|
||||
stdio,
|
||||
} as ForkOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if xvfb-run is actually available on the system
|
||||
*/
|
||||
#isXvfbRunAvailable(): boolean {
|
||||
try {
|
||||
execSync('which xvfb-run', { stdio: 'ignore' })
|
||||
this.#log.info('xvfb-run found in PATH')
|
||||
return true
|
||||
} catch {
|
||||
this.#log.info('xvfb-run not found, falling back to regular fork')
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
370
packages/wdio-xvfb/src/XvfbManager.ts
Normal file
370
packages/wdio-xvfb/src/XvfbManager.ts
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
import { promisify } from 'node:util'
|
||||
import { exec } from 'node:child_process'
|
||||
import os from 'node:os'
|
||||
import isCI from 'is-ci'
|
||||
import logger from '@wdio/logger'
|
||||
import type { Capabilities } from '@wdio/types'
|
||||
|
||||
export interface XvfbOptions {
|
||||
/**
|
||||
* Force Xvfb to run even on non-Linux systems (for testing)
|
||||
*/
|
||||
force?: boolean;
|
||||
/**
|
||||
* Override package manager detection (for testing)
|
||||
*/
|
||||
packageManager?: string;
|
||||
/**
|
||||
* Skip xvfb-run availability check and force installation (for testing)
|
||||
*/
|
||||
forceInstall?: boolean;
|
||||
/**
|
||||
* Number of retry attempts for xvfb process failures (default: 3)
|
||||
*/
|
||||
xvfbMaxRetries?: number;
|
||||
/**
|
||||
* Base delay between retries in milliseconds (default: 1000)
|
||||
* Progressive delay will be: baseDelay * attemptNumber
|
||||
*/
|
||||
xvfbRetryDelay?: number;
|
||||
}
|
||||
|
||||
const execAsync = promisify(exec)
|
||||
|
||||
export class XvfbManager {
|
||||
private force: boolean
|
||||
private packageManagerOverride?: string
|
||||
private forceInstall: boolean
|
||||
private maxRetries: number
|
||||
private retryDelay: number
|
||||
private log: ReturnType<typeof logger>
|
||||
|
||||
constructor(options: XvfbOptions = {}) {
|
||||
this.force = options.force ?? false
|
||||
this.packageManagerOverride = options.packageManager
|
||||
this.forceInstall = options.forceInstall ?? false
|
||||
this.maxRetries = options.xvfbMaxRetries ?? 3
|
||||
this.retryDelay = options.xvfbRetryDelay ?? 1000
|
||||
this.log = logger('@wdio/xvfb')
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Xvfb should run on this system
|
||||
*/
|
||||
shouldRun(capabilities?: Capabilities.ResolvedTestrunnerCapabilities): boolean {
|
||||
if (this.force) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Only run on Linux systems
|
||||
if (os.platform() !== 'linux') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if we're in a headless environment (no DISPLAY set or in CI)
|
||||
const hasDisplay = process.env.DISPLAY
|
||||
const inHeadlessEnvironment = !hasDisplay || isCI
|
||||
|
||||
// Force XVFB if headless Chrome flags are detected
|
||||
const hasHeadlessFlag = this.detectHeadlessMode(capabilities)
|
||||
|
||||
return inHeadlessEnvironment || hasHeadlessFlag
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize xvfb-run for use
|
||||
* @returns Promise<boolean> - true if xvfb-run is ready, false if not needed
|
||||
*/
|
||||
public async init(capabilities?: Capabilities.ResolvedTestrunnerCapabilities): Promise<boolean> {
|
||||
this.log.info('XvfbManager.init() called')
|
||||
|
||||
if (!this.shouldRun(capabilities)) {
|
||||
this.log.info('Xvfb not needed on current platform')
|
||||
return false
|
||||
}
|
||||
this.log.info('Xvfb should run, checking if setup is needed')
|
||||
|
||||
try {
|
||||
await this.ensureXvfbRunAvailable()
|
||||
this.log.info('xvfb-run is ready for use')
|
||||
return true
|
||||
} catch (error) {
|
||||
this.log.error('Failed to setup xvfb-run:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure xvfb-run is available, installing if necessary
|
||||
*/
|
||||
private async ensureXvfbRunAvailable(): Promise<void> {
|
||||
this.log.info('Checking if xvfb-run is available...')
|
||||
|
||||
if (!this.forceInstall) {
|
||||
try {
|
||||
// Check if xvfb-run is already available
|
||||
await execAsync('which xvfb-run')
|
||||
this.log.info('xvfb-run found in PATH')
|
||||
return
|
||||
} catch {
|
||||
this.log.info('xvfb-run not found, installing xvfb packages...')
|
||||
}
|
||||
} else {
|
||||
this.log.info('Force install enabled, skipping availability check')
|
||||
}
|
||||
|
||||
// Install packages that include xvfb-run
|
||||
this.log.info('Starting xvfb package installation...')
|
||||
await this.installXvfbPackages()
|
||||
this.log.info('Package installation completed')
|
||||
|
||||
// Verify xvfb-run is now available (skip if force install to allow error testing)
|
||||
if (!this.forceInstall) {
|
||||
this.log.info('Verifying xvfb-run installation...')
|
||||
try {
|
||||
const { stdout } = await execAsync('which xvfb-run')
|
||||
this.log.info(
|
||||
`Successfully installed xvfb-run at: ${stdout.trim()}`
|
||||
)
|
||||
} catch (error) {
|
||||
this.log.error('Failed to install xvfb-run:', error)
|
||||
throw new Error(
|
||||
"xvfb-run is not available after installation. Please install it manually using your distribution's package manager."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if headless mode is enabled in Chrome/Chromium capabilities
|
||||
*/
|
||||
private detectHeadlessMode(capabilities?: Capabilities.ResolvedTestrunnerCapabilities): boolean {
|
||||
if (!capabilities) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Handle both single and multiremote capabilities
|
||||
const caps = capabilities as WebdriverIO.Capabilities | Record<string, WebdriverIO.Capabilities | { capabilities: WebdriverIO.Capabilities }>
|
||||
|
||||
// Check if it's a single capability object (has browser-specific options)
|
||||
if (this.isSingleCapability(caps)) {
|
||||
return this.checkCapabilityForHeadless(caps)
|
||||
}
|
||||
|
||||
// Handle multiremote scenario
|
||||
if (this.isMultiRemoteCapability(caps)) {
|
||||
for (const [, browserConfig] of Object.entries(caps)) {
|
||||
const browserCaps = this.extractCapabilitiesFromBrowserConfig(browserConfig)
|
||||
if (this.checkCapabilityForHeadless(browserCaps)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the capabilities object is a single capability (not multiremote)
|
||||
*/
|
||||
private isSingleCapability(caps: WebdriverIO.Capabilities | Record<string, WebdriverIO.Capabilities | { capabilities: WebdriverIO.Capabilities }>): caps is WebdriverIO.Capabilities {
|
||||
return Boolean(
|
||||
caps['goog:chromeOptions'] ||
|
||||
caps['ms:edgeOptions'] ||
|
||||
caps['moz:firefoxOptions']
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the capabilities object is multiremote
|
||||
*/
|
||||
private isMultiRemoteCapability(caps: WebdriverIO.Capabilities | Record<string, WebdriverIO.Capabilities | { capabilities: WebdriverIO.Capabilities }>): caps is Record<string, WebdriverIO.Capabilities | { capabilities: WebdriverIO.Capabilities }> {
|
||||
return !this.isSingleCapability(caps) && typeof caps === 'object' && caps !== null
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract capabilities from browser config (handles both nested and direct formats)
|
||||
*/
|
||||
private extractCapabilitiesFromBrowserConfig(browserConfig: { capabilities: WebdriverIO.Capabilities } | WebdriverIO.Capabilities): WebdriverIO.Capabilities {
|
||||
if (browserConfig && typeof browserConfig === 'object' && 'capabilities' in browserConfig && browserConfig.capabilities) {
|
||||
return browserConfig.capabilities
|
||||
}
|
||||
return browserConfig as WebdriverIO.Capabilities
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a single capability object for headless flags
|
||||
*/
|
||||
private checkCapabilityForHeadless(caps: WebdriverIO.Capabilities): boolean {
|
||||
if (!caps || typeof caps !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check Chrome options
|
||||
if (this.hasHeadlessFlag(caps['goog:chromeOptions'], ['--headless'])) {
|
||||
this.log.info('Detected headless Chrome flag, forcing XVFB usage')
|
||||
return true
|
||||
}
|
||||
|
||||
// Check Edge options (Chromium-based, uses same flags as Chrome)
|
||||
if (this.hasHeadlessFlag(caps['ms:edgeOptions'], ['--headless'])) {
|
||||
this.log.info('Detected headless Edge flag, forcing XVFB usage')
|
||||
return true
|
||||
}
|
||||
|
||||
// Check Firefox options
|
||||
if (this.hasHeadlessFlag(caps['moz:firefoxOptions'], ['--headless', '-headless'])) {
|
||||
this.log.info('Detected headless Firefox flag, forcing XVFB usage')
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if browser options contain headless flags
|
||||
*/
|
||||
private hasHeadlessFlag(options: { args?: string[] } | undefined, headlessFlags: string[]): boolean {
|
||||
if (!options?.args || !Array.isArray(options.args)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return options.args.some((arg: string) => {
|
||||
if (typeof arg !== 'string') {
|
||||
return false
|
||||
}
|
||||
return headlessFlags.some(flag =>
|
||||
arg === flag || (flag === '--headless' && arg.startsWith('--headless='))
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
protected async detectPackageManager(): Promise<string> {
|
||||
// Use override if provided (for testing)
|
||||
if (this.packageManagerOverride) {
|
||||
return this.packageManagerOverride
|
||||
}
|
||||
|
||||
const packageManagers = [
|
||||
{ command: 'apt-get', name: 'apt' },
|
||||
{ command: 'dnf', name: 'dnf' },
|
||||
{ command: 'yum', name: 'yum' },
|
||||
{ command: 'zypper', name: 'zypper' },
|
||||
{ command: 'pacman', name: 'pacman' },
|
||||
{ command: 'apk', name: 'apk' },
|
||||
{ command: 'xbps-install', name: 'xbps' },
|
||||
]
|
||||
|
||||
for (const { command, name } of packageManagers) {
|
||||
try {
|
||||
await execAsync(`which ${command}`)
|
||||
return name
|
||||
} catch {
|
||||
// Continue to next package manager
|
||||
}
|
||||
}
|
||||
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
private async installXvfbPackages(): Promise<void> {
|
||||
this.log.info('Detecting package manager...')
|
||||
const packageManager = await this.detectPackageManager()
|
||||
this.log.info(`Detected package manager: ${packageManager}`)
|
||||
|
||||
const installCommands: Record<string, string> = {
|
||||
apt: 'sudo apt-get update -qq && sudo apt-get install -y xvfb',
|
||||
dnf: 'sudo dnf makecache && sudo dnf install -y xorg-x11-server-Xvfb',
|
||||
yum: 'sudo yum makecache && sudo yum install -y xorg-x11-server-Xvfb',
|
||||
zypper: 'sudo zypper refresh && sudo zypper install -y xvfb-run',
|
||||
pacman: 'sudo pacman -Sy --noconfirm xorg-server-xvfb',
|
||||
apk: 'sudo apk update && sudo apk add --no-cache xvfb-run',
|
||||
xbps: 'sudo xbps-install -Sy xvfb-run',
|
||||
}
|
||||
|
||||
const command = installCommands[packageManager]
|
||||
if (!command) {
|
||||
throw new Error(
|
||||
`Unsupported package manager: ${packageManager}. Please install Xvfb manually.`
|
||||
)
|
||||
}
|
||||
|
||||
this.log.info(`Installing xvfb packages using command: ${command}`)
|
||||
|
||||
try {
|
||||
this.log.info('Starting package installation command execution...')
|
||||
await execAsync(command, { timeout: 240000 }) // 4 minute timeout
|
||||
this.log.info(
|
||||
'Package installation command completed successfully'
|
||||
)
|
||||
} catch (error) {
|
||||
this.log.error('Package installation command failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command with retry logic for xvfb failures
|
||||
*/
|
||||
public async executeWithRetry<T>(
|
||||
commandFn: () => Promise<T>,
|
||||
context: string = 'xvfb operation'
|
||||
): Promise<T> {
|
||||
let lastError: Error | unknown = null
|
||||
|
||||
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
||||
try {
|
||||
if (attempt === 1) {
|
||||
this.log.info(`🚀 Executing ${context}`)
|
||||
} else {
|
||||
this.log.info(`🔄 Retry attempt ${attempt}/${this.maxRetries}: ${context}`)
|
||||
}
|
||||
|
||||
const result = await commandFn()
|
||||
|
||||
if (attempt > 1) {
|
||||
this.log.info(`✅ Success on attempt ${attempt}/${this.maxRetries}`)
|
||||
}
|
||||
return result
|
||||
} catch (error: unknown) {
|
||||
this.log.info(`❌ Attempt ${attempt}/${this.maxRetries} failed: ${error}`)
|
||||
lastError = error
|
||||
|
||||
// Check if this is an xvfb-related error that we should retry
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const isXvfbError = this.isXvfbError(errorMessage)
|
||||
|
||||
if (!isXvfbError) {
|
||||
this.log.info('Non-xvfb error detected, not retrying')
|
||||
throw error
|
||||
}
|
||||
|
||||
if (attempt < this.maxRetries) {
|
||||
const delay = this.retryDelay * attempt
|
||||
this.log.info(`⏳ Waiting ${delay}ms before retry...`)
|
||||
await new Promise(resolve => setTimeout(resolve, delay))
|
||||
} else {
|
||||
this.log.info(`❌ All ${this.maxRetries} attempts failed`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error is related to xvfb failures
|
||||
*/
|
||||
private isXvfbError(errorMessage: string): boolean {
|
||||
const xvfbErrorPatterns = [
|
||||
'xvfb-run: error: Xvfb failed to start',
|
||||
'Xvfb failed to start',
|
||||
'xvfb-run: error:',
|
||||
'X server died'
|
||||
]
|
||||
|
||||
return xvfbErrorPatterns.some(pattern =>
|
||||
errorMessage.toLowerCase().includes(pattern.toLowerCase())
|
||||
)
|
||||
}
|
||||
}
|
||||
8
packages/wdio-xvfb/src/index.ts
Normal file
8
packages/wdio-xvfb/src/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { XvfbManager } from './XvfbManager.js'
|
||||
|
||||
// export classes
|
||||
export { XvfbManager, type XvfbOptions } from './XvfbManager.js'
|
||||
export { ProcessFactory, type ProcessCreator, type ProcessCreationOptions } from './ProcessFactory.js'
|
||||
|
||||
// Export a default instance for convenience
|
||||
export const xvfb = new XvfbManager()
|
||||
236
packages/wdio-xvfb/tests/ProcessFactory.test.ts
Normal file
236
packages/wdio-xvfb/tests/ProcessFactory.test.ts
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
import type { ChildProcess } from 'node:child_process'
|
||||
|
||||
// Use vi.hoisted to ensure mocks are set up before imports
|
||||
const mockSpawn = vi.hoisted(() => vi.fn())
|
||||
const mockFork = vi.hoisted(() => vi.fn())
|
||||
const mockExecSync = vi.hoisted(() => vi.fn())
|
||||
|
||||
// Mock child_process module
|
||||
vi.mock('node:child_process', () => ({
|
||||
spawn: mockSpawn,
|
||||
fork: mockFork,
|
||||
execSync: mockExecSync
|
||||
}))
|
||||
|
||||
// Mock logger
|
||||
vi.mock('@wdio/logger', () => ({
|
||||
default: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
// Mock XvfbManager
|
||||
const mockXvfbManager = {
|
||||
shouldRun: vi.fn(),
|
||||
init: vi.fn(),
|
||||
executeWithRetry: vi.fn()
|
||||
}
|
||||
|
||||
vi.mock('../src/XvfbManager.js', () => ({
|
||||
XvfbManager: vi.fn(() => mockXvfbManager)
|
||||
}))
|
||||
|
||||
// Import after mocks are set up
|
||||
const { ProcessFactory } = await import('../src/ProcessFactory.js')
|
||||
|
||||
describe('ProcessFactory', () => {
|
||||
let processFactory: InstanceType<typeof ProcessFactory>
|
||||
const mockChildProcess = {
|
||||
on: vi.fn(),
|
||||
send: vi.fn(),
|
||||
kill: vi.fn(),
|
||||
pid: 12345,
|
||||
stdout: null,
|
||||
stderr: null,
|
||||
stdin: null
|
||||
} as unknown as ChildProcess
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
processFactory = new ProcessFactory()
|
||||
|
||||
// Reset mock implementations
|
||||
mockSpawn.mockReturnValue(mockChildProcess)
|
||||
mockFork.mockReturnValue(mockChildProcess)
|
||||
mockExecSync.mockReturnValue('success')
|
||||
mockXvfbManager.shouldRun.mockReturnValue(false)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create instance with default XvfbManager', () => {
|
||||
const factory = new ProcessFactory()
|
||||
expect(factory).toBeInstanceOf(ProcessFactory)
|
||||
})
|
||||
|
||||
it('should create instance with custom XvfbManager', () => {
|
||||
const customManager = mockXvfbManager
|
||||
const factory = new ProcessFactory(customManager as any)
|
||||
expect(factory).toBeInstanceOf(ProcessFactory)
|
||||
})
|
||||
})
|
||||
|
||||
describe('createWorkerProcess', () => {
|
||||
const scriptPath = '/path/to/script.js'
|
||||
const args = ['--arg1', '--arg2']
|
||||
const options = {
|
||||
cwd: '/working/dir',
|
||||
env: { NODE_ENV: 'test' },
|
||||
execArgv: ['--inspect'],
|
||||
stdio: ['inherit', 'pipe', 'pipe', 'ipc'] as const
|
||||
}
|
||||
|
||||
describe('when xvfb should not run', () => {
|
||||
beforeEach(() => {
|
||||
mockXvfbManager.shouldRun.mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('should use regular fork when xvfb is not needed', async () => {
|
||||
const result = await processFactory.createWorkerProcess(scriptPath, args, options)
|
||||
|
||||
expect(mockXvfbManager.shouldRun).toHaveBeenCalled()
|
||||
expect(mockFork).toHaveBeenCalledWith(scriptPath, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
execArgv: options.execArgv,
|
||||
stdio: options.stdio
|
||||
})
|
||||
expect(mockSpawn).not.toHaveBeenCalled()
|
||||
expect(result).toBe(mockChildProcess)
|
||||
})
|
||||
|
||||
it('should use fork with default execArgv when not provided', async () => {
|
||||
const optionsWithoutExecArgv = { ...options }
|
||||
delete optionsWithoutExecArgv.execArgv
|
||||
|
||||
await processFactory.createWorkerProcess(scriptPath, args, optionsWithoutExecArgv)
|
||||
|
||||
expect(mockFork).toHaveBeenCalledWith(scriptPath, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
execArgv: [],
|
||||
stdio: options.stdio
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('when xvfb should run', () => {
|
||||
beforeEach(() => {
|
||||
mockXvfbManager.shouldRun.mockReturnValue(true)
|
||||
mockXvfbManager.executeWithRetry.mockImplementation(async (fn) => await fn())
|
||||
})
|
||||
|
||||
it('should use executeWithRetry when xvfb is available', async () => {
|
||||
mockExecSync.mockReturnValue('/usr/bin/xvfb-run')
|
||||
|
||||
const mockProcess = {
|
||||
...mockChildProcess,
|
||||
on: vi.fn()
|
||||
} as unknown as ChildProcess
|
||||
mockSpawn.mockReturnValue(mockProcess)
|
||||
|
||||
const result = await processFactory.createWorkerProcess(scriptPath, args, options)
|
||||
|
||||
expect(mockXvfbManager.shouldRun).toHaveBeenCalled()
|
||||
expect(mockExecSync).toHaveBeenCalledWith('which xvfb-run', { stdio: 'ignore' })
|
||||
expect(mockXvfbManager.executeWithRetry).toHaveBeenCalledWith(
|
||||
expect.any(Function),
|
||||
'xvfb worker process creation'
|
||||
)
|
||||
expect(mockFork).not.toHaveBeenCalled()
|
||||
expect(result).toBe(mockProcess)
|
||||
})
|
||||
|
||||
it('should fallback to fork when xvfb-run is not available', async () => {
|
||||
mockExecSync.mockImplementation(() => {
|
||||
throw new Error('Command not found')
|
||||
})
|
||||
|
||||
const result = await processFactory.createWorkerProcess(scriptPath, args, options)
|
||||
|
||||
expect(mockXvfbManager.shouldRun).toHaveBeenCalled()
|
||||
expect(mockExecSync).toHaveBeenCalledWith('which xvfb-run', { stdio: 'ignore' })
|
||||
expect(mockXvfbManager.executeWithRetry).not.toHaveBeenCalled()
|
||||
expect(mockFork).toHaveBeenCalledWith(scriptPath, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
execArgv: options.execArgv,
|
||||
stdio: options.stdio
|
||||
})
|
||||
expect(mockSpawn).not.toHaveBeenCalled()
|
||||
expect(result).toBe(mockChildProcess)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle minimal options', async () => {
|
||||
mockXvfbManager.shouldRun.mockReturnValue(false)
|
||||
const minimalOptions = {}
|
||||
|
||||
await processFactory.createWorkerProcess(scriptPath, args, minimalOptions)
|
||||
|
||||
expect(mockFork).toHaveBeenCalledWith(scriptPath, args, {
|
||||
cwd: undefined,
|
||||
env: undefined,
|
||||
execArgv: [],
|
||||
stdio: undefined
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle empty args array', async () => {
|
||||
mockXvfbManager.shouldRun.mockReturnValue(false)
|
||||
|
||||
await processFactory.createWorkerProcess(scriptPath, [], options)
|
||||
|
||||
expect(mockFork).toHaveBeenCalledWith(scriptPath, [], {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
execArgv: options.execArgv,
|
||||
stdio: options.stdio
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('integration with XvfbManager', () => {
|
||||
it('should call shouldRun on the XvfbManager instance', async () => {
|
||||
const customManager = {
|
||||
shouldRun: vi.fn().mockReturnValue(false),
|
||||
executeWithRetry: vi.fn()
|
||||
}
|
||||
const factory = new ProcessFactory(customManager as any)
|
||||
|
||||
await factory.createWorkerProcess('/script.js', [], {})
|
||||
|
||||
expect(customManager.shouldRun).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should respect XvfbManager decision about whether to run xvfb', async () => {
|
||||
const customManager = {
|
||||
shouldRun: vi.fn().mockReturnValue(true),
|
||||
executeWithRetry: vi.fn().mockImplementation(async (fn) => await fn())
|
||||
}
|
||||
const factory = new ProcessFactory(customManager as any)
|
||||
mockExecSync.mockReturnValue('/usr/bin/xvfb-run')
|
||||
|
||||
const mockProcess = {
|
||||
...mockChildProcess,
|
||||
on: vi.fn()
|
||||
} as unknown as ChildProcess
|
||||
mockSpawn.mockReturnValue(mockProcess)
|
||||
|
||||
await factory.createWorkerProcess('/script.js', [], {})
|
||||
|
||||
expect(customManager.shouldRun).toHaveBeenCalled()
|
||||
expect(customManager.executeWithRetry).toHaveBeenCalled()
|
||||
expect(mockFork).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
518
packages/wdio-xvfb/tests/XvfbManager.test.ts
Normal file
518
packages/wdio-xvfb/tests/XvfbManager.test.ts
Normal file
|
|
@ -0,0 +1,518 @@
|
|||
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'
|
||||
|
||||
// Use vi.hoisted to ensure mocks are set up before imports
|
||||
const mockExecAsync = vi.hoisted(() => vi.fn())
|
||||
const mockPlatform = vi.hoisted(() => vi.fn())
|
||||
const mockIsCI = vi.hoisted(() => ({ value: false }))
|
||||
|
||||
// Mock all the modules before importing anything else
|
||||
vi.mock('node:child_process', () => ({
|
||||
exec: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('node:util', () => ({
|
||||
promisify: vi.fn(() => mockExecAsync)
|
||||
}))
|
||||
|
||||
vi.mock('node:os', () => ({
|
||||
default: {
|
||||
platform: mockPlatform
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('is-ci', () => ({
|
||||
get default() {
|
||||
return mockIsCI.value
|
||||
}
|
||||
}))
|
||||
|
||||
vi.mock('@wdio/logger', () => ({
|
||||
default: vi.fn(() => ({
|
||||
info: vi.fn(),
|
||||
error: vi.fn(),
|
||||
warn: vi.fn()
|
||||
}))
|
||||
}))
|
||||
|
||||
// Import after mocks are set up
|
||||
const { XvfbManager } = await import('../src/XvfbManager.js')
|
||||
|
||||
describe('XvfbManager', () => {
|
||||
let manager: InstanceType<typeof XvfbManager>
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
manager = new XvfbManager()
|
||||
|
||||
// Reset environment
|
||||
delete process.env.DISPLAY
|
||||
mockIsCI.value = false
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should create instance with default options', () => {
|
||||
const manager = new XvfbManager()
|
||||
expect(manager).toBeInstanceOf(XvfbManager)
|
||||
})
|
||||
|
||||
it('should create instance with custom options', () => {
|
||||
const manager = new XvfbManager({
|
||||
force: true,
|
||||
packageManager: 'apt',
|
||||
xvfbMaxRetries: 5,
|
||||
xvfbRetryDelay: 2000
|
||||
})
|
||||
expect(manager).toBeInstanceOf(XvfbManager)
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldRun', () => {
|
||||
it('should return true when forced', () => {
|
||||
const manager = new XvfbManager({ force: true })
|
||||
mockPlatform.mockReturnValue('darwin') // Non-Linux platform
|
||||
|
||||
expect(manager.shouldRun()).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false on non-Linux platforms', () => {
|
||||
mockPlatform.mockReturnValue('darwin')
|
||||
|
||||
expect(manager.shouldRun()).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true on Linux without DISPLAY', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
delete process.env.DISPLAY
|
||||
|
||||
expect(manager.shouldRun()).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true on Linux in CI environment', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = true
|
||||
|
||||
expect(manager.shouldRun()).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false on Linux with existing DISPLAY (not in CI)', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
expect(manager.shouldRun()).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true when Chrome headless flag is detected', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
const capabilities = {
|
||||
'goog:chromeOptions': {
|
||||
args: ['--headless']
|
||||
}
|
||||
}
|
||||
|
||||
expect(manager.shouldRun(capabilities)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true when Chrome headless=new flag is detected', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
const capabilities = {
|
||||
'goog:chromeOptions': {
|
||||
args: ['--headless=new']
|
||||
}
|
||||
}
|
||||
|
||||
expect(manager.shouldRun(capabilities)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true when Chrome headless=old flag is detected', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
const capabilities = {
|
||||
'goog:chromeOptions': {
|
||||
args: ['--headless=old']
|
||||
}
|
||||
}
|
||||
|
||||
expect(manager.shouldRun(capabilities)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true when Firefox headless flag is detected', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
const capabilities = {
|
||||
'moz:firefoxOptions': {
|
||||
args: ['--headless']
|
||||
}
|
||||
}
|
||||
|
||||
expect(manager.shouldRun(capabilities)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return true when Firefox -headless flag is detected', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
const capabilities = {
|
||||
'moz:firefoxOptions': {
|
||||
args: ['-headless']
|
||||
}
|
||||
}
|
||||
|
||||
expect(manager.shouldRun(capabilities)).toBe(true)
|
||||
})
|
||||
|
||||
it('should handle array of capabilities (multiremote)', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
const capabilities = {
|
||||
browser1: {
|
||||
capabilities: {
|
||||
'goog:chromeOptions': {
|
||||
args: ['--headless']
|
||||
}
|
||||
}
|
||||
},
|
||||
browser2: {
|
||||
capabilities: {
|
||||
'moz:firefoxOptions': {
|
||||
args: ['--disable-gpu']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect(manager.shouldRun(capabilities)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false when no headless flags in capabilities', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
const capabilities = {
|
||||
'goog:chromeOptions': {
|
||||
args: ['--disable-gpu']
|
||||
}
|
||||
}
|
||||
|
||||
expect(manager.shouldRun(capabilities)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle capabilities without args', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
const capabilities = {
|
||||
'goog:chromeOptions': {}
|
||||
}
|
||||
|
||||
expect(manager.shouldRun(capabilities)).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle empty capabilities', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
expect(manager.shouldRun({})).toBe(false)
|
||||
})
|
||||
|
||||
it('should handle undefined capabilities', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
expect(manager.shouldRun(undefined)).toBe(false)
|
||||
})
|
||||
|
||||
it('should return true when Edge headless flag is detected (ms:edgeOptions)', () => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
|
||||
const capabilities = {
|
||||
'ms:edgeOptions': {
|
||||
args: ['--headless']
|
||||
}
|
||||
}
|
||||
|
||||
expect(manager.shouldRun(capabilities)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('init', () => {
|
||||
beforeEach(() => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
})
|
||||
|
||||
it('should setup xvfb-run when needed', async () => {
|
||||
mockExecAsync.mockResolvedValue({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
|
||||
|
||||
const result = await manager.init()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockExecAsync).toHaveBeenCalledWith('which xvfb-run')
|
||||
})
|
||||
|
||||
it('should not setup when not needed', async () => {
|
||||
mockPlatform.mockReturnValue('darwin')
|
||||
|
||||
const result = await manager.init()
|
||||
|
||||
expect(result).toBe(false)
|
||||
})
|
||||
|
||||
it('should setup xvfb-run when headless capabilities are provided', async () => {
|
||||
process.env.DISPLAY = ':0'
|
||||
mockIsCI.value = false
|
||||
mockExecAsync.mockResolvedValue({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
|
||||
|
||||
const capabilities = {
|
||||
'goog:chromeOptions': {
|
||||
args: ['--headless']
|
||||
}
|
||||
}
|
||||
|
||||
const result = await manager.init(capabilities)
|
||||
|
||||
expect(result).toBe(true)
|
||||
})
|
||||
|
||||
it('should install xvfb when xvfb-run is not available', async () => {
|
||||
// Mock xvfb-run not found first, then found after installation
|
||||
mockExecAsync
|
||||
.mockRejectedValueOnce(new Error('Command not found'))
|
||||
.mockResolvedValueOnce({ stdout: '', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/which', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'apt-get install success', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
|
||||
|
||||
const manager = new XvfbManager()
|
||||
|
||||
// Mock platform and environment
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
delete process.env.DISPLAY
|
||||
|
||||
const result = await manager.init()
|
||||
|
||||
expect(result).toBe(true)
|
||||
expect(mockExecAsync).toHaveBeenCalledWith('which xvfb-run')
|
||||
expect(mockExecAsync).toHaveBeenCalledWith('which apt-get')
|
||||
expect(mockExecAsync).toHaveBeenCalledWith(
|
||||
'sudo apt-get update -qq && sudo apt-get install -y xvfb',
|
||||
{ timeout: 240000 }
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cross-distribution support', () => {
|
||||
beforeEach(() => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
})
|
||||
|
||||
it('should detect Ubuntu distribution', async () => {
|
||||
// Mock xvfb-run not found, then package manager detection
|
||||
mockExecAsync
|
||||
.mockRejectedValueOnce(new Error('Command not found'))
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/apt-get', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
|
||||
|
||||
const manager = new XvfbManager()
|
||||
delete process.env.DISPLAY
|
||||
|
||||
await manager.init()
|
||||
|
||||
expect(mockExecAsync).toHaveBeenCalledWith('which apt-get')
|
||||
expect(mockExecAsync).toHaveBeenCalledWith(
|
||||
'sudo apt-get update -qq && sudo apt-get install -y xvfb',
|
||||
{ timeout: 240000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('should detect dnf package manager', async () => {
|
||||
// Mock xvfb-run not found, then package manager detection
|
||||
mockExecAsync
|
||||
.mockRejectedValueOnce(new Error('Command not found'))
|
||||
.mockRejectedValueOnce(new Error('apt-get not found'))
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/dnf', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
|
||||
|
||||
const manager = new XvfbManager()
|
||||
delete process.env.DISPLAY
|
||||
|
||||
await manager.init()
|
||||
|
||||
expect(mockExecAsync).toHaveBeenCalledWith('which dnf')
|
||||
expect(mockExecAsync).toHaveBeenCalledWith(
|
||||
'sudo dnf makecache && sudo dnf install -y xorg-x11-server-Xvfb',
|
||||
{ timeout: 240000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('should detect pacman package manager', async () => {
|
||||
// Mock xvfb-run not found, then package manager detection
|
||||
mockExecAsync
|
||||
.mockRejectedValueOnce(new Error('Command not found'))
|
||||
.mockRejectedValueOnce(new Error('apt-get not found'))
|
||||
.mockRejectedValueOnce(new Error('dnf not found'))
|
||||
.mockRejectedValueOnce(new Error('yum not found'))
|
||||
.mockRejectedValueOnce(new Error('zypper not found'))
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/pacman', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
|
||||
|
||||
const manager = new XvfbManager()
|
||||
delete process.env.DISPLAY
|
||||
|
||||
await manager.init()
|
||||
|
||||
expect(mockExecAsync).toHaveBeenCalledWith('which pacman')
|
||||
expect(mockExecAsync).toHaveBeenCalledWith(
|
||||
'sudo pacman -Sy --noconfirm xorg-server-xvfb',
|
||||
{ timeout: 240000 }
|
||||
)
|
||||
})
|
||||
|
||||
it('should detect dnf when apt-get is not available', async () => {
|
||||
// Mock xvfb-run not found, then package manager detection
|
||||
mockExecAsync
|
||||
.mockRejectedValueOnce(new Error('Command not found'))
|
||||
.mockRejectedValueOnce(new Error('apt-get not found'))
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/dnf', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'installation success', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: '/usr/bin/xvfb-run\n', stderr: '' })
|
||||
|
||||
const manager = new XvfbManager()
|
||||
delete process.env.DISPLAY
|
||||
|
||||
await manager.init()
|
||||
|
||||
expect(mockExecAsync).toHaveBeenCalledWith('which apt-get')
|
||||
expect(mockExecAsync).toHaveBeenCalledWith('which dnf')
|
||||
})
|
||||
|
||||
it('should handle unsupported package managers gracefully', async () => {
|
||||
// Mock all package managers as not found
|
||||
mockExecAsync
|
||||
.mockRejectedValueOnce(new Error('Command not found'))
|
||||
.mockRejectedValue(new Error('Package manager not found'))
|
||||
|
||||
const manager = new XvfbManager()
|
||||
delete process.env.DISPLAY
|
||||
|
||||
await expect(manager.init()).rejects.toThrow(
|
||||
'Unsupported package manager: unknown. Please install Xvfb manually.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('executeWithRetry', () => {
|
||||
beforeEach(() => {
|
||||
mockPlatform.mockReturnValue('linux')
|
||||
})
|
||||
|
||||
it('should succeed on first attempt', async () => {
|
||||
const mockFn = vi.fn().mockResolvedValue('success')
|
||||
|
||||
const result = await manager.executeWithRetry(mockFn, 'test operation')
|
||||
|
||||
expect(result).toBe('success')
|
||||
expect(mockFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should retry on xvfb-related errors', async () => {
|
||||
const manager = new XvfbManager({ xvfbMaxRetries: 2, xvfbRetryDelay: 10 })
|
||||
const mockFn = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('xvfb-run: error: Xvfb failed to start'))
|
||||
.mockResolvedValueOnce('success')
|
||||
|
||||
const result = await manager.executeWithRetry(mockFn, 'test operation')
|
||||
|
||||
expect(result).toBe('success')
|
||||
expect(mockFn).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should retry with progressive delay', async () => {
|
||||
const manager = new XvfbManager({ xvfbMaxRetries: 3, xvfbRetryDelay: 100 })
|
||||
const mockFn = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('Xvfb failed to start'))
|
||||
.mockRejectedValueOnce(new Error('xvfb-run: error:'))
|
||||
.mockResolvedValueOnce('success')
|
||||
|
||||
const startTime = Date.now()
|
||||
const result = await manager.executeWithRetry(mockFn, 'test operation')
|
||||
const endTime = Date.now()
|
||||
|
||||
expect(result).toBe('success')
|
||||
expect(mockFn).toHaveBeenCalledTimes(3)
|
||||
// Should have waited at least 100ms + 200ms = 300ms for two retries
|
||||
expect(endTime - startTime).toBeGreaterThan(280)
|
||||
})
|
||||
|
||||
it('should not retry on non-xvfb errors', async () => {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('Regular error'))
|
||||
|
||||
await expect(manager.executeWithRetry(mockFn, 'test operation')).rejects.toThrow('Regular error')
|
||||
expect(mockFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should exhaust retries and throw last error', async () => {
|
||||
const manager = new XvfbManager({ xvfbMaxRetries: 2, xvfbRetryDelay: 10 })
|
||||
const mockFn = vi.fn()
|
||||
.mockRejectedValueOnce(new Error('xvfb-run: error: Xvfb failed to start'))
|
||||
.mockRejectedValueOnce(new Error('X server died'))
|
||||
|
||||
await expect(manager.executeWithRetry(mockFn, 'test operation')).rejects.toThrow('X server died')
|
||||
expect(mockFn).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('should detect various xvfb error patterns', async () => {
|
||||
const manager = new XvfbManager({ xvfbMaxRetries: 1, xvfbRetryDelay: 10 })
|
||||
|
||||
const errorPatterns = [
|
||||
'xvfb-run: error: Xvfb failed to start',
|
||||
'Xvfb failed to start',
|
||||
'xvfb-run: error: something else',
|
||||
'X server died'
|
||||
]
|
||||
|
||||
for (const errorMessage of errorPatterns) {
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error(errorMessage))
|
||||
|
||||
await expect(manager.executeWithRetry(mockFn, 'test')).rejects.toThrow(errorMessage)
|
||||
expect(mockFn).toHaveBeenCalledTimes(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle case insensitive error matching', async () => {
|
||||
const manager = new XvfbManager({ xvfbMaxRetries: 1, xvfbRetryDelay: 10 })
|
||||
const mockFn = vi.fn().mockRejectedValue(new Error('XVFB-RUN: ERROR: XVFB FAILED TO START'))
|
||||
|
||||
await expect(manager.executeWithRetry(mockFn, 'test')).rejects.toThrow('XVFB-RUN: ERROR: XVFB FAILED TO START')
|
||||
expect(mockFn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
10
packages/wdio-xvfb/tsconfig.json
Normal file
10
packages/wdio-xvfb/tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
||||
10
packages/wdio-xvfb/tsconfig.prod.json
Normal file
10
packages/wdio-xvfb/tsconfig.prod.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.prod.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./build",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
]
|
||||
}
|
||||
150
pnpm-lock.yaml
150
pnpm-lock.yaml
|
|
@ -291,6 +291,9 @@ importers:
|
|||
'@wdio/types':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/wdio-types
|
||||
'@wdio/xvfb':
|
||||
specifier: workspace:*
|
||||
version: link:../packages/wdio-xvfb
|
||||
babel-plugin-transform-hook-names:
|
||||
specifier: ^1.0.2
|
||||
version: 1.0.2(@babel/core@7.27.7)
|
||||
|
|
@ -443,7 +446,7 @@ importers:
|
|||
version: 5.2.4(vite@5.4.19(@types/node@24.0.11)(terser@5.43.1))(vue@3.5.17(typescript@5.8.3))
|
||||
expect-webdriverio:
|
||||
specifier: ^5.3.4
|
||||
version: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.16.2)(webdriverio@9.17.0(puppeteer-core@24.12.1))
|
||||
version: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.18.0)(webdriverio@9.18.4(puppeteer-core@24.12.1))
|
||||
mocha:
|
||||
specifier: ^10.2.0
|
||||
version: 10.8.2
|
||||
|
|
@ -988,7 +991,7 @@ importers:
|
|||
dependencies:
|
||||
expect-webdriverio:
|
||||
specifier: ^5.3.4
|
||||
version: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.16.2)(webdriverio@9.16.2(puppeteer-core@24.12.1))
|
||||
version: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.18.0)(webdriverio@9.16.2(puppeteer-core@24.12.1))
|
||||
webdriverio:
|
||||
specifier: ^9.0.0
|
||||
version: 9.16.2(puppeteer-core@24.12.1)
|
||||
|
|
@ -1133,12 +1136,15 @@ importers:
|
|||
'@wdio/types':
|
||||
specifier: workspace:*
|
||||
version: link:../wdio-types
|
||||
'@wdio/xvfb':
|
||||
specifier: workspace:*
|
||||
version: link:../wdio-xvfb
|
||||
exit-hook:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0
|
||||
expect-webdriverio:
|
||||
specifier: ^5.3.4
|
||||
version: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@packages+wdio-logger)(webdriverio@9.17.0(puppeteer-core@24.12.1))
|
||||
version: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@packages+wdio-logger)(webdriverio@9.18.4(puppeteer-core@24.12.1))
|
||||
split2:
|
||||
specifier: ^4.1.0
|
||||
version: 4.2.0
|
||||
|
|
@ -1489,6 +1495,25 @@ importers:
|
|||
specifier: workspace:*
|
||||
version: link:../wdio-globals
|
||||
|
||||
packages/wdio-xvfb:
|
||||
dependencies:
|
||||
'@wdio/logger':
|
||||
specifier: workspace:*
|
||||
version: link:../wdio-logger
|
||||
is-ci:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
devDependencies:
|
||||
'@types/is-ci':
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
'@types/node':
|
||||
specifier: ^20.0.0
|
||||
version: 20.19.5
|
||||
'@wdio/types':
|
||||
specifier: workspace:*
|
||||
version: link:../wdio-types
|
||||
|
||||
packages/webdriver:
|
||||
dependencies:
|
||||
'@types/node':
|
||||
|
|
@ -1895,7 +1920,7 @@ importers:
|
|||
version: link:../../../packages/wdio-utils
|
||||
expect-webdriverio:
|
||||
specifier: ^5.3.4
|
||||
version: 5.3.4(@wdio/globals@packages+wdio-globals)(@wdio/logger@9.16.2)(webdriverio@packages+webdriverio)
|
||||
version: 5.3.4(@wdio/globals@packages+wdio-globals)(@wdio/logger@9.18.0)(webdriverio@packages+webdriverio)
|
||||
webdriver:
|
||||
specifier: workspace:*
|
||||
version: link:../../../packages/webdriver
|
||||
|
|
@ -5652,6 +5677,9 @@ packages:
|
|||
'@types/ip@1.1.3':
|
||||
resolution: {integrity: sha512-64waoJgkXFTYnCYDUWgSATJ/dXEBanVkaP5d4Sbk7P6U7cTTMhxVyROTckc6JKdwCrgnAjZMn0k3177aQxtDEA==}
|
||||
|
||||
'@types/is-ci@3.0.4':
|
||||
resolution: {integrity: sha512-AkCYCmwlXeuH89DagDCzvCAyltI2v9lh3U3DqSg/GrBYoReAaWwxfXCqMx9UV5MajLZ4ZFwZzV4cABGIxk2XRw==}
|
||||
|
||||
'@types/is-glob@4.0.4':
|
||||
resolution: {integrity: sha512-3mFBtIPQ0TQetKRDe94g8YrxJZxdMillMGegyv6zRBXvq4peRRhf2wLZ/Dl53emtTsC29dQQBwYvovS20yXpiQ==}
|
||||
|
||||
|
|
@ -6075,8 +6103,8 @@ packages:
|
|||
resolution: {integrity: sha512-a7zDSNzpGgkb6mWrg9GWPmvh/sZFzaf86/iBjCv+n2DTY0+8v8NLruRQmWuCaQAlLVhM3XAqmB+fWLqxDhdvOA==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@wdio/config@9.17.0':
|
||||
resolution: {integrity: sha512-AmP9J1GiCUj8g9aNeCuJRKAo+dET8rukVzrGQVq8jdqkj4PLjLA2hbfcQ8zQi/xwVUebnuIUT5weaomgKyb7Ng==}
|
||||
'@wdio/config@9.18.0':
|
||||
resolution: {integrity: sha512-fN+Z7SkKjb0u3UUMSxMN4d+CCZQKZhm/tx3eX7Rv+3T78LtpOjlesBYQ7Ax3tQ3tp8hgEo+CoOXU0jHEYubFrg==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@wdio/dot-reporter@9.16.2':
|
||||
|
|
@ -6104,6 +6132,10 @@ packages:
|
|||
resolution: {integrity: sha512-6A1eVpNPToWupLIo8CXStth4HJGTfxKsAiKtwE0xQFKyDM8uPTm3YO3Nf15vCSHbmsncbYVEo7QrUwRUEB4YUg==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@wdio/logger@9.18.0':
|
||||
resolution: {integrity: sha512-HdzDrRs+ywAqbXGKqe1i/bLtCv47plz4TvsHFH3j729OooT5VH38ctFn5aLXgECmiAKDkmH/A6kOq2Zh5DIxww==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@wdio/protocols@9.16.2':
|
||||
resolution: {integrity: sha512-h3k97/lzmyw5MowqceAuY3HX/wGJojXHkiPXA3WlhGPCaa2h4+GovV2nJtRvknCKsE7UHA1xB5SWeI8MzloBew==}
|
||||
|
||||
|
|
@ -6130,8 +6162,8 @@ packages:
|
|||
resolution: {integrity: sha512-bsRdEUXUTYvznXH/Z+p6HDzHSjMI6I6bnu8WXWTeDDDyqybWK5D8cbZvs8A/kMmGXoz1GZkSBHxy4Z5NTg8OQg==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@wdio/utils@9.17.0':
|
||||
resolution: {integrity: sha512-z9k8+DbhyJPLs88w/SLcUuXMdoPq2r/lE2qQ79pqWAG0jmm7kJqkBvZyndaTrSrGbh+NgjTEgh1epD1PHYtnwg==}
|
||||
'@wdio/utils@9.18.0':
|
||||
resolution: {integrity: sha512-M+QH05FUw25aFXZfjb+V16ydKoURgV61zeZrMjQdW2aAiks3F5iiI9pgqYT5kr1kHZcMy8gawGqQQ+RVfKYscQ==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
|
|
@ -6893,10 +6925,6 @@ packages:
|
|||
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ci-info@4.2.0:
|
||||
resolution: {integrity: sha512-cYY9mypksY8NRqgDB1XD1RiJL338v/551niynFTGkZOO2LHuB2OmOYxDIe/ttN9AHwrqdum1360G3ald0W9kCg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ci-info@4.3.0:
|
||||
resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -9309,6 +9337,10 @@ packages:
|
|||
resolution: {integrity: sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==}
|
||||
hasBin: true
|
||||
|
||||
is-ci@4.1.0:
|
||||
resolution: {integrity: sha512-Ab9bQDQ11lWootZUI5qxgN2ZXwxNI5hTwnsvOc1wyxQ7zQ8OkEDw79mI0+9jI3x432NfwbVRru+3noJfXF6lSQ==}
|
||||
hasBin: true
|
||||
|
||||
is-core-module@2.16.1:
|
||||
resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -13739,8 +13771,8 @@ packages:
|
|||
resolution: {integrity: sha512-T7QKqD+N0hfvrxq/am5wqdOuyOy7F2tGS7X2f/7jyhrxSPG6Q0cNkSt4gCwla+q3nDMivCP0QIPc7mAVSx5AYQ==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
webdriver@9.17.0:
|
||||
resolution: {integrity: sha512-vDdW5SeAe1Fn2N79cdrRIHnbTcXlpIM1f0Ki8AHGsC0DkvYRyshnuI6kgySXN8y4+Thy/xo+vDdr0CLJF2Zqgw==}
|
||||
webdriver@9.18.0:
|
||||
resolution: {integrity: sha512-07lC4FLj45lHJo0FvLjUp5qkjzEGWJWKGsxLoe9rQ2Fg88iYsqgr9JfSj8qxHpazBaBd+77+ZtpmMZ2X2D1Zuw==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
|
||||
webdriverio@9.16.2:
|
||||
|
|
@ -13752,8 +13784,8 @@ packages:
|
|||
puppeteer-core:
|
||||
optional: true
|
||||
|
||||
webdriverio@9.17.0:
|
||||
resolution: {integrity: sha512-LXLDhEUQC3AiYAPWZmpkZXBLaLzXlEGQskpG0CLx6S5hXAWNHs5wItgozrH9CfG7N6j26mc2avWN2ruy/vwrWQ==}
|
||||
webdriverio@9.18.4:
|
||||
resolution: {integrity: sha512-Q/gghz/Zt7EhTnbDQfLb61WgSwCksXZE60lEzmDXe4fULCH/6Js5IWUsne3W+BRy6nXeVvFscHD/d7S77dbamw==}
|
||||
engines: {node: '>=18.20.0'}
|
||||
peerDependencies:
|
||||
puppeteer-core: '>=22.x || <=24.x'
|
||||
|
|
@ -15879,7 +15911,7 @@ snapshots:
|
|||
'@cucumber/ci-environment': 10.0.1
|
||||
'@cucumber/cucumber-expressions': 17.1.0
|
||||
'@cucumber/gherkin': 28.0.0
|
||||
'@cucumber/gherkin-streams': 5.0.1(@cucumber/gherkin@28.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@24.1.0))(@cucumber/messages@24.1.0)
|
||||
'@cucumber/gherkin-streams': 5.0.1(@cucumber/gherkin@28.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@26.0.1))(@cucumber/messages@24.1.0)
|
||||
'@cucumber/gherkin-utils': 9.0.0
|
||||
'@cucumber/html-formatter': 21.6.0(@cucumber/messages@24.1.0)
|
||||
'@cucumber/message-streams': 4.0.1(@cucumber/messages@24.1.0)
|
||||
|
|
@ -15919,7 +15951,7 @@ snapshots:
|
|||
yaml: 2.8.0
|
||||
yup: 1.2.0
|
||||
|
||||
'@cucumber/gherkin-streams@5.0.1(@cucumber/gherkin@28.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@24.1.0))(@cucumber/messages@24.1.0)':
|
||||
'@cucumber/gherkin-streams@5.0.1(@cucumber/gherkin@28.0.0)(@cucumber/message-streams@4.0.1(@cucumber/messages@26.0.1))(@cucumber/messages@24.1.0)':
|
||||
dependencies:
|
||||
'@cucumber/gherkin': 28.0.0
|
||||
'@cucumber/message-streams': 4.0.1(@cucumber/messages@24.1.0)
|
||||
|
|
@ -19279,6 +19311,10 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/node': 20.19.5
|
||||
|
||||
'@types/is-ci@3.0.4':
|
||||
dependencies:
|
||||
ci-info: 3.9.0
|
||||
|
||||
'@types/is-glob@4.0.4': {}
|
||||
|
||||
'@types/istanbul-lib-coverage@2.0.6': {}
|
||||
|
|
@ -19853,11 +19889,11 @@ snapshots:
|
|||
- bare-buffer
|
||||
- supports-color
|
||||
|
||||
'@wdio/config@9.17.0':
|
||||
'@wdio/config@9.18.0':
|
||||
dependencies:
|
||||
'@wdio/logger': 9.16.2
|
||||
'@wdio/logger': 9.18.0
|
||||
'@wdio/types': 9.16.2
|
||||
'@wdio/utils': 9.17.0
|
||||
'@wdio/utils': 9.18.0
|
||||
deepmerge-ts: 7.1.5
|
||||
glob: 10.4.5
|
||||
import-meta-resolve: 4.1.0
|
||||
|
|
@ -19899,13 +19935,13 @@ snapshots:
|
|||
|
||||
'@wdio/globals@9.17.0(expect-webdriverio@5.3.4)(webdriverio@9.16.2(puppeteer-core@24.12.1))':
|
||||
dependencies:
|
||||
expect-webdriverio: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.16.2)(webdriverio@9.16.2(puppeteer-core@24.12.1))
|
||||
expect-webdriverio: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.18.0)(webdriverio@9.16.2(puppeteer-core@24.12.1))
|
||||
webdriverio: 9.16.2(puppeteer-core@24.12.1)
|
||||
|
||||
'@wdio/globals@9.17.0(expect-webdriverio@5.3.4)(webdriverio@9.17.0(puppeteer-core@24.12.1))':
|
||||
'@wdio/globals@9.17.0(expect-webdriverio@5.3.4)(webdriverio@9.18.4(puppeteer-core@24.12.1))':
|
||||
dependencies:
|
||||
expect-webdriverio: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.16.2)(webdriverio@9.17.0(puppeteer-core@24.12.1))
|
||||
webdriverio: 9.17.0(puppeteer-core@24.12.1)
|
||||
expect-webdriverio: 5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.18.0)(webdriverio@9.18.4(puppeteer-core@24.12.1))
|
||||
webdriverio: 9.18.4(puppeteer-core@24.12.1)
|
||||
|
||||
'@wdio/logger@9.16.2':
|
||||
dependencies:
|
||||
|
|
@ -19914,6 +19950,14 @@ snapshots:
|
|||
loglevel-plugin-prefix: 0.8.4
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
'@wdio/logger@9.18.0':
|
||||
dependencies:
|
||||
chalk: 5.4.1
|
||||
loglevel: 1.9.2
|
||||
loglevel-plugin-prefix: 0.8.4
|
||||
safe-regex2: 5.0.0
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
'@wdio/protocols@9.16.2': {}
|
||||
|
||||
'@wdio/repl@9.16.2':
|
||||
|
|
@ -19970,10 +20014,10 @@ snapshots:
|
|||
- bare-buffer
|
||||
- supports-color
|
||||
|
||||
'@wdio/utils@9.17.0':
|
||||
'@wdio/utils@9.18.0':
|
||||
dependencies:
|
||||
'@puppeteer/browsers': 2.10.5
|
||||
'@wdio/logger': 9.16.2
|
||||
'@wdio/logger': 9.18.0
|
||||
'@wdio/types': 9.16.2
|
||||
decamelize: 6.0.0
|
||||
deepmerge-ts: 7.1.5
|
||||
|
|
@ -20919,8 +20963,6 @@ snapshots:
|
|||
|
||||
ci-info@3.9.0: {}
|
||||
|
||||
ci-info@4.2.0: {}
|
||||
|
||||
ci-info@4.3.0: {}
|
||||
|
||||
citty@0.1.6:
|
||||
|
|
@ -22243,7 +22285,7 @@ snapshots:
|
|||
'@babel/helper-validator-identifier': 7.27.1
|
||||
'@eslint-community/eslint-utils': 4.7.0(eslint@9.30.0(jiti@2.4.2))
|
||||
'@eslint/plugin-kit': 0.2.8
|
||||
ci-info: 4.2.0
|
||||
ci-info: 4.3.0
|
||||
clean-regexp: 1.0.0
|
||||
core-js-compat: 3.43.0
|
||||
eslint: 9.30.0(jiti@2.4.2)
|
||||
|
|
@ -22464,41 +22506,41 @@ snapshots:
|
|||
|
||||
expect-type@1.2.1: {}
|
||||
|
||||
expect-webdriverio@5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.16.2)(webdriverio@9.16.2(puppeteer-core@24.12.1)):
|
||||
expect-webdriverio@5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.18.0)(webdriverio@9.16.2(puppeteer-core@24.12.1)):
|
||||
dependencies:
|
||||
'@vitest/snapshot': 3.2.4
|
||||
'@wdio/globals': 9.17.0(expect-webdriverio@5.3.4)(webdriverio@9.16.2(puppeteer-core@24.12.1))
|
||||
'@wdio/logger': 9.16.2
|
||||
'@wdio/logger': 9.18.0
|
||||
expect: 30.0.4
|
||||
jest-matcher-utils: 30.0.3
|
||||
lodash.isequal: 4.5.0
|
||||
webdriverio: 9.16.2(puppeteer-core@24.12.1)
|
||||
|
||||
expect-webdriverio@5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.16.2)(webdriverio@9.17.0(puppeteer-core@24.12.1)):
|
||||
expect-webdriverio@5.3.4(@wdio/globals@9.17.0)(@wdio/logger@9.18.0)(webdriverio@9.18.4(puppeteer-core@24.12.1)):
|
||||
dependencies:
|
||||
'@vitest/snapshot': 3.2.4
|
||||
'@wdio/globals': 9.17.0(expect-webdriverio@5.3.4)(webdriverio@9.17.0(puppeteer-core@24.12.1))
|
||||
'@wdio/logger': 9.16.2
|
||||
'@wdio/globals': 9.17.0(expect-webdriverio@5.3.4)(webdriverio@9.18.4(puppeteer-core@24.12.1))
|
||||
'@wdio/logger': 9.18.0
|
||||
expect: 30.0.4
|
||||
jest-matcher-utils: 30.0.3
|
||||
lodash.isequal: 4.5.0
|
||||
webdriverio: 9.17.0(puppeteer-core@24.12.1)
|
||||
webdriverio: 9.18.4(puppeteer-core@24.12.1)
|
||||
|
||||
expect-webdriverio@5.3.4(@wdio/globals@9.17.0)(@wdio/logger@packages+wdio-logger)(webdriverio@9.17.0(puppeteer-core@24.12.1)):
|
||||
expect-webdriverio@5.3.4(@wdio/globals@9.17.0)(@wdio/logger@packages+wdio-logger)(webdriverio@9.18.4(puppeteer-core@24.12.1)):
|
||||
dependencies:
|
||||
'@vitest/snapshot': 3.2.4
|
||||
'@wdio/globals': 9.17.0(expect-webdriverio@5.3.4)(webdriverio@9.17.0(puppeteer-core@24.12.1))
|
||||
'@wdio/globals': 9.17.0(expect-webdriverio@5.3.4)(webdriverio@9.18.4(puppeteer-core@24.12.1))
|
||||
'@wdio/logger': link:packages/wdio-logger
|
||||
expect: 30.0.4
|
||||
jest-matcher-utils: 30.0.3
|
||||
lodash.isequal: 4.5.0
|
||||
webdriverio: 9.17.0(puppeteer-core@24.12.1)
|
||||
webdriverio: 9.18.4(puppeteer-core@24.12.1)
|
||||
|
||||
expect-webdriverio@5.3.4(@wdio/globals@packages+wdio-globals)(@wdio/logger@9.16.2)(webdriverio@packages+webdriverio):
|
||||
expect-webdriverio@5.3.4(@wdio/globals@packages+wdio-globals)(@wdio/logger@9.18.0)(webdriverio@packages+webdriverio):
|
||||
dependencies:
|
||||
'@vitest/snapshot': 3.2.4
|
||||
'@wdio/globals': link:packages/wdio-globals
|
||||
'@wdio/logger': 9.16.2
|
||||
'@wdio/logger': 9.18.0
|
||||
expect: 30.0.4
|
||||
jest-matcher-utils: 30.0.3
|
||||
lodash.isequal: 4.5.0
|
||||
|
|
@ -23829,6 +23871,10 @@ snapshots:
|
|||
dependencies:
|
||||
ci-info: 3.9.0
|
||||
|
||||
is-ci@4.1.0:
|
||||
dependencies:
|
||||
ci-info: 4.3.0
|
||||
|
||||
is-core-module@2.16.1:
|
||||
dependencies:
|
||||
hasown: 2.0.2
|
||||
|
|
@ -24475,7 +24521,7 @@ snapshots:
|
|||
|
||||
libnpmpublish@9.0.9:
|
||||
dependencies:
|
||||
ci-info: 4.2.0
|
||||
ci-info: 4.3.0
|
||||
normalize-package-data: 6.0.2
|
||||
npm-package-arg: 11.0.2
|
||||
npm-registry-fetch: 17.1.0
|
||||
|
|
@ -29212,15 +29258,15 @@ snapshots:
|
|||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
webdriver@9.17.0:
|
||||
webdriver@9.18.0:
|
||||
dependencies:
|
||||
'@types/node': 20.19.5
|
||||
'@types/ws': 8.18.1
|
||||
'@wdio/config': 9.17.0
|
||||
'@wdio/logger': 9.16.2
|
||||
'@wdio/config': 9.18.0
|
||||
'@wdio/logger': 9.18.0
|
||||
'@wdio/protocols': 9.16.2
|
||||
'@wdio/types': 9.16.2
|
||||
'@wdio/utils': 9.17.0
|
||||
'@wdio/utils': 9.18.0
|
||||
deepmerge-ts: 7.1.5
|
||||
https-proxy-agent: 7.0.6
|
||||
undici: 6.21.3
|
||||
|
|
@ -29266,16 +29312,16 @@ snapshots:
|
|||
- supports-color
|
||||
- utf-8-validate
|
||||
|
||||
webdriverio@9.17.0(puppeteer-core@24.12.1):
|
||||
webdriverio@9.18.4(puppeteer-core@24.12.1):
|
||||
dependencies:
|
||||
'@types/node': 20.19.5
|
||||
'@types/sinonjs__fake-timers': 8.1.5
|
||||
'@wdio/config': 9.17.0
|
||||
'@wdio/logger': 9.16.2
|
||||
'@wdio/config': 9.18.0
|
||||
'@wdio/logger': 9.18.0
|
||||
'@wdio/protocols': 9.16.2
|
||||
'@wdio/repl': 9.16.2
|
||||
'@wdio/types': 9.16.2
|
||||
'@wdio/utils': 9.17.0
|
||||
'@wdio/utils': 9.18.0
|
||||
archiver: 7.0.1
|
||||
aria-query: 5.3.2
|
||||
cheerio: 1.1.0
|
||||
|
|
@ -29290,9 +29336,9 @@ snapshots:
|
|||
query-selector-shadow-dom: 1.0.1
|
||||
resq: 1.11.0
|
||||
rgb2hex: 0.2.5
|
||||
serialize-error: 11.0.3
|
||||
serialize-error: 12.0.0
|
||||
urlpattern-polyfill: 10.1.0
|
||||
webdriver: 9.17.0
|
||||
webdriver: 9.18.0
|
||||
optionalDependencies:
|
||||
puppeteer-core: 24.12.1
|
||||
transitivePeerDependencies:
|
||||
|
|
|
|||
Loading…
Reference in a new issue