-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(@captn/react): add additional hooks
- use-resettable-state - use-unload - use-text-to-image
- Loading branch information
Showing
7 changed files
with
430 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
import { renderHook, act } from "@testing-library/react"; | ||
|
||
import { useResettableState } from "../use-resettable-state"; | ||
|
||
describe("useResettableState", () => { | ||
// Enable fake timers | ||
beforeAll(() => { | ||
jest.useFakeTimers(); | ||
}); | ||
|
||
// Clean up after tests run | ||
afterAll(() => { | ||
jest.useRealTimers(); | ||
}); | ||
|
||
it("should reset state to initial value after specified delay", () => { | ||
// Initial values for the test | ||
const initialValue = 0; | ||
const temporaryValue = 5; | ||
const delay = 1000; | ||
|
||
// Render the hook | ||
const { result } = renderHook(() => useResettableState(initialValue, delay)); | ||
|
||
// Check initial state | ||
expect(result.current[0]).toBe(initialValue); | ||
|
||
// Set temporary state | ||
act(() => { | ||
result.current[1](temporaryValue); | ||
}); | ||
|
||
// Check if the state has changed to the temporary value | ||
expect(result.current[0]).toBe(temporaryValue); | ||
|
||
// Fast-forward time to just before the delay time | ||
act(() => { | ||
jest.advanceTimersByTime(delay - 1); | ||
}); | ||
|
||
// Verify that the state has not yet reset | ||
expect(result.current[0]).toBe(temporaryValue); | ||
|
||
// Fast-forward to complete the delay | ||
act(() => { | ||
jest.advanceTimersByTime(1); | ||
}); | ||
|
||
// Check if the state has reset to initial value | ||
expect(result.current[0]).toBe(initialValue); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { renderHook, act } from "@testing-library/react"; | ||
|
||
import { useSDK } from "../use-sdk"; | ||
import { useTextToImage } from "../use-text-to-image"; | ||
|
||
jest.mock("../use-sdk", () => ({ | ||
useSDK: jest.fn(), | ||
})); | ||
|
||
jest.mock("../use-unload", () => ({ | ||
useUnload: jest.fn(), | ||
})); | ||
|
||
describe("useTextToImage", () => { | ||
let sendMock; | ||
let onMessageHandler; | ||
|
||
beforeEach(() => { | ||
sendMock = jest.fn(); | ||
onMessageHandler = jest.fn(); | ||
|
||
// Mock the useSDK to simulate receiving messages | ||
(useSDK as jest.Mock).mockImplementation((appId, { onMessage }) => { | ||
onMessageHandler = onMessage; // Capture the onMessage callback for simulation | ||
return { send: sendMock }; | ||
}); | ||
}); | ||
|
||
it("starts the image generation process correctly", () => { | ||
const { result } = renderHook(() => useTextToImage("myAppId")); | ||
|
||
act(() => { | ||
result.current.start({ model: "testModel", model_type: "stable-diffusion" }); | ||
}); | ||
|
||
expect(sendMock).toHaveBeenCalledWith({ | ||
action: "text-to-image:start", | ||
payload: { model: "testModel", model_type: "stable-diffusion" }, | ||
}); | ||
expect(result.current.isLoading).toBeTruthy(); | ||
}); | ||
|
||
it("handles the image generation started message", () => { | ||
const { result } = renderHook(() => useTextToImage("myAppId")); | ||
|
||
// Simulate receiving an IPC message | ||
act(() => { | ||
onMessageHandler({ | ||
action: "text-to-image:started", | ||
payload: "", | ||
}); | ||
}); | ||
|
||
expect(result.current.isRunning).toBeTruthy(); | ||
expect(result.current.isLoading).toBeFalsy(); | ||
}); | ||
|
||
it("stops the image generation process correctly", () => { | ||
const { result } = renderHook(() => useTextToImage("myAppId")); | ||
|
||
act(() => { | ||
result.current.stop(); | ||
}); | ||
|
||
expect(sendMock).toHaveBeenCalledWith({ | ||
action: "text-to-image:stop", | ||
payload: true, | ||
}); | ||
expect(result.current.isLoading).toBeTruthy(); | ||
}); | ||
|
||
it("generates an image with specified parameters", async () => { | ||
const { result } = renderHook(() => useTextToImage("myAppId")); | ||
|
||
// Simulate the condition that sets isRunning to true | ||
act(() => { | ||
onMessageHandler({ | ||
action: "text-to-image:started", | ||
}); | ||
}); | ||
|
||
// Now, call the generate function | ||
act(() => { | ||
result.current.generate({ | ||
prompt: "A beautiful landscape", | ||
negative_prompt: "buildings", | ||
seed: 42, | ||
}); | ||
}); | ||
|
||
// Check if the send function was called with the correct parameters | ||
expect(sendMock).toHaveBeenCalledWith({ | ||
action: "text-to-image:settings", | ||
payload: { | ||
prompt: "A beautiful landscape", | ||
negative_prompt: "buildings", | ||
seed: 42, | ||
}, | ||
}); | ||
|
||
// Check if the state reflects an ongoing generation process | ||
expect(result.current.isGenerating).toBeTruthy(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { renderHook } from "@testing-library/react"; | ||
|
||
import { useSDK } from "../use-sdk"; | ||
import { useUnload } from "../use-unload"; | ||
|
||
// Mock the useSDK hook | ||
jest.mock("../use-sdk", () => ({ | ||
useSDK: jest.fn(), | ||
})); | ||
|
||
describe("useUnload", () => { | ||
beforeEach(() => { | ||
// Reset mocks before each test | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it("adds and removes the beforeunload event listener correctly", () => { | ||
const mockSend = jest.fn(); | ||
(useSDK as jest.Mock).mockImplementation(() => ({ send: mockSend })); | ||
|
||
// Mock addEventListener and removeEventListener | ||
const addEventListenerSpy = jest.spyOn(window, "addEventListener"); | ||
const removeEventListenerSpy = jest.spyOn(window, "removeEventListener"); | ||
|
||
const { unmount } = renderHook(() => useUnload("myAppId", "testAction")); | ||
|
||
// Check if the 'beforeunload' event listener was added | ||
expect(addEventListenerSpy).toHaveBeenCalledWith("beforeunload", expect.any(Function)); | ||
|
||
// Trigger the beforeunload event | ||
const event = new Event("beforeunload"); | ||
window.dispatchEvent(event); | ||
|
||
// Check if the send function was called correctly | ||
expect(mockSend).toHaveBeenCalledWith({ action: "testAction", payload: "myAppId" }); | ||
|
||
// Unmount the hook and check if the event listener was removed | ||
unmount(); | ||
expect(removeEventListenerSpy).toHaveBeenCalledWith("beforeunload", expect.any(Function)); | ||
|
||
// Cleanup spies to avoid memory leaks | ||
addEventListenerSpy.mockRestore(); | ||
removeEventListenerSpy.mockRestore(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import { useCallback, useRef, useState } from "react"; | ||
|
||
/** | ||
* A custom React hook that provides a state variable along with a function to temporarily set its value. | ||
* After a specified delay, the state automatically resets to its initial value. | ||
* | ||
* @template T The type of the state variable. | ||
* @param initialState The initial value of the state. This is also the value to which the state will reset. | ||
* @param delay The duration in milliseconds after which the state will reset to the initialState. | ||
* @returns A tuple containing the current state and a function to set the state temporarily. | ||
* The function accepts a new value and after `delay` milliseconds, it resets the state to `initialState`. | ||
* | ||
* @example | ||
* ```ts | ||
* const [count, setCountTemp] = useResettableState(0, 1000); | ||
* setCountTemp(5); // Sets count to 5, then resets to 0 after 1000 milliseconds | ||
* ``` | ||
*/ | ||
export function useResettableState<T>(initialState: T, delay: number): [T, (value: T) => void] { | ||
const [state, setState] = useState<T>(initialState); | ||
const timer = useRef(-1); | ||
|
||
const setTemporaryState = useCallback( | ||
(value: T) => { | ||
setState(value); | ||
|
||
timer.current = window.setTimeout(() => { | ||
setState(initialState); | ||
}, delay); | ||
}, | ||
[initialState, delay] | ||
); | ||
|
||
return [state, setTemporaryState]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
import { useCallback, useState } from "react"; | ||
|
||
import { useSDK } from "../use-sdk"; | ||
import { useUnload } from "../use-unload"; | ||
|
||
/** | ||
* A custom React hook for managing the lifecycle and state of a text-to-image generation process | ||
* via Inter-Process Communication (IPC). This hook facilitates starting, stopping, and managing | ||
* the image generation process based on text input, utilizing a specific machine learning model. | ||
* | ||
* @param {string} appId - A unique identifier for the application or SDK instance, used to | ||
* configure the underlying `useSDK` and `useUnload` hooks for IPC | ||
* communication. | ||
* | ||
* @returns {object} An object containing: | ||
* - `generate`: A function to trigger the generation of an image from text prompts. Requires | ||
* a payload including a 'prompt', 'negative_prompt', and 'seed'. | ||
* - `start`: A function to start the image generation process, requiring a payload that specifies | ||
* the model and model type. | ||
* - `stop`: A function to stop the image generation process. | ||
* - `isRunning`: A boolean state indicating if the image generation process is currently active. | ||
* - `isLoading`: A boolean state indicating if an operation (start or stop) is in progress. | ||
* - `isGenerating`: A boolean state indicating if the image is currently being generated. | ||
* - `image`: A state holding the last generated image as a string (e.g., a URL or a base64 encoded string). | ||
* | ||
* @remarks | ||
* - The hook uses `useSDK` for sending and receiving IPC messages specific to image generation tasks. | ||
* - It listens for specific IPC messages (`text-to-image:started`, `text-to-image:stopped`, | ||
* `text-to-image:generated`) to update the state accordingly. | ||
* - `useUnload` is utilized to ensure that the image generation process is stopped when the component | ||
* unmounts or the page is unloaded, preventing unfinished processes from lingering. | ||
* - Properly handling the `isRunning`, `isLoading`, and `isGenerating` states is crucial for providing | ||
* feedback to the user and managing UI components related to the process. | ||
* | ||
* @example | ||
* ```tsx | ||
* function App () { | ||
* const { generate, start, stop, isRunning, isLoading, isGenerating, image } = useTextToImage("myAppId"); | ||
* | ||
* return ( | ||
* <div> | ||
* <button onClick={() => start({ model: "modelIdentifier", model_type: "stable-diffusion" })}> | ||
* Start Image Generation | ||
* </button> | ||
* <button onClick={stop} disabled={!isRunning}> | ||
* Stop Image Generation | ||
* </button> | ||
* <button onClick={() => generate({ | ||
* prompt: "A futuristic cityscape", | ||
* negative_prompt: "No people", | ||
* seed: 1234 | ||
* })} disabled={!isRunning || isGenerating}> | ||
* Generate Image | ||
* </button> | ||
* {isLoading && <p>Loading...</p>} | ||
* {image && <img src={image} alt="Generated from text" />} | ||
* </div> | ||
* ); | ||
* }; | ||
* ``` | ||
*/ | ||
export function useTextToImage(appId: string) { | ||
const [isRunning, setIsRunning] = useState(false); | ||
const [isLoading, setIsLoading] = useState(false); | ||
const [isGenerating, setIsGenerating] = useState(false); | ||
const [image, setImage] = useState<string>(""); | ||
|
||
const { send } = useSDK<unknown, string>(appId, { | ||
onMessage(message) { | ||
switch (message.action) { | ||
case "text-to-image:started": { | ||
setIsRunning(true); | ||
setIsLoading(false); | ||
break; | ||
} | ||
|
||
case "text-to-image:stopped": { | ||
setIsRunning(false); | ||
setIsLoading(false); | ||
break; | ||
} | ||
|
||
case "text-to-image:generated": { | ||
setIsGenerating(false); | ||
setImage(message.payload); | ||
break; | ||
} | ||
|
||
default: { | ||
break; | ||
} | ||
} | ||
}, | ||
}); | ||
|
||
const start = useCallback( | ||
(payload: { | ||
// Example: "stabilityai/stable-diffusion-xl-base-1.0/sd_xl_base_1.0_0.9vae.safetensors" | ||
model: string; | ||
model_type: "stable-diffusion-xl" | "stable-diffusion"; | ||
}) => { | ||
setIsLoading(true); | ||
send({ | ||
action: "text-to-image:start", | ||
payload, | ||
}); | ||
}, | ||
[send] | ||
); | ||
|
||
const stop = useCallback(() => { | ||
setIsLoading(true); | ||
send({ action: "text-to-image:stop", payload: true }); | ||
}, [send]); | ||
|
||
const generate = useCallback( | ||
(payload: { prompt: string; negative_prompt: string; seed: number }) => { | ||
if (isRunning) { | ||
setIsGenerating(true); | ||
send({ | ||
action: "text-to-image:settings", | ||
payload, | ||
}); | ||
} | ||
}, | ||
[send, isRunning] | ||
); | ||
|
||
useUnload(appId, "text-to-image:stop"); | ||
|
||
return { generate, start, stop, isRunning, isLoading, isGenerating, image }; | ||
} |
Oops, something went wrong.