55 act ,
66 waitFor ,
77} from '@testing-library/react' ;
8- import { describe , it , expect , beforeEach } from 'vitest' ;
8+ import { describe , it , expect , beforeEach , vi } from 'vitest' ;
99import { invoke } from '@tauri-apps/api/core' ;
1010import { AboutTab } from './AboutTab' ;
1111
@@ -42,18 +42,21 @@ beforeEach(() => {
4242} ) ;
4343
4444describe ( 'AboutTab' , ( ) => {
45- it ( 'renders the Updates section with Current version and Last checked rows ' , async ( ) => {
45+ it ( 'renders the Updates hero showing up-to-date status and a check button ' , async ( ) => {
4646 render ( < AboutTab { ...SAMPLE_PROPS } /> ) ;
47- await waitFor ( ( ) => screen . getByText ( 'Current version' ) ) ;
48- expect ( screen . getByText ( 'Last checked' ) ) . toBeInTheDocument ( ) ;
47+ await waitFor ( ( ) =>
48+ expect ( screen . getByText ( 'Thuki is up to date' ) ) . toBeInTheDocument ( ) ,
49+ ) ;
4950 expect (
50- screen . getByRole ( 'button' , { name : / c h e c k n o w / i } ) ,
51+ screen . getByRole ( 'button' , { name : / c h e c k f o r u p d a t e s / i } ) ,
5152 ) . toBeInTheDocument ( ) ;
5253 } ) ;
5354
54- it ( 'shows Never for last checked when last_check_at_unix is null' , async ( ) => {
55+ it ( 'shows " Never checked for updates" when last_check_at_unix is null' , async ( ) => {
5556 render ( < AboutTab { ...SAMPLE_PROPS } /> ) ;
56- await waitFor ( ( ) => expect ( screen . getByText ( 'Never' ) ) . toBeInTheDocument ( ) ) ;
57+ await waitFor ( ( ) =>
58+ expect ( screen . getByText ( 'Never checked for updates' ) ) . toBeInTheDocument ( ) ,
59+ ) ;
5760 } ) ;
5861
5962 it ( 'shows relative time when last_check_at_unix is set' , async ( ) => {
@@ -70,21 +73,133 @@ describe('AboutTab', () => {
7073 } ) ;
7174 render ( < AboutTab { ...SAMPLE_PROPS } /> ) ;
7275 await waitFor ( ( ) =>
73- expect ( screen . getByText ( '2 minutes ago' ) ) . toBeInTheDocument ( ) ,
76+ expect (
77+ screen . getByText ( 'Last checked 2 minutes ago' ) ,
78+ ) . toBeInTheDocument ( ) ,
79+ ) ;
80+ } ) ;
81+
82+ it ( 'renders the available state when an update is pending' , async ( ) => {
83+ invokeMock . mockImplementation ( async ( cmd : string ) => {
84+ if ( cmd === 'get_updater_state' ) {
85+ return {
86+ last_check_at_unix : Math . floor ( Date . now ( ) / 1000 ) ,
87+ update : { version : '0.9.0' , notes_url : null } ,
88+ settings_snoozed_until : null ,
89+ chat_snoozed_until : null ,
90+ } ;
91+ }
92+ return defaultInvoke ( cmd ) ;
93+ } ) ;
94+ render ( < AboutTab { ...SAMPLE_PROPS } /> ) ;
95+ await waitFor ( ( ) =>
96+ expect ( screen . getByText ( 'Thuki 0.9.0 is ready' ) ) . toBeInTheDocument ( ) ,
7497 ) ;
7598 } ) ;
7699
77- it ( 'calls check_for_update when Check now clicked' , async ( ) => {
100+ it ( 'calls check_for_update when Check for updates is clicked' , async ( ) => {
78101 invokeMock . mockImplementation ( async ( cmd : string ) => defaultInvoke ( cmd ) ) ;
79102 render ( < AboutTab { ...SAMPLE_PROPS } /> ) ;
80- await waitFor ( ( ) => screen . getByRole ( 'button' , { name : / c h e c k n o w / i } ) ) ;
103+ await waitFor ( ( ) =>
104+ screen . getByRole ( 'button' , { name : / c h e c k f o r u p d a t e s / i } ) ,
105+ ) ;
81106 await act ( async ( ) => {
82- fireEvent . click ( screen . getByRole ( 'button' , { name : / c h e c k n o w / i } ) ) ;
107+ fireEvent . click (
108+ screen . getByRole ( 'button' , { name : / c h e c k f o r u p d a t e s / i } ) ,
109+ ) ;
83110 await Promise . resolve ( ) ;
84111 } ) ;
85112 expect ( invokeMock ) . toHaveBeenCalledWith ( 'check_for_update' ) ;
86113 } ) ;
87114
115+ it ( 'disables the button while checking and re-enables after the animation hold' , async ( ) => {
116+ vi . useFakeTimers ( { shouldAdvanceTime : true } ) ;
117+ try {
118+ invokeMock . mockImplementation ( async ( cmd : string ) => {
119+ if ( cmd === 'check_for_update' ) {
120+ return {
121+ last_check_at_unix : Math . floor ( Date . now ( ) / 1000 ) ,
122+ update : null ,
123+ settings_snoozed_until : null ,
124+ chat_snoozed_until : null ,
125+ } ;
126+ }
127+ return defaultInvoke ( cmd ) ;
128+ } ) ;
129+ render ( < AboutTab { ...SAMPLE_PROPS } /> ) ;
130+ await waitFor ( ( ) =>
131+ screen . getByRole ( 'button' , { name : / c h e c k f o r u p d a t e s / i } ) ,
132+ ) ;
133+ const btn = screen . getByRole ( 'button' , { name : / c h e c k f o r u p d a t e s / i } ) ;
134+ await act ( async ( ) => {
135+ fireEvent . click ( btn ) ;
136+ await Promise . resolve ( ) ;
137+ } ) ;
138+ expect ( btn ) . toHaveAttribute ( 'data-checking' , 'true' ) ;
139+ expect ( btn ) . toBeDisabled ( ) ;
140+
141+ // A second click while checking is a no-op.
142+ const callsBefore = invokeMock . mock . calls . filter (
143+ ( c : unknown [ ] ) => c [ 0 ] === 'check_for_update' ,
144+ ) . length ;
145+ await act ( async ( ) => {
146+ fireEvent . click ( btn ) ;
147+ await Promise . resolve ( ) ;
148+ } ) ;
149+ const callsAfter = invokeMock . mock . calls . filter (
150+ ( c : unknown [ ] ) => c [ 0 ] === 'check_for_update' ,
151+ ) . length ;
152+ expect ( callsAfter ) . toBe ( callsBefore ) ;
153+
154+ // Advance past the animation hold so the timer callback resets state.
155+ await act ( async ( ) => {
156+ vi . advanceTimersByTime ( 1200 ) ;
157+ await Promise . resolve ( ) ;
158+ } ) ;
159+ expect ( btn ) . toHaveAttribute ( 'data-checking' , 'false' ) ;
160+ expect ( btn ) . not . toBeDisabled ( ) ;
161+ } finally {
162+ vi . useRealTimers ( ) ;
163+ }
164+ } ) ;
165+
166+ it ( 'clears the pending animation timer on unmount' , async ( ) => {
167+ vi . useFakeTimers ( { shouldAdvanceTime : true } ) ;
168+ try {
169+ invokeMock . mockImplementation ( async ( cmd : string ) => {
170+ if ( cmd === 'check_for_update' ) {
171+ return {
172+ last_check_at_unix : Math . floor ( Date . now ( ) / 1000 ) ,
173+ update : null ,
174+ settings_snoozed_until : null ,
175+ chat_snoozed_until : null ,
176+ } ;
177+ }
178+ return defaultInvoke ( cmd ) ;
179+ } ) ;
180+ const { unmount } = render ( < AboutTab { ...SAMPLE_PROPS } /> ) ;
181+ await waitFor ( ( ) =>
182+ screen . getByRole ( 'button' , { name : / c h e c k f o r u p d a t e s / i } ) ,
183+ ) ;
184+ await act ( async ( ) => {
185+ fireEvent . click (
186+ screen . getByRole ( 'button' , { name : / c h e c k f o r u p d a t e s / i } ) ,
187+ ) ;
188+ await Promise . resolve ( ) ;
189+ } ) ;
190+ // Unmount while the post-check timer is still pending. The cleanup
191+ // effect must clear it; otherwise vitest fake timers would still hold
192+ // a queued callback on unmount.
193+ unmount ( ) ;
194+ await act ( async ( ) => {
195+ vi . advanceTimersByTime ( 2000 ) ;
196+ await Promise . resolve ( ) ;
197+ } ) ;
198+ } finally {
199+ vi . useRealTimers ( ) ;
200+ }
201+ } ) ;
202+
88203 it ( 'renders the Permissions section' , async ( ) => {
89204 render ( < AboutTab { ...SAMPLE_PROPS } /> ) ;
90205 await waitFor ( ( ) => screen . getByText ( 'Accessibility' ) ) ;
0 commit comments