Skip to content

Commit f853c89

Browse files
authored
Merge pull request #752 from SolidOS/ordered
add tests on Ordered Multiple/Collection with rdflib v2.3.8
2 parents e96a640 + e17e1f5 commit f853c89

3 files changed

Lines changed: 180 additions & 5 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
"uuid": "^13.0.2"
100100
},
101101
"peerDependencies": {
102-
"rdflib": "^2.3.7",
102+
"rdflib": "^2.3.8",
103103
"solid-logic": "^4.0.7"
104104
},
105105
"devDependencies": {
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import { silenceDebugMessages } from '../../helpers/debugger'
2+
import { Collection, literal, namedNode } from 'rdflib'
3+
import ns from '../../../../src/ns'
4+
import { store } from 'solid-logic'
5+
import { field } from '../../../../src/widgets/forms'
6+
import { clearStore } from '../../helpers/clearStore'
7+
8+
silenceDebugMessages()
9+
afterEach(clearStore)
10+
11+
const docUri = 'http://example.com/doc.ttl'
12+
const doc = namedNode(docUri)
13+
const subject = namedNode('http://example.com/#person')
14+
const form = namedNode('http://example.com/#multipleForm')
15+
const subform = namedNode('http://example.com/#subForm')
16+
const property = namedNode('http://schema.org/knowsLanguage')
17+
const xsdBoolean = namedNode('http://www.w3.org/2001/XMLSchema#boolean')
18+
19+
/** Helper: return all list-head triples for subject/property in the test document */
20+
function getListHeads () {
21+
return store.each(subject, property, null as any, doc)
22+
}
23+
24+
/** Helper: wait for pending microtasks and short async operations to settle */
25+
function waitForAsync () {
26+
return new Promise(resolve => setTimeout(resolve, 10))
27+
}
28+
29+
/** Set up the minimum store triples needed for an ordered Multiple field */
30+
function setupOrderedMultipleForm () {
31+
store.add(form, ns.rdf('type'), ns.ui('Multiple'), doc)
32+
store.add(form, ns.ui('property'), property, doc)
33+
// ui:ordered true as a proper xsd:boolean literal
34+
store.add(form, ns.ui('ordered'), literal('true', undefined, xsdBoolean), doc)
35+
store.add(form, ns.ui('part'), subform, doc)
36+
// Subform: an empty Group (no parts)
37+
store.add(subform, ns.rdf('type'), ns.ui('Group'), doc)
38+
store.add(subform, ns.ui('parts'), new Collection([]), doc)
39+
}
40+
41+
/** Render the Multiple field and return {box, body} */
42+
function renderMultipleField () {
43+
const container = document.createElement('div')
44+
const box = field[ns.ui('Multiple').uri](
45+
document, container, {}, subject, form, doc, jest.fn()
46+
) as HTMLElement
47+
// body is the first child div of box, and has the refresh method attached
48+
const body = box.firstChild as HTMLElement & { refresh?: () => void }
49+
return { box, body, container }
50+
}
51+
52+
describe('Multiple ordered field', () => {
53+
describe('createListIfNecessary: recovers existing Collection from store', () => {
54+
it('uses an existing Collection in the store instead of creating a duplicate', () => {
55+
setupOrderedMultipleForm()
56+
57+
// Pre-populate the store with an existing Collection as list head
58+
const existingCollection = new Collection([])
59+
store.add(subject, property, existingCollection, doc)
60+
61+
renderMultipleField()
62+
63+
// After rendering, there should still be exactly ONE list head
64+
const heads = getListHeads()
65+
expect(heads.length).toBe(1)
66+
expect(heads[0]).toBe(existingCollection)
67+
})
68+
69+
it('creates no new list head when no items exist yet (list created lazily on add)', () => {
70+
setupOrderedMultipleForm()
71+
72+
renderMultipleField()
73+
74+
// No list head should exist until the user adds an item
75+
expect(getListHeads().length).toBe(0)
76+
})
77+
})
78+
79+
describe('refresh: syncs list variable to store', () => {
80+
it('exposes a refresh method on the body element for live updates', () => {
81+
setupOrderedMultipleForm()
82+
83+
const { body } = renderMultipleField()
84+
85+
expect(typeof body.refresh).toBe('function')
86+
})
87+
88+
it('renders existing list items from a pre-populated Collection', () => {
89+
setupOrderedMultipleForm()
90+
91+
const item1 = namedNode('http://example.com/#item1')
92+
const item2 = namedNode('http://example.com/#item2')
93+
const col = new Collection([item1, item2])
94+
store.add(subject, property, col, doc)
95+
96+
renderMultipleField()
97+
98+
// The list head should remain intact with the original 2 elements
99+
const heads = getListHeads()
100+
expect(heads.length).toBe(1)
101+
expect((heads[0] as Collection).elements.length).toBe(2)
102+
})
103+
104+
it('keeps the list in sync when refresh is called after a simulated document reload', () => {
105+
setupOrderedMultipleForm()
106+
107+
const originalCollection = new Collection([namedNode('http://example.com/#item1')])
108+
store.add(subject, property, originalCollection, doc)
109+
110+
const { body } = renderMultipleField()
111+
112+
// Simulate a document reload: add a SECOND Collection (the bug scenario).
113+
// This happens when putBack invalidates the fetch cache and a subsequent
114+
// updateMany triggers a re-fetch, which adds a new Collection to the store
115+
// without removing the old one.
116+
const reloadedCollection = new Collection([namedNode('http://example.com/#item1')])
117+
store.add(subject, property, reloadedCollection, doc)
118+
119+
expect(getListHeads().length).toBe(2)
120+
121+
// After refresh, the field's internal list should be synced to one of the collections.
122+
// The refresh function itself does not remove duplicates — that happens in saveListThenRefresh.
123+
body.refresh!()
124+
125+
// Both heads still exist (removal happens during save)
126+
expect(getListHeads().length).toBe(2)
127+
})
128+
})
129+
130+
describe('end-to-end: add button creates a single list head', () => {
131+
it('clicking add produces exactly one Collection list-head triple in the store', async () => {
132+
setupOrderedMultipleForm()
133+
134+
const { box } = renderMultipleField()
135+
136+
// Initially no list head
137+
expect(getListHeads().length).toBe(0)
138+
139+
// Find and click the add/tail div (second child of box, after body)
140+
const children = box.children
141+
const tail = children[children.length - 1] as HTMLElement
142+
tail.click()
143+
144+
// Allow async operations (addItem + saveListThenRefresh) to complete
145+
await waitForAsync()
146+
147+
// After clicking add, exactly one list head should exist in the store
148+
const heads = getListHeads()
149+
expect(heads.length).toBe(1)
150+
expect(heads[0].termType).toBe('Collection')
151+
})
152+
153+
it('clicking add twice produces one list head with two elements', async () => {
154+
setupOrderedMultipleForm()
155+
156+
const { box } = renderMultipleField()
157+
158+
const children = box.children
159+
const tail = children[children.length - 1] as HTMLElement
160+
161+
// Click add twice
162+
tail.click()
163+
await waitForAsync()
164+
tail.click()
165+
await waitForAsync()
166+
167+
const heads = getListHeads()
168+
expect(heads.length).toBe(1)
169+
const collection = heads[0] as Collection
170+
expect(collection.termType).toBe('Collection')
171+
expect(collection.elements.length).toBe(2)
172+
})
173+
})
174+
})
175+

0 commit comments

Comments
 (0)