@qualflare/cypress 0.3.0 → 0.5.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 CHANGED
@@ -5,7 +5,7 @@
5
5
  [![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](./LICENSE)
6
6
 
7
7
  A native Cypress reporter for [Qualflare](https://qualflare.com) — captures test results directly
8
- from your Cypress run: suite/test status, real retry counts, screenshots, step-by-step command
8
+ from your Cypress run: suite/test status, per-attempt retry history, screenshots, step-by-step command
9
9
  traces, and author-facing metadata (labels, links, tags, custom attachments) — and writes them into
10
10
  a report directory that [`qualflare-cli`](https://github.com/Qualflare/qualflare-cli) uploads. No
11
11
  post-hoc file parsing, no intermediate report format to hand-roll, no network access needed from
@@ -120,6 +120,23 @@ wrong value cannot fail at run time — this package makes no network calls —
120
120
  CLIs merge as before.
121
121
  - **Command-log step nesting is two levels only** (Cypress's own API limit) — `qualflare.step()`
122
122
  supports arbitrary nesting depth.
123
+ - **An all-passing spec's video is not captured by default.** Cypress records one video per spec,
124
+ and it is only copied when a case in that spec failed. Set `videoOnFailureOnly: false` to keep a
125
+ green spec's video too; uploading it is separately opt-in via the CLI's `--upload-artifacts`.
126
+ - **Step timing is an approximation** — command-log steps are timed from log events, not from
127
+ instrumented start/stop boundaries. `qualflare.step()` timing is exact.
128
+ - **`parameter()` outside a step is not masked** — `masked` is a display hint for the UI; the
129
+ server never redacts the value, so never put a real secret in one. See
130
+ [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md#qualflareparameter-outside-a-step-has-no-masking).
131
+ - **Attachment caps are two budgets, not one pool** — `maxAttachmentBytes` bounds a single
132
+ attachment and `maxTotalAttachmentBytes` the whole run; anything over either is dropped
133
+ outright rather than truncated. Raising them is the easiest way to push a request past
134
+ `/collect`'s body limit. See
135
+ [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md#per-caseper-attachment-caps-are-independent-not-pooled).
136
+ - **Retries carry per-attempt errors, but everything else is the final attempt** — `Case.attempts`
137
+ records each attempt's status, duration and error; steps, labels, links, tags, priority,
138
+ properties and attachments come from the last attempt only, so an abandoned attempt's step trace
139
+ is discarded rather than replayed alongside the final one.
123
140
 
124
141
  Full details in [`docs/LIMITATIONS.md`](./docs/LIMITATIONS.md).
125
142
 
package/dist/index.cjs CHANGED
@@ -45,6 +45,8 @@ var MAX_LABELS_PER_CASE = 100;
45
45
  var MAX_LINKS_PER_CASE = 20;
46
46
  var MAX_TAGS_PER_CASE = 64;
47
47
  var MAX_TAG_LENGTH = 255;
48
+ var MAX_ATTEMPTS_PER_CASE = 50;
49
+ var MAX_ATTEMPT_MESSAGE_RUNES = 8192;
48
50
  var MAX_VIDEO_UPLOAD_BYTES = 50 * 1024 * 1024;
49
51
  var MAX_STEPS_PER_TEST_ATTEMPT = 300;
50
52
 
@@ -298,6 +300,18 @@ function registerCommandLogListener(buffer) {
298
300
  });
299
301
  }
300
302
 
303
+ // src/shared/text.ts
304
+ function truncateRunes(value, maxRunes) {
305
+ if (value.length <= maxRunes) {
306
+ return value;
307
+ }
308
+ const runes = Array.from(value);
309
+ if (runes.length <= maxRunes) {
310
+ return value;
311
+ }
312
+ return runes.slice(0, maxRunes).join("");
313
+ }
314
+
301
315
  // src/browser/case-builder.ts
302
316
  function combineSteps(autoSteps, manualSteps) {
303
317
  const auto = autoSteps ?? [];
@@ -314,6 +328,26 @@ function combineSteps(autoSteps, manualSteps) {
314
328
  });
315
329
  return [...auto, ...offsetManual];
316
330
  }
331
+ function buildAttempts(attempts) {
332
+ if (attempts.length < 2) {
333
+ return void 0;
334
+ }
335
+ let kept = attempts;
336
+ if (attempts.length > MAX_ATTEMPTS_PER_CASE) {
337
+ kept = [...attempts.slice(0, MAX_ATTEMPTS_PER_CASE - 1), attempts[attempts.length - 1]];
338
+ }
339
+ return kept.map((a, i) => {
340
+ const attempt = {
341
+ attempt: i + 1,
342
+ status: a.status,
343
+ duration: msToNs(a.duration)
344
+ };
345
+ if (a.error) {
346
+ attempt.message = truncateRunes(a.error, MAX_ATTEMPT_MESSAGE_RUNES);
347
+ }
348
+ return attempt;
349
+ });
350
+ }
317
351
  function collapseAttempts(attempts) {
318
352
  if (attempts.length === 0) {
319
353
  throw new Error("collapseAttempts: at least one attempt is required");
@@ -322,11 +356,13 @@ function collapseAttempts(attempts) {
322
356
  const retryCount = attempts.length - 1;
323
357
  const isFlaky = retryCount > 0 && final.status === "passed" && attempts.some((a) => a.status !== "passed");
324
358
  const duration = attempts.reduce((sum, a) => sum + a.duration, 0);
359
+ const attemptHistory = buildAttempts(attempts);
325
360
  return {
326
361
  status: final.status,
327
362
  duration,
328
363
  retryCount,
329
364
  isFlaky,
365
+ ...attemptHistory ? { attempts: attemptHistory } : {},
330
366
  error: final.status === "passed" ? void 0 : final.error,
331
367
  steps: combineSteps(final.steps, final.manualSteps),
332
368
  labels: final.labels,
@@ -353,6 +389,8 @@ function flushCase(test, attempts) {
353
389
  duration: msToNs(collapsed.duration),
354
390
  retryCount: collapsed.retryCount,
355
391
  isFlaky: collapsed.isFlaky,
392
+ // Already nanoseconds — collapseAttempts converts, unlike `duration` above.
393
+ attempts: collapsed.attempts,
356
394
  error: collapsed.error,
357
395
  steps: collapsed.steps,
358
396
  // qualflare.* author-facing metadata API calls (labels/links/tags/