theus-cli-linux-arm64 1.2.0 → 1.2.2

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.
@@ -0,0 +1,321 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ // Efecto lateral: cdp-core.js cuelga TheusCdpCore de globalThis (mismo
3
+ // mecanismo que usa el service worker vía importScripts).
4
+ import './cdp-core.js'
5
+
6
+ type EntradaConsola = { ts: number; nivel: string; texto: string; url?: string }
7
+ type EntradaRed = {
8
+ id: string
9
+ metodo: string
10
+ url: string
11
+ tipo: string
12
+ estado: number | null
13
+ mime: string | null
14
+ tam: number | null
15
+ inicioMs: number
16
+ duracionMs: number | null
17
+ error: string | null
18
+ }
19
+ type EstadoRed = { entradas: EntradaRed[]; porId: Map<string, EntradaRed> }
20
+ type DefTecla = {
21
+ modifiers: number
22
+ key: string
23
+ code: string
24
+ keyCode: number
25
+ text?: string
26
+ }
27
+ type ResumenAX = {
28
+ elementos: Array<{
29
+ ref: string
30
+ backendDOMNodeId: number
31
+ rol: string
32
+ nombre: string
33
+ valor?: string
34
+ }>
35
+ truncado: boolean
36
+ totalCandidatos: number
37
+ }
38
+
39
+ type TheusCdpCore = {
40
+ pushCircular<T>(buffer: T[], item: T, max: number): T | null
41
+ formatearEventoConsola(
42
+ method: string,
43
+ params: Record<string, unknown>,
44
+ ahoraMs?: number,
45
+ ): EntradaConsola | null
46
+ crearEstadoRed(): EstadoRed
47
+ plegarEventoRed(
48
+ estado: EstadoRed,
49
+ method: string,
50
+ params: Record<string, unknown>,
51
+ max: number,
52
+ ahoraMs?: number,
53
+ ): boolean
54
+ filtrarEntradas<T>(
55
+ entradas: T[],
56
+ opciones: {
57
+ pattern?: string
58
+ limit?: number
59
+ onlyErrors?: boolean
60
+ campoTexto: string
61
+ },
62
+ ): T[]
63
+ parsearCombinacionTeclas(combo: string): DefTecla | null
64
+ resumirArbolAX(
65
+ nodos: unknown[],
66
+ filtro: string | undefined,
67
+ maxElementos: number,
68
+ ): ResumenAX
69
+ planRecompresionCaptura(
70
+ base64Length: number,
71
+ anchoPx: number,
72
+ opciones?: { maxAncho?: number; maxBase64?: number },
73
+ ): { reprocesar: boolean; escala?: number; calidad?: number }
74
+ horaLocal(ms: number): string
75
+ }
76
+
77
+ const Core = (globalThis as unknown as { TheusCdpCore: TheusCdpCore }).TheusCdpCore
78
+
79
+ describe('buffers circulares', () => {
80
+ test('expulsa el más antiguo al desbordar y lo devuelve', () => {
81
+ const buffer: number[] = []
82
+ expect(Core.pushCircular(buffer, 1, 2)).toBeNull()
83
+ expect(Core.pushCircular(buffer, 2, 2)).toBeNull()
84
+ expect(Core.pushCircular(buffer, 3, 2)).toBe(1)
85
+ expect(buffer).toEqual([2, 3])
86
+ })
87
+ })
88
+
89
+ describe('formatearEventoConsola', () => {
90
+ test('consoleAPICalled une los argumentos', () => {
91
+ const entrada = Core.formatearEventoConsola(
92
+ 'Runtime.consoleAPICalled',
93
+ {
94
+ type: 'warning',
95
+ args: [
96
+ { type: 'string', value: 'hola' },
97
+ { type: 'number', value: 42 },
98
+ { type: 'object', description: 'Object' },
99
+ ],
100
+ },
101
+ 1000,
102
+ )!
103
+ expect(entrada.nivel).toBe('warning')
104
+ expect(entrada.texto).toBe('hola 42 Object')
105
+ expect(entrada.ts).toBe(1000)
106
+ })
107
+
108
+ test('exceptionThrown es nivel error con detalle y url', () => {
109
+ const entrada = Core.formatearEventoConsola('Runtime.exceptionThrown', {
110
+ exceptionDetails: {
111
+ text: 'Uncaught',
112
+ url: 'https://ejemplo.pe/app.js',
113
+ exception: { description: 'TypeError: x is not a function' },
114
+ },
115
+ })!
116
+ expect(entrada.nivel).toBe('error')
117
+ expect(entrada.texto).toBe('Uncaught TypeError: x is not a function')
118
+ expect(entrada.url).toBe('https://ejemplo.pe/app.js')
119
+ })
120
+
121
+ test('Log.entryAdded conserva nivel y texto; otros eventos devuelven null', () => {
122
+ const entrada = Core.formatearEventoConsola('Log.entryAdded', {
123
+ entry: { level: 'error', text: 'Mixed Content', url: 'https://x.pe' },
124
+ })!
125
+ expect(entrada.nivel).toBe('error')
126
+ expect(entrada.texto).toBe('Mixed Content')
127
+ expect(Core.formatearEventoConsola('Network.responseReceived', {})).toBeNull()
128
+ })
129
+ })
130
+
131
+ describe('plegarEventoRed', () => {
132
+ test('pliega el ciclo completo de una petición', () => {
133
+ const estado = Core.crearEstadoRed()
134
+ Core.plegarEventoRed(
135
+ estado,
136
+ 'Network.requestWillBeSent',
137
+ {
138
+ requestId: 'r1',
139
+ type: 'XHR',
140
+ request: { method: 'POST', url: 'https://api.pe/v1' },
141
+ },
142
+ 10,
143
+ 1000,
144
+ )
145
+ Core.plegarEventoRed(
146
+ estado,
147
+ 'Network.responseReceived',
148
+ { requestId: 'r1', response: { status: 201, mimeType: 'application/json' } },
149
+ 10,
150
+ )
151
+ Core.plegarEventoRed(
152
+ estado,
153
+ 'Network.loadingFinished',
154
+ { requestId: 'r1', encodedDataLength: 512 },
155
+ 10,
156
+ 1350,
157
+ )
158
+ expect(estado.entradas).toHaveLength(1)
159
+ const e = estado.entradas[0]!
160
+ expect(e.metodo).toBe('POST')
161
+ expect(e.estado).toBe(201)
162
+ expect(e.mime).toBe('application/json')
163
+ expect(e.tam).toBe(512)
164
+ expect(e.duracionMs).toBe(350)
165
+ expect(e.error).toBeNull()
166
+ })
167
+
168
+ test('loadingFailed registra el error', () => {
169
+ const estado = Core.crearEstadoRed()
170
+ Core.plegarEventoRed(
171
+ estado,
172
+ 'Network.requestWillBeSent',
173
+ { requestId: 'r2', request: { method: 'GET', url: 'https://caida.pe' } },
174
+ 10,
175
+ )
176
+ Core.plegarEventoRed(
177
+ estado,
178
+ 'Network.loadingFailed',
179
+ { requestId: 'r2', errorText: 'net::ERR_CONNECTION_REFUSED' },
180
+ 10,
181
+ )
182
+ expect(estado.entradas[0]!.error).toBe('net::ERR_CONNECTION_REFUSED')
183
+ })
184
+
185
+ test('al expulsar una entrada del buffer circular limpia su índice', () => {
186
+ const estado = Core.crearEstadoRed()
187
+ for (let i = 0; i < 3; i++) {
188
+ Core.plegarEventoRed(
189
+ estado,
190
+ 'Network.requestWillBeSent',
191
+ { requestId: `r${i}`, request: { method: 'GET', url: `https://p.pe/${i}` } },
192
+ 2,
193
+ )
194
+ }
195
+ expect(estado.entradas).toHaveLength(2)
196
+ expect(estado.porId.has('r0')).toBe(false)
197
+ expect(estado.porId.has('r2')).toBe(true)
198
+ })
199
+ })
200
+
201
+ describe('filtrarEntradas', () => {
202
+ const consola: EntradaConsola[] = [
203
+ { ts: 1, nivel: 'log', texto: '[MiApp] arranque' },
204
+ { ts: 2, nivel: 'error', texto: '[MiApp] fallo de red' },
205
+ { ts: 3, nivel: 'log', texto: 'ruido de terceros' },
206
+ { ts: 4, nivel: 'error', texto: 'otro fallo' },
207
+ ]
208
+
209
+ test('filtra por regex y corta a las más recientes', () => {
210
+ const r = Core.filtrarEntradas(consola, {
211
+ pattern: 'MiApp',
212
+ limit: 1,
213
+ campoTexto: 'texto',
214
+ })
215
+ expect(r).toHaveLength(1)
216
+ expect(r[0]!.texto).toBe('[MiApp] fallo de red')
217
+ })
218
+
219
+ test('onlyErrors deja solo errores; sin limit usa 100 por defecto', () => {
220
+ const r = Core.filtrarEntradas(consola, { onlyErrors: true, campoTexto: 'texto' })
221
+ expect(r.map(e => e.ts)).toEqual([2, 4])
222
+ })
223
+
224
+ test('un pattern inválido lanza error honesto', () => {
225
+ let caught: unknown
226
+ try {
227
+ Core.filtrarEntradas(consola, { pattern: '(', campoTexto: 'texto' })
228
+ } catch (e) {
229
+ caught = e
230
+ }
231
+ expect(caught instanceof Error).toBe(true)
232
+ expect((caught as Error).message).toContain('regex')
233
+ })
234
+ })
235
+
236
+ describe('parsearCombinacionTeclas', () => {
237
+ test('tecla simple imprime texto', () => {
238
+ const def = Core.parsearCombinacionTeclas('a')!
239
+ expect(def).toMatchObject({ modifiers: 0, key: 'a', code: 'KeyA', keyCode: 65 })
240
+ expect(def.text).toBe('a')
241
+ })
242
+
243
+ test('ctrl+a no imprime texto y lleva el modificador Ctrl', () => {
244
+ const def = Core.parsearCombinacionTeclas('ctrl+a')!
245
+ expect(def.modifiers).toBe(2)
246
+ expect(def.keyCode).toBe(65)
247
+ expect(def.text).toBeUndefined()
248
+ })
249
+
250
+ test('ctrl+shift+t combina modificadores y sube a mayúscula', () => {
251
+ const def = Core.parsearCombinacionTeclas('ctrl+shift+t')!
252
+ expect(def.modifiers).toBe(10)
253
+ expect(def.key).toBe('T')
254
+ })
255
+
256
+ test('Enter y F5 se reconocen; lo desconocido devuelve null', () => {
257
+ expect(Core.parsearCombinacionTeclas('Enter')!.keyCode).toBe(13)
258
+ expect(Core.parsearCombinacionTeclas('F5')!.keyCode).toBe(116)
259
+ expect(Core.parsearCombinacionTeclas('teclarara')).toBeNull()
260
+ expect(Core.parsearCombinacionTeclas('')).toBeNull()
261
+ })
262
+ })
263
+
264
+ describe('resumirArbolAX', () => {
265
+ const nodos = [
266
+ {
267
+ backendDOMNodeId: 11,
268
+ role: { value: 'button' },
269
+ name: { value: 'Enviar' },
270
+ },
271
+ {
272
+ backendDOMNodeId: 12,
273
+ role: { value: 'textbox' },
274
+ name: { value: 'Correo' },
275
+ value: { value: 'a@b.pe' },
276
+ },
277
+ { backendDOMNodeId: 13, role: { value: 'heading' }, name: { value: 'Título' } },
278
+ { backendDOMNodeId: 14, role: { value: 'GenericContainer' }, name: { value: '' } },
279
+ { backendDOMNodeId: 15, role: { value: 'link' }, name: { value: 'Inicio' }, ignored: true },
280
+ ]
281
+
282
+ test('interactive (por defecto) solo devuelve interactivos con refs 1..n', () => {
283
+ const r = Core.resumirArbolAX(nodos, undefined, 150)
284
+ expect(r.elementos.map(e => e.rol)).toEqual(['button', 'textbox'])
285
+ expect(r.elementos.map(e => e.ref)).toEqual(['1', '2'])
286
+ expect(r.elementos[1]!.valor).toBe('a@b.pe')
287
+ expect(r.truncado).toBe(false)
288
+ })
289
+
290
+ test('all añade estructura pero sigue ignorando nodos ignored y genéricos', () => {
291
+ const r = Core.resumirArbolAX(nodos, 'all', 150)
292
+ expect(r.elementos.map(e => e.rol)).toEqual(['button', 'textbox', 'heading'])
293
+ })
294
+
295
+ test('trunca honestamente y lo declara', () => {
296
+ const r = Core.resumirArbolAX(nodos, undefined, 1)
297
+ expect(r.elementos).toHaveLength(1)
298
+ expect(r.truncado).toBe(true)
299
+ expect(r.totalCandidatos).toBe(2)
300
+ })
301
+ })
302
+
303
+ describe('planRecompresionCaptura', () => {
304
+ test('no reprocesa capturas pequeñas y estrechas', () => {
305
+ expect(Core.planRecompresionCaptura(1000, 800)).toEqual({ reprocesar: false })
306
+ })
307
+
308
+ test('escala cuando excede el ancho máximo', () => {
309
+ const plan = Core.planRecompresionCaptura(1000, 3200, { maxAncho: 1600 })
310
+ expect(plan.reprocesar).toBe(true)
311
+ expect(plan.escala).toBe(0.5)
312
+ expect(plan.calidad).toBe(0.8)
313
+ })
314
+
315
+ test('baja la calidad cuando excede el tamaño del canal', () => {
316
+ const plan = Core.planRecompresionCaptura(13 * 1024 * 1024, 1200)
317
+ expect(plan.reprocesar).toBe(true)
318
+ expect(plan.escala).toBe(1)
319
+ expect(plan.calidad).toBe(0.6)
320
+ })
321
+ })