@geometra/mcp 1.59.1 → 1.60.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/dist/server.js +1 -75
- package/dist/session-state.js +2 -1
- package/dist/session.d.ts +1 -2
- package/dist/session.js +4 -25
- package/package.json +5 -3
- package/dist/__tests__/ats-integration.test.d.ts +0 -1
- package/dist/__tests__/ats-integration.test.js +0 -891
- package/dist/__tests__/connect-utils.test.d.ts +0 -1
- package/dist/__tests__/connect-utils.test.js +0 -255
- package/dist/__tests__/proxy-session-actions.test.d.ts +0 -1
- package/dist/__tests__/proxy-session-actions.test.js +0 -356
- package/dist/__tests__/proxy-session-recovery.test.d.ts +0 -1
- package/dist/__tests__/proxy-session-recovery.test.js +0 -262
- package/dist/__tests__/server-batch-results.test.d.ts +0 -1
- package/dist/__tests__/server-batch-results.test.js +0 -1777
- package/dist/__tests__/server-filters.test.d.ts +0 -1
- package/dist/__tests__/server-filters.test.js +0 -57
- package/dist/__tests__/server-session-resolution.test.d.ts +0 -1
- package/dist/__tests__/server-session-resolution.test.js +0 -88
- package/dist/__tests__/session-isolation.test.d.ts +0 -1
- package/dist/__tests__/session-isolation.test.js +0 -308
- package/dist/__tests__/session-model.test.d.ts +0 -1
- package/dist/__tests__/session-model.test.js +0 -815
- package/dist/__tests__/values-equivalent.test.d.ts +0 -1
- package/dist/__tests__/values-equivalent.test.js +0 -52
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,255 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { describe, expect, it } from 'vitest';
|
|
6
|
-
import { CONNECT_TARGET_EXACTLY_ONE_ERROR, formatConnectFailureMessage, isHttpUrl, normalizeConnectTarget, } from '../connect-utils.js';
|
|
7
|
-
import { formatProxyStartupFailure, parseProxyReadySignalLine, resolveProxyScriptPath, resolveProxyScriptPathWith, } from '../proxy-spawn.js';
|
|
8
|
-
describe('normalizeConnectTarget', () => {
|
|
9
|
-
it('accepts explicit pageUrl for http(s) pages', () => {
|
|
10
|
-
const result = normalizeConnectTarget({ pageUrl: 'https://example.com/jobs/123' });
|
|
11
|
-
expect(result).toEqual({
|
|
12
|
-
ok: true,
|
|
13
|
-
value: {
|
|
14
|
-
kind: 'proxy',
|
|
15
|
-
pageUrl: 'https://example.com/jobs/123',
|
|
16
|
-
autoCoercedFromUrl: false,
|
|
17
|
-
},
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
it('rejects non-http pageUrl protocols', () => {
|
|
21
|
-
const result = normalizeConnectTarget({ pageUrl: 'ws://127.0.0.1:3100' });
|
|
22
|
-
expect(result).toEqual({
|
|
23
|
-
ok: false,
|
|
24
|
-
error: 'pageUrl must use http:// or https:// (received ws:)',
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
it('auto-coerces http url input onto the proxy path', () => {
|
|
28
|
-
const result = normalizeConnectTarget({ url: 'https://jobs.example.com/apply' });
|
|
29
|
-
expect(result).toEqual({
|
|
30
|
-
ok: true,
|
|
31
|
-
value: {
|
|
32
|
-
kind: 'proxy',
|
|
33
|
-
pageUrl: 'https://jobs.example.com/apply',
|
|
34
|
-
autoCoercedFromUrl: true,
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
it('accepts ws url input for already-running peers', () => {
|
|
39
|
-
const result = normalizeConnectTarget({ url: 'ws://127.0.0.1:3100' });
|
|
40
|
-
expect(result).toEqual({
|
|
41
|
-
ok: true,
|
|
42
|
-
value: {
|
|
43
|
-
kind: 'ws',
|
|
44
|
-
wsUrl: 'ws://127.0.0.1:3100/',
|
|
45
|
-
autoCoercedFromUrl: false,
|
|
46
|
-
},
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
it('accepts wss url input for already-running peers (TLS WebSocket)', () => {
|
|
50
|
-
const result = normalizeConnectTarget({ url: 'wss://example.com/socket' });
|
|
51
|
-
expect(result).toEqual({
|
|
52
|
-
ok: true,
|
|
53
|
-
value: {
|
|
54
|
-
kind: 'ws',
|
|
55
|
-
wsUrl: 'wss://example.com/socket',
|
|
56
|
-
autoCoercedFromUrl: false,
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
it('rejects ambiguous and empty connect inputs', () => {
|
|
61
|
-
expect(normalizeConnectTarget({})).toEqual({
|
|
62
|
-
ok: false,
|
|
63
|
-
error: CONNECT_TARGET_EXACTLY_ONE_ERROR,
|
|
64
|
-
});
|
|
65
|
-
expect(normalizeConnectTarget({ url: 'ws://127.0.0.1:3100', pageUrl: 'https://example.com' })).toEqual({
|
|
66
|
-
ok: false,
|
|
67
|
-
error: CONNECT_TARGET_EXACTLY_ONE_ERROR,
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
it('rejects invalid pageUrl strings (URL parser failure)', () => {
|
|
71
|
-
const result = normalizeConnectTarget({ pageUrl: 'https://exam ple.com' });
|
|
72
|
-
expect(result.ok).toBe(false);
|
|
73
|
-
if (!result.ok) {
|
|
74
|
-
expect(result.error.startsWith('Invalid pageUrl:')).toBe(true);
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
it('rejects non-http(s) url when using pageUrl (explicit)', () => {
|
|
78
|
-
expect(normalizeConnectTarget({ pageUrl: 'file:///tmp/x.html' })).toEqual({
|
|
79
|
-
ok: false,
|
|
80
|
-
error: 'pageUrl must use http:// or https:// (received file:)',
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
it('rejects invalid url strings for the url field', () => {
|
|
84
|
-
const result = normalizeConnectTarget({ url: ':::not-a-url' });
|
|
85
|
-
expect(result.ok).toBe(false);
|
|
86
|
-
if (!result.ok) {
|
|
87
|
-
expect(result.error).toContain('Invalid url:');
|
|
88
|
-
expect(result.error).toContain('ws://');
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
it('rejects unsupported protocols on the url field (neither http(s) nor ws(s))', () => {
|
|
92
|
-
expect(normalizeConnectTarget({ url: 'ftp://files.example.com/pub' })).toEqual({
|
|
93
|
-
ok: false,
|
|
94
|
-
error: 'Unsupported url protocol ftp:. Use ws://... for an already-running Geometra server, or http:// / https:// for webpages.',
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
it('trims whitespace-only inputs to empty (same as omitting url/pageUrl)', () => {
|
|
98
|
-
expect(normalizeConnectTarget({ url: ' ', pageUrl: undefined })).toEqual({
|
|
99
|
-
ok: false,
|
|
100
|
-
error: CONNECT_TARGET_EXACTLY_ONE_ERROR,
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
it('trims whitespace-only pageUrl to empty (symmetric with url-only whitespace)', () => {
|
|
104
|
-
expect(normalizeConnectTarget({ pageUrl: ' ' })).toEqual({
|
|
105
|
-
ok: false,
|
|
106
|
-
error: CONNECT_TARGET_EXACTLY_ONE_ERROR,
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
describe('isHttpUrl', () => {
|
|
111
|
-
it('accepts http and https URLs', () => {
|
|
112
|
-
expect(isHttpUrl('https://example.com/path')).toBe(true);
|
|
113
|
-
expect(isHttpUrl('http://localhost:8080/')).toBe(true);
|
|
114
|
-
});
|
|
115
|
-
it('accepts IPv6 literal hosts (URL parser canonical form)', () => {
|
|
116
|
-
expect(isHttpUrl('https://[::1]:8443/')).toBe(true);
|
|
117
|
-
expect(isHttpUrl('http://[2001:db8::1]/')).toBe(true);
|
|
118
|
-
});
|
|
119
|
-
it('rejects ws(s), file, and other schemes', () => {
|
|
120
|
-
expect(isHttpUrl('ws://127.0.0.1:3100')).toBe(false);
|
|
121
|
-
expect(isHttpUrl('wss://example.com')).toBe(false);
|
|
122
|
-
expect(isHttpUrl('file:///tmp/x')).toBe(false);
|
|
123
|
-
});
|
|
124
|
-
it('rejects malformed strings', () => {
|
|
125
|
-
expect(isHttpUrl('not a url')).toBe(false);
|
|
126
|
-
expect(isHttpUrl('')).toBe(false);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
describe('formatConnectFailureMessage', () => {
|
|
130
|
-
it('adds a targeted hint when ws connect fails for a normal webpage flow', () => {
|
|
131
|
-
const message = formatConnectFailureMessage(new Error('WebSocket error connecting to ws://localhost:3100: connect ECONNREFUSED'), { kind: 'ws', wsUrl: 'ws://localhost:3100', autoCoercedFromUrl: false });
|
|
132
|
-
expect(message).toContain('ECONNREFUSED');
|
|
133
|
-
expect(message).toContain('pageUrl: "https://…"');
|
|
134
|
-
});
|
|
135
|
-
it('adds the same hint when ws connect fails with DNS resolution errors (wrong host or offline resolver)', () => {
|
|
136
|
-
const message = formatConnectFailureMessage(new Error('getaddrinfo ENOTFOUND bad.example.com'), { kind: 'ws', wsUrl: 'ws://bad.example.com:3100', autoCoercedFromUrl: false });
|
|
137
|
-
expect(message).toContain('ENOTFOUND');
|
|
138
|
-
expect(message).toContain('pageUrl:');
|
|
139
|
-
});
|
|
140
|
-
it('adds an install hint when the proxy package cannot be resolved', () => {
|
|
141
|
-
const message = formatConnectFailureMessage(new Error('Could not resolve @geometra/proxy from mcp'), { kind: 'proxy', pageUrl: 'https://example.com', autoCoercedFromUrl: false });
|
|
142
|
-
expect(message).toContain('Could not resolve @geometra/proxy');
|
|
143
|
-
expect(message).toContain('@geometra/proxy');
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
describe('proxy ready helpers', () => {
|
|
147
|
-
it('resolves the bundled proxy CLI entry in the source tree', () => {
|
|
148
|
-
const scriptPath = resolveProxyScriptPath();
|
|
149
|
-
expect(existsSync(scriptPath)).toBe(true);
|
|
150
|
-
expect(path.basename(scriptPath)).toBe('index.js');
|
|
151
|
-
expect(scriptPath.includes(`${path.sep}proxy${path.sep}`)).toBe(true);
|
|
152
|
-
});
|
|
153
|
-
it('resolves the bundled proxy CLI entry from a packaged dependency layout', () => {
|
|
154
|
-
const tempRoot = mkdtempSync(path.join(tmpdir(), 'geometra-proxy-resolve-'));
|
|
155
|
-
try {
|
|
156
|
-
const scopeDir = path.join(tempRoot, 'node_modules', '@geometra');
|
|
157
|
-
const packageDir = path.join(scopeDir, 'proxy');
|
|
158
|
-
const probePath = path.join(tempRoot, 'probe.cjs');
|
|
159
|
-
mkdirSync(scopeDir, { recursive: true });
|
|
160
|
-
mkdirSync(path.join(packageDir, 'src'), { recursive: true });
|
|
161
|
-
writeFileSync(path.join(packageDir, 'package.json'), JSON.stringify({
|
|
162
|
-
name: '@geometra/proxy',
|
|
163
|
-
version: '0.0.0-test',
|
|
164
|
-
type: 'module',
|
|
165
|
-
}));
|
|
166
|
-
writeFileSync(path.join(packageDir, 'tsconfig.build.json'), JSON.stringify({
|
|
167
|
-
extends: path.resolve(process.cwd(), 'tsconfig.base.json'),
|
|
168
|
-
compilerOptions: {
|
|
169
|
-
outDir: 'dist',
|
|
170
|
-
rootDir: 'src',
|
|
171
|
-
noEmit: false,
|
|
172
|
-
},
|
|
173
|
-
include: ['src'],
|
|
174
|
-
}));
|
|
175
|
-
writeFileSync(path.join(packageDir, 'src', 'index.ts'), 'console.log("proxy");\n');
|
|
176
|
-
writeFileSync(probePath, 'module.exports = {}');
|
|
177
|
-
const customRequire = createRequire(probePath);
|
|
178
|
-
const scriptPath = resolveProxyScriptPathWith(customRequire);
|
|
179
|
-
expect(existsSync(scriptPath)).toBe(true);
|
|
180
|
-
expect(path.basename(scriptPath)).toBe('index.js');
|
|
181
|
-
}
|
|
182
|
-
finally {
|
|
183
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
it('prefers the current workspace proxy dist over a bundled nested dependency in source checkouts', () => {
|
|
187
|
-
const tempRoot = mkdtempSync(path.join(tmpdir(), 'geometra-proxy-workspace-prefer-'));
|
|
188
|
-
try {
|
|
189
|
-
const workspaceDistDir = path.join(tempRoot, 'packages', 'proxy', 'dist');
|
|
190
|
-
const bundledProxyDir = path.join(tempRoot, 'mcp', 'node_modules', '@geometra', 'proxy');
|
|
191
|
-
const bundledDistDir = path.join(bundledProxyDir, 'dist');
|
|
192
|
-
const mcpDistDir = path.join(tempRoot, 'mcp', 'dist');
|
|
193
|
-
const probePath = path.join(mcpDistDir, 'proxy-spawn.cjs');
|
|
194
|
-
mkdirSync(workspaceDistDir, { recursive: true });
|
|
195
|
-
mkdirSync(bundledDistDir, { recursive: true });
|
|
196
|
-
mkdirSync(mcpDistDir, { recursive: true });
|
|
197
|
-
writeFileSync(path.join(workspaceDistDir, 'index.js'), 'export const source = "workspace";\n');
|
|
198
|
-
writeFileSync(path.join(bundledDistDir, 'index.js'), 'export const source = "bundled";\n');
|
|
199
|
-
writeFileSync(path.join(bundledProxyDir, 'package.json'), JSON.stringify({
|
|
200
|
-
name: '@geometra/proxy',
|
|
201
|
-
version: '0.0.0-test',
|
|
202
|
-
type: 'module',
|
|
203
|
-
}));
|
|
204
|
-
writeFileSync(probePath, 'module.exports = {};\n');
|
|
205
|
-
const customRequire = createRequire(probePath);
|
|
206
|
-
const scriptPath = resolveProxyScriptPathWith(customRequire, mcpDistDir);
|
|
207
|
-
expect(scriptPath).toBe(path.join(workspaceDistDir, 'index.js'));
|
|
208
|
-
}
|
|
209
|
-
finally {
|
|
210
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
it('falls back to the packaged sibling proxy dist when package exports are stale', () => {
|
|
214
|
-
const tempRoot = mkdtempSync(path.join(tmpdir(), 'geometra-proxy-stale-exports-'));
|
|
215
|
-
try {
|
|
216
|
-
const proxyDir = path.join(tempRoot, 'node_modules', '@geometra', 'proxy');
|
|
217
|
-
const mcpDistDir = path.join(tempRoot, 'node_modules', '@geometra', 'mcp', 'dist');
|
|
218
|
-
const proxyDistDir = path.join(proxyDir, 'dist');
|
|
219
|
-
const probePath = path.join(mcpDistDir, 'proxy-spawn.cjs');
|
|
220
|
-
mkdirSync(proxyDistDir, { recursive: true });
|
|
221
|
-
mkdirSync(mcpDistDir, { recursive: true });
|
|
222
|
-
writeFileSync(path.join(proxyDir, 'package.json'), JSON.stringify({
|
|
223
|
-
name: '@geometra/proxy',
|
|
224
|
-
type: 'module',
|
|
225
|
-
exports: {
|
|
226
|
-
'.': {
|
|
227
|
-
import: './dist/index.js',
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
}, null, 2));
|
|
231
|
-
writeFileSync(path.join(proxyDistDir, 'index.js'), 'export {};\n');
|
|
232
|
-
writeFileSync(probePath, 'module.exports = {};\n');
|
|
233
|
-
const customRequire = createRequire(probePath);
|
|
234
|
-
const scriptPath = resolveProxyScriptPathWith(customRequire, mcpDistDir);
|
|
235
|
-
expect(scriptPath).toBe(path.join(proxyDistDir, 'index.js'));
|
|
236
|
-
}
|
|
237
|
-
finally {
|
|
238
|
-
rmSync(tempRoot, { recursive: true, force: true });
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
it('parses structured proxy ready JSON', () => {
|
|
242
|
-
const wsUrl = parseProxyReadySignalLine('{"type":"geometra-proxy-ready","wsUrl":"ws://127.0.0.1:41237","pageUrl":"https://example.com"}');
|
|
243
|
-
expect(wsUrl).toBe('ws://127.0.0.1:41237');
|
|
244
|
-
});
|
|
245
|
-
it('still accepts legacy human-readable ready logs', () => {
|
|
246
|
-
const wsUrl = parseProxyReadySignalLine('[geometra-proxy] WebSocket listening on ws://127.0.0.1:3200');
|
|
247
|
-
expect(wsUrl).toBe('ws://127.0.0.1:3200');
|
|
248
|
-
});
|
|
249
|
-
it('adds install and port conflict hints to proxy startup failures', () => {
|
|
250
|
-
const chromiumHint = formatProxyStartupFailure("browserType.launch: Executable doesn't exist at /tmp/chromium", { pageUrl: 'https://example.com', port: 0 });
|
|
251
|
-
expect(chromiumHint).toContain('npx playwright install chromium');
|
|
252
|
-
const portHint = formatProxyStartupFailure('listen EADDRINUSE: address already in use 127.0.0.1:3337', { pageUrl: 'https://example.com', port: 3337 });
|
|
253
|
-
expect(portHint).toContain('Requested port 3337 is unavailable');
|
|
254
|
-
});
|
|
255
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
import { afterAll, describe, expect, it } from 'vitest';
|
|
2
|
-
import { WebSocketServer } from 'ws';
|
|
3
|
-
import { connect, disconnect, sendClick, sendFillFields, sendListboxPick, sendNavigate } from '../session.js';
|
|
4
|
-
describe('proxy-backed MCP actions', () => {
|
|
5
|
-
afterAll(() => {
|
|
6
|
-
disconnect();
|
|
7
|
-
});
|
|
8
|
-
it('waits for final listbox outcome instead of resolving on intermediate updates', async () => {
|
|
9
|
-
const wss = new WebSocketServer({ port: 0 });
|
|
10
|
-
wss.on('connection', ws => {
|
|
11
|
-
ws.on('message', raw => {
|
|
12
|
-
const msg = JSON.parse(String(raw));
|
|
13
|
-
if (msg.type === 'resize') {
|
|
14
|
-
ws.send(JSON.stringify({
|
|
15
|
-
type: 'frame',
|
|
16
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
17
|
-
tree: { kind: 'box', props: {}, semantic: { tag: 'body', role: 'group' }, children: [] },
|
|
18
|
-
}));
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
if (msg.type === 'listboxPick') {
|
|
22
|
-
ws.send(JSON.stringify({
|
|
23
|
-
type: 'frame',
|
|
24
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
25
|
-
tree: { kind: 'box', props: {}, semantic: { tag: 'body', role: 'group' }, children: [] },
|
|
26
|
-
}));
|
|
27
|
-
setTimeout(() => {
|
|
28
|
-
ws.send(JSON.stringify({
|
|
29
|
-
type: 'error',
|
|
30
|
-
requestId: msg.requestId,
|
|
31
|
-
message: 'listboxPick: no visible option matching \"Japan\"',
|
|
32
|
-
}));
|
|
33
|
-
}, 20);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
const port = await new Promise((resolve, reject) => {
|
|
38
|
-
wss.once('listening', () => {
|
|
39
|
-
const address = wss.address();
|
|
40
|
-
if (typeof address === 'object' && address)
|
|
41
|
-
resolve(address.port);
|
|
42
|
-
else
|
|
43
|
-
reject(new Error('Failed to resolve ephemeral WebSocket port'));
|
|
44
|
-
});
|
|
45
|
-
wss.once('error', reject);
|
|
46
|
-
});
|
|
47
|
-
try {
|
|
48
|
-
const session = await connect(`ws://127.0.0.1:${port}`);
|
|
49
|
-
await expect(sendListboxPick(session, 'Japan', {
|
|
50
|
-
fieldLabel: 'Country',
|
|
51
|
-
exact: true,
|
|
52
|
-
})).rejects.toThrow('listboxPick: no visible option matching "Japan"');
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
disconnect();
|
|
56
|
-
await new Promise((resolve, reject) => wss.close(err => (err ? reject(err) : resolve())));
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
it('falls back to the latest observed update when a legacy peer does not send request-scoped ack', async () => {
|
|
60
|
-
const wss = new WebSocketServer({ port: 0 });
|
|
61
|
-
wss.on('connection', ws => {
|
|
62
|
-
ws.on('message', raw => {
|
|
63
|
-
const msg = JSON.parse(String(raw));
|
|
64
|
-
if (msg.type === 'resize') {
|
|
65
|
-
ws.send(JSON.stringify({
|
|
66
|
-
type: 'frame',
|
|
67
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
68
|
-
tree: { kind: 'box', props: {}, semantic: { tag: 'body', role: 'group' }, children: [] },
|
|
69
|
-
}));
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
if (msg.type === 'event') {
|
|
73
|
-
ws.send(JSON.stringify({
|
|
74
|
-
type: 'frame',
|
|
75
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
76
|
-
tree: { kind: 'box', props: {}, semantic: { tag: 'body', role: 'group', ariaLabel: 'Updated' }, children: [] },
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
const port = await new Promise((resolve, reject) => {
|
|
82
|
-
wss.once('listening', () => {
|
|
83
|
-
const address = wss.address();
|
|
84
|
-
if (typeof address === 'object' && address)
|
|
85
|
-
resolve(address.port);
|
|
86
|
-
else
|
|
87
|
-
reject(new Error('Failed to resolve ephemeral WebSocket port'));
|
|
88
|
-
});
|
|
89
|
-
wss.once('error', reject);
|
|
90
|
-
});
|
|
91
|
-
try {
|
|
92
|
-
const session = await connect(`ws://127.0.0.1:${port}`);
|
|
93
|
-
await expect(sendClick(session, 5, 5, 60)).resolves.toMatchObject({ status: 'updated', timeoutMs: 60 });
|
|
94
|
-
}
|
|
95
|
-
finally {
|
|
96
|
-
disconnect();
|
|
97
|
-
await new Promise((resolve, reject) => wss.close(err => (err ? reject(err) : resolve())));
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
it('waits for the post-batch update before resolving fillFields acks', async () => {
|
|
101
|
-
const wss = new WebSocketServer({ port: 0 });
|
|
102
|
-
let seenMessage;
|
|
103
|
-
wss.on('connection', ws => {
|
|
104
|
-
ws.on('message', raw => {
|
|
105
|
-
const msg = JSON.parse(String(raw));
|
|
106
|
-
if (msg.type === 'resize') {
|
|
107
|
-
ws.send(JSON.stringify({
|
|
108
|
-
type: 'frame',
|
|
109
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
110
|
-
tree: { kind: 'box', props: {}, semantic: { tag: 'body', role: 'group' }, children: [] },
|
|
111
|
-
}));
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
if (msg.type === 'fillFields') {
|
|
115
|
-
seenMessage = msg;
|
|
116
|
-
ws.send(JSON.stringify({
|
|
117
|
-
type: 'frame',
|
|
118
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
119
|
-
tree: {
|
|
120
|
-
kind: 'box',
|
|
121
|
-
props: {},
|
|
122
|
-
semantic: { tag: 'body', role: 'group', ariaLabel: 'Filled' },
|
|
123
|
-
children: [],
|
|
124
|
-
},
|
|
125
|
-
}));
|
|
126
|
-
ws.send(JSON.stringify({
|
|
127
|
-
type: 'ack',
|
|
128
|
-
requestId: msg.requestId,
|
|
129
|
-
result: {
|
|
130
|
-
pageUrl: 'https://jobs.example.com/application',
|
|
131
|
-
invalidCount: 0,
|
|
132
|
-
alertCount: 0,
|
|
133
|
-
dialogCount: 0,
|
|
134
|
-
busyCount: 0,
|
|
135
|
-
},
|
|
136
|
-
}));
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
});
|
|
140
|
-
const port = await new Promise((resolve, reject) => {
|
|
141
|
-
wss.once('listening', () => {
|
|
142
|
-
const address = wss.address();
|
|
143
|
-
if (typeof address === 'object' && address)
|
|
144
|
-
resolve(address.port);
|
|
145
|
-
else
|
|
146
|
-
reject(new Error('Failed to resolve ephemeral WebSocket port'));
|
|
147
|
-
});
|
|
148
|
-
wss.once('error', reject);
|
|
149
|
-
});
|
|
150
|
-
try {
|
|
151
|
-
const session = await connect(`ws://127.0.0.1:${port}`);
|
|
152
|
-
await expect(sendFillFields(session, [
|
|
153
|
-
{ kind: 'text', fieldId: 'ff:0.0', fieldLabel: 'Full name', value: 'Taylor Applicant' },
|
|
154
|
-
{ kind: 'choice', fieldId: 'ff:0.1', fieldLabel: 'Country', value: 'Germany' },
|
|
155
|
-
], 80)).resolves.toMatchObject({
|
|
156
|
-
status: 'updated',
|
|
157
|
-
timeoutMs: 80,
|
|
158
|
-
result: {
|
|
159
|
-
pageUrl: 'https://jobs.example.com/application',
|
|
160
|
-
invalidCount: 0,
|
|
161
|
-
alertCount: 0,
|
|
162
|
-
},
|
|
163
|
-
});
|
|
164
|
-
expect(seenMessage).toMatchObject({
|
|
165
|
-
type: 'fillFields',
|
|
166
|
-
fields: [
|
|
167
|
-
{ kind: 'text', fieldId: 'ff:0.0', fieldLabel: 'Full name', value: 'Taylor Applicant' },
|
|
168
|
-
{ kind: 'choice', fieldId: 'ff:0.1', fieldLabel: 'Country', value: 'Germany' },
|
|
169
|
-
],
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
finally {
|
|
173
|
-
disconnect();
|
|
174
|
-
await new Promise((resolve, reject) => wss.close(err => (err ? reject(err) : resolve())));
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
it('ignores invalid patch paths instead of mutating ancestor layout nodes', async () => {
|
|
178
|
-
const wss = new WebSocketServer({ port: 0 });
|
|
179
|
-
wss.on('connection', ws => {
|
|
180
|
-
ws.on('message', raw => {
|
|
181
|
-
const msg = JSON.parse(String(raw));
|
|
182
|
-
if (msg.type === 'resize') {
|
|
183
|
-
ws.send(JSON.stringify({
|
|
184
|
-
type: 'frame',
|
|
185
|
-
layout: {
|
|
186
|
-
x: 0,
|
|
187
|
-
y: 0,
|
|
188
|
-
width: 200,
|
|
189
|
-
height: 100,
|
|
190
|
-
children: [{ x: 10, y: 20, width: 30, height: 40, children: [] }],
|
|
191
|
-
},
|
|
192
|
-
tree: {
|
|
193
|
-
kind: 'box',
|
|
194
|
-
props: {},
|
|
195
|
-
semantic: { tag: 'body', role: 'group' },
|
|
196
|
-
children: [{ kind: 'box', props: {}, semantic: { tag: 'div', role: 'group' }, children: [] }],
|
|
197
|
-
},
|
|
198
|
-
}));
|
|
199
|
-
setTimeout(() => {
|
|
200
|
-
ws.send(JSON.stringify({
|
|
201
|
-
type: 'patch',
|
|
202
|
-
patches: [{ path: [9], x: 999, y: 999 }],
|
|
203
|
-
}));
|
|
204
|
-
}, 10);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
});
|
|
208
|
-
const port = await new Promise((resolve, reject) => {
|
|
209
|
-
wss.once('listening', () => {
|
|
210
|
-
const address = wss.address();
|
|
211
|
-
if (typeof address === 'object' && address)
|
|
212
|
-
resolve(address.port);
|
|
213
|
-
else
|
|
214
|
-
reject(new Error('Failed to resolve ephemeral WebSocket port'));
|
|
215
|
-
});
|
|
216
|
-
wss.once('error', reject);
|
|
217
|
-
});
|
|
218
|
-
try {
|
|
219
|
-
const session = await connect(`ws://127.0.0.1:${port}`);
|
|
220
|
-
await new Promise(resolve => setTimeout(resolve, 30));
|
|
221
|
-
expect(session.layout).toMatchObject({
|
|
222
|
-
x: 0,
|
|
223
|
-
y: 0,
|
|
224
|
-
children: [{ x: 10, y: 20 }],
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
finally {
|
|
228
|
-
disconnect();
|
|
229
|
-
await new Promise((resolve, reject) => wss.close(err => (err ? reject(err) : resolve())));
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
it('supports in-session navigation and waits for the resulting frame', async () => {
|
|
233
|
-
const wss = new WebSocketServer({ port: 0 });
|
|
234
|
-
const received = [];
|
|
235
|
-
wss.on('connection', ws => {
|
|
236
|
-
ws.on('message', raw => {
|
|
237
|
-
const msg = JSON.parse(String(raw));
|
|
238
|
-
received.push(msg);
|
|
239
|
-
if (msg.type === 'resize') {
|
|
240
|
-
ws.send(JSON.stringify({
|
|
241
|
-
type: 'frame',
|
|
242
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
243
|
-
tree: { kind: 'box', props: {}, semantic: { tag: 'body', role: 'group' }, children: [] },
|
|
244
|
-
}));
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
if (msg.type === 'navigate') {
|
|
248
|
-
ws.send(JSON.stringify({
|
|
249
|
-
type: 'frame',
|
|
250
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
251
|
-
tree: {
|
|
252
|
-
kind: 'box',
|
|
253
|
-
props: {},
|
|
254
|
-
semantic: { tag: 'body', role: 'group' },
|
|
255
|
-
children: [],
|
|
256
|
-
},
|
|
257
|
-
}));
|
|
258
|
-
ws.send(JSON.stringify({
|
|
259
|
-
type: 'ack',
|
|
260
|
-
requestId: msg.requestId,
|
|
261
|
-
result: { pageUrl: msg.url },
|
|
262
|
-
}));
|
|
263
|
-
}
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
const port = await new Promise((resolve, reject) => {
|
|
267
|
-
wss.once('listening', () => {
|
|
268
|
-
const address = wss.address();
|
|
269
|
-
if (typeof address === 'object' && address)
|
|
270
|
-
resolve(address.port);
|
|
271
|
-
else
|
|
272
|
-
reject(new Error('Failed to resolve ephemeral WebSocket port'));
|
|
273
|
-
});
|
|
274
|
-
wss.once('error', reject);
|
|
275
|
-
});
|
|
276
|
-
try {
|
|
277
|
-
const session = await connect(`ws://127.0.0.1:${port}`);
|
|
278
|
-
await expect(sendNavigate(session, 'https://jobs.example.com/application', 80)).resolves.toMatchObject({
|
|
279
|
-
status: 'updated',
|
|
280
|
-
timeoutMs: 80,
|
|
281
|
-
result: { pageUrl: 'https://jobs.example.com/application' },
|
|
282
|
-
});
|
|
283
|
-
expect(received.some(message => message.type === 'navigate' && message.url === 'https://jobs.example.com/application')).toBe(true);
|
|
284
|
-
}
|
|
285
|
-
finally {
|
|
286
|
-
disconnect();
|
|
287
|
-
await new Promise((resolve, reject) => wss.close(err => (err ? reject(err) : resolve())));
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
it('reconnects once when an action is sent on a closed socket', async () => {
|
|
291
|
-
const wss = new WebSocketServer({ port: 0 });
|
|
292
|
-
let connectionCount = 0;
|
|
293
|
-
wss.on('connection', ws => {
|
|
294
|
-
connectionCount += 1;
|
|
295
|
-
ws.on('message', raw => {
|
|
296
|
-
const msg = JSON.parse(String(raw));
|
|
297
|
-
if (msg.type === 'resize') {
|
|
298
|
-
ws.send(JSON.stringify({
|
|
299
|
-
type: 'frame',
|
|
300
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
301
|
-
tree: { kind: 'box', props: {}, semantic: { tag: 'body', role: 'group' }, children: [] },
|
|
302
|
-
}));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
if (msg.type === 'event') {
|
|
306
|
-
ws.send(JSON.stringify({
|
|
307
|
-
type: 'frame',
|
|
308
|
-
layout: { x: 0, y: 0, width: 1024, height: 768, children: [] },
|
|
309
|
-
tree: {
|
|
310
|
-
kind: 'box',
|
|
311
|
-
props: {},
|
|
312
|
-
semantic: { tag: 'body', role: 'group', ariaLabel: 'Reconnected' },
|
|
313
|
-
children: [],
|
|
314
|
-
},
|
|
315
|
-
}));
|
|
316
|
-
ws.send(JSON.stringify({
|
|
317
|
-
type: 'ack',
|
|
318
|
-
requestId: msg.requestId,
|
|
319
|
-
result: { ok: true },
|
|
320
|
-
}));
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
});
|
|
324
|
-
const port = await new Promise((resolve, reject) => {
|
|
325
|
-
wss.once('listening', () => {
|
|
326
|
-
const address = wss.address();
|
|
327
|
-
if (typeof address === 'object' && address)
|
|
328
|
-
resolve(address.port);
|
|
329
|
-
else
|
|
330
|
-
reject(new Error('Failed to resolve ephemeral WebSocket port'));
|
|
331
|
-
});
|
|
332
|
-
wss.once('error', reject);
|
|
333
|
-
});
|
|
334
|
-
try {
|
|
335
|
-
const session = await connect(`ws://127.0.0.1:${port}`);
|
|
336
|
-
await new Promise(resolve => {
|
|
337
|
-
if (session.ws.readyState === session.ws.CLOSED) {
|
|
338
|
-
resolve();
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
session.ws.once('close', () => resolve());
|
|
342
|
-
session.ws.close();
|
|
343
|
-
});
|
|
344
|
-
await expect(sendClick(session, 5, 5, 150)).resolves.toMatchObject({
|
|
345
|
-
status: 'updated',
|
|
346
|
-
timeoutMs: 150,
|
|
347
|
-
result: { ok: true },
|
|
348
|
-
});
|
|
349
|
-
expect(connectionCount).toBeGreaterThanOrEqual(2);
|
|
350
|
-
}
|
|
351
|
-
finally {
|
|
352
|
-
disconnect();
|
|
353
|
-
await new Promise((resolve, reject) => wss.close(err => (err ? reject(err) : resolve())));
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|