Skip to content

Commit

Permalink
Added ButtonsPanel component
Browse files Browse the repository at this point in the history
  • Loading branch information
viktor-podzigun committed Jun 12, 2023
1 parent 1c39c63 commit 513fd92
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 2 deletions.
5 changes: 4 additions & 1 deletion src/Button.mjs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
/**
* @typedef {import("./Button").ButtonProps} ButtonProps
*/
import React, { useState } from "react";
import * as UI from "./UI.mjs";

const h = React.createElement;

/**
* @param {import('./Button').ButtonProps} props
* @param {ButtonProps} props
*/
const Button = (props) => {
const [focused, setFocused] = useState(false);
Expand Down
14 changes: 14 additions & 0 deletions src/ButtonsPanel.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Widgets } from "blessed";

export interface ButtonsPanelAction {
label: string;
onAction(): void;
}

export interface ButtonsPanelProps {
top: number;
actions: ButtonsPanelAction[];
style: Widgets.Types.TStyle;
padding?: number;
margin?: number;
}
51 changes: 51 additions & 0 deletions src/ButtonsPanel.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* @typedef {import("./ButtonsPanel").ButtonsPanelProps} ButtonsPanelProps
*/
import React from "react";
import Button from "./Button.mjs";

const h = React.createElement;

/**
* @param {ButtonsPanelProps} props
*/
const ButtonsPanel = (props) => {
const { buttonComp } = ButtonsPanel;
const padding = " ".repeat(props.padding ? props.padding : 0);
const margin = props.margin ? props.margin : 0;

let width = 0;
const buttons = props.actions.map((action) => {
const pos = width > 0 ? width + margin : 0;
const label = `${padding}${action.label}${padding}`;
width = pos + label.length;

return h(buttonComp, {
key: label,
left: pos,
top: 0,
label: label,
style: props.style,
onPress: () => {
Promise.resolve().then(action.onAction); //execute on the next tick
},
});
});

return h(
"box",
{
width: width,
height: 1,
left: "center",
top: props.top,
style: props.style,
},
...buttons
);
};

ButtonsPanel.displayName = "ButtonsPanel";
ButtonsPanel.buttonComp = Button;

export default ButtonsPanel;
9 changes: 8 additions & 1 deletion test/Button.test.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/**
* @typedef {import('../src/Button').ButtonProps} ButtonProps
*/
import React from "react";
import TestRenderer from "react-test-renderer";
import { assertComponents } from "react-assert";
Expand Down Expand Up @@ -59,6 +62,10 @@ describe("Button.test.mjs", () => {
});
});

/**
* @param {() => void} onPress
* @returns {ButtonProps}
*/
function getButtonProps(onPress = () => {}) {
return {
left: 1,
Expand All @@ -78,7 +85,7 @@ function getButtonProps(onPress = () => {}) {

/**
* @param {TestRenderer.ReactTestInstance} result
* @param {import('../src/Button').ButtonProps} props
* @param {ButtonProps} props
* @param {boolean} focused
*/
function assertButton(result, props, focused) {
Expand Down
155 changes: 155 additions & 0 deletions test/ButtonsPanel.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* @typedef {import("../src/ButtonsPanel").ButtonsPanelAction} ButtonsPanelAction
* @typedef {import("../src/ButtonsPanel").ButtonsPanelProps} ButtonsPanelProps
*/
import React from "react";
import TestRenderer from "react-test-renderer";
import { assertComponents } from "react-assert";
import assert from "node:assert/strict";
import mockFunction from "mock-fn";
import ButtonsPanel from "../src/ButtonsPanel.mjs";

const h = React.createElement;

const { describe, it } = await (async () => {
// @ts-ignore
return process.isBun // @ts-ignore
? Promise.resolve({ describe: (_, fn) => fn(), it: test })
: import("node:test");
})();

// @ts-ignore
ButtonsPanel.buttonComp = "ButtonMock";
const { buttonComp } = ButtonsPanel;

describe("ButtonsPanel.test.mjs", () => {
it("should call onAction1 when press button1", async () => {
//given
const onAction1 = mockFunction();
const onAction2 = mockFunction();
const props = getButtonsPanelProps([
getAction("button1", onAction1),
getAction("button2", onAction2),
]);
const comp = TestRenderer.create(h(ButtonsPanel, props)).root;
const [b1] = comp.findAllByType(buttonComp);

//when
b1.props.onPress();

//then
await Promise.resolve().then(() => {
assert.deepEqual(onAction1.times, 1);
assert.deepEqual(onAction2.times, 0);
});
});

it("should call onAction2 when press button2", async () => {
//given
const onAction1 = mockFunction();
const onAction2 = mockFunction();
const props = getButtonsPanelProps([
getAction("button1", onAction1),
getAction("button2", onAction2),
]);
const comp = TestRenderer.create(h(ButtonsPanel, props)).root;
const [_, b2] = comp.findAllByType(buttonComp);

//when
b2.props.onPress();

//then
await Promise.resolve().then(() => {
assert.deepEqual(onAction1.times, 0);
assert.deepEqual(onAction2.times, 1);
});
});

it("should render component", () => {
//given
const onAction = mockFunction();
const props = getButtonsPanelProps([
getAction("test btn", onAction),
getAction("test btn2", onAction),
]);

//when
const result = TestRenderer.create(h(ButtonsPanel, props)).root;

//then
assert.deepEqual(ButtonsPanel.displayName, "ButtonsPanel");
assertButtonsPanel(result, props, [
{ action: " test btn ", pos: 0 },
{ action: " test btn2 ", pos: 15 },
]);
});
});

/**
* @param {string} label
* @param {() => void} onAction
* @returns {ButtonsPanelAction}
*/
function getAction(label, onAction) {
return {
label,
onAction,
};
}

/**
* @param {ButtonsPanelAction[]} actions
* @returns {ButtonsPanelProps}
*/
function getButtonsPanelProps(actions) {
return {
top: 1,
actions,
style: {
fg: "white",
bg: "blue",
focus: {
fg: "cyan",
bg: "black",
},
},
padding: 2,
margin: 3,
};
}

/**
* @param {TestRenderer.ReactTestInstance} result
* @param {ButtonsPanelProps} props
* @param {{action: string, pos: number}[]} actions
*/
function assertButtonsPanel(result, props, actions) {
const buttonsWidth =
actions.map((_) => _.action.length).reduce((_1, _2) => _1 + _2) +
(actions.length - 1) * (props.margin ? props.margin : 0);

const resultButtons = result.findAllByType(buttonComp).map((_) => _.props);

assertComponents(
result.children,
h(
"box",
{
width: buttonsWidth,
height: 1,
top: props.top,
left: "center",
style: props.style,
},
...actions.map(({ action, pos }, i) => {
return h(buttonComp, {
left: pos,
top: 0,
label: action,
style: props.style,
onPress: resultButtons[i].onPress,
});
})
)
);
}
1 change: 1 addition & 0 deletions test/all.mjs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
await import("./Button.test.mjs");
await import("./ButtonsPanel.test.mjs");
await import("./UI.test.mjs");

0 comments on commit 513fd92

Please sign in to comment.