1+ import { beforeEach , describe , expect , it , jest } from '@jest/globals'
2+ import { namedNode } from 'rdflib'
3+ import { authSession , authn } from 'solid-logic'
4+ import { PeopleSearch } from './PeopleSearch'
5+ import './index'
6+ import ns from '../../../../ns'
7+
8+ jest . mock ( '@solid-data-modules/contacts-rdflib' , ( ) => ( {
9+ __esModule : true ,
10+ default : jest . fn ( ) . mockImplementation ( ( ) => ( {
11+ listAddressBooks : jest . fn ( async ( ) => ( { publicUris : [ ] , privateUris : [ ] } ) ) ,
12+ readAddressBook : jest . fn ( async ( ) => ( { contacts : [ ] } ) )
13+ } ) )
14+ } ) )
15+
16+ jest . mock ( 'solid-logic' , ( ) => ( {
17+ authSession : {
18+ events : {
19+ on : jest . fn ( ) ,
20+ off : jest . fn ( )
21+ }
22+ } ,
23+ authn : {
24+ currentUser : jest . fn ( )
25+ } ,
26+ solidLogicSingleton : {
27+ store : null
28+ }
29+ } ) )
30+
31+ const mockCurrentUser = authn . currentUser as jest . Mock
32+ const mockOn = authSession . events . on as jest . Mock
33+ const mockOff = authSession . events . off as jest . Mock
34+
35+ function getPortalRoot ( ) {
36+ const portalHost = document . querySelector ( '[data-solid-ui-combobox-portal]' ) as HTMLDivElement | null
37+ return portalHost ?. shadowRoot ?? null
38+ }
39+
40+ async function flushUpdates ( ) {
41+ await Promise . resolve ( )
42+ await Promise . resolve ( )
43+ await Promise . resolve ( )
44+ }
45+
46+ describe ( 'SolidUIPeopleSearch' , ( ) => {
47+ beforeEach ( ( ) => {
48+ document . body . innerHTML = ''
49+ mockCurrentUser . mockReset ( )
50+ mockOn . mockReset ( )
51+ mockOff . mockReset ( )
52+ ; ( globalThis as typeof globalThis & { fetch ?: typeof fetch } ) . fetch = undefined
53+ } )
54+
55+ it ( 'is defined as a custom element' , ( ) => {
56+ expect ( customElements . get ( 'solid-ui-people-search' ) ) . toBe ( PeopleSearch )
57+ } )
58+
59+ it ( 'shows a sign-in message when no user is authenticated' , async ( ) => {
60+ mockCurrentUser . mockReturnValue ( null )
61+
62+ const peopleSearch = new PeopleSearch ( )
63+ document . body . appendChild ( peopleSearch )
64+ await peopleSearch . updateComplete
65+
66+ const status = peopleSearch . shadowRoot ?. querySelector ( '.status' ) as HTMLElement
67+ const combobox = peopleSearch . shadowRoot ?. querySelector ( 'solid-ui-combobox' ) as HTMLElement
68+
69+ expect ( combobox ) . not . toBeNull ( )
70+ expect ( status . textContent ) . toContain ( 'Sign in to search contacts.' )
71+ expect ( mockOn ) . toHaveBeenCalledWith ( 'login' , expect . any ( Function ) )
72+ expect ( mockOn ) . toHaveBeenCalledWith ( 'logout' , expect . any ( Function ) )
73+ } )
74+
75+ it ( 'loads FOAF suggestions and emits person-select with relationship details' , async ( ) => {
76+ const me = namedNode ( 'https://example.com/profile/card#me' )
77+ const friend = namedNode ( 'https://alice.example/profile/card#me' )
78+
79+ mockCurrentUser . mockReturnValue ( me )
80+
81+ const store = {
82+ fetcher : {
83+ load : jest . fn ( async ( ) => undefined )
84+ } ,
85+ updater : { } ,
86+ each : jest . fn ( ( subject : NamedNode , predicate : NamedNode ) => {
87+ if ( subject . value === me . value && predicate . value === ns . foaf ( 'knows' ) . value ) {
88+ return [ friend ]
89+ }
90+ return [ ]
91+ } ) ,
92+ any : jest . fn ( ( subject : NamedNode , predicate : NamedNode ) => {
93+ if ( subject . value === friend . value && predicate . value === ns . foaf ( 'name' ) . value ) {
94+ return { value : 'Alice Example' }
95+ }
96+ return null
97+ } ) ,
98+ anyValue : jest . fn ( ( ) => null )
99+ } as any
100+
101+ const openSpy = jest . spyOn ( window , 'open' ) . mockReturnValue ( null )
102+ const selected = jest . fn ( )
103+
104+ const peopleSearch = new PeopleSearch ( )
105+ peopleSearch . store = store
106+ peopleSearch . openProfilesOnSelect = false
107+ peopleSearch . addEventListener ( 'person-select' , ( event : Event ) => {
108+ selected ( ( event as CustomEvent ) . detail )
109+ } )
110+
111+ document . body . appendChild ( peopleSearch )
112+ await peopleSearch . updateComplete
113+ await flushUpdates ( )
114+ await peopleSearch . updateComplete
115+
116+ const combobox = peopleSearch . shadowRoot ?. querySelector ( 'solid-ui-combobox' ) as any
117+ const input = combobox . shadowRoot ?. querySelector ( 'input.text-input' ) as HTMLInputElement
118+
119+ input . value = 'Alice'
120+ input . dispatchEvent ( new Event ( 'input' , { bubbles : true , composed : true } ) )
121+ await flushUpdates ( )
122+ await combobox . updateComplete
123+
124+ const portalRoot = getPortalRoot ( )
125+ const options = portalRoot ?. querySelectorAll ( '[role="option"]' ) as NodeListOf < HTMLElement >
126+
127+ expect ( options ) . toHaveLength ( 1 )
128+ expect ( options [ 0 ] . textContent ) . toContain ( 'Alice Example' )
129+ expect ( options [ 0 ] . textContent ) . toContain ( 'Friend' )
130+
131+ options [ 0 ] . click ( )
132+ await flushUpdates ( )
133+
134+ expect ( selected ) . toHaveBeenCalledWith ( {
135+ person : {
136+ name : 'Alice Example' ,
137+ webId : 'https://alice.example/profile/card#me' ,
138+ relationshipLabel : 'Friend'
139+ }
140+ } )
141+ expect ( openSpy ) . not . toHaveBeenCalled ( )
142+
143+ openSpy . mockRestore ( )
144+ } )
145+ } )
0 commit comments