Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot]andthomasnordquist ee5fd58341 Fix shell variable expansion in mosquitto health check
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-24 12:10:40 +00:00
copilot-swe-agent[bot]andthomasnordquist 1047f5f8a8 Fix mosquitto service syntax errors by replacing with mqtt-explorer-ui-tests container and direct installation
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-24 12:08:54 +00:00
copilot-swe-agent[bot] 24c503bc98 Initial plan 2025-12-24 12:03:24 +00:00
Thomas Nordquist 6f8ad41ef5 Change MQTT broker host in tests workflow 2025-12-24 12:42:12 +01:00
Copilotandthomasnordquist 1ec10bb007 Fix browser UI tests: Enable anonymous access for mosquitto 2 (#994)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-24 12:05:02 +01:00
Thomas Nordquist 70e75061b2 Change MQTT broker host to localhost in tests 2025-12-24 10:11:41 +01:00
Copilotandthomasnordquist c0b2950ecb Configure MQTT broker hostname based on execution context (#993)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-24 08:23:42 +01:00
Thomas Nordquist f946ae39d7 Add bash shell specification for S3 upload step 2025-12-23 23:42:12 +01:00
Copilotandthomasnordquist 43ff3e81f0 Segment demo video by scene with embedded GIF segments (#990)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-23 23:11:18 +01:00
Copilotandthomasnordquist 79a8cdf1fd Refactor copilot instructions: add meta-directives and condense content (#992)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-23 23:07:02 +01:00
Copilotandthomasnordquist 16c190818c Set TESTS_MQTT_BROKER_HOST globally for all jobs with mosquitto services (#989)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-23 22:38:32 +01:00
Copilotandthomasnordquist de367e755f Fix server build: Remove Electron dependency from events/index exports (#987)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-23 21:46:40 +01:00
Copilot 6e355decbf Replace react-vis with visx, add component testing infrastructure, and update Electron packages (#959) 2025-12-23 21:45:33 +01:00
Copilotandthomasnordquist d4dbc36a8a Set Content-Type header when uploading demo-video GIF to S3 (#986)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-23 20:24:58 +01:00
Copilotandthomasnordquist 0e82a8baad [WIP] Run mosquitto as a service for browser UI tests (#985)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-23 19:57:26 +01:00
Copilotandthomasnordquist 0016f2d364 Remove redundant EventBus replacement pattern causing potential circular dependency (#984)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-23 19:52:59 +01:00
38 changed files with 2937 additions and 844 deletions
+63 -243
View File
@@ -1,263 +1,83 @@
# GitHub Copilot Agent Instructions for MQTT Explorer
# copilot-instructions.md - Agent Long-Term Memory
## Test Suites
## META-INSTRUCTIONS (IMMUTABLE)
MQTT Explorer has several test suites to ensure code quality and reliability:
1. **Long-term memory**: If you learn something during a session that would save time in future sessions, add it to `.github/copilot-instructions.md`
2. **Paper knowledge must go**: If something is no longer true, update or remove it immediately
3. **Evaluate after every session**: Consider whether the instructions need updates based on what you learned
4. **Concise and useful**: All information must be actionable, current, and concise
### Unit Tests
## Test Commands
**App tests** - Frontend component and logic tests:
**Unit tests:**
- `yarn test` - All unit tests (app + backend)
- `yarn test:app` - Frontend tests only
- `yarn test:backend` - Backend tests only
**Integration tests:**
- `yarn test:ui` - Browser tests (requires `yarn build` first)
- `yarn test:demo-video` - UI recording (requires Xvfb, mosquitto, tmux, ffmpeg)
- `yarn test:mcp` - Model Context Protocol tests
- `yarn test:all` - All tests (unit + demo-video)
- `./scripts/runBrowserTests.sh` - Browser mode UI tests (requires mosquitto service)
**CI jobs:** `test`, `ui-tests`, `demo-video`, `test-browser`, `browser-ui-tests`
**Important:** Browser UI tests require MQTT broker. In CI, GitHub Actions health checks ensure the mosquitto service is ready before tests run.
## Browser Mode
**Prerequisites:** Node.js ≥24, Yarn, Mosquitto broker (for testing)
**Development (hot reload):**
```bash
yarn test:app
# Or: cd app && yarn test
export MQTT_EXPLORER_USERNAME=admin MQTT_EXPLORER_PASSWORD=yourpass
yarn dev:server
# Backend: http://localhost:3000, Frontend: http://localhost:8080 (use this one)
```
**Backend tests** - Data model and business logic tests:
**Production:**
```bash
yarn test:backend
# Or: cd backend && yarn test
yarn build:server
export MQTT_EXPLORER_USERNAME=admin MQTT_EXPLORER_PASSWORD=yourpass
yarn start:server # http://localhost:3000
```
**Run all unit tests**:
```bash
yarn test
```
### Integration Tests
**UI test suite** - Independent, deterministic browser tests:
```bash
yarn test:ui
# Requires: yarn build
```
**Demo video generation** - UI test recording with video capture:
```bash
yarn test:demo-video
# Requires: Xvfb, mosquitto broker, tmux, ffmpeg
# For development: Use ./scripts/uiTests.sh for full video recording setup
```
**MCP introspection tests** - Model Context Protocol tests:
```bash
yarn test:mcp
```
**Run all tests** (unit + demo-video):
```bash
yarn test:all
```
### CI/CD Test Execution
In CI environments, tests run in isolated containers with all dependencies pre-installed:
- `test` job: Runs unit tests (app + backend)
- `ui-tests` job: Runs UI test suite with screenshots
- `demo-video` job: Generates demo video with full recording setup
- `test-browser` job: Runs browser mode smoke tests
**Build artifacts:** `dist/src/server.js`, `app/build/*.js`, `app/build/index.html`
## Debugging Browser Mode
### Prerequisites
- Node.js 24 or higher
- Yarn package manager
- Running Mosquitto MQTT broker (for testing)
**DevTools checks:**
- Console: JavaScript errors, CSP errors (security headers), WebSocket issues
- Network: Static asset loading, WebSocket status, API auth
### Development Mode (with Hot Reload)
**Common issues:**
- **Blank page/CSP errors:** Add `'unsafe-eval'` to `scriptSrc` in `src/server.ts` helmet config
- **Auth loop:** WebSocket auth failing - check Network → WS → Messages and server logs
- **Theme errors:** Verify both ThemeProvider and LegacyThemeProvider in `app/src/index.tsx`
1. **Set credentials (required):**
```bash
export MQTT_EXPLORER_USERNAME=admin
export MQTT_EXPLORER_PASSWORD=your_password
```
2. **Start development servers:**
```bash
yarn dev:server
```
This runs two servers in parallel:
- Backend server on http://localhost:3000 (serves API, WebSocket, authentication)
- Webpack dev server on http://localhost:8080 (serves frontend with hot reload)
3. **Access the application:**
- Navigate to http://localhost:8080 (NOT :3000)
- Webpack dev server proxies API/WebSocket requests to backend on port 3000
- Hot reload enabled - changes to React components update automatically
### Production Mode (Production Build)
1. **Build the browser version:**
```bash
yarn build:server
```
This compiles TypeScript and builds the optimized webpack bundle
2. **Start the server:**
```bash
# Set credentials (required) - these are for the browser login page
export MQTT_EXPLORER_USERNAME=admin
export MQTT_EXPLORER_PASSWORD=your_password
# Start server
yarn start:server
# OR: node dist/src/server.js
```
Server will run on http://localhost:3000 (serves both frontend and backend)
3. **Login to the application:**
- Navigate to http://localhost:3000
- Enter the username and password you set in the environment variables
- Click "LOGIN" button
- After successful login, the main application will load
- The MQTT Connection modal will appear where you can configure broker connections
### Debugging with Browser DevTools
1. **Open browser DevTools:**
- Navigate to http://localhost:3000
- Press F12 or right-click → Inspect
2. **Check Console tab:**
- Look for JavaScript errors
- CSP (Content Security Policy) errors indicate security header issues
- Network errors indicate API/WebSocket connection issues
3. **Check Network tab:**
- Verify static assets load correctly (JS bundles, CSS)
- Check WebSocket connection status
- Monitor API calls for authentication issues
4. **Common Issues:**
**Blank page / CSP errors:**
- Symptom: Console shows `EvalError: ... violates Content Security Policy`
- Cause: webpack runtime requires `unsafe-eval` for code splitting
- Fix: Add `'unsafe-eval'` to `scriptSrc` in `src/server.ts` helmet config
**Authentication loop:**
- Symptom: Login dialog keeps reappearing
- Cause: WebSocket authentication failing
- Debug: Check browser Network tab → WS → Messages
- Check: Server logs for authentication errors
**Theme errors:**
- Symptom: App loads but styling is broken
- Cause: Material-UI theme not loading correctly
- Check: Console for theme-related errors
- Verify: Both ThemeProvider and LegacyThemeProvider in `app/src/index.tsx`
**Expected console warnings (non-fatal):**
- React 18 type warnings with Material-UI v5 components (dozens of "Failed prop type" warnings)
- `TypeError: Cannot read properties of undefined (reading 'on')` from IpcRendererEventBus - this is expected in browser mode as there's no Electron IPC
- MUI locale warnings for `en-US` - expected, app uses available locales
- `componentWillReceiveProps` deprecation warnings - from legacy TreeComponent
- ACE editor autocomplete warnings - expected, features not imported
- CSP worker violation for ACE editor - known issue, editor still functions
These warnings don't prevent the application from functioning correctly.
### Using Playwright for Automated Testing
**Expected warnings (non-fatal):**
- React 18 + Material-UI v5 type warnings
- IpcRendererEventBus errors (no Electron IPC in browser mode)
- MUI locale, componentWillReceiveProps, ACE editor warnings
**WebSocket debugging:**
```bash
# Start server in background
export MQTT_EXPLORER_USERNAME=admin
export MQTT_EXPLORER_PASSWORD=test123
node dist/src/server.js &
# Use Playwright browser tool (in Copilot agent context)
playwright-browser_navigate http://localhost:3000
playwright-browser_take_screenshot --filename debug.png
playwright-browser_console_messages # Check for errors
node dist/src/server.js 2>&1 | tee server.log
# Check DevTools → Network → WS → Messages for handshake
# CORS: check ALLOWED_ORIGINS env var
```
### Expected UI Flow
**Security notes:**
- `unsafe-eval` in CSP required for webpack (security tradeoff)
- Never hardcode credentials (use env vars)
- Production: use HTTPS with reverse proxy
- Rate limit: 5 auth attempts/15min/IP
- File upload limit: 16MB
1. **Login Page** (https://github.com/user-attachments/assets/383305e1-2169-433c-a668-5a05da0c343a)
- Enter username and password from environment variables
- Click "LOGIN" button
2. **Main Application After Login** (https://github.com/user-attachments/assets/cc4d665f-2665-4289-b2fc-dc4986f9ab5b)
- Application loads with sidebar, topic tree, value panel, and publish panel
- MQTT Connection modal appears automatically for first-time setup
- Configure broker connection (host, port, credentials, etc.)
- Click "CONNECT" to establish MQTT connection
3. **Application Features:**
- Topic tree on the left shows MQTT topic hierarchy
- Value panel shows selected topic's message content
- Publish panel allows sending MQTT messages
- Charts panel for numeric value visualization
- Settings drawer for app configuration
### Debugging WebSocket Connection
1. **Check server logs:**
```bash
node dist/src/server.js 2>&1 | tee server.log
```
2. **Check browser WebSocket:**
- DevTools → Network → WS tab
- Look for socket.io connection
- Check Messages tab for authentication handshake
3. **Common WebSocket issues:**
- CORS errors: Check `ALLOWED_ORIGINS` environment variable
- Authentication errors: Verify credentials in sessionStorage
- Connection refused: Server not running or port blocked
### Development vs Production
**Development mode:**
```bash
yarn dev:server
# Runs webpack-dev-server with hot reload
# More verbose error messages
# Source maps enabled
```
**Production mode:**
```bash
NODE_ENV=production yarn build:server
NODE_ENV=production node dist/src/server.js
# Minified bundles
# Generic error messages (security)
# HSTS enabled
```
### Build Artifacts
After `yarn build:server`, check:
- `dist/src/server.js` - Compiled server code
- `app/build/*.js` - Webpack bundles
- `app/build/index.html` - Entry point HTML
### Troubleshooting Checklist
- [ ] Node.js version >=24
- [ ] `yarn install` completed without errors
- [ ] TypeScript compilation successful (`npx tsc`)
- [ ] Webpack build successful (check `app/build/` directory)
- [ ] Server starts without errors
- [ ] Can access http://localhost:3000
- [ ] Login dialog appears
- [ ] No CSP errors in console
- [ ] WebSocket connects successfully
- [ ] App renders after login
### Security Considerations
When debugging, be aware that:
- `unsafe-eval` in CSP is required for webpack but reduces security
- Credentials should never be hardcoded (use environment variables)
- In production, use HTTPS with a reverse proxy (nginx/Apache)
- Rate limiting is active (5 auth attempts per 15 min per IP)
- File upload size limit is 16MB
### Related Files
- `src/server.ts` - Express server with security middleware
- `app/webpack.browser.config.mjs` - Browser-specific webpack config
- `app/src/browserEventBus.ts` - Socket.io client for browser mode
- `app/src/components/BrowserAuthWrapper.tsx` - Authentication dialog
- `app/src/index.tsx` - React app entry point with theme providers
**Key files:**
- `src/server.ts` - Express server, security middleware
- `app/webpack.browser.config.mjs` - Browser webpack config
- `app/src/browserEventBus.ts` - Socket.io client
- `app/src/components/BrowserAuthWrapper.tsx` - Auth dialog
- `app/src/index.tsx` - React entry, theme providers
@@ -14,6 +14,9 @@ on:
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
env:
TESTS_MQTT_BROKER_HOST: localhost
TESTS_MQTT_BROKER_PORT: 1883
steps:
- name: Checkout code
+15 -13
View File
@@ -30,22 +30,26 @@ jobs:
packages: write
id-token: write
services:
# MQTT broker for testing
mosquitto:
image: eclipse-mosquitto:2
ports:
- 1883:1883
options: >-
--health-cmd "mosquitto_sub -t '$SYS/#' -C 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
TESTS_MQTT_BROKER_HOST: localhost
TESTS_MQTT_BROKER_PORT: 1883
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup and start mosquitto
run: |
sudo apt-get update
sudo apt-get install -y mosquitto
sudo mkdir -p /etc/mosquitto/conf.d
echo "listener 1883" | sudo tee /etc/mosquitto/conf.d/default.conf
echo "allow_anonymous true" | sudo tee -a /etc/mosquitto/conf.d/default.conf
sudo systemctl start mosquitto
sudo systemctl status mosquitto
# Wait for mosquitto to be ready
timeout 10 bash -c 'until mosquitto_sub -t "\$SYS/#" -C 1 > /dev/null 2>&1; do sleep 1; done'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
@@ -191,8 +195,6 @@ jobs:
MQTT_EXPLORER_USERNAME: test
MQTT_EXPLORER_PASSWORD: test123
BROWSER_MODE_URL: http://localhost:3000
MQTT_BROKER_HOST: localhost
MQTT_BROKER_PORT: 1883
- name: Clean up browser test container
if: always()
+76 -49
View File
@@ -28,20 +28,15 @@ jobs:
volumes:
- ./:/app
options: --user root
services:
mosquitto:
image: eclipse-mosquitto:2
ports:
- 1883:1883
options: >-
--health-cmd "mosquitto_sub -t '$SYS/#' -C 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
env:
TESTS_MQTT_BROKER_HOST: mosquitto
TESTS_MQTT_BROKER_PORT: 1883
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Start mosquitto
run: mosquitto -c /etc/mosquitto/conf.d/default.conf -d
- name: Install Packages
run: yarn install --frozen-lockfile
- name: Build Browser Mode
@@ -49,9 +44,6 @@ jobs:
- name: Run Browser UI Tests
timeout-minutes: 10
run: ./scripts/runBrowserTests.sh
env:
MQTT_BROKER_HOST: mosquitto
MQTT_BROKER_PORT: 1883
- name: Upload Test Screenshots
if: always()
uses: actions/upload-artifact@v4
@@ -68,10 +60,15 @@ jobs:
volumes:
- ./:/app
options: --user root
env:
TESTS_MQTT_BROKER_HOST: localhost
TESTS_MQTT_BROKER_PORT: 1883
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Start mosquitto
run: mosquitto -c /etc/mosquitto/conf.d/default.conf -d
- name: Install Packages
run: yarn install --frozen-lockfile
- name: Build
@@ -80,12 +77,12 @@ jobs:
run: yarn ui-test
- name: Post-processing
run: ./scripts/prepareVideo.sh
- name: Generate unique filename
id: filename
- name: Generate unique base path
id: basepath
run: |
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
FILENAME="pr-${{ github.event.pull_request.number }}-${TIMESTAMP}.gif"
echo "filename=${FILENAME}" >> $GITHUB_OUTPUT
BASEPATH="pr-${{ github.event.pull_request.number }}-${TIMESTAMP}"
echo "basepath=${BASEPATH}" >> $GITHUB_OUTPUT
- name: Install AWS CLI v2
run: |
apt-get update && apt-get install -y unzip
@@ -99,65 +96,95 @@ jobs:
aws-access-key-id: ${{ vars.AWS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: 'eu-central-1'
- name: Upload to S3
id: upload
- name: Upload full video to S3
env:
AWS_BUCKET: ${{ vars.AWS_BUCKET }}
FILENAME: ${{ steps.filename.outputs.filename }}
BASEPATH: ${{ steps.basepath.outputs.basepath }}
run: |
# Upload GIF
aws s3api put-object \
--bucket ${AWS_BUCKET} \
--key artifacts/${FILENAME} \
--body ./ui-test.gif
- name: Generate file URL
--key artifacts/${BASEPATH}/ui-test.gif \
--body ./ui-test.gif \
--content-type image/gif
# Upload MP4
aws s3api put-object \
--bucket ${AWS_BUCKET} \
--key artifacts/${BASEPATH}/ui-test.mp4 \
--body ./ui-test.mp4 \
--content-type video/mp4
- name: Upload video segments to S3
env:
AWS_BUCKET: ${{ vars.AWS_BUCKET }}
BASEPATH: ${{ steps.basepath.outputs.basepath }}
shell: bash
run: |
# Upload all GIF segment files if they exist
shopt -s nullglob # Make glob return empty list if no matches
for segment in segment-*.gif; do
echo "Uploading $segment..."
aws s3api put-object \
--bucket ${AWS_BUCKET} \
--key artifacts/${BASEPATH}/${segment} \
--body ./${segment} \
--content-type image/gif
done
shopt -u nullglob # Restore default behavior
- name: Generate file URLs
id: fileurl
env:
AWS_BUCKET: ${{ vars.AWS_BUCKET }}
FILENAME: ${{ steps.filename.outputs.filename }}
BASEPATH: ${{ steps.basepath.outputs.basepath }}
run: |
FILE_URL="https://${AWS_BUCKET}.s3.eu-central-1.amazonaws.com/artifacts/${FILENAME}"
echo "file-url=${FILE_URL}" >> $GITHUB_OUTPUT
echo "Uploaded to: ${FILE_URL}"
- name: Show URL
run: echo '${{ steps.fileurl.outputs.file-url }}'
id: artifact-upload-step
- run: echo '<picture><img src="${{ steps.fileurl.outputs.file-url }}"></picture>' \
>> $GITHUB_STEP_SUMMARY
BASE_URL="https://${AWS_BUCKET}.s3.eu-central-1.amazonaws.com/artifacts/${BASEPATH}"
echo "base-url=${BASE_URL}" >> $GITHUB_OUTPUT
echo "Uploaded to: ${BASE_URL}"
- name: Generate markdown summary
id: markdown
env:
BASE_URL: ${{ steps.fileurl.outputs.base-url }}
run: |
MARKDOWN=$(node ./scripts/generateMarkdownSummary.js "${BASE_URL}")
echo "markdown<<EOF" >> $GITHUB_OUTPUT
echo "$MARKDOWN" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Add to workflow summary
env:
MARKDOWN: ${{ steps.markdown.outputs.markdown }}
run: |
echo "$MARKDOWN" >> $GITHUB_STEP_SUMMARY
- name: Post video to PR
uses: actions/github-script@v7
env:
VIDEO_URL: ${{ steps.fileurl.outputs.file-url }}
MARKDOWN: ${{ steps.markdown.outputs.markdown }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const videoUrl = process.env.VIDEO_URL;
const markdown = process.env.MARKDOWN;
github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## 🎬 Demo Video Generated\n\n![Demo Video](${videoUrl})\n\n_This video will expire in 90 days._`
body: markdown
});
test-browser:
runs-on: ubuntu-latest
services:
mosquitto:
image: eclipse-mosquitto:2
ports:
- 1883:1883
options: >-
--health-cmd "mosquitto_sub -t '$SYS/#' -C 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
container:
image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
volumes:
- ./:/app
options: --user root
env:
TESTS_MQTT_BROKER_HOST: localhost
TESTS_MQTT_BROKER_PORT: 1883
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '24'
- name: Start mosquitto
run: mosquitto -c /etc/mosquitto/conf.d/default.conf -d
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Install Playwright Browsers
+13 -1
View File
@@ -23,4 +23,16 @@ browser-debug-screenshot.png
app/.webpack-cache
# Temporary files
/tmp
/tmp
# Demo video artifacts
scenes.json
segment-*.mp4
segment-*.gif
ui-test.mp4
ui-test.gif
app.mp4
app2.mp4
app720.gif
qrawvideorgb24.yuv
intro.png
+6
View File
@@ -10,6 +10,12 @@ ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
# Configure mosquitto for anonymous access (required for tests)
RUN mkdir -p /etc/mosquitto/conf.d && \
echo "listener 1883" > /etc/mosquitto/conf.d/default.conf && \
echo "allow_anonymous true" >> /etc/mosquitto/conf.d/default.conf && \
echo "persistence false" >> /etc/mosquitto/conf.d/default.conf
# Install Playwright and browsers
# This ensures Playwright browsers are pre-installed in the container
RUN npm install -g playwright@1.57.0 && \
+309
View File
@@ -0,0 +1,309 @@
# React Component Testing Guide
This guide explains how to write tests for React components in the MQTT-Explorer project using the generic testing utilities.
## Overview
We use the following testing stack:
- **Mocha** - Test framework
- **Chai** - Assertion library
- **React Testing Library** - React component testing utilities
- **JSDOM** - DOM implementation for Node.js
- **Custom Test Utilities** - Located in `src/utils/spec/testUtils.tsx`
## Quick Start
### 1. Create a Test File
Test files should be placed next to the component they test with the `.spec.tsx` extension:
```
src/components/MyComponent/
├── MyComponent.tsx
└── MyComponent.spec.tsx
```
### 2. Basic Test Structure
```typescript
import React from 'react'
import { expect } from 'chai'
import { describe, it } from 'mocha'
import MyComponent from './MyComponent'
import { renderWithProviders } from '../../utils/spec/testUtils'
describe('MyComponent', () => {
it('should render correctly', () => {
const { container } = renderWithProviders(<MyComponent />)
expect(container).to.exist
})
})
```
## Using Test Utilities
### `renderWithProviders`
This function wraps your component with necessary providers (Theme, Redux).
```typescript
import { renderWithProviders } from '../../utils/spec/testUtils'
// Render with theme only (default)
const { container } = renderWithProviders(<MyComponent />, { withTheme: true })
// Render with both theme and Redux
const { container } = renderWithProviders(<MyComponent />, {
withTheme: true,
withRedux: true
})
// Render with custom theme
import { createTheme } from '@mui/material/styles'
const darkTheme = createTheme({ palette: { mode: 'dark' } })
const { container } = renderWithProviders(<MyComponent />, {
theme: darkTheme
})
// Render with custom Redux store
import { configureStore } from '@reduxjs/toolkit'
const customStore = configureStore({ /* ... */ })
const { container } = renderWithProviders(<MyComponent />, {
store: customStore,
withRedux: true
})
```
### `createMockChartData`
Helper function to generate mock chart data:
```typescript
import { createMockChartData } from '../../utils/spec/testUtils'
// Create 10 data points (default)
const data = createMockChartData()
// Create specific number of points
const data = createMockChartData(50)
```
### Global Mocks
The test utilities automatically set up global mocks:
- **ResizeObserver** - Mocked for components using `react-resize-detector`
## Common Testing Patterns
### Testing Rendering
```typescript
it('should render without crashing', () => {
const { container } = renderWithProviders(<MyComponent />)
expect(container).to.exist
})
it('should render specific element', () => {
const { container } = renderWithProviders(<MyComponent />)
const element = container.querySelector('.my-class')
expect(element).to.exist
})
```
### Testing Props
```typescript
it('should accept all valid props', () => {
const props = {
title: 'Test',
value: 123,
onchange: () => {},
}
const { container } = renderWithProviders(<MyComponent {...props} />)
expect(container).to.exist
})
```
### Testing User Interactions
```typescript
import { userEvent } from '../../utils/spec/testUtils'
it('should handle click events', async () => {
const { container } = renderWithProviders(<MyComponent />)
const button = container.querySelector('button')
if (button) {
await userEvent.click(button)
// Assert expected behavior
}
})
```
### Testing Different States
```typescript
it('should render empty state', () => {
const { container } = renderWithProviders(<MyComponent data={[]} />)
// Assert empty state
})
it('should render with data', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<MyComponent data={data} />)
// Assert data is rendered
})
```
### Testing SVG Components
```typescript
it('should render SVG elements', () => {
const { container } = renderWithProviders(<ChartComponent />)
const svg = container.querySelector('svg')
expect(svg).to.exist
const paths = container.querySelectorAll('path')
expect(paths.length).to.be.greaterThan(0)
const circles = container.querySelectorAll('circle')
expect(circles.length).to.equal(5)
})
```
### Testing Edge Cases
```typescript
describe('Edge Cases', () => {
it('should handle negative values', () => {
const data = [{ x: 1, y: -10 }, { x: 2, y: -20 }]
const { container } = renderWithProviders(<MyComponent data={data} />)
expect(container.querySelector('svg')).to.exist
})
it('should handle empty arrays', () => {
const { container } = renderWithProviders(<MyComponent data={[]} />)
expect(container).to.exist
})
it('should handle very large numbers', () => {
const data = [{ x: 1, y: 1000000 }]
const { container } = renderWithProviders(<MyComponent data={data} />)
expect(container).to.exist
})
})
```
## Running Tests
### Run all tests
```bash
yarn test
```
### Run specific test file
```bash
npx mocha --require tsx --require source-map-support/register "src/components/MyComponent/MyComponent.spec.tsx"
```
### Run tests in watch mode
```bash
npx mocha --require tsx --require source-map-support/register --watch "src/**/*.spec.{ts,tsx}"
```
## Best Practices
1. **Test Behavior, Not Implementation**
- Focus on what the component does, not how it does it
- Test user-facing behavior and output
2. **Use Descriptive Test Names**
```typescript
// Good
it('should render error message when validation fails')
// Bad
it('test1')
```
3. **Group Related Tests**
```typescript
describe('MyComponent', () => {
describe('Rendering', () => {
it('should render correctly')
it('should render with props')
})
describe('Interactions', () => {
it('should handle clicks')
it('should handle keyboard input')
})
})
```
4. **Keep Tests Independent**
- Each test should be able to run in isolation
- Don't rely on test execution order
- Clean up after each test if needed
5. **Test Edge Cases**
- Empty data
- Null/undefined values
- Very large/small numbers
- Negative values
- Single item arrays
6. **Use Chai Assertions**
```typescript
expect(value).to.exist
expect(value).to.be.true
expect(value).to.equal(expected)
expect(array).to.have.length(5)
expect(number).to.be.greaterThan(0)
```
## Example: Complete Test Suite
See `src/components/Chart/Chart.spec.tsx` for a comprehensive example that demonstrates:
- Multiple test groups (Rendering, Data Visualization, Edge Cases, etc.)
- Testing with different props and configurations
- Testing SVG elements
- Testing theme integration
- Performance testing
- Edge case coverage
## Troubleshooting
### "ResizeObserver is not defined"
This is automatically mocked by the test utilities. Make sure you're importing from `testUtils.tsx`.
### "Cannot find module"
Check your import paths. Remember to use relative paths from the test file.
### "Window is not defined"
Make sure `jsdom-global/register` is imported in testUtils.tsx.
### Tests timing out
Increase the timeout in your test:
```typescript
it('should complete async operation', function() {
this.timeout(5000) // 5 seconds
// test code
})
```
## Adding New Test Utilities
To add new helper functions, update `src/utils/spec/testUtils.tsx`:
```typescript
export function myNewHelper() {
// Helper implementation
}
```
Then use it in your tests:
```typescript
import { myNewHelper } from '../../utils/spec/testUtils'
```
+13 -3
View File
@@ -6,8 +6,8 @@
"scripts": {
"build": "webpack --mode production",
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
"test": "mocha --require tsx --require source-map-support/register --recursive src/*/**/*.spec.ts",
"mochatest": "mocha --require tsx --require source-map-support/register --recursive src/*/**/*.spec.ts"
"test": "mocha --require tsx --require source-map-support/register --recursive 'src/*/**/*.spec.{ts,tsx}'",
"mochatest": "mocha --require tsx --require source-map-support/register --recursive 'src/*/**/*.spec.{ts,tsx}'"
},
"engines": {
"node": ">=20"
@@ -21,7 +21,12 @@
"@mui/lab": "^7.0.1-beta.20",
"@mui/material": "^7.3.6",
"@mui/styles": "^6.4.8",
"@react-spring/web": "^9.7.5",
"@types/react-transition-group": "^4.4.11",
"@visx/axis": "^3.10.1",
"@visx/grid": "^3.5.0",
"@visx/tooltip": "^3.3.0",
"@visx/xychart": "^3.10.2",
"ace-builds": "^1.4.11",
"axios": "^1.13.2",
"compare-versions": "^6.1.1",
@@ -51,7 +56,6 @@
"react-resize-detector": "^11.0.1",
"react-split-pane": "^0.1.92",
"react-transition-group": "^4.4.5",
"react-vis": "^1.12.1",
"redux": "^5.0.1",
"redux-batched-actions": "^0.5.0",
"redux-thunk": "^3.1.0",
@@ -62,6 +66,10 @@
},
"devDependencies": {
"@babel/runtime": "^7.28.4",
"@reduxjs/toolkit": "2.5.0",
"@testing-library/dom": "10.4.0",
"@testing-library/react": "16.1.0",
"@testing-library/user-event": "14.5.2",
"@types/d3": "^7.4.3",
"@types/diff": "^7.0.0",
"@types/get-value": "^3.0.5",
@@ -81,6 +89,8 @@
"css-loader": "^7.1.2",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.6.3",
"jsdom": "25.0.1",
"jsdom-global": "3.0.2",
"lodash": "^4.17.21",
"mocha": "^10.8.2",
"moment": "^2.30.1",
@@ -0,0 +1,112 @@
import { expect } from 'chai'
import { renderWithProviders } from '../../utils/spec/testUtils'
import Chart from './Chart'
describe('Chart X-Axis Domain Investigation', () => {
it('should spread points across X-axis with sequential timestamps', () => {
// Create 5 data points, 1 second (1000ms) apart
const now = Date.now()
const data = [
{ x: now - 4000, y: 20 },
{ x: now - 3000, y: 21 },
{ x: now - 2000, y: 22 },
{ x: now - 1000, y: 23 },
{ x: now, y: 24 }
]
const { container } = renderWithProviders(<Chart data={data} />)
// Find all circle elements (data points)
const circles = container.querySelectorAll('svg circle')
expect(circles).to.have.length(5)
// Extract cx (X-position) values
const cxValues: number[] = []
circles.forEach(circle => {
const cx = circle.getAttribute('cx')
if (cx) {
cxValues.push(parseFloat(cx))
}
})
// Log for debugging
console.log('\n========== X-AXIS DOMAIN INVESTIGATION ==========')
console.log('Data X values (timestamps):')
data.forEach((d, i) => console.log(` Point ${i}: ${d.x} (${new Date(d.x).toISOString()})`))
console.log(`\nData X range: ${data[data.length - 1].x - data[0].x}ms (${(data[data.length - 1].x - data[0].x) / 1000}s)`)
console.log('\nRendered circle CX positions:')
cxValues.forEach((cx, i) => console.log(` Circle ${i}: cx=${cx.toFixed(2)}px`))
const minCx = Math.min(...cxValues)
const maxCx = Math.max(...cxValues)
const cxRange = maxCx - minCx
console.log(`\nCX position range: ${cxRange.toFixed(2)}px (from ${minCx.toFixed(2)} to ${maxCx.toFixed(2)})`)
console.log(`Points per pixel: ${(cxValues.length / cxRange).toFixed(4)}`)
// Calculate spacing between consecutive points
const spacings: number[] = []
for (let i = 1; i < cxValues.length; i++) {
spacings.push(cxValues[i] - cxValues[i - 1])
}
console.log('\nSpacing between consecutive points:')
spacings.forEach((s, i) => console.log(` ${i} to ${i+1}: ${s.toFixed(2)}px`))
const avgSpacing = spacings.reduce((a, b) => a + b, 0) / spacings.length
console.log(`Average spacing: ${avgSpacing.toFixed(2)}px`)
console.log('=================================================\n')
// Assertions:
// 1. Points should be spread out (CX range should be significant, not bunched)
expect(cxRange).to.be.greaterThan(50, 'Points should be spread across at least 50px')
// 2. Points should be in ascending order (left to right)
for (let i = 1; i < cxValues.length; i++) {
expect(cxValues[i]).to.be.greaterThan(cxValues[i - 1],
`Point ${i} (cx=${cxValues[i]}) should be to the right of point ${i-1} (cx=${cxValues[i-1]})`)
}
// 3. Spacing should be relatively uniform (since data points are equally spaced in time)
const spacingVariance = spacings.map(s => Math.abs(s - avgSpacing))
const maxVariance = Math.max(...spacingVariance)
expect(maxVariance).to.be.lessThan(avgSpacing * 0.5,
'Spacing between points should be relatively uniform')
})
it('should handle points bunched at far right correctly', () => {
// Simulate the "bunched up" scenario with very large timestamps
const largeTimestamp = 1703347200000 // Dec 23, 2023
const data = [
{ x: largeTimestamp, y: 20 },
{ x: largeTimestamp + 1000, y: 21 },
{ x: largeTimestamp + 2000, y: 22 },
{ x: largeTimestamp + 3000, y: 23 },
{ x: largeTimestamp + 4000, y: 24 }
]
const { container } = renderWithProviders(<Chart data={data} />)
const circles = container.querySelectorAll('svg circle')
const cxValues: number[] = []
circles.forEach(circle => {
const cx = circle.getAttribute('cx')
if (cx) {
cxValues.push(parseFloat(cx))
}
})
console.log('\n========== LARGE TIMESTAMP TEST ==========')
console.log('Data X values:', data.map(d => d.x))
console.log('Rendered CX values:', cxValues.map(v => v.toFixed(2)))
const minCx = Math.min(...cxValues)
const maxCx = Math.max(...cxValues)
console.log(`CX range: ${(maxCx - minCx).toFixed(2)}px`)
console.log('==========================================\n')
// Points should still be spread out even with large timestamps
expect(maxCx - minCx).to.be.greaterThan(50,
'Points with large timestamps should still be spread across the chart')
})
})
+514
View File
@@ -0,0 +1,514 @@
/**
* Chart Component Tests
*
* These tests verify the Chart component functionality including:
* - Rendering with various data configurations
* - Theme integration
* - Interactive features (tooltips, hover states)
* - Different curve interpolation types
* - Custom domains and ranges
* - Responsive behavior
*/
import React from 'react'
import { expect } from 'chai'
import { describe, it } from 'mocha'
import Chart, { Props as ChartProps } from './Chart'
import { renderWithProviders, createMockChartData, screen } from '../../utils/spec/testUtils'
import { PlotCurveTypes } from '../../reducers/Charts'
describe('Chart Component', () => {
describe('Basic Rendering', () => {
it('should render without crashing with valid data', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container).to.exist
expect(container.querySelector('svg')).to.exist
})
it('should render NoData component when data is empty', () => {
const { container } = renderWithProviders(<Chart data={[]} />, { withTheme: true })
expect(container).to.exist
// NoData component should be rendered
const noDataElement = container.querySelector('div')
expect(noDataElement).to.exist
})
it('should render chart with correct height', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
const chartContainer = container.querySelector('[style*="height"]') as HTMLElement
expect(chartContainer).to.exist
expect(chartContainer.style.height).to.equal('150px')
})
it('should render SVG chart elements', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
// Check for SVG element
const svg = container.querySelector('svg')
expect(svg).to.exist
// Check for chart elements (paths for line series)
const paths = container.querySelectorAll('path')
expect(paths.length).to.be.greaterThan(0)
})
})
describe('Data Visualization', () => {
it('should render data points as glyphs', () => {
const data = createMockChartData(3)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
// Check for circles (glyphs representing data points)
const circles = container.querySelectorAll('circle')
expect(circles.length).to.be.greaterThan(0)
})
it('should render exact number of data points matching data length', () => {
const dataLength = 5
const data = createMockChartData(dataLength)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
// Each data point should render as a circle
const circles = container.querySelectorAll('circle')
expect(circles.length).to.equal(dataLength, `Expected ${dataLength} circles for ${dataLength} data points`)
// Verify each circle has proper attributes
circles.forEach((circle, index) => {
expect(circle.getAttribute('cx')).to.exist
expect(circle.getAttribute('cy')).to.exist
expect(circle.getAttribute('r')).to.equal('3')
expect(circle.getAttribute('fill')).to.exist
})
})
it('should position data points with valid coordinates', () => {
const data = createMockChartData(3)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
const circles = container.querySelectorAll('circle')
circles.forEach((circle) => {
const cx = parseFloat(circle.getAttribute('cx') || '0')
const cy = parseFloat(circle.getAttribute('cy') || '0')
// Coordinates should be valid numbers
expect(cx).to.be.a('number')
expect(cy).to.be.a('number')
expect(isNaN(cx)).to.be.false
expect(isNaN(cy)).to.be.false
// Coordinates should be within chart bounds (positive values)
expect(cx).to.be.greaterThan(0)
expect(cy).to.be.greaterThan(0)
})
})
it('should render line connecting data points', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
// Line series should create a path element
const paths = container.querySelectorAll('path')
expect(paths.length).to.be.greaterThan(0)
})
it('should handle single data point', () => {
const data = [{ x: Date.now(), y: 50 }]
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
const circles = container.querySelectorAll('circle')
expect(circles.length).to.equal(1, 'Single data point should render as one circle')
})
it('should handle large datasets', () => {
const data = createMockChartData(100)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
const circles = container.querySelectorAll('circle')
expect(circles.length).to.equal(100, '100 data points should render as 100 circles')
})
})
describe('Curve Interpolation', () => {
const curveTypes: PlotCurveTypes[] = ['curve', 'linear', 'cubic_basis_spline', 'step_after', 'step_before']
curveTypes.forEach((interpolation) => {
it(`should render with ${interpolation} interpolation`, () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(
<Chart data={data} interpolation={interpolation} />,
{ withTheme: true }
)
expect(container.querySelector('svg')).to.exist
const paths = container.querySelectorAll('path')
expect(paths.length).to.be.greaterThan(0)
})
})
})
describe('Custom Styling', () => {
it('should apply custom color', () => {
const data = createMockChartData(5)
const customColor = '#ff0000'
const { container } = renderWithProviders(
<Chart data={data} color={customColor} />,
{ withTheme: true }
)
// Check if custom color is applied to line or glyphs
const svg = container.querySelector('svg')
expect(svg).to.exist
})
it('should use theme colors when no custom color provided', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
})
})
describe('Custom Domains and Ranges', () => {
it('should render with custom Y range', () => {
const data = createMockChartData(5)
const range: [number, number] = [0, 100]
const { container } = renderWithProviders(
<Chart data={data} range={range} />,
{ withTheme: true }
)
expect(container.querySelector('svg')).to.exist
})
it('should render with custom time range', () => {
const data = createMockChartData(5)
const timeRangeStart = 60000 // 1 minute
const { container } = renderWithProviders(
<Chart data={data} timeRangeStart={timeRangeStart} />,
{ withTheme: true }
)
expect(container.querySelector('svg')).to.exist
})
it('should render with partial Y range (only min)', () => {
const data = createMockChartData(5)
const range: [number?, number?] = [0, undefined]
const { container } = renderWithProviders(
<Chart data={data} range={range} />,
{ withTheme: true }
)
expect(container.querySelector('svg')).to.exist
})
it('should render with partial Y range (only max)', () => {
const data = createMockChartData(5)
const range: [number?, number?] = [undefined, 100]
const { container } = renderWithProviders(
<Chart data={data} range={range} />,
{ withTheme: true }
)
expect(container.querySelector('svg')).to.exist
})
})
describe('Chart Components', () => {
it('should render Y-axis', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
// Y-axis should be present (look for axis group or tick marks)
const svg = container.querySelector('svg')
expect(svg).to.exist
// Axis typically contains text elements for labels
const texts = container.querySelectorAll('text')
expect(texts.length).to.be.greaterThan(0)
})
it('should render X-axis with time labels', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
// X-axis should be present with text labels
const svg = container.querySelector('svg')
expect(svg).to.exist
// X-axis has text labels for timestamps
const texts = container.querySelectorAll('text')
expect(texts.length).to.be.greaterThan(0, 'X-axis and Y-axis should have text labels')
// At least one text element should contain time format (e.g., contains ":")
let hasTimeFormat = false
texts.forEach((text) => {
if (text.textContent && text.textContent.includes(':')) {
hasTimeFormat = true
}
})
expect(hasTimeFormat).to.be.true
})
it('should render both X and Y axes', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
const svg = container.querySelector('svg')
expect(svg).to.exist
// Both axes should render tick marks (lines)
const lines = container.querySelectorAll('line')
expect(lines.length).to.be.greaterThan(0, 'Axes should render tick marks')
// Both axes should have labels (text)
const texts = container.querySelectorAll('text')
expect(texts.length).to.be.greaterThan(2, 'Both axes should have multiple labels')
})
it('should render grid lines', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
// Grid lines are rendered as line elements
const svg = container.querySelector('svg')
expect(svg).to.exist
})
it('should have proper chart margins', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
const svg = container.querySelector('svg')
expect(svg).to.exist
// SVG should have proper dimensions
expect(svg?.getAttribute('width')).to.exist
expect(svg?.getAttribute('height')).to.exist
})
})
describe('Edge Cases', () => {
it('should handle negative values', () => {
const data = [
{ x: Date.now() - 2000, y: -50 },
{ x: Date.now() - 1000, y: -25 },
{ x: Date.now(), y: -75 },
]
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
})
it('should handle zero values', () => {
const data = [
{ x: Date.now() - 2000, y: 0 },
{ x: Date.now() - 1000, y: 0 },
{ x: Date.now(), y: 0 },
]
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
})
it('should handle very large numbers', () => {
const data = [
{ x: Date.now() - 2000, y: 1000000 },
{ x: Date.now() - 1000, y: 2000000 },
{ x: Date.now(), y: 3000000 },
]
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
// Y-axis should abbreviate large numbers
const texts = container.querySelectorAll('text')
expect(texts.length).to.be.greaterThan(0)
})
it('should handle identical values', () => {
const data = [
{ x: Date.now() - 2000, y: 50 },
{ x: Date.now() - 1000, y: 50 },
{ x: Date.now(), y: 50 },
]
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
})
})
describe('Component Props', () => {
it('should accept all valid props without errors', () => {
const data = createMockChartData(5)
const props: ChartProps = {
data,
interpolation: 'curve',
range: [0, 100],
timeRangeStart: 60000,
color: '#00ff00',
}
const { container } = renderWithProviders(<Chart {...props} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
})
it('should work with minimal props', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
})
})
describe('Theme Integration', () => {
it('should render in light theme', () => {
const data = createMockChartData(5)
const { container } = renderWithProviders(<Chart data={data} />, { withTheme: true })
expect(container.querySelector('svg')).to.exist
})
// Note: Testing dark theme would require a custom theme provider
// This demonstrates how the test structure supports theme variations
})
describe('Performance', () => {
it('should memoize component with same props', () => {
const data = createMockChartData(5)
const { rerender } = renderWithProviders(<Chart data={data} />, { withTheme: true })
// Component should not re-render with same props due to React.memo
expect(() => {
rerender(<Chart data={data} />)
}).to.not.throw()
})
it('should handle rapid data updates', () => {
const { rerender, container } = renderWithProviders(
<Chart data={createMockChartData(5)} />,
{ withTheme: true }
)
// Simulate rapid updates
for (let i = 0; i < 10; i++) {
rerender(<Chart data={createMockChartData(5)} />)
}
expect(container.querySelector('svg')).to.exist
})
})
describe('Interactive Data Updates', () => {
it('should dynamically update when data points are added', () => {
// Start with 3 data points
const initialData = createMockChartData(3)
const { rerender, container } = renderWithProviders(
<Chart data={initialData} />,
{ withTheme: true }
)
// Verify initial state: should have 3 data points
const initialCircles = container.querySelectorAll('circle')
expect(initialCircles.length).to.equal(3, 'Should initially render 3 data points')
// Verify each initial circle has valid attributes
initialCircles.forEach((circle, index) => {
const cx = circle.getAttribute('cx')
const cy = circle.getAttribute('cy')
const r = circle.getAttribute('r')
expect(cx).to.exist
expect(cy).to.exist
expect(r).to.equal('3')
expect(parseFloat(cx!)).to.be.a('number').and.not.NaN
expect(parseFloat(cy!)).to.be.a('number').and.not.NaN
})
// Update state: add 2 more data points (total 5)
const updatedData = createMockChartData(5)
rerender(<Chart data={updatedData} />)
// Verify updated state: should now have 5 data points
const updatedCircles = container.querySelectorAll('circle')
expect(updatedCircles.length).to.equal(5, 'Should render 5 data points after update')
// Verify each updated circle has valid attributes
updatedCircles.forEach((circle, index) => {
const cx = circle.getAttribute('cx')
const cy = circle.getAttribute('cy')
const r = circle.getAttribute('r')
const fill = circle.getAttribute('fill')
expect(cx).to.exist
expect(cy).to.exist
expect(r).to.equal('3')
expect(fill).to.exist
expect(parseFloat(cx!)).to.be.a('number').and.not.NaN
expect(parseFloat(cy!)).to.be.a('number').and.not.NaN
expect(parseFloat(cy!)).to.be.greaterThan(0, 'Y coordinate should be positive')
})
// Verify the line path is updated to connect all 5 points
const linePath = container.querySelector('path[stroke]')
expect(linePath).to.exist
expect(linePath!.getAttribute('d')).to.exist
// The path should start with MoveTo (M) command and contain curve/line commands
const pathData = linePath!.getAttribute('d')
expect(pathData).to.include('M') // MoveTo command for first point
// Path may contain 'L' (line) or 'C' (curve) commands depending on interpolation
expect(pathData!.length).to.be.greaterThan(10, 'Path should have substantial data for 5 points')
})
it('should handle data point removal', () => {
// Start with 5 data points
const initialData = createMockChartData(5)
const { rerender, container } = renderWithProviders(
<Chart data={initialData} />,
{ withTheme: true }
)
// Verify initial state
let circles = container.querySelectorAll('circle')
expect(circles.length).to.equal(5, 'Should initially render 5 data points')
// Remove 2 data points (now 3)
const reducedData = createMockChartData(3)
rerender(<Chart data={reducedData} />)
// Verify reduced state
circles = container.querySelectorAll('circle')
expect(circles.length).to.equal(3, 'Should render 3 data points after removal')
})
it('should maintain chart structure during data updates', () => {
const initialData = createMockChartData(3)
const { rerender, container } = renderWithProviders(
<Chart data={initialData} />,
{ withTheme: true }
)
// Verify chart structure exists initially
expect(container.querySelector('svg')).to.exist
expect(container.querySelectorAll('line').length).to.be.greaterThan(0, 'Should have axis/grid lines')
expect(container.querySelectorAll('text').length).to.be.greaterThan(0, 'Should have axis labels')
// Update data
const updatedData = createMockChartData(5)
rerender(<Chart data={updatedData} />)
// Verify chart structure is maintained after update
expect(container.querySelector('svg')).to.exist
expect(container.querySelectorAll('line').length).to.be.greaterThan(0, 'Should still have axis/grid lines')
expect(container.querySelectorAll('text').length).to.be.greaterThan(0, 'Should still have axis labels')
})
})
})
+101 -36
View File
@@ -1,8 +1,7 @@
import '../../react-vis-compat' // React 19 compatibility shim for react-vis
import DateFormatter from '../helper/DateFormatter'
import NoData from './NoData'
import NumberFormatter from '../helper/NumberFormatter'
import React, { memo, useCallback, useRef, useEffect } from 'react'
import React, { memo, useCallback, useMemo } from 'react'
import TooltipComponent from './TooltipComponent'
import { useResizeDetector } from 'react-resize-detector'
import { emphasize, useTheme } from '@mui/material/styles'
@@ -11,8 +10,7 @@ import { PlotCurveTypes } from '../../reducers/Charts'
import { Point, Tooltip } from './Model'
import { useCustomXDomain } from './effects/useCustomXDomain'
import { useCustomYDomain } from './effects/useCustomYDomain'
import 'react-vis/dist/style.css'
const { XYPlot, LineMarkSeries, YAxis, HorizontalGridLines, Hint } = require('react-vis')
import { XYChart, Axis, Grid, LineSeries, GlyphSeries } from '@visx/xychart'
const abbreviate = require('number-abbreviate')
export interface Props {
@@ -23,10 +21,14 @@ export interface Props {
color?: string
}
const CHART_HEIGHT = 150
export default memo((props: Props) => {
const theme = useTheme()
const [tooltip, setTooltip] = React.useState<Tooltip | undefined>()
const [hoveredPoint, setHoveredPoint] = React.useState<Point | undefined>()
const { width = 300, ref } = useResizeDetector()
const chartContainerRef = React.useRef<HTMLDivElement>(null)
const hintFormatter = React.useCallback(
(point: any) => [
@@ -39,14 +41,19 @@ export default memo((props: Props) => {
const onMouseLeave = React.useCallback(() => {
setTooltip(undefined)
setHoveredPoint(undefined)
}, [])
const showTooltip = React.useCallback((point: Point, something: { event: MouseEvent }) => {
if (!something) {
return
}
setTooltip({ point, value: hintFormatter(point), element: something.event.target as any })
}, [])
const showTooltip = React.useCallback(
(point: Point) => {
if (!chartContainerRef.current) {
return
}
setHoveredPoint(point)
setTooltip({ point, value: hintFormatter(point), element: chartContainerRef.current })
},
[hintFormatter]
)
const paletteColor =
theme.palette.mode === 'light' ? theme.palette.secondary.dark : theme.palette.primary.light
@@ -54,47 +61,105 @@ export default memo((props: Props) => {
const highlightSelectedPoint = useCallback(
(point: Point) => {
const highlight = tooltip && tooltip.point.x === point.x && tooltip.point.y === point.y
const highlight = hoveredPoint && hoveredPoint.x === point.x && hoveredPoint.y === point.y
return highlight ? emphasize(color, 0.8) : color
},
[tooltip, color]
[hoveredPoint, color]
)
const formatYAxis = useCallback((num: number) => abbreviate(num), [])
const formatXAxis = useCallback((timestamp: number) => {
const date = new Date(timestamp)
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds = date.getSeconds().toString().padStart(2, '0')
return `${hours}:${minutes}:${seconds}`
}, [])
const xDomain = useCustomXDomain(props)
const yDomain = useCustomYDomain(props)
const data = props.data
const hasData = data.length > 0
const dummyDomain = [-1, 1]
const dummyDomain: [number, number] = [-1, 1]
const dummyData = [{ x: -2, y: -2 }]
const accessors = useMemo(
() => ({
xAccessor: (d: Point) => d.x,
yAccessor: (d: Point) => d.y,
}),
[]
)
return (
<div>
<div ref={ref} style={{ height: '150px', width: '100%', position: 'relative' }}>
<div ref={ref} style={{ height: `${CHART_HEIGHT}px`, width: '100%', position: 'relative' }}>
{data.length === 0 ? <NoData /> : null}
<XYPlot
width={width || 300}
height={180}
yDomain={hasData ? yDomain : dummyDomain}
xDomain={hasData ? xDomain : dummyDomain}
onMouseLeave={onMouseLeave}
>
<HorizontalGridLines />
<YAxis width={45} tickFormat={formatYAxis} />
<LineMarkSeries
color={color}
colorType="literal"
getColor={highlightSelectedPoint}
onValueMouseOver={showTooltip}
size={3}
data={hasData ? data : dummyData}
curve={mapCurveType(props.interpolation)}
/>
<Hint value={{ x: 0, y: 0 }} style={{ pointerEvents: 'none' }}>
<TooltipComponent tooltip={tooltip} />
</Hint>
</XYPlot>
<div ref={chartContainerRef}>
<XYChart
width={width || 300}
height={CHART_HEIGHT}
margin={{ top: 10, right: 10, bottom: 30, left: 50 }}
xScale={{ type: 'time', domain: xDomain || dummyDomain }}
yScale={{ type: 'linear', domain: hasData ? yDomain : dummyDomain }}
onPointerOut={onMouseLeave}
>
<Grid rows={true} columns={false} stroke={theme.palette.divider} strokeOpacity={0.3} />
<Axis
orientation="left"
numTicks={5}
tickFormat={formatYAxis}
stroke={theme.palette.text.secondary}
tickStroke={theme.palette.text.secondary}
tickLabelProps={() => ({ fontSize: 11, fill: theme.palette.text.secondary })}
/>
<Axis
orientation="bottom"
numTicks={4}
tickFormat={formatXAxis}
stroke={theme.palette.text.secondary}
tickStroke={theme.palette.text.secondary}
tickLabelProps={() => ({ fontSize: 10, fill: theme.palette.text.secondary, textAnchor: 'middle' })}
/>
<LineSeries
dataKey="line"
data={hasData ? data : dummyData}
xAccessor={accessors.xAccessor}
yAccessor={accessors.yAccessor}
stroke={color}
strokeWidth={2}
curve={mapCurveType(props.interpolation)}
onPointerMove={(datum) => {
if (datum && datum.datum) {
const point = datum.datum as Point
showTooltip(point)
}
}}
/>
<GlyphSeries
dataKey="points"
data={hasData ? data : dummyData}
xAccessor={accessors.xAccessor}
yAccessor={accessors.yAccessor}
renderGlyph={(glyphProps) => {
const point = glyphProps.datum as Point
const pointColor = highlightSelectedPoint(point)
return (
<circle
cx={glyphProps.x}
cy={glyphProps.y}
r={3}
fill={pointColor}
/>
)
}}
/>
</XYChart>
</div>
{/* Custom tooltip outside of visx to maintain exact same appearance */}
<TooltipComponent tooltip={tooltip} />
</div>
</div>
)
@@ -3,8 +3,22 @@ import { Props } from '../Chart'
export function useCustomXDomain(props: Props): [number, number] | undefined {
return useMemo(() => {
if (props.data.length === 0) {
return undefined
}
const lastDataPoint = [...props.data].sort((a, b) => b.x - a.x)[0]
const lastDataDate = lastDataPoint ? lastDataPoint.x : Date.now()
return props.timeRangeStart ? [Date.now() - props.timeRangeStart, lastDataDate] : undefined
if (props.timeRangeStart) {
// Custom time range mode
return [Date.now() - props.timeRangeStart, lastDataDate]
} else {
// Auto-calculate from data (like react-vis did)
const xValues = props.data.map(d => d.x)
const minX = Math.min(...xValues)
const maxX = Math.max(...xValues)
return [minX, maxX]
}
}, [props.data, props.timeRangeStart])
}
+8 -6
View File
@@ -1,17 +1,19 @@
import { PlotCurveTypes } from '../../reducers/Charts'
import * as d3Shape from 'd3-shape'
export function mapCurveType(type: PlotCurveTypes | undefined) {
switch (type) {
case 'curve':
return 'curveMonotoneX'
return d3Shape.curveMonotoneX
case 'linear':
return 'curveLinear'
return d3Shape.curveLinear
case 'cubic_basis_spline':
return 'curveBasis'
return d3Shape.curveBasis
case 'step_after':
return 'curveStepAfter'
return d3Shape.curveStepAfter
case 'step_before':
return 'curveStepBefore'
return d3Shape.curveStepBefore
default:
return 'curveMonotoneX'
return d3Shape.curveMonotoneX
}
}
+41 -11
View File
@@ -46,10 +46,15 @@ function mapWidth(width: 'big' | 'medium' | 'small' | undefined, calculatedSpaci
}
}
// Helper function to generate unique keys for charts
const getChartKey = (chart: ChartParameters) => `${chart.topic}-${chart.dotPath || ''}`
function ChartPanel(props: Props) {
const chartsInView = props.charts.count()
const [spacing, setSpacing] = React.useState(spacingForChartCount(chartsInView))
const nodeRefsMap = React.useRef<Map<string, React.RefObject<HTMLDivElement>>>(new Map())
React.useEffect(() => {
props.actions.chart.loadCharts()
@@ -65,17 +70,42 @@ function ChartPanel(props: Props) {
}
}, [chartsInView])
const charts = props.charts.map(chartParameters => (
<CSSTransition
key={`${chartParameters.topic}-${chartParameters.dotPath || ''}`}
timeout={{ enter: 500, exit: 500 }}
classNames="example"
>
<Grid item xs={mapWidth(chartParameters.width, spacing)}>
<ChartWithTreeNode tree={props.tree} parameters={chartParameters} />
</Grid>
</CSSTransition>
))
// Clean up refs for removed charts
React.useEffect(() => {
const currentKeys = new Set(props.charts.map(getChartKey).toArray())
const refsToDelete: string[] = []
nodeRefsMap.current.forEach((_, key) => {
if (!currentKeys.has(key)) {
refsToDelete.push(key)
}
})
refsToDelete.forEach(key => nodeRefsMap.current.delete(key))
}, [props.charts])
const charts = props.charts.map(chartParameters => {
const key = getChartKey(chartParameters)
// Get or create a ref for this specific chart
if (!nodeRefsMap.current.has(key)) {
nodeRefsMap.current.set(key, React.createRef<HTMLDivElement>())
}
const nodeRef = nodeRefsMap.current.get(key)!
return (
<CSSTransition
key={key}
timeout={{ enter: 500, exit: 500 }}
classNames="example"
nodeRef={nodeRef}
>
<Grid item xs={mapWidth(chartParameters.width, spacing)} ref={nodeRef}>
<ChartWithTreeNode tree={props.tree} parameters={chartParameters} />
</Grid>
</CSSTransition>
)
})
return (
<div className={props.classes.container}>
-28
View File
@@ -1,28 +0,0 @@
/**
* React 19 compatibility shim for react-vis
*
* react-vis uses React internals that were removed in React 19.
* This shim adds back the missing internals to maintain compatibility.
*/
import * as React from 'react'
// Add missing React internals that react-vis expects
if (typeof React !== 'undefined') {
const internals = (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
if (internals) {
// ReactCurrentOwner was removed in React 19 but react-vis expects it
if (!internals.ReactCurrentOwner) {
internals.ReactCurrentOwner = {
current: null,
}
}
// ReactCurrentDispatcher compatibility
if (!internals.ReactCurrentDispatcher) {
internals.ReactCurrentDispatcher = {
current: null,
}
}
}
}
+166
View File
@@ -0,0 +1,166 @@
/**
* Generic test utilities for React component testing
*
* This file provides a reusable testing setup that can be used across all component tests.
* It includes:
* - Custom render function with theme and Redux providers
* - Common test utilities and matchers
* - Mock setup helpers
*/
import React from 'react'
import { render, RenderOptions, RenderResult } from '@testing-library/react'
import { ThemeProvider, createTheme } from '@mui/material/styles'
import { Provider } from 'react-redux'
import { configureStore } from '@reduxjs/toolkit'
// Setup JSDOM environment for tests
import 'jsdom-global/register'
// Setup global mocks
mockResizeObserver()
/**
* Helper to create a ResizeObserver mock
* This is set up globally for all tests
*/
export function mockResizeObserver() {
const MockResizeObserver = class ResizeObserver {
observe() {}
unobserve() {}
disconnect() {}
} as any
if (typeof global.ResizeObserver === 'undefined') {
global.ResizeObserver = MockResizeObserver
}
if (typeof window !== 'undefined' && typeof window.ResizeObserver === 'undefined') {
(window as any).ResizeObserver = MockResizeObserver
}
}
/**
* Default theme for testing
*/
const defaultTheme = createTheme({
palette: {
mode: 'light',
primary: {
main: '#1976d2',
light: '#42a5f5',
dark: '#1565c0',
},
secondary: {
main: '#9c27b0',
light: '#ba68c8',
dark: '#7b1fa2',
},
text: {
primary: 'rgba(0, 0, 0, 0.87)',
secondary: 'rgba(0, 0, 0, 0.6)',
},
divider: 'rgba(0, 0, 0, 0.12)',
},
})
/**
* Default Redux store for testing
*/
const createTestStore = (initialState = {}) => {
return configureStore({
reducer: {
// Add minimal reducers as needed
charts: (state = { charts: [] }) => state,
connection: (state = {}) => state,
},
preloadedState: initialState,
})
}
/**
* Custom render options
*/
interface CustomRenderOptions extends Omit<RenderOptions, 'wrapper'> {
theme?: typeof defaultTheme
store?: ReturnType<typeof createTestStore>
withTheme?: boolean
withRedux?: boolean
}
/**
* Custom render function that wraps components with necessary providers
*
* @example
* ```tsx
* import { renderWithProviders } from '../utils/spec/testUtils'
*
* describe('MyComponent', () => {
* it('renders correctly', () => {
* const { getByText } = renderWithProviders(<MyComponent />)
* expect(getByText('Hello')).toBeDefined()
* })
* })
* ```
*/
export function renderWithProviders(
ui: React.ReactElement,
{
theme = defaultTheme,
store = createTestStore(),
withTheme = true,
withRedux = false,
...renderOptions
}: CustomRenderOptions = {}
): RenderResult {
let Wrapper: React.FC<{ children: React.ReactNode }>
if (withRedux && withTheme) {
Wrapper = ({ children }) => (
<Provider store={store}>
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
</Provider>
)
} else if (withRedux) {
Wrapper = ({ children }) => (
<Provider store={store}>
{children}
</Provider>
)
} else if (withTheme) {
Wrapper = ({ children }) => (
<ThemeProvider theme={theme}>
{children}
</ThemeProvider>
)
} else {
Wrapper = ({ children }) => <>{children}</>
}
return render(ui, { wrapper: Wrapper, ...renderOptions })
}
/**
* Helper to create mock chart data
*/
export function createMockChartData(count: number = 10): Array<{ x: number; y: number }> {
const now = Date.now()
return Array.from({ length: count }, (_, i) => ({
x: now - (count - i) * 1000,
y: Math.random() * 100,
}))
}
/**
* Helper to wait for async operations
*/
export const waitFor = async (ms: number = 100) => {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* Re-export everything from @testing-library/react for convenience
*/
export * from '@testing-library/react'
export { default as userEvent } from '@testing-library/user-event'
-4
View File
@@ -54,10 +54,6 @@ export default {
// Point to browser event bus when importing from '../../../../events'
resource.request = path.resolve(__dirname, 'src', 'browserEventBus.ts')
}),
// Replace EventSystem/EventBus directly as well
new webpack.NormalModuleReplacementPlugin(/events[\\/]EventSystem[\\/]EventBus$/, resource => {
resource.request = path.resolve(__dirname, 'src', 'browserEventBus.ts')
}),
// Exclude IPC-based EventBus files completely
new webpack.IgnorePlugin({
resourceRegExp: /IpcRendererEventBus\.ts$/,
+771 -283
View File
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -1,5 +1,8 @@
export * from './Events'
export * from './EventsV2'
export * from './EventSystem/EventDispatcher'
export * from './EventSystem/EventBus'
// EventBus exports removed - this file contains Electron-specific imports
// which should not be loaded in server/browser mode
// Electron code should import directly from './EventSystem/EventBus'
// export * from './EventSystem/EventBus'
export * from './EventSystem/EventBusInterface'
+2 -2
View File
@@ -118,7 +118,7 @@
"chai": "^4.5.0",
"cspell": "^8.19.4",
"electron": "39.2.7",
"electron-builder": "^26.0.12",
"electron-builder": "^26.4.0",
"mocha": "^10.8.2",
"mustache": "^4.2.0",
"npm-run-all": "^4.1.5",
@@ -144,7 +144,7 @@
"debug": "^4.4.3",
"dot-prop": "^5.3.0",
"electron-log": "^5.4.3",
"electron-updater": "^6.6.2",
"electron-updater": "^6.7.3",
"express": "^5.2.1",
"express-rate-limit": "^8.2.1",
"express-validator": "^7.3.1",
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
set -e
# Read scenes.json and cut video into segments as GIFs
if [ ! -f "scenes.json" ]; then
echo "scenes.json not found"
exit 1
fi
if [ ! -f "ui-test.mp4" ]; then
echo "ui-test.mp4 not found"
exit 1
fi
echo "Cutting video into GIF segments based on scenes.json..."
GIF_SCALE="1024"
# Parse scenes.json and cut video segments as GIFs
node -e "
const fs = require('fs');
const { spawn } = require('child_process');
const scenes = JSON.parse(fs.readFileSync('scenes.json', 'utf8'));
console.log('Creating GIF segments...');
// Sanitize scene name to prevent path traversal and command injection
function sanitizeName(name) {
// Remove any characters that aren't alphanumeric, dash, or underscore
return name.replace(/[^a-zA-Z0-9_-]/g, '-');
}
async function cutSegmentAsGif(scene, index) {
const safeName = sanitizeName(scene.name);
const segmentName = \`segment-\${String(index + 1).padStart(2, '0')}-\${safeName}\`;
const paletteFile = \`\${segmentName}-palette.png\`;
const outputFile = \`\${segmentName}.gif\`;
const startTime = scene.start / 1000; // Convert ms to seconds
const duration = scene.duration / 1000; // Convert ms to seconds
console.log(\`Creating \${outputFile} (start: \${startTime}s, duration: \${duration}s)\`);
// Step 1: Generate palette for this segment
await new Promise((resolve, reject) => {
const ffmpeg = spawn('ffmpeg', [
'-y',
'-ss', startTime.toString(),
'-t', duration.toString(),
'-i', 'ui-test.mp4',
'-vf', 'fps=10,scale=${process.env.GIF_SCALE || 1024}:-1:flags=lanczos,palettegen',
paletteFile
]);
ffmpeg.on('close', (code) => {
if (code === 0) {
resolve();
} else {
console.error(\`Failed to create palette for \${outputFile}\`);
reject(new Error(\`ffmpeg palette generation exited with code \${code}\`));
}
});
});
// Step 2: Create GIF using the palette
await new Promise((resolve, reject) => {
const ffmpeg = spawn('ffmpeg', [
'-y',
'-ss', startTime.toString(),
'-t', duration.toString(),
'-i', 'ui-test.mp4',
'-i', paletteFile,
'-filter_complex', 'fps=10,scale=${process.env.GIF_SCALE || 1024}:-1:flags=lanczos[x];[x][1:v]paletteuse',
outputFile
]);
ffmpeg.on('close', (code) => {
// Clean up palette file
try {
fs.unlinkSync(paletteFile);
} catch (e) {
// Ignore cleanup errors
}
if (code === 0) {
resolve();
} else {
console.error(\`Failed to create \${outputFile}\`);
reject(new Error(\`ffmpeg GIF creation exited with code \${code}\`));
}
});
});
}
(async () => {
for (let i = 0; i < scenes.length; i++) {
await cutSegmentAsGif(scenes[i], i);
}
console.log('All GIF segments created successfully');
})().catch(err => {
console.error(err);
process.exit(1);
});
"
echo "Video segments created successfully"
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env node
const fs = require('fs');
// Read scenes.json
const scenes = JSON.parse(fs.readFileSync('scenes.json', 'utf8'));
// Get base URL from command line arguments
const baseUrl = process.argv[2];
if (!baseUrl) {
console.error('Usage: node generateMarkdownSummary.js <base-url>');
process.exit(1);
}
// Sanitize scene name to prevent path traversal
function sanitizeName(name) {
// Remove any characters that aren't alphanumeric, dash, or underscore
return name.replace(/[^a-zA-Z0-9_-]/g, '-');
}
// Generate markdown
let markdown = '## 🎬 Demo Video Generated\n\n';
markdown += `### Full Video\n\n`;
markdown += `[📥 Download Full Video (MP4)](${baseUrl}/ui-test.mp4) | [GIF](${baseUrl}/ui-test.gif)\n\n`;
markdown += `---\n\n`;
markdown += `### 📑 Video Segments\n\n`;
markdown += `<details>\n`;
markdown += `<summary>Click to expand segments</summary>\n\n`;
scenes.forEach((scene, index) => {
const safeName = sanitizeName(scene.name);
const segmentFile = `segment-${String(index + 1).padStart(2, '0')}-${safeName}.gif`;
const title = scene.title || scene.name;
const duration = (scene.duration / 1000).toFixed(1);
markdown += `<details>\n`;
markdown += `<summary><strong>${index + 1}. ${title}</strong> (${duration}s)</summary>\n\n`;
markdown += `![${title}](${baseUrl}/${segmentFile})\n\n`;
markdown += `</details>\n\n`;
});
markdown += `</details>\n\n`;
markdown += `_Videos will expire in 90 days._`;
console.log(markdown);
+9
View File
@@ -25,3 +25,12 @@ rm ffmpeg_info palette*.png qrawvideorgb24.yuv
mv app.mp4 ui-test.mp4
mv app720.gif ui-test.gif
# Cut video into segments based on scenes.json
echo "Cutting video into segments..."
if [ -f "scenes.json" ]; then
./scripts/cutVideoSegments.sh
else
echo "Warning: scenes.json not found, skipping segment creation"
fi
+5 -5
View File
@@ -10,8 +10,8 @@
# MQTT_EXPLORER_PASSWORD - Password for browser authentication (default: test123)
# PORT - Server port (default: 3000)
# BROWSER_MODE_URL - URL for browser tests (set automatically)
# MQTT_BROKER_HOST - MQTT broker host for tests (default: 127.0.0.1)
# MQTT_BROKER_PORT - MQTT broker port for tests (default: 1883)
# TESTS_MQTT_BROKER_HOST - MQTT broker host for tests (required, default: 127.0.0.1)
# TESTS_MQTT_BROKER_PORT - MQTT broker port for tests (default: 1883)
#
set -e
@@ -52,10 +52,10 @@ done
# Run browser tests
export BROWSER_MODE_URL="http://localhost:${PORT}"
export MQTT_BROKER_HOST="${MQTT_BROKER_HOST:-127.0.0.1}"
export MQTT_BROKER_PORT="${MQTT_BROKER_PORT:-1883}"
export TESTS_MQTT_BROKER_HOST="${TESTS_MQTT_BROKER_HOST:-127.0.0.1}"
export TESTS_MQTT_BROKER_PORT="${TESTS_MQTT_BROKER_PORT:-1883}"
echo "Using MQTT broker at $MQTT_BROKER_HOST:$MQTT_BROKER_PORT"
echo "Using MQTT broker at $TESTS_MQTT_BROKER_HOST:$TESTS_MQTT_BROKER_PORT"
yarn test:browser
TEST_EXIT_CODE=$?
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env node
/**
* Integration test for video segmentation workflow
*
* This test validates:
* 1. SceneBuilder generates scenes with titles
* 2. Scenes are saved to scenes.json
* 3. cutVideoSegments script processes scenes correctly
* 4. generateMarkdownSummary creates proper output
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
const { execSync } = require('child_process');
function testSceneGeneration() {
console.log('Testing scene generation...');
// Check if compiled files exist
const distPath = path.join(__dirname, '../dist/src/spec/SceneBuilder.js');
if (!fs.existsSync(distPath)) {
console.log(' ⚠ Skipping test - TypeScript not compiled. Run "npx tsc" first.');
return null;
}
// Import and test SceneBuilder
const SceneBuilder = require('../dist/src/spec/SceneBuilder').SceneBuilder;
const SCENE_TITLES = require('../dist/src/spec/SceneBuilder').SCENE_TITLES;
// Verify SCENE_TITLES exist
const titleKeys = Object.keys(SCENE_TITLES);
if (titleKeys.length === 0) {
throw new Error('SCENE_TITLES is empty');
}
console.log(` ✓ Found ${titleKeys.length} scene titles`);
// Create a sample scene
const builder = new SceneBuilder();
const testScenes = [
{ name: 'connect', start: 0, stop: 1000, duration: 1000, title: SCENE_TITLES.connect },
{ name: 'numeric_plots', start: 1000, stop: 2000, duration: 1000, title: SCENE_TITLES.numeric_plots },
{ name: 'end', start: 2000, stop: 3000, duration: 1000, title: SCENE_TITLES.end },
];
// Manually populate scenes for testing
builder.scenes = testScenes;
// Save to JSON
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-workflow-'));
const scenesPath = path.join(tempDir, 'scenes.json');
fs.writeFileSync(scenesPath, JSON.stringify(builder.scenes, null, 2));
console.log(` ✓ Saved scenes to ${scenesPath}`);
return { tempDir, scenesPath, scenes: builder.scenes };
}
function testCuttingLogic(tempDir, scenes) {
console.log('Testing cutting logic...');
// Create dummy video file
const videoPath = path.join(tempDir, 'ui-test.mp4');
fs.writeFileSync(videoPath, 'dummy video content');
// Process scenes like the cutting script would (now creates GIFs)
const expectedSegments = [];
scenes.forEach((scene, index) => {
const outputFile = `segment-${String(index + 1).padStart(2, '0')}-${scene.name}.gif`;
const startTime = scene.start / 1000;
const duration = scene.duration / 1000;
expectedSegments.push({
filename: outputFile,
start: startTime,
duration: duration,
title: scene.title
});
});
console.log(` ✓ Processed ${expectedSegments.length} segments`);
// Verify segment naming
expectedSegments.forEach(seg => {
if (!seg.filename.match(/^segment-\d{2}-.+\.gif$/)) {
throw new Error(`Invalid segment filename: ${seg.filename}`);
}
});
console.log(' ✓ All segment filenames are valid');
return expectedSegments;
}
function testMarkdownGeneration(tempDir) {
console.log('Testing markdown generation...');
const baseUrl = 'https://example.com/test-pr-123';
const scenesPath = path.join(tempDir, 'scenes.json');
// Run markdown generation script
const originalCwd = process.cwd();
process.chdir(tempDir);
let markdown;
try {
markdown = execSync(
`node "${path.join(__dirname, 'generateMarkdownSummary.js')}" "${baseUrl}"`,
{ encoding: 'utf8' }
);
} finally {
process.chdir(originalCwd);
}
// Verify markdown content
if (!markdown.includes('## 🎬 Demo Video Generated')) {
throw new Error('Markdown missing title');
}
if (!markdown.includes('### Full Video')) {
throw new Error('Markdown missing full video section');
}
if (!markdown.includes('### 📑 Video Segments')) {
throw new Error('Markdown missing segments section');
}
if (!markdown.includes('<details>')) {
throw new Error('Markdown missing collapsible sections');
}
if (!markdown.includes('Connecting to MQTT Broker')) {
throw new Error('Markdown missing scene title');
}
if (!markdown.includes(baseUrl)) {
throw new Error('Markdown missing base URL');
}
console.log(' ✓ Markdown structure is valid');
console.log(' ✓ Markdown contains all required sections');
return markdown;
}
function cleanup(tempDir) {
console.log('Cleaning up...');
fs.rmSync(tempDir, { recursive: true, force: true });
console.log(' ✓ Temporary files removed');
}
// Run tests
try {
console.log('=== Video Segmentation Integration Test ===\n');
const result = testSceneGeneration();
if (!result) {
console.log('\n=== Test Skipped (TypeScript not compiled) ===\n');
console.log('Run "npx tsc" to compile TypeScript before running this test.');
process.exit(0);
}
const { tempDir, scenesPath, scenes } = result;
const segments = testCuttingLogic(tempDir, scenes);
const markdown = testMarkdownGeneration(tempDir);
cleanup(tempDir);
console.log('\n=== All Tests Passed ✓ ===\n');
// Display sample output
console.log('Sample Markdown Output:');
console.log('─'.repeat(60));
console.log(markdown.split('\n').slice(0, 25).join('\n'));
console.log('...');
console.log('─'.repeat(60));
process.exit(0);
} catch (error) {
console.error('\n✗ Test failed:', error.message);
console.error(error.stack);
process.exit(1);
}
+2 -1
View File
@@ -19,7 +19,8 @@ import {
import { shouldAutoUpdate, handleAutoUpdate } from './autoUpdater'
import { registerCrashReporter } from './registerCrashReporter'
import { makeOpenDialogRpc, makeSaveDialogRpc } from '../events/OpenDialogRequest'
import { backendRpc, backendEvents, getAppVersion, writeToFile, readFromFile } from '../events'
import { getAppVersion, writeToFile, readFromFile } from '../events'
import { backendRpc, backendEvents } from '../events/EventSystem/EventBus'
import { RpcEvents } from '../events/EventsV2'
registerCrashReporter()
+50
View File
@@ -0,0 +1,50 @@
import { expect } from 'chai'
import { SceneBuilder, SCENE_TITLES } from './SceneBuilder'
describe('SceneBuilder', () => {
it('should record scenes with titles', async () => {
const builder = new SceneBuilder()
await builder.record('connect', async () => {
// Simulate some work
await new Promise(resolve => setTimeout(resolve, 100))
})
expect(builder.scenes).to.have.length(1)
expect(builder.scenes[0].name).to.equal('connect')
expect(builder.scenes[0].title).to.equal('Connecting to MQTT Broker')
expect(builder.scenes[0].duration).to.be.greaterThan(90)
})
it('should have titles for all scene types', () => {
const sceneNames = Object.keys(SCENE_TITLES)
expect(sceneNames.length).to.be.greaterThan(0)
// Verify each scene has a non-empty title
sceneNames.forEach(name => {
expect(SCENE_TITLES[name as keyof typeof SCENE_TITLES]).to.be.a('string')
expect(SCENE_TITLES[name as keyof typeof SCENE_TITLES].length).to.be.greaterThan(0)
})
})
it('should record multiple scenes in sequence', async () => {
const builder = new SceneBuilder()
await builder.record('connect', async () => {
await new Promise(resolve => setTimeout(resolve, 50))
})
await builder.record('numeric_plots', async () => {
await new Promise(resolve => setTimeout(resolve, 50))
})
expect(builder.scenes).to.have.length(2)
expect(builder.scenes[0].name).to.equal('connect')
expect(builder.scenes[0].title).to.equal('Connecting to MQTT Broker')
expect(builder.scenes[1].name).to.equal('numeric_plots')
expect(builder.scenes[1].title).to.equal('Plot Topic History')
// Second scene should start at or after first one ends
expect(builder.scenes[1].start).to.be.at.least(builder.scenes[0].stop)
})
})
+20
View File
@@ -3,6 +3,7 @@ export interface Scene {
start: number
stop: number
duration: number
title?: string
}
export type SceneNames =
@@ -22,6 +23,24 @@ export type SceneNames =
| 'sparkplugb-decoding'
| 'end'
export const SCENE_TITLES: Record<SceneNames, string> = {
connect: 'Connecting to MQTT Broker',
topic_updates: 'Topic Updates',
numeric_plots: 'Plot Topic History',
'json-formatting': 'Formatted Messages',
diffs: 'Diff Capability',
publish_topic: 'Publish Topics',
json_formatting_publish: 'JSON Formatting Publish',
clipboard: 'Copy to Clipboard',
topic_filter: 'Search Topic Hierarchy',
delete_retained_topics: 'Delete Retained Topics',
settings: 'Settings',
customize_subscriptions: 'Customize Subscriptions',
keyboard_shortcuts: 'Keyboard Shortcuts',
'sparkplugb-decoding': 'SparkplugB Decoding',
end: 'The End',
}
export class SceneBuilder {
public scenes: Array<Scene> = []
public offset = Date.now()
@@ -36,6 +55,7 @@ export class SceneBuilder {
start,
stop,
duration: stop - start,
title: SCENE_TITLES[name],
})
}
}
+4 -2
View File
@@ -48,7 +48,9 @@ setTimeout(
const runningUiTestOnCi = os.platform() === 'darwin' ? [] : ['--runningUiTestOnCi']
async function doStuff() {
console.log('Waiting for MQTT Broker on port 1880 (no auth)')
const brokerHost = process.env.TESTS_MQTT_BROKER_HOST || '127.0.0.1'
const brokerPort = process.env.TESTS_MQTT_BROKER_PORT || '1883'
console.log(`Waiting for MQTT Broker at ${brokerHost}:${brokerPort} (no auth)`)
await mockMqtt()
console.log('Starting playwright/electron')
@@ -73,7 +75,7 @@ async function doStuff() {
const scenes = new SceneBuilder()
await scenes.record('connect', async () => {
await connectTo('127.0.0.1', page)
await connectTo(brokerHost, page)
await MockSparkplug.run() // Start sparkplug client after connect or birth topics will be missed
await sleep(1000)
})
+2 -1
View File
@@ -55,7 +55,8 @@ describe('expandTopic UI Helper - Isolated Test', function () {
await page.locator('//label[contains(text(), "Host")]/..//input').waitFor({ timeout: 10000 })
console.log('Connecting to MQTT broker...')
await connectTo('127.0.0.1', page)
const brokerHost = process.env.TESTS_MQTT_BROKER_HOST || '127.0.0.1'
await connectTo(brokerHost, page)
await sleep(3000) // Give time for topics to load
console.log('Setup complete')
})
+4 -2
View File
@@ -15,7 +15,9 @@ process.on('unhandledRejection' as any, (error: Error | any) => {
const runningUiTestOnCi = os.platform() === 'darwin' ? [] : ['--runningUiTestOnCi']
async function doStuff() {
console.log('Waiting for MQTT Broker on port 1880 (no auth)')
const brokerHost = process.env.TESTS_MQTT_BROKER_HOST || '127.0.0.1'
const brokerPort = process.env.TESTS_MQTT_BROKER_PORT || '1883'
console.log(`Waiting for MQTT Broker at ${brokerHost}:${brokerPort} (no auth)`)
await mockMqtt()
console.log('Starting playwright/electron')
@@ -39,7 +41,7 @@ async function doStuff() {
await createFakeMousePointer(browser)
// Wait for Username input to be visible
await browser.locator('//label[contains(text(), "Username")]/..//input')
await connectTo('127.0.0.1', browser)
await connectTo(brokerHost, browser)
stopMqttUpdates()
await sleep(1000, true)
const heapDump = await getHeapDump(browser)
+3 -4
View File
@@ -16,15 +16,14 @@ export async function createTestMock(): Promise<mqtt.MqttClient> {
return mqttClient
}
// Use MQTT_BROKER_HOST from environment, default to localhost
const brokerHost = process.env.MQTT_BROKER_HOST || '127.0.0.1'
const brokerPort = process.env.MQTT_BROKER_PORT || '1883'
// Use TESTS_MQTT_BROKER_HOST from environment, default to localhost
const brokerHost = process.env.TESTS_MQTT_BROKER_HOST || '127.0.0.1'
const brokerPort = process.env.TESTS_MQTT_BROKER_PORT || '1883'
const brokerUrl = `mqtt://${brokerHost}:${brokerPort}`
console.log(`Connecting to MQTT broker at ${brokerUrl}`)
return new Promise((resolve, reject) => {
console.log('Connecting to MQTT broker at mqtt://127.0.0.1:1883...')
const client = mqtt.connect(brokerUrl, {
username: '',
password: '',
+8 -1
View File
@@ -11,7 +11,14 @@ function startServer(): Promise<mqtt.MqttClient> {
function connectMqtt(): Promise<mqtt.MqttClient> {
return new Promise(resolve => {
const client = mqtt.connect('mqtt://127.0.0.1:1883', {
// Use TESTS_MQTT_BROKER_HOST from environment, default to localhost
const brokerHost = process.env.TESTS_MQTT_BROKER_HOST || '127.0.0.1'
const brokerPort = process.env.TESTS_MQTT_BROKER_PORT || '1883'
const brokerUrl = `mqtt://${brokerHost}:${brokerPort}`
console.log(`Connecting to MQTT broker at ${brokerUrl}`)
const client = mqtt.connect(brokerUrl, {
username: '',
password: '',
})
+3 -1
View File
@@ -23,8 +23,10 @@ export interface MockSparkplugClient {
}
const sample = (function () {
const brokerHost = process.env.TESTS_MQTT_BROKER_HOST || '127.0.0.1'
const brokerPort = process.env.TESTS_MQTT_BROKER_PORT || '1883'
let config = {
serverUrl: 'tcp://127.0.0.1:1883',
serverUrl: `tcp://${brokerHost}:${brokerPort}`,
username: '',
password: '',
groupId: 'Sparkplug Devices',
+3 -3
View File
@@ -32,7 +32,7 @@ import type { MqttClient } from 'mqtt'
* - Handle MQTT asynchronous operations properly
*
* Prerequisites:
* - MQTT broker running (default: localhost:1883, configurable via MQTT_BROKER_HOST and MQTT_BROKER_PORT)
* - MQTT broker running (default: localhost:1883, configurable via TESTS_MQTT_BROKER_HOST and TESTS_MQTT_BROKER_PORT)
* - Application built with `yarn build`
*/
// tslint:disable:only-arrow-functions ter-prefer-arrow-callback no-unused-expression
@@ -125,8 +125,8 @@ describe('MQTT Explorer Comprehensive UI Tests', function () {
page = await electronApp.firstWindow({ timeout: 30000 })
await page.locator('//label[contains(text(), "Host")]/..//input').waitFor({ timeout: 10000 })
// Use MQTT_BROKER_HOST from environment, default to localhost
const brokerHost = process.env.MQTT_BROKER_HOST || '127.0.0.1'
// Use TESTS_MQTT_BROKER_HOST from environment, default to localhost
const brokerHost = process.env.TESTS_MQTT_BROKER_HOST || '127.0.0.1'
console.log(`Connecting to MQTT broker at ${brokerHost}...`)
await connectTo(brokerHost, page)
await sleep(3000) // Give time for all topics to load
+1 -1
View File
@@ -134,7 +134,7 @@ describe('MQTT Explorer UI Tests', function () {
}
console.log('Connecting to MQTT broker...')
const brokerHost = process.env.MQTT_BROKER_HOST || '127.0.0.1'
const brokerHost = process.env.TESTS_MQTT_BROKER_HOST || '127.0.0.1'
await connectTo(brokerHost, page)
await sleep(3000) // Give time for topics to load
console.log('Setup complete')
+1
View File
@@ -28,6 +28,7 @@
"src/spec/ui-tests-comprehensive.spec.ts",
"src/spec/expandTopic.spec.ts",
"src/spec/security-tests.spec.ts",
"src/spec/SceneBuilder.spec.ts",
"scripts/*.ts"
],
"exclude": ["node_modules"]
+254 -142
View File
@@ -625,16 +625,7 @@
ajv "^6.12.0"
ajv-keywords "^3.4.1"
"@electron/asar@3.2.18":
version "3.2.18"
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.18.tgz#fa607f829209bab8b9e0ce6658d3fe81b2cba517"
integrity sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==
dependencies:
commander "^5.0.0"
glob "^7.1.6"
minimatch "^3.0.4"
"@electron/asar@^3.2.7":
"@electron/asar@3.4.1", "@electron/asar@^3.3.1":
version "3.4.1"
resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.4.1.tgz#4e9196a4b54fba18c56cd8d5cac67c5bdc588065"
integrity sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==
@@ -667,7 +658,7 @@
optionalDependencies:
global-agent "^3.0.0"
"@electron/node-gyp@10.2.0-electron.1", "@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2":
"@electron/node-gyp@10.2.0-electron.1":
version "10.2.0-electron.1"
resolved "https://registry.yarnpkg.com/@electron/node-gyp/-/node-gyp-10.2.0-electron.1.tgz#ca5f125dcd0ffb275797c0c418c0d64005e0f815"
integrity sha512-YdpRE6qSNYyf7gBv1LBDc8OAs8f/mZthzM1k4pFzodNq8dBGf64MWC5Bq8VVlgdafjQXLpINHvtRAUC9uinoqw==
@@ -700,10 +691,10 @@
debug "^4.4.0"
promise-retry "^2.0.1"
"@electron/osx-sign@1.3.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@electron/osx-sign/-/osx-sign-1.3.1.tgz#faf7eeca7ca004a6be541dc4cf7a1bd59ec59b1c"
integrity sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw==
"@electron/osx-sign@1.3.3":
version "1.3.3"
resolved "https://registry.yarnpkg.com/@electron/osx-sign/-/osx-sign-1.3.3.tgz#af751510488318d9f7663694af85819690d75583"
integrity sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==
dependencies:
compare-version "^0.1.2"
debug "^4.3.4"
@@ -712,32 +703,32 @@
minimist "^1.2.6"
plist "^3.0.5"
"@electron/rebuild@3.7.0":
version "3.7.0"
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.7.0.tgz#82e20c467ddedbb295d7f641592c52e68c141e9f"
integrity sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw==
"@electron/rebuild@4.0.1":
version "4.0.1"
resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-4.0.1.tgz#0620d5bb71a0b8b09a86fb9fa979244e1fcc10bf"
integrity sha512-iMGXb6Ib7H/Q3v+BKZJoETgF9g6KMNZVbsO4b7Dmpgb5qTFqyFTzqW9F3TOSHdybv2vKYKzSS9OiZL+dcJb+1Q==
dependencies:
"@electron/node-gyp" "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2"
"@malept/cross-spawn-promise" "^2.0.0"
chalk "^4.0.0"
debug "^4.1.1"
detect-libc "^2.0.1"
fs-extra "^10.0.0"
got "^11.7.0"
node-abi "^3.45.0"
node-api-version "^0.2.0"
graceful-fs "^4.2.11"
node-abi "^4.2.0"
node-api-version "^0.2.1"
node-gyp "^11.2.0"
ora "^5.1.0"
read-binary-file-arch "^1.0.6"
semver "^7.3.5"
tar "^6.0.5"
yargs "^17.0.1"
"@electron/universal@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-2.0.1.tgz#7b070ab355e02957388f3dbd68e2c3cd08c448ae"
integrity sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA==
"@electron/universal@2.0.3":
version "2.0.3"
resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-2.0.3.tgz#1680df6ced8f128ca0ff24e29c2165d41d78b3ce"
integrity sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==
dependencies:
"@electron/asar" "^3.2.7"
"@electron/asar" "^3.3.1"
"@malept/cross-spawn-promise" "^2.0.0"
debug "^4.3.1"
dir-compare "^4.2.0"
@@ -1034,6 +1025,17 @@
lodash "^4.17.15"
tmp-promise "^3.0.2"
"@npmcli/agent@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@npmcli/agent/-/agent-3.0.0.tgz#1685b1fbd4a1b7bb4f930cbb68ce801edfe7aa44"
integrity sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==
dependencies:
agent-base "^7.1.0"
http-proxy-agent "^7.0.0"
https-proxy-agent "^7.0.1"
lru-cache "^10.0.1"
socks-proxy-agent "^8.0.3"
"@npmcli/agent@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@npmcli/agent/-/agent-4.0.0.tgz#2bb2b1c0a170940511554a7986ae2a8be9fedcce"
@@ -1106,6 +1108,13 @@
"@gar/promisify" "^1.1.3"
semver "^7.3.5"
"@npmcli/fs@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-4.0.0.tgz#a1eb1aeddefd2a4a347eca0fab30bc62c0e1c0f2"
integrity sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==
dependencies:
semver "^7.3.5"
"@npmcli/fs@^5.0.0":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-5.0.0.tgz#674619771907342b3d1ac197aaf1deeb657e3539"
@@ -1569,7 +1578,7 @@
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
"@tufjs/canonical-json@2.0.0":
@@ -1883,6 +1892,11 @@ abbrev@^1.0.0:
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abbrev@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-3.0.1.tgz#8ac8b3b5024d31464fe2a5feeea9f4536bf44025"
integrity sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==
abbrev@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-4.0.0.tgz#ec933f0e27b6cd60e89b5c6b2a304af42209bb05"
@@ -1918,7 +1932,7 @@ accepts@~1.3.4:
agent-base@6, agent-base@^6.0.2:
version "6.0.2"
resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
@@ -2047,44 +2061,45 @@ app-builder-bin@5.0.0-alpha.12:
resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-5.0.0-alpha.12.tgz#2daf82f8badc698e0adcc95ba36af4ff0650dc80"
integrity sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w==
app-builder-lib@26.0.12:
version "26.0.12"
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.0.12.tgz#2e33df936e0f78d4266b058ece90308ea981eefb"
integrity sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw==
app-builder-lib@26.4.0:
version "26.4.0"
resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-26.4.0.tgz#649b4a98b51a90141b73e4f12a74ac5bc0f2eff4"
integrity sha512-Uas6hNe99KzP3xPWxh5LGlH8kWIVjZixzmMJHNB9+6hPyDpjc7NQMkVgi16rQDdpCFy22ZU5sp8ow7tvjeMgYQ==
dependencies:
"@develar/schema-utils" "~2.6.5"
"@electron/asar" "3.2.18"
"@electron/asar" "3.4.1"
"@electron/fuses" "^1.8.0"
"@electron/notarize" "2.5.0"
"@electron/osx-sign" "1.3.1"
"@electron/rebuild" "3.7.0"
"@electron/universal" "2.0.1"
"@electron/osx-sign" "1.3.3"
"@electron/rebuild" "4.0.1"
"@electron/universal" "2.0.3"
"@malept/flatpak-bundler" "^0.4.0"
"@types/fs-extra" "9.0.13"
async-exit-hook "^2.0.1"
builder-util "26.0.11"
builder-util-runtime "9.3.1"
builder-util "26.3.4"
builder-util-runtime "9.5.1"
chromium-pickle-js "^0.2.0"
config-file-ts "0.2.8-rc1"
ci-info "4.3.1"
debug "^4.3.4"
dotenv "^16.4.5"
dotenv-expand "^11.0.6"
ejs "^3.1.8"
electron-publish "26.0.11"
electron-publish "26.3.4"
fs-extra "^10.1.0"
hosted-git-info "^4.1.0"
is-ci "^3.0.0"
isbinaryfile "^5.0.0"
jiti "^2.4.2"
js-yaml "^4.1.0"
json5 "^2.2.3"
lazy-val "^1.0.5"
minimatch "^10.0.0"
minimatch "^10.0.3"
plist "3.1.0"
resedit "^1.7.0"
semver "^7.3.8"
semver "~7.7.3"
tar "^6.1.12"
temp-file "^3.4.0"
tiny-async-pool "1.3.0"
which "^5.0.0"
append-transform@^2.0.0:
version "2.0.0"
@@ -2372,7 +2387,15 @@ buffer@^6.0.3:
base64-js "^1.3.1"
ieee754 "^1.2.1"
builder-util-runtime@9.3.1, builder-util-runtime@^9.3.1:
builder-util-runtime@9.5.1:
version "9.5.1"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.5.1.tgz#74125fb374d1ecbf472ae1787485485ff7619702"
integrity sha512-qt41tMfgHTllhResqM5DcnHyDIWNgzHvuY2jDcYP9iaGpkWxTUzV6GQjDeLnlR1/DtdlcsWQbA7sByMpmJFTLQ==
dependencies:
debug "^4.3.4"
sax "^1.2.4"
builder-util-runtime@^9.3.1:
version "9.3.1"
resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.3.1.tgz#0daedde0f6d381f2a00a50a407b166fe7dca1a67"
integrity sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ==
@@ -2380,22 +2403,21 @@ builder-util-runtime@9.3.1, builder-util-runtime@^9.3.1:
debug "^4.3.4"
sax "^1.2.4"
builder-util@26.0.11:
version "26.0.11"
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.0.11.tgz#ad85b92c93f2b976b973e1d87337e0c6813fcb8f"
integrity sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA==
builder-util@26.3.4:
version "26.3.4"
resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-26.3.4.tgz#eb20e2e2895fe360360eddded5d8cf12ad2aad60"
integrity sha512-aRn88mYMktHxzdqDMF6Ayj0rKoX+ZogJ75Ck7RrIqbY/ad0HBvnS2xA4uHfzrGr5D2aLL3vU6OBEH4p0KMV2XQ==
dependencies:
"7zip-bin" "~5.2.0"
"@types/debug" "^4.1.6"
app-builder-bin "5.0.0-alpha.12"
builder-util-runtime "9.3.1"
builder-util-runtime "9.5.1"
chalk "^4.1.2"
cross-spawn "^7.0.6"
debug "^4.3.4"
fs-extra "^10.1.0"
http-proxy-agent "^7.0.0"
https-proxy-agent "^7.0.0"
is-ci "^3.0.0"
js-yaml "^4.1.0"
sanitize-filename "^1.6.3"
source-map-support "^0.5.19"
@@ -2437,6 +2459,24 @@ cacache@^16.1.0:
tar "^6.1.11"
unique-filename "^2.0.0"
cacache@^19.0.1:
version "19.0.1"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-19.0.1.tgz#3370cc28a758434c85c2585008bd5bdcff17d6cd"
integrity sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==
dependencies:
"@npmcli/fs" "^4.0.0"
fs-minipass "^3.0.0"
glob "^10.2.2"
lru-cache "^10.0.1"
minipass "^7.0.3"
minipass-collect "^2.0.1"
minipass-flush "^1.0.5"
minipass-pipeline "^1.2.4"
p-map "^7.0.2"
ssri "^12.0.0"
tar "^7.4.3"
unique-filename "^4.0.0"
cacache@^20.0.0, cacache@^20.0.1, cacache@^20.0.3:
version "20.0.3"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-20.0.3.tgz#bd65205d5e6d86e02bbfaf8e4ce6008f1b81d119"
@@ -2634,21 +2674,16 @@ chromium-pickle-js@^0.2.0:
resolved "https://registry.npmjs.org/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz"
integrity sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==
ci-info@^3.2.0:
version "3.9.0"
resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz"
integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==
ci-info@4.3.1, ci-info@^4.2.0, ci-info@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.1.tgz#355ad571920810b5623e11d40232f443f16f1daa"
integrity sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==
ci-info@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.0.0.tgz#65466f8b280fc019b9f50a5388115d17a63a44f2"
integrity sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==
ci-info@^4.3.1:
version "4.3.1"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-4.3.1.tgz#355ad571920810b5623e11d40232f443f16f1daa"
integrity sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==
cidr-regex@5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/cidr-regex/-/cidr-regex-5.0.1.tgz#4b3972457b06445832929f6f268b477fe0372c1f"
@@ -2897,14 +2932,6 @@ config-chain@^1.1.11:
ini "^1.3.4"
proto-list "~1.2.1"
config-file-ts@0.2.8-rc1:
version "0.2.8-rc1"
resolved "https://registry.yarnpkg.com/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz#fb7fc6ccb2e313f69dbeb78f1db0b00038049de0"
integrity sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg==
dependencies:
glob "^10.3.12"
typescript "^5.4.3"
content-disposition@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.1.tgz#a8b7bbeb2904befdfb6787e5c0c086959f605f9b"
@@ -3336,14 +3363,13 @@ dir-glob@^3.0.0, dir-glob@^3.0.1:
dependencies:
path-type "^4.0.0"
dmg-builder@26.0.12:
version "26.0.12"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.0.12.tgz#6996ad0bab80a861c9a7b33ee9734d4f60566b46"
integrity sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w==
dmg-builder@26.4.0:
version "26.4.0"
resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-26.4.0.tgz#dc7edec167b06b1542804e6e4220d1bcaf952b71"
integrity sha512-ce4Ogns4VMeisIuCSK0C62umG0lFy012jd8LMZ6w/veHUeX4fqfDrGe+HTWALAEwK6JwKP+dhPvizhArSOsFbg==
dependencies:
app-builder-lib "26.0.12"
builder-util "26.0.11"
builder-util-runtime "9.3.1"
app-builder-lib "26.4.0"
builder-util "26.3.4"
fs-extra "^10.1.0"
iconv-lite "^0.6.2"
js-yaml "^4.1.0"
@@ -3439,18 +3465,18 @@ ejs@^3.1.8:
dependencies:
jake "^10.8.5"
electron-builder@^26.0.12:
version "26.0.12"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.0.12.tgz#797af2e70efdd96c9ea5d8a8164b8728c90d65ff"
integrity sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA==
electron-builder@^26.4.0:
version "26.4.0"
resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-26.4.0.tgz#f9a8598f045ded4cbaf41efb7bb18a89619c5dd2"
integrity sha512-FCUqvdq2AULL+Db2SUGgjOYTbrgkPxZtCjqIZGnjH9p29pTWyesQqBIfvQBKa6ewqde87aWl49n/WyI/NyUBog==
dependencies:
app-builder-lib "26.0.12"
builder-util "26.0.11"
builder-util-runtime "9.3.1"
app-builder-lib "26.4.0"
builder-util "26.3.4"
builder-util-runtime "9.5.1"
chalk "^4.1.2"
dmg-builder "26.0.12"
ci-info "^4.2.0"
dmg-builder "26.4.0"
fs-extra "^10.1.0"
is-ci "^3.0.0"
lazy-val "^1.0.5"
simple-update-notifier "2.0.0"
yargs "^17.6.2"
@@ -3460,14 +3486,14 @@ electron-log@^5.4.3:
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.4.3.tgz#02a90baf4256950ca416095db6e5745268584d20"
integrity sha512-sOUsM3LjZdugatazSQ/XTyNcw8dfvH1SYhXWiJyfYodAAKOZdHs0txPiLDXFzOZbhXgAgshQkshH2ccq0feyLQ==
electron-publish@26.0.11:
version "26.0.11"
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.0.11.tgz#92c9329a101af2836d9d228c82966eca1eee9a7b"
integrity sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A==
electron-publish@26.3.4:
version "26.3.4"
resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-26.3.4.tgz#ed05f1ccbb7ee1e53b4140d92735e26fa4bfefd7"
integrity sha512-5/ouDPb73SkKuay2EXisPG60LTFTMNHWo2WLrK5GDphnWK9UC+yzYrzVeydj078Yk4WUXi0+TaaZsNd6Zt5k/A==
dependencies:
"@types/fs-extra" "^9.0.11"
builder-util "26.0.11"
builder-util-runtime "9.3.1"
builder-util "26.3.4"
builder-util-runtime "9.5.1"
chalk "^4.1.2"
form-data "^4.0.0"
fs-extra "^10.1.0"
@@ -3479,18 +3505,18 @@ electron-to-chromium@^1.5.263:
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz#5d84f2df8cdb6bfe7e873706bb21bd4bfb574dc7"
integrity sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==
electron-updater@^6.6.2:
version "6.6.2"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.6.2.tgz#3e65e044f1a99b00d61e200e24de8e709c69ce99"
integrity sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw==
electron-updater@^6.7.3:
version "6.7.3"
resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.7.3.tgz#c710b00cbce72f5fd2cc88f8c5f7535cae3097a3"
integrity sha512-EgkT8Z9noqXKbwc3u5FkJA+r48jwZ5DTUiOkJMOTEEH//n5Am6wfQGz7nvSFEA2oIAMv9jRzn5JKTyWeSKOPgg==
dependencies:
builder-util-runtime "9.3.1"
builder-util-runtime "9.5.1"
fs-extra "^10.1.0"
js-yaml "^4.1.0"
lazy-val "^1.0.5"
lodash.escaperegexp "^4.1.2"
lodash.isequal "^4.5.0"
semver "^7.6.3"
semver "~7.7.3"
tiny-typed-emitter "^2.1.0"
electron@39.2.7:
@@ -4411,16 +4437,17 @@ glob-parent@~5.1.2:
dependencies:
is-glob "^4.0.1"
glob@^10.3.12:
version "10.3.14"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.14.tgz#36501f871d373fe197fc5794588d0aa71e69ff68"
integrity sha512-4fkAqu93xe9Mk7le9v0y3VrPDqLKHarNi2s4Pv7f2yOvfhWfhc7hRPHC/JyqMqb8B/Dt/eGS4n7ykwf3fOsl8g==
glob@^10.2.2:
version "10.5.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-10.5.0.tgz#8ec0355919cd3338c28428a23d4f24ecc5fe738c"
integrity sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==
dependencies:
foreground-child "^3.1.0"
jackspeak "^2.3.6"
minimatch "^9.0.1"
minipass "^7.0.4"
path-scurry "^1.11.0"
jackspeak "^3.1.2"
minimatch "^9.0.4"
minipass "^7.1.2"
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
glob@^13.0.0:
version "13.0.0"
@@ -4689,7 +4716,7 @@ http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.1:
http-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
dependencies:
"@tootallnate/once" "2"
@@ -4979,13 +5006,6 @@ is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7:
resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz"
integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==
is-ci@^3.0.0:
version "3.0.1"
resolved "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz"
integrity sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==
dependencies:
ci-info "^3.2.0"
is-cidr@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/is-cidr/-/is-cidr-6.0.1.tgz#125e9dead938b6fa996aa500662a5e9f88f338f4"
@@ -5273,10 +5293,10 @@ istanbul-reports@^3.0.2:
html-escaper "^2.0.0"
istanbul-lib-report "^3.0.0"
jackspeak@^2.3.6:
version "2.3.6"
resolved "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz"
integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
jackspeak@^3.1.2:
version "3.4.3"
resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a"
integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
dependencies:
"@isaacs/cliui" "^8.0.2"
optionalDependencies:
@@ -5297,6 +5317,11 @@ java-properties@^1.0.2:
resolved "https://registry.yarnpkg.com/java-properties/-/java-properties-1.0.2.tgz#ccd1fa73907438a5b5c38982269d0e771fe78211"
integrity sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==
jiti@^2.4.2:
version "2.6.1"
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92"
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
js-base64@^3.7.8:
version "3.7.8"
resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.8.tgz#af44496bc09fa178ed9c4adf67eb2b46f5c6d2a4"
@@ -5755,6 +5780,23 @@ make-fetch-happen@^10.2.1:
socks-proxy-agent "^7.0.0"
ssri "^9.0.0"
make-fetch-happen@^14.0.3:
version "14.0.3"
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz#d74c3ecb0028f08ab604011e0bc6baed483fcdcd"
integrity sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==
dependencies:
"@npmcli/agent" "^3.0.0"
cacache "^19.0.1"
http-cache-semantics "^4.1.1"
minipass "^7.0.2"
minipass-fetch "^4.0.0"
minipass-flush "^1.0.5"
minipass-pipeline "^1.2.4"
negotiator "^1.0.0"
proc-log "^5.0.0"
promise-retry "^2.0.1"
ssri "^12.0.0"
make-fetch-happen@^15.0.0, make-fetch-happen@^15.0.1, make-fetch-happen@^15.0.3:
version "15.0.3"
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-15.0.3.tgz#1578d72885f2b3f9e5daa120b36a14fc31a84610"
@@ -5899,7 +5941,7 @@ mimic-response@^3.1.0:
resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz"
integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==
minimatch@^10.0.0, minimatch@^10.0.3, minimatch@^10.1.1:
minimatch@^10.0.3, minimatch@^10.1.1:
version "10.1.1"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.1.1.tgz#e6e61b9b0c1dcab116b5a7d1458e8b6ae9e73a55"
integrity sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==
@@ -5920,14 +5962,7 @@ minimatch@^5.0.1, minimatch@^5.1.6:
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.1:
version "9.0.4"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz"
integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==
dependencies:
brace-expansion "^2.0.1"
minimatch@^9.0.3:
minimatch@^9.0.3, minimatch@^9.0.4:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
@@ -5964,6 +5999,17 @@ minipass-fetch@^2.0.3:
optionalDependencies:
encoding "^0.1.13"
minipass-fetch@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-4.0.1.tgz#f2d717d5a418ad0b1a7274f9b913515d3e78f9e5"
integrity sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==
dependencies:
minipass "^7.0.3"
minipass-sized "^1.0.3"
minizlib "^3.0.1"
optionalDependencies:
encoding "^0.1.13"
minipass-fetch@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-5.0.0.tgz#644ed3fa172d43b3163bb32f736540fc138c4afb"
@@ -6168,11 +6214,16 @@ mz@^2.4.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
negotiator@0.6.3, negotiator@^0.6.3:
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
negotiator@^0.6.3:
version "0.6.4"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
negotiator@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
@@ -6193,19 +6244,19 @@ nice-try@^1.0.4:
resolved "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-abi@^3.45.0:
version "3.85.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.85.0.tgz#b115d575e52b2495ef08372b058e13d202875a7d"
integrity sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==
node-abi@^4.2.0:
version "4.24.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-4.24.0.tgz#fcc1b4c645ffb4c0f39e2dbfb9c41698ba7e782e"
integrity sha512-u2EC1CeNe25uVtX3EZbdQ275c74zdZmmpzrHEQh2aIYqoVjlglfUpOX9YY85x1nlBydEKDVaSmMNhR7N82Qj8A==
dependencies:
semver "^7.3.5"
semver "^7.6.3"
node-addon-api@^1.6.3:
version "1.7.2"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d"
integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==
node-api-version@^0.2.0:
node-api-version@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/node-api-version/-/node-api-version-0.2.1.tgz#19bad54f6d65628cbee4e607a325e4488ace2de9"
integrity sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==
@@ -6222,6 +6273,22 @@ node-emoji@^2.2.0:
emojilib "^2.4.0"
skin-tone "^2.0.0"
node-gyp@^11.2.0:
version "11.5.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-11.5.0.tgz#82661b5f40647a7361efe918e3cea76d297fcc56"
integrity sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==
dependencies:
env-paths "^2.2.0"
exponential-backoff "^3.1.1"
graceful-fs "^4.2.6"
make-fetch-happen "^14.0.3"
nopt "^8.0.0"
proc-log "^5.0.0"
semver "^7.3.5"
tar "^7.4.3"
tinyglobby "^0.2.12"
which "^5.0.0"
node-gyp@^12.1.0:
version "12.1.0"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-12.1.0.tgz#302fc2d3fec36975cfb8bfee7a6bf6b7f0be9553"
@@ -6257,6 +6324,13 @@ nopt@^6.0.0:
dependencies:
abbrev "^1.0.0"
nopt@^8.0.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-8.1.0.tgz#b11d38caf0f8643ce885818518064127f602eae3"
integrity sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==
dependencies:
abbrev "^3.0.0"
nopt@^9.0.0:
version "9.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-9.0.0.tgz#6bff0836b2964d24508b6b41b5a9a49c4f4a1f96"
@@ -6720,6 +6794,11 @@ package-hash@^4.0.0:
lodash.flattendeep "^4.4.0"
release-zalgo "^1.0.0"
package-json-from-dist@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
pacote@^21.0.0, pacote@^21.0.2, pacote@^21.0.4:
version "21.0.4"
resolved "https://registry.yarnpkg.com/pacote/-/pacote-21.0.4.tgz#59cd2a2b5a4c8c1b625f33991a96b136d1c05d95"
@@ -6883,10 +6962,10 @@ path-parse@^1.0.7:
resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz"
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
path-scurry@^1.11.0:
version "1.11.0"
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.0.tgz#332d64e9726bf667fb348e5a1c71005c09ad741a"
integrity sha512-LNHTaVkzaYaLGlO+0u3rQTz7QrHTFOuKyba9JMTQutkmtNew8dw8wOD7mTU/5fCPZzCWpfW0XnQKzY61P0aTaw==
path-scurry@^1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2"
integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
dependencies:
lru-cache "^10.2.0"
minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
@@ -7062,6 +7141,11 @@ proc-log@^2.0.1:
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-2.0.1.tgz#8f3f69a1f608de27878f91f5c688b225391cb685"
integrity sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw==
proc-log@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-5.0.0.tgz#e6c93cf37aef33f835c53485f314f50ea906a9d8"
integrity sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==
proc-log@^6.0.0, proc-log@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-6.1.0.tgz#18519482a37d5198e231133a70144a50f21f0215"
@@ -7641,14 +7725,14 @@ semver@^7.1.1, semver@^7.1.2, semver@^7.3.7, semver@^7.5.2:
resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13"
integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==
semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3:
semver@^7.3.2, semver@^7.3.5, semver@^7.5.3:
version "7.6.0"
resolved "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz"
integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==
dependencies:
lru-cache "^6.0.0"
semver@^7.5.4, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2, semver@^7.7.3:
semver@^7.5.4, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2, semver@^7.7.3, semver@~7.7.3:
version "7.7.3"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.3.tgz#4b5f4143d007633a8dc671cd0a6ef9147b8bb946"
integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
@@ -8054,6 +8138,13 @@ sprintf-js@~1.0.2:
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
ssri@^12.0.0:
version "12.0.0"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-12.0.0.tgz#bcb4258417c702472f8191981d3c8a771fee6832"
integrity sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==
dependencies:
minipass "^7.0.3"
ssri@^13.0.0:
version "13.0.0"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-13.0.0.tgz#4226b303dc474003d88905f9098cb03361106c74"
@@ -8336,7 +8427,7 @@ tagged-tag@^1.0.0:
tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.2.1:
version "6.2.1"
resolved "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a"
integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==
dependencies:
chownr "^2.0.0"
@@ -8747,7 +8838,7 @@ typedarray@^0.0.6:
resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript@^5.4.3, typescript@^5.9.3:
typescript@^5.9.3:
version "5.9.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.9.3.tgz#5b4f59e15310ab17a216f5d6cf53ee476ede670f"
integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
@@ -8816,6 +8907,13 @@ unique-filename@^2.0.0:
dependencies:
unique-slug "^3.0.0"
unique-filename@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-4.0.0.tgz#a06534d370e7c977a939cd1d11f7f0ab8f1fed13"
integrity sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==
dependencies:
unique-slug "^5.0.0"
unique-filename@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-5.0.0.tgz#8b17bbde1a7ca322dd1a1d23fe17c2b798c43f8f"
@@ -8830,6 +8928,13 @@ unique-slug@^3.0.0:
dependencies:
imurmurhash "^0.1.4"
unique-slug@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-5.0.0.tgz#ca72af03ad0dbab4dad8aa683f633878b1accda8"
integrity sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==
dependencies:
imurmurhash "^0.1.4"
unique-slug@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-6.0.0.tgz#f46fd688a9bd972fd356c23d95812a3a4862ed88"
@@ -8994,11 +9099,18 @@ which@^1.2.9:
which@^2.0.1, which@^2.0.2:
version "2.0.2"
resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
which@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/which/-/which-5.0.0.tgz#d93f2d93f79834d4363c7d0c23e00d07c466c8d6"
integrity sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==
dependencies:
isexe "^3.1.1"
which@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/which/-/which-6.0.0.tgz#a3a721a14cdd9b991a722e493c177eeff82ff32a"