diff --git a/.circleci/config.yml b/.circleci/config.yml
index ac71ee446c..2354e05129 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,272 +1,245 @@
-version: 2
+version: 2.1
-dependencies:
- pre:
- - curl -L -o google-chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- - sudo dpkg -i google-chrome.deb
- - sudo sed -i 's|HERE/chrome\"|HERE/chrome\" --disable-setuid-sandbox|g' /opt/google/chrome/google-chrome
- - rm google-chrome.deb
jobs:
build:
- docker:
- - image: cimg/php:8.1.12-node
- name: restarters.test
- environment:
- - DB_CONNECTION: mysql
- - DB_HOST: 127.0.0.1
- - DB_PORT: 3306
- - DB_DATABASE: restarters_db
- - DB_USERNAME: restarters
- - DB_PASSWORD: s3cr3t
- - TZ: "UTC"
- - image: cimg/mysql:8.0
- environment:
- # You can connect once ssh'd in using mysql -u root -p -h 127.0.0.1
- - MYSQL_ROOT_PASSWORD: s3cr3t
- - MYSQL_DATABASE: restarters_db
- - MYSQL_USER: restarters
- - MYSQL_PASSWORD: s3cr3t
- - image: mcr.microsoft.com/playwright:focal
- environment:
- NODE_ENV: development
- TZ: "UTC"
- - image: 'bitnami/mariadb:latest'
- name: mariadb
- environment:
- - ALLOW_EMPTY_PASSWORD=yes
- - MARIADB_PORT_NUMBER=3307
- - MARIADB_USER=bn_mediawiki
- - MARIADB_DATABASE=bitnami_mediawiki
- - image: 'bitnami/mediawiki-archived:1'
- name: mediawiki
- labels:
- kompose.service.type: nodeport
- environment:
- - MEDIAWIKI_DATABASE_HOST=mariadb
- - MEDIAWIKI_DATABASE_PORT_NUMBER=3307
- - MEDIAWIKI_DATABASE_USER=bn_mediawiki
- - MEDIAWIKI_DATABASE_NAME=bitnami_mediawiki
- - ALLOW_EMPTY_PASSWORD=yes
- - MEDIAWIKI_EXTERNAL_HTTP_PORT_NUMBER=8080
- - MEDIAWIKI_HOST=mediawiki
- - TZ: "UTC"
- depends_on:
- - mariadb
- entrypoint:
- - /bin/bash
- - -c
- - sleep 60; /opt/bitnami/scripts/mediawiki/entrypoint.sh "/opt/bitnami/scripts/apache/run.sh"
- - image: 'docker.io/bitnami/postgresql:11'
- name: postgresql
-# No volumes on CircleCI
-# volumes:
-# - 'postgresql_data:/bitnami/postgresql'
- environment:
- - ALLOW_EMPTY_PASSWORD=yes
- - POSTGRESQL_USERNAME=bn_discourse
- - POSTGRESQL_DATABASE=bitnami_discourse
-# No networks on CircleCI
-# networks:
-# - app-network
-
- - image: docker.io/bitnami/redis:6.0
- name: restarters_discourse_redis
- environment:
- - ALLOW_EMPTY_PASSWORD=yes
-# volumes:
-# - 'redis_data:/bitnami/discourse'
-# networks:
-# - app-network
-
- - image: docker.io/bitnami/discourse:2
- name: restarters_discourse
-# No ports on CircleCI
-# ports:
-# - '8003:80'
-# volumes:
-# - 'discourse_data:/bitnami/discourse'
- depends_on:
- - postgresql
- - restarters_discourse_redis
- environment:
- - ALLOW_EMPTY_PASSWORD=yes
- - DISCOURSE_USERNAME=someuser
- - DISCOURSE_PASSWORD=mustbetencharacters
- - DISCOURSE_HOST=www.example.com:8003
- - DISCOURSE_PORT_NUMBER=80
- - DISCOURSE_DATABASE_HOST=postgresql
- - DISCOURSE_DATABASE_PORT_NUMBER=5432
- - DISCOURSE_DATABASE_USER=bn_discourse
- - DISCOURSE_DATABASE_NAME=bitnami_discourse
- - DISCOURSE_REDIS_HOST=restarters_discourse_redis
- - DISCOURSE_REDIS_PORT_NUMBER=6379
- - POSTGRESQL_CLIENT_POSTGRES_USER=postgres
- - POSTGRESQL_CLIENT_CREATE_DATABASE_NAME=bitnami_discourse
- - POSTGRESQL_CLIENT_CREATE_DATABASE_EXTENSIONS=hstore,pg_trgm
- - DISCOURSE_EXTRA_CONF_CONTENT=personal_message_enabled_groups \= 10
-# networks:
-# - app-network
-
- - image: docker.io/bitnami/discourse:latest
- name: restarters_discourse_sidekiq
- depends_on:
- - restarters_discourse
-# volumes:
-# - 'sidekiq_data:/bitnami/discourse'
- command: /opt/bitnami/scripts/discourse-sidekiq/run.sh
- environment:
- - ALLOW_EMPTY_PASSWORD=yes
- - DISCOURSE_HOST=www.example.com
- - DISCOURSE_DATABASE_HOST=postgresql
- - DISCOURSE_DATABASE_PORT_NUMBER=5432
- - DISCOURSE_DATABASE_USER=bn_discourse
- - DISCOURSE_DATABASE_NAME=bitnami_discourse
- - DISCOURSE_REDIS_HOST=restarters_discourse_redis
- - DISCOURSE_REDIS_PORT_NUMBER=6379
-# networks:
-# - app-network
+ machine:
+ image: ubuntu-2204:current
+ resource_class: large
+ environment:
+ - TZ: "UTC"
steps:
- checkout
- - run: sudo bash -c "echo 'Acquire::Retries "3";' > /etc/apt/apt.conf.d/80-retries"
- - run: sudo apt update
- - run: sudo apt install dnsutils openssl zip unzip git libxml2-dev libzip-dev zlib1g-dev libcurl4-openssl-dev iputils-ping default-mysql-client vim libpng-dev libgmp-dev libjpeg-turbo8-dev
- - run: sudo apt-get install php-xmlrpc php8.1-intl php8.1-xdebug php8.1-mbstring php8.1-simplexml php8.1-curl php8.1-zip postgresql-client php8.1-gd php8.1-xmlrpc php8.1-mysql php-mysql
- - run: sudo pecl install xdebug
-
- # We now need Node 18 for Playwright.
- - run: sudo curl -sL https://deb.nodesource.com/setup_18.x | sudo bash -
- - run: sudo apt update
- - run: sudo apt -y install nodejs
- - run: sudo rm /usr/local/bin/node
-
- - run: cp .env.example .env
-
- # Need access to timezones.
- - run: mysql --host="127.0.0.1" -u root -ps3cr3t -e "GRANT SELECT ON mysql.time_zone_name TO 'restarters'@'%';"
-
- # We have Discourse on CircleCI. The API key is inserted using psql below.
- - run: sed -i 's/FEATURE__DISCOURSE_INTEGRATION=.*$/FEATURE__DISCOURSE_INTEGRATION=true/g' .env
- - run: sed -i 's/DISCOURSE_URL=.*$/DISCOURSE_URL=http:\/\/restarters_discourse/g' .env
- - run: sed -i 's/DISCOURSE_APIKEY=.*$/DISCOURSE_APIKEY=fb71f38ca2b8b7cd6a041e57fd8202c9937088f0ecae7db40722bd758dda92fc/g' .env
- - run: sed -i 's/DISCOURSE_APIUSER=.*$/DISCOURSE_APIUSER=someuser/g' .env
-
- # ...and Mediawiki.
- # Disable wiki as problems getting that running.
- # - run: sed -i 's/FEATURE__WIKI_INTEGRATION=.*$/FEATURE__WIKI_INTEGRATION=true/g' .env
- - run: sed -i 's/WIKI_URL=.*$/WIKI_URL=http:\/\/mediawiki:8080/g' .env
- - run: sed -i 's/WIKI_DB=.*$/WIKI_DB=bitnami_mediawiki/g' .env
- - run: sed -i 's/WIKI_USER=.*$/WIKI_USER=user/g' .env
- - run: sed -i 's/WIKI_PASSWORD=.*$/WIKI_PASSWORD=bitnami123/g' .env
- - run: sed -i 's/WIKI_APIUSER=.*$/WIKI_APIUSER=user/g' .env
- - run: sed -i 's/WIKI_APIPASSWORD=.*$/WIKI_APIPASSWORD=bitnami123/g' .env
-
- # Playwright needs the debug bar not to appear
- - run: sed -i 's/APP_DEBUG=.*$/APP_DEBUG=FALSE/g' .env
-
- # ...and runs on localhost.
- - run: sed -i 's/SESSION_DOMAIN=.*$/SESSION_DOMAIN=localhost/g' .env
-
- # ...and needs honeypot rate-limiting needs to be turned off.
- - run: sed -i 's/HONEYPOT_DISABLE=.*$/HONEYPOT_DISABLE=TRUE/g' .env
-
- - run: wget https://getcomposer.org/composer-2.phar -O composer.phar; rm -rf vendor; echo Y | php8.1 composer.phar install
- - run: npm install
- - run: php artisan lang:js --no-lib resources/js/translations.js
- - run: npx playwright install
- - run: npx playwright install-deps
- - run: npm install -D @playwright/test
-
- - run: php artisan key:generate
- - run: mysql --host="127.0.0.1" -u root -ps3cr3t -e "SET PERSIST log_bin_trust_function_creators = 1;"
- - run: php artisan migrate
- - run: php artisan l5-swagger:generate
-
- - run: wget -O phpunit https://phar.phpunit.de/phpunit-9.phar ; chmod +x phpunit
-
- # The phpunit and playwright tests require an uploads directory in a slightly different place. Not really
- # worth fixing.
- - run: mkdir uploads
- - run: mkdir public/uploads
-
- # Wait for Discourse to finish initialising.
- - run: while ! nc -z restarters_discourse 80; do sleep 1 ; done
+
+ # Install Task
+ - run:
+ name: Install Task
+ command: |
+ sudo sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin
+ task --version
- # Add the config we need.
- - run: psql -h postgresql -U postgres -c "INSERT INTO api_keys (id, user_id, created_by_id, created_at, updated_at, allowed_ips, hidden, last_used_at, revoked_at, description, key_hash, truncated_key) VALUES (1, NULL, 1, '2021-10-25 13:56:20.033338', '2021-10-25 13:56:20.033338', NULL, false, NULL, NULL, 'Restarters', 'd89e9dfacfb611fbaf004807648187ce7ed474df44dcb0ada230fab5c8dd6a5b', '9fd7');" bitnami_discourse
- - run: php artisan discourse:setting personal_message_enabled_groups 10
+ # Set up environment file
+ - run:
+ name: Setup environment
+ command: |
+ cp .env.example .env
+
+ # Configure for Docker Compose setup
+ sed -i 's/DB_HOST=.*$/DB_HOST=restarters_db/g' .env
+ sed -i 's/DB_DATABASE=.*$/DB_DATABASE=restarters_db_test/g' .env
+ sed -i 's/DB_USERNAME=.*$/DB_USERNAME=restarters/g' .env
+ sed -i 's/DB_PASSWORD=.*$/DB_PASSWORD=s3cr3t/g' .env
+
+ # Configure Discourse integration
+ sed -i 's/FEATURE__DISCOURSE_INTEGRATION=.*$/FEATURE__DISCOURSE_INTEGRATION=true/g' .env
+ sed -i 's/DISCOURSE_URL=.*$/DISCOURSE_URL=http:\/\/restarters_discourse/g' .env
+ sed -i 's/DISCOURSE_APIKEY=.*$/DISCOURSE_APIKEY=fb71f38ca2b8b7cd6a041e57fd8202c9937088f0ecae7db40722bd758dda92fc/g' .env
+ sed -i 's/DISCOURSE_APIUSER=.*$/DISCOURSE_APIUSER=someuser/g' .env
+
+ # Configure for testing
+ sed -i 's/APP_DEBUG=.*$/APP_DEBUG=FALSE/g' .env
+ sed -i 's/SESSION_DOMAIN=.*$/SESSION_DOMAIN=localhost/g' .env
+ sed -i 's/HONEYPOT_DISABLE=.*$/HONEYPOT_DISABLE=TRUE/g' .env
+ sed -i 's/APP_URL=.*$/APP_URL=http:\/\/localhost:8001/g' .env
+
+ # Add environment variables from CircleCI
+ echo "" >> .env
+ echo "GOOGLE_API_CONSOLE_KEY=$GOOGLE_API_CONSOLE_KEY" >> .env
+ echo "MAPBOX_TOKEN=$MAPBOX_TOKEN" >> .env
+
+ # Start Docker services using Task
+ - run:
+ name: Start Docker services
+ command: |
+ # Set environment variable for CircleCI detection
+ export CIRCLECI=true
+ # Start all services using Task
+ task docker:up-all
+ no_output_timeout: 10m
- # Run phpunit. Discourse makes things slow, so up the timeout.
+ # Wait for initial application build to complete
- run:
- command: export XDEBUG_MODE=coverage;./phpunit -d memory_limit=1024M --bootstrap vendor/autoload.php --coverage-clover tests/clover.xml --configuration ./phpunit.xml
- no_output_timeout: 45m
+ name: Wait for application build
+ command: |
+ echo "Waiting for initial application build to complete..."
+ # Wait for build completion by checking for expected build artifacts
+ max_attempts=60 # 10 minutes total
+ attempt=0
+ while [ $attempt -lt $max_attempts ]; do
+ if docker exec restarters bash -c "[ -f public/mix-manifest.json ] || [ -f public/css/app.css ]"; then
+ echo "✓ Application build complete - build artifacts found"
+ break
+ fi
+ echo " Build not complete, waiting... (attempt $((attempt + 1))/$max_attempts)"
+ sleep 10
+ attempt=$((attempt + 1))
+ done
+ if [ $attempt -eq $max_attempts ]; then
+ echo "⚠️ Build artifacts not found after $max_attempts attempts, proceeding anyway"
+ fi
- # Coveralls is pernickety about the location it uploads from existing.
- - run: mkdir build; mkdir build/logs; php vendor/bin/php-coveralls -v -x tests/clover.xml
+ # Wait for services to be ready
+ - run:
+ name: Wait for services
+ command: |
+ task docker:wait-for-services-all
- # Run the Jest tests.
- - run: npm run jest
+ # Setup database and application
+ - run:
+ name: Setup application
+ command: |
+ # Grant timezone access - run directly on MySQL container
+ docker exec restarters_db mysql -u root -ps3cr3t -e "GRANT SELECT ON mysql.time_zone_name TO 'restarters'@'%';"
+
+ # Setup additional configuration needed for CI (most setup already done by docker_run.sh)
+ # Set MySQL function creators using session variable (compatible with MySQL 5.7)
+ docker exec restarters_db mysql -u root -ps3cr3t -e "SET GLOBAL log_bin_trust_function_creators = 1;"
+
+ # Generate additional Laravel artifacts for testing
+ docker exec restarters php artisan l5-swagger:generate
+
+ # Setup Discourse API
+ - run:
+ name: Setup Discourse
+ command: |
+ # Add API key to Discourse - run directly on PostgreSQL container
+ docker exec postgresql psql -U postgres -c "INSERT INTO api_keys (id, user_id, created_by_id, created_at, updated_at, allowed_ips, hidden, last_used_at, revoked_at, description, key_hash, truncated_key) VALUES (1, NULL, 1, '2021-10-25 13:56:20.033338', '2021-10-25 13:56:20.033338', NULL, false, NULL, NULL, 'Restarters', 'd89e9dfacfb611fbaf004807648187ce7ed474df44dcb0ada230fab5c8dd6a5b', '9fd7');" bitnami_discourse
+
+ # Configure Discourse settings
+ docker exec restarters php artisan discourse:setting personal_message_enabled_groups 10
- # Run the Playwright tests.
- #
- # Zap groups set up by the UT; this can confuse Playwright tests.
- - run: mysql --host="127.0.0.1" -u root -ps3cr3t -e "use restarters_db;SET foreign_key_checks=0;DELETE FROM \`groups\` WHERE location IS NULL;SET foreign_key_checks=1;"
- - run: php artisan cache:clear
- # Ignore the return code from the tinker; the user might exist from the phpunit tests. If it doesn't and
- # the create fails, the tests will fail too.
- - run: echo "App\User::create(['name'=>'Jane Bloggs','email'=>'jane@bloggs.net','password'=>Hash::make('passw0rd'),'role'=>2,'consent_past_data'=>'2021-01-01','consent_future_data'=>'2021-01-01','consent_gdpr'=>'2021-01-01']);" | php artisan tinker || true
- # Build the web app.
- - run: export NODE_OPTIONS=--max-old-space-size=8192; npm rebuild node-sass; npm run prod
- - run: npx playwright install
- # Set up a real nginx/fpm server. This improves the speed of the tests enormously as artisan serve uses the
- # single-threaded php built-in web server.
- - run: sudo apt-get install nginx php8.1-fpm php8.1-mysql php8.1-pdo
- - run: sudo cp /home/circleci/project/.circleci/nginx.conf /etc/nginx/sites-available/default
- - run: sudo sed -i 's/www-data/circleci/g' /etc/php/8.1/fpm/pool.d/www.conf
- - run: sudo /etc/init.d/php8.1-fpm start
- - run: sudo sed -i 's/user .*;/user circleci;/g' /etc/nginx/nginx.conf
- - run: sudo /etc/init.d/nginx start
- # We're running against localhost.
- - run: sudo sed -i 's/APP_URL=.*$/APP_URL=http:\/\/localhost/g' /home/circleci/project/.env
- # Fix up Google key from CircleCI config.
- - run: cp .env /tmp/.env
- - run: echo "" >> /tmp/.env
- - run: echo GOOGLE_API_CONSOLE_KEY=$GOOGLE_API_CONSOLE_KEY >> /tmp/.env
- - run: echo MAPBOX_TOKEN=$MAPBOX_TOKEN >> /tmp/.env
- - run: sudo cp /tmp/.env /home/circleci/project/.env
- # Comment out throttle:api in App/Http/Kernel.php otherwise it kicks in during Playwright tests.
- - run: sudo sed -i 's/.throttle:api.,//g' /home/circleci/project/app/Http/Kernel.php
+ # Run PHPUnit tests
+ - run:
+ name: Run PHPUnit tests
+ command: |
+ # Create test results directory on host
+ mkdir -p /tmp/test-results/phpunit
+ # Run PHPUnit with JUnit XML output - using direct docker exec to avoid quote issues
+ docker exec restarters bash -c "export XDEBUG_MODE=coverage; ./vendor/bin/phpunit -d memory_limit=1024M --bootstrap vendor/autoload.php --coverage-clover tests/clover.xml --log-junit /tmp/phpunit-results.xml --configuration ./phpunit.xml --teamcity"
+ # Copy test results to host
+ docker cp restarters:/tmp/phpunit-results.xml /tmp/test-results/phpunit/results.xml
+ no_output_timeout: 45m
+
+ # Upload coverage
+ - run:
+ name: Upload coverage
+ command: |
+ docker exec -e COVERALLS_REPO_TOKEN="$COVERALLS_REPO_TOKEN" restarters bash -c "mkdir -p build/logs && php vendor/bin/php-coveralls -v -x tests/clover.xml"
- # Determine which port to use for Playwright tests
+ # Run Jest tests
- run:
- name: Check if port 8000 is available
+ name: Run Jest tests
command: |
- if nc -z localhost 8000; then
- echo "export PLAYWRIGHT_BASE_URL=http://localhost:8000" >> $BASH_ENV
- echo "Port 8000 is open, using localhost:8000"
- else
- echo "export PLAYWRIGHT_BASE_URL=http://localhost" >> $BASH_ENV
- echo "Port 8000 not available, using localhost"
- fi
-
- # Now run the tests.
+ # Create test results directory for Jest
+ mkdir -p /tmp/test-results/jest
+ # Run Jest with JUnit output
+ docker exec restarters bash -c "npm i jest-junit; JEST_JUNIT_OUTPUT_DIR=/tmp/test-results npm run jest -- --testResultsProcessor=jest-junit"
+ # Copy test results to host if they exist
+ docker cp restarters:/tmp/test-results/junit.xml /tmp/test-results/jest/junit.xml || echo "Jest results not found, skipping"
+
+ # Prepare for Playwright tests
- run:
- name: Playwright Tests
+ name: Prepare for Playwright tests
+ command: |
+ docker exec restarters php artisan cache:clear
+
+ # Delete all devices from database to ensure clean state for Playwright tests
+ docker exec restarters bash -c "echo \"DB::statement('SET foreign_key_checks=0'); App\\\\Device::truncate(); DB::statement('SET foreign_key_checks=1');\" | php artisan tinker" || true
+
+ # Create test user
+ docker exec restarters bash -c "echo \"App\\\\User::firstOrCreate(['email'=>'jane@bloggs.net'], ['name'=>'Jane Bloggs','password'=>Hash::make('passw0rd'),'role'=>2,'consent_past_data'=>'2021-01-01','consent_future_data'=>'2021-01-01','consent_gdpr'=>'2021-01-01']);\" | php artisan tinker" || true
+
+ # Build assets
+ docker exec restarters bash -c "export NODE_OPTIONS=--max-old-space-size=8192; npm rebuild node-sass; npm run prod"
+
+ # Disable API throttling for tests
+ docker exec restarters bash -c "sed -i 's/.throttle:api.,//g' /var/www/app/Http/Kernel.php"
+
+ # Run main Playwright tests (excluding autocomplete)
+ - run:
+ name: Run main Playwright tests
+ command: |
+ mkdir -p /tmp/test-results/playwright
+ # Create Playwright results directory inside container and run tests
+ # Use -t flag for pseudo-TTY to ensure unbuffered output
+ docker exec -t restarters bash -c "
+ export PLAYWRIGHT_TEST=true
+ export PLAYWRIGHT_DEBUG=true
+ export PWTEST_SKIP_TEST_OUTPUT=0
+ export DEBUG=playwright
+ export PLAYWRIGHT_BASE_URL=http://restarters_nginx
+ export PW_TEST_HTML_REPORT_OPEN=never
+ export FORCE_COLOR=1
+ # Unbuffer output and add periodic keepalive
+ stdbuf -oL -eL npx playwright test --reporter=html | while IFS= read -r line; do
+ echo \"\$line\"
+ echo '[CircleCI] Playwright test running...' >&2
+ done
+ "
no_output_timeout: 10m
+
+ # Setup test data for autocomplete test
+ - run:
+ name: Setup autocomplete test data
command: |
- source $BASH_ENV
- # Enable debug logging for Playwright tests in CI
- export PLAYWRIGHT_DEBUG=true
- export DEBUG=playwright
- npx playwright test --reporter=list
-
- # Store test artifacts (screenshots, videos, traces, test reports)
+ echo "Setting up test data for autocomplete test..."
+ # Run tinker script and check for success message instead of exit code
+ docker exec restarters bash -c "
+ php artisan tinker setup-autocomplete-test-data.php > /tmp/setup-output.log 2>&1
+ if grep -q 'Test data setup complete!' /tmp/setup-output.log; then
+ echo 'Setup completed successfully!'
+ cat /tmp/setup-output.log
+ exit 0
+ else
+ echo 'Setup failed!'
+ cat /tmp/setup-output.log
+ exit 1
+ fi
+ "
+ no_output_timeout: 5m
+
+ # Run autocomplete Playwright test separately because it needs special setup.
+ - run:
+ name: Run autocomplete Playwright test
+ command: |
+ mkdir -p /tmp/test-results/playwright-autocomplete
+ # Run the autocomplete test with its own configuration
+ # Use -t flag for pseudo-TTY and add background keepalive process
+ docker exec -t restarters bash -c "
+ export PLAYWRIGHT_TEST=true
+ export PLAYWRIGHT_DEBUG=true
+ export PWTEST_SKIP_TEST_OUTPUT=0
+ export DEBUG=playwright
+ export PLAYWRIGHT_BASE_URL=http://restarters_nginx
+ export PW_TEST_HTML_REPORT_OPEN=never
+ export FORCE_COLOR=1
+
+ # Start background keepalive process
+ (while true; do sleep 30; echo '[CircleCI] Autocomplete test still running...'; done) &
+ KEEPALIVE_PID=\$!
+
+ # Run the test with unbuffered output
+ stdbuf -oL -eL npx playwright test --config=playwright.autocomplete.config.js --reporter=html
+
+ # Clean up keepalive process
+ kill \$KEEPALIVE_PID 2>/dev/null || true
+ "
+ no_output_timeout: 15m
+
+ # Copy test results and artifacts
+ - run:
+ name: Copy Playwright artifacts
+ command: |
+ # List what's available in the container
+ docker exec restarters bash -c "ls -la /tmp/test-results/* || echo 'No playwright directories found'"
+ # Copy test results and artifacts to host if they exist
+ docker cp restarters:/tmp/test-results /tmp/test-results/playwright/ || echo "Playwright HTML report not found"
+ when: always
+
+ # Store artifacts
- store_artifacts:
path: /tmp/test-results
destination: playwright-test-results
- # Store test results for CircleCI UI
- store_test_results:
path: /tmp/test-results
-
-
diff --git a/CLAUDE.md b/CLAUDE.md
index 6d24af4689..534f578c5f 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -147,4 +147,7 @@ npm test
- Only translate fr and fr-BE
## Development Warnings
-- Don't try to test changes when you're running on Windows.
\ No newline at end of file
+- Don't try to test changes when you're running on Windows.
+
+## Workflow Guidelines
+- When you create files, add them to git
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 886d43baa8..9f0c71cb1a 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -9,11 +9,19 @@ RUN apt-get update && \
unzip \
npm \
vim \
+ netcat-openbsd \
default-mysql-client \
postgresql-client && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
+# Install Playwright system dependencies
+# We need to install @playwright/test first to get the install-deps command
+RUN npm install -g @playwright/test && \
+ npm install -g jest-junit && \
+ npx playwright install-deps && \
+ npm uninstall -g @playwright/test
+
ADD --chmod=0755 https://github.com/mlocati/docker-php-extension-installer/releases/download/2.7.31/install-php-extensions /usr/local/bin/
RUN install-php-extensions \
@@ -23,6 +31,9 @@ RUN install-php-extensions \
xmlrpc \
xdebug \
intl \
+ exif \
+ pcntl \
+ curl \
gd
# Install composer. Don't run composer install yet - see docker_run.sh
diff --git a/Taskfile.yml b/Taskfile.yml
index fb7754c566..71141eaa4e 100644
--- a/Taskfile.yml
+++ b/Taskfile.yml
@@ -8,9 +8,19 @@ dotenv:
env:
UID:
- sh: id -u
+ sh: |
+ if command -v id >/dev/null 2>&1; then
+ id -u
+ else
+ echo "1000"
+ fi
GID:
- sh: id -g
+ sh: |
+ if command -v id >/dev/null 2>&1; then
+ id -g
+ else
+ echo "1000"
+ fi
vars:
DOCKER_CMD:
@@ -159,4 +169,95 @@ tasks:
task docker:run:artisan -- migrate
cmds:
- - docker exec -it restarters php artisan "{{ .CLI_ARGS }}"
\ No newline at end of file
+ - docker exec -it restarters php artisan "{{ .CLI_ARGS }}"
+
+ docker:wait-for-services-*:
+ desc: Wait for Docker services to be ready and responding for a given profile (Usage - task docker:wait-for-services-[core|debug|discourse|all])
+ summary: |
+ Wait for Docker services to be ready by checking their health endpoints.
+ This task will check that services are listening on their ports and returning
+ plausible responses before proceeding, but only for the services in the specified profile.
+
+ For just the core services, use:
+ task docker:wait-for-services-core
+
+ To include debug tools (phpMyAdmin, Mailhog), use:
+ task docker:wait-for-services-debug
+
+ To include Discourse services, use:
+ task docker:wait-for-services-discourse
+
+ To wait for all services, use:
+ task docker:wait-for-services-all
+
+ The task checks:
+ - Core profile: MySQL database, Restarters web app
+ - Debug profile: Core + phpMyAdmin, Mailhog
+ - Discourse profile: Core + Discourse, PostgreSQL, Redis, Sidekiq
+ - All profile: All services
+
+ requires: *PROFILE_REQUIRES
+
+ vars: *PROFILE_VARS
+
+ cmds:
+ - |
+ # Cross-platform sleep function
+ cross_platform_sleep() {
+ if command -v sleep >/dev/null 2>&1; then
+ sleep "$1"
+ elif command -v powershell >/dev/null 2>&1; then
+ powershell -Command "Start-Sleep -Seconds $1"
+ else
+ # Fallback using ping (works on most systems)
+ ping -n $(($1 + 1)) 127.0.0.1 >/dev/null 2>&1 || ping -c $1 127.0.0.1 >/dev/null 2>&1
+ fi
+ }
+
+ # Generic wait function that takes: service_name, check_command, max_attempts, sleep_interval
+ wait_for_service() {
+ local service_name="$1"
+ local check_command="$2"
+ local max_attempts="$3"
+ local sleep_interval="$4"
+
+ echo "Waiting for $service_name..."
+ local attempt=0
+ while [ $attempt -lt $max_attempts ]; do
+ if eval "$check_command" >/dev/null 2>&1; then
+ echo "✓ $service_name is ready"
+ return 0
+ fi
+ echo " $service_name not ready, waiting... (attempt $((attempt + 1))/$max_attempts)"
+ cross_platform_sleep "$sleep_interval"
+ attempt=$((attempt + 1))
+ done
+ echo "❌ $service_name failed to start after $max_attempts attempts"
+ exit 1
+ }
+
+ echo "Waiting for services in profile: {{.PROFILE}}"
+ echo ""
+
+ # Wait for core services (always needed)
+ wait_for_service "MySQL database" "docker exec restarters_db mysqladmin ping -h localhost -u root -ps3cr3t --silent" 60 5
+ wait_for_service "Restarters web application" "curl -f -s http://localhost:8001" 120 5
+
+ # Wait for debug services (if in debug or all profile)
+ {{- if or (eq .PROFILE "debug") (eq .PROFILE "all") }}
+ wait_for_service "phpMyAdmin" "curl -f -s http://localhost:8002" 60 5
+ wait_for_service "Mailhog" "curl -f -s http://localhost:8025" 60 5
+ {{- else }}
+ echo "✓ phpMyAdmin not in profile {{.PROFILE}}, skipping"
+ echo "✓ Mailhog not in profile {{.PROFILE}}, skipping"
+ {{- end }}
+
+ # Wait for discourse services (if in discourse or all profile)
+ {{- if or (eq .PROFILE "discourse") (eq .PROFILE "all") }}
+ wait_for_service "PostgreSQL" "docker exec postgresql pg_isready -U postgres" 60 5
+ wait_for_service "Discourse" "curl -f -s http://localhost:8003" 120 10
+ {{- else }}
+ echo "✓ PostgreSQL not in profile {{.PROFILE}}, skipping"
+ echo "✓ Discourse not in profile {{.PROFILE}}, skipping"
+ {{- end }}
+ - echo "🎉 All services in profile {{.PROFILE}} are ready!"
\ No newline at end of file
diff --git a/app/Device.php b/app/Device.php
index 8d71743555..9396997e6d 100644
--- a/app/Device.php
+++ b/app/Device.php
@@ -50,6 +50,14 @@ class Device extends Model implements Auditable
use \OwenIt\Auditing\Auditable;
protected $table = 'devices';
+
+ /**
+ * Check if we're running in CircleCI environment
+ */
+ private static function isCircleCI()
+ {
+ return env('CIRCLECI', false) || env('CI', false);
+ }
protected $primaryKey = 'iddevices';
/**
* The attributes that are mass assignable.
@@ -269,9 +277,13 @@ public function eCo2Diverted($emissionRatio, $displacementFactor)
if ($this->category == env('MISC_CATEGORY_ID_POWERED') && $this->estimate > 0) {
$footprint = $this->estimate * $emissionRatio;
} else {
- $footprint = \Cache::remember('category-' . $this->category, 15, function() {
- return $this->deviceCategory;
- })->footprint;
+ if (self::isCircleCI()) {
+ $footprint = $this->deviceCategory->footprint;
+ } else {
+ $footprint = \Cache::remember('category-' . $this->category, 15, function() {
+ return $this->deviceCategory;
+ })->footprint;
+ }
}
}
@@ -305,17 +317,25 @@ public function eWasteDiverted()
{
$ewasteDiverted = 0;
- $powered = \Cache::remember('category-powered-' . $this->category, 15, function() {
- return $this->deviceCategory->powered;
- });
+ if (self::isCircleCI()) {
+ $powered = $this->deviceCategory->powered;
+ } else {
+ $powered = \Cache::remember('category-powered-' . $this->category, 15, function() {
+ return $this->deviceCategory->powered;
+ });
+ }
if ($this->isFixed() && $powered) {
if ($this->category == env('MISC_CATEGORY_ID_POWERED') && $this->estimate > 0) {
$ewasteDiverted = $this->estimate;
} else {
- $category = \Cache::remember('category-' . $this->category, 15, function() {
- return $this->deviceCategory;
- });
+ if (self::isCircleCI()) {
+ $category = $this->deviceCategory;
+ } else {
+ $category = \Cache::remember('category-' . $this->category, 15, function() {
+ return $this->deviceCategory;
+ });
+ }
$ewasteDiverted = $category->weight;
}
@@ -454,7 +474,7 @@ public static function getItemTypes()
// used by the item types.
//
// This is slow and the results don't change much, so we use a cache.
- if (Cache::has('item_types')) {
+ if (!self::isCircleCI() && Cache::has('item_types')) {
$types = Cache::get('item_types');
} else {
$types = DB::select(DB::raw("
@@ -485,7 +505,9 @@ public static function getItemTypes()
AND LENGTH(t.item_type) > 0
GROUP BY t.item_type, t.powered;
"));
- \Cache::put('item_types', $types, 24 * 3600);
+ if (!self::isCircleCI()) {
+ \Cache::put('item_types', $types, 24 * 3600);
+ }
}
return $types;
diff --git a/database/migrations/2020_10_27_101759_repair_status_to_string_data.php b/database/migrations/2020_10_27_101759_repair_status_to_string_data.php
index f3833f5a98..c3721a8f0c 100644
--- a/database/migrations/2020_10_27_101759_repair_status_to_string_data.php
+++ b/database/migrations/2020_10_27_101759_repair_status_to_string_data.php
@@ -33,24 +33,6 @@ public function up()
DB::table('devices')
->where('repair_status', 0)
->update(['repair_status_str' => 'Unknown']);
- DB::unprepared("CREATE TRIGGER `repair_status_str_up`
-BEFORE UPDATE ON `devices` FOR EACH ROW
-SET NEW.repair_status_str = CASE
- WHEN NEW.repair_status = 1 THEN 'Fixed'
- WHEN NEW.repair_status = 2 THEN 'Repairable'
- WHEN NEW.repair_status = 3 THEN 'End of life'
- ELSE 'Unknown'
-END;
-");
- DB::unprepared("CREATE TRIGGER `repair_status_str_in`
-BEFORE INSERT ON `devices` FOR EACH ROW
-SET NEW.repair_status_str = CASE
- WHEN NEW.repair_status = 1 THEN 'Fixed'
- WHEN NEW.repair_status = 2 THEN 'Repairable'
- WHEN NEW.repair_status = 3 THEN 'End of life'
- ELSE 'Unknown'
-END;
-");
}
/**
@@ -60,8 +42,6 @@ public function up()
*/
public function down()
{
- DB::unprepared('DROP TRIGGER IF EXISTS `repair_status_str_in`');
- DB::unprepared('DROP TRIGGER IF EXISTS `repair_status_str_up`');
if (Schema::hasColumn('devices', 'repair_status_str')) {
Schema::table('devices', function (Blueprint $table) {
$table->dropColumn('repair_status_str');
diff --git a/database/migrations/2025_01_08_000001_drop_repair_status_triggers.php b/database/migrations/2025_01_08_000001_drop_repair_status_triggers.php
new file mode 100644
index 0000000000..3edf06a2e0
--- /dev/null
+++ b/database/migrations/2025_01_08_000001_drop_repair_status_triggers.php
@@ -0,0 +1,28 @@
+/dev/null || true
+# Generic wait function that takes: service_name, check_command, max_attempts, sleep_interval
+wait_for_service() {
+ local service_name="$1"
+ local check_command="$2"
+ local max_attempts="$3"
+ local sleep_interval="$4"
+
+ echo "Waiting for $service_name..."
+ local attempt=0
+ while [ $attempt -lt $max_attempts ]; do
+ if eval "$check_command" >/dev/null 2>&1; then
+ echo "✓ $service_name is ready"
+ return 0
fi
-done
+ echo " $service_name not ready, waiting... (attempt $((attempt + 1))/$max_attempts)"
+ sleep "$sleep_interval"
+ attempt=$((attempt + 1))
+ done
+ echo "❌ $service_name failed to start after $max_attempts attempts"
+ exit 1
+}
# Ensure storage directories exist and have correct permissions
mkdir -p storage/framework/cache/data
@@ -47,19 +59,37 @@ mkdir -p storage/framework/sessions
mkdir -p storage/framework/views
mkdir -p storage/logs
mkdir -p bootstrap/cache
+mkdir -p uploads
+mkdir -p public/uploads
+
+# Only change ownership of directories that need it, excluding .git and other system files
+# This prevents permission errors on files owned by the host system
+echo "Fixing file permissions with ${USER_ID}:${GROUP_ID}"
+for dir in storage bootstrap/cache vendor node_modules uploads public/uploads; do
+ if [ -d "$dir" ]; then
+ chown -R ${USER_ID}:${GROUP_ID} "$dir" 2>/dev/null || true
+ fi
+done
+
+# Wait for MySQL database to be ready before running migrations
+wait_for_service "MySQL database" "nc -z restarters_db 3306" 60 5
php artisan migrate
npm install --legacy-peer-deps
npm rebuild node-sass
php artisan lang:js --no-lib resources/js/translations.js
+# Install Playwright for testing (system deps already in Dockerfile)
+npm install -D @playwright/test
+npx playwright install
+
npm run watch-poll&
php artisan key:generate
php artisan cache:clear
php artisan config:clear
# Ensure we have the admin user
-echo "User::create(['name'=>'Jane Bloggs','email'=>'jane@bloggs.net','password'=>Hash::make('passw0rd'),'role'=>2,'consent_past_data'=>'2021-01-01','consent_future_data'=>'2021-01-01','consent_gdpr'=>'2021-01-01']);" | php artisan tinker
+echo "User::firstOrCreate(['email'=>'jane@bloggs.net'], ['name'=>'Jane Bloggs','password'=>Hash::make('passw0rd'),'role'=>2,'consent_past_data'=>'2021-01-01','consent_future_data'=>'2021-01-01','consent_gdpr'=>'2021-01-01']);" | php artisan tinker
php-fpm
diff --git a/docs/local-development.md b/docs/local-development.md
index bc3072dc0c..b654958a70 100644
--- a/docs/local-development.md
+++ b/docs/local-development.md
@@ -96,6 +96,26 @@ task docker:up-discourse
task docker:up-all
```
+**Waiting for Services to be Ready**
+
+After starting services, you can wait for them to be fully ready and responding:
+
+```bash
+# Wait for core services to be ready
+task docker:wait-for-services-core
+
+# Wait for debug services to be ready
+task docker:wait-for-services-debug
+
+# Wait for Discourse services to be ready
+task docker:wait-for-services-discourse
+
+# Wait for all services to be ready
+task docker:wait-for-services-all
+```
+
+The wait commands will check that services are listening on their expected ports and return proper responses.
+
### 4. Initial Setup
The core application container will automatically:
@@ -143,6 +163,13 @@ task docker:run:bash -- [command]
task docker:run:artisan -- [command]
```
+### Checking Service Health
+
+```bash
+# View container logs if services aren't starting properly
+task docker:logs
+```
+
### Stopping the Environment
```bash
diff --git a/package-lock.json b/package-lock.json
index 5b4aa07174..863030dcdd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -58,7 +58,7 @@
"devDependencies": {
"@babel/core": "^7.22.8",
"@babel/preset-env": "^7.22.7",
- "@playwright/test": "^1.14.1",
+ "@playwright/test": "^1.53.2",
"@vue/test-utils": "^1.3.6",
"@vue/vue2-jest": "^28.1.0",
"axios": "^0.28",
@@ -102,6 +102,27 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@csstools/css-calc": "^2.1.3",
+ "@csstools/css-color-parser": "^3.0.9",
+ "@csstools/css-parser-algorithms": "^3.0.4",
+ "@csstools/css-tokenizer": "^3.0.3",
+ "lru-cache": "^10.4.3"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@@ -1930,6 +1951,121 @@
"node": ">=0.1.90"
}
},
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
+ "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.0.10",
+ "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
+ "integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "peer": true,
+ "dependencies": {
+ "@csstools/color-helpers": "^5.0.2",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@discoveryjs/json-ext": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz",
@@ -3132,31 +3268,28 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.23.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.23.0.tgz",
- "integrity": "sha512-RPWI8AHBVBiDyfTuxi1BhOaY3yaVy3S5ZsGKkSGGeWpZtSgN4SerInCYvgh9+EunIAK4RJQo+bzupbAn5pVqHQ==",
+ "version": "1.53.2",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.2.tgz",
+ "integrity": "sha512-tEB2U5z74ebBeyfGNZ3Jfg29AnW+5HlWhvHtb/Mqco9pFdZU1ZLNdVb2UtB5CvmiilNr2ZfVH/qMmAROG/XTzw==",
"dev": true,
"dependencies": {
- "@types/node": "*",
- "playwright-core": "1.23.0"
+ "playwright": "1.53.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
- "node": ">=14"
+ "node": ">=18"
}
},
- "node_modules/@playwright/test/node_modules/playwright-core": {
- "version": "1.23.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.23.0.tgz",
- "integrity": "sha512-Zzhyr5RZGoJ1ek2sgfJCt2076kdOg8hnNwFBqAYeLySiutXyxSQk93vZ5gbnFiWV1sHvueCcwla9n35acUTxtA==",
- "dev": true,
- "bin": {
- "playwright": "cli.js"
- },
- "engines": {
- "node": ">=14"
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "peer": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
}
},
"node_modules/@sentry/browser": {
@@ -3445,6 +3578,12 @@
"@types/range-parser": "*"
}
},
+ "node_modules/@types/geojson": {
+ "version": "7946.0.16",
+ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
+ "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
+ "peer": true
+ },
"node_modules/@types/glob": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
@@ -3560,6 +3699,15 @@
"integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==",
"dev": true
},
+ "node_modules/@types/leaflet": {
+ "version": "1.9.19",
+ "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.19.tgz",
+ "integrity": "sha512-pB+n2daHcZPF2FDaWa+6B0a0mSDf4dPU35y5iTXsx7x/PzzshiX5atYiS1jlBn43X7XvM8AP+AB26lnSk0J4GA==",
+ "peer": true,
+ "dependencies": {
+ "@types/geojson": "*"
+ }
+ },
"node_modules/@types/mime": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz",
@@ -6642,9 +6790,9 @@
}
},
"node_modules/decimal.js": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
- "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==",
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"dev": true
},
"node_modules/dedent": {
@@ -12267,8 +12415,7 @@
"node_modules/jquery": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
- "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==",
- "dev": true
+ "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
},
"node_modules/jquery-bootgrid": {
"version": "1.3.1",
@@ -12360,6 +12507,46 @@
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "cssstyle": "^4.2.1",
+ "data-urls": "^5.0.0",
+ "decimal.js": "^10.5.0",
+ "html-encoding-sniffer": "^4.0.0",
+ "http-proxy-agent": "^7.0.2",
+ "https-proxy-agent": "^7.0.6",
+ "is-potential-custom-element-name": "^1.0.1",
+ "nwsapi": "^2.2.16",
+ "parse5": "^7.2.1",
+ "rrweb-cssom": "^0.8.0",
+ "saxes": "^6.0.0",
+ "symbol-tree": "^3.2.4",
+ "tough-cookie": "^5.1.1",
+ "w3c-xmlserializer": "^5.0.0",
+ "webidl-conversions": "^7.0.0",
+ "whatwg-encoding": "^3.1.1",
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.1.1",
+ "ws": "^8.18.0",
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
"node_modules/jsdom-global": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/jsdom-global/-/jsdom-global-3.0.2.tgz",
@@ -12369,6 +12556,203 @@
"jsdom": ">=10.0.0"
}
},
+ "node_modules/jsdom/node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/jsdom/node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/jsdom/node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/jsdom/node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/jsdom/node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/jsdom/node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/jsdom/node_modules/ws": {
+ "version": "8.18.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsdom/node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/jsesc": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
@@ -16128,9 +16512,9 @@
}
},
"node_modules/nwsapi": {
- "version": "2.2.7",
- "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.7.tgz",
- "integrity": "sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==",
+ "version": "2.2.20",
+ "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
+ "integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
"dev": true
},
"node_modules/nyc": {
@@ -16606,6 +16990,32 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -16770,31 +17180,33 @@
}
},
"node_modules/playwright": {
- "version": "1.23.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.23.0.tgz",
- "integrity": "sha512-rfZfuLueBPGV3UdEqPQxS8Uw7TgVMATWSPb3O0oV8SZBmVAhOndkOU9MPP8dxJoQI68r94yevuObPr14PhW9Xg==",
+ "version": "1.53.2",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.2.tgz",
+ "integrity": "sha512-6K/qQxVFuVQhRQhFsVZ9fGeatxirtrpPgxzBYWyZLEXJzqYwuL4fuNmfOfD5et1tJE4GScKyPNeLhZeRwuTU3A==",
"dev": true,
- "hasInstallScript": true,
"dependencies": {
- "playwright-core": "1.23.0"
+ "playwright-core": "1.53.2"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
- "node": ">=14"
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
}
},
- "node_modules/playwright/node_modules/playwright-core": {
- "version": "1.23.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.23.0.tgz",
- "integrity": "sha512-Zzhyr5RZGoJ1ek2sgfJCt2076kdOg8hnNwFBqAYeLySiutXyxSQk93vZ5gbnFiWV1sHvueCcwla9n35acUTxtA==",
+ "node_modules/playwright-core": {
+ "version": "1.53.2",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.2.tgz",
+ "integrity": "sha512-ox/OytMy+2w1jcYEYlOo1Hhp8hZkLCximMTUTMBXjGUA1KoFfiSZ+DU+3a739jsPY0yoKH2TFy9S2fsJas8yAw==",
"dev": true,
"bin": {
- "playwright": "cli.js"
+ "playwright-core": "cli.js"
},
"engines": {
- "node": ">=14"
+ "node": ">=18"
}
},
"node_modules/popper.js": {
@@ -17527,9 +17939,9 @@
"dev": true
},
"node_modules/punycode": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
- "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"engines": {
"node": ">=6"
}
@@ -18010,6 +18422,13 @@
"inherits": "^2.0.1"
}
},
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -18137,6 +18556,19 @@
"integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
"dev": true
},
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
"node_modules/schema-utils": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz",
@@ -19338,6 +19770,26 @@
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "peer": true
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@@ -19966,6 +20418,29 @@
"browser-process-hrtime": "^1.0.0"
}
},
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/walker": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz",
diff --git a/package.json b/package.json
index 45285b2161..f3c5610269 100644
--- a/package.json
+++ b/package.json
@@ -9,12 +9,13 @@
"prod": "npm run production",
"production": "mix --production",
"test": "playwright test",
+ "test:autocomplete": "playwright test --config=playwright.autocomplete.config.js",
"jest": "jest --verbose"
},
"devDependencies": {
"@babel/core": "^7.22.8",
"@babel/preset-env": "^7.22.7",
- "@playwright/test": "^1.14.1",
+ "@playwright/test": "^1.53.2",
"@vue/test-utils": "^1.3.6",
"@vue/vue2-jest": "^28.1.0",
"axios": "^0.28",
diff --git a/phpunit.xml b/phpunit.xml
index aae78708d6..f971b45c2f 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -17,6 +17,7 @@
./tests/Unit
./tests/Unit/NotificationsTest.php
./tests/Unit/CharsetTest.php
+ ./tests/Unit/Events/EventStateTest.php
@@ -27,7 +28,8 @@
-
+
+
diff --git a/playwright.autocomplete.config.js b/playwright.autocomplete.config.js
new file mode 100644
index 0000000000..f721d41d41
--- /dev/null
+++ b/playwright.autocomplete.config.js
@@ -0,0 +1,49 @@
+// playwright.autocomplete.config.js
+// Configuration specifically for the autocomplete test
+// @ts-check
+const { devices } = require('@playwright/test');
+
+/** @type {import('@playwright/test').PlaywrightTestConfig} */
+const config = {
+ // Generate trace if a test fails; can be viewed using something like:
+ // npx playwright show-trace test-results/group-Can-create-group-Desktop-Chromium-retry1/trace.zip
+ // Only use 1 worker, otherwise we hit CSRF issues.
+ workers: 1,
+
+ // Only run the autocomplete test
+ grep: /Automatic category suggestion from item type/,
+
+ use: {
+ trace: 'on',
+ // Take screenshot on failure for debugging
+ screenshot: 'on',
+ // Also capture video on failure for additional context
+ video: 'on',
+ // Configurable timeout for waitForURL operations
+ navigationTimeout: 30000,
+ // Add header to identify Playwright requests
+ extraHTTPHeaders: {
+ 'X-Playwright-Test': 'true'
+ },
+ },
+ projects: [
+ {
+ name: 'Desktop Chromium',
+ use: {
+ browserName: 'chromium',
+ baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:8000'
+ },
+ },
+ ],
+ testDir: 'tests/Integration',
+ outputDir: '/tmp/test-results',
+
+ // Flakiness
+ // Timeout per test - needs to be less than 10 minutes to avoid Circle CI timeout kicking in.
+ timeout: 10 * 60 * 1000, // Increased timeout for the slow autocomplete test
+ navigationTimeout: 2 * 60 * 1000,
+ actionTimeout: 2 * 60 * 1000,
+ retries: 0
+};
+
+module.exports = config;
\ No newline at end of file
diff --git a/playwright.config.js b/playwright.config.js
index 1cc292bc15..f06565249f 100644
--- a/playwright.config.js
+++ b/playwright.config.js
@@ -9,6 +9,8 @@ const config = {
// Only use 1 worker, otherwise we hit CSRF issues.
workers: 1,
+ // Exclude the slow autocomplete test from the main test run
+ grep: /^(?!.*Automatic category suggestion from item type)/,
use: {
trace: 'on',
diff --git a/resources/js/components/DeviceWeight.test.js b/resources/js/components/DeviceWeight.test.js
index 2f71b4dac0..abb1148726 100644
--- a/resources/js/components/DeviceWeight.test.js
+++ b/resources/js/components/DeviceWeight.test.js
@@ -11,6 +11,9 @@ import DeviceWeight from './DeviceWeight.vue'
test('DeviceWeight', () => {
const wrapper = mount(DeviceWeight, {
mixins: [LangMixin],
+ propsData: {
+ required: false
+ }
})
expect(wrapper.html()).toContain('impact calculation')
diff --git a/setup-autocomplete-test-data.php b/setup-autocomplete-test-data.php
new file mode 100644
index 0000000000..a534597fa1
--- /dev/null
+++ b/setup-autocomplete-test-data.php
@@ -0,0 +1,153 @@
+first();
+if (!$testUser) {
+ echo "Creating test user...\n";
+ $testUser = User::create([
+ 'name' => 'Test User',
+ 'email' => 'test@restarters.test',
+ 'password' => bcrypt('password'),
+ 'role' => 2, // Admin role
+ 'invites' => 1,
+ 'country' => 'GB',
+ 'city' => 'London',
+ ]);
+}
+
+// Find or create a test group
+$testGroup = Group::where('name', 'Autocomplete Test Group')->first();
+if (!$testGroup) {
+ echo "Creating test group...\n";
+ $testGroup = Group::create([
+ 'name' => 'Autocomplete Test Group',
+ 'location' => 'London, UK',
+ 'latitude' => 51.5074,
+ 'longitude' => -0.1278,
+ 'free_text' => 'Test group for autocomplete functionality',
+ 'approved' => true,
+ ]);
+}
+
+// Find or create a test event
+$testEvent = Party::where('venue', 'Autocomplete Test Venue')->first();
+if (!$testEvent) {
+ echo "Creating test event...\n";
+ $testEvent = Party::create([
+ 'venue' => 'Autocomplete Test Venue',
+ 'location' => 'London, UK',
+ 'latitude' => 51.5074,
+ 'longitude' => -0.1278,
+ 'event_date' => now()->subDays(1),
+ 'start' => '10:00',
+ 'end' => '16:00',
+ 'group' => $testGroup->idgroups,
+ 'free_text' => 'Test event for autocomplete functionality',
+ 'approved' => true,
+ 'wordpress_post_id' => 1,
+ ]);
+}
+
+// Get categories for mapping
+$categories = Category::all()->keyBy('name');
+
+// Test data: item types and their expected suggested categories
+$testCases = [
+ ['itemType' => 'Food processor', 'expectedCategory' => 'Small kitchen item', 'powered' => true],
+ ['itemType' => 'Blender', 'expectedCategory' => 'Small kitchen item', 'powered' => true],
+ ['itemType' => 'TV', 'expectedCategory' => 'Flat screen 32-37"', 'powered' => true],
+ ['itemType' => 'Phone', 'expectedCategory' => 'Mobile', 'powered' => true],
+ ['itemType' => 'Printer', 'expectedCategory' => 'Printer/scanner', 'powered' => true],
+ ['itemType' => 'Television', 'expectedCategory' => 'Flat screen 32-37"', 'powered' => true],
+ ['itemType' => 'Télévision', 'expectedCategory' => 'Flat screen 32-37"', 'powered' => true],
+ ['itemType' => 'Toaster', 'expectedCategory' => 'Toaster', 'powered' => true],
+ ['itemType' => 'Microwave oven', 'expectedCategory' => 'None of the above', 'powered' => true],
+ ['itemType' => 'Heater', 'expectedCategory' => 'None of the above', 'powered' => true],
+];
+
+$deviceCount = 0;
+
+// Create the expected mappings (3 devices each to ensure they win the count algorithm)
+foreach ($testCases as $testCase) {
+ echo "Creating 3 devices for '{$testCase['itemType']}' → '{$testCase['expectedCategory']}'\n";
+
+ $category = $categories->get($testCase['expectedCategory']);
+ if (!$category) {
+ echo "Warning: Category '{$testCase['expectedCategory']}' not found, using first category\n";
+ $category = $categories->first();
+ }
+
+ for ($i = 0; $i < 3; $i++) {
+ Device::create([
+ 'category' => $category->idcategories,
+ 'category_creation' => $category->idcategories,
+ 'estimate' => 100,
+ 'item_type' => $testCase['itemType'],
+ 'brand' => 'Test Brand',
+ 'model' => 'Test Model ' . ($i + 1),
+ 'age' => 5,
+ 'repair_status' => 1, // Fixed
+ 'spare_parts' => 1,
+ 'event' => $testEvent->idevents,
+ 'problem' => 'Test problem description',
+ 'wiki' => 1,
+ 'do_it_yourself' => 0,
+ ]);
+ $deviceCount++;
+ }
+}
+
+// Create some conflicting data with fewer items to ensure our expected categories win
+$conflictingData = [
+ ['itemType' => 'Food processor', 'category' => 'None of the above', 'count' => 2],
+ ['itemType' => 'TV', 'category' => 'Flat screen 15-17"', 'count' => 1],
+ ['itemType' => 'Phone', 'category' => 'Handheld entertainment device', 'count' => 2],
+ ['itemType' => 'Printer', 'category' => 'PC accessory', 'count' => 1],
+ ['itemType' => 'Toaster', 'category' => 'Small kitchen item', 'count' => 2],
+];
+
+foreach ($conflictingData as $conflict) {
+ echo "Creating {$conflict['count']} conflicting devices for '{$conflict['itemType']}' → '{$conflict['category']}'\n";
+
+ $category = $categories->get($conflict['category']);
+ if (!$category) {
+ echo "Warning: Category '{$conflict['category']}' not found, using first category\n";
+ $category = $categories->first();
+ }
+
+ for ($i = 0; $i < $conflict['count']; $i++) {
+ Device::create([
+ 'category' => $category->idcategories,
+ 'category_creation' => $category->idcategories,
+ 'estimate' => 100,
+ 'item_type' => $conflict['itemType'],
+ 'brand' => 'Conflict Brand',
+ 'model' => 'Conflict Model ' . ($i + 1),
+ 'age' => 5,
+ 'repair_status' => 1, // Fixed
+ 'spare_parts' => 1,
+ 'event' => $testEvent->idevents,
+ 'problem' => 'Conflict test problem description',
+ 'wiki' => 1,
+ 'do_it_yourself' => 0,
+ ]);
+ $deviceCount++;
+ }
+}
+
+echo "Created {$deviceCount} test devices successfully\n";
+echo "Test group ID: {$testGroup->idgroups}\n";
+echo "Test event ID: {$testEvent->idevents}\n";
+echo "Test data setup complete!\n";
\ No newline at end of file
diff --git a/tests/Feature/Devices/EditTest.php b/tests/Feature/Devices/EditTest.php
index be3d7b11fb..7af0f6d122 100644
--- a/tests/Feature/Devices/EditTest.php
+++ b/tests/Feature/Devices/EditTest.php
@@ -116,6 +116,7 @@ public function testDeviceEditAddImage() {
$params = [];
$response = $this->json('POST', '/device/image-upload/' . $iddevices, $params);
+ $response->assertSuccessful();
$ret = json_decode($response->getContent(), TRUE);
self::assertEquals(true, $ret['success']);
self::assertEquals($iddevices, $ret['iddevices']);
diff --git a/tests/Feature/Users/MenusTest.php b/tests/Feature/Users/MenusTest.php
index 0d87de6cbb..cadf55d778 100644
--- a/tests/Feature/Users/MenusTest.php
+++ b/tests/Feature/Users/MenusTest.php
@@ -79,6 +79,7 @@ public function testSections($role, $present, $translator, $adminMenu)
$up->save();
}
+ $this->get('/logout');
$this->actingAs($user);
$response = $this->get('/user/menus');
diff --git a/tests/Feature/Users/RecoverTest.php b/tests/Feature/Users/RecoverTest.php
index b277a19f71..5d63eac191 100644
--- a/tests/Feature/Users/RecoverTest.php
+++ b/tests/Feature/Users/RecoverTest.php
@@ -15,6 +15,9 @@
class RecoverTest extends TestCase
{
+ private $recovery = null;
+ private $recoveryCode = null;
+
private function getCode($recovery) {
if (preg_match('/recovery=(.*?)($|&)/', $recovery, $matches)) {
return($matches[1]);
diff --git a/tests/Integration/device.test.js b/tests/Integration/device.test.js
index e684ce8820..b4ebf5086e 100644
--- a/tests/Integration/device.test.js
+++ b/tests/Integration/device.test.js
@@ -89,45 +89,11 @@ test('Automatic category suggestion from item type', async ({page, baseURL}) =>
{ itemType: 'Heater', expectedCategory: 'None of the above', powered: true }
]
- // Set up test data: create multiple devices for each item type to ensure autocomplete works
- // The getItemTypes() method uses a count-based algorithm, so we need sufficient data
- console.log('Setting up autocomplete test data...')
-
- let deviceCount = 0
-
- // Create the expected mappings (5 devices each to ensure they win the count algorithm)
- for (const testCase of testCases) {
- console.log(`Creating 5 devices for '${testCase.itemType}' → '${testCase.expectedCategory}'`)
-
- for (let i = 0; i < 5; i++) {
- await addDevice(page, baseURL, eventid, testCase.powered, false, false, false, testCase.itemType, testCase.expectedCategory)
- deviceCount++
- }
- }
-
- // Create some conflicting data with fewer items to ensure our expected categories win
- const conflictingData = [
- { itemType: 'Food processor', category: 'None of the above', count: 2 },
- { itemType: 'TV', category: 'Flat screen 15-17"', count: 3 },
- { itemType: 'Phone', category: 'Handheld entertainment device', count: 2 },
- { itemType: 'Printer', category: 'PC accessory', count: 1 },
- { itemType: 'Toaster', category: 'Small kitchen item', count: 2 }
- ]
-
- for (const conflict of conflictingData) {
- console.log(`Creating ${conflict.count} conflicting devices for '${conflict.itemType}' → '${conflict.category}'`)
-
- for (let i = 0; i < conflict.count; i++) {
- await addDevice(page, baseURL, eventid, true, false, false, false, conflict.itemType, conflict.category)
- deviceCount++
- }
- }
-
- console.log(`Created ${deviceCount} test devices successfully`)
-
- console.log('Testing autocomplete functionality...')
+ // Test data is now set up by the PHP script in CircleCI before this test runs
// Note: The items.js store will automatically fetch fresh data since we're running under Playwright
+ console.log('Testing autocomplete functionality...')
for (const testCase of testCases) {
+ console.log('Testing', testCase.itemType, testCase.expectedCategory, testCase.powered)
// Test the UI behavior for category autocomplete
// Go to event view page
diff --git a/tests/Integration/utils.js b/tests/Integration/utils.js
index 915b744fc2..861fd5b6f9 100644
--- a/tests/Integration/utils.js
+++ b/tests/Integration/utils.js
@@ -189,7 +189,7 @@ exports.addDevice = async function(page, baseURL, idevents, powered, photo, fixe
// Get current device count.
await page.waitForSelector(addsel)
- var current = await page.locator('h3:visible').count()
+ var current = await page.locator('.device-info:visible').count()
log('Current device count', { current })
// Click the add button.
@@ -244,7 +244,7 @@ exports.addDevice = async function(page, baseURL, idevents, powered, photo, fixe
// Wait for device to show.
log('Waiting for device to appear in list')
- await expect(page.locator('h3:visible')).toHaveCount(current + 1)
+ await expect(page.locator('.device-info:visible')).toHaveCount(current + 1)
// Check that the photo appears.
log('Opening device for verification')
@@ -275,6 +275,32 @@ exports.addDevice = async function(page, baseURL, idevents, powered, photo, fixe
log('Device added successfully')
}
+// Fast device creation for bulk test data - skips verification steps
+exports.addDeviceFast = async function(page, baseURL, idevents, powered, itemType, category) {
+ log('Starting fast device addition', { idevents, powered, itemType, category })
+
+ var addsel = powered ? '.add-powered-device-desktop' : '.add-unpowered-device-desktop'
+
+ // Click the add button.
+ await page.locator(addsel).click()
+
+ // Set item type
+ await page.fill('.item-type:visible input', itemType)
+ await page.keyboard.press('Tab')
+
+ // Set category
+ await page.keyboard.type(category)
+ await page.keyboard.press('Enter')
+
+ // Submit without verification
+ await page.locator('text=Add item >> visible=true').click()
+
+ // Wait briefly for submission to complete
+ await page.waitForTimeout(500)
+
+ log('Fast device added successfully')
+}
+
exports.unfollowGroup = async function(page, idgroups) {
await page.goto('/group/view/' + idgroups)
diff --git a/tests/TestCase.php b/tests/TestCase.php
index bb3ac4ba70..52be6eeae6 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -43,6 +43,12 @@ abstract class TestCase extends BaseTestCase
private $DOM = null;
public $lastResponse = null;
+ private $host = null;
+ private $group = null;
+ private $event_start_utc = null;
+ private $event_end_utc = null;
+ private $OpenAPIValidator = null;
+
protected function setUp(): void
{
parent::setUp();
diff --git a/tests/Unit/NotificationsTest.php b/tests/Unit/NotificationsTest.php
index 1eabceb9a7..12c58e0188 100644
--- a/tests/Unit/NotificationsTest.php
+++ b/tests/Unit/NotificationsTest.php
@@ -83,6 +83,9 @@ class NotificationsTest extends TestCase
private $outputs = [];
+ private $useren = null;
+ private $userfr = null;
+
protected function setUp(): void {
parent::setUp();
@@ -806,12 +809,12 @@ public function testGenerateOutputs()
{
$notificationen = new $class($this->params, $this->useren);
- $outputs[$class]['mail']['en'] = $notificationen->toMail($this->useren)->toArray();
- $outputs[$class]['array']['en'] = $notificationen->toArray($this->useren);
+ $this->outputs[$class]['mail']['en'] = $notificationen->toMail($this->useren)->toArray();
+ $this->outputs[$class]['array']['en'] = $notificationen->toArray($this->useren);
$notificationfr = new $class($this->params, $this->userfr);
- $outputs[$class]['mail']['fr'] = $notificationfr->toMail($this->userfr)->toArray();
- $outputs[$class]['array']['fr'] = $notificationfr->toArray($this->userfr);
+ $this->outputs[$class]['mail']['fr'] = $notificationfr->toMail($this->userfr)->toArray();
+ $this->outputs[$class]['array']['fr'] = $notificationfr->toArray($this->userfr);
}
// $this->recursive_print('$this->outputs', $outputs);