From db0962ba8f05ff0d04812f5d95c450edb9fcdfc7 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 25 Feb 2026 20:05:32 +0800 Subject: [PATCH 01/31] test(kitten-lynx): impl vitest test suite, tap interaction, and AGENTS docs --- .../testing-library/kitten-lynx/AGENTS.md | 43 + .../testing-library/kitten-lynx/Dockerfile | 50 + .../testing-library/kitten-lynx/package.json | 39 + .../kitten-lynx/src/CDPChannel.ts | 163 ++ .../kitten-lynx/src/ElementNode.ts | 96 ++ .../testing-library/kitten-lynx/src/Lynx.ts | 92 + .../kitten-lynx/src/LynxView.ts | 147 ++ .../testing-library/kitten-lynx/src/index.ts | 4 + .../kitten-lynx/start-emulator.sh | 28 + .../kitten-lynx/tests/lynx.spec.ts | 33 + .../testing-library/kitten-lynx/tsconfig.json | 19 + .../kitten-lynx/vitest.config.ts | 9 + pnpm-lock.yaml | 1499 ++++++++++++++++- 13 files changed, 2186 insertions(+), 36 deletions(-) create mode 100644 packages/testing-library/kitten-lynx/AGENTS.md create mode 100644 packages/testing-library/kitten-lynx/Dockerfile create mode 100644 packages/testing-library/kitten-lynx/package.json create mode 100644 packages/testing-library/kitten-lynx/src/CDPChannel.ts create mode 100644 packages/testing-library/kitten-lynx/src/ElementNode.ts create mode 100644 packages/testing-library/kitten-lynx/src/Lynx.ts create mode 100644 packages/testing-library/kitten-lynx/src/LynxView.ts create mode 100644 packages/testing-library/kitten-lynx/src/index.ts create mode 100644 packages/testing-library/kitten-lynx/start-emulator.sh create mode 100644 packages/testing-library/kitten-lynx/tests/lynx.spec.ts create mode 100644 packages/testing-library/kitten-lynx/tsconfig.json create mode 100644 packages/testing-library/kitten-lynx/vitest.config.ts diff --git a/packages/testing-library/kitten-lynx/AGENTS.md b/packages/testing-library/kitten-lynx/AGENTS.md new file mode 100644 index 0000000000..a322377d1d --- /dev/null +++ b/packages/testing-library/kitten-lynx/AGENTS.md @@ -0,0 +1,43 @@ +# @lynx-test/kitten-lynx + +This document provides context, architecture guidelines, and workflows for agents interacting with the `kitten-lynx` framework. + +## Overview + +`kitten-lynx` is a Puppeteer-like testing library designed for interacting with the Lynx browser engine and Lynx Explorer Android application. It utilizes the `@lynx-js/debug-router-connector` to establish WebSocket connections via ADB to an Android Emulator running the `com.lynx.explorer` app. + +Through the Chrome DevTools Protocol (CDP), `kitten-lynx` enables: + +- Starting and tearing down `LynxView` instances. +- Spawning pages using deep link intents (`adb shell am start`). +- Navigating and reading the DOM structure. +- Querying elements via `DOM.querySelector`. +- Reading styles, attributes, and precise boundary boxes of elements. +- Simulating native touches through `Input.emulateTouchFromMouseEvent`. + +## Architecture Details + +### Connections & Sessions + +1. **Lynx.ts**: The entry point. Handles `DebugRouterConnector` initialization, spawning devices, waiting for a client attachment, and enabling DevTools switches inside Lynx (`enable_devtool`). +2. **LynxView.ts**: Manages individual pages. Executes `adb` intents to start the Lynx Shell Activity for a specific URL, then establishes a CDP session utilizing `onAttachedToTarget`. +3. **CDPChannel.ts**: A wrapper that abstracts sending and receiving asynchronous `Customized` CDP commands over the router. +4. **ElementNode.ts**: A wrapper around `nodeId`s matching an element. Implements interactive methods like `getAttribute()`, `computedStyleMap()`, and `tap()`. + +### Prerequisites + +For the library to interact successfully: + +- Host machine must have Docker installed with the `ubuntu-24.04-emu` image containing the Android SDK and configured ADB. +- Inside the emulator, the Lynx Explorer APK must be installed. +- ADB port `5555` should be exposed or forwarded to control the emulator programmatically. +- Typical commands use `pnpm run test` starting `vitest` logic inside the Node wrapper. + +## Adding Features + +When extending the `kitten-lynx` testing library, adhere to these rules: + +1. **Protocol Typings**: Only update `Protocol` types in `src/CDPChannel.ts` when implementing new standard CDP requests (e.g. `Page.reload`, `DOM.getOuterHTML`). +2. **Puppeteer Equivalency**: Maintain an API design similar to Puppeteer/Playwright. Add element-level logic inside the `ElementNode` class (e.g., `type()`, `boundingBox()`) and page-level logic inside `LynxView` (e.g., `evaluate()`, `screenshot()`). +3. **Session Reconnection**: Be mindful of device timeouts. CDP requests time out after 5000ms. Keep connection tests tolerant of emulator boot/warm-up times gracefully in test suites (`tests/lynx.spec.ts`). +4. **Vitest Verification**: Before pushing feature changes, verify functionality using `pnpm run build && pnpm run test` inside the package root folder. diff --git a/packages/testing-library/kitten-lynx/Dockerfile b/packages/testing-library/kitten-lynx/Dockerfile new file mode 100644 index 0000000000..85f529013b --- /dev/null +++ b/packages/testing-library/kitten-lynx/Dockerfile @@ -0,0 +1,50 @@ +FROM ubuntu:24.04 + +# Avoid prompts from apt +ENV DEBIAN_FRONTEND=noninteractive + +# Install dependencies required for Android SDK and Emulator +RUN apt-get update && apt-get install -y \ + openjdk-17-jdk \ + wget \ + unzip \ + git \ + curl \ + cpu-checker \ + qemu-kvm \ + libvirt-daemon-system \ + libvirt-clients \ + bridge-utils \ + socat \ + && rm -rf /var/lib/apt/lists/* + +# Set up Android SDK environment variables +ENV ANDROID_HOME=/opt/android-sdk +ENV PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator + +# Download and install Android command line tools +RUN mkdir -p ${ANDROID_HOME}/cmdline-tools && \ + cd ${ANDROID_HOME}/cmdline-tools && \ + wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdline-tools.zip && \ + unzip -q cmdline-tools.zip && \ + rm cmdline-tools.zip && \ + mv cmdline-tools latest + +# Accept licenses and install required SDK components +RUN yes | sdkmanager --licenses && \ + sdkmanager "platform-tools" "platforms;android-34" "emulator" "system-images;android-34;google_apis;x86_64" + +# Create Android Virtual Device (AVD) +RUN echo "no" | avdmanager create avd -n test_device -k "system-images;android-34;google_apis;x86_64" --device "pixel" --force + +# Download Lynx Explorer APK +RUN curl -L -o /opt/LynxExplorer.apk https://github.com/lynx-family/lynx/releases/download/3.6.0/LynxExplorer-noasan-release.apk + +# Add startup script +COPY start-emulator.sh /start-emulator.sh +RUN chmod +x /start-emulator.sh + +# Expose ADB port +EXPOSE 5555 + +ENTRYPOINT ["/start-emulator.sh"] diff --git a/packages/testing-library/kitten-lynx/package.json b/packages/testing-library/kitten-lynx/package.json new file mode 100644 index 0000000000..e2fd96a232 --- /dev/null +++ b/packages/testing-library/kitten-lynx/package.json @@ -0,0 +1,39 @@ +{ + "name": "@lynx-test/kitten-lynx", + "version": "0.1.0", + "description": "A testing framework executing the Lynx explorer Android application", + "keywords": [ + "Lynx", + "testing", + "android", + "emulator" + ], + "author": { + "name": "lynx-stack" + }, + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "test": "vitest run" + }, + "dependencies": { + "@lynx-js/debug-router-connector": "0.0.6" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.0.0" + } +} diff --git a/packages/testing-library/kitten-lynx/src/CDPChannel.ts b/packages/testing-library/kitten-lynx/src/CDPChannel.ts new file mode 100644 index 0000000000..6d4ab51f2a --- /dev/null +++ b/packages/testing-library/kitten-lynx/src/CDPChannel.ts @@ -0,0 +1,163 @@ +import type { DebugRouterConnector } from '@lynx-js/debug-router-connector'; + +const sessionIdToChannel: Record> = {}; +type Quad = [number, number, number, number, number, number, number, number]; +export interface NodeInfoInGetDocument { + nodeId: number; + children: NodeInfoInGetDocument[]; + attributes: string[]; + nodeName: string; +} +interface Protocol { + 'DOM.getDocument': { + params: { + depth: number; + pierce?: boolean; + }; + return: { + root: NodeInfoInGetDocument; + }; + }; + 'DOM.querySelector': { + params: { + nodeId: number; + selector: string; + }; + return: { + nodeId: number; + }; + }; + 'DOM.getAttributes': { + params: { + nodeId: number; + }; + return: { + attributes: string[]; + }; + }; + 'DOM.getBoxModel': { + params: { + nodeId: number; + }; + return: { + model: { + content: Quad; + padding: Quad; + border: Quad; + margin: Quad; + width: number; + height: number; + }; + }; + }; + 'CSS.getComputedStyleForNode': { + params: { + nodeId: number; + }; + return: { + computedStyle: { name: string; value: string }[]; + }; + }; + 'Input.emulateTouchFromMouseEvent': { + params: { + type: 'mousePressed' | 'mouseReleased' | 'mouseMoved'; + x: number; + y: number; + timestamp: number; + button: 'left' | 'middle' | 'right'; + deltaX?: number; + deltaY?: number; + }; + return: unknown; + }; +} + +export class CDPChannel { + private _id = 0; + static from( + sessionId: number, + clientId: number, + connector: DebugRouterConnector, + ): CDPChannel { + const maybeChannel = sessionIdToChannel[sessionId]?.deref(); + if (maybeChannel) return maybeChannel; + else { + const channel = new CDPChannel(connector, clientId, sessionId); + sessionIdToChannel[sessionId] = new WeakRef(channel); + return channel; + } + } + constructor( + private _connector: DebugRouterConnector, + private _clientId: number, + private _sessionId: number, + ) {} + + async send( + method: T, + params: Protocol[T]['params'], + ): Promise { + const id = ++this._id; + + const { promise, resolve, reject } = Promise.withResolvers< + Protocol[T]['return'] + >(); + + const msgId = `CDP-${id}`; + const listener = ( + { message, id: sourceId }: { message: string; id: number }, + ) => { + const parsed = JSON.parse(message); + if (parsed.event !== 'Customized' || parsed.data?.type !== 'CDP') { + return; + } + + const cdpData = parsed.data.data; + if (cdpData.session_id !== this._sessionId) return; + + const cdpMessage = typeof cdpData.message === 'string' + ? JSON.parse(cdpData.message) + : cdpData.message; + if (cdpMessage.id !== id) return; + + this._connector.off('usb-client-message', listener); + + if (cdpMessage.error) { + reject(new Error(cdpMessage.error.message)); + } else { + resolve(cdpMessage.result); + } + }; + + this._connector.on('usb-client-message', listener); + + this._connector.sendMessageToApp( + this._clientId, + JSON.stringify({ + event: 'Customized', + data: { + type: 'CDP', + data: { + client_id: this._clientId, + session_id: this._sessionId, + message: { + method, + id, + params, + }, + }, + sender: this._clientId, + }, + from: this._clientId, + }), + ); + + // Add simple timeout + setTimeout(() => { + this._connector.off('usb-client-message', listener); + reject(new Error(`Timeout waiting for CDP method: ${method}`)); + }, 5000); + + return promise; + } +} diff --git a/packages/testing-library/kitten-lynx/src/ElementNode.ts b/packages/testing-library/kitten-lynx/src/ElementNode.ts new file mode 100644 index 0000000000..576d635ed8 --- /dev/null +++ b/packages/testing-library/kitten-lynx/src/ElementNode.ts @@ -0,0 +1,96 @@ +import type { LynxView } from './LynxView.js'; + +const idToElementNode = new WeakMap[]>(); + +export class ElementNode { + static fromId(id: number, lynxView: LynxView): ElementNode { + const currentViewMap = idToElementNode.get(lynxView); + if (currentViewMap) { + const couldBeElementNode = currentViewMap[id]?.deref(); + if (couldBeElementNode) return couldBeElementNode; + } + + const node = new ElementNode(id, lynxView); + const ref = new WeakRef(node); + + if (currentViewMap) { + currentViewMap[id] = ref; + } else { + const newMap: WeakRef[] = []; + newMap[id] = ref; + idToElementNode.set(lynxView, newMap); + } + return node; + } + + constructor(public readonly nodeId: number, private _lynxView: LynxView) {} + + async tap(): Promise { + const { model } = await this._lynxView._channel.send('DOM.getBoxModel', { + nodeId: this.nodeId, + }); + + // Calculate center coordinates + let x = 0, y = 0; + // Content box is usually defined by 8 coordinates [x1,y1, x2,y2, x3,y3, x4,y4] + // Top-left and bottom-right average gives center + if (model.content && model.content.length === 8) { + x = (model.content[0]! + model.content[4]!) / 2; + y = (model.content[1]! + model.content[5]!) / 2; + } else { + throw new Error( + `Could not determine coordinates for node ${this.nodeId}`, + ); + } + + const timestamp = Date.now(); + await this._lynxView._channel.send('Input.emulateTouchFromMouseEvent', { + type: 'mousePressed', + x, + y, + button: 'left', + timestamp, + }); + + // Wait briefly to simulate actual human tap duration + await new Promise(resolve => setTimeout(resolve, 50)); + + await this._lynxView._channel.send('Input.emulateTouchFromMouseEvent', { + type: 'mouseReleased', + x, + y, + button: 'left', + timestamp: timestamp + 50, + }); + } + + async getAttribute(name: string): Promise { + if (name === 'id') { + name = 'idSelector'; + } + const ret = await this._lynxView._channel.send('DOM.getAttributes', { + nodeId: this.nodeId, + }); + const attributes: Record = {}; + for (let ii = 0; ii < ret.attributes.length - 1; ii += 2) { + const attrName = ret.attributes[ii]!; + const value = ret.attributes[ii + 1]!; + attributes[attrName] = value; + } + return attributes[name] ?? null; + } + + async computedStyleMap(): Promise> { + const map = new Map(); + const ret = await this._lynxView._channel.send( + 'CSS.getComputedStyleForNode', + { + nodeId: this.nodeId, + }, + ); + for (const style of ret.computedStyle) { + map.set(style.name, style.value); + } + return map; + } +} diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts new file mode 100644 index 0000000000..2e3ed1d7e8 --- /dev/null +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -0,0 +1,92 @@ +import { + DebugRouterConnector, + MultiOpenStatus, +} from '@lynx-js/debug-router-connector'; +import { LynxView } from './LynxView.js'; + +export class Lynx { + private _connector: DebugRouterConnector | null = null; + private _currentClientId: number = -1; + + static async connect(): Promise { + const lynx = new Lynx(); + + lynx._connector = new DebugRouterConnector({ + manualConnect: true, + enableWebSocket: true, // Used for local debugging in docker/adb + enableDesktop: true, + }); + + const { promise: startedPromise, resolve: startedResolve } = Promise + .withResolvers(); + + // Auto-remove connector upon disconnect + lynx._connector.setMultiOpenCallback({ + statusChanged(status: MultiOpenStatus) { + if (status === MultiOpenStatus.unattached) { + lynx._connector = null; + } + }, + }); + + await lynx._connector.startWSServer(); + + lynx._connector.on('device-connected', (device) => { + device.startWatchClient(); + }); + + const { promise: clientPromise, resolve: clientResolve } = Promise + .withResolvers(); + + lynx._connector.on('client-connected', async (client) => { + startedResolve(); // Unblock initial wait + clientResolve(client.clientId()); + }); + + if (lynx._connector.devices.size === 0) { + const devices = await lynx._connector.connectDevices(1000); + if (devices.length === 0) { + throw new Error('Failed to connect to Lynx: no device found.'); + } + } + + // Wait until a client is attached + lynx._currentClientId = await clientPromise; + + // Force enable devtools toggle if required + lynx._connector.sendMessageToApp( + lynx._currentClientId, + JSON.stringify({ + event: 'Customized', + data: { + type: 'SetGlobalSwitch', + data: { + client_id: lynx._currentClientId, + message: JSON.stringify({ + global_key: 'enable_devtool', + global_value: true, + id: 10000, + }), + session_id: -1, + }, + sender: lynx._currentClientId, + }, + from: lynx._currentClientId, + }), + ); + + return lynx; + } + + async newPage(): Promise { + if (!this._connector || this._currentClientId === -1) { + throw new Error('Not connected. Call Lynx.connect() first.'); + } + return new LynxView(this._connector, this._currentClientId); + } + + async close(): Promise { + // Optional: terminate the connector properly + // Note: API bindings for `close` on `DebugRouterConnector` depend on upstream + } +} diff --git a/packages/testing-library/kitten-lynx/src/LynxView.ts b/packages/testing-library/kitten-lynx/src/LynxView.ts new file mode 100644 index 0000000000..2429139f91 --- /dev/null +++ b/packages/testing-library/kitten-lynx/src/LynxView.ts @@ -0,0 +1,147 @@ +import type { DebugRouterConnector } from '@lynx-js/debug-router-connector'; +import { CDPChannel } from './CDPChannel.js'; +import type { NodeInfoInGetDocument } from './CDPChannel.js'; +import { ElementNode } from './ElementNode.js'; + +const idToLynxView: Record> = {}; + +export class LynxView { + private static incId = 1; + private _root?: ElementNode; + _channel!: CDPChannel; + readonly id: number; + + static getLynxViewById(id: string): LynxView | undefined { + return idToLynxView[id]?.deref(); + } + + constructor( + private _connector: DebugRouterConnector, + private _clientId: number, + ) { + this.id = LynxView.incId++; + idToLynxView[this.id.toString()] = new WeakRef(this); + } + + async goto(url: string, _options?: unknown): Promise { + const { exec } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const execAsync = promisify(exec); + + // Launch the URL through adb using the custom launcher intent + await execAsync( + `adb shell am start -n com.lynx.explorer/.LynxViewShellActivity -d "${url}"`, + ); + + // Wait for the page to attach to CDP + await new Promise(resolve => setTimeout(resolve, 1000)); + + const { promise, resolve, reject } = Promise.withResolvers(); + let isSettled = false; + const listener = ({ message, id }: { message: string; id: number }) => { + if (id !== this._clientId) return; + const parsed = JSON.parse(message); + if ( + parsed.event === 'Customized' && parsed.data?.type === 'SessionList' + ) { + const sessions = parsed.data.data; + if (sessions.length > 0) { + isSettled = true; + this._connector.off('usb-client-message', listener); + resolve(sessions[sessions.length - 1].session_id); + } + } + }; + this._connector.on('usb-client-message', listener); + + // Periodically ask for SessionList until settled or timeout + const pollInterval = setInterval(() => { + if (isSettled) { + clearInterval(pollInterval); + return; + } + this._connector.sendMessageToApp( + this._clientId, + JSON.stringify({ + event: 'Customized', + data: { + type: 'ListSession', + data: [], + sender: this._clientId, + }, + from: this._clientId, + }), + ); + }, 500); + + setTimeout(() => { + if (!isSettled) { + isSettled = true; + clearInterval(pollInterval); + this._connector.off('usb-client-message', listener); + reject(new Error('Timeout waiting for session')); + } + }, 5000); + + const sessionId = await promise; + await this.onAttachedToTarget(sessionId); + } + + async locator(selector: string): Promise { + if (!this._root) { + throw new Error('Not connected to a document yet. Call goto() first.'); + } + const { nodeId } = await this._channel.send('DOM.querySelector', { + nodeId: this._root.nodeId, + selector, + }); + if (nodeId !== -1) { + return ElementNode.fromId(nodeId, this); + } + return; + } + + async onAttachedToTarget(sessionId: number) { + if (!this._channel) { + this._channel = CDPChannel.from( + sessionId, + this._clientId, + this._connector, + ); + // Enable DOM agent + await this._channel.send('Runtime.enable' as any, {}); // Enable Runtime to get events + const response = await this._channel.send('DOM.getDocument', { + depth: -1, + }); + const root = response.root.children[0]!; + this._root = ElementNode.fromId(root.nodeId, this); + } + } + + #contentToStringImpl(buffer: string[], node: NodeInfoInGetDocument) { + const tagName = node.nodeName.toLowerCase(); + buffer.push('<', tagName); + for (let ii = 0; ii < node.attributes.length; ii += 2) { + let key = node.attributes[ii]!.toLowerCase(); + const value = node.attributes[ii + 1]!; + if (key === 'idselector') { + key = 'id'; + } + buffer.push(' ', key, '="', value, '"'); + } + buffer.push('>'); + for (const child of node.children) { + this.#contentToStringImpl(buffer, child); + } + buffer.push(''); + } + + async content(): Promise { + const document = await this._channel.send('DOM.getDocument', { + depth: -1, + }); + const buffer: string[] = []; + this.#contentToStringImpl(buffer, document.root); + return buffer.join(''); + } +} diff --git a/packages/testing-library/kitten-lynx/src/index.ts b/packages/testing-library/kitten-lynx/src/index.ts new file mode 100644 index 0000000000..a23e135c6b --- /dev/null +++ b/packages/testing-library/kitten-lynx/src/index.ts @@ -0,0 +1,4 @@ +export { Lynx } from './Lynx.js'; +export { LynxView } from './LynxView.js'; +export { ElementNode } from './ElementNode.js'; +export { CDPChannel } from './CDPChannel.js'; diff --git a/packages/testing-library/kitten-lynx/start-emulator.sh b/packages/testing-library/kitten-lynx/start-emulator.sh new file mode 100644 index 0000000000..d802deae40 --- /dev/null +++ b/packages/testing-library/kitten-lynx/start-emulator.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -e + +# Start the Android emulator in headless mode +echo "Starting emulator..." +${ANDROID_HOME}/emulator/emulator -avd test_device -no-window -no-audio -gpu swiftshader_indirect & + +# Wait for the emulator to boot +echo "Waiting for emulator to become ready..." +adb wait-for-device +while [ "$(adb shell getprop sys.boot_completed | tr -d '\r')" != "1" ]; do + sleep 2 +done + +echo "Emulator is ready." + +echo "Installing Lynx Explorer..." +adb install -r /opt/LynxExplorer.apk + +echo "Launching Lynx Explorer..." +adb shell monkey -p com.lynx.explorer -c android.intent.category.LAUNCHER 1 + +# Forward ADB port 5555 to make scrcpy work from the host via port 5556 +echo "Setting up ADB port forwarding..." +socat TCP-LISTEN:5556,fork,bind=0.0.0.0 TCP:127.0.0.1:5555 & + +# Keep the container running +tail -f /dev/null diff --git a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts new file mode 100644 index 0000000000..ea790bea2a --- /dev/null +++ b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts @@ -0,0 +1,33 @@ +import { Lynx } from '../src/Lynx.js'; +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; + +describe('kitten-lynx testing framework', () => { + let lynx: Lynx; + + beforeAll(async () => { + lynx = await Lynx.connect(); + }); + + afterAll(async () => { + // Assuming disconnect exists or processes will naturally end + }); + + it('can navigate to a page and read the DOM', async () => { + const page = await lynx.newPage(); + await page.goto('https://lynxjs.org/api/react/index.bundle'); + + const content = await page.content(); + expect(content).toContain('= 10'} + '@aws-crypto/crc32@5.2.0': + resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + + '@aws-crypto/sha256-browser@5.2.0': + resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} + + '@aws-crypto/sha256-js@5.2.0': + resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} + engines: {node: '>=16.0.0'} + + '@aws-crypto/supports-web-crypto@5.2.0': + resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} + + '@aws-crypto/util@5.2.0': + resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + + '@aws-sdk/client-s3@3.669.0': + resolution: {integrity: sha512-diqkwSqWjgAZ5NdYOtpYI/JlwsFDmo7x2Q+QOHevFaCEp4vgZOwJyXE3DQbKoO1X10fMH2vOUULgonPJd4ClUQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/client-sso-oidc@3.669.0': + resolution: {integrity: sha512-E7uYOS3Axhe36Zeq6iLC9kjF1mMEyCQ4fXud11h22rbjq7PFUtN2Omekrch37eUx3BFj1jMePnuTnT98t5LWnw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.669.0 + + '@aws-sdk/client-sso@3.669.0': + resolution: {integrity: sha512-WNpfNYIHzehLv98F+KolJglXNjJOTbOvIbSZ2XAnebVLmXCWeEEd1r4dIH0oI2dHtLbQ/h3uqaeQhsVQjLAxpw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/client-sts@3.669.0': + resolution: {integrity: sha512-1XdOBtHKCVxVkEDiy+oktJNSsySj3ADyh58KpHaqgvCQKV3vmfkr0YO5dG4kqq+TflNwdfl1YgMuOUiCulOWeQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/core@3.667.0': + resolution: {integrity: sha512-pMcDVI7Tmdsc8R3sDv0Omj/4iRParGY+uJtAfF669WnZfDfaBQaix2Mq7+Mu08vdjqO9K3gicFvjk9S1VLmOKA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-env@3.667.0': + resolution: {integrity: sha512-zZbrkkaPc54WXm+QAnpuv0LPNfsts0HPPd+oCECGs7IQRaFsGj187cwvPg9RMWDFZqpm64MdBDoA8OQHsqzYCw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-http@3.667.0': + resolution: {integrity: sha512-sjtybFfERZWiqTY7fswBxKQLvUkiCucOWyqh3IaPo/4nE1PXRnaZCVG0+kRBPrYIxWqiVwytvZzMJy8sVZcG0A==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-ini@3.669.0': + resolution: {integrity: sha512-YHhfH7w29BmMPnOK0BrBhEy2IRFRSRGSCyz3jtqpG883CZ2Lxan/AzaJDfKRdz350KPgbMMBwbPkIrqNIsg8iw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.669.0 + + '@aws-sdk/credential-provider-node@3.669.0': + resolution: {integrity: sha512-O506azQcq6N1gnDX870MXXL9LHlhX0k6BlLMM6IDClxVDnlNkK3+n2cAEKSy8HwZJcRlekcsKz/AS2CxjPY+fg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-process@3.667.0': + resolution: {integrity: sha512-HZHnvop32fKgsNHkdhVaul7UzQ25sEc0j9yqA4bjhtbk0ECl42kj3f1pJ+ZU/YD9ut8lMJs/vVqiOdNThVdeBw==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-sso@3.669.0': + resolution: {integrity: sha512-HvpMJQ8xZuEGjadARVOfORPZx4U23PC5Jf6Fj+/NWK4VxEXhvf8J037fvGp3xRds5wUeuBBbhWX+Cbt0lbLCwQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.667.0': + resolution: {integrity: sha512-t8CFlZMD/1p/8Cli3rvRiTJpjr/8BO64gw166AHgFZYSN2h95L2l1tcW0jpsc3PprA32nLg1iQVKYt4WGM4ugw==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sts': ^3.667.0 + + '@aws-sdk/middleware-bucket-endpoint@3.667.0': + resolution: {integrity: sha512-XGz4jMAkDoTyFdtLz7ZF+C05IAhCTC1PllpvTBaj821z/L0ilhbqVhrT/f2Buw8Id/K5A390csGXgusXyrFFjA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-expect-continue@3.667.0': + resolution: {integrity: sha512-0TiSL9S5DSG95NHGIz6qTMuV7GDKVn8tvvGSrSSZu/wXO3JaYSH0AElVpYfc4PtPRqVpEyNA7nnc7W56mMCLWQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-flexible-checksums@3.669.0': + resolution: {integrity: sha512-01UQLoUzVwWMf+b+AEuwJ2lluBD+Cp8AcbyEHqvEaPdjGKHIS4BCvnY70mZYnAfRtL8R2h9tt7iI61oWU3Gjkg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-host-header@3.667.0': + resolution: {integrity: sha512-Z7fIAMQnPegs7JjAQvlOeWXwpMRfegh5eCoIP6VLJIeR6DLfYKbP35JBtt98R6DXslrN2RsbTogjbxPEDQfw1w==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-location-constraint@3.667.0': + resolution: {integrity: sha512-ob85H3HhT3/u5O+x0o557xGZ78vSNeSSwMaSitxdsfs2hOuoUl1uk+OeLpi1hkuJnL41FPpokV7TVII2XrFfmg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-logger@3.667.0': + resolution: {integrity: sha512-PtTRNpNm/5c746jRgZCNg4X9xEJIwggkGJrF0GP9AB1ANg4pc/sF2Fvn1NtqPe9wtQ2stunJprnm5WkCHN7QiA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.667.0': + resolution: {integrity: sha512-U5glWD3ehFohzpUpopLtmqAlDurGWo2wRGPNgi4SwhWU7UDt6LS7E/UvJjqC0CUrjlzOw+my2A+Ncf+fisMhxQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-sdk-s3@3.669.0': + resolution: {integrity: sha512-b2QUQ7DcIcVCUFhvmFEDI90BemvQhO0ntIajllLqQSy88PSNdLDCVx5mIzfxaaK/1tdY/UsEDRRm1kMQHJDQpg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-ssec@3.667.0': + resolution: {integrity: sha512-1wuAUZIkmZIvOmGg5qNQU821CGFHhkuKioxXgNh0DpUxZ9+AeiV7yorJr+bqkb2KBFv1i1TnzGRecvKf/KvZIQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/middleware-user-agent@3.669.0': + resolution: {integrity: sha512-K8ScPi45zjJrj5Y2gRqVsvKKQCQbvQBfYGcBw9ZOx9TTavH80bOCBjWg/GFnvs4f37tqVc1wMN2oGvcTF6HveQ==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/region-config-resolver@3.667.0': + resolution: {integrity: sha512-iNr+JhhA902JMKHG9IwT9YdaEx6KGl6vjAL5BRNeOjfj4cZYMog6Lz/IlfOAltMtT0w88DAHDEFrBd2uO0l2eg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/signature-v4-multi-region@3.669.0': + resolution: {integrity: sha512-TVwlWAxfBHnFjnfTBQWUhzVJzjwVhkq1+KR0JZV7JrfqeyBOdZjAaV9ie3VNY9HUouecq1fDuKaSwe4JiWQsHg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/token-providers@3.667.0': + resolution: {integrity: sha512-ZecJlG8p6D4UTYlBHwOWX6nknVtw/OBJ3yPXTSajBjhUlj9lE2xvejI8gl4rqkyLXk7z3bki+KR4tATbMaM9yg==} + engines: {node: '>=16.0.0'} + peerDependencies: + '@aws-sdk/client-sso-oidc': ^3.667.0 + + '@aws-sdk/types@3.667.0': + resolution: {integrity: sha512-gYq0xCsqFfQaSL/yT1Gl1vIUjtsg7d7RhnUfsXaHt8xTxOKRTdH9GjbesBjXOzgOvB0W0vfssfreSNGFlOOMJg==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/util-arn-parser@3.568.0': + resolution: {integrity: sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/util-endpoints@3.667.0': + resolution: {integrity: sha512-X22SYDAuQJWnkF1/q17pkX3nGw5XMD9YEUbmt87vUnRq7iyJ3JOpl6UKOBeUBaL838wA5yzdbinmCITJ/VZ1QA==} + engines: {node: '>=16.0.0'} + + '@aws-sdk/util-locate-window@3.965.4': + resolution: {integrity: sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-user-agent-browser@3.667.0': + resolution: {integrity: sha512-y1pKlNzNpxzddM0QSnfIfIbi3Z9LTag1VDjYyZRbEGGSVip2J00qKsET+979nRezWMyJgw5GPBQR3Y+rN+jh0Q==} + + '@aws-sdk/util-user-agent-node@3.669.0': + resolution: {integrity: sha512-9jxCYrgggy2xd44ZASqI7AMiRVaSiFp+06Kg8BQSU0ijKpBJlwcsqIS8pDT/n6LxuOw2eV5ipvM2C0r1iKzrGA==} + engines: {node: '>=16.0.0'} + peerDependencies: + aws-crt: '>=1.0.0' + peerDependenciesMeta: + aws-crt: + optional: true + + '@aws-sdk/xml-builder@3.662.0': + resolution: {integrity: sha512-ikLkXn0igUpnJu2mCZjklvmcDGWT9OaLRv3JyC/cRkTaaSrblPjPM7KKsltxdMTLQ+v7fjCN0TsJpxphMfaOPA==} + engines: {node: '>=16.0.0'} + '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -2532,6 +2708,19 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@devicefarmer/adbkit-logcat@2.1.3': + resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==} + engines: {node: '>= 4'} + + '@devicefarmer/adbkit-monkey@1.2.1': + resolution: {integrity: sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==} + engines: {node: '>= 0.10.4'} + + '@devicefarmer/adbkit@3.2.4': + resolution: {integrity: sha512-8Iq5aGMD5jUvg86rJnU4zbsYfIaWMl7Ch+BhGnVRD1BqaVs0fQAUOMQzqGrFe01GlbBx3GX5T0ppNQC5IizL3g==} + engines: {node: '>= 0.10.4'} + hasBin: true + '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -3199,6 +3388,9 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} + '@lynx-js/debug-router-connector@0.0.6': + resolution: {integrity: sha512-ZJAFRfG/zzfAF7+GxtWuT/VTRNaTt0FDvlDiJJdHhHY44YenCvuPSuDnKnqO3U8TREtLrJVDBKgrAvTik6NgRA==} + '@lynx-js/internal-preact@10.28.4-ee7bb26': resolution: {integrity: sha512-Ym60mgGjj/Xc/5yfludUCUXVCyVACUsNaA7E9Uct2yArsT5nlvp9bflUoLrTNMvApKvbhUBCTx27aNYCquhEYg==} @@ -4155,6 +4347,212 @@ packages: '@sinclair/typebox@0.34.37': resolution: {integrity: sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==} + '@smithy/abort-controller@3.1.9': + resolution: {integrity: sha512-yiW0WI30zj8ZKoSYNx90no7ugVn3khlyH/z5W8qtKBtVE6awRALbhSG+2SAHA1r6bO/6M9utxYKVZ3PCJ1rWxw==} + engines: {node: '>=16.0.0'} + + '@smithy/chunked-blob-reader-native@3.0.1': + resolution: {integrity: sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==} + + '@smithy/chunked-blob-reader@4.0.0': + resolution: {integrity: sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==} + + '@smithy/config-resolver@3.0.13': + resolution: {integrity: sha512-Gr/qwzyPaTL1tZcq8WQyHhTZREER5R1Wytmz4WnVGL4onA3dNk6Btll55c8Vr58pLdvWZmtG8oZxJTw3t3q7Jg==} + engines: {node: '>=16.0.0'} + + '@smithy/core@2.5.7': + resolution: {integrity: sha512-8olpW6mKCa0v+ibCjoCzgZHQx1SQmZuW/WkrdZo73wiTprTH6qhmskT60QLFdT9DRa5mXxjz89kQPZ7ZSsoqqg==} + engines: {node: '>=16.0.0'} + + '@smithy/credential-provider-imds@3.2.8': + resolution: {integrity: sha512-ZCY2yD0BY+K9iMXkkbnjo+08T2h8/34oHd0Jmh6BZUSZwaaGlGCyBT/3wnS7u7Xl33/EEfN4B6nQr3Gx5bYxgw==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-codec@3.1.10': + resolution: {integrity: sha512-323B8YckSbUH0nMIpXn7HZsAVKHYHFUODa8gG9cHo0ySvA1fr5iWaNT+iIL0UCqUzG6QPHA3BSsBtRQou4mMqQ==} + + '@smithy/eventstream-serde-browser@3.0.14': + resolution: {integrity: sha512-kbrt0vjOIihW3V7Cqj1SXQvAI5BR8SnyQYsandva0AOR307cXAc+IhPngxIPslxTLfxwDpNu0HzCAq6g42kCPg==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-serde-config-resolver@3.0.11': + resolution: {integrity: sha512-P2pnEp4n75O+QHjyO7cbw/vsw5l93K/8EWyjNCAAybYwUmj3M+hjSQZ9P5TVdUgEG08ueMAP5R4FkuSkElZ5tQ==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-serde-node@3.0.13': + resolution: {integrity: sha512-zqy/9iwbj8Wysmvi7Lq7XFLeDgjRpTbCfwBhJa8WbrylTAHiAu6oQTwdY7iu2lxigbc9YYr9vPv5SzYny5tCXQ==} + engines: {node: '>=16.0.0'} + + '@smithy/eventstream-serde-universal@3.0.13': + resolution: {integrity: sha512-L1Ib66+gg9uTnqp/18Gz4MDpJPKRE44geOjOQ2SVc0eiaO5l255ADziATZgjQjqumC7yPtp1XnjHlF1srcwjKw==} + engines: {node: '>=16.0.0'} + + '@smithy/fetch-http-handler@3.2.9': + resolution: {integrity: sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==} + + '@smithy/fetch-http-handler@4.1.3': + resolution: {integrity: sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA==} + + '@smithy/hash-blob-browser@3.1.10': + resolution: {integrity: sha512-elwslXOoNunmfS0fh55jHggyhccobFkexLYC1ZeZ1xP2BTSrcIBaHV2b4xUQOdctrSNOpMqOZH1r2XzWTEhyfA==} + + '@smithy/hash-node@3.0.11': + resolution: {integrity: sha512-emP23rwYyZhQBvklqTtwetkQlqbNYirDiEEwXl2v0GYWMnCzxst7ZaRAnWuy28njp5kAH54lvkdG37MblZzaHA==} + engines: {node: '>=16.0.0'} + + '@smithy/hash-stream-node@3.1.10': + resolution: {integrity: sha512-olomK/jZQ93OMayW1zfTHwcbwBdhcZOHsyWyiZ9h9IXvc1mCD/VuvzbLb3Gy/qNJwI4MANPLctTp2BucV2oU/Q==} + engines: {node: '>=16.0.0'} + + '@smithy/invalid-dependency@3.0.11': + resolution: {integrity: sha512-NuQmVPEJjUX6c+UELyVz8kUx8Q539EDeNwbRyu4IIF8MeV7hUtq1FB3SHVyki2u++5XLMFqngeMKk7ccspnNyQ==} + + '@smithy/is-array-buffer@2.2.0': + resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} + engines: {node: '>=14.0.0'} + + '@smithy/is-array-buffer@3.0.0': + resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} + engines: {node: '>=16.0.0'} + + '@smithy/md5-js@3.0.11': + resolution: {integrity: sha512-3NM0L3i2Zm4bbgG6Ymi9NBcxXhryi3uE8fIfHJZIOfZVxOkGdjdgjR9A06SFIZCfnEIWKXZdm6Yq5/aPXFFhsQ==} + + '@smithy/middleware-content-length@3.0.13': + resolution: {integrity: sha512-zfMhzojhFpIX3P5ug7jxTjfUcIPcGjcQYzB9t+rv0g1TX7B0QdwONW+ATouaLoD7h7LOw/ZlXfkq4xJ/g2TrIw==} + engines: {node: '>=16.0.0'} + + '@smithy/middleware-endpoint@3.2.8': + resolution: {integrity: sha512-OEJZKVUEhMOqMs3ktrTWp7UvvluMJEvD5XgQwRePSbDg1VvBaL8pX8mwPltFn6wk1GySbcVwwyldL8S+iqnrEQ==} + engines: {node: '>=16.0.0'} + + '@smithy/middleware-retry@3.0.34': + resolution: {integrity: sha512-yVRr/AAtPZlUvwEkrq7S3x7Z8/xCd97m2hLDaqdz6ucP2RKHsBjEqaUA2ebNv2SsZoPEi+ZD0dZbOB1u37tGCA==} + engines: {node: '>=16.0.0'} + + '@smithy/middleware-serde@3.0.11': + resolution: {integrity: sha512-KzPAeySp/fOoQA82TpnwItvX8BBURecpx6ZMu75EZDkAcnPtO6vf7q4aH5QHs/F1s3/snQaSFbbUMcFFZ086Mw==} + engines: {node: '>=16.0.0'} + + '@smithy/middleware-stack@3.0.11': + resolution: {integrity: sha512-1HGo9a6/ikgOMrTrWL/WiN9N8GSVYpuRQO5kjstAq4CvV59bjqnh7TbdXGQ4vxLD3xlSjfBjq5t1SOELePsLnA==} + engines: {node: '>=16.0.0'} + + '@smithy/node-config-provider@3.1.12': + resolution: {integrity: sha512-O9LVEu5J/u/FuNlZs+L7Ikn3lz7VB9hb0GtPT9MQeiBmtK8RSY3ULmsZgXhe6VAlgTw0YO+paQx4p8xdbs43vQ==} + engines: {node: '>=16.0.0'} + + '@smithy/node-http-handler@3.3.3': + resolution: {integrity: sha512-BrpZOaZ4RCbcJ2igiSNG16S+kgAc65l/2hmxWdmhyoGWHTLlzQzr06PXavJp9OBlPEG/sHlqdxjWmjzV66+BSQ==} + engines: {node: '>=16.0.0'} + + '@smithy/property-provider@3.1.11': + resolution: {integrity: sha512-I/+TMc4XTQ3QAjXfOcUWbSS073oOEAxgx4aZy8jHaf8JQnRkq2SZWw8+PfDtBvLUjcGMdxl+YwtzWe6i5uhL/A==} + engines: {node: '>=16.0.0'} + + '@smithy/protocol-http@4.1.8': + resolution: {integrity: sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==} + engines: {node: '>=16.0.0'} + + '@smithy/querystring-builder@3.0.11': + resolution: {integrity: sha512-u+5HV/9uJaeLj5XTb6+IEF/dokWWkEqJ0XiaRRogyREmKGUgZnNecLucADLdauWFKUNbQfulHFEZEdjwEBjXRg==} + engines: {node: '>=16.0.0'} + + '@smithy/querystring-parser@3.0.11': + resolution: {integrity: sha512-Je3kFvCsFMnso1ilPwA7GtlbPaTixa3WwC+K21kmMZHsBEOZYQaqxcMqeFFoU7/slFjKDIpiiPydvdJm8Q/MCw==} + engines: {node: '>=16.0.0'} + + '@smithy/service-error-classification@3.0.11': + resolution: {integrity: sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog==} + engines: {node: '>=16.0.0'} + + '@smithy/shared-ini-file-loader@3.1.12': + resolution: {integrity: sha512-1xKSGI+U9KKdbG2qDvIR9dGrw3CNx+baqJfyr0igKEpjbHL5stsqAesYBzHChYHlelWtb87VnLWlhvfCz13H8Q==} + engines: {node: '>=16.0.0'} + + '@smithy/signature-v4@4.2.4': + resolution: {integrity: sha512-5JWeMQYg81TgU4cG+OexAWdvDTs5JDdbEZx+Qr1iPbvo91QFGzjy0IkXAKaXUHqmKUJgSHK0ZxnCkgZpzkeNTA==} + engines: {node: '>=16.0.0'} + + '@smithy/smithy-client@3.7.0': + resolution: {integrity: sha512-9wYrjAZFlqWhgVo3C4y/9kpc68jgiSsKUnsFPzr/MSiRL93+QRDafGTfhhKAb2wsr69Ru87WTiqSfQusSmWipA==} + engines: {node: '>=16.0.0'} + + '@smithy/types@3.7.2': + resolution: {integrity: sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==} + engines: {node: '>=16.0.0'} + + '@smithy/url-parser@3.0.11': + resolution: {integrity: sha512-TmlqXkSk8ZPhfc+SQutjmFr5FjC0av3GZP4B/10caK1SbRwe/v+Wzu/R6xEKxoNqL+8nY18s1byiy6HqPG37Aw==} + + '@smithy/util-base64@3.0.0': + resolution: {integrity: sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-body-length-browser@3.0.0': + resolution: {integrity: sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==} + + '@smithy/util-body-length-node@3.0.0': + resolution: {integrity: sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==} + engines: {node: '>=16.0.0'} + + '@smithy/util-buffer-from@2.2.0': + resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} + engines: {node: '>=14.0.0'} + + '@smithy/util-buffer-from@3.0.0': + resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} + engines: {node: '>=16.0.0'} + + '@smithy/util-config-provider@3.0.0': + resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-defaults-mode-browser@3.0.34': + resolution: {integrity: sha512-FumjjF631lR521cX+svMLBj3SwSDh9VdtyynTYDAiBDEf8YPP5xORNXKQ9j0105o5+ARAGnOOP/RqSl40uXddA==} + engines: {node: '>= 10.0.0'} + + '@smithy/util-defaults-mode-node@3.0.34': + resolution: {integrity: sha512-vN6aHfzW9dVVzkI0wcZoUXvfjkl4CSbM9nE//08lmUMyf00S75uuCpTrqF9uD4bD9eldIXlt53colrlwKAT8Gw==} + engines: {node: '>= 10.0.0'} + + '@smithy/util-endpoints@2.1.7': + resolution: {integrity: sha512-tSfcqKcN/Oo2STEYCABVuKgJ76nyyr6skGl9t15hs+YaiU06sgMkN7QYjo0BbVw+KT26zok3IzbdSOksQ4YzVw==} + engines: {node: '>=16.0.0'} + + '@smithy/util-hex-encoding@3.0.0': + resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-middleware@3.0.11': + resolution: {integrity: sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow==} + engines: {node: '>=16.0.0'} + + '@smithy/util-retry@3.0.11': + resolution: {integrity: sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-stream@3.3.4': + resolution: {integrity: sha512-SGhGBG/KupieJvJSZp/rfHHka8BFgj56eek9px4pp7lZbOF+fRiVr4U7A3y3zJD8uGhxq32C5D96HxsTC9BckQ==} + engines: {node: '>=16.0.0'} + + '@smithy/util-uri-escape@3.0.0': + resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} + engines: {node: '>=16.0.0'} + + '@smithy/util-utf8@2.3.0': + resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} + engines: {node: '>=14.0.0'} + + '@smithy/util-utf8@3.0.0': + resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} + engines: {node: '>=16.0.0'} + + '@smithy/util-waiter@3.2.0': + resolution: {integrity: sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg==} + engines: {node: '>=16.0.0'} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -4488,6 +4886,9 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@22.0.0': + resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==} + '@types/node@24.10.13': resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} @@ -4559,6 +4960,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@9.0.8': + resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} + '@types/webpack-sources@3.2.3': resolution: {integrity: sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==} @@ -4951,6 +5355,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + address@1.2.2: + resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} + engines: {node: '>= 10.0.0'} + agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} @@ -5180,6 +5588,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -5201,6 +5612,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bowser@2.14.1: + resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} + brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -5235,6 +5649,9 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + bufferpack@0.0.6: + resolution: {integrity: sha512-MTWvLHElqczrIVhge9qHtqgNigJFyh0+tCDId5yCbFAfuekHWIG+uAgvoHVflwrDPuY/e47JE1ki5qcM7w4uLg==} + builtin-modules@5.0.0: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} @@ -5705,6 +6122,15 @@ packages: supports-color: optional: true + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -5826,6 +6252,11 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + detect-port@1.6.1: + resolution: {integrity: sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==} + engines: {node: '>= 4.0.0'} + hasBin: true + devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -5890,6 +6321,9 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -6325,6 +6759,10 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fast-xml-parser@4.4.1: + resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} + hasBin: true + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -6885,6 +7323,9 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + ip@2.0.1: + resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -7829,6 +8270,9 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8187,6 +8631,10 @@ packages: engines: {node: '>=18'} hasBin: true + plist@3.0.6: + resolution: {integrity: sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==} + engines: {node: '>=6'} + plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} @@ -8488,6 +8936,14 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + q@1.5.1: + resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} + engines: {node: '>=0.6.0', teleport: '>=0.2.0'} + deprecated: |- + You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. + + (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) + qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -9246,6 +9702,9 @@ packages: resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} engines: {node: '>=6.0.0'} + split@1.0.1: + resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -9366,6 +9825,9 @@ packages: resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} engines: {node: '>=0.10.0'} + strnum@1.1.2: + resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} + style-to-js@1.1.16: resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} @@ -9755,6 +10217,9 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@6.11.1: + resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -9811,6 +10276,9 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + unzipper@0.12.3: + resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==} + upath@2.0.1: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} @@ -9850,6 +10318,9 @@ packages: '@types/react': optional: true + utf8@3.0.0: + resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -9861,6 +10332,10 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -10318,41 +10793,546 @@ snapshots: '@ast-grep/napi-win32-ia32-msvc': 0.37.0 '@ast-grep/napi-win32-x64-msvc': 0.37.0 - '@babel/code-frame@7.26.2': + '@aws-crypto/crc32@5.2.0': dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.667.0 + tslib: 2.8.1 - '@babel/code-frame@7.29.0': + '@aws-crypto/crc32c@5.2.0': dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.667.0 + tslib: 2.8.1 - '@babel/compat-data@7.28.6': {} + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.667.0 + '@aws-sdk/util-locate-window': 3.965.4 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 - '@babel/core@7.29.0': + '@aws-crypto/sha256-browser@5.2.0': dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color + '@aws-crypto/sha256-js': 5.2.0 + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.667.0 + '@aws-sdk/util-locate-window': 3.965.4 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 - '@babel/generator@7.28.3': + '@aws-crypto/sha256-js@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.667.0 + tslib: 2.8.1 + + '@aws-crypto/supports-web-crypto@5.2.0': + dependencies: + tslib: 2.8.1 + + '@aws-crypto/util@5.2.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/util-utf8': 2.3.0 + tslib: 2.8.1 + + '@aws-sdk/client-s3@3.669.0': + dependencies: + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.669.0(@aws-sdk/client-sts@3.669.0) + '@aws-sdk/client-sts': 3.669.0 + '@aws-sdk/core': 3.667.0 + '@aws-sdk/credential-provider-node': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0) + '@aws-sdk/middleware-bucket-endpoint': 3.667.0 + '@aws-sdk/middleware-expect-continue': 3.667.0 + '@aws-sdk/middleware-flexible-checksums': 3.669.0 + '@aws-sdk/middleware-host-header': 3.667.0 + '@aws-sdk/middleware-location-constraint': 3.667.0 + '@aws-sdk/middleware-logger': 3.667.0 + '@aws-sdk/middleware-recursion-detection': 3.667.0 + '@aws-sdk/middleware-sdk-s3': 3.669.0 + '@aws-sdk/middleware-ssec': 3.667.0 + '@aws-sdk/middleware-user-agent': 3.669.0 + '@aws-sdk/region-config-resolver': 3.667.0 + '@aws-sdk/signature-v4-multi-region': 3.669.0 + '@aws-sdk/types': 3.667.0 + '@aws-sdk/util-endpoints': 3.667.0 + '@aws-sdk/util-user-agent-browser': 3.667.0 + '@aws-sdk/util-user-agent-node': 3.669.0 + '@aws-sdk/xml-builder': 3.662.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.7 + '@smithy/eventstream-serde-browser': 3.0.14 + '@smithy/eventstream-serde-config-resolver': 3.0.11 + '@smithy/eventstream-serde-node': 3.0.13 + '@smithy/fetch-http-handler': 3.2.9 + '@smithy/hash-blob-browser': 3.1.10 + '@smithy/hash-node': 3.0.11 + '@smithy/hash-stream-node': 3.1.10 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/md5-js': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.8 + '@smithy/middleware-retry': 3.0.34 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.34 + '@smithy/util-defaults-mode-node': 3.0.34 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-stream': 3.3.4 + '@smithy/util-utf8': 3.0.0 + '@smithy/util-waiter': 3.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0)': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sts': 3.669.0 + '@aws-sdk/core': 3.667.0 + '@aws-sdk/credential-provider-node': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0) + '@aws-sdk/middleware-host-header': 3.667.0 + '@aws-sdk/middleware-logger': 3.667.0 + '@aws-sdk/middleware-recursion-detection': 3.667.0 + '@aws-sdk/middleware-user-agent': 3.669.0 + '@aws-sdk/region-config-resolver': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@aws-sdk/util-endpoints': 3.667.0 + '@aws-sdk/util-user-agent-browser': 3.667.0 + '@aws-sdk/util-user-agent-node': 3.669.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.7 + '@smithy/fetch-http-handler': 3.2.9 + '@smithy/hash-node': 3.0.11 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.8 + '@smithy/middleware-retry': 3.0.34 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.34 + '@smithy/util-defaults-mode-node': 3.0.34 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sso@3.669.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.667.0 + '@aws-sdk/middleware-host-header': 3.667.0 + '@aws-sdk/middleware-logger': 3.667.0 + '@aws-sdk/middleware-recursion-detection': 3.667.0 + '@aws-sdk/middleware-user-agent': 3.669.0 + '@aws-sdk/region-config-resolver': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@aws-sdk/util-endpoints': 3.667.0 + '@aws-sdk/util-user-agent-browser': 3.667.0 + '@aws-sdk/util-user-agent-node': 3.669.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.7 + '@smithy/fetch-http-handler': 3.2.9 + '@smithy/hash-node': 3.0.11 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.8 + '@smithy/middleware-retry': 3.0.34 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.34 + '@smithy/util-defaults-mode-node': 3.0.34 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/client-sts@3.669.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/client-sso-oidc': 3.669.0(@aws-sdk/client-sts@3.669.0) + '@aws-sdk/core': 3.667.0 + '@aws-sdk/credential-provider-node': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0) + '@aws-sdk/middleware-host-header': 3.667.0 + '@aws-sdk/middleware-logger': 3.667.0 + '@aws-sdk/middleware-recursion-detection': 3.667.0 + '@aws-sdk/middleware-user-agent': 3.669.0 + '@aws-sdk/region-config-resolver': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@aws-sdk/util-endpoints': 3.667.0 + '@aws-sdk/util-user-agent-browser': 3.667.0 + '@aws-sdk/util-user-agent-node': 3.669.0 + '@smithy/config-resolver': 3.0.13 + '@smithy/core': 2.5.7 + '@smithy/fetch-http-handler': 3.2.9 + '@smithy/hash-node': 3.0.11 + '@smithy/invalid-dependency': 3.0.11 + '@smithy/middleware-content-length': 3.0.13 + '@smithy/middleware-endpoint': 3.2.8 + '@smithy/middleware-retry': 3.0.34 + '@smithy/middleware-serde': 3.0.11 + '@smithy/middleware-stack': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/node-http-handler': 3.3.3 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-base64': 3.0.0 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-body-length-node': 3.0.0 + '@smithy/util-defaults-mode-browser': 3.0.34 + '@smithy/util-defaults-mode-node': 3.0.34 + '@smithy/util-endpoints': 2.1.7 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + transitivePeerDependencies: + - aws-crt + + '@aws-sdk/core@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/core': 2.5.7 + '@smithy/node-config-provider': 3.1.12 + '@smithy/property-provider': 3.1.11 + '@smithy/protocol-http': 4.1.8 + '@smithy/signature-v4': 4.2.4 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + '@smithy/util-middleware': 3.0.11 + fast-xml-parser: 4.4.1 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-env@3.667.0': + dependencies: + '@aws-sdk/core': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@smithy/property-provider': 3.1.11 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-http@3.667.0': + dependencies: + '@aws-sdk/core': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@smithy/fetch-http-handler': 3.2.9 + '@smithy/node-http-handler': 3.3.3 + '@smithy/property-provider': 3.1.11 + '@smithy/protocol-http': 4.1.8 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + '@smithy/util-stream': 3.3.4 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-ini@3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0)': + dependencies: + '@aws-sdk/client-sts': 3.669.0 + '@aws-sdk/core': 3.667.0 + '@aws-sdk/credential-provider-env': 3.667.0 + '@aws-sdk/credential-provider-http': 3.667.0 + '@aws-sdk/credential-provider-process': 3.667.0 + '@aws-sdk/credential-provider-sso': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0)) + '@aws-sdk/credential-provider-web-identity': 3.667.0(@aws-sdk/client-sts@3.669.0) + '@aws-sdk/types': 3.667.0 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + + '@aws-sdk/credential-provider-node@3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0)': + dependencies: + '@aws-sdk/credential-provider-env': 3.667.0 + '@aws-sdk/credential-provider-http': 3.667.0 + '@aws-sdk/credential-provider-ini': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0) + '@aws-sdk/credential-provider-process': 3.667.0 + '@aws-sdk/credential-provider-sso': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0)) + '@aws-sdk/credential-provider-web-identity': 3.667.0(@aws-sdk/client-sts@3.669.0) + '@aws-sdk/types': 3.667.0 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - '@aws-sdk/client-sts' + - aws-crt + + '@aws-sdk/credential-provider-process@3.667.0': + dependencies: + '@aws-sdk/core': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-sso@3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))': + dependencies: + '@aws-sdk/client-sso': 3.669.0 + '@aws-sdk/core': 3.667.0 + '@aws-sdk/token-providers': 3.667.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0)) + '@aws-sdk/types': 3.667.0 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + transitivePeerDependencies: + - '@aws-sdk/client-sso-oidc' + - aws-crt + + '@aws-sdk/credential-provider-web-identity@3.667.0(@aws-sdk/client-sts@3.669.0)': + dependencies: + '@aws-sdk/client-sts': 3.669.0 + '@aws-sdk/core': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@smithy/property-provider': 3.1.11 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-bucket-endpoint@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@aws-sdk/util-arn-parser': 3.568.0 + '@smithy/node-config-provider': 3.1.12 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-config-provider': 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-expect-continue@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.669.0': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-sdk/core': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@smithy/is-array-buffer': 3.0.0 + '@smithy/node-config-provider': 3.1.12 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-host-header@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-location-constraint@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.669.0': + dependencies: + '@aws-sdk/core': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@aws-sdk/util-arn-parser': 3.568.0 + '@smithy/core': 2.5.7 + '@smithy/node-config-provider': 3.1.12 + '@smithy/protocol-http': 4.1.8 + '@smithy/signature-v4': 4.2.4 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-stream': 3.3.4 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-ssec@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.669.0': + dependencies: + '@aws-sdk/core': 3.667.0 + '@aws-sdk/types': 3.667.0 + '@aws-sdk/util-endpoints': 3.667.0 + '@smithy/core': 2.5.7 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/region-config-resolver@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/node-config-provider': 3.1.12 + '@smithy/types': 3.7.2 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.11 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.669.0': + dependencies: + '@aws-sdk/middleware-sdk-s3': 3.669.0 + '@aws-sdk/types': 3.667.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/signature-v4': 4.2.4 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/token-providers@3.667.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))': + dependencies: + '@aws-sdk/client-sso-oidc': 3.669.0(@aws-sdk/client-sts@3.669.0) + '@aws-sdk/types': 3.667.0 + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/types@3.667.0': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/util-arn-parser@3.568.0': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-endpoints@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/types': 3.7.2 + '@smithy/util-endpoints': 2.1.7 + tslib: 2.8.1 + + '@aws-sdk/util-locate-window@3.965.4': + dependencies: + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-browser@3.667.0': + dependencies: + '@aws-sdk/types': 3.667.0 + '@smithy/types': 3.7.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.669.0': + dependencies: + '@aws-sdk/middleware-user-agent': 3.669.0 + '@aws-sdk/types': 3.667.0 + '@smithy/node-config-provider': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@aws-sdk/xml-builder@3.662.0': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@babel/code-frame@7.26.2': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.6': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': dependencies: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 @@ -10884,7 +11864,7 @@ snapshots: '@codspeed/core@5.2.0': dependencies: - axios: 1.11.0 + axios: 1.11.0(debug@4.3.4) find-up: 6.3.0 form-data: 4.0.4 node-gyp-build: 4.8.4 @@ -10922,6 +11902,22 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@devicefarmer/adbkit-logcat@2.1.3': {} + + '@devicefarmer/adbkit-monkey@1.2.1': {} + + '@devicefarmer/adbkit@3.2.4': + dependencies: + '@devicefarmer/adbkit-logcat': 2.1.3 + '@devicefarmer/adbkit-monkey': 1.2.1 + bluebird: 3.7.2 + commander: 9.5.0 + debug: 4.3.7 + node-forge: 1.3.1 + split: 1.0.1 + transitivePeerDependencies: + - supports-color + '@discoveryjs/json-ext@0.5.7': {} '@dprint/darwin-arm64@0.51.1': @@ -11460,6 +12456,33 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} + '@lynx-js/debug-router-connector@0.0.6': + dependencies: + '@aws-sdk/client-s3': 3.669.0 + '@devicefarmer/adbkit': 3.2.4 + '@types/uuid': 9.0.8 + axios: 1.11.0(debug@4.3.4) + bufferpack: 0.0.6 + chalk: 4.1.2 + debug: 4.3.4 + detect-port: 1.6.1 + express: 5.2.1 + fs-extra: 11.3.3 + ip: 2.0.1 + plist: 3.0.6 + q: 1.5.1 + signal-exit: 4.1.0 + unzipper: 0.12.3 + utf8: 3.0.0 + uuid: 9.0.1 + ws: 8.19.0 + yargs: 15.4.1 + transitivePeerDependencies: + - aws-crt + - bufferutil + - supports-color + - utf-8-validate + '@lynx-js/internal-preact@10.28.4-ee7bb26': {} '@lynx-js/lynx-core@0.1.3': {} @@ -12204,7 +13227,7 @@ snapshots: '@rsdoctor/sdk': 1.2.3(@rspack/core@1.7.7(@swc/helpers@0.5.18))(webpack@5.105.2) '@rsdoctor/types': 1.2.3(@rspack/core@1.7.7(@swc/helpers@0.5.18))(webpack@5.105.2) '@rsdoctor/utils': 1.2.3(@rspack/core@1.7.7(@swc/helpers@0.5.18))(webpack@5.105.2) - axios: 1.11.0 + axios: 1.11.0(debug@4.3.4) browserslist-load-config: 1.0.0 enhanced-resolve: 5.12.0 filesize: 10.1.6 @@ -12646,6 +13669,345 @@ snapshots: '@sinclair/typebox@0.34.37': {} + '@smithy/abort-controller@3.1.9': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader-native@3.0.1': + dependencies: + '@smithy/util-base64': 3.0.0 + tslib: 2.8.1 + + '@smithy/chunked-blob-reader@4.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/config-resolver@3.0.13': + dependencies: + '@smithy/node-config-provider': 3.1.12 + '@smithy/types': 3.7.2 + '@smithy/util-config-provider': 3.0.0 + '@smithy/util-middleware': 3.0.11 + tslib: 2.8.1 + + '@smithy/core@2.5.7': + dependencies: + '@smithy/middleware-serde': 3.0.11 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-body-length-browser': 3.0.0 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-stream': 3.3.4 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@smithy/credential-provider-imds@3.2.8': + dependencies: + '@smithy/node-config-provider': 3.1.12 + '@smithy/property-provider': 3.1.11 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + tslib: 2.8.1 + + '@smithy/eventstream-codec@3.1.10': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 3.7.2 + '@smithy/util-hex-encoding': 3.0.0 + tslib: 2.8.1 + + '@smithy/eventstream-serde-browser@3.0.14': + dependencies: + '@smithy/eventstream-serde-universal': 3.0.13 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@3.0.13': + dependencies: + '@smithy/eventstream-serde-universal': 3.0.13 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/eventstream-serde-universal@3.0.13': + dependencies: + '@smithy/eventstream-codec': 3.1.10 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@3.2.9': + dependencies: + '@smithy/protocol-http': 4.1.8 + '@smithy/querystring-builder': 3.0.11 + '@smithy/types': 3.7.2 + '@smithy/util-base64': 3.0.0 + tslib: 2.8.1 + + '@smithy/fetch-http-handler@4.1.3': + dependencies: + '@smithy/protocol-http': 4.1.8 + '@smithy/querystring-builder': 3.0.11 + '@smithy/types': 3.7.2 + '@smithy/util-base64': 3.0.0 + tslib: 2.8.1 + + '@smithy/hash-blob-browser@3.1.10': + dependencies: + '@smithy/chunked-blob-reader': 4.0.0 + '@smithy/chunked-blob-reader-native': 3.0.1 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/hash-node@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@smithy/hash-stream-node@3.1.10': + dependencies: + '@smithy/types': 3.7.2 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@smithy/invalid-dependency@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/is-array-buffer@2.2.0': + dependencies: + tslib: 2.8.1 + + '@smithy/is-array-buffer@3.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/md5-js@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@smithy/middleware-content-length@3.0.13': + dependencies: + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@3.2.8': + dependencies: + '@smithy/core': 2.5.7 + '@smithy/middleware-serde': 3.0.11 + '@smithy/node-config-provider': 3.1.12 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + '@smithy/url-parser': 3.0.11 + '@smithy/util-middleware': 3.0.11 + tslib: 2.8.1 + + '@smithy/middleware-retry@3.0.34': + dependencies: + '@smithy/node-config-provider': 3.1.12 + '@smithy/protocol-http': 4.1.8 + '@smithy/service-error-classification': 3.0.11 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-retry': 3.0.11 + tslib: 2.8.1 + uuid: 9.0.1 + + '@smithy/middleware-serde@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/middleware-stack@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/node-config-provider@3.1.12': + dependencies: + '@smithy/property-provider': 3.1.11 + '@smithy/shared-ini-file-loader': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/node-http-handler@3.3.3': + dependencies: + '@smithy/abort-controller': 3.1.9 + '@smithy/protocol-http': 4.1.8 + '@smithy/querystring-builder': 3.0.11 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/property-provider@3.1.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/protocol-http@4.1.8': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/querystring-builder@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + '@smithy/util-uri-escape': 3.0.0 + tslib: 2.8.1 + + '@smithy/querystring-parser@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/service-error-classification@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + + '@smithy/shared-ini-file-loader@3.1.12': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/signature-v4@4.2.4': + dependencies: + '@smithy/is-array-buffer': 3.0.0 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-middleware': 3.0.11 + '@smithy/util-uri-escape': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@smithy/smithy-client@3.7.0': + dependencies: + '@smithy/core': 2.5.7 + '@smithy/middleware-endpoint': 3.2.8 + '@smithy/middleware-stack': 3.0.11 + '@smithy/protocol-http': 4.1.8 + '@smithy/types': 3.7.2 + '@smithy/util-stream': 3.3.4 + tslib: 2.8.1 + + '@smithy/types@3.7.2': + dependencies: + tslib: 2.8.1 + + '@smithy/url-parser@3.0.11': + dependencies: + '@smithy/querystring-parser': 3.0.11 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/util-base64@3.0.0': + dependencies: + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@3.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-body-length-node@3.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-buffer-from@2.2.0': + dependencies: + '@smithy/is-array-buffer': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-buffer-from@3.0.0': + dependencies: + '@smithy/is-array-buffer': 3.0.0 + tslib: 2.8.1 + + '@smithy/util-config-provider@3.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-defaults-mode-browser@3.0.34': + dependencies: + '@smithy/property-provider': 3.1.11 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + bowser: 2.14.1 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@3.0.34': + dependencies: + '@smithy/config-resolver': 3.0.13 + '@smithy/credential-provider-imds': 3.2.8 + '@smithy/node-config-provider': 3.1.12 + '@smithy/property-provider': 3.1.11 + '@smithy/smithy-client': 3.7.0 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/util-endpoints@2.1.7': + dependencies: + '@smithy/node-config-provider': 3.1.12 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/util-hex-encoding@3.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-middleware@3.0.11': + dependencies: + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/util-retry@3.0.11': + dependencies: + '@smithy/service-error-classification': 3.0.11 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + + '@smithy/util-stream@3.3.4': + dependencies: + '@smithy/fetch-http-handler': 4.1.3 + '@smithy/node-http-handler': 3.3.3 + '@smithy/types': 3.7.2 + '@smithy/util-base64': 3.0.0 + '@smithy/util-buffer-from': 3.0.0 + '@smithy/util-hex-encoding': 3.0.0 + '@smithy/util-utf8': 3.0.0 + tslib: 2.8.1 + + '@smithy/util-uri-escape@3.0.0': + dependencies: + tslib: 2.8.1 + + '@smithy/util-utf8@2.3.0': + dependencies: + '@smithy/util-buffer-from': 2.2.0 + tslib: 2.8.1 + + '@smithy/util-utf8@3.0.0': + dependencies: + '@smithy/util-buffer-from': 3.0.0 + tslib: 2.8.1 + + '@smithy/util-waiter@3.2.0': + dependencies: + '@smithy/abort-controller': 3.1.9 + '@smithy/types': 3.7.2 + tslib: 2.8.1 + '@socket.io/component-emitter@3.1.2': {} '@standard-schema/spec@1.0.0': {} @@ -12976,6 +14338,10 @@ snapshots: '@types/node@12.20.55': {} + '@types/node@22.0.0': + dependencies: + undici-types: 6.11.1 + '@types/node@24.10.13': dependencies: undici-types: 7.16.0 @@ -13046,6 +14412,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@9.0.8': {} + '@types/webpack-sources@3.2.3': dependencies: '@types/node': 24.10.13 @@ -13496,6 +14864,8 @@ snapshots: acorn@8.15.0: {} + address@1.2.2: {} + agent-base@7.1.3: {} aggregate-error@3.1.0: @@ -13665,9 +15035,9 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 - axios@1.11.0: + axios@1.11.0(debug@4.3.4): dependencies: - follow-redirects: 1.15.6 + follow-redirects: 1.15.6(debug@4.3.4) form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -13742,6 +15112,8 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bluebird@3.7.2: {} + body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -13799,6 +15171,8 @@ snapshots: boolbase@1.0.0: {} + bowser@2.14.1: {} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -13841,6 +15215,8 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bufferpack@0.0.6: {} + builtin-modules@5.0.0: {} bundle-name@4.1.0: @@ -14022,8 +15398,7 @@ snapshots: commander@7.2.0: {} - commander@9.5.0: - optional: true + commander@9.5.0: {} comment-json@4.2.5: dependencies: @@ -14304,6 +15679,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.4: + dependencies: + ms: 2.1.2 + debug@4.3.7: dependencies: ms: 2.1.3 @@ -14386,6 +15765,13 @@ snapshots: detect-node@2.1.0: {} + detect-port@1.6.1: + dependencies: + address: 1.2.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -14456,6 +15842,10 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -15104,6 +16494,10 @@ snapshots: fast-uri@3.1.0: {} + fast-xml-parser@4.4.1: + dependencies: + strnum: 1.1.2 + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -15219,7 +16613,9 @@ snapshots: flexsearch@0.8.212: {} - follow-redirects@1.15.6: {} + follow-redirects@1.15.6(debug@4.3.4): + optionalDependencies: + debug: 4.3.4 for-each@0.3.5: dependencies: @@ -15673,7 +17069,7 @@ snapshots: http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.6 + follow-redirects: 1.15.6(debug@4.3.4) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -15769,6 +17165,8 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + ip@2.0.1: {} + ipaddr.js@1.9.1: {} ipaddr.js@2.3.0: {} @@ -17008,6 +18406,8 @@ snapshots: ms@2.0.0: {} + ms@2.1.2: {} + ms@2.1.3: {} multicast-dns@7.2.5: @@ -17360,6 +18760,11 @@ snapshots: optionalDependencies: fsevents: 2.3.2 + plist@3.0.6: + dependencies: + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + plist@3.1.0: dependencies: '@xmldom/xmldom': 0.8.11 @@ -17634,6 +19039,8 @@ snapshots: punycode@2.3.1: {} + q@1.5.1: {} + qs@6.13.0: dependencies: side-channel: 1.1.0 @@ -18546,6 +19953,10 @@ snapshots: transitivePeerDependencies: - supports-color + split@1.0.1: + dependencies: + through: 2.3.8 + sprintf-js@1.0.3: {} stable-hash-x@0.1.1: {} @@ -18666,6 +20077,8 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + strnum@1.1.2: {} + style-to-js@1.1.16: dependencies: style-to-object: 1.0.8 @@ -19092,6 +20505,8 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@6.11.1: {} + undici-types@7.16.0: {} undici@6.23.0: {} @@ -19184,6 +20599,14 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + unzipper@0.12.3: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.3.3 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + upath@2.0.1: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -19218,12 +20641,16 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 + utf8@3.0.0: {} + util-deprecate@1.0.2: {} utils-merge@1.0.1: {} uuid@8.3.2: {} + uuid@9.0.1: {} + v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.29 From e32778344ac91e7132149ded93308c905fc94440 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:55:04 +0800 Subject: [PATCH 02/31] feat(kitten-lynx): impl core testing API and finish test runner --- .../kitten-lynx/src/CDPChannel.ts | 31 + .../kitten-lynx/src/ElementNode.ts | 3 - .../testing-library/kitten-lynx/src/Lynx.ts | 39 +- .../kitten-lynx/src/LynxView.ts | 159 ++++-- packages/testing-library/kitten-lynx/test.out | 528 ++++++++++++++++++ .../kitten-lynx/tests/lynx.spec.ts | 9 +- .../testing-library/kitten-lynx/tsconfig.json | 4 +- 7 files changed, 704 insertions(+), 69 deletions(-) create mode 100644 packages/testing-library/kitten-lynx/test.out diff --git a/packages/testing-library/kitten-lynx/src/CDPChannel.ts b/packages/testing-library/kitten-lynx/src/CDPChannel.ts index 6d4ab51f2a..387941b5e6 100644 --- a/packages/testing-library/kitten-lynx/src/CDPChannel.ts +++ b/packages/testing-library/kitten-lynx/src/CDPChannel.ts @@ -58,6 +58,12 @@ interface Protocol { computedStyle: { name: string; value: string }[]; }; }; + 'Page.navigate': { + params: { + url: string; + }; + return: unknown; + }; 'Input.emulateTouchFromMouseEvent': { params: { type: 'mousePressed' | 'mouseReleased' | 'mouseMoved'; @@ -160,4 +166,29 @@ export class CDPChannel { return promise; } + + onEvent(method: string, listener: (params: any) => void): () => void { + const handler = ( + { message, id: sourceId }: { message: string; id: number }, + ) => { + const parsed = JSON.parse(message); + if (parsed.event !== 'Customized' || parsed.data?.type !== 'CDP') { + return; + } + + const cdpData = parsed.data.data; + if (cdpData.session_id !== this._sessionId) return; + + const cdpMessage = typeof cdpData.message === 'string' + ? JSON.parse(cdpData.message) + : cdpData.message; + + if (cdpMessage.method === method) { + listener(cdpMessage.params); + } + }; + + this._connector.on('usb-client-message', handler); + return () => this._connector.off('usb-client-message', handler); + } } diff --git a/packages/testing-library/kitten-lynx/src/ElementNode.ts b/packages/testing-library/kitten-lynx/src/ElementNode.ts index 576d635ed8..f92072f944 100644 --- a/packages/testing-library/kitten-lynx/src/ElementNode.ts +++ b/packages/testing-library/kitten-lynx/src/ElementNode.ts @@ -52,9 +52,6 @@ export class ElementNode { timestamp, }); - // Wait briefly to simulate actual human tap duration - await new Promise(resolve => setTimeout(resolve, 50)); - await this._lynxView._channel.send('Input.emulateTouchFromMouseEvent', { type: 'mouseReleased', x, diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index 2e3ed1d7e8..9e115d9934 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -6,6 +6,7 @@ import { LynxView } from './LynxView.js'; export class Lynx { private _connector: DebugRouterConnector | null = null; + private _currentClient: any | null = null; private _currentClientId: number = -1; static async connect(): Promise { @@ -48,10 +49,30 @@ export class Lynx { if (devices.length === 0) { throw new Error('Failed to connect to Lynx: no device found.'); } + for (const device of devices) { + device.startWatchClient(); + } } - // Wait until a client is attached - lynx._currentClientId = await clientPromise; + const usbClients = lynx._connector.getAllUsbClients(); + if (usbClients.length > 0) { + lynx._currentClient = usbClients[0]; + lynx._currentClientId = usbClients[0]!.clientId(); + } else { + const existingClients = lynx._connector.getAllAppClients(); + if (existingClients.length > 0) { + lynx._currentClient = existingClients[0]; + lynx._currentClientId = existingClients[0]!.clientId; + } else { + // Wait until a client is attached + const clientId = await clientPromise; + const allUsbs = lynx._connector.getAllUsbClients(); + lynx._currentClient = allUsbs.find((c: any) => + c.clientId() === clientId + ) || null; + lynx._currentClientId = clientId; + } + } // Force enable devtools toggle if required lynx._connector.sendMessageToApp( @@ -82,11 +103,19 @@ export class Lynx { if (!this._connector || this._currentClientId === -1) { throw new Error('Not connected. Call Lynx.connect() first.'); } - return new LynxView(this._connector, this._currentClientId); + return new LynxView( + this._connector, + this._currentClientId, + this._currentClient, + ); } async close(): Promise { - // Optional: terminate the connector properly - // Note: API bindings for `close` on `DebugRouterConnector` depend on upstream + if (this._connector) { + if (this._connector.wss) { + this._connector.wss.close(); + } + this._connector = null; + } } } diff --git a/packages/testing-library/kitten-lynx/src/LynxView.ts b/packages/testing-library/kitten-lynx/src/LynxView.ts index 2429139f91..93b318b8e2 100644 --- a/packages/testing-library/kitten-lynx/src/LynxView.ts +++ b/packages/testing-library/kitten-lynx/src/LynxView.ts @@ -18,73 +18,116 @@ export class LynxView { constructor( private _connector: DebugRouterConnector, private _clientId: number, + private _client?: any, ) { this.id = LynxView.incId++; idToLynxView[this.id.toString()] = new WeakRef(this); } async goto(url: string, _options?: unknown): Promise { - const { exec } = await import('node:child_process'); - const { promisify } = await import('node:util'); - const execAsync = promisify(exec); - - // Launch the URL through adb using the custom launcher intent - await execAsync( - `adb shell am start -n com.lynx.explorer/.LynxViewShellActivity -d "${url}"`, - ); - - // Wait for the page to attach to CDP - await new Promise(resolve => setTimeout(resolve, 1000)); - - const { promise, resolve, reject } = Promise.withResolvers(); - let isSettled = false; - const listener = ({ message, id }: { message: string; id: number }) => { - if (id !== this._clientId) return; - const parsed = JSON.parse(message); + if (!this._channel) { if ( - parsed.event === 'Customized' && parsed.data?.type === 'SessionList' + this._client && typeof this._client.sendClientMessage === 'function' ) { - const sessions = parsed.data.data; - if (sessions.length > 0) { + const { promise: newClientPromise, resolve: newClientResolve } = Promise + .withResolvers(); + const clientListener = (client: any) => newClientResolve(client); + this._connector.on('client-connected', clientListener); + + this._client.sendClientMessage('App.openPage', { url }); + + const newClientTimeout = setTimeout(() => { + this._connector.off('client-connected', clientListener); + newClientResolve(null); + }, 10000); + + const newClient = await newClientPromise; + clearTimeout(newClientTimeout); + this._connector.off('client-connected', clientListener); + + if (newClient) { + this._client = newClient; + this._clientId = newClient.clientId(); + } + } + + // Fetch initial session + const { promise, resolve, reject } = Promise.withResolvers(); + let isSettled = false; + const listener = ({ message, id }: { message: string; id: number }) => { + const parsed = JSON.parse(message); + // Log all messages locally to find the session + if ( + parsed.event === 'Customized' && parsed.data?.type === 'SessionList' + ) { + const sessions = parsed.data.data; + if (sessions.length > 0) { + isSettled = true; + this._connector.off('usb-client-message', listener); + resolve(sessions[sessions.length - 1].session_id); + } + } + }; + this._connector.on('usb-client-message', listener); + + // Periodically ask for SessionList until settled or timeout + const pollInterval = setInterval(() => { + if (isSettled) { + clearInterval(pollInterval); + return; + } + this._connector.sendMessageToApp( + this._clientId, + JSON.stringify({ + event: 'Customized', + data: { + type: 'ListSession', + data: [], + sender: this._clientId, + }, + from: this._clientId, + }), + ); + }, 500); + + setTimeout(() => { + if (!isSettled) { isSettled = true; + clearInterval(pollInterval); this._connector.off('usb-client-message', listener); - resolve(sessions[sessions.length - 1].session_id); + reject(new Error('Timeout waiting for session')); } - } - }; - this._connector.on('usb-client-message', listener); - - // Periodically ask for SessionList until settled or timeout - const pollInterval = setInterval(() => { - if (isSettled) { - clearInterval(pollInterval); - return; - } - this._connector.sendMessageToApp( - this._clientId, - JSON.stringify({ - event: 'Customized', - data: { - type: 'ListSession', - data: [], - sender: this._clientId, - }, - from: this._clientId, - }), - ); - }, 500); - - setTimeout(() => { - if (!isSettled) { - isSettled = true; - clearInterval(pollInterval); - this._connector.off('usb-client-message', listener); - reject(new Error('Timeout waiting for session')); - } + }, 15000); + + const sessionId = await promise; + await this.onAttachedToTarget(sessionId); + } + + const { promise: execPromise, resolve: execResolve } = Promise + .withResolvers(); + const off = this._channel.onEvent('DOM.childNodeInserted', () => { + off(); + execResolve(); + }); + + const fallbackTimeout = setTimeout(() => { + off(); + execResolve(); }, 5000); - const sessionId = await promise; - await this.onAttachedToTarget(sessionId); + // Launch the URL via CDP Page.navigate message + await this._channel.send('Page.navigate', { url }); + + // Wait for the new page to establish its execution context instead of fixed timers + await execPromise; + clearTimeout(fallbackTimeout); + + // Refresh the DOM because navigation happened + const response = await this._channel.send('DOM.getDocument', { + depth: -1, + }); + const root = response.root.children[0]!; + this._root = ElementNode.fromId(root.nodeId, this); } async locator(selector: string): Promise { @@ -108,8 +151,12 @@ export class LynxView { this._clientId, this._connector, ); - // Enable DOM agent - await this._channel.send('Runtime.enable' as any, {}); // Enable Runtime to get events + // Enable DOM and Page agents + await Promise.all([ + this._channel.send('Runtime.enable' as any, {}), + this._channel.send('Page.enable' as any, {}).catch(() => {}), + this._channel.send('DOM.enable' as any, {}).catch(() => {}), + ]); const response = await this._channel.send('DOM.getDocument', { depth: -1, }); diff --git a/packages/testing-library/kitten-lynx/test.out b/packages/testing-library/kitten-lynx/test.out new file mode 100644 index 0000000000..78d5e68514 --- /dev/null +++ b/packages/testing-library/kitten-lynx/test.out @@ -0,0 +1,528 @@ + +> @lynx-test/kitten-lynx@0.1.0 test /home/haoyang/lynx/lynx-stack/packages/testing-library/kitten-lynx +> vitest run + + + RUN v3.2.4 /home/haoyang/lynx/lynx-stack/packages/testing-library/kitten-lynx + +stdout | tests/lynx.spec.ts > kitten-lynx testing framework +[2026-02-25 21:07:57] INFO startMonitorMultiOpen +[2026-02-25 21:07:57] INFO MultiOpen: switch to attached + +stdout | tests/lynx.spec.ts > kitten-lynx testing framework > can navigate to a page and read the DOM +[DEBUG DOM.getDocument root] { + "attributes": [], + "backendNodeId": 11, + "childNodeCount": 1, + "children": [ + { + "attributes": [], + "backendNodeId": 10, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "clip-radius", + "true", + "flatten", + "false", + "style", + "height:100%;" + ], + "backendNodeId": 13, + "childNodeCount": 2, + "children": [ + { + "attributes": [ + "clip-radius", + "true", + "flatten", + "false", + "class", + "page__light" + ], + "backendNodeId": 14, + "childNodeCount": 3, + "children": [ + { + "attributes": [ + "class", + "page-header" + ], + "backendNodeId": 15, + "childNodeCount": 3, + "children": [ + { + "attributes": [ + "mode", + "aspectFit", + "src", + "data:image/png;base64,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", + "class", + "logo" + ], + "backendNodeId": 16, + "childNodeCount": 0, + "children": [], + "localName": "image", + "nodeId": 16, + "nodeName": "IMAGE", + "nodeType": 1, + "nodeValue": "", + "parentId": 15 + }, + { + "attributes": [ + "class", + "home-title__light" + ], + "backendNodeId": 17, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "text", + "Lynx Explorer" + ], + "backendNodeId": 18, + "childNodeCount": 0, + "children": [], + "localName": "raw-text", + "nodeId": 18, + "nodeName": "RAW-TEXT", + "nodeType": 1, + "nodeValue": "", + "parentId": 17 + } + ], + "localName": "text", + "nodeId": 17, + "nodeName": "TEXT", + "nodeType": 1, + "nodeValue": "", + "parentId": 15 + }, + { + "attributes": [ + "class", + "scan" + ], + "backendNodeId": 19, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "accessibility-element", + "true", + "accessibility-label", + "Open Scan", + "accessibility-traits", + "button", + "src", + "data:image/png;base64,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", + "bindtap", + "-3:1:", + "class", + "scan-icon" + ], + "backendNodeId": 33, + "childNodeCount": 0, + "children": [], + "localName": "image", + "nodeId": 33, + "nodeName": "IMAGE", + "nodeType": 1, + "nodeValue": "", + "parentId": 19 + } + ], + "localName": "view", + "nodeId": 19, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 15 + } + ], + "localName": "view", + "nodeId": 15, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 14 + }, + { + "attributes": [ + "class", + "input-card-url__light" + ], + "backendNodeId": 20, + "childNodeCount": 3, + "children": [ + { + "attributes": [ + "class", + "bold-text__light" + ], + "backendNodeId": 21, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "text", + "Card URL" + ], + "backendNodeId": 22, + "childNodeCount": 0, + "children": [], + "localName": "raw-text", + "nodeId": 22, + "nodeName": "RAW-TEXT", + "nodeType": 1, + "nodeValue": "", + "parentId": 21 + } + ], + "localName": "text", + "nodeId": 21, + "nodeName": "TEXT", + "nodeType": 1, + "nodeValue": "", + "parentId": 20 + }, + { + "attributes": [ + "placeholder", + "Enter Card URL", + "text-color", + "#000000", + "bindinput", + "-4:6:", + "class", + "input-box" + ], + "backendNodeId": 23, + "childNodeCount": 0, + "children": [], + "localName": "explorer-input", + "nodeId": 23, + "nodeName": "EXPLORER-INPUT", + "nodeType": 1, + "nodeValue": "", + "parentId": 20 + }, + { + "attributes": [ + "accessibility-element", + "true", + "accessibility-label", + "Open Schema", + "accessibility-traits", + "button", + "bindtap", + "-4:9:", + "class", + "connect-button__light" + ], + "backendNodeId": 24, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "accessibility-element", + "false", + "style", + "font-size:16px;color:#ffffff;line-height:22px;" + ], + "backendNodeId": 25, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "text", + "Go" + ], + "backendNodeId": 26, + "childNodeCount": 0, + "children": [], + "localName": "raw-text", + "nodeId": 26, + "nodeName": "RAW-TEXT", + "nodeType": 1, + "nodeValue": "", + "parentId": 25 + } + ], + "localName": "text", + "nodeId": 25, + "nodeName": "TEXT", + "nodeType": 1, + "nodeValue": "", + "parentId": 24 + } + ], + "localName": "view", + "nodeId": 24, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 20 + } + ], + "localName": "view", + "nodeId": 20, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 14 + }, + { + "attributes": [ + "accessibility-element", + "true", + "accessibility-label", + "Open Show Cases", + "accessibility-traits", + "button", + "bindtap", + "-4:11:", + "class", + "showcase__light" + ], + "backendNodeId": 27, + "childNodeCount": 3, + "children": [ + { + "attributes": [ + "src", + "data:image/png;base64,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", + "class", + "showcase-icon" + ], + "backendNodeId": 28, + "childNodeCount": 0, + "children": [], + "localName": "image", + "nodeId": 28, + "nodeName": "IMAGE", + "nodeType": 1, + "nodeValue": "", + "parentId": 27 + }, + { + "attributes": [ + "accessibility-element", + "false", + "class", + "text__light" + ], + "backendNodeId": 29, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "text", + "Showcase" + ], + "backendNodeId": 30, + "childNodeCount": 0, + "children": [], + "localName": "raw-text", + "nodeId": 30, + "nodeName": "RAW-TEXT", + "nodeType": 1, + "nodeValue": "", + "parentId": 29 + } + ], + "localName": "text", + "nodeId": 29, + "nodeName": "TEXT", + "nodeType": 1, + "nodeValue": "", + "parentId": 27 + }, + { + "attributes": [ + "style", + "justify-content:center;margin-left:auto;margin-bottom:auto;margin-right:5%;margin-top:auto;" + ], + "backendNodeId": 31, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "src", + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGGSURBVHgB7do7SkNRFIXh7QNSWAgWFoKVjZ1TcBQWztDewnEIVhZaCIIBC0FBUHAvroEQEjjNfqywPlhFHtXPvUVujpmIiIiIiIiIyFbZsVjHvkvfqW/uu/O9GZE9i3Pou/Yd/b8+8J37nn2fRmLX4pz5Zivv4fWVTVcWhchAm1BFirzF3n0Xvv01n+E9itstMtCv78mmELSRIgPBl5FHig4E1JEyAgFtpKxAQBkpMxDQRcoOBFSRKgIBTaSqQEARqTIQtI9UHQhaR+oQCNpG6hIIWkbqFAjaReoWCEYj3ft+LFjHQDASCd95tWAVTxRHRf+hMKRrIDyOxWPZ2YbPv32PlqBjoJE4N74PS9DiMl4yGmduSToFahcHugRqGQc6BGobB6oDtY4DlYHax4GqQBRxoCIQTRzIDkQVBzID0cWBrECUcSAjEG0cyDijSBsHIn/N44widRzIPqO4QBEHKp4H0cSByEAPNsVYRhUHMs4onth0RvrFd2tEcUREREREREREttAfMpSHesEfTNcAAAAASUVORK5CYII=", + "class", + "forward-icon" + ], + "backendNodeId": 32, + "childNodeCount": 0, + "children": [], + "localName": "image", + "nodeId": 32, + "nodeName": "IMAGE", + "nodeType": 1, + "nodeValue": "", + "parentId": 31 + } + ], + "localName": "view", + "nodeId": 31, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 27 + } + ], + "localName": "view", + "nodeId": 27, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 14 + } + ], + "localName": "view", + "nodeId": 14, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 13 + }, + { + "attributes": [ + "clip-radius", + "true", + "flatten", + "false", + "class", + "navigator__light" + ], + "backendNodeId": 34, + "childNodeCount": 2, + "children": [ + { + "attributes": [ + "accessibility-element", + "true", + "accessibility-label", + "Show Home Page", + "accessibility-traits", + "button", + "bindtap", + "-5:1:", + "class", + "button" + ], + "backendNodeId": 35, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "src", + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAABwCAYAAAApIp91AAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAPqSURBVHgB7Z1NTlNRGIa/00InamRoAJO7BBKtIY7KDlgCrEBZAWEFwArAFegO6JAoiV1CB2Ic6gAHLb3He4tRkd72u+39+c7b9xmQUg705+F+DyXpPSKEEEIISXECju9Ea/Jz0Bl/0mj13GW/L2DAShzLuxkeJpfe3v+KO5Hh6pHr9b8LCJAS/XYUyWjwPrm4lbGkL83WDspR2RAw/PZmR26HnyVbYEoq+WK8FgAoib69+UZG8YU4v6ZYHqVr/av1QwkciHF617/BcXJxT+Yi7E4GL1HRPy3BdjLocarsn5ZgOxmsxJz90xJkJ4Mbp4v3T0s4nQxKYoH90xJEJ4MZpwX3T0sQnQxCYkn902K+k6bHaXX902Kzk2Yl1tA/LeY6aXKc1tQ/LeY6aU5izf3TYqqTZsapvf5pqb+TJiQa7p+WWjtZ+zg13j8ttXayVomB9E9LbZ2sZZyG2z8t1XaycokA/dNSWScrHacg/dNSWScrkwjWPy2VdLL0cYrfPy3ldbJUiUvUPy2ldLK0cbpk/dNSSidLkbik/dNSeCcLHafsX16K6WRhEtm/uVm4k4WMU/ZvIRbu5MIS2b9CWKiTc49T9q8s8ndyLonsX+nk6mTuccr+VUKuTuaSyP5VirqTqnHK/tXN9E7OlMj+mSGzk1Ml/hZ4IemhTSwwUeT0JlKgNZKDanj2/5WZEn072hIKNIjv+Ncb99KWfSTGo0iITYailNiI+TIiEOBORrSMUCIAlAgAJQJAiQBQIgCUCAAlAkCJAFAiAJQIACUCQIkAUCIAlAgAJQJAiQCsSHC4fakUfybGCU6i+3R9LhXi2+vmJXKcAkCJAFAiAJQIACUCQIkAUCIAAb7Yz8cy7O4NK/HP7t43g7+7e48GyYv3DbjdvXHH6c3w+OH27CnJda2B+f/C5AFSon/xfDf5uJe9QHZRdvZOwTwSG3Fn5pqR3xUQMCVqTtHS8E8FBL7EAIASAaBEACgRAEoEgBIBoEQAKBEASgSAEgGgRAAoEQBKBIASAaBEACgRAEoEgBIBoEQAKBEASgSAEgGgRACyJXrXF3vo3j/h3ex1sfshKpy992w077vJlni70jP4AD6oVnk3e53zXdHxTizhpO8uv3T/vSpT4vhdQ16OxAou3bEsVt0fd5U8SD9V+Ln7+FX3C9EcnYxv2wrePXgOpjbRXV2fjE/+U/uDcF1pxDvu8ltf/S23rf1kZJ5O+Fmn8qh1IErGt5nctoHnIDmo/MGk8/ioN4b2Lzc7yQiKpHp6yR3vyZz47WeRDFfvNsh60uy67vzvS/TtjS2JG1Hl+2ilf588Xuktct8JIYSQZeAXK6rzwC0a4W0AAAAASUVORK5CYII=", + "class", + "icon" + ], + "backendNodeId": 36, + "childNodeCount": 0, + "children": [], + "localName": "image", + "nodeId": 36, + "nodeName": "IMAGE", + "nodeType": 1, + "nodeValue": "", + "parentId": 35 + } + ], + "localName": "view", + "nodeId": 35, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 34 + }, + { + "attributes": [ + "accessibility-element", + "true", + "accessibility-label", + "Show Settings Page", + "accessibility-traits", + "button", + "bindtap", + "-5:3:", + "class", + "button" + ], + "backendNodeId": 37, + "childNodeCount": 1, + "children": [ + { + "attributes": [ + "src", + "data:image/png;base64,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", + "class", + "icon" + ], + "backendNodeId": 38, + "childNodeCount": 0, + "children": [], + "localName": "image", + "nodeId": 38, + "nodeName": "IMAGE", + "nodeType": 1, + "nodeValue": "", + "parentId": 37 + } + ], + "localName": "view", + "nodeId": 37, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 34 + } + ], + "localName": "view", + "nodeId": 34, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 13 + } + ], + "localName": "view", + "nodeId": 13, + "nodeName": "VIEW", + "nodeType": 1, + "nodeValue": "", + "parentId": 10 + } + ], + "localName": "page", + "nodeId": 10, + "nodeName": "PAGE", + "nodeType": 1, + "nodeValue": "" + } + ], + "localName": "", + "nodeId": 11, + "nodeName": "#document", + "nodeType": 9, + "nodeValue": "" +} + + ❯ tests/lynx.spec.ts (1 test | 1 failed) 2718ms + × kitten-lynx testing framework > can navigate to a page and read the DOM 1698ms + → expected '<#document> Date: Thu, 26 Feb 2026 19:05:24 +0800 Subject: [PATCH 03/31] feat: Automatically restart the Lynx app on all connected ADB devices when `Lynx.connect()` is called. --- .../testing-library/kitten-lynx/src/Lynx.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index 9e115d9934..0726a45bdd 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -4,12 +4,50 @@ import { } from '@lynx-js/debug-router-connector'; import { LynxView } from './LynxView.js'; +import { execSync } from 'child_process'; + export class Lynx { private _connector: DebugRouterConnector | null = null; private _currentClient: any | null = null; private _currentClientId: number = -1; static async connect(): Promise { + try { + const output = execSync('adb devices').toString(); + const lines = output.split('\n'); + const adbDevices = []; + for (let i = 1; i < lines.length; i++) { + const line = lines[i]?.trim(); + if (line && line.endsWith('device')) { + const parts = line.split('\t'); + if (parts.length >= 1 && parts[0]) { + adbDevices.push(parts[0]); + } + } + } + + for (const deviceId of adbDevices) { + try { + console.log( + `[Lynx] Restarting com.lynx.explorer on device ${deviceId}...`, + ); + execSync(`adb -s ${deviceId} shell am force-stop com.lynx.explorer`); + execSync( + `adb -s ${deviceId} shell monkey -p com.lynx.explorer -c android.intent.category.LAUNCHER 1`, + ); + } catch (e) { + console.error( + `[Lynx] Failed to restart app on device ${deviceId}:`, + e, + ); + } + } + } catch (e) { + console.warn( + '[Lynx] Failed to list ADB devices or adb is not available.', + ); + } + const lynx = new Lynx(); lynx._connector = new DebugRouterConnector({ From d1e79b31826c24af49eb846e369fc0d0c2e72606 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Fri, 27 Feb 2026 21:36:48 +0800 Subject: [PATCH 04/31] feat: Introduce configurable container images for reusable workflows and add new Android emulator tests. --- .github/workflows/deploy-main.yml | 2 ++ .github/workflows/test.yml | 32 +++++++++++++++++++ .github/workflows/workflow-test.yml | 6 +++- .../testing-library/kitten-lynx/src/Lynx.ts | 2 +- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index 97e0c527a7..a5e25747f2 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -102,6 +102,7 @@ jobs: with: runs-on: lynx-custom-container is-web: true + container-image: mcr.microsoft.com/playwright:v1.58.2-noble run: | export NODE_OPTIONS="--max-old-space-size=32768" export PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-report.junit.xml @@ -116,6 +117,7 @@ jobs: with: runs-on: lynx-custom-container is-web: true + container-image: mcr.microsoft.com/playwright:v1.58.2-noble run: | export NODE_OPTIONS="--max-old-space-size=32768" export PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-report.junit.xml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2090c18e6e..74a03a3e60 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,6 +75,7 @@ jobs: with: runs-on: lynx-custom-container is-web: true + container-image: mcr.microsoft.com/playwright:v1.58.2-noble run: | export NODE_OPTIONS="--max-old-space-size=32768" pnpm --filter @lynx-js/web-tests run lh || echo "Lighthouse failed" @@ -91,6 +92,7 @@ jobs: with: runs-on: lynx-custom-container is-web: true + container-image: mcr.microsoft.com/playwright:v1.58.2-noble codecov-flags: "e2e" web-report-path: "packages/web-platform/web-elements/playwright-report" run: | @@ -116,6 +118,7 @@ jobs: with: runs-on: lynx-custom-container is-web: true + container-image: mcr.microsoft.com/playwright:v1.58.2-noble web-report-name: "playwright-${{ matrix.thread }}-${{ matrix.render }}-shard${{ matrix.shard }}" codecov-flags: "e2e" run: | @@ -143,6 +146,7 @@ jobs: with: runs-on: lynx-custom-container is-web: true + container-image: mcr.microsoft.com/playwright:v1.58.2-noble web-report-name: "playwright-${{ matrix.render }}-shard${{ matrix.shard }}" web-report-path: "packages/web-platform/web-core-wasm-e2e/playwright-report" codecov-flags: "e2e" @@ -242,6 +246,32 @@ jobs: with: runs-on: lynx-ubuntu-24.04-medium run: pnpm -r run test:type + kitten-lynx-android-emulator: + needs: build + uses: ./.github/workflows/workflow-test.yml + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + name: Kitten Lynx Android Emulator Test + with: + runs-on: lynx-ubuntu-24.04-xlarge + run: | + # 1. Install and configure Docker + sudo apt-get update + sudo apt-get install -y docker.io + sudo systemctl start docker + + # 2. Build and start the Android emulator container + sudo docker build -t kitten-lynx packages/testing-library/kitten-lynx + sudo docker run -d \ + --name kitten-lynx-android \ + --device /dev/kvm \ + --privileged \ + -p 5556:5556 \ + kitten-lynx + + # 3. Run the tests + pnpm --filter @lynx-test/kitten-lynx run test + test-typos: runs-on: lynx-ubuntu-24.04-medium steps: @@ -280,6 +310,7 @@ jobs: --no-cache --logHeapUsage --silent + --exclude packages/testing-library/kitten-lynx/** website: needs: build uses: ./.github/workflows/workflow-website.yml @@ -288,6 +319,7 @@ jobs: - benchmark - code-style-check - eslint + - kitten-lynx-android-emulator - playwright-linux - playwright-web-elements - test-api diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index c66d68c13d..75bcbbd594 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -18,6 +18,9 @@ on: required: false type: boolean default: false + container-image: + required: false + type: string web-report-name: required: false type: string @@ -43,7 +46,8 @@ jobs: runs-on: ${{ inputs.runs-on }} permissions: {} container: - image: ${{ inputs.is-web && 'mcr.microsoft.com/playwright:v1.58.2-noble' || null }} + image: ${{ inputs.container-image || null }} + options: --user 0 env: CI: 1 TURBO_TELEMETRY_DISABLED: 1 diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index 0726a45bdd..fa4b774d8a 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -100,7 +100,7 @@ export class Lynx { const existingClients = lynx._connector.getAllAppClients(); if (existingClients.length > 0) { lynx._currentClient = existingClients[0]; - lynx._currentClientId = existingClients[0]!.clientId; + lynx._currentClientId = existingClients[0]!.clientId(); } else { // Wait until a client is attached const clientId = await clientPromise; From 013788f59c3937d6aedfad69acddf2b0ab611687 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:16:58 +0800 Subject: [PATCH 05/31] refactor: switch Lynx connection to devtool-connector and add configurable device/app package targeting. --- .../testing-library/kitten-lynx/AGENTS.md | 25 +- .../testing-library/kitten-lynx/package.json | 2 +- .../kitten-lynx/src/CDPChannel.ts | 131 +- .../kitten-lynx/src/ElementNode.ts | 32 + .../testing-library/kitten-lynx/src/Lynx.ts | 186 +- .../kitten-lynx/src/LynxView.ts | 214 +-- .../testing-library/kitten-lynx/tsconfig.json | 5 + pnpm-lock.yaml | 1492 +---------------- 8 files changed, 352 insertions(+), 1735 deletions(-) diff --git a/packages/testing-library/kitten-lynx/AGENTS.md b/packages/testing-library/kitten-lynx/AGENTS.md index a322377d1d..c0e49a7e17 100644 --- a/packages/testing-library/kitten-lynx/AGENTS.md +++ b/packages/testing-library/kitten-lynx/AGENTS.md @@ -4,13 +4,12 @@ This document provides context, architecture guidelines, and workflows for agent ## Overview -`kitten-lynx` is a Puppeteer-like testing library designed for interacting with the Lynx browser engine and Lynx Explorer Android application. It utilizes the `@lynx-js/debug-router-connector` to establish WebSocket connections via ADB to an Android Emulator running the `com.lynx.explorer` app. +`kitten-lynx` is a Puppeteer-like testing library designed for interacting with the Lynx browser engine and Lynx Explorer Android application. It utilizes the `@lynx-js/devtool-connector` (stateless, short-lived connection architecture) to communicate with Lynx apps running on Android devices via ADB. Through the Chrome DevTools Protocol (CDP), `kitten-lynx` enables: - Starting and tearing down `LynxView` instances. -- Spawning pages using deep link intents (`adb shell am start`). -- Navigating and reading the DOM structure. +- Navigating to Lynx bundle URLs and reading the DOM structure. - Querying elements via `DOM.querySelector`. - Reading styles, attributes, and precise boundary boxes of elements. - Simulating native touches through `Input.emulateTouchFromMouseEvent`. @@ -19,11 +18,17 @@ Through the Chrome DevTools Protocol (CDP), `kitten-lynx` enables: ### Connections & Sessions -1. **Lynx.ts**: The entry point. Handles `DebugRouterConnector` initialization, spawning devices, waiting for a client attachment, and enabling DevTools switches inside Lynx (`enable_devtool`). -2. **LynxView.ts**: Manages individual pages. Executes `adb` intents to start the Lynx Shell Activity for a specific URL, then establishes a CDP session utilizing `onAttachedToTarget`. -3. **CDPChannel.ts**: A wrapper that abstracts sending and receiving asynchronous `Customized` CDP commands over the router. +1. **Lynx.ts**: The entry point. Initializes `Connector` with `AndroidTransport`, discovers ADB devices, restarts the target app, and polls `listClients()` to find the Lynx client. Accepts `ConnectOptions` to target a specific device and app package. +2. **LynxView.ts**: Manages individual pages. Attaches to a CDP session via `sendListSessionMessage()`, sends `Page.navigate` to load a Lynx bundle, then polls sessions by URL to find and re-attach to the correct session (apps may have multiple Lynx views). +3. **CDPChannel.ts**: A stateless wrapper that sends CDP commands via `connector.sendCDPMessage()`. Each call is a short-lived request/response — no persistent connection is maintained. 4. **ElementNode.ts**: A wrapper around `nodeId`s matching an element. Implements interactive methods like `getAttribute()`, `computedStyleMap()`, and `tap()`. +### Key Design Patterns + +- **Stateless connector**: The `devtool-connector` does not maintain persistent WebSocket connections. Each `sendCDPMessage` / `sendListSessionMessage` call is a self-contained request through ADB/USB transport. +- **Retry-based initialization**: After restarting the app, polling loops handle the delay before the devtool server is ready. `onAttachedToTarget()` only assigns `_channel` after all CDP domain enables succeed, making the whole operation retryable. +- **Session URL matching**: After `Page.navigate`, the Lynx runtime creates a new session for the navigated URL. `goto()` polls `sendListSessionMessage()` and matches sessions by URL (full URL, filename, or suffix) to find the correct one. + ### Prerequisites For the library to interact successfully: @@ -33,6 +38,14 @@ For the library to interact successfully: - ADB port `5555` should be exposed or forwarded to control the emulator programmatically. - Typical commands use `pnpm run test` starting `vitest` logic inside the Node wrapper. +### Known Gotchas + +- **`Page.navigate` does not work like Chrome**: In Lynx, `Page.navigate` tells the runtime to load a new bundle, which creates a **new session** rather than updating the current one in place. You must poll `sendListSessionMessage()` to find the new session by URL and re-attach to it. +- **`App.openPage` is not implemented** in Lynx Explorer 3.6.0. Do not rely on `sendAppMessage('App.openPage')` for navigation. +- **Docker emulator has no internet**: The Android emulator in Docker cannot reach external hosts. For tests using remote bundle URLs, use `adb reverse` port forwarding to serve bundles locally, or ensure the Docker network allows outbound traffic. +- **Multiple ADB targets**: When multiple ADB devices are connected (e.g. physical phone + emulator), use `ConnectOptions.deviceId` to target a specific one. Otherwise the first available client is used, which may be on the wrong device. +- **CDP timeouts**: The connector uses a 5-second `AbortSignal.timeout`. Keep test operations tolerant of emulator boot/warm-up times. + ## Adding Features When extending the `kitten-lynx` testing library, adhere to these rules: diff --git a/packages/testing-library/kitten-lynx/package.json b/packages/testing-library/kitten-lynx/package.json index e2fd96a232..34f27cb5a7 100644 --- a/packages/testing-library/kitten-lynx/package.json +++ b/packages/testing-library/kitten-lynx/package.json @@ -30,7 +30,7 @@ "test": "vitest run" }, "dependencies": { - "@lynx-js/debug-router-connector": "0.0.6" + "@lynx-js/devtool-connector": "workspace:*" }, "devDependencies": { "@types/node": "^22.0.0", diff --git a/packages/testing-library/kitten-lynx/src/CDPChannel.ts b/packages/testing-library/kitten-lynx/src/CDPChannel.ts index 387941b5e6..7956180069 100644 --- a/packages/testing-library/kitten-lynx/src/CDPChannel.ts +++ b/packages/testing-library/kitten-lynx/src/CDPChannel.ts @@ -1,11 +1,18 @@ -import type { DebugRouterConnector } from '@lynx-js/debug-router-connector'; +import type { Connector } from '@lynx-js/devtool-connector'; const sessionIdToChannel: Record> = {}; type Quad = [number, number, number, number, number, number, number, number]; +/** + * Represents a node in the DOM tree returned by `DOM.getDocument`. + */ export interface NodeInfoInGetDocument { + /** Unique DOM node identifier. */ nodeId: number; + /** Child nodes of this node. */ children: NodeInfoInGetDocument[]; + /** Flat array of alternating attribute name/value pairs. */ attributes: string[]; + /** The node's tag name (e.g. `'view'`, `'text'`). */ nodeName: string; } interface Protocol { @@ -78,12 +85,30 @@ interface Protocol { }; } +/** + * A stateless CDP (Chrome DevTools Protocol) channel for sending commands + * to a specific Lynx session. + * + * Each `send()` call is a short-lived request/response via the `Connector`. + * Channels are cached per session ID using `WeakRef` to allow reuse. + */ export class CDPChannel { private _id = 0; + /** + * Get or create a CDPChannel for the given session. + * + * Channels are cached per `sessionId` using `WeakRef`. If a previously + * created channel for the same session is still alive, it is reused. + * + * @param sessionId - The Lynx devtool session ID. + * @param clientId - The client identifier (format: `"deviceId:port"`). + * @param connector - The `Connector` instance for sending messages. + * @returns A `CDPChannel` bound to the specified session. + */ static from( sessionId: number, - clientId: number, - connector: DebugRouterConnector, + clientId: string, + connector: Connector, ): CDPChannel { const maybeChannel = sessionIdToChannel[sessionId]?.deref(); if (maybeChannel) return maybeChannel; @@ -94,101 +119,27 @@ export class CDPChannel { } } constructor( - private _connector: DebugRouterConnector, - private _clientId: number, + private _connector: Connector, + private _clientId: string, private _sessionId: number, ) {} + /** + * Send a CDP command and return the result. + * + * @param method - The CDP method name (e.g. `'DOM.getDocument'`, `'Page.navigate'`). + * @param params - The parameters for the CDP method. + * @returns The CDP response for the given method. + */ async send( method: T, params: Protocol[T]['params'], ): Promise { - const id = ++this._id; - - const { promise, resolve, reject } = Promise.withResolvers< - Protocol[T]['return'] - >(); - - const msgId = `CDP-${id}`; - const listener = ( - { message, id: sourceId }: { message: string; id: number }, - ) => { - const parsed = JSON.parse(message); - if (parsed.event !== 'Customized' || parsed.data?.type !== 'CDP') { - return; - } - - const cdpData = parsed.data.data; - if (cdpData.session_id !== this._sessionId) return; - - const cdpMessage = typeof cdpData.message === 'string' - ? JSON.parse(cdpData.message) - : cdpData.message; - if (cdpMessage.id !== id) return; - - this._connector.off('usb-client-message', listener); - - if (cdpMessage.error) { - reject(new Error(cdpMessage.error.message)); - } else { - resolve(cdpMessage.result); - } - }; - - this._connector.on('usb-client-message', listener); - - this._connector.sendMessageToApp( + return await this._connector.sendCDPMessage( this._clientId, - JSON.stringify({ - event: 'Customized', - data: { - type: 'CDP', - data: { - client_id: this._clientId, - session_id: this._sessionId, - message: { - method, - id, - params, - }, - }, - sender: this._clientId, - }, - from: this._clientId, - }), + this._sessionId, + method, + params as any, ); - - // Add simple timeout - setTimeout(() => { - this._connector.off('usb-client-message', listener); - reject(new Error(`Timeout waiting for CDP method: ${method}`)); - }, 5000); - - return promise; - } - - onEvent(method: string, listener: (params: any) => void): () => void { - const handler = ( - { message, id: sourceId }: { message: string; id: number }, - ) => { - const parsed = JSON.parse(message); - if (parsed.event !== 'Customized' || parsed.data?.type !== 'CDP') { - return; - } - - const cdpData = parsed.data.data; - if (cdpData.session_id !== this._sessionId) return; - - const cdpMessage = typeof cdpData.message === 'string' - ? JSON.parse(cdpData.message) - : cdpData.message; - - if (cdpMessage.method === method) { - listener(cdpMessage.params); - } - }; - - this._connector.on('usb-client-message', handler); - return () => this._connector.off('usb-client-message', handler); } } diff --git a/packages/testing-library/kitten-lynx/src/ElementNode.ts b/packages/testing-library/kitten-lynx/src/ElementNode.ts index f92072f944..c65d8072dd 100644 --- a/packages/testing-library/kitten-lynx/src/ElementNode.ts +++ b/packages/testing-library/kitten-lynx/src/ElementNode.ts @@ -2,7 +2,22 @@ import type { LynxView } from './LynxView.js'; const idToElementNode = new WeakMap[]>(); +/** + * Represents a DOM element in a Lynx page. + * + * Wraps a CDP `nodeId` and provides methods for inspecting attributes, + * computed styles, and simulating user interactions like taps. + */ export class ElementNode { + /** + * Get or create an ElementNode for the given node ID within a LynxView. + * + * Nodes are cached per LynxView using `WeakRef` to allow reuse and GC. + * + * @param id - The CDP node ID. + * @param lynxView - The owning LynxView instance. + * @returns An `ElementNode` bound to the given node. + */ static fromId(id: number, lynxView: LynxView): ElementNode { const currentViewMap = idToElementNode.get(lynxView); if (currentViewMap) { @@ -25,6 +40,12 @@ export class ElementNode { constructor(public readonly nodeId: number, private _lynxView: LynxView) {} + /** + * Simulate a tap (touch press + release) on the center of this element. + * + * Uses `DOM.getBoxModel` to find the element's center coordinates, + * then dispatches `mousePressed` and `mouseReleased` events. + */ async tap(): Promise { const { model } = await this._lynxView._channel.send('DOM.getBoxModel', { nodeId: this.nodeId, @@ -61,6 +82,12 @@ export class ElementNode { }); } + /** + * Get the value of a named attribute on this element. + * + * @param name - The attribute name. Use `'id'` for the Lynx `idSelector` attribute. + * @returns The attribute value, or `null` if the attribute is not present. + */ async getAttribute(name: string): Promise { if (name === 'id') { name = 'idSelector'; @@ -77,6 +104,11 @@ export class ElementNode { return attributes[name] ?? null; } + /** + * Get all computed CSS styles for this element. + * + * @returns A `Map` of CSS property names to their computed values. + */ async computedStyleMap(): Promise> { const map = new Map(); const ret = await this._lynxView._channel.send( diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index fa4b774d8a..b653e61707 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -1,17 +1,62 @@ -import { - DebugRouterConnector, - MultiOpenStatus, -} from '@lynx-js/debug-router-connector'; +import { Connector } from '@lynx-js/devtool-connector'; +import { AndroidTransport } from '@lynx-js/devtool-connector/transport'; import { LynxView } from './LynxView.js'; import { execSync } from 'child_process'; +/** + * Options for configuring the connection to a Lynx device. + */ +export interface ConnectOptions { + /** + * ADB device serial to target (e.g. `"localhost:5555"`, `"emulator-5554"`). + * When multiple ADB devices are connected, use this to select the correct one. + * If omitted, uses the first available device. + */ + deviceId?: string; + /** + * App package name to launch on the device. + * @default "com.lynx.explorer" + */ + appPackage?: string; +} + +const DEFAULT_APP_PACKAGE = 'com.lynx.explorer'; + +/** + * Main entry point for the kitten-lynx testing framework. + * + * Provides Puppeteer-like APIs for connecting to a Lynx app running on an + * Android device (physical or emulator) via ADB and the Chrome DevTools Protocol. + * + * @example + * ```typescript + * const lynx = await Lynx.connect({ deviceId: 'localhost:5555' }); + * const page = await lynx.newPage(); + * await page.goto('http://example.com/bundle.lynx.bundle'); + * const content = await page.content(); + * await lynx.close(); + * ``` + */ export class Lynx { - private _connector: DebugRouterConnector | null = null; + private _connector: Connector | null = null; private _currentClient: any | null = null; - private _currentClientId: number = -1; + private _currentClientId: string = ''; + + /** + * Connect to a Lynx app on an Android device. + * + * Discovers ADB devices, restarts the target app, and waits for a Lynx + * devtool client to become available. + * + * @param options - Connection options to specify target device and app package. + * @returns A connected `Lynx` instance ready for creating pages. + * @throws If no Lynx client is found after 10 seconds of polling. + */ + static async connect(options?: ConnectOptions): Promise { + const targetDevice = options?.deviceId; + const appPackage = options?.appPackage ?? DEFAULT_APP_PACKAGE; - static async connect(): Promise { try { const output = execSync('adb devices').toString(); const lines = output.split('\n'); @@ -26,14 +71,19 @@ export class Lynx { } } - for (const deviceId of adbDevices) { + // If a specific device is requested, only restart that one + const devicesToRestart = targetDevice + ? adbDevices.filter(d => d === targetDevice) + : adbDevices; + + for (const deviceId of devicesToRestart) { try { console.log( - `[Lynx] Restarting com.lynx.explorer on device ${deviceId}...`, + `[Lynx] Restarting ${appPackage} on device ${deviceId}...`, ); - execSync(`adb -s ${deviceId} shell am force-stop com.lynx.explorer`); + execSync(`adb -s ${deviceId} shell am force-stop ${appPackage}`); execSync( - `adb -s ${deviceId} shell monkey -p com.lynx.explorer -c android.intent.category.LAUNCHER 1`, + `adb -s ${deviceId} shell monkey -p ${appPackage} -c android.intent.category.LAUNCHER 1`, ); } catch (e) { console.error( @@ -50,95 +100,43 @@ export class Lynx { const lynx = new Lynx(); - lynx._connector = new DebugRouterConnector({ - manualConnect: true, - enableWebSocket: true, // Used for local debugging in docker/adb - enableDesktop: true, - }); - - const { promise: startedPromise, resolve: startedResolve } = Promise - .withResolvers(); - - // Auto-remove connector upon disconnect - lynx._connector.setMultiOpenCallback({ - statusChanged(status: MultiOpenStatus) { - if (status === MultiOpenStatus.unattached) { - lynx._connector = null; - } - }, - }); - - await lynx._connector.startWSServer(); - - lynx._connector.on('device-connected', (device) => { - device.startWatchClient(); - }); - - const { promise: clientPromise, resolve: clientResolve } = Promise - .withResolvers(); + lynx._connector = new Connector([new AndroidTransport()]); - lynx._connector.on('client-connected', async (client) => { - startedResolve(); // Unblock initial wait - clientResolve(client.clientId()); - }); + let clients = await lynx._connector.listClients(); + let attempts = 0; + while (clients.length === 0 && attempts < 20) { + await new Promise(r => setTimeout(r, 500)); + clients = await lynx._connector.listClients(); + attempts++; + } - if (lynx._connector.devices.size === 0) { - const devices = await lynx._connector.connectDevices(1000); - if (devices.length === 0) { - throw new Error('Failed to connect to Lynx: no device found.'); - } - for (const device of devices) { - device.startWatchClient(); - } + // Filter clients by deviceId if specified (client.id format: "deviceId:port") + if (targetDevice) { + clients = clients.filter(c => c.id.startsWith(targetDevice + ':')); } - const usbClients = lynx._connector.getAllUsbClients(); - if (usbClients.length > 0) { - lynx._currentClient = usbClients[0]; - lynx._currentClientId = usbClients[0]!.clientId(); - } else { - const existingClients = lynx._connector.getAllAppClients(); - if (existingClients.length > 0) { - lynx._currentClient = existingClients[0]; - lynx._currentClientId = existingClients[0]!.clientId(); - } else { - // Wait until a client is attached - const clientId = await clientPromise; - const allUsbs = lynx._connector.getAllUsbClients(); - lynx._currentClient = allUsbs.find((c: any) => - c.clientId() === clientId - ) || null; - lynx._currentClientId = clientId; - } + if (clients.length === 0) { + throw new Error( + targetDevice + ? `Failed to connect to Lynx: no client found on device "${targetDevice}" after 10 seconds.` + : 'Failed to connect to Lynx: no client found after 10 seconds.', + ); } - // Force enable devtools toggle if required - lynx._connector.sendMessageToApp( - lynx._currentClientId, - JSON.stringify({ - event: 'Customized', - data: { - type: 'SetGlobalSwitch', - data: { - client_id: lynx._currentClientId, - message: JSON.stringify({ - global_key: 'enable_devtool', - global_value: true, - id: 10000, - }), - session_id: -1, - }, - sender: lynx._currentClientId, - }, - from: lynx._currentClientId, - }), - ); + lynx._currentClient = clients[0]; + lynx._currentClientId = clients[0]!.id; return lynx; } + /** + * Create a new page (LynxView) for navigating and interacting with Lynx content. + * + * @returns A new {@link LynxView} instance bound to the current client. + * @throws If not connected. Call {@link Lynx.connect} first. + */ async newPage(): Promise { - if (!this._connector || this._currentClientId === -1) { + if (!this._connector || this._currentClientId === '') { throw new Error('Not connected. Call Lynx.connect() first.'); } return new LynxView( @@ -148,12 +146,10 @@ export class Lynx { ); } + /** + * Close the connection and release resources. + */ async close(): Promise { - if (this._connector) { - if (this._connector.wss) { - this._connector.wss.close(); - } - this._connector = null; - } + this._connector = null; } } diff --git a/packages/testing-library/kitten-lynx/src/LynxView.ts b/packages/testing-library/kitten-lynx/src/LynxView.ts index 93b318b8e2..9797d90d7a 100644 --- a/packages/testing-library/kitten-lynx/src/LynxView.ts +++ b/packages/testing-library/kitten-lynx/src/LynxView.ts @@ -1,128 +1,123 @@ -import type { DebugRouterConnector } from '@lynx-js/debug-router-connector'; +import type { Connector } from '@lynx-js/devtool-connector'; import { CDPChannel } from './CDPChannel.js'; import type { NodeInfoInGetDocument } from './CDPChannel.js'; import { ElementNode } from './ElementNode.js'; const idToLynxView: Record> = {}; +/** + * Represents a Lynx page instance, similar to Puppeteer's `Page`. + * + * Provides methods for navigating to Lynx bundle URLs, querying the DOM, + * and reading page content. Created via {@link Lynx.newPage}. + */ export class LynxView { private static incId = 1; private _root?: ElementNode; _channel!: CDPChannel; readonly id: number; + /** + * Retrieve a previously created LynxView by its string ID. + * + * @param id - The string representation of the LynxView's numeric ID. + * @returns The LynxView if still alive, or `undefined` if garbage-collected. + */ static getLynxViewById(id: string): LynxView | undefined { return idToLynxView[id]?.deref(); } constructor( - private _connector: DebugRouterConnector, - private _clientId: number, + private _connector: Connector, + private _clientId: string, private _client?: any, ) { this.id = LynxView.incId++; idToLynxView[this.id.toString()] = new WeakRef(this); } + /** + * Navigate to a Lynx bundle URL. + * + * Attaches to an existing CDP session, sends `Page.navigate`, then polls + * for the session whose URL matches the target bundle. Re-attaches to + * the matched session and refreshes the DOM tree. + * + * @param url - The Lynx bundle URL to navigate to. + * @throws If no session can be attached or no session matches the URL. + */ async goto(url: string, _options?: unknown): Promise { + // Attach to any existing session first so we can send Page.navigate if (!this._channel) { - if ( - this._client && typeof this._client.sendClientMessage === 'function' - ) { - const { promise: newClientPromise, resolve: newClientResolve } = Promise - .withResolvers(); - const clientListener = (client: any) => newClientResolve(client); - this._connector.on('client-connected', clientListener); - - this._client.sendClientMessage('App.openPage', { url }); - - const newClientTimeout = setTimeout(() => { - this._connector.off('client-connected', clientListener); - newClientResolve(null); - }, 10000); - - const newClient = await newClientPromise; - clearTimeout(newClientTimeout); - this._connector.off('client-connected', clientListener); - - if (newClient) { - this._client = newClient; - this._clientId = newClient.clientId(); - } - } - - // Fetch initial session - const { promise, resolve, reject } = Promise.withResolvers(); - let isSettled = false; - const listener = ({ message, id }: { message: string; id: number }) => { - const parsed = JSON.parse(message); - // Log all messages locally to find the session - if ( - parsed.event === 'Customized' && parsed.data?.type === 'SessionList' - ) { - const sessions = parsed.data.data; + for (let attempt = 0; attempt < 20; attempt++) { + try { + const sessions = await this._connector.sendListSessionMessage( + this._clientId, + ); if (sessions.length > 0) { - isSettled = true; - this._connector.off('usb-client-message', listener); - resolve(sessions[sessions.length - 1].session_id); + const sessionId = sessions[sessions.length - 1]!.session_id; + await this.onAttachedToTarget(sessionId); + break; } + } catch { + // Session listing or CDP enable failed — app may still be initializing } - }; - this._connector.on('usb-client-message', listener); - - // Periodically ask for SessionList until settled or timeout - const pollInterval = setInterval(() => { - if (isSettled) { - clearInterval(pollInterval); - return; - } - this._connector.sendMessageToApp( - this._clientId, - JSON.stringify({ - event: 'Customized', - data: { - type: 'ListSession', - data: [], - sender: this._clientId, - }, - from: this._clientId, - }), - ); - }, 500); - - setTimeout(() => { - if (!isSettled) { - isSettled = true; - clearInterval(pollInterval); - this._connector.off('usb-client-message', listener); - reject(new Error('Timeout waiting for session')); - } - }, 15000); + await new Promise(r => setTimeout(r, 500)); + } - const sessionId = await promise; - await this.onAttachedToTarget(sessionId); + if (!this._channel) { + throw new Error('Failed to attach to a session'); + } } - const { promise: execPromise, resolve: execResolve } = Promise - .withResolvers(); - const off = this._channel.onEvent('DOM.childNodeInserted', () => { - off(); - execResolve(); - }); + // Navigate via CDP Page.navigate + try { + await this._channel.send('Page.navigate', { url }); + } catch { + // ignore — not all Lynx versions support this + } - const fallbackTimeout = setTimeout(() => { - off(); - execResolve(); - }, 5000); + // Extract the bundle filename from the URL for matching + const urlPath = url.split('/').pop() || url; - // Launch the URL via CDP Page.navigate message - await this._channel.send('Page.navigate', { url }); + // Poll for the session whose URL matches the navigated bundle + let matchedSessionId: number | undefined; + for (let attempt = 0; attempt < 20; attempt++) { + await new Promise(r => setTimeout(r, 500)); + try { + const sessions = await this._connector.sendListSessionMessage( + this._clientId, + ); + const matched = sessions.find( + s => s.url === url || s.url === urlPath || url.endsWith(s.url), + ); + if (matched) { + matchedSessionId = matched.session_id; + break; + } + } catch { + // ignore and retry + } + } - // Wait for the new page to establish its execution context instead of fixed timers - await execPromise; - clearTimeout(fallbackTimeout); + // Re-attach to the correct session if it differs from the current one + if (matchedSessionId !== undefined) { + this._channel = undefined as any; + for (let attempt = 0; attempt < 10; attempt++) { + try { + await this.onAttachedToTarget(matchedSessionId); + break; + } catch { + await new Promise(r => setTimeout(r, 500)); + } + } + } + + if (!this._channel) { + throw new Error('Failed to attach to session for URL: ' + url); + } - // Refresh the DOM because navigation happened + // Refresh the DOM tree from the attached session const response = await this._channel.send('DOM.getDocument', { depth: -1, }); @@ -130,6 +125,13 @@ export class LynxView { this._root = ElementNode.fromId(root.nodeId, this); } + /** + * Find the first element matching a CSS selector in the current page. + * + * @param selector - A CSS selector string (e.g. `'view'`, `'#my-id'`). + * @returns The matched {@link ElementNode}, or `undefined` if not found. + * @throws If no page has been loaded yet. Call {@link goto} first. + */ async locator(selector: string): Promise { if (!this._root) { throw new Error('Not connected to a document yet. Call goto() first.'); @@ -144,24 +146,36 @@ export class LynxView { return; } + /** + * Attach to a CDP session by session ID and enable required domains. + * + * Creates a {@link CDPChannel}, enables `Runtime`, `Page`, and `DOM` domains, + * and fetches the initial document tree. Only sets `_channel` after all + * operations succeed, making this method safely retryable. + * + * @param sessionId - The Lynx devtool session ID to attach to. + * @internal + */ async onAttachedToTarget(sessionId: number) { if (!this._channel) { - this._channel = CDPChannel.from( + const channel = CDPChannel.from( sessionId, this._clientId, this._connector, ); - // Enable DOM and Page agents + // Enable DOM and Page agents — may fail if devtool server isn't ready await Promise.all([ - this._channel.send('Runtime.enable' as any, {}), - this._channel.send('Page.enable' as any, {}).catch(() => {}), - this._channel.send('DOM.enable' as any, {}).catch(() => {}), + channel.send('Runtime.enable' as any, {}), + channel.send('Page.enable' as any, {}).catch(() => {}), + channel.send('DOM.enable' as any, {}).catch(() => {}), ]); - const response = await this._channel.send('DOM.getDocument', { + const response = await channel.send('DOM.getDocument', { depth: -1, }); const root = response.root.children[0]!; this._root = ElementNode.fromId(root.nodeId, this); + // Only set channel after everything succeeds + this._channel = channel; } } @@ -183,6 +197,14 @@ export class LynxView { buffer.push(''); } + /** + * Serialize the current page's DOM tree to an HTML-like string. + * + * Fetches a fresh `DOM.getDocument` snapshot and recursively converts + * all nodes to a string representation with tag names and attributes. + * + * @returns The serialized DOM content of the page. + */ async content(): Promise { const document = await this._channel.send('DOM.getDocument', { depth: -1, diff --git a/packages/testing-library/kitten-lynx/tsconfig.json b/packages/testing-library/kitten-lynx/tsconfig.json index 90a4cfc737..67ef1b624d 100644 --- a/packages/testing-library/kitten-lynx/tsconfig.json +++ b/packages/testing-library/kitten-lynx/tsconfig.json @@ -13,6 +13,11 @@ "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "@lynx-js/devtool-connector": ["../../mcp-servers/devtool-connector/dist/index.d.ts"], + "@lynx-js/devtool-connector/transport": ["../../mcp-servers/devtool-connector/dist/transport/index.d.ts"], + }, }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a32d885699..6fa81503f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1224,9 +1224,9 @@ importers: packages/testing-library/kitten-lynx: dependencies: - '@lynx-js/debug-router-connector': - specifier: 0.0.6 - version: 0.0.6 + '@lynx-js/devtool-connector': + specifier: workspace:* + version: link:../../mcp-servers/devtool-connector devDependencies: '@types/node': specifier: ^22.0.0 @@ -2094,169 +2094,6 @@ packages: resolution: {integrity: sha512-Hb4o6h1Pf6yRUAX07DR4JVY7dmQw+RVQMW5/m55GoiAT/VRoKCWBtIUPPOnqDVhbx1Cjfil9b6EDrgJsUAujEQ==} engines: {node: '>= 10'} - '@aws-crypto/crc32@5.2.0': - resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/crc32c@5.2.0': - resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} - - '@aws-crypto/sha1-browser@5.2.0': - resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} - - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/client-s3@3.669.0': - resolution: {integrity: sha512-diqkwSqWjgAZ5NdYOtpYI/JlwsFDmo7x2Q+QOHevFaCEp4vgZOwJyXE3DQbKoO1X10fMH2vOUULgonPJd4ClUQ==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/client-sso-oidc@3.669.0': - resolution: {integrity: sha512-E7uYOS3Axhe36Zeq6iLC9kjF1mMEyCQ4fXud11h22rbjq7PFUtN2Omekrch37eUx3BFj1jMePnuTnT98t5LWnw==} - engines: {node: '>=16.0.0'} - peerDependencies: - '@aws-sdk/client-sts': ^3.669.0 - - '@aws-sdk/client-sso@3.669.0': - resolution: {integrity: sha512-WNpfNYIHzehLv98F+KolJglXNjJOTbOvIbSZ2XAnebVLmXCWeEEd1r4dIH0oI2dHtLbQ/h3uqaeQhsVQjLAxpw==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/client-sts@3.669.0': - resolution: {integrity: sha512-1XdOBtHKCVxVkEDiy+oktJNSsySj3ADyh58KpHaqgvCQKV3vmfkr0YO5dG4kqq+TflNwdfl1YgMuOUiCulOWeQ==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/core@3.667.0': - resolution: {integrity: sha512-pMcDVI7Tmdsc8R3sDv0Omj/4iRParGY+uJtAfF669WnZfDfaBQaix2Mq7+Mu08vdjqO9K3gicFvjk9S1VLmOKA==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/credential-provider-env@3.667.0': - resolution: {integrity: sha512-zZbrkkaPc54WXm+QAnpuv0LPNfsts0HPPd+oCECGs7IQRaFsGj187cwvPg9RMWDFZqpm64MdBDoA8OQHsqzYCw==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/credential-provider-http@3.667.0': - resolution: {integrity: sha512-sjtybFfERZWiqTY7fswBxKQLvUkiCucOWyqh3IaPo/4nE1PXRnaZCVG0+kRBPrYIxWqiVwytvZzMJy8sVZcG0A==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/credential-provider-ini@3.669.0': - resolution: {integrity: sha512-YHhfH7w29BmMPnOK0BrBhEy2IRFRSRGSCyz3jtqpG883CZ2Lxan/AzaJDfKRdz350KPgbMMBwbPkIrqNIsg8iw==} - engines: {node: '>=16.0.0'} - peerDependencies: - '@aws-sdk/client-sts': ^3.669.0 - - '@aws-sdk/credential-provider-node@3.669.0': - resolution: {integrity: sha512-O506azQcq6N1gnDX870MXXL9LHlhX0k6BlLMM6IDClxVDnlNkK3+n2cAEKSy8HwZJcRlekcsKz/AS2CxjPY+fg==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/credential-provider-process@3.667.0': - resolution: {integrity: sha512-HZHnvop32fKgsNHkdhVaul7UzQ25sEc0j9yqA4bjhtbk0ECl42kj3f1pJ+ZU/YD9ut8lMJs/vVqiOdNThVdeBw==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/credential-provider-sso@3.669.0': - resolution: {integrity: sha512-HvpMJQ8xZuEGjadARVOfORPZx4U23PC5Jf6Fj+/NWK4VxEXhvf8J037fvGp3xRds5wUeuBBbhWX+Cbt0lbLCwQ==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.667.0': - resolution: {integrity: sha512-t8CFlZMD/1p/8Cli3rvRiTJpjr/8BO64gw166AHgFZYSN2h95L2l1tcW0jpsc3PprA32nLg1iQVKYt4WGM4ugw==} - engines: {node: '>=16.0.0'} - peerDependencies: - '@aws-sdk/client-sts': ^3.667.0 - - '@aws-sdk/middleware-bucket-endpoint@3.667.0': - resolution: {integrity: sha512-XGz4jMAkDoTyFdtLz7ZF+C05IAhCTC1PllpvTBaj821z/L0ilhbqVhrT/f2Buw8Id/K5A390csGXgusXyrFFjA==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/middleware-expect-continue@3.667.0': - resolution: {integrity: sha512-0TiSL9S5DSG95NHGIz6qTMuV7GDKVn8tvvGSrSSZu/wXO3JaYSH0AElVpYfc4PtPRqVpEyNA7nnc7W56mMCLWQ==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/middleware-flexible-checksums@3.669.0': - resolution: {integrity: sha512-01UQLoUzVwWMf+b+AEuwJ2lluBD+Cp8AcbyEHqvEaPdjGKHIS4BCvnY70mZYnAfRtL8R2h9tt7iI61oWU3Gjkg==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/middleware-host-header@3.667.0': - resolution: {integrity: sha512-Z7fIAMQnPegs7JjAQvlOeWXwpMRfegh5eCoIP6VLJIeR6DLfYKbP35JBtt98R6DXslrN2RsbTogjbxPEDQfw1w==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/middleware-location-constraint@3.667.0': - resolution: {integrity: sha512-ob85H3HhT3/u5O+x0o557xGZ78vSNeSSwMaSitxdsfs2hOuoUl1uk+OeLpi1hkuJnL41FPpokV7TVII2XrFfmg==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/middleware-logger@3.667.0': - resolution: {integrity: sha512-PtTRNpNm/5c746jRgZCNg4X9xEJIwggkGJrF0GP9AB1ANg4pc/sF2Fvn1NtqPe9wtQ2stunJprnm5WkCHN7QiA==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/middleware-recursion-detection@3.667.0': - resolution: {integrity: sha512-U5glWD3ehFohzpUpopLtmqAlDurGWo2wRGPNgi4SwhWU7UDt6LS7E/UvJjqC0CUrjlzOw+my2A+Ncf+fisMhxQ==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/middleware-sdk-s3@3.669.0': - resolution: {integrity: sha512-b2QUQ7DcIcVCUFhvmFEDI90BemvQhO0ntIajllLqQSy88PSNdLDCVx5mIzfxaaK/1tdY/UsEDRRm1kMQHJDQpg==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/middleware-ssec@3.667.0': - resolution: {integrity: sha512-1wuAUZIkmZIvOmGg5qNQU821CGFHhkuKioxXgNh0DpUxZ9+AeiV7yorJr+bqkb2KBFv1i1TnzGRecvKf/KvZIQ==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/middleware-user-agent@3.669.0': - resolution: {integrity: sha512-K8ScPi45zjJrj5Y2gRqVsvKKQCQbvQBfYGcBw9ZOx9TTavH80bOCBjWg/GFnvs4f37tqVc1wMN2oGvcTF6HveQ==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/region-config-resolver@3.667.0': - resolution: {integrity: sha512-iNr+JhhA902JMKHG9IwT9YdaEx6KGl6vjAL5BRNeOjfj4cZYMog6Lz/IlfOAltMtT0w88DAHDEFrBd2uO0l2eg==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/signature-v4-multi-region@3.669.0': - resolution: {integrity: sha512-TVwlWAxfBHnFjnfTBQWUhzVJzjwVhkq1+KR0JZV7JrfqeyBOdZjAaV9ie3VNY9HUouecq1fDuKaSwe4JiWQsHg==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/token-providers@3.667.0': - resolution: {integrity: sha512-ZecJlG8p6D4UTYlBHwOWX6nknVtw/OBJ3yPXTSajBjhUlj9lE2xvejI8gl4rqkyLXk7z3bki+KR4tATbMaM9yg==} - engines: {node: '>=16.0.0'} - peerDependencies: - '@aws-sdk/client-sso-oidc': ^3.667.0 - - '@aws-sdk/types@3.667.0': - resolution: {integrity: sha512-gYq0xCsqFfQaSL/yT1Gl1vIUjtsg7d7RhnUfsXaHt8xTxOKRTdH9GjbesBjXOzgOvB0W0vfssfreSNGFlOOMJg==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/util-arn-parser@3.568.0': - resolution: {integrity: sha512-XUKJWWo+KOB7fbnPP0+g/o5Ulku/X53t7i/h+sPHr5xxYTJJ9CYnbToo95mzxe7xWvkLrsNtJ8L+MnNn9INs2w==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/util-endpoints@3.667.0': - resolution: {integrity: sha512-X22SYDAuQJWnkF1/q17pkX3nGw5XMD9YEUbmt87vUnRq7iyJ3JOpl6UKOBeUBaL838wA5yzdbinmCITJ/VZ1QA==} - engines: {node: '>=16.0.0'} - - '@aws-sdk/util-locate-window@3.965.4': - resolution: {integrity: sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-user-agent-browser@3.667.0': - resolution: {integrity: sha512-y1pKlNzNpxzddM0QSnfIfIbi3Z9LTag1VDjYyZRbEGGSVip2J00qKsET+979nRezWMyJgw5GPBQR3Y+rN+jh0Q==} - - '@aws-sdk/util-user-agent-node@3.669.0': - resolution: {integrity: sha512-9jxCYrgggy2xd44ZASqI7AMiRVaSiFp+06Kg8BQSU0ijKpBJlwcsqIS8pDT/n6LxuOw2eV5ipvM2C0r1iKzrGA==} - engines: {node: '>=16.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - - '@aws-sdk/xml-builder@3.662.0': - resolution: {integrity: sha512-ikLkXn0igUpnJu2mCZjklvmcDGWT9OaLRv3JyC/cRkTaaSrblPjPM7KKsltxdMTLQ+v7fjCN0TsJpxphMfaOPA==} - engines: {node: '>=16.0.0'} - '@babel/code-frame@7.26.2': resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==} engines: {node: '>=6.9.0'} @@ -2708,19 +2545,6 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} - '@devicefarmer/adbkit-logcat@2.1.3': - resolution: {integrity: sha512-yeaGFjNBc/6+svbDeul1tNHtNChw6h8pSHAt5D+JsedUrMTN7tla7B15WLDyekxsuS2XlZHRxpuC6m92wiwCNw==} - engines: {node: '>= 4'} - - '@devicefarmer/adbkit-monkey@1.2.1': - resolution: {integrity: sha512-ZzZY/b66W2Jd6NHbAhLyDWOEIBWC11VizGFk7Wx7M61JZRz7HR9Cq5P+65RKWUU7u6wgsE8Lmh9nE4Mz+U2eTg==} - engines: {node: '>= 0.10.4'} - - '@devicefarmer/adbkit@3.2.4': - resolution: {integrity: sha512-8Iq5aGMD5jUvg86rJnU4zbsYfIaWMl7Ch+BhGnVRD1BqaVs0fQAUOMQzqGrFe01GlbBx3GX5T0ppNQC5IizL3g==} - engines: {node: '>= 0.10.4'} - hasBin: true - '@discoveryjs/json-ext@0.5.7': resolution: {integrity: sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==} engines: {node: '>=10.0.0'} @@ -3388,9 +3212,6 @@ packages: '@leichtgewicht/ip-codec@2.0.5': resolution: {integrity: sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==} - '@lynx-js/debug-router-connector@0.0.6': - resolution: {integrity: sha512-ZJAFRfG/zzfAF7+GxtWuT/VTRNaTt0FDvlDiJJdHhHY44YenCvuPSuDnKnqO3U8TREtLrJVDBKgrAvTik6NgRA==} - '@lynx-js/internal-preact@10.28.4-ee7bb26': resolution: {integrity: sha512-Ym60mgGjj/Xc/5yfludUCUXVCyVACUsNaA7E9Uct2yArsT5nlvp9bflUoLrTNMvApKvbhUBCTx27aNYCquhEYg==} @@ -4347,212 +4168,6 @@ packages: '@sinclair/typebox@0.34.37': resolution: {integrity: sha512-2TRuQVgQYfy+EzHRTIvkhv2ADEouJ2xNS/Vq+W5EuuewBdOrvATvljZTxHWZSTYr2sTjTHpGvucaGAt67S2akw==} - '@smithy/abort-controller@3.1.9': - resolution: {integrity: sha512-yiW0WI30zj8ZKoSYNx90no7ugVn3khlyH/z5W8qtKBtVE6awRALbhSG+2SAHA1r6bO/6M9utxYKVZ3PCJ1rWxw==} - engines: {node: '>=16.0.0'} - - '@smithy/chunked-blob-reader-native@3.0.1': - resolution: {integrity: sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ==} - - '@smithy/chunked-blob-reader@4.0.0': - resolution: {integrity: sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ==} - - '@smithy/config-resolver@3.0.13': - resolution: {integrity: sha512-Gr/qwzyPaTL1tZcq8WQyHhTZREER5R1Wytmz4WnVGL4onA3dNk6Btll55c8Vr58pLdvWZmtG8oZxJTw3t3q7Jg==} - engines: {node: '>=16.0.0'} - - '@smithy/core@2.5.7': - resolution: {integrity: sha512-8olpW6mKCa0v+ibCjoCzgZHQx1SQmZuW/WkrdZo73wiTprTH6qhmskT60QLFdT9DRa5mXxjz89kQPZ7ZSsoqqg==} - engines: {node: '>=16.0.0'} - - '@smithy/credential-provider-imds@3.2.8': - resolution: {integrity: sha512-ZCY2yD0BY+K9iMXkkbnjo+08T2h8/34oHd0Jmh6BZUSZwaaGlGCyBT/3wnS7u7Xl33/EEfN4B6nQr3Gx5bYxgw==} - engines: {node: '>=16.0.0'} - - '@smithy/eventstream-codec@3.1.10': - resolution: {integrity: sha512-323B8YckSbUH0nMIpXn7HZsAVKHYHFUODa8gG9cHo0ySvA1fr5iWaNT+iIL0UCqUzG6QPHA3BSsBtRQou4mMqQ==} - - '@smithy/eventstream-serde-browser@3.0.14': - resolution: {integrity: sha512-kbrt0vjOIihW3V7Cqj1SXQvAI5BR8SnyQYsandva0AOR307cXAc+IhPngxIPslxTLfxwDpNu0HzCAq6g42kCPg==} - engines: {node: '>=16.0.0'} - - '@smithy/eventstream-serde-config-resolver@3.0.11': - resolution: {integrity: sha512-P2pnEp4n75O+QHjyO7cbw/vsw5l93K/8EWyjNCAAybYwUmj3M+hjSQZ9P5TVdUgEG08ueMAP5R4FkuSkElZ5tQ==} - engines: {node: '>=16.0.0'} - - '@smithy/eventstream-serde-node@3.0.13': - resolution: {integrity: sha512-zqy/9iwbj8Wysmvi7Lq7XFLeDgjRpTbCfwBhJa8WbrylTAHiAu6oQTwdY7iu2lxigbc9YYr9vPv5SzYny5tCXQ==} - engines: {node: '>=16.0.0'} - - '@smithy/eventstream-serde-universal@3.0.13': - resolution: {integrity: sha512-L1Ib66+gg9uTnqp/18Gz4MDpJPKRE44geOjOQ2SVc0eiaO5l255ADziATZgjQjqumC7yPtp1XnjHlF1srcwjKw==} - engines: {node: '>=16.0.0'} - - '@smithy/fetch-http-handler@3.2.9': - resolution: {integrity: sha512-hYNVQOqhFQ6vOpenifFME546f0GfJn2OiQ3M0FDmuUu8V/Uiwy2wej7ZXxFBNqdx0R5DZAqWM1l6VRhGz8oE6A==} - - '@smithy/fetch-http-handler@4.1.3': - resolution: {integrity: sha512-6SxNltSncI8s689nvnzZQc/dPXcpHQ34KUj6gR/HBroytKOd/isMG3gJF/zBE1TBmTT18TXyzhg3O3SOOqGEhA==} - - '@smithy/hash-blob-browser@3.1.10': - resolution: {integrity: sha512-elwslXOoNunmfS0fh55jHggyhccobFkexLYC1ZeZ1xP2BTSrcIBaHV2b4xUQOdctrSNOpMqOZH1r2XzWTEhyfA==} - - '@smithy/hash-node@3.0.11': - resolution: {integrity: sha512-emP23rwYyZhQBvklqTtwetkQlqbNYirDiEEwXl2v0GYWMnCzxst7ZaRAnWuy28njp5kAH54lvkdG37MblZzaHA==} - engines: {node: '>=16.0.0'} - - '@smithy/hash-stream-node@3.1.10': - resolution: {integrity: sha512-olomK/jZQ93OMayW1zfTHwcbwBdhcZOHsyWyiZ9h9IXvc1mCD/VuvzbLb3Gy/qNJwI4MANPLctTp2BucV2oU/Q==} - engines: {node: '>=16.0.0'} - - '@smithy/invalid-dependency@3.0.11': - resolution: {integrity: sha512-NuQmVPEJjUX6c+UELyVz8kUx8Q539EDeNwbRyu4IIF8MeV7hUtq1FB3SHVyki2u++5XLMFqngeMKk7ccspnNyQ==} - - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} - - '@smithy/is-array-buffer@3.0.0': - resolution: {integrity: sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ==} - engines: {node: '>=16.0.0'} - - '@smithy/md5-js@3.0.11': - resolution: {integrity: sha512-3NM0L3i2Zm4bbgG6Ymi9NBcxXhryi3uE8fIfHJZIOfZVxOkGdjdgjR9A06SFIZCfnEIWKXZdm6Yq5/aPXFFhsQ==} - - '@smithy/middleware-content-length@3.0.13': - resolution: {integrity: sha512-zfMhzojhFpIX3P5ug7jxTjfUcIPcGjcQYzB9t+rv0g1TX7B0QdwONW+ATouaLoD7h7LOw/ZlXfkq4xJ/g2TrIw==} - engines: {node: '>=16.0.0'} - - '@smithy/middleware-endpoint@3.2.8': - resolution: {integrity: sha512-OEJZKVUEhMOqMs3ktrTWp7UvvluMJEvD5XgQwRePSbDg1VvBaL8pX8mwPltFn6wk1GySbcVwwyldL8S+iqnrEQ==} - engines: {node: '>=16.0.0'} - - '@smithy/middleware-retry@3.0.34': - resolution: {integrity: sha512-yVRr/AAtPZlUvwEkrq7S3x7Z8/xCd97m2hLDaqdz6ucP2RKHsBjEqaUA2ebNv2SsZoPEi+ZD0dZbOB1u37tGCA==} - engines: {node: '>=16.0.0'} - - '@smithy/middleware-serde@3.0.11': - resolution: {integrity: sha512-KzPAeySp/fOoQA82TpnwItvX8BBURecpx6ZMu75EZDkAcnPtO6vf7q4aH5QHs/F1s3/snQaSFbbUMcFFZ086Mw==} - engines: {node: '>=16.0.0'} - - '@smithy/middleware-stack@3.0.11': - resolution: {integrity: sha512-1HGo9a6/ikgOMrTrWL/WiN9N8GSVYpuRQO5kjstAq4CvV59bjqnh7TbdXGQ4vxLD3xlSjfBjq5t1SOELePsLnA==} - engines: {node: '>=16.0.0'} - - '@smithy/node-config-provider@3.1.12': - resolution: {integrity: sha512-O9LVEu5J/u/FuNlZs+L7Ikn3lz7VB9hb0GtPT9MQeiBmtK8RSY3ULmsZgXhe6VAlgTw0YO+paQx4p8xdbs43vQ==} - engines: {node: '>=16.0.0'} - - '@smithy/node-http-handler@3.3.3': - resolution: {integrity: sha512-BrpZOaZ4RCbcJ2igiSNG16S+kgAc65l/2hmxWdmhyoGWHTLlzQzr06PXavJp9OBlPEG/sHlqdxjWmjzV66+BSQ==} - engines: {node: '>=16.0.0'} - - '@smithy/property-provider@3.1.11': - resolution: {integrity: sha512-I/+TMc4XTQ3QAjXfOcUWbSS073oOEAxgx4aZy8jHaf8JQnRkq2SZWw8+PfDtBvLUjcGMdxl+YwtzWe6i5uhL/A==} - engines: {node: '>=16.0.0'} - - '@smithy/protocol-http@4.1.8': - resolution: {integrity: sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw==} - engines: {node: '>=16.0.0'} - - '@smithy/querystring-builder@3.0.11': - resolution: {integrity: sha512-u+5HV/9uJaeLj5XTb6+IEF/dokWWkEqJ0XiaRRogyREmKGUgZnNecLucADLdauWFKUNbQfulHFEZEdjwEBjXRg==} - engines: {node: '>=16.0.0'} - - '@smithy/querystring-parser@3.0.11': - resolution: {integrity: sha512-Je3kFvCsFMnso1ilPwA7GtlbPaTixa3WwC+K21kmMZHsBEOZYQaqxcMqeFFoU7/slFjKDIpiiPydvdJm8Q/MCw==} - engines: {node: '>=16.0.0'} - - '@smithy/service-error-classification@3.0.11': - resolution: {integrity: sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog==} - engines: {node: '>=16.0.0'} - - '@smithy/shared-ini-file-loader@3.1.12': - resolution: {integrity: sha512-1xKSGI+U9KKdbG2qDvIR9dGrw3CNx+baqJfyr0igKEpjbHL5stsqAesYBzHChYHlelWtb87VnLWlhvfCz13H8Q==} - engines: {node: '>=16.0.0'} - - '@smithy/signature-v4@4.2.4': - resolution: {integrity: sha512-5JWeMQYg81TgU4cG+OexAWdvDTs5JDdbEZx+Qr1iPbvo91QFGzjy0IkXAKaXUHqmKUJgSHK0ZxnCkgZpzkeNTA==} - engines: {node: '>=16.0.0'} - - '@smithy/smithy-client@3.7.0': - resolution: {integrity: sha512-9wYrjAZFlqWhgVo3C4y/9kpc68jgiSsKUnsFPzr/MSiRL93+QRDafGTfhhKAb2wsr69Ru87WTiqSfQusSmWipA==} - engines: {node: '>=16.0.0'} - - '@smithy/types@3.7.2': - resolution: {integrity: sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg==} - engines: {node: '>=16.0.0'} - - '@smithy/url-parser@3.0.11': - resolution: {integrity: sha512-TmlqXkSk8ZPhfc+SQutjmFr5FjC0av3GZP4B/10caK1SbRwe/v+Wzu/R6xEKxoNqL+8nY18s1byiy6HqPG37Aw==} - - '@smithy/util-base64@3.0.0': - resolution: {integrity: sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ==} - engines: {node: '>=16.0.0'} - - '@smithy/util-body-length-browser@3.0.0': - resolution: {integrity: sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ==} - - '@smithy/util-body-length-node@3.0.0': - resolution: {integrity: sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA==} - engines: {node: '>=16.0.0'} - - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} - - '@smithy/util-buffer-from@3.0.0': - resolution: {integrity: sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA==} - engines: {node: '>=16.0.0'} - - '@smithy/util-config-provider@3.0.0': - resolution: {integrity: sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ==} - engines: {node: '>=16.0.0'} - - '@smithy/util-defaults-mode-browser@3.0.34': - resolution: {integrity: sha512-FumjjF631lR521cX+svMLBj3SwSDh9VdtyynTYDAiBDEf8YPP5xORNXKQ9j0105o5+ARAGnOOP/RqSl40uXddA==} - engines: {node: '>= 10.0.0'} - - '@smithy/util-defaults-mode-node@3.0.34': - resolution: {integrity: sha512-vN6aHfzW9dVVzkI0wcZoUXvfjkl4CSbM9nE//08lmUMyf00S75uuCpTrqF9uD4bD9eldIXlt53colrlwKAT8Gw==} - engines: {node: '>= 10.0.0'} - - '@smithy/util-endpoints@2.1.7': - resolution: {integrity: sha512-tSfcqKcN/Oo2STEYCABVuKgJ76nyyr6skGl9t15hs+YaiU06sgMkN7QYjo0BbVw+KT26zok3IzbdSOksQ4YzVw==} - engines: {node: '>=16.0.0'} - - '@smithy/util-hex-encoding@3.0.0': - resolution: {integrity: sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ==} - engines: {node: '>=16.0.0'} - - '@smithy/util-middleware@3.0.11': - resolution: {integrity: sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow==} - engines: {node: '>=16.0.0'} - - '@smithy/util-retry@3.0.11': - resolution: {integrity: sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ==} - engines: {node: '>=16.0.0'} - - '@smithy/util-stream@3.3.4': - resolution: {integrity: sha512-SGhGBG/KupieJvJSZp/rfHHka8BFgj56eek9px4pp7lZbOF+fRiVr4U7A3y3zJD8uGhxq32C5D96HxsTC9BckQ==} - engines: {node: '>=16.0.0'} - - '@smithy/util-uri-escape@3.0.0': - resolution: {integrity: sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg==} - engines: {node: '>=16.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@3.0.0': - resolution: {integrity: sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA==} - engines: {node: '>=16.0.0'} - - '@smithy/util-waiter@3.2.0': - resolution: {integrity: sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg==} - engines: {node: '>=16.0.0'} - '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -4960,9 +4575,6 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - '@types/uuid@9.0.8': - resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} - '@types/webpack-sources@3.2.3': resolution: {integrity: sha512-4nZOdMwSPHZ4pTEZzSp0AsTM4K7Qmu40UKW4tJDiOVs20UzYF9l+qUe4s0ftfN0pin06n+5cWWDJXH+sbhAiDw==} @@ -5355,10 +4967,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - address@1.2.2: - resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} - engines: {node: '>= 10.0.0'} - agent-base@7.1.3: resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==} engines: {node: '>= 14'} @@ -5588,9 +5196,6 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - bluebird@3.7.2: - resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} - body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -5612,9 +5217,6 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bowser@2.14.1: - resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} - brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -5649,9 +5251,6 @@ packages: buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} - bufferpack@0.0.6: - resolution: {integrity: sha512-MTWvLHElqczrIVhge9qHtqgNigJFyh0+tCDId5yCbFAfuekHWIG+uAgvoHVflwrDPuY/e47JE1ki5qcM7w4uLg==} - builtin-modules@5.0.0: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} @@ -6122,15 +5721,6 @@ packages: supports-color: optional: true - debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - debug@4.3.7: resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} engines: {node: '>=6.0'} @@ -6252,11 +5842,6 @@ packages: detect-node@2.1.0: resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} - detect-port@1.6.1: - resolution: {integrity: sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==} - engines: {node: '>= 4.0.0'} - hasBin: true - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -6321,9 +5906,6 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} - duplexer2@0.1.4: - resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} - duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -6759,10 +6341,6 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-parser@4.4.1: - resolution: {integrity: sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw==} - hasBin: true - fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -7323,9 +6901,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - ip@2.0.1: - resolution: {integrity: sha512-lJUL9imLTNi1ZfXT+DU6rBBdbiKGBuay9B6xGSPVjUeQwaH1RIGqef8RZkUtHioLmSNpPR5M4HVKJGm1j8FWVQ==} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -8270,9 +7845,6 @@ packages: ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -8631,10 +8203,6 @@ packages: engines: {node: '>=18'} hasBin: true - plist@3.0.6: - resolution: {integrity: sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA==} - engines: {node: '>=6'} - plist@3.1.0: resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} engines: {node: '>=10.4.0'} @@ -8936,14 +8504,6 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - q@1.5.1: - resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} - engines: {node: '>=0.6.0', teleport: '>=0.2.0'} - deprecated: |- - You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other. - - (For a CapTP with native promises, see @endo/eventual-send and @endo/captp) - qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -9702,9 +9262,6 @@ packages: resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} engines: {node: '>=6.0.0'} - split@1.0.1: - resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} - sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -9825,9 +9382,6 @@ packages: resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} engines: {node: '>=0.10.0'} - strnum@1.1.2: - resolution: {integrity: sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==} - style-to-js@1.1.16: resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} @@ -10276,9 +9830,6 @@ packages: unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} - unzipper@0.12.3: - resolution: {integrity: sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==} - upath@2.0.1: resolution: {integrity: sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==} engines: {node: '>=4'} @@ -10318,9 +9869,6 @@ packages: '@types/react': optional: true - utf8@3.0.0: - resolution: {integrity: sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==} - util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -10332,10 +9880,6 @@ packages: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - v8-to-istanbul@9.3.0: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} @@ -10793,554 +10337,49 @@ snapshots: '@ast-grep/napi-win32-ia32-msvc': 0.37.0 '@ast-grep/napi-win32-x64-msvc': 0.37.0 - '@aws-crypto/crc32@5.2.0': + '@babel/code-frame@7.26.2': dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.667.0 - tslib: 2.8.1 + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 - '@aws-crypto/crc32c@5.2.0': + '@babel/code-frame@7.29.0': dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.667.0 - tslib: 2.8.1 + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 - '@aws-crypto/sha1-browser@5.2.0': - dependencies: - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.667.0 - '@aws-sdk/util-locate-window': 3.965.4 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 + '@babel/compat-data@7.28.6': {} - '@aws-crypto/sha256-browser@5.2.0': + '@babel/core@7.29.0': dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.667.0 - '@aws-sdk/util-locate-window': 3.965.4 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.28.6 + '@babel/parser': 7.29.0 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color - '@aws-crypto/sha256-js@5.2.0': + '@babel/generator@7.28.3': dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.667.0 - tslib: 2.8.1 + '@babel/parser': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 - - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-sdk/client-s3@3.669.0': - dependencies: - '@aws-crypto/sha1-browser': 5.2.0 - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.669.0(@aws-sdk/client-sts@3.669.0) - '@aws-sdk/client-sts': 3.669.0 - '@aws-sdk/core': 3.667.0 - '@aws-sdk/credential-provider-node': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0) - '@aws-sdk/middleware-bucket-endpoint': 3.667.0 - '@aws-sdk/middleware-expect-continue': 3.667.0 - '@aws-sdk/middleware-flexible-checksums': 3.669.0 - '@aws-sdk/middleware-host-header': 3.667.0 - '@aws-sdk/middleware-location-constraint': 3.667.0 - '@aws-sdk/middleware-logger': 3.667.0 - '@aws-sdk/middleware-recursion-detection': 3.667.0 - '@aws-sdk/middleware-sdk-s3': 3.669.0 - '@aws-sdk/middleware-ssec': 3.667.0 - '@aws-sdk/middleware-user-agent': 3.669.0 - '@aws-sdk/region-config-resolver': 3.667.0 - '@aws-sdk/signature-v4-multi-region': 3.669.0 - '@aws-sdk/types': 3.667.0 - '@aws-sdk/util-endpoints': 3.667.0 - '@aws-sdk/util-user-agent-browser': 3.667.0 - '@aws-sdk/util-user-agent-node': 3.669.0 - '@aws-sdk/xml-builder': 3.662.0 - '@smithy/config-resolver': 3.0.13 - '@smithy/core': 2.5.7 - '@smithy/eventstream-serde-browser': 3.0.14 - '@smithy/eventstream-serde-config-resolver': 3.0.11 - '@smithy/eventstream-serde-node': 3.0.13 - '@smithy/fetch-http-handler': 3.2.9 - '@smithy/hash-blob-browser': 3.1.10 - '@smithy/hash-node': 3.0.11 - '@smithy/hash-stream-node': 3.1.10 - '@smithy/invalid-dependency': 3.0.11 - '@smithy/md5-js': 3.0.11 - '@smithy/middleware-content-length': 3.0.13 - '@smithy/middleware-endpoint': 3.2.8 - '@smithy/middleware-retry': 3.0.34 - '@smithy/middleware-serde': 3.0.11 - '@smithy/middleware-stack': 3.0.11 - '@smithy/node-config-provider': 3.1.12 - '@smithy/node-http-handler': 3.3.3 - '@smithy/protocol-http': 4.1.8 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - '@smithy/url-parser': 3.0.11 - '@smithy/util-base64': 3.0.0 - '@smithy/util-body-length-browser': 3.0.0 - '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.34 - '@smithy/util-defaults-mode-node': 3.0.34 - '@smithy/util-endpoints': 2.1.7 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-retry': 3.0.11 - '@smithy/util-stream': 3.3.4 - '@smithy/util-utf8': 3.0.0 - '@smithy/util-waiter': 3.2.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0)': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sts': 3.669.0 - '@aws-sdk/core': 3.667.0 - '@aws-sdk/credential-provider-node': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0) - '@aws-sdk/middleware-host-header': 3.667.0 - '@aws-sdk/middleware-logger': 3.667.0 - '@aws-sdk/middleware-recursion-detection': 3.667.0 - '@aws-sdk/middleware-user-agent': 3.669.0 - '@aws-sdk/region-config-resolver': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@aws-sdk/util-endpoints': 3.667.0 - '@aws-sdk/util-user-agent-browser': 3.667.0 - '@aws-sdk/util-user-agent-node': 3.669.0 - '@smithy/config-resolver': 3.0.13 - '@smithy/core': 2.5.7 - '@smithy/fetch-http-handler': 3.2.9 - '@smithy/hash-node': 3.0.11 - '@smithy/invalid-dependency': 3.0.11 - '@smithy/middleware-content-length': 3.0.13 - '@smithy/middleware-endpoint': 3.2.8 - '@smithy/middleware-retry': 3.0.34 - '@smithy/middleware-serde': 3.0.11 - '@smithy/middleware-stack': 3.0.11 - '@smithy/node-config-provider': 3.1.12 - '@smithy/node-http-handler': 3.3.3 - '@smithy/protocol-http': 4.1.8 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - '@smithy/url-parser': 3.0.11 - '@smithy/util-base64': 3.0.0 - '@smithy/util-body-length-browser': 3.0.0 - '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.34 - '@smithy/util-defaults-mode-node': 3.0.34 - '@smithy/util-endpoints': 2.1.7 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-retry': 3.0.11 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/client-sso@3.669.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.667.0 - '@aws-sdk/middleware-host-header': 3.667.0 - '@aws-sdk/middleware-logger': 3.667.0 - '@aws-sdk/middleware-recursion-detection': 3.667.0 - '@aws-sdk/middleware-user-agent': 3.669.0 - '@aws-sdk/region-config-resolver': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@aws-sdk/util-endpoints': 3.667.0 - '@aws-sdk/util-user-agent-browser': 3.667.0 - '@aws-sdk/util-user-agent-node': 3.669.0 - '@smithy/config-resolver': 3.0.13 - '@smithy/core': 2.5.7 - '@smithy/fetch-http-handler': 3.2.9 - '@smithy/hash-node': 3.0.11 - '@smithy/invalid-dependency': 3.0.11 - '@smithy/middleware-content-length': 3.0.13 - '@smithy/middleware-endpoint': 3.2.8 - '@smithy/middleware-retry': 3.0.34 - '@smithy/middleware-serde': 3.0.11 - '@smithy/middleware-stack': 3.0.11 - '@smithy/node-config-provider': 3.1.12 - '@smithy/node-http-handler': 3.3.3 - '@smithy/protocol-http': 4.1.8 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - '@smithy/url-parser': 3.0.11 - '@smithy/util-base64': 3.0.0 - '@smithy/util-body-length-browser': 3.0.0 - '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.34 - '@smithy/util-defaults-mode-node': 3.0.34 - '@smithy/util-endpoints': 2.1.7 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-retry': 3.0.11 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/client-sts@3.669.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/client-sso-oidc': 3.669.0(@aws-sdk/client-sts@3.669.0) - '@aws-sdk/core': 3.667.0 - '@aws-sdk/credential-provider-node': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0) - '@aws-sdk/middleware-host-header': 3.667.0 - '@aws-sdk/middleware-logger': 3.667.0 - '@aws-sdk/middleware-recursion-detection': 3.667.0 - '@aws-sdk/middleware-user-agent': 3.669.0 - '@aws-sdk/region-config-resolver': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@aws-sdk/util-endpoints': 3.667.0 - '@aws-sdk/util-user-agent-browser': 3.667.0 - '@aws-sdk/util-user-agent-node': 3.669.0 - '@smithy/config-resolver': 3.0.13 - '@smithy/core': 2.5.7 - '@smithy/fetch-http-handler': 3.2.9 - '@smithy/hash-node': 3.0.11 - '@smithy/invalid-dependency': 3.0.11 - '@smithy/middleware-content-length': 3.0.13 - '@smithy/middleware-endpoint': 3.2.8 - '@smithy/middleware-retry': 3.0.34 - '@smithy/middleware-serde': 3.0.11 - '@smithy/middleware-stack': 3.0.11 - '@smithy/node-config-provider': 3.1.12 - '@smithy/node-http-handler': 3.3.3 - '@smithy/protocol-http': 4.1.8 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - '@smithy/url-parser': 3.0.11 - '@smithy/util-base64': 3.0.0 - '@smithy/util-body-length-browser': 3.0.0 - '@smithy/util-body-length-node': 3.0.0 - '@smithy/util-defaults-mode-browser': 3.0.34 - '@smithy/util-defaults-mode-node': 3.0.34 - '@smithy/util-endpoints': 2.1.7 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-retry': 3.0.11 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/core': 2.5.7 - '@smithy/node-config-provider': 3.1.12 - '@smithy/property-provider': 3.1.11 - '@smithy/protocol-http': 4.1.8 - '@smithy/signature-v4': 4.2.4 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - '@smithy/util-middleware': 3.0.11 - fast-xml-parser: 4.4.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-env@3.667.0': - dependencies: - '@aws-sdk/core': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@smithy/property-provider': 3.1.11 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.667.0': - dependencies: - '@aws-sdk/core': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@smithy/fetch-http-handler': 3.2.9 - '@smithy/node-http-handler': 3.3.3 - '@smithy/property-provider': 3.1.11 - '@smithy/protocol-http': 4.1.8 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - '@smithy/util-stream': 3.3.4 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0)': - dependencies: - '@aws-sdk/client-sts': 3.669.0 - '@aws-sdk/core': 3.667.0 - '@aws-sdk/credential-provider-env': 3.667.0 - '@aws-sdk/credential-provider-http': 3.667.0 - '@aws-sdk/credential-provider-process': 3.667.0 - '@aws-sdk/credential-provider-sso': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0)) - '@aws-sdk/credential-provider-web-identity': 3.667.0(@aws-sdk/client-sts@3.669.0) - '@aws-sdk/types': 3.667.0 - '@smithy/credential-provider-imds': 3.2.8 - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt - - '@aws-sdk/credential-provider-node@3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0)': - dependencies: - '@aws-sdk/credential-provider-env': 3.667.0 - '@aws-sdk/credential-provider-http': 3.667.0 - '@aws-sdk/credential-provider-ini': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))(@aws-sdk/client-sts@3.669.0) - '@aws-sdk/credential-provider-process': 3.667.0 - '@aws-sdk/credential-provider-sso': 3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0)) - '@aws-sdk/credential-provider-web-identity': 3.667.0(@aws-sdk/client-sts@3.669.0) - '@aws-sdk/types': 3.667.0 - '@smithy/credential-provider-imds': 3.2.8 - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - '@aws-sdk/client-sts' - - aws-crt - - '@aws-sdk/credential-provider-process@3.667.0': - dependencies: - '@aws-sdk/core': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.669.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))': - dependencies: - '@aws-sdk/client-sso': 3.669.0 - '@aws-sdk/core': 3.667.0 - '@aws-sdk/token-providers': 3.667.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0)) - '@aws-sdk/types': 3.667.0 - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - - aws-crt - - '@aws-sdk/credential-provider-web-identity@3.667.0(@aws-sdk/client-sts@3.669.0)': - dependencies: - '@aws-sdk/client-sts': 3.669.0 - '@aws-sdk/core': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@smithy/property-provider': 3.1.11 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/middleware-bucket-endpoint@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@aws-sdk/util-arn-parser': 3.568.0 - '@smithy/node-config-provider': 3.1.12 - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - '@smithy/util-config-provider': 3.0.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-expect-continue@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/middleware-flexible-checksums@3.669.0': - dependencies: - '@aws-crypto/crc32': 5.2.0 - '@aws-crypto/crc32c': 5.2.0 - '@aws-sdk/core': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@smithy/is-array-buffer': 3.0.0 - '@smithy/node-config-provider': 3.1.12 - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-host-header@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/middleware-location-constraint@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/middleware-logger@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/middleware-recursion-detection@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/middleware-sdk-s3@3.669.0': - dependencies: - '@aws-sdk/core': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@aws-sdk/util-arn-parser': 3.568.0 - '@smithy/core': 2.5.7 - '@smithy/node-config-provider': 3.1.12 - '@smithy/protocol-http': 4.1.8 - '@smithy/signature-v4': 4.2.4 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - '@smithy/util-config-provider': 3.0.0 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-stream': 3.3.4 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - - '@aws-sdk/middleware-ssec@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/middleware-user-agent@3.669.0': - dependencies: - '@aws-sdk/core': 3.667.0 - '@aws-sdk/types': 3.667.0 - '@aws-sdk/util-endpoints': 3.667.0 - '@smithy/core': 2.5.7 - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/region-config-resolver@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/node-config-provider': 3.1.12 - '@smithy/types': 3.7.2 - '@smithy/util-config-provider': 3.0.0 - '@smithy/util-middleware': 3.0.11 - tslib: 2.8.1 - - '@aws-sdk/signature-v4-multi-region@3.669.0': - dependencies: - '@aws-sdk/middleware-sdk-s3': 3.669.0 - '@aws-sdk/types': 3.667.0 - '@smithy/protocol-http': 4.1.8 - '@smithy/signature-v4': 4.2.4 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.667.0(@aws-sdk/client-sso-oidc@3.669.0(@aws-sdk/client-sts@3.669.0))': - dependencies: - '@aws-sdk/client-sso-oidc': 3.669.0(@aws-sdk/client-sts@3.669.0) - '@aws-sdk/types': 3.667.0 - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/types@3.667.0': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/util-arn-parser@3.568.0': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-endpoints@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/types': 3.7.2 - '@smithy/util-endpoints': 2.1.7 - tslib: 2.8.1 - - '@aws-sdk/util-locate-window@3.965.4': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-browser@3.667.0': - dependencies: - '@aws-sdk/types': 3.667.0 - '@smithy/types': 3.7.2 - bowser: 2.14.1 - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-node@3.669.0': - dependencies: - '@aws-sdk/middleware-user-agent': 3.669.0 - '@aws-sdk/types': 3.667.0 - '@smithy/node-config-provider': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.662.0': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@babel/code-frame@7.26.2': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/code-frame@7.29.0': - dependencies: - '@babel/helper-validator-identifier': 7.28.5 - js-tokens: 4.0.0 - picocolors: 1.1.1 - - '@babel/compat-data@7.28.6': {} - - '@babel/core@7.29.0': - dependencies: - '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) - '@babel/helpers': 7.28.6 - '@babel/parser': 7.29.0 - '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/remapping': 2.3.5 - convert-source-map: 2.0.0 - debug: 4.4.3 - gensync: 1.0.0-beta.2 - json5: 2.2.3 - semver: 6.3.1 - transitivePeerDependencies: - - supports-color - - '@babel/generator@7.28.3': - dependencies: - '@babel/parser': 7.29.0 - '@babel/types': 7.29.0 - '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 - jsesc: 3.1.0 - - '@babel/generator@7.29.1': + '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 @@ -11864,7 +10903,7 @@ snapshots: '@codspeed/core@5.2.0': dependencies: - axios: 1.11.0(debug@4.3.4) + axios: 1.11.0 find-up: 6.3.0 form-data: 4.0.4 node-gyp-build: 4.8.4 @@ -11902,22 +10941,6 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} - '@devicefarmer/adbkit-logcat@2.1.3': {} - - '@devicefarmer/adbkit-monkey@1.2.1': {} - - '@devicefarmer/adbkit@3.2.4': - dependencies: - '@devicefarmer/adbkit-logcat': 2.1.3 - '@devicefarmer/adbkit-monkey': 1.2.1 - bluebird: 3.7.2 - commander: 9.5.0 - debug: 4.3.7 - node-forge: 1.3.1 - split: 1.0.1 - transitivePeerDependencies: - - supports-color - '@discoveryjs/json-ext@0.5.7': {} '@dprint/darwin-arm64@0.51.1': @@ -12456,33 +11479,6 @@ snapshots: '@leichtgewicht/ip-codec@2.0.5': {} - '@lynx-js/debug-router-connector@0.0.6': - dependencies: - '@aws-sdk/client-s3': 3.669.0 - '@devicefarmer/adbkit': 3.2.4 - '@types/uuid': 9.0.8 - axios: 1.11.0(debug@4.3.4) - bufferpack: 0.0.6 - chalk: 4.1.2 - debug: 4.3.4 - detect-port: 1.6.1 - express: 5.2.1 - fs-extra: 11.3.3 - ip: 2.0.1 - plist: 3.0.6 - q: 1.5.1 - signal-exit: 4.1.0 - unzipper: 0.12.3 - utf8: 3.0.0 - uuid: 9.0.1 - ws: 8.19.0 - yargs: 15.4.1 - transitivePeerDependencies: - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - '@lynx-js/internal-preact@10.28.4-ee7bb26': {} '@lynx-js/lynx-core@0.1.3': {} @@ -13227,7 +12223,7 @@ snapshots: '@rsdoctor/sdk': 1.2.3(@rspack/core@1.7.7(@swc/helpers@0.5.18))(webpack@5.105.2) '@rsdoctor/types': 1.2.3(@rspack/core@1.7.7(@swc/helpers@0.5.18))(webpack@5.105.2) '@rsdoctor/utils': 1.2.3(@rspack/core@1.7.7(@swc/helpers@0.5.18))(webpack@5.105.2) - axios: 1.11.0(debug@4.3.4) + axios: 1.11.0 browserslist-load-config: 1.0.0 enhanced-resolve: 5.12.0 filesize: 10.1.6 @@ -13669,345 +12665,6 @@ snapshots: '@sinclair/typebox@0.34.37': {} - '@smithy/abort-controller@3.1.9': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/chunked-blob-reader-native@3.0.1': - dependencies: - '@smithy/util-base64': 3.0.0 - tslib: 2.8.1 - - '@smithy/chunked-blob-reader@4.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/config-resolver@3.0.13': - dependencies: - '@smithy/node-config-provider': 3.1.12 - '@smithy/types': 3.7.2 - '@smithy/util-config-provider': 3.0.0 - '@smithy/util-middleware': 3.0.11 - tslib: 2.8.1 - - '@smithy/core@2.5.7': - dependencies: - '@smithy/middleware-serde': 3.0.11 - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - '@smithy/util-body-length-browser': 3.0.0 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-stream': 3.3.4 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@3.2.8': - dependencies: - '@smithy/node-config-provider': 3.1.12 - '@smithy/property-provider': 3.1.11 - '@smithy/types': 3.7.2 - '@smithy/url-parser': 3.0.11 - tslib: 2.8.1 - - '@smithy/eventstream-codec@3.1.10': - dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 3.7.2 - '@smithy/util-hex-encoding': 3.0.0 - tslib: 2.8.1 - - '@smithy/eventstream-serde-browser@3.0.14': - dependencies: - '@smithy/eventstream-serde-universal': 3.0.13 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/eventstream-serde-config-resolver@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/eventstream-serde-node@3.0.13': - dependencies: - '@smithy/eventstream-serde-universal': 3.0.13 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/eventstream-serde-universal@3.0.13': - dependencies: - '@smithy/eventstream-codec': 3.1.10 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@3.2.9': - dependencies: - '@smithy/protocol-http': 4.1.8 - '@smithy/querystring-builder': 3.0.11 - '@smithy/types': 3.7.2 - '@smithy/util-base64': 3.0.0 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@4.1.3': - dependencies: - '@smithy/protocol-http': 4.1.8 - '@smithy/querystring-builder': 3.0.11 - '@smithy/types': 3.7.2 - '@smithy/util-base64': 3.0.0 - tslib: 2.8.1 - - '@smithy/hash-blob-browser@3.1.10': - dependencies: - '@smithy/chunked-blob-reader': 4.0.0 - '@smithy/chunked-blob-reader-native': 3.0.1 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/hash-node@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - '@smithy/util-buffer-from': 3.0.0 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - - '@smithy/hash-stream-node@3.1.10': - dependencies: - '@smithy/types': 3.7.2 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - - '@smithy/invalid-dependency@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/is-array-buffer@3.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/md5-js@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - - '@smithy/middleware-content-length@3.0.13': - dependencies: - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/middleware-endpoint@3.2.8': - dependencies: - '@smithy/core': 2.5.7 - '@smithy/middleware-serde': 3.0.11 - '@smithy/node-config-provider': 3.1.12 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - '@smithy/url-parser': 3.0.11 - '@smithy/util-middleware': 3.0.11 - tslib: 2.8.1 - - '@smithy/middleware-retry@3.0.34': - dependencies: - '@smithy/node-config-provider': 3.1.12 - '@smithy/protocol-http': 4.1.8 - '@smithy/service-error-classification': 3.0.11 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-retry': 3.0.11 - tslib: 2.8.1 - uuid: 9.0.1 - - '@smithy/middleware-serde@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/middleware-stack@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/node-config-provider@3.1.12': - dependencies: - '@smithy/property-provider': 3.1.11 - '@smithy/shared-ini-file-loader': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/node-http-handler@3.3.3': - dependencies: - '@smithy/abort-controller': 3.1.9 - '@smithy/protocol-http': 4.1.8 - '@smithy/querystring-builder': 3.0.11 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/property-provider@3.1.11': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/protocol-http@4.1.8': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/querystring-builder@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - '@smithy/util-uri-escape': 3.0.0 - tslib: 2.8.1 - - '@smithy/querystring-parser@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/service-error-classification@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - - '@smithy/shared-ini-file-loader@3.1.12': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/signature-v4@4.2.4': - dependencies: - '@smithy/is-array-buffer': 3.0.0 - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - '@smithy/util-hex-encoding': 3.0.0 - '@smithy/util-middleware': 3.0.11 - '@smithy/util-uri-escape': 3.0.0 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - - '@smithy/smithy-client@3.7.0': - dependencies: - '@smithy/core': 2.5.7 - '@smithy/middleware-endpoint': 3.2.8 - '@smithy/middleware-stack': 3.0.11 - '@smithy/protocol-http': 4.1.8 - '@smithy/types': 3.7.2 - '@smithy/util-stream': 3.3.4 - tslib: 2.8.1 - - '@smithy/types@3.7.2': - dependencies: - tslib: 2.8.1 - - '@smithy/url-parser@3.0.11': - dependencies: - '@smithy/querystring-parser': 3.0.11 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/util-base64@3.0.0': - dependencies: - '@smithy/util-buffer-from': 3.0.0 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - - '@smithy/util-body-length-browser@3.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-body-length-node@3.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-buffer-from@2.2.0': - dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-buffer-from@3.0.0': - dependencies: - '@smithy/is-array-buffer': 3.0.0 - tslib: 2.8.1 - - '@smithy/util-config-provider@3.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-defaults-mode-browser@3.0.34': - dependencies: - '@smithy/property-provider': 3.1.11 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - bowser: 2.14.1 - tslib: 2.8.1 - - '@smithy/util-defaults-mode-node@3.0.34': - dependencies: - '@smithy/config-resolver': 3.0.13 - '@smithy/credential-provider-imds': 3.2.8 - '@smithy/node-config-provider': 3.1.12 - '@smithy/property-provider': 3.1.11 - '@smithy/smithy-client': 3.7.0 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/util-endpoints@2.1.7': - dependencies: - '@smithy/node-config-provider': 3.1.12 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/util-hex-encoding@3.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-middleware@3.0.11': - dependencies: - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/util-retry@3.0.11': - dependencies: - '@smithy/service-error-classification': 3.0.11 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - - '@smithy/util-stream@3.3.4': - dependencies: - '@smithy/fetch-http-handler': 4.1.3 - '@smithy/node-http-handler': 3.3.3 - '@smithy/types': 3.7.2 - '@smithy/util-base64': 3.0.0 - '@smithy/util-buffer-from': 3.0.0 - '@smithy/util-hex-encoding': 3.0.0 - '@smithy/util-utf8': 3.0.0 - tslib: 2.8.1 - - '@smithy/util-uri-escape@3.0.0': - dependencies: - tslib: 2.8.1 - - '@smithy/util-utf8@2.3.0': - dependencies: - '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-utf8@3.0.0': - dependencies: - '@smithy/util-buffer-from': 3.0.0 - tslib: 2.8.1 - - '@smithy/util-waiter@3.2.0': - dependencies: - '@smithy/abort-controller': 3.1.9 - '@smithy/types': 3.7.2 - tslib: 2.8.1 - '@socket.io/component-emitter@3.1.2': {} '@standard-schema/spec@1.0.0': {} @@ -14412,8 +13069,6 @@ snapshots: '@types/unist@3.0.3': {} - '@types/uuid@9.0.8': {} - '@types/webpack-sources@3.2.3': dependencies: '@types/node': 24.10.13 @@ -14864,8 +13519,6 @@ snapshots: acorn@8.15.0: {} - address@1.2.2: {} - agent-base@7.1.3: {} aggregate-error@3.1.0: @@ -15035,9 +13688,9 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 - axios@1.11.0(debug@4.3.4): + axios@1.11.0: dependencies: - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6 form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -15112,8 +13765,6 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - bluebird@3.7.2: {} - body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -15171,8 +13822,6 @@ snapshots: boolbase@1.0.0: {} - bowser@2.14.1: {} - brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -15215,8 +13864,6 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 - bufferpack@0.0.6: {} - builtin-modules@5.0.0: {} bundle-name@4.1.0: @@ -15398,7 +14045,8 @@ snapshots: commander@7.2.0: {} - commander@9.5.0: {} + commander@9.5.0: + optional: true comment-json@4.2.5: dependencies: @@ -15679,10 +14327,6 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.3.4: - dependencies: - ms: 2.1.2 - debug@4.3.7: dependencies: ms: 2.1.3 @@ -15765,13 +14409,6 @@ snapshots: detect-node@2.1.0: {} - detect-port@1.6.1: - dependencies: - address: 1.2.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -15842,10 +14479,6 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 - duplexer2@0.1.4: - dependencies: - readable-stream: 2.3.8 - duplexer@0.1.2: {} eastasianwidth@0.2.0: {} @@ -16494,10 +15127,6 @@ snapshots: fast-uri@3.1.0: {} - fast-xml-parser@4.4.1: - dependencies: - strnum: 1.1.2 - fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -16613,9 +15242,7 @@ snapshots: flexsearch@0.8.212: {} - follow-redirects@1.15.6(debug@4.3.4): - optionalDependencies: - debug: 4.3.4 + follow-redirects@1.15.6: {} for-each@0.3.5: dependencies: @@ -17069,7 +15696,7 @@ snapshots: http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -17165,8 +15792,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - ip@2.0.1: {} - ipaddr.js@1.9.1: {} ipaddr.js@2.3.0: {} @@ -18406,8 +17031,6 @@ snapshots: ms@2.0.0: {} - ms@2.1.2: {} - ms@2.1.3: {} multicast-dns@7.2.5: @@ -18760,11 +17383,6 @@ snapshots: optionalDependencies: fsevents: 2.3.2 - plist@3.0.6: - dependencies: - base64-js: 1.5.1 - xmlbuilder: 15.1.1 - plist@3.1.0: dependencies: '@xmldom/xmldom': 0.8.11 @@ -19039,8 +17657,6 @@ snapshots: punycode@2.3.1: {} - q@1.5.1: {} - qs@6.13.0: dependencies: side-channel: 1.1.0 @@ -19953,10 +18569,6 @@ snapshots: transitivePeerDependencies: - supports-color - split@1.0.1: - dependencies: - through: 2.3.8 - sprintf-js@1.0.3: {} stable-hash-x@0.1.1: {} @@ -20077,8 +18689,6 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 - strnum@1.1.2: {} - style-to-js@1.1.16: dependencies: style-to-object: 1.0.8 @@ -20599,14 +19209,6 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unzipper@0.12.3: - dependencies: - bluebird: 3.7.2 - duplexer2: 0.1.4 - fs-extra: 11.3.3 - graceful-fs: 4.2.11 - node-int64: 0.4.0 - upath@2.0.1: {} update-browserslist-db@1.2.3(browserslist@4.28.1): @@ -20641,16 +19243,12 @@ snapshots: optionalDependencies: '@types/react': 19.2.14 - utf8@3.0.0: {} - util-deprecate@1.0.2: {} utils-merge@1.0.1: {} uuid@8.3.2: {} - uuid@9.0.1: {} - v8-to-istanbul@9.3.0: dependencies: '@jridgewell/trace-mapping': 0.3.29 From 161619f836839db275a82043d10fb2985a93b70a Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:25:33 +0800 Subject: [PATCH 06/31] feat: Migrate Android testing documentation to Waydroid and enhance app connection reliability with refactored `Lynx.connect` and `am start` fallback. --- .../src/transport/android.ts | 35 ++++++- .../testing-library/kitten-lynx/AGENTS.md | 21 ++-- .../testing-library/kitten-lynx/src/Lynx.ts | 98 +++++++++---------- .../kitten-lynx/tests/lynx.spec.ts | 2 +- 4 files changed, 93 insertions(+), 63 deletions(-) diff --git a/packages/mcp-servers/devtool-connector/src/transport/android.ts b/packages/mcp-servers/devtool-connector/src/transport/android.ts index 5a9fe76c3f..f21f660b52 100644 --- a/packages/mcp-servers/devtool-connector/src/transport/android.ts +++ b/packages/mcp-servers/devtool-connector/src/transport/android.ts @@ -169,13 +169,38 @@ export class AndroidTransport implements Transport { '1', ]); debug(`openApp LAUNCHER output ${output}`); - if (output.includes('No activities found')) { + + if (!output.includes('Events injected:')) { + debug('openApp monkey failed, trying fallback to am start'); + const dumpsysOutput = await adb.subprocess.noneProtocol.spawnWaitText([ + 'dumpsys', + 'package', + packageName, + ]); + const mainActionIndex = dumpsysOutput.indexOf( + 'android.intent.action.MAIN:', + ); + if (mainActionIndex !== -1) { + const slice = dumpsysOutput.slice( + mainActionIndex, + mainActionIndex + 200, + ); + const match = slice.match(/([a-zA-Z0-9._]+\/[a-zA-Z0-9._]+)/); + if (match?.[1]) { + debug(`openApp am start fallback: ${match[1]}`); + const amOutput = await adb.subprocess.noneProtocol.spawnWaitText([ + 'am', + 'start', + '-n', + match[1], + ]); + debug(`openApp am start output: ${amOutput}`); + return; + } + } throw new Error( - `No launchable activity found for package ${packageName}.`, + `Failed to open app ${packageName}: monkey aborted and fallback failed.`, ); } - if (output.includes('monkey aborted')) { - throw new Error(`Failed to open app ${packageName}.`); - } } } diff --git a/packages/testing-library/kitten-lynx/AGENTS.md b/packages/testing-library/kitten-lynx/AGENTS.md index c0e49a7e17..ac6ae7712b 100644 --- a/packages/testing-library/kitten-lynx/AGENTS.md +++ b/packages/testing-library/kitten-lynx/AGENTS.md @@ -33,18 +33,27 @@ Through the Chrome DevTools Protocol (CDP), `kitten-lynx` enables: For the library to interact successfully: -- Host machine must have Docker installed with the `ubuntu-24.04-emu` image containing the Android SDK and configured ADB. -- Inside the emulator, the Lynx Explorer APK must be installed. -- ADB port `5555` should be exposed or forwarded to control the emulator programmatically. +- The host machine (or CI environment) must have **Waydroid** installed and initialized. Waydroid provides a much lighter and faster Android environment compared to Docker-based emulators. +- The Waydroid container must be running (e.g. `waydroid session start`). +- ADB must be authorized. To avoid manual authorization prompts (especially in CI), configure Waydroid properties before starting: + ```bash + sudo waydroid prop set persist.waydroid.adb_enabled 1 + sudo waydroid prop set persist.adb.tcp.port 5555 + sudo waydroid prop set ro.adb.secure 0 + ``` +- The Lynx Explorer APK must be installed inside Waydroid (`waydroid app install /path/to/LynxExplorer.apk`). - Typical commands use `pnpm run test` starting `vitest` logic inside the Node wrapper. ### Known Gotchas - **`Page.navigate` does not work like Chrome**: In Lynx, `Page.navigate` tells the runtime to load a new bundle, which creates a **new session** rather than updating the current one in place. You must poll `sendListSessionMessage()` to find the new session by URL and re-attach to it. - **`App.openPage` is not implemented** in Lynx Explorer 3.6.0. Do not rely on `sendAppMessage('App.openPage')` for navigation. -- **Docker emulator has no internet**: The Android emulator in Docker cannot reach external hosts. For tests using remote bundle URLs, use `adb reverse` port forwarding to serve bundles locally, or ensure the Docker network allows outbound traffic. -- **Multiple ADB targets**: When multiple ADB devices are connected (e.g. physical phone + emulator), use `ConnectOptions.deviceId` to target a specific one. Otherwise the first available client is used, which may be on the wrong device. -- **CDP timeouts**: The connector uses a 5-second `AbortSignal.timeout`. Keep test operations tolerant of emulator boot/warm-up times. +- **Network access & Local Serving**: Waydroid shares the host's network. However, if the container lacks direct internet access (common in some CI setups): + 1. Serve your Lynx bundles from the host (e.g., `python3 -m http.server 8080`). + 2. Use `adb reverse tcp:8080 tcp:8080` to map the host port into Waydroid. + 3. Navigate to `http://localhost:8080/your.bundle`. +- **Multiple ADB targets**: When multiple ADB devices are connected (e.g. physical phone + Waydroid), use `ConnectOptions.deviceId` to target a specific one (e.g. `192.168.240.112:5555`). Otherwise the first available client is used, which may be on the wrong device. +- **CDP timeouts**: The connector uses a 5-second `AbortSignal.timeout`. Keep test operations tolerant of Waydroid boot/warm-up times. ## Adding Features diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index b653e61707..e56ceb2008 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -57,74 +57,70 @@ export class Lynx { const targetDevice = options?.deviceId; const appPackage = options?.appPackage ?? DEFAULT_APP_PACKAGE; - try { - const output = execSync('adb devices').toString(); - const lines = output.split('\n'); - const adbDevices = []; - for (let i = 1; i < lines.length; i++) { - const line = lines[i]?.trim(); - if (line && line.endsWith('device')) { - const parts = line.split('\t'); - if (parts.length >= 1 && parts[0]) { - adbDevices.push(parts[0]); - } - } - } + const lynx = new Lynx(); + lynx._connector = new Connector([new AndroidTransport()]); - // If a specific device is requested, only restart that one - const devicesToRestart = targetDevice - ? adbDevices.filter(d => d === targetDevice) - : adbDevices; + let deviceIdToUse = targetDevice; + if (!deviceIdToUse) { + const devices = await lynx._connector.listDevices(); + if (devices.length === 0) { + throw new Error('Failed to connect to Lynx: no devices found.'); + } - for (const deviceId of devicesToRestart) { + for (const device of devices) { try { - console.log( - `[Lynx] Restarting ${appPackage} on device ${deviceId}...`, - ); - execSync(`adb -s ${deviceId} shell am force-stop ${appPackage}`); - execSync( - `adb -s ${deviceId} shell monkey -p ${appPackage} -c android.intent.category.LAUNCHER 1`, - ); + const apps = await lynx._connector.listAvailableApps(device.id); + if (apps.some(app => app.packageName === appPackage)) { + deviceIdToUse = device.id; + break; + } } catch (e) { - console.error( - `[Lynx] Failed to restart app on device ${deviceId}:`, - e, - ); + // Ignore errors checking apps on a specific device } } + + if (!deviceIdToUse) { + deviceIdToUse = devices[0]!.id; + } + } + + console.log( + `[Lynx] Restarting ${appPackage} on device ${deviceIdToUse}...`, + ); + try { + execSync(`adb -s ${deviceIdToUse} shell am force-stop ${appPackage}`); } catch (e) { - console.warn( - '[Lynx] Failed to list ADB devices or adb is not available.', + console.error( + `[Lynx] Failed to force-stop app on device ${deviceIdToUse}:`, + e, ); } - const lynx = new Lynx(); - - lynx._connector = new Connector([new AndroidTransport()]); - - let clients = await lynx._connector.listClients(); - let attempts = 0; - while (clients.length === 0 && attempts < 20) { - await new Promise(r => setTimeout(r, 500)); - clients = await lynx._connector.listClients(); - attempts++; + try { + await lynx._connector.openApp(deviceIdToUse, appPackage); + } catch (e) { + console.error(`[Lynx] Failed to open app on device ${deviceIdToUse}:`, e); + throw e; } - // Filter clients by deviceId if specified (client.id format: "deviceId:port") - if (targetDevice) { - clients = clients.filter(c => c.id.startsWith(targetDevice + ':')); - } + const clients = await lynx._connector.listClients(); + + // Filter clients by deviceId and target package + const encodedDeviceId = encodeURIComponent(deviceIdToUse); + const matchedClients = clients.filter( + c => + c.id.startsWith(encodedDeviceId + ':') + && c.info.AppProcessName === appPackage, + ); - if (clients.length === 0) { + if (matchedClients.length === 0) { throw new Error( - targetDevice - ? `Failed to connect to Lynx: no client found on device "${targetDevice}" after 10 seconds.` - : 'Failed to connect to Lynx: no client found after 10 seconds.', + `Failed to connect to Lynx: no client found for ${appPackage} on device "${deviceIdToUse}".`, ); } - lynx._currentClient = clients[0]; - lynx._currentClientId = clients[0]!.id; + lynx._currentClient = matchedClients[0]; + lynx._currentClientId = matchedClients[0]!.id; return lynx; } diff --git a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts index 8d13a48814..4272c71788 100644 --- a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts +++ b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts @@ -16,7 +16,7 @@ describe('kitten-lynx testing framework', () => { const page = await lynx.newPage(); await page.goto( - 'https://lynxjs.org/next/lynx-examples/hello-world/dist/main.lynx.bundle', + 'http://10.91.84.156:3000/main.lynx.bundle', ); const content = await page.content(); From 9d6ef893499d56d1fd77f7a194782a6f2d31e719 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:47:55 +0800 Subject: [PATCH 07/31] ci: Replace Docker-based Android emulator with Waydroid for CI tests. --- .github/workflows/test.yml | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 74a03a3e60..31eb9c4a0f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -255,21 +255,30 @@ jobs: with: runs-on: lynx-ubuntu-24.04-xlarge run: | - # 1. Install and configure Docker - sudo apt-get update - sudo apt-get install -y docker.io - sudo systemctl start docker + # 1. Initialize Waydroid properties + sudo waydroid prop set persist.waydroid.adb_enabled 1 + sudo waydroid prop set persist.adb.tcp.port 5555 + sudo waydroid prop set ro.adb.secure 0 - # 2. Build and start the Android emulator container - sudo docker build -t kitten-lynx packages/testing-library/kitten-lynx - sudo docker run -d \ - --name kitten-lynx-android \ - --device /dev/kvm \ - --privileged \ - -p 5556:5556 \ - kitten-lynx + # 2. Start Waydroid session with headless compositor + weston --backend=headless-backend.so --socket=wayland-1 --idle-time=0 & + export WAYLAND_DISPLAY=wayland-1 + waydroid session start & - # 3. Run the tests + # 3. Wait for Waydroid to boot + echo "Waiting for waydroid to boot..." + sleep 10 + adb wait-for-device + while [ "$(adb shell getprop sys.boot_completed | tr -d '\r')" != "1" ]; do + sleep 2 + done + echo "Waydroid is ready." + + # 4. Install Lynx Explorer + wget -q https://github.com/lynx-family/lynx/releases/download/3.6.0/LynxExplorer-noasan-release.apk -O LynxExplorer.apk + waydroid app install LynxExplorer.apk + + # 5. Run the tests pnpm --filter @lynx-test/kitten-lynx run test test-typos: From c542281cd572a4cb8abddf7479454d0fd69d1def Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:54:07 +0800 Subject: [PATCH 08/31] feat: Install Waydroid and initialize it with a vanilla image. --- .github/workflows/test.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 31eb9c4a0f..3397a8a4d1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -255,7 +255,14 @@ jobs: with: runs-on: lynx-ubuntu-24.04-xlarge run: | + # 0. Install Waydroid + sudo apt-get update + sudo apt-get install -y curl ca-certificates + curl https://repo.waydro.id | sudo bash + sudo apt-get install -y waydroid weston + # 1. Initialize Waydroid properties + sudo waydroid init -f -s VANILLA sudo waydroid prop set persist.waydroid.adb_enabled 1 sudo waydroid prop set persist.adb.tcp.port 5555 sudo waydroid prop set ro.adb.secure 0 From 6a5c0d5f9adcde8b78286983182fa23227acb465 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Mon, 9 Mar 2026 12:32:31 +0800 Subject: [PATCH 09/31] chore: Add kmod package to Waydroid installation in the test workflow. --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 3397a8a4d1..713f2bb33c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -259,7 +259,7 @@ jobs: sudo apt-get update sudo apt-get install -y curl ca-certificates curl https://repo.waydro.id | sudo bash - sudo apt-get install -y waydroid weston + sudo apt-get install -y waydroid weston kmod # 1. Initialize Waydroid properties sudo waydroid init -f -s VANILLA From d17cc1be69bdb3cf82192b7ab0d2d2c9660def92 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Mon, 9 Mar 2026 13:56:53 +0800 Subject: [PATCH 10/31] ci: Install Waydroid kernel modules and their dependencies in the test workflow. --- .github/workflows/test.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 713f2bb33c..25a3726daa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -255,11 +255,17 @@ jobs: with: runs-on: lynx-ubuntu-24.04-xlarge run: | - # 0. Install Waydroid + # 0. Install Waydroid and kernel modules sudo apt-get update - sudo apt-get install -y curl ca-certificates + sudo apt-get install -y curl ca-certificates software-properties-common linux-headers-$(uname -r) kmod + sudo add-apt-repository -y ppa:morphis/anbox-support + sudo apt-get update + sudo apt-get install -y anbox-modules-dkms + sudo modprobe binder_linux || true + sudo modprobe ashmem_linux || true + curl https://repo.waydro.id | sudo bash - sudo apt-get install -y waydroid weston kmod + sudo apt-get install -y waydroid weston # 1. Initialize Waydroid properties sudo waydroid init -f -s VANILLA From 5030f5cd4f95db045941060990b12ad307000848 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:35:39 +0800 Subject: [PATCH 11/31] + fix --- .github/workflows/test.yml | 44 ++++++++++++++++---------------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 25a3726daa..75e09b1d32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -253,45 +253,37 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} name: Kitten Lynx Android Emulator Test with: - runs-on: lynx-ubuntu-24.04-xlarge + runs-on: lynx-ubuntu-22.04-physical-medium run: | - # 0. Install Waydroid and kernel modules + # 1. Install dependencies for Android Emulator sudo apt-get update - sudo apt-get install -y curl ca-certificates software-properties-common linux-headers-$(uname -r) kmod - sudo add-apt-repository -y ppa:morphis/anbox-support - sudo apt-get update - sudo apt-get install -y anbox-modules-dkms - sudo modprobe binder_linux || true - sudo modprobe ashmem_linux || true + sudo apt-get install -y libvirt-daemon-system libvirt-clients bridge-utils socat cpu-checker qemu-kvm - curl https://repo.waydro.id | sudo bash - sudo apt-get install -y waydroid weston + # 2. Set up Android SDK environment + # The runner seems to have a messy SDK structure. Let's try to install the required image. + yes | sdkmanager --licenses || true + sdkmanager "system-images;android-34;google_apis;x86_64" - # 1. Initialize Waydroid properties - sudo waydroid init -f -s VANILLA - sudo waydroid prop set persist.waydroid.adb_enabled 1 - sudo waydroid prop set persist.adb.tcp.port 5555 - sudo waydroid prop set ro.adb.secure 0 + # 3. Create AVD + echo "no" | avdmanager create avd -n test_device -k "system-images;android-34;google_apis;x86_64" --device "pixel" --force - # 2. Start Waydroid session with headless compositor - weston --backend=headless-backend.so --socket=wayland-1 --idle-time=0 & - export WAYLAND_DISPLAY=wayland-1 - waydroid session start & + # 4. Start Emulator + echo "Starting emulator..." + ${ANDROID_HOME}/emulator/emulator -avd test_device -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect & - # 3. Wait for Waydroid to boot - echo "Waiting for waydroid to boot..." - sleep 10 + # 5. Wait for boot + echo "Waiting for emulator to boot..." adb wait-for-device while [ "$(adb shell getprop sys.boot_completed | tr -d '\r')" != "1" ]; do sleep 2 done - echo "Waydroid is ready." + echo "Emulator is ready." - # 4. Install Lynx Explorer + # 6. Install Lynx Explorer wget -q https://github.com/lynx-family/lynx/releases/download/3.6.0/LynxExplorer-noasan-release.apk -O LynxExplorer.apk - waydroid app install LynxExplorer.apk + adb install -r LynxExplorer.apk - # 5. Run the tests + # 7. Run the tests pnpm --filter @lynx-test/kitten-lynx run test test-typos: From 5bf624f966b31b9ae022f375681b776aa571147e Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:13:25 +0800 Subject: [PATCH 12/31] fix: Explicitly set ANDROID_HOME and use `--sdk_root` with `sdkmanager` for Android SDK setup in CI. --- .github/workflows/test.yml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75e09b1d32..68cf9a6b85 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -260,9 +260,13 @@ jobs: sudo apt-get install -y libvirt-daemon-system libvirt-clients bridge-utils socat cpu-checker qemu-kvm # 2. Set up Android SDK environment - # The runner seems to have a messy SDK structure. Let's try to install the required image. - yes | sdkmanager --licenses || true - sdkmanager "system-images;android-34;google_apis;x86_64" + # The runner seems to have a messy SDK structure. Let's find the SDK root. + export ANDROID_HOME=${ANDROID_HOME:-/home/runner/Android} + echo "Using ANDROID_HOME: $ANDROID_HOME" + + # Accept licenses and install image using explicit sdk_root + yes | sdkmanager --sdk_root=$ANDROID_HOME --licenses || true + sdkmanager --sdk_root=$ANDROID_HOME "system-images;android-34;google_apis;x86_64" # 3. Create AVD echo "no" | avdmanager create avd -n test_device -k "system-images;android-34;google_apis;x86_64" --device "pixel" --force From 634592f7eb87efc7dda6b48f35c4b2c6db2df6e5 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:14:36 +0800 Subject: [PATCH 13/31] + fix --- packages/testing-library/kitten-lynx/src/Lynx.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index e56ceb2008..8f0838c0a8 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -122,6 +122,12 @@ export class Lynx { lynx._currentClient = matchedClients[0]; lynx._currentClientId = matchedClients[0]!.id; + await lynx._connector.setGlobalSwitch( + lynx._currentClientId, + 'enable_devtool', + true, + ); + return lynx; } From 2ea97fbe09c09d91e43f63149561e1388d031dff Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:50:00 +0800 Subject: [PATCH 14/31] + remove install stage --- .github/workflows/test.yml | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 68cf9a6b85..6589ffb361 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -255,25 +255,9 @@ jobs: with: runs-on: lynx-ubuntu-22.04-physical-medium run: | - # 1. Install dependencies for Android Emulator - sudo apt-get update - sudo apt-get install -y libvirt-daemon-system libvirt-clients bridge-utils socat cpu-checker qemu-kvm - - # 2. Set up Android SDK environment - # The runner seems to have a messy SDK structure. Let's find the SDK root. - export ANDROID_HOME=${ANDROID_HOME:-/home/runner/Android} - echo "Using ANDROID_HOME: $ANDROID_HOME" - - # Accept licenses and install image using explicit sdk_root - yes | sdkmanager --sdk_root=$ANDROID_HOME --licenses || true - sdkmanager --sdk_root=$ANDROID_HOME "system-images;android-34;google_apis;x86_64" - - # 3. Create AVD - echo "no" | avdmanager create avd -n test_device -k "system-images;android-34;google_apis;x86_64" --device "pixel" --force - # 4. Start Emulator echo "Starting emulator..." - ${ANDROID_HOME}/emulator/emulator -avd test_device -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect & + ${ANDROID_HOME}/emulator/emulator -avd Nexus_5_API_28 -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect & # 5. Wait for boot echo "Waiting for emulator to boot..." From 895852963760db1ed7e8a52f064ab1d3f971189e Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 10 Mar 2026 18:07:19 +0800 Subject: [PATCH 15/31] + fix url --- packages/testing-library/kitten-lynx/src/LynxView.ts | 5 +++++ packages/testing-library/kitten-lynx/tests/lynx.spec.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/testing-library/kitten-lynx/src/LynxView.ts b/packages/testing-library/kitten-lynx/src/LynxView.ts index 9797d90d7a..e1d96d59ca 100644 --- a/packages/testing-library/kitten-lynx/src/LynxView.ts +++ b/packages/testing-library/kitten-lynx/src/LynxView.ts @@ -51,6 +51,11 @@ export class LynxView { if (!this._channel) { for (let attempt = 0; attempt < 20; attempt++) { try { + await this._connector.setGlobalSwitch( + this._clientId, + 'enable_devtool', + true, + ); const sessions = await this._connector.sendListSessionMessage( this._clientId, ); diff --git a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts index 4272c71788..413bfdc5a2 100644 --- a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts +++ b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts @@ -16,10 +16,11 @@ describe('kitten-lynx testing framework', () => { const page = await lynx.newPage(); await page.goto( - 'http://10.91.84.156:3000/main.lynx.bundle', + 'https://lynxjs.org/next/lynx-examples/hello-world/dist/main.lynx.bundle', ); const content = await page.content(); + console.log('[Test] Page content:', content); expect(content).toContain('have fun'); const rootElement = await page.locator('view'); From 69e6d66e31d1c16f1e2df6405edc79900c5130a6 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 10 Mar 2026 19:29:10 +0800 Subject: [PATCH 16/31] + waiting time --- packages/testing-library/kitten-lynx/src/LynxView.ts | 9 ++------- packages/testing-library/kitten-lynx/tests/lynx.spec.ts | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/testing-library/kitten-lynx/src/LynxView.ts b/packages/testing-library/kitten-lynx/src/LynxView.ts index e1d96d59ca..8e0f8f1901 100644 --- a/packages/testing-library/kitten-lynx/src/LynxView.ts +++ b/packages/testing-library/kitten-lynx/src/LynxView.ts @@ -49,13 +49,8 @@ export class LynxView { async goto(url: string, _options?: unknown): Promise { // Attach to any existing session first so we can send Page.navigate if (!this._channel) { - for (let attempt = 0; attempt < 20; attempt++) { + for (let attempt = 0; attempt < 60; attempt++) { try { - await this._connector.setGlobalSwitch( - this._clientId, - 'enable_devtool', - true, - ); const sessions = await this._connector.sendListSessionMessage( this._clientId, ); @@ -87,7 +82,7 @@ export class LynxView { // Poll for the session whose URL matches the navigated bundle let matchedSessionId: number | undefined; - for (let attempt = 0; attempt < 20; attempt++) { + for (let attempt = 0; attempt < 60; attempt++) { await new Promise(r => setTimeout(r, 500)); try { const sessions = await this._connector.sendListSessionMessage( diff --git a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts index 413bfdc5a2..ed82936dd8 100644 --- a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts +++ b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts @@ -33,5 +33,5 @@ describe('kitten-lynx testing framework', () => { // Perform a tap action to verify the method executes successfully await expect(rootElement.tap()).resolves.toBeUndefined(); } - }, 30000); // Increase timeout to 30s as connecting/launching emulator app can be slow + }, 60000); // Increase timeout to 60s as connecting/launching emulator app can be slow }); From fdce55f178b792cc214bb246397052622902cd74 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 10 Mar 2026 21:48:11 +0800 Subject: [PATCH 17/31] + fix --- .../kitten-lynx/src/ElementNode.ts | 13 ++- .../src/{LynxView.ts => KittenLynxView.ts} | 100 ++++++++++-------- .../testing-library/kitten-lynx/src/Lynx.ts | 50 +++++---- .../testing-library/kitten-lynx/src/index.ts | 2 +- .../kitten-lynx/tests/lynx.spec.ts | 1 - 5 files changed, 94 insertions(+), 72 deletions(-) rename packages/testing-library/kitten-lynx/src/{LynxView.ts => KittenLynxView.ts} (72%) diff --git a/packages/testing-library/kitten-lynx/src/ElementNode.ts b/packages/testing-library/kitten-lynx/src/ElementNode.ts index c65d8072dd..10e61b16e2 100644 --- a/packages/testing-library/kitten-lynx/src/ElementNode.ts +++ b/packages/testing-library/kitten-lynx/src/ElementNode.ts @@ -1,6 +1,6 @@ -import type { LynxView } from './LynxView.js'; +import type { KittenLynxView } from './KittenLynxView.js'; -const idToElementNode = new WeakMap[]>(); +const idToElementNode = new WeakMap[]>(); /** * Represents a DOM element in a Lynx page. @@ -15,10 +15,10 @@ export class ElementNode { * Nodes are cached per LynxView using `WeakRef` to allow reuse and GC. * * @param id - The CDP node ID. - * @param lynxView - The owning LynxView instance. + * @param lynxView - The owning KittenLynxView instance. * @returns An `ElementNode` bound to the given node. */ - static fromId(id: number, lynxView: LynxView): ElementNode { + static fromId(id: number, lynxView: KittenLynxView): ElementNode { const currentViewMap = idToElementNode.get(lynxView); if (currentViewMap) { const couldBeElementNode = currentViewMap[id]?.deref(); @@ -38,7 +38,10 @@ export class ElementNode { return node; } - constructor(public readonly nodeId: number, private _lynxView: LynxView) {} + constructor( + public readonly nodeId: number, + private _lynxView: KittenLynxView, + ) {} /** * Simulate a tap (touch press + release) on the center of this element. diff --git a/packages/testing-library/kitten-lynx/src/LynxView.ts b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts similarity index 72% rename from packages/testing-library/kitten-lynx/src/LynxView.ts rename to packages/testing-library/kitten-lynx/src/KittenLynxView.ts index 8e0f8f1901..641e0f38b7 100644 --- a/packages/testing-library/kitten-lynx/src/LynxView.ts +++ b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts @@ -2,8 +2,9 @@ import type { Connector } from '@lynx-js/devtool-connector'; import { CDPChannel } from './CDPChannel.js'; import type { NodeInfoInGetDocument } from './CDPChannel.js'; import { ElementNode } from './ElementNode.js'; +import { setTimeout } from 'node:timers/promises'; -const idToLynxView: Record> = {}; +const idToKittenLynxView: Record> = {}; /** * Represents a Lynx page instance, similar to Puppeteer's `Page`. @@ -11,7 +12,7 @@ const idToLynxView: Record> = {}; * Provides methods for navigating to Lynx bundle URLs, querying the DOM, * and reading page content. Created via {@link Lynx.newPage}. */ -export class LynxView { +export class KittenLynxView { private static incId = 1; private _root?: ElementNode; _channel!: CDPChannel; @@ -23,8 +24,8 @@ export class LynxView { * @param id - The string representation of the LynxView's numeric ID. * @returns The LynxView if still alive, or `undefined` if garbage-collected. */ - static getLynxViewById(id: string): LynxView | undefined { - return idToLynxView[id]?.deref(); + static getKittenLynxViewById(id: string): KittenLynxView | undefined { + return idToKittenLynxView[id]?.deref(); } constructor( @@ -32,8 +33,8 @@ export class LynxView { private _clientId: string, private _client?: any, ) { - this.id = LynxView.incId++; - idToLynxView[this.id.toString()] = new WeakRef(this); + this.id = KittenLynxView.incId++; + idToKittenLynxView[this.id.toString()] = new WeakRef(this); } /** @@ -47,71 +48,86 @@ export class LynxView { * @throws If no session can be attached or no session matches the URL. */ async goto(url: string, _options?: unknown): Promise { - // Attach to any existing session first so we can send Page.navigate + const urlPath = url.split('/').pop() || url; + + // Wait until the Lynx app has booted and registered its devtool server. + // We confirm this by waiting until at least one session is reported. if (!this._channel) { - for (let attempt = 0; attempt < 60; attempt++) { + const startTime = Date.now(); + let ready = false; + while (Date.now() - startTime < 60000) { try { const sessions = await this._connector.sendListSessionMessage( this._clientId, ); if (sessions.length > 0) { - const sessionId = sessions[sessions.length - 1]!.session_id; - await this.onAttachedToTarget(sessionId); + ready = true; break; } - } catch { - // Session listing or CDP enable failed — app may still be initializing + } catch (error: any) { + // ignore error while booting } - await new Promise(r => setTimeout(r, 500)); + await setTimeout(500); } - - if (!this._channel) { - throw new Error('Failed to attach to a session'); + if (!ready) { + throw new Error( + 'Timeout waiting for Lynx App devtool to boot completely before navigation', + ); } } - // Navigate via CDP Page.navigate try { - await this._channel.send('Page.navigate', { url }); + const msg = await this._connector.sendAppMessage( + this._clientId, + 'App.openPage', + { + url, + }, + ); } catch { - // ignore — not all Lynx versions support this + const msg = await this._connector.sendMessage(this._clientId, { + event: 'Customized', + data: { + type: 'OpenCard', + data: { + type: 'url', + url, + }, + sender: -1, + }, + from: -1, + }); } - // Extract the bundle filename from the URL for matching - const urlPath = url.split('/').pop() || url; - // Poll for the session whose URL matches the navigated bundle let matchedSessionId: number | undefined; - for (let attempt = 0; attempt < 60; attempt++) { - await new Promise(r => setTimeout(r, 500)); + const navStartTime = Date.now(); + while (Date.now() - navStartTime < 30000) { + await setTimeout(500); try { const sessions = await this._connector.sendListSessionMessage( this._clientId, ); + const matched = sessions.find( - s => s.url === url || s.url === urlPath || url.endsWith(s.url), + s => + s.url === url || s.url === urlPath || url.endsWith(s.url) + || s.url.endsWith(urlPath), ); if (matched) { matchedSessionId = matched.session_id; break; } - } catch { - // ignore and retry + } catch (error: any) { + console.error(error); } } - // Re-attach to the correct session if it differs from the current one - if (matchedSessionId !== undefined) { - this._channel = undefined as any; - for (let attempt = 0; attempt < 10; attempt++) { - try { - await this.onAttachedToTarget(matchedSessionId); - break; - } catch { - await new Promise(r => setTimeout(r, 500)); - } - } + console.log('matchedSessionId', matchedSessionId); + if (matchedSessionId === undefined) { + throw new Error('cannot find session for URL: ' + url); } + await this.onAttachedToTarget(matchedSessionId); if (!this._channel) { throw new Error('Failed to attach to session for URL: ' + url); @@ -163,18 +179,12 @@ export class LynxView { this._clientId, this._connector, ); - // Enable DOM and Page agents — may fail if devtool server isn't ready - await Promise.all([ - channel.send('Runtime.enable' as any, {}), - channel.send('Page.enable' as any, {}).catch(() => {}), - channel.send('DOM.enable' as any, {}).catch(() => {}), - ]); + const response = await channel.send('DOM.getDocument', { depth: -1, }); const root = response.root.children[0]!; this._root = ElementNode.fromId(root.nodeId, this); - // Only set channel after everything succeeds this._channel = channel; } } diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index 8f0838c0a8..9c4e1d64ad 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -1,6 +1,6 @@ import { Connector } from '@lynx-js/devtool-connector'; import { AndroidTransport } from '@lynx-js/devtool-connector/transport'; -import { LynxView } from './LynxView.js'; +import { KittenLynxView } from './KittenLynxView.js'; import { execSync } from 'child_process'; @@ -60,28 +60,38 @@ export class Lynx { const lynx = new Lynx(); lynx._connector = new Connector([new AndroidTransport()]); - let deviceIdToUse = targetDevice; - if (!deviceIdToUse) { - const devices = await lynx._connector.listDevices(); - if (devices.length === 0) { - throw new Error('Failed to connect to Lynx: no devices found.'); + const devices = await lynx._connector.listDevices(); + if (devices.length === 0) { + throw new Error('Failed to connect to Lynx: no devices found.'); + } + + let devicesToSearch = devices; + if (targetDevice) { + devicesToSearch = devices.filter(d => d.id === targetDevice); + if (devicesToSearch.length === 0) { + throw new Error( + `Failed to connect to Lynx: device ${targetDevice} not found.`, + ); } + } - for (const device of devices) { - try { - const apps = await lynx._connector.listAvailableApps(device.id); - if (apps.some(app => app.packageName === appPackage)) { - deviceIdToUse = device.id; - break; - } - } catch (e) { - // Ignore errors checking apps on a specific device + let deviceIdToUse: string | undefined; + for (const device of devicesToSearch) { + try { + const apps = await lynx._connector.listAvailableApps(device.id); + if (apps.some(app => app.packageName === appPackage)) { + deviceIdToUse = device.id; + break; } + } catch (e) { + console.error(e); } + } - if (!deviceIdToUse) { - deviceIdToUse = devices[0]!.id; - } + if (!deviceIdToUse) { + throw new Error( + `Failed to connect to Lynx: app ${appPackage} not found on any available device.`, + ); } console.log( @@ -137,11 +147,11 @@ export class Lynx { * @returns A new {@link LynxView} instance bound to the current client. * @throws If not connected. Call {@link Lynx.connect} first. */ - async newPage(): Promise { + async newPage(): Promise { if (!this._connector || this._currentClientId === '') { throw new Error('Not connected. Call Lynx.connect() first.'); } - return new LynxView( + return new KittenLynxView( this._connector, this._currentClientId, this._currentClient, diff --git a/packages/testing-library/kitten-lynx/src/index.ts b/packages/testing-library/kitten-lynx/src/index.ts index a23e135c6b..67785bf2f0 100644 --- a/packages/testing-library/kitten-lynx/src/index.ts +++ b/packages/testing-library/kitten-lynx/src/index.ts @@ -1,4 +1,4 @@ export { Lynx } from './Lynx.js'; -export { LynxView } from './LynxView.js'; +export { KittenLynxView } from './KittenLynxView.js'; export { ElementNode } from './ElementNode.js'; export { CDPChannel } from './CDPChannel.js'; diff --git a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts index ed82936dd8..ccc71b1c57 100644 --- a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts +++ b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts @@ -20,7 +20,6 @@ describe('kitten-lynx testing framework', () => { ); const content = await page.content(); - console.log('[Test] Page content:', content); expect(content).toContain('have fun'); const rootElement = await page.locator('view'); From e1ca7fe5f6aafb3b7ae4b762437797f85e660c30 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 10 Mar 2026 22:12:12 +0800 Subject: [PATCH 18/31] ci: Start and verify Lynx Explorer application before running tests in the CI workflow. --- .github/workflows/test.yml | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6589ffb361..66b2f4903d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -271,7 +271,29 @@ jobs: wget -q https://github.com/lynx-family/lynx/releases/download/3.6.0/LynxExplorer-noasan-release.apk -O LynxExplorer.apk adb install -r LynxExplorer.apk - # 7. Run the tests + # 7. Start Lynx Explorer and verify + echo "Starting Lynx Explorer..." + if ! adb shell pm list packages | grep -q com.lynx.explorer; then + echo "Error: com.lynx.explorer is not installed!" + exit 1 + fi + adb shell monkey -p com.lynx.explorer -c android.intent.category.LAUNCHER 1 + + echo "Waiting for Lynx Explorer to start..." + MAX_RETRIES=10 + RETRY_COUNT=0 + while ! adb shell pidof com.lynx.explorer > /dev/null; do + if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then + echo "Error: com.lynx.explorer failed to start!" + exit 1 + fi + echo "Waiting... ($RETRY_COUNT/$MAX_RETRIES)" + sleep 2 + RETRY_COUNT=$((RETRY_COUNT+1)) + done + echo "Lynx Explorer is running." + + # 8. Run the tests pnpm --filter @lynx-test/kitten-lynx run test test-typos: From b3290b992718e09a5bd7b185f313c11c5be17445 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 11 Mar 2026 11:53:08 +0800 Subject: [PATCH 19/31] + log --- .github/workflows/test.yml | 7 +- .../kitten-lynx/src/KittenLynxView.ts | 85 ++++++++++++++++--- .../testing-library/kitten-lynx/src/Lynx.ts | 9 ++ .../kitten-lynx/tests/lynx.spec.ts | 12 ++- 4 files changed, 97 insertions(+), 16 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 66b2f4903d..d54f58f088 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -294,7 +294,12 @@ jobs: echo "Lynx Explorer is running." # 8. Run the tests - pnpm --filter @lynx-test/kitten-lynx run test + if ! pnpm --filter @lynx-test/kitten-lynx run test; then + echo "Tests failed! Dumping ADB logcat..." + adb logcat -d > adb_logcat_dump.txt + cat adb_logcat_dump.txt | tail -n 1000 + exit 1 + fi test-typos: runs-on: lynx-ubuntu-24.04-medium diff --git a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts index 641e0f38b7..a9c0cb7419 100644 --- a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts +++ b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts @@ -53,19 +53,36 @@ export class KittenLynxView { // Wait until the Lynx app has booted and registered its devtool server. // We confirm this by waiting until at least one session is reported. if (!this._channel) { + console.log(`[goto] Waiting for devtool to boot...`); const startTime = Date.now(); let ready = false; + let bootLoops = 0; while (Date.now() - startTime < 60000) { + bootLoops++; try { const sessions = await this._connector.sendListSessionMessage( this._clientId, ); + if (bootLoops % 10 === 0) { + console.log( + `[goto] list sessions returned ${sessions.length} sessions (loop ${bootLoops})`, + ); + } if (sessions.length > 0) { + console.log( + `[goto] Devtool booted in ${Date.now() - startTime}ms. Sessions:`, + JSON.stringify(sessions), + ); ready = true; break; } } catch (error: any) { - // ignore error while booting + if (bootLoops % 10 === 0) { + console.error( + `[goto] list sessions error (loop ${bootLoops}):`, + error.message || error, + ); + } } await setTimeout(500); } @@ -76,6 +93,7 @@ export class KittenLynxView { } } + console.log(`[goto] Sending App.openPage to URL: ${url}`); try { const msg = await this._connector.sendAppMessage( this._clientId, @@ -84,46 +102,85 @@ export class KittenLynxView { url, }, ); - } catch { - const msg = await this._connector.sendMessage(this._clientId, { - event: 'Customized', - data: { - type: 'OpenCard', + console.log(`[goto] App.openPage succeeded:`, msg); + } catch (e: any) { + console.log( + `[goto] App.openPage failed, falling back to Customized OpenCard. Error:`, + e.message, + ); + try { + const msg = await this._connector.sendMessage(this._clientId, { + event: 'Customized', data: { - type: 'url', - url, + type: 'OpenCard', + data: { + type: 'url', + url, + }, + sender: -1, }, - sender: -1, - }, - from: -1, - }); + from: -1, + }); + console.log(`[goto] Customized OpenCard succeeded:`, msg); + } catch (fallbackErr: any) { + console.error( + `[goto] Customized OpenCard failed:`, + fallbackErr.message, + ); + } } // Poll for the session whose URL matches the navigated bundle + console.log(`[goto] Polling for session matching URL: ${url}`); let matchedSessionId: number | undefined; const navStartTime = Date.now(); + let pollLoops = 0; while (Date.now() - navStartTime < 30000) { + pollLoops++; await setTimeout(500); try { const sessions = await this._connector.sendListSessionMessage( this._clientId, ); + if (pollLoops % 10 === 0) { + console.log( + `[goto] (Loop ${pollLoops}) Available sessions:`, + JSON.stringify(sessions.map(s => s.url)), + ); + } + const matched = sessions.find( s => s.url === url || s.url === urlPath || url.endsWith(s.url) || s.url.endsWith(urlPath), ); if (matched) { + console.log( + `[goto] Found matched session after ${ + Date.now() - navStartTime + }ms: id=${matched.session_id}, url=${matched.url}`, + ); matchedSessionId = matched.session_id; break; } } catch (error: any) { - console.error(error); + if (pollLoops % 10 === 0) { + console.error( + `[goto] list sessions error in polling (loop ${pollLoops}):`, + error.message || error, + ); + } } } - console.log('matchedSessionId', matchedSessionId); + if (matchedSessionId === undefined) { + console.error( + `[goto] Failed to find session for URL after 30000ms: ${url}`, + ); + } else { + console.log('matchedSessionId', matchedSessionId); + } if (matchedSessionId === undefined) { throw new Error('cannot find session for URL: ' + url); } diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index 9c4e1d64ad..0536d9c6cf 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -113,7 +113,16 @@ export class Lynx { throw e; } + console.log( + `[Lynx] Waiting 2 seconds for app to initialize before listing clients...`, + ); + await new Promise(resolve => setTimeout(resolve, 2000)); + const clients = await lynx._connector.listClients(); + console.log(`[Lynx] Found ${clients.length} clients in total.`); + for (const c of clients) { + console.log(`[Lynx] Client ID: ${c.id}, App: ${c.info?.AppProcessName}`); + } // Filter clients by deviceId and target package const encodedDeviceId = encodeURIComponent(deviceIdToUse); diff --git a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts index ccc71b1c57..556faceb7a 100644 --- a/packages/testing-library/kitten-lynx/tests/lynx.spec.ts +++ b/packages/testing-library/kitten-lynx/tests/lynx.spec.ts @@ -13,24 +13,34 @@ describe('kitten-lynx testing framework', () => { }); it('can navigate to a page and read the DOM', async () => { + console.log('[test] creating new page...'); const page = await lynx.newPage(); + console.log('[test] page created.'); + console.log('[test] navigating to hello-world bundle...'); await page.goto( 'https://lynxjs.org/next/lynx-examples/hello-world/dist/main.lynx.bundle', ); + console.log('[test] navigation complete.'); + console.log('[test] getting page content...'); const content = await page.content(); + console.log(`[test] page content received, length: ${content.length}`); expect(content).toContain('have fun'); + console.log('[test] locator view...'); const rootElement = await page.locator('view'); expect(rootElement).toBeDefined(); if (rootElement) { + console.log('[test] getting computed styles...'); const styles = await rootElement.computedStyleMap(); expect(styles.size).toBeGreaterThan(0); // Perform a tap action to verify the method executes successfully + console.log('[test] tapping root element...'); await expect(rootElement.tap()).resolves.toBeUndefined(); } - }, 60000); // Increase timeout to 60s as connecting/launching emulator app can be slow + console.log('[test] finished successfully'); + }, 90000); // Increase timeout to 90s as connecting/launching emulator app can be slow }); From 30c8fda229b13b965b986e94f3c562ee2a43fd19 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 11 Mar 2026 13:53:58 +0800 Subject: [PATCH 20/31] + fix --- packages/testing-library/kitten-lynx/src/KittenLynxView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts index a9c0cb7419..aceab5fce9 100644 --- a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts +++ b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts @@ -68,7 +68,7 @@ export class KittenLynxView { `[goto] list sessions returned ${sessions.length} sessions (loop ${bootLoops})`, ); } - if (sessions.length > 0) { + if (Array.isArray(sessions)) { console.log( `[goto] Devtool booted in ${Date.now() - startTime}ms. Sessions:`, JSON.stringify(sessions), From 57aca0c3c1304c140c78102eb2c7698a9531ee75 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Wed, 11 Mar 2026 16:39:52 +0800 Subject: [PATCH 21/31] + essential infos --- .changeset/sharp-dragons-search.md | 5 + .../testing-library/kitten-lynx/AGENTS.md | 19 +- .../testing-library/kitten-lynx/Dockerfile | 50 -- .../kitten-lynx/start-emulator.sh | 28 - packages/testing-library/kitten-lynx/test.out | 528 ------------------ 5 files changed, 11 insertions(+), 619 deletions(-) create mode 100644 .changeset/sharp-dragons-search.md delete mode 100644 packages/testing-library/kitten-lynx/Dockerfile delete mode 100644 packages/testing-library/kitten-lynx/start-emulator.sh delete mode 100644 packages/testing-library/kitten-lynx/test.out diff --git a/.changeset/sharp-dragons-search.md b/.changeset/sharp-dragons-search.md new file mode 100644 index 0000000000..8b895dbf1c --- /dev/null +++ b/.changeset/sharp-dragons-search.md @@ -0,0 +1,5 @@ +--- +"@lynx-test/kitten-lynx": patch +--- + +feat: initial commit diff --git a/packages/testing-library/kitten-lynx/AGENTS.md b/packages/testing-library/kitten-lynx/AGENTS.md index ac6ae7712b..5ef6566d04 100644 --- a/packages/testing-library/kitten-lynx/AGENTS.md +++ b/packages/testing-library/kitten-lynx/AGENTS.md @@ -33,27 +33,20 @@ Through the Chrome DevTools Protocol (CDP), `kitten-lynx` enables: For the library to interact successfully: -- The host machine (or CI environment) must have **Waydroid** installed and initialized. Waydroid provides a much lighter and faster Android environment compared to Docker-based emulators. -- The Waydroid container must be running (e.g. `waydroid session start`). -- ADB must be authorized. To avoid manual authorization prompts (especially in CI), configure Waydroid properties before starting: - ```bash - sudo waydroid prop set persist.waydroid.adb_enabled 1 - sudo waydroid prop set persist.adb.tcp.port 5555 - sudo waydroid prop set ro.adb.secure 0 - ``` -- The Lynx Explorer APK must be installed inside Waydroid (`waydroid app install /path/to/LynxExplorer.apk`). +- The host machine (or CI environment) must have an Android environment (emulator or real device) running with ADB enabled and authorized. +- The Lynx Explorer APK must be installed on the device (e.g., `adb install /path/to/LynxExplorer.apk`). The latest apk chould be found here `https://github.com/lynx-family/lynx/releases` - Typical commands use `pnpm run test` starting `vitest` logic inside the Node wrapper. ### Known Gotchas - **`Page.navigate` does not work like Chrome**: In Lynx, `Page.navigate` tells the runtime to load a new bundle, which creates a **new session** rather than updating the current one in place. You must poll `sendListSessionMessage()` to find the new session by URL and re-attach to it. - **`App.openPage` is not implemented** in Lynx Explorer 3.6.0. Do not rely on `sendAppMessage('App.openPage')` for navigation. -- **Network access & Local Serving**: Waydroid shares the host's network. However, if the container lacks direct internet access (common in some CI setups): +- **Network access & Local Serving**: If the Android environment lacks direct internet access to your host machine's local server (common in some CI setups): 1. Serve your Lynx bundles from the host (e.g., `python3 -m http.server 8080`). - 2. Use `adb reverse tcp:8080 tcp:8080` to map the host port into Waydroid. + 2. Use `adb reverse tcp:8080 tcp:8080` to map the host port to the device. 3. Navigate to `http://localhost:8080/your.bundle`. -- **Multiple ADB targets**: When multiple ADB devices are connected (e.g. physical phone + Waydroid), use `ConnectOptions.deviceId` to target a specific one (e.g. `192.168.240.112:5555`). Otherwise the first available client is used, which may be on the wrong device. -- **CDP timeouts**: The connector uses a 5-second `AbortSignal.timeout`. Keep test operations tolerant of Waydroid boot/warm-up times. +- **Multiple ADB targets**: When multiple ADB devices are connected (e.g. physical phone + emulator), use `ConnectOptions.deviceId` to target a specific one (e.g. `192.168.240.112:5555`). Otherwise the first available client is used, which may be on the wrong device. +- **CDP timeouts**: The connector uses a 5-second `AbortSignal.timeout`. Keep test operations tolerant of emulator boot/warm-up times. ## Adding Features diff --git a/packages/testing-library/kitten-lynx/Dockerfile b/packages/testing-library/kitten-lynx/Dockerfile deleted file mode 100644 index 85f529013b..0000000000 --- a/packages/testing-library/kitten-lynx/Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -FROM ubuntu:24.04 - -# Avoid prompts from apt -ENV DEBIAN_FRONTEND=noninteractive - -# Install dependencies required for Android SDK and Emulator -RUN apt-get update && apt-get install -y \ - openjdk-17-jdk \ - wget \ - unzip \ - git \ - curl \ - cpu-checker \ - qemu-kvm \ - libvirt-daemon-system \ - libvirt-clients \ - bridge-utils \ - socat \ - && rm -rf /var/lib/apt/lists/* - -# Set up Android SDK environment variables -ENV ANDROID_HOME=/opt/android-sdk -ENV PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator - -# Download and install Android command line tools -RUN mkdir -p ${ANDROID_HOME}/cmdline-tools && \ - cd ${ANDROID_HOME}/cmdline-tools && \ - wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O cmdline-tools.zip && \ - unzip -q cmdline-tools.zip && \ - rm cmdline-tools.zip && \ - mv cmdline-tools latest - -# Accept licenses and install required SDK components -RUN yes | sdkmanager --licenses && \ - sdkmanager "platform-tools" "platforms;android-34" "emulator" "system-images;android-34;google_apis;x86_64" - -# Create Android Virtual Device (AVD) -RUN echo "no" | avdmanager create avd -n test_device -k "system-images;android-34;google_apis;x86_64" --device "pixel" --force - -# Download Lynx Explorer APK -RUN curl -L -o /opt/LynxExplorer.apk https://github.com/lynx-family/lynx/releases/download/3.6.0/LynxExplorer-noasan-release.apk - -# Add startup script -COPY start-emulator.sh /start-emulator.sh -RUN chmod +x /start-emulator.sh - -# Expose ADB port -EXPOSE 5555 - -ENTRYPOINT ["/start-emulator.sh"] diff --git a/packages/testing-library/kitten-lynx/start-emulator.sh b/packages/testing-library/kitten-lynx/start-emulator.sh deleted file mode 100644 index d802deae40..0000000000 --- a/packages/testing-library/kitten-lynx/start-emulator.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash -set -e - -# Start the Android emulator in headless mode -echo "Starting emulator..." -${ANDROID_HOME}/emulator/emulator -avd test_device -no-window -no-audio -gpu swiftshader_indirect & - -# Wait for the emulator to boot -echo "Waiting for emulator to become ready..." -adb wait-for-device -while [ "$(adb shell getprop sys.boot_completed | tr -d '\r')" != "1" ]; do - sleep 2 -done - -echo "Emulator is ready." - -echo "Installing Lynx Explorer..." -adb install -r /opt/LynxExplorer.apk - -echo "Launching Lynx Explorer..." -adb shell monkey -p com.lynx.explorer -c android.intent.category.LAUNCHER 1 - -# Forward ADB port 5555 to make scrcpy work from the host via port 5556 -echo "Setting up ADB port forwarding..." -socat TCP-LISTEN:5556,fork,bind=0.0.0.0 TCP:127.0.0.1:5555 & - -# Keep the container running -tail -f /dev/null diff --git a/packages/testing-library/kitten-lynx/test.out b/packages/testing-library/kitten-lynx/test.out deleted file mode 100644 index 78d5e68514..0000000000 --- a/packages/testing-library/kitten-lynx/test.out +++ /dev/null @@ -1,528 +0,0 @@ - -> @lynx-test/kitten-lynx@0.1.0 test /home/haoyang/lynx/lynx-stack/packages/testing-library/kitten-lynx -> vitest run - - - RUN v3.2.4 /home/haoyang/lynx/lynx-stack/packages/testing-library/kitten-lynx - -stdout | tests/lynx.spec.ts > kitten-lynx testing framework -[2026-02-25 21:07:57] INFO startMonitorMultiOpen -[2026-02-25 21:07:57] INFO MultiOpen: switch to attached - -stdout | tests/lynx.spec.ts > kitten-lynx testing framework > can navigate to a page and read the DOM -[DEBUG DOM.getDocument root] { - "attributes": [], - "backendNodeId": 11, - "childNodeCount": 1, - "children": [ - { - "attributes": [], - "backendNodeId": 10, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "clip-radius", - "true", - "flatten", - "false", - "style", - "height:100%;" - ], - "backendNodeId": 13, - "childNodeCount": 2, - "children": [ - { - "attributes": [ - "clip-radius", - "true", - "flatten", - "false", - "class", - "page__light" - ], - "backendNodeId": 14, - "childNodeCount": 3, - "children": [ - { - "attributes": [ - "class", - "page-header" - ], - "backendNodeId": 15, - "childNodeCount": 3, - "children": [ - { - "attributes": [ - "mode", - "aspectFit", - "src", - "data:image/png;base64,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", - "class", - "logo" - ], - "backendNodeId": 16, - "childNodeCount": 0, - "children": [], - "localName": "image", - "nodeId": 16, - "nodeName": "IMAGE", - "nodeType": 1, - "nodeValue": "", - "parentId": 15 - }, - { - "attributes": [ - "class", - "home-title__light" - ], - "backendNodeId": 17, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "text", - "Lynx Explorer" - ], - "backendNodeId": 18, - "childNodeCount": 0, - "children": [], - "localName": "raw-text", - "nodeId": 18, - "nodeName": "RAW-TEXT", - "nodeType": 1, - "nodeValue": "", - "parentId": 17 - } - ], - "localName": "text", - "nodeId": 17, - "nodeName": "TEXT", - "nodeType": 1, - "nodeValue": "", - "parentId": 15 - }, - { - "attributes": [ - "class", - "scan" - ], - "backendNodeId": 19, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "accessibility-element", - "true", - "accessibility-label", - "Open Scan", - "accessibility-traits", - "button", - "src", - "data:image/png;base64,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", - "bindtap", - "-3:1:", - "class", - "scan-icon" - ], - "backendNodeId": 33, - "childNodeCount": 0, - "children": [], - "localName": "image", - "nodeId": 33, - "nodeName": "IMAGE", - "nodeType": 1, - "nodeValue": "", - "parentId": 19 - } - ], - "localName": "view", - "nodeId": 19, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 15 - } - ], - "localName": "view", - "nodeId": 15, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 14 - }, - { - "attributes": [ - "class", - "input-card-url__light" - ], - "backendNodeId": 20, - "childNodeCount": 3, - "children": [ - { - "attributes": [ - "class", - "bold-text__light" - ], - "backendNodeId": 21, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "text", - "Card URL" - ], - "backendNodeId": 22, - "childNodeCount": 0, - "children": [], - "localName": "raw-text", - "nodeId": 22, - "nodeName": "RAW-TEXT", - "nodeType": 1, - "nodeValue": "", - "parentId": 21 - } - ], - "localName": "text", - "nodeId": 21, - "nodeName": "TEXT", - "nodeType": 1, - "nodeValue": "", - "parentId": 20 - }, - { - "attributes": [ - "placeholder", - "Enter Card URL", - "text-color", - "#000000", - "bindinput", - "-4:6:", - "class", - "input-box" - ], - "backendNodeId": 23, - "childNodeCount": 0, - "children": [], - "localName": "explorer-input", - "nodeId": 23, - "nodeName": "EXPLORER-INPUT", - "nodeType": 1, - "nodeValue": "", - "parentId": 20 - }, - { - "attributes": [ - "accessibility-element", - "true", - "accessibility-label", - "Open Schema", - "accessibility-traits", - "button", - "bindtap", - "-4:9:", - "class", - "connect-button__light" - ], - "backendNodeId": 24, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "accessibility-element", - "false", - "style", - "font-size:16px;color:#ffffff;line-height:22px;" - ], - "backendNodeId": 25, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "text", - "Go" - ], - "backendNodeId": 26, - "childNodeCount": 0, - "children": [], - "localName": "raw-text", - "nodeId": 26, - "nodeName": "RAW-TEXT", - "nodeType": 1, - "nodeValue": "", - "parentId": 25 - } - ], - "localName": "text", - "nodeId": 25, - "nodeName": "TEXT", - "nodeType": 1, - "nodeValue": "", - "parentId": 24 - } - ], - "localName": "view", - "nodeId": 24, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 20 - } - ], - "localName": "view", - "nodeId": 20, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 14 - }, - { - "attributes": [ - "accessibility-element", - "true", - "accessibility-label", - "Open Show Cases", - "accessibility-traits", - "button", - "bindtap", - "-4:11:", - "class", - "showcase__light" - ], - "backendNodeId": 27, - "childNodeCount": 3, - "children": [ - { - "attributes": [ - "src", - "data:image/png;base64,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", - "class", - "showcase-icon" - ], - "backendNodeId": 28, - "childNodeCount": 0, - "children": [], - "localName": "image", - "nodeId": 28, - "nodeName": "IMAGE", - "nodeType": 1, - "nodeValue": "", - "parentId": 27 - }, - { - "attributes": [ - "accessibility-element", - "false", - "class", - "text__light" - ], - "backendNodeId": 29, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "text", - "Showcase" - ], - "backendNodeId": 30, - "childNodeCount": 0, - "children": [], - "localName": "raw-text", - "nodeId": 30, - "nodeName": "RAW-TEXT", - "nodeType": 1, - "nodeValue": "", - "parentId": 29 - } - ], - "localName": "text", - "nodeId": 29, - "nodeName": "TEXT", - "nodeType": 1, - "nodeValue": "", - "parentId": 27 - }, - { - "attributes": [ - "style", - "justify-content:center;margin-left:auto;margin-bottom:auto;margin-right:5%;margin-top:auto;" - ], - "backendNodeId": 31, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "src", - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAGGSURBVHgB7do7SkNRFIXh7QNSWAgWFoKVjZ1TcBQWztDewnEIVhZaCIIBC0FBUHAvroEQEjjNfqywPlhFHtXPvUVujpmIiIiIiIiIyFbZsVjHvkvfqW/uu/O9GZE9i3Pou/Yd/b8+8J37nn2fRmLX4pz5Zivv4fWVTVcWhchAm1BFirzF3n0Xvv01n+E9itstMtCv78mmELSRIgPBl5FHig4E1JEyAgFtpKxAQBkpMxDQRcoOBFSRKgIBTaSqQEARqTIQtI9UHQhaR+oQCNpG6hIIWkbqFAjaReoWCEYj3ft+LFjHQDASCd95tWAVTxRHRf+hMKRrIDyOxWPZ2YbPv32PlqBjoJE4N74PS9DiMl4yGmduSToFahcHugRqGQc6BGobB6oDtY4DlYHax4GqQBRxoCIQTRzIDkQVBzID0cWBrECUcSAjEG0cyDijSBsHIn/N44widRzIPqO4QBEHKp4H0cSByEAPNsVYRhUHMs4onth0RvrFd2tEcUREREREREREttAfMpSHesEfTNcAAAAASUVORK5CYII=", - "class", - "forward-icon" - ], - "backendNodeId": 32, - "childNodeCount": 0, - "children": [], - "localName": "image", - "nodeId": 32, - "nodeName": "IMAGE", - "nodeType": 1, - "nodeValue": "", - "parentId": 31 - } - ], - "localName": "view", - "nodeId": 31, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 27 - } - ], - "localName": "view", - "nodeId": 27, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 14 - } - ], - "localName": "view", - "nodeId": 14, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 13 - }, - { - "attributes": [ - "clip-radius", - "true", - "flatten", - "false", - "class", - "navigator__light" - ], - "backendNodeId": 34, - "childNodeCount": 2, - "children": [ - { - "attributes": [ - "accessibility-element", - "true", - "accessibility-label", - "Show Home Page", - "accessibility-traits", - "button", - "bindtap", - "-5:1:", - "class", - "button" - ], - "backendNodeId": 35, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "src", - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHEAAABwCAYAAAApIp91AAAACXBIWXMAACxLAAAsSwGlPZapAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAPqSURBVHgB7Z1NTlNRGIa/00InamRoAJO7BBKtIY7KDlgCrEBZAWEFwArAFegO6JAoiV1CB2Ic6gAHLb3He4tRkd72u+39+c7b9xmQUg705+F+DyXpPSKEEEIISXECju9Ea/Jz0Bl/0mj13GW/L2DAShzLuxkeJpfe3v+KO5Hh6pHr9b8LCJAS/XYUyWjwPrm4lbGkL83WDspR2RAw/PZmR26HnyVbYEoq+WK8FgAoib69+UZG8YU4v6ZYHqVr/av1QwkciHF617/BcXJxT+Yi7E4GL1HRPy3BdjLocarsn5ZgOxmsxJz90xJkJ4Mbp4v3T0s4nQxKYoH90xJEJ4MZpwX3T0sQnQxCYkn902K+k6bHaXX902Kzk2Yl1tA/LeY6aXKc1tQ/LeY6aU5izf3TYqqTZsapvf5pqb+TJiQa7p+WWjtZ+zg13j8ttXayVomB9E9LbZ2sZZyG2z8t1XaycokA/dNSWScrHacg/dNSWScrkwjWPy2VdLL0cYrfPy3ldbJUiUvUPy2ldLK0cbpk/dNSSidLkbik/dNSeCcLHafsX16K6WRhEtm/uVm4k4WMU/ZvIRbu5MIS2b9CWKiTc49T9q8s8ndyLonsX+nk6mTuccr+VUKuTuaSyP5VirqTqnHK/tXN9E7OlMj+mSGzk1Ml/hZ4IemhTSwwUeT0JlKgNZKDanj2/5WZEn072hIKNIjv+Ncb99KWfSTGo0iITYailNiI+TIiEOBORrSMUCIAlAgAJQJAiQBQIgCUCAAlAkCJAFAiAJQIACUCQIkAUCIAlAgAJQJAiQCsSHC4fakUfybGCU6i+3R9LhXi2+vmJXKcAkCJAFAiAJQIACUCQIkAUCIAAb7Yz8cy7O4NK/HP7t43g7+7e48GyYv3DbjdvXHH6c3w+OH27CnJda2B+f/C5AFSon/xfDf5uJe9QHZRdvZOwTwSG3Fn5pqR3xUQMCVqTtHS8E8FBL7EAIASAaBEACgRAEoEgBIBoEQAKBEASgSAEgGgRAAoEQBKBIASAaBEACgRAEoEgBIBoEQAKBEASgSAEgGgRACyJXrXF3vo3j/h3ex1sfshKpy992w077vJlni70jP4AD6oVnk3e53zXdHxTizhpO8uv3T/vSpT4vhdQ16OxAou3bEsVt0fd5U8SD9V+Ln7+FX3C9EcnYxv2wrePXgOpjbRXV2fjE/+U/uDcF1pxDvu8ltf/S23rf1kZJ5O+Fmn8qh1IErGt5nctoHnIDmo/MGk8/ioN4b2Lzc7yQiKpHp6yR3vyZz47WeRDFfvNsh60uy67vzvS/TtjS2JG1Hl+2ilf588Xuktct8JIYSQZeAXK6rzwC0a4W0AAAAASUVORK5CYII=", - "class", - "icon" - ], - "backendNodeId": 36, - "childNodeCount": 0, - "children": [], - "localName": "image", - "nodeId": 36, - "nodeName": "IMAGE", - "nodeType": 1, - "nodeValue": "", - "parentId": 35 - } - ], - "localName": "view", - "nodeId": 35, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 34 - }, - { - "attributes": [ - "accessibility-element", - "true", - "accessibility-label", - "Show Settings Page", - "accessibility-traits", - "button", - "bindtap", - "-5:3:", - "class", - "button" - ], - "backendNodeId": 37, - "childNodeCount": 1, - "children": [ - { - "attributes": [ - "src", - "data:image/png;base64,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", - "class", - "icon" - ], - "backendNodeId": 38, - "childNodeCount": 0, - "children": [], - "localName": "image", - "nodeId": 38, - "nodeName": "IMAGE", - "nodeType": 1, - "nodeValue": "", - "parentId": 37 - } - ], - "localName": "view", - "nodeId": 37, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 34 - } - ], - "localName": "view", - "nodeId": 34, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 13 - } - ], - "localName": "view", - "nodeId": 13, - "nodeName": "VIEW", - "nodeType": 1, - "nodeValue": "", - "parentId": 10 - } - ], - "localName": "page", - "nodeId": 10, - "nodeName": "PAGE", - "nodeType": 1, - "nodeValue": "" - } - ], - "localName": "", - "nodeId": 11, - "nodeName": "#document", - "nodeType": 9, - "nodeValue": "" -} - - ❯ tests/lynx.spec.ts (1 test | 1 failed) 2718ms - × kitten-lynx testing framework > can navigate to a page and read the DOM 1698ms - → expected '<#document> Date: Thu, 12 Mar 2026 15:52:42 +0800 Subject: [PATCH 23/31] + fix dedupe --- .../testing-library/kitten-lynx/package.json | 4 ---- pnpm-lock.yaml | 19 ------------------- 2 files changed, 23 deletions(-) diff --git a/packages/testing-library/kitten-lynx/package.json b/packages/testing-library/kitten-lynx/package.json index 34f27cb5a7..b22e18dc3d 100644 --- a/packages/testing-library/kitten-lynx/package.json +++ b/packages/testing-library/kitten-lynx/package.json @@ -31,9 +31,5 @@ }, "dependencies": { "@lynx-js/devtool-connector": "workspace:*" - }, - "devDependencies": { - "@types/node": "^22.0.0", - "typescript": "^5.0.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6fa81503f0..19cdd821f4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1227,13 +1227,6 @@ importers: '@lynx-js/devtool-connector': specifier: workspace:* version: link:../../mcp-servers/devtool-connector - devDependencies: - '@types/node': - specifier: ^22.0.0 - version: 22.0.0 - typescript: - specifier: ^5.0.0 - version: 5.9.3 packages/testing-library/testing-environment: devDependencies: @@ -4501,9 +4494,6 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@22.0.0': - resolution: {integrity: sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==} - '@types/node@24.10.13': resolution: {integrity: sha512-oH72nZRfDv9lADUBSo104Aq7gPHpQZc4BTx38r9xf9pg5LfP6EzSyH2n7qFmmxRQXh7YlUXODcYsg6PuTDSxGg==} @@ -9771,9 +9761,6 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - undici-types@6.11.1: - resolution: {integrity: sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==} - undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -12995,10 +12982,6 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@22.0.0': - dependencies: - undici-types: 6.11.1 - '@types/node@24.10.13': dependencies: undici-types: 7.16.0 @@ -19115,8 +19098,6 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - undici-types@6.11.1: {} - undici-types@7.16.0: {} undici@6.23.0: {} From 53fadb5de47623fdb8a0b376f3511864ebf685ca Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:25:24 +0800 Subject: [PATCH 24/31] docs: Add comprehensive JSDoc comments and a new README to clarify KittenLynx API usage and internal mechanics for agents. --- .../testing-library/kitten-lynx/README.md | 113 ++++++++++++++++++ .../kitten-lynx/src/CDPChannel.ts | 43 +++++-- .../kitten-lynx/src/ElementNode.ts | 59 +++++++-- .../kitten-lynx/src/KittenLynxView.ts | 76 ++++++++---- .../testing-library/kitten-lynx/src/Lynx.ts | 38 ++++-- 5 files changed, 272 insertions(+), 57 deletions(-) create mode 100644 packages/testing-library/kitten-lynx/README.md diff --git a/packages/testing-library/kitten-lynx/README.md b/packages/testing-library/kitten-lynx/README.md new file mode 100644 index 0000000000..c4e4929b8b --- /dev/null +++ b/packages/testing-library/kitten-lynx/README.md @@ -0,0 +1,113 @@ +# Kitten-Lynx (🐾 testing-library) + +**Kitten-Lynx** is a Puppeteer-like / Playwright-like testing library. It is designed specifically for interacting with the **Lynx browser engine** and the **Lynx Explorer Android application**. + +If you are an AI Agent (or a developer) reading this, this document is optimized to be as clear and straightforward as possible to help you write tests and understand the architecture without guessing. + +--- + +## 🌟 What does it do? + +Using the Chrome DevTools Protocol (CDP) over USB/ADB, `kitten-lynx` gives you the power to: + +1. Automatically open the Lynx Explorer app on an Android emulator or physical device. +2. Navigate to `.lynx.bundle` URLs. +3. Access the Lynx DOM (Document Object Model) tree. +4. Find elements using CSS Selectors (e.g. `page.locator('#my-id')`). +5. Read element styles and attributes. +6. Simulate native touch gestures (like tapping on buttons). + +--- + +## 🏗️ Architecture Explained (For Agents) + +In standard Web Playwright/Puppeteer, you connect to a persistent browser WebSocket. **Lynx is different.** + +1. **Stateless Connector:** This library uses `@lynx-js/devtool-connector` which operates via Android Debug Bridge (ADB). It sends isolated Request/Response commands. There is no long-living socket. +2. **Session Hopping:** When you tell Lynx to navigate to a new URL, Lynx creates an entirely **new debugging session**. +3. **`Lynx.ts`**: Handles the physical device connection, force-stops the app, restarts it, and ensures the Master devtool switch is ON. +4. **`KittenLynxView.ts`**: Represents a single "Page". When you call `goto(url)`, it sends the navigate command, and then intensely **polls** the ADB session list until it finds the new session matching your URL, and re-attaches to it. +5. **`ElementNode.ts`**: Represents a physical tag (like `` or ``). Cached via `WeakRef` to save memory. Uses native coordinate math via `DOM.getBoxModel` to simulate real screen taps. + +--- + +## 🚀 Quick Start Guide + +### Prerequisites + +- You must have an Android Emulator or device running via `adb`. +- The Lynx Explorer APK must be installed (`adb install LynxExplorer.apk`). +- (In CI) Ensure your test runner can reach your local bundle dev server (you might need `adb reverse tcp:8080 tcp:8080`). + +### Example Test Script + +Here is the blueprint for a standard test written using `kitten-lynx` and `vitest`: + +```typescript +import { expect, test, beforeAll, afterAll } from 'vitest'; +import { Lynx } from '@lynx-js/kitten-lynx'; +import type { KittenLynxView } from '@lynx-js/kitten-lynx'; + +let lynx: Lynx; +let page: KittenLynxView; + +// Setup: Connect to device +beforeAll(async () => { + // Connects to the first available ADB device and opens com.lynx.explorer + lynx = await Lynx.connect(); + page = await lynx.newPage(); +}, 60000); // Give ADB enough time to boot! + +// Teardown: Clean up resources +afterAll(async () => { + await lynx.close(); +}); + +test('Basic Navigation and Interaction', async () => { + // 1. Navigate to the bundle (Will poll until the session is found) + await page.goto('http://10.0.2.2:8080/dist/main.lynx.bundle'); + + // 2. Locate an element by CSS Selector + const button = await page.locator('#submit-btn'); + expect(button).toBeDefined(); + + // 3. Read an attribute. + // (Note: 'id' maps internally to Lynx's 'idSelector') + const idValue = await button!.getAttribute('id'); + expect(idValue).toBe('submit-btn'); + + // 4. Read computed CSS styles + const styles = await button!.computedStyleMap(); + expect(styles.get('display')).toBe('flex'); + + // 5. Simulate a native tap + await button!.tap(); + + // 6. Assert DOM changes (re-query the new element) + const successText = await page.locator('.success-message'); + expect(successText).toBeDefined(); +}, 30000); +``` + +--- + +## ⚠️ Known Gotchas & Pitfalls + +If you are writing scripts and tests, memorize these rules: + +1. **`goto()` implies a Session Change:** After `page.goto()`, the old node IDs are dead. Always query elements _after_ the navigation finishes. +2. **Timeouts:** Android emulators take time to boot. The devtool ADB bridge takes time to synchronize. Always set high timeouts for setup hooks (`beforeAll(..., 60000)`). +3. **No `App.openPage` locally:** Lynx Explorer 3.6.0 does not support `App.openPage` properly in some fallback layers. `kitten-lynx` falls back to a Custom OpenCard event automatically. You do not need to worry about this, but do not be alarmed by terminal warnings. +4. **Id Selector:** Standard web writes ``. Lynx internally uses `idSelector="test"`. The `ElementNode.getAttribute('id')` handles this mapping automatically for you. Do not query `'idSelector'` directly. +5. **DOM Snapshots:** You can call `await page.content()` to get a massive HTML-like string of the current Lynx DOM. This is extremely helpful for debugging what is actually rendering! + +--- + +## 🛠️ Extending the Library + +If you need to add a newly supported CDP command: + +1. Open `src/CDPChannel.ts`. +2. Add the strictly-typed parameter and return shapes to the `Protocol` interface block at the top of the file. +3. Call `await this._channel.send('Domain.methodName', params)` in `KittenLynxView` or `ElementNode`. +4. Run `pnpm run build && pnpm run test` before committing. diff --git a/packages/testing-library/kitten-lynx/src/CDPChannel.ts b/packages/testing-library/kitten-lynx/src/CDPChannel.ts index 7956180069..0185faab59 100644 --- a/packages/testing-library/kitten-lynx/src/CDPChannel.ts +++ b/packages/testing-library/kitten-lynx/src/CDPChannel.ts @@ -95,15 +95,16 @@ interface Protocol { export class CDPChannel { private _id = 0; /** - * Get or create a CDPChannel for the given session. + * Retrieves or instantiates a stateless `CDPChannel` for a specific Lynx session. * - * Channels are cached per `sessionId` using `WeakRef`. If a previously - * created channel for the same session is still alive, it is reused. + * **Design pattern (For Agents):** + * This testing library uses a **stateless** CDP architecture to avoid hanging Websocket connections + * over unstable ADB links. Channels are cached per `sessionId` via `WeakRef` to reduce allocations. * - * @param sessionId - The Lynx devtool session ID. - * @param clientId - The client identifier (format: `"deviceId:port"`). - * @param connector - The `Connector` instance for sending messages. - * @returns A `CDPChannel` bound to the specified session. + * @param sessionId - The numeric Lynx devtool session ID assigned by the Lynx runtime. + * @param clientId - The unique device-port identifier for the client app (e.g., `"emulator-5554:40121"`). + * @param connector - The active `Connector` instance powering the ADB transport. + * @returns A `CDPChannel` reference bound to the requested session. */ static from( sessionId: number, @@ -118,6 +119,17 @@ export class CDPChannel { return channel; } } + /** + * Constructs a new CDP Channel strictly bound to a single Lynx session. + * + * **Note for Agents:** + * Favor using `CDPChannel.from()` over manually newing up a channel, as `from()` handles + * WeakRef caching to minimize memory footprint. + * + * @param _connector - Low-level ADB transport controller. + * @param _clientId - Bound devtool target identification string. + * @param _sessionId - Narrowly scoped Lynx view session ID. + */ constructor( private _connector: Connector, private _clientId: string, @@ -125,11 +137,20 @@ export class CDPChannel { ) {} /** - * Send a CDP command and return the result. + * Dispatches a strongly-typed Chrome DevTools Protocol (CDP) method command and awaits its return block. + * + * **Agent Usage:** + * This is the beating heart of all interactions in `kitten-lynx`. Instead of maintaining a socket, + * every `send(...)` call constructs a self-contained ADB request and awaits the isolated response. + * + * **Typings:** + * Notice that the generics strictly bind to the `Protocol` interface defined at the top of this file. + * If you need to invoke a CDP command that TypeScript rejects, you must update the `Protocol` interface first! * - * @param method - The CDP method name (e.g. `'DOM.getDocument'`, `'Page.navigate'`). - * @param params - The parameters for the CDP method. - * @returns The CDP response for the given method. + * @typeParam T - The literal string name of the CDP method (e.g., `'DOM.getDocument'`, `'Page.navigate'`). + * @param method - The command to send to the Lynx devtool server. + * @param params - A strongly-typed payload object required by the CDP method. + * @returns A promise resolving to a strongly-typed response object matching the expected return structure of the `method`. */ async send( method: T, diff --git a/packages/testing-library/kitten-lynx/src/ElementNode.ts b/packages/testing-library/kitten-lynx/src/ElementNode.ts index 10e61b16e2..74835f8ff5 100644 --- a/packages/testing-library/kitten-lynx/src/ElementNode.ts +++ b/packages/testing-library/kitten-lynx/src/ElementNode.ts @@ -10,13 +10,16 @@ const idToElementNode = new WeakMap[]>(); */ export class ElementNode { /** - * Get or create an ElementNode for the given node ID within a LynxView. + * Retrieves or creates an `ElementNode` instance corresponding to a specific Lynx DOM node ID. * - * Nodes are cached per LynxView using `WeakRef` to allow reuse and GC. + * **Agent Caching Strategy:** + * Nodes are aggressively cached per `KittenLynxView` instance using `WeakRef`. + * This means if an Agent queries the same DOM node twice before garbage collection, + * it returns the same `ElementNode` reference, reducing memory overhead during extensive DOM crawling. * - * @param id - The CDP node ID. - * @param lynxView - The owning KittenLynxView instance. - * @returns An `ElementNode` bound to the given node. + * @param id - The numeric CDP node ID to wrap. + * @param lynxView - The `KittenLynxView` instance this node belongs to. + * @returns An `ElementNode` bound to the provided node ID and view. */ static fromId(id: number, lynxView: KittenLynxView): ElementNode { const currentViewMap = idToElementNode.get(lynxView); @@ -38,16 +41,35 @@ export class ElementNode { return node; } + /** + * Initializes a new `ElementNode` instance to represent a Lynx Component or Tag. + * + * **Note for Agents:** + * Similar to `KittenLynxView`, you should rarely call this directly. + * Rely on `KittenLynxView.locator()` or `ElementNode.fromId()` for instantiations. + * + * @param nodeId - The unique CDP numeric ID denoting this element in the Lynx renderer. + * @param _lynxView - The parent `KittenLynxView` instance used to dispatch subsequent CDP queries. + */ constructor( public readonly nodeId: number, private _lynxView: KittenLynxView, ) {} /** - * Simulate a tap (touch press + release) on the center of this element. + * Simulates a native user tap (touch press followed by touch release) directly on the center of this element. * - * Uses `DOM.getBoxModel` to find the element's center coordinates, - * then dispatches `mousePressed` and `mouseReleased` events. + * **Internal Mechanics (For Agents):** + * This is not a simulated DOM event (like `element.click()` in Web). It is a highly accurate native-layer + * gesture dispatch: + * 1. Fetches the exact boundary coordinates via `DOM.getBoxModel`. + * 2. Calculates the absolute center `(x, y)` of the content box. + * 3. Dispatches an `Input.emulateTouchFromMouseEvent` (`'mousePressed'`) over the ADB bridge. + * 4. Waits 50ms, then dispatches the corresponding `'mouseReleased'`. + * + * **Crucial:** Ensure the element is visible on the screen before calling this, or the coordinate calculation might fail or hit nothing. + * + * @throws Error if the layout box model cannot be computed natively. */ async tap(): Promise { const { model } = await this._lynxView._channel.send('DOM.getBoxModel', { @@ -86,10 +108,15 @@ export class ElementNode { } /** - * Get the value of a named attribute on this element. + * Retrieves the string value of a specified attribute on this element. * - * @param name - The attribute name. Use `'id'` for the Lynx `idSelector` attribute. - * @returns The attribute value, or `null` if the attribute is not present. + * **Agent Quirk / Gotcha:** + * In Lynx, standard web `id="foo"` attributes are actually stored as `idSelector="foo"` internally. + * This library provides a seamless shim: if you query `getAttribute('id')`, it automatically + * queries `idSelector` instead. No manual handling of `idSelector` is required on your part. + * + * @param name - The name of the attribute to fetch. + * @returns A promise resolving to the string value of the attribute, or `null` if the attribute does not exist. */ async getAttribute(name: string): Promise { if (name === 'id') { @@ -108,9 +135,15 @@ export class ElementNode { } /** - * Get all computed CSS styles for this element. + * Fetches all actively computed CSS properties for this specific element. + * + * **Agent Usage:** + * This relies on `CSS.getComputedStyleForNode`. It returns the fully resolved style values + * post-layout calculation (e.g., resolving `100%` into absolute `px` values). + * It is highly recommended to use this for visual assertions (e.g., verifying a box is indeed `display: none` + * or has a specific `background-color`). * - * @returns A `Map` of CSS property names to their computed values. + * @returns A promise resolving to a native JS `Map`, where keys are CSS property names and values are their computed string expressions. */ async computedStyleMap(): Promise> { const map = new Map(); diff --git a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts index aceab5fce9..0a1f6ff4cc 100644 --- a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts +++ b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts @@ -19,15 +19,29 @@ export class KittenLynxView { readonly id: number; /** - * Retrieve a previously created LynxView by its string ID. + * Retrieves a previously created `KittenLynxView` instance using its stringified numeric ID. * - * @param id - The string representation of the LynxView's numeric ID. - * @returns The LynxView if still alive, or `undefined` if garbage-collected. + * **Why this is useful for Agents:** + * This is primarily used for cross-referencing or finding an existing view without passing the object reference around. + * + * @param id - The string representation of the LynxView's numeric ID (e.g., `'1'`, `'2'`). + * @returns The `KittenLynxView` instance if it is still alive in memory, or `undefined` if it has been garbage-collected. */ static getKittenLynxViewById(id: string): KittenLynxView | undefined { return idToKittenLynxView[id]?.deref(); } + /** + * Initializes a new `KittenLynxView` instance. + * + * **Note for Agents:** + * You generally should avoid calling this constructor directly. Instead, use `Lynx.newPage()` to properly + * initialize a `KittenLynxView` instance bound to the active ADB connection and client. + * + * @param _connector - The low-level `Connector` instance used to dispatch CDP messages over ADB/USB. + * @param _clientId - The unique client identifier (typically `:`). + * @param _client - Optional client metadata object containing internal app states. + */ constructor( private _connector: Connector, private _clientId: string, @@ -38,14 +52,20 @@ export class KittenLynxView { } /** - * Navigate to a Lynx bundle URL. + * Navigates the Lynx App to a specific Lynx bundle URL and attaches to the corresponding CDP session. * - * Attaches to an existing CDP session, sends `Page.navigate`, then polls - * for the session whose URL matches the target bundle. Re-attaches to - * the matched session and refreshes the DOM tree. + * **How it works (Crucial for Agents to understand):** + * Unlike standard web browsers, calling `Page.navigate` in Lynx creates a **new** debugging session + * instead of reusing the current one. This method abstracts away that complexity by: + * 1. Waiting for the devtool server to boot. + * 2. Sending an `App.openPage` (or a fallback message) to trigger the navigation. + * 3. **Polling the session list** over ADB to find a new session whose URL matches the target bundle URL. + * 4. Automatically re-attaching to the matched session and fetching the initial DOM tree (`DOM.getDocument`). * - * @param url - The Lynx bundle URL to navigate to. - * @throws If no session can be attached or no session matches the URL. + * @param url - The absolute URL of the Lynx bundle to navigate to (e.g., `'http://localhost:8080/dist/main.lynx.bundle'`). + * @param _options - Currently unused. Reserved for future navigation options. + * @throws An error if it times out waiting for the devtool server to boot (60s limit). + * @throws An error if the specific session for the URL cannot be found (30s limit) or cannot be attached. */ async goto(url: string, _options?: unknown): Promise { const urlPath = url.split('/').pop() || url; @@ -199,11 +219,16 @@ export class KittenLynxView { } /** - * Find the first element matching a CSS selector in the current page. + * Locates the first DOM element matching the provided CSS selector in the current page. + * + * **Agent Usage:** + * This operates identically to `document.querySelector` or Playwright's `page.locator()`. + * It relies on the `DOM` CDP domain. Always ensure that `goto()` has completed successfully before calling this, + * otherwise the DOM tree will not exist. * - * @param selector - A CSS selector string (e.g. `'view'`, `'#my-id'`). - * @returns The matched {@link ElementNode}, or `undefined` if not found. - * @throws If no page has been loaded yet. Call {@link goto} first. + * @param selector - A valid CSS selector string targeting the desired element (e.g., `'view'`, `'#submit-btn'`, `'.container > text'`). + * @returns A promise resolving to an `ElementNode` containing methods to interact with the element, or `undefined` if no node matched. + * @throws Error if the method is called before a page is loaded via `goto()`. */ async locator(selector: string): Promise { if (!this._root) { @@ -220,14 +245,17 @@ export class KittenLynxView { } /** - * Attach to a CDP session by session ID and enable required domains. + * Attaches the LynxView to a specific Lynx devtool session and initializes the CDP channel. + * + * **Internal Mechanics:** + * This method creates a `CDPChannel` for the specific `sessionId` and immediately fires + * a `DOM.getDocument` request to cache the root document node. This must succeed for the page to be interactable. * - * Creates a {@link CDPChannel}, enables `Runtime`, `Page`, and `DOM` domains, - * and fetches the initial document tree. Only sets `_channel` after all - * operations succeed, making this method safely retryable. + * **Note for Agents:** + * This is generally marked as internal, but it is not `private`. You normally do not need to call this manually, + * as `goto()` handles session attachment automatically. * - * @param sessionId - The Lynx devtool session ID to attach to. - * @internal + * @param sessionId - The numeric Lynx devtool session ID to attach to, discovered via `sendListSessionMessage`. */ async onAttachedToTarget(sessionId: number) { if (!this._channel) { @@ -265,12 +293,14 @@ export class KittenLynxView { } /** - * Serialize the current page's DOM tree to an HTML-like string. + * Serializes the current page's entire DOM tree into an HTML-like string format. * - * Fetches a fresh `DOM.getDocument` snapshot and recursively converts - * all nodes to a string representation with tag names and attributes. + * **Agent Usage:** + * This is highly useful for debugging and logging the current state of the Lynx App DOM. + * It forces a fresh `DOM.getDocument` snapshot from the CDP server and recursively walks the tree + * to build a string containing tags and attributes (e.g., `...`). * - * @returns The serialized DOM content of the page. + * @returns A promise resolving to a string representing the serialized DOM content of the page. */ async content(): Promise { const document = await this._channel.send('DOM.getDocument', { diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index 0536d9c6cf..5756b9dd59 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -44,14 +44,21 @@ export class Lynx { private _currentClientId: string = ''; /** - * Connect to a Lynx app on an Android device. + * Main setup method. Connects to the Lynx app devtool server over ADB. * - * Discovers ADB devices, restarts the target app, and waits for a Lynx - * devtool client to become available. + * **Agent Guide on the Connection Flow:** + * 1. Discovers the target ADB device (physical Android or emulator). + * 2. Force-stops the target app to ensure a clean state (`adb shell am force-stop`). + * 3. Launches the application (usually Lynx Explorer) on the device. + * 4. Queries the `Connector` for available clients and matches by device ID and package name. + * 5. Enables the master devtool switch (`enable_devtool`). * - * @param options - Connection options to specify target device and app package. - * @returns A connected `Lynx` instance ready for creating pages. - * @throws If no Lynx client is found after 10 seconds of polling. + * **When to use:** + * This should be the first method invoked in any test script or interaction flow. + * + * @param options - Configure connection variables, such as `appPackage` and `deviceId` (useful if multiple devices are attached). + * @returns A Promise resolving to a connected `Lynx` instance ready to spawn new pages. + * @throws Errors if devices aren't found, the app is missing, or the target client fails to initialize. */ static async connect(options?: ConnectOptions): Promise { const targetDevice = options?.deviceId; @@ -151,10 +158,15 @@ export class Lynx { } /** - * Create a new page (LynxView) for navigating and interacting with Lynx content. + * Spawns a new page representation for the connected Lynx environment. + * + * **Agent Usage:** + * Similar to Puppeteer's `browser.newPage()`. Once you have a `Lynx` connection instance safely created + * via `Lynx.connect()`, call this method to obtain a `KittenLynxView`. You can then call `post.goto(url)` + * on the view to navigate to a Lynx Bundle. * - * @returns A new {@link LynxView} instance bound to the current client. - * @throws If not connected. Call {@link Lynx.connect} first. + * @returns A Promise resolving to a new `KittenLynxView` instance bound to the active ADB client. + * @throws Error if the Lynx connection isn't properly initialized first. */ async newPage(): Promise { if (!this._connector || this._currentClientId === '') { @@ -168,7 +180,13 @@ export class Lynx { } /** - * Close the connection and release resources. + * Closes the active ADB/CDP connection and releases associated resources. + * + * **Agent Usage:** + * Ensure this is called in your test's teardown block (e.g., in `afterAll()`) to prevent + * floating Node.js processes or hanging ADB connections that can ruin subsequent test runs. + * + * @returns A Promise resolving when the cleanup operation is fully processed. */ async close(): Promise { this._connector = null; From da748a5199a0b60b3c2f371ba111827be92e7362 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:03:56 +0800 Subject: [PATCH 25/31] Remove `container-image` parameter from workflows and adjust test exclusions and dependencies. --- .github/workflows/deploy-main.yml | 2 -- .github/workflows/test.yml | 6 ------ .github/workflows/workflow-test.yml | 6 +----- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/workflows/deploy-main.yml b/.github/workflows/deploy-main.yml index a5e25747f2..97e0c527a7 100644 --- a/.github/workflows/deploy-main.yml +++ b/.github/workflows/deploy-main.yml @@ -102,7 +102,6 @@ jobs: with: runs-on: lynx-custom-container is-web: true - container-image: mcr.microsoft.com/playwright:v1.58.2-noble run: | export NODE_OPTIONS="--max-old-space-size=32768" export PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-report.junit.xml @@ -117,7 +116,6 @@ jobs: with: runs-on: lynx-custom-container is-web: true - container-image: mcr.microsoft.com/playwright:v1.58.2-noble run: | export NODE_OPTIONS="--max-old-space-size=32768" export PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-report.junit.xml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac14026db6..8ff5509753 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -75,7 +75,6 @@ jobs: with: runs-on: lynx-custom-container is-web: true - container-image: mcr.microsoft.com/playwright:v1.58.2-noble run: | export NODE_OPTIONS="--max-old-space-size=32768" pnpm --filter @lynx-js/web-tests run lh || echo "Lighthouse failed" @@ -92,7 +91,6 @@ jobs: with: runs-on: lynx-custom-container is-web: true - container-image: mcr.microsoft.com/playwright:v1.58.2-noble codecov-flags: "e2e" web-report-path: "packages/web-platform/web-elements/playwright-report" run: | @@ -118,7 +116,6 @@ jobs: with: runs-on: lynx-custom-container is-web: true - container-image: mcr.microsoft.com/playwright:v1.58.2-noble web-report-name: "playwright-${{ matrix.thread }}-${{ matrix.render }}-shard${{ matrix.shard }}" codecov-flags: "e2e" run: | @@ -146,7 +143,6 @@ jobs: with: runs-on: lynx-custom-container is-web: true - container-image: mcr.microsoft.com/playwright:v1.58.2-noble web-report-name: "playwright-${{ matrix.render }}-shard${{ matrix.shard }}" web-report-path: "packages/web-platform/web-core-wasm-e2e/playwright-report" codecov-flags: "e2e" @@ -339,7 +335,6 @@ jobs: --no-cache --logHeapUsage --silent - --exclude packages/testing-library/kitten-lynx/vitest.config.ts website: needs: build uses: ./.github/workflows/workflow-website.yml @@ -348,7 +343,6 @@ jobs: - benchmark - code-style-check - eslint - - kitten-lynx-android-emulator - playwright-linux - playwright-web-elements - test-api diff --git a/.github/workflows/workflow-test.yml b/.github/workflows/workflow-test.yml index 75bcbbd594..c66d68c13d 100644 --- a/.github/workflows/workflow-test.yml +++ b/.github/workflows/workflow-test.yml @@ -18,9 +18,6 @@ on: required: false type: boolean default: false - container-image: - required: false - type: string web-report-name: required: false type: string @@ -46,8 +43,7 @@ jobs: runs-on: ${{ inputs.runs-on }} permissions: {} container: - image: ${{ inputs.container-image || null }} - options: --user 0 + image: ${{ inputs.is-web && 'mcr.microsoft.com/playwright:v1.58.2-noble' || null }} env: CI: 1 TURBO_TELEMETRY_DISABLED: 1 From f264485bd27ce6811ac697b1199b1c0e72f965e5 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:17:54 +0800 Subject: [PATCH 26/31] refactor: rename `@lynx-test/kitten-lynx` to `@lynx-js/kitten-lynx-test-infra` across all relevant files. --- .changeset/sharp-dragons-search.md | 2 +- .github/workflows/test.yml | 2 +- packages/testing-library/kitten-lynx/AGENTS.md | 2 +- packages/testing-library/kitten-lynx/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/sharp-dragons-search.md b/.changeset/sharp-dragons-search.md index 8b895dbf1c..c336f31cd7 100644 --- a/.changeset/sharp-dragons-search.md +++ b/.changeset/sharp-dragons-search.md @@ -1,5 +1,5 @@ --- -"@lynx-test/kitten-lynx": patch +"@lynx-js/kitten-lynx-test-infra": patch --- feat: initial commit diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8ff5509753..338b8487e3 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -290,7 +290,7 @@ jobs: echo "Lynx Explorer is running." # 8. Run the tests - if ! pnpm --filter @lynx-test/kitten-lynx run test; then + if ! pnpm --filter @lynx-js/kitten-lynx-test-infra run test; then echo "Tests failed! Dumping ADB logcat..." adb logcat -d > adb_logcat_dump.txt cat adb_logcat_dump.txt | tail -n 1000 diff --git a/packages/testing-library/kitten-lynx/AGENTS.md b/packages/testing-library/kitten-lynx/AGENTS.md index 553db6a600..f49e1bb4a2 100644 --- a/packages/testing-library/kitten-lynx/AGENTS.md +++ b/packages/testing-library/kitten-lynx/AGENTS.md @@ -1,4 +1,4 @@ -# @lynx-test/kitten-lynx +# @lynx-js/kitten-lynx-test-infra This document provides context, architecture guidelines, and workflows for agents interacting with the `kitten-lynx` framework. diff --git a/packages/testing-library/kitten-lynx/package.json b/packages/testing-library/kitten-lynx/package.json index b22e18dc3d..65e96b4701 100644 --- a/packages/testing-library/kitten-lynx/package.json +++ b/packages/testing-library/kitten-lynx/package.json @@ -1,5 +1,5 @@ { - "name": "@lynx-test/kitten-lynx", + "name": "@lynx-js/kitten-lynx-test-infra", "version": "0.1.0", "description": "A testing framework executing the Lynx explorer Android application", "keywords": [ From b92aa16ba7b5ab04c4ccebcf562c5e46ebd3c312 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 12 Mar 2026 19:26:13 +0800 Subject: [PATCH 27/31] refactor: Migrate ADB interactions to `@yume-chan/adb` and adjust touch event emulation with a 50ms delay. --- .../testing-library/kitten-lynx/package.json | 4 +- .../kitten-lynx/src/CDPChannel.ts | 3 +- .../kitten-lynx/src/ElementNode.ts | 6 +- .../testing-library/kitten-lynx/src/Lynx.ts | 18 +++++- pnpm-lock.yaml | 64 ++++++++++--------- 5 files changed, 57 insertions(+), 38 deletions(-) diff --git a/packages/testing-library/kitten-lynx/package.json b/packages/testing-library/kitten-lynx/package.json index 65e96b4701..c8cf291e81 100644 --- a/packages/testing-library/kitten-lynx/package.json +++ b/packages/testing-library/kitten-lynx/package.json @@ -30,6 +30,8 @@ "test": "vitest run" }, "dependencies": { - "@lynx-js/devtool-connector": "workspace:*" + "@lynx-js/devtool-connector": "workspace:*", + "@yume-chan/adb": "catalog:adb", + "@yume-chan/adb-server-node-tcp": "catalog:adb" } } diff --git a/packages/testing-library/kitten-lynx/src/CDPChannel.ts b/packages/testing-library/kitten-lynx/src/CDPChannel.ts index 0185faab59..23f491f370 100644 --- a/packages/testing-library/kitten-lynx/src/CDPChannel.ts +++ b/packages/testing-library/kitten-lynx/src/CDPChannel.ts @@ -76,7 +76,7 @@ interface Protocol { type: 'mousePressed' | 'mouseReleased' | 'mouseMoved'; x: number; y: number; - timestamp: number; + timestamp?: number; button: 'left' | 'middle' | 'right'; deltaX?: number; deltaY?: number; @@ -93,7 +93,6 @@ interface Protocol { * Channels are cached per session ID using `WeakRef` to allow reuse. */ export class CDPChannel { - private _id = 0; /** * Retrieves or instantiates a stateless `CDPChannel` for a specific Lynx session. * diff --git a/packages/testing-library/kitten-lynx/src/ElementNode.ts b/packages/testing-library/kitten-lynx/src/ElementNode.ts index 74835f8ff5..07556aec95 100644 --- a/packages/testing-library/kitten-lynx/src/ElementNode.ts +++ b/packages/testing-library/kitten-lynx/src/ElementNode.ts @@ -1,4 +1,5 @@ import type { KittenLynxView } from './KittenLynxView.js'; +import { setTimeout } from 'node:timers/promises'; const idToElementNode = new WeakMap[]>(); @@ -89,21 +90,20 @@ export class ElementNode { ); } - const timestamp = Date.now(); await this._lynxView._channel.send('Input.emulateTouchFromMouseEvent', { type: 'mousePressed', x, y, button: 'left', - timestamp, }); + await setTimeout(50); + await this._lynxView._channel.send('Input.emulateTouchFromMouseEvent', { type: 'mouseReleased', x, y, button: 'left', - timestamp: timestamp + 50, }); } diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index 5756b9dd59..fd476259c7 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -1,9 +1,9 @@ import { Connector } from '@lynx-js/devtool-connector'; import { AndroidTransport } from '@lynx-js/devtool-connector/transport'; +import { AdbServerClient } from '@yume-chan/adb'; +import { AdbServerNodeTcpConnector } from '@yume-chan/adb-server-node-tcp'; import { KittenLynxView } from './KittenLynxView.js'; -import { execSync } from 'child_process'; - /** * Options for configuring the connection to a Lynx device. */ @@ -105,7 +105,19 @@ export class Lynx { `[Lynx] Restarting ${appPackage} on device ${deviceIdToUse}...`, ); try { - execSync(`adb -s ${deviceIdToUse} shell am force-stop ${appPackage}`); + const client = new AdbServerClient( + new AdbServerNodeTcpConnector({ port: 5037 }), + ); + const adb = await client.createAdb({ serial: deviceIdToUse }); + try { + await adb.subprocess.noneProtocol.spawnWaitText([ + 'am', + 'force-stop', + appPackage, + ]); + } finally { + await adb.close(); + } } catch (e) { console.error( `[Lynx] Failed to force-stop app on device ${deviceIdToUse}:`, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 19cdd821f4..3c38ae30bd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,7 +275,7 @@ importers: version: 3.7.0 '@rsbuild/plugin-babel': specifier: 1.1.0 - version: 1.1.0(@rsbuild/core@1.7.3) + version: 1.1.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) '@types/react': specifier: ^18.3.28 version: 18.3.28 @@ -1112,10 +1112,10 @@ importers: version: link:../../web-platform/web-elements '@rsbuild/plugin-less': specifier: 1.6.0 - version: 1.6.0(@rsbuild/core@1.7.3) + version: 1.6.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) '@rsbuild/plugin-sass': specifier: 1.5.0 - version: 1.5.0(@rsbuild/core@1.7.3) + version: 1.5.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) commander: specifier: ^13.1.0 version: 13.1.0 @@ -1130,7 +1130,7 @@ importers: version: 1.1.1 rsbuild-plugin-tailwindcss: specifier: 0.2.4 - version: 0.2.4(@rsbuild/core@1.7.3)(tailwindcss@4.2.1) + version: 0.2.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(tailwindcss@4.2.1) rslog: specifier: ^1.3.2 version: 1.3.2 @@ -1227,6 +1227,12 @@ importers: '@lynx-js/devtool-connector': specifier: workspace:* version: link:../../mcp-servers/devtool-connector + '@yume-chan/adb': + specifier: catalog:adb + version: 2.5.1 + '@yume-chan/adb-server-node-tcp': + specifier: catalog:adb + version: 2.5.2 packages/testing-library/testing-environment: devDependencies: @@ -1968,13 +1974,13 @@ importers: version: 7.33.4(@types/node@24.10.13) '@rsbuild/plugin-sass': specifier: 1.5.0 - version: 1.5.0(@rsbuild/core@1.7.3) + version: 1.5.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) '@rsbuild/plugin-type-check': specifier: 1.3.4 - version: 1.3.4(@rsbuild/core@1.7.3)(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3) + version: 1.3.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3) '@rsbuild/plugin-typed-css-modules': specifier: 1.2.2 - version: 1.2.2(@rsbuild/core@1.7.3) + version: 1.2.2(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) '@rspress/core': specifier: 2.0.3 version: 2.0.3(@types/react@19.2.14)(core-js@3.48.0) @@ -12086,13 +12092,13 @@ snapshots: optionalDependencies: core-js: 3.48.0 - '@rsbuild/plugin-babel@1.1.0(@rsbuild/core@1.7.3)': + '@rsbuild/plugin-babel@1.1.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@rsbuild/core': 1.7.3 + '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) '@types/babel__core': 7.20.5 deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -12132,9 +12138,9 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 - '@rsbuild/plugin-less@1.6.0(@rsbuild/core@1.7.3)': + '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))': dependencies: - '@rsbuild/core': 1.7.3 + '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -12163,6 +12169,15 @@ snapshots: reduce-configs: 1.1.1 sass-embedded: 1.97.3 + '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))': + dependencies: + '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) + deepmerge: 4.3.1 + loader-utils: 2.0.4 + postcss: 8.5.6 + reduce-configs: 1.1.1 + sass-embedded: 1.97.3 + '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@1.7.3)': dependencies: fast-glob: 3.3.3 @@ -12171,19 +12186,6 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 - '@rsbuild/plugin-type-check@1.3.4(@rsbuild/core@1.7.3)(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3)': - dependencies: - deepmerge: 4.3.1 - json5: 2.2.3 - reduce-configs: 1.1.1 - ts-checker-rspack-plugin: 1.3.0(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3) - optionalDependencies: - '@rsbuild/core': 1.7.3 - transitivePeerDependencies: - - '@rspack/core' - - tslib - - typescript - '@rsbuild/plugin-type-check@1.3.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3)': dependencies: deepmerge: 4.3.1 @@ -12201,6 +12203,10 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 + '@rsbuild/plugin-typed-css-modules@1.2.2(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))': + optionalDependencies: + '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) + '@rsdoctor/client@1.2.3': {} '@rsdoctor/core@1.2.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.7(@swc/helpers@0.5.18))(webpack@5.105.2)': @@ -18069,15 +18075,15 @@ snapshots: optionalDependencies: '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) - rsbuild-plugin-tailwindcss@0.2.4(@rsbuild/core@1.7.3)(tailwindcss@4.2.1): + rsbuild-plugin-tailwindcss@0.2.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(tailwindcss@3.4.19): dependencies: - tailwindcss: 4.2.1 + tailwindcss: 3.4.19 optionalDependencies: - '@rsbuild/core': 1.7.3 + '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) - rsbuild-plugin-tailwindcss@0.2.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(tailwindcss@3.4.19): + rsbuild-plugin-tailwindcss@0.2.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(tailwindcss@4.2.1): dependencies: - tailwindcss: 3.4.19 + tailwindcss: 4.2.1 optionalDependencies: '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) From 32ebdfe5972daf73d5d44eac584c679972bc4349 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:04:56 +0800 Subject: [PATCH 28/31] chore: update pnpm lockfile --- pnpm-lock.yaml | 58 +++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c38ae30bd..4cdb1ccf39 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -275,7 +275,7 @@ importers: version: 3.7.0 '@rsbuild/plugin-babel': specifier: 1.1.0 - version: 1.1.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) + version: 1.1.0(@rsbuild/core@1.7.3) '@types/react': specifier: ^18.3.28 version: 18.3.28 @@ -1112,10 +1112,10 @@ importers: version: link:../../web-platform/web-elements '@rsbuild/plugin-less': specifier: 1.6.0 - version: 1.6.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) + version: 1.6.0(@rsbuild/core@1.7.3) '@rsbuild/plugin-sass': specifier: 1.5.0 - version: 1.5.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) + version: 1.5.0(@rsbuild/core@1.7.3) commander: specifier: ^13.1.0 version: 13.1.0 @@ -1130,7 +1130,7 @@ importers: version: 1.1.1 rsbuild-plugin-tailwindcss: specifier: 0.2.4 - version: 0.2.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(tailwindcss@4.2.1) + version: 0.2.4(@rsbuild/core@1.7.3)(tailwindcss@4.2.1) rslog: specifier: ^1.3.2 version: 1.3.2 @@ -1974,13 +1974,13 @@ importers: version: 7.33.4(@types/node@24.10.13) '@rsbuild/plugin-sass': specifier: 1.5.0 - version: 1.5.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) + version: 1.5.0(@rsbuild/core@1.7.3) '@rsbuild/plugin-type-check': specifier: 1.3.4 - version: 1.3.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3) + version: 1.3.4(@rsbuild/core@1.7.3)(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3) '@rsbuild/plugin-typed-css-modules': specifier: 1.2.2 - version: 1.2.2(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0)) + version: 1.2.2(@rsbuild/core@1.7.3) '@rspress/core': specifier: 2.0.3 version: 2.0.3(@types/react@19.2.14)(core-js@3.48.0) @@ -12092,13 +12092,13 @@ snapshots: optionalDependencies: core-js: 3.48.0 - '@rsbuild/plugin-babel@1.1.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))': + '@rsbuild/plugin-babel@1.1.0(@rsbuild/core@1.7.3)': dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) + '@rsbuild/core': 1.7.3 '@types/babel__core': 7.20.5 deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -12138,9 +12138,9 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 - '@rsbuild/plugin-less@1.6.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))': + '@rsbuild/plugin-less@1.6.0(@rsbuild/core@1.7.3)': dependencies: - '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) + '@rsbuild/core': 1.7.3 deepmerge: 4.3.1 reduce-configs: 1.1.1 @@ -12169,15 +12169,6 @@ snapshots: reduce-configs: 1.1.1 sass-embedded: 1.97.3 - '@rsbuild/plugin-sass@1.5.0(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))': - dependencies: - '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) - deepmerge: 4.3.1 - loader-utils: 2.0.4 - postcss: 8.5.6 - reduce-configs: 1.1.1 - sass-embedded: 1.97.3 - '@rsbuild/plugin-source-build@1.0.4(@rsbuild/core@1.7.3)': dependencies: fast-glob: 3.3.3 @@ -12186,6 +12177,19 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 + '@rsbuild/plugin-type-check@1.3.4(@rsbuild/core@1.7.3)(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3)': + dependencies: + deepmerge: 4.3.1 + json5: 2.2.3 + reduce-configs: 1.1.1 + ts-checker-rspack-plugin: 1.3.0(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3) + optionalDependencies: + '@rsbuild/core': 1.7.3 + transitivePeerDependencies: + - '@rspack/core' + - tslib + - typescript + '@rsbuild/plugin-type-check@1.3.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(@rspack/core@1.7.7(@swc/helpers@0.5.18))(tslib@2.8.1)(typescript@5.9.3)': dependencies: deepmerge: 4.3.1 @@ -12203,10 +12207,6 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.3 - '@rsbuild/plugin-typed-css-modules@1.2.2(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))': - optionalDependencies: - '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) - '@rsdoctor/client@1.2.3': {} '@rsdoctor/core@1.2.3(@rsbuild/core@1.7.3)(@rspack/core@1.7.7(@swc/helpers@0.5.18))(webpack@5.105.2)': @@ -18075,15 +18075,15 @@ snapshots: optionalDependencies: '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) - rsbuild-plugin-tailwindcss@0.2.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(tailwindcss@3.4.19): + rsbuild-plugin-tailwindcss@0.2.4(@rsbuild/core@1.7.3)(tailwindcss@4.2.1): dependencies: - tailwindcss: 3.4.19 + tailwindcss: 4.2.1 optionalDependencies: - '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) + '@rsbuild/core': 1.7.3 - rsbuild-plugin-tailwindcss@0.2.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(tailwindcss@4.2.1): + rsbuild-plugin-tailwindcss@0.2.4(@rsbuild/core@2.0.0-beta.3(core-js@3.48.0))(tailwindcss@3.4.19): dependencies: - tailwindcss: 4.2.1 + tailwindcss: 3.4.19 optionalDependencies: '@rsbuild/core': 2.0.0-beta.3(core-js@3.48.0) From 1d38cc86c3d833d2bcbd9586d18487233287e8ec Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:47:02 +0800 Subject: [PATCH 29/31] + report coverage --- .github/workflows/test.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 338b8487e3..9608227092 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -290,12 +290,7 @@ jobs: echo "Lynx Explorer is running." # 8. Run the tests - if ! pnpm --filter @lynx-js/kitten-lynx-test-infra run test; then - echo "Tests failed! Dumping ADB logcat..." - adb logcat -d > adb_logcat_dump.txt - cat adb_logcat_dump.txt | tail -n 1000 - exit 1 - fi + pnpm --filter @lynx-js/kitten-lynx-test-infra run test --coverage --reporter=github-actions --reporter=dot --reporter=junit --outputFile=test-report.junit.xml --coverage.reporter='json' --coverage.reporter='text' --testTimeout=50000 --no-cache --logHeapUsage --silent test-typos: runs-on: lynx-ubuntu-24.04-medium From c3b1fd708d295ca3ab4eeb0ef7e7297943339363 Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:51:51 +0800 Subject: [PATCH 30/31] feat: Improve `kitten-lynx` test stability by implementing local server for bundles, refining session matching logic, and adding resource cleanup. --- .github/workflows/test.yml | 7 +++ .../devtool-connector/src/index.ts | 6 ++ .../testing-library/kitten-lynx/README.md | 4 +- .../kitten-lynx/src/KittenLynxView.ts | 56 +++++++++++++------ .../testing-library/kitten-lynx/src/Lynx.ts | 5 ++ 5 files changed, 60 insertions(+), 18 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9608227092..0751487014 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -258,8 +258,15 @@ jobs: # 5. Wait for boot echo "Waiting for emulator to boot..." adb wait-for-device + BOOT_TIMEOUT=60 + BOOT_ELAPSED=0 while [ "$(adb shell getprop sys.boot_completed | tr -d '\r')" != "1" ]; do + if [ $BOOT_ELAPSED -ge $BOOT_TIMEOUT ]; then + echo "Error: Emulator failed to boot within ${BOOT_TIMEOUT} seconds!" + exit 1 + fi sleep 2 + BOOT_ELAPSED=$((BOOT_ELAPSED+2)) done echo "Emulator is ready." diff --git a/packages/mcp-servers/devtool-connector/src/index.ts b/packages/mcp-servers/devtool-connector/src/index.ts index 2dc102903e..18efd11156 100644 --- a/packages/mcp-servers/devtool-connector/src/index.ts +++ b/packages/mcp-servers/devtool-connector/src/index.ts @@ -119,6 +119,12 @@ export class Connector { .flatMap(({ value }) => value); } + async close(): Promise { + await Promise.allSettled( + this.#transports.map(t => t.close()), + ); + } + async listAvailableApps(deviceId: string): Promise { const transport = await this.#findTransportWithDeviceId(deviceId); diff --git a/packages/testing-library/kitten-lynx/README.md b/packages/testing-library/kitten-lynx/README.md index c4e4929b8b..a5f4694541 100644 --- a/packages/testing-library/kitten-lynx/README.md +++ b/packages/testing-library/kitten-lynx/README.md @@ -45,8 +45,8 @@ Here is the blueprint for a standard test written using `kitten-lynx` and `vites ```typescript import { expect, test, beforeAll, afterAll } from 'vitest'; -import { Lynx } from '@lynx-js/kitten-lynx'; -import type { KittenLynxView } from '@lynx-js/kitten-lynx'; +import { Lynx } from '@lynx-js/kitten-lynx-test-infra'; +import type { KittenLynxView } from '@lynx-js/kitten-lynx-test-infra'; let lynx: Lynx; let page: KittenLynxView; diff --git a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts index 0a1f6ff4cc..0c8bd11c8e 100644 --- a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts +++ b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts @@ -113,6 +113,18 @@ export class KittenLynxView { } } + let existingSessionIds = new Set(); + try { + const existing = await this._connector.sendListSessionMessage( + this._clientId, + ); + if (Array.isArray(existing)) { + existing.forEach(s => existingSessionIds.add(s.session_id)); + } + } catch (e) { + // ignore + } + console.log(`[goto] Sending App.openPage to URL: ${url}`); try { const msg = await this._connector.sendAppMessage( @@ -170,11 +182,25 @@ export class KittenLynxView { ); } - const matched = sessions.find( + const newSessionMatches = sessions.filter( s => - s.url === url || s.url === urlPath || url.endsWith(s.url) - || s.url.endsWith(urlPath), + !existingSessionIds.has(s.session_id) + && (s.url === url || s.url === urlPath || url.endsWith(s.url) + || s.url.endsWith(urlPath)), ); + let matched = newSessionMatches[0]; + + if (!matched) { + const suffixMatches = sessions.filter( + s => + s.url === url || s.url === urlPath || url.endsWith(s.url) + || s.url.endsWith(urlPath), + ); + if (suffixMatches.length === 1) { + matched = suffixMatches[0]; + } + } + if (matched) { console.log( `[goto] Found matched session after ${ @@ -258,20 +284,18 @@ export class KittenLynxView { * @param sessionId - The numeric Lynx devtool session ID to attach to, discovered via `sendListSessionMessage`. */ async onAttachedToTarget(sessionId: number) { - if (!this._channel) { - const channel = CDPChannel.from( - sessionId, - this._clientId, - this._connector, - ); + const channel = CDPChannel.from( + sessionId, + this._clientId, + this._connector, + ); - const response = await channel.send('DOM.getDocument', { - depth: -1, - }); - const root = response.root.children[0]!; - this._root = ElementNode.fromId(root.nodeId, this); - this._channel = channel; - } + const response = await channel.send('DOM.getDocument', { + depth: -1, + }); + const root = response.root.children[0]!; + this._channel = channel; + this._root = ElementNode.fromId(root.nodeId, this); } #contentToStringImpl(buffer: string[], node: NodeInfoInGetDocument) { diff --git a/packages/testing-library/kitten-lynx/src/Lynx.ts b/packages/testing-library/kitten-lynx/src/Lynx.ts index fd476259c7..d2279e31ea 100644 --- a/packages/testing-library/kitten-lynx/src/Lynx.ts +++ b/packages/testing-library/kitten-lynx/src/Lynx.ts @@ -201,6 +201,11 @@ export class Lynx { * @returns A Promise resolving when the cleanup operation is fully processed. */ async close(): Promise { + if (this._connector) { + await this._connector.close(); + } this._connector = null; + this._currentClient = null; + this._currentClientId = ''; } } From 1f6a24bfedda0be02c51f0dfa420e44131356e1f Mon Sep 17 00:00:00 2001 From: pupiltong <12288479+PupilTong@users.noreply.github.com> Date: Tue, 17 Mar 2026 19:18:52 +0800 Subject: [PATCH 31/31] feat: Implement `cmd package resolve-activity` for Android app launch, update TypeScript module resolution to `Bundler`, and remove immediate `DOM.getDocument` call on session attach. --- .../src/transport/android.ts | 74 +++++++++++-------- .../kitten-lynx/src/KittenLynxView.ts | 7 -- .../testing-library/kitten-lynx/tsconfig.json | 4 +- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/packages/mcp-servers/devtool-connector/src/transport/android.ts b/packages/mcp-servers/devtool-connector/src/transport/android.ts index f21f660b52..0b00f73d65 100644 --- a/packages/mcp-servers/devtool-connector/src/transport/android.ts +++ b/packages/mcp-servers/devtool-connector/src/transport/android.ts @@ -3,7 +3,8 @@ // LICENSE file in the root directory of this source tree. import type { SocketConnectOpts } from 'node:net'; -import { type Adb, AdbServerClient } from '@yume-chan/adb'; +import { AdbServerClient } from '@yume-chan/adb'; +import type { Adb } from '@yume-chan/adb'; import { AdbServerNodeTcpConnector } from '@yume-chan/adb-server-node-tcp'; import createDebug from 'debug'; @@ -14,7 +15,7 @@ import type { OpenAppOptions, Transport, TransportConnectOptions, -} from './transport.ts'; +} from './transport.js'; const debug = createDebug('devtool-mcp-server:connector:android'); @@ -159,6 +160,46 @@ export class AndroidTransport implements Transport { debug(`openApp clear data output ${output}`); } + // Attempt to use cmd package resolve-activity like appium-adb + try { + const resolveOutput = await adb.subprocess.noneProtocol.spawnWaitText([ + 'cmd', + 'package', + 'resolve-activity', + '--brief', + packageName, + ]); + const lines = resolveOutput.split('\n').map((line) => line.trim()); + const activityName = lines.find((line) => line.includes('/')); + + if ( + activityName + && activityName !== 'android/com.android.internal.app.ResolverActivity' + ) { + debug(`openApp am start: ${activityName}`); + const amOutput = await adb.subprocess.noneProtocol.spawnWaitText([ + 'am', + 'start', + '-a', + 'android.intent.action.MAIN', + '-c', + 'android.intent.category.LAUNCHER', + '-f', + '0x10200000', + '-n', + activityName, + ]); + debug(`openApp am start output: ${amOutput}`); + if (!(/^error:/im.exec(amOutput))) { + return; + } + } + } catch (e) { + debug(`openApp cmd package resolve-activity failed: %o`, e); + } + + // Fallback to monkey + debug('openApp trying fallback to monkey'); const output = await adb.subprocess.noneProtocol.spawnWaitText([ // adb shell monkey -p -c android.intent.category.LAUNCHER 1 'monkey', @@ -171,35 +212,8 @@ export class AndroidTransport implements Transport { debug(`openApp LAUNCHER output ${output}`); if (!output.includes('Events injected:')) { - debug('openApp monkey failed, trying fallback to am start'); - const dumpsysOutput = await adb.subprocess.noneProtocol.spawnWaitText([ - 'dumpsys', - 'package', - packageName, - ]); - const mainActionIndex = dumpsysOutput.indexOf( - 'android.intent.action.MAIN:', - ); - if (mainActionIndex !== -1) { - const slice = dumpsysOutput.slice( - mainActionIndex, - mainActionIndex + 200, - ); - const match = slice.match(/([a-zA-Z0-9._]+\/[a-zA-Z0-9._]+)/); - if (match?.[1]) { - debug(`openApp am start fallback: ${match[1]}`); - const amOutput = await adb.subprocess.noneProtocol.spawnWaitText([ - 'am', - 'start', - '-n', - match[1], - ]); - debug(`openApp am start output: ${amOutput}`); - return; - } - } throw new Error( - `Failed to open app ${packageName}: monkey aborted and fallback failed.`, + `Failed to open app ${packageName}: appium-adb style activation and monkey fallback both failed.`, ); } } diff --git a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts index 0c8bd11c8e..b0d16367c9 100644 --- a/packages/testing-library/kitten-lynx/src/KittenLynxView.ts +++ b/packages/testing-library/kitten-lynx/src/KittenLynxView.ts @@ -235,13 +235,6 @@ export class KittenLynxView { if (!this._channel) { throw new Error('Failed to attach to session for URL: ' + url); } - - // Refresh the DOM tree from the attached session - const response = await this._channel.send('DOM.getDocument', { - depth: -1, - }); - const root = response.root.children[0]!; - this._root = ElementNode.fromId(root.nodeId, this); } /** diff --git a/packages/testing-library/kitten-lynx/tsconfig.json b/packages/testing-library/kitten-lynx/tsconfig.json index 67ef1b624d..44d7dc5c8d 100644 --- a/packages/testing-library/kitten-lynx/tsconfig.json +++ b/packages/testing-library/kitten-lynx/tsconfig.json @@ -2,8 +2,8 @@ "compilerOptions": { "target": "ESNext", "lib": ["ESNext", "DOM"], - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "ESNext", + "moduleResolution": "Bundler", "declaration": true, "declarationMap": true, "sourceMap": true,