Skip to content
This repository was archived by the owner on Oct 3, 2025. It is now read-only.

Commit 486fc57

Browse files
authored
Merge pull request #93 from platformsh/91-add-design-debugger
Add design debugger
2 parents 30a23e8 + d14fbb2 commit 486fc57

File tree

5 files changed

+322
-0
lines changed

5 files changed

+322
-0
lines changed

frontend/src/App.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import CodeExample from "./components/CodeExample";
2020
import { PROJECT_ID } from "./config";
2121

2222
import commands from "./commands.json";
23+
import DesignDebugger from "./theme/debug/DesignDebugger";
2324

2425
function App() {
2526
const [environment, setEnvironment] = useState<string | null>(null);
@@ -107,9 +108,28 @@ services:
107108
}
108109
}, [environment, sessionStorageType]);
109110

111+
const debugEnabled =
112+
process.env.REACT_APP_ENABLE_DESIGN_DEBUG &&
113+
process.env.REACT_APP_ENABLE_DESIGN_DEBUG !== "false" &&
114+
process.env.REACT_APP_ENABLE_DESIGN_DEBUG !== "0";
115+
116+
const DesignDebug = () => {
117+
return debugEnabled ? (
118+
<DesignDebugger
119+
defaultEnvironment={environment}
120+
defaultStorage={sessionStorageType}
121+
defaultErrorState={fatalErrorMessage}
122+
onEnvironmentChange={(environment) => setEnvironment(environment)}
123+
onStorageChange={(storageType) => setSessionStorageType(storageType)}
124+
onErrorChange={(errorState) => setFatalErrorMessage(errorState)}
125+
/>
126+
) : null;
127+
};
128+
110129
if (fatalErrorMessage)
111130
return (
112131
<ErrorPage header="We cannot fetch your data">
132+
<DesignDebug />
113133
<p className="mt-2 mb-2">
114134
{" "}
115135
There was an error fetching data from your Python backend at{" "}
@@ -128,6 +148,7 @@ services:
128148

129149
return (
130150
<>
151+
<DesignDebug />
131152
<div
132153
className={`max-w-7xl w-fill px-6 2xl:pl-0 m-auto transition duration-500`}
133154
>
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React from "react";
2+
import { render, fireEvent, screen } from "@testing-library/react";
3+
import "@testing-library/jest-dom";
4+
import DesignDebugger from "./DesignDebugger";
5+
6+
describe("DesignDebugger", () => {
7+
// Test for initial state based on default props
8+
it("initializes with default props", () => {
9+
const defaultProps = {
10+
defaultEnvironment: "Development",
11+
defaultStorage: "redis",
12+
defaultErrorState: null,
13+
};
14+
render(<DesignDebugger {...defaultProps} />);
15+
16+
// @ts-ignore
17+
expect(screen.getByTestId("environmentInput").value).toBe(
18+
defaultProps.defaultEnvironment,
19+
);
20+
// @ts-ignore
21+
expect(screen.getByTestId("sessionStorageTypeInput").value).toBe(
22+
defaultProps.defaultStorage,
23+
);
24+
expect(screen.getByTestId("fatalErrorInput")).not.toBeChecked();
25+
});
26+
27+
// Test for environment change functionality
28+
it("changes environment selection", () => {
29+
const mockEnvironmentChange = jest.fn();
30+
render(<DesignDebugger onEnvironmentChange={mockEnvironmentChange} />);
31+
32+
const select = screen.getByTestId("environmentInput");
33+
fireEvent.change(select, { target: { value: "Staging" } });
34+
35+
// @ts-ignore
36+
expect(select.value).toBe("Staging");
37+
expect(mockEnvironmentChange).toHaveBeenCalledWith("Staging");
38+
});
39+
40+
// Test for storage change functionality
41+
it("changes session storage selection", () => {
42+
const mockStorageChange = jest.fn();
43+
render(<DesignDebugger onStorageChange={mockStorageChange} />);
44+
45+
const select = screen.getByTestId("sessionStorageTypeInput");
46+
fireEvent.change(select, { target: { value: "file" } });
47+
48+
// @ts-ignore
49+
expect(select.value).toBe("file");
50+
expect(mockStorageChange).toHaveBeenCalledWith("file");
51+
});
52+
53+
// Test for error emulation checkbox
54+
it("toggles error emulation", () => {
55+
const mockErrorChange = jest.fn();
56+
render(<DesignDebugger onErrorChange={mockErrorChange} />);
57+
58+
const checkbox = screen.getByTestId("fatalErrorInput");
59+
fireEvent.click(checkbox);
60+
61+
expect(checkbox).toBeChecked();
62+
expect(mockErrorChange).toHaveBeenCalledWith("Emulated error message");
63+
64+
fireEvent.click(checkbox);
65+
66+
expect(checkbox).not.toBeChecked();
67+
expect(mockErrorChange).toHaveBeenCalledWith(null);
68+
});
69+
});
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import React, { useState } from "react";
2+
import Draggable from "./Draggable";
3+
4+
interface DesignDebuggerProps {
5+
defaultEnvironment?: string | null;
6+
defaultStorage?: string | null;
7+
defaultErrorState?: string | null;
8+
onEnvironmentChange?: (environment: string | null) => void;
9+
onStorageChange?: (storageType: string | null) => void;
10+
onErrorChange?: (error: string | null) => void;
11+
}
12+
13+
const DesignDebugger: React.FC<DesignDebuggerProps> = (props) => {
14+
const [environment, setEnvironment] = useState<string | null>(
15+
props.defaultEnvironment || "production",
16+
);
17+
const [sessionStorageType, setSessionStorageType] = useState<string | null>(
18+
props.defaultStorage || "file",
19+
);
20+
const [fatalErrorMessage, setFatalErrorMessage] = useState<string | null>(
21+
props.defaultErrorState || null,
22+
);
23+
24+
const handleEnvironmentChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
25+
const value = e.target.value;
26+
setEnvironment(value);
27+
props.onEnvironmentChange && props.onEnvironmentChange(value);
28+
};
29+
30+
const handleStorageChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
31+
const value = e.target.value;
32+
setSessionStorageType(value);
33+
props.onStorageChange && props.onStorageChange(value);
34+
};
35+
36+
const handleErrorChange = (e: React.ChangeEvent<HTMLInputElement>) => {
37+
const errorMessage = e.target.checked ? "Emulated error message" : null;
38+
setFatalErrorMessage(errorMessage);
39+
props.onErrorChange && props.onErrorChange(errorMessage);
40+
};
41+
42+
return (
43+
<Draggable>
44+
<div className="flex flex-col space-y-4 p-4 bg-gray-100 rounded-md shadow-sm">
45+
<div className="flex items-center space-x-4">
46+
<label className="text-gray-700 font-medium">Environment:</label>
47+
<select
48+
data-testid={"environmentInput"}
49+
value={environment || ""}
50+
onChange={handleEnvironmentChange}
51+
className="ml-2 border p-2 rounded-md text-gray-600"
52+
>
53+
<option value="Production">Production</option>
54+
<option value="Staging">Staging</option>
55+
<option value="Development">Development</option>
56+
</select>
57+
</div>
58+
59+
<div className="flex items-center space-x-4">
60+
<label className="text-gray-700 font-medium">Session Storage:</label>
61+
<select
62+
data-testid={"sessionStorageTypeInput"}
63+
value={sessionStorageType || ""}
64+
onChange={handleStorageChange}
65+
className="ml-2 border p-2 rounded-md text-gray-600"
66+
>
67+
<option value="file">File</option>
68+
<option value="redis">Redis</option>
69+
</select>
70+
</div>
71+
72+
<div className="flex items-center space-x-4">
73+
<label className="text-gray-700 font-medium mr-2">
74+
Emulate Error:
75+
</label>
76+
<input
77+
data-testid={"fatalErrorInput"}
78+
type="checkbox"
79+
checked={fatalErrorMessage !== null}
80+
onChange={handleErrorChange}
81+
className="border rounded-md"
82+
/>
83+
</div>
84+
</div>
85+
</Draggable>
86+
);
87+
};
88+
89+
export default DesignDebugger;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import React from "react";
2+
import { render, fireEvent, screen } from "@testing-library/react";
3+
import Draggable from "./Draggable";
4+
5+
describe("Draggable", () => {
6+
beforeEach(() => {
7+
Storage.prototype.getItem = jest.fn(() => JSON.stringify({ x: 50, y: 50 }));
8+
Storage.prototype.setItem = jest.fn();
9+
});
10+
11+
it("renders children", () => {
12+
render(
13+
<Draggable>
14+
<div>Test Child</div>
15+
</Draggable>,
16+
);
17+
expect(screen.getByText("Test Child")).toBeInTheDocument();
18+
});
19+
20+
it("updates position on drag", () => {
21+
render(
22+
<Draggable>
23+
<div>Test Child</div>
24+
</Draggable>,
25+
);
26+
const draggableElement = screen.getByTestId("draggable");
27+
28+
// Simulate mouse down
29+
fireEvent.mouseDown(draggableElement, { clientX: 50, clientY: 50 });
30+
31+
// Simulate mouse move
32+
fireEvent.mouseMove(document, { clientX: 100, clientY: 100 });
33+
34+
// Simulate mouse up
35+
fireEvent.mouseUp(document);
36+
37+
// Check if the position was updated
38+
expect(draggableElement.style.left).toBe("50px");
39+
expect(draggableElement.style.top).toBe("50px");
40+
});
41+
42+
it("saves position to localStorage on mouse up", () => {
43+
// Set initial position
44+
Storage.prototype.getItem = jest.fn(() => JSON.stringify({ x: 0, y: 0 }));
45+
46+
render(
47+
<Draggable>
48+
<div>Test Child</div>
49+
</Draggable>,
50+
);
51+
const draggableElement = screen.getByTestId("draggable");
52+
53+
// Simulate a drag operation
54+
fireEvent.mouseDown(draggableElement, { clientX: 0, clientY: 0 });
55+
fireEvent.mouseMove(document, { clientX: 100, clientY: 100 });
56+
fireEvent.mouseUp(document);
57+
58+
// Assuming the draggable element updates its position after a drag,
59+
// the new position would be something like { x: 100, y: 100 }
60+
// This will depend on how your component calculates the new position
61+
expect(localStorage.setItem).toHaveBeenCalledWith(
62+
"draggablePosition",
63+
JSON.stringify({ x: 100, y: 100 }),
64+
);
65+
});
66+
});
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, { useState, useRef, useEffect } from "react";
2+
3+
interface DraggableProps {
4+
children: React.ReactNode;
5+
}
6+
7+
const Draggable: React.FC<DraggableProps> = ({ children }) => {
8+
const initialPosition = JSON.parse(
9+
localStorage.getItem("draggablePosition") || '{ "x": 0, "y": 0 }',
10+
);
11+
const [isDragging, setIsDragging] = useState(false);
12+
const [position, setPosition] = useState(initialPosition);
13+
const [offset, setOffset] = useState({ x: 0, y: 0 });
14+
const ref = useRef<HTMLDivElement>(null);
15+
16+
const onMouseDown = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
17+
if (ref.current) {
18+
const rect = ref.current.getBoundingClientRect();
19+
setOffset({
20+
x: event.clientX - rect.left,
21+
y: event.clientY - rect.top,
22+
});
23+
}
24+
setIsDragging(true);
25+
};
26+
27+
useEffect(() => {
28+
const onMouseUp = () => {
29+
setIsDragging(false);
30+
localStorage.setItem("draggablePosition", JSON.stringify(position));
31+
};
32+
33+
const onMouseMove = (event: MouseEvent) => {
34+
if (isDragging) {
35+
let newX = event.clientX - offset.x;
36+
let newY = event.clientY - offset.y;
37+
38+
// Boundary checks
39+
if (ref.current) {
40+
const maxX = window.innerWidth - ref.current.offsetWidth;
41+
const maxY = window.innerHeight - ref.current.offsetHeight;
42+
43+
if (newX < 0) newX = 0;
44+
if (newY < 0) newY = 0;
45+
if (newX > maxX) newX = maxX;
46+
if (newY > maxY) newY = maxY;
47+
48+
setPosition({ x: newX, y: newY });
49+
}
50+
}
51+
};
52+
document.addEventListener("mousemove", onMouseMove);
53+
document.addEventListener("mouseup", onMouseUp);
54+
return () => {
55+
document.removeEventListener("mousemove", onMouseMove);
56+
document.removeEventListener("mouseup", onMouseUp);
57+
};
58+
}, [isDragging, offset.x, offset.y, position]);
59+
60+
return (
61+
<div
62+
ref={ref}
63+
data-testid={"draggable"}
64+
onMouseDown={onMouseDown}
65+
className="absolute z-50 cursor-grab select-none bg-gray-300 rounded flex flex-col justify-center items-center"
66+
style={{ left: position.x, top: position.y }}
67+
>
68+
<div className="bg-gray-500 text-white text-sm font-bold p-1 rounded-t w-full flex justify-between items-center">
69+
<span>Debugger (Drag Me)</span>
70+
<span className="text-2xl"></span>
71+
</div>
72+
<div className="p-2">{children}</div>
73+
</div>
74+
);
75+
};
76+
77+
export default Draggable;

0 commit comments

Comments
 (0)