UNPKG

@archer-edu/dxp-directus-cli

Version:
323 lines 15.1 kB
"use strict"; 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