feat: adding fullstory sdk package files
This commit is contained in:
parent
4822e4502d
commit
47da5d782c
@ -1,11 +0,0 @@
|
||||
{
|
||||
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
|
||||
"env": {
|
||||
"node": true,
|
||||
"es2021": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2021,
|
||||
"sourceType": "module"
|
||||
}
|
||||
}
|
281
README.md
281
README.md
@ -1 +1,280 @@
|
||||
# fullstory-asset-sdk
|
||||
# FullStory Asset SDK
|
||||
|
||||
A lightweight SDK for integrating **FullStory session tracking and asset uploading** into React applications with CLI automation.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Installation Guide](#installation-guide)
|
||||
- [Usage Documentation](#usage-documentation)
|
||||
- [Configuration Options](#configuration-options)
|
||||
- [Implementation Examples](#implementation-examples)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Contributing Guidelines](#contributing-guidelines)
|
||||
- [Technical Details](#technical-details)
|
||||
|
||||
---
|
||||
|
||||
## Installation Guide
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js `>=14.0`
|
||||
- npm or yarn installed
|
||||
- A **FullStory API Key**
|
||||
- A valid **FullStory Organization ID** (`orgId`)
|
||||
|
||||
### Installation
|
||||
|
||||
You can install the package using `npm` or `yarn`:
|
||||
|
||||
```sh
|
||||
npm install fullstory-asset-sdk
|
||||
# or
|
||||
yarn add fullstory-asset-sdk
|
||||
```
|
||||
|
||||
This will install:
|
||||
|
||||
- A **React Provider Component** (`FullStoryProvider`) for easy integration.
|
||||
- A **CLI tool** (`fullstory-uploader`) for manual asset uploads.
|
||||
- A **cross-platform asset uploader** to manage FullStory assets.
|
||||
|
||||
---
|
||||
|
||||
## Usage Documentation
|
||||
|
||||
### 1. Using the FullStory Provider in a React App
|
||||
|
||||
To dynamically track sessions and manage FullStory, wrap your application with the `FullStoryProvider`:
|
||||
|
||||
```jsx
|
||||
import React from "react";
|
||||
import { FullStoryProvider } from "fullstory-asset-sdk";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<FullStoryProvider
|
||||
apiKey="fs-api-key-here"
|
||||
assetMapPath="path/to/asset_map.json"
|
||||
orgId="your-org-id"
|
||||
>
|
||||
<h1>My React App</h1>
|
||||
</FullStoryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
### 2. CLI Tool for Asset Uploading
|
||||
|
||||
The package includes a command-line interface (CLI) for manually uploading assets.
|
||||
|
||||
```sh
|
||||
npx fullstory-uploader -k YOUR_API_KEY -a path/to/asset-map.json
|
||||
```
|
||||
|
||||
#### CLI Options
|
||||
|
||||
| Option | Description |
|
||||
| ----------------------- | ------------------------------------------ |
|
||||
| `-k, --apiKey <key>` | FullStory API Key (required) |
|
||||
| `-a, --assetMap <path>` | Path to the Asset Map JSON file (required) |
|
||||
|
||||
This command **uploads assets** to FullStory, ensuring they are correctly mapped.
|
||||
|
||||
---
|
||||
|
||||
## Configuration Options
|
||||
|
||||
When using `FullStoryProvider`, you need to pass:
|
||||
|
||||
- `apiKey` (**Required**) - Your FullStory API Key
|
||||
- `orgId` (**Required**) - Your FullStory Organization ID
|
||||
- `assetMapPath` (**Required**) - Path to your asset map file
|
||||
|
||||
Example:
|
||||
|
||||
```jsx
|
||||
<FullStoryProvider apiKey="your-api-key" orgId="your-org-id" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Examples
|
||||
|
||||
### 1. JavaScript React Project
|
||||
|
||||
This package **fully supports JavaScript projects** even though it is written in TypeScript. You can directly use it in a `JS`-based React app.
|
||||
|
||||
```js
|
||||
import { FullStoryProvider } from "fullstory-asset-sdk";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<FullStoryProvider
|
||||
apiKey="fs-api-key-here"
|
||||
assetMapPath="path/to/asset_map.json"
|
||||
orgId="your-org-id"
|
||||
>
|
||||
<h1>My React App</h1>
|
||||
</FullStoryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
### 2. Using with Next.js
|
||||
|
||||
If using **Next.js**, import `FullStoryProvider` inside `_app.js`:
|
||||
|
||||
```js
|
||||
import { FullStoryProvider } from "fullstory-asset-sdk";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
return (
|
||||
<FullStoryProvider
|
||||
apiKey="fs-api-key-here"
|
||||
assetMapPath="path/to/asset_map.json"
|
||||
orgId="your-org-id"
|
||||
>
|
||||
<Component {...pageProps} />
|
||||
</FullStoryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default MyApp;
|
||||
```
|
||||
|
||||
### 3. Integrating with Webpack/Vite
|
||||
|
||||
For Webpack or Vite projects, **ensure the asset uploader runs before build**.
|
||||
|
||||
Example Webpack config:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
"process.env.FS_API_KEY": JSON.stringify(process.env.FS_API_KEY),
|
||||
}),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### 1. FullStory Not Loading
|
||||
|
||||
- Ensure the API key and `orgId` are **correct**.
|
||||
- Check the console for errors using:
|
||||
```sh
|
||||
npx fullstory-uploader -k YOUR_API_KEY -a path/to/asset-map.json
|
||||
```
|
||||
|
||||
#### 2. Asset Upload Fails
|
||||
|
||||
- Verify `assetMapPath` is correct.
|
||||
- Check internet connectivity.
|
||||
|
||||
#### 3. CLI Command Not Found
|
||||
|
||||
If the CLI tool isn’t recognized:
|
||||
|
||||
```sh
|
||||
npx fullstory-uploader --help
|
||||
```
|
||||
|
||||
or try reinstalling:
|
||||
|
||||
```sh
|
||||
npm install -g fullstory-asset-sdk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
fullstory-asset-sdk/
|
||||
│── src/
|
||||
│ ├── FullStoryProvider.tsx # React Provider Component
|
||||
│ ├── fullstoryUploader.ts # Asset upload logic
|
||||
│ ├── index.ts # Package entry point
|
||||
│ ├── cli.ts # CLI command script
|
||||
│ ├── generate-asset-map-id.js # Cross-platform asset uploader
|
||||
│── tests/ # Jest test files
|
||||
│── package.json # Package metadata & dependencies
|
||||
│── README.md # Documentation
|
||||
```
|
||||
|
||||
### Development Setup
|
||||
|
||||
To set up the project locally:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/CarGDev/fullstory-asset-sdk.git
|
||||
cd fullstory-asset-sdk
|
||||
npm install
|
||||
```
|
||||
|
||||
To run tests:
|
||||
|
||||
```sh
|
||||
npm test
|
||||
```
|
||||
|
||||
### Submitting Issues
|
||||
|
||||
Before opening an issue:
|
||||
|
||||
- **Check existing issues** to avoid duplicates.
|
||||
- Provide **steps to reproduce** the problem.
|
||||
- Include **error messages and logs**.
|
||||
|
||||
Create an issue here: [GitHub Issues](https://github.com/CarGDev/fullstory-asset-sdk/issues)
|
||||
|
||||
---
|
||||
|
||||
## Technical Details
|
||||
|
||||
### 1. How the Package Works
|
||||
|
||||
- **Cross-Platform Asset Uploading:**
|
||||
- The script (`generate-asset-map-id.js`) **detects OS** and downloads the correct FullStory asset uploader binary.
|
||||
- **Dynamic Asset Map Handling:**
|
||||
|
||||
- The provider **fetches the asset map** dynamically during app initialization.
|
||||
|
||||
- **Optimized Performance:**
|
||||
- The SDK only loads FullStory **when needed**.
|
||||
- Avoids reloading FullStory **on re-renders**.
|
||||
|
||||
### 2. Security Considerations
|
||||
|
||||
- API keys should **never be hardcoded**.
|
||||
- Use **environment variables** to store credentials.
|
||||
```sh
|
||||
export FS_API_KEY="your-api-key"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Final Notes
|
||||
|
||||
This SDK makes **FullStory integration seamless** by automating:
|
||||
|
||||
- **Session tracking**
|
||||
- **Asset management**
|
||||
- **React provider initialization**
|
||||
|
||||
For any issues or contributions, check:
|
||||
👉 **[GitHub Repository](https://github.com/CarGDev/fullstory-asset-sdk)**
|
||||
|
||||
|
42
bin/generate-asset-map-id.js
Normal file
42
bin/generate-asset-map-id.js
Normal file
@ -0,0 +1,42 @@
|
||||
const { execSync } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const os = require("os");
|
||||
|
||||
const fullStoryUrls = {
|
||||
win32:
|
||||
"https://downloads.fullstory.com/asset-uploader/fullstory-asset-uploader-windows.exe",
|
||||
darwin:
|
||||
"https://downloads.fullstory.com/asset-uploader/fullstory-asset-uploader-mac",
|
||||
linux:
|
||||
"https://downloads.fullstory.com/asset-uploader/fullstory-asset-uploader-linux",
|
||||
};
|
||||
|
||||
const currentOS = os.platform();
|
||||
const fullStoryUrl = fullStoryUrls[currentOS];
|
||||
|
||||
if (!fullStoryUrl) {
|
||||
console.error(`❌ Unsupported OS: ${currentOS}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const binDir = path.join(__dirname, "../bin");
|
||||
const outputFile = path.join(binDir, "fullstory-asset-uploader");
|
||||
|
||||
if (!fs.existsSync(binDir)) {
|
||||
fs.mkdirSync(binDir, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`⬇️ Downloading FullStory Asset Uploader for ${currentOS}...`);
|
||||
execSync(`curl -o ${outputFile} ${fullStoryUrl}`, { stdio: "inherit" });
|
||||
|
||||
if (currentOS !== "win32") {
|
||||
fs.chmodSync(outputFile, "755");
|
||||
}
|
||||
|
||||
console.log("✅ FullStory Asset Uploader installed successfully!");
|
||||
} catch (error) {
|
||||
console.error("❌ Failed to download FullStory Asset Uploader", error);
|
||||
process.exit(1);
|
||||
}
|
25
eslint.config.js
Normal file
25
eslint.config.js
Normal file
@ -0,0 +1,25 @@
|
||||
const js = require("@eslint/js");
|
||||
const ts = require("@typescript-eslint/eslint-plugin");
|
||||
const tsParser = require("@typescript-eslint/parser");
|
||||
|
||||
/** @type {import("eslint").Linter.FlatConfig[]} */
|
||||
module.exports = [
|
||||
js.configs.recommended,
|
||||
{
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
parserOptions: {
|
||||
project: "./tsconfig.json",
|
||||
tsconfigRootDir: process.cwd(),
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
"@typescript-eslint": ts,
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
|
14
jest.config.js
Normal file
14
jest.config.js
Normal file
@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
transform: {
|
||||
"^.+\\.tsx?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
useESM: true
|
||||
}
|
||||
]
|
||||
},
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
};
|
||||
|
1725
package-lock.json
generated
1725
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@ -28,14 +28,16 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rm -rf dist",
|
||||
"generate-asset-map": "node bin/generate-asset-map-id.js",
|
||||
"start": "npm run generate-asset-map && react-scripts start",
|
||||
"test": "jest --coverage",
|
||||
"lint": "eslint 'src/**/*.ts'",
|
||||
"prepare": "npm run build",
|
||||
"prepublishOnly": "npm run build"
|
||||
"prepare": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.13.4",
|
||||
"@types/react": "^19.0.10",
|
||||
"eslint": "^9.20.1",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
@ -46,6 +48,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fullstory/browser": "^2.0.6",
|
||||
"minimist": "^1.2.8"
|
||||
"@typescript-eslint/eslint-plugin": "^8.24.1",
|
||||
"@typescript-eslint/parser": "^8.24.1",
|
||||
"axios": "^1.7.9",
|
||||
"chalk": "^5.4.1",
|
||||
"commander": "^13.1.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"fs-extra": "^11.3.0",
|
||||
"minimist": "^1.2.8",
|
||||
"react": "^18.0.0",
|
||||
"ts-node": "^10.9.2"
|
||||
},
|
||||
"bin": {
|
||||
"fullstory-uploader": "dist/cli.js"
|
||||
}
|
||||
}
|
||||
|
79
src/FullStoryProvider.tsx
Normal file
79
src/FullStoryProvider.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
ReactNode,
|
||||
} from "react";
|
||||
import FullStoryUploader from "./fullstoryUploader";
|
||||
|
||||
// Define TypeScript interface for the context
|
||||
interface FullStoryContextProps {
|
||||
assetMapId: string | null;
|
||||
}
|
||||
|
||||
// Create a React Context with default values
|
||||
const FullStoryContext = createContext<FullStoryContextProps>({
|
||||
assetMapId: null,
|
||||
});
|
||||
|
||||
// Custom hook to access FullStory context
|
||||
export const useFullStory = () => useContext(FullStoryContext);
|
||||
|
||||
// Define props for the FullStoryProvider
|
||||
interface FullStoryProviderProps {
|
||||
apiKey: string;
|
||||
assetMapPath: string;
|
||||
orgId: string; // Mandatory orgId
|
||||
children: ReactNode; // ReactNode for proper children typing
|
||||
}
|
||||
|
||||
export const FullStoryProvider: React.FC<FullStoryProviderProps> = ({
|
||||
apiKey,
|
||||
assetMapPath,
|
||||
orgId,
|
||||
children,
|
||||
}) => {
|
||||
const [assetMapId, setAssetMapId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const uploader = new FullStoryUploader({ apiKey, assetMapPath });
|
||||
|
||||
uploader
|
||||
.uploadAssets()
|
||||
.then(() => {
|
||||
console.log("✅ FullStory Assets Uploaded Successfully");
|
||||
|
||||
// Example: Generate a dynamic asset_map_id
|
||||
const generatedId = Date.now().toString(36);
|
||||
setAssetMapId(generatedId);
|
||||
|
||||
// ✅ Define FullStory global variables with type assertions
|
||||
(window as any)._fs_host = "fullstory.com";
|
||||
(window as any)._fs_script = "edge.fullstory.com/s/fs.js";
|
||||
(window as any)._fs_namespace = "FS";
|
||||
(window as any)._fs_org = orgId;
|
||||
(window as any)._fs_asset_map_id = generatedId;
|
||||
|
||||
const script = document.createElement("script");
|
||||
script.src = `https://${(window as any)._fs_script}`;
|
||||
script.async = true;
|
||||
script.crossOrigin = "anonymous";
|
||||
script.onerror = () =>
|
||||
console.error("❌ Error loading FullStory script");
|
||||
|
||||
document.body.appendChild(script);
|
||||
|
||||
return () => document.body.removeChild(script);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("❌ FullStory Asset Upload Failed:", error);
|
||||
});
|
||||
}, [apiKey, assetMapPath, orgId]);
|
||||
|
||||
return (
|
||||
<FullStoryContext.Provider value={{ assetMapId }}>
|
||||
{children}
|
||||
</FullStoryContext.Provider>
|
||||
);
|
||||
};
|
29
src/cli.ts
Normal file
29
src/cli.ts
Normal file
@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env node
|
||||
import { program } from "commander";
|
||||
import * as dotenv from "dotenv";
|
||||
import FullStoryUploader from "./fullstoryUploader";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
program
|
||||
.version("0.0.1")
|
||||
.description("FullStory Asset Uploader CLI")
|
||||
.requiredOption("-k, --apiKey <key>", "FullStory API Key")
|
||||
.requiredOption("-a, --assetMap <path>", "Path to Asset Map JSON file")
|
||||
.action((options) => {
|
||||
const uploader = new FullStoryUploader({
|
||||
apiKey: options.apiKey,
|
||||
assetMapPath: options.assetMap,
|
||||
});
|
||||
|
||||
uploader.uploadAssets();
|
||||
});
|
||||
|
||||
program.configureOutput({
|
||||
writeErr: (str) => {
|
||||
console.error(str.trim());
|
||||
process.exit(1);
|
||||
},
|
||||
});
|
||||
|
||||
program.parse(process.argv);
|
49
src/fullstoryUploader.ts
Normal file
49
src/fullstoryUploader.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import fs from "fs-extra";
|
||||
import * as path from "path";
|
||||
import axios from "axios";
|
||||
|
||||
interface UploadOptions {
|
||||
apiKey: string;
|
||||
assetMapPath: string;
|
||||
}
|
||||
|
||||
class FullStoryUploader {
|
||||
private apiKey: string;
|
||||
private assetMapPath: string;
|
||||
|
||||
constructor(options: UploadOptions) {
|
||||
this.apiKey = options.apiKey;
|
||||
this.assetMapPath = options.assetMapPath;
|
||||
}
|
||||
|
||||
public async uploadAssets() {
|
||||
try {
|
||||
if (!fs.existsSync(this.assetMapPath)) {
|
||||
console.error(`❌ Asset map file not found: ${this.assetMapPath}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const assetMap = fs.readFileSync(this.assetMapPath, "utf-8");
|
||||
const response = await axios.post(
|
||||
"https://api.fullstory.com/assets/upload",
|
||||
JSON.parse(assetMap),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (response.status === 200) {
|
||||
console.log("✅ Assets uploaded successfully!");
|
||||
} else {
|
||||
console.error(`❌ Failed to upload assets. Status: ${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("❌ Error uploading assets", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FullStoryUploader;
|
@ -0,0 +1,6 @@
|
||||
import FullStoryUploader from "./fullstoryUploader";
|
||||
|
||||
export function uploadAssets(apiKey: string, assetMapPath: string) {
|
||||
const uploader = new FullStoryUploader({ apiKey, assetMapPath });
|
||||
return uploader.uploadAssets();
|
||||
}
|
196
tests/cli.test.ts
Normal file
196
tests/cli.test.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { jest } from "@jest/globals";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
// Create mock functions
|
||||
const mockUploadAssets = jest.fn();
|
||||
const mockFullStoryUploader = jest.fn().mockImplementation(() => ({
|
||||
uploadAssets: mockUploadAssets,
|
||||
}));
|
||||
|
||||
// Mock modules before importing the CLI
|
||||
jest.mock("../src/fullstoryUploader", () => ({
|
||||
__esModule: true,
|
||||
default: mockFullStoryUploader,
|
||||
}));
|
||||
|
||||
// Mock dotenv
|
||||
jest.mock("dotenv", () => ({
|
||||
config: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("FullStory Asset Uploader CLI", () => {
|
||||
const testAssetMapPath = path.resolve(__dirname, "test-asset-map.json");
|
||||
const testApiKey = "test-api-key";
|
||||
|
||||
// Create a test asset map file before tests
|
||||
beforeAll(() => {
|
||||
const testAssetMap = {
|
||||
assets: [
|
||||
{ id: "asset1", path: "./asset1.js" },
|
||||
{ id: "asset2", path: "./asset2.css" },
|
||||
],
|
||||
};
|
||||
|
||||
fs.writeFileSync(testAssetMapPath, JSON.stringify(testAssetMap, null, 2));
|
||||
});
|
||||
|
||||
// Clean up after tests
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(testAssetMapPath)) {
|
||||
fs.unlinkSync(testAssetMapPath);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear all mocks before each test
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Reset modules to ensure clean Commander instance for each test
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it("should exit with an error when missing required arguments", () => {
|
||||
// Mock console.error and process.exit
|
||||
const consoleSpy = jest
|
||||
.spyOn(console, "error")
|
||||
.mockImplementation(() => {});
|
||||
const exitSpy = jest.spyOn(process, "exit").mockImplementation((code) => {
|
||||
throw new Error(`process.exit(${code}) was called`);
|
||||
});
|
||||
|
||||
try {
|
||||
// Run CLI with no arguments
|
||||
require("../src/cli");
|
||||
} catch (e) {
|
||||
const error = e as Error;
|
||||
expect(error.message).toContain("process.exit(1)");
|
||||
}
|
||||
|
||||
// ✅ Ensure an error message was logged
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("error: required option"),
|
||||
);
|
||||
|
||||
// ✅ Ensure process.exit was called
|
||||
expect(exitSpy).toHaveBeenCalledWith(1);
|
||||
|
||||
// Restore original implementations
|
||||
consoleSpy.mockRestore();
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should initialize FullStoryUploader and call uploadAssets with correct options", () => {
|
||||
// Mock process.argv
|
||||
const originalArgv = process.argv;
|
||||
process.argv = [
|
||||
"node",
|
||||
"cli.js",
|
||||
"--apiKey",
|
||||
testApiKey,
|
||||
"--assetMap",
|
||||
testAssetMapPath,
|
||||
];
|
||||
|
||||
try {
|
||||
// Import CLI module which will execute the program
|
||||
require("../src/cli");
|
||||
|
||||
// Verify FullStoryUploader was constructed with correct params
|
||||
expect(mockFullStoryUploader).toHaveBeenCalledWith({
|
||||
apiKey: testApiKey,
|
||||
assetMapPath: testAssetMapPath,
|
||||
});
|
||||
|
||||
// Verify uploadAssets was called
|
||||
expect(mockUploadAssets).toHaveBeenCalled();
|
||||
} finally {
|
||||
// Restore original process.argv
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
});
|
||||
|
||||
it("should initialize FullStoryUploader with short flag options", () => {
|
||||
// Mock process.argv
|
||||
const originalArgv = process.argv;
|
||||
process.argv = ["node", "cli.js", "-k", testApiKey, "-a", testAssetMapPath];
|
||||
|
||||
try {
|
||||
// Import CLI module which will execute the program
|
||||
require("../src/cli");
|
||||
|
||||
// Verify FullStoryUploader was constructed with correct params
|
||||
expect(mockFullStoryUploader).toHaveBeenCalledWith({
|
||||
apiKey: testApiKey,
|
||||
assetMapPath: testAssetMapPath,
|
||||
});
|
||||
|
||||
// Verify uploadAssets was called
|
||||
expect(mockUploadAssets).toHaveBeenCalled();
|
||||
} finally {
|
||||
// Restore original process.argv
|
||||
process.argv = originalArgv;
|
||||
}
|
||||
});
|
||||
|
||||
it("should display version information with --version flag", () => {
|
||||
// ✅ Mock process.exit to prevent Jest from failing
|
||||
const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {
|
||||
throw new Error("process.exit(0) was called");
|
||||
});
|
||||
|
||||
// ✅ Capture stdout output
|
||||
const originalWrite = process.stdout.write;
|
||||
let output = "";
|
||||
process.stdout.write = (chunk: any) => {
|
||||
output += chunk;
|
||||
return true;
|
||||
};
|
||||
|
||||
// Mock process.argv
|
||||
const originalArgv = process.argv;
|
||||
process.argv = ["node", "cli.js", "--version"];
|
||||
|
||||
try {
|
||||
require("../src/cli");
|
||||
} catch (e) {
|
||||
// Ignore expected process.exit error
|
||||
const error = e as Error;
|
||||
expect(error.message).toContain("process.exit");
|
||||
}
|
||||
|
||||
// ✅ Ensure the version is in the output
|
||||
expect(output).toContain("0.0.1");
|
||||
|
||||
// Restore mocks and original argv
|
||||
process.stdout.write = originalWrite;
|
||||
process.argv = originalArgv;
|
||||
exitSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should display help information with --help flag", () => {
|
||||
// Mock console.log and process.exit
|
||||
const consoleSpy = jest.spyOn(console, "log").mockImplementation(() => {});
|
||||
const exitSpy = jest.spyOn(process, "exit").mockImplementation((code) => {
|
||||
throw new Error(`process.exit(${code}) was called`);
|
||||
});
|
||||
|
||||
// Mock process.argv
|
||||
const originalArgv = process.argv;
|
||||
process.argv = ["node", "cli.js", "--help"];
|
||||
|
||||
try {
|
||||
// Import CLI module
|
||||
require("../src/cli");
|
||||
} catch (e) {
|
||||
// Expected error from process.exit()
|
||||
const error = e as Error;
|
||||
expect(error.message).toContain("process.exit");
|
||||
}
|
||||
|
||||
// Restore mocks and original argv
|
||||
consoleSpy.mockRestore();
|
||||
exitSpy.mockRestore();
|
||||
process.argv = originalArgv;
|
||||
});
|
||||
});
|
77
tests/fullstoryUploader.test.ts
Normal file
77
tests/fullstoryUploader.test.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import FullStoryUploader from "../src/fullstoryUploader";
|
||||
import fs from "fs-extra";
|
||||
import axios from "axios";
|
||||
|
||||
// Mock fs-extra to simulate file existence and content reading
|
||||
jest.mock("fs-extra", () => ({
|
||||
existsSync: jest.fn(() => true),
|
||||
readFileSync: jest.fn(() => JSON.stringify({ assets: {}, version: "2" })),
|
||||
}));
|
||||
|
||||
// Mock axios to simulate API responses
|
||||
jest.mock("axios");
|
||||
|
||||
describe("FullStoryUploader", () => {
|
||||
const mockApiKey = "test-api-key";
|
||||
const mockAssetMapPath = "test/assets.json";
|
||||
|
||||
it("should upload assets successfully", async () => {
|
||||
// Mock successful API response
|
||||
(axios.post as jest.Mock).mockResolvedValue({ status: 200 });
|
||||
|
||||
const uploader = new FullStoryUploader({
|
||||
apiKey: mockApiKey,
|
||||
assetMapPath: mockAssetMapPath,
|
||||
});
|
||||
|
||||
const consoleSpy = jest.spyOn(console, "log").mockImplementation();
|
||||
await uploader.uploadAssets();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("✅ Assets uploaded successfully!"),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should handle file not found error", async () => {
|
||||
// Simulate a missing file
|
||||
(fs.existsSync as jest.Mock).mockReturnValue(false);
|
||||
|
||||
const uploader = new FullStoryUploader({
|
||||
apiKey: mockApiKey,
|
||||
assetMapPath: "nonexistent.json",
|
||||
});
|
||||
|
||||
const consoleSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
await uploader.uploadAssets();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("❌ Asset map file not found"),
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("should handle API errors", async () => {
|
||||
// Ensure the file exists so the API call runs
|
||||
(fs.existsSync as jest.Mock).mockReturnValue(true);
|
||||
|
||||
// Simulate an API error
|
||||
const apiError = new Error("API error");
|
||||
(axios.post as jest.Mock).mockRejectedValue(apiError);
|
||||
|
||||
const uploader = new FullStoryUploader({
|
||||
apiKey: mockApiKey,
|
||||
assetMapPath: mockAssetMapPath,
|
||||
});
|
||||
|
||||
const consoleSpy = jest.spyOn(console, "error").mockImplementation();
|
||||
await uploader.uploadAssets();
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining("❌ Error uploading assets"),
|
||||
apiError, // This ensures we match the error object in the log
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
70
tests/index.test.ts
Normal file
70
tests/index.test.ts
Normal file
@ -0,0 +1,70 @@
|
||||
|
||||
import { jest } from '@jest/globals';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// Create mock functions with explicit types
|
||||
const mockUploadAssets = jest.fn<() => Promise<{ success: boolean }>>().mockResolvedValue({ success: true });
|
||||
const mockFullStoryUploader = jest.fn().mockImplementation(() => ({
|
||||
uploadAssets: mockUploadAssets,
|
||||
}));
|
||||
|
||||
// Mock modules before importing the index
|
||||
jest.mock('../src/fullstoryUploader', () => ({
|
||||
__esModule: true,
|
||||
default: mockFullStoryUploader,
|
||||
}));
|
||||
|
||||
// Import the module under test
|
||||
import { uploadAssets } from '../src/index';
|
||||
|
||||
describe('Index Module', () => {
|
||||
const testAssetMapPath = path.resolve(__dirname, 'test-asset-map.json');
|
||||
const testApiKey = 'test-api-key';
|
||||
|
||||
beforeAll(() => {
|
||||
const testAssetMap = {
|
||||
assets: [
|
||||
{ id: 'asset1', path: './asset1.js' },
|
||||
{ id: 'asset2', path: './asset2.css' },
|
||||
],
|
||||
};
|
||||
|
||||
fs.writeFileSync(testAssetMapPath, JSON.stringify(testAssetMap, null, 2));
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (fs.existsSync(testAssetMapPath)) {
|
||||
fs.unlinkSync(testAssetMapPath);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should initialize FullStoryUploader with correct params and call uploadAssets', async () => {
|
||||
const result = await uploadAssets(testApiKey, testAssetMapPath);
|
||||
|
||||
expect(mockFullStoryUploader).toHaveBeenCalledWith({
|
||||
apiKey: testApiKey,
|
||||
assetMapPath: testAssetMapPath,
|
||||
});
|
||||
|
||||
expect(mockUploadAssets).toHaveBeenCalled();
|
||||
expect(result).toEqual({ success: true });
|
||||
});
|
||||
|
||||
it('should propagate errors from FullStoryUploader.uploadAssets', async () => {
|
||||
const testError = new Error('Upload failed');
|
||||
mockUploadAssets.mockRejectedValueOnce(testError);
|
||||
|
||||
await expect(uploadAssets(testApiKey, testAssetMapPath)).rejects.toThrow(testError);
|
||||
|
||||
expect(mockFullStoryUploader).toHaveBeenCalledWith({
|
||||
apiKey: testApiKey,
|
||||
assetMapPath: testAssetMapPath,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -5,7 +5,9 @@
|
||||
"target": "ES2022",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true
|
||||
"sourceMap": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
Loading…
x
Reference in New Issue
Block a user