|
34 | 34 | CAT_MODEL_CHANGE_ASSET_PATH = PROJECT_ROOT / "static" / "assets" / "neko-idle" / "cat_model_change.gif" |
35 | 35 |
|
36 | 36 |
|
| 37 | +def _source_slice_between(source, start_marker, end_marker, block_name): |
| 38 | + start = source.find(start_marker) |
| 39 | + assert start != -1, f"{block_name} start marker not found: {start_marker}" |
| 40 | + end = source.find(end_marker, start + len(start_marker)) |
| 41 | + assert end != -1, f"{block_name} end marker not found after start: {end_marker}" |
| 42 | + assert start < end, f"{block_name} start marker must precede end marker" |
| 43 | + return source[start:end] |
| 44 | + |
| 45 | + |
| 46 | +def _assert_source_contains(block, expected, block_name): |
| 47 | + assert expected in block, f"{block_name} missing expected source: {expected}" |
| 48 | + |
| 49 | + |
| 50 | +def _assert_source_order(block, block_name, *expected_markers): |
| 51 | + positions = [] |
| 52 | + for marker in expected_markers: |
| 53 | + position = block.find(marker) |
| 54 | + assert position != -1, f"{block_name} missing expected source: {marker}" |
| 55 | + positions.append(position) |
| 56 | + assert positions == sorted(positions), f"{block_name} expected order: {' -> '.join(expected_markers)}" |
| 57 | + |
| 58 | + |
37 | 59 | def test_return_button_idle_tier_assets_are_mapped_in_source(): |
38 | 60 | source = AVATAR_UI_BUTTONS_PATH.read_text(encoding="utf-8") |
39 | 61 | app_ui_source = APP_UI_PATH.read_text(encoding="utf-8") |
@@ -497,6 +519,139 @@ def test_cat1_walk_to_minimized_chat_contract_is_present(): |
497 | 519 | assert "if (!window.__NEKO_MULTI_WINDOW__)" in source |
498 | 520 |
|
499 | 521 |
|
| 522 | +def test_cat1_walk_is_blocked_while_return_ball_drag_is_active_or_pending(): |
| 523 | + source = AVATAR_UI_BUTTONS_PATH.read_text(encoding="utf-8") |
| 524 | + |
| 525 | + drag_setup = _source_slice_between( |
| 526 | + source, |
| 527 | + "ManagerPrototype._setupReturnButtonDrag = function(container) {", |
| 528 | + "container.addEventListener('mousedown'", |
| 529 | + "return button drag setup", |
| 530 | + ) |
| 531 | + handle_start = _source_slice_between( |
| 532 | + source, |
| 533 | + "const handleStart = (clientX, clientY) => {", |
| 534 | + "const handleMove = (clientX, clientY) => {", |
| 535 | + "return button drag start handler", |
| 536 | + ) |
| 537 | + handle_end = _source_slice_between( |
| 538 | + source, |
| 539 | + "const handleEnd = () => {", |
| 540 | + "container.addEventListener('mousedown'", |
| 541 | + "return button drag end handler", |
| 542 | + ) |
| 543 | + |
| 544 | + for expected in ( |
| 545 | + "let dragSafetyTimer = 0;", |
| 546 | + "let dragSafetyToken = 0;", |
| 547 | + "const clearDragSafetyTimer = () => {", |
| 548 | + "const resetDragStateAfterMissingEnd = (safetyToken) => {", |
| 549 | + "if (dragSafetyToken !== safetyToken || !isDragging) return;", |
| 550 | + "const finishDragState = (moved) => {", |
| 551 | + "container.setAttribute('data-dragging', 'false');", |
| 552 | + "_dispatchNekoIdleReturnBallManualMove(container, 'return-ball-drag-end'", |
| 553 | + ): |
| 554 | + _assert_source_contains(drag_setup, expected, "return button drag setup") |
| 555 | + _assert_source_order( |
| 556 | + drag_setup, |
| 557 | + "return button drag setup helpers", |
| 558 | + "const finishDragState = (moved) => {", |
| 559 | + "const resetDragStateAfterMissingEnd = (safetyToken) => {", |
| 560 | + "finishDragState(moved);", |
| 561 | + ) |
| 562 | + _assert_source_contains( |
| 563 | + handle_start, |
| 564 | + "container.setAttribute('data-dragging', 'pending')", |
| 565 | + "return button drag start handler", |
| 566 | + ) |
| 567 | + _assert_source_contains(handle_start, "const safetyToken = dragSafetyToken + 1", "return button drag start handler") |
| 568 | + _assert_source_contains(handle_start, "dragSafetyTimer = setTimeout(() => {", "return button drag start handler") |
| 569 | + _assert_source_contains( |
| 570 | + handle_start, |
| 571 | + "resetDragStateAfterMissingEnd(safetyToken);", |
| 572 | + "return button drag start handler", |
| 573 | + ) |
| 574 | + _assert_source_contains(handle_start, "}, 5000);", "return button drag start handler") |
| 575 | + _assert_source_order( |
| 576 | + handle_start, |
| 577 | + "return button drag start handler", |
| 578 | + "clearDragSafetyTimer();", |
| 579 | + "container.setAttribute('data-dragging', 'pending')", |
| 580 | + "dragSafetyTimer = setTimeout(() => {", |
| 581 | + ) |
| 582 | + _assert_source_contains(handle_end, "clearDragSafetyTimer();", "return button drag end handler") |
| 583 | + _assert_source_contains( |
| 584 | + handle_end, |
| 585 | + "finishDragState(moved);", |
| 586 | + "return button drag end handler", |
| 587 | + ) |
| 588 | + _assert_source_order( |
| 589 | + handle_end, |
| 590 | + "return button drag end handler", |
| 591 | + "clearDragSafetyTimer();", |
| 592 | + "if (isDragging) {", |
| 593 | + "finishDragState(moved);", |
| 594 | + ) |
| 595 | + |
| 596 | + sync_block = _source_slice_between( |
| 597 | + source, |
| 598 | + "function _syncNekoIdleCat1Journey", |
| 599 | + "function _scheduleNekoIdleCat1JourneySync(button)", |
| 600 | + "cat1 journey sync", |
| 601 | + ) |
| 602 | + for expected in ( |
| 603 | + "const initialContainer = _getNekoIdleReturnContainerFromButton(button)", |
| 604 | + "const initialDragging = initialContainer && initialContainer.getAttribute('data-dragging')", |
| 605 | + "if (initialDragging && initialDragging !== 'false') return", |
| 606 | + ): |
| 607 | + _assert_source_contains(sync_block, expected, "cat1 journey sync") |
| 608 | + _assert_source_order( |
| 609 | + sync_block, |
| 610 | + "cat1 journey sync drag guard", |
| 611 | + "if (_isNekoIdleCompactSurfaceDragging()) return", |
| 612 | + "if (initialDragging && initialDragging !== 'false') return", |
| 613 | + "const normalizedTier", |
| 614 | + ) |
| 615 | + |
| 616 | + container_observer = _source_slice_between( |
| 617 | + source, |
| 618 | + "state.containerObserver = new MutationObserver(() => {", |
| 619 | + "state.containerObserver.observe(container", |
| 620 | + "cat1 container observer", |
| 621 | + ) |
| 622 | + _assert_source_contains( |
| 623 | + container_observer, |
| 624 | + "const observerDragging = container.getAttribute('data-dragging');", |
| 625 | + "cat1 container observer", |
| 626 | + ) |
| 627 | + _assert_source_contains( |
| 628 | + container_observer, |
| 629 | + "if (observerDragging && observerDragging !== 'false') return;", |
| 630 | + "cat1 container observer", |
| 631 | + ) |
| 632 | + |
| 633 | + walk_start = _source_slice_between( |
| 634 | + source, |
| 635 | + "function _startNekoIdleCat1Walk", |
| 636 | + "function _scheduleNekoIdleCat1WalkStart", |
| 637 | + "cat1 walk start", |
| 638 | + ) |
| 639 | + for expected in ( |
| 640 | + "if (_isNekoIdleReturnDragActionActive(button)) return", |
| 641 | + "const walkContainer = _getNekoIdleReturnContainerFromButton(button)", |
| 642 | + "const walkDragging = walkContainer && walkContainer.getAttribute('data-dragging')", |
| 643 | + "if (walkDragging && walkDragging !== 'false') return", |
| 644 | + ): |
| 645 | + _assert_source_contains(walk_start, expected, "cat1 walk start") |
| 646 | + _assert_source_order( |
| 647 | + walk_start, |
| 648 | + "cat1 walk start drag guard", |
| 649 | + "if (!state) return", |
| 650 | + "if (_isNekoIdleReturnDragActionActive(button)) return", |
| 651 | + "const profile = state.profile", |
| 652 | + ) |
| 653 | + |
| 654 | + |
500 | 655 | def test_return_button_idle_tier_assets_are_version_tracked(): |
501 | 656 | for path in (APP_UI_PATH, APP_INTERPAGE_PATH, COMMON_UI_HUD_PATH, |
502 | 657 | APP_REACT_CHAT_WINDOW_PATH, |
|
0 commit comments