diff --git a/test/Makefile b/test/Makefile
index da6b0d6ae..78234458c 100755
--- a/test/Makefile
+++ b/test/Makefile
@@ -11,7 +11,7 @@ LIBS:=$(subst -lecolab,$(ECOLAB_HOME)/lib/libecolab.a,$(LIBS))
VPATH= .. ../schema ../model ../engine ../RESTService ../RavelCAPI/civita ../RavelCAPI $(ECOLAB_HOME)/include
-GTESTOBJS=testCSVParser.o testCanvas.o testDerivative.o testExpressionWalker.o testGrid.o testLatexToPango.o testLockGroup.o testMdl.o testMinsky.o testModel.o testPannableTab.o testPhillips.o testPlotWidget.o testPubTab.o testSaver.o testStr.o testTensorOps.o testUnits.o testUserFunction.o testVariable.o testVariablePane.o testXVector.o testZStream.o ticket-1461.o
+GTESTOBJS=testCSVParser.o testCanvas.o testDerivative.o testExpressionWalker.o testGrid.o testLatexToPango.o testLockGroup.o testMdl.o testMinsky.o testModel.o testPannableTab.o testPhillips.o testPlotWidget.o testPubTab.o testSaver.o testSheet.o testStr.o testTensorOps.o testUnits.o testUserFunction.o testVariable.o testVariablePane.o testXVector.o testZStream.o ticket-1461.o
MINSKYOBJS=localMinsky.o ../libminsky.a
FLAGS:=-I.. -I../RESTService -I../RavelCAPI/civita -I../RavelCAPI $(FLAGS)
diff --git a/test/testSheet.cc b/test/testSheet.cc
new file mode 100644
index 000000000..be5fb901d
--- /dev/null
+++ b/test/testSheet.cc
@@ -0,0 +1,258 @@
+/*
+ @copyright Steve Keen 2024
+ @author Russell Standish
+ This file is part of Minsky.
+
+ Minsky is free software: you can redistribute it and/or modify it
+ under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Minsky is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Minsky. If not, see .
+*/
+#include "sheet.h"
+#include "minsky_epilogue.h"
+#include
+
+using namespace minsky;
+using namespace std;
+
+namespace
+{
+ class SheetTest : public Sheet, public ::testing::Test
+ {
+ protected:
+ void SetUp() override
+ {
+ // Set default dimensions for tests
+ iWidth(200);
+ iHeight(150);
+ moveTo(100, 100);
+ }
+ };
+
+ TEST_F(SheetTest, construction)
+ {
+ Sheet sheet;
+ EXPECT_EQ(1, sheet.portsSize());
+ EXPECT_EQ(100, sheet.iWidth());
+ EXPECT_EQ(100, sheet.iHeight());
+ }
+
+ TEST_F(SheetTest, corners)
+ {
+ moveTo(100, 100);
+ iWidth(200);
+ iHeight(150);
+ double z = zoomFactor();
+
+ auto cornerPoints = corners();
+ ASSERT_EQ(4, cornerPoints.size());
+
+ // Check that corners are at expected positions
+ EXPECT_DOUBLE_EQ(100 - 0.5 * 200 * z, cornerPoints[0].x());
+ EXPECT_DOUBLE_EQ(100 - 0.5 * 150 * z, cornerPoints[0].y());
+
+ EXPECT_DOUBLE_EQ(100 + 0.5 * 200 * z, cornerPoints[1].x());
+ EXPECT_DOUBLE_EQ(100 - 0.5 * 150 * z, cornerPoints[1].y());
+
+ EXPECT_DOUBLE_EQ(100 - 0.5 * 200 * z, cornerPoints[2].x());
+ EXPECT_DOUBLE_EQ(100 + 0.5 * 150 * z, cornerPoints[2].y());
+
+ EXPECT_DOUBLE_EQ(100 + 0.5 * 200 * z, cornerPoints[3].x());
+ EXPECT_DOUBLE_EQ(100 + 0.5 * 150 * z, cornerPoints[3].y());
+ }
+
+ TEST_F(SheetTest, onResizeHandle)
+ {
+ moveTo(100, 100);
+ iWidth(200);
+ iHeight(150);
+ double z = zoomFactor();
+ double w = 0.5 * 200 * z;
+ double h = 0.5 * 150 * z;
+
+ // Test bottom-right resize handle
+ EXPECT_TRUE(onResizeHandle(100 + w, 100 + h));
+
+ // Test top-right resize handle
+ EXPECT_TRUE(onResizeHandle(100 + w, 100 - h));
+
+ // Test bottom-left resize handle (when showRavel is false)
+ showRavel = false;
+ EXPECT_TRUE(onResizeHandle(100 - w, 100 + h));
+
+ // Test points not on resize handle
+ EXPECT_FALSE(onResizeHandle(100, 100));
+ }
+
+ TEST_F(SheetTest, onRavelButton)
+ {
+ moveTo(100, 100);
+ iWidth(200);
+ iHeight(150);
+ double z = zoomFactor();
+ double w = 0.5 * 200 * z;
+ double h = 0.5 * 150 * z;
+
+ // Without inputRavel, should return false
+ EXPECT_FALSE(onRavelButton(100 - w, 100 - h));
+ }
+
+ TEST_F(SheetTest, inItem)
+ {
+ moveTo(100, 100);
+ iWidth(200);
+ iHeight(150);
+
+ // Test point inside item
+ EXPECT_TRUE(inItem(100, 100));
+
+ // Test point outside item
+ EXPECT_FALSE(inItem(0, 0));
+ EXPECT_FALSE(inItem(300, 300));
+ }
+
+ TEST_F(SheetTest, clickType)
+ {
+ moveTo(100, 100);
+ iWidth(200);
+ iHeight(150);
+ double z = zoomFactor();
+ double w = 0.5 * 200 * z;
+ double h = 0.5 * 150 * z;
+
+ // Test resize handle
+ EXPECT_EQ(ClickType::onResize, clickType(100 + w, 100 + h));
+
+ // Test inside item
+ EXPECT_EQ(ClickType::inItem, clickType(100, 100));
+
+ // Test on item border
+ EXPECT_EQ(ClickType::onItem, clickType(100 + w * 0.9, 100 + h * 0.9));
+
+ // Test outside item
+ EXPECT_EQ(ClickType::outside, clickType(0, 0));
+ }
+
+ TEST_F(SheetTest, contains)
+ {
+ moveTo(100, 100);
+ iWidth(200);
+ iHeight(150);
+
+ // Test point inside item
+ EXPECT_TRUE(contains(100, 100));
+
+ // Test point outside item
+ EXPECT_FALSE(contains(0, 0));
+ }
+
+ TEST_F(SheetTest, scrollUpDown)
+ {
+ // Initial state - no scrolling possible without data
+ EXPECT_FALSE(scrollUp());
+ EXPECT_FALSE(scrollDown());
+ }
+
+ TEST_F(SheetTest, onKeyPress)
+ {
+ // Test arrow key handling
+ // Up arrow (0xff52)
+ EXPECT_FALSE(onKeyPress(0xff52, "", 0));
+
+ // Down arrow (0xff54)
+ EXPECT_FALSE(onKeyPress(0xff54, "", 0));
+
+ // Right arrow (0xff53)
+ EXPECT_FALSE(onKeyPress(0xff53, "", 0));
+
+ // Left arrow (0xff51)
+ EXPECT_FALSE(onKeyPress(0xff51, "", 0));
+
+ // Other key
+ EXPECT_FALSE(onKeyPress(0x0041, "A", 0));
+ }
+
+ TEST_F(SheetTest, exportAsCSVWithoutValue)
+ {
+ // Attempting to export without a value should throw an error
+ EXPECT_THROW(exportAsCSV("/tmp/test.csv", false), std::exception);
+ }
+
+ TEST_F(SheetTest, setSliceIndicator)
+ {
+ // With no value, setSliceIndicator should handle gracefully
+ setSliceIndicator();
+ // No exception should be thrown
+ }
+
+ TEST_F(SheetTest, showRavelFlag)
+ {
+ // Test the showRavel flag
+ showRavel = false;
+ EXPECT_FALSE(showRavel);
+
+ showRavel = true;
+ EXPECT_TRUE(showRavel);
+ }
+
+ TEST_F(SheetTest, showSliceFlags)
+ {
+ // Test ShowSlice enumeration values
+ showRowSlice = ShowSlice::head;
+ EXPECT_EQ(ShowSlice::head, showRowSlice);
+
+ showRowSlice = ShowSlice::tail;
+ EXPECT_EQ(ShowSlice::tail, showRowSlice);
+
+ showRowSlice = ShowSlice::headAndTail;
+ EXPECT_EQ(ShowSlice::headAndTail, showRowSlice);
+
+ showColSlice = ShowSlice::head;
+ EXPECT_EQ(ShowSlice::head, showColSlice);
+ }
+
+ TEST_F(SheetTest, dimensionProperties)
+ {
+ // Test width and height setters
+ iWidth(300);
+ EXPECT_EQ(300, iWidth());
+
+ iHeight(250);
+ EXPECT_EQ(250, iHeight());
+ }
+
+ TEST_F(SheetTest, positionProperties)
+ {
+ // Test position setters
+ moveTo(150, 175);
+ EXPECT_DOUBLE_EQ(150, x());
+ EXPECT_DOUBLE_EQ(175, y());
+ }
+
+ TEST_F(SheetTest, copyOperations)
+ {
+ Sheet sheet1;
+ sheet1.iWidth(300);
+ sheet1.iHeight(250);
+
+ Sheet sheet2;
+ sheet2 = sheet1; // Should be a no-op
+
+ // Original values should remain unchanged
+ EXPECT_EQ(100, sheet2.iWidth());
+ EXPECT_EQ(100, sheet2.iHeight());
+
+ // Test copy constructor
+ Sheet sheet3(sheet1); // Should be a no-op
+ EXPECT_EQ(100, sheet3.iWidth());
+ EXPECT_EQ(100, sheet3.iHeight());
+ }
+}