diff --git a/test/debayer-geometry.cpp b/test/debayer-geometry.cpp
new file mode 100644
index 00000000..cfc757fa
--- /dev/null
+++ b/test/debayer-geometry.cpp
@@ -0,0 +1,63 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2026, Red Hat Inc.
+ *
+ * Sensor crop tests
+ */
+
+#include <iostream>
+
+#include <libcamera/geometry.h>
+
+#include "libcamera/internal/software_isp/debayer_geometry.h"
+
+#include "test.h"
+
+using namespace libcamera;
+
+class DebayerGeometryTest : public Test
+{
+protected:
+	int run() override
+	{
+		if (testCrop())
+			return TestFail;
+
+		return TestPass;
+	}
+
+private:
+	int testCrop()
+	{
+		static const struct {
+			Size input;
+			Size native;
+			Size output;
+			Rectangle expected;
+		} cases[] = {
+			{ { 1920, 1080 }, { 1920, 1080 }, { 1920, 1080 }, { 0, 0, 1920, 1080 } },
+			{ { 1928, 1092 }, { 1920, 1092 }, { 1920, 1092 }, { 4, 0, 1920, 1092 } },
+			/* Output resolution changes must not shrink the sensor window. */
+			{ { 1928, 1092 }, { 1920, 1092 }, { 1920, 1080 }, { 4, 6, 1920, 1080 } },
+			{ { 1928, 1092 }, { 1920, 1092 }, { 1280, 720 }, { 4, 6, 1920, 1080 } },
+			{ { 1928, 1092 }, { 1920, 1092 }, { 640, 480 }, { 236, 0, 1456, 1092 } },
+			{ { 1928, 1092 }, { 1920, 1092 }, { 512, 768 }, { 600, 0, 728, 1092 } },
+			{ { 1928, 1092 }, { 1920, 1092 }, { 1280, 640 }, { 4, 66, 1920, 960 } },
+			/* A smaller Bayer border leaves an odd top coordinate. */
+			{ { 1928, 1092 }, { 1924, 1092 }, { 1280, 720 }, { 2, 5, 1924, 1082 } },
+		};
+
+		for (const auto &entry : cases) {
+			Rectangle crop = debayerCrop(entry.input, entry.native, entry.output);
+			if (crop != entry.expected) {
+				std::cerr << "Crop " << entry.input << " -> " << entry.output
+					  << " is " << crop << ", expected " << entry.expected << std::endl;
+				return TestFail;
+			}
+		}
+
+		return TestPass;
+	}
+};
+
+TEST_REGISTER(DebayerGeometryTest)
diff --git a/test/meson.build b/test/meson.build
index 363a2829..5027b7b1 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -25,60 +25,61 @@ endif
 
 subdir('libtest')
 
 subdir('camera')
 subdir('controls')
 subdir('gstreamer')
 subdir('ipa')
 subdir('ipc')
 subdir('log')
 subdir('media_device')
 subdir('process')
 subdir('py')
 subdir('serialization')
 subdir('stream')
 subdir('v4l2_compat')
 subdir('v4l2_subdevice')
 subdir('v4l2_videodevice')
 
 public_tests = [
     {'name': 'color-space', 'sources': ['color-space.cpp']},
     {'name': 'geometry', 'sources': ['geometry.cpp']},
     {'name': 'public-api', 'sources': ['public-api.cpp']},
     {'name': 'signal', 'sources': ['signal.cpp']},
     {'name': 'transform', 'sources': ['transform.cpp']},
 ]
 
 internal_tests = [
     {'name': 'bayer-format', 'sources': ['bayer-format.cpp']},
     {'name': 'byte-stream-buffer', 'sources': ['byte-stream-buffer.cpp']},
     {'name': 'camera-sensor', 'sources': ['camera-sensor.cpp']},
+    {'name': 'debayer-geometry', 'sources': ['debayer-geometry.cpp']},
     {'name': 'delayed_controls', 'sources': ['delayed_controls.cpp']},
     {'name': 'event', 'sources': ['event.cpp']},
     {'name': 'event-dispatcher', 'sources': ['event-dispatcher.cpp']},
     {'name': 'event-thread', 'sources': ['event-thread.cpp']},
     {'name': 'file', 'sources': ['file.cpp']},
     {'name': 'flags', 'sources': ['flags.cpp']},
     {'name': 'hotplug-cameras', 'sources': ['hotplug-cameras.cpp']},
     {'name': 'matrix', 'sources': ['matrix.cpp']},
     {'name': 'message', 'sources': ['message.cpp']},
     {'name': 'object', 'sources': ['object.cpp']},
     {'name': 'object-delete', 'sources': ['object-delete.cpp']},
     {'name': 'object-invoke', 'sources': ['object-invoke.cpp']},
     {'name': 'pixel-format', 'sources': ['pixel-format.cpp']},
     {'name': 'shared-fd', 'sources': ['shared-fd.cpp']},
     {'name': 'signal-threads', 'sources': ['signal-threads.cpp']},
     {'name': 'threads', 'sources': 'threads.cpp', 'dependencies': [libthreads]},
     {'name': 'timer', 'sources': ['timer.cpp']},
     {'name': 'timer-fail', 'sources': ['timer-fail.cpp'], 'should_fail': true},
     {'name': 'timer-thread', 'sources': ['timer-thread.cpp']},
     {'name': 'unique-fd', 'sources': ['unique-fd.cpp']},
     {'name': 'utils', 'sources': ['utils.cpp']},
     {'name': 'value-node', 'sources': ['value-node.cpp']},
     {'name': 'vector', 'sources': ['vector.cpp']},
     {'name': 'yaml-parser', 'sources': ['yaml-parser.cpp']},
 ]
 
 internal_non_parallel_tests = [
     {'name': 'fence', 'sources': ['fence.cpp']},
     {'name': 'mapped-buffer', 'sources': ['mapped-buffer.cpp']},
 ]
