dbn-cli 0.3.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,67 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { Box, Transition, Grid, ANSI } from './index.ts';
4
+
5
+ test('Grid.calculateWidths distributes widths correctly', () => {
6
+ const widths = Grid.calculateWidths(100, [
7
+ { weight: 1 },
8
+ { weight: 1 }
9
+ ]);
10
+ assert.deepEqual(widths, [50, 50]);
11
+
12
+ const weightedWidths = Grid.calculateWidths(100, [
13
+ { weight: 1 },
14
+ { weight: 3 }
15
+ ]);
16
+ assert.deepEqual(weightedWidths, [25, 75]);
17
+
18
+ const minWidths = Grid.calculateWidths(100, [
19
+ { weight: 1, minWidth: 40 },
20
+ { weight: 1 }
21
+ ]);
22
+ // Total weight 2, available 100 - 40 = 60.
23
+ // Col 1: 40 + (1/2 * 60) = 70
24
+ // Col 2: 0 + (1/2 * 60) = 30
25
+ assert.deepEqual(minWidths, [70, 30]);
26
+ });
27
+
28
+ test('Grid.calculateWidths caps extreme weights', () => {
29
+ // Avg weight = (1 + 10) / 2 = 5.5. Max weight = 22.
30
+ // Wait, if I have 1 and 100, avg is 50.5, max is 202. That won't cap.
31
+ // If I have [1, 1, 1, 10], avg is 13/4 = 3.25. Max is 13. Capped 10 is fine.
32
+ // If I have [1, 1, 1, 20], avg is 23/4 = 5.75. Max is 23.
33
+ // Let's use [1, 1, 1, 1, 1, 100]. Avg = 105/6 = 17.5. Max = 70. 100 capped to 70.
34
+ const configs = [
35
+ { weight: 1 }, { weight: 1 }, { weight: 1 }, { weight: 1 }, { weight: 1 }, { weight: 100 }
36
+ ];
37
+ const widths = Grid.calculateWidths(100, configs);
38
+ // Total capped weight = 1+1+1+1+1+70 = 75.
39
+ // Each 1 weight gets 1/75 * 100 = 1.33 -> 1
40
+ // 70 weight gets 70/75 * 100 = 93.33 -> 93
41
+ // Sum = 1*5 + 93 = 98. Last col gets remainder: 93 + 2 = 95.
42
+ assert.deepEqual(widths, [1, 1, 1, 1, 1, 95]);
43
+ });
44
+
45
+ test('Box renders with background and padding', () => {
46
+ const box = new Box({ width: 10, background: '#000000', padding: 1 });
47
+ const result = box.render('HI');
48
+
49
+ // ANSI.bg('#000000') + ' ' + 'HI' + ' '.repeat(10 - 1 - 2 - 1) + ' ' + ANSI.reset
50
+ // width 10, padding 1. Inner width 8. 'HI' is 2. Fill 6.
51
+ // BG + ' ' (pad) + 'HI' + ' ' (fill) + ' ' (pad) + RESET
52
+ const expected = `${ANSI.bg('#000000')} HI ${ANSI.reset}`;
53
+ assert.strictEqual(result, expected);
54
+ });
55
+
56
+ test('Box handles alignment', () => {
57
+ const box = new Box({ width: 10, padding: 0 });
58
+
59
+ assert.strictEqual(box.render('HI', { align: 'right' }), ' HI');
60
+ assert.strictEqual(box.render('HI', { align: 'center' }), ' HI ');
61
+ });
62
+
63
+ test('Transition.draw generates correct ANSI', () => {
64
+ const result = Transition.draw(5, '#FF0000', '#0000FF');
65
+ const expected = `${ANSI.fg('#FF0000')}${ANSI.bg('#0000FF')}▀▀▀▀▀${ANSI.reset}`;
66
+ assert.strictEqual(result, expected);
67
+ });
@@ -0,0 +1,101 @@
1
+ import { ANSI, wrapAnsiBg } from './utils.ts';
2
+ import { getVisibleWidth } from '../../utils/format.ts';
3
+ import type { Color, Alignment, LayoutOptions, ColumnConfig } from './types.ts';
4
+
5
+ export * from './types.ts';
6
+ export * from './utils.ts';
7
+
8
+ export class Box {
9
+ private options: LayoutOptions;
10
+
11
+ constructor(options: LayoutOptions) {
12
+ this.options = {
13
+ padding: 0,
14
+ ...options
15
+ };
16
+ }
17
+
18
+ render(content: string, options: { align?: Alignment; background?: Color } = {}): string {
19
+ const { width, background: defaultBg, padding = 0 } = this.options;
20
+ const bg = options.background || defaultBg || '';
21
+
22
+ const innerWidth = width - (padding * 2);
23
+ const contentWidth = getVisibleWidth(content);
24
+
25
+ let line = '';
26
+ if (bg) line += ANSI.bg(bg);
27
+
28
+ // Left padding
29
+ line += ' '.repeat(padding);
30
+
31
+ const fill = Math.max(0, innerWidth - contentWidth);
32
+ const safeContent = wrapAnsiBg(content, bg);
33
+
34
+ if (options.align === 'right') {
35
+ line += ' '.repeat(fill) + safeContent;
36
+ } else if (options.align === 'center') {
37
+ const leftFill = Math.floor(fill / 2);
38
+ const rightFill = fill - leftFill;
39
+ line += ' '.repeat(leftFill) + safeContent + ' '.repeat(rightFill);
40
+ } else {
41
+ line += safeContent + ' '.repeat(fill);
42
+ }
43
+
44
+ // Right padding
45
+ line += ' '.repeat(padding);
46
+
47
+ if (bg) line += ANSI.reset;
48
+
49
+ return line;
50
+ }
51
+ }
52
+
53
+ export class Transition {
54
+ static draw(width: number, topBg: Color, bottomBg: Color): string {
55
+ return `${ANSI.fg(topBg)}${ANSI.bg(bottomBg)}${ANSI.blockUpper.repeat(width)}${ANSI.reset}`;
56
+ }
57
+
58
+ static drawInverted(width: number, topBg: Color, bottomBg: Color): string {
59
+ return `${ANSI.fg(bottomBg)}${ANSI.bg(topBg)}${ANSI.blockLower.repeat(width)}${ANSI.reset}`;
60
+ }
61
+ }
62
+
63
+ export class Grid {
64
+ static calculateWidths(totalWidth: number, configs: ColumnConfig[]): number[] {
65
+ const numCols = configs.length;
66
+ if (numCols === 0) return [];
67
+
68
+ const minWidths = configs.map(c => c.minWidth ?? 0);
69
+ const weights = configs.map(c => c.weight ?? 1);
70
+
71
+ const totalMinWidth = minWidths.reduce((a, b) => a + b, 0);
72
+ const availableWidth = totalWidth - totalMinWidth;
73
+
74
+ if (availableWidth <= 0) {
75
+ // If not enough space, distribute equally based on minWidths or just evenly
76
+ const equalWidth = Math.floor(totalWidth / numCols);
77
+ return new Array(numCols).fill(equalWidth);
78
+ }
79
+
80
+ // Cap weights to prevent extreme ratios (max 4x average)
81
+ const avgWeight = weights.reduce((a, b) => a + b, 0) / numCols;
82
+ const maxWeight = avgWeight * 4;
83
+ const cappedWeights = weights.map(w => Math.min(w, maxWeight));
84
+ const totalWeight = cappedWeights.reduce((a, b) => a + b, 0);
85
+
86
+ if (totalWeight === 0) {
87
+ const equalWidth = Math.floor(totalWidth / numCols);
88
+ return new Array(numCols).fill(equalWidth);
89
+ }
90
+
91
+ const widths = cappedWeights.map((w, i) => minWidths[i] + Math.floor((w / totalWeight) * availableWidth));
92
+
93
+ // Distribute rounding remainder to the last column
94
+ const usedWidth = widths.reduce((a, b) => a + b, 0);
95
+ if (usedWidth < totalWidth) {
96
+ widths[widths.length - 1] += (totalWidth - usedWidth);
97
+ }
98
+
99
+ return widths;
100
+ }
101
+ }
@@ -0,0 +1,16 @@
1
+ export type Color = string; // Hex color string like '#FFFFFF'
2
+
3
+ export type Alignment = 'left' | 'right' | 'center';
4
+
5
+ export interface LayoutOptions {
6
+ width: number;
7
+ height?: number;
8
+ padding?: number;
9
+ background?: Color;
10
+ }
11
+
12
+ export interface ColumnConfig {
13
+ weight?: number;
14
+ minWidth?: number;
15
+ maxWidth?: number;
16
+ }
@@ -0,0 +1,35 @@
1
+ export const ANSI = {
2
+ reset: '\x1b[0m',
3
+ bold: '\x1b[1m',
4
+ dim: '\x1b[2m',
5
+ inverse: '\x1b[7m',
6
+ // TrueColor (24-bit) foreground
7
+ fg: (hex: string) => {
8
+ const { r, g, b } = hexToRgb(hex);
9
+ return `\x1b[38;2;${r};${g};${b}m`;
10
+ },
11
+ // TrueColor (24-bit) background
12
+ bg: (hex: string) => {
13
+ const { r, g, b } = hexToRgb(hex);
14
+ return `\x1b[48;2;${r};${g};${b}m`;
15
+ },
16
+ blockUpper: '▀',
17
+ blockLower: '▄',
18
+ };
19
+
20
+ function hexToRgb(hex: string) {
21
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
22
+ return result ? {
23
+ r: parseInt(result[1], 16),
24
+ g: parseInt(result[2], 16),
25
+ b: parseInt(result[3], 16)
26
+ } : { r: 0, g: 0, b: 0 };
27
+ }
28
+
29
+ /**
30
+ * Ensures background color is maintained if content contains ANSI resets.
31
+ */
32
+ export function wrapAnsiBg(content: string, bg: string): string {
33
+ if (!bg) return content;
34
+ return content.replaceAll(ANSI.reset, ANSI.reset + ANSI.bg(bg));
35
+ }
@@ -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
+ });