""" Unit tests for engine.canvas.Canvas. Tests the core 2D rendering surface without any display dependencies. """ from engine.canvas import Canvas, CanvasRegion class TestCanvasRegion: """Tests for CanvasRegion dataclass.""" def test_is_valid_positive_dimensions(self): """Positive width and height returns True.""" region = CanvasRegion(0, 0, 10, 5) assert region.is_valid() is True def test_is_valid_zero_width(self): """Zero width returns False.""" region = CanvasRegion(0, 0, 0, 5) assert region.is_valid() is False def test_is_valid_zero_height(self): """Zero height returns False.""" region = CanvasRegion(0, 0, 10, 0) assert region.is_valid() is False def test_is_valid_negative_dimensions(self): """Negative dimensions return False.""" region = CanvasRegion(0, 0, -1, 5) assert region.is_valid() is False def test_rows_computes_correct_set(self): """rows() returns set of row indices in region.""" region = CanvasRegion(2, 3, 4, 2) assert region.rows() == {3, 4} class TestCanvas: """Tests for Canvas class.""" def test_init_default_dimensions(self): """Default width=80, height=24.""" canvas = Canvas() assert canvas.width == 80 assert canvas.height == 24 assert len(canvas._grid) == 24 assert len(canvas._grid[0]) == 80 def test_init_custom_dimensions(self): """Custom dimensions are set correctly.""" canvas = Canvas(100, 50) assert canvas.width == 100 assert canvas.height == 50 def test_clear_empties_grid(self): """clear() resets all cells to spaces.""" canvas = Canvas(5, 3) canvas.put_text(0, 0, "Hello") canvas.clear() region = canvas.get_region(0, 0, 5, 3) assert all(all(cell == " " for cell in row) for row in region) def test_clear_marks_entire_canvas_dirty(self): """clear() marks entire canvas as dirty.""" canvas = Canvas(10, 5) canvas.clear() dirty = canvas.get_dirty_regions() assert len(dirty) == 1 assert dirty[0].x == 0 and dirty[0].y == 0 assert dirty[0].width == 10 and dirty[0].height == 5 def test_put_text_single_char(self): """put_text writes a single character at position.""" canvas = Canvas(10, 5) canvas.put_text(3, 2, "X") assert canvas._grid[2][3] == "X" def test_put_text_multiple_chars(self): """put_text writes multiple characters in a row.""" canvas = Canvas(10, 5) canvas.put_text(2, 1, "ABC") assert canvas._grid[1][2] == "A" assert canvas._grid[1][3] == "B" assert canvas._grid[1][4] == "C" def test_put_text_ignores_overflow_right(self): """Characters beyond width are ignored.""" canvas = Canvas(5, 5) canvas.put_text(3, 0, "XYZ") assert canvas._grid[0][3] == "X" assert canvas._grid[0][4] == "Y" # Z would be at index 5, which is out of bounds def test_put_text_ignores_overflow_bottom(self): """Rows beyond height are ignored.""" canvas = Canvas(5, 3) canvas.put_text(0, 5, "test") # Row 5 doesn't exist, nothing should be written assert all(cell == " " for row in canvas._grid for cell in row) def test_put_text_marks_dirty_region(self): """put_text marks the written area as dirty.""" canvas = Canvas(10, 5) canvas.put_text(2, 1, "Hello") dirty = canvas.get_dirty_regions() assert len(dirty) == 1 assert dirty[0].x == 2 and dirty[0].y == 1 assert dirty[0].width == 5 and dirty[0].height == 1 def test_put_text_empty_string_no_dirty(self): """Empty string does not create dirty region.""" canvas = Canvas(10, 5) canvas.put_text(0, 0, "") assert not canvas.is_dirty() def test_put_region_single_cell(self): """put_region writes a single cell correctly.""" canvas = Canvas(5, 5) content = [["X"]] canvas.put_region(2, 2, content) assert canvas._grid[2][2] == "X" def test_put_region_multiple_rows(self): """put_region writes multiple rows correctly.""" canvas = Canvas(10, 10) content = [["A", "B"], ["C", "D"]] canvas.put_region(1, 1, content) assert canvas._grid[1][1] == "A" assert canvas._grid[1][2] == "B" assert canvas._grid[2][1] == "C" assert canvas._grid[2][2] == "D" def test_put_region_partial_out_of_bounds(self): """put_region clips content that extends beyond canvas bounds.""" canvas = Canvas(5, 5) content = [["A", "B", "C"], ["D", "E", "F"]] canvas.put_region(4, 4, content) # Only cell (4,4) should be within bounds assert canvas._grid[4][4] == "A" # Others are out of bounds assert canvas._grid[4][5] == " " if 5 < 5 else True # index 5 doesn't exist assert canvas._grid[5][4] == " " if 5 < 5 else True # row 5 doesn't exist def test_put_region_marks_dirty(self): """put_region marks dirty region covering written area (clipped).""" canvas = Canvas(10, 10) content = [["A", "B", "C"], ["D", "E", "F"]] canvas.put_region(2, 2, content) dirty = canvas.get_dirty_regions() assert len(dirty) == 1 assert dirty[0].x == 2 and dirty[0].y == 2 assert dirty[0].width == 3 and dirty[0].height == 2 def test_fill_rectangle(self): """fill() fills a rectangular region with character.""" canvas = Canvas(10, 10) canvas.fill(2, 2, 3, 2, "*") for y in range(2, 4): for x in range(2, 5): assert canvas._grid[y][x] == "*" def test_fill_entire_canvas(self): """fill() can fill entire canvas.""" canvas = Canvas(5, 3) canvas.fill(0, 0, 5, 3, "#") for row in canvas._grid: assert all(cell == "#" for cell in row) def test_fill_empty_region_no_dirty(self): """fill with zero dimensions does not mark dirty.""" canvas = Canvas(10, 10) canvas.fill(0, 0, 0, 5, "X") assert not canvas.is_dirty() def test_fill_clips_to_bounds(self): """fill clips to canvas boundaries.""" canvas = Canvas(5, 5) canvas.fill(3, 3, 5, 5, "X") # Should only fill within bounds: (3,3) to (4,4) assert canvas._grid[3][3] == "X" assert canvas._grid[3][4] == "X" assert canvas._grid[4][3] == "X" assert canvas._grid[4][4] == "X" # Out of bounds should remain spaces assert canvas._grid[5] if 5 < 5 else True # row 5 doesn't exist def test_get_region_extracts_subgrid(self): """get_region returns correct rectangular subgrid.""" canvas = Canvas(10, 10) for y in range(10): for x in range(10): canvas._grid[y][x] = chr(ord("A") + (x % 26)) region = canvas.get_region(2, 3, 4, 2) assert len(region) == 2 assert len(region[0]) == 4 assert region[0][0] == "C" # (2,3) = 'C' assert region[1][2] == "E" # (4,4) = 'E' def test_get_region_out_of_bounds_returns_spaces(self): """get_region pads out-of-bounds areas with spaces.""" canvas = Canvas(5, 5) canvas.put_text(0, 0, "HELLO") # Region overlapping right edge: cols 3-4 inside, col5+ outside region = canvas.get_region(3, 0, 5, 2) assert region[0][0] == "L" assert region[0][1] == "O" assert region[0][2] == " " # col5 out of bounds assert all(cell == " " for cell in region[1]) def test_get_region_flat_returns_lines(self): """get_region_flat returns list of joined strings.""" canvas = Canvas(10, 5) canvas.put_text(0, 0, "FIRST") canvas.put_text(0, 1, "SECOND") flat = canvas.get_region_flat(0, 0, 6, 2) assert flat == ["FIRST ", "SECOND"] def test_mark_dirty_manual(self): """mark_dirty() can be called manually to mark arbitrary region.""" canvas = Canvas(10, 10) canvas.mark_dirty(5, 5, 3, 2) dirty = canvas.get_dirty_regions() assert len(dirty) == 1 assert dirty[0] == CanvasRegion(5, 5, 3, 2) def test_get_dirty_rows_union(self): """get_dirty_rows() returns union of all dirty row indices.""" canvas = Canvas(10, 10) canvas.put_text(0, 0, "A") # row 0 canvas.put_text(0, 2, "B") # row 2 canvas.mark_dirty(0, 1, 1, 1) # row 1 rows = canvas.get_dirty_rows() assert rows == {0, 1, 2} def test_is_dirty_after_operations(self): """is_dirty() returns True after any modifying operation.""" canvas = Canvas(10, 10) assert not canvas.is_dirty() canvas.put_text(0, 0, "X") assert canvas.is_dirty() _ = canvas.get_dirty_regions() # resets assert not canvas.is_dirty() def test_resize_same_size_no_change(self): """resize with same dimensions does nothing.""" canvas = Canvas(10, 5) canvas.put_text(0, 0, "TEST") canvas.resize(10, 5) assert canvas._grid[0][0] == "T" def test_resize_larger_preserves_content(self): """resize to larger canvas preserves existing content.""" canvas = Canvas(5, 3) canvas.put_text(1, 1, "AB") canvas.resize(10, 6) assert canvas.width == 10 assert canvas.height == 6 assert canvas._grid[1][1] == "A" assert canvas._grid[1][2] == "B" # New area should be spaces assert canvas._grid[0][0] == " " def test_resize_smaller_truncates(self): """resize to smaller canvas drops content outside new bounds.""" canvas = Canvas(10, 5) canvas.put_text(8, 4, "XYZ") canvas.resize(5, 3) assert canvas.width == 5 assert canvas.height == 3 # Content at (8,4) should be lost # But content within new bounds should remain canvas2 = Canvas(10, 5) canvas2.put_text(2, 2, "HI") canvas2.resize(5, 3) assert canvas2._grid[2][2] == "H" def test_resize_does_not_auto_mark_dirty(self): """resize() does not automatically mark dirty (caller responsibility).""" canvas = Canvas(10, 10) canvas.put_text(0, 0, "A") _ = canvas.get_dirty_regions() # reset canvas.resize(5, 5) # Resize doesn't mark dirty - this is current implementation assert not canvas.is_dirty()