|
| 1 | +""" |
| 2 | +Tests for update_paragraph_style batch operation support. |
| 3 | +
|
| 4 | +Covers the helpers, validation, and batch manager integration. |
| 5 | +""" |
| 6 | + |
| 7 | +import pytest |
| 8 | +from unittest.mock import AsyncMock, Mock |
| 9 | + |
| 10 | +from gdocs.docs_helpers import ( |
| 11 | + build_paragraph_style, |
| 12 | + create_update_paragraph_style_request, |
| 13 | +) |
| 14 | +from gdocs.managers.validation_manager import ValidationManager |
| 15 | + |
| 16 | + |
| 17 | +class TestBuildParagraphStyle: |
| 18 | + def test_no_params_returns_empty(self): |
| 19 | + style, fields = build_paragraph_style() |
| 20 | + assert style == {} |
| 21 | + assert fields == [] |
| 22 | + |
| 23 | + def test_heading_zero_maps_to_normal_text(self): |
| 24 | + style, fields = build_paragraph_style(heading_level=0) |
| 25 | + assert style["namedStyleType"] == "NORMAL_TEXT" |
| 26 | + |
| 27 | + def test_heading_maps_to_named_style(self): |
| 28 | + style, _ = build_paragraph_style(heading_level=3) |
| 29 | + assert style["namedStyleType"] == "HEADING_3" |
| 30 | + |
| 31 | + def test_heading_out_of_range_raises(self): |
| 32 | + with pytest.raises(ValueError): |
| 33 | + build_paragraph_style(heading_level=7) |
| 34 | + |
| 35 | + def test_line_spacing_scaled_to_percentage(self): |
| 36 | + style, _ = build_paragraph_style(line_spacing=1.5) |
| 37 | + assert style["lineSpacing"] == 150.0 |
| 38 | + |
| 39 | + def test_dimension_field_uses_pt_unit(self): |
| 40 | + style, _ = build_paragraph_style(indent_start=36.0) |
| 41 | + assert style["indentStart"] == {"magnitude": 36.0, "unit": "PT"} |
| 42 | + |
| 43 | + def test_multiple_params_combined(self): |
| 44 | + style, fields = build_paragraph_style( |
| 45 | + heading_level=2, alignment="CENTER", space_below=12.0 |
| 46 | + ) |
| 47 | + assert len(fields) == 3 |
| 48 | + assert style["alignment"] == "CENTER" |
| 49 | + |
| 50 | + |
| 51 | +class TestCreateUpdateParagraphStyleRequest: |
| 52 | + def test_returns_none_when_no_styles(self): |
| 53 | + assert create_update_paragraph_style_request(1, 10) is None |
| 54 | + |
| 55 | + def test_produces_correct_api_structure(self): |
| 56 | + result = create_update_paragraph_style_request(1, 10, heading_level=1) |
| 57 | + inner = result["updateParagraphStyle"] |
| 58 | + assert inner["range"] == {"startIndex": 1, "endIndex": 10} |
| 59 | + assert inner["paragraphStyle"]["namedStyleType"] == "HEADING_1" |
| 60 | + assert inner["fields"] == "namedStyleType" |
| 61 | + |
| 62 | + |
| 63 | +class TestValidateParagraphStyleParams: |
| 64 | + @pytest.fixture() |
| 65 | + def vm(self): |
| 66 | + return ValidationManager() |
| 67 | + |
| 68 | + def test_all_none_rejected(self, vm): |
| 69 | + is_valid, _ = vm.validate_paragraph_style_params() |
| 70 | + assert not is_valid |
| 71 | + |
| 72 | + def test_wrong_types_rejected(self, vm): |
| 73 | + assert not vm.validate_paragraph_style_params(heading_level=1.5)[0] |
| 74 | + assert not vm.validate_paragraph_style_params(alignment=123)[0] |
| 75 | + assert not vm.validate_paragraph_style_params(line_spacing="double")[0] |
| 76 | + |
| 77 | + def test_negative_indent_start_rejected(self, vm): |
| 78 | + is_valid, msg = vm.validate_paragraph_style_params(indent_start=-5.0) |
| 79 | + assert not is_valid |
| 80 | + assert "non-negative" in msg |
| 81 | + |
| 82 | + def test_negative_indent_first_line_allowed(self, vm): |
| 83 | + """Hanging indent requires negative first-line indent.""" |
| 84 | + assert vm.validate_paragraph_style_params(indent_first_line=-18.0)[0] |
| 85 | + |
| 86 | + def test_batch_validation_wired_up(self, vm): |
| 87 | + valid_ops = [ |
| 88 | + {"type": "update_paragraph_style", "start_index": 1, |
| 89 | + "end_index": 20, "heading_level": 2}, |
| 90 | + ] |
| 91 | + assert vm.validate_batch_operations(valid_ops)[0] |
| 92 | + |
| 93 | + no_style_ops = [ |
| 94 | + {"type": "update_paragraph_style", "start_index": 1, "end_index": 20}, |
| 95 | + ] |
| 96 | + assert not vm.validate_batch_operations(no_style_ops)[0] |
| 97 | + |
| 98 | + |
| 99 | +class TestBatchManagerIntegration: |
| 100 | + @pytest.fixture() |
| 101 | + def manager(self): |
| 102 | + from gdocs.managers.batch_operation_manager import BatchOperationManager |
| 103 | + |
| 104 | + return BatchOperationManager(Mock()) |
| 105 | + |
| 106 | + def test_build_request_and_description(self, manager): |
| 107 | + op = { |
| 108 | + "type": "update_paragraph_style", |
| 109 | + "start_index": 1, "end_index": 50, |
| 110 | + "heading_level": 2, "alignment": "CENTER", "line_spacing": 1.5, |
| 111 | + } |
| 112 | + request, desc = manager._build_operation_request(op, "update_paragraph_style") |
| 113 | + assert "updateParagraphStyle" in request |
| 114 | + assert "heading: H2" in desc |
| 115 | + assert "1.5x" in desc |
| 116 | + |
| 117 | + @pytest.mark.asyncio |
| 118 | + async def test_end_to_end_execute(self, manager): |
| 119 | + manager._execute_batch_requests = AsyncMock(return_value={"replies": [{}]}) |
| 120 | + success, message, meta = await manager.execute_batch_operations( |
| 121 | + "doc-123", |
| 122 | + [{"type": "update_paragraph_style", "start_index": 1, |
| 123 | + "end_index": 20, "heading_level": 1}], |
| 124 | + ) |
| 125 | + assert success |
| 126 | + assert meta["operations_count"] == 1 |
0 commit comments