qapture2 0.7.1 → 0.7.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.
- package/README.md +16 -0
- package/dist/{chunk-WAOYPLZA.cjs → chunk-CIFJMD2B.cjs} +60 -12
- package/dist/{chunk-JSEFC2VP.js → chunk-NVFFN7SC.js} +60 -12
- package/dist/index.cjs +5 -5
- package/dist/index.js +1 -1
- package/dist/next.cjs +4 -4
- package/dist/next.js +1 -1
- package/dist/standalone.cjs +2 -2
- package/dist/standalone.js +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -315,6 +315,22 @@ rewritten in place, and a render that still throws is retried once with
|
|
|
315
315
|
decoration stripped. `npm run modern-css-test` proves it, and proves itself:
|
|
316
316
|
it first asserts that raw html2canvas *still* dies on the same fixture.
|
|
317
317
|
|
|
318
|
+
**v0.7.2 fixed the last of the mis-framing: clicking an element only captured
|
|
319
|
+
the part of it that was on screen.** Dragged regions are clamped into the
|
|
320
|
+
viewport before capture, so they were always safe — but an element *pick* is a
|
|
321
|
+
raw `getBoundingClientRect()`, and a table column, a sidebar, a long form or a
|
|
322
|
+
wide toolbar routinely extends past the fold. You got a fragment. And when the
|
|
323
|
+
element started *above* the viewport, the crop slid down the page to fill its
|
|
324
|
+
height, so the screenshot was the right size showing the wrong content — a
|
|
325
|
+
failure with nothing on screen to reveal it.
|
|
326
|
+
|
|
327
|
+
Captures now render the union of the viewport and the selection, so an element
|
|
328
|
+
that leaves the screen in any direction is rendered whole, up to 4000px per
|
|
329
|
+
side (past a four-viewport render budget the scale drops, not the framing).
|
|
330
|
+
The exact engine can't photograph off-screen pixels, so there the selection is
|
|
331
|
+
trimmed to the visible part instead — cropped, never displaced. `npm run
|
|
332
|
+
element-capture-test` measures all three overflow directions.
|
|
333
|
+
|
|
318
334
|
### `exact` — opt-in, pixel-for-pixel
|
|
319
335
|
|
|
320
336
|
Uses the Screen Capture API to photograph **this tab's real composited
|
|
@@ -2537,6 +2537,8 @@ var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
|
2537
2537
|
var MAX_SHOT_EDGE = 1800;
|
|
2538
2538
|
var WEBP_QUALITY = 0.92;
|
|
2539
2539
|
var STUCK_ATTR = "data-qa-stuck";
|
|
2540
|
+
var MAX_CAPTURE_EDGE = 4e3;
|
|
2541
|
+
var MAX_RENDER_VIEWPORTS = 4;
|
|
2540
2542
|
function withTimeout(promise, ms) {
|
|
2541
2543
|
let timer;
|
|
2542
2544
|
return Promise.race([
|
|
@@ -2702,20 +2704,60 @@ function cropCanvas(full, rect, scale) {
|
|
|
2702
2704
|
ctx.drawImage(full, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2703
2705
|
return out;
|
|
2704
2706
|
}
|
|
2707
|
+
function viewportSize() {
|
|
2708
|
+
return {
|
|
2709
|
+
vw: document.documentElement.clientWidth || window.innerWidth,
|
|
2710
|
+
vh: document.documentElement.clientHeight || window.innerHeight
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2713
|
+
function intersectViewport(rect) {
|
|
2714
|
+
const { vw, vh } = viewportSize();
|
|
2715
|
+
const left = Math.max(0, rect.left);
|
|
2716
|
+
const top = Math.max(0, rect.top);
|
|
2717
|
+
const right = Math.min(vw, rect.left + rect.width);
|
|
2718
|
+
const bottom = Math.min(vh, rect.top + rect.height);
|
|
2719
|
+
if (right - left < 2 || bottom - top < 2) return null;
|
|
2720
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
2721
|
+
}
|
|
2722
|
+
function planRender(rect, sx, sy, vw, vh) {
|
|
2723
|
+
let left = rect.left + sx;
|
|
2724
|
+
let top = rect.top + sy;
|
|
2725
|
+
let width = rect.width;
|
|
2726
|
+
let height = rect.height;
|
|
2727
|
+
if (width > MAX_CAPTURE_EDGE) {
|
|
2728
|
+
left = Math.max(left, Math.min(sx, left + width - MAX_CAPTURE_EDGE));
|
|
2729
|
+
width = MAX_CAPTURE_EDGE;
|
|
2730
|
+
}
|
|
2731
|
+
if (height > MAX_CAPTURE_EDGE) {
|
|
2732
|
+
top = Math.max(top, Math.min(sy, top + height - MAX_CAPTURE_EDGE));
|
|
2733
|
+
height = MAX_CAPTURE_EDGE;
|
|
2734
|
+
}
|
|
2735
|
+
const x = Math.floor(Math.min(sx, left));
|
|
2736
|
+
const y = Math.floor(Math.min(sy, top));
|
|
2737
|
+
const right = Math.ceil(Math.max(sx + vw, left + width));
|
|
2738
|
+
const bottom = Math.ceil(Math.max(sy + vh, top + height));
|
|
2739
|
+
return {
|
|
2740
|
+
render: { x, y, width: right - x, height: bottom - y },
|
|
2741
|
+
crop: { left: left - x, top: top - y, width, height }
|
|
2742
|
+
};
|
|
2743
|
+
}
|
|
2705
2744
|
async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
2706
2745
|
const { default: html2canvas } = await import('html2canvas');
|
|
2707
|
-
const
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2746
|
+
const { vw, vh } = viewportSize();
|
|
2747
|
+
const plan = planRender(rect, sx, sy, vw, vh);
|
|
2748
|
+
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
2749
|
+
const budget = vw * vh * MAX_RENDER_VIEWPORTS;
|
|
2750
|
+
const area = plan.render.width * plan.render.height;
|
|
2751
|
+
if (area > budget) scale *= Math.sqrt(budget / area);
|
|
2710
2752
|
const marked = markStuckElements();
|
|
2711
2753
|
try {
|
|
2712
2754
|
const full = await withTimeout(
|
|
2713
2755
|
html2canvas(document.body, {
|
|
2714
|
-
//
|
|
2715
|
-
x:
|
|
2716
|
-
y:
|
|
2717
|
-
width:
|
|
2718
|
-
height:
|
|
2756
|
+
// The box to render, in document coordinates.
|
|
2757
|
+
x: plan.render.x,
|
|
2758
|
+
y: plan.render.y,
|
|
2759
|
+
width: plan.render.width,
|
|
2760
|
+
height: plan.render.height,
|
|
2719
2761
|
scale,
|
|
2720
2762
|
useCORS: true,
|
|
2721
2763
|
allowTaint: true,
|
|
@@ -2724,6 +2766,10 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
2724
2766
|
// CSS Color 4 treatment the document walk applies.
|
|
2725
2767
|
backgroundColor: safeBackgroundColor(resolvePageBackground(), FALLBACK_PAGE_BACKGROUND),
|
|
2726
2768
|
logging: false,
|
|
2769
|
+
// The clone's own scroll + viewport, which is what `position: fixed`
|
|
2770
|
+
// resolves against. It stays the LIVE viewport even when the render
|
|
2771
|
+
// box is bigger, so fixed chrome is drawn where the tester sees it
|
|
2772
|
+
// rather than smeared over the whole render.
|
|
2727
2773
|
scrollX: sx,
|
|
2728
2774
|
scrollY: sy,
|
|
2729
2775
|
windowWidth: vw,
|
|
@@ -2737,7 +2783,7 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
2737
2783
|
HTML2CANVAS_TIMEOUT_MS
|
|
2738
2784
|
);
|
|
2739
2785
|
if (!full) return null;
|
|
2740
|
-
return cropCanvas(full,
|
|
2786
|
+
return cropCanvas(full, plan.crop, scale);
|
|
2741
2787
|
} finally {
|
|
2742
2788
|
unmarkStuckElements(marked);
|
|
2743
2789
|
}
|
|
@@ -2750,8 +2796,10 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
2750
2796
|
const sx = scroll?.x ?? window.scrollX;
|
|
2751
2797
|
const sy = scroll?.y ?? window.scrollY;
|
|
2752
2798
|
if (prefer !== "dom" && getExactCaptureStatus() === "live") {
|
|
2799
|
+
const visible = intersectViewport(rect);
|
|
2800
|
+
if (!visible) return { status: "empty" };
|
|
2753
2801
|
try {
|
|
2754
|
-
const canvas = await withOverlayHidden(() => grabExactRegion(
|
|
2802
|
+
const canvas = await withOverlayHidden(() => grabExactRegion(visible));
|
|
2755
2803
|
if (canvas) {
|
|
2756
2804
|
const blob = await encodeShot(canvas);
|
|
2757
2805
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
@@ -9957,5 +10005,5 @@ function Qapture({ config }) {
|
|
|
9957
10005
|
exports.Qapture = Qapture;
|
|
9958
10006
|
exports.deleteQaDatabase = deleteQaDatabase;
|
|
9959
10007
|
exports.initQaStudio = initQaStudio;
|
|
9960
|
-
//# sourceMappingURL=chunk-
|
|
9961
|
-
//# sourceMappingURL=chunk-
|
|
10008
|
+
//# sourceMappingURL=chunk-CIFJMD2B.cjs.map
|
|
10009
|
+
//# sourceMappingURL=chunk-CIFJMD2B.cjs.map
|
|
@@ -2530,6 +2530,8 @@ var FALLBACK_PAGE_BACKGROUND = "#ffffff";
|
|
|
2530
2530
|
var MAX_SHOT_EDGE = 1800;
|
|
2531
2531
|
var WEBP_QUALITY = 0.92;
|
|
2532
2532
|
var STUCK_ATTR = "data-qa-stuck";
|
|
2533
|
+
var MAX_CAPTURE_EDGE = 4e3;
|
|
2534
|
+
var MAX_RENDER_VIEWPORTS = 4;
|
|
2533
2535
|
function withTimeout(promise, ms) {
|
|
2534
2536
|
let timer;
|
|
2535
2537
|
return Promise.race([
|
|
@@ -2695,20 +2697,60 @@ function cropCanvas(full, rect, scale) {
|
|
|
2695
2697
|
ctx.drawImage(full, sx, sy, sw, sh, 0, 0, sw, sh);
|
|
2696
2698
|
return out;
|
|
2697
2699
|
}
|
|
2700
|
+
function viewportSize() {
|
|
2701
|
+
return {
|
|
2702
|
+
vw: document.documentElement.clientWidth || window.innerWidth,
|
|
2703
|
+
vh: document.documentElement.clientHeight || window.innerHeight
|
|
2704
|
+
};
|
|
2705
|
+
}
|
|
2706
|
+
function intersectViewport(rect) {
|
|
2707
|
+
const { vw, vh } = viewportSize();
|
|
2708
|
+
const left = Math.max(0, rect.left);
|
|
2709
|
+
const top = Math.max(0, rect.top);
|
|
2710
|
+
const right = Math.min(vw, rect.left + rect.width);
|
|
2711
|
+
const bottom = Math.min(vh, rect.top + rect.height);
|
|
2712
|
+
if (right - left < 2 || bottom - top < 2) return null;
|
|
2713
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
2714
|
+
}
|
|
2715
|
+
function planRender(rect, sx, sy, vw, vh) {
|
|
2716
|
+
let left = rect.left + sx;
|
|
2717
|
+
let top = rect.top + sy;
|
|
2718
|
+
let width = rect.width;
|
|
2719
|
+
let height = rect.height;
|
|
2720
|
+
if (width > MAX_CAPTURE_EDGE) {
|
|
2721
|
+
left = Math.max(left, Math.min(sx, left + width - MAX_CAPTURE_EDGE));
|
|
2722
|
+
width = MAX_CAPTURE_EDGE;
|
|
2723
|
+
}
|
|
2724
|
+
if (height > MAX_CAPTURE_EDGE) {
|
|
2725
|
+
top = Math.max(top, Math.min(sy, top + height - MAX_CAPTURE_EDGE));
|
|
2726
|
+
height = MAX_CAPTURE_EDGE;
|
|
2727
|
+
}
|
|
2728
|
+
const x = Math.floor(Math.min(sx, left));
|
|
2729
|
+
const y = Math.floor(Math.min(sy, top));
|
|
2730
|
+
const right = Math.ceil(Math.max(sx + vw, left + width));
|
|
2731
|
+
const bottom = Math.ceil(Math.max(sy + vh, top + height));
|
|
2732
|
+
return {
|
|
2733
|
+
render: { x, y, width: right - x, height: bottom - y },
|
|
2734
|
+
crop: { left: left - x, top: top - y, width, height }
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2698
2737
|
async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
2699
2738
|
const { default: html2canvas } = await import('html2canvas');
|
|
2700
|
-
const
|
|
2701
|
-
const
|
|
2702
|
-
|
|
2739
|
+
const { vw, vh } = viewportSize();
|
|
2740
|
+
const plan = planRender(rect, sx, sy, vw, vh);
|
|
2741
|
+
let scale = Math.min(window.devicePixelRatio || 1, 2);
|
|
2742
|
+
const budget = vw * vh * MAX_RENDER_VIEWPORTS;
|
|
2743
|
+
const area = plan.render.width * plan.render.height;
|
|
2744
|
+
if (area > budget) scale *= Math.sqrt(budget / area);
|
|
2703
2745
|
const marked = markStuckElements();
|
|
2704
2746
|
try {
|
|
2705
2747
|
const full = await withTimeout(
|
|
2706
2748
|
html2canvas(document.body, {
|
|
2707
|
-
//
|
|
2708
|
-
x:
|
|
2709
|
-
y:
|
|
2710
|
-
width:
|
|
2711
|
-
height:
|
|
2749
|
+
// The box to render, in document coordinates.
|
|
2750
|
+
x: plan.render.x,
|
|
2751
|
+
y: plan.render.y,
|
|
2752
|
+
width: plan.render.width,
|
|
2753
|
+
height: plan.render.height,
|
|
2712
2754
|
scale,
|
|
2713
2755
|
useCORS: true,
|
|
2714
2756
|
allowTaint: true,
|
|
@@ -2717,6 +2759,10 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
2717
2759
|
// CSS Color 4 treatment the document walk applies.
|
|
2718
2760
|
backgroundColor: safeBackgroundColor(resolvePageBackground(), FALLBACK_PAGE_BACKGROUND),
|
|
2719
2761
|
logging: false,
|
|
2762
|
+
// The clone's own scroll + viewport, which is what `position: fixed`
|
|
2763
|
+
// resolves against. It stays the LIVE viewport even when the render
|
|
2764
|
+
// box is bigger, so fixed chrome is drawn where the tester sees it
|
|
2765
|
+
// rather than smeared over the whole render.
|
|
2720
2766
|
scrollX: sx,
|
|
2721
2767
|
scrollY: sy,
|
|
2722
2768
|
windowWidth: vw,
|
|
@@ -2730,7 +2776,7 @@ async function captureViaDom(rect, sx, sy, aggressiveColors = false) {
|
|
|
2730
2776
|
HTML2CANVAS_TIMEOUT_MS
|
|
2731
2777
|
);
|
|
2732
2778
|
if (!full) return null;
|
|
2733
|
-
return cropCanvas(full,
|
|
2779
|
+
return cropCanvas(full, plan.crop, scale);
|
|
2734
2780
|
} finally {
|
|
2735
2781
|
unmarkStuckElements(marked);
|
|
2736
2782
|
}
|
|
@@ -2743,8 +2789,10 @@ async function captureRegion(rect, scroll, prefer = "auto") {
|
|
|
2743
2789
|
const sx = scroll?.x ?? window.scrollX;
|
|
2744
2790
|
const sy = scroll?.y ?? window.scrollY;
|
|
2745
2791
|
if (prefer !== "dom" && getExactCaptureStatus() === "live") {
|
|
2792
|
+
const visible = intersectViewport(rect);
|
|
2793
|
+
if (!visible) return { status: "empty" };
|
|
2746
2794
|
try {
|
|
2747
|
-
const canvas = await withOverlayHidden(() => grabExactRegion(
|
|
2795
|
+
const canvas = await withOverlayHidden(() => grabExactRegion(visible));
|
|
2748
2796
|
if (canvas) {
|
|
2749
2797
|
const blob = await encodeShot(canvas);
|
|
2750
2798
|
if (blob) return { status: "ok", blob, engine: "exact" };
|
|
@@ -9948,5 +9996,5 @@ function Qapture({ config }) {
|
|
|
9948
9996
|
}
|
|
9949
9997
|
|
|
9950
9998
|
export { Qapture, deleteQaDatabase, initQaStudio };
|
|
9951
|
-
//# sourceMappingURL=chunk-
|
|
9952
|
-
//# sourceMappingURL=chunk-
|
|
9999
|
+
//# sourceMappingURL=chunk-NVFFN7SC.js.map
|
|
10000
|
+
//# sourceMappingURL=chunk-NVFFN7SC.js.map
|
package/dist/index.cjs
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkCIFJMD2B_cjs = require('./chunk-CIFJMD2B.cjs');
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
Object.defineProperty(exports, "QaStudio", {
|
|
8
8
|
enumerable: true,
|
|
9
|
-
get: function () { return
|
|
9
|
+
get: function () { return chunkCIFJMD2B_cjs.Qapture; }
|
|
10
10
|
});
|
|
11
11
|
Object.defineProperty(exports, "Qapture", {
|
|
12
12
|
enumerable: true,
|
|
13
|
-
get: function () { return
|
|
13
|
+
get: function () { return chunkCIFJMD2B_cjs.Qapture; }
|
|
14
14
|
});
|
|
15
15
|
Object.defineProperty(exports, "deleteQaDatabase", {
|
|
16
16
|
enumerable: true,
|
|
17
|
-
get: function () { return
|
|
17
|
+
get: function () { return chunkCIFJMD2B_cjs.deleteQaDatabase; }
|
|
18
18
|
});
|
|
19
19
|
Object.defineProperty(exports, "initQaStudio", {
|
|
20
20
|
enumerable: true,
|
|
21
|
-
get: function () { return
|
|
21
|
+
get: function () { return chunkCIFJMD2B_cjs.initQaStudio; }
|
|
22
22
|
});
|
|
23
23
|
//# sourceMappingURL=index.cjs.map
|
|
24
24
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.js
CHANGED
package/dist/next.cjs
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var
|
|
4
|
+
var chunkCIFJMD2B_cjs = require('./chunk-CIFJMD2B.cjs');
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
Object.defineProperty(exports, "QaStudio", {
|
|
9
9
|
enumerable: true,
|
|
10
|
-
get: function () { return
|
|
10
|
+
get: function () { return chunkCIFJMD2B_cjs.Qapture; }
|
|
11
11
|
});
|
|
12
12
|
Object.defineProperty(exports, "Qapture", {
|
|
13
13
|
enumerable: true,
|
|
14
|
-
get: function () { return
|
|
14
|
+
get: function () { return chunkCIFJMD2B_cjs.Qapture; }
|
|
15
15
|
});
|
|
16
16
|
Object.defineProperty(exports, "initQaStudio", {
|
|
17
17
|
enumerable: true,
|
|
18
|
-
get: function () { return
|
|
18
|
+
get: function () { return chunkCIFJMD2B_cjs.initQaStudio; }
|
|
19
19
|
});
|
|
20
20
|
//# sourceMappingURL=next.cjs.map
|
|
21
21
|
//# sourceMappingURL=next.cjs.map
|
package/dist/next.js
CHANGED
package/dist/standalone.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkCIFJMD2B_cjs = require('./chunk-CIFJMD2B.cjs');
|
|
4
4
|
|
|
5
5
|
// src/standalone.ts
|
|
6
6
|
if (typeof window !== "undefined" && typeof customElements !== "undefined" && !customElements.get("qapture-widget")) {
|
|
@@ -38,7 +38,7 @@ if (typeof window !== "undefined" && typeof customElements !== "undefined" && !c
|
|
|
38
38
|
|
|
39
39
|
Object.defineProperty(exports, "initQaStudio", {
|
|
40
40
|
enumerable: true,
|
|
41
|
-
get: function () { return
|
|
41
|
+
get: function () { return chunkCIFJMD2B_cjs.initQaStudio; }
|
|
42
42
|
});
|
|
43
43
|
//# sourceMappingURL=standalone.cjs.map
|
|
44
44
|
//# sourceMappingURL=standalone.cjs.map
|
package/dist/standalone.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qapture2",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Drop-in, AI-aware in-browser QA capture widget. A tester annotates the live app (element/region + auto-screenshot + note), tracks a graded testing journey, and exports a ZIP your own coding agent reads. Ships zero AI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"standalone-smoke": "node scripts/standalone-smoke.mjs",
|
|
90
90
|
"browser-test": "node scripts/browser-test.mjs",
|
|
91
91
|
"capture-accuracy-test": "node scripts/capture-accuracy-test.mjs",
|
|
92
|
+
"element-capture-test": "node scripts/element-capture-test.mjs",
|
|
92
93
|
"loop-features-test": "node scripts/loop-features-test.mjs",
|
|
93
94
|
"walk-test": "node scripts/walk-test.mjs",
|
|
94
95
|
"modern-css-test": "node scripts/modern-css-test.mjs",
|