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