Files
paseo/packages/app/e2e/mobile/clipboard-image.mjs
Mohamed Boudra d1b705a0cd fix(app): restore Android clipboard image pasting (#4758)
Decode clipboard image data URLs locally because native fetch rejects them. Add native-runtime and byte-integrity regression coverage, and clarify empty-clipboard guidance.
2026-09-12 09:50:25 +02:00

97 lines
3.9 KiB
JavaScript

// Run against a native dev app loaded from this checkout:
// node packages/app/e2e/mobile/clipboard-image.mjs <metro-port> <app-id>
// Exercises real native attachment persistence; Node's fetch(data:) hides this regression.
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { setTimeout as delay } from "node:timers/promises";
import { WebSocket } from "ws";
const [, , metroPort, appId] = process.argv;
assert(metroPort && appId, "Pass the Metro port and native app ID");
const targets = await fetch(`http://127.0.0.1:${Number(metroPort)}/json/list`).then((r) =>
r.json(),
);
const target = targets.find((candidate) => candidate.appId === appId);
assert(target, `Missing Hermes target for ${appId}`);
const png = await readFile(new URL("../../assets/images/icon.png", import.meta.url));
const base64 = png.toString("base64");
const socket = new WebSocket(target.webSocketDebuggerUrl);
await new Promise((resolve, reject) => {
socket.once("open", resolve);
socket.once("error", reject);
});
let nextId = 0;
function evaluate(expression) {
return new Promise((resolve, reject) => {
const id = ++nextId;
const timeout = setTimeout(() => {
socket.off("message", onMessage);
reject(new Error("Hermes evaluation timed out"));
}, 10000);
function onMessage(message) {
const response = JSON.parse(message.toString());
if (response.id !== id) return;
clearTimeout(timeout);
socket.off("message", onMessage);
if (response.error || response.result.exceptionDetails) {
reject(new Error(JSON.stringify(response.error ?? response.result.exceptionDetails)));
} else {
resolve(response.result.result.value);
}
}
socket.on("message", onMessage);
socket.send(
JSON.stringify({
id,
method: "Runtime.evaluate",
params: { expression, returnByValue: true },
}),
);
});
}
async function waitForNativeClipboardImage() {
const deadline = Date.now() + 15000;
do {
const result = await evaluate("globalThis.__clipboardImageTest");
if (result) return result;
await delay(100);
} while (Date.now() < deadline);
assert.fail("Native image persistence timed out");
}
try {
// Metro's debugger module table locates the same public service used by the composer.
// Hermes does not await promises through Runtime.evaluate, so collect the async result.
await evaluate(`(() => {
const entry = Array.from(__r.getModules().entries()).find(
([, module]) => module.verboseName === "src/attachments/service.ts"
);
if (!entry) throw new Error("Open the composer before running this test");
const service = __r(entry[0]);
globalThis.__clipboardImageTest = null;
service.persistAttachmentFromDataUrl({
dataUrl: ${JSON.stringify(`data:image/png;base64,${base64}`)},
fileName: "clipboard.png"
}).then(attachment => {
return Promise.all([
service.encodeAttachmentsForSend([attachment]),
service.resolveAttachmentPreviewUrl(attachment)
]).then(([encoded, preview]) => ({ attachment, encoded, preview }))
.finally(() => service.deleteAttachments([attachment]));
}).then(result => { globalThis.__clipboardImageTest = result; })
.catch(error => { globalThis.__clipboardImageTest = { error: String(error) }; });
})()`);
const result = await waitForNativeClipboardImage();
assert.equal(result.error, undefined, result.error);
assert.equal(result.attachment.storageType, "native-file");
assert.equal(result.attachment.byteSize, png.length);
assert.equal(result.attachment.fileName, "clipboard.png");
assert.match(result.preview, /^file:\/\//);
assert.deepEqual(result.encoded, [{ data: base64, mimeType: "image/png" }]);
console.log(`PASS: native clipboard PNG persisted, previewed, and encoded (${png.length} bytes)`);
} finally {
await evaluate("delete globalThis.__clipboardImageTest").finally(() => socket.close());
}