diff --git a/.gitignore b/.gitignore
index 6ea54cb..13b8e83 100644
--- a/.gitignore
+++ b/.gitignore
@@ -84,10 +84,10 @@ Logs/*
.vscode/*
out/*
lib/*
-themes/*
-src/urChatBasic/backend/JoinClassLoader.java
-src/urChatBasic/frontend/utils/UIManagerDefaults.java
+themes
release
+test-output
+report
# not tested
-build/tag-build-win.ps1
+build/tag-build-win.ps1
\ No newline at end of file
diff --git a/README.md b/README.md
index db5abb9..788e0fb 100644
--- a/README.md
+++ b/README.md
@@ -5,14 +5,39 @@ urChat is a Java based IRC Client designed around simplicity and minimal resourc
Contributions
======
-**Currently targeting ![Milestone v0.4.0](https://github.com/matty-r/urChat/milestone/3)**
+**Currently targeting ![Milestone v0.4.0](https://github.com/matty-r/urChat/milestone/3)**
If you would like to assist in the development of urChat take a look at the Issues associated with the project. Please let me know if you wish to tackle a certain issue.
+Test/Code Coverage Dependencies
+======
+Dependencies required only for running the tests:
+
+Create a **lib/test** directory with the following files:
+
+* [Junit 4](https://repo1.maven.org/maven2/junit/junit/4.13.2/junit-4.13.2.jar)
+* [Hamcrest Core](https://repo1.maven.org/maven2/org/hamcrest/hamcrest-core/1.3/hamcrest-core-1.3.jar)
+* [TestNG 7.8.0](https://repo1.maven.org/maven2/org/testng/testng/7.8.0/testng-7.8.0.jar)
+* [JCommander](https://repo1.maven.org/maven2/com/beust/jcommander/1.82/jcommander-1.82.jar)
+* [SLF4J](https://repo1.maven.org/maven2/org/slf4j/slf4j-api/2.0.9/slf4j-api-2.0.9.jar)
+
+Extract jacocoagent.jar and jacococli.jar into the **lib/coverage** directory
+
+* [Jacoco](https://search.maven.org/remotecontent?filepath=org/jacoco/jacoco/0.8.11/jacoco-0.8.11.zip)
+
Usage
======
Ensure you've got Java 17 available on your system, download and run the latest JAR release (https://github.com/matty-r/urChat/releases). If you'd like to try out the Theme functionality, create a 'themes' directory next to the urChat.jar and download the FlatLAF.jar release and place within that directory. The theme can be selected under the client options page.
+Test Usage
+======
+
+Using the testng.xml - must be in the same directory as urchat.jar
+* java -cp "urTestRunner.jar" org.testng.TestNG testng.xml
+
+Without testng.xml
+* java -jar urTestRunner.jar
+
Screenshots
======
diff --git a/build/test-build-linux.sh b/build/test-build-linux.sh
new file mode 100755
index 0000000..a6dcf54
--- /dev/null
+++ b/build/test-build-linux.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+
+# Save the current directory to a variable
+initial_dir=$(pwd)
+
+# Clone the repository into a temporary directory
+temp_dir=$(mktemp -d)
+git clone . "$temp_dir"
+cd "$temp_dir"
+
+# Compile the Java files for the main jar
+find src -name "*.java" -exec javac -d "bin" {} +
+
+cd "bin"
+
+# Copy the images directory
+cp -r "$initial_dir/src/images" "."
+
+# Create a manifest file specifying the main class
+echo "Main-Class: urChatBasic.frontend.DriverGUI" > ../manifest.txt
+
+# Create the JAR file with the manifest and compiled class files, includes the lib and images directory in the created JAR file
+jar -cfm "urchat.jar" ../manifest.txt .
+
+# Delete all the files not needed to compile the test runner
+rm -rf "images"
+rm -rf "urChatBasic"
+
+# Copy the lib directory
+cp -r "$initial_dir/lib" "$temp_dir/"
+
+# Copy the test libs
+cp -r "$temp_dir/lib/test/" "$temp_dir/bin/"
+
+# Compile the Java files for the urTestRunner, using the urchat.jar as a source of the lib files
+find ../tests -name "*.java" -exec javac -cp "urchat.jar:test/*" -d . {} +
+
+# Extract the test libs to be included in urTestRunner.jar
+mkdir -p "$temp_dir/extracted_libs"
+cd "$temp_dir/extracted_libs"
+
+for file in "$temp_dir"/lib/test/*.jar; do
+ jar xf "$file"
+done
+
+cd "$temp_dir/bin"
+
+# Move the main.jar back into the temp dir (we don't want it included in urTestRunner)
+mv "urchat.jar" "$temp_dir"
+
+# Delete the test libs
+rm -rf "test"
+
+# Create a manifest file for urTestRunner
+echo "Main-Class: URTestRunner" > ../testmanifest.txt
+echo "Class-Path: urchat.jar test/*" >> ../testmanifest.txt
+
+# Compile to urTestRunner.jar using the testmanifest.txt, the contents of the current directory (/bin) plus the extracted_libs
+jar -cfm "urTestRunner.jar" "$temp_dir/testmanifest.txt" . -C "$temp_dir/extracted_libs/" .
+
+mv "urTestRunner.jar" "$temp_dir"
+
+cd "$temp_dir"
+
+mkdir -p "report"
+
+# run with jacoco agent to build coverage.exec
+java -javaagent:lib/coverage/jacocoagent.jar=destfile=coverage.exec -cp "urchat.jar:urTestRunner.jar" org.testng.TestNG ./build/testng_release.xml
+
+# build html report pointing to the source .java files
+java -jar lib/coverage/jacococli.jar report coverage.exec --classfiles urchat.jar --html report --sourcefiles src/
+
+# Move the JARs to the release directory
+mkdir -p "$initial_dir/release"
+mv "$temp_dir/urchat.jar" "$initial_dir/release"
+mv "$temp_dir/urTestRunner.jar" "$initial_dir/release"
+
+# Jacoco Output
+mv "report" "$initial_dir"
+
+# TestNG Output
+mv "test-output" "$initial_dir"
+
+# Clean up the temporary directory
+cd "$initial_dir"
+
+rm -rf "$temp_dir"
diff --git a/build/testng.xml b/build/testng.xml
new file mode 100644
index 0000000..226e038
--- /dev/null
+++ b/build/testng.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build/testng_release.xml b/build/testng_release.xml
new file mode 100644
index 0000000..a90cd1f
--- /dev/null
+++ b/build/testng_release.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/src/urChatBasic/UIManagerDefaults.java b/src/urChatBasic/UIManagerDefaults.java
new file mode 100644
index 0000000..0ec91a2
--- /dev/null
+++ b/src/urChatBasic/UIManagerDefaults.java
@@ -0,0 +1,747 @@
+package urChatBasic;
+/*
+ * This programs uses the information found in the UIManager
+ * to create a table of key/value pairs for each Swing component.
+ */
+
+import java.awt.*;
+import java.awt.event.*;
+import java.awt.image.*;
+import java.util.*;
+import javax.swing.*;
+import javax.swing.border.*;
+import javax.swing.plaf.*;
+import javax.swing.table.*;
+
+public class UIManagerDefaults implements ActionListener, ItemListener
+{
+ private final static String[] COLUMN_NAMES = {"Key", "Value", "Sample"};
+ private static String selectedItem;
+
+ private JComponent contentPane;
+ private JMenuBar menuBar;
+ private JComboBox comboBox;
+ private JRadioButton byComponent;
+ private JTable table;
+ private TreeMap> items;
+ private HashMap models;
+
+ /*
+ * Constructor
+ */
+ public UIManagerDefaults()
+ {
+ items = new TreeMap>();
+ models = new HashMap();
+
+ contentPane = new JPanel( new BorderLayout() );
+ contentPane.add(buildNorthComponent(), BorderLayout.NORTH);
+ contentPane.add(buildCenterComponent(), BorderLayout.CENTER);
+
+ resetComponents();
+ }
+
+ /*
+ * The content pane should be added to a high level container
+ */
+ public JComponent getContentPane()
+ {
+ return contentPane;
+ }
+
+ /*
+ * A menu can also be added which provides the ability to switch
+ * between different LAF's.
+ */
+ public JMenuBar getMenuBar()
+ {
+ if (menuBar == null)
+ menuBar = createMenuBar();
+
+ return menuBar;
+ }
+
+ /*
+ * This panel is added to the North of the content pane
+ */
+ private JComponent buildNorthComponent()
+ {
+ comboBox = new JComboBox();
+
+ JLabel label = new JLabel("Select Item:");
+ label.setDisplayedMnemonic('S');
+ label.setLabelFor( comboBox );
+
+ byComponent = new JRadioButton("By Component", true);
+ byComponent.setMnemonic('C');
+ byComponent.addActionListener( this );
+
+ JRadioButton byValueType = new JRadioButton("By Value Type");
+ byValueType.setMnemonic('V');
+ byValueType.addActionListener( this );
+
+ ButtonGroup group = new ButtonGroup();
+ group.add(byComponent);
+ group.add(byValueType);
+
+ JPanel panel = new JPanel();
+ panel.setBorder( new EmptyBorder(15, 0, 15, 0) );
+ panel.add( label );
+ panel.add( comboBox );
+ panel.add( byComponent );
+ panel.add( byValueType );
+ return panel;
+ }
+
+ /*
+ * This panel is added to the Center of the content pane
+ */
+ private JComponent buildCenterComponent()
+ {
+ DefaultTableModel model = new DefaultTableModel(COLUMN_NAMES, 0);
+ table = new JTable(model);
+ table.setAutoCreateColumnsFromModel( false );
+ table.getColumnModel().getColumn(0).setPreferredWidth(250);
+ table.getColumnModel().getColumn(1).setPreferredWidth(500);
+ table.getColumnModel().getColumn(2).setPreferredWidth(100);
+ table.getColumnModel().getColumn(2).setCellRenderer( new SampleRenderer() );
+ Dimension d = table.getPreferredSize();
+ d.height = 350;
+ table.setPreferredScrollableViewportSize( d );
+
+ return new JScrollPane( table );
+ }
+
+ /*
+ * When the LAF is changed we need to reset the content pane
+ */
+ public void resetComponents()
+ {
+ items.clear();
+ models.clear();
+ ((DefaultTableModel)table.getModel()).setRowCount(0);
+
+ buildItemsMap();
+
+ Vector comboBoxItems = new Vector(50);
+ Iterator keys = items.keySet().iterator();
+
+ while (keys.hasNext())
+ {
+ Object key = keys.next();
+ comboBoxItems.add( (String)key );
+ }
+
+ comboBox.removeItemListener( this );
+ comboBox.setModel( new DefaultComboBoxModel( comboBoxItems ) );
+ comboBox.setSelectedIndex(-1);
+ comboBox.addItemListener( this );
+ comboBox.requestFocusInWindow();
+
+ if (selectedItem != null)
+ comboBox.setSelectedItem(selectedItem);
+ }
+
+ /*
+ * The item map will contain items for each component or
+ * items for each attribute type.
+ */
+ private TreeMap buildItemsMap()
+ {
+ UIDefaults defaults = UIManager.getLookAndFeelDefaults();
+
+ // Build of Map of items and a Map of attributes for each item
+
+ for ( Enumeration enumm = defaults.keys(); enumm.hasMoreElements(); )
+ {
+ Object key = enumm.nextElement();
+ Object value = defaults.get( key );
+
+ String itemName = getItemName(key.toString(), value);
+
+ if (itemName == null) continue;
+
+ // Get the attribute map for this componenent, or
+ // create a map when one is not found
+
+ TreeMap attributeMap = items.get( itemName );
+
+ if (attributeMap == null)
+ {
+ attributeMap = new TreeMap();
+ items.put(itemName, attributeMap);
+ }
+
+ // Add the attribute to the map for this componenent
+
+ attributeMap.put(key.toString(), value );
+ }
+
+ return items;
+ }
+
+ /*
+ * Parse the key to determine the item name to use
+ */
+ private String getItemName(String key, Object value)
+ {
+ // Seems like this is an old check required for JDK1.4.2
+
+ if (key.startsWith("class") || key.startsWith("javax"))
+ return null;
+
+ if (byComponent.isSelected())
+ return getComponentName(key, value);
+ else
+ return getValueName(key, value);
+ }
+
+ private String getComponentName(String key, Object value)
+ {
+ // The key is of the form:
+ // "componentName.componentProperty", or
+ // "componentNameUI", or
+ // "someOtherString"
+
+ String componentName;
+
+ int pos = componentNameEndOffset( key );
+
+ if (pos != -1)
+ {
+ componentName = key.substring( 0, pos );
+ }
+ else if (key.endsWith( "UI" ) )
+ {
+ componentName = key.substring( 0, key.length() - 2 );
+ }
+ else if (value instanceof ColorUIResource)
+ {
+ componentName = "System Colors";
+ }
+ else
+ {
+ componentName = "Miscellaneous";
+ }
+
+ // Fix inconsistency
+
+ if (componentName.equals("Checkbox"))
+ {
+ componentName = "CheckBox";
+ }
+
+ return componentName;
+ }
+
+ private int componentNameEndOffset(String key)
+ {
+ // Handle Nimbus properties first
+
+ // "ComboBox.scrollPane", "Table.editor" and "Tree.cellEditor"
+ // have different format even within the Nimbus properties.
+ // (the component name is specified in quotes)
+
+ if (key.startsWith("\""))
+ return key.indexOf("\"", 1) + 1;
+
+ int pos = key.indexOf( ":" );
+
+ if (pos != -1)
+ return pos;
+
+ pos = key.indexOf( "[" );
+
+ if (pos != -1)
+ return pos;
+
+ // Handle normal properties
+
+ return key.indexOf( "." );
+ }
+
+ private String getValueName(String key, Object value)
+ {
+ if (value instanceof Icon)
+ return "Icon";
+ else if (value instanceof Font)
+ return "Font";
+ else if (value instanceof Border)
+ return "Border";
+ else if (value instanceof Color)
+ return "Color";
+ else if (value instanceof Insets)
+ return "Insets";
+ else if (value instanceof Boolean)
+ return "Boolean";
+ else if (value instanceof Dimension)
+ return "Dimension";
+ else if (value instanceof Number)
+ return "Number";
+ else if (value instanceof Painter)
+ return "Painter";
+ else if (key.endsWith("UI"))
+ return "UI";
+ else if (key.endsWith("InputMap"))
+ return "InputMap";
+ else if (key.endsWith("RightToLeft"))
+ return "InputMap";
+ else if (key.endsWith("radient"))
+ return "Gradient";
+ else
+ {
+ return "The Rest";
+ }
+ }
+
+ /**
+ * Create menu bar
+ */
+ private JMenuBar createMenuBar()
+ {
+ JMenuBar menuBar = new JMenuBar();
+
+ menuBar.add( createFileMenu() );
+ menuBar.add( createLAFMenu() );
+
+ return menuBar;
+ }
+
+ /**
+ * Create menu items for the Application menu
+ */
+ private JMenu createFileMenu()
+ {
+ JMenu menu = new JMenu("Application");
+ menu.setMnemonic('A');
+
+ menu.addSeparator();
+ menu.add( new ExitAction() );
+
+ return menu;
+ }
+
+ /**
+ * Create menu items for the Look & Feel menu
+ */
+ private JMenu createLAFMenu()
+ {
+ ButtonGroup bg = new ButtonGroup();
+
+ JMenu menu = new JMenu("Look & Feel");
+ menu.setMnemonic('L');
+
+ String lafId = UIManager.getLookAndFeel().getID();
+ UIManager.LookAndFeelInfo[] lafInfo = UIManager.getInstalledLookAndFeels();
+
+ for (int i = 0; i < lafInfo.length; i++)
+ {
+ String laf = lafInfo[i].getClassName();
+ String name= lafInfo[i].getName();
+
+ Action action = new ChangeLookAndFeelAction(this, laf, name);
+ JRadioButtonMenuItem mi = new JRadioButtonMenuItem( action );
+ menu.add( mi );
+ bg.add( mi );
+
+ if (name.equals(lafId))
+ {
+ mi.setSelected(true);
+ }
+ }
+
+ return menu;
+ }
+
+ /*
+ * Implement the ActionListener interface
+ */
+ public void actionPerformed(ActionEvent e)
+ {
+ selectedItem = null;
+ resetComponents();
+ comboBox.requestFocusInWindow();
+ }
+
+ /*
+ * Implement the ItemListener interface
+ */
+ public void itemStateChanged(ItemEvent e)
+ {
+ String itemName = (String)e.getItem();
+ changeTableModel( itemName );
+ updateRowHeights();
+ selectedItem = itemName;
+ }
+
+ /*
+ * Change the TabelModel in the table for the selected item
+ */
+ private void changeTableModel(String itemName)
+ {
+ // The model has been created previously so just use it
+
+ DefaultTableModel model = models.get( itemName );
+
+ if (model != null)
+ {
+ table.setModel( model );
+ return;
+ }
+
+ // Create a new model for the requested item
+ // and add the attributes of the item to the model
+
+ model = new DefaultTableModel(COLUMN_NAMES, 0);
+ Map attributes = (Map)items.get( itemName );
+
+ Iterator ai = attributes.keySet().iterator();
+
+ while (ai.hasNext())
+ {
+ String attribute = (String)ai.next();
+ Object value = attributes.get(attribute);
+
+ Vector