Files
2026-06-17 14:00:51 +02:00

395 lines
17 KiB
Java

package jmri.jmrix.dccpp.swing.exrail;
import jmri.jmrix.ConnectionStatus;
import jmri.jmrix.dccpp.DCCppCommandStation;
import jmri.jmrix.dccpp.DCCppExrailEntry;
import jmri.jmrix.dccpp.DCCppExrailEntry.State;
import jmri.jmrix.dccpp.DCCppInterfaceScaffold;
import jmri.jmrix.dccpp.DCCppMessage;
import jmri.jmrix.dccpp.DCCppReply;
import jmri.jmrix.dccpp.DCCppSystemConnectionMemo;
import jmri.util.JUnitUtil;
import jmri.util.junit.annotations.DisabledIfHeadless;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.jupiter.api.*;
import org.netbeans.jemmy.QueueTool;
import org.netbeans.jemmy.operators.JButtonOperator;
import org.netbeans.jemmy.operators.JDialogOperator;
import org.netbeans.jemmy.operators.JTextFieldOperator;
/**
* Tests for DCCppExrailFrame.
*
* @author Chad Francis Copyright (C) 2026
*/
@DisabledIfHeadless
public class DCCppExrailFrameTest extends jmri.util.JmriJFrameTestBase {
private DCCppInterfaceScaffold tc;
private DCCppSystemConnectionMemo memo;
@Test
public void testTitleIncludesPrefix() {
Assertions.assertTrue(frame.getTitle().contains("D"), "title should contain system prefix");
}
@Test
public void testPopulatesOnIdListReply() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
Assertions.assertEquals(0, exrailFrame.getEntryCount(), "should start empty");
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 2"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 2 A \"Yard Switcher\""));
Assertions.assertEquals(2, exrailFrame.getEntryCount(), "should have two entries after replies");
}
@Test
public void testSequentialRequestDispatch() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
int outboundBefore = tc.outbound.size();
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 2"));
Assertions.assertEquals(outboundBefore + 1, tc.outbound.size(),
"only first <JA id> should be sent immediately after IDs list");
Assertions.assertEquals(DCCppMessage.makeAutomationIDMsg(1).toString(),
tc.outbound.lastElement().toString(), "first request should be for ID 1");
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
Assertions.assertEquals(outboundBefore + 2, tc.outbound.size(),
"second <JA id> should be sent only after first reply arrives");
Assertions.assertEquals(DCCppMessage.makeAutomationIDMsg(2).toString(),
tc.outbound.lastElement().toString(), "second request should be for ID 2");
exrailFrame.message(DCCppReply.parseDCCppReply("jA 2 A \"Yard Switcher\""));
Assertions.assertEquals(outboundBefore + 2, tc.outbound.size(),
"no further requests once queue is drained");
}
@Test
public void testKnownIdNotRequeued() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
int outboundBefore = tc.outbound.size();
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
Assertions.assertEquals(outboundBefore, tc.outbound.size(),
"already-known ID should not be re-requested");
}
@Test
public void testCaptionUpdateApplied() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Original\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jB 1 \"Updated Caption\""));
Assertions.assertEquals("Updated Caption", exrailFrame.getEntry(1).getDisplayName());
}
@Test
public void testStateUpdateApplied() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Loop\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jB 1 2"));
Assertions.assertEquals(State.HIDDEN, exrailFrame.getEntry(1).getState());
}
@Test
public void testTriggerEnabledForActiveEntry() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
Assertions.assertTrue(exrailFrame.isRowTriggerEnabled(0),
"row trigger should be enabled for a non-DISABLED entry regardless of power state");
}
@Test
public void testTriggerEnabledWhenPowerOff() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
tc.sendTestMessage(DCCppReply.parseDCCppReply("p 0"));
Assertions.assertTrue(exrailFrame.isRowTriggerEnabled(0),
"row trigger should be enabled regardless of track power state");
}
@Test
public void testInitSendsAutomationIDsRequest() {
Assertions.assertFalse(tc.outbound.isEmpty(), "should have sent <JA> on initComponents");
Assertions.assertTrue(tc.outbound.get(0).isAutomationIDsMessage(), "first outbound should be <JA>");
}
@Test
public void testListenerRegisteredOnInit() {
Assertions.assertTrue(tc.numListeners() > 0, "frame should register as listener on initComponents");
}
@Test
public void testHiddenEntriesNotCounted() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 2"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Visible Route\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 2 A \"Hidden Auto\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jB 2 2")); // state=2 → hidden
Assertions.assertEquals(1, exrailFrame.getEntryCount(), "hidden entry should be excluded from count");
}
@Test
public void testTriggerRoute() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 2"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
DCCppExrailEntry entry = exrailFrame.getEntry(1);
Assertions.assertNotNull(entry);
exrailFrame.triggerEntry(entry, 0);
Assertions.assertEquals(DCCppMessage.makeStartExrailMsg(1).toString(),
tc.outbound.lastElement().toString(), "should send route start command");
}
@Test
public void testTriggerAutomation() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 2 3"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 2 A \"Yard Switcher\""));
DCCppExrailEntry entry = exrailFrame.getEntry(2);
Assertions.assertNotNull(entry);
exrailFrame.triggerEntry(entry, 1234);
Assertions.assertEquals(DCCppMessage.makeStartExrailMsg(2, 1234).toString(),
tc.outbound.lastElement().toString(), "should send automation start command with loco address");
}
@Test
public void testReconnectClearsEntriesAndRefetches() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 2"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 2 A \"Yard Switcher\""));
Assertions.assertEquals(2, exrailFrame.getEntryCount(), "should have entries before reconnect");
ConnectionStatus.instance().setConnectionState(memo, ConnectionStatus.CONNECTION_UP);
Assertions.assertEquals(0, exrailFrame.getEntryCount(), "entries should be cleared on reconnect");
Assertions.assertTrue(tc.outbound.lastElement().isAutomationIDsMessage(),
"should re-send <JA> on reconnect");
}
@Test
public void testReconnectNotTriggeredOnDisconnect() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 2"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 2 A \"Yard Switcher\""));
int outboundBefore = tc.outbound.size();
ConnectionStatus.instance().setConnectionState(memo, ConnectionStatus.CONNECTION_DOWN);
Assertions.assertEquals(2, exrailFrame.getEntryCount(), "entries should be preserved on disconnect");
Assertions.assertEquals(outboundBefore, tc.outbound.size(), "should not re-send <JA> on disconnect");
}
@Test
public void testRowClickFiresRoute() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
int outboundBefore = tc.outbound.size();
exrailFrame.triggerRowForTest(0);
// setValueAt wraps the trigger call in SwingUtilities.invokeLater, so wait for it.
String expected = DCCppMessage.makeStartExrailMsg(1).toString();
JUnitUtil.waitFor(() ->
tc.outbound.size() > outboundBefore
&& tc.outbound.lastElement().toString().equals(expected),
"row click should enqueue the route start command");
}
@Test
public void testDisabledEntryButtonNotEditable() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jB 1 4")); // state=4 -> DISABLED
Assertions.assertFalse(exrailFrame.isRowTriggerEnabled(0),
"row trigger should be disabled for entries in DISABLED state");
}
@Test
public void testButtonLabelDefaultsToSet() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
Assertions.assertEquals("Set", exrailFrame.getRowButtonLabel(0),
"button label should default to 'Set' when no caption");
}
@Test
public void testCaptionUsedAsButtonLabel() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jB 1 \"Go!\""));
Assertions.assertEquals("Go!", exrailFrame.getRowButtonLabel(0),
"caption should be used as button label");
}
@Test
public void testNameColumnShowsDescriptionWhenCaptionSet() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 R \"Station Loop\""));
exrailFrame.message(DCCppReply.parseDCCppReply("jB 1 \"Go!\""));
Assertions.assertEquals("Station Loop", exrailFrame.getRowName(0),
"name column should always show description, not caption");
}
@Test
public void testNoDialogAfterWindowDisposed() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 A \"Yard Switcher\""));
// Dispose the frame, then fire a trigger — no dialog should appear.
exrailFrame.dispose();
exrailFrame.triggerRowForTest(0);
new QueueTool().waitEmpty();
// If a dialog had appeared it would block the test; reaching here means it didn't.
Assertions.assertNull(exrailFrame.getLastLocoAddress(),
"no address should be stored when window is disposed before trigger fires");
}
@Test
public void testLocoAddressStoredAfterSuccessfulTrigger() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 A \"Yard Switcher\""));
Thread t = new Thread(() -> {
JDialogOperator d = new JDialogOperator("Start Automation");
new JTextFieldOperator(d).setText("1234");
new JButtonOperator(d, "OK").push();
});
t.start();
exrailFrame.triggerRowForTest(0);
JUnitUtil.waitFor(() -> !t.isAlive(), "jemmy thread finished");
new QueueTool().waitEmpty();
Assertions.assertEquals("1234", exrailFrame.getLastLocoAddress(),
"last loco address should be stored after successful trigger");
}
@Test
public void testLocoAddressPrefilledOnSecondTrigger() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 A \"Yard Switcher\""));
Thread t1 = new Thread(() -> {
JDialogOperator d = new JDialogOperator("Start Automation");
new JTextFieldOperator(d).setText("1234");
new JButtonOperator(d, "OK").push();
});
t1.start();
exrailFrame.triggerRowForTest(0);
JUnitUtil.waitFor(() -> !t1.isAlive(), "first jemmy thread finished");
new QueueTool().waitEmpty();
AtomicReference<String> capturedInitial = new AtomicReference<>();
Thread t2 = new Thread(() -> {
JDialogOperator d = new JDialogOperator("Start Automation");
capturedInitial.set(new JTextFieldOperator(d).getText());
new JButtonOperator(d, "OK").push();
});
t2.start();
exrailFrame.triggerRowForTest(0);
JUnitUtil.waitFor(() -> !t2.isAlive(), "second jemmy thread finished");
new QueueTool().waitEmpty();
Assertions.assertEquals("1234", capturedInitial.get(),
"second dialog should pre-fill the last entered address");
}
@Test
public void testInvalidLocoAddressShowsErrorAndRePrompts() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 A \"Yard Switcher\""));
Thread t = new Thread(() -> {
JDialogOperator input1 = new JDialogOperator("Start Automation");
new JTextFieldOperator(input1).setText("0");
new JButtonOperator(input1, "OK").push();
new JButtonOperator(new JDialogOperator("Invalid Loco Address"), "OK").push();
JDialogOperator input2 = new JDialogOperator("Start Automation");
new JTextFieldOperator(input2).setText("1234");
new JButtonOperator(input2, "OK").push();
});
t.start();
exrailFrame.triggerRowForTest(0);
JUnitUtil.waitFor(() -> !t.isAlive(), "jemmy thread finished");
new QueueTool().waitEmpty();
Assertions.assertEquals("1234", exrailFrame.getLastLocoAddress(),
"valid address entered after retry should be stored");
}
@Test
public void testCancelAfterErrorExitsWithoutSending() {
DCCppExrailFrame exrailFrame = (DCCppExrailFrame) frame;
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1"));
exrailFrame.message(DCCppReply.parseDCCppReply("jA 1 A \"Yard Switcher\""));
int outboundBefore = tc.outbound.size();
Thread t = new Thread(() -> {
JDialogOperator input1 = new JDialogOperator("Start Automation");
new JTextFieldOperator(input1).setText("abc");
new JButtonOperator(input1, "OK").push();
new JButtonOperator(new JDialogOperator("Invalid Loco Address"), "OK").push();
new JButtonOperator(new JDialogOperator("Start Automation"), "Cancel").push();
});
t.start();
exrailFrame.triggerRowForTest(0);
JUnitUtil.waitFor(() -> !t.isAlive(), "dialog sequence complete");
new QueueTool().waitEmpty();
Assertions.assertEquals(outboundBefore, tc.outbound.size(), "cancel after error should not send a command");
Assertions.assertNull(exrailFrame.getLastLocoAddress(), "cancel after error should not store an address");
}
@BeforeEach
@Override
public void setUp() {
JUnitUtil.setUp();
tc = new DCCppInterfaceScaffold(new DCCppCommandStation());
memo = new DCCppSystemConnectionMemo(tc);
frame = new DCCppExrailFrame(memo);
frame.initComponents();
}
@AfterEach
@Override
public void tearDown() {
memo.getDCCppTrafficController().terminateThreads();
memo.dispose();
memo = null;
tc = null;
JUnitUtil.deregisterBlockManagerShutdownTask();
super.tearDown();
}
}