Repository: learningequality/kolibri

Issue:
Custom icons in Video Player are missing
### Observed behavior
@christianmemije reports:
Video player is missing custom icons, never got the points snackbar or completion icon

![screen shot 2018-10-23 at 3 56 58 pm](https://user-images.githubusercontent.com/10248067/47395715-9a9e1280-d6dc-11e8-9267-6806e3df0030.png)

### Expected behavior
All icons should show up.

### User-facing consequences
Alarm, confusion, despair.

### Steps to reproduce
Play a video?

### Context
 * 0.11.0-beta1




Discussion:

Resolving patch:
diff --git a/build_tools/i18n/fonts.py b/build_tools/i18n/fonts.py
--- a/build_tools/i18n/fonts.py
+++ b/build_tools/i18n/fonts.py
@@ -18,6 +18,7 @@
 from fontTools import subset
 
 logging.basicConfig(format="%(levelname)s: %(message)s", level=logging.INFO)
+logging.getLogger("fontTools").setLevel(logging.WARNING)
 logging.StreamHandler(sys.stdout)
 
 
@@ -100,9 +101,8 @@ def _gen_font_face(family, url, is_bold, unicodes):
 
 
 @utils.memoize
-def _woff_font_name(name, is_ui, is_full, is_bold):
-    return "{type}.{scope}.{name}.{weight}.woff".format(
-        type="ui" if is_ui else "content",
+def _woff_font_name(name, is_full, is_bold):
+    return "{scope}.{name}.{weight}.woff".format(
         name=name,
         scope="full" if is_full else "subset",
         weight="700" if is_bold else "400",
@@ -110,23 +110,15 @@ def _woff_font_name(name, is_ui, is_full, is_bold):
 
 
 @utils.memoize
-def _woff_font_path(name, is_ui, is_full, is_bold):
-    return os.path.join(OUTPUT_PATH, _woff_font_name(name, is_ui, is_full, is_bold))
+def _woff_font_path(name, is_full, is_bold):
+    return os.path.join(OUTPUT_PATH, _woff_font_name(name, is_full, is_bold))
 
 
 @utils.memoize
-def _ttf_font_path(font_info, is_ui=False, is_bold=False):
-    """
-    'User Interface' variants of fonts are sometimes available for languages with
-    tall glyph heights, and are redrawn to fit within constrained vertical space.
-    Not all fonts have or need a 'UI' variant.
-
-    Normal variants are easier to read but have fewer guarantees about whether they
-    will fit within UI elements.
-    """
-    font_name = font_info["name"]
+def _ttf_font_path(font_info, is_bold=False):
+    font_name = font_info[utils.KEY_NAME]
     weight = "Bold" if is_bold else "Regular"
-    if is_ui and font_info["has_ui_variant"]:
+    if font_info[utils.KEY_HAS_UI]:
         return os.path.join(TTF_PATH, "{}UI-{}.ttf".format(font_name, weight))
     return os.path.join(TTF_PATH, "{}-{}.ttf".format(font_name, weight))
 
@@ -137,7 +129,7 @@ def _load_font(path):
 
 def _get_font_info(name):
     for font_info in FONT_MANIFEST:
-        if font_info["name"] == name:
+        if font_info[utils.KEY_NAME] == name:
             return font_info
     raise KeyError(name)
 
@@ -194,9 +186,9 @@ def _font_glyphs(font_path):
     return glyphs
 
 
-def _full_font_face(font_family, font_name, is_ui, is_bold, omit_glyphs=set()):
-    file_name = _woff_font_name(font_name, is_ui=is_ui, is_full=True, is_bold=is_bold)
-    file_path = _woff_font_path(font_name, is_ui=is_ui, is_full=True, is_bold=is_bold)
+def _full_font_face(font_family, font_name, is_bold, omit_glyphs=set()):
+    file_name = _woff_font_name(font_name, is_full=True, is_bold=is_bold)
+    file_path = _woff_font_path(font_name, is_full=True, is_bold=is_bold)
     glyphs = _font_glyphs(file_path) - omit_glyphs
     if not glyphs:
         return ""
@@ -219,33 +211,11 @@ def _modern_font_faces():
     # all available fonts
     for font_info in FONT_MANIFEST:
 
-        # regular content
-        font_faces.append(
-            _full_font_face(
-                "noto-content-full",
-                font_info["name"],
-                is_ui=False,
-                is_bold=False,
-                omit_glyphs=previous_glyphs,
-            )
-        )
-        # bold content
-        font_faces.append(
-            _full_font_face(
-                "noto-content-full",
-                font_info["name"],
-                is_ui=False,
-                is_bold=True,
-                omit_glyphs=previous_glyphs,
-            )
-        )
-
         # regular UI
         font_faces.append(
             _full_font_face(
-                "noto-ui-full",
-                font_info["name"],
-                is_ui=font_info["has_ui_variant"],
+                "noto-full",
+                font_info[utils.KEY_NAME],
                 is_bold=False,
                 omit_glyphs=previous_glyphs,
             )
@@ -253,24 +223,23 @@ def _modern_font_faces():
         # bold UI
         font_faces.append(
             _full_font_face(
-                "noto-ui-full",
-                font_info["name"],
-                is_ui=font_info["has_ui_variant"],
+                "noto-full",
+                font_info[utils.KEY_NAME],
                 is_bold=True,
                 omit_glyphs=previous_glyphs,
             )
         )
 
-        # Assumes all four variants have the same glyphs, from the Content Regular font
+        # Assumes all four variants have the same glyphs, from the content Regular font
         previous_glyphs |= _font_glyphs(
-            _woff_font_path(font_info["name"], is_ui=False, is_full=True, is_bold=False)
+            _woff_font_path(font_info[utils.KEY_NAME], is_full=True, is_bold=False)
         )
 
     return "".join(font_faces)
 
 
 def _write_inline_ui_font(file_object, font_name, font_family, is_bold):
-    font_path = _woff_font_path(font_name, is_ui=True, is_full=False, is_bold=is_bold)
+    font_path = _woff_font_path(font_name, is_full=False, is_bold=is_bold)
     with io.open(font_path, mode="rb") as f:
         data = f.read()
     data_uri = "data:application/x-font-woff;charset=utf-8;base64,{}".format(
@@ -289,6 +258,18 @@ def _write_inline_ui_font(file_object, font_name, font_family, is_bold):
     )
 
 
+CSS_HEADER = """
+/*
+ * This is an auto-generated file, so any manual edits will be overridden.
+ *
+ * To regenerate, see instructions here:
+ *   https://kolibri-dev.readthedocs.io/en/develop/references/i18n.html
+ *
+ * This file was generated by build_tools/i18n/fonts.py
+ */
+"""
+
+
 def _generate_css_for_language(lang):
     css_file_modern = os.path.join(
         OUTPUT_PATH, "fonts.{}.modern.css".format(lang[utils.KEY_INTL_CODE])
@@ -297,51 +278,21 @@ def _generate_css_for_language(lang):
         OUTPUT_PATH, "fonts.{}.basic.css".format(lang[utils.KEY_INTL_CODE])
     )
     with open(css_file_modern, "w") as modern, open(css_file_basic, "w") as basic:
-        # Common subsets of UI font for both modern and basic
-        _write_inline_ui_font(modern, "Common", "noto-ui-subset", is_bold=False)
-        _write_inline_ui_font(modern, "Common", "noto-ui-subset", is_bold=True)
-        _write_inline_ui_font(basic, "Common", "noto-ui-subset", is_bold=False)
-        _write_inline_ui_font(basic, "Common", "noto-ui-subset", is_bold=True)
+        # Write headers
+        modern.write(CSS_HEADER)
+        basic.write(CSS_HEADER)
 
         # Language-specific subsets of UI font for both modern and basic
         lang_name = lang[utils.KEY_INTL_CODE]
-        _write_inline_ui_font(modern, lang_name, "noto-ui-subset", is_bold=False)
-        _write_inline_ui_font(modern, lang_name, "noto-ui-subset", is_bold=True)
-        _write_inline_ui_font(basic, lang_name, "noto-ui-subset", is_bold=False)
-        _write_inline_ui_font(basic, lang_name, "noto-ui-subset", is_bold=True)
+        _write_inline_ui_font(modern, lang_name, "noto-subset", is_bold=False)
+        _write_inline_ui_font(modern, lang_name, "noto-subset", is_bold=True)
+        _write_inline_ui_font(basic, lang_name, "noto-subset", is_bold=False)
+        _write_inline_ui_font(basic, lang_name, "noto-subset", is_bold=True)
 
         # Full UI font of default language for basic only
         name = lang[utils.KEY_DEFAULT_FONT]
-        font_info = _get_font_info(name)
-
-        basic.write(
-            _full_font_face(
-                "noto-ui-full",
-                font_name=name,
-                is_ui=font_info["has_ui_variant"],
-                is_bold=False,
-            )
-        )
-        basic.write(
-            _full_font_face(
-                "noto-ui-full",
-                font_name=name,
-                is_ui=font_info["has_ui_variant"],
-                is_bold=True,
-            )
-        )
-
-        # Full content font of default language for basic only
-        basic.write(
-            _full_font_face(
-                "noto-content-full", font_name=name, is_ui=False, is_bold=False
-            )
-        )
-        basic.write(
-            _full_font_face(
-                "noto-content-full", font_name=name, is_ui=False, is_bold=True
-            )
-        )
+        basic.write(_full_font_face("noto-full", font_name=name, is_bold=False))
+        basic.write(_full_font_face("noto-full", font_name=name, is_bold=True))
 
 
 def command_gen_css():
@@ -350,11 +301,7 @@ def command_gen_css():
 
     Both versions include the common and language-specific application 'UI' subset fonts
     inline to load quickly and prevent a flash of unstyled text, at least for all
-    application text.
-
-    Full 'content' font files are linked and will load asynchronously. This means that
-    content might have a flash of unstyled text while the font is loading. However, we
-    provide the UI subsets as backup so in many cases the flash might not be significant.
+    application text. Full font files are linked and will load asynchronously.
 
     # Modern behavior
 
@@ -379,12 +326,14 @@ def command_gen_css():
     logging.info("Fonts: generating css...")
 
     # generate language-specific font files
-    for lang in utils.supported_languages(include_english=True, include_in_context=True):
+    languages = utils.supported_languages(include_in_context=True, include_english=True)
+    for lang in languages:
         _generate_css_for_language(lang)
 
     # for all modern browsers, add all fonts references segmented by unicode range
     css_file_modern = os.path.join(OUTPUT_PATH, "all-fonts.css")
     with open(css_file_modern, "w") as f:
+        f.write(CSS_HEADER)
         f.write(_modern_font_faces())
 
     logging.info("Fonts: finished generating css")
@@ -395,22 +344,17 @@ def command_gen_css():
 """
 
 
-def _write_full_font(font_info, is_ui=False, is_bold=False):
-    font = _load_font(_ttf_font_path(font_info, is_ui=False, is_bold=is_bold))
-    font.save(
-        _woff_font_path(font_info["name"], is_ui=is_ui, is_full=True, is_bold=is_bold)
-    )
+def _write_full_font(font_info, is_bold):
+    font = _load_font(_ttf_font_path(font_info, is_bold=is_bold))
+    font.save(_woff_font_path(font_info[utils.KEY_NAME], is_full=True, is_bold=is_bold))
 
 
 def command_gen_full_fonts():
     logging.info("Fonts: generating full fonts...")
 
     for font_info in FONT_MANIFEST:
-        _write_full_font(font_info, is_ui=False, is_bold=False)
-        _write_full_font(font_info, is_ui=False, is_bold=True)
-        if font_info["has_ui_variant"]:
-            _write_full_font(font_info, is_ui=True, is_bold=False)
-            _write_full_font(font_info, is_ui=True, is_bold=True)
+        _write_full_font(font_info, is_bold=False)
+        _write_full_font(font_info, is_bold=True)
 
     logging.info("Fonts: finished generating full fonts")
 
@@ -420,61 +364,84 @@ def command_gen_full_fonts():
 """
 
 
-def _get_subset_font(source_file_path, code_points):
+def _get_subset_font(source_file_path, text):
     """
-    Given a TTF file and a set of code points, returns a new, in-memory fontTools
-    Font object that has only the glyphs specified in the set.
+    Given a TTF file and some text, returns a new, in-memory fontTools Font object that
+    has only the glyphs specified in the set.
+
+    Note that passing actual text instead of a glyph set to the subsetter allows it to
+    generate appropriate ligatures and other features important for correct rendering.
     """
     if not os.path.exists(source_file_path):
         logging.error("Fonts: '{}' not found".format(source_file_path))
 
     font = _load_font(source_file_path)
     subsetter = subset.Subsetter(options=FONT_TOOLS_OPTIONS)
-    subsetter.populate(unicodes=code_points)
+    subsetter.populate(text=text)
     subsetter.subset(font)
     return font
 
 
-def _get_lang_glyphs(lang, locale_dir):
-    code_points = set()
+def _get_lang_strings(locale_dir):
+    """
+    Text used in a particular language
+    """
+
+    strings = []
+
     for file_name in os.listdir(locale_dir):
         if not file_name.endswith(".json"):
             continue
+
         file_path = os.path.join(locale_dir, file_name)
         with io.open(file_path, mode="r", encoding="utf-8") as f:
-            strings = json.load(f).values()
+            lang_strings = json.load(f).values()
+
+        for s in lang_strings:
+            strings.append(s)
+            strings.append(s.upper())
 
-        for string in strings:
-            for char in string:
-                code_points.add(ord(char))
-                # include upper-case variant for use in buttons
-                code_points.add(ord(char.upper()))
-    return code_points
+    return strings
 
 
 @utils.memoize
-def _get_common_glyphs():
+def _get_common_strings():
     """
-    Glyphs necessary for all languages: displaying the language switcher,
-    Kolibri version numbers, the 'copywrite' symbol, and other un-translated text
+    Text useful for all languages: displaying the language switcher, Kolibri version
+    numbers, symbols, and other un-translated text
     """
-    # null, form feed, carriage return
-    code_points = set((0x0, 0xC, 0xD))
-    # copywrite, m-dash, ellipsis, curly quotes
-    for c in "©–…‘’“”":
-        code_points.add(ord(c))
+
+    # Special characters that are used directly in untranslated template strings.
+    # Search the codebase with this regex to find new ones: [^\x00-\x7F©–—…‘’“”•→›]
+    strings = [
+        chr(0x0),  # null
+        chr(0xC),  # form feed
+        chr(0xD),  # carriage return
+        "©",
+        "–",  # en dash
+        "—",  # em dash
+        "…",
+        "‘",
+        "’",
+        "“",
+        "”",
+        "•",
+        "→",
+        "›",
+    ]
+
     # all the basic printable ascii characters
-    for c in range(32, 127):
-        code_points.add(c)
-    # glyphs from language names, both lower- and upper-case
-    for lang in utils.supported_languages():
-        for c in lang[utils.KEY_LANG_NAME]:
-            code_points.add(ord(c))
-            code_points.add(ord(c.upper()))
-        for c in lang[utils.KEY_ENG_NAME]:
-            code_points.add(ord(c))
-            code_points.add(ord(c.upper()))
-    return code_points
+    strings.extend([chr(c) for c in range(32, 127)])
+
+    # text from language names, both lower- and upper-case
+    languages = utils.supported_languages(include_in_context=True, include_english=True)
+    for lang in languages:
+        strings.append(lang[utils.KEY_LANG_NAME])
+        strings.append(lang[utils.KEY_LANG_NAME].upper())
+        strings.append(lang[utils.KEY_ENG_NAME])
+        strings.append(lang[utils.KEY_ENG_NAME].upper())
+
+    return strings
 
 
 def _merge_fonts(fonts, output_file_path):
@@ -501,20 +468,21 @@ def _cannot_merge(font):
     return font["head"].unitsPerEm != 1000
 
 
-def _subset_and_merge_ui_fonts(glyphs, reg_woff_path, bold_woff_path):
+def _subset_and_merge_fonts(text, reg_woff_path, bold_woff_path):
     """
-    Given a list of glyphs, generate both a bold and a regular font with all glyphs.
+    Given text, generate both a bold and a regular font that can render it.
     """
     reg_subsets = []
     bold_subsets = []
+    skipped = []
     for font_info in FONT_MANIFEST:
-        reg_ttf_path = _ttf_font_path(font_info, is_ui=True, is_bold=False)
-        bold_ttf_path = _ttf_font_path(font_info, is_ui=True, is_bold=True)
-        reg_subset = _get_subset_font(reg_ttf_path, glyphs)
-        bold_subset = _get_subset_font(bold_ttf_path, glyphs)
+        reg_ttf_path = _ttf_font_path(font_info, is_bold=False)
+        bold_ttf_path = _ttf_font_path(font_info, is_bold=True)
+        reg_subset = _get_subset_font(reg_ttf_path, text)
+        bold_subset = _get_subset_font(bold_ttf_path, text)
 
         if _cannot_merge(reg_subset) or _cannot_merge(bold_subset):
-            logging.warning("Fonts: {} has incompatible metrics".format(reg_ttf_path))
+            skipped.append(font_info[utils.KEY_NAME])
             continue
 
         reg_subsets.append(reg_subset)
@@ -523,32 +491,35 @@ def _subset_and_merge_ui_fonts(glyphs, reg_woff_path, bold_woff_path):
     _merge_fonts(reg_subsets, os.path.join(OUTPUT_PATH, reg_woff_path))
     _merge_fonts(bold_subsets, os.path.join(OUTPUT_PATH, bold_woff_path))
 
+    logging.warning(
+        "Could not subset and merge fonts: {}. These will show a flash of unstyled text.".format(
+            ", ".join(skipped)
+        )
+    )
+
 
 def command_gen_subset_fonts():
     """
-    Creates custom fonts that attempt to contain all the glyphs (and only the glyphs)
+    Creates custom fonts that attempt to contain all the glyphs and other font features
     that are used in user-facing text for the translation in each language.
     """
     logging.info("Fonts: generating subset fonts...")
 
-    # First, generate common fonts
-    common_glyphs = _get_common_glyphs()
+    common_strings = _get_common_strings()
 
-    reg_font_path = _woff_font_path("Common", is_ui=True, is_full=False, is_bold=False)
-    bold_font_path = _woff_font_path("Common", is_ui=True, is_full=False, is_bold=True)
-    _subset_and_merge_ui_fonts(common_glyphs, reg_font_path, bold_font_path)
-
-    # Next, generate a UI font subset for each language based on app text
-    for lang in utils.supported_languages(include_english=True):
-        lang_glyphs = _get_lang_glyphs(lang, utils.local_locale_path(lang))
-        lang_glyphs |= _get_lang_glyphs(lang, utils.local_perseus_locale_path(lang))
-        lang_glyphs -= common_glyphs
+    languages = utils.supported_languages(include_in_context=True, include_english=True)
+    for lang in languages:
+        strings = []
+        strings.extend(common_strings)
+        strings.extend(_get_lang_strings(utils.local_locale_path(lang)))
+        strings.extend(_get_lang_strings(utils.local_perseus_locale_path(lang)))
 
         name = lang[utils.KEY_INTL_CODE]
-
-        reg_font_path = _woff_font_path(name, is_ui=True, is_full=False, is_bold=False)
-        bold_font_path = _woff_font_path(name, is_ui=True, is_full=False, is_bold=True)
-        _subset_and_merge_ui_fonts(lang_glyphs, reg_font_path, bold_font_path)
+        _subset_and_merge_fonts(
+            text=" ".join(strings),
+            reg_woff_path=_woff_font_path(name, is_full=False, is_bold=False),
+            bold_woff_path=_woff_font_path(name, is_full=False, is_bold=True),
+        )
 
     logging.info("Fonts: created")
 
@@ -563,6 +534,14 @@ def _get_variations(font_name):
     For a given font name, return information about the four relevant variations on
     the font, and how we expect it the files to be named on github.
 
+    'UI' (User Interface) variants of fonts are sometimes available for languages with
+    tall glyph heights, and are redrawn to fit within constrained vertical space.
+    Not all fonts have (or need to have) a 'UI' variant.
+
+    'Content' variants are easier to read but are not guaranteed to fit within UI
+    elements or avoid overlapping when rendered in blocks of text. 'Tall' alphabets
+    need to have more line height allotted when content fonts are used.
+
     Returns four tuples of the form:
         (directory_name, file_name, is_ui)
     """
@@ -615,6 +594,13 @@ def command_add_source_fonts(font_name):
             logging.warning("\tNot found: {}".format(target_url))
             if is_ui:
                 has_ui_variant = False
+            else:
+                logging.error(
+                    "Font '{}' was not found. You can add it manually.".format(
+                        font_name
+                    )
+                )
+                sys.exit(1)
             continue
         else:
             r.raise_for_status()
@@ -627,12 +613,14 @@ def command_add_source_fonts(font_name):
     # update manifest
     already_exists = False
     for font_info in new_manifest:
-        if font_info["name"] == font_name:
+        if font_info[utils.KEY_NAME] == font_name:
             already_exists = True
-            font_info["has_ui_variant"] = has_ui_variant
+            font_info[utils.KEY_HAS_UI] = has_ui_variant
             continue
     if not already_exists:
-        new_manifest.append({"name": font_name, "has_ui_variant": has_ui_variant})
+        new_manifest.append(
+            {utils.KEY_NAME: font_name, utils.KEY_HAS_UI: has_ui_variant}
+        )
     logging.info(
         "Updating manifest - '{}' {} UI variant".format(
             font_name, "with" if has_ui_variant else "without"
@@ -640,7 +628,7 @@ def command_add_source_fonts(font_name):
     )
     with open(FONTS_MANIFEST_PATH, "w") as mf:
         json.dump(
-            sorted(new_manifest, key=lambda d: d["name"]),
+            sorted(new_manifest, key=lambda d: d[utils.KEY_NAME]),
             mf,
             indent=2,
             separators=(",", ": "),  # helps prevent trailing whitespace
@@ -667,13 +655,10 @@ def main():
         type=str,
     )
     subparsers.add_parser(
-        "generate-subset-fonts",
-        help="Generate subset UI fonts and CSS based on app text",
-    )
-    subparsers.add_parser(
-        "generate-full-fonts", help="Generate full UI and content fonts"
+        "generate-subset-fonts", help="Generate subset fonts based on app text"
     )
-    subparsers.add_parser("generate-css", help="Generate CSS")
+    subparsers.add_parser("generate-full-fonts", help="Generate full fonts")
+    subparsers.add_parser("generate-css", help="Generate CSS for all languages")
     args = parser.parse_args()
 
     if args.command == "add-source-font":
diff --git a/build_tools/i18n/utils.py b/build_tools/i18n/utils.py
--- a/build_tools/i18n/utils.py
+++ b/build_tools/i18n/utils.py
@@ -37,6 +37,7 @@
     sys.exit(1)
 
 
+# Keys used in supported_languages.json
 KEY_CROWDIN_CODE = "crowdin_code"
 KEY_INTL_CODE = "intl_code"
 KEY_LANG_NAME = "language_name"
@@ -51,16 +52,24 @@
     KEY_DEFAULT_FONT: "NotoSans",
 }
 
+# Keys used in simple internal 'font info' dictionaries
+KEY_NAME = "name"
+KEY_HAS_UI = "has_ui_variant"
+
 
 def to_locale(language):
     """
     Turns a language name (en-us) into a locale name (en_US).
+    Logic is derived from Django so be careful about changing it.
     """
-    p = language.find('-')
+    p = language.find("-")
     if p >= 0:
-        if len(language[p + 1:]) > 2:
-            return language[:p].lower() + '_' + language[p + 1].upper() + language[p + 2:].lower()
-        return language[:p].lower() + '_' + language[p + 1:].upper()
+        if len(language[p + 1 :]) > 2:
+            return "{}_{}".format(
+                language[:p].lower(),
+                language[p + 1].upper() + language[p + 2 :].lower(),
+            )
+        return "{}_{}".format(language[:p].lower(), language[p + 1 :].upper())
     else:
         return language.lower()
 
@@ -93,9 +102,13 @@ def supported_languages(include_in_context=False, include_english=False):
 
 @memoize
 def local_locale_path(lang_object):
-    return os.path.join(LOCALE_PATH, to_locale(lang_object[KEY_INTL_CODE]), "LC_MESSAGES")
+    return os.path.join(
+        LOCALE_PATH, to_locale(lang_object[KEY_INTL_CODE]), "LC_MESSAGES"
+    )
 
 
 @memoize
 def local_perseus_locale_path(lang_object):
-    return os.path.join(PERSEUS_LOCALE_PATH, to_locale(lang_object[KEY_INTL_CODE]), "LC_MESSAGES")
+    return os.path.join(
+        PERSEUS_LOCALE_PATH, to_locale(lang_object[KEY_INTL_CODE]), "LC_MESSAGES"
+    )


Test patch:
diff --git a/kolibri/core/assets/test/views/k-grid-item.spec.js b/kolibri/core/assets/test/views/k-grid-item.spec.js
--- a/kolibri/core/assets/test/views/k-grid-item.spec.js
+++ b/kolibri/core/assets/test/views/k-grid-item.spec.js
@@ -30,7 +30,7 @@ describe('KGridItem component', () => {
       allowableCols.forEach(j => {
         if (i <= j) {
           const wrapper = makeWrapper({ size: i }, { numCols: j, gutterWidth: 16 });
-          expect(isAllowed(wrapper.classes()[0])).toEqual(true);
+          expect(isAllowed(wrapper.classes()[1])).toEqual(true);
         }
       });
     });
@@ -39,7 +39,7 @@ describe('KGridItem component', () => {
     responsiveLayouts.forEach(numCols => {
       allowedPercentages.forEach(size => {
         const wrapper = makeWrapper({ size, percentage: true }, { numCols, gutterWidth: 16 });
-        expect(isAllowed(wrapper.classes()[0])).toEqual(true);
+        expect(isAllowed(wrapper.classes()[1])).toEqual(true);
       });
     });
   });
@@ -84,48 +84,48 @@ describe('KGridItem component', () => {
   it('should handle text-based responsive sizes - small', () => {
     const wrapper = makeWrapper({ sizes: '1, 2, 3' }, SMALL_GRID);
     wrapper.vm._updateWindow({ width: 400, height: 400 });
-    expect(wrapper.classes()[0]).toEqual('pure-u-6-24');
+    expect(wrapper.classes()[1]).toEqual('pure-u-6-24');
   });
 
   it('should handle text-based responsive sizes - medium', () => {
     const wrapper = makeWrapper({ sizes: '1, 2, 3' }, MEDIUM_GRID);
     wrapper.vm._updateWindow({ width: 700, height: 700 });
-    expect(wrapper.classes()[0]).toEqual('pure-u-6-24');
+    expect(wrapper.classes()[1]).toEqual('pure-u-6-24');
   });
 
   it('should handle text-based responsive sizes - large', () => {
     const wrapper = makeWrapper({ sizes: '1, 2, 3' }, LARGE_GRID);
     wrapper.vm._updateWindow({ width: 900, height: 900 });
-    expect(wrapper.classes()[0]).toEqual('pure-u-6-24');
+    expect(wrapper.classes()[1]).toEqual('pure-u-6-24');
   });
 
   it('should handle array-based responsive sizes - small', () => {
     const wrapper = makeWrapper({ sizes: [3, 5, 9] }, SMALL_GRID);
     wrapper.vm._updateWindow({ width: 400, height: 400 });
-    expect(wrapper.classes()[0]).toEqual('pure-u-18-24');
+    expect(wrapper.classes()[1]).toEqual('pure-u-18-24');
   });
 
   it('should handle array-based responsive sizes - medium', () => {
     const wrapper = makeWrapper({ sizes: [3, 5, 9] }, MEDIUM_GRID);
     wrapper.vm._updateWindow({ width: 700, height: 700 });
-    expect(wrapper.classes()[0]).toEqual('pure-u-15-24');
+    expect(wrapper.classes()[1]).toEqual('pure-u-15-24');
   });
 
   it('should handle array-based responsive sizes - large', () => {
     const wrapper = makeWrapper({ sizes: [3, 5, 9] }, LARGE_GRID);
     wrapper.vm._updateWindow({ width: 900, height: 900 });
-    expect(wrapper.classes()[0]).toEqual('pure-u-18-24');
+    expect(wrapper.classes()[1]).toEqual('pure-u-18-24');
   });
 
   it('should handle array-based responsive percentage sizes - large', () => {
     const wrapper = makeWrapper({ sizes: [25, 50, 25], percentage: true }, LARGE_GRID);
     wrapper.vm._updateWindow({ width: 900, height: 900 });
-    expect(wrapper.classes()[0]).toEqual('pure-u-6-24');
+    expect(wrapper.classes()[1]).toEqual('pure-u-6-24');
   });
 
   it('should handle text-based non-responsive percentage size', () => {
     const wrapper = makeWrapper({ size: '50', percentage: true }, MEDIUM_GRID);
     wrapper.vm._updateWindow({ width: 700, height: 700 });
-    expect(wrapper.classes()[0]).toEqual('pure-u-12-24');
+    expect(wrapper.classes()[1]).toEqual('pure-u-12-24');
   });
 });