tracereports 0.2.0 → 0.3.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.es.md CHANGED
@@ -12,7 +12,8 @@ npm install --save-dev tracereports
12
12
  - **Playwright Test**: el reporter envía cada test con sus pasos, error y capturas, más los traces y
13
13
  videos que guarda Playwright; con los fixtures, también la red que vio el navegador, el DOM al
14
14
  fallar y la consola del navegador.
15
- - **Selenium WebDriver o cualquier runner**: captura y DOM al fallar, y una API para pasos y red.
15
+ - **Selenium WebDriver o cualquier runner**: con `seleniumOptions(options)` y `withDriver`, la red (pestaña
16
+ Red, Lab) y la consola de cada test, y la captura y el DOM al fallar ([guía de Selenium](https://github.com/josemiguellopez/tracereports/blob/main/docs/es/selenium.md)).
16
17
  - **Nunca frena ni rompe tus tests**: la evidencia sale en segundo plano, con reintentos y sin
17
18
  duplicados; los shards pueden unirse a la misma ejecución.
18
19
  - **Sin servidor**: si el servidor está caído o rechaza el token, la ejecución se graba en local;
package/README.md CHANGED
@@ -12,7 +12,8 @@ npm install --save-dev tracereports
12
12
  - **Playwright Test**: the reporter sends each test with its steps, error and screenshots, plus the
13
13
  traces and videos Playwright keeps; with the fixtures, also the network the browser saw, the DOM
14
14
  at the failure and the browser console.
15
- - **Selenium WebDriver or any runner**: screenshot and DOM on failure, and an API for steps and network.
15
+ - **Selenium WebDriver or any runner**: with `seleniumOptions(options)` and `withDriver`, each test's network
16
+ (Network tab, Lab) and console, and the screenshot and DOM on failure ([Selenium guide](https://github.com/josemiguellopez/tracereports/blob/main/docs/en/selenium.md)).
16
17
  - **Never slows down or breaks your tests**: evidence goes out in the background with retries and
17
18
  no duplicates; shards can join the same run.
18
19
  - **Without a server**: if the server is down or rejects the token, the run is recorded locally;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tracereports",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "TraceReports client for JavaScript/TypeScript: Playwright Test reporter and fixtures, Selenium WebDriver helpers and a client for any runner.",
5
5
  "keywords": [
6
6
  "testing",
package/src/index.d.ts CHANGED
@@ -26,6 +26,10 @@ export interface RunOptions { environment?: string; project?: string; branch?: s
26
26
  export interface TestOptions {
27
27
  category?: string; description?: string; key?: string; suite?: string; params?: string; worker?: string | number;
28
28
  status?: string; onFailure?: (t: TraceTest, err: unknown) => unknown | Promise<unknown>;
29
+ /** Right after the test starts (e.g. discard the browser logs of earlier tests). */
30
+ onStart?: (t: TraceTest) => unknown | Promise<unknown>;
31
+ /** Before the test is closed, passed or failed (`err` is null when it passed): network, console... */
32
+ onEnd?: (t: TraceTest, err: unknown) => unknown | Promise<unknown>;
29
33
  }
30
34
 
31
35
  export type Status = "INFO" | "PASS" | "FAIL" | "WARNING" | "SKIP";
package/src/index.js CHANGED
@@ -27,7 +27,7 @@ import { Mirror } from "./mirror.js";
27
27
  import { Sender } from "./transport.js";
28
28
  import { downloadBinary } from "./binary.js";
29
29
 
30
- export const VERSION = "0.1.0";
30
+ export const VERSION = "0.3.0";
31
31
  const STATUSES = new Set(["INFO", "PASS", "FAIL", "WARNING", "SKIP"]);
32
32
  const truthy = (v) => ["1", "true", "yes"].includes(String(v || "").toLowerCase());
33
33
 
@@ -314,19 +314,23 @@ export class TraceReports {
314
314
 
315
315
  /**
316
316
  * Corre `fn` como un test: lo inicia, lo cierra según el resultado y, si falla, adjunta la
317
- * captura y el snapshot de `onFailure` (opcional). El error se vuelve a lanzar.
317
+ * captura y el snapshot de `onFailure` (opcional). `onStart` corre al empezar y `onEnd` antes de
318
+ * cerrarlo, pase o falle (la red y la consola de `withDriver`). El error se vuelve a lanzar.
318
319
  */
319
320
  async test(name, opts, fn) {
320
321
  if (typeof opts === "function") [fn, opts] = [opts, {}];
321
322
  const t = await this.startTest(name, opts);
323
+ // la evidencia nunca rompe el test
324
+ const hook = async (h, ...args) => { if (h) { try { await h(t, ...args); } catch { /* sin evidencia */ } } };
325
+ await hook(opts.onStart);
322
326
  try {
323
327
  const out = await fn(t);
328
+ await hook(opts.onEnd, null);
324
329
  t.finish(opts.status);
325
330
  return out;
326
331
  } catch (err) {
327
- if (opts.onFailure) {
328
- try { await opts.onFailure(t, err); } catch { /* la evidencia nunca rompe el test */ }
329
- }
332
+ await hook(opts.onFailure, err);
333
+ await hook(opts.onEnd, err);
330
334
  t.fail(`${err?.name || "Error"}: ${err?.message ?? err}`);
331
335
  t.finish("FAIL", { error: err });
332
336
  throw err;
package/src/selenium.js CHANGED
@@ -1,8 +1,14 @@
1
- // Selenium WebDriver (selenium-webdriver) con TraceReports. Sirve con cualquier runner
2
- // (node:test, Mocha, Jest…): no importa selenium-webdriver, usa el `driver` que le pases.
1
+ // Selenium WebDriver (selenium-webdriver) con TraceReports: red, consola, captura y DOM. Sirve con
2
+ // cualquier runner (node:test, Mocha, Jest…): no importa selenium-webdriver, usa el `driver` que le
3
+ // pases.
3
4
  //
5
+ // import { Builder } from "selenium-webdriver";
6
+ // import chrome from "selenium-webdriver/chrome.js";
4
7
  // import { TraceReports } from "tracereports";
5
- // import { withDriver, screenshot } from "tracereports/selenium";
8
+ // import { seleniumOptions, withDriver, screenshot } from "tracereports/selenium";
9
+ //
10
+ // const options = seleniumOptions(new chrome.Options()); // activa los logs de red y de consola
11
+ // const driver = await new Builder().forBrowser("chrome").setChromeOptions(options).build();
6
12
  //
7
13
  // const cr = new TraceReports();
8
14
  // await cr.startRun("Regresión web", { environment: "chrome" });
@@ -12,12 +18,27 @@
12
18
  // });
13
19
  // await cr.finishRun();
14
20
  //
15
- // Si el test falla, `withDriver` adjunta la captura del momento y el snapshot del DOM (con él el
16
- // reporte recomienda selectores cuando se rompe un locator). Selenium no expone la red del
17
- // navegador: para la pestaña "Red" usa Playwright o envía las llamadas con `t.network([...])`.
21
+ // `withDriver` envía la red (pestaña "Red", Lab) y la consola de cada test, y si falla la captura del
22
+ // momento y el snapshot del DOM (con él el reporte recomienda selectores cuando se rompe un locator).
23
+ // La red y la consola salen del log de rendimiento de Chrome y Edge (Firefox no lo tiene): hay que
24
+ // crear el driver con `seleniumOptions(options)`. El body de una llamada hecha antes de que la
25
+ // página navegara puede no estar ya disponible; el resto de la llamada sí.
18
26
 
19
27
  import { DOM_SCRIPT } from "./dom.js";
20
28
 
29
+ const LOG_PREFS = { performance: "ALL", browser: "ALL" };
30
+ const BODY_TYPES = new Set(["xhr", "fetch"]);
31
+ const TEXT_MIME = ["json", "text", "xml", "javascript", "html", "x-www-form-urlencoded", "graphql"];
32
+
33
+ /** Activa en las opciones de Chrome o Edge los logs de rendimiento (red) y del navegador (consola). */
34
+ export function seleniumOptions(options) {
35
+ options.set("goog:loggingPrefs", { ...LOG_PREFS });
36
+ if (/edge/i.test(options?.constructor?.name || "") || options.get?.("browserName") === "MicrosoftEdge") {
37
+ options.set("ms:loggingPrefs", { ...LOG_PREFS });
38
+ }
39
+ return options;
40
+ }
41
+
21
42
  /** Captura de pantalla como base64 (lo que acepta `t.screenshot`), o null. */
22
43
  export async function screenshot(driver) {
23
44
  try {
@@ -36,14 +57,179 @@ export async function captureDom(driver) {
36
57
  }
37
58
  }
38
59
 
39
- /** Opciones para `cr.test(...)` que, al fallar, adjuntan captura y DOM desde `driver`. */
60
+ function* events(entries) {
61
+ for (const entry of entries || []) {
62
+ let msg;
63
+ try {
64
+ msg = JSON.parse(entry.message).message;
65
+ } catch {
66
+ continue;
67
+ }
68
+ if (msg?.method?.startsWith("Network.")) yield [msg.method, msg.params || {}];
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Convierte las entradas del log "performance" (`driver.manage().logs().get("performance")`) en
74
+ * conexiones (el formato de `t.network`). Cada una lleva `_requestId` para pedir su body después.
75
+ */
76
+ export function connectionsFromLog(entries) {
77
+ const byId = new Map();
78
+ const order = [];
79
+ const extraReq = new Map();
80
+ const extraRes = new Map();
81
+ const ms = (from, to) => (from == null || to == null ? undefined : Math.round((to - from) * 1000));
82
+ const setResponse = (c, r) => {
83
+ c.status = Number(r.status || 0);
84
+ c.status_text = r.statusText || "";
85
+ c.mime_type = r.mimeType || "";
86
+ c.response_headers = { ...(r.headers || {}) };
87
+ };
88
+ for (const [method, p] of events(entries)) {
89
+ const id = p.requestId;
90
+ if (!id) continue;
91
+ if (method === "Network.requestWillBeSent") {
92
+ const prev = byId.get(id);
93
+ if (prev && p.redirectResponse) {
94
+ // la redirección cierra el salto anterior con su propia respuesta
95
+ setResponse(prev, p.redirectResponse);
96
+ prev.duration_ms = ms(prev._ts, p.timestamp);
97
+ prev._requestId = null;
98
+ }
99
+ const req = p.request || {};
100
+ const c = {
101
+ _requestId: id, _ts: p.timestamp, method: req.method || "", url: req.url || "",
102
+ resource_type: String(p.type || "").toLowerCase(), request_headers: { ...(req.headers || {}) },
103
+ post_data: req.postData || "", _hasPostData: Boolean(req.hasPostData || req.postData),
104
+ started_at: p.wallTime ? Math.round(p.wallTime * 1000) : 0, status: 0,
105
+ };
106
+ byId.set(id, c);
107
+ order.push(c);
108
+ } else if (method === "Network.requestWillBeSentExtraInfo") {
109
+ extraReq.set(id, { ...(extraReq.get(id) || {}), ...(p.headers || {}) });
110
+ } else if (method === "Network.responseReceived" && byId.has(id)) {
111
+ const c = byId.get(id);
112
+ setResponse(c, p.response || {});
113
+ if (p.type) c.resource_type = String(p.type).toLowerCase();
114
+ } else if (method === "Network.responseReceivedExtraInfo") {
115
+ extraRes.set(id, { ...(extraRes.get(id) || {}), ...(p.headers || {}) });
116
+ } else if (method === "Network.loadingFinished" && byId.has(id)) {
117
+ const c = byId.get(id);
118
+ c.duration_ms = ms(c._ts, p.timestamp);
119
+ } else if (method === "Network.loadingFailed" && byId.has(id)) {
120
+ const c = byId.get(id);
121
+ c.failed = true;
122
+ c.error_text = p.errorText || (p.canceled ? "canceled" : "failed");
123
+ c.duration_ms = ms(c._ts, p.timestamp);
124
+ }
125
+ }
126
+ const out = [];
127
+ for (const c of order) {
128
+ if (/^(data|blob):/.test(c.url)) continue;
129
+ // los headers reales que salieron, con la cookie de sesión
130
+ if (extraReq.has(c._requestId)) c.request_headers = { ...c.request_headers, ...extraReq.get(c._requestId) };
131
+ if (extraRes.has(c._requestId) && c.response_headers) c.response_headers = { ...c.response_headers, ...extraRes.get(c._requestId) };
132
+ delete c._ts;
133
+ out.push(c);
134
+ }
135
+ return out;
136
+ }
137
+
138
+ async function cdp(driver, cmd, params) {
139
+ try {
140
+ return (await driver.sendAndGetDevToolsCommand(cmd, params)) || {};
141
+ } catch {
142
+ return {}; // otro navegador, o el recurso ya no existe (la página navegó)
143
+ }
144
+ }
145
+
146
+ function wantsBody(c) {
147
+ if (c.failed || !c._requestId) return false;
148
+ if (!BODY_TYPES.has(c.resource_type) && c.status < 400) return false;
149
+ const mime = String(c.mime_type || "").toLowerCase();
150
+ return !mime || TEXT_MIME.some((t) => mime.includes(t));
151
+ }
152
+
153
+ async function readLog(driver, kind) {
154
+ try {
155
+ return await driver.manage().logs().get(kind);
156
+ } catch {
157
+ return [];
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Las llamadas de red desde la última lectura (el log se vacía al leerlo: cada test lee las
163
+ * suyas), con los bodies de las APIs y de las respuestas con error. [] si el driver no tiene el
164
+ * log de rendimiento (ver `seleniumOptions`).
165
+ */
166
+ export async function captureNetwork(driver) {
167
+ const conns = connectionsFromLog(await readLog(driver, "performance"));
168
+ for (const c of conns) {
169
+ if (c._hasPostData && !c.post_data && c._requestId) {
170
+ c.post_data = (await cdp(driver, "Network.getRequestPostData", { requestId: c._requestId })).postData || "";
171
+ }
172
+ if (wantsBody(c)) {
173
+ const res = await cdp(driver, "Network.getResponseBody", { requestId: c._requestId });
174
+ if (typeof res.body === "string") {
175
+ const body = res.base64Encoded ? Buffer.from(res.body, "base64").toString("utf8") : res.body;
176
+ c.body_size = Buffer.byteLength(body);
177
+ c.response_body = body;
178
+ }
179
+ }
180
+ delete c._requestId;
181
+ delete c._hasPostData;
182
+ }
183
+ return conns;
184
+ }
185
+
186
+ /** Errores y warnings de la consola desde la última lectura (el formato de `t.console`). */
187
+ export async function captureConsole(driver) {
188
+ const out = [];
189
+ for (const e of await readLog(driver, "browser")) {
190
+ const name = String(e?.level?.name ?? e?.level ?? "").toUpperCase();
191
+ let level = name === "SEVERE" ? "error" : name === "WARNING" ? "warning" : "";
192
+ if (!level) continue;
193
+ if (level === "error" && /\bUncaught\b/.test(e.message || "")) level = "pageerror";
194
+ out.push({ level, text: String(e.message || ""), location: "", timestamp: Number(e.timestamp || 0) });
195
+ }
196
+ return out;
197
+ }
198
+
199
+ /** Vacía los logs del driver: lo de antes del test (otro test con el mismo driver) no se envía. */
200
+ export async function discardLogs(driver) {
201
+ await readLog(driver, "performance");
202
+ await readLog(driver, "browser");
203
+ }
204
+
205
+ /** Envía al test la red y la consola desde la última lectura. Devuelve cuántas llamadas envió. */
206
+ export async function reportSelenium(t, driver) {
207
+ const conns = await captureNetwork(driver);
208
+ if (conns.length) t.network(conns);
209
+ const consoleEntries = await captureConsole(driver);
210
+ if (consoleEntries.length) t.console(consoleEntries);
211
+ return conns.length;
212
+ }
213
+
214
+ /**
215
+ * Opciones para `cr.test(...)` con un driver de Selenium: al empezar descarta los logs anteriores,
216
+ * al terminar envía la red y la consola del test, y si falla la captura y el DOM.
217
+ */
40
218
  export function withDriver(driver, opts = {}) {
41
219
  return {
42
220
  ...opts,
221
+ async onStart(t) {
222
+ await discardLogs(driver);
223
+ if (opts.onStart) await opts.onStart(t);
224
+ },
43
225
  async onFailure(t, err) {
44
226
  t.screenshot(await screenshot(driver), "Captura al fallar", "FAIL");
45
227
  t.dom(await captureDom(driver));
46
228
  if (opts.onFailure) await opts.onFailure(t, err);
47
229
  },
230
+ async onEnd(t, err) {
231
+ await reportSelenium(t, driver);
232
+ if (opts.onEnd) await opts.onEnd(t, err);
233
+ },
48
234
  };
49
235
  }