diff --git a/juicebox.install b/juicebox.install
new file mode 100644
index 0000000..25d687e
--- /dev/null
+++ b/juicebox.install
@@ -0,0 +1,36 @@
+<?php
+
+
+/**
+ * @file
+ * Install, update and uninstall functions for the Juicebox module.
+ */
+
+
+/**
+ * Implements hook_requirements().
+ */
+function juicebox_requirements($phase) {
+  $requirements = array();
+  // Check on the installation status of the Juicebox library. Note that
+  // we do not do this check during the 'install' phase as the libraries API
+  // functions will not always be available then (if installing Drupal via an
+  // install profile, etc.).
+  if ($phase == 'runtime') {
+    // Get locally installed library details.
+    $library = juicebox_library_detect(TRUE, TRUE);
+    $requirements['juicebox'] = array(
+      'title' => t('Juicebox Javascript Library'),
+    );
+    if ($library['installed']) {
+      $requirements['juicebox']['value'] = $library['version'];
+      $requirements['juicebox']['severity'] = REQUIREMENT_INFO;
+    }
+    else {
+      $requirements['juicebox']['value'] = $library['error'];
+      $requirements['juicebox']['description'] = $library['error message'];
+      $requirements['juicebox']['severity'] = REQUIREMENT_ERROR;
+    }
+  }
+  return $requirements;
+}
diff --git a/src/Controller/JuiceboxXmlControllerField.php b/src/Controller/JuiceboxXmlControllerField.php
index 2e0b564..4655941 100644
--- a/src/Controller/JuiceboxXmlControllerField.php
+++ b/src/Controller/JuiceboxXmlControllerField.php
@@ -10,7 +10,7 @@ namespace Drupal\juicebox\Controller;
 /**
  * Controller routines for field-based XML.
  */
-class JuiceboxXmlControllerField extends JuiceboxXMLControllerBase {
+class JuiceboxXmlControllerField extends JuiceboxXmlControllerBase {
   
   // Base properties that reference source data.
   protected $idArgs;
diff --git a/src/JuiceboxGalleryDrupal.php b/src/JuiceboxGalleryDrupal.php
index e158a0f..02cb84b 100644
--- a/src/JuiceboxGalleryDrupal.php
+++ b/src/JuiceboxGalleryDrupal.php
@@ -447,7 +447,7 @@ class JuiceboxGalleryDrupal extends JuiceboxGalleryDecorator implements Juicebox
       '#type' => 'details',
       '#title' => t('Juicebox Library - Pro / Manual Config'),
       '#open' => FALSE,
-      '#description' => '<p>' . t('Specify any additional Juicebox library configuration options (such as "Pro" options) here.<br/>Options set here always take precedence over those set in the "Lite" options above if there is a conflict.') . '</p>',
+      '#description' => t('Specify any additional Juicebox library configuration options (such as "Pro" options) here.<br/>Options set here always take precedence over those set in the "Lite" options above if there is a conflict.'),
       '#weight' => 20,
     );
     $form['manual_config'] = array(
diff --git a/src/Tests/JuiceboxBaseCase.php b/src/Tests/JuiceboxBaseCase.php
new file mode 100644
index 0000000..e254090
--- /dev/null
+++ b/src/Tests/JuiceboxBaseCase.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * @file
+ * Common helper methods for Juicebox module tests.
+ */
+
+namespace Drupal\juicebox\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+
+/**
+ * Common helper class for Juicebox module tests.
+ */
+abstract class JuiceboxBaseCase extends WebTestBase {
+
+  // Common variables.
+  protected $webUser;
+  // Properties to store details of the field that will be use in a field
+  // formatter test.
+  protected $instBundle;
+  protected $instFieldName;
+  protected $instFieldType;
+  
+  /**
+   * Setup a new content type, with a field that uses the Juicebox field
+   * formatter, and create a test node that uses it.
+   */
+  protected function prepNode() {
+    // Create a new content type.
+    $edit = array(
+      'name' => $this->instBundle,
+      'type' => $this->instBundle,
+    );
+    $this->drupalPostForm('admin/structure/types/add', $edit, t('Save and manage fields'));
+    // Add a file/image field to the new type.
+    $this->initFieldInstance();
+    // Create a node of the new type with a test image.
+    $this->node = $this->createNodeWithImage(current($this->drupalGetTestFiles('image')));
+  }
+
+  /**
+   * Helper to setup a file/image field and instance on a specified content
+   * type.
+   */
+  protected function initFieldInstance() {
+    // Add a field of type image to the core article type.
+    $field_name_friendly = $this->randomString();
+    $edit = array(
+      'fields[_add_new_field][label]' => $field_name_friendly,
+      'fields[_add_new_field][field_name]' => str_replace('field_', '', $this->instFieldName),
+      'fields[_add_new_field][type]' => $this->instFieldType,
+    );
+    $this->drupalPostForm('admin/structure/types/manage/' . $this->instBundle . '/fields', $edit, t('Save'));
+    // Make sure we all unlimited values.
+    $edit = array(
+      'field[cardinality]' => '-1',
+    );
+    $this->drupalPostForm(NULL, array(), t('Save field settings'));
+    // Toggle some options for the instance (depending on the field type) to
+    // ensure that caption fields are available.
+    $edit = array();
+    if ($this->instFieldType == 'image') {
+      $edit['field[settings][alt_field]'] = TRUE;
+      $edit['field[settings][title_field]'] = TRUE;
+    }
+    if ($this->instFieldType == 'file') {
+      $edit['field[settings][description_field]'] = TRUE;
+      $edit['field[settings][file_extensions]'] = 'txt,jpg,png,mp3,rtf,docx,pdf';
+    }
+    $this->drupalPostForm(NULL, $edit, t('Save settings'));
+    $this->assertText(t('Saved @name configuration.', array('@name' => $field_name_friendly)));
+  }
+
+  /**
+   * Helper to activate a Juicebox field formatter on a field.
+   */
+  protected function activateJuiceboxFieldFormatter() {
+    $this->drupalGet('admin/structure/types/manage/' . $this->instBundle . '/display');
+    $edit = array(
+      'fields[' . $this->instFieldName . '][type]' => 'juicebox_formatter',
+    );
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText(t('Your settings have been saved.'), 'Juicebox field formatter activated.');
+  }
+
+  /**
+   * Helper to upload an image to a node.
+   *
+   * @param $image
+   *   A file object representing the image to upload.
+   */
+  protected function createNodeWithImage($image) {
+    $edit = array(
+      'title[0][value]' => 'Test Juicebox Gallery Node',
+      'files[' . $this->instFieldName . '_0]' => drupal_realpath($image->uri),
+    );
+    $this->drupalPostForm('node/add/' . $this->instBundle, $edit, t('Save and publish'));
+    // Get ID of the newly created node from the current URL.
+    $matches = array();
+    preg_match('/node\/([0-9]+)/', $this->getUrl(), $matches);
+    if (isset($matches[1])) {
+      $nid = $matches[1];
+      // Now re-edit the node to add title and caption values for the newly
+      // uploaded image. This could probably also be done above with
+      // DrupalWebTestCase::drupalPostAJAX(), but this works too.
+      $edit = array(
+        'body[0][value]' => 'Some body content on node ' . $nid . ' <strong>with formatting</strong>',
+      );
+      if ($this->instFieldType == 'image') {
+        $edit[$this->instFieldName . '[0][title]'] = 'Some title text for field ' . $this->instFieldName . ' on node ' . $nid;
+        $edit[$this->instFieldName . '[0][alt]'] = 'Some alt text for field ' . $this->instFieldName . ' on node ' . $nid . ' <strong>with formatting</strong>';
+      }
+      if ($this->instFieldType == 'file') {
+        $edit[$this->instFieldName . '[0][description]'] = 'Some description text for field ' . $this->instFieldName . ' on node ' . $nid . ' <strong>with formatting</strong>';
+      }
+      $this->drupalPostForm('node/' . $nid . '/edit', $edit, t('Save and keep published'));
+      return \Drupal\node\Entity\Node::load($nid);
+    }
+    return FALSE;
+  }
+
+}
diff --git a/src/Tests/JuiceboxConfCase.php b/src/Tests/JuiceboxConfCase.php
new file mode 100644
index 0000000..cea9f5c
--- /dev/null
+++ b/src/Tests/JuiceboxConfCase.php
@@ -0,0 +1,126 @@
+<?php
+
+/**
+ * @file
+ * Test case for gallery-specific Juicebox configuration options.
+ */
+
+namespace Drupal\juicebox\Tests;
+
+
+/**
+ * Tests gallery-specific configuration logic for Juicebox galleries.
+ *
+ * @group Juicebox
+ */
+class JuiceboxConfCase extends JuiceboxBaseCase {
+  
+  public static $modules = array('node', 'field_ui', 'image', 'juicebox');
+  public $instBundle = 'juicebox_gallery';
+  public $instFieldName = 'field_image';
+  public $instFieldType = 'image';
+  
+  /**
+   * Define setup tasks.
+   */
+  public function setUp() {
+    parent::setUp();
+    // Create and login user.
+    $this->webUser = $this->drupalCreateUser(array('access content', 'access administration pages', 'administer site configuration', 'administer content types', 'administer nodes', 'administer node fields', 'administer node display', 'bypass node access'));
+    $this->drupalLogin($this->webUser);
+    // Prep a node with an image/file field and create a test entity.
+    $this->prepNode();
+    // Activte the field formatter for our new node instance.
+    $this->activateJuiceboxFieldFormatter();
+  }
+
+  /**
+   * Test common Lite configuration logic for a Juicebox formatter.
+   */
+  public function testConfigLite() {
+    $node = $this->node;
+    // Check control case without custom configuration.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertRaw("gallerywidth:'100%',galleryheight:'100%',backgroundcolor:'#222222'", 'Expected default configuration options set in embed javascript.');
+    $this->drupalGet('juicebox/xml/field/node/' . $node->id() . '/' . $this->instFieldName . '/full');
+    $this->assertRaw('<juicebox gallerywidth="100%" galleryheight="100%" backgroundcolor="#222222" textcolor="rgba(255,255,255,1)" thumbframecolor="rgba(255,255,255,.5)" showopenbutton="TRUE" showexpandbutton="TRUE" showthumbsbutton="TRUE" usethumbdots="FALSE" usefullscreenexpand="FALSE">', 'Expected default configuration options set in XML.');
+    // Alter settings to contain custom values.
+    $this->drupalPostAjaxForm('admin/structure/types/manage/' . $this->instBundle . '/display', array(), $this->instFieldName . '_settings_edit', NULL, array(), array(), 'field-ui-display-overview-form');
+    $edit = array(
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_galleryWidth]' => '50%',
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_galleryHeight]' => '200px',
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_backgroundColor]' => 'red',
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_textColor]' => 'green',
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_thumbFrameColor]' => 'blue',
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_showOpenButton]' => FALSE,
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_showExpandButton]' => FALSE,
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_showThumbsButton]' => FALSE,
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_useThumbDots]' => TRUE,
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_useFullscreenExpand]' => TRUE,
+    );
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText(t('Your settings have been saved.'), 'Gallery configuration changes saved.');
+    // Check for correct embed markup.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertRaw("gallerywidth:'50%',galleryheight:'200px',backgroundcolor:'red'", 'Expected custom Lite configuration options set in embed javascript.');
+    // Check for correct XML.
+    $this->drupalGet('juicebox/xml/field/node/' . $node->id() . '/' . $this->instFieldName . '/full');
+    $this->assertRaw('<juicebox gallerywidth="50%" galleryheight="200px" backgroundcolor="red" textcolor="green" thumbframecolor="blue" showopenbutton="FALSE" showexpandbutton="FALSE" showthumbsbutton="FALSE" usethumbdots="TRUE" usefullscreenexpand="TRUE">', 'Expected custom Lite configuration options set in XML.');
+  }
+
+  /**
+   * Test common Pro configuration logic for a Juicebox formatter.
+   */
+  public function testConfigPro() {
+    $node = $this->node;
+    // Set new manual options and also add a manual customization that's
+    // intended to override a custom Lite option.
+    $this->drupalPostAjaxForm('admin/structure/types/manage/' . $this->instBundle . '/display', array(), $this->instFieldName . '_settings_edit', NULL, array(), array(), 'field-ui-display-overview-form');
+    $edit = array(
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][jlib_showExpandButton]' => FALSE,
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][manual_config]' => "sHoWoPeNbUtToN=\"FALSE\"\nshowexpandbutton=\"TRUE\"\ngallerywidth=\"50%\"\nmyCustomSetting=\"boomsauce\"",
+    );
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText(t('Your settings have been saved.'), 'Gallery configuration changes saved.');
+    // Check for correct embed markup.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertRaw("gallerywidth:'50%',galleryheight:'100%',backgroundcolor:'#222222'", 'Expected custom configuration options set in embed javascript.');
+    // Check for correct XML.
+    $this->drupalGet('juicebox/xml/field/node/' . $node->id() . '/' . $this->instFieldName . '/full');
+    $this->assertRaw('<juicebox gallerywidth="50%" galleryheight="100%" backgroundcolor="#222222" textcolor="rgba(255,255,255,1)" thumbframecolor="rgba(255,255,255,.5)" showopenbutton="FALSE" showexpandbutton="TRUE" showthumbsbutton="TRUE" usethumbdots="FALSE" usefullscreenexpand="FALSE" mycustomsetting="boomsauce">', 'Expected custom Pro configuration options set in XML.');
+  }
+
+  /**
+   * Test common Advanced configuration logic for a Juicebox formatter.
+   */
+  public function testConfigAdvanced() {
+    $node = $this->node;
+    // Get the urls to the main image with and without "large" styling.
+    $uri = \Drupal\file\Entity\File::load($node->{$this->instFieldName}[0]->target_id)->getFileUri();
+    $test_image_url = file_create_url($uri);
+    $image_style = entity_load('image_style', 'juicebox_medium');
+    $test_image_url_formatted = $image_style->buildUrl($uri);
+    // Check control case without custom configuration.
+    $this->drupalGet('juicebox/xml/field/node/' . $node->id() . '/' . $this->instFieldName . '/full');
+    $this->assertRaw('linkTarget="_blank"', 'Default linkTarget setting found.');
+    $this->assertRaw('linkURL="' . $test_image_url, 'Test unstyled image found in XML');
+    // Set new advanced options.
+    $this->drupalPostAjaxForm('admin/structure/types/manage/' . $this->instBundle . '/display', array(), $this->instFieldName . '_settings_edit', NULL, array(), array(), 'field-ui-display-overview-form');
+    $edit = array(
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][image_style]' => 'juicebox_medium',
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][linkurl_source]' => 'image_styled',
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][linkurl_target]' => '_self',
+      'fields[' . $this->instFieldName . '][settings_edit_form][settings][custom_parent_classes]' => 'my-custom-wrapper',
+    );
+    $this->drupalPostForm(NULL, $edit, t('Save'));
+    $this->assertText(t('Your settings have been saved.'), 'Gallery configuration changes saved.');
+    // Check case with custom configuration.
+    $this->drupalGet('juicebox/xml/field/node/' . $node->id() . '/' . $this->instFieldName . '/full');
+    $this->assertRaw('linkTarget="_self"', 'Updated linkTarget setting found in XML.');
+    $this->assertRaw('linkURL="' . \Drupal\Component\Utility\String::checkPlain($test_image_url_formatted), 'Test styled image found in XML for linkURL.');
+    // Also check for custom class in embed code.
+    $this->drupalGet('node/' . $node->id());
+    $this->assertRaw('class="juicebox-parent my-custom-wrapper"', 'Custom class found in embed code.');
+  }
+
+}
