respira/src/stores/usePatternStore.test.ts
copilot-swe-agent[bot] 5296590a45 fix: Revert pattern validation to local hook to prevent infinite re-renders
The selectPatternValidation selector doesn't work well with Zustand because:
1. It requires machineInfo parameter from a different store
2. Passing machineInfo creates new object references on every render
3. This breaks Zustand's memoization and causes infinite loops

Solution:
- Reverted usePatternValidation.ts to original implementation with useMemo
- Removed selectPatternValidation and usePatternValidationFromStore
- Removed PatternValidationResult type (not needed)
- Updated tests to remove validation selector tests (12 tests remain)

The store selectors (selectPatternCenter, selectRotatedBounds, etc) are
still useful for components that only need those specific values, but
validation logic that depends on external state should stay local.

Co-authored-by: jhbruhn <1036566+jhbruhn@users.noreply.github.com>
2025-12-28 08:17:20 +00:00

200 lines
6 KiB
TypeScript

import { describe, it, expect, beforeEach } from "vitest";
import {
usePatternStore,
selectPatternCenter,
selectUploadedPatternCenter,
selectRotatedBounds,
selectRotationCenterShift,
} from "./usePatternStore";
import type { PesPatternData } from "../formats/import/pesImporter";
// Mock pattern data for testing
const createMockPesData = (
bounds = {
minX: -100,
maxX: 100,
minY: -50,
maxY: 50,
},
): PesPatternData => ({
stitches: [[0, 0, 0, 0]],
threads: [],
uniqueColors: [],
penData: new Uint8Array(),
penStitches: {
stitches: [],
colorBlocks: [],
bounds: { minX: 0, maxX: 0, minY: 0, maxY: 0 },
},
colorCount: 1,
stitchCount: 1,
bounds,
});
describe("usePatternStore selectors", () => {
beforeEach(() => {
// Reset store state before each test
const state = usePatternStore.getState();
state.setPattern(createMockPesData(), "test.pes");
state.resetPatternOffset();
state.resetRotation();
state.clearUploadedPattern();
});
describe("selectPatternCenter", () => {
it("should return null when no pattern is loaded", () => {
// Clear the pattern
usePatternStore.setState({ pesData: null });
const center = selectPatternCenter(usePatternStore.getState());
expect(center).toBeNull();
});
it("should calculate center correctly for symmetric bounds", () => {
const state = usePatternStore.getState();
const center = selectPatternCenter(state);
expect(center).not.toBeNull();
expect(center!.x).toBe(0); // (minX + maxX) / 2 = (-100 + 100) / 2 = 0
expect(center!.y).toBe(0); // (minY + maxY) / 2 = (-50 + 50) / 2 = 0
});
it("should calculate center correctly for asymmetric bounds", () => {
const pesData = createMockPesData({
minX: 0,
maxX: 200,
minY: 0,
maxY: 100,
});
usePatternStore.setState({ pesData });
const state = usePatternStore.getState();
const center = selectPatternCenter(state);
expect(center).not.toBeNull();
expect(center!.x).toBe(100); // (0 + 200) / 2
expect(center!.y).toBe(50); // (0 + 100) / 2
});
});
describe("selectUploadedPatternCenter", () => {
it("should return null when no uploaded pattern", () => {
const state = usePatternStore.getState();
const center = selectUploadedPatternCenter(state);
expect(center).toBeNull();
});
it("should calculate center of uploaded pattern", () => {
const uploadedData = createMockPesData({
minX: 50,
maxX: 150,
minY: 25,
maxY: 75,
});
usePatternStore
.getState()
.setUploadedPattern(uploadedData, { x: 0, y: 0 });
const state = usePatternStore.getState();
const center = selectUploadedPatternCenter(state);
expect(center).not.toBeNull();
expect(center!.x).toBe(100); // (50 + 150) / 2
expect(center!.y).toBe(50); // (25 + 75) / 2
});
});
describe("selectRotatedBounds", () => {
it("should return original bounds when no rotation", () => {
const state = usePatternStore.getState();
const result = selectRotatedBounds(state);
expect(result).not.toBeNull();
expect(result!.bounds).toEqual({
minX: -100,
maxX: 100,
minY: -50,
maxY: 50,
});
expect(result!.center).toEqual({ x: 0, y: 0 });
});
it("should return null when no pattern", () => {
usePatternStore.setState({ pesData: null });
const state = usePatternStore.getState();
const result = selectRotatedBounds(state);
expect(result).toBeNull();
});
it("should calculate rotated bounds for 90 degree rotation", () => {
usePatternStore.getState().setPatternRotation(90);
const state = usePatternStore.getState();
const result = selectRotatedBounds(state);
expect(result).not.toBeNull();
// After 90° rotation, X and Y bounds should swap
expect(result!.bounds.minX).toBeCloseTo(-50, 0);
expect(result!.bounds.maxX).toBeCloseTo(50, 0);
expect(result!.bounds.minY).toBeCloseTo(-100, 0);
expect(result!.bounds.maxY).toBeCloseTo(100, 0);
});
it("should expand bounds for 45 degree rotation", () => {
usePatternStore.getState().setPatternRotation(45);
const state = usePatternStore.getState();
const result = selectRotatedBounds(state);
expect(result).not.toBeNull();
// After 45° rotation, bounds should expand
expect(Math.abs(result!.bounds.minX)).toBeGreaterThan(100);
expect(Math.abs(result!.bounds.minY)).toBeGreaterThan(50);
});
});
describe("selectRotationCenterShift", () => {
it("should return zero shift when no rotation", () => {
const state = usePatternStore.getState();
const rotatedBounds = state.pesData!.bounds;
const shift = selectRotationCenterShift(state, rotatedBounds);
expect(shift).toEqual({ x: 0, y: 0 });
});
it("should return null when no pattern", () => {
usePatternStore.setState({ pesData: null });
const state = usePatternStore.getState();
const shift = selectRotationCenterShift(state, {
minX: 0,
maxX: 100,
minY: 0,
maxY: 100,
});
expect(shift).toBeNull();
});
it("should calculate center shift for asymmetric pattern", () => {
const pesData = createMockPesData({
minX: 0,
maxX: 200,
minY: 0,
maxY: 100,
});
usePatternStore.setState({ pesData });
usePatternStore.getState().setPatternRotation(90);
const state = usePatternStore.getState();
const rotatedBounds = selectRotatedBounds(state)!.bounds;
const shift = selectRotationCenterShift(state, rotatedBounds);
expect(shift).not.toBeNull();
// Original center: (100, 50)
// After 90° rotation around center, new center should be slightly different
// due to the asymmetric bounds
expect(shift!.x).toBeCloseTo(0, 0);
expect(shift!.y).toBeCloseTo(0, 0);
});
});
});