@archer-edu/dxp-directus-cli
Version:
323 lines • 15.1 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.runGhostInspector = void 0;
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const DEFAULT_API_BASE_URL = "https://api.ghostinspector.com/v1";
const DEFAULT_POLL_INTERVAL = 30;
const DEFAULT_MAX_ATTEMPTS = 40;
const RESULT_PAGE_SIZE = 50;
const register = (program) => {
program
.command("ghost-inspector-run [startUrl]")
.alias("gir")
.description("Runs the Ghost Inspector suite configured in package.json.")
.option("--api-key <apiKey>", "Ghost Inspector API key (or set DIRECTUS_CLI_GHOST_API_KEY)")
.option("--suite-id <suiteId>", "Ghost Inspector suite ID. Overrides package.json configuration.")
.option("--start-url <startUrl>", "Alternate start URL to use for this suite execution.")
.option("--poll-interval <pollInterval>", "Polling interval in seconds")
.option("--max-attempts <maxAttempts>", "Maximum number of polling attempts")
.option("--ignore-failures", "Exit successfully even when the Ghost Inspector suite fails")
.action((startUrl, options) => __awaiter(void 0, void 0, void 0, function* () {
try {
yield runGhostInspector({
apiKey: options.apiKey,
suiteId: options.suiteId,
startUrl: options.startUrl || startUrl,
pollInterval: options.pollInterval,
maxAttempts: options.maxAttempts,
ignoreFailures: options.ignoreFailures,
});
}
catch (error) {
console.error(error.message);
process.exitCode = 1;
}
}));
};
exports.default = register;
function runGhostInspector(options, dependencies = {}) {
return __awaiter(this, void 0, void 0, function* () {
const deps = resolveDependencies(dependencies);
const resolved = resolveOptions(options, deps.env, deps.cwd);
deps.logger(`Triggering Ghost Inspector suite ${resolved.suiteId}...`);
const suiteResultId = yield executeSuite(resolved, deps.fetch);
deps.logger(`Ghost Inspector suite started. Suite result ID: ${suiteResultId}`);
deps.logger(`Polling every ${resolved.pollInterval}s for up to ${formatMinutes(resolved.pollInterval, resolved.maxAttempts)} minutes...`);
for (let attempt = 1; attempt <= resolved.maxAttempts; attempt++) {
yield deps.sleep(resolved.pollInterval * 1000);
deps.logger(`Attempt ${attempt}/${resolved.maxAttempts}...`);
const result = yield getSuiteResult(resolved, suiteResultId, deps.fetch);
if (!isSuiteResultComplete(result)) {
deps.logger(formatRunningStatus(result));
continue;
}
deps.logger(formatCompletedStatus(result));
if (result.passing !== true && result.passing !== false) {
throw new Error(`Ghost Inspector suite did not pass: ${formatSuiteResultUrl(suiteResultId)}`);
}
const failedResults = result.passing === false
? yield fetchFailedResults(resolved, suiteResultId, deps.fetch, deps.logger)
: [];
const ignoredFailure = result.passing === false && resolved.ignoreFailures;
if (!result.passing && !resolved.ignoreFailures) {
throw new Error(`Ghost Inspector suite failed: ${formatSuiteResultUrl(suiteResultId)}`);
}
if (ignoredFailure) {
deps.logger("Ghost Inspector suite failed, but failures are ignored.");
}
return {
suiteId: resolved.suiteId,
suiteResultId,
passing: result.passing,
ignoredFailure,
result,
failedResults,
};
}
throw new Error(`Timed out waiting for Ghost Inspector suite result ${suiteResultId} after ${formatMinutes(resolved.pollInterval, resolved.maxAttempts)} minutes.`);
});
}
exports.runGhostInspector = runGhostInspector;
function resolveDependencies(dependencies) {
return {
fetch: dependencies.fetch || defaultFetch,
sleep: dependencies.sleep || sleep,
logger: dependencies.logger || console.log,
env: dependencies.env || process.env,
cwd: dependencies.cwd || process.cwd(),
};
}
function defaultFetch(url, init) {
return __awaiter(this, void 0, void 0, function* () {
if (typeof fetch !== "function") {
throw new Error("Global fetch is required to call Ghost Inspector. Use Node 18 or newer.");
}
return yield fetch(url, init);
});
}
function resolveOptions(options, env, cwd) {
const apiKey = options.apiKey || env.DIRECTUS_CLI_GHOST_API_KEY;
if (!apiKey) {
throw new Error("A Ghost Inspector API key is required. Pass --api-key or set DIRECTUS_CLI_GHOST_API_KEY.");
}
const suiteId = resolveSuiteId(options, env, cwd);
const pollInterval = parsePositiveInteger(options.pollInterval || getCliEnv(env, "GHOST_INSPECTOR_POLL_INTERVAL"), DEFAULT_POLL_INTERVAL, "poll-interval");
const maxAttempts = parsePositiveInteger(options.maxAttempts || getCliEnv(env, "GHOST_INSPECTOR_MAX_ATTEMPTS"), DEFAULT_MAX_ATTEMPTS, "max-attempts");
return {
apiKey,
suiteId,
startUrl: normalizeOptionalString(options.startUrl || getCliEnv(env, "GHOST_INSPECTOR_START_URL")),
pollInterval,
maxAttempts,
ignoreFailures: resolveIgnoreFailures(options.ignoreFailures, env),
apiBaseUrl: trimTrailingSlash(options.apiBaseUrl || getCliEnv(env, "GHOST_INSPECTOR_API_BASE_URL") || DEFAULT_API_BASE_URL),
};
}
function resolveSuiteId(options, env, cwd) {
const suiteId = options.suiteId || getCliEnv(env, "GHOST_INSPECTOR_SUITE_ID") || getPackageEnvSuiteId(env) || getPackageJsonSuiteId(cwd);
if (suiteId)
return suiteId;
throw new Error("A Ghost Inspector suite ID is required. Pass --suite-id, set DIRECTUS_CLI_GHOST_INSPECTOR_SUITE_ID, or add ghostInspectorSuiteId to package.json.");
}
function getPackageEnvSuiteId(env) {
return env.npm_package_ghostInspectorSuiteId ||
env.npm_package_ghostinspectorSuiteId ||
env.npm_package_ghost_inspector_suite_id ||
env.npm_package_ghostInspector_suiteId ||
env.npm_package_ghostInspector_suite_id;
}
function getPackageJsonSuiteId(cwd) {
const packagePath = path_1.default.resolve(cwd, "package.json");
if (!(0, fs_1.existsSync)(packagePath))
return undefined;
const packageJson = JSON.parse((0, fs_1.readFileSync)(packagePath, "utf8"));
if (!isRecord(packageJson))
return undefined;
const directSuiteId = normalizeSuiteEntry(packageJson);
if (directSuiteId)
return directSuiteId;
const ghostInspector = packageJson.ghostInspector || packageJson.ghost_inspector;
if (ghostInspector) {
return normalizeSuiteEntry(ghostInspector);
}
return undefined;
}
function normalizeSuiteEntry(entry) {
if (typeof entry === "string" && entry.trim())
return entry.trim();
if (!isRecord(entry))
return undefined;
const suiteId = entry.suiteId || entry.ghostInspectorSuiteId || entry.ghost_inspector_suite_id;
return typeof suiteId === "string" && suiteId.trim() ? suiteId.trim() : undefined;
}
function executeSuite(options, fetcher) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield fetcher(`${options.apiBaseUrl}/suites/${encodeURIComponent(options.suiteId)}/execute/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(getExecuteSuiteBody(options)),
});
const body = yield readApiResponse(response, "execute suite");
const suiteResultId = extractSuiteResultId(body.data);
if (!suiteResultId) {
throw new Error(`Ghost Inspector did not return a suite result ID: ${JSON.stringify(body.data)}`);
}
return suiteResultId;
});
}
function getExecuteSuiteBody(options) {
return Object.assign({ apiKey: options.apiKey, immediate: 1 }, (options.startUrl ? { startUrl: options.startUrl } : {}));
}
function getSuiteResult(options, suiteResultId, fetcher) {
return __awaiter(this, void 0, void 0, function* () {
const response = yield fetcher(`${options.apiBaseUrl}/suite-results/${encodeURIComponent(suiteResultId)}/?${apiKeyQuery(options.apiKey)}`);
const body = yield readApiResponse(response, "get suite result");
if (!body.data) {
throw new Error(`Ghost Inspector suite result response did not include data for ${suiteResultId}.`);
}
return body.data;
});
}
function fetchFailedResults(options, suiteResultId, fetcher, logger) {
return __awaiter(this, void 0, void 0, function* () {
const results = [];
let offset = 0;
let hasMore = true;
while (hasMore) {
const params = new URLSearchParams({
apiKey: options.apiKey,
count: RESULT_PAGE_SIZE.toString(),
offset: offset.toString(),
});
const response = yield fetcher(`${options.apiBaseUrl}/suite-results/${encodeURIComponent(suiteResultId)}/results/?${params.toString()}`);
const body = yield readApiResponse(response, "list suite result tests");
const page = body.data || [];
results.push(...page);
hasMore = page.length === RESULT_PAGE_SIZE;
if (hasMore)
offset += RESULT_PAGE_SIZE;
}
const failedResults = results.filter(result => result.passing === false);
if (failedResults.length > 0) {
logger("Failed Ghost Inspector tests:");
failedResults.forEach(result => {
logger(`- ${formatTestResultName(result)}: ${formatTestResultUrl(result)}`);
});
}
return failedResults;
});
}
function readApiResponse(response, label) {
return __awaiter(this, void 0, void 0, function* () {
const text = yield response.text();
let body;
try {
body = JSON.parse(text);
}
catch (_error) {
throw new Error(`Ghost Inspector ${label} request returned non-JSON content (${response.status} ${response.statusText}).`);
}
if (!response.ok) {
throw new Error(`Ghost Inspector ${label} request failed (${response.status} ${response.statusText}): ${JSON.stringify(body)}`);
}
if (body.code && body.code !== "SUCCESS") {
throw new Error(`Ghost Inspector ${label} request failed: ${body.message || body.error || body.code}`);
}
return body;
});
}
function extractSuiteResultId(data) {
if (!data)
return undefined;
const result = Array.isArray(data) ? data[0] : data;
if (!result)
return undefined;
return result._id || ("suiteResult" in result && typeof result.suiteResult === "string" ? result.suiteResult : undefined);
}
function isSuiteResultComplete(result) {
return Boolean(result.dateExecutionFinished) || typeof result.passing === "boolean";
}
function formatRunningStatus(result) {
var _a, _b, _c;
const passing = (_a = result.countPassing) !== null && _a !== void 0 ? _a : 0;
const failing = (_b = result.countFailing) !== null && _b !== void 0 ? _b : 0;
const unknown = (_c = result.countUnknown) !== null && _c !== void 0 ? _c : 0;
return `Status: running | passing: ${passing} | failing: ${failing} | unknown: ${unknown}`;
}
function formatCompletedStatus(result) {
var _a, _b, _c, _d, _e, _f;
return [
`Ghost Inspector suite completed: ${result.name || result._id}`,
`passing=${String(result.passing)}`,
`tests ${(_a = result.countPassing) !== null && _a !== void 0 ? _a : 0} passed/${(_b = result.countFailing) !== null && _b !== void 0 ? _b : 0} failed/${(_c = result.countUnknown) !== null && _c !== void 0 ? _c : 0} unknown`,
`screenshots ${(_d = result.countScreenshotComparePassing) !== null && _d !== void 0 ? _d : 0} passed/${(_e = result.countScreenshotCompareFailing) !== null && _e !== void 0 ? _e : 0} failed/${(_f = result.countScreenshotCompareUnknown) !== null && _f !== void 0 ? _f : 0} unknown`,
].join(" | ");
}
function formatTestResultName(result) {
return result.name || result.testName || result._id;
}
function formatTestResultUrl(result) {
return result.url || `https://app.ghostinspector.com/results/${result._id}`;
}
function formatSuiteResultUrl(suiteResultId) {
return `https://app.ghostinspector.com/suite-results/${suiteResultId}`;
}
function apiKeyQuery(apiKey) {
return new URLSearchParams({ apiKey }).toString();
}
function formatMinutes(pollInterval, maxAttempts) {
return ((pollInterval * maxAttempts) / 60).toFixed(1);
}
function parsePositiveInteger(value, defaultValue, name) {
if (value == null || value === "")
return defaultValue;
const parsedValue = typeof value === "number" ? value : parseInt(value, 10);
if (!Number.isInteger(parsedValue) || parsedValue < 1) {
throw new Error(`A positive integer is required for ${name}.`);
}
return parsedValue;
}
function resolveIgnoreFailures(value, env) {
if (value != null)
return value;
return parseBoolean(getCliEnv(env, "GHOST_INSPECTOR_IGNORE_FAILURES"));
}
function parseBoolean(value) {
if (!value)
return false;
return ["1", "true", "yes", "y", "on"].includes(value.toLowerCase());
}
function normalizeOptionalString(value) {
if (!value)
return undefined;
const trimmed = value.trim();
return trimmed || undefined;
}
function getCliEnv(env, name) {
return env[`DIRECTUS_CLI_${name}`] || env[`DIRECUTS_CLI_${name}`];
}
function trimTrailingSlash(value) {
return value.replace(/\/+$/, "");
}
function isRecord(value) {
return typeof value === "object" && value !== null;
}
function sleep(milliseconds) {
return new Promise(resolve => setTimeout(resolve, milliseconds));
}
//# sourceMappingURL=ghost-inspector-run.js.map