diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..83210e8 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,35 @@ +name: PR Check + +on: + pull_request: + branches: + - main + +jobs: + test-and-lint: + name: Test and Format Check + runs-on: ubuntu-latest + steps: + - name: Checkout Repo + uses: actions/checkout@v3 + + - name: Enable Corepack + run: corepack enable + + - name: Setup Node.js 20.x + uses: actions/setup-node@v3 + with: + node-version: 20.x + + - name: Install Dependencies + run: yarn + + - name: Run Tests and Coverage + run: | + yarn test + yarn test:coverage + + - name: Check Formatting + run: | + # Run format check on all workspaces + yarn workspaces foreach --all run format --check \ No newline at end of file diff --git a/.gitignore b/.gitignore index ba3856f..40c8749 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,8 @@ #!.yarn/cache .pnp.* -dist +**/*/dist +**/*/coverage /.idea/ /packages/sdk/package.tgz node_modules diff --git a/package.json b/package.json index fcec745..992e53c 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,25 @@ "packageManager": "yarn@4.2.2", "scripts": { "build": "yarn workspaces foreach --all run build", - "lint": "yarn workspaces foreach --all run lint", - "release": "yarn build && yarn changeset publish" + "test": "cd packages/sdk && NODE_OPTIONS='--no-warnings --experimental-vm-modules' yarn node $(yarn bin jest) --config jest.config.js", + "release": "yarn build && yarn changeset publish", + "format": "yarn workspaces foreach --all run format", + "clean": "yarn workspaces foreach --all run clean && rm -rf .yarn .pnp.cjs .pnp.loader.mjs" }, "workspaces": [ "packages/*" ], "dependencies": { - "@changesets/cli": "^2.27.8" + "@changesets/cli": "^2.27.8", + "typescript": "^5.7.2" + }, + "devDependencies": { + "@babel/core": "^7.26.0", + "@babel/preset-env": "^7.26.0", + "@babel/preset-typescript": "^7.26.0", + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.11", + "jest": "^29.7.0", + "prettier": "^3.3.3" } } diff --git a/packages/sdk/babel.config.js b/packages/sdk/babel.config.js new file mode 100644 index 0000000..a0b8524 --- /dev/null +++ b/packages/sdk/babel.config.js @@ -0,0 +1,6 @@ +module.exports = { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + '@babel/preset-typescript', + ], +}; \ No newline at end of file diff --git a/packages/sdk/jest.config.js b/packages/sdk/jest.config.js new file mode 100644 index 0000000..862c19d --- /dev/null +++ b/packages/sdk/jest.config.js @@ -0,0 +1,21 @@ +/** @type {import('jest').Config} */ +const config = { + testEnvironment: "jsdom", + transform: { + "^.+\\.(t|j)sx?$": "babel-jest", + }, + extensionsToTreatAsEsm: [".ts"], + moduleNameMapper: { + "^(\\.{1,2}/.*)\\.js$": "$1", + }, + coverageDirectory: "coverage", + collectCoverageFrom: [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/**/__tests__/**", + ], + testMatch: ["/src/**/__tests__/**/*.test.ts"], + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], +}; + +module.exports = config; diff --git a/packages/sdk/package.json b/packages/sdk/package.json index df1148b..059c6ca 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -13,20 +13,27 @@ }, "packageManager": "yarn@4.2.2", "scripts": { - "build": "yarn build:esm && yarn build:cjs", - "build:esm": "mkdir -p dist/esm && tsc -p tsconfig.json && echo '{\"type\":\"module\"}' > dist/esm/package.json", - "build:cjs": "mkdir -p dist/cjs && tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", - "lint": "ts-standard --project tsconfig.json", - "test": "vitest", - "arethetypeswrong": "yarn build && yarn pack && yarn dlx @arethetypeswrong/cli package.tgz" + "build": "tsc --build", + "test": "jest --config jest.config.js", + "test:watch": "jest --config jest.config.js --watch", + "test:coverage": "jest --config jest.config.js --coverage", + "format": "prettier --write .", + "clean": "rm -rf dist coverage node_modules" }, "packages": [ "packages/*" ], "devDependencies": { - "jsdom": "^25.0.1", - "ts-standard": "*", - "typescript": "^5.6.2", - "vitest": "^2.1.8" + "@babel/core": "^7.23.6", + "@babel/preset-env": "^7.23.6", + "@babel/preset-typescript": "^7.23.3", + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.11", + "@types/node": "^20.10.5", + "babel-jest": "^29.7.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "ts-jest": "^29.1.1", + "typescript": "^5.6.2" } } diff --git a/packages/sdk/src/__tests__/index.test.ts b/packages/sdk/src/__tests__/index.test.ts new file mode 100644 index 0000000..9d7c083 --- /dev/null +++ b/packages/sdk/src/__tests__/index.test.ts @@ -0,0 +1,132 @@ +import { + jest, + expect, + describe, + it, + beforeEach, + afterEach, +} from "@jest/globals"; +import { createPhantom, Position, CreatePhantomConfig } from "../index.js"; +import { SDK_URL } from "../constants.js"; + +describe("Position enum", () => { + it("should have the correct values", () => { + expect(Position.bottomRight).toBe("bottom-right"); + expect(Position.bottomLeft).toBe("bottom-left"); + expect(Position.topRight).toBe("top-right"); + expect(Position.topLeft).toBe("top-left"); + }); +}); + +describe("createPhantom", () => { + let mockContainer: HTMLDivElement; + let originalHead: HTMLElement | null; + + beforeEach(() => { + originalHead = document.head; + mockContainer = document.createElement("div"); + Object.defineProperty(document, "head", { + value: mockContainer, + writable: true, + }); + + jest.spyOn(document, "createElement"); + jest.spyOn(mockContainer, "insertBefore"); + jest.spyOn(mockContainer, "removeChild"); + }); + + afterEach(() => { + jest.restoreAllMocks(); + Object.defineProperty(document, "head", { + value: originalHead, + writable: true, + }); + }); + + it("should create script tag with default config", () => { + createPhantom(); + expect(document.createElement).toHaveBeenCalledWith("script"); + expect(mockContainer.insertBefore).toHaveBeenCalled(); + }); + + it("should add correct URL parameters based on config", () => { + const config: CreatePhantomConfig = { + zIndex: 999, + hideLauncherBeforeOnboarded: true, + colorScheme: "dark", + paddingBottom: 20, + paddingRight: 20, + paddingTop: 20, + paddingLeft: 20, + position: Position.bottomRight, + }; + + createPhantom(config); + + const scriptElement = ( + document.createElement as jest.MockedFunction< + typeof document.createElement + > + ).mock.results[0].value as HTMLScriptElement; + const srcUrl = new URL(scriptElement.src); + + expect(srcUrl.searchParams.get("zIndex")).toBe("999"); + expect(srcUrl.searchParams.get("hideLauncherBeforeOnboarded")).toBe("true"); + expect(srcUrl.searchParams.get("colorScheme")).toBe("dark"); + expect(srcUrl.searchParams.get("paddingBottom")).toBe("20"); + expect(srcUrl.searchParams.get("paddingRight")).toBe("20"); + expect(srcUrl.searchParams.get("paddingTop")).toBe("20"); + expect(srcUrl.searchParams.get("paddingLeft")).toBe("20"); + expect(srcUrl.searchParams.get("position")).toBe("bottom-right"); + }); + + it("should handle partial config", () => { + const config: CreatePhantomConfig = { + zIndex: 999, + position: Position.topRight, + }; + + createPhantom(config); + + const scriptElement = ( + document.createElement as jest.MockedFunction< + typeof document.createElement + > + ).mock.results[0].value as HTMLScriptElement; + const srcUrl = new URL(scriptElement.src); + + expect(srcUrl.searchParams.get("zIndex")).toBe("999"); + expect(srcUrl.searchParams.get("position")).toBe("top-right"); + expect(srcUrl.searchParams.get("colorScheme")).toBeNull(); + expect(srcUrl.searchParams.get("paddingBottom")).toBeNull(); + }); + + it("should set correct script attributes", () => { + createPhantom(); + const scriptElement = ( + document.createElement as jest.MockedFunction< + typeof document.createElement + > + ).mock.results[0].value as HTMLScriptElement; + expect(scriptElement.type).toBe("module"); + expect(scriptElement.src).toContain(SDK_URL); + }); + + it("should handle invalid config values gracefully", () => { + const config = { + zIndex: -1, + position: "invalid-position" as Position, + }; + + createPhantom(config as CreatePhantomConfig); + const scriptElement = ( + document.createElement as jest.MockedFunction< + typeof document.createElement + > + ).mock.results[0].value as HTMLScriptElement; + const srcUrl = new URL(scriptElement.src); + + expect(srcUrl.searchParams.get("zIndex")).toBe("-1"); + expect(srcUrl.searchParams.get("position")).toBe("invalid-position"); + }); +}); diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json index ba53a84..1baf6e1 100644 --- a/packages/sdk/tsconfig.json +++ b/packages/sdk/tsconfig.json @@ -1,23 +1,15 @@ { "compilerOptions": { - "outDir": "dist/esm", - "allowSyntheticDefaultImports": false, - "allowUnreachableCode": false, - "checkJs": false, - "declaration": true, - "declarationMap": true, - "esModuleInterop": false, // This is a viral option, do not enable https://www.semver-ts.org/#module-interop - "lib": ["es2020", "dom"], + "target": "es2020", "module": "esnext", - "noFallthroughCasesInSwitch": true, - "noImplicitAny": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noUncheckedIndexedAccess": false, - "noUnusedLocals": true, - "noUnusedParameters": true, - "sourceMap": true, + "moduleResolution": "node", + "esModuleInterop": true, "strict": true, - "target": "es2020" - } + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "**/__tests__/**"] } diff --git a/packages/sdk/tsconfig.test.json b/packages/sdk/tsconfig.test.json new file mode 100644 index 0000000..cd1301a --- /dev/null +++ b/packages/sdk/tsconfig.test.json @@ -0,0 +1,19 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["jest", "node"], + "esModuleInterop": true, + "isolatedModules": true, + "jsx": "react", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "target": "es2020", + "strict": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "src/**/__tests__/**/*.ts"], + "exclude": ["node_modules"] +}