diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index ce33de8609..9a01d4e8fe 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -5,4 +5,5 @@ add_test_files(
strings_func.cpp
test_main.cpp
test_script_admin.cpp
+ test_window_desc.cpp
)
diff --git a/src/tests/test_window_desc.cpp b/src/tests/test_window_desc.cpp
new file mode 100644
index 0000000000..8421a9dc34
--- /dev/null
+++ b/src/tests/test_window_desc.cpp
@@ -0,0 +1,49 @@
+/*
+ * This file is part of OpenTTD.
+ * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
+ * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see .
+ */
+
+/** @file test_window_desc.cpp Test WindowDescs for valid widget parts. */
+
+#include "../stdafx.h"
+
+#include "../3rdparty/catch2/catch.hpp"
+
+#include "../window_gui.h"
+
+/**
+ * List of WindowDescs. Defined in window.cpp but not exposed as this unit-test is the only other place that needs it.
+ * WindowDesc is a self-registering class so all WindowDescs will be included in the list.
+ */
+extern std::vector *_window_descs;
+
+TEST_CASE("WindowDesc - ini_key uniqueness")
+{
+ std::set seen;
+
+ for (const WindowDesc *window_desc : *_window_descs) {
+
+ if (window_desc->ini_key == nullptr) continue;
+
+ CAPTURE(window_desc->ini_key);
+ CHECK((seen.find(window_desc->ini_key) == std::end(seen)));
+
+ seen.insert(window_desc->ini_key);
+ }
+}
+
+TEST_CASE("WindowDesc - ini_key validity")
+{
+ const WindowDesc *window_desc = GENERATE(from_range(std::begin(*_window_descs), std::end(*_window_descs)));
+
+ bool has_inikey = window_desc->ini_key != nullptr;
+ bool has_widget = std::any_of(window_desc->nwid_begin, window_desc->nwid_end, [](const NWidgetPart &part) { return part.type == WWT_DEFSIZEBOX || part.type == WWT_STICKYBOX; });
+
+ INFO(fmt::format("{}:{}", window_desc->file, window_desc->line));
+ CAPTURE(has_inikey);
+ CAPTURE(has_widget);
+
+ CHECK((has_widget == has_inikey));
+}
diff --git a/src/window.cpp b/src/window.cpp
index de514ec8d5..2047a6efd7 100644
--- a/src/window.cpp
+++ b/src/window.cpp
@@ -96,7 +96,7 @@ SpecialMouseMode _special_mouse_mode; ///< Mode of the mouse.
* List of all WindowDescs.
* This is a pointer to ensure initialisation order with the various static WindowDesc instances.
*/
-static std::vector *_window_descs = nullptr;
+std::vector *_window_descs = nullptr;
/** Config file to store WindowDesc */
std::string _windows_file;