screenkitten
Version:
A cross-platform Node.js library for taking screenshots on iOS simulators and Android devices/emulators
234 lines (228 loc) • 7.97 kB
JavaScript
// src/ios.ts
import execa from "execa";
// src/errors.ts
var ScreenkittenError = class extends Error {
constructor(message, options) {
super(message, options);
this.name = "ScreenkittenError";
}
};
var ScreenkittenDeviceNotFoundError = class extends ScreenkittenError {
constructor(deviceId, cause) {
super(`Device not found: ${deviceId}`, { cause });
this.name = "ScreenkittenDeviceNotFoundError";
}
};
var ScreenkittenXcrunNotFoundError = class extends ScreenkittenError {
constructor(xcrunPath, cause) {
super(`xcrun not found at path: ${xcrunPath}`, { cause });
this.name = "ScreenkittenXcrunNotFoundError";
}
};
var ScreenkittenAdbNotFoundError = class extends ScreenkittenError {
constructor(adbPath, cause) {
super(`adb not found at path: ${adbPath}`, { cause });
this.name = "ScreenkittenAdbNotFoundError";
}
};
var ScreenkittenIOSSimulatorError = class extends ScreenkittenError {
constructor(deviceId, cause) {
super(`iOS Simulator not available or not booted: ${deviceId}`, { cause });
this.name = "ScreenkittenIOSSimulatorError";
}
};
var ScreenkittenAndroidDeviceError = class extends ScreenkittenError {
constructor(deviceId, cause) {
super(`Android device/emulator not available: ${deviceId}`, { cause });
this.name = "ScreenkittenAndroidDeviceError";
}
};
var ScreenkittenFileWriteError = class extends ScreenkittenError {
constructor(outputPath, cause) {
super(`Failed to write screenshot file: ${outputPath}`, { cause });
this.name = "ScreenkittenFileWriteError";
}
};
var ScreenkittenOperationAbortedError = class extends ScreenkittenError {
constructor(operation = "operation") {
super(`${operation} was aborted`);
this.name = "ScreenkittenOperationAbortedError";
}
};
var ScreenkittenScreenshotFailedError = class extends ScreenkittenError {
constructor(platform, cause) {
super(`${platform.toUpperCase()} screenshot command failed`, { cause });
this.name = "ScreenkittenScreenshotFailedError";
}
};
var ScreenkittenInvalidTypeError = class extends ScreenkittenError {
constructor(type) {
super(`Invalid screenshot type: ${type}. Valid types are: png, jpeg`);
this.name = "ScreenkittenInvalidTypeError";
}
};
// src/utils.ts
import crypto from "node:crypto";
import path from "node:path";
import os from "node:os";
function doHandleError(handler, error, result) {
if (handler) {
if (handler === "throw") {
throw error;
} else if (handler === "ignore") {
return result;
} else {
handler(error);
return result;
}
}
throw error;
}
function createTempScreenshotPath(platform, extension) {
const timestamp = Date.now();
const uuid = crypto.randomUUID().slice(0, 8);
const filename = `${platform}-screenshot-${timestamp}-${uuid}.${extension}`;
return path.join(os.tmpdir(), filename);
}
// src/ios.ts
var ScreenkittenIOS = class {
xcrunPath;
options;
constructor(options) {
this.options = options;
this.xcrunPath = options.xcrunPath || "/usr/bin/xcrun";
}
async takeScreenshot(overrideOptions = {}) {
var _a;
const options = { ...this.options, ...overrideOptions };
const deviceId = options.deviceId || "booted";
const type = options.type || "png";
const display = options.display || "internal";
const mask = options.mask || "ignored";
const outputPath = options.outputPath || createTempScreenshotPath("ios", type);
const onError = options.onError || "throw";
if (type !== "png" && type !== "jpeg") {
const error = new ScreenkittenInvalidTypeError(type);
return doHandleError(onError, error, Promise.resolve(outputPath));
}
const args = [
"simctl",
"io",
deviceId,
"screenshot",
"--type",
type,
"--display",
display,
"--mask",
mask,
outputPath
];
try {
if ((_a = options.abortSignal) == null ? void 0 : _a.aborted) {
throw new ScreenkittenOperationAbortedError("iOS screenshot");
}
await execa(this.xcrunPath, args, {
signal: options.abortSignal
});
return outputPath;
} catch (error) {
const screenshotError = this._classifyError(error, deviceId);
return doHandleError(onError, screenshotError, Promise.resolve(outputPath));
}
}
_classifyError(error, deviceId) {
if (error instanceof Error) {
if (error.message.includes("ENOENT") || error.message.includes("command not found")) {
return new ScreenkittenXcrunNotFoundError(this.xcrunPath, error);
} else if (error.message.includes("Invalid device") || error.message.includes("device not found")) {
return new ScreenkittenIOSSimulatorError(deviceId, error);
} else if (error.message.includes("aborted") || error.name === "AbortError") {
return new ScreenkittenOperationAbortedError("iOS screenshot");
} else {
return new ScreenkittenScreenshotFailedError("ios", error);
}
} else {
return new ScreenkittenScreenshotFailedError("ios");
}
}
};
// src/android.ts
import fs from "node:fs/promises";
import execa2 from "execa";
var ScreenkittenAndroid = class {
adbPath;
options;
constructor(options) {
this.options = options;
this.adbPath = options.adbPath || "adb";
}
async takeScreenshot(overrideOptions = {}) {
var _a;
const options = { ...this.options, ...overrideOptions };
const deviceId = options.deviceId || "booted";
const outputPath = options.outputPath || createTempScreenshotPath("android", "png");
const onError = options.onError || "throw";
try {
if ((_a = options.abortSignal) == null ? void 0 : _a.aborted) {
throw new ScreenkittenOperationAbortedError("Android screenshot");
}
const args = deviceId === "booted" ? ["exec-out", "screencap", "-p"] : ["-s", deviceId, "exec-out", "screencap", "-p"];
const result = await execa2(this.adbPath, args, {
encoding: null,
// Return buffer for binary data
signal: options.abortSignal
});
await fs.writeFile(outputPath, result.stdout);
return outputPath;
} catch (error) {
const screenshotError = this._classifyError(error, deviceId, outputPath);
return doHandleError(onError, screenshotError, Promise.resolve(outputPath));
}
}
_classifyError(error, deviceId, outputPath) {
if (error instanceof Error) {
if (error.message.includes("ENOENT") || error.message.includes("command not found")) {
return new ScreenkittenAdbNotFoundError(this.adbPath, error);
} else if (error.message.includes("device not found") || error.message.includes("device offline")) {
return new ScreenkittenAndroidDeviceError(deviceId, error);
} else if (error.message.includes("aborted") || error.name === "AbortError") {
return new ScreenkittenOperationAbortedError("Android screenshot");
} else if (error.code === "ENOENT" || error.code === "EACCES") {
return new ScreenkittenFileWriteError(outputPath, error);
} else {
return new ScreenkittenScreenshotFailedError("android", error);
}
} else {
return new ScreenkittenScreenshotFailedError("android");
}
}
};
// src/index.ts
function screenkitten(options) {
switch (options.platform) {
case "ios": {
return new ScreenkittenIOS(options);
}
case "android": {
return new ScreenkittenAndroid(options);
}
default: {
throw new Error(`Unsupported platform: ${options.platform}`);
}
}
}
export {
ScreenkittenAdbNotFoundError,
ScreenkittenAndroidDeviceError,
ScreenkittenDeviceNotFoundError,
ScreenkittenError,
ScreenkittenFileWriteError,
ScreenkittenIOSSimulatorError,
ScreenkittenInvalidTypeError,
ScreenkittenOperationAbortedError,
ScreenkittenScreenshotFailedError,
ScreenkittenXcrunNotFoundError,
screenkitten
};
//# sourceMappingURL=index.mjs.map