11import { describe , it , expect , vi , beforeEach } from "vitest" ;
2- import { render , screen , fireEvent , waitFor } from "@solidjs/testing-library" ;
2+ import { render , screen , waitFor } from "@solidjs/testing-library" ;
33import userEvent from "@testing-library/user-event" ;
44import type { RepoRef , RepoEntry } from "../../../src/app/services/api" ;
55
@@ -38,6 +38,7 @@ const otherorgRepos: RepoEntry[] = [
3838describe ( "RepoSelector" , ( ) => {
3939 beforeEach ( ( ) => {
4040 vi . clearAllMocks ( ) ;
41+ vi . restoreAllMocks ( ) ;
4142 } ) ;
4243
4344 it ( "shows loading while fetching repos" , async ( ) => {
@@ -100,6 +101,7 @@ describe("RepoSelector", () => {
100101 } ) ;
101102
102103 it ( "filters repos by text input" , async ( ) => {
104+ const user = userEvent . setup ( ) ;
103105 vi . mocked ( api . fetchRepos ) . mockResolvedValue ( myorgRepos ) ;
104106
105107 render ( ( ) => (
@@ -111,7 +113,7 @@ describe("RepoSelector", () => {
111113 } ) ;
112114
113115 const filterInput = screen . getByPlaceholderText ( / F i l t e r r e p o s / i) ;
114- fireEvent . input ( filterInput , { target : { value : "repo-a" } } ) ;
116+ await user . type ( filterInput , "repo-a" ) ;
115117
116118 await waitFor ( ( ) => {
117119 screen . getByText ( "repo-a" ) ;
@@ -132,9 +134,8 @@ describe("RepoSelector", () => {
132134 screen . getByText ( "repo-a" ) ;
133135 } ) ;
134136
135- // "Select All" button in the org header (there may be multiple — use the first one)
137+ // With a single org: [global Select All, per-org Select All] — click the per-org (last) one
136138 const selectAllBtns = screen . getAllByText ( "Select All" ) ;
137- // The per-org one is inside the org group; for a single org there's only one
138139 await user . click ( selectAllBtns [ selectAllBtns . length - 1 ] ) ;
139140
140141 expect ( onChange ) . toHaveBeenCalled ( ) ;
@@ -215,13 +216,14 @@ describe("RepoSelector", () => {
215216 } ) ;
216217
217218 it ( "shows relative time next to each repo" , async ( ) => {
219+ vi . spyOn ( Date , "now" ) . mockReturnValue ( new Date ( "2026-03-24T12:00:00Z" ) . getTime ( ) ) ;
218220 vi . mocked ( api . fetchRepos ) . mockResolvedValue ( myorgRepos ) ;
219221 render ( ( ) => (
220222 < RepoSelector selectedOrgs = { [ "myorg" ] } selected = { [ ] } onChange = { vi . fn ( ) } />
221223 ) ) ;
222224 await waitFor ( ( ) => {
223- const labels = screen . getAllByText ( / a g o | y e s t e r d a y | j u s t n o w | l a s t / i ) ;
224- expect ( labels . length ) . toBeGreaterThanOrEqual ( 2 ) ;
225+ screen . getByText ( "4 days ago" ) ;
226+ screen . getByText ( "2 days ago" ) ;
225227 } ) ;
226228 } ) ;
227229
@@ -261,4 +263,53 @@ describe("RepoSelector", () => {
261263 } ) ;
262264 expect ( screen . queryByText ( / a g o | y e s t e r d a y | j u s t n o w | l a s t / i) ) . toBeNull ( ) ;
263265 } ) ;
266+
267+ it ( "global Select All strips pushedAt from onChange payload" , async ( ) => {
268+ const user = userEvent . setup ( ) ;
269+ vi . mocked ( api . fetchRepos ) . mockImplementation ( ( _client , org ) => {
270+ if ( org === "myorg" ) return Promise . resolve ( myorgRepos ) ;
271+ return Promise . resolve ( otherorgRepos ) ;
272+ } ) ;
273+ const onChange = vi . fn ( ) ;
274+ render ( ( ) => (
275+ < RepoSelector selectedOrgs = { [ "myorg" , "otherog" ] } selected = { [ ] } onChange = { onChange } />
276+ ) ) ;
277+ await waitFor ( ( ) => {
278+ screen . getByText ( "repo-a" ) ;
279+ screen . getByText ( "repo-c" ) ;
280+ } ) ;
281+ // The first "Select All" button is the global one in the header
282+ const selectAllBtns = screen . getAllByText ( "Select All" ) ;
283+ await user . click ( selectAllBtns [ 0 ] ) ;
284+ expect ( onChange ) . toHaveBeenCalled ( ) ;
285+ const result = onChange . mock . calls [ 0 ] [ 0 ] as RepoRef [ ] ;
286+ expect ( result . length ) . toBe ( 3 ) ;
287+ for ( const r of result ) {
288+ expect ( r ) . not . toHaveProperty ( "pushedAt" ) ;
289+ }
290+ } ) ;
291+
292+ it ( "preserves org order when all repos have null pushedAt" , async ( ) => {
293+ const nullOrg1 : RepoEntry [ ] = [
294+ { owner : "stale-org" , name : "null-repo-1" , fullName : "stale-org/null-repo-1" , pushedAt : null } ,
295+ ] ;
296+ const nullOrg2 : RepoEntry [ ] = [
297+ { owner : "active-org" , name : "null-repo-2" , fullName : "active-org/null-repo-2" , pushedAt : null } ,
298+ ] ;
299+ vi . mocked ( api . fetchRepos ) . mockImplementation ( ( _client , org ) => {
300+ if ( org === "stale-org" ) return Promise . resolve ( nullOrg1 ) ;
301+ return Promise . resolve ( nullOrg2 ) ;
302+ } ) ;
303+ render ( ( ) => (
304+ < RepoSelector selectedOrgs = { [ "stale-org" , "active-org" ] } selected = { [ ] } onChange = { vi . fn ( ) } />
305+ ) ) ;
306+ await waitFor ( ( ) => {
307+ screen . getByText ( "null-repo-1" ) ;
308+ screen . getByText ( "null-repo-2" ) ;
309+ } ) ;
310+ const orgHeaders = screen . getAllByText ( / ^ ( a c t i v e - o r g | s t a l e - o r g ) $ / ) ;
311+ // Both have null pushedAt → comparator returns 0 → original order preserved
312+ expect ( orgHeaders [ 0 ] . textContent ) . toBe ( "stale-org" ) ;
313+ expect ( orgHeaders [ 1 ] . textContent ) . toBe ( "active-org" ) ;
314+ } ) ;
264315} ) ;
0 commit comments