Skip to content

feat(test): add vitest setup and widget container tests #3102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 141 additions & 0 deletions react/lib/grid-stack-render-provider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { gridWidgetContainersMap } from './grid-stack-render-provider';

// Mock GridStack type
class MockGridStack {
el: HTMLElement;
constructor() {
this.el = document.createElement('div');
}
}

describe('GridStackRenderProvider', () => {
beforeEach(() => {
// Clear the WeakMap before each test
gridWidgetContainersMap.constructor.prototype.clear?.call(gridWidgetContainersMap);
});

it('should store widget containers in WeakMap for each grid instance', () => {
// Mock grid instances
const grid1 = new MockGridStack() as any;
const grid2 = new MockGridStack() as any;
const widget1 = { id: '1', grid: grid1 };
const widget2 = { id: '2', grid: grid2 };
const element1 = document.createElement('div');
const element2 = document.createElement('div');

// Simulate renderCB
const renderCB = (element, widget) => {
if (widget.id && widget.grid) {
// Get or create the widget container map for this grid instance
let containers = gridWidgetContainersMap.get(widget.grid);
if (!containers) {
containers = new Map();
gridWidgetContainersMap.set(widget.grid, containers);
}
containers.set(widget.id, element);
}
};

renderCB(element1, widget1);
renderCB(element2, widget2);

const containers1 = gridWidgetContainersMap.get(grid1);
const containers2 = gridWidgetContainersMap.get(grid2);

expect(containers1?.get('1')).toBe(element1);
expect(containers2?.get('2')).toBe(element2);
});

it('should not have containers for different grid instances mixed up', () => {
const grid1 = new MockGridStack() as any;
const grid2 = new MockGridStack() as any;
const widget1 = { id: '1', grid: grid1 };
const widget2 = { id: '2', grid: grid1 };
const widget3 = { id: '3', grid: grid2 };
const element1 = document.createElement('div');
const element2 = document.createElement('div');
const element3 = document.createElement('div');

// Simulate renderCB
const renderCB = (element: HTMLElement, widget: any) => {
if (widget.id && widget.grid) {
let containers = gridWidgetContainersMap.get(widget.grid);
if (!containers) {
containers = new Map();
gridWidgetContainersMap.set(widget.grid, containers);
}
containers.set(widget.id, element);
}
};

renderCB(element1, widget1);
renderCB(element2, widget2);
renderCB(element3, widget3);

const containers1 = gridWidgetContainersMap.get(grid1);
const containers2 = gridWidgetContainersMap.get(grid2);

// Grid1 should have widgets 1 and 2
expect(containers1?.size).toBe(2);
expect(containers1?.get('1')).toBe(element1);
expect(containers1?.get('2')).toBe(element2);
expect(containers1?.get('3')).toBeUndefined();

// Grid2 should only have widget 3
expect(containers2?.size).toBe(1);
expect(containers2?.get('3')).toBe(element3);
expect(containers2?.get('1')).toBeUndefined();
expect(containers2?.get('2')).toBeUndefined();
});

it('should clean up when grid instance is deleted from WeakMap', () => {
const grid = new MockGridStack() as any;
const widget = { id: '1', grid };
const element = document.createElement('div');

// Add to WeakMap
const containers = new Map<string, HTMLElement>();
containers.set(widget.id, element);
gridWidgetContainersMap.set(grid, containers);

// Verify it exists
expect(gridWidgetContainersMap.has(grid)).toBe(true);

// Delete from WeakMap
gridWidgetContainersMap.delete(grid);

// Verify it's gone
expect(gridWidgetContainersMap.has(grid)).toBe(false);
});

it('should handle multiple widgets in the same grid', () => {
const grid = new MockGridStack() as any;
const widgets = [
{ id: '1', grid },
{ id: '2', grid },
{ id: '3', grid },
];
const elements = widgets.map(() => document.createElement('div'));

// Simulate renderCB for all widgets
widgets.forEach((widget, index) => {
const element = elements[index];
if (widget.id && widget.grid) {
let containers = gridWidgetContainersMap.get(widget.grid);
if (!containers) {
containers = new Map();
gridWidgetContainersMap.set(widget.grid, containers);
}
containers.set(widget.id, element);
}
});

const containers = gridWidgetContainersMap.get(grid);
expect(containers?.size).toBe(3);
expect(containers?.get('1')).toBe(elements[0]);
expect(containers?.get('2')).toBe(elements[1]);
expect(containers?.get('3')).toBe(elements[2]);
});
});

26 changes: 24 additions & 2 deletions react/lib/grid-stack-render-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import { GridStack, GridStackOptions, GridStackWidget } from "gridstack";
import { GridStackRenderContext } from "./grid-stack-render-context";
import isEqual from "react-fast-compare";

// WeakMap to store widget containers for each grid instance
export const gridWidgetContainersMap = new WeakMap<GridStack, Map<string, HTMLElement>>();

export function GridStackRenderProvider({ children }: PropsWithChildren) {
const {
_gridStack: { value: gridStack, set: setGridStack },
Expand All @@ -21,8 +24,17 @@ export function GridStackRenderProvider({ children }: PropsWithChildren) {
const optionsRef = useRef<GridStackOptions>(initialOptions);

const renderCBFn = useCallback(
(element: HTMLElement, widget: GridStackWidget) => {
if (widget.id) {
(element: HTMLElement, widget: GridStackWidget & { grid?: GridStack }) => {
if (widget.id && widget.grid) {
// Get or create the widget container map for this grid instance
let containers = gridWidgetContainersMap.get(widget.grid);
if (!containers) {
containers = new Map<string, HTMLElement>();
gridWidgetContainersMap.set(widget.grid, containers);
}
containers.set(widget.id, element);

// Also update the local ref for backward compatibility
widgetContainersRef.current.set(widget.id, element);
}
},
Expand Down Expand Up @@ -50,6 +62,8 @@ export function GridStackRenderProvider({ children }: PropsWithChildren) {
gridStack.removeAll(false);
gridStack.destroy(false);
widgetContainersRef.current.clear();
// Clean up the WeakMap entry for this grid instance
gridWidgetContainersMap.delete(gridStack);
optionsRef.current = initialOptions;
setGridStack(initGrid());
} catch (e) {
Expand All @@ -73,6 +87,14 @@ export function GridStackRenderProvider({ children }: PropsWithChildren) {
value={useMemo(
() => ({
getWidgetContainer: (widgetId: string) => {
// First try to get from the current grid instance's map
if (gridStack) {
const containers = gridWidgetContainersMap.get(gridStack);
if (containers?.has(widgetId)) {
return containers.get(widgetId) || null;
}
}
// Fallback to local ref for backward compatibility
return widgetContainersRef.current.get(widgetId) || null;
},
}),
Expand Down
9 changes: 7 additions & 2 deletions react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,28 @@
"start": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest",
"test:ui": "vitest --ui"
},
"dependencies": {
"gridstack": "^12.2.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-fast-compare": "^3.2.2"
"react-fast-compare": "^3.2.2",
"vitest": "^3.2.4"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/ui": "^3.2.4",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"jsdom": "^26.1.0",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.19"
Expand Down
14 changes: 12 additions & 2 deletions react/src/demo/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
GridStackRenderProvider,
useGridStackContext,
} from "../../lib";
// import { GridStackRenderProvider } from "../../lib/grid-stack-render-provider-single";

import "gridstack/dist/gridstack.css";
import "./demo.css";
Expand Down Expand Up @@ -134,17 +135,26 @@ const gridOptions: GridStackOptions = {
export function GridStackDemo() {
// ! Uncontrolled
const [initialOptions] = useState(gridOptions);
const [initialOptions2] = useState({});

return (
<>
<GridStackProvider initialOptions={initialOptions}>
<Toolbar />

<GridStackRenderProvider>
<GridStackRenderProvider>
<GridStackRender componentMap={COMPONENT_MAP} />
</GridStackRenderProvider>
<DebugInfo />
</GridStackProvider>

<GridStackProvider initialOptions={initialOptions2}>
<Toolbar />
<GridStackRenderProvider>
<GridStackRender componentMap={COMPONENT_MAP} />
</GridStackRenderProvider>
<DebugInfo />
</GridStackProvider>
</>
);
}

Expand Down
4 changes: 4 additions & 0 deletions react/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
},
})
Loading