From 71c278a6dc01b10ad1d1bf588a1a0b87f5631f17 Mon Sep 17 00:00:00 2001 From: Andrew Holzbach Date: Thu, 16 Jul 2026 15:53:07 -0500 Subject: [PATCH 1/2] test: add coverage suite for the onboarding PermissionsStep --- .../__tests__/PermissionsStep.test.tsx | 758 ++++++++++++++++++ 1 file changed, 758 insertions(+) create mode 100644 src/view/onboarding/__tests__/PermissionsStep.test.tsx diff --git a/src/view/onboarding/__tests__/PermissionsStep.test.tsx b/src/view/onboarding/__tests__/PermissionsStep.test.tsx new file mode 100644 index 00000000..7c760006 --- /dev/null +++ b/src/view/onboarding/__tests__/PermissionsStep.test.tsx @@ -0,0 +1,758 @@ +import { render, screen, fireEvent, act } from '@testing-library/react'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { PermissionsStep } from '../PermissionsStep'; +import { + invoke, + enableChannelCaptureWithResponses, +} from '../../../testUtils/mocks/tauri'; + +const BASE_RESPONSES = { + consume_pending_grant_resume: null, + check_accessibility_permission: false, +}; + +describe('PermissionsStep', () => { + beforeEach(() => { + invoke.mockClear(); + }); + + it('renders the title and the Accessibility step as active, Screen Recording as waiting', async () => { + enableChannelCaptureWithResponses(BASE_RESPONSES); + + render(); + await act(async () => {}); + + expect(screen.getByText("Let's get Thuki set up")).toBeInTheDocument(); + expect(screen.getByText('Accessibility')).toBeInTheDocument(); + expect( + screen.getByText('Needed for /screen to capture your entire screen'), + ).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Grant Accessibility Access' }), + ).toBeInTheDocument(); + expect(screen.queryByText('Granted')).not.toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'Open Screen Recording Settings' }), + ).not.toBeInTheDocument(); + }); + + it('brightens the CTA on hover and reverts on mouse leave', async () => { + enableChannelCaptureWithResponses(BASE_RESPONSES); + + render(); + await act(async () => {}); + + const button = screen.getByRole('button', { + name: 'Grant Accessibility Access', + }); + fireEvent.mouseEnter(button); + expect(button.style.filter).toBe('brightness(1.1)'); + + fireEvent.mouseLeave(button); + expect(button.style.filter).toBe('none'); + }); + + it('does not brighten the CTA on hover while it is disabled', async () => { + // Once a grant flow is in flight the button stays disabled ("Checking...") + // until polling detects the grant, so hovering it must not brighten it. + enableChannelCaptureWithResponses({ + ...BASE_RESPONSES, + reset_and_relaunch_for_grant: false, + open_accessibility_settings: undefined, + }); + + render(); + await act(async () => {}); + + await act(async () => { + fireEvent.click( + screen.getByRole('button', { name: 'Grant Accessibility Access' }), + ); + }); + + const button = screen.getByRole('button', { name: 'Checking...' }); + fireEvent.mouseEnter(button); + expect(button.style.filter).toBe('none'); + }); + + it('shows Accessibility as done and Screen Recording as active when already granted on mount', async () => { + enableChannelCaptureWithResponses({ + ...BASE_RESPONSES, + check_accessibility_permission: true, + }); + + render(); + await act(async () => {}); + + expect(screen.getByText('Granted')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Open Screen Recording Settings' }), + ).toBeInTheDocument(); + expect( + screen.queryByRole('button', { name: 'Grant Accessibility Access' }), + ).not.toBeInTheDocument(); + }); + + it('requests accessibility settings when the grant button is clicked (no relaunch needed)', async () => { + enableChannelCaptureWithResponses({ + ...BASE_RESPONSES, + reset_and_relaunch_for_grant: false, + open_accessibility_settings: undefined, + }); + + render(); + await act(async () => {}); + + const button = screen.getByRole('button', { + name: 'Grant Accessibility Access', + }); + await act(async () => { + fireEvent.click(button); + }); + + expect(invoke).toHaveBeenCalledWith('reset_and_relaunch_for_grant', { + service: 'Accessibility', + }); + expect(invoke).toHaveBeenCalledWith('open_accessibility_settings'); + expect( + screen.getByRole('button', { name: 'Checking...' }), + ).toBeInTheDocument(); + }); + + it('does not open settings when reset_and_relaunch_for_grant signals a relaunch is in progress', async () => { + enableChannelCaptureWithResponses({ + ...BASE_RESPONSES, + reset_and_relaunch_for_grant: true, + }); + + render(); + await act(async () => {}); + + const button = screen.getByRole('button', { + name: 'Grant Accessibility Access', + }); + await act(async () => { + fireEvent.click(button); + }); + + expect(invoke).toHaveBeenCalledWith('reset_and_relaunch_for_grant', { + service: 'Accessibility', + }); + expect(invoke).not.toHaveBeenCalledWith('open_accessibility_settings'); + }); + + describe('polling flows', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('advances Accessibility to granted once polling detects the permission', async () => { + let accessibilityGranted = false; + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'consume_pending_grant_resume') return null; + if (cmd === 'check_accessibility_permission') + return accessibilityGranted; + if (cmd === 'reset_and_relaunch_for_grant') return false; + if (cmd === 'open_accessibility_settings') return undefined; + return undefined; + }); + + render(); + await act(async () => {}); + + const button = screen.getByRole('button', { + name: 'Grant Accessibility Access', + }); + await act(async () => { + fireEvent.click(button); + }); + + // First tick still finds it ungranted, exercising the "not yet" branch. + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); + expect( + screen.getByRole('button', { name: 'Checking...' }), + ).toBeInTheDocument(); + + accessibilityGranted = true; + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); + + expect(screen.getByText('Granted')).toBeInTheDocument(); + expect( + screen.getByRole('button', { name: 'Open Screen Recording Settings' }), + ).toBeInTheDocument(); + }); + + it('does not open Screen Recording settings when reset_and_relaunch_for_grant signals a relaunch is in progress', async () => { + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'consume_pending_grant_resume') return null; + if (cmd === 'check_accessibility_permission') return true; + if (cmd === 'reset_and_relaunch_for_grant') return true; + return undefined; + }); + + render(); + await act(async () => {}); + + const button = screen.getByRole('button', { + name: 'Open Screen Recording Settings', + }); + await act(async () => { + fireEvent.click(button); + }); + + expect(invoke).toHaveBeenCalledWith('reset_and_relaunch_for_grant', { + service: 'ScreenCapture', + }); + expect(invoke).not.toHaveBeenCalledWith( + 'request_screen_recording_access', + ); + }); + + it('requests screen recording access and settings when its CTA is clicked', async () => { + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'consume_pending_grant_resume') return null; + if (cmd === 'check_accessibility_permission') return true; + if (cmd === 'reset_and_relaunch_for_grant') return false; + if (cmd === 'request_screen_recording_access') return undefined; + if (cmd === 'open_screen_recording_settings') return undefined; + if (cmd === 'check_screen_recording_tcc_granted') return false; + return undefined; + }); + + render(); + await act(async () => {}); + + const button = screen.getByRole('button', { + name: 'Open Screen Recording Settings', + }); + await act(async () => { + fireEvent.click(button); + }); + + expect(invoke).toHaveBeenCalledWith('request_screen_recording_access'); + expect(invoke).toHaveBeenCalledWith('open_screen_recording_settings'); + expect( + screen.getAllByRole('button', { name: 'Checking...' }).length, + ).toBeGreaterThan(0); + }); + + it('shows Quit & Reopen once Screen Recording polling detects the permission, and it invokes quit_and_relaunch', async () => { + let screenGranted = false; + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'consume_pending_grant_resume') return null; + if (cmd === 'check_accessibility_permission') return true; + if (cmd === 'reset_and_relaunch_for_grant') return false; + if (cmd === 'request_screen_recording_access') return undefined; + if (cmd === 'open_screen_recording_settings') return undefined; + if (cmd === 'check_screen_recording_tcc_granted') return screenGranted; + if (cmd === 'quit_and_relaunch') return undefined; + return undefined; + }); + + render(); + await act(async () => {}); + + const openSettingsButton = screen.getByRole('button', { + name: 'Open Screen Recording Settings', + }); + await act(async () => { + fireEvent.click(openSettingsButton); + }); + + // First tick still finds it ungranted, exercising the "not yet" branch. + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); + expect( + screen.getAllByRole('button', { name: 'Checking...' }).length, + ).toBeGreaterThan(0); + + screenGranted = true; + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); + + expect( + screen.getByText( + 'macOS requires a restart for Screen Recording to take effect', + ), + ).toBeInTheDocument(); + + const quitButton = screen.getByRole('button', { + name: 'Quit and Reopen Thuki', + }); + await act(async () => { + fireEvent.click(quitButton); + }); + + expect(invoke).toHaveBeenCalledWith('quit_and_relaunch'); + }); + }); + + describe('resume flow', () => { + it('auto-resumes the Accessibility flow when the backend reports a pending Accessibility resume', async () => { + enableChannelCaptureWithResponses({ + ...BASE_RESPONSES, + consume_pending_grant_resume: 'Accessibility', + }); + + render(); + await act(async () => {}); + + expect(invoke).toHaveBeenCalledWith('open_accessibility_settings'); + expect( + screen.getByRole('button', { name: 'Checking...' }), + ).toBeInTheDocument(); + }); + + it('auto-resumes the Screen Recording flow when the backend reports a pending ScreenCapture resume and Accessibility is already granted', async () => { + enableChannelCaptureWithResponses({ + consume_pending_grant_resume: 'ScreenCapture', + check_accessibility_permission: true, + }); + + render(); + await act(async () => {}); + + expect(invoke).toHaveBeenCalledWith('request_screen_recording_access'); + expect(invoke).toHaveBeenCalledWith('open_screen_recording_settings'); + }); + + it('does not auto-resume the Screen Recording flow when Accessibility is not yet granted', async () => { + enableChannelCaptureWithResponses({ + consume_pending_grant_resume: 'ScreenCapture', + check_accessibility_permission: false, + }); + + render(); + await act(async () => {}); + + expect(invoke).not.toHaveBeenCalledWith( + 'request_screen_recording_access', + ); + }); + }); + + describe('unmount guards', () => { + // These exercise the mountedRef.current === false branches: an invoke() + // call resolves after the component has already unmounted, and the + // resulting state update / follow-on call must be skipped rather than + // throwing on an unmounted component. + + it('skips the mount-effect state updates when unmounted before consume_pending_grant_resume resolves', async () => { + let resolveResume: (value: string | null) => void = () => {}; + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'consume_pending_grant_resume') { + return new Promise((resolve) => { + resolveResume = resolve; + }); + } + return undefined; + }); + + const { unmount } = render(); + unmount(); + await act(async () => { + resolveResume(null); + await Promise.resolve(); + }); + + // If the guard were missing, the mount effect would fall through to + // its next line regardless of what `resume` resolved to. + expect(invoke).not.toHaveBeenCalledWith('check_accessibility_permission'); + }); + + it('skips the mount-effect state updates when unmounted before check_accessibility_permission resolves', async () => { + let resolveCheck: (value: boolean) => void = () => {}; + // resume is 'Accessibility' so that, if the guard below were missing, + // the effect would fall through to auto-starting startAccessibilityFlow + // (and thus invoke open_accessibility_settings) regardless of what + // check_accessibility_permission resolves to — giving the guard an + // observable effect to assert against. + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'consume_pending_grant_resume') return 'Accessibility'; + if (cmd === 'check_accessibility_permission') { + return new Promise((resolve) => { + resolveCheck = resolve; + }); + } + return undefined; + }); + + const { unmount } = render(); + await act(async () => {}); + unmount(); + await act(async () => { + resolveCheck(true); + await Promise.resolve(); + }); + + expect(invoke).not.toHaveBeenCalledWith('open_accessibility_settings'); + }); + + it('skips starting Accessibility polling when unmounted right after settings succeed', async () => { + let resolveOpenSettings: () => void = () => {}; + enableChannelCaptureWithResponses(BASE_RESPONSES); + vi.useFakeTimers(); + + const { unmount } = render(); + await act(async () => {}); + + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'reset_and_relaunch_for_grant') return false; + if (cmd === 'open_accessibility_settings') { + return new Promise((resolve) => { + resolveOpenSettings = resolve; + }); + } + return undefined; + }); + + const button = screen.getByRole('button', { + name: 'Grant Accessibility Access', + }); + await act(async () => { + fireEvent.click(button); + }); + + unmount(); + await act(async () => { + resolveOpenSettings(); + await Promise.resolve(); + }); + + // Clear the mount effect's own earlier check_accessibility_permission + // call so the assertion below only reflects calls made from here on. + invoke.mockClear(); + + // If the guard were missing, setInterval would have been scheduled; + // advancing past a full tick and seeing no poll call proves it wasn't. + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); + expect(invoke).not.toHaveBeenCalledWith('check_accessibility_permission'); + + vi.useRealTimers(); + }); + + it('skips a poll tick while the previous Accessibility poll invoke is still in flight', async () => { + let resolveFirstCheck: (value: boolean) => void = () => {}; + let checkCallCount = 0; + enableChannelCaptureWithResponses(BASE_RESPONSES); + vi.useFakeTimers(); + + render(); + await act(async () => {}); + + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'reset_and_relaunch_for_grant') return false; + if (cmd === 'open_accessibility_settings') return undefined; + if (cmd === 'check_accessibility_permission') { + checkCallCount += 1; + if (checkCallCount === 1) { + return new Promise((resolve) => { + resolveFirstCheck = resolve; + }); + } + return true; + } + return undefined; + }); + + const button = screen.getByRole('button', { + name: 'Grant Accessibility Access', + }); + await act(async () => { + fireEvent.click(button); + }); + + // First tick starts and never resolves yet; the second tick fires + // while it's still in flight and must be skipped by the guard. + await act(async () => { + await vi.advanceTimersByTimeAsync(1000); + }); + expect(checkCallCount).toBe(1); + + await act(async () => { + resolveFirstCheck(false); + await Promise.resolve(); + }); + + vi.useRealTimers(); + }); + + it('skips a poll-tick state update when unmounted while check_accessibility_permission is in flight', async () => { + let resolveGrantedCheck: (value: boolean) => void = () => {}; + let firstTickStarted = false; + enableChannelCaptureWithResponses(BASE_RESPONSES); + vi.useFakeTimers(); + + const { unmount } = render(); + await act(async () => {}); + + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'reset_and_relaunch_for_grant') return false; + if (cmd === 'open_accessibility_settings') return undefined; + if (cmd === 'check_accessibility_permission') { + firstTickStarted = true; + return new Promise((resolve) => { + resolveGrantedCheck = resolve; + }); + } + return undefined; + }); + + const button = screen.getByRole('button', { + name: 'Grant Accessibility Access', + }); + await act(async () => { + fireEvent.click(button); + }); + + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); + expect(firstTickStarted).toBe(true); + + unmount(); + // If this guard were missing, `granted` resolving true would run + // `stopAxPolling()` (a harmless no-op here, since unmount's own + // cleanup already cleared the interval) and `setAccessibilityStatus` + // on a gone component. Neither has a further invoke() call to assert + // against, so "resolving this after unmount does not throw" is the + // strongest assertion available for this specific guard. + await act(async () => { + resolveGrantedCheck(true); + await Promise.resolve(); + }); + + vi.useRealTimers(); + }); + + it('skips resetting Accessibility status when unmounted before reset_and_relaunch_for_grant resolves', async () => { + let resolveReset: (value: boolean) => void = () => {}; + enableChannelCaptureWithResponses(BASE_RESPONSES); + + const { unmount } = render(); + await act(async () => {}); + + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'reset_and_relaunch_for_grant') { + return new Promise((resolve) => { + resolveReset = resolve; + }); + } + return undefined; + }); + + const button = screen.getByRole('button', { + name: 'Grant Accessibility Access', + }); + await act(async () => { + fireEvent.click(button); + }); + + unmount(); + await act(async () => { + resolveReset(false); + await Promise.resolve(); + }); + + expect(invoke).not.toHaveBeenCalledWith('open_accessibility_settings'); + }); + + it('skips starting Screen Recording polling when unmounted right after settings succeed', async () => { + let resolveOpenSettings: () => void = () => {}; + enableChannelCaptureWithResponses({ + ...BASE_RESPONSES, + check_accessibility_permission: true, + }); + vi.useFakeTimers(); + + const { unmount } = render(); + await act(async () => {}); + + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'reset_and_relaunch_for_grant') return false; + if (cmd === 'request_screen_recording_access') return undefined; + if (cmd === 'open_screen_recording_settings') { + return new Promise((resolve) => { + resolveOpenSettings = resolve; + }); + } + return undefined; + }); + + const button = screen.getByRole('button', { + name: 'Open Screen Recording Settings', + }); + await act(async () => { + fireEvent.click(button); + }); + + unmount(); + await act(async () => { + resolveOpenSettings(); + await Promise.resolve(); + }); + + // If the guard were missing, setInterval would have been scheduled; + // advancing past a full tick and seeing no poll call proves it wasn't. + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); + expect(invoke).not.toHaveBeenCalledWith( + 'check_screen_recording_tcc_granted', + ); + + vi.useRealTimers(); + }); + + it('skips a poll tick while the previous Screen Recording poll invoke is still in flight', async () => { + let resolveFirstCheck: (value: boolean) => void = () => {}; + let checkCallCount = 0; + enableChannelCaptureWithResponses({ + ...BASE_RESPONSES, + check_accessibility_permission: true, + }); + vi.useFakeTimers(); + + render(); + await act(async () => {}); + + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'reset_and_relaunch_for_grant') return false; + if (cmd === 'request_screen_recording_access') return undefined; + if (cmd === 'open_screen_recording_settings') return undefined; + if (cmd === 'check_screen_recording_tcc_granted') { + checkCallCount += 1; + if (checkCallCount === 1) { + return new Promise((resolve) => { + resolveFirstCheck = resolve; + }); + } + return true; + } + return undefined; + }); + + const button = screen.getByRole('button', { + name: 'Open Screen Recording Settings', + }); + await act(async () => { + fireEvent.click(button); + }); + + // First tick starts and never resolves yet; the second tick fires + // while it's still in flight and must be skipped by the guard. + await act(async () => { + await vi.advanceTimersByTimeAsync(1000); + }); + expect(checkCallCount).toBe(1); + + await act(async () => { + resolveFirstCheck(false); + await Promise.resolve(); + }); + + vi.useRealTimers(); + }); + + it('skips a poll-tick state update when unmounted while check_screen_recording_tcc_granted is in flight', async () => { + let resolveGrantedCheck: (value: boolean) => void = () => {}; + let firstTickStarted = false; + enableChannelCaptureWithResponses({ + ...BASE_RESPONSES, + check_accessibility_permission: true, + }); + vi.useFakeTimers(); + + const { unmount } = render(); + await act(async () => {}); + + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'reset_and_relaunch_for_grant') return false; + if (cmd === 'request_screen_recording_access') return undefined; + if (cmd === 'open_screen_recording_settings') return undefined; + if (cmd === 'check_screen_recording_tcc_granted') { + firstTickStarted = true; + return new Promise((resolve) => { + resolveGrantedCheck = resolve; + }); + } + return undefined; + }); + + const button = screen.getByRole('button', { + name: 'Open Screen Recording Settings', + }); + await act(async () => { + fireEvent.click(button); + }); + + await act(async () => { + await vi.advanceTimersByTimeAsync(500); + }); + expect(firstTickStarted).toBe(true); + + unmount(); + // If this guard were missing, `granted` resolving true would run + // `stopScreenPolling()` (a harmless no-op here, since unmount's own + // cleanup already cleared the interval) and `setScreenRecordingStatus` + // on a gone component. Neither has a further invoke() call to assert + // against, so "resolving this after unmount does not throw" is the + // strongest assertion available for this specific guard. + await act(async () => { + resolveGrantedCheck(true); + await Promise.resolve(); + }); + + vi.useRealTimers(); + }); + + it('skips resetting Screen Recording status when unmounted before reset_and_relaunch_for_grant resolves', async () => { + let resolveReset: (value: boolean) => void = () => {}; + enableChannelCaptureWithResponses({ + ...BASE_RESPONSES, + check_accessibility_permission: true, + }); + + const { unmount } = render(); + await act(async () => {}); + + invoke.mockImplementation(async (cmd: string) => { + if (cmd === 'reset_and_relaunch_for_grant') { + return new Promise((resolve) => { + resolveReset = resolve; + }); + } + return undefined; + }); + + const button = screen.getByRole('button', { + name: 'Open Screen Recording Settings', + }); + await act(async () => { + fireEvent.click(button); + }); + + unmount(); + await act(async () => { + resolveReset(false); + await Promise.resolve(); + }); + + expect(invoke).not.toHaveBeenCalledWith( + 'request_screen_recording_access', + ); + }); + }); +}); From 5b4f04529c6ae4a4f9098d0b68ffb8105ae236c9 Mon Sep 17 00:00:00 2001 From: Andrew Holzbach Date: Mon, 20 Jul 2026 07:40:38 +0200 Subject: [PATCH 2/2] test: tighten PermissionsStep suite and drop superseded OnboardingView tests --- src/__tests__/OnboardingView.test.tsx | 844 ------------------ .../__tests__/PermissionsStep.test.tsx | 26 +- 2 files changed, 8 insertions(+), 862 deletions(-) delete mode 100644 src/__tests__/OnboardingView.test.tsx diff --git a/src/__tests__/OnboardingView.test.tsx b/src/__tests__/OnboardingView.test.tsx deleted file mode 100644 index e54d28d4..00000000 --- a/src/__tests__/OnboardingView.test.tsx +++ /dev/null @@ -1,844 +0,0 @@ -import { render, screen, fireEvent, act } from '@testing-library/react'; -import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; -import { PermissionsStep } from '../view/onboarding/PermissionsStep'; -import { invoke } from '../testUtils/mocks/tauri'; - -describe('OnboardingView', () => { - beforeEach(() => { - invoke.mockClear(); - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - function setupPermissions(accessibility: boolean, screenRecording = false) { - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'consume_pending_grant_resume') return null; - if (cmd === 'reset_and_relaunch_for_grant') return false; - if (cmd === 'check_accessibility_permission') return accessibility; - if (cmd === 'check_screen_recording_permission') return screenRecording; - if (cmd === 'check_screen_recording_tcc_granted') return false; - if (cmd === 'request_screen_recording_access') return; - if (cmd === 'open_screen_recording_settings') return; - if (cmd === 'open_accessibility_settings') return; - }); - } - - it('shows step 1 as active when accessibility is not granted', async () => { - setupPermissions(false); - render(); - await act(async () => {}); - - expect(screen.getByText('Accessibility')).toBeInTheDocument(); - expect( - screen.getByRole('button', { name: /grant accessibility/i }), - ).toBeInTheDocument(); - }); - - it('shows the onboarding title', async () => { - setupPermissions(false); - render(); - await act(async () => {}); - - expect(screen.getByText("Let's get Thuki set up")).toBeInTheDocument(); - }); - - it('skips to step 2 when accessibility is already granted on mount', async () => { - setupPermissions(true); - render(); - await act(async () => {}); - - expect( - screen.queryByRole('button', { name: /grant accessibility/i }), - ).toBeNull(); - expect( - screen.getByRole('button', { name: /open screen recording settings/i }), - ).toBeInTheDocument(); - }); - - it('clicking grant accessibility invokes request command', async () => { - setupPermissions(false); - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - expect(invoke).toHaveBeenCalledWith('reset_and_relaunch_for_grant', { - service: 'Accessibility', - }); - expect(invoke).toHaveBeenCalledWith('open_accessibility_settings'); - }); - - it('clicking grant accessibility skips inline flow when backend signals relaunch', async () => { - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'consume_pending_grant_resume') return null; - if (cmd === 'reset_and_relaunch_for_grant') return true; - if (cmd === 'check_accessibility_permission') return false; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'open_accessibility_settings') return; - }); - - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - // Backend reports a relaunch is in flight, so the frontend must not - // open System Settings or start polling: the relaunched process owns - // both responsibilities via the consume_pending_grant_resume marker. - expect(invoke).not.toHaveBeenCalledWith('open_accessibility_settings'); - }); - - it('auto-resumes the accessibility flow when consume returns Accessibility', async () => { - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'consume_pending_grant_resume') return 'Accessibility'; - if (cmd === 'reset_and_relaunch_for_grant') return false; - if (cmd === 'check_accessibility_permission') return false; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'open_accessibility_settings') return; - }); - - render(); - // Drain the two sequential awaits inside the mount IIFE. - await act(async () => {}); - await act(async () => {}); - - expect(invoke).toHaveBeenCalledWith('consume_pending_grant_resume'); - expect(invoke).toHaveBeenCalledWith('open_accessibility_settings'); - // Click button shows "Checking..." because the resume kicked the flow - // into the requesting state without a click. - expect( - screen.getByRole('button', { name: /checking/i }), - ).toBeInTheDocument(); - }); - - it('auto-resumes the screen recording flow when consume returns ScreenCapture', async () => { - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'consume_pending_grant_resume') return 'ScreenCapture'; - if (cmd === 'reset_and_relaunch_for_grant') return false; - if (cmd === 'check_accessibility_permission') return true; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'check_screen_recording_tcc_granted') return false; - if (cmd === 'request_screen_recording_access') return; - if (cmd === 'open_screen_recording_settings') return; - }); - - render(); - await act(async () => {}); - await act(async () => {}); - - expect(invoke).toHaveBeenCalledWith('request_screen_recording_access'); - expect(invoke).toHaveBeenCalledWith('open_screen_recording_settings'); - }); - - it('does not auto-resume screen recording when accessibility is not yet granted', async () => { - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'consume_pending_grant_resume') return 'ScreenCapture'; - if (cmd === 'reset_and_relaunch_for_grant') return false; - if (cmd === 'check_accessibility_permission') return false; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'request_screen_recording_access') return; - if (cmd === 'open_screen_recording_settings') return; - }); - - render(); - await act(async () => {}); - await act(async () => {}); - - // ScreenCapture resume only kicks in when AX is already granted; here it - // must NOT have triggered the request_screen_recording_access path. - expect(invoke).not.toHaveBeenCalledWith('request_screen_recording_access'); - }); - - it('shows spinner while polling after grant request', async () => { - setupPermissions(false); - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - // Button should be disabled/spinner state while checking - const btn = screen.getByRole('button', { - name: /checking|grant accessibility/i, - }); - expect(btn).toBeDisabled(); - }); - - it('keeps polling when accessibility not yet granted on first poll interval', async () => { - let accessibilityGranted = false; - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'check_accessibility_permission') return accessibilityGranted; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'open_accessibility_settings') return; - }); - - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - // First poll fires but permission still false - await act(async () => { - await vi.advanceTimersByTimeAsync(500); - }); - - // Still on step 1, open screen recording button not yet shown - expect( - screen.queryByRole('button', { name: /open screen recording settings/i }), - ).toBeNull(); - - // Now grant it and fire second poll - accessibilityGranted = true; - await act(async () => { - await vi.advanceTimersByTimeAsync(500); - }); - - // Step 2 now active - expect( - screen.getByRole('button', { name: /open screen recording settings/i }), - ).toBeInTheDocument(); - }); - - it('advances to step 2 when polling detects accessibility granted', async () => { - let accessibilityGranted = false; - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'check_accessibility_permission') return accessibilityGranted; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'open_accessibility_settings') return; - }); - - render(); - await act(async () => {}); - - // Click grant - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - // Grant becomes true before next poll - accessibilityGranted = true; - - // Advance one poll interval - await act(async () => { - await vi.advanceTimersByTimeAsync(500); - }); - - // Step 2 should now be active - expect( - screen.getByRole('button', { name: /open screen recording settings/i }), - ).toBeInTheDocument(); - }); - - it('step 1 shows granted badge after accessibility is detected', async () => { - let accessibilityGranted = false; - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'check_accessibility_permission') return accessibilityGranted; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'open_accessibility_settings') return; - }); - - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - accessibilityGranted = true; - await act(async () => { - await vi.advanceTimersByTimeAsync(500); - }); - - expect(screen.getByText('Granted')).toBeInTheDocument(); - }); - - it('clicking open screen recording settings registers app and opens settings', async () => { - setupPermissions(true); - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - // First clears any stale ScreenCapture grant left from a previous - // binary, then registers Thuki in TCC + opens Settings. - expect(invoke).toHaveBeenCalledWith('reset_and_relaunch_for_grant', { - service: 'ScreenCapture', - }); - expect(invoke).toHaveBeenCalledWith('request_screen_recording_access'); - expect(invoke).toHaveBeenCalledWith('open_screen_recording_settings'); - }); - - it('clicking screen recording skips inline flow when backend signals relaunch', async () => { - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'consume_pending_grant_resume') return null; - if (cmd === 'reset_and_relaunch_for_grant') return true; - if (cmd === 'check_accessibility_permission') return true; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'request_screen_recording_access') return; - if (cmd === 'open_screen_recording_settings') return; - }); - - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - // Relaunch is in flight; inline flow must not register/open settings. - expect(invoke).not.toHaveBeenCalledWith('request_screen_recording_access'); - expect(invoke).not.toHaveBeenCalledWith('open_screen_recording_settings'); - }); - - it('shows spinner while polling after opening screen recording settings', async () => { - setupPermissions(true); - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - // Button should be disabled/spinner state while polling for tcc grant - const btn = screen.getByRole('button', { - name: /checking|open screen recording settings/i, - }); - expect(btn).toBeDisabled(); - }); - - it('does not show quit and reopen immediately after clicking screen recording button', async () => { - setupPermissions(true); - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - // Should NOT show quit & reopen until tcc grant is detected - expect(screen.queryByRole('button', { name: /quit.*reopen/i })).toBeNull(); - }); - - it('keeps polling when screen recording tcc not yet granted', async () => { - let tccGranted = false; - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'check_accessibility_permission') return true; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'request_screen_recording_access') return; - if (cmd === 'open_screen_recording_settings') return; - if (cmd === 'check_screen_recording_tcc_granted') return tccGranted; - }); - - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - // First poll: still not granted - await act(async () => { - await vi.advanceTimersByTimeAsync(500); - }); - - expect(screen.queryByRole('button', { name: /quit.*reopen/i })).toBeNull(); - - // Grant it - tccGranted = true; - await act(async () => { - await vi.advanceTimersByTimeAsync(500); - }); - - expect( - screen.getByRole('button', { name: /quit.*reopen/i }), - ).toBeInTheDocument(); - }); - - it('shows quit and reopen after screen recording tcc grant is detected', async () => { - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'check_accessibility_permission') return true; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'request_screen_recording_access') return; - if (cmd === 'open_screen_recording_settings') return; - if (cmd === 'check_screen_recording_tcc_granted') return true; - }); - - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - await act(async () => { - await vi.advanceTimersByTimeAsync(500); - }); - - expect( - screen.getByRole('button', { name: /quit.*reopen/i }), - ).toBeInTheDocument(); - }); - - it('clicking quit and reopen invokes quit_and_relaunch', async () => { - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'check_accessibility_permission') return true; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'request_screen_recording_access') return; - if (cmd === 'open_screen_recording_settings') return; - if (cmd === 'check_screen_recording_tcc_granted') return true; - }); - - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - await act(async () => { - await vi.advanceTimersByTimeAsync(500); - }); - - await act(async () => { - fireEvent.click(screen.getByRole('button', { name: /quit.*reopen/i })); - }); - - expect(invoke).toHaveBeenCalledWith('quit_and_relaunch'); - }); - - it('shows screen recording step info', async () => { - setupPermissions(true); - render(); - await act(async () => {}); - - expect(screen.getByText('Screen Recording')).toBeInTheDocument(); - }); - - it('shows both steps regardless of current active step', async () => { - setupPermissions(false); - render(); - await act(async () => {}); - - expect(screen.getByText('Accessibility')).toBeInTheDocument(); - expect(screen.getByText('Screen Recording')).toBeInTheDocument(); - }); - - it('does not emit console.error when unmounted during accessibility polling', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - setupPermissions(false); - const { unmount } = render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - act(() => unmount()); - - // Timer ticks after unmount must not trigger React state-update warnings. - await act(async () => { - await vi.advanceTimersByTimeAsync(1000); - }); - - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); - }); - - it('does not emit console.error when unmounted during screen recording polling', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - - invoke.mockImplementation(async (cmd: string) => { - if (cmd === 'check_accessibility_permission') return true; - if (cmd === 'check_screen_recording_permission') return false; - if (cmd === 'request_screen_recording_access') return; - if (cmd === 'open_screen_recording_settings') return; - if (cmd === 'check_screen_recording_tcc_granted') return false; - }); - - const { unmount } = render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - act(() => unmount()); - - await act(async () => { - await vi.advanceTimersByTimeAsync(1000); - }); - - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); - }); - - it('hovering the CTA button applies brightness filter when enabled', async () => { - setupPermissions(false); - render(); - await act(async () => {}); - - const btn = screen.getByRole('button', { name: /grant accessibility/i }); - fireEvent.mouseEnter(btn); - // The button is not disabled so hovered=true applies brightness(1.1). - // Verify the element is still present and interactive (no errors thrown). - expect(btn).toBeInTheDocument(); - fireEvent.mouseLeave(btn); - expect(btn).toBeInTheDocument(); - }); - - it('hovering a disabled CTA button does not apply brightness filter', async () => { - setupPermissions(false); - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - // Button is now disabled/polling - const btn = screen.getByRole('button', { - name: /checking|grant accessibility/i, - }); - expect(btn).toBeDisabled(); - // mouseEnter on a disabled button must not toggle hovered state - fireEvent.mouseEnter(btn); - expect(btn).toBeDisabled(); - fireEvent.mouseLeave(btn); - expect(btn).toBeDisabled(); - }); - - // ─── Defensive guard coverage ───────────────────────────────────────────── - // The following tests exercise the early-return branches that protect against - // stale state updates and concurrent invocations. These branches cannot be - // reached through the happy-path tests because the invoke mock resolves - // synchronously; here we use deferred promises to keep invocations in-flight - // long enough to trigger each guard. - - it('ignores resume marker when component unmounts before mount-effect resolves', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - let resolveResume!: (v: string | null) => void; - invoke.mockImplementation((cmd: string) => { - if (cmd === 'consume_pending_grant_resume') - return new Promise((r) => { - resolveResume = r; - }); - return Promise.resolve(); - }); - - const { unmount } = render(); - // The mount IIFE awaits consume_pending_grant_resume first; it is - // suspended waiting for `resolveResume`. - - act(() => unmount()); // mountedRef → false - - await act(async () => { - resolveResume(null); // first guard fires; IIFE returns early - }); - - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); - }); - - it('ignores initial accessibility check result when component unmounts mid-flight', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - let resolveInitial!: (v: boolean) => void; - invoke.mockImplementation((cmd: string) => { - if (cmd === 'consume_pending_grant_resume') return Promise.resolve(null); - if (cmd === 'check_accessibility_permission') - return new Promise((r) => { - resolveInitial = r; - }); - return Promise.resolve(); - }); - - const { unmount } = render(); - // Drain the consume await so the IIFE advances to the - // check_accessibility_permission await and exposes resolveInitial. - await act(async () => {}); - - act(() => unmount()); // mountedRef → false - - await act(async () => { - resolveInitial(true); // post-AX guard fires; IIFE returns early - }); - - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); - }); - - it('ax in-flight guard prevents concurrent permission checks', async () => { - let pollCallCount = 0; - let resolveFirstPoll!: (v: boolean) => void; - invoke.mockImplementation((cmd: string) => { - if (cmd === 'check_accessibility_permission') { - pollCallCount++; - if (pollCallCount === 1) return Promise.resolve(false); // initial check - return new Promise((r) => { - resolveFirstPoll = r; - }); // poll hangs - } - if (cmd === 'open_accessibility_settings') return Promise.resolve(); - return Promise.resolve(); - }); - - render(); - await act(async () => {}); // initial check done - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - // First tick: callback starts, sets in-flight=true, invoke hangs. - // Second tick (while first is still in-flight): guard returns early. - act(() => { - vi.advanceTimersByTime(500); - vi.advanceTimersByTime(500); - }); - - // Only one poll call (initial was count=1, first poll was count=2; second - // tick was blocked - no count=3). - expect(pollCallCount).toBe(2); - - await act(async () => { - resolveFirstPoll(false); - }); - }); - - it('ignores ax poll result when component unmounts during in-flight check', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - let callCount = 0; - let resolvePoll!: (v: boolean) => void; - invoke.mockImplementation((cmd: string) => { - if (cmd === 'check_accessibility_permission') { - callCount++; - if (callCount === 1) return Promise.resolve(false); - return new Promise((r) => { - resolvePoll = r; - }); - } - if (cmd === 'open_accessibility_settings') return Promise.resolve(); - return Promise.resolve(); - }); - - const { unmount } = render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - // Fire one tick so the poll invoke is in-flight (hanging). - act(() => vi.advanceTimersByTime(500)); - - // Unmount while the invoke is still pending; this clears the interval but - // the in-flight promise is still alive. - act(() => unmount()); - - // Resolving the promise must not trigger a React state update. - await act(async () => { - resolvePoll(true); - }); - - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); - }); - - it('ignores accessibility handler when component unmounts during open-settings call', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - let resolveOpen!: (v?: unknown) => void; - invoke.mockImplementation((cmd: string) => { - if (cmd === 'consume_pending_grant_resume') return Promise.resolve(null); - if (cmd === 'reset_and_relaunch_for_grant') return Promise.resolve(false); - if (cmd === 'check_accessibility_permission') - return Promise.resolve(false); - if (cmd === 'check_screen_recording_permission') - return Promise.resolve(false); - if (cmd === 'open_accessibility_settings') - return new Promise((r) => { - resolveOpen = r; - }); - return Promise.resolve(); - }); - - const { unmount } = render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /grant accessibility/i }), - ); - }); - - // The click handler is now suspended inside startAccessibilityFlow on - // open_accessibility_settings; resolveOpen is set. - act(() => unmount()); - - await act(async () => { - resolveOpen(); // post-open mountedRef guard at PermissionsStep.tsx:192 fires - }); - - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); - }); - - it('ignores screen recording handler when component unmounts during open-settings call', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - let resolveOpen!: (v?: unknown) => void; - invoke.mockImplementation((cmd: string) => { - if (cmd === 'check_accessibility_permission') - return Promise.resolve(true); - if (cmd === 'request_screen_recording_access') return Promise.resolve(); - if (cmd === 'open_screen_recording_settings') - return new Promise((r) => { - resolveOpen = r; - }); // hangs - if (cmd === 'check_screen_recording_tcc_granted') - return Promise.resolve(false); - return Promise.resolve(); - }); - - const { unmount } = render(); - await act(async () => {}); // accessibility granted - - // Flush microtasks so the handler advances past the first await - // (request_screen_recording_access resolves) and suspends on the second - // (open_screen_recording_settings hangs), setting resolveOpen. - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - act(() => unmount()); // mountedRef → false - - await act(async () => { - resolveOpen(); // mountedRef guard at line 225 fires; returns early - }); - - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); - }); - - it('screen in-flight guard prevents concurrent tcc checks', async () => { - let tccCallCount = 0; - let resolveFirstPoll!: (v: boolean) => void; - invoke.mockImplementation((cmd: string) => { - if (cmd === 'check_accessibility_permission') - return Promise.resolve(true); - if (cmd === 'request_screen_recording_access') return Promise.resolve(); - if (cmd === 'open_screen_recording_settings') return Promise.resolve(); - if (cmd === 'check_screen_recording_tcc_granted') { - tccCallCount++; - return new Promise((r) => { - resolveFirstPoll = r; - }); - } - return Promise.resolve(); - }); - - render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - act(() => { - vi.advanceTimersByTime(500); // first tick: in-flight - vi.advanceTimersByTime(500); // second tick: guard blocks it - }); - - expect(tccCallCount).toBe(1); - - await act(async () => { - resolveFirstPoll(false); - }); - }); - - it('ignores screen poll result when component unmounts during in-flight tcc check', async () => { - const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - let resolvePoll!: (v: boolean) => void; - invoke.mockImplementation((cmd: string) => { - if (cmd === 'check_accessibility_permission') - return Promise.resolve(true); - if (cmd === 'request_screen_recording_access') return Promise.resolve(); - if (cmd === 'open_screen_recording_settings') return Promise.resolve(); - if (cmd === 'check_screen_recording_tcc_granted') - return new Promise((r) => { - resolvePoll = r; - }); - return Promise.resolve(); - }); - - const { unmount } = render(); - await act(async () => {}); - - await act(async () => { - fireEvent.click( - screen.getByRole('button', { name: /open screen recording settings/i }), - ); - }); - - act(() => vi.advanceTimersByTime(500)); // poll fires, invoke hangs - - act(() => unmount()); // clears interval; in-flight promise still alive - - await act(async () => { - resolvePoll(true); // mountedRef guard at line 234 fires; returns early - }); - - expect(errorSpy).not.toHaveBeenCalled(); - errorSpy.mockRestore(); - }); -}); diff --git a/src/view/onboarding/__tests__/PermissionsStep.test.tsx b/src/view/onboarding/__tests__/PermissionsStep.test.tsx index 7c760006..5350d30b 100644 --- a/src/view/onboarding/__tests__/PermissionsStep.test.tsx +++ b/src/view/onboarding/__tests__/PermissionsStep.test.tsx @@ -58,7 +58,6 @@ describe('PermissionsStep', () => { enableChannelCaptureWithResponses({ ...BASE_RESPONSES, reset_and_relaunch_for_grant: false, - open_accessibility_settings: undefined, }); render(); @@ -97,7 +96,6 @@ describe('PermissionsStep', () => { enableChannelCaptureWithResponses({ ...BASE_RESPONSES, reset_and_relaunch_for_grant: false, - open_accessibility_settings: undefined, }); render(); @@ -240,8 +238,8 @@ describe('PermissionsStep', () => { expect(invoke).toHaveBeenCalledWith('request_screen_recording_access'); expect(invoke).toHaveBeenCalledWith('open_screen_recording_settings'); expect( - screen.getAllByRole('button', { name: 'Checking...' }).length, - ).toBeGreaterThan(0); + screen.getByRole('button', { name: 'Checking...' }), + ).toBeInTheDocument(); }); it('shows Quit & Reopen once Screen Recording polling detects the permission, and it invokes quit_and_relaunch', async () => { @@ -272,8 +270,8 @@ describe('PermissionsStep', () => { await vi.advanceTimersByTimeAsync(500); }); expect( - screen.getAllByRole('button', { name: 'Checking...' }).length, - ).toBeGreaterThan(0); + screen.getByRole('button', { name: 'Checking...' }), + ).toBeInTheDocument(); screenGranted = true; await act(async () => { @@ -347,6 +345,10 @@ describe('PermissionsStep', () => { // resulting state update / follow-on call must be skipped rather than // throwing on an unmounted component. + afterEach(() => { + vi.useRealTimers(); + }); + it('skips the mount-effect state updates when unmounted before consume_pending_grant_resume resolves', async () => { let resolveResume: (value: string | null) => void = () => {}; invoke.mockImplementation(async (cmd: string) => { @@ -439,8 +441,6 @@ describe('PermissionsStep', () => { await vi.advanceTimersByTimeAsync(500); }); expect(invoke).not.toHaveBeenCalledWith('check_accessibility_permission'); - - vi.useRealTimers(); }); it('skips a poll tick while the previous Accessibility poll invoke is still in flight', async () => { @@ -485,8 +485,6 @@ describe('PermissionsStep', () => { resolveFirstCheck(false); await Promise.resolve(); }); - - vi.useRealTimers(); }); it('skips a poll-tick state update when unmounted while check_accessibility_permission is in flight', async () => { @@ -533,8 +531,6 @@ describe('PermissionsStep', () => { resolveGrantedCheck(true); await Promise.resolve(); }); - - vi.useRealTimers(); }); it('skips resetting Accessibility status when unmounted before reset_and_relaunch_for_grant resolves', async () => { @@ -612,8 +608,6 @@ describe('PermissionsStep', () => { expect(invoke).not.toHaveBeenCalledWith( 'check_screen_recording_tcc_granted', ); - - vi.useRealTimers(); }); it('skips a poll tick while the previous Screen Recording poll invoke is still in flight', async () => { @@ -662,8 +656,6 @@ describe('PermissionsStep', () => { resolveFirstCheck(false); await Promise.resolve(); }); - - vi.useRealTimers(); }); it('skips a poll-tick state update when unmounted while check_screen_recording_tcc_granted is in flight', async () => { @@ -714,8 +706,6 @@ describe('PermissionsStep', () => { resolveGrantedCheck(true); await Promise.resolve(); }); - - vi.useRealTimers(); }); it('skips resetting Screen Recording status when unmounted before reset_and_relaunch_for_grant resolves', async () => {