Repository: zulip/zulip

Issue:
markdown/tabbed_sections: Raise exception for missing tab name.
As discovered in #19807, missing tab names are currently silently ignored by our `tabbed_sections` Markdown extension, this is not right. We should raise an exception somewhere so that missing tab names are caught before they make their way into production. This should hopefully be a quick fix! Ideally, we should do this in a manner such that something in `zerver.tests.test_markdown` or some other test file fails when a tab name is missing.

Thanks to @alya for reporting this bug! 


Discussion:
Hello @zulip/server-markdown members, this issue was labeled with the "area: markdown" label, so you may want to check it out!

<!-- areaLabelAddition -->

I would expect `test_docs` to fail if an exception is thrown when rendering a page, since I believe we render all /help/ pages in those tests.
Hi, I'd like to work on this 
@zulipbot claim
Welcome to Zulip, @pradyumn014! We just sent you an invite to collaborate on this repository at https://github.com/zulip/zulip/invitations. Please accept this invite in order to claim this issue and begin a fun, rewarding experience contributing to Zulip!

Here's some tips to get you off to a good start:

- Join me on the [Zulip developers' server](https://chat.zulip.org), to get help, chat about this issue, and meet the other developers.
- [Unwatch this repository](https://help.github.com/articles/unwatching-repositories/), so that you don't get 100 emails a day.

As you work on this issue, you'll also want to refer to the [Zulip code contribution guide](https://zulip.readthedocs.io/en/latest/contributing/index.html), as well as the rest of the developer documentation on that site.

See you on the other side (that is, the pull request side)!


Resolving patch:
diff --git a/zerver/lib/markdown/tabbed_sections.py b/zerver/lib/markdown/tabbed_sections.py
--- a/zerver/lib/markdown/tabbed_sections.py
+++ b/zerver/lib/markdown/tabbed_sections.py
@@ -27,7 +27,7 @@
 """.strip()
 
 NAV_LIST_ITEM_TEMPLATE = """
-<li data-language="{data_language}" tabindex="0">{name}</li>
+<li data-language="{data_language}" tabindex="0">{label}</li>
 """.strip()
 
 DIV_TAB_CONTENT_TEMPLATE = """
@@ -38,7 +38,7 @@
 
 # If adding new entries here, also check if you need to update
 # tabbed-instructions.js
-TAB_DISPLAY_NAMES = {
+TAB_SECTION_LABELS = {
     "desktop-web": "Desktop/Web",
     "ios": "iOS",
     "android": "Android",
@@ -73,6 +73,7 @@
     "not-stream": "From other views",
     "via-recent-topics": "Via recent topics",
     "via-left-sidebar": "Via left sidebar",
+    "instructions-for-all-platforms": "Instructions for all platforms",
 }
 
 
@@ -97,7 +98,10 @@ def run(self, lines: List[str]) -> List[str]:
             else:
                 tab_class = "no-tabs"
                 tab_section["tabs"] = [
-                    {"tab_name": "null_tab", "start": tab_section["start_tabs_index"]}
+                    {
+                        "tab_name": "instructions-for-all-platforms",
+                        "start": tab_section["start_tabs_index"],
+                    }
                 ]
             nav_bar = self.generate_nav_bar(tab_section)
             content_blocks = self.generate_content_blocks(tab_section, lines)
@@ -137,10 +141,16 @@ def generate_content_blocks(self, tab_section: Dict[str, Any], lines: List[str])
     def generate_nav_bar(self, tab_section: Dict[str, Any]) -> str:
         li_elements = []
         for tab in tab_section["tabs"]:
-            li = NAV_LIST_ITEM_TEMPLATE.format(
-                data_language=tab.get("tab_name"), name=TAB_DISPLAY_NAMES.get(tab.get("tab_name"))
-            )
+            tab_name = tab.get("tab_name")
+            tab_label = TAB_SECTION_LABELS.get(tab_name)
+            if tab_label is None:
+                raise ValueError(
+                    f"Tab '{tab_name}' is not present in TAB_SECTION_LABELS in zerver/lib/markdown/tabbed_sections.py"
+                )
+
+            li = NAV_LIST_ITEM_TEMPLATE.format(data_language=tab_name, label=tab_label)
             li_elements.append(li)
+
         return NAV_BAR_TEMPLATE.format(tabs="\n".join(li_elements))
 
     def parse_tabs(self, lines: List[str]) -> Optional[Dict[str, Any]]:


Test patch:
diff --git a/templates/zerver/tests/markdown/test_tabbed_sections_missing_tabs.md b/templates/zerver/tests/markdown/test_tabbed_sections_missing_tabs.md
new file mode 100644
--- /dev/null
+++ b/templates/zerver/tests/markdown/test_tabbed_sections_missing_tabs.md
@@ -0,0 +1,11 @@
+# Heading
+
+{start_tabs}
+{tab|ios}
+iOS instructions
+
+{tab|minix}
+
+Minix instructions. We expect an exception because the minix tab doesn't have a declared label.
+
+{end_tabs}
diff --git a/zerver/tests/test_templates.py b/zerver/tests/test_templates.py
--- a/zerver/tests/test_templates.py
+++ b/zerver/tests/test_templates.py
@@ -76,10 +76,10 @@ def test_markdown_tabbed_sections_extension(self) -> None:
 <p>
   <div class="code-section no-tabs" markdown="1">
     <ul class="nav">
-      <li data-language="null_tab" tabindex="0">None</li>
+      <li data-language="instructions-for-all-platforms" tabindex="0">Instructions for all platforms</li>
     </ul>
     <div class="blocks">
-      <div data-language="null_tab" markdown="1"></p>
+      <div data-language="instructions-for-all-platforms" markdown="1"></p>
         <p>Instructions for all platforms</p>
       <p></div>
     </div>
@@ -92,6 +92,15 @@ def test_markdown_tabbed_sections_extension(self) -> None:
         expected_html_sans_whitespace = expected_html.replace(" ", "").replace("\n", "")
         self.assertEqual(content_sans_whitespace, expected_html_sans_whitespace)
 
+    def test_markdown_tabbed_sections_missing_tabs(self) -> None:
+        template = get_template("tests/test_markdown.html")
+        context = {
+            "markdown_test_file": "zerver/tests/markdown/test_tabbed_sections_missing_tabs.md",
+        }
+        expected_regex = "^Tab 'minix' is not present in TAB_SECTION_LABELS in zerver/lib/markdown/tabbed_sections.py$"
+        with self.assertRaisesRegex(ValueError, expected_regex):
+            template.render(context)
+
     def test_markdown_nested_code_blocks(self) -> None:
         template = get_template("tests/test_markdown.html")
         context = {