|
5 | 5 | parseRows, |
6 | 6 | serializeRows, |
7 | 7 | inferFields, |
| 8 | + initialDatabaseContents, |
8 | 9 | buildDefaultViews, |
9 | 10 | type GenId |
10 | 11 | } from '@shared/database-csv' |
@@ -142,6 +143,98 @@ describe('inferFields', () => { |
142 | 143 | }) |
143 | 144 | }) |
144 | 145 |
|
| 146 | +describe('initialDatabaseContents', () => { |
| 147 | + it('without a seed is the empty id + Name grid', () => { |
| 148 | + const { sidecar, rows } = initialDatabaseContents(undefined, counterGenId()) |
| 149 | + expect(rows).toEqual([]) |
| 150 | + expect(sidecar.fields.map((f) => [f.name, f.type, f.hidden ?? false])).toEqual([ |
| 151 | + ['id', 'text', true], |
| 152 | + ['Name', 'text', false] |
| 153 | + ]) |
| 154 | + expect(sidecar.idFieldId).toBe(sidecar.fields[0].id) |
| 155 | + expect(sidecar.views[0].hiddenFieldIds).toEqual([sidecar.idFieldId]) |
| 156 | + expect(serializeRows(rows, sidecar.fields)).toBe('id,Name\n') |
| 157 | + }) |
| 158 | + |
| 159 | + it('types a seeded table like an adopted CSV and keys rows by field id (#832)', () => { |
| 160 | + const { sidecar, rows } = initialDatabaseContents( |
| 161 | + { |
| 162 | + headers: ['Task', 'Done', 'Due', 'Hours'], |
| 163 | + rows: [ |
| 164 | + ['Write **draft**', 'x', '2026-03-01', '2'], |
| 165 | + ['Ship, then rest', '', '2026-03-09', '0.5'] |
| 166 | + ] |
| 167 | + }, |
| 168 | + counterGenId() |
| 169 | + ) |
| 170 | + const byName = new Map(sidecar.fields.map((f) => [f.name, f])) |
| 171 | + // A hidden id field is synthesized in front of the table's own columns. |
| 172 | + expect(sidecar.fields[0].name).toBe('id') |
| 173 | + expect(sidecar.fields[0].id).toBe(sidecar.idFieldId) |
| 174 | + expect(byName.get('Task')!.type).toBe('text') |
| 175 | + expect(byName.get('Done')!.type).toBe('checkbox') |
| 176 | + expect(byName.get('Due')!.type).toBe('date') |
| 177 | + expect(byName.get('Hours')!.type).toBe('number') |
| 178 | + expect(rows).toHaveLength(2) |
| 179 | + for (const row of rows) { |
| 180 | + expect(row.id).toBeTruthy() |
| 181 | + expect(row.cells[sidecar.idFieldId]).toBe(row.id) |
| 182 | + } |
| 183 | + expect(rows[0].cells[byName.get('Task')!.id]).toBe('Write **draft**') |
| 184 | + expect(rows[1].cells[byName.get('Done')!.id]).toBe('') |
| 185 | + // What lands in data.csv: RFC 4180 quoting for the comma, markdown kept verbatim. |
| 186 | + const csv = serializeRows(rows, sidecar.fields) |
| 187 | + expect(csv.split('\n')[0]).toBe('id,Task,Done,Due,Hours') |
| 188 | + expect(csv).toContain(',"Ship, then rest",,2026-03-09,0.5\n') |
| 189 | + expect(parseRows(csv, sidecar.fields, sidecar.idFieldId)).toEqual(rows) |
| 190 | + }) |
| 191 | + |
| 192 | + it('adopts an all-unique id column from the seed instead of synthesizing one', () => { |
| 193 | + const { sidecar, rows } = initialDatabaseContents( |
| 194 | + { headers: ['id', 'Name'], rows: [['a1', 'Alpha'], ['b2', 'Beta']] }, |
| 195 | + counterGenId() |
| 196 | + ) |
| 197 | + expect(sidecar.fields.map((f) => f.name)).toEqual(['id', 'Name']) |
| 198 | + expect(sidecar.fields[0].hidden).toBe(true) |
| 199 | + expect(rows.map((r) => r.id)).toEqual(['a1', 'b2']) |
| 200 | + }) |
| 201 | + |
| 202 | + it('renames blank and duplicate headers and pads short rows', () => { |
| 203 | + const { sidecar, rows } = initialDatabaseContents( |
| 204 | + { headers: ['', 'Name', 'Name'], rows: [['only one cell']] }, |
| 205 | + counterGenId() |
| 206 | + ) |
| 207 | + expect(sidecar.fields.map((f) => f.name)).toEqual(['id', 'Column 1', 'Name', 'Name (2)']) |
| 208 | + const [, c1, n1, n2] = sidecar.fields |
| 209 | + expect(rows[0].cells[c1.id]).toBe('only one cell') |
| 210 | + expect(rows[0].cells[n1.id]).toBe('') |
| 211 | + expect(rows[0].cells[n2.id]).toBe('') |
| 212 | + }) |
| 213 | + |
| 214 | + it('carries positive column widths onto the matching fields and drops the rest', () => { |
| 215 | + const { sidecar } = initialDatabaseContents( |
| 216 | + { |
| 217 | + headers: ['A', 'B', 'C'], |
| 218 | + rows: [], |
| 219 | + columnWidths: [120, null, -4] |
| 220 | + }, |
| 221 | + counterGenId() |
| 222 | + ) |
| 223 | + const widths = sidecar.fields.map((f) => f.width) |
| 224 | + expect(widths).toEqual([undefined, 120, undefined, undefined]) |
| 225 | + }) |
| 226 | + |
| 227 | + it('a header-only seed makes an empty database with those fields', () => { |
| 228 | + const { sidecar, rows } = initialDatabaseContents( |
| 229 | + { headers: ['Title', 'Notes'], rows: [] }, |
| 230 | + counterGenId() |
| 231 | + ) |
| 232 | + expect(rows).toEqual([]) |
| 233 | + expect(sidecar.fields.map((f) => f.name)).toEqual(['id', 'Title', 'Notes']) |
| 234 | + expect(sidecar.fields.slice(1).every((f) => f.type === 'text')).toBe(true) |
| 235 | + }) |
| 236 | +}) |
| 237 | + |
145 | 238 | describe('transforms', () => { |
146 | 239 | const fields: DbField[] = [ |
147 | 240 | { id: 'name', name: 'Name', type: 'text' }, |
|
0 commit comments