2026-05-10 03:06:54 +07:00
|
|
|
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
|
|
|
import { describe, expect, it, vi } from "vitest";
|
2026-05-21 00:02:12 +07:00
|
|
|
import type { Layer, LayerEffect } from "@pien-studio/types";
|
2026-05-10 03:06:54 +07:00
|
|
|
import { useFaceBlurWorkflow } from "./use-face-blur-workflow";
|
|
|
|
|
|
|
|
|
|
function makeImageLayer(overrides: Partial<Layer> = {}): Layer {
|
|
|
|
|
return {
|
|
|
|
|
id: "layer-1",
|
2026-05-21 00:02:12 +07:00
|
|
|
type: "raster",
|
2026-05-31 03:03:49 +07:00
|
|
|
asset: { kind: "inline", uri: "data:image/png;base64,abc" },
|
2026-05-10 03:06:54 +07:00
|
|
|
x: 0,
|
|
|
|
|
y: 0,
|
2026-05-31 03:03:49 +07:00
|
|
|
width: 100,
|
|
|
|
|
height: 100,
|
2026-05-10 03:06:54 +07:00
|
|
|
scale: 1,
|
|
|
|
|
rotation: 0,
|
|
|
|
|
opacity: 1,
|
2026-05-21 00:02:12 +07:00
|
|
|
effects: [],
|
|
|
|
|
visible: true,
|
2026-05-10 03:06:54 +07:00
|
|
|
...overrides,
|
2026-05-31 03:03:49 +07:00
|
|
|
} as Layer;
|
2026-05-10 03:06:54 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("useFaceBlurWorkflow", () => {
|
|
|
|
|
it("selects all detected faces by default when no existing face blur", async () => {
|
2026-05-21 00:02:12 +07:00
|
|
|
const setLayerEffect = vi.fn();
|
|
|
|
|
const removeLayerEffect = vi.fn();
|
2026-05-10 03:06:54 +07:00
|
|
|
const selectedLayer = makeImageLayer();
|
|
|
|
|
const faceDetections = [
|
2026-05-31 03:03:49 +07:00
|
|
|
{
|
|
|
|
|
x: 1,
|
|
|
|
|
y: 2,
|
|
|
|
|
width: 10,
|
|
|
|
|
height: 12,
|
|
|
|
|
label: "a",
|
|
|
|
|
sourceWidth: 100,
|
|
|
|
|
sourceHeight: 100,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
x: 5,
|
|
|
|
|
y: 8,
|
|
|
|
|
width: 7,
|
|
|
|
|
height: 9,
|
|
|
|
|
label: "b",
|
|
|
|
|
sourceWidth: 100,
|
|
|
|
|
sourceHeight: 100,
|
|
|
|
|
},
|
2026-05-10 03:06:54 +07:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() =>
|
|
|
|
|
useFaceBlurWorkflow({
|
|
|
|
|
selectedLayer,
|
|
|
|
|
faceDetectionsLayerId: "layer-1",
|
|
|
|
|
faceDetections,
|
2026-05-21 00:02:12 +07:00
|
|
|
setLayerEffect,
|
|
|
|
|
removeLayerEffect,
|
2026-05-10 03:06:54 +07:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.selectedFaceIndices).toEqual([0, 1]);
|
2026-05-31 03:03:49 +07:00
|
|
|
const faceBlurEffect = result.current.faceBlurPreview?.effects.find(
|
|
|
|
|
(e) => e.kind === "face-blur",
|
|
|
|
|
);
|
|
|
|
|
expect(
|
|
|
|
|
faceBlurEffect?.kind === "face-blur"
|
|
|
|
|
? faceBlurEffect.regions
|
|
|
|
|
: undefined,
|
|
|
|
|
).toHaveLength(2);
|
2026-05-10 03:06:54 +07:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("keeps selection empty when selected image already has blur", async () => {
|
2026-05-21 00:02:12 +07:00
|
|
|
const setLayerEffect = vi.fn();
|
|
|
|
|
const removeLayerEffect = vi.fn();
|
|
|
|
|
const faceBlurEffect: LayerEffect = {
|
|
|
|
|
kind: "face-blur",
|
|
|
|
|
enabled: true,
|
|
|
|
|
method: "gaussian",
|
|
|
|
|
amount: 14,
|
|
|
|
|
regions: [{ x: 0, y: 0, width: 4, height: 4 }],
|
|
|
|
|
};
|
|
|
|
|
const selectedLayer = makeImageLayer({ effects: [faceBlurEffect] });
|
2026-05-10 03:06:54 +07:00
|
|
|
|
|
|
|
|
const { result } = renderHook(() =>
|
|
|
|
|
useFaceBlurWorkflow({
|
|
|
|
|
selectedLayer,
|
|
|
|
|
faceDetectionsLayerId: "layer-1",
|
2026-05-31 03:03:49 +07:00
|
|
|
faceDetections: [
|
|
|
|
|
{
|
|
|
|
|
x: 1,
|
|
|
|
|
y: 2,
|
|
|
|
|
width: 10,
|
|
|
|
|
height: 12,
|
|
|
|
|
label: "a",
|
|
|
|
|
sourceWidth: 100,
|
|
|
|
|
sourceHeight: 100,
|
|
|
|
|
},
|
|
|
|
|
],
|
2026-05-21 00:02:12 +07:00
|
|
|
setLayerEffect,
|
|
|
|
|
removeLayerEffect,
|
2026-05-10 03:06:54 +07:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.selectedFaceIndices).toEqual([]);
|
|
|
|
|
expect(result.current.faceBlurPreview).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("toggles face selection and applies blur to selected regions", async () => {
|
2026-05-21 00:02:12 +07:00
|
|
|
const setLayerEffect = vi.fn();
|
|
|
|
|
const removeLayerEffect = vi.fn();
|
2026-05-10 03:06:54 +07:00
|
|
|
const selectedLayer = makeImageLayer();
|
|
|
|
|
const faceDetections = [
|
2026-05-31 03:03:49 +07:00
|
|
|
{
|
|
|
|
|
x: 1,
|
|
|
|
|
y: 2,
|
|
|
|
|
width: 10,
|
|
|
|
|
height: 12,
|
|
|
|
|
label: "a",
|
|
|
|
|
sourceWidth: 100,
|
|
|
|
|
sourceHeight: 100,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
x: 50,
|
|
|
|
|
y: 60,
|
|
|
|
|
width: 20,
|
|
|
|
|
height: 22,
|
|
|
|
|
label: "b",
|
|
|
|
|
sourceWidth: 100,
|
|
|
|
|
sourceHeight: 100,
|
|
|
|
|
},
|
2026-05-10 03:06:54 +07:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const { result } = renderHook(() =>
|
|
|
|
|
useFaceBlurWorkflow({
|
|
|
|
|
selectedLayer,
|
|
|
|
|
faceDetectionsLayerId: "layer-1",
|
|
|
|
|
faceDetections,
|
2026-05-21 00:02:12 +07:00
|
|
|
setLayerEffect,
|
|
|
|
|
removeLayerEffect,
|
2026-05-10 03:06:54 +07:00
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.selectedFaceIndices).toEqual([0, 1]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.toggleFaceIndex(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.current.selectedFaceIndices).toEqual([0]);
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.blurFaces(result.current.selectedFaceIndices);
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-21 00:02:12 +07:00
|
|
|
expect(setLayerEffect).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(setLayerEffect).toHaveBeenCalledWith(
|
2026-05-10 03:06:54 +07:00
|
|
|
"layer-1",
|
|
|
|
|
expect.objectContaining({
|
2026-05-21 00:02:12 +07:00
|
|
|
kind: "face-blur",
|
2026-05-10 03:06:54 +07:00
|
|
|
method: "gaussian",
|
|
|
|
|
amount: 14,
|
|
|
|
|
regions: expect.arrayContaining([
|
2026-05-31 03:03:49 +07:00
|
|
|
expect.objectContaining({
|
|
|
|
|
x: 1,
|
|
|
|
|
y: 2,
|
|
|
|
|
width: 10,
|
|
|
|
|
height: 12,
|
|
|
|
|
censorColor: "#111111",
|
|
|
|
|
sourceWidth: 100,
|
|
|
|
|
sourceHeight: 100,
|
|
|
|
|
}),
|
2026-05-10 03:06:54 +07:00
|
|
|
]),
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
expect(result.current.selectedFaceIndices).toEqual([]);
|
|
|
|
|
expect(result.current.faceBlurPreview).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
});
|