Files
MyGoNavi/frontend/src/utils/tabDisplay.test.ts
Syngnat c405eb08b5 feat(tabs): 支持标签展示配置并提示保存 SQL 文件
- 新增标签展示元素配置,支持单行、双行布局和元素排序

- 在设置面板提供标签展示入口并持久化用户配置

- 标签右键菜单增加标签设置入口并优化悬浮信息展示

- 关闭外部 SQL 文件标签前检测未保存草稿并支持保存后关闭
2026-06-02 11:16:25 +08:00

357 lines
11 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { SavedConnection, TabData } from '../types';
import {
applyTabDisplaySettingsPatch,
buildTabDisplayModel,
buildTabDisplayTitle,
resolveTabDisplayElementOrder,
resolveConnectionHostSummary,
sanitizeTabDisplaySettings,
switchTabDisplayLayout,
stripSchemaFromTabObjectLabel,
} from './tabDisplay';
const redisConnection: SavedConnection = {
id: 'redis-1',
name: '订单缓存',
config: {
type: 'redis',
host: '10.10.0.12',
port: 6379,
user: '',
database: '',
hosts: ['10.10.0.13:6379', '10.10.0.14:6379'],
},
};
describe('tabDisplay', () => {
it('builds compact host summary for multi-host redis connections', () => {
expect(resolveConnectionHostSummary(redisConnection.config)).toBe('10.10.0.12 +2');
});
it('adds connection and host identity to redis key tabs', () => {
const redisKeysTab: TabData = {
id: 'redis-keys-redis-1-db0',
title: 'db0',
type: 'redis-keys',
connectionId: 'redis-1',
redisDB: 0,
};
expect(buildTabDisplayTitle(redisKeysTab, redisConnection)).toBe('[订单缓存 | 10.10.0.12 +2] db0');
});
it('normalizes redis command and monitor tabs to db-scoped labels', () => {
const commandTab: TabData = {
id: 'cmd-1',
title: '命令 - db1',
type: 'redis-command',
connectionId: 'redis-1',
redisDB: 1,
};
const monitorTab: TabData = {
id: 'monitor-1',
title: '监控: 订单缓存',
type: 'redis-monitor',
connectionId: 'redis-1',
redisDB: 1,
};
expect(buildTabDisplayTitle(commandTab, redisConnection)).toBe('[订单缓存 | 10.10.0.12 +2] 命令 - db1');
expect(buildTabDisplayTitle(monitorTab, redisConnection)).toBe('[订单缓存 | 10.10.0.12 +2] 监控 - db1');
});
it('keeps table tabs on the existing prefix strategy', () => {
const tableTab: TabData = {
id: 'table-1',
title: 'orders',
type: 'table',
connectionId: 'redis-1',
dbName: 'app',
tableName: 'orders',
};
expect(buildTabDisplayTitle(tableTab, redisConnection)).toBe('[订单缓存] orders');
});
it('hides schema prefixes from schema-qualified table tab labels', () => {
const connection: SavedConnection = {
id: 'kingbase-1',
name: 'Kingbase DEV',
config: {
type: 'kingbase',
host: '127.0.0.1',
port: 54321,
user: 'SYSTEM',
database: 'appdb',
},
};
const tableTab: TabData = {
id: 'kingbase-1-appdb-table-ldf_server.andon_events',
title: 'ldf_server.andon_events',
type: 'table',
connectionId: 'kingbase-1',
dbName: 'appdb',
tableName: 'ldf_server.andon_events',
};
expect(buildTabDisplayTitle(tableTab, connection)).toBe('[DEV] andon_events');
});
it('hides schema prefixes from design and definition tab labels', () => {
const designTab: TabData = {
id: 'design-1',
title: '表结构 (public.orders)',
type: 'design',
connectionId: 'pg-1',
dbName: 'app',
tableName: 'public.orders',
readOnly: true,
};
const viewTab: TabData = {
id: 'view-1',
title: '视图: reporting.active_users',
type: 'view-def',
connectionId: 'pg-1',
dbName: 'app',
viewName: 'reporting.active_users',
};
const triggerTab: TabData = {
id: 'trigger-1',
title: '触发器: audit.users_bi',
type: 'trigger',
connectionId: 'pg-1',
dbName: 'app',
triggerName: 'audit.users_bi',
};
const routineTab: TabData = {
id: 'routine-1',
title: '存储过程: reporting.refresh_stats',
type: 'routine-def',
connectionId: 'pg-1',
dbName: 'app',
routineName: 'reporting.refresh_stats',
routineType: 'PROCEDURE',
};
expect(buildTabDisplayTitle(designTab)).toBe('表结构 (orders)');
expect(buildTabDisplayTitle(viewTab)).toBe('视图: active_users');
expect(buildTabDisplayTitle(triggerTab)).toBe('触发器: users_bi');
expect(buildTabDisplayTitle(routineTab)).toBe('存储过程: refresh_stats');
});
it('keeps quoted dots inside object names when hiding schema prefixes', () => {
expect(stripSchemaFromTabObjectLabel('"sales.schema"."order.items"')).toBe('order.items');
expect(stripSchemaFromTabObjectLabel('\\"ldf_server\\".\\"andon_events\\"')).toBe('andon_events');
expect(stripSchemaFromTabObjectLabel('[dbo].[order.items]')).toBe('order.items');
});
it('builds configurable single-line tab labels from ordered elements', () => {
const connection: SavedConnection = {
id: 'kingbase-1',
name: 'Kingbase DEV',
config: {
type: 'kingbase',
host: '192.168.10.8',
port: 54321,
user: 'SYSTEM',
database: 'appdb',
},
};
const tableTab: TabData = {
id: 'kingbase-1-appdb-table-ldf_server.andon_events',
title: 'ldf_server.andon_events',
type: 'table',
connectionId: 'kingbase-1',
dbName: 'appdb',
tableName: 'ldf_server.andon_events',
};
expect(buildTabDisplayTitle(tableTab, connection, {
layout: 'single',
primaryElements: ['object', 'schema', 'host'],
secondaryElements: [],
})).toBe('andon_events SCHEMA:ldf_server 192.168.10.8');
});
it('builds the default configurable model with connection, type and compact object name', () => {
const connection: SavedConnection = {
id: 'kingbase-1',
name: 'Kingbase DEV',
config: {
type: 'kingbase',
host: '192.168.10.8',
port: 54321,
user: 'SYSTEM',
database: 'appdb',
},
};
const tableTab: TabData = {
id: 'kingbase-1-appdb-table-ldf_server.andon_events',
title: 'ldf_server.andon_events',
type: 'table',
connectionId: 'kingbase-1',
dbName: 'appdb',
tableName: 'ldf_server.andon_events',
};
expect(buildTabDisplayModel(tableTab, connection).fullTitle).toBe('[DEV] TABLE andon_events');
});
it('keeps query tab labels compact when the title is raw SQL', () => {
const connection: SavedConnection = {
id: 'mysql-1',
name: '开发240',
config: {
type: 'mysql',
host: '192.168.1.240',
port: 3306,
user: 'root',
database: 'front_end_sys',
},
};
const queryTab: TabData = {
id: 'query-1',
title: 'select * from fs_org_auth_application where application_id is not null;',
type: 'query',
connectionId: 'mysql-1',
dbName: 'front_end_sys',
query: 'select * from fs_org_auth_application where application_id is not null;',
};
const model = buildTabDisplayModel(queryTab, connection);
expect(model.primaryText).toBe('[开发240] SQL 新建查询');
expect(model.fullTitle).not.toContain('fs_org_auth_application');
expect(model.fullTitle).not.toContain('select *');
});
it('uses SQL file names as compact query tab object labels', () => {
const queryTab: TabData = {
id: 'query-file-1',
title: 'select * from very_long_table_name;',
type: 'query',
connectionId: 'mysql-1',
filePath: '/Users/me/sql/monthly-report.sql',
query: 'select * from very_long_table_name;',
};
const model = buildTabDisplayModel(queryTab);
expect(model.primaryText).toBe('SQL monthly-report.sql');
});
it('builds configurable double-line tab display models', () => {
const connection: SavedConnection = {
id: 'pg-1',
name: 'Postgres PROD',
config: {
type: 'postgres',
host: '10.0.0.9',
port: 5432,
user: 'postgres',
database: 'analytics',
},
};
const tableTab: TabData = {
id: 'pg-1-analytics-table-reporting.events',
title: 'reporting.events',
type: 'table',
connectionId: 'pg-1',
dbName: 'analytics',
tableName: 'reporting.events',
};
const model = buildTabDisplayModel(tableTab, connection, {
layout: 'double',
primaryElements: ['kind', 'object'],
secondaryElements: ['connection', 'database', 'schema', 'host'],
});
expect(model.layout).toBe('double');
expect(model.primaryText).toBe('TABLE events');
expect(model.secondaryText).toBe('[PROD]·analytics·SCHEMA:reporting·10.0.0.9');
expect(model.fullTitle).toBe('TABLE events · [PROD]·analytics·SCHEMA:reporting·10.0.0.9');
});
it('sanitizes tab display settings with fallback defaults', () => {
expect(sanitizeTabDisplaySettings({
layout: 'invalid' as never,
primaryElements: ['schema', 'schema', 'bad' as never],
secondaryElements: ['object', 'schema', 'host'],
})).toEqual({
layout: 'single',
primaryElements: ['schema'],
secondaryElements: ['object', 'host'],
});
expect(sanitizeTabDisplaySettings({
layout: 'double',
primaryElements: ['bad' as never],
secondaryElements: [],
})).toEqual({
layout: 'double',
primaryElements: ['connection', 'kind', 'object'],
secondaryElements: [],
});
expect(sanitizeTabDisplaySettings({
layout: 'single',
primaryElements: ['object'],
secondaryElements: [],
single: {
primaryElements: ['object', 'object', 'host'],
secondaryElements: ['bad' as never],
},
double: {
primaryElements: ['kind', 'object'],
secondaryElements: ['connection', 'kind', 'schema'],
},
})).toEqual({
layout: 'single',
primaryElements: ['object'],
secondaryElements: [],
single: {
primaryElements: ['object', 'host'],
secondaryElements: [],
},
double: {
primaryElements: ['kind', 'object'],
secondaryElements: ['connection', 'schema'],
},
});
});
it('resolves visible tab display elements before hidden elements', () => {
expect(resolveTabDisplayElementOrder({
layout: 'double',
primaryElements: ['object', 'kind'],
secondaryElements: ['host'],
})).toEqual(['object', 'kind', 'host', 'connection', 'database', 'schema']);
});
it('keeps separate single-line and double-line settings when switching layouts', () => {
const doubleConfigured = sanitizeTabDisplaySettings({
layout: 'double',
primaryElements: ['kind', 'object'],
secondaryElements: ['connection', 'database'],
});
const singleLayout = switchTabDisplayLayout(doubleConfigured, 'single');
const singleConfigured = applyTabDisplaySettingsPatch(singleLayout, {
primaryElements: ['object', 'host'],
secondaryElements: [],
});
const restoredDouble = switchTabDisplayLayout(singleConfigured, 'double');
const restoredSingle = switchTabDisplayLayout(restoredDouble, 'single');
expect(restoredDouble.layout).toBe('double');
expect(restoredDouble.primaryElements).toEqual(['kind', 'object']);
expect(restoredDouble.secondaryElements).toEqual(['connection', 'database']);
expect(restoredSingle.layout).toBe('single');
expect(restoredSingle.primaryElements).toEqual(['object', 'host']);
expect(restoredSingle.secondaryElements).toEqual([]);
});
});