Coverage for tests/components/test_crosshair_graphic_view.py: 100%

636 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-03 15:51 +0000

1import pytest 

2from unittest.mock import Mock, patch, MagicMock, PropertyMock 

3from PyQt6.QtCore import Qt, QPointF, QRectF, QEvent 

4from PyQt6.QtGui import QMouseEvent, QPainter, QColor 

5from PyQt6.QtWidgets import QGraphicsView, QGraphicsScene, QGraphicsLineItem, QWidget 

6 

7from main.components.crosshair_graphic_view import CrosshairGraphicsView 

8 

9 

10@pytest.fixture 

11def mock_parent_viewer(): 

12 """Mock for the parent viewer.""" 

13 parent = QWidget() 

14 parent.img_data = Mock() # Simulates image data presence 

15 parent.handle_click_coordinates = Mock() 

16 parent.update_cross_view_lines = Mock() 

17 return parent 

18 

19 

20@pytest.fixture 

21def graphics_scene(): 

22 """Creates a QGraphicsScene for testing.""" 

23 scene = QGraphicsScene() 

24 scene.setSceneRect(0, 0, 512, 512) 

25 return scene 

26 

27 

28class TestCrosshairGraphicsViewInitialization: 

29 """Tests for CrosshairGraphicsView initialization.""" 

30 

31 def test_initialization_basic(self, qtbot): 

32 """Test basic initialization.""" 

33 view = CrosshairGraphicsView(view_idx=0) 

34 qtbot.addWidget(view) 

35 

36 assert view.view_idx == 0 

37 assert view.parent_viewer is None 

38 assert view.crosshair_h is None 

39 assert view.crosshair_v is None 

40 assert view.crosshair_visible is False 

41 

42 def test_initialization_with_parent(self, qtbot, mock_parent_viewer): 

43 """Test initialization with parent.""" 

44 view = CrosshairGraphicsView(view_idx=1, parent=mock_parent_viewer) 

45 qtbot.addWidget(view) 

46 

47 assert view.view_idx == 1 

48 assert view.parent_viewer == mock_parent_viewer 

49 

50 def test_initialization_different_view_indices(self, qtbot): 

51 """Test initialization with different indices.""" 

52 for idx in [0, 1, 2, 3, 10]: 

53 view = CrosshairGraphicsView(view_idx=idx) 

54 qtbot.addWidget(view) 

55 assert view.view_idx == idx 

56 

57 def test_mouse_tracking_enabled(self, qtbot): 

58 """Test that mouse tracking is enabled.""" 

59 view = CrosshairGraphicsView(view_idx=0) 

60 qtbot.addWidget(view) 

61 

62 assert view.hasMouseTracking() 

63 

64 def test_drag_mode_set(self, qtbot): 

65 """Test that drag mode is NoDrag.""" 

66 view = CrosshairGraphicsView(view_idx=0) 

67 qtbot.addWidget(view) 

68 

69 assert view.dragMode() == QGraphicsView.DragMode.NoDrag 

70 

71 def test_render_hints_set(self, qtbot): 

72 """Test that render hints are set.""" 

73 view = CrosshairGraphicsView(view_idx=0) 

74 qtbot.addWidget(view) 

75 

76 hints = view.renderHints() 

77 

78 assert hints & QPainter.RenderHint.Antialiasing 

79 assert hints & QPainter.RenderHint.SmoothPixmapTransform 

80 

81 

82class TestSetupCrosshairs: 

83 """Tests for the setup_crosshairs method.""" 

84 

85 def test_setup_crosshairs_with_scene(self, qtbot, graphics_scene): 

86 """Test setup crosshairs with scene.""" 

87 view = CrosshairGraphicsView(view_idx=0) 

88 qtbot.addWidget(view) 

89 view.setScene(graphics_scene) 

90 

91 view.setup_crosshairs() 

92 

93 assert view.crosshair_h is not None 

94 assert view.crosshair_v is not None 

95 assert isinstance(view.crosshair_h, QGraphicsLineItem) 

96 assert isinstance(view.crosshair_v, QGraphicsLineItem) 

97 assert not view.crosshair_h.isVisible() 

98 assert not view.crosshair_v.isVisible() 

99 

100 def test_setup_crosshairs_without_scene(self, qtbot): 

101 """Test setup crosshairs without scene.""" 

102 view = CrosshairGraphicsView(view_idx=0) 

103 qtbot.addWidget(view) 

104 

105 # Should not crash 

106 view.setup_crosshairs() 

107 

108 assert view.crosshair_h is None 

109 assert view.crosshair_v is None 

110 

111 def test_setup_crosshairs_color(self, qtbot, graphics_scene): 

112 """Test that crosshairs have the correct color.""" 

113 view = CrosshairGraphicsView(view_idx=0) 

114 qtbot.addWidget(view) 

115 view.setScene(graphics_scene) 

116 

117 view.setup_crosshairs() 

118 

119 # Check color (yellow with alpha 180) 

120 pen_h = view.crosshair_h.pen() 

121 pen_v = view.crosshair_v.pen() 

122 

123 assert pen_h.color() == QColor(255, 255, 0, 180) 

124 assert pen_v.color() == QColor(255, 255, 0, 180) 

125 assert pen_h.width() == 1 

126 assert pen_v.width() == 1 

127 

128 def test_setup_crosshairs_multiple_calls(self, qtbot, graphics_scene): 

129 """Test multiple calls to setup_crosshairs.""" 

130 view = CrosshairGraphicsView(view_idx=0) 

131 qtbot.addWidget(view) 

132 view.setScene(graphics_scene) 

133 

134 view.setup_crosshairs() 

135 first_h = view.crosshair_h 

136 first_v = view.crosshair_v 

137 

138 view.setup_crosshairs() 

139 

140 # New crosshairs should have been created 

141 assert view.crosshair_h is not None 

142 assert view.crosshair_v is not None 

143 

144 

145class TestMouseMoveEvent: 

146 """Tests for mouseMoveEvent.""" 

147 

148 def test_mouse_move_emits_coordinate_changed(self, qtbot, mock_parent_viewer, graphics_scene): 

149 """Test that mouse move emits coordinate_changed.""" 

150 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

151 qtbot.addWidget(view) 

152 view.setScene(graphics_scene) 

153 view.setup_crosshairs() 

154 

155 with qtbot.waitSignal(view.coordinate_changed, timeout=1000) as blocker: 

156 event = QMouseEvent( 

157 QMouseEvent.Type.MouseMove, 

158 QPointF(100, 100), 

159 Qt.MouseButton.NoButton, 

160 Qt.MouseButton.NoButton, 

161 Qt.KeyboardModifier.NoModifier 

162 ) 

163 view.mouseMoveEvent(event) 

164 

165 # Check signal emitted 

166 assert blocker.signal_triggered 

167 args = blocker.args 

168 assert args[0] == 0 # view_idx 

169 

170 def test_mouse_move_updates_crosshairs(self, qtbot, mock_parent_viewer, graphics_scene): 

171 """Test that mouse move updates crosshairs.""" 

172 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

173 qtbot.addWidget(view) 

174 view.setScene(graphics_scene) 

175 view.setup_crosshairs() 

176 

177 with patch.object(view, 'update_crosshairs') as mock_update: 

178 event = QMouseEvent( 

179 QMouseEvent.Type.MouseMove, 

180 QPointF(100, 150), 

181 Qt.MouseButton.NoButton, 

182 Qt.MouseButton.NoButton, 

183 Qt.KeyboardModifier.NoModifier 

184 ) 

185 view.mouseMoveEvent(event) 

186 

187 mock_update.assert_called_once() 

188 

189 def test_mouse_move_without_scene(self, qtbot, mock_parent_viewer): 

190 """Test mouse move without scene.""" 

191 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

192 qtbot.addWidget(view) 

193 

194 # Should not crash 

195 event = QMouseEvent( 

196 QMouseEvent.Type.MouseMove, 

197 QPointF(100, 100), 

198 Qt.MouseButton.NoButton, 

199 Qt.MouseButton.NoButton, 

200 Qt.KeyboardModifier.NoModifier 

201 ) 

202 view.mouseMoveEvent(event) 

203 

204 def test_mouse_move_without_parent_viewer(self, qtbot, graphics_scene): 

205 """Test mouse move without parent viewer.""" 

206 view = CrosshairGraphicsView(view_idx=0) 

207 qtbot.addWidget(view) 

208 view.setScene(graphics_scene) 

209 

210 # Should not crash 

211 event = QMouseEvent( 

212 QMouseEvent.Type.MouseMove, 

213 QPointF(100, 100), 

214 Qt.MouseButton.NoButton, 

215 Qt.MouseButton.NoButton, 

216 Qt.KeyboardModifier.NoModifier 

217 ) 

218 view.mouseMoveEvent(event) 

219 

220 def test_mouse_move_without_img_data(self, qtbot, graphics_scene, mock_parent_viewer): 

221 """Test mouse move without img_data.""" 

222 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

223 qtbot.addWidget(view) 

224 view.setScene(graphics_scene) 

225 

226 # Should not crash 

227 event = QMouseEvent( 

228 QMouseEvent.Type.MouseMove, 

229 QPointF(100, 100), 

230 Qt.MouseButton.NoButton, 

231 Qt.MouseButton.NoButton, 

232 Qt.KeyboardModifier.NoModifier 

233 ) 

234 view.mouseMoveEvent(event) 

235 

236 def test_mouse_move_out_of_bounds(self, qtbot, mock_parent_viewer, graphics_scene): 

237 """Test mouse move out of bounds.""" 

238 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

239 qtbot.addWidget(view) 

240 view.setScene(graphics_scene) 

241 view.setup_crosshairs() 

242 

243 with patch.object(view, 'update_crosshairs') as mock_update: 

244 # Coordinates out of bounds 

245 event = QMouseEvent( 

246 QMouseEvent.Type.MouseMove, 

247 QPointF(1000, 1000), 

248 Qt.MouseButton.NoButton, 

249 Qt.MouseButton.NoButton, 

250 Qt.KeyboardModifier.NoModifier 

251 ) 

252 view.mouseMoveEvent(event) 

253 

254 # update_crosshairs should not be called 

255 mock_update.assert_not_called() 

256 

257 def test_mouse_move_negative_coordinates(self, qtbot, mock_parent_viewer, graphics_scene): 

258 """Test mouse move with negative coordinates.""" 

259 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

260 qtbot.addWidget(view) 

261 view.setScene(graphics_scene) 

262 view.setup_crosshairs() 

263 

264 with patch.object(view, 'update_crosshairs') as mock_update: 

265 event = QMouseEvent( 

266 QMouseEvent.Type.MouseMove, 

267 QPointF(-10, -10), 

268 Qt.MouseButton.NoButton, 

269 Qt.MouseButton.NoButton, 

270 Qt.KeyboardModifier.NoModifier 

271 ) 

272 view.mouseMoveEvent(event) 

273 

274 # update_crosshairs should not be called 

275 mock_update.assert_not_called() 

276 

277 

278class TestMousePressEvent: 

279 """Tests for mousePressEvent.""" 

280 

281 def test_left_click_calls_handle_click_coordinates(self, qtbot, mock_parent_viewer, graphics_scene): 

282 """Test that left click calls handle_click_coordinates.""" 

283 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

284 qtbot.addWidget(view) 

285 view.setScene(graphics_scene) 

286 

287 event = QMouseEvent( 

288 QMouseEvent.Type.MouseButtonPress, 

289 QPointF(100, 150), 

290 Qt.MouseButton.LeftButton, 

291 Qt.MouseButton.LeftButton, 

292 Qt.KeyboardModifier.NoModifier 

293 ) 

294 view.mousePressEvent(event) 

295 

296 mock_parent_viewer.handle_click_coordinates.assert_called_once() 

297 args = mock_parent_viewer.handle_click_coordinates.call_args[0] 

298 assert args[0] == 0 # view_idx 

299 

300 def test_right_click_ignored(self, qtbot, mock_parent_viewer, graphics_scene): 

301 """Test that right click is ignored.""" 

302 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

303 qtbot.addWidget(view) 

304 view.setScene(graphics_scene) 

305 

306 event = QMouseEvent( 

307 QMouseEvent.Type.MouseButtonPress, 

308 QPointF(100, 150), 

309 Qt.MouseButton.RightButton, 

310 Qt.MouseButton.RightButton, 

311 Qt.KeyboardModifier.NoModifier 

312 ) 

313 view.mousePressEvent(event) 

314 

315 # handle_click_coordinates should not be called 

316 mock_parent_viewer.handle_click_coordinates.assert_not_called() 

317 

318 def test_middle_click_ignored(self, qtbot, mock_parent_viewer, graphics_scene): 

319 """Test that middle click is ignored.""" 

320 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

321 qtbot.addWidget(view) 

322 view.setScene(graphics_scene) 

323 

324 event = QMouseEvent( 

325 QMouseEvent.Type.MouseButtonPress, 

326 QPointF(100, 150), 

327 Qt.MouseButton.MiddleButton, 

328 Qt.MouseButton.MiddleButton, 

329 Qt.KeyboardModifier.NoModifier 

330 ) 

331 view.mousePressEvent(event) 

332 

333 mock_parent_viewer.handle_click_coordinates.assert_not_called() 

334 

335 def test_click_without_scene(self, qtbot, mock_parent_viewer): 

336 """Test click without scene.""" 

337 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

338 qtbot.addWidget(view) 

339 

340 event = QMouseEvent( 

341 QMouseEvent.Type.MouseButtonPress, 

342 QPointF(100, 150), 

343 Qt.MouseButton.LeftButton, 

344 Qt.MouseButton.LeftButton, 

345 Qt.KeyboardModifier.NoModifier 

346 ) 

347 view.mousePressEvent(event) 

348 

349 # Should not crash 

350 mock_parent_viewer.handle_click_coordinates.assert_not_called() 

351 

352 def test_click_without_parent_viewer(self, qtbot, graphics_scene): 

353 """Test click without parent viewer.""" 

354 view = CrosshairGraphicsView(view_idx=0) 

355 qtbot.addWidget(view) 

356 view.setScene(graphics_scene) 

357 

358 # Should not crash 

359 event = QMouseEvent( 

360 QMouseEvent.Type.MouseButtonPress, 

361 QPointF(100, 150), 

362 Qt.MouseButton.LeftButton, 

363 Qt.MouseButton.LeftButton, 

364 Qt.KeyboardModifier.NoModifier 

365 ) 

366 view.mousePressEvent(event) 

367 

368 def test_click_without_img_data(self, qtbot, graphics_scene, mock_parent_viewer): 

369 """Test click without img_data.""" 

370 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

371 qtbot.addWidget(view) 

372 view.setScene(graphics_scene) 

373 

374 event = QMouseEvent( 

375 QMouseEvent.Type.MouseButtonPress, 

376 QPointF(100, 150), 

377 Qt.MouseButton.LeftButton, 

378 Qt.MouseButton.LeftButton, 

379 Qt.KeyboardModifier.NoModifier 

380 ) 

381 view.mousePressEvent(event) 

382 

383 # Should not crash 

384 assert True 

385 

386 def test_click_out_of_bounds(self, qtbot, mock_parent_viewer, graphics_scene): 

387 """Test click out of bounds.""" 

388 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

389 qtbot.addWidget(view) 

390 view.setScene(graphics_scene) 

391 

392 event = QMouseEvent( 

393 QMouseEvent.Type.MouseButtonPress, 

394 QPointF(1000, 1000), 

395 Qt.MouseButton.LeftButton, 

396 Qt.MouseButton.LeftButton, 

397 Qt.KeyboardModifier.NoModifier 

398 ) 

399 view.mousePressEvent(event) 

400 

401 # handle_click_coordinates should not be called 

402 mock_parent_viewer.handle_click_coordinates.assert_not_called() 

403 

404 def test_click_at_scene_edges(self, qtbot, mock_parent_viewer, graphics_scene): 

405 """Test click at scene edges.""" 

406 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

407 qtbot.addWidget(view) 

408 view.setScene(graphics_scene) 

409 

410 # Scene corners 

411 corners = [ 

412 (0, 0), 

413 (511, 0), 

414 (0, 511), 

415 (511, 511) 

416 ] 

417 

418 for x, y in corners: 

419 mock_parent_viewer.handle_click_coordinates.reset_mock() 

420 

421 event = QMouseEvent( 

422 QMouseEvent.Type.MouseButtonPress, 

423 QPointF(x, y), 

424 Qt.MouseButton.LeftButton, 

425 Qt.MouseButton.LeftButton, 

426 Qt.KeyboardModifier.NoModifier 

427 ) 

428 view.mousePressEvent(event) 

429 

430 mock_parent_viewer.handle_click_coordinates.assert_called_once() 

431 

432 

433class TestUpdateCrosshairs: 

434 """Tests for update_crosshairs.""" 

435 

436 def test_update_crosshairs_basic(self, qtbot, graphics_scene): 

437 """Test basic update crosshairs.""" 

438 view = CrosshairGraphicsView(view_idx=0) 

439 qtbot.addWidget(view) 

440 view.setScene(graphics_scene) 

441 view.setup_crosshairs() 

442 

443 view.update_crosshairs(100, 200) 

444 

445 # Check updated lines 

446 line_h = view.crosshair_h.line() 

447 line_v = view.crosshair_v.line() 

448 

449 assert line_h.y1() == 200 

450 assert line_h.y2() == 200 

451 assert line_v.x1() == 100 

452 assert line_v.x2() == 100 

453 

454 assert view.crosshair_visible 

455 assert view.crosshair_h.isVisible() 

456 assert view.crosshair_v.isVisible() 

457 

458 def test_update_crosshairs_different_positions(self, qtbot, graphics_scene): 

459 """Test update with different positions.""" 

460 view = CrosshairGraphicsView(view_idx=0) 

461 qtbot.addWidget(view) 

462 view.setScene(graphics_scene) 

463 view.setup_crosshairs() 

464 

465 positions = [(0, 0), (256, 256), (511, 511), (100, 400)] 

466 

467 for x, y in positions: 

468 view.update_crosshairs(x, y) 

469 

470 line_h = view.crosshair_h.line() 

471 line_v = view.crosshair_v.line() 

472 

473 assert line_h.y1() == y 

474 assert line_v.x1() == x 

475 

476 def test_update_crosshairs_without_scene(self, qtbot): 

477 """Test update crosshairs without scene.""" 

478 view = CrosshairGraphicsView(view_idx=0) 

479 qtbot.addWidget(view) 

480 

481 # Should not crash 

482 view.update_crosshairs(100, 200) 

483 

484 def test_update_crosshairs_without_setup(self, qtbot, graphics_scene): 

485 """Test update crosshairs without setup.""" 

486 view = CrosshairGraphicsView(view_idx=0) 

487 qtbot.addWidget(view) 

488 view.setScene(graphics_scene) 

489 

490 # Should not crash (crosshair_h and crosshair_v are None) 

491 view.update_crosshairs(100, 200) 

492 

493 def test_update_crosshairs_makes_visible(self, qtbot, graphics_scene): 

494 """Test that update makes crosshairs visible.""" 

495 view = CrosshairGraphicsView(view_idx=0) 

496 qtbot.addWidget(view) 

497 view.setScene(graphics_scene) 

498 view.setup_crosshairs() 

499 

500 assert not view.crosshair_visible 

501 assert not view.crosshair_h.isVisible() 

502 

503 view.update_crosshairs(100, 200) 

504 

505 assert view.crosshair_visible 

506 assert view.crosshair_h.isVisible() 

507 assert view.crosshair_v.isVisible() 

508 

509 def test_update_crosshairs_spans_full_scene(self, qtbot, graphics_scene): 

510 """Test that crosshairs span the full scene.""" 

511 view = CrosshairGraphicsView(view_idx=0) 

512 qtbot.addWidget(view) 

513 view.setScene(graphics_scene) 

514 view.setup_crosshairs() 

515 

516 view.update_crosshairs(256, 256) 

517 

518 line_h = view.crosshair_h.line() 

519 line_v = view.crosshair_v.line() 

520 

521 # Horizontal line: from 0 to width 

522 assert line_h.x1() == 0 

523 assert line_h.x2() == 512 

524 

525 # Vertical line: from 0 to height 

526 assert line_v.y1() == 0 

527 assert line_v.y2() == 512 

528 

529 

530class TestSetCrosshairPosition: 

531 """Tests for set_crosshair_position.""" 

532 

533 def test_set_crosshair_position_basic(self, qtbot, graphics_scene): 

534 """Test basic set crosshair position.""" 

535 view = CrosshairGraphicsView(view_idx=0) 

536 qtbot.addWidget(view) 

537 view.setScene(graphics_scene) 

538 view.setup_crosshairs() 

539 

540 view.set_crosshair_position(150, 250) 

541 

542 line_h = view.crosshair_h.line() 

543 line_v = view.crosshair_v.line() 

544 

545 assert line_h.y1() == 250 

546 assert line_v.x1() == 150 

547 assert view.crosshair_visible 

548 assert view.crosshair_h.isVisible() 

549 assert view.crosshair_v.isVisible() 

550 

551 def test_set_crosshair_position_out_of_bounds(self, qtbot, graphics_scene): 

552 """Test set position out of bounds.""" 

553 view = CrosshairGraphicsView(view_idx=0) 

554 qtbot.addWidget(view) 

555 view.setScene(graphics_scene) 

556 view.setup_crosshairs() 

557 

558 # Out of bounds coordinates should not update 

559 view.set_crosshair_position(1000, 1000) 

560 

561 # Crosshairs should not be visible if they were hidden 

562 # (depends on implementation, might not update) 

563 

564 def test_set_crosshair_position_without_scene(self, qtbot): 

565 """Test set position without scene.""" 

566 view = CrosshairGraphicsView(view_idx=0) 

567 qtbot.addWidget(view) 

568 

569 # Should not crash 

570 view.set_crosshair_position(100, 200) 

571 

572 def test_set_crosshair_position_without_setup(self, qtbot, graphics_scene): 

573 """Test set position without setup.""" 

574 view = CrosshairGraphicsView(view_idx=0) 

575 qtbot.addWidget(view) 

576 view.setScene(graphics_scene) 

577 

578 # Should not crash 

579 view.set_crosshair_position(100, 200) 

580 

581 def test_set_crosshair_position_at_origin(self, qtbot, graphics_scene): 

582 """Test set position at origin.""" 

583 view = CrosshairGraphicsView(view_idx=0) 

584 qtbot.addWidget(view) 

585 view.setScene(graphics_scene) 

586 view.setup_crosshairs() 

587 

588 view.set_crosshair_position(0, 0) 

589 

590 line_h = view.crosshair_h.line() 

591 line_v = view.crosshair_v.line() 

592 

593 assert line_h.y1() == 0 

594 assert line_v.x1() == 0 

595 

596 def test_set_crosshair_position_at_max(self, qtbot, graphics_scene): 

597 """Test set position at max.""" 

598 view = CrosshairGraphicsView(view_idx=0) 

599 qtbot.addWidget(view) 

600 view.setScene(graphics_scene) 

601 view.setup_crosshairs() 

602 

603 view.set_crosshair_position(511, 511) 

604 

605 line_h = view.crosshair_h.line() 

606 line_v = view.crosshair_v.line() 

607 

608 assert line_h.y1() == 511 

609 assert line_v.x1() == 511 

610 

611 

612class TestLeaveEvent: 

613 """Tests for leaveEvent.""" 

614 

615 def test_leave_event_calls_update_cross_view_lines(self, qtbot, mock_parent_viewer, graphics_scene): 

616 """Test that leaveEvent calls update_cross_view_lines.""" 

617 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

618 qtbot.addWidget(view) 

619 view.setScene(graphics_scene) 

620 view.setup_crosshairs() 

621 

622 event = QEvent(QEvent.Type.Leave) 

623 view.leaveEvent(event) 

624 

625 mock_parent_viewer.update_cross_view_lines.assert_called_once() 

626 

627 def test_leave_event_without_crosshairs(self, qtbot, mock_parent_viewer): 

628 """Test leaveEvent without crosshairs.""" 

629 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

630 qtbot.addWidget(view) 

631 

632 # Should not crash 

633 event = QEvent(QEvent.Type.Leave) 

634 view.leaveEvent(event) 

635 

636 def test_leave_event_without_parent_viewer(self, qtbot, graphics_scene): 

637 """Test leaveEvent without parent viewer.""" 

638 view = CrosshairGraphicsView(view_idx=0) 

639 qtbot.addWidget(view) 

640 view.setScene(graphics_scene) 

641 view.setup_crosshairs() 

642 

643 # Should not crash 

644 event = Mock() 

645 # Might fail if parent_viewer is None 

646 try: 

647 view.leaveEvent(event) 

648 except AttributeError: 

649 pass # Acceptable if parent_viewer is None 

650 

651 

652class TestSignals: 

653 """Tests for signals.""" 

654 

655 def test_coordinate_changed_signal_exists(self, qtbot): 

656 """Test that the coordinate_changed signal exists.""" 

657 view = CrosshairGraphicsView(view_idx=0) 

658 qtbot.addWidget(view) 

659 

660 assert hasattr(view, 'coordinate_changed') 

661 

662 def test_coordinate_changed_signal_parameters(self, qtbot, mock_parent_viewer, graphics_scene): 

663 """Test coordinate_changed signal parameters.""" 

664 view = CrosshairGraphicsView(view_idx=2, parent=mock_parent_viewer) 

665 qtbot.addWidget(view) 

666 view.setScene(graphics_scene) 

667 view.setup_crosshairs() 

668 

669 with qtbot.waitSignal(view.coordinate_changed) as blocker: 

670 event = QMouseEvent( 

671 QMouseEvent.Type.MouseMove, 

672 QPointF(123, 456), 

673 Qt.MouseButton.NoButton, 

674 Qt.MouseButton.NoButton, 

675 Qt.KeyboardModifier.NoModifier 

676 ) 

677 view.mouseMoveEvent(event) 

678 

679 args = blocker.args 

680 assert len(args) == 3 

681 assert args[0] == 2 # view_idx 

682 # args[1] and args[2] are x, y (might vary due to mapToScene) 

683 

684 

685class TestEdgeCases: 

686 """Tests for edge cases.""" 

687 

688 def test_very_small_scene(self, qtbot): 

689 """Test with very small scene.""" 

690 view = CrosshairGraphicsView(view_idx=0) 

691 qtbot.addWidget(view) 

692 

693 small_scene = QGraphicsScene() 

694 small_scene.setSceneRect(0, 0, 10, 10) 

695 view.setScene(small_scene) 

696 view.setup_crosshairs() 

697 

698 view.update_crosshairs(5, 5) 

699 

700 # Should not crash 

701 assert view.crosshair_visible 

702 

703 def test_very_large_scene(self, qtbot): 

704 """Test with very large scene.""" 

705 view = CrosshairGraphicsView(view_idx=0) 

706 qtbot.addWidget(view) 

707 

708 large_scene = QGraphicsScene() 

709 large_scene.setSceneRect(0, 0, 10000, 10000) 

710 view.setScene(large_scene) 

711 view.setup_crosshairs() 

712 

713 view.update_crosshairs(5000, 5000) 

714 

715 assert view.crosshair_visible 

716 

717 def test_rectangular_scene(self, qtbot): 

718 """Test with rectangular scene.""" 

719 view = CrosshairGraphicsView(view_idx=0) 

720 qtbot.addWidget(view) 

721 

722 rect_scene = QGraphicsScene() 

723 rect_scene.setSceneRect(0, 0, 800, 400) 

724 view.setScene(rect_scene) 

725 view.setup_crosshairs() 

726 

727 view.update_crosshairs(400, 200) 

728 

729 line_h = view.crosshair_h.line() 

730 line_v = view.crosshair_v.line() 

731 

732 assert line_h.x2() == 800 

733 assert line_v.y2() == 400 

734 

735 def test_rapid_mouse_movements(self, qtbot, mock_parent_viewer, graphics_scene): 

736 """Test rapid mouse movements.""" 

737 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

738 qtbot.addWidget(view) 

739 view.setScene(graphics_scene) 

740 view.setup_crosshairs() 

741 

742 # Simulate 100 rapid movements 

743 for i in range(100): 

744 x = (i * 5) % 512 

745 y = (i * 3) % 512 

746 event = QMouseEvent( 

747 QMouseEvent.Type.MouseMove, 

748 QPointF(x, y), 

749 Qt.MouseButton.NoButton, 

750 Qt.MouseButton.NoButton, 

751 Qt.KeyboardModifier.NoModifier 

752 ) 

753 view.mouseMoveEvent(event) 

754 

755 # Should not crash 

756 assert view.crosshair_visible 

757 

758 def test_rapid_clicks(self, qtbot, mock_parent_viewer, graphics_scene): 

759 """Test rapid clicks.""" 

760 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

761 qtbot.addWidget(view) 

762 view.setScene(graphics_scene) 

763 

764 for i in range(50): 

765 x = (i * 10) % 512 

766 y = (i * 7) % 512 

767 event = QMouseEvent( 

768 QMouseEvent.Type.MouseButtonPress, 

769 QPointF(x, y), 

770 Qt.MouseButton.LeftButton, 

771 Qt.MouseButton.LeftButton, 

772 Qt.KeyboardModifier.NoModifier 

773 ) 

774 view.mousePressEvent(event) 

775 

776 # handle_click_coordinates should have been called 50 times 

777 assert mock_parent_viewer.handle_click_coordinates.call_count == 50 

778 

779 def test_scene_change(self, qtbot): 

780 """Test scene change.""" 

781 view = CrosshairGraphicsView(view_idx=0) 

782 qtbot.addWidget(view) 

783 

784 # First scene 

785 scene1 = QGraphicsScene() 

786 scene1.setSceneRect(0, 0, 256, 256) 

787 view.setScene(scene1) 

788 view.setup_crosshairs() 

789 

790 crosshair_h1 = view.crosshair_h 

791 

792 # Second scene 

793 scene2 = QGraphicsScene() 

794 scene2.setSceneRect(0, 0, 512, 512) 

795 view.setScene(scene2) 

796 view.setup_crosshairs() 

797 

798 # Crosshairs should be new 

799 assert view.crosshair_h is not crosshair_h1 

800 

801 def test_coordinate_precision(self, qtbot, mock_parent_viewer, graphics_scene): 

802 """Test coordinate precision with float.""" 

803 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

804 qtbot.addWidget(view) 

805 view.setScene(graphics_scene) 

806 

807 # Float coordinates should be converted to int 

808 event = QMouseEvent( 

809 QMouseEvent.Type.MouseButtonPress, 

810 QPointF(100.7, 200.3), 

811 Qt.MouseButton.LeftButton, 

812 Qt.MouseButton.LeftButton, 

813 Qt.KeyboardModifier.NoModifier 

814 ) 

815 view.mousePressEvent(event) 

816 

817 # Check that they were passed as int 

818 args = mock_parent_viewer.handle_click_coordinates.call_args[0] 

819 assert isinstance(args[1], int) 

820 assert isinstance(args[2], int) 

821 

822 

823class TestIntegration: 

824 """Integration tests.""" 

825 

826 def test_full_interaction_flow(self, qtbot, mock_parent_viewer, graphics_scene): 

827 """Test full flow: setup -> mouse move -> click.""" 

828 view = CrosshairGraphicsView(view_idx=1, parent=mock_parent_viewer) 

829 qtbot.addWidget(view) 

830 view.setScene(graphics_scene) 

831 view.setup_crosshairs() 

832 

833 # Mouse move 

834 with qtbot.waitSignal(view.coordinate_changed): 

835 move_event = QMouseEvent( 

836 QMouseEvent.Type.MouseMove, 

837 QPointF(150, 200), 

838 Qt.MouseButton.NoButton, 

839 Qt.MouseButton.NoButton, 

840 Qt.KeyboardModifier.NoModifier 

841 ) 

842 view.mouseMoveEvent(move_event) 

843 

844 assert view.crosshair_visible 

845 

846 # Click 

847 click_event = QMouseEvent( 

848 QMouseEvent.Type.MouseButtonPress, 

849 QPointF(150, 200), 

850 Qt.MouseButton.LeftButton, 

851 Qt.MouseButton.LeftButton, 

852 Qt.KeyboardModifier.NoModifier 

853 ) 

854 view.mousePressEvent(click_event) 

855 

856 mock_parent_viewer.handle_click_coordinates.assert_called_once() 

857 

858 def test_multiple_views_interaction(self, qtbot, mock_parent_viewer): 

859 """Test interaction between multiple views.""" 

860 scene1 = QGraphicsScene() 

861 scene1.setSceneRect(0, 0, 512, 512) 

862 

863 scene2 = QGraphicsScene() 

864 scene2.setSceneRect(0, 0, 512, 512) 

865 

866 view1 = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

867 view2 = CrosshairGraphicsView(view_idx=1, parent=mock_parent_viewer) 

868 

869 qtbot.addWidget(view1) 

870 qtbot.addWidget(view2) 

871 

872 view1.setScene(scene1) 

873 view2.setScene(scene2) 

874 

875 view1.setup_crosshairs() 

876 view2.setup_crosshairs() 

877 

878 # Interaction on view1 

879 event = QMouseEvent( 

880 QMouseEvent.Type.MouseButtonPress, 

881 QPointF(100, 100), 

882 Qt.MouseButton.LeftButton, 

883 Qt.MouseButton.LeftButton, 

884 Qt.KeyboardModifier.NoModifier 

885 ) 

886 view1.mousePressEvent(event) 

887 

888 # Check correct view_idx 

889 args1 = mock_parent_viewer.handle_click_coordinates.call_args[0] 

890 assert args1[0] == 0 

891 

892 # Interaction on view2 

893 mock_parent_viewer.handle_click_coordinates.reset_mock() 

894 view2.mousePressEvent(event) 

895 

896 args2 = mock_parent_viewer.handle_click_coordinates.call_args[0] 

897 assert args2[0] == 1 

898 

899 def test_crosshair_synchronization(self, qtbot, graphics_scene): 

900 """Test crosshair synchronization.""" 

901 view = CrosshairGraphicsView(view_idx=0) 

902 qtbot.addWidget(view) 

903 view.setScene(graphics_scene) 

904 view.setup_crosshairs() 

905 

906 # Update via update_crosshairs 

907 view.update_crosshairs(100, 200) 

908 

909 line_h1 = view.crosshair_h.line() 

910 assert line_h1.y1() == 200 

911 

912 # Update via set_crosshair_position 

913 view.set_crosshair_position(150, 250) 

914 

915 line_h2 = view.crosshair_h.line() 

916 assert line_h2.y1() == 250 

917 

918 

919class TestBoundaryConditions: 

920 """Tests for boundary conditions.""" 

921 

922 def test_click_at_zero_zero(self, qtbot, mock_parent_viewer, graphics_scene): 

923 """Test click at (0, 0).""" 

924 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

925 qtbot.addWidget(view) 

926 view.setScene(graphics_scene) 

927 

928 event = QMouseEvent( 

929 QMouseEvent.Type.MouseButtonPress, 

930 QPointF(0, 0), 

931 Qt.MouseButton.LeftButton, 

932 Qt.MouseButton.LeftButton, 

933 Qt.KeyboardModifier.NoModifier 

934 ) 

935 view.mousePressEvent(event) 

936 

937 mock_parent_viewer.handle_click_coordinates.assert_called_once() 

938 

939 def test_click_at_max_coordinates(self, qtbot, mock_parent_viewer, graphics_scene): 

940 """Test click at maximum coordinates.""" 

941 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

942 qtbot.addWidget(view) 

943 view.setScene(graphics_scene) 

944 

945 # Scene is 512x512, so valid max is 511,511 

946 event = QMouseEvent( 

947 QMouseEvent.Type.MouseButtonPress, 

948 QPointF(511, 511), 

949 Qt.MouseButton.LeftButton, 

950 Qt.MouseButton.LeftButton, 

951 Qt.KeyboardModifier.NoModifier 

952 ) 

953 view.mousePressEvent(event) 

954 

955 mock_parent_viewer.handle_click_coordinates.assert_called_once() 

956 

957 def test_click_just_outside_bounds(self, qtbot, mock_parent_viewer, graphics_scene): 

958 """Test click just outside bounds.""" 

959 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

960 qtbot.addWidget(view) 

961 view.setScene(graphics_scene) 

962 

963 # 512 is outside (scene is 0-511) 

964 event = QMouseEvent( 

965 QMouseEvent.Type.MouseButtonPress, 

966 QPointF(512, 512), 

967 Qt.MouseButton.LeftButton, 

968 Qt.MouseButton.LeftButton, 

969 Qt.KeyboardModifier.NoModifier 

970 ) 

971 view.mousePressEvent(event) 

972 

973 # Should not call handle_click_coordinates 

974 mock_parent_viewer.handle_click_coordinates.assert_not_called() 

975 

976 

977class TestViewIndexHandling: 

978 """Tests for view_idx handling.""" 

979 

980 def test_different_view_indices_in_signals(self, qtbot, mock_parent_viewer, graphics_scene): 

981 """Test that view_idx is correct in signals.""" 

982 indices = [0, 1, 2, 5, 10] 

983 

984 for idx in indices: 

985 view = CrosshairGraphicsView(view_idx=idx, parent=mock_parent_viewer) 

986 qtbot.addWidget(view) 

987 view.setScene(graphics_scene) 

988 

989 with qtbot.waitSignal(view.coordinate_changed) as blocker: 

990 event = QMouseEvent( 

991 QMouseEvent.Type.MouseMove, 

992 QPointF(100, 100), 

993 Qt.MouseButton.NoButton, 

994 Qt.MouseButton.NoButton, 

995 Qt.KeyboardModifier.NoModifier 

996 ) 

997 view.mouseMoveEvent(event) 

998 

999 assert blocker.args[0] == idx 

1000 

1001 def test_view_idx_persists(self, qtbot): 

1002 """Test that view_idx persists.""" 

1003 view = CrosshairGraphicsView(view_idx=7) 

1004 qtbot.addWidget(view) 

1005 

1006 assert view.view_idx == 7 

1007 

1008 # After various operations 

1009 scene = QGraphicsScene() 

1010 view.setScene(scene) 

1011 view.setup_crosshairs() 

1012 

1013 assert view.view_idx == 7 

1014 

1015 

1016class TestMemoryAndCleanup: 

1017 """Tests for memory and cleanup.""" 

1018 

1019 def test_scene_items_cleanup(self, qtbot, graphics_scene): 

1020 """Test cleanup of scene items.""" 

1021 view = CrosshairGraphicsView(view_idx=0) 

1022 qtbot.addWidget(view) 

1023 view.setScene(graphics_scene) 

1024 

1025 initial_items = len(graphics_scene.items()) 

1026 

1027 view.setup_crosshairs() 

1028 

1029 # 2 items (lines) should have been added 

1030 assert len(graphics_scene.items()) == initial_items + 2 

1031 

1032 def test_multiple_setup_doesnt_leak(self, qtbot, graphics_scene): 

1033 """Test that multiple setups do not cause memory leaks.""" 

1034 view = CrosshairGraphicsView(view_idx=0) 

1035 qtbot.addWidget(view) 

1036 view.setScene(graphics_scene) 

1037 

1038 # Multiple setups 

1039 for _ in range(10): 

1040 view.setup_crosshairs() 

1041 

1042 # Old items should have been removed/replaced 

1043 # (depends on implementation) 

1044 assert True 

1045 

1046 

1047class TestRenderHints: 

1048 """Tests for render hints.""" 

1049 

1050 def test_antialiasing_enabled(self, qtbot): 

1051 """Test that antialiasing is enabled.""" 

1052 view = CrosshairGraphicsView(view_idx=0) 

1053 qtbot.addWidget(view) 

1054 

1055 hints = view.renderHints() 

1056 assert hints & QPainter.RenderHint.Antialiasing 

1057 

1058 def test_smooth_pixmap_transform_enabled(self, qtbot): 

1059 """Test that smooth pixmap transform is enabled.""" 

1060 view = CrosshairGraphicsView(view_idx=0) 

1061 qtbot.addWidget(view) 

1062 

1063 hints = view.renderHints() 

1064 assert hints & QPainter.RenderHint.SmoothPixmapTransform 

1065 

1066 def test_render_hints_persist(self, qtbot, graphics_scene): 

1067 """Test that render hints persist after operations.""" 

1068 view = CrosshairGraphicsView(view_idx=0) 

1069 qtbot.addWidget(view) 

1070 

1071 view.setScene(graphics_scene) 

1072 view.setup_crosshairs() 

1073 

1074 hints = view.renderHints() 

1075 assert hints & QPainter.RenderHint.Antialiasing 

1076 assert hints & QPainter.RenderHint.SmoothPixmapTransform 

1077 

1078 

1079class TestErrorHandling: 

1080 """Tests for error handling.""" 

1081 

1082 def test_mapToScene_with_invalid_point(self, qtbot, mock_parent_viewer, graphics_scene): 

1083 """Test mapToScene with invalid point.""" 

1084 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

1085 qtbot.addWidget(view) 

1086 view.setScene(graphics_scene) 

1087 

1088 # Point far out of bounds 

1089 event = QMouseEvent( 

1090 QMouseEvent.Type.MouseMove, 

1091 QPointF(999999, 999999), 

1092 Qt.MouseButton.NoButton, 

1093 Qt.MouseButton.NoButton, 

1094 Qt.KeyboardModifier.NoModifier 

1095 ) 

1096 

1097 # Should not crash 

1098 view.mouseMoveEvent(event) 

1099 

1100 def test_operations_without_initialization(self, qtbot): 

1101 """Test operations without complete initialization.""" 

1102 view = CrosshairGraphicsView(view_idx=0) 

1103 qtbot.addWidget(view) 

1104 

1105 # Various operations without scene/crosshair setup 

1106 view.update_crosshairs(100, 100) 

1107 view.set_crosshair_position(100, 100) 

1108 

1109 event = QMouseEvent( 

1110 QMouseEvent.Type.MouseMove, 

1111 QPointF(100, 100), 

1112 Qt.MouseButton.NoButton, 

1113 Qt.MouseButton.NoButton, 

1114 Qt.KeyboardModifier.NoModifier 

1115 ) 

1116 view.mouseMoveEvent(event) 

1117 

1118 # Should not crash 

1119 assert True 

1120 

1121 

1122class TestStateConsistency: 

1123 """Tests for state consistency.""" 

1124 

1125 def test_crosshair_visible_state_consistency(self, qtbot, graphics_scene): 

1126 """Test crosshair_visible state consistency.""" 

1127 view = CrosshairGraphicsView(view_idx=0) 

1128 qtbot.addWidget(view) 

1129 view.setScene(graphics_scene) 

1130 view.setup_crosshairs() 

1131 

1132 # Initially not visible 

1133 assert not view.crosshair_visible 

1134 assert not view.crosshair_h.isVisible() 

1135 

1136 # After update, visible 

1137 view.update_crosshairs(100, 100) 

1138 assert view.crosshair_visible 

1139 assert view.crosshair_h.isVisible() 

1140 assert view.crosshair_v.isVisible() 

1141 

1142 def test_crosshair_position_consistency(self, qtbot, graphics_scene): 

1143 """Test crosshair position consistency.""" 

1144 view = CrosshairGraphicsView(view_idx=0) 

1145 qtbot.addWidget(view) 

1146 view.setScene(graphics_scene) 

1147 view.setup_crosshairs() 

1148 

1149 # Set position 

1150 view.update_crosshairs(123, 456) 

1151 

1152 line_h = view.crosshair_h.line() 

1153 line_v = view.crosshair_v.line() 

1154 

1155 # Check consistency 

1156 assert line_h.y1() == line_h.y2() == 456 

1157 assert line_v.x1() == line_v.x2() == 123 

1158 

1159 

1160class TestDocumentation: 

1161 """Tests for documentation.""" 

1162 

1163 def test_class_docstring(self): 

1164 """Test that the class has a docstring.""" 

1165 assert CrosshairGraphicsView.__doc__ is not None 

1166 assert "crosshair" in CrosshairGraphicsView.__doc__.lower() 

1167 

1168 def test_setup_crosshairs_docstring(self, qtbot): 

1169 """Test that setup_crosshairs has a docstring.""" 

1170 view = CrosshairGraphicsView(view_idx=0) 

1171 qtbot.addWidget(view) 

1172 

1173 assert view.setup_crosshairs.__doc__ is not None 

1174 

1175 

1176class TestAccessibility: 

1177 """Tests for accessibility.""" 

1178 

1179 def test_crosshair_color_visible(self, qtbot, graphics_scene): 

1180 """Test that crosshair color is visible.""" 

1181 view = CrosshairGraphicsView(view_idx=0) 

1182 qtbot.addWidget(view) 

1183 view.setScene(graphics_scene) 

1184 view.setup_crosshairs() 

1185 

1186 pen = view.crosshair_h.pen() 

1187 color = pen.color() 

1188 

1189 # Yellow with alpha should be visible 

1190 assert color.alpha() == 180 

1191 assert color.red() == 255 

1192 assert color.green() == 255 

1193 assert color.blue() == 0 

1194 

1195 def test_crosshair_width_appropriate(self, qtbot, graphics_scene): 

1196 """Test that crosshair width is appropriate.""" 

1197 view = CrosshairGraphicsView(view_idx=0) 

1198 qtbot.addWidget(view) 

1199 view.setScene(graphics_scene) 

1200 view.setup_crosshairs() 

1201 

1202 pen_h = view.crosshair_h.pen() 

1203 pen_v = view.crosshair_v.pen() 

1204 

1205 # Width 1 should be visible but not intrusive 

1206 assert pen_h.width() == 1 

1207 assert pen_v.width() == 1 

1208 

1209 

1210class TestPerformance: 

1211 """Tests for performance.""" 

1212 

1213 def test_many_coordinate_updates(self, qtbot, mock_parent_viewer, graphics_scene): 

1214 """Test with many coordinate updates.""" 

1215 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

1216 qtbot.addWidget(view) 

1217 view.setScene(graphics_scene) 

1218 view.setup_crosshairs() 

1219 

1220 # 1000 updates 

1221 for i in range(1000): 

1222 view.update_crosshairs(i % 512, (i * 2) % 512) 

1223 

1224 # Should not cause performance issues 

1225 assert view.crosshair_visible 

1226 

1227 def test_signal_emission_performance(self, qtbot, mock_parent_viewer, graphics_scene): 

1228 """Test signal emission performance.""" 

1229 view = CrosshairGraphicsView(view_idx=0, parent=mock_parent_viewer) 

1230 qtbot.addWidget(view) 

1231 view.setScene(graphics_scene) 

1232 

1233 signal_count = 0 

1234 

1235 def count_signal(*args): 

1236 nonlocal signal_count 

1237 signal_count += 1 

1238 

1239 view.coordinate_changed.connect(count_signal) 

1240 

1241 # Many mouse events 

1242 for i in range(100): 

1243 event = QMouseEvent( 

1244 QMouseEvent.Type.MouseMove, 

1245 QPointF(i % 512, (i * 2) % 512), 

1246 Qt.MouseButton.NoButton, 

1247 Qt.MouseButton.NoButton, 

1248 Qt.KeyboardModifier.NoModifier 

1249 ) 

1250 view.mouseMoveEvent(event) 

1251 

1252 # Signal should have been emitted many times 

1253 assert signal_count > 0