@sveltium/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -0
- package/package.json +50 -0
- package/scripts/postinstall.js +96 -0
- package/src/client/dom/console-capture.js +143 -0
- package/src/client/dom/element-finder.js +59 -0
- package/src/client/dom/event-dispatcher.js +180 -0
- package/src/client/dom/snapshot-builder.js +229 -0
- package/src/client/index.js +232 -0
- package/src/client/tool-executor.js +630 -0
- package/src/server/index.js +115 -0
- package/src/server/mcp-handler.js +722 -0
- package/src/server/ws-bridge.js +212 -0
- package/types/index.d.ts +58 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# nwjs-mcp
|
|
2
|
+
|
|
3
|
+
MCP (Model Context Protocol) server for NW.js application testing and automation.
|
|
4
|
+
|
|
5
|
+
This package enables AI assistants (like Claude Code) to interact with and test NW.js applications, addressing the limitation that browser automation tools like Playwright don't work with NW.js.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install nwjs-mcp
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
Add to your NW.js app's `node-main` script:
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
// main.js (node-main in package.json)
|
|
19
|
+
if (process.argv.includes('--mcp')) {
|
|
20
|
+
require('nwjs-mcp').startServer({
|
|
21
|
+
mode: 'stdio',
|
|
22
|
+
native: true
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Or in your `package.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"main": "index.html",
|
|
32
|
+
"node-main": "mcp-bootstrap.js"
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Claude Code Integration
|
|
37
|
+
|
|
38
|
+
Add to your `.claude.json` or Claude Code settings:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"nwjs": {
|
|
44
|
+
"command": "path/to/nw.exe",
|
|
45
|
+
"args": ["path/to/your-app", "--mcp"],
|
|
46
|
+
"env": {
|
|
47
|
+
"NWJS_MCP_MODE": "stdio"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Available Tools
|
|
55
|
+
|
|
56
|
+
| Tool | Description |
|
|
57
|
+
|------|-------------|
|
|
58
|
+
| `browser_snapshot` | Get accessibility tree of the page with element refs |
|
|
59
|
+
| `browser_take_screenshot` | Capture page screenshot |
|
|
60
|
+
| `browser_click` | Click an element by ref |
|
|
61
|
+
| `browser_type` | Type text into an element |
|
|
62
|
+
| `browser_press_key` | Press a keyboard key |
|
|
63
|
+
| `browser_evaluate` | Execute JavaScript on the page |
|
|
64
|
+
| `browser_navigate` | Navigate to a URL |
|
|
65
|
+
| `browser_console_messages` | Get console log history |
|
|
66
|
+
| `browser_resize` | Resize the window |
|
|
67
|
+
| `browser_wait_for` | Wait for text or time |
|
|
68
|
+
| `browser_fill_form` | Fill multiple form fields |
|
|
69
|
+
|
|
70
|
+
## API
|
|
71
|
+
|
|
72
|
+
### startServer(options)
|
|
73
|
+
|
|
74
|
+
Start the MCP server.
|
|
75
|
+
|
|
76
|
+
```javascript
|
|
77
|
+
var server = require('nwjs-mcp').startServer({
|
|
78
|
+
mode: 'stdio', // 'stdio' or 'http'
|
|
79
|
+
port: 3000, // HTTP port (only for http mode)
|
|
80
|
+
native: true // Try to load native addon
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### createServer(options)
|
|
85
|
+
|
|
86
|
+
Create server instance without starting.
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
var server = require('nwjs-mcp').createServer({ mode: 'http' })
|
|
90
|
+
// Start later
|
|
91
|
+
server.start()
|
|
92
|
+
// Stop when done
|
|
93
|
+
server.stop()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Server Modes
|
|
97
|
+
|
|
98
|
+
### stdio Mode (Recommended)
|
|
99
|
+
|
|
100
|
+
Uses stdin/stdout for communication. Best for integration with Claude Code.
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
startServer({ mode: 'stdio' })
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### HTTP Mode
|
|
107
|
+
|
|
108
|
+
Runs an HTTP server for network access.
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
startServer({ mode: 'http', port: 3000 })
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Native Addon (Optional)
|
|
115
|
+
|
|
116
|
+
For enhanced features like system-level input (useful for testing native dialogs), install the native addon:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npm install @aspect/nwjs-addons
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Native features:
|
|
123
|
+
- System-level mouse clicks (for native dialogs)
|
|
124
|
+
- System-level keyboard input
|
|
125
|
+
- Window management (focus, minimize)
|
|
126
|
+
- Screen capture
|
|
127
|
+
|
|
128
|
+
The server gracefully falls back to JS-only mode if native addon is unavailable.
|
|
129
|
+
|
|
130
|
+
## Example Usage with Claude Code
|
|
131
|
+
|
|
132
|
+
Once configured, you can ask Claude Code to:
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
"Test my NW.js app - click the login button and verify the form appears"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Claude Code will:
|
|
139
|
+
1. Use `browser_snapshot` to see the page structure
|
|
140
|
+
2. Use `browser_click` to click the login button
|
|
141
|
+
3. Use `browser_snapshot` again to verify the form
|
|
142
|
+
4. Report the results
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sveltium/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP server for NW.js application testing and automation",
|
|
5
|
+
"main": "src/client/index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"nwjs-mcp": "./src/server/index.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./src/client/index.js",
|
|
12
|
+
"./client": "./src/client/index.js",
|
|
13
|
+
"./server": "./src/server/index.js"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src/**/*.js",
|
|
17
|
+
"scripts/**/*.js",
|
|
18
|
+
"types/**/*.d.ts"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node src/server/index.js",
|
|
22
|
+
"postinstall": "node scripts/postinstall.js",
|
|
23
|
+
"test": "echo \"No tests yet\""
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"nwjs",
|
|
27
|
+
"mcp",
|
|
28
|
+
"model-context-protocol",
|
|
29
|
+
"testing",
|
|
30
|
+
"automation",
|
|
31
|
+
"claude"
|
|
32
|
+
],
|
|
33
|
+
"author": {
|
|
34
|
+
"name": "Oosuke Ren",
|
|
35
|
+
"email": "oosukeren@gmail.com",
|
|
36
|
+
"url": "https://github.com/OosukeRen"
|
|
37
|
+
},
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"ws": "^8.0.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"nw": "*"
|
|
44
|
+
},
|
|
45
|
+
"peerDependenciesMeta": {
|
|
46
|
+
"nw": {
|
|
47
|
+
"optional": true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Postinstall script that configures .mcp.json for Claude Code integration
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
var fs = require('fs')
|
|
9
|
+
var path = require('path')
|
|
10
|
+
|
|
11
|
+
// Find the project root - use npm's INIT_CWD which is where npm was run from
|
|
12
|
+
function findProjectRoot() {
|
|
13
|
+
// INIT_CWD is set by npm to the directory where `npm install` was run
|
|
14
|
+
if (process.env.INIT_CWD) {
|
|
15
|
+
return process.env.INIT_CWD
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Fallback: use current working directory
|
|
19
|
+
return process.cwd()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Get the path to the main server index.js in node_modules
|
|
23
|
+
function getServerPath(projectRoot) {
|
|
24
|
+
// Check if we're installed in node_modules of the project (with scope)
|
|
25
|
+
var nodeModulesPath = path.join(projectRoot, 'node_modules', '@sveltium', 'nwjs-mcp', 'src', 'server', 'index.js')
|
|
26
|
+
|
|
27
|
+
// Also check without scope
|
|
28
|
+
var nodeModulesPathNoScope = path.join(projectRoot, 'node_modules', 'nwjs-mcp', 'src', 'server', 'index.js')
|
|
29
|
+
|
|
30
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
31
|
+
return nodeModulesPath
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (fs.existsSync(nodeModulesPathNoScope)) {
|
|
35
|
+
return nodeModulesPathNoScope
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Fallback: use relative to this script (for dev mode)
|
|
39
|
+
var localPath = path.join(__dirname, '..', 'src', 'server', 'index.js')
|
|
40
|
+
return path.resolve(localPath)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function main() {
|
|
44
|
+
// Skip for global installs
|
|
45
|
+
if (process.env.npm_config_global === 'true') {
|
|
46
|
+
console.log('[nwjs-mcp] Global install detected. Run "nwjs-mcp init" in your project to configure .mcp.json')
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
var projectRoot = findProjectRoot()
|
|
51
|
+
if (!projectRoot) {
|
|
52
|
+
console.log('[nwjs-mcp] Could not find project root, skipping .mcp.json configuration')
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
var mcpJsonPath = path.join(projectRoot, '.mcp.json')
|
|
57
|
+
var serverPath = getServerPath(projectRoot)
|
|
58
|
+
|
|
59
|
+
var mcpConfig = {}
|
|
60
|
+
|
|
61
|
+
// Read existing .mcp.json if it exists
|
|
62
|
+
if (fs.existsSync(mcpJsonPath)) {
|
|
63
|
+
try {
|
|
64
|
+
mcpConfig = JSON.parse(fs.readFileSync(mcpJsonPath, 'utf8'))
|
|
65
|
+
} catch (e) {
|
|
66
|
+
console.log('[nwjs-mcp] Could not parse existing .mcp.json, creating new one')
|
|
67
|
+
mcpConfig = {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Ensure mcpServers object exists
|
|
72
|
+
if (!mcpConfig.mcpServers) {
|
|
73
|
+
mcpConfig.mcpServers = {}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Add or update nwjs entry
|
|
77
|
+
mcpConfig.mcpServers.nwjs = {
|
|
78
|
+
command: 'node',
|
|
79
|
+
args: [serverPath]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Write .mcp.json
|
|
83
|
+
try {
|
|
84
|
+
fs.writeFileSync(mcpJsonPath, JSON.stringify(mcpConfig, null, 2) + '\n')
|
|
85
|
+
console.log('[nwjs-mcp] Configured .mcp.json at:', mcpJsonPath)
|
|
86
|
+
console.log('[nwjs-mcp] Server path:', serverPath)
|
|
87
|
+
console.log('[nwjs-mcp] To use nwjs_start_app, either:')
|
|
88
|
+
console.log(' - Install "nw" package: npm install nw')
|
|
89
|
+
console.log(' - Or set NWJS_PATH in .env file')
|
|
90
|
+
console.log(' - Or add NWJS_PATH env to .mcp.json')
|
|
91
|
+
} catch (e) {
|
|
92
|
+
console.log('[nwjs-mcp] Could not write .mcp.json:', e.message)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
main()
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Console Capture
|
|
5
|
+
*
|
|
6
|
+
* Captures console messages for retrieval
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function ConsoleCapture(win) {
|
|
10
|
+
this.win = win
|
|
11
|
+
this.messages = []
|
|
12
|
+
this.maxMessages = 1000
|
|
13
|
+
this.originalConsole = {}
|
|
14
|
+
this.started = false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Start capturing console messages
|
|
19
|
+
*/
|
|
20
|
+
ConsoleCapture.prototype.start = function() {
|
|
21
|
+
if (this.started) {
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var self = this
|
|
26
|
+
var console = this.win.console
|
|
27
|
+
|
|
28
|
+
// Store original methods
|
|
29
|
+
this.originalConsole.log = console.log
|
|
30
|
+
this.originalConsole.info = console.info
|
|
31
|
+
this.originalConsole.warn = console.warn
|
|
32
|
+
this.originalConsole.error = console.error
|
|
33
|
+
this.originalConsole.debug = console.debug
|
|
34
|
+
|
|
35
|
+
// Override console methods
|
|
36
|
+
console.log = function() {
|
|
37
|
+
self._capture('info', arguments)
|
|
38
|
+
self.originalConsole.log.apply(console, arguments)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.info = function() {
|
|
42
|
+
self._capture('info', arguments)
|
|
43
|
+
self.originalConsole.info.apply(console, arguments)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
console.warn = function() {
|
|
47
|
+
self._capture('warning', arguments)
|
|
48
|
+
self.originalConsole.warn.apply(console, arguments)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.error = function() {
|
|
52
|
+
self._capture('error', arguments)
|
|
53
|
+
self.originalConsole.error.apply(console, arguments)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.debug = function() {
|
|
57
|
+
self._capture('debug', arguments)
|
|
58
|
+
self.originalConsole.debug.apply(console, arguments)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.started = true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Stop capturing console messages
|
|
66
|
+
*/
|
|
67
|
+
ConsoleCapture.prototype.stop = function() {
|
|
68
|
+
if (!this.started) {
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
var console = this.win.console
|
|
73
|
+
|
|
74
|
+
// Restore original methods
|
|
75
|
+
console.log = this.originalConsole.log
|
|
76
|
+
console.info = this.originalConsole.info
|
|
77
|
+
console.warn = this.originalConsole.warn
|
|
78
|
+
console.error = this.originalConsole.error
|
|
79
|
+
console.debug = this.originalConsole.debug
|
|
80
|
+
|
|
81
|
+
this.started = false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Capture a console message
|
|
86
|
+
*/
|
|
87
|
+
ConsoleCapture.prototype._capture = function(level, args) {
|
|
88
|
+
var message = ''
|
|
89
|
+
|
|
90
|
+
for (var i = 0; i < args.length; i++) {
|
|
91
|
+
if (i > 0) message += ' '
|
|
92
|
+
|
|
93
|
+
var arg = args[i]
|
|
94
|
+
if (typeof arg === 'object') {
|
|
95
|
+
try {
|
|
96
|
+
message += JSON.stringify(arg)
|
|
97
|
+
} catch (e) {
|
|
98
|
+
message += String(arg)
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
message += String(arg)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.messages.push({
|
|
106
|
+
level: level,
|
|
107
|
+
message: message,
|
|
108
|
+
timestamp: Date.now()
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Trim old messages
|
|
112
|
+
if (this.messages.length > this.maxMessages) {
|
|
113
|
+
this.messages = this.messages.slice(-this.maxMessages)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get messages at or above a given level
|
|
119
|
+
*/
|
|
120
|
+
ConsoleCapture.prototype.getMessages = function(minLevel) {
|
|
121
|
+
var levels = ['debug', 'info', 'warning', 'error']
|
|
122
|
+
var minIndex = levels.indexOf(minLevel || 'info')
|
|
123
|
+
|
|
124
|
+
var result = []
|
|
125
|
+
for (var i = 0; i < this.messages.length; i++) {
|
|
126
|
+
var msg = this.messages[i]
|
|
127
|
+
var msgIndex = levels.indexOf(msg.level)
|
|
128
|
+
if (msgIndex >= minIndex) {
|
|
129
|
+
result.push('[' + msg.level.toUpperCase() + '] ' + msg.message)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Clear all captured messages
|
|
138
|
+
*/
|
|
139
|
+
ConsoleCapture.prototype.clear = function() {
|
|
140
|
+
this.messages = []
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = ConsoleCapture
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Element Finder
|
|
5
|
+
*
|
|
6
|
+
* Finds elements by ref (populated by SnapshotBuilder)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Global ref map (shared with SnapshotBuilder instances)
|
|
10
|
+
var _globalRefMap = {}
|
|
11
|
+
|
|
12
|
+
function ElementFinder(win) {
|
|
13
|
+
this.win = win
|
|
14
|
+
this.doc = win.document
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Set the global ref map (called by SnapshotBuilder after building)
|
|
19
|
+
*/
|
|
20
|
+
ElementFinder.setRefMap = function(refMap) {
|
|
21
|
+
_globalRefMap = refMap
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find element by ref
|
|
26
|
+
*/
|
|
27
|
+
ElementFinder.prototype.findByRef = function(ref) {
|
|
28
|
+
if (ref === 'page') {
|
|
29
|
+
return this.doc.body
|
|
30
|
+
}
|
|
31
|
+
return _globalRefMap[ref] || null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get bounding box of element
|
|
36
|
+
*/
|
|
37
|
+
ElementFinder.prototype.getBounds = function(element) {
|
|
38
|
+
var rect = element.getBoundingClientRect()
|
|
39
|
+
return {
|
|
40
|
+
x: rect.left,
|
|
41
|
+
y: rect.top,
|
|
42
|
+
width: rect.width,
|
|
43
|
+
height: rect.height
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if element is visible
|
|
49
|
+
*/
|
|
50
|
+
ElementFinder.prototype.isVisible = function(element) {
|
|
51
|
+
var style = this.win.getComputedStyle(element)
|
|
52
|
+
if (style.display === 'none' || style.visibility === 'hidden') {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
var rect = element.getBoundingClientRect()
|
|
56
|
+
return rect.width > 0 && rect.height > 0
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = ElementFinder
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Event Dispatcher
|
|
5
|
+
*
|
|
6
|
+
* Dispatches DOM events to elements
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function EventDispatcher(win) {
|
|
10
|
+
this.win = win
|
|
11
|
+
this.doc = win.document
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Click an element
|
|
16
|
+
*/
|
|
17
|
+
EventDispatcher.prototype.click = function(element, button, doubleClick) {
|
|
18
|
+
var buttonCode = 0
|
|
19
|
+
if (button === 'right') buttonCode = 2
|
|
20
|
+
if (button === 'middle') buttonCode = 1
|
|
21
|
+
|
|
22
|
+
// Focus the element if focusable
|
|
23
|
+
if (element.focus) {
|
|
24
|
+
element.focus()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Get element center
|
|
28
|
+
var rect = element.getBoundingClientRect()
|
|
29
|
+
var x = rect.left + rect.width / 2
|
|
30
|
+
var y = rect.top + rect.height / 2
|
|
31
|
+
|
|
32
|
+
// Dispatch mouse events
|
|
33
|
+
this._dispatchMouse(element, 'mousedown', x, y, buttonCode)
|
|
34
|
+
this._dispatchMouse(element, 'mouseup', x, y, buttonCode)
|
|
35
|
+
this._dispatchMouse(element, 'click', x, y, buttonCode)
|
|
36
|
+
|
|
37
|
+
if (doubleClick) {
|
|
38
|
+
this._dispatchMouse(element, 'mousedown', x, y, buttonCode)
|
|
39
|
+
this._dispatchMouse(element, 'mouseup', x, y, buttonCode)
|
|
40
|
+
this._dispatchMouse(element, 'click', x, y, buttonCode)
|
|
41
|
+
this._dispatchMouse(element, 'dblclick', x, y, buttonCode)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Type text into an element
|
|
47
|
+
*/
|
|
48
|
+
EventDispatcher.prototype.type = function(element, text, slowly, submit) {
|
|
49
|
+
// Focus the element
|
|
50
|
+
if (element.focus) {
|
|
51
|
+
element.focus()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (slowly) {
|
|
55
|
+
// Type character by character
|
|
56
|
+
for (var i = 0; i < text.length; i++) {
|
|
57
|
+
var char = text[i]
|
|
58
|
+
this._dispatchKey(element, 'keydown', char)
|
|
59
|
+
this._dispatchKey(element, 'keypress', char)
|
|
60
|
+
|
|
61
|
+
// Update value
|
|
62
|
+
if (element.value !== undefined) {
|
|
63
|
+
element.value += char
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this._dispatchKey(element, 'keyup', char)
|
|
67
|
+
this.dispatchInput(element)
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// Set value directly
|
|
71
|
+
if (element.value !== undefined) {
|
|
72
|
+
element.value = text
|
|
73
|
+
} else if (element.contentEditable === 'true') {
|
|
74
|
+
element.textContent = text
|
|
75
|
+
}
|
|
76
|
+
this.dispatchInput(element)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (submit) {
|
|
80
|
+
this.pressKey('Enter', element)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Press a key
|
|
86
|
+
*/
|
|
87
|
+
EventDispatcher.prototype.pressKey = function(key, target) {
|
|
88
|
+
target = target || this.doc.activeElement || this.doc.body
|
|
89
|
+
|
|
90
|
+
this._dispatchKey(target, 'keydown', key)
|
|
91
|
+
this._dispatchKey(target, 'keypress', key)
|
|
92
|
+
this._dispatchKey(target, 'keyup', key)
|
|
93
|
+
|
|
94
|
+
// Handle Enter key submitting forms
|
|
95
|
+
if (key === 'Enter') {
|
|
96
|
+
var form = target.form || target.closest('form')
|
|
97
|
+
if (form) {
|
|
98
|
+
var submitEvent = new this.win.Event('submit', {
|
|
99
|
+
bubbles: true,
|
|
100
|
+
cancelable: true
|
|
101
|
+
})
|
|
102
|
+
form.dispatchEvent(submitEvent)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Dispatch input event
|
|
109
|
+
*/
|
|
110
|
+
EventDispatcher.prototype.dispatchInput = function(element) {
|
|
111
|
+
var inputEvent = new this.win.Event('input', {
|
|
112
|
+
bubbles: true,
|
|
113
|
+
cancelable: true
|
|
114
|
+
})
|
|
115
|
+
element.dispatchEvent(inputEvent)
|
|
116
|
+
|
|
117
|
+
var changeEvent = new this.win.Event('change', {
|
|
118
|
+
bubbles: true,
|
|
119
|
+
cancelable: true
|
|
120
|
+
})
|
|
121
|
+
element.dispatchEvent(changeEvent)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Dispatch mouse event
|
|
126
|
+
*/
|
|
127
|
+
EventDispatcher.prototype._dispatchMouse = function(element, type, x, y, button) {
|
|
128
|
+
var event = new this.win.MouseEvent(type, {
|
|
129
|
+
view: this.win,
|
|
130
|
+
bubbles: true,
|
|
131
|
+
cancelable: true,
|
|
132
|
+
clientX: x,
|
|
133
|
+
clientY: y,
|
|
134
|
+
button: button
|
|
135
|
+
})
|
|
136
|
+
element.dispatchEvent(event)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Dispatch keyboard event
|
|
141
|
+
*/
|
|
142
|
+
EventDispatcher.prototype._dispatchKey = function(element, type, key) {
|
|
143
|
+
var keyCode = key.length === 1 ? key.charCodeAt(0) : this._getKeyCode(key)
|
|
144
|
+
|
|
145
|
+
var event = new this.win.KeyboardEvent(type, {
|
|
146
|
+
view: this.win,
|
|
147
|
+
bubbles: true,
|
|
148
|
+
cancelable: true,
|
|
149
|
+
key: key,
|
|
150
|
+
code: key.length === 1 ? 'Key' + key.toUpperCase() : key,
|
|
151
|
+
keyCode: keyCode,
|
|
152
|
+
which: keyCode
|
|
153
|
+
})
|
|
154
|
+
element.dispatchEvent(event)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get key code for special keys
|
|
159
|
+
*/
|
|
160
|
+
EventDispatcher.prototype._getKeyCode = function(key) {
|
|
161
|
+
var codes = {
|
|
162
|
+
'Enter': 13,
|
|
163
|
+
'Tab': 9,
|
|
164
|
+
'Escape': 27,
|
|
165
|
+
'Backspace': 8,
|
|
166
|
+
'Delete': 46,
|
|
167
|
+
'ArrowUp': 38,
|
|
168
|
+
'ArrowDown': 40,
|
|
169
|
+
'ArrowLeft': 37,
|
|
170
|
+
'ArrowRight': 39,
|
|
171
|
+
'Home': 36,
|
|
172
|
+
'End': 35,
|
|
173
|
+
'PageUp': 33,
|
|
174
|
+
'PageDown': 34,
|
|
175
|
+
'Space': 32
|
|
176
|
+
}
|
|
177
|
+
return codes[key] || 0
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = EventDispatcher
|