|
18 | 18 | */ |
19 | 19 | import "@testing-library/jest-dom"; |
20 | 20 | import { fireEvent, render, screen, waitFor } from "@testing-library/react"; |
21 | | -import { describe, it, expect, beforeAll, vi } from "vitest"; |
| 21 | +import { beforeAll, describe, expect, it, vi } from "vitest"; |
22 | 22 |
|
23 | 23 | import { AppWrapper } from "src/utils/AppWrapper"; |
24 | 24 |
|
@@ -642,3 +642,110 @@ describe("Selection pinning across scrolling", () => { |
642 | 642 | }); |
643 | 643 | }); |
644 | 644 | }); |
| 645 | + |
| 646 | +describe("Downward drag selection", () => { |
| 647 | + it("coalesces events and extends the selection to the mounted bottom row", async () => { |
| 648 | + render( |
| 649 | + <AppWrapper initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]} />, |
| 650 | + ); |
| 651 | + await waitForLogs(); |
| 652 | + |
| 653 | + const container = screen.getByTestId("virtual-scroll-container"); |
| 654 | + const rows = container.querySelectorAll<HTMLElement>("[data-index]"); |
| 655 | + const anchorRow = rows[0] as HTMLElement; |
| 656 | + const lastRow = rows[rows.length - 1] as HTMLElement; |
| 657 | + const extend = vi.fn(); |
| 658 | + const range = document.createRange(); |
| 659 | + |
| 660 | + range.selectNodeContents(anchorRow); |
| 661 | + |
| 662 | + const selection = { |
| 663 | + anchorNode: anchorRow, |
| 664 | + extend, |
| 665 | + focusNode: anchorRow, |
| 666 | + focusOffset: 0, |
| 667 | + getRangeAt: () => range, |
| 668 | + rangeCount: 1, |
| 669 | + } as unknown as Selection; |
| 670 | + const animationFrames = new Array<FrameRequestCallback>(); |
| 671 | + const getSelectionSpy = vi.spyOn(document, "getSelection").mockReturnValue(selection); |
| 672 | + const requestAnimationFrameSpy = vi |
| 673 | + .spyOn(globalThis, "requestAnimationFrame") |
| 674 | + .mockImplementation((callback) => { |
| 675 | + animationFrames.push(callback); |
| 676 | + |
| 677 | + return animationFrames.length; |
| 678 | + }); |
| 679 | + const cancelAnimationFrameSpy = vi |
| 680 | + .spyOn(globalThis, "cancelAnimationFrame") |
| 681 | + .mockImplementation(() => undefined); |
| 682 | + |
| 683 | + container.getBoundingClientRect = () => ({ bottom: 500 }) as DOMRect; |
| 684 | + lastRow.getBoundingClientRect = () => ({ bottom: 480 }) as DOMRect; |
| 685 | + |
| 686 | + fireEvent.pointerDown(anchorRow, { button: 0, clientY: 200, pointerType: "mouse" }); |
| 687 | + fireEvent.pointerMove(document, { clientY: 490, pointerType: "mouse" }); |
| 688 | + document.dispatchEvent(new Event("selectionchange")); |
| 689 | + fireEvent.scroll(container); |
| 690 | + |
| 691 | + expect(animationFrames).toHaveLength(1); |
| 692 | + animationFrames.shift()?.(0); |
| 693 | + expect(extend).toHaveBeenCalledWith(lastRow, lastRow.childNodes.length); |
| 694 | + |
| 695 | + fireEvent.scroll(container); |
| 696 | + expect(animationFrames).toHaveLength(1); |
| 697 | + animationFrames.shift()?.(1); |
| 698 | + expect(extend).toHaveBeenCalledTimes(2); |
| 699 | + |
| 700 | + fireEvent.scroll(container); |
| 701 | + const pendingAnimationFrame = animationFrames.shift(); |
| 702 | + |
| 703 | + fireEvent.pointerUp(document, { pointerType: "mouse" }); |
| 704 | + expect(cancelAnimationFrameSpy).toHaveBeenCalledWith(1); |
| 705 | + |
| 706 | + pendingAnimationFrame?.(2); |
| 707 | + expect(extend).toHaveBeenCalledTimes(2); |
| 708 | + |
| 709 | + getSelectionSpy.mockRestore(); |
| 710 | + requestAnimationFrameSpy.mockRestore(); |
| 711 | + cancelAnimationFrameSpy.mockRestore(); |
| 712 | + }); |
| 713 | + |
| 714 | + it("only activates for downward primary-mouse drags starting in a log row", async () => { |
| 715 | + render( |
| 716 | + <AppWrapper initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/ti_context"]} />, |
| 717 | + ); |
| 718 | + await waitForLogs(); |
| 719 | + |
| 720 | + const container = screen.getByTestId("virtual-scroll-container"); |
| 721 | + const anchorRow = container.querySelector<HTMLElement>("[data-index]") as HTMLElement; |
| 722 | + const rows = container.querySelectorAll<HTMLElement>("[data-index]"); |
| 723 | + const lastRow = rows[rows.length - 1] as HTMLElement; |
| 724 | + const requestAnimationFrameSpy = vi |
| 725 | + .spyOn(globalThis, "requestAnimationFrame") |
| 726 | + .mockImplementation(() => 1); |
| 727 | + |
| 728 | + container.getBoundingClientRect = () => ({ bottom: 500 }) as DOMRect; |
| 729 | + lastRow.getBoundingClientRect = () => ({ bottom: 700 }) as DOMRect; |
| 730 | + |
| 731 | + fireEvent.pointerDown(container, { button: 0, clientY: 200, pointerType: "mouse" }); |
| 732 | + fireEvent.pointerMove(document, { clientY: 600, pointerType: "mouse" }); |
| 733 | + expect(requestAnimationFrameSpy).not.toHaveBeenCalled(); |
| 734 | + |
| 735 | + fireEvent.pointerDown(anchorRow, { button: 2, clientY: 200, pointerType: "mouse" }); |
| 736 | + fireEvent.pointerMove(document, { clientY: 600, pointerType: "mouse" }); |
| 737 | + expect(requestAnimationFrameSpy).not.toHaveBeenCalled(); |
| 738 | + |
| 739 | + fireEvent.pointerDown(anchorRow, { button: 0, clientY: 200, pointerType: "touch" }); |
| 740 | + fireEvent.pointerMove(document, { clientY: 600, pointerType: "touch" }); |
| 741 | + expect(requestAnimationFrameSpy).not.toHaveBeenCalled(); |
| 742 | + |
| 743 | + fireEvent.pointerDown(anchorRow, { button: 0, clientY: 200, pointerType: "mouse" }); |
| 744 | + fireEvent.pointerMove(document, { clientY: 50, pointerType: "mouse" }); |
| 745 | + fireEvent.scroll(container); |
| 746 | + expect(requestAnimationFrameSpy).not.toHaveBeenCalled(); |
| 747 | + |
| 748 | + fireEvent.pointerUp(document, { pointerType: "mouse" }); |
| 749 | + requestAnimationFrameSpy.mockRestore(); |
| 750 | + }); |
| 751 | +}); |
0 commit comments