diff --git a/package.json b/package.json index 10967af..25b6263 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@octomind/octomind", - "version": "1.0.4", + "version": "1.0.5", "description": "a command line client for octomind apis", "main": "./dist/index.js", "packageManager": "pnpm@9.15.6+sha512.139cab068fdf0b751268179ac5f909b5be72afb4a75c513d1905d151befc8977b593d3cf8671ed83d4d6637c5c94b98ffbce108125de4a5a27a31233601a99de", diff --git a/src/api.ts b/src/api.ts index 4e326fb..6532d38 100644 --- a/src/api.ts +++ b/src/api.ts @@ -17,6 +17,12 @@ import { SuccessResponse, Environment, TestReport, + GetNotificationsOptions, + Notification, + GetTestCaseOptions, + TestCase, + CreateDiscoveryOptions, + DiscoveryResponse, } from "./types"; const BASE_URL = "https://app.octomind.dev/api"; @@ -354,3 +360,133 @@ export const deleteEnvironment = async ( console.log("Environment deleted successfully!"); }; + +export const getNotifications = async ( + options: GetNotificationsOptions, +): Promise => { + if (!options.apiKey) { + console.error("API key is required"); + process.exit(1); + } + + const response = await apiCall( + "get", + `/apiKey/v2/test-targets/${options.testTargetId}/notifications`, + options.apiKey, + ); + + if (options.json) { + outputResult(response); + return; + } + + console.log("Notifications:"); + response.forEach((notification) => { + console.log(`\nID: ${notification.id}`); + console.log(`Type: ${notification.type}`); + console.log(`Created At: ${notification.createdAt}`); + if (notification.payload.testReportId) { + console.log(`Test Report ID: ${notification.payload.testReportId}`); + } + if (notification.payload.testCaseId) { + console.log(`Test Case ID: ${notification.payload.testCaseId}`); + } + if (notification.payload.failed !== undefined) { + console.log(`Failed: ${notification.payload.failed}`); + } + if (notification.ack) { + console.log(`Acknowledged: ${notification.ack}`); + } + }); +}; + +export const getTestCase = async ( + options: GetTestCaseOptions, +): Promise => { + if (!options.apiKey) { + console.error("API key is required"); + process.exit(1); + } + + const response = await apiCall( + "get", + `/apiKey/v2/test-targets/${options.testTargetId}/test-cases/${options.testCaseId}`, + options.apiKey, + ); + + if (options.json) { + outputResult(response); + return; + } + + console.log("Test Case Details:"); + console.log(`ID: ${response.id}`); + console.log(`Description: ${response.description}`); + console.log(`Status: ${response.status}`); + console.log(`Run Status: ${response.runStatus}`); + console.log(`Created At: ${response.createdAt}`); + console.log(`Updated At: ${response.updatedAt}`); + + if (response.elements.length > 0) { + console.log("\nElements:"); + response.elements.forEach((element, index) => { + console.log(`\nElement ${index + 1}:`); + if (element.interaction) { + console.log(` Action: ${element.interaction.action}`); + if (element.interaction.calledWith) { + console.log(` Called With: ${element.interaction.calledWith}`); + } + } + if (element.assertion) { + console.log(` Expectation: ${element.assertion.expectation}`); + if (element.assertion.calledWith) { + console.log(` Called With: ${element.assertion.calledWith}`); + } + } + console.log(" Selectors:"); + element.selectors.forEach((selector) => { + console.log(` - ${selector.selectorType}: ${selector.selector}`); + if (selector.options?.name) { + console.log(` Name: ${selector.options.name}`); + } + }); + }); + } +}; + +export const createDiscovery = async ( + options: CreateDiscoveryOptions, +): Promise => { + if (!options.apiKey) { + console.error("API key is required"); + process.exit(1); + } + + const requestBody = { + name: options.name, + prompt: options.prompt, + ...(options.entryPointUrlPath && { + entryPointUrlPath: options.entryPointUrlPath, + }), + ...(options.prerequisiteId && { prerequisiteId: options.prerequisiteId }), + ...(options.externalId && { externalId: options.externalId }), + ...(options.assignedTagIds && { assignedTagIds: options.assignedTagIds }), + ...(options.folderId && { folderId: options.folderId }), + }; + + const response = await apiCall( + "post", + `/apiKey/v2/test-targets/${options.testTargetId}/discoveries`, + options.apiKey, + requestBody, + ); + + if (options.json) { + outputResult(response); + return; + } + + console.log("Discovery created successfully!"); + console.log(`Discovery ID: ${response.discoveryId}`); + console.log(`Test Case ID: ${response.testCaseId}`); +}; diff --git a/src/cli.ts b/src/cli.ts index a8b93c8..a473769 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -10,6 +10,9 @@ import { registerLocation, unregisterLocation, updateEnvironment, + getNotifications, + getTestCase, + createDiscovery, } from "./api"; const apiKeyOption = new Option( @@ -119,5 +122,33 @@ export const buildCmd = (): Command => { .requiredOption("-t, --test-target-id ", "Test target ID") .requiredOption("-e, --environment-id ", "Environment ID") .action(deleteEnvironment); + + createCommandWithCommonOptions("notifications") + .description("Get notifications for a test target") + .requiredOption("-t, --test-target-id ", "Test target ID") + .action(getNotifications); + + createCommandWithCommonOptions("test-case") + .description("Get details of a specific test case") + .requiredOption("-t, --test-target-id ", "Test target ID") + .requiredOption("-c, --test-case-id ", "Test case ID") + .action(getTestCase); + + createCommandWithCommonOptions("create-discovery") + .description("Create a new test case discovery") + .requiredOption("-t, --test-target-id ", "Test target ID") + .requiredOption("-n, --name ", "Discovery name") + .requiredOption("-p, --prompt ", "Discovery prompt") + .option("-e, --entry-point-url-path ", "Entry point URL path") + .option("--prerequisite-id ", "Prerequisite test case ID") + .option("--external-id ", "External identifier") + .option( + "--assigned-tag-ids ", + "Comma-separated list of tag IDs", + splitter, + ) + .option("--folder-id ", "Folder ID") + .action(createDiscovery); + return program; }; diff --git a/src/types.ts b/src/types.ts index cba3100..503005f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -183,3 +183,155 @@ export interface DeleteEnvironmentOptions { environmentId: string; json?: boolean; } + +export interface Notification { + id: string; + testTargetId: string; + createdAt: string; + updatedAt: string; + payload: { + failed?: boolean; + context?: ExecutionContext; + testReportId?: string; + testCaseId?: string; + }; + type: "REPORT_EXECUTION_FINISHED" | "VALIDATION_PASSED"; + ack?: "IN_WEB_APP" | null; +} + +export interface TestCase { + id: string; + testTargetId: string; + type: string | null; + elements: Array<{ + id: string; + index: number; + interaction?: { + id: string; + action: + | "EXTRACT" + | "ENTER_TEXT" + | "CLICK" + | "SELECT_OPTION" + | "TYPE_TEXT" + | "KEY_PRESS" + | "HOVER" + | "UPLOAD" + | "GO_TO" + | "DRAG_AND_DROP" + | "CLOSE_PAGE" + | "OPEN_EMAIL"; + calledWith?: string | null; + testCaseElementId: string; + } | null; + assertion?: { + id: string; + expectation: + | "VISIBLE" + | "NOT_VISIBLE" + | "TO_BE_CHECKED" + | "NOT_TO_BE_CHECKED" + | "TO_HAVE_VALUE" + | "TO_CONTAIN_TEXT" + | "TO_HAVE_STYLE"; + calledWith?: string | null; + testCaseElementId: string; + } | null; + scrollState: null; + selectors: Array<{ + id: string; + index: number; + selector: string; + selectorType: "TEXT" | "LABEL" | "PLACEHOLDER" | "ROLE"; + options?: { name?: string } | null; + testCaseElementId: string; + scrollStateId: string | null; + }>; + testCaseId: string; + ignoreFailure: boolean; + }>; + createdAt: string; + updatedAt: string; + description: string; + status: "ENABLED" | "DRAFT"; + externalId: string | null; + entryPointUrlPath: string | null; + tags: string[]; + createdBy: "EDIT"; + runStatus: "ON" | "OFF"; + prerequisiteId: string | null; + proposalRunId: string | null; + folderId: string | null; + discovery?: { + id: string; + freePrompt: string; + traceUrl: string | null; + traceJsonManifestUrl: string | null; + status: "OUTDATED"; + abortCause: string | null; + message: string | null; + testCaseId: string; + lastJobExecutionName: string | null; + createdAt: string; + updatedAt: string; + executedTestCaseElements: string[]; + testCase: { + id: string; + testTargetId: string; + description: string; + createdAt: string; + updatedAt: string; + entryPointUrlPath: string | null; + type: string | null; + status: "ENABLED"; + runStatus: "ON"; + interactionStatus: "NEW"; + createdBy: "EDIT"; + proposalRunId: string | null; + externalId: string | null; + folderId: string | null; + prerequisiteId: string | null; + predecessorId: string; + testTarget: { + id: string; + app: string; + createdAt: string; + updatedAt: string; + orgId: string; + testIdAttribute: string | null; + timeoutPerStep: number; + }; + }; + }; +} + +export interface GetNotificationsOptions { + apiKey: string; + testTargetId: string; + json?: boolean; +} + +export interface GetTestCaseOptions { + apiKey: string; + testTargetId: string; + testCaseId: string; + json?: boolean; +} + +export interface CreateDiscoveryOptions { + apiKey: string; + testTargetId: string; + name: string; + prompt: string; + entryPointUrlPath?: string; + prerequisiteId?: string; + externalId?: string; + assignedTagIds?: string[]; + folderId?: string; + json?: boolean; +} + +export interface DiscoveryResponse { + discoveryId: string; + testCaseId: string; +} diff --git a/tests/api.spec.ts b/tests/api.spec.ts index 5669d04..d6ebe85 100644 --- a/tests/api.spec.ts +++ b/tests/api.spec.ts @@ -9,6 +9,9 @@ import { registerLocation, unregisterLocation, updateEnvironment, + getNotifications, + getTestCase, + createDiscovery, } from "../src/api"; import { CreateEnvironmentOptions, @@ -20,6 +23,9 @@ import { RegisterLocationOptions, UnregisterLocationOptions, UpdateEnvironmentOptions, + GetNotificationsOptions, + GetTestCaseOptions, + CreateDiscoveryOptions, } from "../src/types"; jest.mock("axios"); @@ -291,4 +297,151 @@ describe("CLI Commands", () => { }), ); }); + + it("getNotifications", async () => { + const options: GetNotificationsOptions = { + apiKey, + testTargetId: "test-target-id", + json: true, + }; + + mockedAxios.mockResolvedValue({ + data: [ + { + id: "notification1", + testTargetId: "test-target-id", + createdAt: "2025-03-23T10:58:23.479Z", + updatedAt: "2025-03-23T10:58:23.479Z", + payload: { + failed: false, + context: { + source: "manual", + description: "manual test run", + triggeredBy: { + type: "USER", + userId: "user-id", + }, + }, + testReportId: "report-id", + }, + type: "REPORT_EXECUTION_FINISHED", + ack: null, + }, + ], + }); + + await getNotifications(options); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: "get", + url: `${BASE_URL}/api/apiKey/v2/test-targets/test-target-id/notifications`, + headers: expect.objectContaining({ + "X-API-Key": apiKey, + "Content-Type": "application/json", + }), + }), + ); + }); + + it("getTestCase", async () => { + const options: GetTestCaseOptions = { + apiKey, + testTargetId: "test-target-id", + testCaseId: "test-case-id", + json: true, + }; + + mockedAxios.mockResolvedValue({ + data: { + id: "test-case-id", + testTargetId: "test-target-id", + type: null, + elements: [ + { + id: "element1", + index: 0, + interaction: { + id: "interaction1", + action: "CLICK", + testCaseElementId: "element1", + }, + selectors: [ + { + id: "selector1", + index: 0, + selector: "button", + selectorType: "ROLE", + options: { name: "Submit" }, + testCaseElementId: "element1", + scrollStateId: null, + }, + ], + testCaseId: "test-case-id", + ignoreFailure: false, + }, + ], + createdAt: "2025-03-20T21:44:49.186Z", + updatedAt: "2025-03-22T14:09:46.078Z", + description: "Test case description", + status: "ENABLED", + externalId: null, + entryPointUrlPath: null, + tags: [], + createdBy: "EDIT", + runStatus: "ON", + }, + }); + + await getTestCase(options); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: "get", + url: `${BASE_URL}/api/apiKey/v2/test-targets/test-target-id/test-cases/test-case-id`, + headers: expect.objectContaining({ + "X-API-Key": apiKey, + "Content-Type": "application/json", + }), + }), + ); + }); + + it("createDiscovery", async () => { + const options: CreateDiscoveryOptions = { + apiKey, + testTargetId: "test-target-id", + name: "discovery1", + prompt: "make sure current time is visible", + entryPointUrlPath: "/path", + assignedTagIds: ["tag1", "tag2"], + json: true, + }; + + mockedAxios.mockResolvedValue({ + data: { + discoveryId: "discovery-id", + testCaseId: "test-case-id", + }, + }); + + await createDiscovery(options); + + expect(mockedAxios).toHaveBeenCalledWith( + expect.objectContaining({ + method: "post", + url: `${BASE_URL}/api/apiKey/v2/test-targets/test-target-id/discoveries`, + data: expect.objectContaining({ + name: "discovery1", + prompt: "make sure current time is visible", + entryPointUrlPath: "/path", + assignedTagIds: ["tag1", "tag2"], + }), + headers: expect.objectContaining({ + "X-API-Key": apiKey, + "Content-Type": "application/json", + }), + }), + ); + }); });