Skip to content

Commit 802a605

Browse files
Added DevToolPanel component
1 parent ad283ad commit 802a605

File tree

5 files changed

+318
-0
lines changed

5 files changed

+318
-0
lines changed

src/tool/DevToolPanel.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { DevToolValue } from "./DevTool.mjs";
2+
3+
export interface DevToolPanelProps {
4+
devTool: DevToolValue;
5+
logContent: string;
6+
onActivate(devTool: DevToolValue): void;
7+
}

src/tool/DevToolPanel.mjs

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/**
2+
* @typedef {import("./DevToolPanel").DevToolPanelProps} DevToolPanelProps
3+
* @typedef {import("./DevTool.mjs").DevToolValue} DevToolValue
4+
*/
5+
import React from "react";
6+
import LogPanel from "./LogPanel.mjs";
7+
import InputController from "./InputController.mjs";
8+
import ColorPanel from "./ColorPanel.mjs";
9+
import Theme from "../theme/Theme.mjs";
10+
import DevTool from "./DevTool.mjs";
11+
import * as UI from "../UI.mjs";
12+
13+
const h = React.createElement;
14+
15+
/**
16+
* @type {Array<{tool: DevToolValue, name: string}>}
17+
*/
18+
const tools = [
19+
{
20+
tool: DevTool.Logs,
21+
name: "Logs",
22+
},
23+
{
24+
tool: DevTool.Inputs,
25+
name: "Inputs",
26+
},
27+
{
28+
tool: DevTool.Colors,
29+
name: "Colors",
30+
},
31+
];
32+
33+
/**
34+
* @param {DevToolPanelProps} props
35+
* @returns {React.ReactElement | null}
36+
*/
37+
const getDevToolComp = (props) => {
38+
const { logPanelComp, inputController, colorPanelComp } = DevToolPanel;
39+
40+
switch (props.devTool) {
41+
case DevTool.Hidden:
42+
return null;
43+
case DevTool.Logs:
44+
return h(logPanelComp, { content: props.logContent });
45+
case DevTool.Inputs:
46+
return h(inputController);
47+
case DevTool.Colors:
48+
return h(colorPanelComp);
49+
}
50+
};
51+
52+
/**
53+
* @param {DevToolPanelProps} props
54+
*/
55+
const DevToolPanel = (props) => {
56+
const theme = Theme.useTheme().popup.menu;
57+
const comp = getDevToolComp(props);
58+
59+
let width = 0;
60+
const tabs = tools.map(({ tool, name }) => {
61+
const pos = width;
62+
const label = ` ${name} `;
63+
width = pos + label.length;
64+
65+
const style = tool === props.devTool ? theme.focus ?? theme : theme;
66+
const content = UI.renderText2(style, label);
67+
68+
return h("text", {
69+
key: label,
70+
autoFocus: false,
71+
clickable: true,
72+
tags: true,
73+
mouse: true,
74+
left: pos,
75+
onClick: () => {
76+
props.onActivate(tool);
77+
},
78+
content,
79+
});
80+
});
81+
82+
return h(
83+
React.Fragment,
84+
null,
85+
h(
86+
"box",
87+
{ width: "100%", height: 1, style: theme },
88+
h("box", { width, height: 1, left: "center" }, ...tabs)
89+
),
90+
h("box", { top: 1 }, comp)
91+
);
92+
};
93+
94+
DevToolPanel.displayName = "DevToolPanel";
95+
DevToolPanel.logPanelComp = LogPanel;
96+
DevToolPanel.inputController = InputController;
97+
DevToolPanel.colorPanelComp = ColorPanel;
98+
99+
export default DevToolPanel;

test/all.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ await import("./theme/Theme.test.mjs");
66

77
await import("./tool/ColorPanel.test.mjs");
88
await import("./tool/DevTool.test.mjs");
9+
await import("./tool/DevToolPanel.test.mjs");
910
await import("./tool/InputController.test.mjs");
1011
await import("./tool/LogController.test.mjs");
1112
await import("./tool/LogPanel.test.mjs");

test/theme/withThemeContext.mjs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @typedef {import("react").ReactElement} ReactElement
3+
* @typedef {import("../../src/theme/Theme").ThemeType} ThemeType
4+
*/
5+
import React from "react";
6+
import Theme from "../../src/theme/Theme.mjs";
7+
import DefaultTheme from "../../src/theme/DefaultTheme.mjs";
8+
9+
const h = React.createElement;
10+
11+
/**
12+
* @param {ReactElement} element
13+
* @param {ThemeType} theme
14+
* @returns {ReactElement}
15+
*/
16+
const withThemeContext = (element, theme = DefaultTheme) => {
17+
return h(Theme.Context.Provider, { value: theme }, element);
18+
};
19+
20+
export default withThemeContext;

test/tool/DevToolPanel.test.mjs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
/**
2+
* @typedef {import("../../src/tool/DevToolPanel").DevToolPanelProps} DevToolPanelProps
3+
* @typedef {import("../../src/tool/DevTool.mjs").DevToolValue} DevToolValue
4+
* @typedef {import("../../src/theme/Theme.mjs").ThemeType} ThemeType
5+
*/
6+
import React from "react";
7+
import TestRenderer from "react-test-renderer";
8+
import assert from "node:assert/strict";
9+
import { assertComponents, mockComponent } from "react-assert";
10+
import mockFunction from "mock-fn";
11+
import withThemeContext from "../theme/withThemeContext.mjs";
12+
import DefaultTheme from "../../src/theme/DefaultTheme.mjs";
13+
import XTerm256Theme from "../../src/theme/XTerm256Theme.mjs";
14+
import * as UI from "../../src/UI.mjs";
15+
import LogPanel from "../../src/tool/LogPanel.mjs";
16+
import InputController from "../../src/tool/InputController.mjs";
17+
import ColorPanel from "../../src/tool/ColorPanel.mjs";
18+
import DevTool from "../../src/tool/DevTool.mjs";
19+
import DevToolPanel from "../../src/tool/DevToolPanel.mjs";
20+
21+
const h = React.createElement;
22+
23+
const { describe, it } = await (async () => {
24+
// @ts-ignore
25+
return process.isBun // @ts-ignore
26+
? Promise.resolve({ describe: (_, fn) => fn(), it: test })
27+
: import("node:test");
28+
})();
29+
30+
DevToolPanel.logPanelComp = mockComponent(LogPanel);
31+
DevToolPanel.inputController = mockComponent(InputController);
32+
DevToolPanel.colorPanelComp = mockComponent(ColorPanel);
33+
34+
const { logPanelComp, inputController, colorPanelComp } = DevToolPanel;
35+
36+
describe("DevToolPanel.test.mjs", () => {
37+
it("should call onActivate when click on tab", () => {
38+
//given
39+
const onActivate = mockFunction((devTool) => {
40+
assert.deepEqual(devTool, DevTool.Logs);
41+
});
42+
const props = getDevToolPanelProps(DevTool.Colors, "test logs", onActivate);
43+
const comp = TestRenderer.create(
44+
withThemeContext(h(DevToolPanel, props))
45+
).root;
46+
const [tab1] = comp.findAllByType("text");
47+
48+
//when
49+
tab1.props.onClick();
50+
51+
//then
52+
assert.deepEqual(onActivate.times, 1);
53+
});
54+
55+
it("should render Logs component", () => {
56+
//given
57+
const props = getDevToolPanelProps(DevTool.Logs, "test logs");
58+
59+
//when
60+
const renderer = TestRenderer.create(
61+
withThemeContext(h(DevToolPanel, props))
62+
);
63+
64+
//then
65+
assertDevToolPanel(
66+
renderer,
67+
" Logs ",
68+
h(logPanelComp, { content: props.logContent })
69+
);
70+
});
71+
72+
it("should render Inputs component", () => {
73+
//given
74+
const props = getDevToolPanelProps(DevTool.Inputs, "test logs");
75+
76+
//when
77+
const renderer = TestRenderer.create(
78+
withThemeContext(h(DevToolPanel, props))
79+
);
80+
81+
//then
82+
assertDevToolPanel(renderer, " Inputs ", h(inputController));
83+
});
84+
85+
it("should render Colors component", () => {
86+
//given
87+
const currTheme = XTerm256Theme;
88+
const props = getDevToolPanelProps(DevTool.Colors, "test logs");
89+
90+
//when
91+
const renderer = TestRenderer.create(
92+
withThemeContext(h(DevToolPanel, props), currTheme)
93+
);
94+
95+
//then
96+
assertDevToolPanel(renderer, " Colors ", h(colorPanelComp), currTheme);
97+
});
98+
});
99+
100+
/**
101+
* @param {DevToolValue} devTool
102+
* @param {string} logContent
103+
* @param {(devTool: DevToolValue) => void} onActivate
104+
* @returns {DevToolPanelProps}
105+
*/
106+
function getDevToolPanelProps(devTool, logContent, onActivate = (_) => {}) {
107+
return {
108+
devTool,
109+
logContent,
110+
onActivate,
111+
};
112+
}
113+
114+
/**
115+
* @typedef {{label: string, pos: number}} ExpectedTab
116+
*/
117+
118+
/**
119+
* @param {string} label
120+
* @param {number} pos
121+
* @returns {ExpectedTab}
122+
*/
123+
function getExpectedTab(label, pos) {
124+
return {
125+
label,
126+
pos,
127+
};
128+
}
129+
130+
/**
131+
* @type {Array<ExpectedTab>}
132+
*/
133+
const expectedTabs = [
134+
getExpectedTab(" Logs ", 0),
135+
getExpectedTab(" Inputs ", 6),
136+
getExpectedTab(" Colors ", 14),
137+
];
138+
139+
/**
140+
* @param {TestRenderer.ReactTestRenderer} renderer
141+
* @param {string} activeTab
142+
* @param {React.ReactElement} expectedComp
143+
* @param {ThemeType} currTheme
144+
*/
145+
function assertDevToolPanel(
146+
renderer,
147+
activeTab,
148+
expectedComp,
149+
currTheme = DefaultTheme
150+
) {
151+
assert.deepEqual(DevToolPanel.displayName, "DevToolPanel");
152+
153+
const theme = currTheme.popup.menu;
154+
const width = expectedTabs
155+
.map(({ label }) => label.length)
156+
.reduce((_1, _2) => _1 + _2);
157+
158+
assertComponents(
159+
renderer.root.children,
160+
h(
161+
"box",
162+
{
163+
width: "100%",
164+
height: 1,
165+
style: theme,
166+
},
167+
h(
168+
"box",
169+
{
170+
width,
171+
height: 1,
172+
left: "center",
173+
},
174+
...expectedTabs.map(({ label, pos }) => {
175+
const style = label === activeTab ? theme.focus || theme : theme;
176+
const content = UI.renderText2(style, label);
177+
178+
return h("text", {
179+
autoFocus: false,
180+
clickable: true,
181+
tags: true,
182+
mouse: true,
183+
left: pos,
184+
content,
185+
});
186+
})
187+
)
188+
),
189+
h("box", { top: 1 }, expectedComp)
190+
);
191+
}

0 commit comments

Comments
 (0)