| ... | @@ -2484,7 +2484,7 @@ it.each(["client", "frame"] as const)( | ... | @@ -2484,7 +2484,7 @@ it.each(["client", "frame"] as const)( |
| 2484 | }, | 2484 | }, |
| 2485 | ); | 2485 | ); |
| 2486 | | 2486 | |
| 2487 | it.each(["darwin", "win32"] as const)( | 2487 | it.each(["darwin", "win32", "linux"] as const)( |
| 2488 | "extracts the same structured accessibility tree on %s", | 2488 | "extracts the same structured accessibility tree on %s", |
| 2489 | async (platform) => { | 2489 | async (platform) => { |
| 2490 | const bounds = { x: 100, y: 200, width: 800, height: 600 }; | 2490 | const bounds = { x: 100, y: 200, width: 800, height: 600 }; |
| ... | @@ -2492,7 +2492,13 @@ it.each(["darwin", "win32"] as const)( | ... | @@ -2492,7 +2492,13 @@ it.each(["darwin", "win32"] as const)( |
| 2492 | role: "window", | 2492 | role: "window", |
| 2493 | name: "Editor", | 2493 | name: "Editor", |
| 2494 | bounds, | 2494 | bounds, |
| 2495 | tree: async () => ({ name: "Editor", children: [{ name: "Save", children: [] }] }), | 2495 | tree: async () => ({ |
| | 2496 | name: "Editor", |
| | 2497 | children: [ |
| | 2498 | { name: "Save", children: [] }, |
| | 2499 | { name: "Below scroll view", children: [] }, |
| | 2500 | ], |
| | 2501 | }), |
| 2496 | children: async () => [ | 2502 | children: async () => [ |
| 2497 | { | 2503 | { |
| 2498 | role: "button", | 2504 | role: "button", |
| ... | @@ -2500,6 +2506,13 @@ it.each(["darwin", "win32"] as const)( | ... | @@ -2500,6 +2506,13 @@ it.each(["darwin", "win32"] as const)( |
| 2500 | bounds: { x: 300, y: 350, width: 100, height: 50 }, | 2506 | bounds: { x: 300, y: 350, width: 100, height: 50 }, |
| 2501 | children: async () => [], | 2507 | children: async () => [], |
| 2502 | }, | 2508 | }, |
| | 2509 | { |
| | 2510 | role: "static_text", |
| | 2511 | name: "Below scroll view", |
| | 2512 | bounds: { x: 300, y: 1_000, width: 100, height: 50 }, |
| | 2513 | visible: false, |
| | 2514 | children: async () => [], |
| | 2515 | }, |
| 2503 | ], | 2516 | ], |
| 2504 | }; | 2517 | }; |
| 2505 | accessibilityByPidMock.mockReset().mockResolvedValue({ children: async () => [window] }); | 2518 | accessibilityByPidMock.mockReset().mockResolvedValue({ children: async () => [window] }); |
| ... | @@ -2521,6 +2534,12 @@ it.each(["darwin", "win32"] as const)( | ... | @@ -2521,6 +2534,12 @@ it.each(["darwin", "win32"] as const)( |
| 2521 | : undefined, | 2534 | : undefined, |
| 2522 | "Save", | 2535 | "Save", |
| 2523 | ); | 2536 | ); |
| | 2537 | assert.deepInclude( |
| | 2538 | result?.accessibility?.format === "element-tree" |
| | 2539 | ? result.accessibility.root.children[1] |
| | 2540 | : undefined, |
| | 2541 | { name: "Below scroll view", bounds: null, state: { visible: false } }, |
| | 2542 | ); |
| 2524 | assert.lengthOf(accessibilityByPidMock.mock.calls, platform === "win32" ? 0 : 1); | 2543 | assert.lengthOf(accessibilityByPidMock.mock.calls, platform === "win32" ? 0 : 1); |
| 2525 | assert.lengthOf(accessibilityForegroundMock.mock.calls, platform === "win32" ? 1 : 0); | 2544 | assert.lengthOf(accessibilityForegroundMock.mock.calls, platform === "win32" ? 1 : 0); |
| 2526 | }, | 2545 | }, |
| ... | @@ -2559,6 +2578,48 @@ it.each(["darwin", "win32"] as const)( | ... | @@ -2559,6 +2578,48 @@ it.each(["darwin", "win32"] as const)( |
| 2559 | }, | 2578 | }, |
| 2560 | ); | 2579 | ); |
| 2561 | | 2580 | |
| | 2581 | it.each([ |
| | 2582 | ["darwin", "", "value", 650], |
| | 2583 | ["win32", "", "value", 650], |
| | 2584 | ["linux", "x11", "value", 650], |
| | 2585 | ["linux", "wayland", "value", 650], |
| | 2586 | ["darwin", "", "name", 100], |
| | 2587 | ] as const)( |
| | 2588 | "preserves long text outside the scroll view on %s %s (%s)", |
| | 2589 | async (platform, session, field, lines) => { |
| | 2590 | const text = `${"Document line\n".repeat(lines)}End of document`; |
| | 2591 | vi.stubEnv("XDG_SESSION_TYPE", session); |
| | 2592 | const bounds = { x: 0, y: 0, width: 800, height: 600 }; |
| | 2593 | const window = { |
| | 2594 | role: "window", |
| | 2595 | name: "Editor", |
| | 2596 | bounds, |
| | 2597 | tree: async () => ({ name: "Editor", children: [{ [field]: text, children: [] }] }), |
| | 2598 | children: async () => [ |
| | 2599 | { role: "text_area", [field]: text, bounds, children: async () => [] }, |
| | 2600 | ], |
| | 2601 | }; |
| | 2602 | accessibilityByPidMock.mockReset().mockResolvedValue({ children: async () => [window] }); |
| | 2603 | accessibilityForegroundMock |
| | 2604 | .mockReset() |
| | 2605 | .mockResolvedValue({ pid: 123, asElement: () => window }); |
| | 2606 | try { |
| | 2607 | const result = await readAccessibleWindowContext( |
| | 2608 | { title: "Editor", bounds, owner: { processId: 123 } }, |
| | 2609 | platform, |
| | 2610 | "Editor", |
| | 2611 | ); |
| | 2612 | assert.deepEqual(result?.accessibility, { |
| | 2613 | format: "flat-text", |
| | 2614 | text: `Editor\n${text}`, |
| | 2615 | truncated: false, |
| | 2616 | }); |
| | 2617 | } finally { |
| | 2618 | vi.unstubAllEnvs(); |
| | 2619 | } |
| | 2620 | }, |
| | 2621 | ); |
| | 2622 | |
| 2562 | it.each([ | 2623 | it.each([ |
| 2563 | { names: ["⠙ t3code"], expected: "Verified text" }, | 2624 | { names: ["⠙ t3code"], expected: "Verified text" }, |
| 2564 | { names: ["⠋ t3code", "⠙ t3code"], expected: undefined }, | 2625 | { names: ["⠋ t3code", "⠙ t3code"], expected: undefined }, |
| ... | @@ -2686,6 +2747,81 @@ it("falls back to completed flat text when rich traversal reaches the deadline", | ... | @@ -2686,6 +2747,81 @@ it("falls back to completed flat text when rich traversal reaches the deadline", |
| 2686 | } | 2747 | } |
| 2687 | }); | 2748 | }); |
| 2688 | | 2749 | |
| | 2750 | it("keeps a truncated element tree when the flat text read fails", async () => { |
| | 2751 | const bounds = { x: 0, y: 0, width: 800, height: 600 }; |
| | 2752 | accessibilityByPidMock.mockReset().mockResolvedValue({ |
| | 2753 | children: async () => [ |
| | 2754 | { |
| | 2755 | role: "window", |
| | 2756 | name: "Editor", |
| | 2757 | bounds, |
| | 2758 | tree: async () => { |
| | 2759 | throw new Error("Text read failed"); |
| | 2760 | }, |
| | 2761 | children: async () => [ |
| | 2762 | { |
| | 2763 | role: "text_area", |
| | 2764 | value: "x".repeat(8_001), |
| | 2765 | bounds: { x: 10, y: 50, width: 700, height: 500 }, |
| | 2766 | children: async () => [], |
| | 2767 | }, |
| | 2768 | ], |
| | 2769 | }, |
| | 2770 | ], |
| | 2771 | }); |
| | 2772 | |
| | 2773 | const result = await readAccessibleWindowContext( |
| | 2774 | { title: "Editor", bounds, owner: { processId: 123 } }, |
| | 2775 | "darwin", |
| | 2776 | "Editor", |
| | 2777 | ); |
| | 2778 | assert.equal(result?.accessibility?.format, "element-tree"); |
| | 2779 | assert.isTrue(result?.accessibility?.truncated); |
| | 2780 | assert.deepEqual( |
| | 2781 | result?.accessibility?.format === "element-tree" |
| | 2782 | ? result.accessibility.root.children[0]?.bounds |
| | 2783 | : undefined, |
| | 2784 | { x: 10, y: 50, width: 700, height: 500 }, |
| | 2785 | ); |
| | 2786 | }); |
| | 2787 | |
| | 2788 | it("keeps a truncated tree when flat text would not recover any text", async () => { |
| | 2789 | const bounds = { x: 0, y: 0, width: 800, height: 600 }; |
| | 2790 | accessibilityByPidMock.mockReset().mockResolvedValue({ |
| | 2791 | children: async () => [ |
| | 2792 | { |
| | 2793 | role: "window", |
| | 2794 | name: "Editor", |
| | 2795 | bounds, |
| | 2796 | tree: async () => ({ name: "Editor", children: [{ name: "Help", children: [] }] }), |
| | 2797 | children: async () => [ |
| | 2798 | { |
| | 2799 | role: "button", |
| | 2800 | name: "Help", |
| | 2801 | description: "Help text ".repeat(250), |
| | 2802 | bounds, |
| | 2803 | children: async () => [], |
| | 2804 | }, |
| | 2805 | ], |
| | 2806 | }, |
| | 2807 | ], |
| | 2808 | }); |
| | 2809 | |
| | 2810 | const result = await readAccessibleWindowContext( |
| | 2811 | { title: "Editor", bounds, owner: { processId: 123 } }, |
| | 2812 | "darwin", |
| | 2813 | "Editor", |
| | 2814 | ); |
| | 2815 | assert.equal(result?.accessibility?.format, "element-tree"); |
| | 2816 | assert.isTrue(result?.accessibility?.truncated); |
| | 2817 | assert.include( |
| | 2818 | result?.accessibility?.format === "element-tree" |
| | 2819 | ? result.accessibility.root.children[0]?.description |
| | 2820 | : undefined, |
| | 2821 | "Help text", |
| | 2822 | ); |
| | 2823 | }); |
| | 2824 | |
| 2689 | it("times out after three seconds without overlapping the outstanding accessibility read", async () => { | 2825 | it("times out after three seconds without overlapping the outstanding accessibility read", async () => { |
| 2690 | vi.useFakeTimers(); | 2826 | vi.useFakeTimers(); |
| 2691 | accessibilityByPidMock.mockReset(); | 2827 | accessibilityByPidMock.mockReset(); |