diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml
new file mode 100644
index 0000000..292e1fc
--- /dev/null
+++ b/.github/workflows/e2e-tests.yml
@@ -0,0 +1,54 @@
+name: E2E Tests
+
+on:
+ push:
+ branches: [main, 'feat/**']
+ pull_request:
+ branches: [main]
+
+jobs:
+ e2e:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Install Playwright browsers
+ run: npx playwright install chromium --with-deps
+
+ - name: Install fixture app dependencies
+ run: npm run e2e:fixture:install
+
+ - name: Build extension
+ run: npm run build
+
+ - name: Run E2E tests
+ run: xvfb-run --auto-servernum --server-args='-screen 0 1920x1080x24' npx playwright test --config=e2e/playwright.config.ts
+ env:
+ CI: true
+
+ - name: Upload test results
+ uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: playwright-report
+ path: playwright-report/
+ retention-days: 7
+
+ - name: Upload test traces
+ uses: actions/upload-artifact@v4
+ if: failure()
+ with:
+ name: test-traces
+ path: test-results/
+ retention-days: 7
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
new file mode 100644
index 0000000..8e175ba
--- /dev/null
+++ b/.github/workflows/unit-tests.yml
@@ -0,0 +1,35 @@
+name: Unit Tests
+
+on:
+ push:
+ branches: [main, 'feat/**']
+ pull_request:
+ branches: [main]
+
+jobs:
+ unit:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Run unit tests
+ run: npm test
+
+ - name: Upload coverage report
+ uses: actions/upload-artifact@v4
+ if: always()
+ with:
+ name: coverage-report
+ path: coverage/
+ retention-days: 7
diff --git a/.gitignore b/.gitignore
index a544c84..b06f39a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@ node_modules
.DS_STORE
/coverage
dist/*
+e2e/fixtures/aurelia-app/dist
+/playwright-report
+/test-results
diff --git a/README.md b/README.md
index bb7c854..e151d4c 100644
--- a/README.md
+++ b/README.md
@@ -12,7 +12,7 @@ This project is a work in progress. The current version is not yet available on
----
-A browser extension for debugging Aurelia 1 and 2 applications. Features a top-level DevTools tab with modern, professional interface and dual-tab architecture for comprehensive debugging.
+A browser extension for debugging Aurelia 1 and 2 applications. Integrates as a sidebar pane in Chrome's Elements panel for seamless component inspection.
## Features
@@ -71,7 +71,7 @@ Install the latest Node.js and npm versions.
1. Run `npm run start` to start development mode
2. Load the extension in Chrome (see Installation > Manual Installation)
3. Pin the Aurelia Extension in the toolbar to verify detection: "Aurelia 2 detected on this page."
-4. Open Developer Tools and navigate to the "⚡ Aurelia" tab
+4. Open Developer Tools, go to the Elements panel, and find the "Aurelia" sidebar pane
5. For code changes:
- Reload the extension in `chrome://extensions`
- Close and reopen Developer Tools (or Ctrl+R in the DevTools inspect window)
@@ -166,29 +166,31 @@ This event-first approach keeps the surface area tiny (just listen and dispatch)
## Architecture
### Core Components
-- **Main Application** (`src/main.ts`, `src/app.ts`) - Aurelia 2 app rendering the DevTools UI
+- **Sidebar Application** (`src/sidebar/`) - Aurelia 2 app rendering the Elements panel sidebar
- **Extension Scripts**:
- `detector.ts` - Detects Aurelia versions on web pages
- `background.ts` - Service worker managing extension state
- `contentscript.ts` - Finds Aurelia instances in DOM
- - `devtools.js` - Creates the DevTools panel
+ - `devtools.js` - Creates the Elements sidebar pane
### Build System
-- **Vite** - Modern build tool replacing Webpack
+- **Vite** - Modern build tool
- **TypeScript** - Type safety and modern JavaScript features
-- **Aurelia 2** - Framework for the DevTools UI itself
+- **Aurelia 2** - Framework for the sidebar UI itself
### File Structure
```
src/
-├── main.ts # Entry point
-├── app.ts, app.html # Main Aurelia app
-├── backend/ # Debug host and communication
+├── sidebar/ # Sidebar application
+│ ├── main.ts # Entry point
+│ ├── sidebar-app.ts # Main ViewModel
+│ ├── sidebar-app.html # Template
+│ ├── sidebar-app.css # Styles
+│ └── sidebar-debug-host.ts # Communication layer
├── background/ # Service worker
├── contentscript/ # Page content interaction
├── detector/ # Aurelia version detection
-├── devtools/ # DevTools panel creation
-├── resources/elements/ # UI components
+├── devtools/ # Sidebar pane creation
├── shared/ # Common types and utilities
└── popups/ # Extension popup pages
```
diff --git a/docs/deployment.md b/docs/deployment.md
index 4625231..e9d0e25 100644
--- a/docs/deployment.md
+++ b/docs/deployment.md
@@ -190,14 +190,15 @@ All deployments must pass:
```bash
# Required files in dist/:
├── manifest.json # Extension manifest
-├── index.html # DevTools panel entry
+├── sidebar.html # Sidebar pane entry
├── build/
-│ ├── entry.js # Main application
-│ ├── background.js # Service worker
-│ ├── contentscript.js # Content script
-│ └── detector.js # Aurelia detector
-├── images/ # Extension icons
-└── popups/ # Extension popups
+│ ├── sidebar.js # Sidebar application
+│ ├── background.js # Service worker
+│ ├── contentscript.js # Content script
+│ └── detector.js # Aurelia detector
+├── devtools/ # DevTools page
+├── images/ # Extension icons
+└── popups/ # Extension popups
```
## Chrome Web Store Review Process
diff --git a/e2e/fixtures/aurelia-app/index.html b/e2e/fixtures/aurelia-app/index.html
new file mode 100644
index 0000000..a97152c
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Aurelia 2 E2E Fixture
+
+
+
+
+
+
diff --git a/e2e/fixtures/aurelia-app/package-lock.json b/e2e/fixtures/aurelia-app/package-lock.json
new file mode 100644
index 0000000..73098ae
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/package-lock.json
@@ -0,0 +1,1913 @@
+{
+ "name": "aurelia-e2e-fixture",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "aurelia-e2e-fixture",
+ "version": "1.0.0",
+ "dependencies": {
+ "aurelia": "^2.0.0-beta.25"
+ },
+ "devDependencies": {
+ "@aurelia/vite-plugin": "^2.0.0-beta.25",
+ "typescript": "^5.7.2",
+ "vite": "^5.4.10"
+ }
+ },
+ "node_modules/@aurelia/expression-parser": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/expression-parser/-/expression-parser-2.0.0-beta.25.tgz",
+ "integrity": "sha512-aIbXxC+jBjeoNX/Pshrj/RYBFDx03YK5MEBG/DAYZUy9GKFguAMNpWGGj4hbtDncZetKtuDAK8sOPMffaF4Tyg==",
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/kernel": "2.0.0-beta.25"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/fetch-client": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/fetch-client/-/fetch-client-2.0.0-beta.25.tgz",
+ "integrity": "sha512-3bynSrG0gSmUbxaFJokJfEtwJ85bRBsg8RHnczmv3R5xLIMOUcMYpJVOangMM1ek4qEwlzi0Q1srpT2hrxPKlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/kernel": "2.0.0-beta.25",
+ "@aurelia/metadata": "2.0.0-beta.25"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/kernel": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/kernel/-/kernel-2.0.0-beta.25.tgz",
+ "integrity": "sha512-U8I/FH4RJOkXe2igTy4G6U34aSdDLa43f4C+z4IU4jvgVpJTyu9+B92n/tUI7TtE+lOly8sbi7GQbgaPP9lntw==",
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/metadata": "2.0.0-beta.25",
+ "@aurelia/platform": "2.0.0-beta.25"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/metadata": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/metadata/-/metadata-2.0.0-beta.25.tgz",
+ "integrity": "sha512-N/TihhPtbM/2FbcOGkCuCOpTjRyQTz7Z3GyNkJGxiIgTdMnzrpslwoC3GT/Qeh3lP28zR96jfAbAOlSyy8wY6g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/platform": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/platform/-/platform-2.0.0-beta.25.tgz",
+ "integrity": "sha512-dE8Uk9oX5rjhhFgwEqV/NTUOrVrWaYHIv4MQFxiiDqEpTLyMLQDykVk+Q0sxqeIIw8xWw0XNQUs9NNExEcntjw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/platform-browser": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/platform-browser/-/platform-browser-2.0.0-beta.25.tgz",
+ "integrity": "sha512-V9s7/kHB5yLI3P8a5la/0Y/p2h938HAHQ28eJTYwaSLC88y0ZktmG5MUS/uzORCjpANFxK1JrKfpI+v3kGfdZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/platform": "2.0.0-beta.25"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/plugin-conventions": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/plugin-conventions/-/plugin-conventions-2.0.0-beta.25.tgz",
+ "integrity": "sha512-egYB1CykdqCJQj/BHj8agF1r5sB2k8vkjq53I7wEaapSEg7z8lTR0+pU6d9GHy3lYkl+6p++3UIb+zVlPwIK3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/expression-parser": "2.0.0-beta.25",
+ "@aurelia/kernel": "2.0.0-beta.25",
+ "@aurelia/metadata": "2.0.0-beta.25",
+ "@aurelia/platform": "2.0.0-beta.25",
+ "@aurelia/runtime": "2.0.0-beta.25",
+ "@aurelia/runtime-html": "2.0.0-beta.25",
+ "@aurelia/template-compiler": "2.0.0-beta.25",
+ "modify-code": "^2.1.3",
+ "parse5": "^7.1.2",
+ "typescript": "^5.4.2"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/runtime": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/runtime/-/runtime-2.0.0-beta.25.tgz",
+ "integrity": "sha512-geRbXQYBt/IBgVJgvfmbvw9+vOHbtiP8hlnN58jz9/U5nFX3G+UHukOOEraLUk4Jt7OA90bOTfLDIw6JtPLkzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/expression-parser": "2.0.0-beta.25",
+ "@aurelia/kernel": "2.0.0-beta.25",
+ "@aurelia/metadata": "2.0.0-beta.25",
+ "@aurelia/platform": "2.0.0-beta.25"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/runtime-html": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/runtime-html/-/runtime-html-2.0.0-beta.25.tgz",
+ "integrity": "sha512-hKIGPCLd+o8osMq2lfD1xOu6qxdfjVM3D6nAu7oWLvMCPVUY0cNTv4fPSDXO9ZsSUWcsSyqeHgchpqNP9/g3Tg==",
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/expression-parser": "2.0.0-beta.25",
+ "@aurelia/kernel": "2.0.0-beta.25",
+ "@aurelia/metadata": "2.0.0-beta.25",
+ "@aurelia/platform": "2.0.0-beta.25",
+ "@aurelia/platform-browser": "2.0.0-beta.25",
+ "@aurelia/runtime": "2.0.0-beta.25",
+ "@aurelia/template-compiler": "2.0.0-beta.25"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/template-compiler": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/template-compiler/-/template-compiler-2.0.0-beta.25.tgz",
+ "integrity": "sha512-IfgKKtduYt3VbGRDdzvKdA3RqHWQ7l3y0kp0IpS0OQGL0PYFD3xN/mCiHtjaonFs+siJf9C+LvoGhFkfGA6vPg==",
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/expression-parser": "2.0.0-beta.25",
+ "@aurelia/kernel": "2.0.0-beta.25",
+ "@aurelia/metadata": "2.0.0-beta.25"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/@aurelia/vite-plugin/-/vite-plugin-2.0.0-beta.25.tgz",
+ "integrity": "sha512-K3hIg02Q/mLw7zEl8dX1pZCDQtsnOGh4i+imuLnUcASDt4GRCdwFRiako4dyQRm57hD2/Fj1WrdiJCFuP/pVkA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/kernel": "2.0.0-beta.25",
+ "@aurelia/metadata": "2.0.0-beta.25",
+ "@aurelia/platform": "2.0.0-beta.25",
+ "@aurelia/plugin-conventions": "2.0.0-beta.25",
+ "@aurelia/runtime": "2.0.0-beta.25",
+ "@rollup/pluginutils": "5.0.2",
+ "loader-utils": "^2.0.0",
+ "vite": "^7.0.2"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/@aurelia/vite-plugin/node_modules/vite": {
+ "version": "7.2.4",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.4.tgz",
+ "integrity": "sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
+ "integrity": "sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
+ "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
+ "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
+ "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
+ "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
+ "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
+ "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
+ "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
+ "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
+ "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
+ "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
+ "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
+ "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
+ "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
+ "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
+ "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
+ "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
+ "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
+ "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
+ "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
+ "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
+ "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/aurelia": {
+ "version": "2.0.0-beta.25",
+ "resolved": "https://registry.npmjs.org/aurelia/-/aurelia-2.0.0-beta.25.tgz",
+ "integrity": "sha512-97kZ8QaZB65mo5RrQk6Ln9DUoXEfpoBeO8lDaLEk4Bl4NlEBJAY/sHjDRQm83+S7aCAuEzc9NGb2+b0ZNhGlJQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@aurelia/expression-parser": "2.0.0-beta.25",
+ "@aurelia/fetch-client": "2.0.0-beta.25",
+ "@aurelia/kernel": "2.0.0-beta.25",
+ "@aurelia/metadata": "2.0.0-beta.25",
+ "@aurelia/platform": "2.0.0-beta.25",
+ "@aurelia/platform-browser": "2.0.0-beta.25",
+ "@aurelia/runtime": "2.0.0-beta.25",
+ "@aurelia/runtime-html": "2.0.0-beta.25",
+ "@aurelia/template-compiler": "2.0.0-beta.25"
+ },
+ "engines": {
+ "node": ">=20.16.0"
+ }
+ },
+ "node_modules/big.js": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
+ "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/emojis-list": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
+ "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loader-utils": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz",
+ "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "big.js": "^5.2.2",
+ "emojis-list": "^3.0.0",
+ "json5": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=8.9.0"
+ }
+ },
+ "node_modules/modify-code": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/modify-code/-/modify-code-2.1.4.tgz",
+ "integrity": "sha512-/MNa/Wpkki5AHuaZc+L43Di/CMKwcgb740sR5yXNt/f7vwYGPt7g4G5OBPtviW/3sgauuPZZDmY+p5DJ6raHWg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "source-map": "^0.7.4"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.53.3",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
+ "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.53.3",
+ "@rollup/rollup-android-arm64": "4.53.3",
+ "@rollup/rollup-darwin-arm64": "4.53.3",
+ "@rollup/rollup-darwin-x64": "4.53.3",
+ "@rollup/rollup-freebsd-arm64": "4.53.3",
+ "@rollup/rollup-freebsd-x64": "4.53.3",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.3",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.3",
+ "@rollup/rollup-linux-arm64-musl": "4.53.3",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.3",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.3",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.3",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-gnu": "4.53.3",
+ "@rollup/rollup-linux-x64-musl": "4.53.3",
+ "@rollup/rollup-openharmony-arm64": "4.53.3",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.3",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.3",
+ "@rollup/rollup-win32-x64-gnu": "4.53.3",
+ "@rollup/rollup-win32-x64-msvc": "4.53.3",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.7.6",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz",
+ "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/e2e/fixtures/aurelia-app/package.json b/e2e/fixtures/aurelia-app/package.json
new file mode 100644
index 0000000..62b26f0
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "aurelia-e2e-fixture",
+ "private": true,
+ "version": "1.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "aurelia": "^2.0.0-beta.25"
+ },
+ "devDependencies": {
+ "@aurelia/vite-plugin": "^2.0.0-beta.25",
+ "typescript": "^5.7.2",
+ "vite": "^5.4.10"
+ }
+}
diff --git a/e2e/fixtures/aurelia-app/src/app.html b/e2e/fixtures/aurelia-app/src/app.html
new file mode 100644
index 0000000..3f01056
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/src/app.html
@@ -0,0 +1,24 @@
+
+
${message}
+
+
+ Counter Components
+
+
+
+
+
+
+
+ Increment Main
+ Add User
+
+
diff --git a/e2e/fixtures/aurelia-app/src/app.ts b/e2e/fixtures/aurelia-app/src/app.ts
new file mode 100644
index 0000000..f619d01
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/src/app.ts
@@ -0,0 +1,22 @@
+export class App {
+ message = 'Aurelia 2 E2E Test App';
+ counter = 0;
+
+ users = [
+ { name: 'John Doe', email: 'john@example.com', role: 'admin' },
+ { name: 'Jane Smith', email: 'jane@example.com', role: 'user' },
+ ];
+
+ increment() {
+ this.counter++;
+ }
+
+ addUser() {
+ const id = this.users.length + 1;
+ this.users.push({
+ name: `User ${id}`,
+ email: `user${id}@example.com`,
+ role: 'user'
+ });
+ }
+}
diff --git a/e2e/fixtures/aurelia-app/src/components/counter.html b/e2e/fixtures/aurelia-app/src/components/counter.html
new file mode 100644
index 0000000..c8626e3
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/src/components/counter.html
@@ -0,0 +1,6 @@
+
+ ${label}:
+ -
+ ${value}
+ +
+
diff --git a/e2e/fixtures/aurelia-app/src/components/counter.ts b/e2e/fixtures/aurelia-app/src/components/counter.ts
new file mode 100644
index 0000000..8a988d5
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/src/components/counter.ts
@@ -0,0 +1,14 @@
+import { bindable } from 'aurelia';
+
+export class Counter {
+ @bindable value = 0;
+ @bindable label = 'Count';
+
+ increment() {
+ this.value++;
+ }
+
+ decrement() {
+ this.value--;
+ }
+}
diff --git a/e2e/fixtures/aurelia-app/src/components/user-card.html b/e2e/fixtures/aurelia-app/src/components/user-card.html
new file mode 100644
index 0000000..32a3e30
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/src/components/user-card.html
@@ -0,0 +1,8 @@
+
+
${initials}
+
+
${name}
+
${email}
+
${role}
+
+
diff --git a/e2e/fixtures/aurelia-app/src/components/user-card.ts b/e2e/fixtures/aurelia-app/src/components/user-card.ts
new file mode 100644
index 0000000..ca5e7c6
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/src/components/user-card.ts
@@ -0,0 +1,19 @@
+import { bindable } from 'aurelia';
+
+export class UserCard {
+ @bindable name = '';
+ @bindable email = '';
+ @bindable role = 'user';
+
+ get initials() {
+ return this.name
+ .split(' ')
+ .map(n => n[0])
+ .join('')
+ .toUpperCase();
+ }
+
+ get isAdmin() {
+ return this.role === 'admin';
+ }
+}
diff --git a/e2e/fixtures/aurelia-app/src/main.ts b/e2e/fixtures/aurelia-app/src/main.ts
new file mode 100644
index 0000000..b54fc8f
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/src/main.ts
@@ -0,0 +1,9 @@
+import Aurelia from 'aurelia';
+import { App } from './app';
+import { Counter } from './components/counter';
+import { UserCard } from './components/user-card';
+
+new Aurelia()
+ .register(Counter, UserCard)
+ .app(App)
+ .start();
diff --git a/e2e/fixtures/aurelia-app/tsconfig.json b/e2e/fixtures/aurelia-app/tsconfig.json
new file mode 100644
index 0000000..b9f6634
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "target": "ES2022",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
diff --git a/e2e/fixtures/aurelia-app/vite.config.ts b/e2e/fixtures/aurelia-app/vite.config.ts
new file mode 100644
index 0000000..a821617
--- /dev/null
+++ b/e2e/fixtures/aurelia-app/vite.config.ts
@@ -0,0 +1,10 @@
+import { defineConfig } from 'vite';
+import aurelia from '@aurelia/vite-plugin';
+
+export default defineConfig({
+ plugins: [aurelia()],
+ server: {
+ port: 5173,
+ strictPort: true,
+ },
+});
diff --git a/e2e/helpers/extension-fixture.ts b/e2e/helpers/extension-fixture.ts
new file mode 100644
index 0000000..3731b99
--- /dev/null
+++ b/e2e/helpers/extension-fixture.ts
@@ -0,0 +1,46 @@
+import { test as base, chromium, BrowserContext } from '@playwright/test';
+import path from 'path';
+
+export type ExtensionFixtures = {
+ context: BrowserContext;
+ extensionId: string;
+};
+
+export const test = base.extend({
+ context: async ({}, use) => {
+ const extensionPath = path.join(__dirname, '..', '..', 'dist');
+
+ const args = [
+ `--disable-extensions-except=${extensionPath}`,
+ `--load-extension=${extensionPath}`,
+ '--no-first-run',
+ '--disable-default-apps',
+ '--disable-gpu',
+ '--disable-dev-shm-usage',
+ ];
+
+ if (process.env.CI) {
+ args.push('--headless=new');
+ }
+
+ const context = await chromium.launchPersistentContext('', {
+ headless: false,
+ args,
+ });
+
+ await use(context);
+ await context.close();
+ },
+
+ extensionId: async ({ context }, use) => {
+ let serviceWorker = context.serviceWorkers()[0];
+ if (!serviceWorker) {
+ serviceWorker = await context.waitForEvent('serviceworker', { timeout: 30000 });
+ }
+
+ const extensionId = serviceWorker.url().split('/')[2];
+ await use(extensionId);
+ },
+});
+
+export { expect } from '@playwright/test';
diff --git a/e2e/helpers/extension-utils.ts b/e2e/helpers/extension-utils.ts
new file mode 100644
index 0000000..fbc689b
--- /dev/null
+++ b/e2e/helpers/extension-utils.ts
@@ -0,0 +1,39 @@
+import { BrowserContext, Page } from '@playwright/test';
+
+export async function getExtensionPopup(
+ context: BrowserContext,
+ extensionId: string,
+ popupPath: string
+): Promise {
+ const popupUrl = `chrome-extension://${extensionId}/${popupPath}`;
+ const page = await context.newPage();
+ await page.goto(popupUrl);
+ return page;
+}
+
+export async function waitForAureliaDetection(
+ page: Page,
+ expectedVersion: number = 2,
+ timeout: number = 10000
+): Promise {
+ await page.waitForFunction(
+ (version) => (window as any).__AURELIA_DEVTOOLS_DETECTED_VERSION__ === version,
+ expectedVersion,
+ { timeout }
+ );
+}
+
+export async function getDetectionState(page: Page): Promise<{
+ version: number | null;
+ state: string | null;
+}> {
+ return page.evaluate(() => ({
+ version: (window as any).__AURELIA_DEVTOOLS_DETECTED_VERSION__ ?? null,
+ state: (window as any).__AURELIA_DEVTOOLS_DETECTION_STATE__ ?? null,
+ }));
+}
+
+export async function waitForPageLoad(page: Page): Promise {
+ await page.waitForLoadState('domcontentloaded');
+ await page.waitForLoadState('networkidle');
+}
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
new file mode 100644
index 0000000..d460d4d
--- /dev/null
+++ b/e2e/playwright.config.ts
@@ -0,0 +1,36 @@
+import { defineConfig } from '@playwright/test';
+import path from 'path';
+
+const fixtureAppPath = path.join(__dirname, 'fixtures', 'aurelia-app');
+
+export default defineConfig({
+ testDir: './tests',
+ fullyParallel: false,
+ forbidOnly: !!process.env.CI,
+ retries: process.env.CI ? 2 : 0,
+ workers: 1,
+ reporter: process.env.CI ? 'github' : 'html',
+ timeout: 30000,
+
+ use: {
+ baseURL: 'http://localhost:4173',
+ trace: 'on-first-retry',
+ },
+
+ webServer: {
+ command: 'npm run build && npm run preview',
+ cwd: fixtureAppPath,
+ url: 'http://localhost:4173',
+ reuseExistingServer: !process.env.CI,
+ timeout: 120000,
+ },
+
+ projects: [
+ {
+ name: 'chromium-extension',
+ use: {
+ browserName: 'chromium',
+ },
+ },
+ ],
+});
diff --git a/e2e/tests/background.spec.ts b/e2e/tests/background.spec.ts
new file mode 100644
index 0000000..54a92e7
--- /dev/null
+++ b/e2e/tests/background.spec.ts
@@ -0,0 +1,54 @@
+import { test, expect } from '../helpers/extension-fixture';
+import { waitForPageLoad } from '../helpers/extension-utils';
+
+test.describe('Background Script', () => {
+ test('service worker is running', async ({ context, extensionId }) => {
+ const serviceWorker = context.serviceWorkers()[0];
+ expect(serviceWorker).toBeTruthy();
+ expect(serviceWorker.url()).toContain(extensionId);
+ expect(serviceWorker.url()).toContain('background.js');
+ });
+
+ test('extension has correct manifest permissions', async ({ context, extensionId }) => {
+ const manifestPage = await context.newPage();
+ await manifestPage.goto(`chrome-extension://${extensionId}/manifest.json`);
+
+ const manifestText = await manifestPage.locator('body').textContent();
+ const manifest = JSON.parse(manifestText || '{}');
+
+ expect(manifest.manifest_version).toBe(3);
+ expect(manifest.permissions).toContain('activeTab');
+ expect(manifest.content_scripts).toHaveLength(2);
+
+ await manifestPage.close();
+ });
+
+ test('extension icon files exist', async ({ context, extensionId }) => {
+ const iconPage = await context.newPage();
+
+ const response = await iconPage.goto(`chrome-extension://${extensionId}/images/16.png`);
+ expect(response?.status()).toBe(200);
+
+ const greyResponse = await iconPage.goto(`chrome-extension://${extensionId}/images/16-GREY.png`);
+ expect(greyResponse?.status()).toBe(200);
+
+ await iconPage.close();
+ });
+
+ test('content scripts are defined in manifest', async ({ context, extensionId }) => {
+ const manifestPage = await context.newPage();
+ await manifestPage.goto(`chrome-extension://${extensionId}/manifest.json`);
+
+ const manifestText = await manifestPage.locator('body').textContent();
+ const manifest = JSON.parse(manifestText || '{}');
+
+ const contentScripts = manifest.content_scripts;
+ expect(contentScripts).toBeDefined();
+
+ const scriptPaths = contentScripts.flatMap((cs: any) => cs.js);
+ expect(scriptPaths).toContain('build/detector.js');
+ expect(scriptPaths).toContain('build/contentscript.js');
+
+ await manifestPage.close();
+ });
+});
diff --git a/e2e/tests/components.spec.ts b/e2e/tests/components.spec.ts
new file mode 100644
index 0000000..51f68f0
--- /dev/null
+++ b/e2e/tests/components.spec.ts
@@ -0,0 +1,153 @@
+import { test, expect } from '../helpers/extension-fixture';
+import { waitForPageLoad } from '../helpers/extension-utils';
+
+test.describe('Aurelia Components', () => {
+ test('renders multiple custom elements', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ const counters = await page.locator('counter').count();
+ expect(counters).toBe(2);
+
+ const userCards = await page.locator('user-card').count();
+ expect(userCards).toBe(2);
+
+ await page.close();
+ });
+
+ test('counter component has bindable properties', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ const mainCounter = page.locator('counter').first();
+ const label = await mainCounter.locator('.counter-label').textContent();
+ expect(label).toContain('Main Counter');
+
+ await page.close();
+ });
+
+ test('counter increment button works', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ const counter = page.locator('counter').first();
+ const valueBefore = await counter.locator('.counter-value').textContent();
+
+ await counter.locator('.increment').click();
+
+ const valueAfter = await counter.locator('.counter-value').textContent();
+ expect(parseInt(valueAfter || '0')).toBe(parseInt(valueBefore || '0') + 1);
+
+ await page.close();
+ });
+
+ test('user-card displays user information', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ const firstCard = page.locator('user-card').first();
+
+ const name = await firstCard.locator('.user-name').textContent();
+ expect(name).toBe('John Doe');
+
+ const email = await firstCard.locator('.user-email').textContent();
+ expect(email).toBe('john@example.com');
+
+ const role = await firstCard.locator('.user-role').textContent();
+ expect(role).toBe('admin');
+
+ await page.close();
+ });
+
+ test('user-card computed property shows initials', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ const firstCard = page.locator('user-card').first();
+ const initials = await firstCard.locator('.user-avatar').textContent();
+ expect(initials).toBe('JD');
+
+ await page.close();
+ });
+
+ test('add user button creates new user-card', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ const initialCount = await page.locator('user-card').count();
+ expect(initialCount).toBe(2);
+
+ await page.locator('.add-user-btn').click();
+
+ await page.waitForTimeout(500);
+ const newCount = await page.locator('user-card').count();
+ expect(newCount).toBe(3);
+
+ await page.close();
+ });
+
+ test('all components have $au property', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ const hasAuOnApp = await page.evaluate(() => {
+ const app = document.querySelector('app');
+ return app && '$au' in app;
+ });
+ expect(hasAuOnApp).toBe(true);
+
+ const hasAuOnCounter = await page.evaluate(() => {
+ const counter = document.querySelector('counter');
+ return counter && '$au' in counter;
+ });
+ expect(hasAuOnCounter).toBe(true);
+
+ const hasAuOnUserCard = await page.evaluate(() => {
+ const card = document.querySelector('user-card');
+ return card && '$au' in card;
+ });
+ expect(hasAuOnUserCard).toBe(true);
+
+ await page.close();
+ });
+
+ test('component controllers are accessible via $au', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ const controllerInfo = await page.evaluate(() => {
+ const counter = document.querySelector('counter') as any;
+ if (!counter || !counter.$au) return null;
+
+ const controller = counter.$au['au:resource:custom-element'];
+ return {
+ hasController: !!controller,
+ hasViewModel: !!controller?.viewModel,
+ viewModelType: controller?.viewModel?.constructor?.name,
+ };
+ });
+
+ expect(controllerInfo).toBeTruthy();
+ expect(controllerInfo?.hasController).toBe(true);
+ expect(controllerInfo?.hasViewModel).toBe(true);
+ expect(controllerInfo?.viewModelType).toBe('Counter');
+
+ await page.close();
+ });
+});
diff --git a/e2e/tests/content-script.spec.ts b/e2e/tests/content-script.spec.ts
new file mode 100644
index 0000000..e3d557c
--- /dev/null
+++ b/e2e/tests/content-script.spec.ts
@@ -0,0 +1,26 @@
+import { test, expect } from '../helpers/extension-fixture';
+import { waitForPageLoad } from '../helpers/extension-utils';
+
+test.describe('Content Script', () => {
+ test('Aurelia 2 app has $aurelia property on root element', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+ await page.waitForTimeout(2000);
+
+ const hasAureliaProperty = await page.evaluate(() => {
+ const app = document.querySelector('app');
+ return app && '$aurelia' in app;
+ });
+
+ expect(hasAureliaProperty).toBe(true);
+
+ await page.close();
+ });
+
+ test('extension service worker is active', async ({ context, extensionId }) => {
+ expect(extensionId).toBeTruthy();
+ expect(extensionId.length).toBeGreaterThan(10);
+ });
+});
diff --git a/e2e/tests/detection.spec.ts b/e2e/tests/detection.spec.ts
new file mode 100644
index 0000000..557e01a
--- /dev/null
+++ b/e2e/tests/detection.spec.ts
@@ -0,0 +1,65 @@
+import { test, expect } from '../helpers/extension-fixture';
+import { waitForAureliaDetection, getDetectionState, waitForPageLoad } from '../helpers/extension-utils';
+
+test.describe('Aurelia Detection Flow', () => {
+ test('Aurelia fixture app renders correctly', async ({ context }) => {
+ const page = await context.newPage();
+
+ page.on('console', msg => console.log('PAGE LOG:', msg.text()));
+ page.on('pageerror', err => console.log('PAGE ERROR:', err.message));
+
+ await page.goto('/', { waitUntil: 'domcontentloaded' });
+
+ await page.waitForSelector('h1', { timeout: 15000 });
+ const heading = await page.locator('h1').textContent();
+ expect(heading).toBe('Aurelia 2 E2E Test App');
+
+ await page.close();
+ });
+
+ test('Aurelia 2 app has $au property on custom element', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ await page.waitForTimeout(2000);
+
+ const hasAuProperty = await page.evaluate(() => {
+ const app = document.querySelector('app');
+ return app && '$au' in app;
+ });
+
+ expect(hasAuProperty).toBe(true);
+
+ await page.close();
+ });
+
+ test('Aurelia 2 bootstraps successfully', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('/');
+ await waitForPageLoad(page);
+
+ const isBootstrapped = await page.evaluate(() => {
+ const app = document.querySelector('app');
+ return app && ('$au' in app || '$aurelia' in app);
+ });
+
+ expect(isBootstrapped).toBe(true);
+
+ await page.close();
+ });
+
+ test('does not detect Aurelia on non-Aurelia page', async ({ context }) => {
+ const page = await context.newPage();
+
+ await page.goto('about:blank');
+ await page.waitForTimeout(1000);
+
+ const state = await getDetectionState(page);
+ expect(state.version).toBeNull();
+
+ await page.close();
+ });
+});
diff --git a/e2e/tests/popup.spec.ts b/e2e/tests/popup.spec.ts
new file mode 100644
index 0000000..37d0a20
--- /dev/null
+++ b/e2e/tests/popup.spec.ts
@@ -0,0 +1,25 @@
+import { test, expect } from '../helpers/extension-fixture';
+import { getExtensionPopup } from '../helpers/extension-utils';
+
+test.describe('Extension Popup', () => {
+ test('shows "Aurelia not detected" popup by default', async ({ context, extensionId }) => {
+ const popup = await getExtensionPopup(context, extensionId, 'popups/missing.html');
+
+ const content = await popup.locator('h3').textContent();
+ expect(content).toContain('Aurelia not detected');
+
+ await popup.close();
+ });
+
+ test('enabled-v2 popup has correct content', async ({ context, extensionId }) => {
+ const popup = await getExtensionPopup(context, extensionId, 'popups/enabled-v2.html');
+
+ const heading = await popup.locator('h3').textContent();
+ expect(heading).toContain('Aurelia 2 detected');
+
+ const instructions = await popup.locator('p').textContent();
+ expect(instructions).toContain('DevTools');
+
+ await popup.close();
+ });
+});
diff --git a/e2e/tests/sidebar.spec.ts b/e2e/tests/sidebar.spec.ts
new file mode 100644
index 0000000..f71ab2f
--- /dev/null
+++ b/e2e/tests/sidebar.spec.ts
@@ -0,0 +1,415 @@
+import { test, expect } from '../helpers/extension-fixture';
+
+test.describe('Sidebar Panel', () => {
+ test('sidebar HTML loads correctly', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ const root = await page.locator('sidebar-app');
+ await expect(root).toBeVisible();
+
+ await page.close();
+ });
+
+ test('sidebar CSS is loaded', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+
+ const cssResponse = await page.goto(`chrome-extension://${extensionId}/build/sidebar.css`);
+ expect(cssResponse?.status()).toBe(200);
+
+ await page.close();
+ });
+
+ test('sidebar JS is loaded', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+
+ const jsResponse = await page.goto(`chrome-extension://${extensionId}/build/sidebar.js`);
+ expect(jsResponse?.status()).toBe(200);
+
+ await page.close();
+ });
+
+ test('sidebar shows detection state initially', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+ await page.waitForTimeout(1000);
+
+ const checkingState = await page.locator('text=Detecting Aurelia').count();
+ const notFoundState = await page.locator('text=No Aurelia detected').count();
+ const detectedContent = await page.locator('.sidebar-content').count();
+
+ expect(checkingState + notFoundState + detectedContent).toBeGreaterThan(0);
+
+ await page.close();
+ });
+
+ test('sidebar has toolbar when Aurelia detected', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ await page.evaluate(() => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ }
+ });
+
+ await page.waitForTimeout(500);
+
+ const toolbar = page.locator('.toolbar');
+ await expect(toolbar).toBeVisible();
+
+ await page.close();
+ });
+
+ test('sidebar has search input', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ await page.evaluate(() => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ }
+ });
+
+ await page.waitForTimeout(500);
+
+ const searchInput = page.locator('.search-input');
+ await expect(searchInput).toBeVisible();
+
+ await page.close();
+ });
+
+ test('sidebar shows empty state when no element selected', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ await page.evaluate(() => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ app.selectedElement = null;
+ }
+ });
+
+ await page.waitForTimeout(500);
+
+ const emptyState = page.locator('.empty-state');
+ await expect(emptyState).toBeVisible();
+
+ await page.close();
+ });
+
+ test('sidebar displays component info when element is selected', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ const mockComponent = {
+ name: 'my-component',
+ key: 'au:resource:custom-element:my-component',
+ bindables: [
+ { name: 'value', value: 42, type: 'number' },
+ { name: 'label', value: 'Test', type: 'string' },
+ ],
+ properties: [
+ { name: 'count', value: 10, type: 'number' },
+ ],
+ };
+
+ await page.evaluate((component) => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ app.selectedElement = component;
+ app.selectedNodeType = 'custom-element';
+ }
+ }, mockComponent);
+
+ await page.waitForTimeout(500);
+
+ const componentName = page.locator('.component-name');
+ await expect(componentName).toHaveText('my-component');
+
+ await page.close();
+ });
+
+ test('sidebar sections can be expanded and collapsed', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ const mockComponent = {
+ name: 'test-component',
+ key: 'au:resource:custom-element:test-component',
+ bindables: [{ name: 'value', value: 1, type: 'number' }],
+ properties: [{ name: 'prop', value: 'test', type: 'string' }],
+ };
+
+ await page.evaluate((component) => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ app.selectedElement = component;
+ app.selectedNodeType = 'custom-element';
+ }
+ }, mockComponent);
+
+ await page.waitForTimeout(500);
+
+ const bindablesSection = page.locator('.section-header:has-text("Bindables")');
+ await expect(bindablesSection).toBeVisible();
+
+ await bindablesSection.click();
+ await page.waitForTimeout(200);
+
+ await bindablesSection.click();
+ await page.waitForTimeout(200);
+
+ await page.close();
+ });
+
+ test('sidebar displays property values correctly', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ const mockComponent = {
+ name: 'prop-test',
+ key: 'au:resource:custom-element:prop-test',
+ bindables: [
+ { name: 'stringVal', value: 'hello', type: 'string' },
+ { name: 'numberVal', value: 42, type: 'number' },
+ { name: 'boolVal', value: true, type: 'boolean' },
+ ],
+ properties: [],
+ };
+
+ await page.evaluate((component) => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ app.selectedElement = component;
+ app.selectedNodeType = 'custom-element';
+ app.expandedSections.bindables = true;
+ }
+ }, mockComponent);
+
+ await page.waitForTimeout(500);
+
+ const stringProperty = page.locator('.property-name:has-text("stringVal")');
+ await expect(stringProperty).toBeVisible();
+
+ const numberProperty = page.locator('.property-name:has-text("numberVal")');
+ await expect(numberProperty).toBeVisible();
+
+ const boolProperty = page.locator('.property-name:has-text("boolVal")');
+ await expect(boolProperty).toBeVisible();
+
+ await page.close();
+ });
+
+ test('sidebar shows binding context indicator for non-component elements', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ const mockComponent = {
+ name: 'parent-component',
+ key: 'au:resource:custom-element:parent-component',
+ bindables: [],
+ properties: [],
+ };
+
+ await page.evaluate((component) => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ app.selectedElement = component;
+ app.selectedNodeType = 'custom-element';
+ app.selectedElementTagName = 'div';
+ app.isShowingBindingContext = true;
+ }
+ }, mockComponent);
+
+ await page.waitForTimeout(500);
+
+ const bindingContextLabel = page.locator('.binding-context-label');
+ await expect(bindingContextLabel).toBeVisible();
+
+ const selectedElement = page.locator('.selected-element');
+ await expect(selectedElement).toContainText('div');
+
+ await page.close();
+ });
+
+ test('sidebar shows extension invalidated state', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ await page.evaluate(() => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.extensionInvalidated = true;
+ }
+ });
+
+ await page.waitForTimeout(500);
+
+ const invalidatedMessage = page.locator('.state-message.error');
+ await expect(invalidatedMessage).toBeVisible();
+
+ const reloadText = page.locator('text=reload DevTools');
+ await expect(reloadText).toBeVisible();
+
+ await page.close();
+ });
+
+ test('sidebar element picker button exists', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ await page.evaluate(() => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ }
+ });
+
+ await page.waitForTimeout(500);
+
+ const pickerButton = page.locator('.tool-btn').first();
+ await expect(pickerButton).toBeVisible();
+
+ await page.close();
+ });
+
+ test('sidebar follow selection button exists', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ await page.evaluate(() => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ }
+ });
+
+ await page.waitForTimeout(500);
+
+ const toolButtons = await page.locator('.tool-btn').count();
+ expect(toolButtons).toBeGreaterThanOrEqual(2);
+
+ await page.close();
+ });
+});
+
+test.describe('Sidebar Expression Evaluator', () => {
+ test('expression evaluator section exists', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ const mockComponent = {
+ name: 'eval-test',
+ key: 'au:resource:custom-element:eval-test',
+ bindables: [],
+ properties: [],
+ };
+
+ await page.evaluate((component) => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ app.selectedElement = component;
+ app.selectedNodeType = 'custom-element';
+ }
+ }, mockComponent);
+
+ await page.waitForTimeout(500);
+
+ const evaluateSection = page.locator('.section-header:has-text("Evaluate")');
+ await expect(evaluateSection).toBeVisible();
+
+ await page.close();
+ });
+
+ test('expression input and run button exist', async ({ context, extensionId }) => {
+ const page = await context.newPage();
+ await page.goto(`chrome-extension://${extensionId}/sidebar.html`);
+
+ await page.waitForSelector('sidebar-app', { timeout: 10000 });
+
+ const mockComponent = {
+ name: 'eval-test',
+ key: 'au:resource:custom-element:eval-test',
+ bindables: [],
+ properties: [],
+ };
+
+ await page.evaluate((component) => {
+ const appElement = document.querySelector('sidebar-app') as any;
+ const app = appElement?.$controller?.viewModel || appElement?.$au?.['au:resource:custom-element']?.viewModel;
+ if (app) {
+ app.detectionState = 'detected';
+ app.aureliaDetected = true;
+ app.selectedElement = component;
+ app.selectedNodeType = 'custom-element';
+ app.expandedSections.expression = true;
+ }
+ }, mockComponent);
+
+ await page.waitForTimeout(500);
+
+ const expressionInput = page.locator('.expression-input');
+ await expect(expressionInput).toBeVisible();
+
+ const runButton = page.locator('.eval-btn');
+ await expect(runButton).toBeVisible();
+
+ await page.close();
+ });
+});
diff --git a/index.html b/index.html
deleted file mode 100644
index c833c55..0000000
--- a/index.html
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
- Aurelia Devtools
-
-
-
-
-
-
-
diff --git a/manifest.json b/manifest.json
index 3c2810f..c7d09be 100644
--- a/manifest.json
+++ b/manifest.json
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "Aurelia Inspector",
- "version": "0.2",
+ "version": "1.0.0",
"description": "Chrome DevTools extension for inspecting Aurelia v1 and v2 applications.",
"author": "Aurelia Team",
"devtools_page": "devtools/devtools.html",
@@ -14,7 +14,8 @@
{
"resources": [
"devtools/devtools.html",
- "devtools-background.html"
+ "devtools-background.html",
+ "sidebar.html"
],
"matches": [
"*://*/*"
diff --git a/package-lock.json b/package-lock.json
index 37d7703..8fd644d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@aurelia/devtools",
- "version": "0.1.0",
+ "version": "0.2.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@aurelia/devtools",
- "version": "0.1.0",
+ "version": "0.2.0",
"license": "MIT",
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
@@ -16,6 +16,7 @@
"devDependencies": {
"@aurelia/testing": "^2.0.0-beta.25",
"@aurelia/vite-plugin": "^2.0.0-beta.25",
+ "@playwright/test": "^1.57.0",
"@tailwindcss/forms": "^0.5.10",
"@types/chrome": "0.0.290",
"@types/jest": "^29.5.12",
@@ -2097,6 +2098,22 @@
"node": ">= 8"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.57.0.tgz",
+ "integrity": "sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.57.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@rollup/pluginutils": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.0.2.tgz",
@@ -7257,6 +7274,53 @@
"node": ">=8"
}
},
+ "node_modules/playwright": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz",
+ "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.57.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.57.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz",
+ "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
diff --git a/package.json b/package.json
index 078fe5e..059342b 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@aurelia/devtools",
"description": "A browser extension for debugging Aurelia 1 and 2 applications.",
- "version": "0.2.0",
+ "version": "1.0.0",
"repository": {
"type": "git",
"url": "https://github.com/aurelia/devtools"
@@ -15,6 +15,7 @@
"devDependencies": {
"@aurelia/testing": "^2.0.0-beta.25",
"@aurelia/vite-plugin": "^2.0.0-beta.25",
+ "@playwright/test": "^1.57.0",
"@tailwindcss/forms": "^0.5.10",
"@types/chrome": "0.0.290",
"@types/jest": "^29.5.12",
@@ -47,6 +48,11 @@
"analyze": "rimraf dist && vite-bundle-analyzer",
"test": "NODE_OPTIONS=--no-warnings jest --config jest.config.mjs --passWithNoTests",
"test:watch": "NODE_OPTIONS=--no-warnings jest --config jest.config.mjs --watch",
- "test:ci": "NODE_OPTIONS=--no-warnings jest --config jest.config.mjs --runInBand"
+ "test:ci": "NODE_OPTIONS=--no-warnings jest --config jest.config.mjs --runInBand",
+ "test:e2e": "npm run build && npx playwright test --config=e2e/playwright.config.ts",
+ "test:e2e:headed": "npm run build && npx playwright test --config=e2e/playwright.config.ts --headed",
+ "test:e2e:debug": "npm run build && npx playwright test --config=e2e/playwright.config.ts --debug",
+ "test:e2e:ui": "npm run build && npx playwright test --config=e2e/playwright.config.ts --ui",
+ "e2e:fixture:install": "cd e2e/fixtures/aurelia-app && npm install"
}
}
diff --git a/sidebar.html b/sidebar.html
new file mode 100644
index 0000000..dc1d865
--- /dev/null
+++ b/sidebar.html
@@ -0,0 +1,12 @@
+
+
+
+
+ Aurelia DevTools Sidebar
+
+
+
+
+
+
+
diff --git a/src/app.html b/src/app.html
deleted file mode 100644
index 833c467..0000000
--- a/src/app.html
+++ /dev/null
@@ -1,1156 +0,0 @@
-
diff --git a/src/app.ts b/src/app.ts
deleted file mode 100644
index 3feb57b..0000000
--- a/src/app.ts
+++ /dev/null
@@ -1,2029 +0,0 @@
-import { DebugHost, SelectionChanged } from './backend/debug-host';
-import {
- ValueConverterInstance,
- ICustomElementViewModel,
- IPlatform,
-} from 'aurelia';
-import { IControllerInfo, AureliaComponentSnapshot, AureliaComponentTreeNode, AureliaInfo, ComputedPropertyInfo, DISnapshot, EventInteractionRecord, ExternalPanelContext, ExternalPanelDefinition, ExternalPanelSnapshot, InteractionPhase, LifecycleHooksSnapshot, PluginDevtoolsResult, Property, PropertyChangeRecord, PropertySnapshot, RouteSnapshot, SlotSnapshot } from './shared/types';
-import { resolve } from '@aurelia/kernel';
-
-export class App implements ICustomElementViewModel {
- debugInfo: any;
- isDarkTheme: boolean = false;
- JSON = JSON;
-
- // Tab management
- private readonly coreTabs: DevtoolsTabDefinition[] = [
- { id: 'all', label: 'All', icon: '🌲', kind: 'core' },
- { id: 'components', label: 'Components', icon: '📦', kind: 'core' },
- { id: 'attributes', label: 'Attributes', icon: '🔧', kind: 'core' },
- { id: 'interactions', label: 'Interactions', icon: '⏱️', kind: 'core' },
- ];
- activeTab: string = 'all';
- tabs: DevtoolsTabDefinition[] = [...this.coreTabs];
- externalTabs: DevtoolsTabDefinition[] = [];
- externalPanels: Record = {};
- private externalPanelsVersion = 0;
- private externalPanelLoading: Record = {};
- private externalRefreshHandle: ReturnType | null = null;
-
- // Inspector tab data
- selectedElement: IControllerInfo = undefined;
- selectedElementAttributes: IControllerInfo[] = undefined;
- selectedNodeType: 'custom-element' | 'custom-attribute' = 'custom-element';
-
- // Components tab data
- componentSnapshot: AureliaComponentSnapshot = { tree: [], flat: [] };
- allAureliaObjects: AureliaInfo[] = undefined;
- componentTree: ComponentNode[] = [];
- viewMode: 'tree' | 'list' = 'tree';
- selectedBreadcrumb: ComponentNode[] = [];
- selectedComponentId: string = undefined;
- searchQuery: string = '';
- searchMode: 'name' | 'property' | 'all' = 'name';
- isElementPickerActive: boolean = false;
- // Preference: follow Chrome Elements selection automatically
- followChromeSelection: boolean = true;
- // UI: animate refresh icon when user-triggered refresh happens
- isRefreshing: boolean = false;
- propertyRowsRevision: number = 0;
-
- // Interaction timeline
- interactionLog: EventInteractionRecord[] = [];
- interactionLoading: boolean = false;
- interactionError: string | null = null;
- private interactionSignature: string = '';
- private runtimeInteractionListener: ((msg: any, sender: any) => void) | null = null;
-
- // Property watching
- private propertyChangeListener: ((msg: any, sender: any) => void) | null = null;
- private treeChangeListener: ((msg: any, sender: any) => void) | null = null;
-
- // Expression evaluation
- expressionInput: string = '';
- expressionResult: string = '';
- expressionResultType: string = '';
- expressionError: string = '';
- expressionHistory: string[] = [];
- isExpressionPanelOpen: boolean = false;
-
- // Enhanced component inspection
- lifecycleHooks: LifecycleHooksSnapshot | null = null;
- computedProperties: ComputedPropertyInfo[] = [];
- dependencies: DISnapshot | null = null;
- routeInfo: RouteSnapshot | null = null;
- slotInfo: SlotSnapshot | null = null;
-
- // Copy feedback
- copiedPropertyId: string | null = null;
-
- // Detection status
- aureliaDetected: boolean = false;
- aureliaVersion: number | null = null;
- detectionState: 'checking' | 'detected' | 'not-found' | 'disabled' = 'checking';
- extensionInvalidated: boolean = false;
-
- private debugHost: DebugHost = resolve(DebugHost);
- private plat: IPlatform = resolve(IPlatform);
-
- attaching() {
- this.debugHost.attach(this);
- this.isDarkTheme = (chrome?.devtools?.panels as any)?.themeName === 'dark';
- [].join();
-
- if (this.isDarkTheme) {
- document.querySelector('html').style.background = '#202124';
- }
-
- // Restore persisted preference for following Elements selection
- try {
- const persisted = localStorage.getItem('au-devtools.followChromeSelection');
- if (persisted != null) this.followChromeSelection = persisted === 'true';
- } catch {}
-
- // Restore preferred view mode
- try {
- const persistedViewMode = localStorage.getItem('au-devtools.viewMode');
- if (persistedViewMode === 'tree' || persistedViewMode === 'list') {
- this.viewMode = persistedViewMode;
- }
- } catch {}
-
- // Wire interaction push stream from content script
- this.registerInteractionStream();
-
- // Wire property and tree change streams
- this.registerPropertyChangeStream();
- this.registerTreeChangeStream();
-
- // Check detection state set by devtools.js
- this.checkDetectionState();
-
- // Delay component loading to allow Aurelia hooks to install, then try regardless of detection flags
- setTimeout(() => {
- this.checkDetectionState();
- this.loadAllComponents();
- }, 1000);
-
- // Poll for detection state changes
- this.startDetectionPolling();
- this.refreshExternalPanels();
- }
-
- get currentController() {
- return this.selectedElement;
- }
-
- checkDetectionState() {
- if (chrome && chrome.devtools) {
- chrome.devtools.inspectedWindow.eval(
- `({
- state: window.__AURELIA_DEVTOOLS_DETECTION_STATE__,
- version: window.__AURELIA_DEVTOOLS_VERSION__
- })`,
- (result: {state: string, version: number}, isException?: any) => {
- if (!isException && result) {
-
- switch (result.state) {
- case 'detected':
- this.aureliaDetected = true;
- this.aureliaVersion = result.version;
- this.detectionState = 'detected';
- break;
- case 'disabled':
- this.aureliaDetected = false;
- this.aureliaVersion = null;
- this.detectionState = 'disabled';
- break;
- case 'not-found':
- this.aureliaDetected = false;
- this.aureliaVersion = null;
- this.detectionState = 'not-found';
- break;
- case 'checking':
- default:
- this.detectionState = 'checking';
- break;
- }
- }
- }
- );
- }
- }
-
- startDetectionPolling() {
- // Poll for detection state changes every 2 seconds
- setInterval(() => {
- if (this.checkExtensionInvalidated()) {
- return;
- }
- this.checkDetectionState();
- if (this.detectionState === 'disabled') {
- return;
- }
- // Keep trying to load until we find components, regardless of detection flags
- if (!this.allAureliaObjects?.length) {
- this.loadAllComponents();
- } else {
- // Check for tree changes and reload if changed
- this.debugHost.checkComponentTreeChanges().then((hasChanged) => {
- if (hasChanged) {
- this.loadAllComponents();
- }
- });
- }
- this.refreshExternalPanels();
- }, 2000);
- }
-
- checkExtensionInvalidated(): boolean {
- try {
- if (!chrome?.runtime?.id) {
- this.extensionInvalidated = true;
- return true;
- }
- } catch {
- this.extensionInvalidated = true;
- return true;
- }
- return false;
- }
-
- recheckAurelia() {
- this.checkDetectionState();
- if (this.aureliaDetected || this.detectionState === 'not-found') {
- this.loadAllComponents();
- }
- }
-
- // Tab management methods
- switchTab(tabId: string) {
- this.activeTab = tabId;
- if (this.isComponentTab(tabId)) {
- this.loadAllComponents();
- } else if (tabId === 'interactions') {
- this.loadInteractionLog(true);
- } else {
- this.refreshExternalPanels(true);
- this.notifyExternalSelection();
- }
- }
-
- get isExternalTabActive(): boolean {
- return this.isExternalTab(this.activeTab);
- }
-
- get activeExternalIcon(): string {
- return this.getActiveExternalPanel()?.icon || '🧩';
- }
-
- get activeExternalTitle(): string {
- return this.getActiveExternalPanel()?.label || 'Inspector';
- }
-
- get activeExternalDescription(): string | undefined {
- return this.getActiveExternalPanel()?.description;
- }
-
- get activeExternalResult(): PluginDevtoolsResult | undefined {
- return this.getActiveExternalPanel();
- }
-
- get activeExternalError(): string | null {
- const panel = this.getActiveExternalPanel();
- if (!panel) {
- return null;
- }
- return panel.status === 'error' ? panel.error || 'Panel error' : null;
- }
-
- get isActiveExternalLoading(): boolean {
- const panelId = this.getExternalPanelIdFromTab(this.activeTab);
- if (!panelId) {
- return false;
- }
- return !!this.externalPanelLoading[panelId];
- }
-
- refreshActiveExternalTab() {
- if (!this.isExternalTabActive) {
- return;
- }
- this.requestExternalPanelRefresh();
- }
-
- private getActiveExternalPanel(): ExternalPanelDefinition | undefined {
- const panelId = this.getExternalPanelIdFromTab(this.activeTab);
- if (!panelId) {
- return undefined;
- }
- return this.externalPanels[panelId];
- }
-
- private isCoreTab(tabId: string): tabId is CoreTabId {
- return tabId === 'all' || tabId === 'components' || tabId === 'attributes' || tabId === 'interactions';
- }
-
- private isComponentTab(tabId: string): tabId is ComponentTabId {
- return tabId === 'all' || tabId === 'components' || tabId === 'attributes';
- }
-
- private isExternalTab(tabId: string): boolean {
- return !!this.getExternalPanelIdFromTab(tabId);
- }
-
- private getExternalPanelIdFromTab(tabId: string): string | null {
- if (!tabId) {
- return null;
- }
- if (tabId.startsWith('external:')) {
- return tabId.slice('external:'.length);
- }
- const panel = this.externalTabs.find((tab) => tab.id === tabId && tab.panelId);
- return panel?.panelId || null;
- }
-
- refreshExternalPanels(force = false): Promise {
- if (this.detectionState === 'disabled') {
- return Promise.resolve();
- }
-
- return this.debugHost
- .getExternalPanelsSnapshot()
- .then((snapshot: ExternalPanelSnapshot) => {
- if (!snapshot) {
- return;
- }
- if (!force && snapshot.version === this.externalPanelsVersion) {
- return;
- }
- this.externalPanelsVersion = snapshot.version;
- this.applyExternalPanels(snapshot);
- })
- .catch((error) => {
- console.warn('Failed to load external Aurelia panels', error);
- });
- }
-
- private applyExternalPanels(snapshot: ExternalPanelSnapshot) {
- const nextPanels: Record = {};
- const tabs = (snapshot.panels || [])
- .filter((panel) => panel && panel.id)
- .map((panel) => {
- nextPanels[panel.id] = panel;
- this.externalPanelLoading[panel.id] = false;
- return {
- id: `external:${panel.id}`,
- label: panel.label || panel.id,
- icon: panel.icon || '🧩',
- kind: 'external' as const,
- panelId: panel.id,
- description: panel.description,
- order: panel.order ?? 0,
- };
- })
- .sort((a, b) => {
- const orderDiff = (a.order ?? 0) - (b.order ?? 0);
- if (orderDiff !== 0) {
- return orderDiff;
- }
- return a.label.localeCompare(b.label);
- });
-
- this.externalPanels = nextPanels;
- this.externalTabs = tabs;
- this.tabs = [...this.coreTabs, ...this.externalTabs];
-
- if (!this.tabs.some((tab) => tab.id === this.activeTab)) {
- this.activeTab = 'all';
- }
- }
-
- private requestExternalPanelRefresh() {
- if (this.detectionState === 'disabled') {
- return;
- }
- const panelId = this.getExternalPanelIdFromTab(this.activeTab);
- if (!panelId) {
- return;
- }
- this.setExternalPanelLoading(panelId, true);
- this.debugHost.emitExternalPanelEvent('aurelia-devtools:request-panel', {
- id: panelId,
- context: this.buildExternalPanelContext(),
- });
- this.scheduleExternalPanelRefresh();
- }
-
- private scheduleExternalPanelRefresh(delay = 150) {
- if (this.externalRefreshHandle) {
- clearTimeout(this.externalRefreshHandle);
- }
- this.externalRefreshHandle = setTimeout(() => {
- this.refreshExternalPanels(true);
- this.externalRefreshHandle = null;
- }, delay);
- }
-
- private setExternalPanelLoading(panelId: string, isLoading: boolean) {
- this.externalPanelLoading[panelId] = isLoading;
- }
-
- private buildExternalPanelContext(): ExternalPanelContext {
- const selectedNode = this.selectedComponentId ? this.findComponentById(this.selectedComponentId) : null;
- const selectedInfo = selectedNode ? selectedNode.data.info : null;
-
- return {
- selectedComponentId: this.selectedComponentId,
- selectedNodeType: selectedNode?.type,
- selectedDomPath: selectedNode?.domPath,
- aureliaVersion: this.aureliaVersion,
- selectedInfo,
- };
- }
-
- private notifyExternalSelection() {
- this.debugHost.emitExternalPanelEvent('aurelia-devtools:selection-changed', this.buildExternalPanelContext());
- }
-
- // Component discovery methods
- loadAllComponents(): Promise {
- if (this.detectionState === 'disabled') {
- this.handleComponentSnapshot({ tree: [], flat: [] });
- return Promise.resolve();
- }
-
- return new Promise((resolve, reject) => {
- this.plat.queueMicrotask(() => {
- this.debugHost
- .getAllComponents()
- .then((snapshot) => {
- this.handleComponentSnapshot(snapshot);
- this.refreshExternalPanels();
-
- const hasData = !!(snapshot?.tree?.length || snapshot?.flat?.length);
- if (hasData) {
- this.aureliaDetected = true;
- this.detectionState = 'detected';
- }
-
- resolve();
- })
- .catch((error) => {
- console.warn('Failed to load components:', error);
- this.handleComponentSnapshot({ tree: [], flat: [] });
- reject(error);
- });
- });
- });
- }
-
- // Interaction timeline
- async loadInteractionLog(force = false, silent = false): Promise {
- if (this.interactionLoading && !force && !silent) return;
-
- const previousSignature = this.interactionSignature;
- if (!silent) {
- this.interactionLoading = true;
- this.interactionError = null;
- }
- try {
- const log = await this.debugHost.getInteractionLog();
- const sorted = Array.isArray(log)
- ? [...log].sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0))
- : [];
- const signature = sorted.map((e) => `${e.id}:${e.timestamp}`).join('|');
- this.interactionSignature = signature;
- if (signature !== previousSignature) {
- this.interactionLog = sorted;
- }
- } catch (error) {
- if (!silent) {
- this.interactionError = (error as any)?.message || String(error);
- this.interactionLog = [];
- }
- } finally {
- if (!silent) {
- this.interactionLoading = false;
- }
- }
- }
-
- async replayInteraction(id: string): Promise {
- if (!id) return;
- try {
- await this.debugHost.replayInteraction(id);
- } catch (error) {
- console.warn('Replay failed', error);
- }
- }
-
- async applyInteractionSnapshot(id: string, phase: InteractionPhase): Promise {
- if (!id) return;
- try {
- await this.debugHost.applyInteractionSnapshot(id, phase);
- } catch (error) {
- console.warn('Apply snapshot failed', error);
- }
- }
-
- async clearInteractionLog(): Promise {
- // Optimistically clear local log for immediate UI feedback
- this.interactionLog = [];
- this.interactionSignature = '';
- this.interactionError = null;
-
- try {
- await this.debugHost.clearInteractionLog();
- } catch (error) {
- console.warn('Clear interaction log failed', error);
- }
- }
-
- // Enhanced component inspection
- async loadEnhancedInfo(): Promise {
- const componentKey = this.selectedElement?.key || this.selectedElement?.name;
- if (!componentKey) {
- this.clearEnhancedInfo();
- return;
- }
-
- try {
- const [hooks, computed, deps, route, slots] = await Promise.all([
- this.debugHost.getLifecycleHooks(componentKey),
- this.debugHost.getComputedProperties(componentKey),
- this.debugHost.getDependencies(componentKey),
- this.debugHost.getRouteInfo(componentKey),
- this.debugHost.getSlotInfo(componentKey),
- ]);
-
- this.lifecycleHooks = hooks;
- this.computedProperties = computed || [];
- this.dependencies = deps;
- this.routeInfo = route;
- this.slotInfo = slots;
- } catch (error) {
- this.clearEnhancedInfo();
- }
- }
-
- clearEnhancedInfo(): void {
- this.lifecycleHooks = null;
- this.computedProperties = [];
- this.dependencies = null;
- this.routeInfo = null;
- this.slotInfo = null;
- }
-
- get implementedHooksCount(): number {
- if (!this.lifecycleHooks?.hooks) return 0;
- return this.lifecycleHooks.hooks.filter(h => h.implemented).length;
- }
-
- get totalHooksCount(): number {
- if (!this.lifecycleHooks?.hooks) return 0;
- return this.lifecycleHooks.hooks.length;
- }
-
- get hasRouteParams(): boolean {
- return !!(this.routeInfo && (this.routeInfo.params.length > 0 || this.routeInfo.queryParams.length > 0));
- }
-
- get activeSlotCount(): number {
- if (!this.slotInfo?.slots) return 0;
- return this.slotInfo.slots.filter(s => s.hasContent).length;
- }
-
- get hasComputedProperties(): boolean {
- return this.computedProperties.length > 0;
- }
-
- get hasDependencies(): boolean {
- return !!(this.dependencies?.dependencies?.length);
- }
-
- get hasSlots(): boolean {
- return !!(this.slotInfo?.slots?.length);
- }
-
- get hasInteractions(): boolean {
- return Array.isArray(this.interactionLog) && this.interactionLog.length > 0;
- }
-
- get formattedInteractionLog(): EventInteractionRecord[] {
- return this.interactionLog || [];
- }
-
- private registerInteractionStream() {
- if (!(chrome?.runtime?.onMessage?.addListener)) return;
- this.runtimeInteractionListener = (message: any) => {
- if (message && message.type === 'au-devtools:interaction' && message.entry) {
- this.handleIncomingInteraction(message.entry as EventInteractionRecord);
- }
- };
- chrome.runtime.onMessage.addListener(this.runtimeInteractionListener);
- }
-
- private handleIncomingInteraction(entry: EventInteractionRecord) {
- if (!entry || !entry.id) return;
- const existing = this.interactionLog.find((e) => e.id === entry.id);
- if (existing) return;
- const next = [entry, ...(this.interactionLog || [])];
- next.sort((a, b) => (b.timestamp || 0) - (a.timestamp || 0));
- this.interactionLog = next;
- this.interactionSignature = next.map((e) => `${e.id}:${e.timestamp}`).join('|');
- }
-
- private registerPropertyChangeStream() {
- if (!(chrome?.runtime?.onMessage?.addListener)) return;
- this.propertyChangeListener = (message: any) => {
- if (message && message.type === 'au-devtools:property-change') {
- this.onPropertyChanges(message.changes, message.snapshot);
- }
- };
- chrome.runtime.onMessage.addListener(this.propertyChangeListener);
- }
-
- private registerTreeChangeStream() {
- if (!(chrome?.runtime?.onMessage?.addListener)) return;
- this.treeChangeListener = (message: any) => {
- if (message && message.type === 'au-devtools:tree-change') {
- this.loadAllComponents();
- }
- };
- chrome.runtime.onMessage.addListener(this.treeChangeListener);
- }
-
- onPropertyChanges(changes: PropertyChangeRecord[], snapshot: PropertySnapshot) {
- if (!changes || !changes.length || !this.selectedElement) return;
-
- const selectedKey = this.selectedElement.key || this.selectedElement.name;
- if (!selectedKey || snapshot?.componentKey !== selectedKey) return;
-
- let hasUpdates = false;
-
- for (const change of changes) {
- const bindable = this.selectedElement.bindables?.find((b) => b.name === change.propertyName);
- if (bindable) {
- bindable.value = change.newValue;
- hasUpdates = true;
- continue;
- }
-
- const property = this.selectedElement.properties?.find((p) => p.name === change.propertyName);
- if (property) {
- property.value = change.newValue;
- hasUpdates = true;
- }
- }
-
- if (hasUpdates) {
- this.refreshPropertyBindings();
- }
- }
-
- // User-triggered refresh with spinner animation
- refreshComponents() {
- if (this.isRefreshing) return;
- this.isRefreshing = true;
- this.loadAllComponents()
- .finally(() => {
- // Allow a short delay so the animation is visible even on fast refresh
- setTimeout(() => (this.isRefreshing = false), 300);
- });
- }
-
- public handleComponentSnapshot(snapshot: AureliaComponentSnapshot) {
- const safeSnapshot = snapshot || { tree: [], flat: [] };
- this.componentSnapshot = safeSnapshot;
-
- const rawTree = (safeSnapshot.tree && safeSnapshot.tree.length)
- ? safeSnapshot.tree
- : this.convertFlatListToTreeNodes(safeSnapshot.flat || []);
-
- const fallbackFlat = (safeSnapshot.flat && safeSnapshot.flat.length)
- ? safeSnapshot.flat
- : this.flattenRawTree(rawTree);
-
- this.allAureliaObjects = fallbackFlat;
- this.componentTree = this.mapRawTreeToComponentNodes(rawTree);
-
- if (this.selectedComponentId) {
- const existingNode = this.findComponentById(this.selectedComponentId);
- if (existingNode) {
- this.applySelectionFromNode(existingNode);
- } else {
- this.debugHost.stopPropertyWatching();
- this.selectedComponentId = undefined;
- this.selectedElement = undefined;
- this.selectedElementAttributes = undefined;
- this.selectedBreadcrumb = [];
- this.selectedNodeType = 'custom-element';
- }
- } else {
- this.selectedBreadcrumb = [];
- this.selectedNodeType = 'custom-element';
- }
- }
-
- private convertFlatListToTreeNodes(components: AureliaInfo[]): AureliaComponentTreeNode[] {
- if (!components || !components.length) {
- return [];
- }
-
- const nodes: AureliaComponentTreeNode[] = [];
- const seenElements = new Set();
-
- components.forEach((component, index) => {
- const elementInfo = component?.customElementInfo ?? null;
- const attrs = this.normalizeAttributes(component?.customAttributesInfo ?? [], elementInfo);
-
- if (elementInfo) {
- const elementId = elementInfo.key || elementInfo.name || `flat-${index}`;
- if (!seenElements.has(elementId)) {
- nodes.push({
- id: elementId,
- domPath: '',
- tagName: elementInfo.name ?? null,
- customElementInfo: elementInfo,
- customAttributesInfo: attrs,
- children: [],
- });
- seenElements.add(elementId);
- }
- } else if (attrs.length) {
- attrs.forEach((attr, attrIndex) => {
- nodes.push({
- id: `flat-attr-${index}-${attrIndex}`,
- domPath: '',
- tagName: null,
- customElementInfo: null,
- customAttributesInfo: [attr],
- children: [],
- });
- });
- }
- });
-
- return nodes;
- }
-
- private mapRawTreeToComponentNodes(rawNodes: AureliaComponentTreeNode[]): ComponentNode[] {
- if (!rawNodes || !rawNodes.length) {
- return [];
- }
-
- const mapped: ComponentNode[] = [];
- for (const rawNode of rawNodes) {
- mapped.push(...this.transformRawNode(rawNode, 'root'));
- }
- return this.sortComponentNodes(mapped);
- }
-
- private transformRawNode(rawNode: AureliaComponentTreeNode, parentId: string): ComponentNode[] {
- if (!rawNode) return [];
-
- const elementInfo = rawNode.customElementInfo ?? null;
- const normalizedAttributes = this.normalizeAttributes(rawNode.customAttributesInfo || [], elementInfo);
- const rawChildren = rawNode.children || [];
-
- if (elementInfo) {
- const baseId = rawNode.id || `${parentId}-${Math.random().toString(36).slice(2)}`;
- const elementName = elementInfo.name || rawNode.tagName || 'unknown-element';
-
- const elementNode: ComponentNode = {
- id: baseId,
- name: elementName,
- type: 'custom-element',
- domPath: rawNode.domPath || '',
- tagName: rawNode.tagName || null,
- children: [],
- data: {
- kind: 'element',
- raw: rawNode,
- info: {
- customElementInfo: elementInfo,
- customAttributesInfo: normalizedAttributes,
- },
- },
- expanded: false,
- hasAttributes: normalizedAttributes.length > 0,
- };
-
- const attributeChildren = normalizedAttributes.map((attr, index) => this.createAttributeNode(baseId, rawNode, attr, index));
- elementNode.children.push(...attributeChildren);
-
- for (const child of rawChildren) {
- elementNode.children.push(...this.transformRawNode(child, baseId));
- }
-
- elementNode.children = this.sortComponentNodes(elementNode.children);
- elementNode.hasAttributes = elementNode.children.some(child => child.type === 'custom-attribute');
-
- return [elementNode];
- }
-
- const nodes: ComponentNode[] = [];
- normalizedAttributes.forEach((attr, index) => {
- nodes.push(this.createAttributeNode(parentId, rawNode, attr, index));
- });
-
- for (const child of rawChildren) {
- nodes.push(...this.transformRawNode(child, parentId));
- }
-
- return nodes;
- }
-
- private createAttributeNode(parentId: string, owner: AureliaComponentTreeNode, attr: IControllerInfo, index: number): ComponentNode {
- const attrName = attr?.name || 'custom-attribute';
- const ownerIdentifier = String(owner?.id || owner?.domPath || parentId || 'attr-owner');
- const attrIdentifier = String(attr?.key || attr?.name || `attr-${index}`);
- const nodeId = `${ownerIdentifier}::${attrIdentifier}::${index}`;
- return {
- id: nodeId,
- name: attrName,
- type: 'custom-attribute',
- domPath: owner?.domPath || '',
- tagName: owner?.tagName || null,
- children: [],
- data: {
- kind: 'attribute',
- raw: attr,
- owner,
- info: {
- customElementInfo: null,
- customAttributesInfo: attr ? [attr] : [],
- },
- },
- expanded: false,
- hasAttributes: false,
- };
- }
-
- private flattenRawTree(rawNodes: AureliaComponentTreeNode[]): AureliaInfo[] {
- const collected: AureliaInfo[] = [];
-
- const walk = (nodes: AureliaComponentTreeNode[]) => {
- for (const node of nodes) {
- if (node.customElementInfo || (node.customAttributesInfo && node.customAttributesInfo.length)) {
- collected.push({
- customElementInfo: node.customElementInfo,
- customAttributesInfo: node.customAttributesInfo || [],
- });
- }
- if (node.children && node.children.length) {
- walk(node.children);
- }
- }
- };
-
- walk(rawNodes || []);
- return collected;
- }
-
- get filteredComponentTreeByTab(): ComponentNode[] {
- if (!this.isComponentTab(this.activeTab)) {
- return [];
- }
-
- // First apply tab filtering to the full tree
- let tabFilteredTree: ComponentNode[];
-
- switch (this.activeTab as ComponentTabId) {
- case 'components':
- tabFilteredTree = this.filterTreeByType(this.componentTree, 'custom-element');
- break;
- case 'attributes':
- tabFilteredTree = this.filterTreeByType(this.componentTree, 'custom-attribute');
- break;
- case 'all':
- default:
- tabFilteredTree = this.componentTree;
- break;
- }
-
- // Then apply search filtering to the tab-filtered tree
- if (!this.searchQuery.trim()) {
- return tabFilteredTree;
- }
-
- const query = this.searchQuery.toLowerCase();
- return this.filterComponentTree(tabFilteredTree, query);
- }
-
- get visibleComponentNodes(): ComponentDisplayNode[] {
- const roots = this.filteredComponentTreeByTab;
- if (!roots || !roots.length) {
- return [];
- }
-
- const searchActive = !!this.searchQuery.trim();
- const items: ComponentDisplayNode[] = [];
-
- const traverse = (nodes: ComponentNode[], depth: number) => {
- for (const node of nodes) {
- items.push({ node, depth });
-
- const hasChildren = node.children && node.children.length > 0;
- if (!hasChildren) continue;
-
- const shouldShowChildren = this.viewMode === 'list' || node.expanded || searchActive;
- if (shouldShowChildren) {
- traverse(node.children, depth + 1);
- }
- }
- };
-
- traverse(roots, 0);
- return items;
- }
-
- get breadcrumbSegments(): BreadcrumbSegment[] {
- if (!this.selectedBreadcrumb || !this.selectedBreadcrumb.length) {
- return [];
- }
-
- return this.selectedBreadcrumb.map((node) => ({
- id: node.id,
- label: node.type === 'custom-element' ? `<${node.name}>` : `@${node.name}`,
- type: node.type,
- }));
- }
-
- get totalComponentNodeCount(): number {
- const countNodes = (nodes: ComponentNode[]): number => {
- return nodes.reduce((total, node) => total + 1 + countNodes(node.children || []), 0);
- };
-
- return countNodes(this.componentTree || []);
- }
-
- private normalizeAttributes(attrs: IControllerInfo[], elementInfo: IControllerInfo | null | undefined): IControllerInfo[] {
- if (!attrs || !attrs.length) {
- return [];
- }
-
- const seen = new Set();
-
- return attrs.filter((attr) => {
- if (!attr) return false;
- const key = (attr.key || attr.name || '').toString().toLowerCase();
- const sameAsElement = !!(elementInfo && ((attr.key && elementInfo.key && attr.key === elementInfo.key) || (attr.name && elementInfo.name && attr.name === elementInfo.name)));
- if (sameAsElement) return false;
- if (key && seen.has(key)) return false;
- if (key) {
- seen.add(key);
- }
- return true;
- });
- }
-
- private sortComponentNodes(nodes: ComponentNode[]): ComponentNode[] {
- if (!nodes || !nodes.length) return [];
-
- const cloned = nodes.map((node) => ({
- ...node,
- children: this.sortComponentNodes(node.children || []),
- }));
-
- cloned.sort((a, b) => {
- if (a.type === b.type) {
- return a.name.localeCompare(b.name);
- }
- return a.type === 'custom-element' ? -1 : 1;
- });
-
- return cloned;
- }
-
- private filterTreeByType(nodes: ComponentNode[], type: 'custom-element' | 'custom-attribute'): ComponentNode[] {
- if (!nodes || !nodes.length) {
- return [];
- }
-
- const filtered: ComponentNode[] = [];
-
- for (const node of nodes) {
- const filteredChildren = this.filterTreeByType(node.children || [], type);
-
- if (node.type === type) {
- filtered.push({
- ...node,
- children: filteredChildren,
- });
- continue;
- }
-
- if (filteredChildren.length) {
- filtered.push(...filteredChildren);
- }
- }
-
- return filtered;
- }
-
- selectComponent(componentId: string) {
- const component = this.findComponentById(componentId);
- if (component) {
- this.applySelectionFromNode(component);
- }
- }
-
- private applySelectionFromNode(node: ComponentNode) {
- this.selectedComponentId = node.id;
- this.selectedNodeType = node.type;
-
- if (node.data.kind === 'attribute') {
- const attributeInfo = node.data.raw;
- this.selectedElement = attributeInfo || null;
- this.selectedElement.bindables = this.selectedElement?.bindables || [];
- this.selectedElement.properties = this.selectedElement?.properties || [];
- this.selectedElementAttributes = [];
- this.markPropertyRowsDirty();
- } else {
- const elementInfo = node.data.info.customElementInfo;
- const attributeInfos = node.data.info.customAttributesInfo || [];
- this.selectedElement = elementInfo || null;
- this.selectedElement.bindables = this.selectedElement?.bindables || [];
- this.selectedElement.properties = this.selectedElement?.properties || [];
- this.selectedElementAttributes = attributeInfos.filter((attr) => {
- try {
- if (!attr) return false;
- const sameKey = !!(attr.key && elementInfo?.key && attr.key === elementInfo.key);
- const sameName = !!(attr.name && elementInfo?.name && attr.name === elementInfo.name);
- return !(sameKey || sameName);
- } catch {
- return true;
- }
- });
- this.markPropertyRowsDirty();
- }
-
- const path = this.findNodePathById(node.id);
- if (path && path.length) {
- this.selectedBreadcrumb = [...path];
- path.forEach((ancestor) => {
- if (ancestor.data.kind === 'element') {
- ancestor.expanded = true;
- }
- });
- } else {
- this.selectedBreadcrumb = [node];
- }
-
- // Start watching the selected component for property changes
- const componentKey = this.selectedElement?.key || this.selectedElement?.name;
- if (componentKey) {
- this.debugHost.startPropertyWatching({ componentKey, pollInterval: 500 });
- }
-
- // Load enhanced inspection data
- this.loadEnhancedInfo();
-
- if (this.isExternalTabActive) {
- this.scheduleExternalPanelRefresh();
- }
- this.notifyExternalSelection();
- }
-
- private findNodePathById(nodeId: string): ComponentNode[] | null {
- const path: ComponentNode[] = [];
-
- const traverse = (nodes: ComponentNode[]): boolean => {
- for (const node of nodes) {
- path.push(node);
- if (node.id === nodeId) {
- return true;
- }
- if (node.children && node.children.length && traverse(node.children)) {
- return true;
- }
- path.pop();
- }
- return false;
- };
-
- const found = traverse(this.componentTree || []);
- return found ? [...path] : null;
- }
-
- findComponentById(id: string): ComponentNode | undefined {
- const findInTree = (nodes: ComponentNode[]): ComponentNode | undefined => {
- for (const node of nodes) {
- if (node.id === id) return node;
- const found = findInTree(node.children);
- if (found) return found;
- }
- return undefined;
- };
- return findInTree(this.componentTree);
- }
-
- handleExpandToggle(node: ComponentNode, event?: Event) {
- event?.stopPropagation();
- if (!node?.id) {
- return;
- }
- this.toggleComponentExpansion(node.id);
- }
-
- toggleComponentExpansion(componentId: string) {
- const component = this.findComponentById(componentId);
- if (component) {
- component.expanded = !component.expanded;
- }
- }
-
- handlePropertyRowClick(property: any, event?: Event) {
- if (!property?.canExpand || property?.isEditing) {
- return;
- }
-
- const target = event?.target as HTMLElement | null;
- if (!target) return;
-
- if (target.closest('.property-value-wrapper') || target.closest('.property-editor')) {
- return;
- }
-
- if (target.closest('.expand-button')) {
- return;
- }
-
- event?.stopPropagation();
- this.togglePropertyExpansion(property);
- }
-
- // Search and filter functionality
- filterComponents() {
- // Search filtering is now handled in the filteredComponentTreeByTab getter
- // This method is kept for compatibility but doesn't need to do anything
- // since the reactive getter automatically handles filtering
- }
-
- private filterComponentTree(
- nodes: ComponentNode[],
- query: string
- ): ComponentNode[] {
- const filtered: ComponentNode[] = [];
- const lowerQuery = query.toLowerCase();
-
- for (const node of nodes) {
- let matchesSearch = false;
- let matchedPropertyName: string | null = null;
-
- // Name-based search (with fuzzy matching)
- if (this.searchMode === 'name' || this.searchMode === 'all') {
- const queryVariants = getNamingVariants(query);
- const nodeVariants = getNamingVariants(node.name);
-
- // Exact substring match (highest priority)
- const exactMatch = nodeVariants.some((variant) =>
- queryVariants.some((q) => variant.includes(q))
- );
-
- // Fuzzy match (lower priority)
- const isFuzzyMatch = fuzzyMatch(node.name.toLowerCase(), lowerQuery);
-
- // Type match
- const typeMatch = node.type.toLowerCase().includes(lowerQuery);
-
- matchesSearch = exactMatch || isFuzzyMatch || typeMatch;
- }
-
- // Property value search
- if ((this.searchMode === 'property' || this.searchMode === 'all') && !matchesSearch) {
- const info = node.data.kind === 'element'
- ? node.data.info.customElementInfo
- : node.data.raw;
-
- if (info) {
- matchedPropertyName = this.searchInProperties(info, lowerQuery);
- if (matchedPropertyName) {
- matchesSearch = true;
- }
- }
- }
-
- const filteredChildren = this.filterComponentTree(node.children, query);
-
- if (matchesSearch || filteredChildren.length > 0) {
- const filteredNode: ComponentNode = {
- ...node,
- children: filteredChildren,
- expanded: filteredChildren.length > 0 || node.expanded,
- matchedProperty: matchedPropertyName,
- };
- filtered.push(filteredNode);
- }
- }
-
- return filtered;
- }
-
- private searchInProperties(info: IControllerInfo, query: string): string | null {
- const allProperties = [
- ...(info.bindables || []),
- ...(info.properties || []),
- ];
-
- for (const prop of allProperties) {
- if (!prop) continue;
-
- // Search in property name
- if (prop.name?.toLowerCase().includes(query)) {
- return prop.name;
- }
-
- // Search in property value
- const valueStr = this.propertyValueToSearchString(prop.value);
- if (valueStr.toLowerCase().includes(query)) {
- return prop.name;
- }
- }
-
- return null;
- }
-
- private propertyValueToSearchString(value: unknown): string {
- if (value === null) return 'null';
- if (value === undefined) return 'undefined';
- if (typeof value === 'string') return value;
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
- if (Array.isArray(value)) return `[${value.length} items]`;
- if (typeof value === 'object') {
- try {
- return JSON.stringify(value);
- } catch {
- return '[object]';
- }
- }
- return String(value);
- }
-
- clearSearch() {
- this.searchQuery = '';
- // Filtering automatically updates via the reactive getter
- }
-
- setSearchMode(mode: 'name' | 'property' | 'all') {
- this.searchMode = mode;
- }
-
- cycleSearchMode() {
- const modes: Array<'name' | 'property' | 'all'> = ['name', 'property', 'all'];
- const currentIndex = modes.indexOf(this.searchMode);
- this.searchMode = modes[(currentIndex + 1) % modes.length];
- }
-
- get searchModeLabel(): string {
- switch (this.searchMode) {
- case 'name': return 'Name';
- case 'property': return 'Props';
- case 'all': return 'All';
- }
- }
-
- get searchPlaceholder(): string {
- switch (this.searchMode) {
- case 'name': return 'Search components...';
- case 'property': return 'Search property values...';
- case 'all': return 'Search names & properties...';
- }
- }
-
- // Component highlighting methods
- highlightComponent(component: ComponentNode) {
- // Pass the component info to debugHost for highlighting
- const info = component.data.info;
- this.debugHost.highlightComponent({
- name: component.name,
- type: component.type,
- ...info,
- });
- }
-
- unhighlightComponent() {
- this.debugHost.unhighlightComponent();
- }
-
- // Element picker functionality
- toggleElementPicker() {
- this.isElementPickerActive = !this.isElementPickerActive;
-
- if (this.isElementPickerActive) {
- this.debugHost.startElementPicker();
- } else {
- this.debugHost.stopElementPicker();
- }
- }
-
- onElementPicked(componentInfo: AureliaInfo) {
- // Stop the picker
- this.isElementPickerActive = false;
- this.debugHost.stopElementPicker();
-
- // Find the component in our tree and select it
- const foundComponent = this.findComponentInTreeByInfo(componentInfo);
- if (foundComponent) {
- this.applySelectionFromNode(foundComponent);
- } else {
- // If not found, refresh components and try again
- this.loadAllComponents().then(() => {
- const foundComponentAfterRefresh =
- this.findComponentInTreeByInfo(componentInfo);
- if (foundComponentAfterRefresh) {
- this.applySelectionFromNode(foundComponentAfterRefresh);
- }
- });
- }
- }
-
- toggleFollowChromeSelection() {
- this.followChromeSelection = !this.followChromeSelection;
- try {
- localStorage.setItem('au-devtools.followChromeSelection', String(this.followChromeSelection));
- } catch {}
- }
-
- toggleViewMode() {
- const nextMode = this.viewMode === 'tree' ? 'list' : 'tree';
- this.setViewMode(nextMode);
- }
-
- setViewMode(mode: 'tree' | 'list') {
- if (this.viewMode === mode) {
- return;
- }
- this.viewMode = mode;
- try {
- localStorage.setItem('au-devtools.viewMode', mode);
- } catch {}
- }
-
- revealInElements() {
- const node = this.findComponentById(this.selectedComponentId);
- if (!node) return;
- const info = node.data.info;
- this.debugHost.revealInElements({
- name: node.name,
- type: node.type,
- ...info,
- });
- }
-
- private findComponentInTreeByInfo(
- componentInfo: AureliaInfo
- ): ComponentNode | undefined {
- if (!componentInfo) return undefined;
-
- const targetElement = componentInfo.customElementInfo || null;
- const targetAttributes = componentInfo.customAttributesInfo || [];
- const domPath = (componentInfo as any)?.__auDevtoolsDomPath as string | undefined;
-
- if (domPath) {
- const byPath = this.findNodeByDomPath(domPath, targetElement, targetAttributes);
- if (byPath) {
- return byPath;
- }
- }
-
- return this.searchNodesForMatch(this.componentTree, targetElement, targetAttributes);
- }
-
- private findNodeByDomPath(
- domPath: string,
- targetElement: IControllerInfo | null,
- targetAttributes: IControllerInfo[]
- ): ComponentNode | undefined {
- if (!domPath) return undefined;
-
- let attributeMatch: ComponentNode | undefined;
- let elementMatch: ComponentNode | undefined;
-
- const visit = (nodes: ComponentNode[]) => {
- for (const node of nodes) {
- if (node.domPath === domPath) {
- if (node.data.kind === 'attribute' && targetAttributes.length && this.nodeMatchesAttribute(node, targetAttributes)) {
- attributeMatch = attributeMatch || node;
- }
- if (node.data.kind === 'element' && this.nodeMatchesElement(node, targetElement)) {
- elementMatch = elementMatch || node;
- }
- }
-
- if (node.children?.length) {
- visit(node.children);
- }
- }
- };
-
- visit(this.componentTree || []);
-
- if (targetElement && elementMatch) {
- return elementMatch;
- }
-
- if (!targetElement && targetAttributes.length) {
- return attributeMatch || elementMatch;
- }
-
- return elementMatch || attributeMatch;
- }
-
- private searchNodesForMatch(
- nodes: ComponentNode[],
- targetElement: IControllerInfo | null,
- targetAttributes: IControllerInfo[]
- ): ComponentNode | undefined {
- for (const node of nodes) {
- if (this.nodeMatchesTarget(node, targetElement, targetAttributes)) {
- return node;
- }
-
- const foundInChildren = this.searchNodesForMatch(node.children || [], targetElement, targetAttributes);
- if (foundInChildren) {
- return foundInChildren;
- }
- }
-
- return undefined;
- }
-
- private nodeMatchesTarget(
- node: ComponentNode,
- targetElement: IControllerInfo | null,
- targetAttributes: IControllerInfo[]
- ): boolean {
- if (node.data.kind === 'element') {
- if (targetElement && this.nodeMatchesElement(node, targetElement)) {
- return true;
- }
-
- if (!targetElement && targetAttributes.length) {
- return this.nodeContainsAnyAttribute(node.data.info.customAttributesInfo || [], targetAttributes);
- }
- }
-
- if (node.data.kind === 'attribute') {
- const attrInfo = node.data.raw;
- return targetAttributes.some((candidate) => this.isSameControllerInfo(attrInfo, candidate));
- }
-
- return false;
- }
-
- private nodeMatchesElement(node: ComponentNode, targetElement: IControllerInfo | null): boolean {
- if (!targetElement || node.data.kind !== 'element') {
- return false;
- }
-
- const nodeElement = node.data.info.customElementInfo;
- return this.isSameControllerInfo(nodeElement, targetElement);
- }
-
- private nodeMatchesAttribute(node: ComponentNode, targetAttributes: IControllerInfo[]): boolean {
- if (node.data.kind !== 'attribute' || !targetAttributes?.length) {
- return false;
- }
-
- const attributeInfo = node.data.raw as IControllerInfo;
- return targetAttributes.some((candidate) => this.isSameControllerInfo(attributeInfo, candidate));
- }
-
- private nodeContainsAnyAttribute(
- nodeAttributes: IControllerInfo[],
- targetAttributes: IControllerInfo[]
- ): boolean {
- if (!nodeAttributes?.length || !targetAttributes?.length) {
- return false;
- }
-
- return targetAttributes.some((candidate) =>
- nodeAttributes.some((existing) => this.isSameControllerInfo(existing, candidate))
- );
- }
-
- private isSameControllerInfo(a: IControllerInfo | null | undefined, b: IControllerInfo | null | undefined): boolean {
- if (!a || !b) return false;
-
- if (a.key && b.key && a.key === b.key) {
- return true;
- }
-
- if (a.name && b.name && a.name === b.name) {
- return true;
- }
-
- const aAliases = Array.isArray(a.aliases) ? a.aliases : [];
- const bAliases = Array.isArray(b.aliases) ? b.aliases : [];
-
- return aAliases.some((alias: any) => alias && (alias === b.name || bAliases.includes(alias)))
- || bAliases.some((alias: any) => alias && alias === a.name);
- }
-
- valueChanged(element: IControllerInfo) {
- this.plat.queueMicrotask(() => this.debugHost.updateValues(element));
- }
-
- // Property editing methods for inline properties
- editProperty(property: any) {
- const editableTypes = [
- 'string',
- 'number',
- 'boolean',
- 'bigint',
- 'null',
- 'undefined',
- ];
- if (editableTypes.includes(property.type) || property.canEdit) {
- property.isEditing = true;
- property.originalValue = property.value;
- }
- }
-
- saveProperty(property: any, newValue: string) {
- const originalType = property.type;
- let convertedValue: any = newValue;
-
- try {
- // Convert the value based on the original type
- switch (originalType) {
- case 'number':
- const numValue = Number(newValue);
- if (!isNaN(numValue)) {
- convertedValue = numValue;
- } else {
- property.value = property.originalValue; // Revert on invalid input
- property.isEditing = false;
- delete property.originalValue;
- return;
- }
- break;
-
- case 'boolean':
- const lowerValue = newValue.toLowerCase();
- if (lowerValue === 'true' || lowerValue === 'false') {
- convertedValue = lowerValue === 'true';
- } else {
- property.value = property.originalValue; // Revert on invalid input
- property.isEditing = false;
- delete property.originalValue;
- return;
- }
- break;
-
- case 'null':
- if (newValue === 'null' || newValue === '') {
- convertedValue = null;
- } else {
- convertedValue = newValue; // Convert to string if not null
- property.type = 'string';
- }
- break;
-
- case 'undefined':
- if (newValue === 'undefined' || newValue === '') {
- convertedValue = undefined;
- } else {
- convertedValue = newValue; // Convert to string if not undefined
- property.type = 'string';
- }
- break;
-
- case 'string':
- default:
- convertedValue = newValue;
- property.type = 'string';
- break;
- }
-
- // Update the property value
- property.value = convertedValue;
- property.isEditing = false;
- delete property.originalValue;
-
- // Update the actual property value via debugHost
- this.plat.queueMicrotask(() => {
- this.debugHost.updateValues(this.selectedElement, property);
-
- // Force UI update by refreshing just this property's binding
- this.refreshPropertyBindings();
- });
- } catch (error) {
- console.warn('Failed to convert property value:', error);
- property.value = property.originalValue; // Revert on error
- property.isEditing = false;
- delete property.originalValue;
- }
- }
-
- cancelPropertyEdit(property: any) {
- property.value = property.originalValue;
- property.isEditing = false;
- delete property.originalValue;
- }
-
- // Copy property value to clipboard
- async copyPropertyValue(property: Property, event?: Event) {
- event?.stopPropagation();
-
- let valueToCopy: string;
- if (property.value === null) {
- valueToCopy = 'null';
- } else if (property.value === undefined) {
- valueToCopy = 'undefined';
- } else if (typeof property.value === 'object') {
- try {
- valueToCopy = JSON.stringify(property.value, null, 2);
- } catch {
- valueToCopy = String(property.value);
- }
- } else {
- valueToCopy = String(property.value);
- }
-
- try {
- await navigator.clipboard.writeText(valueToCopy);
- this.copiedPropertyId = `${property.name}-${property.debugId || ''}`;
- setTimeout(() => {
- this.copiedPropertyId = null;
- }, 1500);
- } catch (error) {
- console.warn('Failed to copy to clipboard:', error);
- }
- }
-
- isPropertyCopied(property: Property): boolean {
- return this.copiedPropertyId === `${property.name}-${property.debugId || ''}`;
- }
-
- // Export component state as JSON
- async exportComponentAsJson(copyToClipboard = true) {
- if (!this.selectedElement) return;
-
- const exportData = {
- meta: {
- name: this.selectedElement.name,
- type: this.selectedNodeType,
- key: this.selectedElement.key,
- aliases: this.selectedElement.aliases,
- exportedAt: new Date().toISOString(),
- },
- bindables: this.serializeProperties(this.selectedElement.bindables || []),
- properties: this.serializeProperties(this.selectedElement.properties || []),
- controller: this.selectedElement.controller
- ? { properties: this.serializeProperties(this.selectedElement.controller.properties || []) }
- : undefined,
- customAttributes: this.selectedElementAttributes?.map(attr => ({
- name: attr.name,
- bindables: this.serializeProperties(attr.bindables || []),
- properties: this.serializeProperties(attr.properties || []),
- })) || [],
- };
-
- const jsonString = JSON.stringify(exportData, null, 2);
-
- if (copyToClipboard) {
- try {
- await navigator.clipboard.writeText(jsonString);
- this.copiedPropertyId = '__export__';
- setTimeout(() => {
- this.copiedPropertyId = null;
- }, 1500);
- } catch (error) {
- console.warn('Failed to copy export to clipboard:', error);
- this.downloadJson(jsonString, `${this.selectedElement.name || 'component'}-state.json`);
- }
- } else {
- this.downloadJson(jsonString, `${this.selectedElement.name || 'component'}-state.json`);
- }
- }
-
- get isExportCopied(): boolean {
- return this.copiedPropertyId === '__export__';
- }
-
- private serializeProperties(properties: Property[]): Record {
- const result: Record = {};
- for (const prop of properties) {
- if (prop && prop.name) {
- result[prop.name] = {
- value: prop.value,
- type: prop.type,
- };
- }
- }
- return result;
- }
-
- private downloadJson(content: string, filename: string) {
- const blob = new Blob([content], { type: 'application/json' });
- const url = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(url);
- }
-
- // Expression evaluation
- toggleExpressionPanel() {
- this.isExpressionPanelOpen = !this.isExpressionPanelOpen;
- }
-
- async evaluateExpression() {
- const expression = this.expressionInput.trim();
- if (!expression || !this.selectedElement) return;
-
- this.expressionError = '';
- this.expressionResult = '';
- this.expressionResultType = '';
-
- // Add to history if not duplicate
- if (!this.expressionHistory.includes(expression)) {
- this.expressionHistory = [expression, ...this.expressionHistory.slice(0, 9)];
- }
-
- const componentKey = this.selectedElement.key || this.selectedElement.name;
- if (!componentKey) {
- this.expressionError = 'No component selected';
- return;
- }
-
- const code = `
- (function() {
- try {
- var hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || !hook.evaluateInComponentContext) {
- return { error: 'DevTools hook not available' };
- }
- var result = hook.evaluateInComponentContext(${JSON.stringify(componentKey)}, ${JSON.stringify(expression)});
- return result;
- } catch (e) {
- return { error: e.message || String(e) };
- }
- })()
- `;
-
- if (chrome?.devtools?.inspectedWindow) {
- chrome.devtools.inspectedWindow.eval(
- code,
- (result: any, isException?: any) => {
- if (isException) {
- this.expressionError = String(isException);
- return;
- }
-
- if (result && result.error) {
- this.expressionError = result.error;
- return;
- }
-
- if (result && result.success) {
- this.expressionResultType = result.type || typeof result.value;
- try {
- if (result.value === undefined) {
- this.expressionResult = 'undefined';
- } else if (result.value === null) {
- this.expressionResult = 'null';
- } else if (typeof result.value === 'object') {
- this.expressionResult = JSON.stringify(result.value, null, 2);
- } else {
- this.expressionResult = String(result.value);
- }
- } catch {
- this.expressionResult = String(result.value);
- }
- } else {
- this.expressionError = 'Unknown response format';
- }
- }
- );
- }
- }
-
- selectHistoryExpression(expr: string) {
- this.expressionInput = expr;
- }
-
- clearExpressionResult() {
- this.expressionResult = '';
- this.expressionResultType = '';
- this.expressionError = '';
- }
-
- refreshPropertyBindings() {
- // Force Aurelia to re-render property bindings by updating object references
- if (this.selectedElement) {
- // Ensure arrays exist to avoid template errors
- this.selectedElement.bindables = this.selectedElement.bindables || [];
- this.selectedElement.properties = this.selectedElement.properties || [];
-
- // Update the bindables array reference to trigger change detection
- if (this.selectedElement.bindables) {
- this.selectedElement.bindables = [...this.selectedElement.bindables];
- }
-
- // Update the properties array reference to trigger change detection
- if (this.selectedElement.properties) {
- this.selectedElement.properties = [...this.selectedElement.properties];
- }
- }
- this.markPropertyRowsDirty();
- }
-
- // Object expansion functionality
- togglePropertyExpansion(property: any) {
- if (property.canExpand) {
- if (!property.isExpanded) {
- // Expanding - load the expanded value
-
- if (!property.expandedValue) {
- // Load expanded value using debugHost with callback
- this.loadExpandedPropertyValue(property);
- } else {
- // Already have expanded value, just toggle visibility
- property.isExpanded = true;
- }
- } else {
- // Collapsing - just hide the expanded content
- property.isExpanded = false;
- }
- this.markPropertyRowsDirty();
- } else {
- }
- }
-
- private loadExpandedPropertyValue(property: any) {
- if (property.debugId && chrome && chrome.devtools) {
- const code = `window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__.getExpandedDebugValueForId(${property.debugId})`;
-
- chrome.devtools.inspectedWindow.eval(
- code,
- (result: any, isException?: any) => {
- if (isException) {
- console.warn('Failed to get expanded value:', isException);
- return;
- }
-
- // No-op: remove debug logging after verification
-
- property.expandedValue = result;
- property.isExpanded = true;
-
- // Force UI update without calling refreshPropertyBindings to avoid conflicts with editing
- this.plat.queueMicrotask(() => {
- // Trigger change detection by updating the property reference
- if (this.selectedElement?.bindables?.includes(property)) {
- const index = this.selectedElement.bindables.indexOf(property);
- this.selectedElement.bindables[index] = { ...property };
- }
- if (this.selectedElement?.properties?.includes(property)) {
- const index = this.selectedElement.properties.indexOf(property);
- this.selectedElement.properties[index] = { ...property };
- }
- // Handle controller top-level properties
- if ((this.selectedElement as any)?.controller?.properties?.includes?.(property)) {
- const arr = (this.selectedElement as any).controller.properties as any[];
- const index = arr.indexOf(property);
- if (index !== -1) arr[index] = { ...property };
- }
-
- // Handle nested properties - find parent and update its expanded value
- this.updateNestedPropertyReference(property);
- this.markPropertyRowsDirty();
- });
- }
- );
- }
- }
-
- private updateNestedPropertyReference(property: any) {
- // Helper function to recursively search and update nested properties
- const updateInArray = (arr: any[]): boolean => {
- if (!arr) return false;
-
- for (let i = 0; i < arr.length; i++) {
- const item = arr[i];
- if (item.expandedValue && item.expandedValue.properties) {
- // Check if this property is a child of this item
- const childIndex = item.expandedValue.properties.indexOf(property);
- if (childIndex !== -1) {
- // Found it! Update the parent's expanded value reference
- item.expandedValue = { ...item.expandedValue };
- item.expandedValue.properties = [...item.expandedValue.properties];
- item.expandedValue.properties[childIndex] = { ...property };
- return true;
- }
-
- // Recursively check nested properties
- if (updateInArray(item.expandedValue.properties)) {
- // A nested update occurred, so update this level too
- item.expandedValue = { ...item.expandedValue };
- item.expandedValue.properties = [...item.expandedValue.properties];
- return true;
- }
- }
- }
- return false;
- };
-
- // Check in bindables, properties, and controller properties (for deeper nesting)
- if (this.selectedElement) {
- updateInArray(this.selectedElement.bindables || []);
- updateInArray(this.selectedElement.properties || []);
- // Include controller internals if present
- updateInArray((this.selectedElement as any)?.controller?.properties || []);
- }
- }
-
- private markPropertyRowsDirty() {
- this.propertyRowsRevision++;
- }
-
- getPropertyRows(properties?: Property[], _revision: number = this.propertyRowsRevision): PropertyRow[] {
- if (!properties || !properties.length) {
- return [];
- }
- return this.flattenProperties(properties, 0);
- }
-
- private flattenProperties(properties: Property[], depth: number): PropertyRow[] {
- const rows: PropertyRow[] = [];
- for (const prop of properties) {
- if (!prop) continue;
- rows.push({ property: prop, depth });
- if (prop.isExpanded && prop.expandedValue?.properties?.length) {
- rows.push(...this.flattenProperties(prop.expandedValue.properties, depth + 1));
- }
- }
- return rows;
- }
-}
-
-interface BreadcrumbSegment {
- id: string;
- label: string;
- type: 'custom-element' | 'custom-attribute';
-}
-
-interface PropertyRow {
- property: Property;
- depth: number;
-}
-
-interface ComponentNode {
- id: string;
- name: string;
- type: 'custom-element' | 'custom-attribute';
- domPath: string;
- tagName: string | null;
- children: ComponentNode[];
- data: ComponentNodeData;
- expanded: boolean;
- hasAttributes: boolean;
- matchedProperty?: string | null;
-}
-
-type ComponentNodeData =
- | {
- kind: 'element';
- raw: AureliaComponentTreeNode;
- info: AureliaInfo;
- }
- | {
- kind: 'attribute';
- raw: IControllerInfo;
- owner: AureliaComponentTreeNode;
- info: AureliaInfo;
- };
-
-interface ComponentDisplayNode {
- node: ComponentNode;
- depth: number;
-}
-
-type ComponentTabId = 'all' | 'components' | 'attributes';
-type CoreTabId = ComponentTabId | 'interactions';
-
-interface DevtoolsTabDefinition {
- id: string;
- label: string;
- icon: string;
- kind: 'core' | 'external';
- panelId?: string;
- description?: string;
- order?: number;
-}
-
-export class StringifyValueConverter implements ValueConverterInstance {
- toView(value: unknown) {
- return JSON.stringify(value);
- }
-}
-
-function toKebabCase(str: string): string {
- return str
- .replace(/([a-z])([A-Z])/g, '$1-$2')
- .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
- .toLowerCase();
-}
-
-function toPascalCase(str: string): string {
- return str
- .split('-')
- .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
- .join('');
-}
-
-function toCamelCase(str: string): string {
- const pascal = toPascalCase(str);
- return pascal.charAt(0).toLowerCase() + pascal.slice(1);
-}
-
-function getNamingVariants(name: string): string[] {
- const lower = name.toLowerCase();
- const kebab = toKebabCase(name);
- const pascal = toPascalCase(kebab);
- const camel = toCamelCase(kebab);
- const variants = new Set([lower, kebab.toLowerCase(), pascal.toLowerCase(), camel.toLowerCase()]);
- return [...variants];
-}
-
-function fuzzyMatch(text: string, pattern: string): boolean {
- if (!pattern || !text) return false;
- if (pattern.length > text.length) return false;
-
- let patternIdx = 0;
- for (let i = 0; i < text.length && patternIdx < pattern.length; i++) {
- if (text[i] === pattern[patternIdx]) {
- patternIdx++;
- }
- }
-
- return patternIdx === pattern.length;
-}
diff --git a/src/backend/debug-host.ts b/src/backend/debug-host.ts
deleted file mode 100644
index 3b92a90..0000000
--- a/src/backend/debug-host.ts
+++ /dev/null
@@ -1,1158 +0,0 @@
-import { AureliaComponentSnapshot, AureliaInfo, ComputedPropertyInfo, DISnapshot, EventInteractionRecord, ExternalPanelSnapshot, IControllerInfo, InteractionPhase, LifecycleHooksSnapshot, Property, PropertyChangeRecord, PropertySnapshot, RouteSnapshot, SlotSnapshot, WatchOptions } from '../shared/types';
-import { App } from './../app';
-import { ICustomElementViewModel } from 'aurelia';
-
-declare let aureliaDebugger;
-
-export class SelectionChanged {
- constructor(public debugInfo: IControllerInfo) {}
-}
-
-export class DebugHost implements ICustomElementViewModel {
- consumer: App;
- private pickerPollingInterval: number | null = null;
- private propertyWatchInterval: number | null = null;
- private lastPropertySnapshot: PropertySnapshot | null = null;
- private watchingComponentKey: string | null = null;
- private componentTreeSignature: string = '';
-
- attach(consumer: App) {
- this.consumer = consumer;
- if (chrome && chrome.devtools) {
- chrome.devtools.network.onNavigated.addListener(() => {
- this.getAllComponents().then((snapshot) => {
- this.consumer.handleComponentSnapshot(snapshot || { tree: [], flat: [] });
- });
- });
-
- chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
- // Respect consumer preference for following Elements selection
- if (this.consumer && (this.consumer as any).followChromeSelection === false) {
- return;
- }
- const selectionEval = `
- (function() {
- const target = typeof $0 !== 'undefined' ? $0 : null;
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || !hook.getCustomElementInfo || !target) {
- return null;
- }
-
- function getDomPath(element) {
- if (!element || element.nodeType !== 1) {
- return '';
- }
-
- const segments = [];
- let current = element;
-
- while (current && current.nodeType === 1 && current !== document) {
- const tag = current.tagName ? current.tagName.toLowerCase() : 'unknown';
- let index = 1;
- let sibling = current;
-
- while ((sibling = sibling.previousElementSibling)) {
- if (sibling.tagName === current.tagName) {
- index++;
- }
- }
-
- segments.push(tag + ':nth-of-type(' + index + ')');
- current = current.parentElement;
- }
-
- segments.push('html');
- return segments.reverse().join(' > ');
- }
-
- const info = hook.getCustomElementInfo(target);
- if (!info) {
- return null;
- }
-
- let hostElement = null;
- if (typeof hook.findElementByComponentInfo === 'function') {
- try {
- hostElement = hook.findElementByComponentInfo(info) || null;
- } catch (err) {
- hostElement = null;
- }
- }
-
- if (!hostElement) {
- if (target.nodeType === 1) {
- hostElement = target;
- } else if (target.parentElement) {
- hostElement = target.parentElement;
- }
- }
-
- if (hostElement) {
- const domPath = getDomPath(hostElement);
- if (domPath) {
- info.__auDevtoolsDomPath = domPath;
- if (info.customElementInfo) {
- info.customElementInfo.__auDevtoolsDomPath = domPath;
- }
- if (Array.isArray(info.customAttributesInfo)) {
- info.customAttributesInfo.forEach(function(attr) {
- if (attr) {
- attr.__auDevtoolsDomPath = domPath;
- }
- });
- }
- }
- }
-
- return info;
- })();
- `;
- chrome.devtools.inspectedWindow.eval(
- selectionEval,
- (debugObject: AureliaInfo) => {
- if (!debugObject) return;
- // Sync the selection in our panel/component tree
- if (this.consumer && typeof this.consumer.onElementPicked === 'function') {
- this.consumer.onElementPicked(debugObject);
- } else {
- // Fallback: update basic fields
- this.consumer.selectedElement = debugObject?.customElementInfo;
- this.consumer.selectedElementAttributes = debugObject?.customAttributesInfo;
- }
- }
- );
- });
-
- // Initial load of components - add delay to let Aurelia initialize
- setTimeout(() => {
- this.getAllComponents().then((snapshot) => {
- this.consumer.handleComponentSnapshot(snapshot || { tree: [], flat: [] });
- });
- }, 1000);
- }
- }
-
-
- updateValues(
- value: IControllerInfo,
- property?: Property
- ) {
- chrome.devtools.inspectedWindow.eval(
- `window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__.updateValues(${JSON.stringify(
- value
- )}, ${JSON.stringify(property)})`
- );
- }
-
- updateDebugValue(debugInfo: Property) {
- let value = debugInfo.value;
-
- if (debugInfo.type === 'string') {
- value = "'" + value + "'";
- }
-
- let code = `aureliaDebugger.updateValueForId(${debugInfo.debugId}, ${value})`;
- chrome.devtools.inspectedWindow.eval(code);
- }
-
- toggleDebugValueExpansion(debugInfo: Property) {
- if (debugInfo.canExpand) {
- debugInfo.isExpanded = !debugInfo.isExpanded;
-
- if (debugInfo.isExpanded && !debugInfo.expandedValue) {
- let code = `window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__.getExpandedDebugValueForId(${debugInfo.debugId});`;
- chrome.devtools.inspectedWindow.eval(
- code,
- (expandedValue: IControllerInfo) => {
- debugInfo.expandedValue = expandedValue;
- debugInfo.isExpanded = true;
- }
- );
- }
- }
- }
-
- getAllComponents(): Promise {
- return new Promise((resolve) => {
- if (chrome && chrome.devtools) {
- const getComponentsCode = `
- (() => {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook) {
- return { kind: 'empty', data: [] };
- }
-
- try {
- if (hook.getComponentTree) {
- const tree = hook.getComponentTree() || [];
- return { kind: 'tree', data: tree };
- }
-
- if (hook.getAllInfo) {
- const flat = hook.getAllInfo() || [];
- return { kind: 'flat', data: flat };
- }
-
- return { kind: 'empty', data: [] };
- } catch (error) {
- return { kind: 'error', data: [] };
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(
- getComponentsCode,
- (result: { kind: string; data: any }) => {
- if (!result) {
- resolve({ tree: [], flat: [] });
- return;
- }
-
- if (result.kind === 'tree') {
- resolve({ tree: Array.isArray(result.data) ? result.data : [], flat: [] });
- return;
- }
-
- if (result.kind === 'flat') {
- const flatData = Array.isArray(result.data) ? result.data : [];
- resolve({ tree: [], flat: flatData });
- return;
- }
-
- if (!result.data || result.data.length === 0) {
- setTimeout(() => {
- chrome.devtools.inspectedWindow.eval(
- getComponentsCode,
- (second: { kind: string; data: any }) => {
- if (!second || !Array.isArray(second.data) || second.data.length === 0) {
- const fallbackScan = `
- (function(){
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || !hook.getCustomElementInfo) return [];
- const results = [];
- const seen = new Set();
- const els = document.querySelectorAll('*');
- for (const el of els) {
- try {
- const info = hook.getCustomElementInfo(el, false);
- if (info && (info.customElementInfo || (info.customAttributesInfo && info.customAttributesInfo.length))) {
- const key = info.customElementInfo?.key || info.customElementInfo?.name || (info.customAttributesInfo && info.customAttributesInfo.map(a=>a?.key||a?.name).join('|')) || Math.random().toString(36).slice(2);
- if (!seen.has(key)) {
- seen.add(key);
- results.push(info);
- }
- }
- } catch {}
- }
- return { kind: 'flat', data: results };
- } catch { return { kind: 'flat', data: [] }; }
- })();
- `;
- chrome.devtools.inspectedWindow.eval(
- fallbackScan,
- (fallback: { kind: string; data: any }) => {
- if (fallback && fallback.kind === 'tree') {
- resolve({ tree: Array.isArray(fallback.data) ? fallback.data : [], flat: [] });
- } else {
- const fallbackFlat = fallback && Array.isArray(fallback.data) ? fallback.data : [];
- resolve({ tree: [], flat: fallbackFlat });
- }
- }
- );
- } else {
- if (second.kind === 'tree') {
- resolve({ tree: Array.isArray(second.data) ? second.data : [], flat: [] });
- } else {
- const secondFlat = Array.isArray(second.data) ? second.data : [];
- resolve({ tree: [], flat: secondFlat });
- }
- }
- }
- );
- }, 250);
- } else {
- if (result.kind === 'tree') {
- resolve({ tree: Array.isArray(result.data) ? result.data : [], flat: [] });
- } else {
- const fallbackFlat = Array.isArray(result.data) ? result.data : [];
- resolve({ tree: [], flat: fallbackFlat });
- }
- }
- }
- );
- } else {
- resolve({ tree: [], flat: [] });
- }
- });
- }
-
- getExternalPanelsSnapshot(): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve({ version: 0, panels: [] });
- return;
- }
-
- const expression = `
- (function() {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.getExternalPanelsSnapshot !== 'function') {
- return { version: 0, panels: [] };
- }
- try {
- return hook.getExternalPanelsSnapshot();
- } catch (error) {
- return { version: 0, panels: [] };
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(
- expression,
- (result: ExternalPanelSnapshot) => {
- if (!result || typeof result !== 'object') {
- resolve({ version: 0, panels: [] });
- return;
- }
- const version = typeof result.version === 'number' ? result.version : 0;
- const panels = Array.isArray(result.panels) ? result.panels : [];
- resolve({ version, panels });
- }
- );
- });
- }
-
- emitExternalPanelEvent(eventName: string, payload: Record = {}): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(false);
- return;
- }
-
- let serializedPayload = '{}';
- try {
- serializedPayload = JSON.stringify(payload || {});
- } catch {
- serializedPayload = '{}';
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (hook && typeof hook.emitDevtoolsEvent === 'function') {
- return hook.emitDevtoolsEvent(${JSON.stringify(eventName)}, ${serializedPayload});
- }
- window.dispatchEvent(new CustomEvent(${JSON.stringify(eventName)}, { detail: ${serializedPayload} }));
- return true;
- } catch (error) {
- return false;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: boolean) => {
- resolve(Boolean(result));
- });
- });
- }
-
- getInteractionLog(): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve([]);
- return;
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.getInteractionLog !== 'function') {
- return [];
- }
- const log = hook.getInteractionLog();
- return Array.isArray(log) ? log : [];
- } catch (error) {
- return [];
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: EventInteractionRecord[]) => {
- if (!Array.isArray(result)) {
- resolve([]);
- return;
- }
- resolve(result);
- });
- });
- }
-
- replayInteraction(interactionId: string): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(false);
- return;
- }
-
- const expression = `
- (function() {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.replayInteraction !== 'function') {
- return false;
- }
- try {
- return hook.replayInteraction(${JSON.stringify(interactionId)});
- } catch (error) {
- return false;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: boolean) => {
- resolve(Boolean(result));
- });
- });
- }
-
- applyInteractionSnapshot(interactionId: string, phase: InteractionPhase): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(false);
- return;
- }
-
- const expression = `
- (function() {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.applyInteractionSnapshot !== 'function') {
- return false;
- }
- try {
- return hook.applyInteractionSnapshot(${JSON.stringify(interactionId)}, ${JSON.stringify(phase)});
- } catch (error) {
- return false;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: boolean) => {
- resolve(Boolean(result));
- });
- });
- }
-
- clearInteractionLog(): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(false);
- return;
- }
-
- const expression = `
- (function() {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.clearInteractionLog !== 'function') {
- return false;
- }
- try {
- return hook.clearInteractionLog();
- } catch (error) {
- return false;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: boolean) => {
- resolve(Boolean(result));
- });
- });
- }
-
- // Component highlighting functionality
- highlightComponent(componentInfo: any) {
- if (chrome && chrome.devtools) {
- const highlightCode = `
- (() => {
- // Remove existing highlights
- document.querySelectorAll('.aurelia-devtools-highlight').forEach(el => {
- el.classList.remove('aurelia-devtools-highlight');
- });
-
- // Add CSS for highlighting if not exists
- if (!document.getElementById('aurelia-devtools-styles')) {
- const style = document.createElement('style');
- style.id = 'aurelia-devtools-styles';
- style.textContent = \`
- .aurelia-devtools-highlight {
- outline: 2px solid #1967d2 !important;
- outline-offset: -2px !important;
- background-color: rgba(25, 103, 210, 0.1) !important;
- box-shadow: inset 0 0 0 2px rgba(25, 103, 210, 0.3) !important;
- position: relative !important;
- }
- .aurelia-devtools-highlight::after {
- content: '${componentInfo.name || 'component'}';
- position: absolute !important;
- top: -20px !important;
- left: 0 !important;
- background: #1967d2 !important;
- color: white !important;
- padding: 2px 6px !important;
- font-size: 11px !important;
- font-family: monospace !important;
- border-radius: 2px !important;
- z-index: 10000 !important;
- pointer-events: none !important;
- white-space: nowrap !important;
- }
- \`;
- document.head.appendChild(style);
- }
-
- // Find and highlight the element
- const aureliaHook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (aureliaHook && aureliaHook.findElementByComponentInfo) {
- const element = aureliaHook.findElementByComponentInfo(${JSON.stringify(
- componentInfo
- )});
- if (element) {
- element.classList.add('aurelia-devtools-highlight');
- return true;
- }
- }
- return false;
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(highlightCode);
- }
- }
-
- unhighlightComponent() {
- if (chrome && chrome.devtools) {
- const unhighlightCode = `
- document.querySelectorAll('.aurelia-devtools-highlight').forEach(el => {
- el.classList.remove('aurelia-devtools-highlight');
- });
- `;
-
- chrome.devtools.inspectedWindow.eval(unhighlightCode);
- }
- }
-
- // Element picker functionality
- startElementPicker() {
- if (chrome && chrome.devtools) {
- // Start polling for picked components
- this.startPickerPolling();
-
- const pickerCode = `
- (() => {
- // Remove existing picker if any
- window.__aureliaDevtoolsStopPicker && window.__aureliaDevtoolsStopPicker();
-
- let isPickerActive = true;
- let currentHighlight = null;
-
- function getDomPath(element) {
- if (!element || element.nodeType !== 1) {
- return '';
- }
-
- const segments = [];
- let current = element;
-
- while (current && current.nodeType === 1 && current !== document) {
- const tag = current.tagName ? current.tagName.toLowerCase() : 'unknown';
- let index = 1;
- let sibling = current;
-
- while ((sibling = sibling.previousElementSibling)) {
- if (sibling.tagName === current.tagName) {
- index++;
- }
- }
-
- segments.push(tag + ':nth-of-type(' + index + ')');
- current = current.parentElement;
- }
-
- segments.push('html');
- return segments.reverse().join(' > ');
- }
-
- // Add picker styles
- if (!document.getElementById('aurelia-picker-styles')) {
- const style = document.createElement('style');
- style.id = 'aurelia-picker-styles';
- style.textContent = \`
- .aurelia-picker-highlight {
- outline: 2px solid #ff6b35 !important;
- outline-offset: -2px !important;
- background-color: rgba(255, 107, 53, 0.1) !important;
- box-shadow: inset 0 0 0 2px rgba(255, 107, 53, 0.3) !important;
- cursor: crosshair !important;
- position: relative !important;
- }
- .aurelia-picker-highlight::after {
- content: 'Click to select';
- position: absolute !important;
- top: -20px !important;
- left: 0 !important;
- background: #ff6b35 !important;
- color: white !important;
- padding: 2px 6px !important;
- font-size: 11px !important;
- font-family: monospace !important;
- border-radius: 2px !important;
- z-index: 10000 !important;
- pointer-events: none !important;
- white-space: nowrap !important;
- }
- * {
- cursor: crosshair !important;
- }
- \`;
- document.head.appendChild(style);
- }
-
- function highlightElement(element) {
- if (currentHighlight) {
- currentHighlight.classList.remove('aurelia-picker-highlight');
- }
- element.classList.add('aurelia-picker-highlight');
- currentHighlight = element;
- }
-
- function unhighlightElement() {
- if (currentHighlight) {
- currentHighlight.classList.remove('aurelia-picker-highlight');
- currentHighlight = null;
- }
- }
-
- function onMouseOver(event) {
- if (!isPickerActive) return;
- event.stopPropagation();
- highlightElement(event.target);
- }
-
- function onMouseOut(event) {
- if (!isPickerActive) return;
- event.stopPropagation();
- unhighlightElement();
- }
-
- function onClick(event) {
- if (!isPickerActive) return;
- event.preventDefault();
- event.stopPropagation();
-
- const aureliaHook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (aureliaHook) {
- const componentInfo = aureliaHook.getCustomElementInfo(event.target, true);
-
- if (componentInfo) {
- const domPath = getDomPath(event.target);
- componentInfo.__auDevtoolsDomPath = domPath;
- if (componentInfo.customElementInfo) {
- componentInfo.customElementInfo.__auDevtoolsDomPath = domPath;
- }
- if (componentInfo.customAttributesInfo && componentInfo.customAttributesInfo.length) {
- componentInfo.customAttributesInfo.forEach(attr => {
- if (attr) {
- attr.__auDevtoolsDomPath = domPath;
- }
- });
- }
- // Store the picked component info in a global variable that DevTools can access
- window.__AURELIA_DEVTOOLS_PICKED_COMPONENT__ = componentInfo;
- } else {
- window.__AURELIA_DEVTOOLS_PICKED_COMPONENT__ = null;
- }
- }
-
- // Stop picker
- window.__aureliaDevtoolsStopPicker();
- }
-
- // Add event listeners
- document.addEventListener('mouseover', onMouseOver, true);
- document.addEventListener('mouseout', onMouseOut, true);
- document.addEventListener('click', onClick, true);
-
- // Store stop function
- window.__aureliaDevtoolsStopPicker = function() {
- isPickerActive = false;
- unhighlightElement();
- document.removeEventListener('mouseover', onMouseOver, true);
- document.removeEventListener('mouseout', onMouseOut, true);
- document.removeEventListener('click', onClick, true);
-
- // Remove styles
- const pickerStyles = document.getElementById('aurelia-picker-styles');
- if (pickerStyles) {
- pickerStyles.remove();
- }
-
- delete window.__aureliaDevtoolsStopPicker;
- };
-
- return true;
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(pickerCode);
- }
- }
-
- stopElementPicker() {
- if (chrome && chrome.devtools) {
- // Stop polling
- this.stopPickerPolling();
-
- const stopPickerCode = `
- window.__aureliaDevtoolsStopPicker && window.__aureliaDevtoolsStopPicker();
- `;
-
- chrome.devtools.inspectedWindow.eval(stopPickerCode);
- }
- }
-
- private startPickerPolling() {
- // Stop any existing polling
- this.stopPickerPolling();
-
- // Start polling for picked components
- this.pickerPollingInterval = setInterval(() => {
- this.checkForPickedComponent();
- }, 100) as any;
- }
-
- private stopPickerPolling() {
- if (this.pickerPollingInterval) {
- clearInterval(this.pickerPollingInterval);
- this.pickerPollingInterval = null;
- }
- }
-
- private checkForPickedComponent() {
- if (chrome && chrome.devtools) {
- const checkCode = `
- (() => {
- const picked = window.__AURELIA_DEVTOOLS_PICKED_COMPONENT__;
- if (picked) {
- // Clear the picked component
- window.__AURELIA_DEVTOOLS_PICKED_COMPONENT__ = null;
- return picked;
- }
- return null;
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(
- checkCode,
- (componentInfo: AureliaInfo) => {
- if (componentInfo) {
- this.stopPickerPolling(); // Stop polling when we get a component
- this.consumer.onElementPicked(componentInfo);
- }
- }
- );
- }
- }
-
- // Reveal a component's DOM element in the Elements panel
- revealInElements(componentInfo: any) {
- if (chrome && chrome.devtools) {
- const code = `(() => {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || !hook.findElementByComponentInfo) return false;
- const el = hook.findElementByComponentInfo(${JSON.stringify(componentInfo)});
- if (el) {
- // Built-in DevTools helper to select and reveal an element
- // eslint-disable-next-line no-undef
- inspect(el);
- return true;
- }
- return false;
- } catch { return false; }
- })();`;
- chrome.devtools.inspectedWindow.eval(code);
- }
- }
-
- startPropertyWatching(options: WatchOptions) {
- this.stopPropertyWatching();
-
- this.watchingComponentKey = options.componentKey;
- const interval = options.pollInterval || 500;
-
- this.getPropertySnapshot(options.componentKey).then((snapshot) => {
- this.lastPropertySnapshot = snapshot;
- });
-
- this.propertyWatchInterval = setInterval(() => {
- this.checkForPropertyChanges();
- }, interval) as any;
- }
-
- stopPropertyWatching() {
- if (this.propertyWatchInterval) {
- clearInterval(this.propertyWatchInterval);
- this.propertyWatchInterval = null;
- }
- this.watchingComponentKey = null;
- this.lastPropertySnapshot = null;
- }
-
- private checkForPropertyChanges() {
- if (!this.watchingComponentKey) return;
-
- this.getPropertySnapshot(this.watchingComponentKey).then((snapshot) => {
- if (!snapshot || !this.lastPropertySnapshot) {
- this.lastPropertySnapshot = snapshot;
- return;
- }
-
- const changes = this.diffPropertySnapshots(this.lastPropertySnapshot, snapshot);
-
- if (changes.length > 0) {
- this.lastPropertySnapshot = snapshot;
- if (this.consumer && typeof this.consumer.onPropertyChanges === 'function') {
- this.consumer.onPropertyChanges(changes, snapshot);
- }
- }
- });
- }
-
- private diffPropertySnapshots(
- oldSnapshot: PropertySnapshot,
- newSnapshot: PropertySnapshot
- ): PropertyChangeRecord[] {
- const changes: PropertyChangeRecord[] = [];
-
- const compareProperties = (
- oldProps: Array<{ name: string; value: unknown; type: string }>,
- newProps: Array<{ name: string; value: unknown; type: string }>,
- propertyType: 'bindable' | 'property'
- ) => {
- const oldMap = new Map(oldProps.map((p) => [p.name, p]));
- const newMap = new Map(newProps.map((p) => [p.name, p]));
-
- for (const [name, newProp] of newMap) {
- const oldProp = oldMap.get(name);
- if (!oldProp) {
- changes.push({
- componentKey: newSnapshot.componentKey,
- propertyName: name,
- propertyType,
- oldValue: undefined,
- newValue: newProp.value,
- timestamp: newSnapshot.timestamp,
- });
- } else if (!this.valuesEqual(oldProp.value, newProp.value)) {
- changes.push({
- componentKey: newSnapshot.componentKey,
- propertyName: name,
- propertyType,
- oldValue: oldProp.value,
- newValue: newProp.value,
- timestamp: newSnapshot.timestamp,
- });
- }
- }
- };
-
- compareProperties(oldSnapshot.bindables, newSnapshot.bindables, 'bindable');
- compareProperties(oldSnapshot.properties, newSnapshot.properties, 'property');
-
- return changes;
- }
-
- private valuesEqual(a: unknown, b: unknown): boolean {
- if (a === b) return true;
- if (typeof a !== typeof b) return false;
- if (a === null || b === null) return a === b;
- if (typeof a === 'object') {
- try {
- return JSON.stringify(a) === JSON.stringify(b);
- } catch {
- return false;
- }
- }
- return false;
- }
-
- getPropertySnapshot(componentKey: string): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(null);
- return;
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || !hook.getComponentByKey) {
- return null;
- }
- const info = hook.getComponentByKey(${JSON.stringify(componentKey)});
- if (!info) {
- return null;
- }
- const serializeValue = (val) => {
- if (val === null) return { value: null, type: 'null' };
- if (val === undefined) return { value: undefined, type: 'undefined' };
- const t = typeof val;
- if (t === 'function') return { value: '[Function]', type: 'function' };
- if (t === 'object') {
- try {
- return { value: JSON.parse(JSON.stringify(val)), type: Array.isArray(val) ? 'array' : 'object' };
- } catch {
- return { value: '[Object]', type: 'object' };
- }
- }
- return { value: val, type: t };
- };
- const bindables = (info.bindables || []).map(b => ({
- name: b.name,
- ...serializeValue(b.value)
- }));
- const properties = (info.properties || []).map(p => ({
- name: p.name,
- ...serializeValue(p.value)
- }));
- return {
- componentKey: ${JSON.stringify(componentKey)},
- bindables,
- properties,
- timestamp: Date.now()
- };
- } catch (e) {
- return null;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: PropertySnapshot | null) => {
- resolve(result);
- });
- });
- }
-
- refreshSelectedComponent(): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools) || !this.watchingComponentKey) {
- resolve(null);
- return;
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || !hook.getComponentByKey) {
- return null;
- }
- return hook.getComponentByKey(${JSON.stringify(this.watchingComponentKey)});
- } catch (e) {
- return null;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: IControllerInfo | null) => {
- resolve(result);
- });
- });
- }
-
- checkComponentTreeChanges(): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(false);
- return;
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook) return '';
-
- const getSignature = (nodes) => {
- if (!nodes || !nodes.length) return '';
- return nodes.map(n => {
- const key = n.customElementInfo?.key || n.customElementInfo?.name || n.id || '';
- const attrKeys = (n.customAttributesInfo || []).map(a => a?.key || a?.name || '').join(',');
- const childSig = n.children ? getSignature(n.children) : '';
- return key + ':' + attrKeys + '(' + childSig + ')';
- }).join('|');
- };
-
- if (hook.getComponentTree) {
- const tree = hook.getComponentTree() || [];
- return getSignature(tree);
- }
-
- if (hook.getAllInfo) {
- const flat = hook.getAllInfo() || [];
- return flat.map(c => (c.customElementInfo?.key || c.customElementInfo?.name || '')).join('|');
- }
-
- return '';
- } catch (e) {
- return '';
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (signature: string) => {
- const hasChanged = signature !== this.componentTreeSignature;
- this.componentTreeSignature = signature;
- resolve(hasChanged);
- });
- });
- }
-
- getLifecycleHooks(componentKey: string): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(null);
- return;
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.getLifecycleHooks !== 'function') {
- return null;
- }
- return hook.getLifecycleHooks(${JSON.stringify(componentKey)});
- } catch (error) {
- return null;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: LifecycleHooksSnapshot | null) => {
- resolve(result);
- });
- });
- }
-
- getComputedProperties(componentKey: string): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve([]);
- return;
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.getComputedProperties !== 'function') {
- return [];
- }
- return hook.getComputedProperties(${JSON.stringify(componentKey)}) || [];
- } catch (error) {
- return [];
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: ComputedPropertyInfo[]) => {
- resolve(Array.isArray(result) ? result : []);
- });
- });
- }
-
- getDependencies(componentKey: string): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(null);
- return;
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.getDependencies !== 'function') {
- return null;
- }
- return hook.getDependencies(${JSON.stringify(componentKey)});
- } catch (error) {
- return null;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: DISnapshot | null) => {
- resolve(result);
- });
- });
- }
-
- getRouteInfo(componentKey: string): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(null);
- return;
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.getRouteInfo !== 'function') {
- return null;
- }
- return hook.getRouteInfo(${JSON.stringify(componentKey)});
- } catch (error) {
- return null;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: RouteSnapshot | null) => {
- resolve(result);
- });
- });
- }
-
- getSlotInfo(componentKey: string): Promise {
- return new Promise((resolve) => {
- if (!(chrome && chrome.devtools)) {
- resolve(null);
- return;
- }
-
- const expression = `
- (function() {
- try {
- const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
- if (!hook || typeof hook.getSlotInfo !== 'function') {
- return null;
- }
- return hook.getSlotInfo(${JSON.stringify(componentKey)});
- } catch (error) {
- return null;
- }
- })();
- `;
-
- chrome.devtools.inspectedWindow.eval(expression, (result: SlotSnapshot | null) => {
- resolve(result);
- });
- });
- }
-}
diff --git a/src/devtools/devtools.js b/src/devtools/devtools.js
index 9045a24..86d6cac 100644
--- a/src/devtools/devtools.js
+++ b/src/devtools/devtools.js
@@ -1,6 +1,6 @@
-let panelCreated = false;
let detectedVersion = null;
let elementsSidebarPane = null;
+let sidebarCreated = false;
const optOutCheckExpression = `
(function() {
@@ -36,45 +36,33 @@ function installHooksIfAllowed() {
chrome.devtools.inspectedWindow.eval(hooksAsStringv2);
}
-// Always create the panel immediately
-function createAureliaPanel() {
- if (panelCreated) return;
-
- const panelHtml = "index.html";
- const panelTitle = "Aurelia";
-
- chrome.devtools.panels.create(
- panelTitle,
- "images/16.png",
- panelHtml,
- function (panel) {
- panelCreated = true;
-
- // Set initial detection state to false - let the app handle detection unless opted out
- chrome.devtools.inspectedWindow.eval(`
- if (!(${optOutCheckExpression})) {
- window.__AURELIA_DEVTOOLS_DETECTION_STATE__ = 'checking';
- }
- `);
-
- // Proactively install hooks when the panel opens
- installHooksIfAllowed();
+// Set initial detection state
+function initializeDetectionState() {
+ chrome.devtools.inspectedWindow.eval(`
+ if (!(${optOutCheckExpression})) {
+ window.__AURELIA_DEVTOOLS_DETECTION_STATE__ = 'checking';
}
- );
+ `);
+ installHooksIfAllowed();
}
-// Create an Elements sidebar to show Aurelia info for $0
+// Create an enhanced Elements sidebar - this is now the primary UI
function createElementsSidebar() {
if (!chrome?.devtools?.panels?.elements?.createSidebarPane) return;
if (elementsSidebarPane) return;
chrome.devtools.panels.elements.createSidebarPane('Aurelia', function(pane) {
elementsSidebarPane = pane;
+ sidebarCreated = true;
+
let pageSet = false;
try {
- pane.setPage('index.html');
+ // Use the new sidebar-specific HTML page
+ // Path is relative to extension root, not devtools folder
+ pane.setPage('../sidebar.html');
pageSet = true;
} catch (error) {
+ console.error('Failed to set sidebar page:', error);
pageSet = false;
}
@@ -86,6 +74,7 @@ function createElementsSidebar() {
pane.onShown.addListener(() => installHooksIfAllowed());
} catch {}
} else {
+ // Fallback to setExpression if setPage fails
const updateSidebar = () => {
if (!elementsSidebarPane) return;
const expr = `(() => {
@@ -103,7 +92,7 @@ function createElementsSidebar() {
},
};
- // Get binding info for the selected node (works for text nodes too)
+ // Get binding info for the selected node
if (hook.getNodeBindingInfo) {
const bindingInfo = hook.getNodeBindingInfo($0);
if (bindingInfo) {
@@ -162,19 +151,13 @@ function createElementsSidebar() {
// Update detection state when version is detected
function updateDetectionState(version) {
- if (panelCreated) {
- chrome.devtools.inspectedWindow.eval(`
- window.__AURELIA_DEVTOOLS_DETECTED_VERSION__ = ${version};
- window.__AURELIA_DEVTOOLS_VERSION__ = ${version};
- window.__AURELIA_DEVTOOLS_DETECTION_STATE__ = 'detected';
- `);
- }
+ chrome.devtools.inspectedWindow.eval(`
+ window.__AURELIA_DEVTOOLS_DETECTED_VERSION__ = ${version};
+ window.__AURELIA_DEVTOOLS_VERSION__ = ${version};
+ window.__AURELIA_DEVTOOLS_DETECTION_STATE__ = 'detected';
+ `);
}
-// Create panel immediately when devtools opens
-createAureliaPanel();
-createElementsSidebar();
-
// Listen for Aurelia detection messages
chrome.runtime.onMessage.addListener((req, sender) => {
if (sender.tab && req.aureliaDetected && req.version) {
@@ -251,7 +234,7 @@ chrome.devtools.inspectedWindow.eval(
} else if (result && result.status === 'detected' && result.version) {
detectedVersion = result.version;
updateDetectionState(result.version);
- } else if (panelCreated && result && result.status === 'not-found') {
+ } else if (sidebarCreated && result && result.status === 'not-found') {
chrome.devtools.inspectedWindow.eval(`
window.__AURELIA_DEVTOOLS_DETECTION_STATE__ = 'not-found';
`);
@@ -280,6 +263,8 @@ function install(debugValueLookup) {
const interactionLog = [];
let interactionSequence = 0;
const MAX_INTERACTION_LOG = 200;
+ let isRecordingInteractions = false;
+ const activePropertyWatchers = new Map();
installEventProxy();
installNavigationListeners();
installRouterEventTap();
@@ -298,6 +283,8 @@ function install(debugValueLookup) {
replayInteraction: (id) => replayInteraction(id),
applyInteractionSnapshot: (id, phase) => applyInteractionSnapshot(id, phase),
clearInteractionLog: () => clearInteractionLog(),
+ startInteractionRecording: () => startInteractionRecording(),
+ stopInteractionRecording: () => stopInteractionRecording(),
getAllInfo: (root) => {
root = root ?? document.body;
return [
@@ -762,6 +749,11 @@ function install(debugValueLookup) {
return extractDependencies(controller);
},
+ getEnhancedDISnapshot: (componentKey) => {
+ const controller = findControllerByKey(componentKey);
+ return extractEnhancedDISnapshot(controller);
+ },
+
getRouteInfo: (componentKey) => {
const controller = findControllerByKey(componentKey);
return extractRouteInfo(controller);
@@ -771,6 +763,73 @@ function install(debugValueLookup) {
const controller = findControllerByKey(componentKey);
return extractSlotInfo(controller);
},
+
+ getTemplateSnapshot: (componentKey) => {
+ const controller = findControllerByKey(componentKey);
+ return extractTemplateSnapshot(controller);
+ },
+
+ attachPropertyWatchers: (componentKey) => attachPropertyWatchers(componentKey),
+ detachPropertyWatchers: (componentKey) => detachPropertyWatchers(componentKey),
+ detachAllPropertyWatchers: () => detachAllPropertyWatchers(),
+
+ getComponentByKey: (componentKey) => {
+ const controller = findControllerByKey(componentKey);
+ if (!controller) return null;
+ return extractControllerInfo(controller);
+ },
+
+ getSimplifiedComponentTree: () => {
+ try {
+ const fullTree = hooks.getComponentTree();
+ const simplifyNode = (node) => {
+ const simplified = [];
+
+ if (node.customElementInfo && node.customElementInfo.name) {
+ simplified.push({
+ key: node.customElementInfo.key || node.customElementInfo.name,
+ name: node.customElementInfo.name,
+ tagName: node.tagName || node.customElementInfo.name,
+ type: 'custom-element',
+ hasChildren: (node.children && node.children.length > 0) ||
+ (node.customAttributesInfo && node.customAttributesInfo.length > 0),
+ childCount: (node.children ? node.children.length : 0) +
+ (node.customAttributesInfo ? node.customAttributesInfo.length : 0),
+ children: [
+ ...(node.customAttributesInfo || []).map(attr => ({
+ key: attr.key || attr.name,
+ name: attr.name,
+ tagName: node.tagName || '',
+ type: 'custom-attribute',
+ hasChildren: false,
+ childCount: 0,
+ children: []
+ })),
+ ...(node.children || []).flatMap(child => simplifyNode(child))
+ ]
+ });
+ } else if (node.customAttributesInfo && node.customAttributesInfo.length > 0) {
+ for (const attr of node.customAttributesInfo) {
+ simplified.push({
+ key: attr.key || attr.name,
+ name: attr.name,
+ tagName: node.tagName || '',
+ type: 'custom-attribute',
+ hasChildren: node.children && node.children.length > 0,
+ childCount: node.children ? node.children.length : 0,
+ children: (node.children || []).flatMap(child => simplifyNode(child))
+ });
+ }
+ }
+
+ return simplified;
+ };
+
+ return fullTree.flatMap(root => simplifyNode(root));
+ } catch (error) {
+ return [];
+ }
+ },
};
function installEventProxy() {
@@ -1305,6 +1364,8 @@ function install(debugValueLookup) {
}
function logInteraction(entry, meta) {
+ if (!isRecordingInteractions) return;
+
interactionLog.push({
...entry,
...meta,
@@ -1322,6 +1383,16 @@ function install(debugValueLookup) {
} catch {}
}
+ function startInteractionRecording() {
+ isRecordingInteractions = true;
+ return true;
+ }
+
+ function stopInteractionRecording() {
+ isRecordingInteractions = false;
+ return true;
+ }
+
function installNavigationListeners() {
try {
let lastHref = location.href;
@@ -1601,6 +1672,134 @@ function install(debugValueLookup) {
return null;
}
+ function serializePropertyValue(value) {
+ if (value === null) return { value: null, type: 'null' };
+ if (value === undefined) return { value: undefined, type: 'undefined' };
+ const t = typeof value;
+ if (t === 'function') return { value: '[Function]', type: 'function' };
+ if (t === 'object') {
+ try {
+ return { value: JSON.parse(JSON.stringify(value)), type: Array.isArray(value) ? 'array' : 'object' };
+ } catch {
+ return { value: '[Object]', type: 'object' };
+ }
+ }
+ return { value, type: t };
+ }
+
+ function attachPropertyWatchers(componentKey) {
+ if (!componentKey) return false;
+
+ detachPropertyWatchers(componentKey);
+
+ const controller = findControllerByKey(componentKey);
+ if (!controller) return false;
+
+ const viewModel = controller.viewModel || controller.bindingContext || controller;
+ if (!viewModel) return false;
+
+ const version = detectControllerVersion(controller);
+ if (version !== 2) {
+ return false;
+ }
+
+ const container = controller.container;
+ if (!container) return false;
+
+ let observerLocator = null;
+ try {
+ const IObserverLocator = container.get && container.getAll
+ ? Array.from(container.getAll ? container.getAll(Symbol.for('au:resource:observer-locator')) || [] : []).find(Boolean)
+ : null;
+
+ if (!IObserverLocator) {
+ const tryGet = (key) => {
+ try { return container.get(key); } catch { return null; }
+ };
+ observerLocator = tryGet(Symbol.for('au:resource:observer-locator'))
+ || tryGet('IObserverLocator')
+ || (container.root && container.root.get && tryGet.call(container.root, Symbol.for('au:resource:observer-locator')));
+ } else {
+ observerLocator = IObserverLocator;
+ }
+ } catch {}
+
+ if (!observerLocator) {
+ try {
+ if (typeof viewModel.$controller !== 'undefined' && viewModel.$controller.container) {
+ const c = viewModel.$controller.container;
+ observerLocator = c.get && c.get(Symbol.for('au:resource:observer-locator'));
+ }
+ } catch {}
+ }
+
+ if (!observerLocator || typeof observerLocator.getObserver !== 'function') {
+ return false;
+ }
+
+ const unsubscribers = [];
+ const propertyKeys = Object.keys(viewModel).filter(k =>
+ !k.startsWith('$') &&
+ !k.startsWith('_') &&
+ typeof viewModel[k] !== 'function'
+ );
+
+ for (const key of propertyKeys) {
+ try {
+ const observer = observerLocator.getObserver(viewModel, key);
+ if (!observer || typeof observer.subscribe !== 'function') continue;
+
+ const subscriber = {
+ handleChange(newValue, oldValue) {
+ const serializedNew = serializePropertyValue(newValue);
+ const serializedOld = serializePropertyValue(oldValue);
+ emitDevtoolsEvent('aurelia-devtools:property-change', {
+ componentKey,
+ propertyName: key,
+ newValue: serializedNew.value,
+ oldValue: serializedOld.value,
+ newType: serializedNew.type,
+ oldType: serializedOld.type,
+ timestamp: Date.now()
+ });
+ }
+ };
+
+ observer.subscribe(subscriber);
+ unsubscribers.push(() => {
+ try { observer.unsubscribe(subscriber); } catch {}
+ });
+ } catch {}
+ }
+
+ if (unsubscribers.length > 0) {
+ activePropertyWatchers.set(componentKey, { unsubscribers, viewModel });
+ return true;
+ }
+
+ return false;
+ }
+
+ function detachPropertyWatchers(componentKey) {
+ if (!componentKey) return false;
+
+ const entry = activePropertyWatchers.get(componentKey);
+ if (entry) {
+ for (const unsub of entry.unsubscribers) {
+ try { unsub(); } catch {}
+ }
+ activePropertyWatchers.delete(componentKey);
+ return true;
+ }
+ return false;
+ }
+
+ function detachAllPropertyWatchers() {
+ for (const [key] of activePropertyWatchers) {
+ detachPropertyWatchers(key);
+ }
+ }
+
function extractLifecycleHooks(controller) {
if (!controller) return null;
@@ -1740,6 +1939,350 @@ function install(debugValueLookup) {
}
}
+ function getRegistrationCount(container) {
+ try {
+ const resolvers = container._resolvers || container['_resolvers'];
+ if (resolvers && resolvers.size !== undefined) {
+ return resolvers.size;
+ }
+ } catch {}
+ return -1;
+ }
+
+ function getContainerOwnerName(container) {
+ try {
+ if (container.root === container) return 'Root';
+
+ const res = container.res;
+ if (res) {
+ for (const key in res) {
+ if (key.startsWith('au:resource:custom-element:')) {
+ return key.replace('au:resource:custom-element:', '');
+ }
+ }
+ }
+
+ if (container._resolver && container._resolver.Type) {
+ return container._resolver.Type.name || null;
+ }
+ } catch {}
+ return null;
+ }
+
+ function extractContainerHierarchy(controller) {
+ const container = controller.container;
+ if (!container) return null;
+
+ try {
+ const ancestors = [];
+ let current = container;
+ let depthCounter = 0;
+
+ while (current) {
+ ancestors.push({
+ id: typeof current.id === 'number' ? current.id : depthCounter,
+ depth: typeof current.depth === 'number' ? current.depth : depthCounter,
+ isRoot: current.parent === null,
+ registrationCount: getRegistrationCount(current),
+ ownerName: getContainerOwnerName(current)
+ });
+ current = current.parent;
+ depthCounter++;
+ }
+
+ return {
+ current: ancestors[0],
+ ancestors: ancestors.slice(1)
+ };
+ } catch {
+ return null;
+ }
+ }
+
+ function findProvidingContainerDepth(container, key) {
+ let current = container;
+ let depth = 0;
+
+ while (current) {
+ try {
+ if (typeof current.has === 'function' && current.has(key, false)) {
+ return typeof current.depth === 'number' ? current.depth : depth;
+ }
+ } catch {}
+ current = current.parent;
+ depth++;
+ }
+
+ return -1;
+ }
+
+ function findProvidingContainer(container, key) {
+ let current = container;
+ let depthCounter = 0;
+
+ while (current) {
+ try {
+ if (typeof current.has === 'function' && current.has(key, false)) {
+ return {
+ id: typeof current.id === 'number' ? current.id : depthCounter,
+ depth: typeof current.depth === 'number' ? current.depth : depthCounter,
+ isRoot: current.parent === null,
+ registrationCount: getRegistrationCount(current)
+ };
+ }
+ } catch {}
+ current = current.parent;
+ depthCounter++;
+ }
+
+ return null;
+ }
+
+ function getValueType(value) {
+ if (value === null) return 'null';
+ if (value === undefined) return 'undefined';
+ if (Array.isArray(value)) return 'Array';
+ const type = typeof value;
+ if (type === 'object' && value.constructor && value.constructor.name !== 'Object') {
+ return value.constructor.name;
+ }
+ return type;
+ }
+
+ function serializeForDevtools(value, depth = 0, maxDepth = 2, seen = new WeakSet()) {
+ if (depth > maxDepth) return '[Max depth]';
+ if (value === null) return null;
+ if (value === undefined) return undefined;
+
+ const type = typeof value;
+
+ if (type === 'string' || type === 'number' || type === 'boolean') {
+ return value;
+ }
+
+ if (type === 'function') {
+ return '[Function: ' + (value.name || 'anonymous') + ']';
+ }
+
+ if (type === 'object') {
+ if (seen.has(value)) return '[Circular]';
+ seen.add(value);
+
+ if (Array.isArray(value)) {
+ if (value.length > 10) {
+ return '[Array(' + value.length + ')]';
+ }
+ return value.slice(0, 10).map(function(v) {
+ return serializeForDevtools(v, depth + 1, maxDepth, seen);
+ });
+ }
+
+ if (value.constructor && value.constructor.name !== 'Object') {
+ const className = value.constructor.name;
+ const preview = { __type__: className };
+ const keys = Object.keys(value).slice(0, 5);
+ keys.forEach(function(k) {
+ preview[k] = serializeForDevtools(value[k], depth + 1, maxDepth, seen);
+ });
+ return preview;
+ }
+
+ const result = {};
+ const keys = Object.keys(value).slice(0, 10);
+ keys.forEach(function(k) {
+ result[k] = serializeForDevtools(value[k], depth + 1, maxDepth, seen);
+ });
+ return result;
+ }
+
+ return String(value);
+ }
+
+ function getInstancePreview(instance, maxProps) {
+ if (!instance || typeof instance !== 'object') return null;
+
+ try {
+ const preview = {};
+ const keys = Object.keys(instance);
+ let count = 0;
+
+ for (let i = 0; i < keys.length && count < maxProps; i++) {
+ const key = keys[i];
+ if (key.startsWith('_') || key.startsWith('$')) continue;
+
+ const val = instance[key];
+ const type = typeof val;
+
+ if (type === 'function') continue;
+
+ if (type === 'string' || type === 'number' || type === 'boolean' || val === null) {
+ preview[key] = val;
+ count++;
+ } else if (Array.isArray(val)) {
+ preview[key] = '[Array(' + val.length + ')]';
+ count++;
+ } else if (type === 'object') {
+ preview[key] = '{...}';
+ count++;
+ }
+ }
+
+ return Object.keys(preview).length > 0 ? preview : null;
+ } catch {
+ return null;
+ }
+ }
+
+ function extractEnhancedDependencies(controller) {
+ const version = detectControllerVersion(controller);
+ if (version !== 2) return null;
+
+ const container = controller.container;
+ const Type = controller.definition && controller.definition.Type;
+ if (!Type || !Type.inject || !container) return null;
+
+ try {
+ const deps = [];
+ const injectKeys = Array.isArray(Type.inject) ? Type.inject : [Type.inject];
+
+ injectKeys.forEach(function(key) {
+ const dep = {
+ name: extractKeyName(key) || 'unknown',
+ key: String(key),
+ type: inferDependencyType(key),
+ containerDepth: findProvidingContainerDepth(container, key),
+ containerInfo: findProvidingContainer(container, key),
+ resolvedValue: null,
+ resolvedType: null,
+ instanceName: null,
+ instancePreview: null
+ };
+
+ try {
+ const resolved = container.get(key);
+ dep.resolvedType = getValueType(resolved);
+
+ if (resolved && typeof resolved === 'object') {
+ const ctorName = resolved.constructor ? resolved.constructor.name : null;
+ if (ctorName && ctorName !== 'Object' && ctorName.length > 1) {
+ dep.instanceName = ctorName;
+ }
+ dep.instancePreview = getInstancePreview(resolved, 4);
+ } else {
+ dep.resolvedValue = resolved;
+ }
+ } catch {}
+
+ deps.push(dep);
+ });
+
+ return deps;
+ } catch {
+ return null;
+ }
+ }
+
+ function inferServiceType(key) {
+ const keyStr = String(key);
+ if (keyStr.includes('au:resource:')) {
+ return 'resource';
+ }
+ return inferDependencyType(key);
+ }
+
+ function isInternalAureliaKey(keyStr) {
+ if (keyStr.startsWith('au:')) return true;
+ if (keyStr.startsWith('IWindow')) return true;
+ if (keyStr.startsWith('ILocation')) return true;
+ if (keyStr.startsWith('IHistory')) return true;
+ if (keyStr.includes('__au_')) return true;
+ if (keyStr === 'function Object() { [native code] }') return true;
+ if (keyStr === 'function Array() { [native code] }') return true;
+ return false;
+ }
+
+ function extractAvailableServices(controller) {
+ const version = detectControllerVersion(controller);
+ if (version !== 2) return null;
+
+ const container = controller.container;
+ if (!container) return null;
+
+ try {
+ const services = [];
+ const seen = new Set();
+ let current = container;
+ let isAncestor = false;
+
+ while (current) {
+ try {
+ const resolvers = current._resolvers || current['_resolvers'];
+ if (resolvers && typeof resolvers.forEach === 'function') {
+ resolvers.forEach(function(resolver, key) {
+ const keyStr = String(key);
+
+ if (isInternalAureliaKey(keyStr)) return;
+ if (seen.has(keyStr)) return;
+
+ seen.add(keyStr);
+
+ const name = extractKeyName(key) || keyStr;
+ if (name.length <= 2) return;
+
+ services.push({
+ name: name,
+ key: keyStr,
+ type: inferServiceType(key),
+ isFromAncestor: isAncestor
+ });
+ });
+ }
+ } catch {}
+
+ current = current.parent;
+ isAncestor = true;
+ }
+
+ return services;
+ } catch {
+ return null;
+ }
+ }
+
+ function extractEnhancedDISnapshot(controller) {
+ if (!controller) return null;
+
+ const version = detectControllerVersion(controller);
+ if (version !== 2) {
+ return extractDependencies(controller);
+ }
+
+ try {
+ const result = {
+ version: 2,
+ dependencies: [],
+ containerHierarchy: null,
+ availableServices: []
+ };
+
+ try {
+ result.dependencies = extractEnhancedDependencies(controller) || [];
+ } catch {}
+
+ try {
+ result.containerHierarchy = extractContainerHierarchy(controller);
+ } catch {}
+
+ try {
+ result.availableServices = extractAvailableServices(controller) || [];
+ } catch {}
+
+ return result;
+ } catch {
+ return extractDependencies(controller);
+ }
+ }
+
function extractRouteInfo(controller) {
if (!controller) return null;
@@ -1823,6 +2366,229 @@ function install(debugValueLookup) {
}
}
+ function extractTemplateSnapshot(controller) {
+ if (!controller) return null;
+
+ try {
+ const version = detectControllerVersion(controller);
+ const viewModel = controller.viewModel || controller.bindingContext || controller;
+ const componentKey = controller.definition?.key || controller.definition?.name ||
+ controller.behavior?.elementName || controller.behavior?.attributeName || 'unknown';
+ const componentName = controller.definition?.name ||
+ controller.behavior?.elementName || controller.behavior?.attributeName || 'unknown';
+
+ const bindings = extractTemplateBindings(controller, version);
+ const controllers = extractTemplateControllers(controller, version);
+
+ let hasSlots = false;
+ let shadowMode = 'none';
+ let isContainerless = false;
+
+ if (version === 2) {
+ const def = controller.definition || controller._compiledDef;
+ hasSlots = def?.hasSlots || false;
+ shadowMode = def?.shadowOptions?.mode || 'none';
+ isContainerless = def?.containerless || false;
+ }
+
+ return {
+ componentKey,
+ componentName,
+ bindings,
+ controllers,
+ instructions: [],
+ hasSlots,
+ shadowMode,
+ isContainerless
+ };
+ } catch {
+ return null;
+ }
+ }
+
+ function extractTemplateBindings(controller, version) {
+ const bindings = [];
+ let bindingId = 0;
+
+ try {
+ let bindingList = [];
+
+ if (version === 2) {
+ bindingList = controller.bindings || [];
+ } else {
+ const view = controller.view;
+ bindingList = view?.bindings || [];
+ }
+
+ for (const binding of bindingList) {
+ if (!binding) continue;
+
+ const ast = binding.ast || binding.sourceExpression || binding.expression;
+ if (!ast) continue;
+
+ const kind = ast.$kind || ast.kind || ast.type || (ast.constructor && ast.constructor.name) || 'unknown';
+ const expr = unparseExpression(ast);
+
+ let type = 'property';
+ if (kind === 'Interpolation' || kind === 'InterpolationBinding') {
+ type = 'interpolation';
+ } else if (binding.targetEvent || kind === 'ListenerBinding' || kind === 'Listener') {
+ type = 'listener';
+ } else if (kind === 'RefBinding' || binding.targetProperty === '$ref') {
+ type = 'ref';
+ } else if (kind === 'LetBinding') {
+ type = 'let';
+ } else if (binding.targetAttribute || kind === 'AttributeBinding') {
+ type = 'attribute';
+ }
+
+ let mode = 'default';
+ if (binding.mode !== undefined) {
+ const modeMap = { 0: 'default', 1: 'oneTime', 2: 'toView', 3: 'fromView', 4: 'twoWay' };
+ mode = modeMap[binding.mode] || 'default';
+ } else if (binding.updateSource && binding.updateTarget) {
+ mode = 'twoWay';
+ } else if (binding.updateTarget) {
+ mode = 'toView';
+ } else if (binding.updateSource) {
+ mode = 'fromView';
+ }
+
+ let value = undefined;
+ let valueType = 'unknown';
+ try {
+ if (binding._value !== undefined) {
+ value = binding._value;
+ } else if (binding.value !== undefined) {
+ value = binding.value;
+ } else if (binding._scope && ast) {
+ const scope = binding._scope;
+ if (scope.bindingContext) {
+ const propName = ast.name || (ast.object && ast.object.name);
+ if (propName && scope.bindingContext[propName] !== undefined) {
+ value = scope.bindingContext[propName];
+ }
+ }
+ }
+ valueType = value === null ? 'null' : value === undefined ? 'undefined' :
+ Array.isArray(value) ? 'array' : typeof value;
+ if (valueType === 'object' || valueType === 'array') {
+ value = formatPreview(value);
+ }
+ } catch {}
+
+ const target = binding.targetProperty || binding.targetEvent || binding.targetAttribute || 'unknown';
+
+ bindings.push({
+ id: `binding-${bindingId++}`,
+ type,
+ expression: expr,
+ target,
+ value,
+ valueType,
+ mode,
+ isBound: binding.isBound !== false
+ });
+ }
+ } catch {}
+
+ return bindings;
+ }
+
+ function extractTemplateControllers(controller, version) {
+ const controllers = [];
+ let controllerId = 0;
+
+ try {
+ const children = controller.children || [];
+
+ for (const child of children) {
+ if (!child) continue;
+
+ const defName = child.definition?.name || child.vmKind || '';
+ const vmKind = child.vmKind;
+
+ if (vmKind === 'synthetic' || defName === 'if' || defName === 'else' ||
+ defName === 'repeat' || defName === 'with' || defName === 'switch' ||
+ defName === 'case' || defName === 'au-slot' || defName === 'portal') {
+
+ const vm = child.viewModel;
+ let type = defName || 'other';
+ let expression = '';
+ let isActive = child.isActive !== false;
+ let condition = undefined;
+ let items = undefined;
+ let itemCount = undefined;
+ let localVariable = undefined;
+ let cachedViews = undefined;
+
+ if (type === 'if' || type === 'else') {
+ condition = vm?.value;
+ isActive = !!condition;
+ if (vm?.ifView) cachedViews = 1;
+ if (vm?.elseView) cachedViews = (cachedViews || 0) + 1;
+ }
+
+ if (type === 'repeat') {
+ const repeatVm = vm;
+ localVariable = repeatVm?.local;
+ itemCount = repeatVm?.items?.length ?? repeatVm?._normalizedItems?.length ?? 0;
+
+ if (repeatVm?.forOf) {
+ expression = unparseExpression(repeatVm.forOf);
+ }
+
+ const maxItems = 10;
+ items = [];
+ const normalizedItems = repeatVm?._normalizedItems || repeatVm?.items || [];
+ const count = Math.min(normalizedItems.length, maxItems);
+
+ for (let i = 0; i < count; i++) {
+ const itemValue = normalizedItems[i];
+ items.push({
+ index: i,
+ value: formatPreview(itemValue),
+ isFirst: i === 0,
+ isLast: i === normalizedItems.length - 1,
+ isEven: i % 2 === 0,
+ isOdd: i % 2 === 1
+ });
+ }
+
+ if (normalizedItems.length > maxItems) {
+ items.push({
+ index: maxItems,
+ value: `... and ${normalizedItems.length - maxItems} more`,
+ isFirst: false,
+ isLast: true,
+ isEven: false,
+ isOdd: false
+ });
+ }
+ }
+
+ controllers.push({
+ id: `tc-${controllerId++}`,
+ type,
+ name: defName,
+ expression,
+ isActive,
+ condition,
+ items,
+ itemCount,
+ localVariable,
+ cachedViews
+ });
+ }
+
+ const nestedControllers = extractTemplateControllers(child, version);
+ controllers.push(...nestedControllers);
+ }
+ } catch {}
+
+ return controllers;
+ }
+
return { hooks, debugValueLookup };
function registerExternalPanelBridge() {
@@ -3357,3 +4123,8 @@ chrome.runtime.onConnect.addListener((port) => {
chrome.devtools.network.onNavigated.addListener(() => {
installHooksIfAllowed();
});
+
+// Initialize: create sidebar only (no top-level panel)
+// Must be at the end of the file after hooksAsStringv2 is defined
+initializeDetectionState();
+createElementsSidebar();
diff --git a/src/shared/types.ts b/src/shared/types.ts
index e35837e..92be92e 100644
--- a/src/shared/types.ts
+++ b/src/shared/types.ts
@@ -222,6 +222,45 @@ export interface DISnapshot {
containerDepth: number;
}
+export interface ContainerInfo {
+ id: number;
+ depth: number;
+ isRoot: boolean;
+ registrationCount: number;
+ ownerName?: string;
+}
+
+export interface EnhancedDependencyInfo {
+ name: string;
+ key: string;
+ type: 'service' | 'token' | 'interface' | 'unknown';
+ containerDepth: number;
+ containerInfo: ContainerInfo | null;
+ resolvedValue?: unknown;
+ resolvedType?: string;
+ instanceName?: string;
+ instancePreview?: Record;
+}
+
+export interface ContainerHierarchy {
+ current: ContainerInfo;
+ ancestors: ContainerInfo[];
+}
+
+export interface AvailableService {
+ name: string;
+ key: string;
+ type: 'service' | 'token' | 'interface' | 'resource' | 'unknown';
+ isFromAncestor: boolean;
+}
+
+export interface EnhancedDISnapshot {
+ version: 2;
+ dependencies: EnhancedDependencyInfo[];
+ containerHierarchy: ContainerHierarchy | null;
+ availableServices: AvailableService[];
+}
+
export interface RouteParamInfo {
name: string;
value: string;
@@ -245,3 +284,84 @@ export interface SlotSnapshot {
slots: SlotInfo[];
hasDefaultSlot: boolean;
}
+
+export interface ComponentTreeNode {
+ key: string;
+ name: string;
+ tagName: string;
+ type: 'custom-element' | 'custom-attribute';
+ hasChildren: boolean;
+ childCount: number;
+ isExpanded?: boolean;
+ children?: ComponentTreeNode[];
+}
+
+export interface ComponentTreeRow {
+ node: ComponentTreeNode;
+ depth: number;
+}
+
+export interface TimelineEvent {
+ id: string;
+ type: 'property-change' | 'lifecycle' | 'interaction';
+ componentKey: string;
+ componentName: string;
+ timestamp: number;
+ detail: string;
+ data?: Record;
+}
+
+// Template Debugger Types
+export type BindingMode = 'oneTime' | 'toView' | 'fromView' | 'twoWay' | 'default';
+
+export interface TemplateBinding {
+ id: string;
+ type: 'property' | 'attribute' | 'interpolation' | 'listener' | 'ref' | 'let';
+ expression: string;
+ target: string;
+ value: unknown;
+ valueType: string;
+ mode?: BindingMode;
+ isBound: boolean;
+}
+
+export interface RepeatItem {
+ index: number;
+ key?: string;
+ value: unknown;
+ isFirst: boolean;
+ isLast: boolean;
+ isEven: boolean;
+ isOdd: boolean;
+}
+
+export interface TemplateControllerInfo {
+ id: string;
+ type: 'if' | 'else' | 'repeat' | 'with' | 'switch' | 'case' | 'au-slot' | 'portal' | 'other';
+ name: string;
+ expression?: string;
+ isActive: boolean;
+ condition?: unknown;
+ items?: RepeatItem[];
+ itemCount?: number;
+ localVariable?: string;
+ cachedViews?: number;
+}
+
+export interface TemplateInstructionInfo {
+ type: string;
+ description: string;
+ target?: string;
+ details?: Record;
+}
+
+export interface TemplateSnapshot {
+ componentKey: string;
+ componentName: string;
+ bindings: TemplateBinding[];
+ controllers: TemplateControllerInfo[];
+ instructions: TemplateInstructionInfo[];
+ hasSlots: boolean;
+ shadowMode: 'open' | 'closed' | 'none';
+ isContainerless: boolean;
+}
diff --git a/src/main.ts b/src/sidebar/main.ts
similarity index 69%
rename from src/main.ts
rename to src/sidebar/main.ts
index 832e9b8..5105b9d 100644
--- a/src/main.ts
+++ b/src/sidebar/main.ts
@@ -1,14 +1,14 @@
import Aurelia, { DI, IPlatform, PLATFORM, Registration } from 'aurelia';
import { StandardConfiguration } from '@aurelia/runtime-html';
-import './styles.css';
-import { App } from './app';
-
+import './sidebar-app.css';
+import { SidebarApp } from './sidebar-app';
const aurelia = new Aurelia(
DI.createContainer().register(
Registration.instance(IPlatform, PLATFORM),
- StandardConfiguration
+ StandardConfiguration
)
-).app(App);
+).app(SidebarApp);
+
aurelia.start();
diff --git a/src/sidebar/sidebar-app.css b/src/sidebar/sidebar-app.css
new file mode 100644
index 0000000..6da4a1a
--- /dev/null
+++ b/src/sidebar/sidebar-app.css
@@ -0,0 +1,1509 @@
+/* Sidebar-specific styles - optimized for ~350px width */
+
+:root {
+ --sidebar-bg: #fff;
+ --sidebar-text: #1f2937;
+ --sidebar-text-secondary: #6b7280;
+ --sidebar-border: #e5e7eb;
+ --sidebar-hover: #f3f4f6;
+ --sidebar-active: #dbeafe;
+ --sidebar-accent: #2563eb;
+
+ --type-string: #d73a49;
+ --type-number: #005cc5;
+ --type-boolean: #e36209;
+ --type-null: #6f42c1;
+ --type-object: #22863a;
+ --type-function: #6f42c1;
+
+ --font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
+ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+.dark {
+ --sidebar-bg: #1e1e1e;
+ --sidebar-text: #e5e5e5;
+ --sidebar-text-secondary: #9ca3af;
+ --sidebar-border: #3f3f3f;
+ --sidebar-hover: #2d2d2d;
+ --sidebar-active: #1e3a5f;
+ --sidebar-accent: #60a5fa;
+
+ --type-string: #f28b82;
+ --type-number: #8ab4f8;
+ --type-boolean: #fdd663;
+ --type-null: #c58af9;
+ --type-object: #81c995;
+ --type-function: #c58af9;
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: var(--font-sans);
+ font-size: 11px;
+ line-height: 1.4;
+ color: var(--sidebar-text);
+ background: var(--sidebar-bg);
+ overflow-x: hidden;
+}
+
+/* State messages */
+.state-message {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 12px;
+ color: var(--sidebar-text-secondary);
+}
+
+.state-message.error {
+ color: #dc2626;
+}
+
+.state-message.warning {
+ color: #d97706;
+}
+
+.state-message .icon {
+ font-size: 14px;
+ width: 20px;
+ text-align: center;
+}
+
+.state-message .icon.spinner {
+ animation: spin 1s linear infinite;
+}
+
+@keyframes spin {
+ from { transform: rotate(0deg); }
+ to { transform: rotate(360deg); }
+}
+
+/* Toolbar */
+.toolbar {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 6px;
+ border-bottom: 1px solid var(--sidebar-border);
+ background: var(--sidebar-bg);
+ position: sticky;
+ top: 0;
+ z-index: 10;
+}
+
+.tool-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--sidebar-text-secondary);
+ border-radius: 4px;
+ cursor: pointer;
+ flex-shrink: 0;
+}
+
+.tool-btn:hover {
+ background: var(--sidebar-hover);
+ color: var(--sidebar-text);
+}
+
+.tool-btn.active {
+ background: var(--sidebar-active);
+ color: var(--sidebar-accent);
+}
+
+/* Search */
+.search-wrapper {
+ flex: 1;
+ position: relative;
+ min-width: 0;
+}
+
+.search-input {
+ width: 100%;
+ height: 24px;
+ padding: 0 24px 0 8px;
+ border: 1px solid var(--sidebar-border);
+ border-radius: 4px;
+ background: var(--sidebar-bg);
+ color: var(--sidebar-text);
+ font-size: 11px;
+ outline: none;
+}
+
+.search-input:focus {
+ border-color: var(--sidebar-accent);
+}
+
+.clear-btn {
+ position: absolute;
+ right: 2px;
+ top: 50%;
+ transform: translateY(-50%);
+ width: 20px;
+ height: 20px;
+ border: none;
+ background: transparent;
+ color: var(--sidebar-text-secondary);
+ cursor: pointer;
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.search-dropdown {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ max-height: 200px;
+ overflow-y: auto;
+ background: var(--sidebar-bg);
+ border: 1px solid var(--sidebar-border);
+ border-radius: 4px;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ z-index: 100;
+ margin-top: 2px;
+}
+
+.search-result {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 8px;
+ cursor: pointer;
+}
+
+.search-result:hover {
+ background: var(--sidebar-hover);
+}
+
+.result-icon {
+ font-size: 12px;
+}
+
+.result-name {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Empty state */
+.empty-state {
+ padding: 16px 12px;
+ color: var(--sidebar-text-secondary);
+ text-align: center;
+ font-size: 11px;
+}
+
+/* Component header */
+.component-header {
+ display: flex;
+ align-items: flex-start;
+ gap: 6px;
+ padding: 6px 8px;
+ background: var(--sidebar-hover);
+ border-bottom: 1px solid var(--sidebar-border);
+}
+
+.component-header-info {
+ flex: 1;
+ min-width: 0;
+}
+
+.component-main-line {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.component-icon {
+ font-size: 12px;
+}
+
+.component-name {
+ font-family: var(--font-mono);
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--sidebar-accent);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.binding-context-label {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ margin-top: 2px;
+ padding-left: 18px;
+ font-size: 10px;
+ color: var(--sidebar-text-secondary);
+}
+
+.context-indicator {
+ color: var(--sidebar-accent);
+ font-size: 9px;
+}
+
+.selected-element {
+ font-family: var(--font-mono);
+ color: var(--type-object);
+}
+
+.context-text {
+ font-style: italic;
+}
+
+.header-actions {
+ display: flex;
+ gap: 2px;
+}
+
+.icon-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--sidebar-text-secondary);
+ border-radius: 3px;
+ cursor: pointer;
+}
+
+.icon-btn:hover {
+ background: var(--sidebar-border);
+ color: var(--sidebar-text);
+}
+
+.icon-btn.copied {
+ color: #22c55e;
+}
+
+/* Sections */
+.sections {
+ overflow-y: auto;
+}
+
+.section {
+ border-bottom: 1px solid var(--sidebar-border);
+}
+
+.section-header {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 5px 8px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.section-header:hover {
+ background: var(--sidebar-hover);
+}
+
+.expand-icon {
+ font-size: 8px;
+ color: var(--sidebar-text-secondary);
+ width: 10px;
+}
+
+.section-title {
+ font-size: 11px;
+ font-weight: 500;
+ flex: 1;
+}
+
+.section-count {
+ font-size: 10px;
+ color: var(--sidebar-text-secondary);
+ background: var(--sidebar-border);
+ padding: 1px 5px;
+ border-radius: 8px;
+}
+
+.section-content {
+ padding: 2px 0;
+ background: var(--sidebar-bg);
+}
+
+/* Property rows */
+.property-row {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ min-height: 20px;
+ font-family: var(--font-mono);
+ font-size: 11px;
+}
+
+.property-row:hover {
+ background: var(--sidebar-hover);
+}
+
+.property-row.sub {
+ font-size: 10px;
+}
+
+.expand-btn {
+ width: 14px;
+ height: 14px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--sidebar-text-secondary);
+ cursor: pointer;
+ font-size: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.property-name {
+ color: var(--type-object);
+ flex-shrink: 0;
+}
+
+.property-colon {
+ color: var(--sidebar-text-secondary);
+ flex-shrink: 0;
+}
+
+.property-value {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ cursor: default;
+}
+
+.property-value.type-string { color: var(--type-string); }
+.property-value.type-number { color: var(--type-number); }
+.property-value.type-boolean { color: var(--type-boolean); }
+.property-value.type-null { color: var(--type-null); }
+.property-value.type-object { color: var(--type-object); }
+.property-value.type-function { color: var(--type-function); }
+
+.property-editor {
+ flex: 1;
+ min-width: 50px;
+ height: 18px;
+ padding: 0 4px;
+ border: 1px solid var(--sidebar-accent);
+ border-radius: 2px;
+ background: var(--sidebar-bg);
+ color: var(--sidebar-text);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ outline: none;
+}
+
+.copy-btn {
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--sidebar-text-secondary);
+ cursor: pointer;
+ font-size: 10px;
+ opacity: 0;
+ flex-shrink: 0;
+}
+
+.property-row:hover .copy-btn {
+ opacity: 1;
+}
+
+.copy-btn.copied {
+ color: #22c55e;
+ opacity: 1;
+}
+
+/* Attribute items */
+.attribute-item {
+ padding: 2px 0;
+}
+
+.attribute-name {
+ padding: 2px 8px;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--type-function);
+ font-weight: 500;
+}
+
+/* Lifecycle hooks */
+.hook-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 8px;
+ font-size: 11px;
+}
+
+.hook-indicator {
+ width: 14px;
+ font-size: 10px;
+ text-align: center;
+}
+
+.hook-indicator.implemented {
+ color: #22c55e;
+}
+
+.hook-indicator.not-implemented {
+ color: var(--sidebar-text-secondary);
+}
+
+.hook-name {
+ font-family: var(--font-mono);
+}
+
+.hook-async {
+ font-size: 9px;
+ color: var(--sidebar-accent);
+ padding: 1px 4px;
+ background: var(--sidebar-active);
+ border-radius: 3px;
+}
+
+/* Computed properties */
+.computed-type {
+ font-size: 9px;
+ color: var(--sidebar-text-secondary);
+ margin-left: auto;
+}
+
+/* Dependencies */
+.dependency-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 8px;
+ font-size: 11px;
+}
+
+.dep-icon {
+ font-size: 10px;
+ color: var(--sidebar-accent);
+}
+
+.dep-name {
+ font-family: var(--font-mono);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Enhanced DI (v2) */
+.section-badge.v2 {
+ margin-left: auto;
+ font-size: 9px;
+ padding: 1px 4px;
+ border-radius: 3px;
+ background: #8b5cf6;
+ color: white;
+}
+
+.dark .section-badge.v2 {
+ background: #7c3aed;
+}
+
+.di-subsection {
+ margin-bottom: 8px;
+}
+
+.di-subsection:last-child {
+ margin-bottom: 0;
+}
+
+.subsection-title {
+ font-size: 10px;
+ font-weight: 500;
+ color: var(--sidebar-text-secondary);
+ padding: 2px 8px;
+ margin-bottom: 2px;
+}
+
+.subsection-title.clickable {
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+}
+
+.subsection-title.clickable:hover {
+ color: var(--sidebar-text);
+}
+
+.dependency-row-enhanced {
+ padding: 4px 8px;
+ border-bottom: 1px solid var(--sidebar-border);
+}
+
+.dependency-row-enhanced:last-child {
+ border-bottom: none;
+}
+
+.dep-main {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.dep-instance-name {
+ font-family: var(--font-mono);
+ font-weight: 500;
+ color: var(--sidebar-accent);
+}
+
+.dep-key-name {
+ font-family: var(--font-mono);
+ color: var(--sidebar-text-secondary);
+}
+
+.dep-preview {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 3px;
+ padding-left: 16px;
+}
+
+.preview-prop {
+ font-size: 10px;
+ background: var(--sidebar-hover);
+ padding: 1px 4px;
+ border-radius: 2px;
+}
+
+.preview-key {
+ color: var(--sidebar-text-secondary);
+}
+
+.preview-val {
+ font-family: var(--font-mono);
+ color: var(--type-string);
+}
+
+.container-tree {
+ padding: 2px 8px;
+}
+
+.container-node {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 1px 0;
+ font-size: 10px;
+}
+
+.container-marker {
+ color: var(--sidebar-text-secondary);
+}
+
+.container-marker.current {
+ color: var(--sidebar-accent);
+}
+
+.container-id {
+ font-family: var(--font-mono);
+ color: var(--sidebar-text-secondary);
+}
+
+.container-owner {
+ font-family: var(--font-mono);
+ font-size: 10px;
+}
+
+.container-owner.current {
+ color: var(--sidebar-accent);
+ font-weight: 500;
+}
+
+.container-node.current .container-id {
+ color: var(--sidebar-accent);
+ font-weight: 500;
+}
+
+.services-list {
+ max-height: 150px;
+ overflow-y: auto;
+}
+
+.service-row {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 1px 8px;
+ font-size: 10px;
+}
+
+.svc-icon {
+ font-size: 8px;
+ color: var(--sidebar-text-secondary);
+}
+
+.svc-name {
+ font-family: var(--font-mono);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.inherited-badge {
+ font-size: 8px;
+ padding: 0 3px;
+ border-radius: 2px;
+ background: var(--sidebar-hover);
+ color: var(--sidebar-text-secondary);
+}
+
+.empty-state {
+ padding: 8px;
+ text-align: center;
+ font-size: 10px;
+ color: var(--sidebar-text-secondary);
+ font-style: italic;
+}
+
+/* Route info */
+.route-info {
+ padding: 4px 8px;
+}
+
+.route-path {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ color: var(--sidebar-accent);
+}
+
+.route-params {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ margin-top: 4px;
+}
+
+.param {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ padding: 1px 4px;
+ background: var(--sidebar-hover);
+ border-radius: 3px;
+}
+
+/* Slots */
+.slot-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 8px;
+ font-size: 11px;
+}
+
+.slot-indicator {
+ font-size: 10px;
+}
+
+.slot-indicator.filled {
+ color: #22c55e;
+}
+
+.slot-indicator.empty {
+ color: var(--sidebar-text-secondary);
+}
+
+.slot-name {
+ font-family: var(--font-mono);
+}
+
+/* Expression evaluator */
+.expression-panel {
+ padding: 6px 8px;
+}
+
+.expression-input-wrapper {
+ display: flex;
+ gap: 4px;
+}
+
+.expression-input {
+ flex: 1;
+ height: 24px;
+ padding: 0 8px;
+ border: 1px solid var(--sidebar-border);
+ border-radius: 4px;
+ background: var(--sidebar-bg);
+ color: var(--sidebar-text);
+ font-family: var(--font-mono);
+ font-size: 11px;
+ outline: none;
+}
+
+.expression-input:focus {
+ border-color: var(--sidebar-accent);
+}
+
+.eval-btn {
+ height: 24px;
+ padding: 0 10px;
+ border: none;
+ background: var(--sidebar-accent);
+ color: #fff;
+ border-radius: 4px;
+ font-size: 11px;
+ cursor: pointer;
+}
+
+.eval-btn:hover {
+ opacity: 0.9;
+}
+
+.expression-error {
+ margin-top: 6px;
+ padding: 6px;
+ background: #fef2f2;
+ border: 1px solid #fecaca;
+ border-radius: 4px;
+ color: #dc2626;
+ font-size: 10px;
+}
+
+.dark .expression-error {
+ background: #450a0a;
+ border-color: #7f1d1d;
+}
+
+.expression-result {
+ margin-top: 6px;
+ padding: 6px;
+ background: var(--sidebar-hover);
+ border-radius: 4px;
+}
+
+.result-type {
+ font-size: 9px;
+ color: var(--sidebar-text-secondary);
+ text-transform: uppercase;
+}
+
+.result-value {
+ margin-top: 4px;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ white-space: pre-wrap;
+ word-break: break-all;
+ max-height: 100px;
+ overflow-y: auto;
+}
+
+.expression-history {
+ margin-top: 6px;
+ border-top: 1px solid var(--sidebar-border);
+ padding-top: 6px;
+}
+
+.history-item {
+ padding: 3px 6px;
+ font-family: var(--font-mono);
+ font-size: 10px;
+ color: var(--sidebar-text-secondary);
+ cursor: pointer;
+ border-radius: 3px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.history-item:hover {
+ background: var(--sidebar-hover);
+ color: var(--sidebar-text);
+}
+
+/* Component Tree Panel */
+.tree-panel {
+ border-bottom: 1px solid var(--sidebar-border);
+}
+
+.tree-header {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 5px 8px;
+ cursor: pointer;
+ user-select: none;
+ background: var(--sidebar-hover);
+}
+
+.tree-header:hover {
+ background: var(--sidebar-active);
+}
+
+.tree-title {
+ font-size: 11px;
+ font-weight: 500;
+ flex: 1;
+}
+
+.tree-count {
+ font-size: 10px;
+ color: var(--sidebar-text-secondary);
+ background: var(--sidebar-border);
+ padding: 1px 5px;
+ border-radius: 8px;
+}
+
+.refresh-btn {
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--sidebar-text-secondary);
+ cursor: pointer;
+ font-size: 12px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 3px;
+}
+
+.refresh-btn:hover {
+ background: var(--sidebar-border);
+ color: var(--sidebar-text);
+}
+
+.tree-content {
+ max-height: 250px;
+ overflow-y: auto;
+ padding: 2px 0;
+ background: var(--sidebar-bg);
+}
+
+.tree-node {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 2px 8px;
+ min-height: 20px;
+ cursor: pointer;
+ user-select: none;
+}
+
+.tree-node:hover {
+ background: var(--sidebar-hover);
+}
+
+.tree-node.selected {
+ background: var(--sidebar-active);
+}
+
+.tree-node.selected .tree-node-name {
+ color: var(--sidebar-accent);
+ font-weight: 500;
+}
+
+.tree-expand-btn {
+ width: 14px;
+ height: 14px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--sidebar-text-secondary);
+ cursor: pointer;
+ font-size: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.tree-expand-btn:hover {
+ color: var(--sidebar-text);
+}
+
+.tree-expand-spacer {
+ width: 14px;
+ height: 14px;
+ flex-shrink: 0;
+}
+
+.tree-node-icon {
+ font-size: 10px;
+ color: var(--sidebar-text-secondary);
+ flex-shrink: 0;
+}
+
+.tree-node-icon.custom-element {
+ color: var(--sidebar-accent);
+}
+
+.tree-node-icon.custom-attribute {
+ color: var(--type-function);
+}
+
+.tree-node-name {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.tree-node-badge {
+ font-size: 8px;
+ padding: 1px 4px;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.tree-node-badge.attr {
+ background: var(--type-function);
+ color: white;
+}
+
+/* Timeline / Interaction Recorder */
+.recording-indicator {
+ color: #dc2626;
+ animation: pulse 1s ease-in-out infinite;
+ margin-left: auto;
+ font-size: 10px;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.4; }
+}
+
+.timeline-panel {
+ padding: 0;
+}
+
+.timeline-controls {
+ display: flex;
+ gap: 4px;
+ padding: 6px 8px;
+ border-bottom: 1px solid var(--sidebar-border);
+}
+
+.timeline-btn {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ border: 1px solid var(--sidebar-border);
+ border-radius: 3px;
+ background: var(--sidebar-bg);
+ color: var(--sidebar-text);
+ font-size: 10px;
+ cursor: pointer;
+ transition: all 0.15s ease;
+}
+
+.timeline-btn:hover:not(:disabled) {
+ background: var(--sidebar-hover);
+}
+
+.timeline-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.timeline-btn .btn-icon {
+ font-size: 9px;
+}
+
+.timeline-btn.start .btn-icon {
+ color: #22c55e;
+}
+
+.timeline-btn.stop .btn-icon {
+ color: #dc2626;
+}
+
+.timeline-btn.clear .btn-icon {
+ color: var(--sidebar-text-secondary);
+}
+
+.timeline-empty {
+ padding: 16px 8px;
+ text-align: center;
+ color: var(--sidebar-text-secondary);
+ font-size: 10px;
+}
+
+.timeline-empty.recording {
+ color: #dc2626;
+}
+
+.timeline-events {
+ max-height: 300px;
+ overflow-y: auto;
+}
+
+.timeline-event {
+ padding: 6px 8px;
+ border-bottom: 1px solid var(--sidebar-border);
+ cursor: pointer;
+ transition: background 0.15s ease;
+}
+
+.timeline-event:hover {
+ background: var(--sidebar-hover);
+}
+
+.timeline-event:last-child {
+ border-bottom: none;
+}
+
+.event-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.event-time {
+ font-family: var(--font-mono);
+ font-size: 9px;
+ color: var(--sidebar-text-secondary);
+ flex-shrink: 0;
+}
+
+.event-type {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ font-weight: 500;
+ color: var(--sidebar-accent);
+}
+
+.event-component {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ color: var(--type-object);
+ cursor: pointer;
+}
+
+.event-component:hover {
+ text-decoration: underline;
+}
+
+.event-handler {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ color: var(--type-function);
+}
+
+.event-duration {
+ font-size: 9px;
+ color: var(--sidebar-text-secondary);
+ margin-left: auto;
+}
+
+.event-details {
+ margin-top: 6px;
+ padding-top: 6px;
+ border-top: 1px dashed var(--sidebar-border);
+}
+
+.event-snapshot {
+ margin-bottom: 4px;
+}
+
+.snapshot-label {
+ font-size: 9px;
+ font-weight: 500;
+ color: var(--sidebar-text-secondary);
+ display: block;
+ margin-bottom: 2px;
+}
+
+.snapshot-data {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 8px;
+ padding-left: 8px;
+}
+
+.snapshot-prop {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ color: var(--sidebar-text);
+}
+
+.event-error {
+ font-family: var(--font-mono);
+ font-size: 10px;
+ color: #dc2626;
+ margin-top: 4px;
+}
+
+/* Event type colors */
+.timeline-event.event-property {
+ border-left: 2px solid var(--type-object);
+}
+
+.timeline-event.event-lifecycle {
+ border-left: 2px solid var(--type-function);
+}
+
+.timeline-event.event-interaction {
+ border-left: 2px solid var(--sidebar-accent);
+}
+
+.timeline-event.event-binding {
+ border-left: 2px solid var(--type-string);
+}
+
+/* Template Debugger */
+.template-panel {
+ padding: 0;
+}
+
+.template-subsection {
+ padding: 6px 0;
+ border-bottom: 1px solid var(--sidebar-border);
+}
+
+.template-subsection:last-child {
+ border-bottom: none;
+}
+
+.subsection-title {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 8px;
+ font-size: 10px;
+ font-weight: 600;
+ color: var(--sidebar-text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.subsection-icon {
+ font-size: 11px;
+}
+
+/* Template Bindings */
+.template-binding {
+ padding: 4px 8px;
+ cursor: pointer;
+ transition: background 0.15s ease;
+ border-left: 2px solid transparent;
+}
+
+.template-binding:hover {
+ background: var(--sidebar-hover);
+}
+
+.template-binding.property {
+ border-left-color: var(--sidebar-accent);
+}
+
+.template-binding.attribute {
+ border-left-color: var(--type-function);
+}
+
+.template-binding.interpolation {
+ border-left-color: var(--type-string);
+}
+
+.template-binding.listener {
+ border-left-color: var(--type-boolean);
+}
+
+.template-binding.ref {
+ border-left-color: var(--type-null);
+}
+
+.template-binding.let {
+ border-left-color: var(--type-number);
+}
+
+.binding-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-family: var(--font-mono);
+ font-size: 10px;
+}
+
+.binding-type-icon {
+ width: 16px;
+ text-align: center;
+ color: var(--sidebar-text-secondary);
+ flex-shrink: 0;
+}
+
+.binding-target {
+ font-weight: 500;
+ color: var(--sidebar-accent);
+}
+
+.binding-mode {
+ font-size: 9px;
+ padding: 1px 4px;
+ border-radius: 2px;
+ flex-shrink: 0;
+}
+
+.binding-mode.mode-one-time {
+ background: var(--type-null);
+ color: white;
+}
+
+.binding-mode.mode-to-view {
+ background: var(--sidebar-accent);
+ color: white;
+}
+
+.binding-mode.mode-from-view {
+ background: var(--type-boolean);
+ color: white;
+}
+
+.binding-mode.mode-two-way {
+ background: var(--type-object);
+ color: white;
+}
+
+.binding-mode.mode-default {
+ background: var(--sidebar-text-secondary);
+ color: white;
+}
+
+.binding-expression {
+ color: var(--type-string);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.binding-details {
+ margin-top: 6px;
+ padding: 6px 8px;
+ background: var(--sidebar-hover);
+ border-radius: 3px;
+}
+
+.binding-detail-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 0;
+}
+
+.detail-label {
+ font-size: 9px;
+ color: var(--sidebar-text-secondary);
+ min-width: 50px;
+}
+
+.detail-value {
+ font-family: var(--font-mono);
+ font-size: 10px;
+}
+
+/* Template Controllers */
+.template-controller {
+ padding: 4px 8px;
+ cursor: pointer;
+ transition: background 0.15s ease;
+ border-left: 2px solid var(--sidebar-accent);
+}
+
+.template-controller:hover {
+ background: var(--sidebar-hover);
+}
+
+.template-controller.if,
+.template-controller.else {
+ border-left-color: var(--type-boolean);
+}
+
+.template-controller.repeat {
+ border-left-color: var(--type-object);
+}
+
+.template-controller.with {
+ border-left-color: var(--type-function);
+}
+
+.template-controller.inactive {
+ opacity: 0.6;
+}
+
+.controller-header {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-family: var(--font-mono);
+ font-size: 10px;
+}
+
+.controller-type-icon {
+ width: 16px;
+ text-align: center;
+ flex-shrink: 0;
+}
+
+.controller-type {
+ font-weight: 600;
+ color: var(--sidebar-accent);
+}
+
+.controller-expression {
+ color: var(--type-string);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.controller-status {
+ font-size: 10px;
+ flex-shrink: 0;
+}
+
+.controller-status.active {
+ color: #22c55e;
+}
+
+.controller-status.inactive {
+ color: #dc2626;
+}
+
+.controller-details {
+ margin-top: 6px;
+ padding: 6px 8px;
+ background: var(--sidebar-hover);
+ border-radius: 3px;
+}
+
+.controller-detail-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 0;
+}
+
+/* Repeat Items */
+.repeat-details {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.repeat-items {
+ margin-top: 6px;
+ padding-top: 6px;
+ border-top: 1px dashed var(--sidebar-border);
+}
+
+.repeat-items-header {
+ font-size: 9px;
+ color: var(--sidebar-text-secondary);
+ margin-bottom: 4px;
+}
+
+.repeat-item {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 2px 0;
+ font-family: var(--font-mono);
+ font-size: 10px;
+}
+
+.item-index {
+ color: var(--type-number);
+ min-width: 24px;
+}
+
+.item-value {
+ color: var(--sidebar-text);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+}
+
+.item-flags {
+ display: flex;
+ gap: 3px;
+ flex-shrink: 0;
+}
+
+.item-flag {
+ font-size: 8px;
+ padding: 1px 3px;
+ border-radius: 2px;
+ background: var(--sidebar-border);
+ color: var(--sidebar-text-secondary);
+}
+
+.item-flag.first {
+ background: var(--type-object);
+ color: white;
+}
+
+.item-flag.last {
+ background: var(--type-function);
+ color: white;
+}
+
+/* Template Meta */
+.template-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px;
+ padding: 6px 8px;
+ border-top: 1px solid var(--sidebar-border);
+}
+
+.meta-badge {
+ font-size: 9px;
+ padding: 2px 6px;
+ border-radius: 3px;
+ background: var(--sidebar-hover);
+ color: var(--sidebar-text-secondary);
+}
+
+.meta-badge.slots {
+ background: var(--type-function);
+ color: white;
+}
+
+.meta-badge.shadow {
+ background: var(--type-null);
+ color: white;
+}
+
+.meta-badge.containerless {
+ background: var(--type-boolean);
+ color: white;
+}
diff --git a/src/sidebar/sidebar-app.html b/src/sidebar/sidebar-app.html
new file mode 100644
index 0000000..e2ab424
--- /dev/null
+++ b/src/sidebar/sidebar-app.html
@@ -0,0 +1,617 @@
+
+
+
+
!
+
+
Extension Invalidated
+
Please reload DevTools
+
+
+
+
+
+ ↻
+ Detecting Aurelia...
+
+
+
+ ?
+ No Aurelia detected
+
+
+
+ ⊖
+ DevTools disabled for this page
+
+
+
+
+
diff --git a/src/sidebar/sidebar-app.ts b/src/sidebar/sidebar-app.ts
new file mode 100644
index 0000000..7780532
--- /dev/null
+++ b/src/sidebar/sidebar-app.ts
@@ -0,0 +1,1085 @@
+import { ICustomElementViewModel, IPlatform } from 'aurelia';
+import { resolve } from '@aurelia/kernel';
+import { SidebarDebugHost } from './sidebar-debug-host';
+import {
+ AureliaInfo,
+ ComponentTreeNode,
+ ComponentTreeRow,
+ ComputedPropertyInfo,
+ DISnapshot,
+ EnhancedDISnapshot,
+ EventInteractionRecord,
+ IControllerInfo,
+ LifecycleHooksSnapshot,
+ Property,
+ PropertyChangeRecord,
+ PropertySnapshot,
+ RouteSnapshot,
+ SlotSnapshot,
+ TemplateBinding,
+ TemplateControllerInfo,
+ TemplateSnapshot,
+} from '../shared/types';
+
+export class SidebarApp implements ICustomElementViewModel {
+ isDarkTheme = false;
+
+ // Detection state
+ aureliaDetected = false;
+ aureliaVersion: number | null = null;
+ detectionState: 'checking' | 'detected' | 'not-found' | 'disabled' = 'checking';
+ extensionInvalidated = false;
+
+ // Selected component
+ selectedElement: IControllerInfo | null = null;
+ selectedElementAttributes: IControllerInfo[] = [];
+ selectedNodeType: 'custom-element' | 'custom-attribute' = 'custom-element';
+ selectedElementTagName: string | null = null;
+ isShowingBindingContext = false;
+
+ // Element picker
+ isElementPickerActive = false;
+ followChromeSelection = true;
+
+ // Component tree
+ componentTree: ComponentTreeNode[] = [];
+ expandedTreeNodes: Set = new Set();
+ selectedTreeNodeKey: string | null = null;
+ isTreePanelExpanded = true;
+ treeRevision = 0;
+
+ // Timeline / Interaction recorder
+ isRecording = false;
+ timelineEvents: EventInteractionRecord[] = [];
+ expandedTimelineEvents: Set = new Set();
+ private interactionListener: ((msg: any) => void) | null = null;
+
+ // Search
+ searchQuery = '';
+ searchResults: SearchResult[] = [];
+ isSearchOpen = false;
+
+ // Collapsible sections state - use object for Aurelia reactivity
+ expandedSections: Record = {
+ bindables: true,
+ properties: true,
+ controller: false,
+ attributes: false,
+ lifecycle: false,
+ computed: false,
+ dependencies: false,
+ route: false,
+ slots: false,
+ expression: false,
+ timeline: false,
+ template: false,
+ };
+
+ // Enhanced inspection
+ lifecycleHooks: LifecycleHooksSnapshot | null = null;
+ computedProperties: ComputedPropertyInfo[] = [];
+ dependencies: DISnapshot | null = null;
+ routeInfo: RouteSnapshot | null = null;
+ slotInfo: SlotSnapshot | null = null;
+
+ // Template debugger
+ templateSnapshot: TemplateSnapshot | null = null;
+ expandedBindings: Set = new Set();
+ expandedControllers: Set = new Set();
+
+ // Enhanced DI inspection (Aurelia 2 only)
+ enhancedDI: EnhancedDISnapshot | null = null;
+ showAvailableServices = false;
+
+ // Expression evaluation
+ expressionInput = '';
+ expressionResult = '';
+ expressionResultType = '';
+ expressionError = '';
+ expressionHistory: string[] = [];
+
+ // Property editing
+ copiedPropertyId: string | null = null;
+ propertyRowsRevision = 0;
+
+ // Property change listener
+ private propertyChangeListener: ((msg: any, sender: any) => void) | null = null;
+
+ private debugHost: SidebarDebugHost = resolve(SidebarDebugHost);
+ private plat: IPlatform = resolve(IPlatform);
+
+ attaching() {
+ this.debugHost.attach(this);
+ this.isDarkTheme = (chrome?.devtools?.panels as any)?.themeName === 'dark';
+
+ if (this.isDarkTheme) {
+ document.documentElement.classList.add('dark');
+ }
+
+ // Restore preferences
+ try {
+ const persisted = localStorage.getItem('au-devtools.followChromeSelection');
+ if (persisted != null) this.followChromeSelection = persisted === 'true';
+ } catch {}
+
+ // Register property change listener
+ this.registerPropertyChangeStream();
+
+ // Register interaction listener for timeline
+ this.registerInteractionStream();
+
+ // Check detection state
+ this.checkDetectionState();
+
+ // Start detection polling
+ this.startDetectionPolling();
+
+ // Load component tree
+ this.loadComponentTree();
+ }
+
+ detaching() {
+ if (this.propertyChangeListener && chrome?.runtime?.onMessage?.removeListener) {
+ chrome.runtime.onMessage.removeListener(this.propertyChangeListener);
+ }
+ if (this.interactionListener && chrome?.runtime?.onMessage?.removeListener) {
+ chrome.runtime.onMessage.removeListener(this.interactionListener);
+ }
+ }
+
+ private registerPropertyChangeStream() {
+ if (!(chrome?.runtime?.onMessage?.addListener)) return;
+ this.propertyChangeListener = (message: any) => {
+ if (message?.type === 'au-devtools:property-change') {
+ this.onPropertyChanges(message.changes, message.snapshot);
+ }
+ };
+ chrome.runtime.onMessage.addListener(this.propertyChangeListener);
+ }
+
+ private registerInteractionStream() {
+ if (!(chrome?.runtime?.onMessage?.addListener)) return;
+ this.interactionListener = (message: any) => {
+ if (message?.type === 'au-devtools:interaction' && this.isRecording) {
+ this.timelineEvents = [...this.timelineEvents, message.entry];
+ }
+ };
+ chrome.runtime.onMessage.addListener(this.interactionListener);
+ }
+
+ onPropertyChanges(changes: PropertyChangeRecord[], snapshot: PropertySnapshot) {
+ if (!changes?.length || !this.selectedElement) return;
+
+ const selectedKey = this.selectedElement.key || this.selectedElement.name;
+ if (!selectedKey || snapshot?.componentKey !== selectedKey) return;
+
+ let hasUpdates = false;
+
+ for (const change of changes) {
+ const bindable = this.selectedElement.bindables?.find(b => b.name === change.propertyName);
+ if (bindable) {
+ bindable.value = change.newValue;
+ hasUpdates = true;
+ continue;
+ }
+
+ const property = this.selectedElement.properties?.find(p => p.name === change.propertyName);
+ if (property) {
+ property.value = change.newValue;
+ hasUpdates = true;
+ }
+ }
+
+ if (hasUpdates) {
+ this.markPropertyRowsDirty();
+ }
+ }
+
+ checkDetectionState() {
+ if (chrome?.devtools) {
+ chrome.devtools.inspectedWindow.eval(
+ `({
+ state: window.__AURELIA_DEVTOOLS_DETECTION_STATE__,
+ version: window.__AURELIA_DEVTOOLS_VERSION__
+ })`,
+ (result: { state: string; version: number }, isException?: any) => {
+ if (!isException && result) {
+ switch (result.state) {
+ case 'detected':
+ this.aureliaDetected = true;
+ this.aureliaVersion = result.version;
+ this.detectionState = 'detected';
+ break;
+ case 'disabled':
+ this.aureliaDetected = false;
+ this.aureliaVersion = null;
+ this.detectionState = 'disabled';
+ break;
+ case 'not-found':
+ this.aureliaDetected = false;
+ this.aureliaVersion = null;
+ this.detectionState = 'not-found';
+ break;
+ default:
+ this.detectionState = 'checking';
+ break;
+ }
+ }
+ }
+ );
+ }
+ }
+
+ startDetectionPolling() {
+ setInterval(() => {
+ if (this.checkExtensionInvalidated()) return;
+ this.checkDetectionState();
+ }, 2000);
+ }
+
+ checkExtensionInvalidated(): boolean {
+ try {
+ if (!chrome?.runtime?.id) {
+ this.extensionInvalidated = true;
+ return true;
+ }
+ } catch {
+ this.extensionInvalidated = true;
+ return true;
+ }
+ return false;
+ }
+
+ // Called by debug host when element is selected in Elements panel
+ onElementPicked(componentInfo: AureliaInfo) {
+ this.isElementPickerActive = false;
+ this.debugHost.stopElementPicker();
+
+ if (!componentInfo) {
+ this.clearSelection();
+ return;
+ }
+
+ const elementInfo = componentInfo.customElementInfo;
+ const attributesInfo = componentInfo.customAttributesInfo || [];
+
+ // Track the actual selected element and whether we're showing inherited binding context
+ this.selectedElementTagName = (componentInfo as any).__selectedElement || null;
+ this.isShowingBindingContext = !!(componentInfo as any).__isBindingContext;
+
+ if (elementInfo) {
+ this.selectedElement = elementInfo;
+ this.selectedElement.bindables = this.selectedElement.bindables || [];
+ this.selectedElement.properties = this.selectedElement.properties || [];
+ this.selectedElementAttributes = attributesInfo;
+ this.selectedNodeType = 'custom-element';
+ } else if (attributesInfo.length > 0) {
+ this.selectedElement = attributesInfo[0];
+ this.selectedElement.bindables = this.selectedElement.bindables || [];
+ this.selectedElement.properties = this.selectedElement.properties || [];
+ this.selectedElementAttributes = [];
+ this.selectedNodeType = 'custom-attribute';
+ } else {
+ this.clearSelection();
+ return;
+ }
+
+ // Start watching for property changes
+ const componentKey = this.selectedElement?.key || this.selectedElement?.name;
+ if (componentKey) {
+ this.debugHost.startPropertyWatching({ componentKey, pollInterval: 500 });
+ this.selectedTreeNodeKey = componentKey;
+ }
+
+ // Load enhanced info
+ this.loadEnhancedInfo();
+ this.markPropertyRowsDirty();
+
+ // Refresh tree if needed
+ if (!this.componentTree?.length) {
+ this.loadComponentTree();
+ }
+ }
+
+ clearSelection() {
+ this.debugHost.stopPropertyWatching();
+ this.selectedElement = null;
+ this.selectedElementAttributes = [];
+ this.selectedElementTagName = null;
+ this.isShowingBindingContext = false;
+ this.clearEnhancedInfo();
+ }
+
+ // Section expansion
+ toggleSection(sectionId: string) {
+ this.expandedSections[sectionId] = !this.expandedSections[sectionId];
+ }
+
+ // Element picker
+ toggleElementPicker() {
+ this.isElementPickerActive = !this.isElementPickerActive;
+
+ if (this.isElementPickerActive) {
+ this.debugHost.startElementPicker();
+ } else {
+ this.debugHost.stopElementPicker();
+ }
+ }
+
+ toggleFollowChromeSelection() {
+ this.followChromeSelection = !this.followChromeSelection;
+ try {
+ localStorage.setItem('au-devtools.followChromeSelection', String(this.followChromeSelection));
+ } catch {}
+ }
+
+ // Component tree
+ async loadComponentTree(): Promise {
+ try {
+ const tree = await this.debugHost.getComponentTree();
+ this.componentTree = tree;
+ this.treeRevision++;
+ } catch {
+ this.componentTree = [];
+ }
+ }
+
+ toggleTreePanel(): void {
+ this.isTreePanelExpanded = !this.isTreePanelExpanded;
+ }
+
+ toggleTreeNode(node: ComponentTreeNode, event?: Event): void {
+ event?.stopPropagation();
+ if (!node.hasChildren) return;
+
+ const key = node.key;
+ if (this.expandedTreeNodes.has(key)) {
+ this.expandedTreeNodes.delete(key);
+ } else {
+ this.expandedTreeNodes.add(key);
+ }
+ this.treeRevision++;
+ }
+
+ selectTreeNode(node: ComponentTreeNode): void {
+ this.selectedTreeNodeKey = node.key;
+ this.debugHost.selectComponentByKey(node.key);
+ }
+
+ getTreeRows(_revision: number = this.treeRevision): ComponentTreeRow[] {
+ if (!this.componentTree?.length) return [];
+ return this.flattenTreeNodes(this.componentTree, 0);
+ }
+
+ private flattenTreeNodes(nodes: ComponentTreeNode[], depth: number): ComponentTreeRow[] {
+ const rows: ComponentTreeRow[] = [];
+ for (const node of nodes) {
+ if (!node) continue;
+ rows.push({ node, depth });
+ if (this.expandedTreeNodes.has(node.key) && node.children?.length) {
+ rows.push(...this.flattenTreeNodes(node.children, depth + 1));
+ }
+ }
+ return rows;
+ }
+
+ isTreeNodeExpanded(node: ComponentTreeNode): boolean {
+ return this.expandedTreeNodes.has(node.key);
+ }
+
+ isTreeNodeSelected(node: ComponentTreeNode): boolean {
+ return this.selectedTreeNodeKey === node.key;
+ }
+
+ get hasComponentTree(): boolean {
+ return this.componentTree.length > 0;
+ }
+
+ get componentTreeCount(): number {
+ const countNodes = (nodes: ComponentTreeNode[]): number => {
+ let count = 0;
+ for (const node of nodes) {
+ count++;
+ if (node.children?.length) {
+ count += countNodes(node.children);
+ }
+ }
+ return count;
+ };
+ return countNodes(this.componentTree);
+ }
+
+ // Timeline / Interaction Recorder
+ async startRecording(): Promise {
+ this.isRecording = true;
+ await this.debugHost.startInteractionRecording();
+ }
+
+ async stopRecording(): Promise {
+ this.isRecording = false;
+ await this.debugHost.stopInteractionRecording();
+ }
+
+ clearTimeline(): void {
+ this.timelineEvents = [];
+ this.expandedTimelineEvents.clear();
+ this.debugHost.clearInteractionLog();
+ }
+
+ toggleTimelineEvent(event: EventInteractionRecord): void {
+ const id = event.id;
+ if (this.expandedTimelineEvents.has(id)) {
+ this.expandedTimelineEvents.delete(id);
+ } else {
+ this.expandedTimelineEvents.add(id);
+ }
+ this.timelineEvents = [...this.timelineEvents];
+ }
+
+ isTimelineEventExpanded(event: EventInteractionRecord): boolean {
+ return this.expandedTimelineEvents.has(event.id);
+ }
+
+ selectTimelineComponent(event: EventInteractionRecord): void {
+ if (event.target?.componentKey) {
+ this.debugHost.selectComponentByKey(event.target.componentKey);
+ }
+ }
+
+ get hasTimelineEvents(): boolean {
+ return this.timelineEvents.length > 0;
+ }
+
+ get timelineEventCount(): number {
+ return this.timelineEvents.length;
+ }
+
+ formatTimelineTimestamp(timestamp: number): string {
+ const date = new Date(timestamp);
+ const time = date.toLocaleTimeString('en-US', {
+ hour12: false,
+ hour: '2-digit',
+ minute: '2-digit',
+ second: '2-digit',
+ });
+ const ms = String(date.getMilliseconds()).padStart(3, '0');
+ return `${time}.${ms}`;
+ }
+
+ getTimelineEventTypeClass(type: string): string {
+ const typeMap: Record = {
+ 'property-change': 'event-property',
+ 'lifecycle': 'event-lifecycle',
+ 'interaction': 'event-interaction',
+ 'binding': 'event-binding',
+ };
+ return typeMap[type] || 'event-default';
+ }
+
+ // Template Debugger
+ get hasTemplateInfo(): boolean {
+ return this.templateSnapshot !== null &&
+ (this.templateSnapshot.bindings.length > 0 ||
+ this.templateSnapshot.controllers.length > 0);
+ }
+
+ get templateBindings(): TemplateBinding[] {
+ return this.templateSnapshot?.bindings || [];
+ }
+
+ get templateControllers(): TemplateControllerInfo[] {
+ return this.templateSnapshot?.controllers || [];
+ }
+
+ get templateBindingsCount(): number {
+ return this.templateSnapshot?.bindings?.length || 0;
+ }
+
+ get templateControllersCount(): number {
+ return this.templateSnapshot?.controllers?.length || 0;
+ }
+
+ toggleBindingExpand(binding: TemplateBinding): void {
+ if (this.expandedBindings.has(binding.id)) {
+ this.expandedBindings.delete(binding.id);
+ } else {
+ this.expandedBindings.add(binding.id);
+ }
+ this.templateSnapshot = { ...this.templateSnapshot! };
+ }
+
+ isBindingExpanded(binding: TemplateBinding): boolean {
+ return this.expandedBindings.has(binding.id);
+ }
+
+ toggleControllerExpand(controller: TemplateControllerInfo): void {
+ if (this.expandedControllers.has(controller.id)) {
+ this.expandedControllers.delete(controller.id);
+ } else {
+ this.expandedControllers.add(controller.id);
+ }
+ this.templateSnapshot = { ...this.templateSnapshot! };
+ }
+
+ isControllerExpanded(controller: TemplateControllerInfo): boolean {
+ return this.expandedControllers.has(controller.id);
+ }
+
+ getBindingTypeIcon(type: string): string {
+ const icons: Record = {
+ 'property': '→',
+ 'attribute': '@',
+ 'interpolation': '${}',
+ 'listener': '⚡',
+ 'ref': '🔗',
+ 'let': '𝑙',
+ };
+ return icons[type] || '•';
+ }
+
+ getBindingModeClass(mode: string): string {
+ const classes: Record = {
+ 'oneTime': 'mode-one-time',
+ 'toView': 'mode-to-view',
+ 'fromView': 'mode-from-view',
+ 'twoWay': 'mode-two-way',
+ 'default': 'mode-default',
+ };
+ return classes[mode] || 'mode-default';
+ }
+
+ getBindingModeLabel(mode: string): string {
+ const labels: Record = {
+ 'oneTime': 'one-time',
+ 'toView': '→',
+ 'fromView': '←',
+ 'twoWay': '↔',
+ 'default': '→',
+ };
+ return labels[mode] || '→';
+ }
+
+ getControllerTypeIcon(type: string): string {
+ const icons: Record = {
+ 'if': '❓',
+ 'else': '❔',
+ 'repeat': '↻',
+ 'with': '📦',
+ 'switch': '⚪',
+ 'case': '⚫',
+ 'au-slot': '📁',
+ 'portal': '🔼',
+ };
+ return icons[type] || '◆';
+ }
+
+ formatBindingValue(value: unknown): string {
+ if (value === undefined) return 'undefined';
+ if (value === null) return 'null';
+ if (typeof value === 'string') return `"${value}"`;
+ if (typeof value === 'object') return JSON.stringify(value);
+ return String(value);
+ }
+
+ // Search
+ handleSearchInput(event: Event) {
+ const input = event.target as HTMLInputElement;
+ this.searchQuery = input.value;
+
+ if (this.searchQuery.trim()) {
+ this.performSearch();
+ this.isSearchOpen = true;
+ } else {
+ this.searchResults = [];
+ this.isSearchOpen = false;
+ }
+ }
+
+ async performSearch() {
+ const query = this.searchQuery.toLowerCase().trim();
+ if (!query) {
+ this.searchResults = [];
+ return;
+ }
+
+ const results = await this.debugHost.searchComponents(query);
+ this.searchResults = results;
+ }
+
+ selectSearchResult(result: SearchResult) {
+ this.debugHost.selectComponentByKey(result.key);
+ this.searchQuery = '';
+ this.searchResults = [];
+ this.isSearchOpen = false;
+ }
+
+ clearSearch() {
+ this.searchQuery = '';
+ this.searchResults = [];
+ this.isSearchOpen = false;
+ }
+
+ // Enhanced info
+ async loadEnhancedInfo(): Promise {
+ const componentKey = this.selectedElement?.key || this.selectedElement?.name;
+ if (!componentKey) {
+ this.clearEnhancedInfo();
+ return;
+ }
+
+ try {
+ const [hooks, computed, enhancedDI, route, slots, template] = await Promise.all([
+ this.debugHost.getLifecycleHooks(componentKey),
+ this.debugHost.getComputedProperties(componentKey),
+ this.debugHost.getEnhancedDISnapshot(componentKey),
+ this.debugHost.getRouteInfo(componentKey),
+ this.debugHost.getSlotInfo(componentKey),
+ this.debugHost.getTemplateSnapshot(componentKey),
+ ]);
+
+ this.lifecycleHooks = hooks;
+ this.computedProperties = computed || [];
+ this.routeInfo = route;
+ this.slotInfo = slots;
+ this.templateSnapshot = template;
+ this.expandedBindings.clear();
+ this.expandedControllers.clear();
+
+ if (enhancedDI && 'version' in enhancedDI && enhancedDI.version === 2) {
+ this.enhancedDI = enhancedDI as EnhancedDISnapshot;
+ this.dependencies = null;
+ } else {
+ this.enhancedDI = null;
+ this.dependencies = enhancedDI as DISnapshot;
+ }
+ } catch {
+ this.clearEnhancedInfo();
+ }
+ }
+
+ clearEnhancedInfo() {
+ this.lifecycleHooks = null;
+ this.computedProperties = [];
+ this.dependencies = null;
+ this.enhancedDI = null;
+ this.showAvailableServices = false;
+ this.routeInfo = null;
+ this.slotInfo = null;
+ this.templateSnapshot = null;
+ this.expandedBindings.clear();
+ this.expandedControllers.clear();
+ }
+
+ get implementedHooksCount(): number {
+ if (!this.lifecycleHooks?.hooks) return 0;
+ return this.lifecycleHooks.hooks.filter(h => h.implemented).length;
+ }
+
+ get totalHooksCount(): number {
+ return this.lifecycleHooks?.hooks?.length || 0;
+ }
+
+ get activeSlotCount(): number {
+ if (!this.slotInfo?.slots) return 0;
+ return this.slotInfo.slots.filter(s => s.hasContent).length;
+ }
+
+ get hasBindables(): boolean {
+ return (this.selectedElement?.bindables?.length ?? 0) > 0;
+ }
+
+ get hasProperties(): boolean {
+ return (this.selectedElement?.properties?.length ?? 0) > 0;
+ }
+
+ get hasController(): boolean {
+ return !!(this.selectedElement as any)?.controller?.properties?.length;
+ }
+
+ get hasCustomAttributes(): boolean {
+ return this.selectedElementAttributes.length > 0;
+ }
+
+ get hasLifecycleHooks(): boolean {
+ return (this.lifecycleHooks?.hooks?.length ?? 0) > 0;
+ }
+
+ get hasComputedProperties(): boolean {
+ return this.computedProperties.length > 0;
+ }
+
+ get hasDependencies(): boolean {
+ return (this.dependencies?.dependencies?.length ?? 0) > 0;
+ }
+
+ get isEnhancedDI(): boolean {
+ return this.enhancedDI?.version === 2;
+ }
+
+ get hasEnhancedDependencies(): boolean {
+ return (this.enhancedDI?.dependencies?.length ?? 0) > 0;
+ }
+
+ get hasContainerHierarchy(): boolean {
+ return !!this.enhancedDI?.containerHierarchy;
+ }
+
+ get availableServicesCount(): number {
+ return this.enhancedDI?.availableServices?.length ?? 0;
+ }
+
+ get containerAncestorsReversed(): Array<{ id: number; depth: number; isRoot: boolean; registrationCount: number }> {
+ if (!this.enhancedDI?.containerHierarchy?.ancestors) return [];
+ return [...this.enhancedDI.containerHierarchy.ancestors].reverse();
+ }
+
+ toggleAvailableServices(): void {
+ this.showAvailableServices = !this.showAvailableServices;
+ }
+
+ formatResolvedValue(value: unknown): string {
+ if (value === null) return 'null';
+ if (value === undefined) return 'undefined';
+ if (typeof value === 'string') return `"${value}"`;
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
+ if (typeof value === 'object') {
+ if ('__type__' in (value as object)) {
+ const typed = value as { __type__: string };
+ return `${typed.__type__} {...}`;
+ }
+ return JSON.stringify(value).slice(0, 50) + (JSON.stringify(value).length > 50 ? '...' : '');
+ }
+ return String(value);
+ }
+
+ get hasRouteInfo(): boolean {
+ return !!(this.routeInfo?.currentRoute);
+ }
+
+ get hasSlots(): boolean {
+ return (this.slotInfo?.slots?.length ?? 0) > 0;
+ }
+
+ // Property editing
+ editProperty(property: Property & { originalValue?: unknown }) {
+ const editableTypes = ['string', 'number', 'boolean', 'bigint', 'null', 'undefined'];
+ if (editableTypes.includes(property.type) || property.canEdit) {
+ property.isEditing = true;
+ (property as any).originalValue = property.value;
+ }
+ }
+
+ saveProperty(property: Property, newValue: string) {
+ const originalType = property.type;
+ let convertedValue: unknown = newValue;
+
+ try {
+ switch (originalType) {
+ case 'number': {
+ const numValue = Number(newValue);
+ if (isNaN(numValue)) {
+ this.revertProperty(property);
+ return;
+ }
+ convertedValue = numValue;
+ break;
+ }
+ case 'boolean': {
+ const lower = newValue.toLowerCase();
+ if (lower !== 'true' && lower !== 'false') {
+ this.revertProperty(property);
+ return;
+ }
+ convertedValue = lower === 'true';
+ break;
+ }
+ case 'null':
+ convertedValue = newValue === 'null' || newValue === '' ? null : newValue;
+ if (convertedValue !== null) property.type = 'string';
+ break;
+ case 'undefined':
+ convertedValue = newValue === 'undefined' || newValue === '' ? undefined : newValue;
+ if (convertedValue !== undefined) property.type = 'string';
+ break;
+ default:
+ convertedValue = newValue;
+ property.type = 'string';
+ break;
+ }
+
+ property.value = convertedValue;
+ property.isEditing = false;
+ delete (property as any).originalValue;
+
+ this.plat.queueMicrotask(() => {
+ this.debugHost.updateValues(this.selectedElement!, property);
+ this.markPropertyRowsDirty();
+ });
+ } catch {
+ this.revertProperty(property);
+ }
+ }
+
+ cancelPropertyEdit(property: Property) {
+ this.revertProperty(property);
+ }
+
+ private revertProperty(property: Property) {
+ property.value = (property as any).originalValue;
+ property.isEditing = false;
+ delete (property as any).originalValue;
+ }
+
+ async copyPropertyValue(property: Property, event?: Event) {
+ event?.stopPropagation();
+
+ let valueToCopy: string;
+ if (property.value === null) {
+ valueToCopy = 'null';
+ } else if (property.value === undefined) {
+ valueToCopy = 'undefined';
+ } else if (typeof property.value === 'object') {
+ try {
+ valueToCopy = JSON.stringify(property.value, null, 2);
+ } catch {
+ valueToCopy = String(property.value);
+ }
+ } else {
+ valueToCopy = String(property.value);
+ }
+
+ try {
+ await navigator.clipboard.writeText(valueToCopy);
+ this.copiedPropertyId = `${property.name}-${property.debugId || ''}`;
+ setTimeout(() => {
+ this.copiedPropertyId = null;
+ }, 1500);
+ } catch {}
+ }
+
+ isPropertyCopied(property: Property): boolean {
+ return this.copiedPropertyId === `${property.name}-${property.debugId || ''}`;
+ }
+
+ // Property expansion
+ togglePropertyExpansion(property: Property) {
+ if (!property.canExpand) return;
+
+ if (!property.isExpanded) {
+ if (!property.expandedValue) {
+ this.loadExpandedPropertyValue(property);
+ } else {
+ property.isExpanded = true;
+ }
+ } else {
+ property.isExpanded = false;
+ }
+ this.markPropertyRowsDirty();
+ }
+
+ private loadExpandedPropertyValue(property: Property) {
+ if (property.debugId && chrome?.devtools) {
+ const code = `window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__.getExpandedDebugValueForId(${property.debugId})`;
+
+ chrome.devtools.inspectedWindow.eval(code, (result: any, isException?: any) => {
+ if (isException) return;
+
+ property.expandedValue = result;
+ property.isExpanded = true;
+ this.markPropertyRowsDirty();
+ });
+ }
+ }
+
+ private markPropertyRowsDirty() {
+ this.propertyRowsRevision++;
+ }
+
+ getPropertyRows(properties?: Property[], _revision: number = this.propertyRowsRevision): PropertyRow[] {
+ if (!properties?.length) return [];
+ return this.flattenProperties(properties, 0);
+ }
+
+ private flattenProperties(properties: Property[], depth: number): PropertyRow[] {
+ const rows: PropertyRow[] = [];
+ for (const prop of properties) {
+ if (!prop) continue;
+ rows.push({ property: prop, depth });
+ if (prop.isExpanded && prop.expandedValue?.properties?.length) {
+ rows.push(...this.flattenProperties(prop.expandedValue.properties, depth + 1));
+ }
+ }
+ return rows;
+ }
+
+ // Expression evaluation
+ async evaluateExpression() {
+ const expression = this.expressionInput.trim();
+ if (!expression || !this.selectedElement) return;
+
+ this.expressionError = '';
+ this.expressionResult = '';
+ this.expressionResultType = '';
+
+ if (!this.expressionHistory.includes(expression)) {
+ this.expressionHistory = [expression, ...this.expressionHistory.slice(0, 9)];
+ }
+
+ const componentKey = this.selectedElement.key || this.selectedElement.name;
+ if (!componentKey) {
+ this.expressionError = 'No component selected';
+ return;
+ }
+
+ const code = `
+ (function() {
+ try {
+ var hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook || !hook.evaluateInComponentContext) {
+ return { error: 'DevTools hook not available' };
+ }
+ var result = hook.evaluateInComponentContext(${JSON.stringify(componentKey)}, ${JSON.stringify(expression)});
+ return result;
+ } catch (e) {
+ return { error: e.message || String(e) };
+ }
+ })()
+ `;
+
+ if (chrome?.devtools?.inspectedWindow) {
+ chrome.devtools.inspectedWindow.eval(code, (result: any, isException?: any) => {
+ if (isException) {
+ this.expressionError = String(isException);
+ return;
+ }
+
+ if (result?.error) {
+ this.expressionError = result.error;
+ return;
+ }
+
+ if (result?.success) {
+ this.expressionResultType = result.type || typeof result.value;
+ try {
+ if (result.value === undefined) {
+ this.expressionResult = 'undefined';
+ } else if (result.value === null) {
+ this.expressionResult = 'null';
+ } else if (typeof result.value === 'object') {
+ this.expressionResult = JSON.stringify(result.value, null, 2);
+ } else {
+ this.expressionResult = String(result.value);
+ }
+ } catch {
+ this.expressionResult = String(result.value);
+ }
+ } else {
+ this.expressionError = 'Unknown response format';
+ }
+ });
+ }
+ }
+
+ selectHistoryExpression(expr: string) {
+ this.expressionInput = expr;
+ }
+
+ clearExpressionResult() {
+ this.expressionResult = '';
+ this.expressionResultType = '';
+ this.expressionError = '';
+ }
+
+ // Export
+ async exportComponentAsJson() {
+ if (!this.selectedElement) return;
+
+ const exportData = {
+ meta: {
+ name: this.selectedElement.name,
+ type: this.selectedNodeType,
+ key: this.selectedElement.key,
+ exportedAt: new Date().toISOString(),
+ },
+ bindables: this.serializeProperties(this.selectedElement.bindables || []),
+ properties: this.serializeProperties(this.selectedElement.properties || []),
+ customAttributes: this.selectedElementAttributes.map(attr => ({
+ name: attr.name,
+ bindables: this.serializeProperties(attr.bindables || []),
+ properties: this.serializeProperties(attr.properties || []),
+ })),
+ };
+
+ const jsonString = JSON.stringify(exportData, null, 2);
+
+ try {
+ await navigator.clipboard.writeText(jsonString);
+ this.copiedPropertyId = '__export__';
+ setTimeout(() => {
+ this.copiedPropertyId = null;
+ }, 1500);
+ } catch {}
+ }
+
+ private serializeProperties(properties: Property[]): Record {
+ const result: Record = {};
+ for (const prop of properties) {
+ if (prop?.name) {
+ result[prop.name] = { value: prop.value, type: prop.type };
+ }
+ }
+ return result;
+ }
+
+ get isExportCopied(): boolean {
+ return this.copiedPropertyId === '__export__';
+ }
+
+ // Reveal in Elements
+ revealInElements() {
+ if (!this.selectedElement) return;
+ this.debugHost.revealInElements({
+ name: this.selectedElement.name,
+ type: this.selectedNodeType,
+ customElementInfo: this.selectedNodeType === 'custom-element' ? this.selectedElement : null,
+ customAttributesInfo: this.selectedNodeType === 'custom-attribute' ? [this.selectedElement] : this.selectedElementAttributes,
+ });
+ }
+
+ // Format property value for display
+ formatPropertyValue(value: unknown): string {
+ if (value === null) return 'null';
+ if (value === undefined) return 'undefined';
+ if (typeof value === 'string') return `"${value}"`;
+ if (typeof value === 'object') {
+ if (Array.isArray(value)) return `Array(${value.length})`;
+ return '{...}';
+ }
+ return String(value);
+ }
+
+ getPropertyTypeClass(type: string): string {
+ const typeMap: Record = {
+ string: 'type-string',
+ number: 'type-number',
+ boolean: 'type-boolean',
+ null: 'type-null',
+ undefined: 'type-null',
+ object: 'type-object',
+ array: 'type-object',
+ function: 'type-function',
+ };
+ return typeMap[type] || 'type-default';
+ }
+}
+
+interface SearchResult {
+ key: string;
+ name: string;
+ type: 'custom-element' | 'custom-attribute';
+}
+
+interface PropertyRow {
+ property: Property;
+ depth: number;
+}
diff --git a/src/sidebar/sidebar-debug-host.ts b/src/sidebar/sidebar-debug-host.ts
new file mode 100644
index 0000000..3968320
--- /dev/null
+++ b/src/sidebar/sidebar-debug-host.ts
@@ -0,0 +1,822 @@
+import {
+ AureliaInfo,
+ ComponentTreeNode,
+ ComputedPropertyInfo,
+ DISnapshot,
+ EnhancedDISnapshot,
+ IControllerInfo,
+ LifecycleHooksSnapshot,
+ Property,
+ PropertySnapshot,
+ RouteSnapshot,
+ SlotSnapshot,
+ TemplateSnapshot,
+ WatchOptions,
+} from '../shared/types';
+import { SidebarApp } from './sidebar-app';
+
+interface SearchResult {
+ key: string;
+ name: string;
+ type: 'custom-element' | 'custom-attribute';
+}
+
+export class SidebarDebugHost {
+ consumer: SidebarApp | null = null;
+ private pickerPollingInterval: number | null = null;
+ private propertyWatchInterval: number | null = null;
+ private lastPropertySnapshot: PropertySnapshot | null = null;
+ private watchingComponentKey: string | null = null;
+ private useEventDrivenWatching = false;
+
+ attach(consumer: SidebarApp) {
+ this.consumer = consumer;
+
+ if (chrome?.devtools) {
+ // Listen for element selection changes in the Elements panel
+ chrome.devtools.panels.elements.onSelectionChanged.addListener(() => {
+ if (this.consumer && this.consumer.followChromeSelection) {
+ this.getSelectedElementInfo();
+ }
+ });
+
+ // Get initial selection
+ setTimeout(() => {
+ if (this.consumer?.followChromeSelection) {
+ this.getSelectedElementInfo();
+ }
+ }, 500);
+ }
+ }
+
+ private getSelectedElementInfo() {
+ if (!chrome?.devtools) return;
+
+ const selectionEval = `
+ (function() {
+ const target = typeof $0 !== 'undefined' ? $0 : null;
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook || !target) {
+ return null;
+ }
+
+ function getDomPath(element) {
+ if (!element || element.nodeType !== 1) return '';
+ const segments = [];
+ let current = element;
+ while (current && current.nodeType === 1 && current !== document) {
+ const tag = current.tagName ? current.tagName.toLowerCase() : 'unknown';
+ let index = 1;
+ let sibling = current;
+ while ((sibling = sibling.previousElementSibling)) {
+ if (sibling.tagName === current.tagName) index++;
+ }
+ segments.push(tag + ':nth-of-type(' + index + ')');
+ current = current.parentElement;
+ }
+ segments.push('html');
+ return segments.reverse().join(' > ');
+ }
+
+ // Get component info - traverse=true means it will walk up the DOM
+ // to find the nearest Aurelia component/binding context
+ let info = null;
+ let isBindingContext = false;
+ const selectedTagName = target.tagName ? target.tagName.toLowerCase() : 'unknown';
+
+ if (hook.getCustomElementInfo) {
+ info = hook.getCustomElementInfo(target, false);
+ }
+
+ // If no component found via getCustomElementInfo, try to find binding context
+ // by walking up the DOM manually looking for $au or .au properties
+ if (!info || (!info.customElementInfo && (!info.customAttributesInfo || !info.customAttributesInfo.length))) {
+ let el = target.parentElement;
+ while (el && el !== document.body) {
+ // Check for Aurelia v2
+ if (el.$au) {
+ const auV2 = el.$au;
+ const customElement = auV2['au:resource:custom-element'];
+ if (customElement) {
+ // Re-call with this element to get proper info extraction
+ info = hook.getCustomElementInfo(el, false);
+ if (info) {
+ isBindingContext = true;
+ break;
+ }
+ }
+ }
+ // Check for Aurelia v1
+ if (el.au && el.au.controller) {
+ info = hook.getCustomElementInfo(el, false);
+ if (info) {
+ isBindingContext = true;
+ break;
+ }
+ }
+ el = el.parentElement;
+ }
+ }
+
+ if (!info) return null;
+
+ const domPath = getDomPath(target);
+ info.__auDevtoolsDomPath = domPath;
+ info.__selectedElement = selectedTagName;
+ info.__isBindingContext = isBindingContext;
+ if (info.customElementInfo) {
+ info.customElementInfo.__auDevtoolsDomPath = domPath;
+ }
+ if (Array.isArray(info.customAttributesInfo)) {
+ info.customAttributesInfo.forEach(attr => {
+ if (attr) attr.__auDevtoolsDomPath = domPath;
+ });
+ }
+
+ return info;
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(selectionEval, (info: AureliaInfo) => {
+ if (info && this.consumer) {
+ this.consumer.onElementPicked(info);
+ }
+ });
+ }
+
+ updateValues(componentInfo: IControllerInfo, property?: Property) {
+ if (!chrome?.devtools) return;
+
+ chrome.devtools.inspectedWindow.eval(
+ `window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__.updateValues(${JSON.stringify(
+ componentInfo
+ )}, ${JSON.stringify(property)})`
+ );
+ }
+
+ // Element picker
+ startElementPicker() {
+ if (!chrome?.devtools) return;
+
+ this.startPickerPolling();
+
+ const pickerCode = `
+ (() => {
+ window.__aureliaDevtoolsStopPicker && window.__aureliaDevtoolsStopPicker();
+
+ let isPickerActive = true;
+ let currentHighlight = null;
+
+ function getDomPath(element) {
+ if (!element || element.nodeType !== 1) return '';
+ const segments = [];
+ let current = element;
+ while (current && current.nodeType === 1 && current !== document) {
+ const tag = current.tagName ? current.tagName.toLowerCase() : 'unknown';
+ let index = 1;
+ let sibling = current;
+ while ((sibling = sibling.previousElementSibling)) {
+ if (sibling.tagName === current.tagName) index++;
+ }
+ segments.push(tag + ':nth-of-type(' + index + ')');
+ current = current.parentElement;
+ }
+ segments.push('html');
+ return segments.reverse().join(' > ');
+ }
+
+ if (!document.getElementById('aurelia-picker-styles')) {
+ const style = document.createElement('style');
+ style.id = 'aurelia-picker-styles';
+ style.textContent = \`
+ .aurelia-picker-highlight {
+ outline: 2px solid #ff6b35 !important;
+ outline-offset: -2px !important;
+ background-color: rgba(255, 107, 53, 0.1) !important;
+ cursor: crosshair !important;
+ }
+ * { cursor: crosshair !important; }
+ \`;
+ document.head.appendChild(style);
+ }
+
+ function highlightElement(element) {
+ if (currentHighlight) currentHighlight.classList.remove('aurelia-picker-highlight');
+ element.classList.add('aurelia-picker-highlight');
+ currentHighlight = element;
+ }
+
+ function unhighlightElement() {
+ if (currentHighlight) {
+ currentHighlight.classList.remove('aurelia-picker-highlight');
+ currentHighlight = null;
+ }
+ }
+
+ function onMouseOver(event) {
+ if (!isPickerActive) return;
+ event.stopPropagation();
+ highlightElement(event.target);
+ }
+
+ function onMouseOut(event) {
+ if (!isPickerActive) return;
+ event.stopPropagation();
+ unhighlightElement();
+ }
+
+ function onClick(event) {
+ if (!isPickerActive) return;
+ event.preventDefault();
+ event.stopPropagation();
+
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (hook) {
+ const info = hook.getCustomElementInfo(event.target, true);
+ if (info) {
+ const domPath = getDomPath(event.target);
+ info.__auDevtoolsDomPath = domPath;
+ if (info.customElementInfo) info.customElementInfo.__auDevtoolsDomPath = domPath;
+ if (info.customAttributesInfo) {
+ info.customAttributesInfo.forEach(attr => {
+ if (attr) attr.__auDevtoolsDomPath = domPath;
+ });
+ }
+ window.__AURELIA_DEVTOOLS_PICKED_COMPONENT__ = info;
+ } else {
+ window.__AURELIA_DEVTOOLS_PICKED_COMPONENT__ = null;
+ }
+ }
+
+ window.__aureliaDevtoolsStopPicker();
+ }
+
+ document.addEventListener('mouseover', onMouseOver, true);
+ document.addEventListener('mouseout', onMouseOut, true);
+ document.addEventListener('click', onClick, true);
+
+ window.__aureliaDevtoolsStopPicker = function() {
+ isPickerActive = false;
+ unhighlightElement();
+ document.removeEventListener('mouseover', onMouseOver, true);
+ document.removeEventListener('mouseout', onMouseOut, true);
+ document.removeEventListener('click', onClick, true);
+ const styles = document.getElementById('aurelia-picker-styles');
+ if (styles) styles.remove();
+ delete window.__aureliaDevtoolsStopPicker;
+ };
+
+ return true;
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(pickerCode);
+ }
+
+ stopElementPicker() {
+ if (!chrome?.devtools) return;
+
+ this.stopPickerPolling();
+
+ chrome.devtools.inspectedWindow.eval(
+ `window.__aureliaDevtoolsStopPicker && window.__aureliaDevtoolsStopPicker();`
+ );
+ }
+
+ private startPickerPolling() {
+ this.stopPickerPolling();
+ this.pickerPollingInterval = setInterval(() => {
+ this.checkForPickedComponent();
+ }, 100) as unknown as number;
+ }
+
+ private stopPickerPolling() {
+ if (this.pickerPollingInterval) {
+ clearInterval(this.pickerPollingInterval);
+ this.pickerPollingInterval = null;
+ }
+ }
+
+ private checkForPickedComponent() {
+ if (!chrome?.devtools) return;
+
+ chrome.devtools.inspectedWindow.eval(
+ `(() => {
+ const picked = window.__AURELIA_DEVTOOLS_PICKED_COMPONENT__;
+ if (picked) {
+ window.__AURELIA_DEVTOOLS_PICKED_COMPONENT__ = null;
+ return picked;
+ }
+ return null;
+ })();`,
+ (info: AureliaInfo) => {
+ if (info && this.consumer) {
+ this.stopPickerPolling();
+ this.consumer.onElementPicked(info);
+ }
+ }
+ );
+ }
+
+ // Property watching
+ startPropertyWatching(options: WatchOptions) {
+ this.stopPropertyWatching();
+ this.watchingComponentKey = options.componentKey;
+
+ this.tryEventDrivenWatching(options.componentKey).then(success => {
+ if (success) {
+ this.useEventDrivenWatching = true;
+ } else {
+ this.useEventDrivenWatching = false;
+ const interval = options.pollInterval || 500;
+
+ this.getPropertySnapshot(options.componentKey).then(snapshot => {
+ this.lastPropertySnapshot = snapshot;
+ });
+
+ this.propertyWatchInterval = setInterval(() => {
+ this.checkForPropertyChanges();
+ }, interval) as unknown as number;
+ }
+ });
+ }
+
+ private tryEventDrivenWatching(componentKey: string): Promise {
+ return new Promise(resolve => {
+ if (!chrome?.devtools) {
+ resolve(false);
+ return;
+ }
+
+ const expr = `
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook || typeof hook.attachPropertyWatchers !== 'function') return false;
+ return hook.attachPropertyWatchers(${JSON.stringify(componentKey)});
+ } catch { return false; }
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(expr, (result: boolean) => {
+ resolve(result === true);
+ });
+ });
+ }
+
+ stopPropertyWatching() {
+ if (this.propertyWatchInterval) {
+ clearInterval(this.propertyWatchInterval);
+ this.propertyWatchInterval = null;
+ }
+
+ if (this.useEventDrivenWatching && this.watchingComponentKey && chrome?.devtools) {
+ const componentKey = this.watchingComponentKey;
+ chrome.devtools.inspectedWindow.eval(`
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (hook && typeof hook.detachPropertyWatchers === 'function') {
+ hook.detachPropertyWatchers(${JSON.stringify(componentKey)});
+ }
+ } catch {}
+ })();
+ `);
+ }
+
+ this.watchingComponentKey = null;
+ this.lastPropertySnapshot = null;
+ this.useEventDrivenWatching = false;
+ }
+
+ private checkForPropertyChanges() {
+ if (!this.watchingComponentKey) return;
+
+ this.getPropertySnapshot(this.watchingComponentKey).then(snapshot => {
+ if (!snapshot || !this.lastPropertySnapshot) {
+ this.lastPropertySnapshot = snapshot;
+ return;
+ }
+
+ const changes = this.diffPropertySnapshots(this.lastPropertySnapshot, snapshot);
+ if (changes.length > 0) {
+ this.lastPropertySnapshot = snapshot;
+ if (this.consumer) {
+ this.consumer.onPropertyChanges(changes, snapshot);
+ }
+ }
+ });
+ }
+
+ private diffPropertySnapshots(oldSnap: PropertySnapshot, newSnap: PropertySnapshot) {
+ const changes: any[] = [];
+
+ const compare = (oldProps: any[], newProps: any[], type: string) => {
+ const oldMap = new Map(oldProps.map(p => [p.name, p]));
+ const newMap = new Map(newProps.map(p => [p.name, p]));
+
+ for (const [name, newProp] of newMap) {
+ const oldProp = oldMap.get(name);
+ if (!oldProp || !this.valuesEqual(oldProp.value, newProp.value)) {
+ changes.push({
+ componentKey: newSnap.componentKey,
+ propertyName: name,
+ propertyType: type,
+ oldValue: oldProp?.value,
+ newValue: newProp.value,
+ timestamp: newSnap.timestamp,
+ });
+ }
+ }
+ };
+
+ compare(oldSnap.bindables, newSnap.bindables, 'bindable');
+ compare(oldSnap.properties, newSnap.properties, 'property');
+
+ return changes;
+ }
+
+ private valuesEqual(a: unknown, b: unknown): boolean {
+ if (a === b) return true;
+ if (typeof a !== typeof b) return false;
+ if (a === null || b === null) return a === b;
+ if (typeof a === 'object') {
+ try {
+ return JSON.stringify(a) === JSON.stringify(b);
+ } catch {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ getPropertySnapshot(componentKey: string): Promise {
+ return new Promise(resolve => {
+ if (!chrome?.devtools) {
+ resolve(null);
+ return;
+ }
+
+ const expr = `
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook || !hook.getComponentByKey) return null;
+ const info = hook.getComponentByKey(${JSON.stringify(componentKey)});
+ if (!info) return null;
+
+ const serialize = val => {
+ if (val === null) return { value: null, type: 'null' };
+ if (val === undefined) return { value: undefined, type: 'undefined' };
+ const t = typeof val;
+ if (t === 'function') return { value: '[Function]', type: 'function' };
+ if (t === 'object') {
+ try {
+ return { value: JSON.parse(JSON.stringify(val)), type: Array.isArray(val) ? 'array' : 'object' };
+ } catch { return { value: '[Object]', type: 'object' }; }
+ }
+ return { value: val, type: t };
+ };
+
+ return {
+ componentKey: ${JSON.stringify(componentKey)},
+ bindables: (info.bindables || []).map(b => ({ name: b.name, ...serialize(b.value) })),
+ properties: (info.properties || []).map(p => ({ name: p.name, ...serialize(p.value) })),
+ timestamp: Date.now()
+ };
+ } catch { return null; }
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(expr, (result: PropertySnapshot | null) => {
+ resolve(result);
+ });
+ });
+ }
+
+ // Search
+ searchComponents(query: string): Promise {
+ return new Promise(resolve => {
+ if (!chrome?.devtools) {
+ resolve([]);
+ return;
+ }
+
+ const expr = `
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook) return [];
+
+ const results = [];
+ const query = ${JSON.stringify(query.toLowerCase())};
+
+ const processTree = (nodes) => {
+ if (!nodes) return;
+ for (const node of nodes) {
+ if (node.customElementInfo) {
+ const name = node.customElementInfo.name || '';
+ if (name.toLowerCase().includes(query)) {
+ results.push({
+ key: node.customElementInfo.key || name,
+ name: name,
+ type: 'custom-element'
+ });
+ }
+ }
+ if (node.customAttributesInfo) {
+ for (const attr of node.customAttributesInfo) {
+ if (attr && attr.name && attr.name.toLowerCase().includes(query)) {
+ results.push({
+ key: attr.key || attr.name,
+ name: attr.name,
+ type: 'custom-attribute'
+ });
+ }
+ }
+ }
+ if (node.children) processTree(node.children);
+ }
+ };
+
+ if (hook.getComponentTree) {
+ processTree(hook.getComponentTree() || []);
+ } else if (hook.getAllInfo) {
+ const flat = hook.getAllInfo() || [];
+ for (const item of flat) {
+ if (item.customElementInfo) {
+ const name = item.customElementInfo.name || '';
+ if (name.toLowerCase().includes(query)) {
+ results.push({
+ key: item.customElementInfo.key || name,
+ name: name,
+ type: 'custom-element'
+ });
+ }
+ }
+ }
+ }
+
+ return results.slice(0, 20);
+ } catch { return []; }
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(expr, (results: SearchResult[]) => {
+ resolve(Array.isArray(results) ? results : []);
+ });
+ });
+ }
+
+ selectComponentByKey(key: string): void {
+ if (!chrome?.devtools) return;
+
+ const expr = `
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook) return null;
+
+ const targetKey = ${JSON.stringify(key)};
+ let result = null;
+
+ // Try getComponentByKey first if available
+ if (hook.getComponentByKey) {
+ const info = hook.getComponentByKey(targetKey);
+ if (info) {
+ result = { customElementInfo: info, customAttributesInfo: [] };
+ }
+ }
+
+ // Otherwise search through the tree
+ if (!result) {
+ const findInTree = (nodes) => {
+ if (!nodes) return null;
+ for (const node of nodes) {
+ if (node.customElementInfo) {
+ const nodeKey = node.customElementInfo.key || node.customElementInfo.name;
+ if (nodeKey === targetKey) {
+ return {
+ customElementInfo: node.customElementInfo,
+ customAttributesInfo: node.customAttributesInfo || []
+ };
+ }
+ }
+ if (node.customAttributesInfo) {
+ for (const attr of node.customAttributesInfo) {
+ const attrKey = attr.key || attr.name;
+ if (attrKey === targetKey) {
+ return {
+ customElementInfo: null,
+ customAttributesInfo: [attr]
+ };
+ }
+ }
+ }
+ if (node.children) {
+ const found = findInTree(node.children);
+ if (found) return found;
+ }
+ }
+ return null;
+ };
+
+ if (hook.getComponentTree) {
+ result = findInTree(hook.getComponentTree() || []);
+ }
+ }
+
+ // Fallback to flat list
+ if (!result && hook.getAllInfo) {
+ const flat = hook.getAllInfo() || [];
+ for (const item of flat) {
+ if (item.customElementInfo) {
+ const nodeKey = item.customElementInfo.key || item.customElementInfo.name;
+ if (nodeKey === targetKey) {
+ result = {
+ customElementInfo: item.customElementInfo,
+ customAttributesInfo: item.customAttributesInfo || []
+ };
+ break;
+ }
+ }
+ }
+ }
+
+ // If found, also select the element in the Elements panel
+ if (result && hook.findElementByComponentInfo) {
+ const el = hook.findElementByComponentInfo(result);
+ if (el) {
+ inspect(el);
+ }
+ }
+
+ return result;
+ } catch { return null; }
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(expr, (info: AureliaInfo) => {
+ if (info && this.consumer) {
+ this.consumer.onElementPicked(info);
+ }
+ });
+ }
+
+ // Reveal in Elements
+ revealInElements(componentInfo: any) {
+ if (!chrome?.devtools) return;
+
+ const code = `(() => {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook || !hook.findElementByComponentInfo) return false;
+ const el = hook.findElementByComponentInfo(${JSON.stringify(componentInfo)});
+ if (el) {
+ inspect(el);
+ return true;
+ }
+ return false;
+ } catch { return false; }
+ })();`;
+
+ chrome.devtools.inspectedWindow.eval(code);
+ }
+
+ // Enhanced info methods
+ getLifecycleHooks(componentKey: string): Promise {
+ return this.evalHook('getLifecycleHooks', componentKey);
+ }
+
+ getComputedProperties(componentKey: string): Promise {
+ return this.evalHook('getComputedProperties', componentKey).then(r => r || []);
+ }
+
+ getDependencies(componentKey: string): Promise {
+ return this.evalHook('getDependencies', componentKey);
+ }
+
+ getEnhancedDISnapshot(componentKey: string): Promise {
+ return this.evalHook('getEnhancedDISnapshot', componentKey);
+ }
+
+ getRouteInfo(componentKey: string): Promise {
+ return this.evalHook('getRouteInfo', componentKey);
+ }
+
+ getSlotInfo(componentKey: string): Promise {
+ return this.evalHook('getSlotInfo', componentKey);
+ }
+
+ getTemplateSnapshot(componentKey: string): Promise {
+ return this.evalHook('getTemplateSnapshot', componentKey);
+ }
+
+ getComponentTree(): Promise {
+ return new Promise(resolve => {
+ if (!chrome?.devtools) {
+ resolve([]);
+ return;
+ }
+
+ const expr = `
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook || typeof hook.getSimplifiedComponentTree !== 'function') return [];
+ return hook.getSimplifiedComponentTree();
+ } catch { return []; }
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(expr, (result: ComponentTreeNode[]) => {
+ resolve(Array.isArray(result) ? result : []);
+ });
+ });
+ }
+
+ // Timeline / Interaction recording
+ startInteractionRecording(): Promise {
+ return new Promise(resolve => {
+ if (!chrome?.devtools) {
+ resolve(false);
+ return;
+ }
+
+ const expr = `
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook || typeof hook.startInteractionRecording !== 'function') return false;
+ return hook.startInteractionRecording();
+ } catch { return false; }
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(expr, (result: boolean) => {
+ resolve(result === true);
+ });
+ });
+ }
+
+ stopInteractionRecording(): Promise {
+ return new Promise(resolve => {
+ if (!chrome?.devtools) {
+ resolve(false);
+ return;
+ }
+
+ const expr = `
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook || typeof hook.stopInteractionRecording !== 'function') return false;
+ return hook.stopInteractionRecording();
+ } catch { return false; }
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(expr, (result: boolean) => {
+ resolve(result === true);
+ });
+ });
+ }
+
+ clearInteractionLog(): void {
+ if (!chrome?.devtools) return;
+
+ chrome.devtools.inspectedWindow.eval(`
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (hook && typeof hook.clearInteractionLog === 'function') {
+ hook.clearInteractionLog();
+ }
+ } catch {}
+ })();
+ `);
+ }
+
+ private evalHook(method: string, componentKey: string): Promise {
+ return new Promise(resolve => {
+ if (!chrome?.devtools) {
+ resolve(null);
+ return;
+ }
+
+ const expr = `
+ (function() {
+ try {
+ const hook = window.__AURELIA_DEVTOOLS_GLOBAL_HOOK__;
+ if (!hook || typeof hook.${method} !== 'function') return null;
+ return hook.${method}(${JSON.stringify(componentKey)});
+ } catch { return null; }
+ })();
+ `;
+
+ chrome.devtools.inspectedWindow.eval(expr, (result: T | null) => {
+ resolve(result);
+ });
+ });
+ }
+}
diff --git a/src/styles.css b/src/styles.css
deleted file mode 100644
index dd44e4c..0000000
--- a/src/styles.css
+++ /dev/null
@@ -1,1872 +0,0 @@
-@import "tailwindcss";
-
-@theme {
- /* Design System Colors - Light Theme */
- --color-background: #ffffff;
- --color-foreground: #202124;
- --color-muted: #f8f9fa;
- --color-muted-foreground: #5f6368;
- --color-accent: #1967d2;
- --color-accent-hover: #1557b0;
- --color-accent-light: #8ab4f8;
- --color-border: #dadce0;
- --color-border-light: #e8eaed;
- --color-border-subtle: #f1f3f4;
- --color-danger: #d93025;
- --color-warning: #e36209;
- --color-success: #1e8e3e;
-
- /* Dark Theme Colors */
- --color-dark-background: #202124;
- --color-dark-foreground: #e8eaed;
- --color-dark-muted: #2d2e30;
- --color-dark-muted-foreground: #9aa0a6;
- --color-dark-accent: #8ab4f8;
- --color-dark-accent-hover: #aecbfa;
- --color-dark-border: #5f6368;
- --color-dark-border-light: #3c4043;
- --color-dark-border-subtle: #48494a;
- --color-dark-danger: #f28b82;
- --color-dark-warning: #fdd663;
- --color-dark-success: #81c995;
-
- /* Syntax Highlighting Colors */
- --color-syntax-string: #d73a49;
- --color-syntax-number: #005cc5;
- --color-syntax-boolean: #e36209;
- --color-syntax-null: #6f42c1;
- --color-syntax-object: #24292e;
- --color-syntax-property: #7c4dff;
-
- /* Dark Syntax Colors */
- --color-dark-syntax-string: #f28b82;
- --color-dark-syntax-number: #8ab4f8;
- --color-dark-syntax-boolean: #fdd663;
- --color-dark-syntax-null: #c58af9;
- --color-dark-syntax-object: #e8eaed;
- --color-dark-syntax-property: #bb86fc;
-
- /* Typography */
- --font-family-sans: "Segoe UI", system-ui, -apple-system, sans-serif;
- --font-family-mono: "Roboto Mono", "Consolas", monospace;
-
- /* Sizing */
- --size-header: 32px;
- --size-tab: 24px;
- --size-icon-sm: 12px;
- --size-icon: 14px;
- --size-icon-md: 16px;
- --size-icon-lg: 18px;
- --size-tree-indent: 16px;
- --size-tree-expand: 20px;
- --size-splitter: 4px;
-
- /* Spacing */
- --space-xs: 2px;
- --space-sm: 4px;
- --space-md: 6px;
- --space-lg: 8px;
- --space-xl: 12px;
- --space-2xl: 16px;
- --space-3xl: 24px;
- --space-4xl: 40px;
-
- /* Radius */
- --radius-sm: 2px;
- --radius: 4px;
- --radius-lg: 8px;
-
- /* Shadows */
- --shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
- --shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
-
- /* Transitions */
- --duration-fast: 0.1s;
- --duration-normal: 0.2s;
- --timing: ease;
-}
-
-/* Base Styles */
-@layer base {
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- body {
- font-family: var(--font-family-sans);
- font-size: 12px;
- line-height: 1.5;
- background: var(--color-background);
- color: var(--color-foreground);
- overflow: hidden;
- }
-
- button, input, optgroup, select, textarea {
- font-family: var(--font-family-sans);
- font-size: 100%;
- line-height: 1.15;
- margin: 0;
- }
-
- button, input {
- overflow: visible;
- }
-
- button, select {
- text-transform: none;
- }
-
- button, [type="button"], [type="reset"], [type="submit"] {
- -webkit-appearance: button;
- }
-
- button::-moz-focus-inner, [type="button"]::-moz-focus-inner,
- [type="reset"]::-moz-focus-inner, [type="submit"]::-moz-focus-inner {
- border-style: none;
- padding: 0;
- }
-
- button:-moz-focusring, [type="button"]:-moz-focusring,
- [type="reset"]:-moz-focusring, [type="submit"]:-moz-focusring {
- outline: 1px dotted ButtonText;
- }
-}
-
-/* Component Classes */
-@layer components {
- /* DevTools Root */
- .devtools-root {
- @apply h-screen bg-white text-gray-900 flex flex-col overflow-hidden;
- font-family: var(--font-family-sans);
- font-size: 12px;
- }
-
- .devtools-root.dark {
- @apply bg-gray-900 text-gray-100;
- }
-
- /* Header */
- .devtools-header {
- @apply bg-gray-50 border-b border-gray-200 px-4 py-2 flex items-center justify-between flex-shrink-0;
- min-height: var(--size-header);
- }
-
- .dark .devtools-header {
- @apply bg-gray-800 border-gray-600;
- }
-
- .header-content {
- @apply flex items-center gap-2 min-w-0 flex-1;
- }
-
- .header-title {
- @apply font-medium text-sm text-blue-600 whitespace-nowrap;
- }
-
- .dark .header-title {
- @apply text-blue-400;
- }
-
- .header-info {
- @apply flex items-center gap-1.5 text-gray-500 text-xs flex-1 min-w-0 justify-end;
- }
-
- .dark .header-info {
- @apply text-gray-400;
- }
-
- /* Tab Bar */
- .tab-bar {
- @apply bg-gray-50 border-b border-gray-200 flex px-4 flex-shrink-0;
- }
-
- .dark .tab-bar {
- @apply bg-gray-800 border-gray-600;
- }
-
- .tab-button {
- @apply bg-transparent border-none py-2 px-3 mr-2 flex items-center gap-1.5 text-xs cursor-pointer
- border-b-2 border-transparent text-gray-600 font-medium transition-all duration-200;
- }
-
- .tab-button:hover {
- @apply bg-gray-100 rounded-t;
- }
-
- .tab-button.active {
- @apply text-blue-600 border-blue-600 bg-white -mb-px;
- }
-
- .dark .tab-button {
- @apply text-gray-400;
- }
-
- .dark .tab-button:hover {
- @apply bg-gray-700;
- }
-
- .dark .tab-button.active {
- @apply text-blue-400 border-blue-400 bg-gray-900;
- }
-
- /* Main Content */
- .devtools-main {
- @apply flex-1 overflow-hidden p-0;
- }
-
- /* Split Pane Layout */
- .components-split-pane {
- @apply flex flex-1 overflow-hidden;
- }
-
- .components-tree-panel {
- @apply flex-shrink-0 w-80 min-w-48 max-w-96 flex flex-col border-r border-gray-200 bg-white;
- }
-
- /* Add custom scrollbar styling to tree panel scrollable areas */
- .components-tree-panel .flex-1.overflow-y-auto {
- scrollbar-width: thin;
- scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
- }
-
- .components-tree-panel .flex-1.overflow-y-auto::-webkit-scrollbar {
- width: 6px;
- }
-
- .components-tree-panel .flex-1.overflow-y-auto::-webkit-scrollbar-track {
- background: transparent;
- }
-
- .components-tree-panel .flex-1.overflow-y-auto::-webkit-scrollbar-thumb {
- background-color: rgba(0, 0, 0, 0.2);
- border-radius: 3px;
- transition: background-color 0.2s ease;
- }
-
- .components-tree-panel .flex-1.overflow-y-auto::-webkit-scrollbar-thumb:hover {
- background-color: rgba(0, 0, 0, 0.4);
- }
-
- .dark .components-tree-panel .flex-1.overflow-y-auto {
- scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
- }
-
- .dark .components-tree-panel .flex-1.overflow-y-auto::-webkit-scrollbar-thumb {
- background-color: rgba(255, 255, 255, 0.3);
- }
-
- .dark .components-tree-panel .flex-1.overflow-y-auto::-webkit-scrollbar-thumb:hover {
- background-color: rgba(255, 255, 255, 0.5);
- }
-
- .dark .components-tree-panel {
- @apply bg-gray-900 border-gray-600;
- }
-
- .splitter-vertical {
- @apply w-1 bg-gray-100 cursor-col-resize flex-shrink-0 border-l border-r border-gray-200;
- }
-
- .splitter-vertical:hover {
- @apply bg-gray-200;
- }
-
- .dark .splitter-vertical {
- @apply bg-gray-700 border-gray-600;
- }
-
- .dark .splitter-vertical:hover {
- @apply bg-gray-600;
- }
-
- .component-details-panel {
- @apply flex-1 flex flex-col bg-white overflow-hidden;
- }
-
- .dark .component-details-panel {
- @apply bg-gray-900;
- }
-
- .component-details {
- @apply h-full flex flex-col;
- }
-
- /* Toolbar */
- .components-toolbar {
- @apply bg-white border-b border-gray-200 p-2 flex flex-col gap-2 flex-shrink-0;
- }
-
- .dark .components-toolbar {
- @apply bg-gray-900 border-gray-600;
- }
-
- .toolbar-top {
- @apply flex items-center gap-3;
- }
-
- .toolbar-button {
- @apply bg-gray-50 border border-gray-200 rounded px-2 py-1.5 flex items-center gap-1 text-xs
- cursor-pointer text-gray-700 transition-colors;
- }
-
- .toolbar-button.icon-only {
- @apply px-1.5 py-1;
- }
-
- /* Refresh animation with subtle scale-in */
- @keyframes spinScale {
- 0% { transform: scale(0.9) rotate(0deg); }
- 10% { transform: scale(1.0) rotate(36deg); }
- 100% { transform: scale(1.0) rotate(360deg); }
- }
- .toolbar-button.spinning .refresh-icon {
- animation: spinScale 0.9s linear infinite;
- transform-origin: 50% 50%;
- will-change: transform;
- }
-
- .toolbar-button:hover {
- @apply bg-gray-100 border-gray-300;
- }
-
- .toolbar-button.active {
- @apply bg-blue-600 border-blue-600 text-white;
- }
-
- .toolbar-button.active:hover {
- @apply bg-blue-700 border-blue-700;
- }
-
- .dark .toolbar-button {
- @apply bg-gray-700 border-gray-600 text-gray-100;
- }
-
- .dark .toolbar-button:hover {
- @apply bg-gray-600 border-blue-400;
- }
-
- .dark .toolbar-button.active {
- @apply bg-blue-400 border-blue-400 text-gray-900;
- }
-
- .dark .toolbar-button.active:hover {
- @apply bg-blue-300 border-blue-300;
- }
-
- .toggle {
- @apply flex items-center gap-1 text-xs text-gray-600;
- }
-
- /* Search */
- .search-container {
- @apply relative flex items-center w-full;
- }
-
- .search-input {
- @apply flex-1 border border-gray-200 rounded px-7 py-1.5 text-xs bg-white text-gray-900 min-w-0;
- }
-
- .search-input:focus {
- @apply outline-none border-blue-600 shadow-sm;
- box-shadow: 0 0 0 1px var(--color-accent);
- }
-
- .search-input::placeholder {
- @apply text-gray-400;
- }
-
- .dark .search-input {
- @apply bg-gray-800 border-gray-600 text-gray-100;
- }
-
- .dark .search-input:focus {
- @apply border-blue-400;
- box-shadow: 0 0 0 1px var(--color-dark-accent);
- }
-
- .dark .search-input::placeholder {
- @apply text-gray-400;
- }
-
- .search-icon {
- @apply absolute left-2 text-gray-600 pointer-events-none z-10;
- }
-
- .dark .search-icon {
- @apply text-gray-400;
- }
-
- .clear-search {
- @apply absolute right-1.5 bg-transparent border-none p-0.5 cursor-pointer text-gray-600
- rounded-sm flex items-center justify-center;
- }
-
- .clear-search:hover {
- @apply bg-black/10 text-gray-900;
- }
-
- .dark .clear-search {
- @apply text-gray-400;
- }
-
- .dark .clear-search:hover {
- @apply bg-white/10 text-gray-100;
- }
-
- /* Component Tree */
- .component-tree {
- @apply py-2;
- }
-
- .component-tree-row {
- @apply w-full;
- }
-
- .component-item {
- @apply flex items-center gap-2 py-1 pr-4 cursor-pointer transition-colors min-h-6 rounded-sm;
- }
-
- .component-item:hover {
- @apply bg-gray-50;
- }
-
- .component-item.selected {
- @apply bg-blue-50 text-blue-600 font-medium;
- }
-
- .component-item.selected .component-name.custom-attribute {
- @apply text-red-600;
- }
-
- .dark .component-item:hover {
- @apply bg-gray-800;
- }
-
- .dark .component-item.selected {
- @apply bg-blue-900 text-blue-400;
- }
-
- .dark .component-item.selected .component-name.custom-attribute {
- @apply text-red-300;
- }
-
- .expand-toggle {
- @apply bg-transparent border-none p-0 mr-1 w-3 h-3 flex items-center justify-center
- cursor-pointer text-gray-600;
- }
-
- .expand-toggle[disabled] {
- @apply cursor-default opacity-60;
- }
-
- .expand-toggle:hover {
- @apply bg-black/10 rounded-sm;
- }
-
- .dark .expand-toggle {
- @apply text-gray-400;
- }
-
- .dark .expand-toggle:hover {
- @apply bg-white/10;
- }
-
- .expand-icon {
- @apply transition-transform duration-100;
- }
-
- .expand-icon.expanded {
- @apply rotate-90;
- }
-
- .component-icon {
- @apply flex items-center flex-shrink-0;
- }
-
- .component-icon.custom-element {
- @apply text-blue-600;
- }
-
- .component-icon.custom-attribute {
- @apply text-red-600;
- }
-
- .dark .component-icon.custom-element {
- @apply text-blue-400;
- }
-
- .dark .component-icon.custom-attribute {
- @apply text-red-400;
- }
-
- .component-name {
- @apply text-xs flex-1 min-w-0;
- font-family: var(--font-family-mono);
- }
-
- .component-name.custom-element {
- @apply text-blue-600 font-medium;
- text-decoration: underline;
- text-decoration-style: dashed;
- text-decoration-color: rgba(37, 99, 235, 0.5);
- text-underline-offset: 3px;
- }
-
- .component-name.custom-attribute {
- @apply text-red-600 italic;
- text-decoration: underline;
- text-decoration-style: dotted;
- text-decoration-color: rgba(220, 38, 38, 0.5);
- text-underline-offset: 3px;
- }
-
- .dark .component-name.custom-element {
- @apply text-blue-400;
- text-decoration-color: rgba(96, 165, 250, 0.5);
- }
-
- .dark .component-name.custom-attribute {
- @apply text-red-400;
- text-decoration-color: rgba(248, 113, 113, 0.5);
- }
-
- .component-badge {
- @apply bg-blue-50 text-blue-600 text-[10px] font-semibold px-1 rounded-full ml-2 min-w-4 text-center;
- }
-
- .dark .component-badge {
- @apply bg-blue-900 text-blue-400;
- }
-
- .view-mode-toggle {
- @apply flex items-center gap-1 ml-2;
- }
-
- .view-mode-icon {
- @apply text-sm leading-none;
- }
-
- .component-breadcrumbs {
- @apply flex items-center gap-1 text-[11px] text-gray-500 mb-2 flex-wrap;
- }
-
- .component-breadcrumbs .breadcrumb-item {
- @apply bg-transparent border-none px-1 py-0.5 rounded-sm cursor-pointer text-gray-600;
- }
-
- .component-breadcrumbs .breadcrumb-item.custom-element {
- @apply text-blue-600;
- }
-
- .component-breadcrumbs .breadcrumb-item.custom-attribute {
- @apply text-red-600 italic;
- }
-
- .component-breadcrumbs .breadcrumb-item:hover {
- @apply bg-gray-100 text-gray-800;
- }
-
- .component-breadcrumbs .breadcrumb-item.active {
- @apply font-medium bg-blue-50;
- }
-
- .dark .component-breadcrumbs {
- @apply text-gray-400;
- }
-
- .dark .component-breadcrumbs .breadcrumb-item {
- @apply text-gray-300;
- }
-
- .dark .component-breadcrumbs .breadcrumb-item.custom-element {
- @apply text-blue-300;
- }
-
- .dark .component-breadcrumbs .breadcrumb-item.custom-attribute {
- @apply text-red-300 italic;
- }
-
- .dark .component-breadcrumbs .breadcrumb-item:hover {
- @apply bg-white/10 text-gray-100;
- }
-
- .dark .component-breadcrumbs .breadcrumb-item.active {
- @apply font-medium bg-blue-900;
- }
-
- .breadcrumb-separator {
- @apply text-gray-400 text-xs;
- }
-
- .dark .breadcrumb-separator {
- @apply text-gray-500;
- }
-
- .dark .component-children {
- @apply border-gray-700;
- }
-
- /* Properties */
- .properties-section {
- @apply mb-4 border border-gray-200 rounded-lg overflow-hidden;
- }
-
- /* Visual separator between property groups */
- .properties-separator {
- @apply my-2 border-t border-gray-200;
- }
-
- .dark .properties-separator {
- @apply border-gray-700;
- }
-
- .properties-section:first-child {
- @apply mt-0;
- }
-
- .properties-section:last-child {
- @apply mb-0;
- }
-
- .dark .properties-section {
- @apply border-gray-600;
- }
-
- .section-header {
- @apply bg-gray-50 px-3 py-1 border-b border-gray-200 flex items-center gap-1.5 font-medium
- text-xs uppercase tracking-wide;
- }
-
- .dark .section-header {
- @apply bg-gray-800 border-gray-600;
- }
-
- .section-title {
- @apply text-gray-900;
- }
-
- .dark .section-title {
- @apply text-gray-100;
- }
-
- .property-count {
- @apply text-gray-600 font-normal;
- }
-
- .dark .property-count {
- @apply text-gray-400;
- }
-
- .section-content {
- @apply bg-white;
- }
-
- .dark .section-content {
- @apply bg-gray-900;
- }
-
- .property-item {
- @apply border-b border-gray-100 px-3 py-0.5;
- }
-
- .property-item:last-child {
- @apply border-b-0;
- }
-
- .dark .property-item {
- @apply border-gray-700;
- }
-
- /* Property Values */
- .property-row {
- @apply text-xs;
- line-height: 1.35;
- }
-
- .property-main {
- @apply flex items-center py-0 min-h-4 gap-1.5;
- }
-
- .property-main.expandable {
- @apply cursor-pointer;
- }
-
- .property-main.expandable:hover .property-name {
- @apply text-blue-600;
- }
-
- .dark .property-main.expandable:hover .property-name {
- @apply text-blue-300;
- }
-
- .property-name {
- @apply font-medium mr-1 flex-shrink-0;
- font-family: var(--font-family-mono);
- color: var(--color-syntax-property);
- }
-
- .dark .property-name {
- color: var(--color-dark-syntax-property);
- }
-
- .property-separator {
- @apply text-gray-600 mr-1.5;
- font-family: var(--font-family-mono);
- }
-
- .dark .property-separator {
- @apply text-gray-400;
- }
-
- .property-value-wrapper {
- @apply flex-1 relative;
- }
-
- .property-value {
- @apply cursor-pointer rounded-sm px-0.5 py-0 -mx-0.5 -my-0.5 break-words;
- font-family: var(--font-family-mono);
- }
-
- .property-value:hover {
- @apply bg-black/5;
- }
-
- .property-value.editable {
- @apply border border-transparent transition-all duration-100;
- }
-
- .property-value.editable:hover {
- @apply bg-blue-50 border-blue-200;
- }
-
- .property-value.readonly {
- @apply cursor-default opacity-70;
- }
-
- .property-value.readonly:hover {
- @apply bg-transparent;
- }
-
- .property-value.string {
- color: var(--color-syntax-string);
- }
-
- .property-value.number {
- color: var(--color-syntax-number);
- }
-
- .property-value.boolean {
- color: var(--color-syntax-boolean);
- @apply font-medium;
- }
-
- .property-value.null,
- .property-value.undefined {
- color: var(--color-syntax-null);
- @apply italic;
- }
-
- .property-value.object,
- .property-value.array,
- .property-value.node {
- color: var(--color-syntax-object);
- @apply italic;
- }
-
- .property-value.binding {
- color: var(--color-syntax-object);
- @apply italic;
- }
-
- .property-value.function {
- color: var(--color-syntax-boolean);
- @apply font-medium italic;
- }
-
- .property-expression {
- @apply inline-flex items-center ml-2 text-gray-500 text-[11px] gap-1;
- font-family: var(--font-family-mono);
- opacity: 0.85;
- }
-
- .dark .property-expression {
- @apply text-gray-400;
- }
-
- .expression-label {
- @apply text-gray-400 text-[10px] font-normal;
- font-family: var(--font-family-sans);
- }
-
- .dark .expression-label {
- @apply text-gray-500;
- }
-
- .expression-code {
- @apply px-1 py-0.5 rounded text-[11px];
- background: rgba(0, 0, 0, 0.05);
- color: var(--color-syntax-property);
- font-family: var(--font-family-mono);
- word-break: break-all;
- max-width: 200px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
-
- .dark .expression-code {
- background: rgba(255, 255, 255, 0.05);
- color: var(--color-dark-syntax-property);
- }
-
- .expand-button {
- @apply flex items-center justify-center w-5 h-5 mr-2 rounded-sm border border-transparent text-gray-500;
- }
-
- .expand-button:hover {
- @apply bg-gray-200 border-gray-300;
- }
-
- .dark .expand-button {
- @apply text-gray-300;
- }
-
- .dark .expand-button:hover {
- @apply bg-gray-700 border-gray-600;
- }
-
- .expand-button svg {
- pointer-events: none;
- }
-
- .dark .property-value:hover {
- @apply bg-white/5;
- }
-
- .dark .property-value.editable:hover {
- @apply bg-blue-400/10 border-blue-400/30;
- }
-
- .dark .property-value.readonly:hover {
- @apply bg-transparent;
- }
-
- .dark .property-value.string {
- color: var(--color-dark-syntax-string);
- }
-
- .dark .property-value.number {
- color: var(--color-dark-syntax-number);
- }
-
- .dark .property-value.boolean {
- color: var(--color-dark-syntax-boolean);
- }
-
- .dark .property-value.null,
- .dark .property-value.undefined {
- color: var(--color-dark-syntax-null);
- }
-
- .dark .property-value.object,
- .dark .property-value.array,
- .dark .property-value.node {
- color: var(--color-dark-syntax-object);
- }
-
- .dark .property-value.binding {
- color: var(--color-dark-syntax-object);
- }
-
- .dark .property-value.function {
- color: var(--color-dark-syntax-boolean);
- }
-
- .property-editor {
- @apply border border-blue-500 rounded px-1 py-0.5 outline-none bg-white w-full;
- font-family: var(--font-family-mono);
- font-size: 12px;
- box-shadow: var(--shadow-sm);
- }
-
- .dark .property-editor {
- @apply bg-gray-700 border-blue-400 text-gray-100;
- }
-
- .property-children {
- @apply ml-4 border-l border-gray-100 pl-2 mt-0.5;
- }
-
- .dark .property-children {
- @apply border-gray-700;
- }
-
- /* Empty States */
- .empty-state {
- @apply p-3 text-gray-600 italic text-xs text-center;
- }
-
- .dark .empty-state {
- @apply text-gray-400;
- }
-
- .empty-object {
- @apply text-gray-600 italic text-xs py-0.5;
- }
-
- .dark .empty-object {
- @apply text-gray-400;
- }
-
- .empty-components {
- @apply flex flex-col items-center justify-center h-full p-10 text-center;
- }
-
- .empty-components .empty-icon {
- @apply text-5xl mb-4 opacity-50;
- }
-
- .empty-components h3 {
- @apply text-base font-normal mb-2 text-gray-600;
- }
-
- .empty-components p {
- @apply text-xs text-gray-500 mb-4 max-w-xs;
- }
-
- .dark .empty-components h3 {
- @apply text-gray-400;
- }
-
- .dark .empty-components p {
- @apply text-gray-500;
- }
-
- .refresh-button {
- @apply bg-blue-600 text-white border-none rounded px-4 py-2 text-xs cursor-pointer;
- }
-
- .refresh-button:hover {
- @apply bg-blue-700;
- }
-
- .dark .refresh-button {
- @apply bg-blue-400 text-gray-900;
- }
-
- .dark .refresh-button:hover {
- @apply bg-blue-300;
- }
-
- /* Details Panel */
- .empty-details {
- @apply flex-1 flex items-center justify-center p-10;
- }
-
- .empty-details-content {
- @apply text-center max-w-xs;
- }
-
- .empty-details-icon {
- @apply mb-4 text-gray-600 text-center flex items-center justify-center;
- }
-
- .empty-details h3 {
- @apply text-base font-normal mb-2 text-gray-600;
- }
-
- .empty-details p {
- @apply text-xs text-gray-500 leading-6;
- }
-
- .dark .empty-details-icon {
- @apply text-gray-400;
- }
-
- .dark .empty-details h3 {
- @apply text-gray-400;
- }
-
- .dark .empty-details p {
- @apply text-gray-500;
- }
-
- .details-header {
- @apply bg-gray-50 border-b border-gray-200 px-4 py-3 flex-shrink-0;
- }
-
- .dark .details-header {
- @apply bg-gray-800 border-gray-600;
- }
-
- .selected-component-info {
- @apply flex items-center gap-2;
- }
-
- .selected-component-name {
- @apply text-sm font-medium text-blue-600;
- font-family: var(--font-family-mono);
- }
-
- .dark .selected-component-name {
- @apply text-blue-400;
- }
-
- .details-content {
- @apply flex-1 overflow-y-auto p-2;
- font-size: 11px;
- /* Custom scrollbar styling */
- scrollbar-width: thin;
- scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
- }
-
- .details-content::-webkit-scrollbar {
- width: 6px;
- }
-
- .details-content::-webkit-scrollbar-track {
- background: transparent;
- }
-
- .details-content::-webkit-scrollbar-thumb {
- background-color: rgba(0, 0, 0, 0.2);
- border-radius: 3px;
- transition: background-color 0.2s ease;
- }
-
- .details-content::-webkit-scrollbar-thumb:hover {
- background-color: rgba(0, 0, 0, 0.4);
- }
-
- .dark .details-content {
- scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
- }
-
- .dark .details-content::-webkit-scrollbar-thumb {
- background-color: rgba(255, 255, 255, 0.3);
- }
-
- .dark .details-content::-webkit-scrollbar-thumb:hover {
- background-color: rgba(255, 255, 255, 0.5);
- }
-
- /* Element Info */
- .element-info {
- @apply bg-gray-50 border border-gray-200 rounded-lg p-3 mb-4;
- }
-
- .dark .element-info {
- @apply bg-gray-800 border-gray-600;
- }
-
- .element-tag {
- @apply text-sm font-medium mb-1;
- font-family: var(--font-family-mono);
- }
-
- .tag-brackets {
- @apply text-gray-600;
- }
-
- .dark .tag-brackets {
- @apply text-gray-400;
- }
-
- .element-name {
- @apply text-blue-600;
- }
-
- .dark .element-name {
- @apply text-blue-400;
- }
-
- .element-aliases {
- @apply text-xs text-gray-600;
- }
-
- .dark .element-aliases {
- @apply text-gray-400;
- }
-
- .aliases-label {
- @apply font-medium;
- }
-
- .alias-list {
- font-family: var(--font-family-mono);
- }
-
- /* Attributes */
- .attribute-group {
- @apply px-3 py-2 border-b border-gray-100;
- }
-
- .attribute-group:last-child {
- @apply border-b-0;
- }
-
- .dark .attribute-group {
- @apply border-gray-700;
- }
-
- .attribute-header {
- @apply mb-2;
- }
-
- .attribute-name {
- @apply text-xs font-medium text-red-600;
- font-family: var(--font-family-mono);
- }
-
- .dark .attribute-name {
- @apply text-red-400;
- }
-
- .component-count {
- @apply text-xs text-gray-600;
- }
-
- .dark .component-count {
- @apply text-gray-400;
- }
-
- /* Inspector View */
- .inspector-view {
- @apply h-full overflow-y-auto;
- /* Custom scrollbar styling */
- scrollbar-width: thin;
- scrollbar-color: rgba(0, 0, 0, 0.2) transparent;
- }
-
- .inspector-view::-webkit-scrollbar {
- width: 6px;
- }
-
- .inspector-view::-webkit-scrollbar-track {
- background: transparent;
- }
-
- .inspector-view::-webkit-scrollbar-thumb {
- background-color: rgba(0, 0, 0, 0.2);
- border-radius: 3px;
- transition: background-color 0.2s ease;
- }
-
- .inspector-view::-webkit-scrollbar-thumb:hover {
- background-color: rgba(0, 0, 0, 0.4);
- }
-
- .dark .inspector-view {
- scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
- }
-
- .dark .inspector-view::-webkit-scrollbar-thumb {
- background-color: rgba(255, 255, 255, 0.3);
- }
-
- .dark .inspector-view::-webkit-scrollbar-thumb:hover {
- background-color: rgba(255, 255, 255, 0.5);
- }
-
- .inspector-panel {
- @apply p-4;
- }
-
- /* Empty Inspector */
- .empty-inspector {
- @apply flex-1 flex items-center justify-center p-10;
- }
-
- .empty-content {
- @apply text-center max-w-xs;
- }
-
- .empty-icon {
- @apply mb-4 text-4xl opacity-50 text-center flex items-center justify-center;
- }
-
- .empty-title {
- @apply text-base font-normal mb-2 text-gray-600;
- }
-
- .dark .empty-title {
- @apply text-gray-400;
- }
-
- .empty-description {
- @apply text-xs text-gray-500 leading-6;
- }
-
- .dark .empty-description {
- @apply text-gray-500;
- }
-
-.max-depth-indicator {
- @apply text-xs text-gray-500 italic ml-2;
-}
-
-.dark .max-depth-indicator {
- @apply text-gray-400;
-}
-
- /* Plugin Panel */
- .plugin-panel {
- display: flex;
- flex-direction: column;
- height: 100%;
- border-radius: 12px;
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid rgba(0, 0, 0, 0.06);
- padding: 16px;
- }
-
- .dark .plugin-panel {
- background: rgba(0, 0, 0, 0.35);
- border-color: rgba(255, 255, 255, 0.08);
- }
-
- .plugin-panel-header {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 16px;
- margin-bottom: 16px;
- }
-
- .plugin-panel-title {
- display: flex;
- align-items: center;
- gap: 12px;
- }
-
- .plugin-panel-icon {
- font-size: 28px;
- }
-
- .plugin-panel-heading {
- margin: 0;
- font-size: 20px;
- }
-
- .plugin-panel-description {
- margin: 4px 0 0;
- color: rgba(0, 0, 0, 0.65);
- font-size: 13px;
- }
-
- .dark .plugin-panel-description {
- color: rgba(255, 255, 255, 0.65);
- }
-
- .plugin-panel-body {
- flex: 1;
- border-radius: 8px;
- padding: 16px;
- background: rgba(0, 0, 0, 0.05);
- overflow-y: auto;
- }
-
- .dark .plugin-panel-body {
- background: rgba(0, 0, 0, 0.4);
- }
-
- .plugin-state {
- text-align: center;
- padding: 48px 12px;
- color: rgba(0, 0, 0, 0.65);
- }
-
- .dark .plugin-state {
- color: rgba(255, 255, 255, 0.75);
- }
-
- .plugin-content {
- display: flex;
- flex-direction: column;
- gap: 16px;
- }
-
- .plugin-summary {
- font-size: 14px;
- line-height: 1.5;
- padding: 12px;
- border-radius: 8px;
- background: rgba(25, 103, 210, 0.1);
- border: 1px solid rgba(25, 103, 210, 0.35);
- }
-
- .plugin-sections {
- display: flex;
- flex-direction: column;
- gap: 12px;
- }
-
- .plugin-section {
- border: 1px solid rgba(0, 0, 0, 0.08);
- border-radius: 8px;
- padding: 12px;
- background: rgba(255, 255, 255, 0.9);
- }
-
- .dark .plugin-section {
- background: rgba(0, 0, 0, 0.25);
- border-color: rgba(255, 255, 255, 0.08);
- }
-
- .plugin-section-header h3 {
- margin: 0 0 4px;
- font-size: 15px;
- }
-
- .plugin-section-header p {
- margin: 0;
- font-size: 12px;
- color: rgba(0, 0, 0, 0.6);
- }
-
- .dark .plugin-section-header p {
- color: rgba(255, 255, 255, 0.65);
- }
-
- .plugin-rows {
- margin: 12px 0 0;
- }
-
- .plugin-row {
- display: grid;
- grid-template-columns: 180px 1fr;
- gap: 12px;
- font-size: 12px;
- padding: 6px 0;
- border-bottom: 1px solid rgba(0, 0, 0, 0.06);
- }
-
- .dark .plugin-row {
- border-color: rgba(255, 255, 255, 0.08);
- }
-
- .plugin-row:last-child {
- border-bottom: none;
- }
-
- .plugin-row dt {
- font-weight: 600;
- color: rgba(0, 0, 0, 0.8);
- }
-
- .dark .plugin-row dt {
- color: rgba(255, 255, 255, 0.85);
- }
-
- .plugin-row-hint {
- display: block;
- font-weight: 400;
- font-size: 11px;
- color: rgba(0, 0, 0, 0.45);
- }
-
- .dark .plugin-row-hint {
- color: rgba(255, 255, 255, 0.5);
- }
-
- .plugin-row dd {
- margin: 0;
- }
-
- .plugin-row pre,
- .plugin-row code {
- background: rgba(0, 0, 0, 0.05);
- border-radius: 6px;
- padding: 8px;
- font-size: 11px;
- white-space: pre-wrap;
- word-break: break-word;
- }
-
- .dark .plugin-row pre,
- .dark .plugin-row code {
- background: rgba(0, 0, 0, 0.45);
- }
-
- .plugin-table-wrapper {
- overflow-x: auto;
- margin-top: 12px;
- }
-
- .plugin-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 12px;
- }
-
- .plugin-table th,
- .plugin-table td {
- border: 1px solid rgba(0, 0, 0, 0.1);
- padding: 6px 8px;
- text-align: left;
- }
-
- .dark .plugin-table th,
- .dark .plugin-table td {
- border-color: rgba(255, 255, 255, 0.15);
- }
-
- .plugin-json {
- border: 1px solid rgba(0, 0, 0, 0.08);
- border-radius: 8px;
- padding: 12px;
- background: rgba(255, 255, 255, 0.85);
- }
-
- .dark .plugin-json {
- background: rgba(0, 0, 0, 0.35);
- border-color: rgba(255, 255, 255, 0.08);
- }
-
- .plugin-json pre {
- margin: 0;
- max-height: 320px;
- overflow: auto;
- font-size: 12px;
- }
-
- /* Interaction timeline */
- .interaction-panel {
- @apply h-full flex flex-col gap-3 p-4 overflow-hidden;
- }
-
- .interaction-header {
- @apply flex items-start justify-between gap-3;
- }
-
- .interaction-header-left {
- @apply flex flex-col gap-1 min-w-0;
- }
-
- .interaction-title {
- @apply text-sm font-semibold;
- }
-
- .interaction-subtitle {
- @apply text-xs text-gray-600 dark:text-gray-400 mt-0.5 max-w-2xl;
- }
-
- .interaction-actions {
- @apply flex items-center gap-2;
- }
-
- .interaction-list {
- @apply flex-1 overflow-y-auto flex flex-col gap-2;
- }
-
- .interaction-row {
- @apply border border-gray-200 dark:border-gray-700 rounded px-3 py-2 flex justify-between gap-3 bg-white dark:bg-gray-900 shadow-sm;
- }
-
- .interaction-meta {
- @apply flex-1 min-w-0;
- }
-
- .interaction-event {
- @apply flex items-center gap-2 text-sm font-medium;
- }
-
- .interaction-event-name {
- @apply text-gray-900 dark:text-gray-100;
- }
-
- .interaction-mode {
- @apply text-[10px] uppercase tracking-wide px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-300;
- }
-
- .interaction-duration {
- @apply text-xs text-gray-500;
- }
-
- .interaction-context {
- @apply text-xs text-gray-600 dark:text-gray-400 mt-0.5 flex flex-wrap gap-1;
- }
-
- .interaction-path {
- @apply text-xs text-gray-500 dark:text-gray-400 mt-0.5 truncate;
- }
-
- .interaction-router {
- @apply text-xs text-blue-700 dark:text-blue-300 mt-0.5 flex flex-wrap gap-1;
- }
-
- .interaction-error {
- @apply text-xs text-red-600 dark:text-red-400 mt-0.5;
- }
-
- .interaction-commands {
- @apply flex flex-col gap-1 flex-shrink-0;
- }
-
- .interaction-empty {
- @apply border border-dashed border-gray-300 dark:border-gray-700 rounded p-6 text-center text-sm text-gray-600 dark:text-gray-400 flex flex-col items-center gap-2;
- }
-
- .interaction-empty--error {
- @apply border-red-300 dark:border-red-600 text-red-700 dark:text-red-300;
- }
-
- /* Search Mode Toggle */
- .search-mode-toggle {
- @apply flex-shrink-0 px-2 py-1 text-xs font-medium rounded border border-gray-300 dark:border-gray-600
- bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 cursor-pointer
- hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors;
- min-width: 42px;
- text-align: center;
- }
-
- .search-input.has-mode-toggle {
- padding-left: 8px;
- }
-
- /* Matched Property Badge */
- .matched-property-badge {
- @apply ml-1 text-xs px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900/40
- text-amber-700 dark:text-amber-300 font-mono truncate max-w-24;
- }
-
- /* Details Header Actions */
- .details-header {
- @apply flex items-center justify-between;
- }
-
- .details-header-actions {
- @apply flex items-center gap-1;
- }
-
- .export-button.copied {
- @apply text-green-600 dark:text-green-400;
- }
-
- /* Copy Property Button */
- .copy-property-button {
- @apply flex-shrink-0 opacity-0 p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-700
- text-gray-500 dark:text-gray-400 transition-all cursor-pointer border-none bg-transparent;
- margin-left: auto;
- }
-
- .property-row:hover .copy-property-button {
- @apply opacity-100;
- }
-
- .copy-property-button.copied {
- @apply opacity-100 text-green-600 dark:text-green-400;
- }
-
- .property-main {
- @apply flex items-center gap-1 flex-1 min-w-0;
- }
-
- /* Expression Evaluation Panel */
- .expression-panel {
- @apply mt-4 border-t border-gray-200 dark:border-gray-700 pt-3;
- }
-
- .expression-panel-toggle {
- @apply flex items-center gap-2 w-full px-2 py-1.5 text-xs font-medium text-gray-700 dark:text-gray-300
- bg-transparent border-none cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors;
- }
-
- .expression-panel-toggle .expand-icon {
- @apply text-gray-500 dark:text-gray-400 transition-transform;
- }
-
- .expression-panel-toggle .expand-icon.expanded {
- @apply rotate-90;
- }
-
- .expression-panel-content {
- @apply mt-2 flex flex-col gap-2;
- }
-
- .expression-input-row {
- @apply flex items-center gap-2;
- }
-
- .expression-input {
- @apply flex-1 px-2 py-1.5 text-xs font-mono border border-gray-300 dark:border-gray-600
- rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100
- focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500;
- }
-
- .expression-history {
- @apply flex flex-wrap items-center gap-1 text-xs;
- }
-
- .expression-history-label {
- @apply text-gray-500 dark:text-gray-400;
- }
-
- .expression-history-item {
- @apply px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300
- hover:bg-gray-200 dark:hover:bg-gray-600 cursor-pointer border-none font-mono text-xs truncate max-w-32;
- }
-
- .expression-result {
- @apply border border-gray-200 dark:border-gray-700 rounded p-2 bg-gray-50 dark:bg-gray-800/50;
- }
-
- .expression-result-header {
- @apply flex items-center gap-2 mb-1;
- }
-
- .expression-result-label {
- @apply text-xs font-medium text-gray-700 dark:text-gray-300;
- }
-
- .expression-result-type {
- @apply text-xs text-gray-500 dark:text-gray-400 font-mono;
- }
-
- .expression-clear-button {
- @apply ml-auto px-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200
- cursor-pointer border-none bg-transparent text-sm font-bold;
- }
-
- .expression-result-value {
- @apply text-xs font-mono whitespace-pre-wrap text-gray-900 dark:text-gray-100
- max-h-32 overflow-auto m-0;
- }
-
- .expression-error {
- @apply text-xs text-red-600 dark:text-red-400;
- }
-
- /* Enhanced Component Inspection Styles */
-
- /* Version Badge */
- .version-badge {
- @apply ml-2 px-1.5 py-0.5 text-xs rounded bg-purple-100 text-purple-700
- dark:bg-purple-900/50 dark:text-purple-300;
- }
-
- /* Lifecycle Hooks Grid */
- .lifecycle-hooks-grid {
- @apply grid gap-1 p-2;
- grid-template-columns: repeat(auto-fill, minmax(90px, 1fr));
- }
-
- .lifecycle-hook-item {
- @apply flex items-center gap-1.5 px-2 py-1 rounded text-xs;
- background: var(--color-muted, #f3f4f6);
- transition: all 0.15s ease;
- }
-
- .dark .lifecycle-hook-item {
- background: var(--color-dark-muted, #374151);
- }
-
- .lifecycle-hook-item.implemented {
- @apply bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400;
- }
-
- .lifecycle-hook-item.not-implemented {
- @apply opacity-50 text-gray-500 dark:text-gray-500;
- }
-
- .hook-indicator {
- @apply w-2 h-2 rounded-full flex-shrink-0;
- }
-
- .hook-indicator.active {
- @apply bg-green-500 dark:bg-green-400;
- }
-
- .hook-indicator.inactive {
- @apply bg-gray-300 dark:bg-gray-600;
- }
-
- .hook-name {
- @apply font-mono text-xs truncate;
- }
-
- .async-badge {
- @apply ml-auto px-1 py-0.5 text-xs rounded bg-amber-100 text-amber-700
- dark:bg-amber-900/50 dark:text-amber-300;
- font-size: 9px;
- }
-
- /* Getter/Setter Badges */
- .getter-setter-badge {
- @apply ml-1 inline-flex gap-0.5;
- }
-
- .badge-get,
- .badge-set {
- @apply px-1 rounded;
- font-size: 9px;
- }
-
- .badge-get {
- @apply bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300;
- }
-
- .badge-set {
- @apply bg-orange-100 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300;
- }
-
- /* Dependencies List */
- .dependencies-list {
- @apply list-none p-0 m-0;
- }
-
- .dependency-item {
- @apply flex items-center gap-2 px-2 py-1.5 border-b border-gray-100 dark:border-gray-700;
- }
-
- .dependency-item:last-child {
- @apply border-b-0;
- }
-
- .dependency-type-icon {
- @apply text-sm flex-shrink-0;
- }
-
- .dependency-name {
- @apply font-mono text-xs flex-1 truncate;
- }
-
- .dependency-type-label {
- @apply text-xs px-1.5 py-0.5 rounded bg-gray-100 text-gray-600
- dark:bg-gray-700 dark:text-gray-400;
- }
-
- /* Route Information */
- .route-info-content {
- @apply p-2 space-y-2;
- }
-
- .route-current,
- .route-params,
- .route-query {
- @apply flex flex-wrap items-start gap-2;
- }
-
- .route-label {
- @apply text-xs font-medium text-gray-500 dark:text-gray-400 min-w-14;
- }
-
- .route-path {
- @apply font-mono text-xs px-1.5 py-0.5 rounded bg-gray-100 dark:bg-gray-700;
- }
-
- .params-list {
- @apply list-none p-0 m-0 flex flex-wrap gap-1;
- }
-
- .param-item {
- @apply flex items-center px-1.5 py-0.5 rounded text-xs bg-gray-100 dark:bg-gray-700;
- }
-
- .param-name {
- @apply font-medium text-purple-600 dark:text-purple-400;
- }
-
- .param-separator {
- @apply mx-0.5 text-gray-400;
- }
-
- .param-value {
- @apply text-green-600 dark:text-green-400;
- }
-
- .navigating-indicator {
- @apply ml-2;
- animation: pulse 1s infinite;
- }
-
- @keyframes pulse {
- 0%,
- 100% {
- opacity: 1;
- }
- 50% {
- opacity: 0.5;
- }
- }
-
- /* Slot List */
- .slots-list {
- @apply list-none p-0 m-0;
- }
-
- .slot-item {
- @apply flex items-center gap-2 px-2 py-1.5 border-b border-gray-100 dark:border-gray-700;
- }
-
- .slot-item:last-child {
- @apply border-b-0;
- }
-
- .slot-item.empty {
- @apply opacity-60;
- }
-
- .slot-indicator {
- @apply w-2 h-2 rounded-full flex-shrink-0;
- }
-
- .slot-indicator.active {
- @apply bg-green-500 dark:bg-green-400;
- }
-
- .slot-indicator.inactive {
- @apply bg-gray-300 dark:bg-gray-600;
- }
-
- .slot-name {
- @apply font-mono text-xs;
- }
-
- .slot-node-count {
- @apply text-xs text-gray-500 dark:text-gray-400;
- }
-
- .slot-empty-label {
- @apply ml-auto text-xs italic text-gray-400 dark:text-gray-500;
- }
-
- /* Extension invalidated overlay */
- .extension-invalidated-overlay {
- @apply fixed inset-0 flex items-center justify-center;
- background-color: var(--color-background);
- z-index: 1000;
- }
-
- .dark .extension-invalidated-overlay {
- background-color: var(--color-dark-background);
- }
-
- .extension-invalidated-content {
- @apply flex flex-col items-center justify-center p-10 text-center max-w-md;
- }
-
- .extension-invalidated-content .empty-icon {
- @apply text-6xl mb-4 opacity-60;
- }
-
- .extension-invalidated-content h3 {
- @apply text-lg font-medium mb-3;
- color: var(--color-foreground);
- }
-
- .dark .extension-invalidated-content h3 {
- color: var(--color-dark-foreground);
- }
-
- .extension-invalidated-content p {
- @apply text-sm leading-relaxed;
- color: var(--color-muted-foreground);
- }
-
- .dark .extension-invalidated-content p {
- color: var(--color-dark-muted-foreground);
- }
-}
diff --git a/tests/app-expression.spec.ts b/tests/app-expression.spec.ts
deleted file mode 100644
index 8ba5b54..0000000
--- a/tests/app-expression.spec.ts
+++ /dev/null
@@ -1,454 +0,0 @@
-import './setup';
-import { ChromeTest } from './setup';
-import { stubDebugHost, stubPlatform } from './helpers';
-
-let AppClass: any;
-
-async function createApp() {
- jest.resetModules();
- const mod = await import('@/app');
- AppClass = mod.App;
- const app = Object.create(AppClass.prototype);
-
- app.coreTabs = [
- { id: 'all', label: 'All', icon: '🌲', kind: 'core' },
- { id: 'components', label: 'Components', icon: '📦', kind: 'core' },
- { id: 'attributes', label: 'Attributes', icon: '🔧', kind: 'core' },
- { id: 'interactions', label: 'Interactions', icon: '⏱️', kind: 'core' },
- ];
- app.activeTab = 'all';
- app.tabs = [...app.coreTabs];
- app.externalTabs = [];
- app.externalPanels = {};
- app.externalPanelsVersion = 0;
- app.externalPanelLoading = {};
- app.externalRefreshHandle = null;
- app.selectedElement = undefined;
- app.selectedElementAttributes = undefined;
- app.allAureliaObjects = undefined;
- app.componentTree = [];
- app.componentSnapshot = { tree: [], flat: [] };
- app.selectedComponentId = undefined;
- app.selectedBreadcrumb = [];
- app.selectedNodeType = 'custom-element';
- app.searchQuery = '';
- app.searchMode = 'name';
- app.viewMode = 'tree';
- app.isElementPickerActive = false;
- app.interactionLog = [];
- app.interactionLoading = false;
- app.interactionError = null;
- app.aureliaDetected = false;
- app.aureliaVersion = null;
- app.detectionState = 'checking';
- app.isRefreshing = false;
- app.expressionInput = '';
- app.expressionResult = '';
- app.expressionResultType = '';
- app.expressionError = '';
- app.expressionHistory = [];
- app.isExpressionPanelOpen = false;
- app.copiedPropertyId = null;
- app.propertyRowsRevision = 0;
-
- const debugHost = stubDebugHost();
- const plat = stubPlatform();
- app.debugHost = debugHost;
- app.plat = plat;
-
- return { app, debugHost, plat };
-}
-
-describe('App expression evaluation', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('evaluateExpression does nothing with empty input', async () => {
- app.expressionInput = '';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
-
- await app.evaluateExpression();
-
- expect(app.expressionError).toBe('');
- expect(app.expressionResult).toBe('');
- });
-
- it('evaluateExpression does nothing without selected element', async () => {
- app.expressionInput = 'this.count';
- app.selectedElement = undefined;
-
- await app.evaluateExpression();
-
- expect(chrome.devtools.inspectedWindow.eval).not.toHaveBeenCalled();
- });
-
- it('evaluateExpression sets error when no component key', async () => {
- app.expressionInput = 'this.count';
- app.selectedElement = {};
-
- await app.evaluateExpression();
-
- expect(app.expressionError).toBe('No component selected');
- });
-
- it('evaluateExpression adds to history', async () => {
- app.expressionInput = 'this.count';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- app.expressionHistory = [];
- ChromeTest.setEvalToReturn([{ result: { success: true, value: 42, type: 'number' } }]);
-
- await app.evaluateExpression();
-
- expect(app.expressionHistory).toContain('this.count');
- });
-
- it('evaluateExpression does not duplicate history', async () => {
- app.expressionInput = 'this.count';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- app.expressionHistory = ['this.count'];
- ChromeTest.setEvalToReturn([{ result: { success: true, value: 42, type: 'number' } }]);
-
- await app.evaluateExpression();
-
- expect(app.expressionHistory.filter((h: string) => h === 'this.count')).toHaveLength(1);
- });
-
- it('evaluateExpression handles eval exception', async () => {
- app.expressionInput = 'this.count';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb(undefined, 'Syntax Error');
- });
-
- await app.evaluateExpression();
-
- expect(app.expressionError).toBe('Syntax Error');
- });
-
- it('evaluateExpression handles result error', async () => {
- app.expressionInput = 'this.count';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- ChromeTest.setEvalToReturn([{ result: { error: 'Component not found' } }]);
-
- await app.evaluateExpression();
-
- expect(app.expressionError).toBe('Component not found');
- });
-
- it('evaluateExpression handles successful number result', async () => {
- app.expressionInput = 'this.count';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- ChromeTest.setEvalToReturn([{ result: { success: true, value: 42, type: 'number' } }]);
-
- await app.evaluateExpression();
-
- expect(app.expressionResult).toBe('42');
- expect(app.expressionResultType).toBe('number');
- expect(app.expressionError).toBe('');
- });
-
- it('evaluateExpression handles successful string result', async () => {
- app.expressionInput = 'this.name';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- ChromeTest.setEvalToReturn([{ result: { success: true, value: 'hello', type: 'string' } }]);
-
- await app.evaluateExpression();
-
- expect(app.expressionResult).toBe('hello');
- expect(app.expressionResultType).toBe('string');
- });
-
- it('evaluateExpression handles undefined result', async () => {
- app.expressionInput = 'this.missing';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- ChromeTest.setEvalToReturn([{ result: { success: true, value: undefined, type: 'undefined' } }]);
-
- await app.evaluateExpression();
-
- expect(app.expressionResult).toBe('undefined');
- });
-
- it('evaluateExpression handles null result', async () => {
- app.expressionInput = 'this.nullable';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- ChromeTest.setEvalToReturn([{ result: { success: true, value: null, type: 'object' } }]);
-
- await app.evaluateExpression();
-
- expect(app.expressionResult).toBe('null');
- });
-
- it('evaluateExpression handles object result', async () => {
- app.expressionInput = 'this.data';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- ChromeTest.setEvalToReturn([{ result: { success: true, value: { foo: 'bar' }, type: 'object' } }]);
-
- await app.evaluateExpression();
-
- expect(app.expressionResult).toContain('foo');
- expect(app.expressionResult).toContain('bar');
- });
-
- it('evaluateExpression handles unknown response format', async () => {
- app.expressionInput = 'this.count';
- app.selectedElement = { name: 'comp', key: 'comp-key' };
- ChromeTest.setEvalToReturn([{ result: {} }]);
-
- await app.evaluateExpression();
-
- expect(app.expressionError).toBe('Unknown response format');
- });
-});
-
-describe('App property expansion', () => {
- let app: any;
- let plat: any;
-
- beforeEach(async () => {
- jest.useFakeTimers();
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- plat = result.plat;
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('togglePropertyExpansion expands expandable properties', () => {
- const prop = { canExpand: true, isExpanded: false, debugId: 1, expandedValue: { properties: [] } };
- app.selectedElement = { bindables: [prop], properties: [] };
-
- app.togglePropertyExpansion(prop);
-
- expect(prop.isExpanded).toBe(true);
- });
-
- it('togglePropertyExpansion collapses expanded properties', () => {
- const prop = { canExpand: true, isExpanded: true, debugId: 1, expandedValue: { properties: [] } };
- app.selectedElement = { bindables: [prop], properties: [] };
-
- app.togglePropertyExpansion(prop);
-
- expect(prop.isExpanded).toBe(false);
- });
-
- it('togglePropertyExpansion does nothing for non-expandable', () => {
- const prop = { canExpand: false, isExpanded: false };
- app.selectedElement = { bindables: [prop], properties: [] };
-
- app.togglePropertyExpansion(prop);
-
- expect(prop.isExpanded).toBe(false);
- });
-
- it('loadExpandedPropertyValue fetches expanded value via eval', () => {
- const prop = { canExpand: true, isExpanded: false, debugId: 42, expandedValue: null };
- app.selectedElement = { bindables: [prop], properties: [] };
-
- ChromeTest.setEvalToReturn([{ result: { properties: [{ name: 'child', value: 'test' }] } }]);
-
- app.loadExpandedPropertyValue(prop);
-
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- const code = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls[0][0];
- expect(code).toContain('getExpandedDebugValueForId(42)');
- });
-
- it('loadExpandedPropertyValue handles eval exception', () => {
- const prop = { canExpand: true, isExpanded: false, debugId: 42, expandedValue: null };
- app.selectedElement = { bindables: [], properties: [prop] };
-
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb(undefined, 'Error loading');
- });
-
- app.loadExpandedPropertyValue(prop);
-
- expect(prop.isExpanded).toBe(false);
- expect(prop.expandedValue).toBeNull();
- });
-
- it('markPropertyRowsDirty increments revision', () => {
- const initial = app.propertyRowsRevision;
- app.markPropertyRowsDirty();
- expect(app.propertyRowsRevision).toBe(initial + 1);
- });
-});
-
-describe('App node path finding', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('findNodePathById returns path to nested node', () => {
- app.componentTree = [
- {
- id: 'root',
- name: 'root',
- children: [
- {
- id: 'child',
- name: 'child',
- children: [
- { id: 'grandchild', name: 'grandchild', children: [] }
- ]
- }
- ]
- }
- ];
-
- const path = app.findNodePathById('grandchild');
-
- expect(path).toHaveLength(3);
- expect(path[0].id).toBe('root');
- expect(path[1].id).toBe('child');
- expect(path[2].id).toBe('grandchild');
- });
-
- it('findNodePathById returns null for non-existent node', () => {
- app.componentTree = [{ id: 'root', name: 'root', children: [] }];
-
- const path = app.findNodePathById('nonexistent');
-
- expect(path).toBeNull();
- });
-
- it('findNodePathById handles empty tree', () => {
- app.componentTree = [];
-
- const path = app.findNodePathById('any');
-
- expect(path).toBeNull();
- });
-});
-
-describe('App component matching', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('nodeMatchesElement returns false for non-element nodes', () => {
- const node = { data: { kind: 'attribute' } };
- const target = { name: 'comp', key: 'comp-key' };
-
- expect(app.nodeMatchesElement(node, target)).toBe(false);
- });
-
- it('nodeMatchesElement returns false without target', () => {
- const node = { data: { kind: 'element', info: { customElementInfo: { name: 'comp' } } } };
-
- expect(app.nodeMatchesElement(node, null)).toBe(false);
- });
-
- it('nodeMatchesAttribute returns false for non-attribute nodes', () => {
- const node = { data: { kind: 'element' } };
-
- expect(app.nodeMatchesAttribute(node, [{ name: 'attr' }])).toBe(false);
- });
-
- it('nodeMatchesAttribute returns false without target attributes', () => {
- const node = { data: { kind: 'attribute', raw: { name: 'attr' } } };
-
- expect(app.nodeMatchesAttribute(node, [])).toBe(false);
- expect(app.nodeMatchesAttribute(node, null)).toBe(false);
- });
-
- it('nodeContainsAnyAttribute returns false for empty arrays', () => {
- expect(app.nodeContainsAnyAttribute([], [{ name: 'attr' }])).toBe(false);
- expect(app.nodeContainsAnyAttribute([{ name: 'attr' }], [])).toBe(false);
- expect(app.nodeContainsAnyAttribute(null, [{ name: 'attr' }])).toBe(false);
- });
-
- it('searchNodesForMatch finds matching element', () => {
- const nodes = [
- {
- id: 'comp1',
- data: { kind: 'element', info: { customElementInfo: { name: 'target', key: 'target-key' }, customAttributesInfo: [] } },
- children: []
- }
- ];
- const target = { name: 'target', key: 'target-key' };
-
- const found = app.searchNodesForMatch(nodes, target, []);
-
- expect(found).toBeDefined();
- expect(found.id).toBe('comp1');
- });
-
- it('searchNodesForMatch searches children', () => {
- const nodes = [
- {
- id: 'parent',
- data: { kind: 'element', info: { customElementInfo: { name: 'parent' }, customAttributesInfo: [] } },
- children: [
- {
- id: 'child',
- data: { kind: 'element', info: { customElementInfo: { name: 'target', key: 'target-key' }, customAttributesInfo: [] } },
- children: []
- }
- ]
- }
- ];
- const target = { name: 'target', key: 'target-key' };
-
- const found = app.searchNodesForMatch(nodes, target, []);
-
- expect(found).toBeDefined();
- expect(found.id).toBe('child');
- });
-});
-
-describe('App handleExpandToggle', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('stops event propagation', () => {
- const node = { id: 'test', expanded: false };
- app.componentTree = [node];
- const event = { stopPropagation: jest.fn() };
-
- app.handleExpandToggle(node, event);
-
- expect(event.stopPropagation).toHaveBeenCalled();
- });
-
- it('does nothing for node without id', () => {
- const node = { expanded: false };
- app.componentTree = [];
- const spy = jest.spyOn(app, 'toggleComponentExpansion');
-
- app.handleExpandToggle(node);
-
- expect(spy).not.toHaveBeenCalled();
- spy.mockRestore();
- });
-
- it('does nothing for null node', () => {
- const spy = jest.spyOn(app, 'toggleComponentExpansion');
-
- app.handleExpandToggle(null);
-
- expect(spy).not.toHaveBeenCalled();
- spy.mockRestore();
- });
-});
diff --git a/tests/app-extras.spec.ts b/tests/app-extras.spec.ts
deleted file mode 100644
index 10eb43b..0000000
--- a/tests/app-extras.spec.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import './setup';
-import { ChromeTest } from './setup';
-import { stubDebugHost, stubPlatform } from './helpers';
-
-describe('App extra behaviors', () => {
- let app: any;
- let AppClass: any;
- let debugHost: any;
- let plat: any;
-
- beforeEach(async () => {
- jest.useFakeTimers();
- ChromeTest.reset();
- jest.resetModules();
- const mod = await import('@/app');
- AppClass = mod.App;
- app = Object.create(AppClass.prototype);
- // seed minimal state
- app.activeTab = 'all';
- app.tabs = [];
- app.selectedElement = undefined;
- app.selectedElementAttributes = undefined;
- app.allAureliaObjects = undefined;
- app.componentTree = [];
- app.filteredComponentTree = [];
- app.selectedComponentId = undefined;
- app.searchQuery = '';
- app.isElementPickerActive = false;
- app.aureliaDetected = false;
- app.aureliaVersion = null;
- app.detectionState = 'checking';
-
- debugHost = stubDebugHost({ revealInElements: jest.fn() });
- plat = stubPlatform();
- app.debugHost = debugHost;
- app.plat = plat;
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('toggleElementPicker starts and stops picker via debugHost', () => {
- expect(app.isElementPickerActive).toBe(false);
- app.toggleElementPicker();
- expect(app.isElementPickerActive).toBe(true);
- expect(debugHost.startElementPicker).toHaveBeenCalled();
-
- app.toggleElementPicker();
- expect(app.isElementPickerActive).toBe(false);
- expect(debugHost.stopElementPicker).toHaveBeenCalled();
- });
-
- it('toggleFollowChromeSelection persists to localStorage', () => {
- const setItem = jest.spyOn(window.localStorage.__proto__, 'setItem');
- const initial = app.followChromeSelection;
- app.toggleFollowChromeSelection();
- expect(app.followChromeSelection).toBe(!initial);
- expect(setItem).toHaveBeenCalledWith('au-devtools.followChromeSelection', String(!initial));
- setItem.mockRestore();
- });
-
- it('valueChanged schedules updateValues microtask', () => {
- const el = { name: 'x' } as any;
- app.valueChanged(el);
- jest.runOnlyPendingTimers();
- expect(debugHost.updateValues).toHaveBeenCalledWith(el);
- });
-
- it('revealInElements calls debugHost.revealInElements with selected component info', () => {
- // Create minimal tree
- const node = {
- id: 'n1', name: 'comp', type: 'custom-element', children: [], expanded: false,
- data: { customElementInfo: { key: 'k' }, customAttributesInfo: [] }
- };
- app.componentTree = [node];
- app.selectedComponentId = 'n1';
-
- app.revealInElements();
- expect(debugHost.revealInElements).toHaveBeenCalledWith(expect.objectContaining({ name: 'comp', type: 'custom-element' }));
- });
-});
diff --git a/tests/app-interactions.spec.ts b/tests/app-interactions.spec.ts
deleted file mode 100644
index e2b2695..0000000
--- a/tests/app-interactions.spec.ts
+++ /dev/null
@@ -1,497 +0,0 @@
-import './setup';
-import { ChromeTest } from './setup';
-import { stubDebugHost, stubPlatform } from './helpers';
-
-let AppClass: any;
-
-async function createApp() {
- jest.resetModules();
- const mod = await import('@/app');
- AppClass = mod.App;
- const app = Object.create(AppClass.prototype);
-
- app.coreTabs = [
- { id: 'all', label: 'All', icon: '🌲', kind: 'core' },
- { id: 'components', label: 'Components', icon: '📦', kind: 'core' },
- { id: 'attributes', label: 'Attributes', icon: '🔧', kind: 'core' },
- { id: 'interactions', label: 'Interactions', icon: '⏱️', kind: 'core' },
- ];
- app.activeTab = 'all';
- app.tabs = [...app.coreTabs];
- app.externalTabs = [];
- app.externalPanels = {};
- app.externalPanelsVersion = 0;
- app.externalPanelLoading = {};
- app.externalRefreshHandle = null;
- app.selectedElement = undefined;
- app.selectedElementAttributes = undefined;
- app.allAureliaObjects = undefined;
- app.componentTree = [];
- app.componentSnapshot = { tree: [], flat: [] };
- app.selectedComponentId = undefined;
- app.selectedBreadcrumb = [];
- app.selectedNodeType = 'custom-element';
- app.searchQuery = '';
- app.searchMode = 'name';
- app.viewMode = 'tree';
- app.isElementPickerActive = false;
- app.interactionLog = [];
- app.interactionLoading = false;
- app.interactionError = null;
- app.interactionSignature = '';
- app.aureliaDetected = false;
- app.aureliaVersion = null;
- app.detectionState = 'checking';
- app.isRefreshing = false;
- app.copiedPropertyId = null;
- app.propertyRowsRevision = 0;
-
- const debugHost = stubDebugHost();
- const plat = stubPlatform();
- app.debugHost = debugHost;
- app.plat = plat;
-
- return { app, debugHost, plat };
-}
-
-describe('App interaction log loading', () => {
- let app: any;
- let debugHost: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- debugHost = result.debugHost;
- });
-
- it('loadInteractionLog fetches and sorts log', async () => {
- debugHost.getInteractionLog.mockResolvedValue([
- { id: 'evt-1', timestamp: 100 },
- { id: 'evt-2', timestamp: 200 },
- ]);
-
- await app.loadInteractionLog();
-
- expect(app.interactionLog).toHaveLength(2);
- expect(app.interactionLog[0].id).toBe('evt-2');
- expect(app.interactionLog[1].id).toBe('evt-1');
- });
-
- it('loadInteractionLog sets loading state', async () => {
- let resolvePromise: Function;
- debugHost.getInteractionLog.mockReturnValue(new Promise(r => { resolvePromise = r; }));
-
- const promise = app.loadInteractionLog();
- expect(app.interactionLoading).toBe(true);
-
- resolvePromise!([]);
- await promise;
- expect(app.interactionLoading).toBe(false);
- });
-
- it('loadInteractionLog handles errors', async () => {
- debugHost.getInteractionLog.mockRejectedValue(new Error('Network error'));
-
- await app.loadInteractionLog();
-
- expect(app.interactionError).toBe('Network error');
- expect(app.interactionLog).toEqual([]);
- });
-
- it('loadInteractionLog silent mode does not update loading state', async () => {
- debugHost.getInteractionLog.mockResolvedValue([]);
- app.interactionLoading = false;
-
- await app.loadInteractionLog(false, true);
-
- expect(app.interactionLoading).toBe(false);
- });
-
- it('loadInteractionLog skips reload if signature unchanged', async () => {
- debugHost.getInteractionLog.mockResolvedValue([
- { id: 'evt-1', timestamp: 100 }
- ]);
-
- await app.loadInteractionLog();
- const firstLog = app.interactionLog;
-
- await app.loadInteractionLog();
- expect(app.interactionLog).toBe(firstLog);
- });
-});
-
-describe('App interaction replay and snapshot', () => {
- let app: any;
- let debugHost: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- debugHost = result.debugHost;
- });
-
- it('replayInteraction calls debugHost', async () => {
- debugHost.replayInteraction.mockResolvedValue(true);
-
- await app.replayInteraction('evt-1');
-
- expect(debugHost.replayInteraction).toHaveBeenCalledWith('evt-1');
- });
-
- it('replayInteraction does nothing for empty id', async () => {
- await app.replayInteraction('');
- await app.replayInteraction(null);
-
- expect(debugHost.replayInteraction).not.toHaveBeenCalled();
- });
-
- it('replayInteraction handles errors gracefully', async () => {
- debugHost.replayInteraction.mockRejectedValue(new Error('Replay failed'));
-
- await expect(app.replayInteraction('evt-1')).resolves.toBeUndefined();
- });
-
- it('applyInteractionSnapshot calls debugHost with phase', async () => {
- debugHost.applyInteractionSnapshot.mockResolvedValue(true);
-
- await app.applyInteractionSnapshot('evt-1', 'before');
-
- expect(debugHost.applyInteractionSnapshot).toHaveBeenCalledWith('evt-1', 'before');
- });
-
- it('applyInteractionSnapshot does nothing for empty id', async () => {
- await app.applyInteractionSnapshot('', 'before');
-
- expect(debugHost.applyInteractionSnapshot).not.toHaveBeenCalled();
- });
-
- it('clearInteractionLog clears log optimistically and calls debugHost', async () => {
- app.interactionLog = [{ id: 'evt-1' }];
- app.interactionSignature = 'evt-1:100';
- debugHost.clearInteractionLog.mockResolvedValue(true);
-
- await app.clearInteractionLog();
-
- expect(app.interactionLog).toEqual([]);
- expect(app.interactionSignature).toBe('');
- expect(debugHost.clearInteractionLog).toHaveBeenCalled();
- });
-});
-
-describe('App incoming interaction handling', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('handleIncomingInteraction adds new entries', () => {
- app.interactionLog = [];
- app.handleIncomingInteraction({ id: 'evt-1', timestamp: 100 });
-
- expect(app.interactionLog).toHaveLength(1);
- expect(app.interactionLog[0].id).toBe('evt-1');
- });
-
- it('handleIncomingInteraction ignores duplicates', () => {
- app.interactionLog = [{ id: 'evt-1', timestamp: 100 }];
- app.handleIncomingInteraction({ id: 'evt-1', timestamp: 100 });
-
- expect(app.interactionLog).toHaveLength(1);
- });
-
- it('handleIncomingInteraction ignores invalid entries', () => {
- app.interactionLog = [];
- app.handleIncomingInteraction(null);
- app.handleIncomingInteraction({});
- app.handleIncomingInteraction({ id: '' });
-
- expect(app.interactionLog).toHaveLength(0);
- });
-
- it('handleIncomingInteraction sorts by timestamp descending', () => {
- app.interactionLog = [{ id: 'evt-1', timestamp: 100 }];
- app.handleIncomingInteraction({ id: 'evt-2', timestamp: 200 });
-
- expect(app.interactionLog[0].id).toBe('evt-2');
- expect(app.interactionLog[1].id).toBe('evt-1');
- });
-
- it('handleIncomingInteraction updates signature', () => {
- app.interactionLog = [];
- app.interactionSignature = '';
- app.handleIncomingInteraction({ id: 'evt-1', timestamp: 100 });
-
- expect(app.interactionSignature).toContain('evt-1');
- });
-});
-
-describe('App property editing', () => {
- let app: any;
- let debugHost: any;
- let plat: any;
-
- beforeEach(async () => {
- jest.useFakeTimers();
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- debugHost = result.debugHost;
- plat = result.plat;
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('editProperty sets editing state for editable types', () => {
- const prop = { type: 'string', value: 'hello', isEditing: false };
- app.editProperty(prop);
- expect(prop.isEditing).toBe(true);
- expect(prop.originalValue).toBe('hello');
- });
-
- it('editProperty does not edit non-editable types', () => {
- const prop = { type: 'function', value: () => {}, isEditing: false };
- app.editProperty(prop);
- expect(prop.isEditing).toBe(false);
- });
-
- it('cancelPropertyEdit reverts value and clears editing state', () => {
- const prop = { type: 'string', value: 'new', isEditing: true, originalValue: 'old' };
- app.cancelPropertyEdit(prop);
- expect(prop.value).toBe('old');
- expect(prop.isEditing).toBe(false);
- expect(prop.originalValue).toBeUndefined();
- });
-
- it('saveProperty handles null conversion', () => {
- const prop = { type: 'null', value: null, isEditing: true, originalValue: null };
- app.selectedElement = { properties: [prop], bindables: [] };
-
- app.saveProperty(prop, 'null');
-
- expect(prop.value).toBeNull();
- expect(prop.isEditing).toBe(false);
- });
-
- it('saveProperty converts null input to string when not null', () => {
- const prop = { type: 'null', value: null, isEditing: true, originalValue: null };
- app.selectedElement = { properties: [prop], bindables: [] };
-
- app.saveProperty(prop, 'some text');
-
- expect(prop.value).toBe('some text');
- expect(prop.type).toBe('string');
- });
-
- it('saveProperty handles undefined conversion', () => {
- const prop = { type: 'undefined', value: undefined, isEditing: true, originalValue: undefined };
- app.selectedElement = { properties: [prop], bindables: [] };
-
- app.saveProperty(prop, 'undefined');
-
- expect(prop.value).toBeUndefined();
- expect(prop.isEditing).toBe(false);
- });
-
- it('saveProperty converts undefined input to string when not undefined', () => {
- const prop = { type: 'undefined', value: undefined, isEditing: true, originalValue: undefined };
- app.selectedElement = { properties: [prop], bindables: [] };
-
- app.saveProperty(prop, 'new value');
-
- expect(prop.value).toBe('new value');
- expect(prop.type).toBe('string');
- });
-
- it('saveProperty reverts on invalid boolean', () => {
- const prop = { type: 'boolean', value: true, isEditing: true, originalValue: true };
- app.selectedElement = { properties: [prop], bindables: [] };
-
- app.saveProperty(prop, 'not-a-boolean');
-
- expect(prop.value).toBe(true);
- expect(prop.isEditing).toBe(false);
- });
-});
-
-describe('App property copying', () => {
- let app: any;
-
- beforeEach(async () => {
- jest.useFakeTimers();
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
-
- Object.assign(navigator, {
- clipboard: {
- writeText: jest.fn().mockResolvedValue(undefined),
- },
- });
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('copyPropertyValue copies null as string', async () => {
- const prop = { name: 'test', value: null, debugId: '1' };
- await app.copyPropertyValue(prop);
-
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith('null');
- });
-
- it('copyPropertyValue copies undefined as string', async () => {
- const prop = { name: 'test', value: undefined, debugId: '1' };
- await app.copyPropertyValue(prop);
-
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith('undefined');
- });
-
- it('copyPropertyValue copies objects as JSON', async () => {
- const prop = { name: 'test', value: { foo: 'bar' }, debugId: '1' };
- await app.copyPropertyValue(prop);
-
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith(JSON.stringify({ foo: 'bar' }, null, 2));
- });
-
- it('copyPropertyValue copies primitives as strings', async () => {
- const prop = { name: 'test', value: 42, debugId: '1' };
- await app.copyPropertyValue(prop);
-
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith('42');
- });
-
- it('copyPropertyValue sets copied state temporarily', async () => {
- const prop = { name: 'test', value: 'hello', debugId: '1' };
- await app.copyPropertyValue(prop);
-
- expect(app.copiedPropertyId).toBe('test-1');
-
- jest.advanceTimersByTime(1600);
- expect(app.copiedPropertyId).toBeNull();
- });
-
- it('isPropertyCopied returns correct state', async () => {
- const prop = { name: 'test', value: 'hello', debugId: '1' };
- expect(app.isPropertyCopied(prop)).toBe(false);
-
- await app.copyPropertyValue(prop);
- expect(app.isPropertyCopied(prop)).toBe(true);
- });
-
- it('copyPropertyValue stops event propagation', async () => {
- const prop = { name: 'test', value: 'hello', debugId: '1' };
- const event = { stopPropagation: jest.fn() };
-
- await app.copyPropertyValue(prop, event);
-
- expect(event.stopPropagation).toHaveBeenCalled();
- });
-});
-
-describe('App component export', () => {
- let app: any;
-
- beforeEach(async () => {
- jest.useFakeTimers();
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
-
- Object.assign(navigator, {
- clipboard: {
- writeText: jest.fn().mockResolvedValue(undefined),
- },
- });
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('exportComponentAsJson does nothing without selected element', async () => {
- app.selectedElement = undefined;
- await app.exportComponentAsJson();
-
- expect(navigator.clipboard.writeText).not.toHaveBeenCalled();
- });
-
- it('exportComponentAsJson copies JSON to clipboard', async () => {
- app.selectedElement = {
- name: 'my-component',
- key: 'my-key',
- aliases: [],
- bindables: [{ name: 'count', value: 5, type: 'number' }],
- properties: [],
- };
- app.selectedNodeType = 'custom-element';
- app.selectedElementAttributes = [];
-
- await app.exportComponentAsJson();
-
- expect(navigator.clipboard.writeText).toHaveBeenCalled();
- const calledWith = (navigator.clipboard.writeText as jest.Mock).mock.calls[0][0];
- const parsed = JSON.parse(calledWith);
- expect(parsed.meta.name).toBe('my-component');
- expect(parsed.bindables.count.value).toBe(5);
- });
-
- it('isExportCopied returns correct state', async () => {
- app.selectedElement = { name: 'comp', bindables: [], properties: [] };
- app.selectedElementAttributes = [];
-
- expect(app.isExportCopied).toBe(false);
-
- await app.exportComponentAsJson();
- expect(app.isExportCopied).toBe(true);
-
- jest.advanceTimersByTime(1600);
- expect(app.isExportCopied).toBe(false);
- });
-});
-
-describe('App component refresh', () => {
- let app: any;
- let debugHost: any;
-
- beforeEach(async () => {
- jest.useFakeTimers();
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- debugHost = result.debugHost;
- debugHost.getAllComponents.mockResolvedValue({ tree: [], flat: [] });
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('refreshComponents sets isRefreshing during load', async () => {
- expect(app.isRefreshing).toBe(false);
-
- app.refreshComponents();
- expect(app.isRefreshing).toBe(true);
-
- jest.runAllTimers();
- await Promise.resolve();
- await Promise.resolve();
- jest.runAllTimers();
-
- expect(app.isRefreshing).toBe(false);
- });
-
- it('refreshComponents does not run if already refreshing', () => {
- app.isRefreshing = true;
- app.refreshComponents();
-
- expect(debugHost.getAllComponents).not.toHaveBeenCalled();
- });
-});
diff --git a/tests/app-utils.spec.ts b/tests/app-utils.spec.ts
deleted file mode 100644
index 33c85ec..0000000
--- a/tests/app-utils.spec.ts
+++ /dev/null
@@ -1,610 +0,0 @@
-import './setup';
-import { ChromeTest } from './setup';
-import { stubDebugHost, stubPlatform } from './helpers';
-
-let AppClass: any;
-
-async function createApp() {
- jest.resetModules();
- const mod = await import('@/app');
- AppClass = mod.App;
- const app = Object.create(AppClass.prototype);
-
- app.coreTabs = [
- { id: 'all', label: 'All', icon: '🌲', kind: 'core' },
- { id: 'components', label: 'Components', icon: '📦', kind: 'core' },
- { id: 'attributes', label: 'Attributes', icon: '🔧', kind: 'core' },
- { id: 'interactions', label: 'Interactions', icon: '⏱️', kind: 'core' },
- ];
- app.activeTab = 'all';
- app.tabs = [...app.coreTabs];
- app.externalTabs = [];
- app.externalPanels = {};
- app.externalPanelsVersion = 0;
- app.externalPanelLoading = {};
- app.externalRefreshHandle = null;
- app.selectedElement = undefined;
- app.selectedElementAttributes = undefined;
- app.allAureliaObjects = undefined;
- app.componentTree = [];
- app.componentSnapshot = { tree: [], flat: [] };
- app.selectedComponentId = undefined;
- app.selectedBreadcrumb = [];
- app.selectedNodeType = 'custom-element';
- app.searchQuery = '';
- app.searchMode = 'name';
- app.viewMode = 'tree';
- app.isElementPickerActive = false;
- app.interactionLog = [];
- app.interactionLoading = false;
- app.interactionError = null;
- app.aureliaDetected = false;
- app.aureliaVersion = null;
- app.detectionState = 'checking';
- app.isRefreshing = false;
- app.expressionInput = '';
- app.expressionResult = '';
- app.expressionResultType = '';
- app.expressionError = '';
- app.expressionHistory = [];
- app.isExpressionPanelOpen = false;
- app.copiedPropertyId = null;
- app.propertyRowsRevision = 0;
- app.followChromeSelection = true;
-
- const debugHost = stubDebugHost();
- const plat = stubPlatform();
- app.debugHost = debugHost;
- app.plat = plat;
-
- return { app, debugHost, plat };
-}
-
-describe('App view mode management', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('toggleViewMode switches between tree and list', () => {
- expect(app.viewMode).toBe('tree');
- app.toggleViewMode();
- expect(app.viewMode).toBe('list');
- app.toggleViewMode();
- expect(app.viewMode).toBe('tree');
- });
-
- it('setViewMode sets mode and persists to localStorage', () => {
- const setItem = jest.spyOn(window.localStorage.__proto__, 'setItem');
- app.setViewMode('list');
- expect(app.viewMode).toBe('list');
- expect(setItem).toHaveBeenCalledWith('au-devtools.viewMode', 'list');
- setItem.mockRestore();
- });
-
- it('setViewMode does nothing when mode is same', () => {
- const setItem = jest.spyOn(window.localStorage.__proto__, 'setItem');
- app.setViewMode('tree');
- expect(setItem).not.toHaveBeenCalled();
- setItem.mockRestore();
- });
-});
-
-describe('App search mode management', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('setSearchMode changes search mode', () => {
- expect(app.searchMode).toBe('name');
- app.setSearchMode('property');
- expect(app.searchMode).toBe('property');
- app.setSearchMode('all');
- expect(app.searchMode).toBe('all');
- });
-
- it('cycleSearchMode cycles through modes', () => {
- expect(app.searchMode).toBe('name');
- app.cycleSearchMode();
- expect(app.searchMode).toBe('property');
- app.cycleSearchMode();
- expect(app.searchMode).toBe('all');
- app.cycleSearchMode();
- expect(app.searchMode).toBe('name');
- });
-
- it('searchModeLabel returns correct labels', () => {
- app.searchMode = 'name';
- expect(app.searchModeLabel).toBe('Name');
- app.searchMode = 'property';
- expect(app.searchModeLabel).toBe('Props');
- app.searchMode = 'all';
- expect(app.searchModeLabel).toBe('All');
- });
-
- it('searchPlaceholder returns correct placeholders', () => {
- app.searchMode = 'name';
- expect(app.searchPlaceholder).toBe('Search components...');
- app.searchMode = 'property';
- expect(app.searchPlaceholder).toBe('Search property values...');
- app.searchMode = 'all';
- expect(app.searchPlaceholder).toBe('Search names & properties...');
- });
-});
-
-describe('App expression panel', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('toggleExpressionPanel toggles panel state', () => {
- expect(app.isExpressionPanelOpen).toBe(false);
- app.toggleExpressionPanel();
- expect(app.isExpressionPanelOpen).toBe(true);
- app.toggleExpressionPanel();
- expect(app.isExpressionPanelOpen).toBe(false);
- });
-
- it('clearExpressionResult clears all expression state', () => {
- app.expressionResult = 'test result';
- app.expressionResultType = 'string';
- app.expressionError = 'some error';
- app.clearExpressionResult();
- expect(app.expressionResult).toBe('');
- expect(app.expressionResultType).toBe('');
- expect(app.expressionError).toBe('');
- });
-
- it('selectHistoryExpression sets input', () => {
- app.selectHistoryExpression('this.count');
- expect(app.expressionInput).toBe('this.count');
- });
-});
-
-describe('App flat list conversion', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('convertFlatListToTreeNodes handles empty input', () => {
- const result = app.convertFlatListToTreeNodes([]);
- expect(result).toEqual([]);
- });
-
- it('convertFlatListToTreeNodes handles null input', () => {
- const result = app.convertFlatListToTreeNodes(null);
- expect(result).toEqual([]);
- });
-
- it('convertFlatListToTreeNodes creates nodes from elements', () => {
- const flat = [
- { customElementInfo: { name: 'comp-a', key: 'comp-a', bindables: [], properties: [], aliases: [] }, customAttributesInfo: [] },
- { customElementInfo: { name: 'comp-b', key: 'comp-b', bindables: [], properties: [], aliases: [] }, customAttributesInfo: [] },
- ];
- const result = app.convertFlatListToTreeNodes(flat);
- expect(result).toHaveLength(2);
- expect(result[0].customElementInfo.name).toBe('comp-a');
- expect(result[1].customElementInfo.name).toBe('comp-b');
- });
-
- it('convertFlatListToTreeNodes deduplicates by key', () => {
- const flat = [
- { customElementInfo: { name: 'comp-a', key: 'same-key', bindables: [], properties: [], aliases: [] }, customAttributesInfo: [] },
- { customElementInfo: { name: 'comp-a', key: 'same-key', bindables: [], properties: [], aliases: [] }, customAttributesInfo: [] },
- ];
- const result = app.convertFlatListToTreeNodes(flat);
- expect(result).toHaveLength(1);
- });
-
- it('convertFlatListToTreeNodes creates attribute-only nodes', () => {
- const flat = [
- { customElementInfo: null, customAttributesInfo: [{ name: 'draggable', key: 'draggable', bindables: [], properties: [], aliases: [] }] },
- ];
- const result = app.convertFlatListToTreeNodes(flat);
- expect(result).toHaveLength(1);
- expect(result[0].customElementInfo).toBeNull();
- expect(result[0].customAttributesInfo).toHaveLength(1);
- });
-});
-
-describe('App property value search', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('propertyValueToSearchString handles null', () => {
- expect(app.propertyValueToSearchString(null)).toBe('null');
- });
-
- it('propertyValueToSearchString handles undefined', () => {
- expect(app.propertyValueToSearchString(undefined)).toBe('undefined');
- });
-
- it('propertyValueToSearchString handles strings', () => {
- expect(app.propertyValueToSearchString('hello')).toBe('hello');
- });
-
- it('propertyValueToSearchString handles numbers', () => {
- expect(app.propertyValueToSearchString(42)).toBe('42');
- });
-
- it('propertyValueToSearchString handles booleans', () => {
- expect(app.propertyValueToSearchString(true)).toBe('true');
- expect(app.propertyValueToSearchString(false)).toBe('false');
- });
-
- it('propertyValueToSearchString handles arrays', () => {
- expect(app.propertyValueToSearchString([1, 2, 3])).toBe('[3 items]');
- });
-
- it('propertyValueToSearchString handles objects', () => {
- const result = app.propertyValueToSearchString({ foo: 'bar' });
- expect(result).toContain('foo');
- expect(result).toContain('bar');
- });
-
- it('searchInProperties finds property by name', () => {
- const info = {
- bindables: [{ name: 'searchableValue', value: 'test', type: 'string' }],
- properties: [],
- };
- const result = app.searchInProperties(info, 'searchable');
- expect(result).toBe('searchableValue');
- });
-
- it('searchInProperties finds property by value', () => {
- const info = {
- bindables: [],
- properties: [{ name: 'message', value: 'hello world', type: 'string' }],
- };
- const result = app.searchInProperties(info, 'hello');
- expect(result).toBe('message');
- });
-
- it('searchInProperties returns null when not found', () => {
- const info = {
- bindables: [{ name: 'count', value: 10, type: 'number' }],
- properties: [],
- };
- const result = app.searchInProperties(info, 'nonexistent');
- expect(result).toBeNull();
- });
-});
-
-describe('App breadcrumb and node count', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('breadcrumbSegments returns empty for no selection', () => {
- app.selectedBreadcrumb = [];
- expect(app.breadcrumbSegments).toEqual([]);
- });
-
- it('breadcrumbSegments formats element and attribute labels', () => {
- app.selectedBreadcrumb = [
- { id: 'e1', name: 'my-element', type: 'custom-element' },
- { id: 'a1', name: 'my-attr', type: 'custom-attribute' },
- ];
- const segments = app.breadcrumbSegments;
- expect(segments).toHaveLength(2);
- expect(segments[0].label).toBe('');
- expect(segments[1].label).toBe('@my-attr');
- });
-
- it('totalComponentNodeCount counts all nodes recursively', () => {
- app.componentTree = [
- {
- id: 'root',
- name: 'root',
- children: [
- { id: 'child1', name: 'child1', children: [] },
- { id: 'child2', name: 'child2', children: [
- { id: 'grandchild', name: 'grandchild', children: [] }
- ] },
- ],
- },
- ];
- expect(app.totalComponentNodeCount).toBe(4);
- });
-
- it('totalComponentNodeCount returns 0 for empty tree', () => {
- app.componentTree = [];
- expect(app.totalComponentNodeCount).toBe(0);
- });
-});
-
-describe('App external panel helpers', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('isExternalTabActive returns false for core tabs', () => {
- app.activeTab = 'all';
- expect(app.isExternalTabActive).toBe(false);
- app.activeTab = 'components';
- expect(app.isExternalTabActive).toBe(false);
- });
-
- it('isExternalTabActive returns true for external tabs', () => {
- app.externalTabs = [{ id: 'external:store', panelId: 'store' }];
- app.activeTab = 'external:store';
- expect(app.isExternalTabActive).toBe(true);
- });
-
- it('activeExternalIcon returns default when no panel', () => {
- app.activeTab = 'all';
- expect(app.activeExternalIcon).toBe('🧩');
- });
-
- it('activeExternalTitle returns default when no panel', () => {
- app.activeTab = 'all';
- expect(app.activeExternalTitle).toBe('Inspector');
- });
-
- it('activeExternalError returns null for non-error panels', () => {
- app.externalTabs = [{ id: 'external:store', panelId: 'store' }];
- app.externalPanels = { store: { id: 'store', status: 'ready' } };
- app.activeTab = 'external:store';
- expect(app.activeExternalError).toBeNull();
- });
-
- it('activeExternalError returns error message for errored panels', () => {
- app.externalTabs = [{ id: 'external:store', panelId: 'store' }];
- app.externalPanels = { store: { id: 'store', status: 'error', error: 'Failed to load' } };
- app.activeTab = 'external:store';
- expect(app.activeExternalError).toBe('Failed to load');
- });
-});
-
-describe('App interaction helpers', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('hasInteractions returns false for empty log', () => {
- app.interactionLog = [];
- expect(app.hasInteractions).toBe(false);
- });
-
- it('hasInteractions returns true for non-empty log', () => {
- app.interactionLog = [{ id: 'evt-1' }];
- expect(app.hasInteractions).toBe(true);
- });
-
- it('formattedInteractionLog returns log or empty array', () => {
- app.interactionLog = [{ id: 'evt-1' }];
- expect(app.formattedInteractionLog).toEqual([{ id: 'evt-1' }]);
- app.interactionLog = null;
- expect(app.formattedInteractionLog).toEqual([]);
- });
-});
-
-describe('App property rows', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('getPropertyRows returns empty for no properties', () => {
- expect(app.getPropertyRows([])).toEqual([]);
- expect(app.getPropertyRows(null)).toEqual([]);
- expect(app.getPropertyRows(undefined)).toEqual([]);
- });
-
- it('getPropertyRows flattens properties with depth', () => {
- const props = [
- { name: 'a', value: 1 },
- { name: 'b', value: 2 },
- ];
- const rows = app.getPropertyRows(props);
- expect(rows).toHaveLength(2);
- expect(rows[0].property.name).toBe('a');
- expect(rows[0].depth).toBe(0);
- });
-
- it('getPropertyRows includes expanded nested properties', () => {
- const props = [
- {
- name: 'parent',
- value: {},
- isExpanded: true,
- expandedValue: {
- properties: [
- { name: 'child', value: 'nested' }
- ]
- }
- }
- ];
- const rows = app.getPropertyRows(props);
- expect(rows).toHaveLength(2);
- expect(rows[0].depth).toBe(0);
- expect(rows[1].property.name).toBe('child');
- expect(rows[1].depth).toBe(1);
- });
-});
-
-describe('App visible component nodes', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('visibleComponentNodes returns empty for no tree', () => {
- app.componentTree = [];
- expect(app.visibleComponentNodes).toEqual([]);
- });
-
- it('visibleComponentNodes includes all in list mode', () => {
- app.viewMode = 'list';
- app.componentTree = [
- {
- id: 'root',
- name: 'root',
- type: 'custom-element',
- expanded: false,
- children: [{ id: 'child', name: 'child', type: 'custom-element', children: [] }]
- }
- ];
- const nodes = app.visibleComponentNodes;
- expect(nodes).toHaveLength(2);
- });
-
- it('visibleComponentNodes respects expanded state in tree mode', () => {
- app.viewMode = 'tree';
- app.componentTree = [
- {
- id: 'root',
- name: 'root',
- type: 'custom-element',
- expanded: false,
- children: [{ id: 'child', name: 'child', type: 'custom-element', children: [] }]
- }
- ];
- let nodes = app.visibleComponentNodes;
- expect(nodes).toHaveLength(1);
-
- app.componentTree[0].expanded = true;
- nodes = app.visibleComponentNodes;
- expect(nodes).toHaveLength(2);
- });
-
- it('visibleComponentNodes expands all during search', () => {
- app.viewMode = 'tree';
- app.searchQuery = 'child';
- app.componentTree = [
- {
- id: 'root',
- name: 'root',
- type: 'custom-element',
- expanded: false,
- children: [{ id: 'child', name: 'child', type: 'custom-element', children: [] }]
- }
- ];
- const nodes = app.visibleComponentNodes;
- expect(nodes).toHaveLength(2);
- });
-});
-
-describe('App filter tree by type', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('filterTreeByType filters elements only', () => {
- const tree = [
- { id: 'e1', type: 'custom-element', children: [
- { id: 'a1', type: 'custom-attribute', children: [] }
- ] }
- ];
- const filtered = app.filterTreeByType(tree, 'custom-element');
- expect(filtered).toHaveLength(1);
- expect(filtered[0].type).toBe('custom-element');
- expect(filtered[0].children).toHaveLength(0);
- });
-
- it('filterTreeByType filters attributes only', () => {
- const tree = [
- { id: 'e1', type: 'custom-element', children: [
- { id: 'a1', type: 'custom-attribute', children: [] }
- ] }
- ];
- const filtered = app.filterTreeByType(tree, 'custom-attribute');
- expect(filtered).toHaveLength(1);
- expect(filtered[0].type).toBe('custom-attribute');
- });
-
- it('filterTreeByType lifts nested matching types', () => {
- const tree = [
- { id: 'e1', type: 'custom-element', children: [
- { id: 'e2', type: 'custom-element', children: [
- { id: 'a1', type: 'custom-attribute', children: [] }
- ] }
- ] }
- ];
- const filtered = app.filterTreeByType(tree, 'custom-attribute');
- expect(filtered).toHaveLength(1);
- expect(filtered[0].id).toBe('a1');
- });
-});
-
-describe('App isSameControllerInfo', () => {
- let app: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- const result = await createApp();
- app = result.app;
- });
-
- it('returns false for null inputs', () => {
- expect(app.isSameControllerInfo(null, null)).toBe(false);
- expect(app.isSameControllerInfo({ name: 'a' }, null)).toBe(false);
- expect(app.isSameControllerInfo(null, { name: 'b' })).toBe(false);
- });
-
- it('matches by key', () => {
- const a = { key: 'comp-key', name: 'comp' };
- const b = { key: 'comp-key', name: 'different' };
- expect(app.isSameControllerInfo(a, b)).toBe(true);
- });
-
- it('matches by name when keys missing', () => {
- const a = { name: 'comp' };
- const b = { name: 'comp' };
- expect(app.isSameControllerInfo(a, b)).toBe(true);
- });
-
- it('matches by alias', () => {
- const a = { name: 'comp-a', aliases: ['alias-shared'] };
- const b = { name: 'comp-b', aliases: ['alias-shared'] };
- expect(app.isSameControllerInfo(a, b)).toBe(true);
- });
-
- it('does not match different controllers', () => {
- const a = { key: 'key-a', name: 'comp-a' };
- const b = { key: 'key-b', name: 'comp-b' };
- expect(app.isSameControllerInfo(a, b)).toBe(false);
- });
-});
diff --git a/tests/app.spec.ts b/tests/app.spec.ts
deleted file mode 100644
index efb5589..0000000
--- a/tests/app.spec.ts
+++ /dev/null
@@ -1,592 +0,0 @@
-import './setup';
-import { ChromeTest } from './setup';
-// We instantiate App without running field initializers to avoid DI
-let AppClass: any;
-import { stubDebugHost, stubPlatform, nextTick } from './helpers';
-import type { AureliaComponentTreeNode, AureliaInfo, IControllerInfo, Property } from '@/shared/types';
-
-function ci(name: string, key?: string): IControllerInfo {
- return {
- name,
- key: key ?? name,
- aliases: [],
- bindables: [],
- properties: []
- } as any;
-}
-
-function ai(element: IControllerInfo | null, attrs: IControllerInfo[] = []): AureliaInfo {
- return {
- customElementInfo: element as any,
- customAttributesInfo: attrs as any
- };
-}
-
-describe('App core logic', () => {
- let app: App;
- let debugHost: any;
- let plat: any;
-
- beforeEach(async () => {
- ChromeTest.reset();
- jest.resetModules();
- // Dynamically import App class
- const mod = await import('@/app');
- AppClass = mod.App;
- // Create instance without running field initializers (avoid DI resolve calls)
- app = Object.create(AppClass.prototype);
- // Seed essential fields
- (app as any).coreTabs = [
- { id: 'all', label: 'All', icon: '🌲', kind: 'core' },
- { id: 'components', label: 'Components', icon: '📦', kind: 'core' },
- { id: 'attributes', label: 'Attributes', icon: '🔧', kind: 'core' },
- { id: 'interactions', label: 'Interactions', icon: '⏱️', kind: 'core' },
- ];
- (app as any).activeTab = 'all';
- (app as any).tabs = [...(app as any).coreTabs];
- (app as any).externalTabs = [];
- (app as any).externalPanels = {};
- (app as any).externalPanelsVersion = 0;
- (app as any).externalPanelLoading = {};
- (app as any).externalRefreshHandle = null;
- (app as any).selectedElement = undefined;
- (app as any).selectedElementAttributes = undefined;
- (app as any).allAureliaObjects = undefined;
- (app as any).componentTree = [];
- (app as any).componentSnapshot = { tree: [], flat: [] };
- (app as any).selectedComponentId = undefined;
- (app as any).selectedBreadcrumb = [];
- (app as any).selectedNodeType = 'custom-element';
- (app as any).searchQuery = '';
- (app as any).searchMode = 'name';
- (app as any).viewMode = 'tree';
- (app as any).isElementPickerActive = false;
- (app as any).interactionLog = [];
- (app as any).interactionLoading = false;
- (app as any).interactionError = null;
- (app as any).aureliaDetected = false;
- (app as any).aureliaVersion = null;
- (app as any).detectionState = 'checking';
-
- debugHost = stubDebugHost();
- plat = stubPlatform();
- // Inject stubs
- (app as any).debugHost = debugHost;
- (app as any).plat = plat;
- });
-
- function rawNode(
- id: string,
- element: IControllerInfo | null,
- attrs: IControllerInfo[] = [],
- children: AureliaComponentTreeNode[] = [],
- domPath: string = ''
- ): AureliaComponentTreeNode {
- return {
- id,
- domPath,
- tagName: element?.name ?? null,
- customElementInfo: element,
- customAttributesInfo: attrs,
- children,
- };
- }
-
- function applyTree(nodes: AureliaComponentTreeNode[]) {
- (app as any).handleComponentSnapshot({ tree: nodes, flat: [] });
- return (app as any).componentTree as any[];
- }
-
- describe('handleComponentSnapshot', () => {
- it('builds nodes, filters duplicate attrs by key/name, sets hasAttributes and sorts', () => {
- const element = ci('todo-item', 'todo-item');
- const dupAttr = ci('todo-item', 'todo-item');
- const realAttr = ci('selectable', 'selectable');
-
- const tree = applyTree([rawNode('todo', element, [dupAttr, realAttr])]);
-
- expect(tree).toHaveLength(1);
- const node = tree[0];
- expect(node.type).toBe('custom-element');
- expect(node.hasAttributes).toBe(true);
- expect(node.children).toHaveLength(1);
- expect(node.children[0].type).toBe('custom-attribute');
- expect(node.children[0].name).toBe('selectable');
- });
-
- it('lifts standalone custom attributes without parent element', () => {
- const attrInfo = ci('draggable');
- const tree = applyTree([rawNode('attr-only', null as any, [attrInfo])]);
-
- expect(tree).toHaveLength(1);
- expect(tree[0].type).toBe('custom-attribute');
- expect(tree[0].name).toBe('draggable');
- });
- });
-
- describe('tab filtering and search filtering', () => {
- function seedTree() {
- return applyTree([
- rawNode('alpha', ci('alpha'), [ci('attr-a'), ci('attr-b')]),
- rawNode('beta', ci('beta'), []),
- ]);
- }
-
- function nodesAreType(nodes: any[], type: 'custom-element' | 'custom-attribute'): boolean {
- return nodes.every((node) => node.type === type && nodesAreType(node.children || [], type));
- }
-
- it('filteredComponentTreeByTab returns only elements for components tab', () => {
- seedTree();
- app.activeTab = 'components';
- const filtered = app.filteredComponentTreeByTab;
- expect(nodesAreType(filtered, 'custom-element')).toBe(true);
- });
-
- it('filteredComponentTreeByTab returns only attributes for attributes tab', () => {
- seedTree();
- app.activeTab = 'attributes';
- const filtered = app.filteredComponentTreeByTab;
- expect(nodesAreType(filtered, 'custom-attribute')).toBe(true);
- });
-
- it('search returns parents of matching descendants and auto-expands them', () => {
- seedTree();
- app.activeTab = 'all';
- app.searchQuery = 'attr-b';
- const filtered = app.filteredComponentTreeByTab;
- // There should be an element parent containing the matching attribute child
- const parent = filtered.find(n => n.type === 'custom-element');
- expect(parent).toBeTruthy();
- expect(parent.expanded).toBe(true);
- expect(parent.children.some((c: any) => c.name.toLowerCase().includes('attr-b'))).toBe(true);
- });
-
- it('clearSearch resets query and returns full tree', () => {
- seedTree();
- app.searchQuery = 'beta';
- expect(app.filteredComponentTreeByTab.length).toBeGreaterThan(0);
- app.clearSearch();
- expect(app.searchQuery).toBe('');
- expect(app.filteredComponentTreeByTab.length).toBeGreaterThan(0);
- });
- });
-
- describe('selection and expansion', () => {
- it('selectComponent sets selectedElement and filters attrs for element node', () => {
- const element = ci('alpha', 'alpha');
- const dupAttr = ci('alpha', 'alpha');
- const otherAttr = ci('x', 'x');
- const tree = applyTree([rawNode('alpha', element, [dupAttr, otherAttr])]);
-
- app.selectComponent(tree[0].id);
- expect(app.selectedElement?.name).toBe('alpha');
- expect(app.selectedElementAttributes?.length).toBe(1);
- expect(app.selectedElementAttributes?.[0].name).toBe('x');
- });
-
- it('selectComponent sets selectedElement to attribute and clears attributes list for attribute node', () => {
- const element = ci('alpha');
- const otherAttr = ci('x');
- const tree = applyTree([rawNode('alpha', element, [otherAttr])]);
- const attrNode = tree[0].children[0];
-
- app.selectComponent(attrNode.id);
- expect(app.selectedElement?.name).toBe('x');
- expect(app.selectedElementAttributes).toEqual([]);
- });
-
- it('toggleComponentExpansion toggles expanded state', () => {
- const tree = applyTree([rawNode('alpha', ci('alpha'))]);
- const id = tree[0].id;
- expect(tree[0].expanded).toBe(false);
- app.toggleComponentExpansion(id);
- expect(((app as any).findComponentById(id).expanded)).toBe(true);
- });
-
- it('onElementPicked selects component using DOM path when provided', () => {
- const tree = applyTree([
- rawNode('root', ci('root'), [], [
- rawNode('child', ci('child'), [], [], 'html > body > child')
- ], 'html > body > root')
- ]);
-
- const pickInfo = {
- customElementInfo: ci('child'),
- customAttributesInfo: [],
- __auDevtoolsDomPath: 'html > body > child'
- } as any;
-
- app.onElementPicked(pickInfo);
-
- expect(app.selectedComponentId).toBe('child');
- expect(app.selectedElement?.name).toBe('child');
- });
-
- it('handlePropertyRowClick toggles expansion when clicking label area', () => {
- const prop: any = { canExpand: true, isExpanded: false };
- const event: any = {
- target: document.createElement('span'),
- stopPropagation: jest.fn(),
- };
- const spy = jest.spyOn(app, 'togglePropertyExpansion');
- (event.target as HTMLElement).classList.add('property-name');
-
- app.handlePropertyRowClick(prop, event);
-
- expect(spy).toHaveBeenCalledWith(prop);
- spy.mockRestore();
- });
-
- it('handlePropertyRowClick ignores clicks in value wrapper', () => {
- const prop: any = { canExpand: true, isExpanded: false };
- const valueEl = document.createElement('span');
- valueEl.className = 'property-value-wrapper';
- const event: any = {
- target: valueEl,
- stopPropagation: jest.fn(),
- };
- const spy = jest.spyOn(app, 'togglePropertyExpansion');
-
- app.handlePropertyRowClick(prop, event);
-
- expect(spy).not.toHaveBeenCalled();
- spy.mockRestore();
- });
- });
-
- describe('external panel integration', () => {
- it('refreshExternalPanels merges snapshot into tabs', async () => {
- const snapshot = {
- version: 1,
- panels: [{ id: 'store', label: 'Store Debugger', icon: '🧠', summary: 'hello' }],
- };
- debugHost.getExternalPanelsSnapshot.mockResolvedValue(snapshot);
-
- await app.refreshExternalPanels(true);
-
- expect(app.tabs.some((tab: any) => tab.id === 'external:store')).toBe(true);
- expect((app as any).externalPanels.store.summary).toBe('hello');
- });
-
- it('switchTab triggers external refresh helpers', () => {
- const spy = jest.spyOn(app, 'refreshExternalPanels').mockResolvedValue(undefined as any);
- (app as any).externalTabs = [{ id: 'external:store', label: 'Store', icon: '🧩', kind: 'external', panelId: 'store' }];
- (app as any).tabs = [...(app as any).coreTabs, ...(app as any).externalTabs];
-
- app.switchTab('external:store');
-
- expect(spy).toHaveBeenCalledWith(true);
- });
-
- it('refreshActiveExternalTab emits request event', () => {
- (app as any).externalTabs = [{ id: 'external:store', label: 'Store', icon: '🧩', kind: 'external', panelId: 'store' }];
- (app as any).tabs = [...(app as any).coreTabs, ...(app as any).externalTabs];
- debugHost.emitExternalPanelEvent.mockResolvedValue(true);
- app.activeTab = 'external:store';
-
- app.refreshActiveExternalTab();
-
- expect(debugHost.emitExternalPanelEvent).toHaveBeenCalledWith(
- 'aurelia-devtools:request-panel',
- expect.objectContaining({ id: 'store' })
- );
- });
-
- it('applySelectionFromNode notifies external panels when active', () => {
- const node: any = {
- id: 'alpha',
- type: 'custom-element',
- domPath: 'html > body:nth-of-type(1)',
- children: [],
- expanded: false,
- hasAttributes: false,
- name: 'alpha',
- tagName: 'div',
- data: { kind: 'element', info: ai(ci('alpha')), raw: null },
- };
- app.activeTab = 'external:store';
-
- (app as any).applySelectionFromNode(node);
-
- expect(debugHost.emitExternalPanelEvent).toHaveBeenCalledWith(
- 'aurelia-devtools:selection-changed',
- expect.objectContaining({ selectedComponentId: 'alpha' })
- );
- });
- });
-
- describe('interaction timeline', () => {
- it('switchTab to interactions triggers log load', () => {
- const spy = jest.spyOn(app, 'loadInteractionLog').mockResolvedValue(undefined as any);
-
- app.switchTab('interactions');
-
- expect(spy).toHaveBeenCalledWith(true);
- spy.mockRestore();
- });
-
- it('applyInteractionSnapshot forwards phase', async () => {
- await app.applyInteractionSnapshot('evt-2', 'before');
- expect(debugHost.applyInteractionSnapshot).toHaveBeenCalledWith('evt-2', 'before');
- });
-
- it('clearInteractionLog clears and reloads', async () => {
- const loadSpy = jest.spyOn(app, 'loadInteractionLog').mockResolvedValue(undefined as any);
-
- await app.clearInteractionLog();
-
- expect(debugHost.clearInteractionLog).toHaveBeenCalled();
- // We clear optimistically; reload is best-effort so only assert the host call
- loadSpy.mockRestore();
- });
- });
-
- describe('property editing and expansion', () => {
- beforeEach(() => {
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- function makeProp(type: Property['type'], value: any): any {
- return { type, value, isEditing: true } as any;
- }
-
- it('saveProperty converts number and calls updateValues', async () => {
- const prop = makeProp('number', 1);
- (app as any).selectedElement = { properties: [prop], bindables: [] };
-
- app.saveProperty(prop, '42');
- // queueMicrotask is implemented with setTimeout(0) in stubPlatform
- jest.runOnlyPendingTimers();
-
- expect(prop.value).toBe(42);
- expect(prop.isEditing).toBe(false);
- expect(debugHost.updateValues).toHaveBeenCalled();
- });
-
- it('saveProperty handles invalid number by revert', () => {
- const original = 5;
- const prop = { type: 'number', value: original, isEditing: true, originalValue: original } as any;
- (app as any).selectedElement = { properties: [prop], bindables: [] };
-
- app.saveProperty(prop, 'not-a-number');
- expect(prop.value).toBe(original);
- expect(prop.isEditing).toBe(false);
- });
-
- it('saveProperty converts boolean', () => {
- const prop = makeProp('boolean', false);
- (app as any).selectedElement = { properties: [prop], bindables: [] };
- app.saveProperty(prop, 'true');
- jest.runOnlyPendingTimers();
- expect(prop.value).toBe(true);
- });
-
- it('togglePropertyExpansion loads expanded value via inspectedWindow.eval', async () => {
- const prop: any = { type: 'object', value: {}, canExpand: true, isExpanded: false, debugId: 7 };
- (app as any).selectedElement = { properties: [prop], bindables: [] };
-
- ChromeTest.setEvalToReturn([{ result: { properties: [{ name: 'child', type: 'string', value: 'x' }] } }]);
-
- app.togglePropertyExpansion(prop);
- // The eval callback is synchronous in our mock; microtask used to reassign references
- jest.runOnlyPendingTimers();
-
- expect(prop.isExpanded).toBe(true);
- expect(prop.expandedValue).toBeTruthy();
- expect(((app as any).selectedElement.properties)[0]).toEqual(expect.objectContaining({ isExpanded: true }));
- });
- });
-
- describe('highlighting delegates to debugHost', () => {
- it('highlightComponent forwards minimal payload', () => {
- const tree = applyTree([rawNode('alpha', ci('alpha'), [ci('x')])]);
- app.highlightComponent(tree[0]);
- expect(debugHost.highlightComponent).toHaveBeenCalledWith(expect.objectContaining({
- name: 'alpha',
- type: 'custom-element',
- customElementInfo: expect.objectContaining({ name: 'alpha' }),
- }));
- });
-
- it('unhighlightComponent forwards to debugHost', () => {
- app.unhighlightComponent();
- expect(debugHost.unhighlightComponent).toHaveBeenCalled();
- });
- });
-
- describe('property watching and reactivity', () => {
- function rawNode(
- id: string,
- element: IControllerInfo | null,
- attrs: IControllerInfo[] = [],
- children: AureliaComponentTreeNode[] = [],
- domPath: string = ''
- ): AureliaComponentTreeNode {
- return {
- id,
- domPath,
- tagName: element?.name ?? null,
- customElementInfo: element,
- customAttributesInfo: attrs,
- children,
- };
- }
-
- function applyTree(nodes: AureliaComponentTreeNode[]) {
- (app as any).handleComponentSnapshot({ tree: nodes, flat: [] });
- return (app as any).componentTree as any[];
- }
-
- it('selectComponent starts property watching with component key', () => {
- const tree = applyTree([rawNode('alpha', ci('alpha', 'alpha-key'))]);
- app.selectComponent(tree[0].id);
-
- expect(debugHost.startPropertyWatching).toHaveBeenCalledWith({
- componentKey: 'alpha-key',
- pollInterval: 500,
- });
- });
-
- it('selectComponent falls back to name if key not available', () => {
- const element = ci('my-element');
- delete (element as any).key;
- const tree = applyTree([rawNode('beta', element)]);
-
- app.selectComponent(tree[0].id);
-
- expect(debugHost.startPropertyWatching).toHaveBeenCalledWith({
- componentKey: 'my-element',
- pollInterval: 500,
- });
- });
-
- it('handleComponentSnapshot stops property watching when selected node is removed', () => {
- const tree = applyTree([rawNode('alpha', ci('alpha'))]);
- app.selectComponent(tree[0].id);
-
- // Clear the component tree so selected component no longer exists
- (app as any).handleComponentSnapshot({ tree: [], flat: [] });
-
- expect(debugHost.stopPropertyWatching).toHaveBeenCalled();
- expect(app.selectedComponentId).toBeUndefined();
- });
-
- it('onPropertyChanges updates bindable values', () => {
- const bindable: any = { name: 'count', value: 0, type: 'number' };
- (app as any).selectedElement = {
- key: 'test-key',
- name: 'test',
- bindables: [bindable],
- properties: [],
- };
-
- const changes = [
- {
- componentKey: 'test-key',
- propertyName: 'count',
- propertyType: 'bindable',
- oldValue: 0,
- newValue: 5,
- timestamp: Date.now(),
- },
- ];
-
- const snapshot = {
- componentKey: 'test-key',
- bindables: [{ name: 'count', value: 5, type: 'number' }],
- properties: [],
- timestamp: Date.now(),
- };
-
- app.onPropertyChanges(changes as any, snapshot as any);
-
- expect(bindable.value).toBe(5);
- });
-
- it('onPropertyChanges updates property values', () => {
- const property: any = { name: 'message', value: 'old', type: 'string' };
- (app as any).selectedElement = {
- key: 'test-key',
- name: 'test',
- bindables: [],
- properties: [property],
- };
-
- const changes = [
- {
- componentKey: 'test-key',
- propertyName: 'message',
- propertyType: 'property',
- oldValue: 'old',
- newValue: 'new',
- timestamp: Date.now(),
- },
- ];
-
- const snapshot = {
- componentKey: 'test-key',
- bindables: [],
- properties: [{ name: 'message', value: 'new', type: 'string' }],
- timestamp: Date.now(),
- };
-
- app.onPropertyChanges(changes as any, snapshot as any);
-
- expect(property.value).toBe('new');
- });
-
- it('onPropertyChanges ignores changes for different component', () => {
- const property: any = { name: 'message', value: 'original', type: 'string' };
- (app as any).selectedElement = {
- key: 'selected-key',
- name: 'selected',
- bindables: [],
- properties: [property],
- };
-
- const changes = [
- {
- componentKey: 'different-key',
- propertyName: 'message',
- propertyType: 'property',
- oldValue: 'old',
- newValue: 'new',
- timestamp: Date.now(),
- },
- ];
-
- const snapshot = {
- componentKey: 'different-key',
- bindables: [],
- properties: [{ name: 'message', value: 'new', type: 'string' }],
- timestamp: Date.now(),
- };
-
- app.onPropertyChanges(changes as any, snapshot as any);
-
- expect(property.value).toBe('original');
- });
-
- it('onPropertyChanges does nothing when no selected element', () => {
- (app as any).selectedElement = undefined;
-
- const changes = [
- {
- componentKey: 'any-key',
- propertyName: 'prop',
- propertyType: 'property',
- oldValue: 'old',
- newValue: 'new',
- timestamp: Date.now(),
- },
- ];
-
- expect(() => app.onPropertyChanges(changes as any, {} as any)).not.toThrow();
- });
- });
-});
diff --git a/tests/debug-host-coverage.spec.ts b/tests/debug-host-coverage.spec.ts
deleted file mode 100644
index 7f94cf1..0000000
--- a/tests/debug-host-coverage.spec.ts
+++ /dev/null
@@ -1,517 +0,0 @@
-import './setup';
-import { ChromeTest } from './setup';
-
-describe('DebugHost additional coverage', () => {
- let DebugHost: any;
- let debugHost: any;
- let mockConsumer: any;
-
- beforeEach(async () => {
- ChromeTest.restoreChrome();
- jest.resetModules();
- ChromeTest.reset();
-
- const mod = await import('@/backend/debug-host');
- DebugHost = mod.DebugHost;
- debugHost = new DebugHost();
-
- mockConsumer = {
- handleComponentSnapshot: jest.fn(),
- onElementPicked: jest.fn(),
- selectedElement: null,
- selectedElementAttributes: null,
- followChromeSelection: true,
- onPropertyChanges: jest.fn(),
- };
- });
-
- afterEach(() => {
- ChromeTest.restoreChrome();
- });
-
- describe('attach with navigation and selection', () => {
- it('handles navigation event when attached', async () => {
- ChromeTest.setEvalToReturn([{ result: { kind: 'tree', data: [] } }]);
-
- debugHost.attach(mockConsumer);
-
- ChromeTest.triggerNavigated('http://localhost/new-page');
- await Promise.resolve();
-
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- });
-
- it('skips selection when followChromeSelection is false', () => {
- mockConsumer.followChromeSelection = false;
- debugHost.attach(mockConsumer);
-
- ChromeTest.triggerSelectionChanged();
-
- expect(chrome.devtools.inspectedWindow.eval).not.toHaveBeenCalled();
- });
-
- it('handles selection with debugObject using onElementPicked', () => {
- debugHost.attach(mockConsumer);
-
- const debugObject = {
- customElementInfo: { name: 'my-element' },
- customAttributesInfo: [{ name: 'my-attr' }],
- };
-
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb(debugObject);
- });
-
- ChromeTest.triggerSelectionChanged();
-
- expect(mockConsumer.onElementPicked).toHaveBeenCalledWith(debugObject);
- });
-
- it('handles selection with null debugObject', () => {
- debugHost.attach(mockConsumer);
-
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb(null);
- });
-
- ChromeTest.triggerSelectionChanged();
-
- expect(mockConsumer.onElementPicked).not.toHaveBeenCalled();
- });
-
- it('falls back to direct property assignment when onElementPicked not available', () => {
- const simpleConsumer = {
- handleComponentSnapshot: jest.fn(),
- selectedElement: null,
- selectedElementAttributes: null,
- followChromeSelection: true,
- };
-
- debugHost.attach(simpleConsumer);
-
- const debugObject = {
- customElementInfo: { name: 'my-element' },
- customAttributesInfo: [{ name: 'my-attr' }],
- };
-
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb(debugObject);
- });
-
- ChromeTest.triggerSelectionChanged();
-
- expect(simpleConsumer.selectedElement).toEqual({ name: 'my-element' });
- expect(simpleConsumer.selectedElementAttributes).toEqual([{ name: 'my-attr' }]);
- });
- });
-
- describe('getAllComponents edge cases', () => {
- it('handles null result from eval', async () => {
- debugHost.consumer = mockConsumer;
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb(null);
- });
-
- const result = await debugHost.getAllComponents();
-
- expect(result).toEqual({ tree: [], flat: [] });
- });
-
- it('handles flat data result', async () => {
- debugHost.consumer = mockConsumer;
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb({ kind: 'flat', data: [{ name: 'comp1' }, { name: 'comp2' }] });
- });
-
- const result = await debugHost.getAllComponents();
-
- expect(result).toEqual({ tree: [], flat: [{ name: 'comp1' }, { name: 'comp2' }] });
- });
-
- it('handles tree data result', async () => {
- debugHost.consumer = mockConsumer;
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb({ kind: 'tree', data: [{ id: 'root', children: [] }] });
- });
-
- const result = await debugHost.getAllComponents();
-
- expect(result).toEqual({ tree: [{ id: 'root', children: [] }], flat: [] });
- });
-
- it('handles non-array data gracefully', async () => {
- debugHost.consumer = mockConsumer;
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb({ kind: 'tree', data: 'not-an-array' });
- });
-
- const result = await debugHost.getAllComponents();
-
- expect(result).toEqual({ tree: [], flat: [] });
- });
-
- it('returns empty when chrome is not available', async () => {
- ChromeTest.removeChrome();
- debugHost.consumer = mockConsumer;
-
- const result = await debugHost.getAllComponents();
-
- expect(result).toEqual({ tree: [], flat: [] });
- ChromeTest.restoreChrome();
- });
- });
-
- describe('updateValues', () => {
- it('calls eval with correct code', () => {
- debugHost.consumer = mockConsumer;
-
- const value = { name: 'comp', key: 'comp-key' };
- const property = { name: 'count', value: 5 };
-
- debugHost.updateValues(value, property);
-
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- const code = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls[0][0];
- expect(code).toContain('updateValues');
- expect(code).toContain('comp');
- });
- });
-
- describe('updateDebugValue', () => {
- it('wraps string values in quotes', () => {
- debugHost.consumer = mockConsumer;
-
- const debugInfo = { debugId: 1, value: 'hello', type: 'string' };
- debugHost.updateDebugValue(debugInfo);
-
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- const code = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls[0][0];
- expect(code).toContain("'hello'");
- });
-
- it('does not wrap non-string values', () => {
- debugHost.consumer = mockConsumer;
-
- const debugInfo = { debugId: 1, value: 42, type: 'number' };
- debugHost.updateDebugValue(debugInfo);
-
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- const code = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls[0][0];
- expect(code).toContain('42');
- expect(code).not.toContain("'42'");
- });
- });
-
- describe('toggleDebugValueExpansion', () => {
- it('expands expandable property without existing value', () => {
- debugHost.consumer = mockConsumer;
-
- const debugInfo = { debugId: 1, canExpand: true, isExpanded: false, expandedValue: null };
-
- ChromeTest.setEvalImplementation((_expr: string, cb: Function) => {
- cb({ properties: [{ name: 'child', value: 'test' }] });
- });
-
- debugHost.toggleDebugValueExpansion(debugInfo);
-
- expect(debugInfo.isExpanded).toBe(true);
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- });
-
- it('collapses already expanded property', () => {
- debugHost.consumer = mockConsumer;
-
- const debugInfo = {
- debugId: 1,
- canExpand: true,
- isExpanded: true,
- expandedValue: { properties: [] },
- };
-
- debugHost.toggleDebugValueExpansion(debugInfo);
-
- expect(debugInfo.isExpanded).toBe(false);
- });
-
- it('does nothing for non-expandable property', () => {
- debugHost.consumer = mockConsumer;
-
- const debugInfo = { debugId: 1, canExpand: false, isExpanded: false };
-
- debugHost.toggleDebugValueExpansion(debugInfo);
-
- expect(debugInfo.isExpanded).toBe(false);
- expect(chrome.devtools.inspectedWindow.eval).not.toHaveBeenCalled();
- });
- });
-
- describe('revealInElements', () => {
- it('calls eval with inspect code', () => {
- debugHost.consumer = mockConsumer;
-
- const componentInfo = { name: 'my-element', key: 'my-key' };
- debugHost.revealInElements(componentInfo);
-
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- const code = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls[0][0];
- expect(code).toContain('findElementByComponentInfo');
- expect(code).toContain('inspect');
- });
- });
-
- describe('property watching', () => {
- beforeEach(() => {
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('starts property watching with specified interval', async () => {
- debugHost.consumer = mockConsumer;
-
- ChromeTest.setEvalToReturn([{ result: { componentKey: 'comp-key', bindables: [], properties: [], timestamp: 1 } }]);
-
- debugHost.startPropertyWatching({ componentKey: 'comp-key', pollInterval: 200 });
-
- expect(debugHost.watchingComponentKey).toBe('comp-key');
-
- jest.advanceTimersByTime(200);
-
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
-
- debugHost.stopPropertyWatching();
- });
-
- it('stops property watching and clears state', () => {
- debugHost.consumer = mockConsumer;
- debugHost.watchingComponentKey = 'comp-key';
- debugHost.propertyWatchInterval = setInterval(() => {}, 1000);
-
- debugHost.stopPropertyWatching();
-
- expect(debugHost.watchingComponentKey).toBeNull();
- expect(debugHost.propertyWatchInterval).toBeNull();
- });
-
- it('detects property changes and notifies consumer', async () => {
- debugHost.consumer = mockConsumer;
- debugHost.watchingComponentKey = 'comp-key';
-
- const oldSnapshot = {
- componentKey: 'comp-key',
- bindables: [{ name: 'count', value: 1, type: 'number' }],
- properties: [],
- timestamp: 1,
- };
- const newSnapshot = {
- componentKey: 'comp-key',
- bindables: [{ name: 'count', value: 2, type: 'number' }],
- properties: [],
- timestamp: 2,
- };
-
- debugHost.lastPropertySnapshot = oldSnapshot;
-
- ChromeTest.setEvalToReturn([{ result: newSnapshot }]);
-
- await debugHost.checkForPropertyChanges();
-
- expect(mockConsumer.onPropertyChanges).toHaveBeenCalled();
- const [changes] = mockConsumer.onPropertyChanges.mock.calls[0];
- expect(changes).toHaveLength(1);
- expect(changes[0].propertyName).toBe('count');
- expect(changes[0].oldValue).toBe(1);
- expect(changes[0].newValue).toBe(2);
- });
-
- it('does not notify when no changes', async () => {
- debugHost.consumer = mockConsumer;
- debugHost.watchingComponentKey = 'comp-key';
-
- const snapshot = {
- componentKey: 'comp-key',
- bindables: [{ name: 'count', value: 1, type: 'number' }],
- properties: [],
- timestamp: 1,
- };
-
- debugHost.lastPropertySnapshot = snapshot;
-
- ChromeTest.setEvalToReturn([{ result: snapshot }]);
-
- await debugHost.checkForPropertyChanges();
-
- expect(mockConsumer.onPropertyChanges).not.toHaveBeenCalled();
- });
-
- it('handles null snapshot during change check', async () => {
- debugHost.consumer = mockConsumer;
- debugHost.watchingComponentKey = 'comp-key';
- debugHost.lastPropertySnapshot = null;
-
- ChromeTest.setEvalToReturn([{ result: null }]);
-
- await debugHost.checkForPropertyChanges();
-
- expect(mockConsumer.onPropertyChanges).not.toHaveBeenCalled();
- });
- });
-
- describe('refreshSelectedComponent', () => {
- it('returns null when no component is being watched', async () => {
- debugHost.consumer = mockConsumer;
- debugHost.watchingComponentKey = null;
-
- const result = await debugHost.refreshSelectedComponent();
-
- expect(result).toBeNull();
- });
-
- it('returns component info when watching', async () => {
- debugHost.consumer = mockConsumer;
- debugHost.watchingComponentKey = 'comp-key';
-
- const componentInfo = { name: 'my-component', key: 'comp-key' };
- ChromeTest.setEvalToReturn([{ result: componentInfo }]);
-
- const result = await debugHost.refreshSelectedComponent();
-
- expect(result).toEqual(componentInfo);
- });
-
- it('returns null when chrome is not available', async () => {
- ChromeTest.removeChrome();
- debugHost.consumer = mockConsumer;
- debugHost.watchingComponentKey = 'comp-key';
-
- const result = await debugHost.refreshSelectedComponent();
-
- expect(result).toBeNull();
- ChromeTest.restoreChrome();
- });
- });
-
- describe('checkComponentTreeChanges', () => {
- it('returns true when tree signature changes', async () => {
- debugHost.consumer = mockConsumer;
- debugHost.componentTreeSignature = 'old-signature';
-
- ChromeTest.setEvalToReturn([{ result: 'new-signature' }]);
-
- const result = await debugHost.checkComponentTreeChanges();
-
- expect(result).toBe(true);
- expect(debugHost.componentTreeSignature).toBe('new-signature');
- });
-
- it('returns false when tree signature unchanged', async () => {
- debugHost.consumer = mockConsumer;
- debugHost.componentTreeSignature = 'same-signature';
-
- ChromeTest.setEvalToReturn([{ result: 'same-signature' }]);
-
- const result = await debugHost.checkComponentTreeChanges();
-
- expect(result).toBe(false);
- });
-
- it('returns false when chrome is not available', async () => {
- ChromeTest.removeChrome();
- debugHost.consumer = mockConsumer;
-
- const result = await debugHost.checkComponentTreeChanges();
-
- expect(result).toBe(false);
- ChromeTest.restoreChrome();
- });
- });
-
- describe('valuesEqual', () => {
- it('returns true for identical primitives', () => {
- expect(debugHost.valuesEqual(1, 1)).toBe(true);
- expect(debugHost.valuesEqual('a', 'a')).toBe(true);
- expect(debugHost.valuesEqual(true, true)).toBe(true);
- });
-
- it('returns false for different primitives', () => {
- expect(debugHost.valuesEqual(1, 2)).toBe(false);
- expect(debugHost.valuesEqual('a', 'b')).toBe(false);
- });
-
- it('returns false for different types', () => {
- expect(debugHost.valuesEqual(1, '1')).toBe(false);
- expect(debugHost.valuesEqual(null, undefined)).toBe(false);
- });
-
- it('returns true for equal objects', () => {
- expect(debugHost.valuesEqual({ a: 1 }, { a: 1 })).toBe(true);
- });
-
- it('returns false for different objects', () => {
- expect(debugHost.valuesEqual({ a: 1 }, { a: 2 })).toBe(false);
- });
-
- it('handles null values', () => {
- expect(debugHost.valuesEqual(null, null)).toBe(true);
- expect(debugHost.valuesEqual(null, {})).toBe(false);
- });
- });
-
- describe('diffPropertySnapshots', () => {
- it('detects new properties', () => {
- const oldSnapshot = {
- componentKey: 'comp',
- bindables: [],
- properties: [],
- timestamp: 1,
- };
- const newSnapshot = {
- componentKey: 'comp',
- bindables: [{ name: 'newProp', value: 1, type: 'number' }],
- properties: [],
- timestamp: 2,
- };
-
- const changes = debugHost.diffPropertySnapshots(oldSnapshot, newSnapshot);
-
- expect(changes).toHaveLength(1);
- expect(changes[0].propertyName).toBe('newProp');
- expect(changes[0].oldValue).toBeUndefined();
- expect(changes[0].newValue).toBe(1);
- });
-
- it('detects changed properties in both bindables and properties', () => {
- const oldSnapshot = {
- componentKey: 'comp',
- bindables: [{ name: 'a', value: 1, type: 'number' }],
- properties: [{ name: 'b', value: 'old', type: 'string' }],
- timestamp: 1,
- };
- const newSnapshot = {
- componentKey: 'comp',
- bindables: [{ name: 'a', value: 2, type: 'number' }],
- properties: [{ name: 'b', value: 'new', type: 'string' }],
- timestamp: 2,
- };
-
- const changes = debugHost.diffPropertySnapshots(oldSnapshot, newSnapshot);
-
- expect(changes).toHaveLength(2);
- expect(changes.map(c => c.propertyName)).toContain('a');
- expect(changes.map(c => c.propertyName)).toContain('b');
- });
- });
-});
-
-describe('SelectionChanged class', () => {
- it('stores debugInfo in constructor', async () => {
- const mod = await import('@/backend/debug-host');
- const debugInfo = { name: 'component', key: 'comp-key' };
-
- const event = new mod.SelectionChanged(debugInfo as any);
-
- expect(event.debugInfo).toBe(debugInfo);
- });
-});
diff --git a/tests/debug-host-extra.spec.ts b/tests/debug-host-extra.spec.ts
deleted file mode 100644
index af6562e..0000000
--- a/tests/debug-host-extra.spec.ts
+++ /dev/null
@@ -1,110 +0,0 @@
-import './setup';
-import { ChromeTest } from './setup';
-import { DebugHost } from '@/backend/debug-host';
-
-class ConsumerStub {
- followChromeSelection = false;
- onElementPicked = jest.fn();
- handleComponentSnapshot = jest.fn();
- componentSnapshot = { tree: [], flat: [] };
-}
-
-describe('DebugHost additional behaviors', () => {
- let host: DebugHost;
- let consumer: ConsumerStub;
-
- beforeEach(() => {
- ChromeTest.reset();
- consumer = new ConsumerStub();
- host = new DebugHost();
- // @ts-ignore
- host.attach(consumer as any);
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- jest.useRealTimers();
- });
-
- it('respects followChromeSelection=false and does not eval on selection change', () => {
- ChromeTest.triggerSelectionChanged();
- expect(chrome.devtools.inspectedWindow.eval).not.toHaveBeenCalled();
- });
-
- it('element picker start/stop triggers eval and polling is stopped after stop', () => {
- const evalMock = chrome.devtools.inspectedWindow.eval as jest.Mock;
- expect(evalMock).not.toHaveBeenCalled();
-
- host.startElementPicker();
- // Picker code is injected immediately once
- expect(evalMock).toHaveBeenCalled();
- const callsAfterStart = evalMock.mock.calls.length;
-
- // Polling will call eval periodically to check for picked component
- jest.advanceTimersByTime(350);
- const callsDuringPolling = evalMock.mock.calls.length;
- expect(callsDuringPolling).toBeGreaterThan(callsAfterStart);
-
- host.stopElementPicker();
- const callsAfterStop = evalMock.mock.calls.length;
- // Further timer advances should not increase call count
- jest.advanceTimersByTime(300);
- expect(evalMock.mock.calls.length).toBe(callsAfterStop);
- });
-
- it('picks a component via single poll and calls consumer.onElementPicked', () => {
- // Make eval return a picked component when checkForPickedComponent runs
- ChromeTest.setEvalImplementation((expr: string, cb?: (r: any) => void) => {
- if (expr && expr.includes('__AURELIA_DEVTOOLS_PICKED_COMPONENT__')) {
- cb && cb({ customElementInfo: { name: 'picked', key: 'picked', bindables: [], properties: [], aliases: [] }, customAttributesInfo: [] });
- }
- });
-
- // Directly invoke poller once
- (host as any).checkForPickedComponent();
- expect(consumer.onElementPicked).toHaveBeenCalled();
- });
-
- it('getInteractionLog proxies through inspectedWindow', async () => {
- const log = [{ id: 'evt-1', eventName: 'click' }];
- ChromeTest.setEvalToReturn([{ result: log }]);
-
- const result = await host.getInteractionLog();
-
- expect(result).toEqual(log);
- const evalArg = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls.pop()?.[0];
- expect(evalArg).toContain('getInteractionLog');
- });
-
- it('replayInteraction delegates to hook', async () => {
- ChromeTest.setEvalToReturn([{ result: true }]);
-
- const ok = await host.replayInteraction('evt-1');
-
- expect(ok).toBe(true);
- const evalArg = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls.pop()?.[0];
- expect(evalArg).toContain('replayInteraction');
- expect(evalArg).toContain('evt-1');
- });
-
- it('applyInteractionSnapshot forwards phase', async () => {
- ChromeTest.setEvalToReturn([{ result: true }]);
-
- const ok = await host.applyInteractionSnapshot('evt-2', 'after');
-
- expect(ok).toBe(true);
- const evalArg = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls.pop()?.[0];
- expect(evalArg).toContain('applyInteractionSnapshot');
- expect(evalArg).toContain('after');
- });
-
- it('clearInteractionLog calls hook clear', async () => {
- ChromeTest.setEvalToReturn([{ result: true }]);
-
- const ok = await host.clearInteractionLog();
-
- expect(ok).toBe(true);
- const evalArg = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls.pop()?.[0];
- expect(evalArg).toContain('clearInteractionLog');
- });
-});
diff --git a/tests/debug-host.spec.ts b/tests/debug-host.spec.ts
deleted file mode 100644
index 4ce76cc..0000000
--- a/tests/debug-host.spec.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-import './setup';
-import { ChromeTest } from './setup';
-import { DebugHost } from '@/backend/debug-host';
-
-/** Minimal App consumer stub */
-class AppConsumerStub {
- componentSnapshot = { tree: [], flat: [] };
- handleComponentSnapshot = jest.fn((snapshot) => {
- this.componentSnapshot = snapshot;
- });
- onElementPicked = jest.fn();
-}
-
-describe('DebugHost', () => {
- let host: DebugHost;
- let consumer: AppConsumerStub;
-
- beforeEach(() => {
- ChromeTest.reset();
- consumer = new AppConsumerStub();
- host = new DebugHost();
- // attach will set up listeners and schedule initial load, but we can ignore timers here
- // @ts-ignore
- host.attach(consumer as any);
- });
-
- it('getAllComponents falls back when initial eval returns empty twice', async () => {
- const fallback = [{ customElementInfo: { name: 'x', key: 'k', bindables: [], properties: [], aliases: [] }, customAttributesInfo: [] }];
- ChromeTest.setEvalToReturn([
- { result: [] },
- { result: { kind: 'flat', data: [] } },
- { result: { kind: 'flat', data: fallback } },
- ]);
-
- const result = await host.getAllComponents();
- expect(result).toEqual({ tree: [], flat: fallback });
- });
-
- it('getAllComponents returns immediate non-empty result', async () => {
- const first = [{
- id: 'root',
- domPath: 'html > body > root:nth-of-type(1)',
- tagName: 'app-root',
- customElementInfo: { name: 'app-root', key: 'app-root', bindables: [], properties: [], aliases: [] },
- customAttributesInfo: [],
- children: [],
- }];
- ChromeTest.setEvalToReturn([{ result: { kind: 'tree', data: first } }]);
-
- const result = await host.getAllComponents();
- expect(result).toEqual({ tree: first, flat: [] });
- });
-
- it('injects DOM path metadata when syncing Chrome selection', () => {
- ChromeTest.triggerSelectionChanged();
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- const code = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls[0][0];
- expect(code).toContain('__auDevtoolsDomPath');
- expect(code).toContain('getDomPath');
- });
-
- it('highlightComponent/unhighlightComponent call inspectedWindow.eval with injected code', () => {
- host.highlightComponent({ name: 'Comp' });
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- const code = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls[0][0];
- expect(code).toContain('aurelia-devtools-highlight');
-
- (chrome.devtools.inspectedWindow.eval as jest.Mock).mockClear();
- host.unhighlightComponent();
- expect(chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
- const code2 = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls[0][0];
- expect(code2).toContain("querySelectorAll('.aurelia-devtools-highlight')");
- });
-
- it('getExternalPanelsSnapshot returns fallback when hook unavailable', async () => {
- ChromeTest.setEvalToReturn([{ result: { version: 0, panels: [] } }]);
- const snapshot = await host.getExternalPanelsSnapshot();
- expect(snapshot).toEqual({ version: 0, panels: [] });
- });
-
- it('emitExternalPanelEvent proxies to inspected window', async () => {
- ChromeTest.setEvalToReturn([{ result: true }]);
- const ok = await host.emitExternalPanelEvent('aurelia-devtools:request-panel', { selectedComponentId: 'alpha' });
- expect(ok).toBe(true);
- const evalCode = (chrome.devtools.inspectedWindow.eval as jest.Mock).mock.calls.pop()?.[0];
- expect(evalCode).toContain('emitDevtoolsEvent');
- });
-});
diff --git a/tests/reactivity.spec.ts b/tests/reactivity.spec.ts
deleted file mode 100644
index 4483552..0000000
--- a/tests/reactivity.spec.ts
+++ /dev/null
@@ -1,365 +0,0 @@
-import './setup';
-import { ChromeTest } from './setup';
-import { DebugHost } from '@/backend/debug-host';
-import { PropertyChangeRecord, PropertySnapshot } from '@/shared/types';
-
-class ConsumerStub {
- followChromeSelection = false;
- onElementPicked = jest.fn();
- handleComponentSnapshot = jest.fn();
- onPropertyChanges = jest.fn();
- componentSnapshot = { tree: [], flat: [] };
-}
-
-describe('DebugHost property watching', () => {
- let host: DebugHost;
- let consumer: ConsumerStub;
-
- beforeEach(() => {
- ChromeTest.reset();
- consumer = new ConsumerStub();
- host = new DebugHost();
- // @ts-ignore
- host.attach(consumer as any);
- jest.useFakeTimers();
- });
-
- afterEach(() => {
- host.stopPropertyWatching();
- jest.useRealTimers();
- });
-
- describe('startPropertyWatching', () => {
- it('should start polling for property changes', () => {
- const evalMock = chrome.devtools.inspectedWindow.eval as jest.Mock;
- evalMock.mockClear();
-
- host.startPropertyWatching({ componentKey: 'test-component' });
-
- // Initial snapshot fetch
- expect(evalMock).toHaveBeenCalled();
-
- // Advance timer to trigger polling
- jest.advanceTimersByTime(600);
- expect(evalMock.mock.calls.length).toBeGreaterThan(1);
- });
-
- it('should stop previous watcher when starting new one', () => {
- host.startPropertyWatching({ componentKey: 'component-1' });
- const intervalsBefore = jest.getTimerCount();
-
- host.startPropertyWatching({ componentKey: 'component-2' });
-
- // Should not have more intervals than before
- expect(jest.getTimerCount()).toBeLessThanOrEqual(intervalsBefore);
- });
-
- it('should respect custom poll interval', () => {
- const evalMock = chrome.devtools.inspectedWindow.eval as jest.Mock;
- evalMock.mockClear();
-
- host.startPropertyWatching({ componentKey: 'test', pollInterval: 1000 });
-
- // Initial call
- const initialCalls = evalMock.mock.calls.length;
-
- // Advance less than poll interval
- jest.advanceTimersByTime(500);
- expect(evalMock.mock.calls.length).toBe(initialCalls);
-
- // Advance past poll interval
- jest.advanceTimersByTime(600);
- expect(evalMock.mock.calls.length).toBeGreaterThan(initialCalls);
- });
- });
-
- describe('stopPropertyWatching', () => {
- it('should stop polling when called', () => {
- const evalMock = chrome.devtools.inspectedWindow.eval as jest.Mock;
-
- host.startPropertyWatching({ componentKey: 'test' });
- evalMock.mockClear();
-
- host.stopPropertyWatching();
-
- jest.advanceTimersByTime(2000);
- expect(evalMock).not.toHaveBeenCalled();
- });
-
- it('should clear internal state', () => {
- host.startPropertyWatching({ componentKey: 'test' });
- host.stopPropertyWatching();
-
- expect((host as any).watchingComponentKey).toBeNull();
- expect((host as any).lastPropertySnapshot).toBeNull();
- });
- });
-
- describe('property change detection', () => {
- it('should detect and report property changes via direct call', () => {
- // Test diffPropertySnapshots directly since polling involves promises
- const oldSnapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [{ name: 'value', value: 'old', type: 'string' }],
- properties: [],
- timestamp: 1000,
- };
-
- const newSnapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [{ name: 'value', value: 'new', type: 'string' }],
- properties: [],
- timestamp: 2000,
- };
-
- // Set up the internal state manually
- (host as any).lastPropertySnapshot = oldSnapshot;
- (host as any).watchingComponentKey = 'test';
-
- // Call diffPropertySnapshots directly
- const changes = (host as any).diffPropertySnapshots(oldSnapshot, newSnapshot);
-
- expect(changes).toHaveLength(1);
- expect(changes[0].propertyName).toBe('value');
- expect(changes[0].oldValue).toBe('old');
- expect(changes[0].newValue).toBe('new');
- });
-
- it('should not report when values are unchanged', () => {
- const snapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [{ name: 'value', value: 'same', type: 'string' }],
- properties: [],
- timestamp: 1000,
- };
-
- const sameSnapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [{ name: 'value', value: 'same', type: 'string' }],
- properties: [],
- timestamp: 2000,
- };
-
- const changes = (host as any).diffPropertySnapshots(snapshot, sameSnapshot);
- expect(changes).toHaveLength(0);
- });
-
- it('should detect changes in both bindables and properties', () => {
- const oldSnapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [{ name: 'bindable1', value: 'a', type: 'string' }],
- properties: [{ name: 'prop1', value: 1, type: 'number' }],
- timestamp: 1000,
- };
-
- const newSnapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [{ name: 'bindable1', value: 'b', type: 'string' }],
- properties: [{ name: 'prop1', value: 2, type: 'number' }],
- timestamp: 2000,
- };
-
- const changes = (host as any).diffPropertySnapshots(oldSnapshot, newSnapshot);
- expect(changes).toHaveLength(2);
- expect(changes.find((c: PropertyChangeRecord) => c.propertyType === 'bindable')).toBeDefined();
- expect(changes.find((c: PropertyChangeRecord) => c.propertyType === 'property')).toBeDefined();
- });
-
- it('should return changes with correct component key and timestamp', () => {
- const oldSnapshot: PropertySnapshot = {
- componentKey: 'my-comp',
- bindables: [],
- properties: [{ name: 'count', value: 10, type: 'number' }],
- timestamp: 1000,
- };
-
- const newSnapshot: PropertySnapshot = {
- componentKey: 'my-comp',
- bindables: [],
- properties: [{ name: 'count', value: 20, type: 'number' }],
- timestamp: 2000,
- };
-
- const changes = (host as any).diffPropertySnapshots(oldSnapshot, newSnapshot);
-
- expect(changes).toHaveLength(1);
- expect(changes[0].componentKey).toBe('my-comp');
- expect(changes[0].timestamp).toBe(2000);
- });
- });
-
- describe('getPropertySnapshot', () => {
- it('should fetch property snapshot via eval', async () => {
- const mockSnapshot: PropertySnapshot = {
- componentKey: 'my-component',
- bindables: [{ name: 'foo', value: 'bar', type: 'string' }],
- properties: [],
- timestamp: Date.now(),
- };
-
- ChromeTest.setEvalImplementation((expr: string, cb?: (r: any) => void) => {
- if (expr && expr.includes('getComponentByKey')) {
- cb && cb(mockSnapshot);
- }
- });
-
- const result = await host.getPropertySnapshot('my-component');
- expect(result).toEqual(mockSnapshot);
- });
-
- it('should return null when hook is not available', async () => {
- ChromeTest.setEvalImplementation((expr: string, cb?: (r: any) => void) => {
- cb && cb(null);
- });
-
- const result = await host.getPropertySnapshot('missing');
- expect(result).toBeNull();
- });
- });
-
- describe('checkComponentTreeChanges', () => {
- it('should detect tree structure changes', async () => {
- let signature = 'initial';
- ChromeTest.setEvalImplementation((expr: string, cb?: (r: any) => void) => {
- if (expr && expr.includes('getSignature')) {
- cb && cb(signature);
- }
- });
-
- // First call - establishes baseline
- const firstResult = await host.checkComponentTreeChanges();
- expect(firstResult).toBe(true); // Changed from empty string
-
- // Second call - no change
- const secondResult = await host.checkComponentTreeChanges();
- expect(secondResult).toBe(false);
-
- // Third call - signature changed
- signature = 'changed';
- const thirdResult = await host.checkComponentTreeChanges();
- expect(thirdResult).toBe(true);
- });
-
- it('should return false when no chrome APIs available', async () => {
- const originalChrome = (global as any).chrome;
- (global as any).chrome = undefined;
-
- const result = await host.checkComponentTreeChanges();
- expect(result).toBe(false);
-
- (global as any).chrome = originalChrome;
- });
- });
-});
-
-describe('DebugHost value comparison', () => {
- let host: DebugHost;
-
- beforeEach(() => {
- ChromeTest.reset();
- host = new DebugHost();
- });
-
- it('should compare primitive values correctly', () => {
- const valuesEqual = (host as any).valuesEqual.bind(host);
-
- expect(valuesEqual(1, 1)).toBe(true);
- expect(valuesEqual('a', 'a')).toBe(true);
- expect(valuesEqual(true, true)).toBe(true);
- expect(valuesEqual(null, null)).toBe(true);
-
- expect(valuesEqual(1, 2)).toBe(false);
- expect(valuesEqual('a', 'b')).toBe(false);
- expect(valuesEqual(true, false)).toBe(false);
- expect(valuesEqual(null, undefined)).toBe(false);
- });
-
- it('should compare objects by JSON serialization', () => {
- const valuesEqual = (host as any).valuesEqual.bind(host);
-
- expect(valuesEqual({ a: 1 }, { a: 1 })).toBe(true);
- expect(valuesEqual([1, 2], [1, 2])).toBe(true);
-
- expect(valuesEqual({ a: 1 }, { a: 2 })).toBe(false);
- expect(valuesEqual([1, 2], [2, 1])).toBe(false);
- });
-
- it('should handle different types', () => {
- const valuesEqual = (host as any).valuesEqual.bind(host);
-
- expect(valuesEqual(1, '1')).toBe(false);
- expect(valuesEqual({}, [])).toBe(false);
- });
-});
-
-describe('Property snapshot diffing', () => {
- let host: DebugHost;
-
- beforeEach(() => {
- ChromeTest.reset();
- host = new DebugHost();
- });
-
- it('should detect added properties', () => {
- const diffSnapshots = (host as any).diffPropertySnapshots.bind(host);
-
- const oldSnapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [],
- properties: [],
- timestamp: 1000,
- };
-
- const newSnapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [{ name: 'newProp', value: 'value', type: 'string' }],
- properties: [],
- timestamp: 2000,
- };
-
- const changes = diffSnapshots(oldSnapshot, newSnapshot);
- expect(changes).toHaveLength(1);
- expect(changes[0].propertyName).toBe('newProp');
- expect(changes[0].oldValue).toBeUndefined();
- expect(changes[0].newValue).toBe('value');
- });
-
- it('should detect value changes', () => {
- const diffSnapshots = (host as any).diffPropertySnapshots.bind(host);
-
- const oldSnapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [],
- properties: [{ name: 'count', value: 1, type: 'number' }],
- timestamp: 1000,
- };
-
- const newSnapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [],
- properties: [{ name: 'count', value: 2, type: 'number' }],
- timestamp: 2000,
- };
-
- const changes = diffSnapshots(oldSnapshot, newSnapshot);
- expect(changes).toHaveLength(1);
- expect(changes[0].propertyName).toBe('count');
- expect(changes[0].oldValue).toBe(1);
- expect(changes[0].newValue).toBe(2);
- expect(changes[0].propertyType).toBe('property');
- });
-
- it('should return empty array when no changes', () => {
- const diffSnapshots = (host as any).diffPropertySnapshots.bind(host);
-
- const snapshot: PropertySnapshot = {
- componentKey: 'test',
- bindables: [{ name: 'val', value: 'same', type: 'string' }],
- properties: [],
- timestamp: 1000,
- };
-
- const changes = diffSnapshots(snapshot, { ...snapshot, timestamp: 2000 });
- expect(changes).toHaveLength(0);
- });
-});
diff --git a/tests/setup.ts b/tests/setup.ts
index df3ec3e..1d3afc8 100644
--- a/tests/setup.ts
+++ b/tests/setup.ts
@@ -41,6 +41,7 @@ const inspectedWindowEval = jest.fn();
const createChromeMock = () => ({
runtime: {
+ id: 'test-extension-id',
onMessage,
sendMessage: jest.fn(),
connect: jest.fn(() => ({ postMessage: jest.fn(), onMessage: mkEvent(), disconnect: jest.fn() }))
@@ -88,9 +89,11 @@ export const ChromeTest = {
return;
}
const queue = values.slice();
- (global as any).chrome.devtools.inspectedWindow.eval.mockImplementation((_expr: string, cb: Function) => {
+ (global as any).chrome.devtools.inspectedWindow.eval.mockImplementation((_expr: string, cb?: Function) => {
const next = queue.length ? queue.shift()! : { result: undefined, exception: undefined };
- cb(next.result, next.exception);
+ if (typeof cb === 'function') {
+ cb(next.result, next.exception);
+ }
});
},
reset: () => {
diff --git a/tests/sidebar-app.spec.ts b/tests/sidebar-app.spec.ts
new file mode 100644
index 0000000..bba3e9e
--- /dev/null
+++ b/tests/sidebar-app.spec.ts
@@ -0,0 +1,1134 @@
+import './setup';
+import { ChromeTest } from './setup';
+import { stubPlatform } from './helpers';
+import type { AureliaInfo, IControllerInfo, Property } from '@/shared/types';
+
+let SidebarAppClass: any;
+
+function stubSidebarDebugHost(overrides: Partial> = {}) {
+ return {
+ attach: jest.fn(),
+ startElementPicker: jest.fn(),
+ stopElementPicker: jest.fn(),
+ startPropertyWatching: jest.fn(),
+ stopPropertyWatching: jest.fn(),
+ updateValues: jest.fn(),
+ revealInElements: jest.fn(),
+ searchComponents: jest.fn().mockResolvedValue([]),
+ selectComponentByKey: jest.fn(),
+ getLifecycleHooks: jest.fn().mockResolvedValue(null),
+ getComputedProperties: jest.fn().mockResolvedValue([]),
+ getDependencies: jest.fn().mockResolvedValue(null),
+ getEnhancedDISnapshot: jest.fn().mockResolvedValue(null),
+ getRouteInfo: jest.fn().mockResolvedValue(null),
+ getSlotInfo: jest.fn().mockResolvedValue(null),
+ getComponentTree: jest.fn().mockResolvedValue([]),
+ startInteractionRecording: jest.fn().mockResolvedValue(true),
+ stopInteractionRecording: jest.fn().mockResolvedValue(true),
+ clearInteractionLog: jest.fn(),
+ getTemplateSnapshot: jest.fn().mockResolvedValue(null),
+ ...overrides
+ };
+}
+
+function ci(name: string, key?: string): IControllerInfo {
+ return {
+ name,
+ key: key ?? name,
+ aliases: [],
+ bindables: [],
+ properties: []
+ } as any;
+}
+
+function ai(element: IControllerInfo | null, attrs: IControllerInfo[] = []): AureliaInfo {
+ return {
+ customElementInfo: element as any,
+ customAttributesInfo: attrs as any
+ };
+}
+
+describe('SidebarApp', () => {
+ let app: any;
+ let debugHost: any;
+ let plat: any;
+
+ beforeEach(async () => {
+ ChromeTest.reset();
+ jest.resetModules();
+
+ const mod = await import('@/sidebar/sidebar-app');
+ SidebarAppClass = mod.SidebarApp;
+
+ app = Object.create(SidebarAppClass.prototype);
+
+ app.isDarkTheme = false;
+ app.aureliaDetected = false;
+ app.aureliaVersion = null;
+ app.detectionState = 'checking';
+ app.extensionInvalidated = false;
+ app.selectedElement = null;
+ app.selectedElementAttributes = [];
+ app.selectedNodeType = 'custom-element';
+ app.selectedElementTagName = null;
+ app.isShowingBindingContext = false;
+ app.isElementPickerActive = false;
+ app.followChromeSelection = true;
+ app.searchQuery = '';
+ app.searchResults = [];
+ app.isSearchOpen = false;
+ app.expandedSections = {
+ bindables: true,
+ properties: true,
+ controller: false,
+ attributes: false,
+ lifecycle: false,
+ computed: false,
+ dependencies: false,
+ route: false,
+ slots: false,
+ expression: false,
+ timeline: false,
+ template: false,
+ };
+ app.componentTree = [];
+ app.expandedTreeNodes = new Set();
+ app.selectedTreeNodeKey = null;
+ app.isTreePanelExpanded = true;
+ app.treeRevision = 0;
+ app.isRecording = false;
+ app.timelineEvents = [];
+ app.expandedTimelineEvents = new Set();
+ app.templateSnapshot = null;
+ app.expandedBindings = new Set();
+ app.expandedControllers = new Set();
+ app.lifecycleHooks = null;
+ app.computedProperties = [];
+ app.dependencies = null;
+ app.routeInfo = null;
+ app.slotInfo = null;
+ app.expressionInput = '';
+ app.expressionResult = '';
+ app.expressionResultType = '';
+ app.expressionError = '';
+ app.expressionHistory = [];
+ app.copiedPropertyId = null;
+ app.propertyRowsRevision = 0;
+
+ debugHost = stubSidebarDebugHost();
+ plat = stubPlatform();
+
+ (app as any).debugHost = debugHost;
+ (app as any).plat = plat;
+ });
+
+ describe('onElementPicked', () => {
+ it('sets selectedElement for custom element', () => {
+ const element = ci('my-component', 'my-key');
+ element.bindables = [{ name: 'value', value: 1, type: 'number' }] as any;
+ element.properties = [{ name: 'count', value: 5, type: 'number' }] as any;
+
+ app.onElementPicked(ai(element));
+
+ expect(app.selectedElement).toBe(element);
+ expect(app.selectedNodeType).toBe('custom-element');
+ expect(app.selectedElementAttributes).toEqual([]);
+ });
+
+ it('sets selectedElement for custom attribute when no element', () => {
+ const attr = ci('my-attr', 'attr-key');
+
+ app.onElementPicked(ai(null, [attr]));
+
+ expect(app.selectedElement).toBe(attr);
+ expect(app.selectedNodeType).toBe('custom-attribute');
+ });
+
+ it('clears selection when no component info', () => {
+ app.selectedElement = ci('existing');
+
+ app.onElementPicked(null as any);
+
+ expect(app.selectedElement).toBeNull();
+ });
+
+ it('tracks binding context info', () => {
+ const element = ci('parent-component');
+ const info = {
+ ...ai(element),
+ __selectedElement: 'div',
+ __isBindingContext: true
+ };
+
+ app.onElementPicked(info);
+
+ expect(app.selectedElementTagName).toBe('div');
+ expect(app.isShowingBindingContext).toBe(true);
+ });
+
+ it('starts property watching when element selected', () => {
+ const element = ci('my-component', 'component-key');
+
+ app.onElementPicked(ai(element));
+
+ expect(debugHost.startPropertyWatching).toHaveBeenCalledWith({
+ componentKey: 'component-key',
+ pollInterval: 500
+ });
+ });
+
+ it('filters out duplicate attributes with same key as element', () => {
+ const element = ci('my-element', 'my-key');
+ const dupAttr = ci('my-element', 'my-key');
+ const realAttr = ci('other-attr', 'other-key');
+
+ app.onElementPicked(ai(element, [dupAttr, realAttr]));
+
+ expect(app.selectedElementAttributes).toHaveLength(2);
+ });
+ });
+
+ describe('clearSelection', () => {
+ it('stops property watching and clears all selection state', () => {
+ app.selectedElement = ci('test');
+ app.selectedElementAttributes = [ci('attr')];
+ app.selectedElementTagName = 'div';
+ app.isShowingBindingContext = true;
+
+ app.clearSelection();
+
+ expect(debugHost.stopPropertyWatching).toHaveBeenCalled();
+ expect(app.selectedElement).toBeNull();
+ expect(app.selectedElementAttributes).toEqual([]);
+ expect(app.selectedElementTagName).toBeNull();
+ expect(app.isShowingBindingContext).toBe(false);
+ });
+ });
+
+ describe('toggleSection', () => {
+ it('toggles section expanded state', () => {
+ expect(app.expandedSections.bindables).toBe(true);
+
+ app.toggleSection('bindables');
+
+ expect(app.expandedSections.bindables).toBe(false);
+
+ app.toggleSection('bindables');
+
+ expect(app.expandedSections.bindables).toBe(true);
+ });
+ });
+
+ describe('toggleElementPicker', () => {
+ it('starts picker when activating', () => {
+ app.isElementPickerActive = false;
+
+ app.toggleElementPicker();
+
+ expect(app.isElementPickerActive).toBe(true);
+ expect(debugHost.startElementPicker).toHaveBeenCalled();
+ });
+
+ it('stops picker when deactivating', () => {
+ app.isElementPickerActive = true;
+
+ app.toggleElementPicker();
+
+ expect(app.isElementPickerActive).toBe(false);
+ expect(debugHost.stopElementPicker).toHaveBeenCalled();
+ });
+ });
+
+ describe('toggleFollowChromeSelection', () => {
+ it('toggles follow state', () => {
+ app.followChromeSelection = true;
+
+ app.toggleFollowChromeSelection();
+
+ expect(app.followChromeSelection).toBe(false);
+ });
+ });
+
+ describe('search', () => {
+ it('handleSearchInput opens search with results', async () => {
+ debugHost.searchComponents.mockResolvedValue([
+ { key: 'comp-1', name: 'my-component', type: 'custom-element' }
+ ]);
+
+ const event = { target: { value: 'my' } } as any;
+ app.handleSearchInput(event);
+
+ expect(app.searchQuery).toBe('my');
+ expect(app.isSearchOpen).toBe(true);
+ });
+
+ it('handleSearchInput clears results when query empty', () => {
+ app.searchResults = [{ key: '1', name: 'test', type: 'custom-element' }];
+ app.isSearchOpen = true;
+
+ const event = { target: { value: '' } } as any;
+ app.handleSearchInput(event);
+
+ expect(app.searchResults).toEqual([]);
+ expect(app.isSearchOpen).toBe(false);
+ });
+
+ it('selectSearchResult calls debugHost and clears search', () => {
+ const result = { key: 'comp-key', name: 'my-comp', type: 'custom-element' as const };
+ app.searchQuery = 'my';
+ app.isSearchOpen = true;
+
+ app.selectSearchResult(result);
+
+ expect(debugHost.selectComponentByKey).toHaveBeenCalledWith('comp-key');
+ expect(app.searchQuery).toBe('');
+ expect(app.isSearchOpen).toBe(false);
+ });
+
+ it('clearSearch resets search state', () => {
+ app.searchQuery = 'test';
+ app.searchResults = [{ key: '1', name: 'test', type: 'custom-element' }];
+ app.isSearchOpen = true;
+
+ app.clearSearch();
+
+ expect(app.searchQuery).toBe('');
+ expect(app.searchResults).toEqual([]);
+ expect(app.isSearchOpen).toBe(false);
+ });
+ });
+
+ describe('property editing', () => {
+ beforeEach(() => {
+ jest.useFakeTimers();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ it('editProperty sets isEditing and stores original value', () => {
+ const prop: any = { name: 'test', value: 'original', type: 'string' };
+
+ app.editProperty(prop);
+
+ expect(prop.isEditing).toBe(true);
+ expect(prop.originalValue).toBe('original');
+ });
+
+ it('saveProperty converts number and calls updateValues', () => {
+ const prop: any = { name: 'count', value: 1, type: 'number', isEditing: true };
+ app.selectedElement = { bindables: [], properties: [prop] };
+
+ app.saveProperty(prop, '42');
+ jest.runOnlyPendingTimers();
+
+ expect(prop.value).toBe(42);
+ expect(prop.isEditing).toBe(false);
+ expect(debugHost.updateValues).toHaveBeenCalled();
+ });
+
+ it('saveProperty reverts on invalid number', () => {
+ const prop: any = { name: 'count', value: 5, type: 'number', isEditing: true, originalValue: 5 };
+ app.selectedElement = { bindables: [], properties: [prop] };
+
+ app.saveProperty(prop, 'not-a-number');
+
+ expect(prop.value).toBe(5);
+ expect(prop.isEditing).toBe(false);
+ });
+
+ it('saveProperty converts boolean true', () => {
+ const prop: any = { name: 'active', value: false, type: 'boolean', isEditing: true };
+ app.selectedElement = { bindables: [], properties: [prop] };
+
+ app.saveProperty(prop, 'true');
+ jest.runOnlyPendingTimers();
+
+ expect(prop.value).toBe(true);
+ });
+
+ it('saveProperty converts boolean false', () => {
+ const prop: any = { name: 'active', value: true, type: 'boolean', isEditing: true };
+ app.selectedElement = { bindables: [], properties: [prop] };
+
+ app.saveProperty(prop, 'false');
+ jest.runOnlyPendingTimers();
+
+ expect(prop.value).toBe(false);
+ });
+
+ it('saveProperty reverts on invalid boolean', () => {
+ const prop: any = { name: 'active', value: true, type: 'boolean', isEditing: true, originalValue: true };
+ app.selectedElement = { bindables: [], properties: [prop] };
+
+ app.saveProperty(prop, 'invalid');
+
+ expect(prop.value).toBe(true);
+ });
+
+ it('cancelPropertyEdit reverts property', () => {
+ const prop: any = { name: 'test', value: 'new', type: 'string', isEditing: true, originalValue: 'original' };
+
+ app.cancelPropertyEdit(prop);
+
+ expect(prop.value).toBe('original');
+ expect(prop.isEditing).toBe(false);
+ });
+ });
+
+ describe('property expansion', () => {
+ it('togglePropertyExpansion does nothing when canExpand is false', () => {
+ const prop: any = { canExpand: false, isExpanded: false };
+
+ app.togglePropertyExpansion(prop);
+
+ expect(prop.isExpanded).toBe(false);
+ });
+
+ it('togglePropertyExpansion expands when already has expandedValue', () => {
+ const prop: any = { canExpand: true, isExpanded: false, expandedValue: { properties: [] } };
+
+ app.togglePropertyExpansion(prop);
+
+ expect(prop.isExpanded).toBe(true);
+ });
+
+ it('togglePropertyExpansion collapses when expanded', () => {
+ const prop: any = { canExpand: true, isExpanded: true };
+
+ app.togglePropertyExpansion(prop);
+
+ expect(prop.isExpanded).toBe(false);
+ });
+ });
+
+ describe('onPropertyChanges', () => {
+ it('updates bindable values', () => {
+ const bindable: any = { name: 'count', value: 0, type: 'number' };
+ app.selectedElement = {
+ key: 'test-key',
+ name: 'test',
+ bindables: [bindable],
+ properties: [],
+ };
+
+ const changes = [{
+ componentKey: 'test-key',
+ propertyName: 'count',
+ propertyType: 'bindable',
+ oldValue: 0,
+ newValue: 5,
+ timestamp: Date.now(),
+ }];
+
+ const snapshot = {
+ componentKey: 'test-key',
+ bindables: [{ name: 'count', value: 5, type: 'number' }],
+ properties: [],
+ timestamp: Date.now(),
+ };
+
+ app.onPropertyChanges(changes, snapshot);
+
+ expect(bindable.value).toBe(5);
+ });
+
+ it('updates property values', () => {
+ const property: any = { name: 'message', value: 'old', type: 'string' };
+ app.selectedElement = {
+ key: 'test-key',
+ name: 'test',
+ bindables: [],
+ properties: [property],
+ };
+
+ const changes = [{
+ componentKey: 'test-key',
+ propertyName: 'message',
+ propertyType: 'property',
+ oldValue: 'old',
+ newValue: 'new',
+ timestamp: Date.now(),
+ }];
+
+ const snapshot = {
+ componentKey: 'test-key',
+ bindables: [],
+ properties: [{ name: 'message', value: 'new', type: 'string' }],
+ timestamp: Date.now(),
+ };
+
+ app.onPropertyChanges(changes, snapshot);
+
+ expect(property.value).toBe('new');
+ });
+
+ it('ignores changes for different component', () => {
+ const property: any = { name: 'message', value: 'original', type: 'string' };
+ app.selectedElement = {
+ key: 'selected-key',
+ name: 'selected',
+ bindables: [],
+ properties: [property],
+ };
+
+ const changes = [{
+ componentKey: 'different-key',
+ propertyName: 'message',
+ propertyType: 'property',
+ oldValue: 'old',
+ newValue: 'new',
+ timestamp: Date.now(),
+ }];
+
+ const snapshot = {
+ componentKey: 'different-key',
+ bindables: [],
+ properties: [],
+ timestamp: Date.now(),
+ };
+
+ app.onPropertyChanges(changes, snapshot);
+
+ expect(property.value).toBe('original');
+ });
+
+ it('does nothing when no selected element', () => {
+ app.selectedElement = null;
+
+ const changes = [{
+ componentKey: 'any-key',
+ propertyName: 'prop',
+ propertyType: 'property',
+ oldValue: 'old',
+ newValue: 'new',
+ timestamp: Date.now(),
+ }];
+
+ expect(() => app.onPropertyChanges(changes, {})).not.toThrow();
+ });
+ });
+
+ describe('computed getters', () => {
+ it('hasBindables returns true when bindables exist', () => {
+ app.selectedElement = { bindables: [{ name: 'test' }], properties: [] };
+ expect(app.hasBindables).toBe(true);
+ });
+
+ it('hasBindables returns false when no bindables', () => {
+ app.selectedElement = { bindables: [], properties: [] };
+ expect(app.hasBindables).toBe(false);
+ });
+
+ it('hasProperties returns true when properties exist', () => {
+ app.selectedElement = { bindables: [], properties: [{ name: 'test' }] };
+ expect(app.hasProperties).toBe(true);
+ });
+
+ it('hasCustomAttributes returns true when attributes exist', () => {
+ app.selectedElementAttributes = [ci('test-attr')];
+ expect(app.hasCustomAttributes).toBe(true);
+ });
+
+ it('hasLifecycleHooks returns true when hooks exist', () => {
+ app.lifecycleHooks = { hooks: [{ name: 'attached', implemented: true }] };
+ expect(app.hasLifecycleHooks).toBe(true);
+ });
+
+ it('implementedHooksCount counts implemented hooks', () => {
+ app.lifecycleHooks = {
+ hooks: [
+ { name: 'attached', implemented: true },
+ { name: 'detached', implemented: false },
+ { name: 'bound', implemented: true },
+ ]
+ };
+ expect(app.implementedHooksCount).toBe(2);
+ });
+
+ it('totalHooksCount returns all hooks', () => {
+ app.lifecycleHooks = { hooks: [{ name: 'a' }, { name: 'b' }, { name: 'c' }] };
+ expect(app.totalHooksCount).toBe(3);
+ });
+
+ it('activeSlotCount counts slots with content', () => {
+ app.slotInfo = {
+ slots: [
+ { name: 'default', hasContent: true },
+ { name: 'header', hasContent: false },
+ { name: 'footer', hasContent: true },
+ ]
+ };
+ expect(app.activeSlotCount).toBe(2);
+ });
+
+ it('hasRouteInfo returns true when currentRoute exists', () => {
+ app.routeInfo = { currentRoute: '/users' };
+ expect(app.hasRouteInfo).toBe(true);
+ });
+
+ it('hasSlots returns true when slots exist', () => {
+ app.slotInfo = { slots: [{ name: 'default' }] };
+ expect(app.hasSlots).toBe(true);
+ });
+
+ it('hasComputedProperties returns true when computed exist', () => {
+ app.computedProperties = [{ name: 'fullName', hasGetter: true }];
+ expect(app.hasComputedProperties).toBe(true);
+ });
+
+ it('hasDependencies returns true when dependencies exist', () => {
+ app.dependencies = { dependencies: [{ name: 'HttpClient' }] };
+ expect(app.hasDependencies).toBe(true);
+ });
+ });
+
+ describe('formatPropertyValue', () => {
+ it('formats null', () => {
+ expect(app.formatPropertyValue(null)).toBe('null');
+ });
+
+ it('formats undefined', () => {
+ expect(app.formatPropertyValue(undefined)).toBe('undefined');
+ });
+
+ it('formats string with quotes', () => {
+ expect(app.formatPropertyValue('hello')).toBe('"hello"');
+ });
+
+ it('formats array with length', () => {
+ expect(app.formatPropertyValue([1, 2, 3])).toBe('Array(3)');
+ });
+
+ it('formats object as {...}', () => {
+ expect(app.formatPropertyValue({ a: 1 })).toBe('{...}');
+ });
+
+ it('formats number as string', () => {
+ expect(app.formatPropertyValue(42)).toBe('42');
+ });
+
+ it('formats boolean as string', () => {
+ expect(app.formatPropertyValue(true)).toBe('true');
+ });
+ });
+
+ describe('getPropertyTypeClass', () => {
+ it('returns type-string for string', () => {
+ expect(app.getPropertyTypeClass('string')).toBe('type-string');
+ });
+
+ it('returns type-number for number', () => {
+ expect(app.getPropertyTypeClass('number')).toBe('type-number');
+ });
+
+ it('returns type-boolean for boolean', () => {
+ expect(app.getPropertyTypeClass('boolean')).toBe('type-boolean');
+ });
+
+ it('returns type-null for null', () => {
+ expect(app.getPropertyTypeClass('null')).toBe('type-null');
+ });
+
+ it('returns type-object for object', () => {
+ expect(app.getPropertyTypeClass('object')).toBe('type-object');
+ });
+
+ it('returns type-function for function', () => {
+ expect(app.getPropertyTypeClass('function')).toBe('type-function');
+ });
+
+ it('returns type-default for unknown', () => {
+ expect(app.getPropertyTypeClass('unknown')).toBe('type-default');
+ });
+ });
+
+ describe('getPropertyRows', () => {
+ it('returns empty array for undefined properties', () => {
+ expect(app.getPropertyRows(undefined)).toEqual([]);
+ });
+
+ it('returns empty array for empty properties', () => {
+ expect(app.getPropertyRows([])).toEqual([]);
+ });
+
+ it('flattens properties with depth', () => {
+ const props = [
+ { name: 'a', value: 1 },
+ { name: 'b', value: 2 },
+ ];
+
+ const rows = app.getPropertyRows(props);
+
+ expect(rows).toHaveLength(2);
+ expect(rows[0].depth).toBe(0);
+ expect(rows[1].depth).toBe(0);
+ });
+
+ it('includes expanded nested properties', () => {
+ const props = [
+ {
+ name: 'obj',
+ value: {},
+ isExpanded: true,
+ expandedValue: {
+ properties: [
+ { name: 'nested', value: 'inner' }
+ ]
+ }
+ }
+ ];
+
+ const rows = app.getPropertyRows(props);
+
+ expect(rows).toHaveLength(2);
+ expect(rows[0].property.name).toBe('obj');
+ expect(rows[0].depth).toBe(0);
+ expect(rows[1].property.name).toBe('nested');
+ expect(rows[1].depth).toBe(1);
+ });
+ });
+
+ describe('expression evaluation', () => {
+ it('selectHistoryExpression sets expressionInput', () => {
+ app.selectHistoryExpression('this.count');
+ expect(app.expressionInput).toBe('this.count');
+ });
+
+ it('clearExpressionResult clears all result state', () => {
+ app.expressionResult = 'some result';
+ app.expressionResultType = 'string';
+ app.expressionError = 'some error';
+
+ app.clearExpressionResult();
+
+ expect(app.expressionResult).toBe('');
+ expect(app.expressionResultType).toBe('');
+ expect(app.expressionError).toBe('');
+ });
+ });
+
+ describe('revealInElements', () => {
+ it('calls debugHost with component info', () => {
+ app.selectedElement = ci('my-component', 'my-key');
+ app.selectedNodeType = 'custom-element';
+ app.selectedElementAttributes = [];
+
+ app.revealInElements();
+
+ expect(debugHost.revealInElements).toHaveBeenCalledWith({
+ name: 'my-component',
+ type: 'custom-element',
+ customElementInfo: app.selectedElement,
+ customAttributesInfo: [],
+ });
+ });
+
+ it('does nothing when no selectedElement', () => {
+ app.selectedElement = null;
+
+ app.revealInElements();
+
+ expect(debugHost.revealInElements).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('checkExtensionInvalidated', () => {
+ it('returns false when chrome.runtime.id exists', () => {
+ expect(app.checkExtensionInvalidated()).toBe(false);
+ expect(app.extensionInvalidated).toBe(false);
+ });
+
+ it('returns true and sets flag when chrome.runtime.id missing', () => {
+ const originalId = (global as any).chrome.runtime.id;
+ delete (global as any).chrome.runtime.id;
+
+ expect(app.checkExtensionInvalidated()).toBe(true);
+ expect(app.extensionInvalidated).toBe(true);
+
+ (global as any).chrome.runtime.id = originalId;
+ });
+ });
+
+ describe('component tree', () => {
+ it('loadComponentTree populates componentTree from debugHost', async () => {
+ const mockTree = [
+ { key: 'app', name: 'App', tagName: 'app', type: 'custom-element', hasChildren: true, childCount: 1 },
+ { key: 'header', name: 'Header', tagName: 'header', type: 'custom-element', hasChildren: false, childCount: 0 },
+ ];
+ debugHost.getComponentTree.mockResolvedValue(mockTree);
+
+ await app.loadComponentTree();
+
+ expect(app.componentTree).toEqual(mockTree);
+ expect(app.treeRevision).toBe(1);
+ });
+
+ it('loadComponentTree handles errors gracefully', async () => {
+ debugHost.getComponentTree.mockRejectedValue(new Error('Network error'));
+
+ await app.loadComponentTree();
+
+ expect(app.componentTree).toEqual([]);
+ });
+
+ it('toggleTreePanel toggles isTreePanelExpanded', () => {
+ expect(app.isTreePanelExpanded).toBe(true);
+
+ app.toggleTreePanel();
+ expect(app.isTreePanelExpanded).toBe(false);
+
+ app.toggleTreePanel();
+ expect(app.isTreePanelExpanded).toBe(true);
+ });
+
+ it('toggleTreeNode expands node with children', () => {
+ const node = { key: 'app', name: 'App', hasChildren: true };
+
+ app.toggleTreeNode(node);
+
+ expect(app.expandedTreeNodes.has('app')).toBe(true);
+ expect(app.treeRevision).toBe(1);
+ });
+
+ it('toggleTreeNode collapses already expanded node', () => {
+ const node = { key: 'app', name: 'App', hasChildren: true };
+ app.expandedTreeNodes.add('app');
+
+ app.toggleTreeNode(node);
+
+ expect(app.expandedTreeNodes.has('app')).toBe(false);
+ });
+
+ it('toggleTreeNode does nothing for node without children', () => {
+ const node = { key: 'leaf', name: 'Leaf', hasChildren: false };
+
+ app.toggleTreeNode(node);
+
+ expect(app.expandedTreeNodes.has('leaf')).toBe(false);
+ });
+
+ it('selectTreeNode updates selection and calls debugHost', () => {
+ const node = { key: 'my-component', name: 'MyComponent' };
+
+ app.selectTreeNode(node);
+
+ expect(app.selectedTreeNodeKey).toBe('my-component');
+ expect(debugHost.selectComponentByKey).toHaveBeenCalledWith('my-component');
+ });
+
+ it('getTreeRows returns empty for empty tree', () => {
+ app.componentTree = [];
+ expect(app.getTreeRows()).toEqual([]);
+ });
+
+ it('getTreeRows flattens tree with depth', () => {
+ app.componentTree = [
+ { key: 'app', name: 'App', hasChildren: true, children: [
+ { key: 'child', name: 'Child', hasChildren: false }
+ ]}
+ ];
+ app.expandedTreeNodes.add('app');
+
+ const rows = app.getTreeRows();
+
+ expect(rows).toHaveLength(2);
+ expect(rows[0].node.key).toBe('app');
+ expect(rows[0].depth).toBe(0);
+ expect(rows[1].node.key).toBe('child');
+ expect(rows[1].depth).toBe(1);
+ });
+
+ it('getTreeRows excludes collapsed children', () => {
+ app.componentTree = [
+ { key: 'app', name: 'App', hasChildren: true, children: [
+ { key: 'child', name: 'Child', hasChildren: false }
+ ]}
+ ];
+
+ const rows = app.getTreeRows();
+
+ expect(rows).toHaveLength(1);
+ expect(rows[0].node.key).toBe('app');
+ });
+
+ it('isTreeNodeExpanded returns correct state', () => {
+ const node = { key: 'test' };
+ expect(app.isTreeNodeExpanded(node)).toBe(false);
+
+ app.expandedTreeNodes.add('test');
+ expect(app.isTreeNodeExpanded(node)).toBe(true);
+ });
+
+ it('isTreeNodeSelected returns correct state', () => {
+ const node = { key: 'test' };
+ expect(app.isTreeNodeSelected(node)).toBe(false);
+
+ app.selectedTreeNodeKey = 'test';
+ expect(app.isTreeNodeSelected(node)).toBe(true);
+ });
+
+ it('hasComponentTree returns true when tree has nodes', () => {
+ app.componentTree = [];
+ expect(app.hasComponentTree).toBe(false);
+
+ app.componentTree = [{ key: 'app', name: 'App' }];
+ expect(app.hasComponentTree).toBe(true);
+ });
+
+ it('componentTreeCount counts all nodes including nested', () => {
+ app.componentTree = [
+ { key: 'app', name: 'App', children: [
+ { key: 'child1', name: 'Child1' },
+ { key: 'child2', name: 'Child2', children: [
+ { key: 'grandchild', name: 'GrandChild' }
+ ]}
+ ]},
+ { key: 'footer', name: 'Footer' }
+ ];
+
+ expect(app.componentTreeCount).toBe(5);
+ });
+ });
+
+ describe('timeline / interaction recorder', () => {
+ it('startRecording sets isRecording and calls debugHost', async () => {
+ await app.startRecording();
+
+ expect(app.isRecording).toBe(true);
+ expect(debugHost.startInteractionRecording).toHaveBeenCalled();
+ });
+
+ it('stopRecording clears isRecording and calls debugHost', async () => {
+ app.isRecording = true;
+
+ await app.stopRecording();
+
+ expect(app.isRecording).toBe(false);
+ expect(debugHost.stopInteractionRecording).toHaveBeenCalled();
+ });
+
+ it('clearTimeline clears events and calls debugHost', () => {
+ app.timelineEvents = [{ id: 'evt-1' }, { id: 'evt-2' }] as any;
+ app.expandedTimelineEvents.add('evt-1');
+
+ app.clearTimeline();
+
+ expect(app.timelineEvents).toEqual([]);
+ expect(app.expandedTimelineEvents.size).toBe(0);
+ expect(debugHost.clearInteractionLog).toHaveBeenCalled();
+ });
+
+ it('toggleTimelineEvent expands event', () => {
+ const event = { id: 'evt-1', eventName: 'click' } as any;
+ app.timelineEvents = [event];
+
+ app.toggleTimelineEvent(event);
+
+ expect(app.expandedTimelineEvents.has('evt-1')).toBe(true);
+ });
+
+ it('toggleTimelineEvent collapses already expanded event', () => {
+ const event = { id: 'evt-1', eventName: 'click' } as any;
+ app.timelineEvents = [event];
+ app.expandedTimelineEvents.add('evt-1');
+
+ app.toggleTimelineEvent(event);
+
+ expect(app.expandedTimelineEvents.has('evt-1')).toBe(false);
+ });
+
+ it('isTimelineEventExpanded returns correct state', () => {
+ const event = { id: 'evt-1' } as any;
+
+ expect(app.isTimelineEventExpanded(event)).toBe(false);
+
+ app.expandedTimelineEvents.add('evt-1');
+ expect(app.isTimelineEventExpanded(event)).toBe(true);
+ });
+
+ it('selectTimelineComponent calls debugHost when target has componentKey', () => {
+ const event = { id: 'evt-1', target: { componentKey: 'my-component' } } as any;
+
+ app.selectTimelineComponent(event);
+
+ expect(debugHost.selectComponentByKey).toHaveBeenCalledWith('my-component');
+ });
+
+ it('selectTimelineComponent does nothing when no target componentKey', () => {
+ const event = { id: 'evt-1', target: null } as any;
+
+ app.selectTimelineComponent(event);
+
+ expect(debugHost.selectComponentByKey).not.toHaveBeenCalled();
+ });
+
+ it('hasTimelineEvents returns true when events exist', () => {
+ app.timelineEvents = [];
+ expect(app.hasTimelineEvents).toBe(false);
+
+ app.timelineEvents = [{ id: 'evt-1' }] as any;
+ expect(app.hasTimelineEvents).toBe(true);
+ });
+
+ it('timelineEventCount returns event count', () => {
+ app.timelineEvents = [{ id: '1' }, { id: '2' }, { id: '3' }] as any;
+ expect(app.timelineEventCount).toBe(3);
+ });
+
+ it('formatTimelineTimestamp formats timestamp with milliseconds', () => {
+ const timestamp = new Date('2024-01-15T10:30:45.123Z').getTime();
+ const result = app.formatTimelineTimestamp(timestamp);
+
+ expect(result).toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/);
+ });
+
+ it('getTimelineEventTypeClass returns correct class for event types', () => {
+ expect(app.getTimelineEventTypeClass('property-change')).toBe('event-property');
+ expect(app.getTimelineEventTypeClass('lifecycle')).toBe('event-lifecycle');
+ expect(app.getTimelineEventTypeClass('interaction')).toBe('event-interaction');
+ expect(app.getTimelineEventTypeClass('binding')).toBe('event-binding');
+ expect(app.getTimelineEventTypeClass('unknown')).toBe('event-default');
+ });
+ });
+
+ describe('template debugger', () => {
+ it('hasTemplateInfo returns false when no snapshot', () => {
+ app.templateSnapshot = null;
+ expect(app.hasTemplateInfo).toBe(false);
+ });
+
+ it('hasTemplateInfo returns false when snapshot has no bindings or controllers', () => {
+ app.templateSnapshot = {
+ componentKey: 'test',
+ componentName: 'test',
+ bindings: [],
+ controllers: [],
+ instructions: [],
+ hasSlots: false,
+ shadowMode: 'none',
+ isContainerless: false,
+ };
+ expect(app.hasTemplateInfo).toBe(false);
+ });
+
+ it('hasTemplateInfo returns true when snapshot has bindings', () => {
+ app.templateSnapshot = {
+ componentKey: 'test',
+ componentName: 'test',
+ bindings: [{ id: 'b1', type: 'property', expression: 'foo', target: 'bar', value: 1, valueType: 'number', isBound: true }],
+ controllers: [],
+ instructions: [],
+ hasSlots: false,
+ shadowMode: 'none',
+ isContainerless: false,
+ } as any;
+ expect(app.hasTemplateInfo).toBe(true);
+ });
+
+ it('templateBindings returns empty array when no snapshot', () => {
+ app.templateSnapshot = null;
+ expect(app.templateBindings).toEqual([]);
+ });
+
+ it('templateBindings returns bindings from snapshot', () => {
+ const bindings = [{ id: 'b1', type: 'property', expression: 'foo' }];
+ app.templateSnapshot = { bindings } as any;
+ expect(app.templateBindings).toEqual(bindings);
+ });
+
+ it('templateControllers returns empty array when no snapshot', () => {
+ app.templateSnapshot = null;
+ expect(app.templateControllers).toEqual([]);
+ });
+
+ it('templateControllers returns controllers from snapshot', () => {
+ const controllers = [{ id: 'c1', type: 'if', isActive: true }];
+ app.templateSnapshot = { controllers } as any;
+ expect(app.templateControllers).toEqual(controllers);
+ });
+
+ it('toggleBindingExpand adds binding to expandedBindings', () => {
+ app.templateSnapshot = { bindings: [], controllers: [] } as any;
+ const binding = { id: 'b1' } as any;
+
+ app.toggleBindingExpand(binding);
+
+ expect(app.expandedBindings.has('b1')).toBe(true);
+ });
+
+ it('toggleBindingExpand removes binding from expandedBindings', () => {
+ app.templateSnapshot = { bindings: [], controllers: [] } as any;
+ app.expandedBindings.add('b1');
+ const binding = { id: 'b1' } as any;
+
+ app.toggleBindingExpand(binding);
+
+ expect(app.expandedBindings.has('b1')).toBe(false);
+ });
+
+ it('isBindingExpanded returns correct state', () => {
+ const binding = { id: 'b1' } as any;
+
+ expect(app.isBindingExpanded(binding)).toBe(false);
+
+ app.expandedBindings.add('b1');
+ expect(app.isBindingExpanded(binding)).toBe(true);
+ });
+
+ it('toggleControllerExpand adds controller to expandedControllers', () => {
+ app.templateSnapshot = { bindings: [], controllers: [] } as any;
+ const controller = { id: 'c1' } as any;
+
+ app.toggleControllerExpand(controller);
+
+ expect(app.expandedControllers.has('c1')).toBe(true);
+ });
+
+ it('isControllerExpanded returns correct state', () => {
+ const controller = { id: 'c1' } as any;
+
+ expect(app.isControllerExpanded(controller)).toBe(false);
+
+ app.expandedControllers.add('c1');
+ expect(app.isControllerExpanded(controller)).toBe(true);
+ });
+
+ it('getBindingTypeIcon returns correct icons', () => {
+ expect(app.getBindingTypeIcon('property')).toBe('→');
+ expect(app.getBindingTypeIcon('listener')).toBe('⚡');
+ expect(app.getBindingTypeIcon('interpolation')).toBe('${}');
+ expect(app.getBindingTypeIcon('unknown')).toBe('•');
+ });
+
+ it('getBindingModeClass returns correct classes', () => {
+ expect(app.getBindingModeClass('oneTime')).toBe('mode-one-time');
+ expect(app.getBindingModeClass('toView')).toBe('mode-to-view');
+ expect(app.getBindingModeClass('fromView')).toBe('mode-from-view');
+ expect(app.getBindingModeClass('twoWay')).toBe('mode-two-way');
+ expect(app.getBindingModeClass('default')).toBe('mode-default');
+ });
+
+ it('getBindingModeLabel returns correct labels', () => {
+ expect(app.getBindingModeLabel('oneTime')).toBe('one-time');
+ expect(app.getBindingModeLabel('toView')).toBe('→');
+ expect(app.getBindingModeLabel('fromView')).toBe('←');
+ expect(app.getBindingModeLabel('twoWay')).toBe('↔');
+ });
+
+ it('getControllerTypeIcon returns correct icons', () => {
+ expect(app.getControllerTypeIcon('if')).toBe('❓');
+ expect(app.getControllerTypeIcon('repeat')).toBe('↻');
+ expect(app.getControllerTypeIcon('unknown')).toBe('◆');
+ });
+
+ it('formatBindingValue formats values correctly', () => {
+ expect(app.formatBindingValue(undefined)).toBe('undefined');
+ expect(app.formatBindingValue(null)).toBe('null');
+ expect(app.formatBindingValue('hello')).toBe('"hello"');
+ expect(app.formatBindingValue(42)).toBe('42');
+ expect(app.formatBindingValue({ a: 1 })).toBe('{"a":1}');
+ });
+ });
+});
diff --git a/tests/sidebar-debug-host.spec.ts b/tests/sidebar-debug-host.spec.ts
new file mode 100644
index 0000000..dbb2b31
--- /dev/null
+++ b/tests/sidebar-debug-host.spec.ts
@@ -0,0 +1,542 @@
+import './setup';
+import { ChromeTest } from './setup';
+
+let SidebarDebugHostClass: any;
+
+function createMockConsumer() {
+ return {
+ followChromeSelection: true,
+ onElementPicked: jest.fn(),
+ onPropertyChanges: jest.fn(),
+ };
+}
+
+describe('SidebarDebugHost', () => {
+ let host: any;
+ let consumer: any;
+
+ beforeEach(async () => {
+ ChromeTest.reset();
+ jest.resetModules();
+ jest.useFakeTimers();
+
+ const mod = await import('@/sidebar/sidebar-debug-host');
+ SidebarDebugHostClass = mod.SidebarDebugHost;
+
+ host = new SidebarDebugHostClass();
+ consumer = createMockConsumer();
+ });
+
+ afterEach(() => {
+ jest.useRealTimers();
+ });
+
+ describe('attach', () => {
+ it('sets consumer reference', () => {
+ host.attach(consumer);
+ expect(host.consumer).toBe(consumer);
+ });
+
+ it('registers selection changed listener', () => {
+ host.attach(consumer);
+
+ expect((global as any).chrome.devtools.panels.elements.onSelectionChanged.hasListeners()).toBe(true);
+ });
+ });
+
+ describe('updateValues', () => {
+ it('calls inspectedWindow.eval with correct arguments', () => {
+ const componentInfo = { name: 'test', key: 'test-key' };
+ const property = { name: 'value', value: 42 };
+
+ host.updateValues(componentInfo, property);
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const call = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(call).toContain('updateValues');
+ });
+ });
+
+ describe('element picker', () => {
+ beforeEach(() => {
+ host.attach(consumer);
+ });
+
+ it('startElementPicker injects picker code', () => {
+ host.startElementPicker();
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('aurelia-picker');
+ });
+
+ it('startElementPicker starts polling', () => {
+ host.startElementPicker();
+
+ expect(host.pickerPollingInterval).not.toBeNull();
+ });
+
+ it('stopElementPicker stops polling', () => {
+ host.startElementPicker();
+ host.stopElementPicker();
+
+ expect(host.pickerPollingInterval).toBeNull();
+ });
+
+ it('stopElementPicker calls cleanup function', () => {
+ host.stopElementPicker();
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('__aureliaDevtoolsStopPicker');
+ });
+ });
+
+ describe('property watching', () => {
+ beforeEach(() => {
+ host.attach(consumer);
+ });
+
+ it('startPropertyWatching sets watching state', async () => {
+ ChromeTest.setEvalToReturn([
+ { result: false },
+ { result: null }
+ ]);
+
+ host.startPropertyWatching({ componentKey: 'test-key', pollInterval: 500 });
+
+ expect(host.watchingComponentKey).toBe('test-key');
+
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(host.propertyWatchInterval).not.toBeNull();
+ });
+
+ it('startPropertyWatching uses event-driven watching when available', async () => {
+ ChromeTest.setEvalToReturn([{ result: true }]);
+
+ host.startPropertyWatching({ componentKey: 'test-key', pollInterval: 500 });
+
+ await Promise.resolve();
+ await Promise.resolve();
+
+ expect(host.watchingComponentKey).toBe('test-key');
+ expect((host as any).useEventDrivenWatching).toBe(true);
+ expect(host.propertyWatchInterval).toBeNull();
+ });
+
+ it('stopPropertyWatching clears watching state', async () => {
+ ChromeTest.setEvalToReturn([
+ { result: false },
+ { result: null }
+ ]);
+ host.startPropertyWatching({ componentKey: 'test-key', pollInterval: 500 });
+
+ await Promise.resolve();
+ await Promise.resolve();
+
+ host.stopPropertyWatching();
+
+ expect(host.watchingComponentKey).toBeNull();
+ expect(host.propertyWatchInterval).toBeNull();
+ });
+
+ it('stopPropertyWatching detaches event-driven watchers', async () => {
+ ChromeTest.setEvalToReturn([{ result: true }]);
+ host.startPropertyWatching({ componentKey: 'test-key' });
+
+ await Promise.resolve();
+ await Promise.resolve();
+
+ ChromeTest.setEvalToReturn([{ result: undefined }]);
+ host.stopPropertyWatching();
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ expect(host.watchingComponentKey).toBeNull();
+ expect((host as any).useEventDrivenWatching).toBe(false);
+ });
+ });
+
+ describe('searchComponents', () => {
+ it('returns empty array when no chrome.devtools', async () => {
+ ChromeTest.removeChrome();
+
+ const results = await host.searchComponents('test');
+
+ expect(results).toEqual([]);
+
+ ChromeTest.restoreChrome();
+ });
+
+ it('calls inspectedWindow.eval with search query', async () => {
+ ChromeTest.setEvalToReturn([{
+ result: [
+ { key: 'comp-1', name: 'my-component', type: 'custom-element' }
+ ]
+ }]);
+
+ const promise = host.searchComponents('my');
+
+ const results = await promise;
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ expect(results).toHaveLength(1);
+ expect(results[0].name).toBe('my-component');
+ });
+
+ it('returns empty array when result is not array', async () => {
+ ChromeTest.setEvalToReturn([{ result: null }]);
+
+ const results = await host.searchComponents('test');
+
+ expect(results).toEqual([]);
+ });
+ });
+
+ describe('selectComponentByKey', () => {
+ beforeEach(() => {
+ host.attach(consumer);
+ });
+
+ it('calls inspectedWindow.eval with component key', () => {
+ ChromeTest.setEvalToReturn([{ result: null }]);
+
+ host.selectComponentByKey('my-key');
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('my-key');
+ });
+
+ it('notifies consumer when component found', () => {
+ const componentInfo = {
+ customElementInfo: { name: 'test', key: 'test-key' },
+ customAttributesInfo: []
+ };
+ ChromeTest.setEvalToReturn([{ result: componentInfo }]);
+
+ host.selectComponentByKey('test-key');
+
+ expect(consumer.onElementPicked).toHaveBeenCalledWith(componentInfo);
+ });
+ });
+
+ describe('revealInElements', () => {
+ it('calls inspectedWindow.eval with component info', () => {
+ ChromeTest.setEvalToReturn([{ result: true }]);
+
+ const componentInfo = {
+ name: 'test',
+ type: 'custom-element',
+ customElementInfo: { name: 'test' },
+ customAttributesInfo: []
+ };
+
+ host.revealInElements(componentInfo);
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('findElementByComponentInfo');
+ expect(code).toContain('inspect');
+ });
+ });
+
+ describe('enhanced info methods', () => {
+ it('getLifecycleHooks calls hook method', async () => {
+ ChromeTest.setEvalToReturn([{
+ result: { hooks: [{ name: 'attached', implemented: true }] }
+ }]);
+
+ const result = await host.getLifecycleHooks('test-key');
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('getLifecycleHooks');
+ expect(result.hooks).toHaveLength(1);
+ });
+
+ it('getComputedProperties returns array', async () => {
+ ChromeTest.setEvalToReturn([{
+ result: [{ name: 'fullName', hasGetter: true }]
+ }]);
+
+ const result = await host.getComputedProperties('test-key');
+
+ expect(result).toHaveLength(1);
+ expect(result[0].name).toBe('fullName');
+ });
+
+ it('getComputedProperties returns empty array on null', async () => {
+ ChromeTest.setEvalToReturn([{ result: null }]);
+
+ const result = await host.getComputedProperties('test-key');
+
+ expect(result).toEqual([]);
+ });
+
+ it('getDependencies calls hook method', async () => {
+ ChromeTest.setEvalToReturn([{
+ result: { dependencies: [{ name: 'HttpClient', type: 'class' }] }
+ }]);
+
+ const result = await host.getDependencies('test-key');
+
+ expect(result.dependencies).toHaveLength(1);
+ });
+
+ it('getRouteInfo calls hook method', async () => {
+ ChromeTest.setEvalToReturn([{
+ result: { currentRoute: '/users', params: [] }
+ }]);
+
+ const result = await host.getRouteInfo('test-key');
+
+ expect(result.currentRoute).toBe('/users');
+ });
+
+ it('getSlotInfo calls hook method', async () => {
+ ChromeTest.setEvalToReturn([{
+ result: { slots: [{ name: 'default', hasContent: true }] }
+ }]);
+
+ const result = await host.getSlotInfo('test-key');
+
+ expect(result.slots).toHaveLength(1);
+ });
+ });
+
+ describe('getPropertySnapshot', () => {
+ it('returns null when no chrome.devtools', async () => {
+ ChromeTest.removeChrome();
+
+ const result = await host.getPropertySnapshot('test-key');
+
+ expect(result).toBeNull();
+
+ ChromeTest.restoreChrome();
+ });
+
+ it('returns snapshot from eval', async () => {
+ const snapshot = {
+ componentKey: 'test-key',
+ bindables: [{ name: 'value', value: 1, type: 'number' }],
+ properties: [],
+ timestamp: Date.now()
+ };
+ ChromeTest.setEvalToReturn([{ result: snapshot }]);
+
+ const result = await host.getPropertySnapshot('test-key');
+
+ expect(result).toEqual(snapshot);
+ });
+ });
+
+ describe('property snapshot diffing', () => {
+ it('detects changes between snapshots', () => {
+ const oldSnap = {
+ componentKey: 'test',
+ bindables: [{ name: 'count', value: 1, type: 'number' }],
+ properties: [],
+ timestamp: 1000
+ };
+
+ const newSnap = {
+ componentKey: 'test',
+ bindables: [{ name: 'count', value: 2, type: 'number' }],
+ properties: [],
+ timestamp: 2000
+ };
+
+ const changes = host.diffPropertySnapshots(oldSnap, newSnap);
+
+ expect(changes).toHaveLength(1);
+ expect(changes[0].propertyName).toBe('count');
+ expect(changes[0].oldValue).toBe(1);
+ expect(changes[0].newValue).toBe(2);
+ });
+
+ it('returns empty array when no changes', () => {
+ const oldSnap = {
+ componentKey: 'test',
+ bindables: [{ name: 'count', value: 1, type: 'number' }],
+ properties: [],
+ timestamp: 1000
+ };
+
+ const newSnap = {
+ componentKey: 'test',
+ bindables: [{ name: 'count', value: 1, type: 'number' }],
+ properties: [],
+ timestamp: 2000
+ };
+
+ const changes = host.diffPropertySnapshots(oldSnap, newSnap);
+
+ expect(changes).toHaveLength(0);
+ });
+ });
+
+ describe('valuesEqual', () => {
+ it('returns true for identical primitives', () => {
+ expect(host.valuesEqual(1, 1)).toBe(true);
+ expect(host.valuesEqual('test', 'test')).toBe(true);
+ expect(host.valuesEqual(true, true)).toBe(true);
+ });
+
+ it('returns false for different primitives', () => {
+ expect(host.valuesEqual(1, 2)).toBe(false);
+ expect(host.valuesEqual('a', 'b')).toBe(false);
+ });
+
+ it('returns true for equal objects', () => {
+ expect(host.valuesEqual({ a: 1 }, { a: 1 })).toBe(true);
+ });
+
+ it('returns false for different objects', () => {
+ expect(host.valuesEqual({ a: 1 }, { a: 2 })).toBe(false);
+ });
+
+ it('handles null values', () => {
+ expect(host.valuesEqual(null, null)).toBe(true);
+ expect(host.valuesEqual(null, 1)).toBe(false);
+ });
+ });
+
+ describe('getComponentTree', () => {
+ it('returns component tree from hook', async () => {
+ const mockTree = [
+ { key: 'app', name: 'App', tagName: 'app', type: 'custom-element', hasChildren: true },
+ { key: 'header', name: 'Header', tagName: 'header', type: 'custom-element', hasChildren: false },
+ ];
+ ChromeTest.setEvalToReturn([{ result: mockTree }]);
+
+ const result = await host.getComponentTree();
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('getSimplifiedComponentTree');
+ expect(result).toEqual(mockTree);
+ });
+
+ it('returns empty array when result is not array', async () => {
+ ChromeTest.setEvalToReturn([{ result: null }]);
+
+ const result = await host.getComponentTree();
+
+ expect(result).toEqual([]);
+ });
+
+ it('returns empty array when chrome.devtools unavailable', async () => {
+ const original = (global as any).chrome.devtools;
+ delete (global as any).chrome.devtools;
+
+ const result = await host.getComponentTree();
+
+ expect(result).toEqual([]);
+
+ (global as any).chrome.devtools = original;
+ });
+ });
+
+ describe('timeline / interaction recording', () => {
+ it('startInteractionRecording calls hook and returns true on success', async () => {
+ ChromeTest.setEvalToReturn([{ result: true }]);
+
+ const result = await host.startInteractionRecording();
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('startInteractionRecording');
+ expect(result).toBe(true);
+ });
+
+ it('startInteractionRecording returns false on failure', async () => {
+ ChromeTest.setEvalToReturn([{ result: false }]);
+
+ const result = await host.startInteractionRecording();
+
+ expect(result).toBe(false);
+ });
+
+ it('startInteractionRecording returns false when chrome.devtools unavailable', async () => {
+ const original = (global as any).chrome.devtools;
+ delete (global as any).chrome.devtools;
+
+ const result = await host.startInteractionRecording();
+
+ expect(result).toBe(false);
+
+ (global as any).chrome.devtools = original;
+ });
+
+ it('stopInteractionRecording calls hook and returns true on success', async () => {
+ ChromeTest.setEvalToReturn([{ result: true }]);
+
+ const result = await host.stopInteractionRecording();
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('stopInteractionRecording');
+ expect(result).toBe(true);
+ });
+
+ it('stopInteractionRecording returns false on failure', async () => {
+ ChromeTest.setEvalToReturn([{ result: false }]);
+
+ const result = await host.stopInteractionRecording();
+
+ expect(result).toBe(false);
+ });
+
+ it('clearInteractionLog calls hook method', () => {
+ ChromeTest.setEvalToReturn([{ result: undefined }]);
+
+ host.clearInteractionLog();
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('clearInteractionLog');
+ });
+
+ it('clearInteractionLog does nothing when chrome.devtools unavailable', () => {
+ const original = (global as any).chrome.devtools;
+ delete (global as any).chrome.devtools;
+
+ expect(() => host.clearInteractionLog()).not.toThrow();
+
+ (global as any).chrome.devtools = original;
+ });
+ });
+
+ describe('getTemplateSnapshot', () => {
+ it('returns template snapshot from hook', async () => {
+ const mockSnapshot = {
+ componentKey: 'test-key',
+ componentName: 'test',
+ bindings: [{ id: 'b1', type: 'property', expression: 'foo' }],
+ controllers: [],
+ instructions: [],
+ hasSlots: false,
+ shadowMode: 'none',
+ isContainerless: false,
+ };
+ ChromeTest.setEvalToReturn([{ result: mockSnapshot }]);
+
+ const result = await host.getTemplateSnapshot('test-key');
+
+ expect((global as any).chrome.devtools.inspectedWindow.eval).toHaveBeenCalled();
+ const code = (global as any).chrome.devtools.inspectedWindow.eval.mock.calls[0][0];
+ expect(code).toContain('getTemplateSnapshot');
+ expect(result).toEqual(mockSnapshot);
+ });
+
+ it('returns null when no result', async () => {
+ ChromeTest.setEvalToReturn([{ result: null }]);
+
+ const result = await host.getTemplateSnapshot('test-key');
+
+ expect(result).toBeNull();
+ });
+ });
+});
diff --git a/vite.config.mjs b/vite.config.mjs
index 5a2bcca..4c4a7e2 100644
--- a/vite.config.mjs
+++ b/vite.config.mjs
@@ -16,7 +16,7 @@ export default defineConfig(({ mode }) => {
targets: [
{ src: 'src/popups', dest: 'dist' },
{ src: 'images', dest: 'dist' },
- { src: 'index.html', dest: 'dist' },
+ { src: 'sidebar.html', dest: 'dist' },
{ src: 'manifest.json', dest: 'dist' },
{ src: 'src/devtools', dest: 'dist' },
],
@@ -28,7 +28,7 @@ export default defineConfig(({ mode }) => {
minify: false,
rollupOptions: {
input: {
- 'build/entry': resolve(__dirname, 'src/main.ts'),
+ 'build/sidebar': resolve(__dirname, 'src/sidebar/main.ts'),
'build/detector': resolve(__dirname, 'src/detector/detector.ts'),
'build/background': resolve(__dirname, 'src/background/background.ts'),
'build/contentscript': resolve(__dirname, 'src/contentscript/contentscript.ts'),