diff --git a/core/MAINTAINERS.txt b/core/MAINTAINERS.txt
index fff63c1..d937411 100644
--- a/core/MAINTAINERS.txt
+++ b/core/MAINTAINERS.txt
@@ -173,6 +173,9 @@ Block module
Book module
- Peter Wolanin 'pwolanin' http://drupal.org/user/49851
+Breakpoint module
+- Peter Droogmans 'attiks' http://drupal.org/user/105002
+
Color module
- ?
diff --git a/core/modules/breakpoint/breakpoint.info b/core/modules/breakpoint/breakpoint.info
new file mode 100644
index 0000000..fee160f
--- /dev/null
+++ b/core/modules/breakpoint/breakpoint.info
@@ -0,0 +1,7 @@
+name = Breakpoint
+description = Manage breakpoints and breakpoint groups for responsive designs.
+package = Core
+version = VERSION
+core = 8.x
+
+dependencies[] = config
diff --git a/core/modules/breakpoint/breakpoint.module b/core/modules/breakpoint/breakpoint.module
new file mode 100644
index 0000000..e2d8bdc
--- /dev/null
+++ b/core/modules/breakpoint/breakpoint.module
@@ -0,0 +1,423 @@
+' . t('About') . '';
+ $output .= '
' . t('The Breakpoint module allows the management of breakpoints and breakpoint groups for responsive designs.') . '
';
+ $output .= '' . t('Uses') . '
';
+ $output .= '';
+ $output .= '- ' . t('Breakpoints') . '
';
+ $output .= '- ' . t('Breakpoints can be defined by themes or other modules, a breakpoints consist of a name and a media query.') . '
';
+ $output .= '- ' . t('Breakpoint groups') . '
';
+ $output .= '- ' . t('Breakpoints can be organized into breakpoint groups, so that it is easier to manage and use them.') . '
';
+ $output .= '- ' . t('Multipliers') . '
';
+ $output .= '- ' . t('Multipliers can be defined for each breakpoint and are needed to handle screens with high dpi.') . '
';
+ $output .= '
';
+
+ return $output;
+ }
+}
+
+/**
+ * Implements hook_enable().
+ *
+ * Import breakpoints from all enabled themes.
+ */
+function breakpoint_enable() {
+ // Import breakpoints from themes.
+ $themes = list_themes();
+ _breakpoint_theme_enabled(array_keys($themes));
+
+ // Import breakpoints from modules.
+ $modules = module_list();
+ _breakpoint_modules_enabled(array_keys($modules));
+}
+
+/**
+ * Implements hook_themes_enabled().
+ *
+ * @param array $theme_list
+ * An array of theme names.
+ *
+ * @see _breakpoint_theme_enabled()
+ */
+function breakpoint_themes_enabled($theme_list) {
+ _breakpoint_theme_enabled($theme_list);
+}
+
+/**
+ * Implements hook_themes_disabled().
+ *
+ * @param array $theme_list
+ * An array of theme names.
+ *
+ * @see _breakpoint_delete_breakpoints()
+ */
+function breakpoint_themes_disabled($theme_list) {
+ _breakpoint_delete_breakpoints($theme_list, Breakpoint::SOURCE_TYPE_THEME);
+}
+
+/**
+ * Implements hook_modules_enabled().
+ *
+ * @param array $modules
+ * An array of the modules that were enabled.
+ *
+ * @see _breakpoint_modules_enabled()
+ */
+function breakpoint_modules_enabled($modules) {
+ _breakpoint_modules_enabled($modules);
+}
+
+/**
+ * Implements hook_modules_uninstalled().
+ *
+ * @param array $modules
+ * An array of the modules that were uninstalled.
+ *
+ * @see _breakpoint_delete_breakpoints()
+ */
+function breakpoint_modules_uninstalled($modules) {
+ _breakpoint_delete_breakpoints($modules, Breakpoint::SOURCE_TYPE_MODULE);
+}
+
+/**
+ * Import breakpoints from all new enabled themes.
+ *
+ * @param array $theme_list
+ * An array of theme names.
+ */
+function _breakpoint_theme_enabled($theme_list) {
+ $themes = list_themes();
+ foreach ($theme_list as $theme_key) {
+ if ($themes[$theme_key]->status) {
+ $media_queries = breakpoint_get_theme_media_queries($theme_key);
+ _breakpoint_import_media_queries($theme_key, $themes[$theme_key]->info['name'], Breakpoint::SOURCE_TYPE_THEME, $media_queries);
+ // Import custom groups.
+ _breakpoint_import_breakpoint_groups($theme_key, Breakpoint::SOURCE_TYPE_THEME);
+ }
+ }
+}
+
+/**
+ * Import breakpoints from all new enabled modules.
+ *
+ * @param array $modules
+ * An array of the modules that were enabled.
+ */
+function _breakpoint_modules_enabled($modules) {
+ foreach ($modules as $module) {
+ $media_queries = breakpoint_get_module_media_queries($module);
+ _breakpoint_import_media_queries($module, $module, Breakpoint::SOURCE_TYPE_MODULE, $media_queries);
+ // Import custom groups.
+ _breakpoint_import_breakpoint_groups($module, Breakpoint::SOURCE_TYPE_MODULE);
+ }
+}
+
+/**
+ * Import media queries from a theme or module and create a default group.
+ *
+ * @param string $group_name
+ * Machine readable name of the breakpoint group.
+ * @param string $label
+ * Human readable name of the breakpoint group.
+ * @param string $sourceType
+ * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE.
+ * @param array $media_queries
+ * An array of breakpoints in the form $breakpoint['name'] = 'media query'.
+ */
+function _breakpoint_import_media_queries($group_name, $label, $source_type, $media_queries) {
+ if (!empty($media_queries)) {
+ // Create a new breakpoint group if it doesn't exist.
+ $breakpoint_group = _breakpoint_group_create_or_load($group_name, $label, $group_name, $source_type);
+
+ // Load all media queries, create a breakpoint for each one and add them
+ // to this breakpoint group.
+ foreach ($media_queries as $name => $media_query) {
+ $breakpoint_group->addBreakpointFromMediaQuery($name, $media_query);
+ }
+
+ $breakpoint_group->save();
+ }
+}
+
+/**
+ * Import breakpoint groups from theme or module.
+ *
+ * @param string $source
+ * The theme or module name
+ * @param string $sourceType
+ * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE.
+ */
+function _breakpoint_import_breakpoint_groups($source, $source_type) {
+ $breakpoint_groups = config($source . '.breakpoint_groups');
+ if ($breakpoint_groups) {
+ foreach ($breakpoint_groups->get() as $group_name => $data) {
+ // Breakpoints is mandatory, extra check since this is coming from config.
+ if (isset($data['breakpoints']) && !empty($data['breakpoints'])) {
+ // Create a new breakpoint group if it doesn't exist.
+ $breakpoint_group = _breakpoint_group_create_or_load($group_name, isset($data['label']) ? $data['label'] : $group_name, $source, $source_type);
+ // Add the breakpoints.
+ $breakpoint_group->addBreakpoints($data['breakpoints']);
+ $breakpoint_group->save();
+ }
+ else {
+ throw new \Exception('Illegal config file detected.');
+ }
+ }
+ }
+}
+
+/**
+ * Remove breakpoints from all disabled themes or uninstalled modules.
+ *
+ * The source type has to match the original source type, otherwise the group
+ * will not be deleted. All groups created by the theme or module will be
+ * deleted as well.
+ *
+ * @param array $list
+ * A list of modules or themes that are disabled.
+ * @param string $sourceType
+ * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE.
+ */
+function _breakpoint_delete_breakpoints($list, $source_type) {
+ $ids = config_get_storage_names_with_prefix('breakpoint.breakpoint_group.' . $source_type . '.');
+ $entity_info = entity_get_info('breakpoint_group');
+
+ // Remove the breakpoint.breakpoint part of the breakpoint identifier.
+ foreach ($ids as &$id) {
+ $id = drupal_substr($id, drupal_strlen($entity_info['config prefix']) + 1);
+ }
+ $breakpoint_groups = entity_load_multiple('breakpoint_group', $ids);
+
+ foreach ($breakpoint_groups as $breakpoint_group) {
+ if ($breakpoint_group->sourceType == $source_type && in_array($breakpoint_group->source, $list)) {
+ // Delete the automatically created breakpoint group.
+ $breakpoint_group->delete();
+
+ // Get all breakpoints defined by this theme/module.
+ $breakpoint_ids = drupal_container()->get('config.storage')->listAll('breakpoint.breakpoint.' . $source_type . '.' . $breakpoint_group->id() . '.');
+ $entity_info = entity_get_info('breakpoint');
+
+ // Remove the breakpoint.breakpoint part of the breakpoint identifier.
+ foreach ($breakpoint_ids as &$breakpoint_id) {
+ $breakpoint_id = drupal_substr($breakpoint_id, drupal_strlen($entity_info['config prefix']) + 1);
+ }
+ $breakpoints = entity_load_multiple('breakpoint', $breakpoint_ids);
+
+ // Make sure we only delete breakpoints defined by this theme/module.
+ foreach ($breakpoints as $breakpoint) {
+ if ($breakpoint->sourceType == $source_type && $breakpoint->source == $breakpoint_group->name) {
+ $breakpoint->delete();
+ }
+ }
+ }
+ }
+
+ // Delete groups defined by a module/theme even if that module/theme didn't
+ // define any breakpoints.
+ foreach ($ids as $id) {
+ // Delete all breakpoint groups defined by the theme or module.
+ _breakpoint_delete_breakpoint_groups($id, $source_type);
+ }
+}
+
+/**
+ * Remove breakpoint groups from all disabled themes or uninstalled modules.
+ *
+ * @param array $group_id
+ * Machine readable name of the breakpoint group.
+ * @param string $sourceType
+ * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE.
+ */
+function _breakpoint_delete_breakpoint_groups($group_id, $source_type) {
+ $breakpoint_groups = entity_load_multiple('breakpoint_group');
+ foreach ($breakpoint_groups as $breakpoint_group) {
+ if ($breakpoint_group->sourceType == $source_type && $breakpoint_group->source == $group_id) {
+ $breakpoint_group->delete();
+ }
+ }
+}
+
+/**
+ * Get a list of available breakpoints from a specified theme.
+ *
+ * @param string $theme_key
+ * The name of the theme.
+ *
+ * @return array
+ * An array of breakpoints in the form $breakpoint['name'] = 'media query'.
+ */
+function breakpoint_get_theme_media_queries($theme_key) {
+ $themes = list_themes();
+ if (!isset($themes[$theme_key])) {
+ throw new \Exception('Illegal theme_key passed.');
+ }
+
+ $config = config($theme_key . '.breakpoints');
+ if ($config) {
+ return $config->get();
+ }
+ return array();
+}
+
+/**
+ * Get a list of available breakpoints from a specified module.
+ *
+ * @param string $module
+ * The name of the module.
+ *
+ * @return array
+ * An array of breakpoints in the form $breakpoint['name'] = 'media query'.
+ */
+function breakpoint_get_module_media_queries($module) {
+ if (!module_exists($module)) {
+ throw new \Exception('Illegal module name passed.');
+ }
+
+ $config = config($module . '.breakpoints');
+ if ($config) {
+ return $config->get();
+ }
+ return array();
+}
+
+/**
+ * Implements hook_entity_info().
+ */
+function breakpoint_entity_info() {
+ // Breakpoint.
+ $types['breakpoint'] = array(
+ 'label' => 'Breakpoint',
+ 'entity class' => 'Drupal\breakpoint\Breakpoint',
+ 'controller class' => 'Drupal\Core\Config\Entity\ConfigStorageController',
+ 'config prefix' => 'breakpoint.breakpoint',
+ 'entity keys' => array(
+ 'id' => 'id',
+ 'label' => 'label',
+ 'uuid' => 'uuid',
+ ),
+ );
+
+ // Breakpoint group.
+ $types['breakpoint_group'] = array(
+ 'label' => 'Breakpoint group',
+ 'entity class' => 'Drupal\breakpoint\BreakpointGroup',
+ 'controller class' => 'Drupal\Core\Config\Entity\ConfigStorageController',
+ 'config prefix' => 'breakpoint.breakpoint_group',
+ 'entity keys' => array(
+ 'id' => 'id',
+ 'label' => 'label',
+ 'uuid' => 'uuid',
+ ),
+ );
+
+ return $types;
+}
+
+/**
+ * Load one breakpoint group by its identifier.
+ *
+ * @param string $id
+ * The id of the breakpoint group to load.
+ *
+ * @return Drupal\breakpoint\BreakpointGroup|false
+ * The breakpoint group, or FALSE if there is no entity with the given id.
+ *
+ * @todo Remove this in a follow-up issue.
+ * @see http://drupal.org/node/1798214
+ */
+function breakpoint_group_load($id) {
+ return entity_load('breakpoint_group', $id);
+}
+
+/**
+ * Load one breakpoint by its identifier.
+ *
+ * @param int $id
+ * The id of the breakpoint to load.
+ *
+ * @return Drupal\breakpoint\Breakpoint
+ * The entity object, or FALSE if there is no entity with the given id.
+ *
+ * @todo Remove this in a follow-up issue.
+ * @see http://drupal.org/node/1798214
+ */
+function breakpoint_load($id) {
+ return entity_load('breakpoint', $id);
+}
+
+/**
+ * Load all breakpoint groups as select options.
+ *
+ * @return array
+ * An array containing breakpoint group labels indexed by their ids.
+ */
+function breakpoint_group_labels() {
+ $options = array();
+ $breakpoint_groups = entity_load_multiple('breakpoint_group');
+ foreach ($breakpoint_groups as $breakpoint_group) {
+ $options[$breakpoint_group->id()] = $breakpoint_group->label();
+ }
+ asort($options);
+ return $options;
+}
+
+/**
+ * Load all breakpoints as select options.
+ *
+ * @return array
+ * An array containing breakpoints indexed by their ids.
+ */
+function breakpoint_labels() {
+ $options = array();
+ $breakpoints = entity_load_multiple('breakpoint');
+ foreach ($breakpoints as $breakpoint) {
+ $options[$breakpoint->id()] = $breakpoint->label() . ' (' . $breakpoint->source . ' - ' . $breakpoint->sourceType . ') [' . $breakpoint->mediaQuery . ']';
+ }
+
+ return $options;
+}
+
+/**
+ * Helper function to easily create/load a breakpoint group.
+ *
+ * @param string $name
+ * Machine readable name of the breakpoint group.
+ * @param string $label
+ * Human readable name of the breakpoint group.
+ * @param string $source
+ * Machine readable name of the defining theme or module.
+ * @param string $sourceType
+ * Either Breakpoint::SOURCE_TYPE_THEME or Breakpoint::SOURCE_TYPE_MODULE.
+ *
+ * @return Drupal\breakpoint\BreakpointGroup
+ */
+function _breakpoint_group_create_or_load($name, $label, $source, $source_type) {
+ // Try loading the breakpoint group.
+ $breakpoint_group = entity_load('breakpoint_group', $source_type . '.' . $source . '.' . $name);
+ // Create a new breakpoint group if it doesn't exist.
+ if (!$breakpoint_group) {
+ // Build a new breakpoint group.
+ $breakpoint_group = entity_create('breakpoint_group', array(
+ 'name' => $name,
+ 'label' => $label,
+ 'source' => $source,
+ 'sourceType' => $source_type,
+ ));
+ }
+ return $breakpoint_group;
+}
diff --git a/core/modules/breakpoint/config/breakpoint.yml b/core/modules/breakpoint/config/breakpoint.yml
new file mode 100644
index 0000000..0ba703d
--- /dev/null
+++ b/core/modules/breakpoint/config/breakpoint.yml
@@ -0,0 +1,2 @@
+multipliers: [1x, 1.5x, 2x]
+
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Breakpoint.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Breakpoint.php
new file mode 100644
index 0000000..50059d0
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Breakpoint.php
@@ -0,0 +1,283 @@
+isValid()) {
+ throw new InvalidBreakpointException('Invalid data detected.');
+ }
+
+ // Build an id if non is set.
+ // Since a particular name can be used by multiple theme/modules we need
+ // to make a unique id.
+ if (empty($this->id)) {
+ $this->id = $this->sourceType . '.' . $this->source . '.' . $this->name;
+ }
+
+ // Set the label if none is set.
+ if (empty($this->label)) {
+ $this->label = $this->name;
+ }
+
+ // Remove unused multipliers.
+ $this->multipliers = array_filter($this->multipliers);
+
+ // Always add '1x' multiplier, use array_key_exists since the value might
+ // be NULL.
+ if (!array_key_exists('1x', $this->multipliers)) {
+ $this->multipliers = array('1x' => '1x') + $this->multipliers;
+ }
+ return parent::save();
+ }
+
+ /**
+ * Duplicates a breakpoint.
+ *
+ * The new breakpoint inherits the media query.
+ *
+ * @return Drupal\breakpoint\Breakpoint
+ */
+ public function duplicate() {
+ return entity_create('breakpoint', array(
+ 'mediaQuery' => $this->mediaQuery,
+ ));
+ }
+
+ /**
+ * Checks if the breakpoint is valid.
+ *
+ * @throws Drupal\breakpoint\InvalidBreakpointSourceTypeException
+ * @throws Drupal\breakpoint\InvalidBreakpointSourceException
+ * @throws Drupal\breakpoint\InvalidBreakpointNameException
+ * @throws Drupal\breakpoint\InvalidBreakpointMediaQueryException
+ *
+ * @see isValidMediaQuery()
+ */
+ public function isValid() {
+ // Check for illegal values in breakpoint source type.
+ if (!in_array($this->sourceType, array(
+ Breakpoint::SOURCE_TYPE_CUSTOM,
+ Breakpoint::SOURCE_TYPE_MODULE,
+ Breakpoint::SOURCE_TYPE_THEME)
+ )) {
+ throw new InvalidBreakpointSourceTypeException(format_string('Invalid source type @source_type', array(
+ '@source_type' => $this->sourceType,
+ )));
+ }
+ // Check for illegal characters in breakpoint source.
+ if (preg_match('/[^a-z_]+/', $this->source)) {
+ throw new InvalidBreakpointSourceException(format_string("Invalid value '@source' for breakpoint source property. Breakpoint source property can only contain lowercase letters and underscores.", array('@source' => $this->source)));
+ }
+ // Check for illegal characters in breakpoint names.
+ if (preg_match('/[^0-9a-z_\-]/', $this->name)) {
+ throw new InvalidBreakpointNameException(format_string("Invalid value '@name' for breakpoint name property. Breakpoint name property can only contain lowercase alphanumeric characters, underscores (_), and hyphens (-).", array('@name' => $this->name)));
+ }
+ return $this::isValidMediaQuery($this->mediaQuery);
+ }
+
+ /**
+ * Checks if a mediaQuery is valid.
+ *
+ * @throws Drupal\breakpoint\InvalidBreakpointMediaQueryException
+ *
+ * @return true
+ * Returns true if the media query is valid.
+ *
+ * @see http://www.w3.org/TR/css3-mediaqueries/
+ * @see http://www.w3.org/Style/CSS/Test/MediaQueries/20120229/reports/implement-report.html
+ * @see https://github.com/adobe/webkit/blob/master/Source/WebCore/css/
+ */
+ public static function isValidMediaQuery($media_query) {
+ $media_features = array(
+ 'width' => 'length', 'min-width' => 'length', 'max-width' => 'length',
+ 'height' => 'length', 'min-height' => 'length', 'max-height' => 'length',
+ 'device-width' => 'length', 'min-device-width' => 'length', 'max-device-width' => 'length',
+ 'device-height' => 'length', 'min-device-height' => 'length', 'max-device-height' => 'length',
+ 'orientation' => array('portrait', 'landscape'),
+ 'aspect-ratio' => 'ratio', 'min-aspect-ratio' => 'ratio', 'max-aspect-ratio' => 'ratio',
+ 'device-aspect-ratio' => 'ratio', 'min-device-aspect-ratio' => 'ratio', 'max-device-aspect-ratio' => 'ratio',
+ 'color' => 'integer', 'min-color' => 'integer', 'max-color' => 'integer',
+ 'color-index' => 'integer', 'min-color-index' => 'integer', 'max-color-index' => 'integer',
+ 'monochrome' => 'integer', 'min-monochrome' => 'integer', 'max-monochrome' => 'integer',
+ 'resolution' => 'resolution', 'min-resolution' => 'resolution', 'max-resolution' => 'resolution',
+ 'scan' => array('progressive', 'interlace'),
+ 'grid' => 'integer',
+ );
+ if ($media_query) {
+ // Strip new lines and trim.
+ $media_query = str_replace(array("\r", "\n"), ' ', trim($media_query));
+
+ // Remove comments /* ... */.
+ $media_query = preg_replace('/\/\*[\s\S]*?\*\//', '', $media_query);
+
+ // Check mediaQuery_list: S* [mediaQuery [ ',' S* mediaQuery ]* ]?
+ $parts = explode(',', $media_query);
+ foreach ($parts as $part) {
+ // Split on ' and '
+ $query_parts = explode(' and ', trim($part));
+ $media_type_found = FALSE;
+ foreach ($query_parts as $query_part) {
+ $matches = array();
+ // Check expression: '(' S* media_feature S* [ ':' S* expr ]? ')' S*
+ if (preg_match('/^\(([\w\-]+)(:\s?([\w\-\.]+))?\)/', trim($query_part), $matches)) {
+ // Single expression.
+ if (isset($matches[1]) && !isset($matches[2])) {
+ if (!array_key_exists($matches[1], $media_features)) {
+ throw new InvalidBreakpointMediaQueryException('Invalid media feature detected.');
+ }
+ }
+ // Full expression.
+ elseif (isset($matches[3]) && !isset($matches[4])) {
+ $value = trim($matches[3]);
+ if (!array_key_exists($matches[1], $media_features)) {
+ throw new InvalidBreakpointMediaQueryException('Invalid media feature detected.');
+ }
+ if (is_array($media_features[$matches[1]])) {
+ // Check if value is allowed.
+ if (!array_key_exists($value, $media_features[$matches[1]])) {
+ throw new InvalidBreakpointMediaQueryException('Value is not allowed.');
+ }
+ }
+ else {
+ switch ($media_features[$matches[1]]) {
+ case 'length':
+ $length_matches = array();
+ if (preg_match('/^(\-)?(\d+(?:\.\d+)?)?((?:|em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|dpi|dpcm))$/i', trim($value), $length_matches)) {
+ // Only -0 is allowed.
+ if ($length_matches[1] === '-' && $length_matches[2] !== '0') {
+ throw new InvalidBreakpointMediaQueryException('Invalid length detected.');
+ }
+ // If there's a unit, a number is needed as well.
+ if ($length_matches[2] === '' && $length_matches[3] !== '') {
+ throw new InvalidBreakpointMediaQueryException('Unit found, value is missing.');
+ }
+ }
+ else {
+ throw new InvalidBreakpointMediaQueryException('Invalid unit detected.');
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ // Check [ONLY | NOT]? S* media_type
+ elseif (preg_match('/^((?:only|not)?\s?)([\w\-]+)$/i', trim($query_part), $matches)) {
+ if ($media_type_found) {
+ throw new InvalidBreakpointMediaQueryException('Only one media type is allowed.');
+ }
+ $media_type_found = TRUE;
+ }
+ else {
+ throw new InvalidBreakpointMediaQueryException('Invalid media query detected.');
+ }
+ }
+ }
+ return TRUE;
+ }
+ throw new InvalidBreakpointMediaQueryException('Media query is empty.');
+ }
+}
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroup.php b/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroup.php
new file mode 100644
index 0000000..de56397
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/BreakpointGroup.php
@@ -0,0 +1,214 @@
+loadAllBreakpoints();
+ }
+
+ /**
+ * Overrides Drupal\Core\Entity\Entity::save().
+ */
+ public function save() {
+ // Check if everything is valid.
+ if (!$this->isValid()) {
+ throw new InvalidBreakpointException('Invalid data detected.');
+ }
+ if (empty($this->id)) {
+ $this->id = $this->sourceType . '.' . $this->source . '.' . $this->name;
+ }
+ // Only save the keys, but return the full objects.
+ $this->breakpoints = array_keys($this->breakpoints);
+ parent::save();
+ $this->loadAllBreakpoints();
+ }
+
+ /**
+ * Checks if the breakpoint group is valid.
+ *
+ * @throws Drupal\breakpoint\InvalidBreakpointSourceTypeException
+ * @throws Drupal\breakpoint\InvalidBreakpointSourceException
+ *
+ * @return true
+ * Returns true if the breakpoint group is valid.
+ */
+ public function isValid() {
+ // Check for illegal values in breakpoint group source type.
+ if (!in_array($this->sourceType, array(
+ Breakpoint::SOURCE_TYPE_CUSTOM,
+ Breakpoint::SOURCE_TYPE_MODULE,
+ Breakpoint::SOURCE_TYPE_THEME)
+ )) {
+ throw new InvalidBreakpointSourceTypeException(format_string('Invalid source type @source_type', array(
+ '@source_type' => $this->sourceType,
+ )));
+ }
+ // Check for illegal characters in breakpoint group source.
+ if (preg_match('/[^a-z_]+/', $this->source) || empty($this->source)) {
+ throw new InvalidBreakpointSourceException(format_string("Invalid value '@source' for breakpoint group source property. Breakpoint group source property can only contain lowercase letters and underscores.", array('@source' => $this->source)));
+ }
+ // Check for illegal characters in breakpoint group name.
+ if (preg_match('/[^a-z0-9_]+/', $this->name || empty($this->name))) {
+ throw new InvalidBreakpointNameException(format_string("Invalid value '@name' for breakpoint group name property. Breakpoint group name property can only contain lowercase letters, numbers and underscores.", array('@name' => $this->name)));
+ }
+ return TRUE;
+ }
+
+ /**
+ * Duplicates a breakpoint group.
+ *
+ * The new breakpoint group inherits the breakpoints.
+ *
+ * @return Drupal\breakpoint\BreakpointGroup
+ */
+ public function duplicate() {
+ return entity_create('breakpoint_group', array(
+ 'breakpoints' => array_keys($this->breakpoints),
+ 'name' => 'clone_of_' . $this->name,
+ ));
+ }
+
+ /**
+ * Adds a breakpoint using a name and a media query.
+ *
+ * @param string $name
+ * The name of the breakpoint.
+ * @param string $media_query
+ * Media query.
+ */
+ public function addBreakpointFromMediaQuery($name, $media_query) {
+ // Use the existing breakpoint if it exists.
+ $breakpoint = entity_load('breakpoint', $this->sourceType . '.' . $this->name . '.' . $name);
+ if (!$breakpoint) {
+ // Build a new breakpoint.
+ $breakpoint = entity_create('breakpoint', array(
+ 'name' => $name,
+ 'label' => $name,
+ 'mediaQuery' => $media_query,
+ 'source' => $this->name,
+ 'sourceType' => $this->sourceType,
+ 'weight' => count($this->breakpoints),
+ ));
+ $breakpoint->save();
+ }
+ $this->breakpoints[$breakpoint->id()] = $breakpoint;
+ }
+
+ /**
+ * Adds one or more breakpoints to this group.
+ *
+ * The breakpoint name is either the machine_name or the id of a breakpoint.
+ *
+ * @param type $breakpoints
+ * Array containing breakpoints keyed by their id.
+ */
+ public function addBreakpoints($breakpoints) {
+ foreach ($breakpoints as $breakpoint_name) {
+ // Check if breakpoint exists, assume $breakpoint_name is a machine name.
+ $breakpoint = entity_load('breakpoint', $this->sourceType . '.' . $this->source . '.' . $breakpoint_name);
+ // If the breakpoint doesn't exist, assume $breakpoint_name is an id.
+ if (!$breakpoint) {
+ $breakpoint = entity_load('breakpoint', $breakpoint_name);
+ }
+ // If the breakpoint doesn't exists, do not add it.
+ if ($breakpoint) {
+ // Add breakpoint to group.
+ $this->breakpoints[$breakpoint->id()] = $breakpoint;
+ }
+ }
+ }
+
+ /**
+ * Loads all breakpoints, remove non-existing ones.
+ *
+ * @return array
+ * Array containing breakpoints keyed by their id.
+ */
+ protected function loadAllBreakpoints() {
+ $breakpoints = $this->breakpoints;
+ $this->breakpoints = array();
+ foreach ($breakpoints as $breakpoint_id) {
+ $breakpoint = breakpoint_load($breakpoint_id);
+ if ($breakpoint) {
+ $this->breakpoints[$breakpoint_id] = $breakpoint;
+ }
+ }
+ }
+}
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/InvalidBreakpointException.php b/core/modules/breakpoint/lib/Drupal/breakpoint/InvalidBreakpointException.php
new file mode 100644
index 0000000..6889fa2
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/InvalidBreakpointException.php
@@ -0,0 +1,13 @@
+ 'Breakpoint general API functions',
+ 'description' => 'Test general API functions of the breakpoint module.',
+ 'group' => 'Breakpoint',
+ );
+ }
+
+ /**
+ * Test Breakpoint::buildConfigName().
+ */
+ public function testConfigName() {
+ // Try an invalid sourceType.
+ $breakpoint = entity_create('breakpoint', array(
+ 'label' => drupal_strtolower($this->randomName()),
+ 'source' => 'custom_module',
+ 'sourceType' => 'oops',
+ ));
+
+ $exception = FALSE;
+ try {
+ $breakpoint->save();
+ }
+ catch (InvalidBreakpointSourceTypeException $e) {
+ $exception = TRUE;
+ }
+ $this->assertTrue($exception, t('breakpoint_config_name: An exception is thrown when an invalid sourceType is entered.'));
+
+ // Try an invalid source.
+ $breakpoint->id = '';
+ $breakpoint->sourceType = Breakpoint::SOURCE_TYPE_CUSTOM;
+ $breakpoint->source = 'custom*_module source';
+
+ $exception = FALSE;
+ try {
+ $breakpoint->save();
+ }
+ catch (InvalidBreakpointSourceException $e) {
+ $exception = TRUE;
+ }
+ $this->assertTrue($exception, t('breakpoint_config_name: An exception is thrown when an invalid source is entered.'));
+
+ // Try an invalid name (make sure there is at least once capital letter).
+ $breakpoint->id = '';
+ $breakpoint->source = 'custom_module';
+ $breakpoint->name = drupal_ucfirst($this->randomName());
+
+ $exception = FALSE;
+ try {
+ $breakpoint->save();
+ }
+ catch (InvalidBreakpointNameException $e) {
+ $exception = TRUE;
+ }
+ $this->assertTrue($exception, t('breakpoint_config_name: An exception is thrown when an invalid name is entered.'));
+
+ // Try a valid breakpoint.
+ $breakpoint->id = '';
+ $breakpoint->name = drupal_strtolower($this->randomName());
+ $breakpoint->mediaQuery = 'all';
+
+ $exception = FALSE;
+ try {
+ $breakpoint->save();
+ }
+ catch (\Exception $e) {
+ $exception = TRUE;
+ }
+ $this->assertFalse($exception, t('breakpoint_config_name: No exception is thrown when a valid breakpoint is passed.'));
+ $this->assertEqual($breakpoint->id(), Breakpoint::SOURCE_TYPE_CUSTOM . '.custom_module.' . $breakpoint->name, t('breakpoint_config_name: A id is set when a valid breakpoint is passed.'));
+ }
+}
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointCrudTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointCrudTest.php
new file mode 100644
index 0000000..38bc183
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointCrudTest.php
@@ -0,0 +1,57 @@
+ 'Breakpoint CRUD operations',
+ 'description' => 'Test creation, loading, updating, deleting of breakpoints.',
+ 'group' => 'Breakpoint',
+ );
+ }
+
+ /**
+ * Test CRUD operations for breakpoints.
+ */
+ public function testBreakpointCrud() {
+ // Add a breakpoint with minimum data only.
+ $breakpoint = entity_create('breakpoint', array(
+ 'label' => drupal_strtolower($this->randomName()),
+ 'mediaQuery' => '(min-width: 600px)',
+ ));
+ $breakpoint->save();
+
+ $this->verifyBreakpoint($breakpoint);
+
+ // Test breakpoint_load_all
+ $all_breakpoints = entity_load_multiple('breakpoint');
+ $config_name = $breakpoint->id();
+ $this->assertTrue(isset($all_breakpoints[$config_name]), t('breakpoint_load_all: New breakpoint is present when loading all breakpoints.'));
+ $this->verifyBreakpoint($breakpoint, $all_breakpoints[$config_name]);
+
+ // Update the breakpoint.
+ $breakpoint->weight = 1;
+ $breakpoint->multipliers['2x'] = '2x';
+ $breakpoint->save();
+ $this->verifyBreakpoint($breakpoint);
+
+ // Delete the breakpoint.
+ $breakpoint->delete();
+ $this->assertFalse(breakpoint_load($config_name), t('breakpoint_load: Loading a deleted breakpoint returns false.'), t('Breakpoints API'));
+ }
+}
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupAPITest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupAPITest.php
new file mode 100644
index 0000000..6639bda
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupAPITest.php
@@ -0,0 +1,81 @@
+ 'Breakpoint group general API functions',
+ 'description' => 'Test general API functions of the breakpoint module.',
+ 'group' => 'Breakpoint',
+ );
+ }
+
+ /**
+ * Test Breakpoint::buildConfigName().
+ */
+ public function testConfigName() {
+ // Try an invalid sourceType.
+ $label = $this->randomName();
+ $breakpoint_group = entity_create('breakpoint_group', array(
+ 'label' => $label,
+ 'name' => drupal_strtolower($label),
+ 'source' => 'custom_module',
+ 'sourceType' => 'oops',
+ ));
+
+ $exception = FALSE;
+ try {
+ $breakpoint_group->save();
+ }
+ catch (InvalidBreakpointSourceTypeException $e) {
+ $exception = TRUE;
+ }
+ $this->assertTrue($exception, t('An exception is thrown when an invalid sourceType is entered.'));
+
+ // Try an invalid source.
+ $breakpoint_group->name = '';
+ $breakpoint_group->sourceType = Breakpoint::SOURCE_TYPE_CUSTOM;
+ $breakpoint_group->source = 'custom*_module source';
+
+ $exception = FALSE;
+ try {
+ $breakpoint_group->save();
+ }
+ catch (InvalidBreakpointSourceException $e) {
+ $exception = TRUE;
+ }
+ $this->assertTrue($exception, t('An exception is thrown when an invalid source is entered.'));
+
+ // Try a valid breakpoint_group.
+ $breakpoint_group->name = 'test';
+ $breakpoint_group->source = 'custom_module_source';
+
+ $exception = FALSE;
+ try {
+ $breakpoint_group->save();
+ }
+ catch (\Exception $e) {
+ $exception = TRUE;
+ }
+ $this->assertFalse($exception, t('No exception is thrown when a valid data is passed.'));
+ }
+}
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupCrudTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupCrudTest.php
new file mode 100644
index 0000000..3d8fd9b
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupCrudTest.php
@@ -0,0 +1,72 @@
+ 'Breakpoint group CRUD operations',
+ 'description' => 'Test creation, loading, updating, deleting of breakpoint groups.',
+ 'group' => 'Breakpoint',
+ );
+ }
+
+ /**
+ * Test CRUD operations for breakpoint groups.
+ */
+ public function testBreakpointGroupCrud() {
+ // Add breakpoints.
+ $breakpoints = array();
+ for ($i = 0; $i <= 3; $i++) {
+ $width = ($i + 1) * 200;
+ $breakpoint = entity_create('breakpoint', array(
+ 'name' => drupal_strtolower($this->randomName()),
+ 'weight' => $i,
+ 'mediaQuery' => "(min-width: {$width}px)",
+ ));
+ $breakpoint->save();
+ $breakpoints[$breakpoint->id()] = $breakpoint;
+ }
+ // Add a breakpoint group with minimum data only.
+ $label = $this->randomName();
+
+ $group = entity_create('breakpoint_group', array(
+ 'label' => $label,
+ 'name' => drupal_strtolower($label),
+ ));
+ $group->save();
+ $this->verifyBreakpointGroup($group);
+
+ // Update the breakpoint group.
+ $group->breakpoints = array_keys($breakpoints);
+ $group->save();
+ $this->verifyBreakpointGroup($group);
+
+ // Duplicate the breakpoint group.
+ $new_set = entity_create('breakpoint_group', array(
+ 'breakpoints' => $group->breakpoints,
+ 'name' => 'clone_of_' . $group->name,
+ ));
+ $duplicated_set = $group->duplicate();
+ $this->verifyBreakpointGroup($duplicated_set, $new_set);
+
+ // Delete the breakpoint group.
+ $group->delete();
+ $this->assertFalse(entity_load('breakpoint_group', $group->id()), t('breakpoint_group_load: Loading a deleted breakpoint group returns false.'), t('Breakpoints API'));
+ }
+}
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupTestBase.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupTestBase.php
new file mode 100644
index 0000000..24a50ab
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointGroupTestBase.php
@@ -0,0 +1,66 @@
+id()) : $compare_set;
+
+ foreach ($properties as $property) {
+ $t_args = array(
+ '%group' => $group->label(),
+ '%property' => $property,
+ );
+ if (is_array($compare_set->{$property})) {
+ $this->assertEqual(array_keys($compare_set->{$property}), array_keys($group->{$property}), t('breakpoint_group_load: Proper %property for breakpoint group %group.', $t_args), $assert_set);
+ }
+ else {
+ $t_args = array(
+ '%group' => $group->label(),
+ '%property' => $property,
+ '%property1' => $compare_set->{$property},
+ '%property2' => $group->{$property},
+ );
+ $this->assertEqual($compare_set->{$property}, $group->{$property}, t('breakpoint_group_load: Proper %property: %property1 == %property2 for breakpoint group %group.', $t_args), $assert_set);
+ }
+ }
+ }
+}
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointMediaQueryTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointMediaQueryTest.php
new file mode 100644
index 0000000..1ebda06
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointMediaQueryTest.php
@@ -0,0 +1,122 @@
+ 'Breakpoint media query tests',
+ 'description' => 'Test validation of media queries.',
+ 'group' => 'Breakpoint',
+ );
+ }
+
+ /**
+ * Test valid media queries.
+ */
+ public function testValidMediaQueries() {
+ $media_queries = array(
+ // Bartik breakpoints.
+ '(min-width: 0px)',
+ 'all and (min-width: 560px) and (max-width:850px)',
+ 'all and (min-width: 851px)',
+ // Seven breakpoints.
+ '(min-width: 0em)',
+ 'screen and (min-width: 40em)',
+ // Stark breakpoints.
+ '(min-width: 0px)',
+ 'all and (min-width: 480px) and (max-width: 959px)',
+ 'all and (min-width: 960px)',
+ '(orientation)',
+ 'all and (orientation)',
+ 'not all and (orientation)',
+ 'only all and (orientation)',
+ 'screen and (width)',
+ 'screen and (width: 0)',
+ 'screen and (width: 0px)',
+ 'screen and (width: 0em)',
+ 'screen and (min-width: -0)',
+ 'screen and (max-width: 0)',
+ 'screen and (max-width: 0.3)',
+ 'screen and (min-width)',
+ // Multiline and comments.
+ 'screen and /* this is a comment */ (min-width)',
+ "screen\nand /* this is a comment */ (min-width)",
+ "screen\n\nand /* this is\n a comment */ (min-width)",
+ );
+
+ foreach ($media_queries as $media_query) {
+ $this->assertTrue(Breakpoint::isValidMediaQuery($media_query), $media_query . ' is valid.');
+ }
+ }
+
+ /**
+ * Test invalid media queries.
+ */
+ public function testInvalidMediaQueries() {
+ $media_queries = array(
+ '',
+ 'not (orientation)',
+ 'only (orientation)',
+ 'all and not all',
+ 'screen and (width: 0xx)',
+ 'screen and (width: -8xx)',
+ 'screen and (width: -xx)',
+ 'screen and (width: xx)',
+ 'screen and (width: px)',
+ 'screen and (width: -8px)',
+ 'screen and (width: -0.8px)',
+ 'screen and (height: 0xx)',
+ 'screen and (height: -8xx)',
+ 'screen and (height: -xx)',
+ 'screen and (height: xx)',
+ 'screen and (height: px)',
+ 'screen and (height: -8px)',
+ 'screen and (height: -0.8px)',
+ 'screen and (device-width: 0xx)',
+ 'screen and (device-width: -8xx)',
+ 'screen and (device-width: -xx)',
+ 'screen and (device-width: xx)',
+ 'screen and (device-width: px)',
+ 'screen and (device-width: -8px)',
+ 'screen and (device-width: -0.8px)',
+ 'screen and (device-height: 0xx)',
+ 'screen and (device-height: -8xx)',
+ 'screen and (device-height: -xx)',
+ 'screen and (device-height: xx)',
+ 'screen and (device-height: px)',
+ 'screen and (device-height: -8px)',
+ 'screen and (device-height: -0.8px)',
+ 'screen and (min-orientation)',
+ 'screen and (max-orientation)',
+ 'screen and (min-orientation: landscape)',
+ 'screen and (max-orientation: landscape)',
+ 'screen and (orientation: bogus)',
+ '(orientation: bogus)',
+ );
+
+ foreach ($media_queries as $media_query) {
+ try {
+ $this->assertFalse(Breakpoint::isValidMediaQuery($media_query), $media_query . ' is not valid.');
+ }
+ catch (InvalidBreakpointMediaQueryException $e) {
+ $this->assertTrue(TRUE, $media_query . ' is not valid.');
+ }
+ }
+ }
+}
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointTestBase.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointTestBase.php
new file mode 100644
index 0000000..27b73c7
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointTestBase.php
@@ -0,0 +1,55 @@
+id()) : $compare_breakpoint;
+ foreach ($properties as $property) {
+ $t_args = array(
+ '%breakpoint' => $breakpoint->label(),
+ '%property' => $property,
+ );
+ $this->assertEqual($compare_breakpoint->{$property}, $breakpoint->{$property}, t('breakpoint_load: Proper %property for breakpoint %breakpoint.', $t_args), $assert_group);
+ }
+ }
+}
diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php
new file mode 100644
index 0000000..7072e02
--- /dev/null
+++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php
@@ -0,0 +1,136 @@
+ 'Breakpoint theme functionality',
+ 'description' => 'Thoroughly test the breakpoints provided by a theme.',
+ 'group' => 'Breakpoint',
+ );
+ }
+
+ /**
+ * Drupal\simpletest\WebTestBase\setUp().
+ */
+ public function setUp() {
+ parent::setUp();
+ theme_enable(array('breakpoint_test_theme'));
+ }
+
+ /**
+ * Test the breakpoints provided by a theme.
+ */
+ public function testThemeBreakpoints() {
+ // Verify the breakpoint group for breakpoint_test_theme was created.
+ $breakpoint_group_obj = entity_create('breakpoint_group', array(
+ 'label' => 'Breakpoint test theme',
+ 'name' => 'breakpoint_test_theme',
+ 'source' => 'breakpoint_test_theme',
+ 'sourceType' => Breakpoint::SOURCE_TYPE_THEME,
+ 'id' => Breakpoint::SOURCE_TYPE_THEME . '.breakpoint_test_theme.breakpoint_test_theme',
+ ));
+ $breakpoint_group_obj->breakpoints = array(
+ 'theme.breakpoint_test_theme.mobile' => array(),
+ 'theme.breakpoint_test_theme.narrow' => array(),
+ 'theme.breakpoint_test_theme.wide' => array(),
+ 'theme.breakpoint_test_theme.tv' => array(),
+ );
+
+ // Verify we can load this breakpoint defined by the theme.
+ $this->verifyBreakpointGroup($breakpoint_group_obj);
+
+ // Disable the test theme and verify the breakpoint group is deleted.
+ theme_disable(array('breakpoint_test_theme'));
+ $this->assertFalse(entity_load('breakpoint_group', $breakpoint_group_obj->id()), t('breakpoint_group_load: Loading a deleted breakpoint group returns false.'), t('Breakpoints API'));
+ }
+
+ /**
+ * Test the breakpoints defined by the custom group.
+ */
+ public function testThemeBreakpointGroup() {
+ // Verify the breakpoint group 'test' was created by breakpoint_test_theme.
+ $breakpoint_group_obj = entity_create('breakpoint_group', array(
+ 'label' => 'Test',
+ 'name' => 'test',
+ 'sourceType' => Breakpoint::SOURCE_TYPE_THEME,
+ 'source' => 'breakpoint_test_theme',
+ 'id' => Breakpoint::SOURCE_TYPE_THEME . '.breakpoint_test_theme.test',
+ ));
+ $breakpoint_group_obj->breakpoints = array(
+ 'theme.breakpoint_test_theme.mobile' => array('1.5x', '2.x'),
+ 'theme.breakpoint_test_theme.narrow' => array(),
+ 'theme.breakpoint_test_theme.wide' => array(),
+ );
+
+ // Verify we can load this breakpoint defined by the theme.
+ $this->verifyBreakpointGroup($breakpoint_group_obj);
+
+ // Disable the test theme and verify the breakpoint group is deleted.
+ theme_disable(array('breakpoint_test_theme'));
+ $this->assertFalse(entity_load('breakpoint_group', $breakpoint_group_obj->id()), t('breakpoint_group_load: Loading a deleted breakpoint group returns false.'), t('Breakpoints API'));
+ }
+
+ /**
+ * Test the breakpoints defined by the custom group in the module.
+ */
+ public function testThemeBreakpointGroupModule() {
+ // Call the import manually, since the testbot needs to enable the module
+ // first, otherwise the theme isn't detected.
+ _breakpoint_import_breakpoint_groups('breakpoint_theme_test', Breakpoint::SOURCE_TYPE_MODULE);
+
+ // Verify the breakpoint group 'module_test' was created by
+ // breakpoint_theme_test module.
+ $breakpoint_group_obj = entity_create('breakpoint_group', array(
+ 'label' => 'Test Module',
+ 'name' => 'module_test',
+ 'sourceType' => Breakpoint::SOURCE_TYPE_MODULE,
+ 'source' => 'breakpoint_theme_test',
+ 'id' => Breakpoint::SOURCE_TYPE_MODULE . '.breakpoint_theme_test.module_test',
+ ));
+ $breakpoint_group_obj->breakpoints = array(
+ 'theme.breakpoint_test_theme.mobile' => array(),
+ 'theme.breakpoint_test_theme.narrow' => array(),
+ 'theme.breakpoint_test_theme.wide' => array(),
+ );
+
+ // Verify we can load this breakpoint defined by the theme.
+ $this->verifyBreakpointGroup($breakpoint_group_obj);
+
+ // Disable the test theme and verify the breakpoint group still exists.
+ theme_disable(array('breakpoint_test_theme'));
+ $this->assertTrue(entity_load('breakpoint_group', $breakpoint_group_obj->id()), 'Breakpoint group still exists if theme is disabled.');
+
+ // Disable the test module and verify the breakpoint group still exists.
+ module_disable(array('breakpoint_theme_test'));
+ $this->assertTrue(entity_load('breakpoint_group', $breakpoint_group_obj->id()), 'Breakpoint group still exists if module is disabled.');
+
+ // Uninstall the test module and verify the breakpoint group is deleted.
+ module_uninstall(array('breakpoint_theme_test'));
+ $this->assertFalse(entity_load('breakpoint_group', $breakpoint_group_obj->id()), 'Breakpoint group is removed if module is uninstalled.');
+ }
+
+}
diff --git a/core/modules/breakpoint/tests/breakpoint_theme_test.info b/core/modules/breakpoint/tests/breakpoint_theme_test.info
new file mode 100644
index 0000000..d2896f6
--- /dev/null
+++ b/core/modules/breakpoint/tests/breakpoint_theme_test.info
@@ -0,0 +1,6 @@
+name = Breakpoint theme test
+description = Test breakpoints provided by themes
+package = Other
+core = 8.x
+hidden = TRUE
+dependencies[] = breakpoint
diff --git a/core/modules/breakpoint/tests/breakpoint_theme_test.module b/core/modules/breakpoint/tests/breakpoint_theme_test.module
new file mode 100644
index 0000000..50b5ff0
--- /dev/null
+++ b/core/modules/breakpoint/tests/breakpoint_theme_test.module
@@ -0,0 +1,13 @@
+