diff --git a/.vscode/launch.json b/.vscode/launch.json index 5418960..40765a9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,35 +1,37 @@ { - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Jest All", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["--runInBand"], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest", - } - }, - { - "type": "node", - "request": "launch", - "name": "Jest Current File", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": [ - "${fileBasenameNoExtension}", - "--config", - "jest.config.js" - ], - "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest", - } + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Jest All", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "--runInBand" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest", } - ] - } \ No newline at end of file + }, + { + "type": "node", + "request": "launch", + "name": "Jest Current File", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "args": [ + "${fileBasenameNoExtension}", + "--config", + "jest.config.js" + ], + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "disableOptimisticBPs": true, + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest", + } + } + ] +} \ No newline at end of file diff --git a/examples/1.png b/examples/1.png deleted file mode 100644 index 84ba207..0000000 Binary files a/examples/1.png and /dev/null differ diff --git a/examples/2.png b/examples/2.png deleted file mode 100644 index f558403..0000000 Binary files a/examples/2.png and /dev/null differ diff --git a/examples/example.spec.ts b/examples/example.spec.ts deleted file mode 100644 index c92b127..0000000 --- a/examples/example.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { VisualRegressionTracker, Config } from "../lib"; -import { readFileSync } from "fs"; - -describe("asd", () => { - const config: Config = { - apiUrl: "http://localhost:4200", - branchName: "master", - project: "Test", - apiKey: "RE85600GVC4A2WHZYR4Z4HA1NFDT", - }; - const vrt = new VisualRegressionTracker(config); - - it("test 2", async () => { - const testResult = await vrt.track({ - name: "Example 2", - // buildId: buildId, - imageBase64: new Buffer(readFileSync("examples/1.png")).toString( - "base64" - ), - os: "Windows", - browser: "Chrome", - viewport: "800x600", - device: "PC", - diffTollerancePercent: 0, - }); - - console.log(testResult); - }); - - it("test 1", async () => { - const testResult = await vrt.track({ - name: "Example 1", - imageBase64: new Buffer(readFileSync("examples/2.png")).toString( - "base64" - ), - os: "Windows", - // browser: "Chrome", - // viewport: "800x600", - // device: "PC", - }); - - console.log(testResult); - }); - - it("test 1 chrome", async () => { - const testResult = await vrt.track({ - name: "Example 1", - imageBase64: new Buffer(readFileSync("examples/1.png")).toString( - "base64" - ), - os: "Windows", - browser: "Chrome", - // viewport: "800x600", - // device: "PC", - diffTollerancePercent: 10.5, - }); - - console.log(testResult); - }); -}); diff --git a/jest.config.js b/jest.config.js index 91a2d2c..a93551f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,4 +1,4 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', -}; \ No newline at end of file + preset: "ts-jest", + testEnvironment: "node", +}; diff --git a/lib/index.ts b/lib/index.ts index a987af6..45f560e 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -1,2 +1,2 @@ -export * from './visualRegressionTracker' -export * from './types' \ No newline at end of file +export * from "./visualRegressionTracker"; +export * from "./types"; diff --git a/lib/types/index.ts b/lib/types/index.ts index 85db895..023462d 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -1,5 +1,5 @@ -export * from './build' -export * from './config' -export * from './testRun' -export * from './testRunResult' -export * from './testRunStatus' \ No newline at end of file +export * from "./build"; +export * from "./config"; +export * from "./testRun"; +export * from "./testRunResult"; +export * from "./testRunStatus"; diff --git a/lib/types/testRunStatus.ts b/lib/types/testRunStatus.ts index b8b7a68..1ff7d81 100644 --- a/lib/types/testRunStatus.ts +++ b/lib/types/testRunStatus.ts @@ -1,5 +1,5 @@ export enum TestRunStatus { - new = 'new', - ok = 'ok', - unresolved = 'unresolved', -} \ No newline at end of file + new = "new", + ok = "ok", + unresolved = "unresolved", +} diff --git a/lib/visualRegressionTracker.spec.ts b/lib/visualRegressionTracker.spec.ts new file mode 100644 index 0000000..b64577a --- /dev/null +++ b/lib/visualRegressionTracker.spec.ts @@ -0,0 +1,341 @@ +import { VisualRegressionTracker } from "./visualRegressionTracker"; +import { Config, Build, TestRun, TestRunResult, TestRunStatus } from "./types"; +import { mocked } from "ts-jest/utils"; +import axios, { AxiosError, AxiosResponse } from "axios"; + +jest.mock("axios"); +const mockedAxios = mocked(axios, true); + +describe("VisualRegressionTracker", () => { + let vrt: VisualRegressionTracker; + const config: Config = { + apiUrl: "http://localhost:4200", + branchName: "develop", + project: "Default project", + apiKey: "CPKVK4JNK24NVNPNGVFQ853HXXEG", + }; + + beforeEach(async () => { + vrt = new VisualRegressionTracker(config); + }); + + afterEach(async () => { + mockedAxios.post.mockReset(); + }); + + describe("track", () => { + const testRun: TestRun = { + name: "name", + imageBase64: "iamge", + os: "os", + device: "device", + viewport: "viewport", + browser: "browser", + }; + + it("should track success", async () => { + const testRunResult: TestRunResult = { + url: "url", + status: TestRunStatus.ok, + pixelMisMatchCount: 12, + diffPercent: 0.12, + diffTollerancePercent: 0, + }; + vrt["startBuild"] = jest.fn(); + vrt["submitTestResult"] = jest.fn().mockResolvedValueOnce(testRunResult); + + await vrt.track(testRun); + + expect(vrt["startBuild"]).toHaveBeenCalled(); + expect(vrt["submitTestResult"]).toHaveBeenCalledWith(testRun); + }); + + it("should track no baseline", async () => { + const testRunResult: TestRunResult = { + url: "url", + status: TestRunStatus.new, + pixelMisMatchCount: 12, + diffPercent: 0.12, + diffTollerancePercent: 0, + }; + vrt["startBuild"] = jest.fn(); + vrt["submitTestResult"] = jest.fn().mockResolvedValueOnce(testRunResult); + + await expect(vrt.track(testRun)).rejects.toThrowError( + new Error("No baseline: " + testRunResult.url) + ); + }); + + it("should track difference", async () => { + const testRunResult: TestRunResult = { + url: "url", + status: TestRunStatus.unresolved, + pixelMisMatchCount: 12, + diffPercent: 0.12, + diffTollerancePercent: 0, + }; + vrt["startBuild"] = jest.fn(); + vrt["submitTestResult"] = jest.fn().mockResolvedValueOnce(testRunResult); + + await expect(vrt.track(testRun)).rejects.toThrowError( + new Error("Difference found: " + testRunResult.url) + ); + }); + }); + + describe("startBuild", () => { + test("shouldStartBuild", async () => { + const buildId = "1312"; + const projectId = "asd"; + const build: Build = { + id: buildId, + projectId: projectId, + }; + mockedAxios.post.mockResolvedValueOnce({ data: build }); + + await vrt["startBuild"](); + + expect(mockedAxios.post).toHaveBeenCalledWith( + `${config.apiUrl}/builds`, + { + branchName: config.branchName, + project: config.project, + }, + { + headers: { + apiKey: config.apiKey, + }, + } + ); + expect(vrt.buildId).toBe(buildId); + expect(vrt.projectId).toBe(projectId); + }); + + test("should throw if no build Id", async () => { + const build = { + id: null, + projectId: "projectId", + }; + mockedAxios.post.mockResolvedValueOnce({ data: build }); + + await expect(vrt["startBuild"]()).rejects.toThrowError( + new Error("Build id is not defined") + ); + }); + + test("should throw if no project Id", async () => { + const build = { + id: "asd", + projectId: null, + }; + mockedAxios.post.mockResolvedValueOnce({ data: build }); + + await expect(vrt["startBuild"]()).rejects.toThrowError( + new Error("Project id is not defined") + ); + }); + + test("should handle exception", async () => { + const error: AxiosError = { + isAxiosError: true, + config: {}, + toJSON: jest.fn(), + name: "", + message: "", + response: { + status: 404, + data: {}, + statusText: "Not found", + headers: {}, + config: {}, + }, + }; + const handleExceptionMock = jest.fn(); + vrt["handleException"] = handleExceptionMock; + mockedAxios.post.mockRejectedValueOnce(error); + + try { + await vrt["startBuild"](); + } catch {} + + expect(handleExceptionMock).toHaveBeenCalledWith(error); + }); + }); + + describe("submitTestResults", () => { + it("should submit test run", async () => { + const testRunResult: TestRunResult = { + url: "url", + status: TestRunStatus.unresolved, + pixelMisMatchCount: 12, + diffPercent: 0.12, + diffTollerancePercent: 0, + }; + const testRun: TestRun = { + name: "name", + imageBase64: "image", + os: "os", + device: "device", + viewport: "viewport", + browser: "browser", + }; + const buildId = "1312"; + const projectId = "asd"; + vrt.buildId = buildId; + vrt.projectId = projectId; + mockedAxios.post.mockResolvedValueOnce({ data: testRunResult }); + + const result = await vrt["submitTestResult"](testRun); + + expect(result).toBe(testRunResult); + expect(mockedAxios.post).toHaveBeenCalledWith( + `${config.apiUrl}/test-runs`, + { + buildId: buildId, + projectId: projectId, + branchName: config.branchName, + name: testRun.name, + imageBase64: testRun.imageBase64, + os: testRun.os, + device: testRun.device, + viewport: testRun.viewport, + browser: testRun.browser, + }, + { + headers: { + apiKey: config.apiKey, + }, + } + ); + }); + + it("should handle exception", async () => { + const error: AxiosError = { + isAxiosError: true, + config: {}, + toJSON: jest.fn(), + name: "", + message: "", + response: { + status: 404, + data: {}, + statusText: "Not found", + headers: {}, + config: {}, + }, + }; + const handleExceptionMock = jest.fn(); + vrt["handleException"] = handleExceptionMock; + mockedAxios.post.mockRejectedValueOnce(error); + + try { + await vrt["submitTestResult"]({ + name: "name", + imageBase64: "image", + }); + } catch {} + + expect(handleExceptionMock).toHaveBeenCalledWith(error); + }); + }); + + test("handleResponse", async () => { + const build: Build = { + id: "id", + projectId: "projectId", + }; + const response: AxiosResponse = { + data: build, + status: 201, + statusText: "Created", + config: {}, + headers: {}, + }; + + const result = await vrt["handleResponse"](response); + + expect(result).toBe(build); + }); + + describe("handleException", () => { + it("error 401", async () => { + const error: AxiosError = { + isAxiosError: true, + config: {}, + toJSON: jest.fn(), + name: "", + message: "", + response: { + status: 401, + data: {}, + statusText: "", + headers: {}, + config: {}, + }, + }; + + await expect(vrt["handleException"](error)).rejects.toBe("Unauthorized"); + }); + + it("error 403", async () => { + const error: AxiosError = { + isAxiosError: true, + config: {}, + toJSON: jest.fn(), + name: "", + message: "", + response: { + status: 403, + data: {}, + statusText: "", + headers: {}, + config: {}, + }, + }; + + await expect(vrt["handleException"](error)).rejects.toBe( + "Api key not authenticated" + ); + }); + + it("error 404", async () => { + const error: AxiosError = { + isAxiosError: true, + config: {}, + toJSON: jest.fn(), + name: "", + message: "", + response: { + status: 404, + data: {}, + statusText: "", + headers: {}, + config: {}, + }, + }; + + await expect(vrt["handleException"](error)).rejects.toBe( + "Project not found" + ); + }); + + it("unknown", async () => { + const error: AxiosError = { + isAxiosError: true, + config: {}, + toJSON: jest.fn(), + name: "asdas", + message: "Unknown error", + response: { + status: 500, + data: {}, + statusText: "Internal exception", + headers: {}, + config: {}, + }, + }; + + await expect(vrt["handleException"](error)).rejects.toBe(error.message); + }); + }); +}); diff --git a/lib/visualRegressionTracker.ts b/lib/visualRegressionTracker.ts index 0ab99bf..f20abf7 100644 --- a/lib/visualRegressionTracker.ts +++ b/lib/visualRegressionTracker.ts @@ -1,5 +1,5 @@ import { Config, Build, TestRun, TestRunResult, TestRunStatus } from "./types"; -import axios, { AxiosRequestConfig } from "axios"; +import axios, { AxiosRequestConfig, AxiosResponse, AxiosError } from "axios"; export class VisualRegressionTracker { config: Config; @@ -18,23 +18,26 @@ export class VisualRegressionTracker { private async startBuild() { if (!this.buildId) { - console.log("Starting new build"); const data = { branchName: this.config.branchName, project: this.config.project, }; - const build = await axios - .post(`${this.config.apiUrl}/builds`, data, this.axiosConfig) - .then(function (response) { - // handle success - return response.data; - }) - .catch(function (error) { - // handle error - return Promise.reject(error); - }); - this.buildId = build.id; - this.projectId = build.projectId; + + const build: Build = await axios + .post(`${this.config.apiUrl}/builds`, data, this.axiosConfig) + .then(this.handleResponse) + .catch(this.handleException); + + if (build.id) { + this.buildId = build.id; + } else { + throw new Error("Build id is not defined"); + } + if (build.projectId) { + this.projectId = build.projectId; + } else { + throw new Error("Project id is not defined"); + } } } @@ -45,16 +48,30 @@ export class VisualRegressionTracker { branchName: this.config.branchName, ...test, }; + return axios .post(`${this.config.apiUrl}/test-runs`, data, this.axiosConfig) - .then(function (response) { - // handle success - return response.data; - }) - .catch(function (error) { - // handle error - return Promise.reject(error); - }); + .then(this.handleResponse) + .catch(this.handleException); + } + + private async handleResponse(response: AxiosResponse) { + return response.data; + } + + private async handleException(error: AxiosError) { + const status = error.response?.status; + if (status === 401) { + return Promise.reject("Unauthorized"); + } + if (status === 403) { + return Promise.reject("Api key not authenticated"); + } + if (status === 404) { + return Promise.reject("Project not found"); + } + + return Promise.reject(error.message); } async track(test: TestRun) { diff --git a/package.json b/package.json index 98ab19f..dceb90f 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "types": "dist/index.d.ts", "scripts": { "test": "jest", + "test:cov": "jest --collectCoverage", "build": "./node_modules/.bin/tsc" }, "repository": {