dbn-cli 0.4.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,434 @@
1
+ import { describe, it, before, after } from 'node:test';
2
+ import assert from 'node:assert';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ import { unlinkSync, existsSync } from 'node:fs';
5
+ import { SQLiteAdapter } from '../adapter/sqlite.ts';
6
+ import { Navigator } from '../ui/navigator.ts';
7
+
8
+ const TEST_DB = './test-navigator.db';
9
+
10
+ describe('Navigator', () => {
11
+ let adapter: SQLiteAdapter;
12
+ let navigator: Navigator;
13
+
14
+ const setupDB = () => {
15
+ // Create test database
16
+ if (existsSync(TEST_DB)) {
17
+ unlinkSync(TEST_DB);
18
+ }
19
+ const db = new DatabaseSync(TEST_DB);
20
+
21
+ db.exec(`
22
+ CREATE TABLE users (
23
+ id INTEGER PRIMARY KEY,
24
+ name TEXT NOT NULL,
25
+ email TEXT UNIQUE
26
+ );
27
+
28
+ CREATE TABLE posts (
29
+ id INTEGER PRIMARY KEY,
30
+ user_id INTEGER,
31
+ title TEXT,
32
+ content TEXT,
33
+ FOREIGN KEY (user_id) REFERENCES users(id)
34
+ );
35
+
36
+ CREATE TABLE logs (
37
+ message TEXT
38
+ );
39
+ `);
40
+
41
+ // Insert test data
42
+ const insertUser = db.prepare('INSERT INTO users (name, email) VALUES (?, ?)');
43
+ insertUser.run('Alice', 'alice@example.com');
44
+ insertUser.run('Bob', 'bob@example.com');
45
+ insertUser.run('Charlie', 'charlie@example.com');
46
+
47
+ const insertPost = db.prepare('INSERT INTO posts (user_id, title, content) VALUES (?, ?, ?)');
48
+ insertPost.run(1, 'Post 1', 'Content 1');
49
+ insertPost.run(1, 'Post 2', 'Content 2');
50
+ insertPost.run(2, 'Post 3', 'Content 3');
51
+
52
+ const insertLog = db.prepare('INSERT INTO logs (message) VALUES (?)');
53
+ insertLog.run('First log entry');
54
+ insertLog.run('Second log entry');
55
+
56
+ db.close();
57
+ };
58
+
59
+ before(() => {
60
+ setupDB();
61
+ // Create adapter and navigator
62
+ adapter = new SQLiteAdapter();
63
+ adapter.connect(TEST_DB);
64
+ navigator = new Navigator(adapter);
65
+ });
66
+
67
+ after(() => {
68
+ adapter.close();
69
+ if (existsSync(TEST_DB)) {
70
+ unlinkSync(TEST_DB);
71
+ }
72
+ });
73
+
74
+ describe('initialization', () => {
75
+ it('should initialize with tables view', () => {
76
+ navigator.init();
77
+ const state = navigator.getState();
78
+
79
+ assert.strictEqual(state.type, 'tables');
80
+ assert.ok(Array.isArray((state as any).tables));
81
+ assert.strictEqual((state as any).cursor, 0);
82
+ assert.strictEqual((state as any).scrollOffset, 0);
83
+ });
84
+
85
+ it('should load all tables', () => {
86
+ const state = navigator.getState() as any;
87
+ assert.ok(state.tables.length >= 2);
88
+
89
+ const tableNames = state.tables.map((t: any) => t.name);
90
+ assert.ok(tableNames.includes('users'));
91
+ assert.ok(tableNames.includes('posts'));
92
+ });
93
+ });
94
+
95
+ describe('navigation in tables view', () => {
96
+ it('should move cursor down', () => {
97
+ navigator.init();
98
+ const initialCursor = (navigator.getState() as any).cursor;
99
+
100
+ navigator.moveDown();
101
+ const newCursor = (navigator.getState() as any).cursor;
102
+
103
+ assert.strictEqual(newCursor, initialCursor + 1);
104
+ });
105
+
106
+ it('should move cursor up', () => {
107
+ navigator.moveDown();
108
+ const initialCursor = (navigator.getState() as any).cursor;
109
+
110
+ navigator.moveUp();
111
+ const newCursor = (navigator.getState() as any).cursor;
112
+
113
+ assert.strictEqual(newCursor, initialCursor - 1);
114
+ });
115
+
116
+ it('should not move cursor below 0', () => {
117
+ navigator.jumpToTop();
118
+ navigator.moveUp();
119
+
120
+ assert.strictEqual((navigator.getState() as any).cursor, 0);
121
+ });
122
+
123
+ it('should not move cursor beyond table count', () => {
124
+ const state = navigator.getState() as any;
125
+ const maxCursor = state.tables.length - 1;
126
+
127
+ navigator.jumpToBottom();
128
+ navigator.moveDown();
129
+
130
+ assert.strictEqual((navigator.getState() as any).cursor, maxCursor);
131
+ });
132
+
133
+ it('should jump to top', () => {
134
+ navigator.moveDown();
135
+ navigator.moveDown();
136
+ navigator.jumpToTop();
137
+
138
+ assert.strictEqual((navigator.getState() as any).cursor, 0);
139
+ });
140
+
141
+ it('should jump to bottom', () => {
142
+ navigator.jumpToTop();
143
+ navigator.jumpToBottom();
144
+
145
+ const state = navigator.getState() as any;
146
+ assert.strictEqual(state.cursor, state.tables.length - 1);
147
+ });
148
+ });
149
+
150
+ describe('entering table detail', () => {
151
+ it('should enter table detail view', () => {
152
+ navigator.init();
153
+ navigator.enter();
154
+
155
+ const state = navigator.getState() as any;
156
+ assert.strictEqual(state.type, 'table-detail');
157
+ assert.ok(state.tableName);
158
+ assert.ok(Array.isArray(state.schema));
159
+ assert.ok(Array.isArray(state.data));
160
+ assert.strictEqual(typeof state.totalRows, 'number');
161
+ assert.strictEqual(state.dataOffset, 0);
162
+ assert.strictEqual(state.dataCursor, 0);
163
+ });
164
+
165
+ it('should load table schema', () => {
166
+ const state = navigator.getState() as any;
167
+ assert.ok(state.schema.length > 0);
168
+
169
+ const schema = state.schema;
170
+ assert.ok(schema[0].name);
171
+ assert.ok(schema[0].type);
172
+ });
173
+
174
+ it('should load table data', () => {
175
+ const state = navigator.getState() as any;
176
+ assert.ok(state.data.length > 0);
177
+ assert.ok(state.totalRows > 0);
178
+ });
179
+ });
180
+
181
+ describe('schema view toggle', () => {
182
+ const enterUsersTable = () => {
183
+ navigator.init();
184
+ const tablesState = navigator.getState() as any;
185
+ const index = tablesState.tables.findIndex((t: any) => t.name === 'users');
186
+ assert.ok(index >= 0);
187
+
188
+ while ((navigator.getState() as any).cursor < index) {
189
+ navigator.moveDown();
190
+ }
191
+
192
+ while ((navigator.getState() as any).cursor > index) {
193
+ navigator.moveUp();
194
+ }
195
+
196
+ navigator.enter();
197
+ };
198
+
199
+ const enterSchemaView = () => {
200
+ enterUsersTable();
201
+ navigator.viewSchema();
202
+ };
203
+
204
+ it('should view schema from table detail', () => {
205
+ enterSchemaView();
206
+
207
+ const state = navigator.getState() as any;
208
+ assert.strictEqual(state.type, 'schema-view');
209
+ assert.ok(state.tableName);
210
+ assert.ok(Array.isArray(state.schema));
211
+ assert.strictEqual(state.cursor, 0);
212
+ assert.strictEqual(state.scrollOffset, 0);
213
+ });
214
+
215
+ it('should navigate in schema view', () => {
216
+ enterSchemaView();
217
+ const state = navigator.getState() as any;
218
+ assert.strictEqual(state.type, 'schema-view');
219
+ assert.strictEqual(state.cursor, 0);
220
+
221
+ navigator.moveDown();
222
+ assert.strictEqual((navigator.getState() as any).cursor, 1);
223
+
224
+ navigator.moveUp();
225
+ assert.strictEqual((navigator.getState() as any).cursor, 0);
226
+ });
227
+
228
+ it('should jump to top/bottom in schema view', () => {
229
+ enterSchemaView();
230
+ navigator.moveDown();
231
+ navigator.jumpToTop();
232
+ assert.strictEqual((navigator.getState() as any).cursor, 0);
233
+
234
+ navigator.jumpToBottom();
235
+ const state = navigator.getState() as any;
236
+ assert.strictEqual(state.cursor, state.schema.length - 1);
237
+ });
238
+
239
+ it('should not move cursor beyond schema bounds', () => {
240
+ enterSchemaView();
241
+ navigator.jumpToTop();
242
+ navigator.moveUp();
243
+ assert.strictEqual((navigator.getState() as any).cursor, 0);
244
+
245
+ navigator.jumpToBottom();
246
+ const maxCursor = (navigator.getState() as any).schema.length - 1;
247
+ navigator.moveDown();
248
+ assert.strictEqual((navigator.getState() as any).cursor, maxCursor);
249
+ });
250
+
251
+ it('should go back to table detail', () => {
252
+ enterSchemaView();
253
+ navigator.back();
254
+
255
+ const state = navigator.getState();
256
+ assert.strictEqual(state.type, 'table-detail');
257
+ });
258
+
259
+ it('should preserve table detail state when toggling', () => {
260
+ enterUsersTable();
261
+ const state = navigator.getState() as any;
262
+ const tableName = state.tableName;
263
+ const dataOffset = state.dataOffset;
264
+ const dataCursor = state.dataCursor;
265
+
266
+ navigator.viewSchema();
267
+ navigator.back();
268
+
269
+ const newState = navigator.getState() as any;
270
+ assert.strictEqual(newState.type, 'table-detail');
271
+ assert.strictEqual(newState.tableName, tableName);
272
+ assert.strictEqual(newState.dataOffset, dataOffset);
273
+ assert.strictEqual(newState.dataCursor, dataCursor);
274
+ });
275
+ });
276
+
277
+ describe('row detail view', () => {
278
+ it('should enter row detail from table detail', () => {
279
+ navigator.init();
280
+ navigator.enter();
281
+ navigator.enter();
282
+
283
+ const state = navigator.getState() as any;
284
+ assert.strictEqual(state.type, 'row-detail');
285
+ assert.ok(state.tableName);
286
+ assert.ok(state.row);
287
+ assert.strictEqual(typeof state.rowIndex, 'number');
288
+ assert.ok(Array.isArray(state.schema));
289
+ });
290
+
291
+ it('should go back to table detail', () => {
292
+ navigator.back();
293
+
294
+ const state = navigator.getState();
295
+ assert.strictEqual(state.type, 'table-detail');
296
+ });
297
+ });
298
+
299
+ describe('delete flow', () => {
300
+ const selectTable = (tableName: string) => {
301
+ navigator.init();
302
+ const tablesState = navigator.getState() as any;
303
+ const index = tablesState.tables.findIndex((t: any) => t.name === tableName);
304
+ assert.ok(index >= 0);
305
+
306
+ while ((navigator.getState() as any).cursor < index) {
307
+ navigator.moveDown();
308
+ }
309
+
310
+ while ((navigator.getState() as any).cursor > index) {
311
+ navigator.moveUp();
312
+ }
313
+
314
+ navigator.enter();
315
+ const state = navigator.getState() as any;
316
+ assert.strictEqual(state.type, 'table-detail');
317
+ assert.strictEqual(state.tableName, tableName);
318
+ return state;
319
+ };
320
+
321
+ it('should require two confirmations before deleting and actually remove the row', () => {
322
+ // Refresh the DB and navigator to ensure clean state
323
+ setupDB();
324
+ adapter.close();
325
+ adapter.connect(TEST_DB);
326
+ navigator.init();
327
+
328
+ // Enter the 'posts' table (no FK constraints on delete)
329
+ const tablesState = navigator.getState() as any;
330
+ const index = tablesState.tables.findIndex((t: any) => t.name === 'posts');
331
+ assert.ok(index >= 0);
332
+ tablesState.cursor = index;
333
+
334
+ navigator.enter();
335
+ let state = navigator.getState() as any;
336
+ const initialTotal = state.totalRows;
337
+
338
+ // Select first post
339
+ state.dataCursor = 0;
340
+
341
+ navigator.requestDelete();
342
+ state = navigator.getState() as any;
343
+ assert.strictEqual(state.deleteConfirm.step, 1);
344
+
345
+ navigator.confirmDelete();
346
+ state = navigator.getState() as any;
347
+ assert.strictEqual(state.deleteConfirm.step, 2);
348
+
349
+ navigator.confirmDelete();
350
+ state = navigator.getState() as any;
351
+ assert.ok(!state.deleteConfirm);
352
+ assert.strictEqual(state.totalRows, initialTotal - 1);
353
+
354
+ // Verify row is gone from state data
355
+ assert.strictEqual(state.totalRows, initialTotal - 1);
356
+
357
+ // Verify row is gone from database
358
+ const dbCount = adapter.getRowCount('posts');
359
+ assert.strictEqual(dbCount, initialTotal - 1);
360
+ });
361
+
362
+ it('should allow canceling a delete request', () => {
363
+ selectTable('users');
364
+ navigator.requestDelete();
365
+ let current = navigator.getState() as any;
366
+ assert.ok(current.deleteConfirm);
367
+
368
+ navigator.cancelDelete();
369
+ current = navigator.getState() as any;
370
+ assert.ok(!current.deleteConfirm);
371
+ assert.strictEqual(navigator.hasPendingDelete(), false);
372
+ });
373
+
374
+ it('should block deletion when table has no primary key', () => {
375
+ selectTable('logs');
376
+ navigator.requestDelete();
377
+ const current = navigator.getState() as any;
378
+ assert.ok(!current.deleteConfirm);
379
+ assert.match(current.notice, /no primary key/i);
380
+ });
381
+ });
382
+
383
+ describe('back navigation', () => {
384
+ it('should maintain state stack', () => {
385
+ navigator.init();
386
+ assert.strictEqual((navigator as any).states.length, 1);
387
+
388
+ navigator.enter();
389
+ assert.strictEqual((navigator as any).states.length, 2);
390
+
391
+ navigator.viewSchema();
392
+ assert.strictEqual((navigator as any).states.length, 3);
393
+
394
+ navigator.back();
395
+ assert.strictEqual((navigator as any).states.length, 2);
396
+
397
+ navigator.back();
398
+ assert.strictEqual((navigator as any).states.length, 1);
399
+ });
400
+
401
+ it('should not go back beyond root', () => {
402
+ navigator.init();
403
+ navigator.back();
404
+
405
+ assert.strictEqual((navigator as any).states.length, 1);
406
+ assert.strictEqual(navigator.getState().type, 'tables');
407
+ });
408
+ });
409
+
410
+ describe('data reload', () => {
411
+ it('should reload table data in table detail', () => {
412
+ navigator.init();
413
+ navigator.enter();
414
+
415
+ const state = navigator.getState() as any;
416
+ const initialData = [...state.data];
417
+
418
+ navigator.reload();
419
+
420
+ const newData = (navigator.getState() as any).data;
421
+ assert.deepStrictEqual(newData, initialData);
422
+ });
423
+
424
+ it('should reload data after offset change', () => {
425
+ const state = navigator.getState() as any;
426
+ state.dataOffset = 1;
427
+
428
+ navigator.reload();
429
+
430
+ // Data should be reloaded from new offset
431
+ assert.ok((navigator.getState() as any).data);
432
+ });
433
+ });
434
+ });