@@ -1503,23 +1503,37 @@ describe("RepoSelector — frozen org order", () => {
15031503 }
15041504
15051505 // Flat (non-accordion) mode only: org names appear as plain text nodes in headers.
1506- // In accordion mode, org names are inside button triggers — use different query selectors (see S6) .
1506+ // In accordion mode, org names are inside button triggers — use getAccordionOrder below .
15071507 function getOrgHeaderOrder ( orgNames : string [ ] ) : string [ ] {
1508- const pattern = new RegExp ( `^(${ orgNames . join ( "|" ) } )$` ) ;
1508+ const escaped = orgNames . map ( ( n ) => n . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ) ;
1509+ const pattern = new RegExp ( `^(${ escaped . join ( "|" ) } )$` ) ;
15091510 return screen . getAllByText ( pattern ) . map ( ( el ) => el . textContent ! ) ;
15101511 }
15111512
1513+ // Accordion mode: org names are inside button triggers — sort by DOM position.
1514+ function getAccordionOrder ( orgNames : string [ ] ) : string [ ] {
1515+ return orgNames
1516+ . map ( ( name ) => ( { name, btn : screen . getByRole ( "button" , { name : new RegExp ( name . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, "\\$&" ) ) } ) } ) )
1517+ . sort ( ( a , b ) => {
1518+ const pos = a . btn . compareDocumentPosition ( b . btn ) ;
1519+ return pos & Node . DOCUMENT_POSITION_FOLLOWING ? - 1 : 1 ;
1520+ } )
1521+ . map ( ( { name } ) => name ) ;
1522+ }
1523+
15121524 beforeEach ( ( ) => {
15131525 vi . clearAllMocks ( ) ;
15141526 vi . restoreAllMocks ( ) ;
15151527 } ) ;
15161528
1529+ const aliceEntry = { login : "alice" , avatarUrl : "" , type : "user" as const } ;
1530+ const acmeEntry = { login : "acme-corp" , avatarUrl : "" , type : "org" as const } ;
1531+ const betaEntry = { login : "beta-org" , avatarUrl : "" , type : "org" as const } ;
1532+ const deltaEntry = { login : "delta-inc" , avatarUrl : "" , type : "org" as const } ;
1533+ const aaaEntry = { login : "aaa-org" , avatarUrl : "" , type : "org" as const } ;
1534+
15171535 // S1: Org order remains stable after repo retry
15181536 it ( "org order remains stable after repo retry for a failed org" , async ( ) => {
1519- const aliceEntry = { login : "alice" , avatarUrl : "" , type : "user" as const } ;
1520- const acmeEntry = { login : "acme-corp" , avatarUrl : "" , type : "org" as const } ;
1521- const betaEntry = { login : "beta-org" , avatarUrl : "" , type : "org" as const } ;
1522-
15231537 vi . mocked ( api . fetchRepos )
15241538 . mockImplementation ( ( _client , org ) => {
15251539 if ( org === "beta-org" ) return Promise . reject ( new Error ( "beta load failed" ) ) ;
@@ -1564,10 +1578,6 @@ describe("RepoSelector — frozen org order", () => {
15641578
15651579 // S2: Org order remains stable when toggling a repo checkbox
15661580 it ( "org order remains stable when toggling a repo checkbox" , async ( ) => {
1567- const aliceEntry = { login : "alice" , avatarUrl : "" , type : "user" as const } ;
1568- const acmeEntry = { login : "acme-corp" , avatarUrl : "" , type : "org" as const } ;
1569- const betaEntry = { login : "beta-org" , avatarUrl : "" , type : "org" as const } ;
1570-
15711581 vi . mocked ( api . fetchRepos ) . mockImplementation ( ( _client , org ) =>
15721582 Promise . resolve ( makeOrgRepos ( org as string ) )
15731583 ) ;
@@ -1608,10 +1618,6 @@ describe("RepoSelector — frozen org order", () => {
16081618 it ( "frozen order is invalidated and re-sorted when a new org is added" , async ( ) => {
16091619 const { createSignal } = await import ( "solid-js" ) ;
16101620
1611- const aliceEntry = { login : "alice" , avatarUrl : "" , type : "user" as const } ;
1612- const deltaEntry = { login : "delta-inc" , avatarUrl : "" , type : "org" as const } ;
1613- const acmeEntry = { login : "acme-corp" , avatarUrl : "" , type : "org" as const } ;
1614-
16151621 vi . mocked ( api . fetchRepos ) . mockImplementation ( ( _client , org ) =>
16161622 Promise . resolve ( makeOrgRepos ( org as string ) )
16171623 ) ;
@@ -1683,20 +1689,10 @@ describe("RepoSelector — frozen org order", () => {
16831689 } ) ;
16841690
16851691 // Verify initial sorted order via accordion trigger buttons
1686- const getAccordionOrder = ( orgNames : string [ ] ) =>
1687- orgNames
1688- . map ( ( name ) => ( { name, btn : screen . getByRole ( "button" , { name : new RegExp ( name ) } ) } ) )
1689- . sort ( ( a , b ) => {
1690- const pos = a . btn . compareDocumentPosition ( b . btn ) ;
1691- return pos & Node . DOCUMENT_POSITION_FOLLOWING ? - 1 : 1 ;
1692- } )
1693- . map ( ( { name } ) => name ) ;
1694-
16951692 const initialOrder = getAccordionOrder ( startOrgs ) ;
16961693 expect ( initialOrder ) . toEqual ( [ "alice" , "acme-corp" , "charlie-co" , "delta-inc" , "echo-labs" , "foxtrot-io" ] ) ;
16971694
16981695 // Add beta-org (7 total)
1699- const betaEntry = { login : "beta-org" , avatarUrl : "" , type : "org" as const } ;
17001696 const newOrgs = [ "alice" , "acme-corp" , "beta-org" , "charlie-co" , "delta-inc" , "echo-labs" , "foxtrot-io" ] ;
17011697 setSelectedOrgs ( newOrgs ) ;
17021698 setOrgEntries ( [ ...startEntries , betaEntry ] ) ;
@@ -1747,15 +1743,6 @@ describe("RepoSelector — frozen org order", () => {
17471743 } ) ;
17481744
17491745 // Verify initial sorted order via accordion trigger buttons
1750- const getAccordionOrder = ( orgNames : string [ ] ) =>
1751- orgNames
1752- . map ( ( name ) => ( { name, btn : screen . getByRole ( "button" , { name : new RegExp ( name ) } ) } ) )
1753- . sort ( ( a , b ) => {
1754- const pos = a . btn . compareDocumentPosition ( b . btn ) ;
1755- return pos & Node . DOCUMENT_POSITION_FOLLOWING ? - 1 : 1 ;
1756- } )
1757- . map ( ( { name } ) => name ) ;
1758-
17591746 const initialOrder = getAccordionOrder ( sevenOrgs ) ;
17601747 expect ( initialOrder ) . toEqual ( [ "alice" , "acme-corp" , "beta-org" , "charlie-co" , "delta-inc" , "echo-labs" , "foxtrot-io" ] ) ;
17611748
@@ -1786,11 +1773,6 @@ describe("RepoSelector — frozen org order", () => {
17861773 it ( "frozen order is invalidated when orgs are simultaneously added and removed" , async ( ) => {
17871774 const { createSignal } = await import ( "solid-js" ) ;
17881775
1789- const aliceEntry = { login : "alice" , avatarUrl : "" , type : "user" as const } ;
1790- const acmeEntry = { login : "acme-corp" , avatarUrl : "" , type : "org" as const } ;
1791- const deltaEntry = { login : "delta-inc" , avatarUrl : "" , type : "org" as const } ;
1792- const aaaEntry = { login : "aaa-org" , avatarUrl : "" , type : "org" as const } ;
1793-
17941776 vi . mocked ( api . fetchRepos ) . mockImplementation ( ( _client , org ) =>
17951777 Promise . resolve ( makeOrgRepos ( org as string ) )
17961778 ) ;
@@ -1839,10 +1821,6 @@ describe("RepoSelector — frozen org order", () => {
18391821 it ( "removing an org invalidates frozen order and re-sorts correctly" , async ( ) => {
18401822 const { createSignal } = await import ( "solid-js" ) ;
18411823
1842- const aliceEntry = { login : "alice" , avatarUrl : "" , type : "user" as const } ;
1843- const acmeEntry = { login : "acme-corp" , avatarUrl : "" , type : "org" as const } ;
1844- const deltaEntry = { login : "delta-inc" , avatarUrl : "" , type : "org" as const } ;
1845-
18461824 vi . mocked ( api . fetchRepos ) . mockImplementation ( ( _client , org ) =>
18471825 Promise . resolve ( makeOrgRepos ( org as string ) )
18481826 ) ;
@@ -1884,4 +1862,67 @@ describe("RepoSelector — frozen org order", () => {
18841862 // delta-inc repo content should also be gone from the display
18851863 expect ( screen . queryByText ( "delta-inc-repo" ) ) . toBeNull ( ) ;
18861864 } ) ;
1865+
1866+ // S8: Adding 2+ orgs simultaneously with staggered loading produces correct final sort
1867+ // Verifies the invalidation→loadedCount guard interaction during trickle-in:
1868+ // after frozenOrder is nulled (key changed), sorting is deferred until all new orgs settle.
1869+ it ( "adding 2+ orgs simultaneously with staggered loading produces correct final sorted order" , async ( ) => {
1870+ const { createSignal } = await import ( "solid-js" ) ;
1871+
1872+ let resolveEcho ! : ( repos : RepoEntry [ ] ) => void ;
1873+ let resolveFoxtrot ! : ( repos : RepoEntry [ ] ) => void ;
1874+ const echoPending = new Promise < RepoEntry [ ] > ( ( res ) => { resolveEcho = res ; } ) ;
1875+ const foxtrotPending = new Promise < RepoEntry [ ] > ( ( res ) => { resolveFoxtrot = res ; } ) ;
1876+
1877+ vi . mocked ( api . fetchRepos ) . mockImplementation ( ( _client , org ) => {
1878+ if ( org === "echo-labs" ) return echoPending ;
1879+ if ( org === "foxtrot-io" ) return foxtrotPending ;
1880+ return Promise . resolve ( makeOrgRepos ( org as string ) ) ;
1881+ } ) ;
1882+
1883+ const [ selectedOrgs , setSelectedOrgs ] = createSignal < string [ ] > ( [ "alice" , "acme-corp" ] ) ;
1884+ const [ orgEntries , setOrgEntries ] = createSignal ( [ aliceEntry , acmeEntry ] ) ;
1885+
1886+ render ( ( ) => (
1887+ < RepoSelector
1888+ selectedOrgs = { selectedOrgs ( ) }
1889+ orgEntries = { orgEntries ( ) }
1890+ selected = { [ ] }
1891+ onChange = { vi . fn ( ) }
1892+ />
1893+ ) ) ;
1894+
1895+ // Wait for initial 2 orgs to load and freeze
1896+ await waitFor ( ( ) => {
1897+ screen . getByText ( "alice-repo" ) ;
1898+ screen . getByText ( "acme-corp-repo" ) ;
1899+ } ) ;
1900+
1901+ const initialOrder = getOrgHeaderOrder ( [ "alice" , "acme-corp" ] ) ;
1902+ expect ( initialOrder ) . toEqual ( [ "alice" , "acme-corp" ] ) ;
1903+
1904+ // Simultaneously add echo-labs and foxtrot-io (both slow)
1905+ const echoEntry = { login : "echo-labs" , avatarUrl : "" , type : "org" as const } ;
1906+ const foxtrotEntry = { login : "foxtrot-io" , avatarUrl : "" , type : "org" as const } ;
1907+ setSelectedOrgs ( [ "alice" , "acme-corp" , "echo-labs" , "foxtrot-io" ] ) ;
1908+ setOrgEntries ( [ aliceEntry , acmeEntry , echoEntry , foxtrotEntry ] ) ;
1909+
1910+ // Resolve echo-labs first — foxtrot-io is still loading.
1911+ // loadedCount (3 settled out of 4) < selectedOrgs.length (4),
1912+ // so the loadedCount guard should prevent a premature sort.
1913+ resolveEcho ( makeOrgRepos ( "echo-labs" ) ) ;
1914+ await waitFor ( ( ) => {
1915+ screen . getByText ( "echo-labs-repo" ) ;
1916+ } ) ;
1917+
1918+ // Now resolve foxtrot-io — all 4 orgs settled, sort should fire and freeze.
1919+ resolveFoxtrot ( makeOrgRepos ( "foxtrot-io" ) ) ;
1920+ await waitFor ( ( ) => {
1921+ screen . getByText ( "foxtrot-io-repo" ) ;
1922+ } ) ;
1923+
1924+ // Final order: alice (user first), then alphabetical: acme-corp, echo-labs, foxtrot-io
1925+ const finalOrder = getOrgHeaderOrder ( [ "alice" , "acme-corp" , "echo-labs" , "foxtrot-io" ] ) ;
1926+ expect ( finalOrder ) . toEqual ( [ "alice" , "acme-corp" , "echo-labs" , "foxtrot-io" ] ) ;
1927+ } ) ;
18871928} ) ;
0 commit comments